From aad153d9cb35a90252a45eeb4ce4d234cd6f512c Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 12 Sep 2024 20:02:33 +0300 Subject: [PATCH] Osrs 225 (#28) * feat: revision 225 (initial clone of 224) * feat: game and client prot ids * feat: update client -> server packets * feat: update server -> client packets (excluding infos) * fix: projanim specific (intellij decided to leave this file behind!) * feat: level support in worldentity info * feat: npc info * feat: player info (extended info blocks) * refactor: change player info to single-packet-per-player-per-tick This is due to the client having changed to no longer require one player info packet per world-entity, instead a single packet can update the state for all players on all world entities. * feat: crcs --- protocol/osrs-225/build.gradle.kts | 1 + .../osrs-225/osrs-225-api/build.gradle.kts | 27 + .../api/AbstractNetworkServiceFactory.kt | 291 ++++ .../protocol/api/ChannelExceptionHandler.kt | 20 + .../protocol/api/EntityInfoProtocols.kt | 197 +++ .../protocol/api/GameConnectionHandler.kt | 47 + .../rsprot/protocol/api/GameMessageCounter.kt | 32 + .../api/GameMessageCounterProvider.kt | 15 + ...mingGameMessageConsumerExceptionHandler.kt | 28 + .../rsprot/protocol/api/InetAddressTracker.kt | 28 + .../protocol/api/InetAddressValidator.kt | 40 + .../protocol/api/LoginChannelInitializer.kt | 54 + .../protocol/api/LoginDecoderService.kt | 31 + .../protocol/api/MessageQueueProvider.kt | 18 + .../net/rsprot/protocol/api/NetworkService.kt | 184 ++ .../kotlin/net/rsprot/protocol/api/Session.kt | 283 ++++ .../rsprot/protocol/api/SessionIdGenerator.kt | 17 + .../protocol/api/StreamCipherProvider.kt | 13 + .../api/bootstrap/BootstrapFactory.kt | 94 + .../protocol/api/channel/ChannelExtensions.kt | 36 + .../api/decoder/IncomingMessageDecoder.kt | 121 ++ .../api/encoder/OutgoingMessageEncoder.kt | 133 ++ .../protocol/api/game/GameMessageDecoder.kt | 87 + .../protocol/api/game/GameMessageEncoder.kt | 20 + .../protocol/api/game/GameMessageHandler.kt | 101 ++ .../api/handlers/ExceptionHandlers.kt | 46 + .../api/handlers/GameMessageHandlers.kt | 52 + .../api/handlers/INetAddressHandlers.kt | 48 + .../protocol/api/handlers/LoginHandlers.kt | 78 + .../DefaultGameMessageCounter.kt | 31 + .../DefaultGameMessageCounterProvider.kt | 11 + ...mingGameMessageConsumerExceptionHandler.kt | 31 + .../DefaultInetAddressTracker.kt | 30 + .../DefaultInetAddressValidator.kt | 40 + .../DefaultLoginDecoderService.kt | 21 + .../DefaultMessageQueueProvider.kt | 13 + .../DefaultSessionIdGenerator.kt | 14 + .../DefaultStreamCipherProvider.kt | 13 + .../protocol/api/js5/Js5ChannelHandler.kt | 139 ++ .../net/rsprot/protocol/api/js5/Js5Client.kt | 335 ++++ .../protocol/api/js5/Js5Configuration.kt | 88 + .../protocol/api/js5/Js5GroupProvider.kt | 19 + .../protocol/api/js5/Js5MessageDecoder.kt | 21 + .../protocol/api/js5/Js5MessageEncoder.kt | 38 + .../net/rsprot/protocol/api/js5/Js5Service.kt | 286 ++++ .../protocol/api/js5/util/IntArrayDeque.kt | 621 +++++++ .../protocol/api/js5/util/UniqueQueue.kt | 51 + .../rsprot/protocol/api/logging/LoggingExt.kt | 64 + .../api/login/GameLoginResponseHandler.kt | 231 +++ .../protocol/api/login/LoginChannelHandler.kt | 221 +++ .../api/login/LoginConnectionHandler.kt | 364 ++++ .../protocol/api/login/LoginMessageDecoder.kt | 56 + .../protocol/api/login/LoginMessageEncoder.kt | 19 + .../MessageDecoderRepositories.kt | 58 + .../MessageEncoderRepositories.kt | 31 + .../protocol/api/suppliers/NpcInfoSupplier.kt | 74 + .../api/suppliers/PlayerInfoSupplier.kt | 43 + .../api/suppliers/WorldEntityInfoSupplier.kt | 39 + .../protocol/api/util/FutureExtensions.kt | 30 + .../util/ZonePartialEnclosedCacheBuffer.kt | 187 ++ .../ZonePartialEnclosedCacheBufferTest.kt | 189 +++ .../osrs-225/osrs-225-common/build.gradle.kts | 13 + .../common/client/OldSchoolClientType.kt | 25 + .../game/outgoing/inv/InventoryObject.kt | 63 + .../osrs-225-desktop/build.gradle.kts | 59 + .../game/outgoing/info/NpcInfoBenchmark.kt | 184 ++ .../game/outgoing/info/PlayerInfoBenchmark.kt | 148 ++ .../protocol/game/outgoing/info/huffman.dat | 17 + .../codec/buttons/If1ButtonDecoder.kt | 19 + .../codec/buttons/If3ButtonDecoder.kt | 26 + .../codec/buttons/IfButtonDDecoder.kt | 29 + .../codec/buttons/IfButtonTDecoder.kt | 30 + .../incoming/codec/buttons/IfSubOpDecoder.kt | 29 + ...ClanSettingsAddBannedFromChannelDecoder.kt | 25 + ...dClanSettingsSetMutedFromChannelDecoder.kt | 27 + .../clan/ClanChannelFullRequestDecoder.kt | 18 + .../codec/clan/ClanChannelKickUserDecoder.kt | 24 + .../clan/ClanSettingsFullRequestDecoder.kt | 18 + .../codec/events/EventAppletFocusDecoder.kt | 18 + .../events/EventCameraPositionDecoder.kt | 20 + .../codec/events/EventKeyboardDecoder.kt | 29 + .../codec/events/EventMouseClickDecoder.kt | 27 + .../codec/events/EventMouseMoveDecoder.kt | 81 + .../codec/events/EventMouseScrollDecoder.kt | 18 + .../events/EventNativeMouseClickDecoder.kt | 23 + .../events/EventNativeMouseMoveDecoder.kt | 81 + .../friendchat/FriendChatJoinLeaveDecoder.kt | 23 + .../codec/friendchat/FriendChatKickDecoder.kt | 18 + .../friendchat/FriendChatSetRankDecoder.kt | 20 + .../game/incoming/codec/locs/OpLoc1Decoder.kt | 25 + .../game/incoming/codec/locs/OpLoc2Decoder.kt | 25 + .../game/incoming/codec/locs/OpLoc3Decoder.kt | 25 + .../game/incoming/codec/locs/OpLoc4Decoder.kt | 25 + .../game/incoming/codec/locs/OpLoc5Decoder.kt | 25 + .../game/incoming/codec/locs/OpLoc6Decoder.kt | 16 + .../game/incoming/codec/locs/OpLocTDecoder.kt | 31 + .../codec/messaging/MessagePrivateDecoder.kt | 26 + .../codec/messaging/MessagePublicDecoder.kt | 63 + .../misc/client/ConnectionTelemetryDecoder.kt | 38 + .../client/DetectModifiedClientDecoder.kt | 18 + .../incoming/codec/misc/client/IdleDecoder.kt | 15 + .../misc/client/MapBuildCompleteDecoder.kt | 15 + .../MembershipPromotionEligibilityDecoder.kt | 22 + .../codec/misc/client/NoTimeoutDecoder.kt | 15 + .../client/ReflectionCheckReplyDecoder.kt | 24 + .../codec/misc/client/SendPingReplyDecoder.kt | 24 + .../misc/client/SoundJingleEndDecoder.kt | 18 + .../codec/misc/client/WindowStatusDecoder.kt | 24 + .../codec/misc/user/BugReportDecoder.kt | 30 + .../codec/misc/user/ClickWorldMapDecoder.kt | 17 + .../codec/misc/user/ClientCheatDecoder.kt | 18 + .../codec/misc/user/CloseModalDecoder.kt | 15 + .../codec/misc/user/HiscoreRequestDecoder.kt | 24 + .../codec/misc/user/IfCrmViewClickDecoder.kt | 29 + .../codec/misc/user/MoveGameClickDecoder.kt | 22 + .../misc/user/MoveMinimapClickDecoder.kt | 55 + .../codec/misc/user/OculusLeaveDecoder.kt | 15 + .../codec/misc/user/SendSnapshotDecoder.kt | 24 + .../misc/user/SetChatFilterSettingsDecoder.kt | 24 + .../codec/misc/user/TeleportDecoder.kt | 24 + .../misc/user/UpdatePlayerModelDecoderOld.kt | 30 + .../game/incoming/codec/npcs/OpNpc1Decoder.kt | 21 + .../game/incoming/codec/npcs/OpNpc2Decoder.kt | 21 + .../game/incoming/codec/npcs/OpNpc3Decoder.kt | 21 + .../game/incoming/codec/npcs/OpNpc4Decoder.kt | 21 + .../game/incoming/codec/npcs/OpNpc5Decoder.kt | 21 + .../game/incoming/codec/npcs/OpNpc6Decoder.kt | 16 + .../game/incoming/codec/npcs/OpNpcTDecoder.kt | 27 + .../game/incoming/codec/objs/OpObj1Decoder.kt | 25 + .../game/incoming/codec/objs/OpObj2Decoder.kt | 25 + .../game/incoming/codec/objs/OpObj3Decoder.kt | 25 + .../game/incoming/codec/objs/OpObj4Decoder.kt | 25 + .../game/incoming/codec/objs/OpObj5Decoder.kt | 25 + .../game/incoming/codec/objs/OpObj6Decoder.kt | 22 + .../game/incoming/codec/objs/OpObjTDecoder.kt | 31 + .../codec/players/OpPlayer1Decoder.kt | 21 + .../codec/players/OpPlayer2Decoder.kt | 21 + .../codec/players/OpPlayer3Decoder.kt | 21 + .../codec/players/OpPlayer4Decoder.kt | 21 + .../codec/players/OpPlayer5Decoder.kt | 21 + .../codec/players/OpPlayer6Decoder.kt | 21 + .../codec/players/OpPlayer7Decoder.kt | 21 + .../codec/players/OpPlayer8Decoder.kt | 21 + .../codec/players/OpPlayerTDecoder.kt | 27 + .../resumed/ResumePCountDialogDecoder.kt | 18 + .../codec/resumed/ResumePNameDialogDecoder.kt | 18 + .../codec/resumed/ResumePObjDialogDecoder.kt | 18 + .../resumed/ResumePStringDialogDecoder.kt | 18 + .../codec/resumed/ResumePauseButtonDecoder.kt | 21 + .../codec/social/FriendListAddDecoder.kt | 18 + .../codec/social/FriendListDelDecoder.kt | 18 + .../codec/social/IgnoreListAddDecoder.kt | 18 + .../codec/social/IgnoreListDelDecoder.kt | 18 + .../DesktopGameMessageDecoderRepository.kt | 210 +++ .../game/incoming/prot/GameClientProt.kt | 169 ++ .../game/incoming/prot/GameClientProtId.kt | 104 ++ .../camera/CamLookAtEasedCoordEncoder.kt | 26 + .../outgoing/codec/camera/CamLookAtEncoder.kt | 26 + .../outgoing/codec/camera/CamModeEncoder.kt | 20 + .../outgoing/codec/camera/CamMoveToArc.kt | 29 + .../codec/camera/CamMoveToCyclesEncoder.kt | 27 + .../outgoing/codec/camera/CamMoveToEncoder.kt | 26 + .../outgoing/codec/camera/CamResetEncoder.kt | 12 + .../codec/camera/CamRotateByEncoder.kt | 25 + .../codec/camera/CamRotateToEncoder.kt | 25 + .../outgoing/codec/camera/CamShakeEncoder.kt | 25 + .../codec/camera/CamSmoothResetEncoder.kt | 25 + .../outgoing/codec/camera/CamTargetEncoder.kt | 38 + .../codec/camera/CamTargetOldEncoder.kt | 35 + .../codec/camera/OculusSyncEncoder.kt | 22 + .../codec/clan/ClanChannelDeltaEncoder.kt | 87 + .../codec/clan/ClanChannelFullEncoder.kt | 58 + .../codec/clan/ClanSettingsDeltaEncoder.kt | 127 ++ .../codec/clan/ClanSettingsFullEncoder.kt | 85 + .../codec/clan/MessageClanChannelEncoder.kt | 31 + .../clan/MessageClanChannelSystemEncoder.kt | 29 + .../codec/clan/VarClanDisableEncoder.kt | 12 + .../codec/clan/VarClanEnableEncoder.kt | 12 + .../outgoing/codec/clan/VarClanEncoder.kt | 35 + .../friendchat/MessageFriendChannelEncoder.kt | 31 + .../UpdateFriendChatChannelFullV1Encoder.kt | 38 + .../UpdateFriendChatChannelFullV2Encoder.kt | 38 + ...pdateFriendChatChannelSingleUserEncoder.kt | 28 + .../codec/interfaces/IfClearInvEncoder.kt | 21 + .../codec/interfaces/IfCloseSubEncoder.kt | 23 + .../codec/interfaces/IfMoveSubEncoder.kt | 23 + .../codec/interfaces/IfOpenSubEncoder.kt | 23 + .../codec/interfaces/IfOpenTopEncoder.kt | 20 + .../codec/interfaces/IfResyncEncoder.kt | 35 + .../codec/interfaces/IfSetAngleEncoder.kt | 24 + .../codec/interfaces/IfSetAnimEncoder.kt | 22 + .../codec/interfaces/IfSetColourEncoder.kt | 22 + .../codec/interfaces/IfSetEventsEncoder.kt | 24 + .../codec/interfaces/IfSetHideEncoder.kt | 22 + .../codec/interfaces/IfSetModelEncoder.kt | 22 + .../interfaces/IfSetNpcHeadActiveEncoder.kt | 22 + .../codec/interfaces/IfSetNpcHeadEncoder.kt | 22 + .../codec/interfaces/IfSetObjectEncoder.kt | 23 + .../interfaces/IfSetPlayerHeadEncoder.kt | 21 + .../IfSetPlayerModelBaseColourEncoder.kt | 23 + .../IfSetPlayerModelBodyTypeEncoder.kt | 22 + .../interfaces/IfSetPlayerModelObjEncoder.kt | 22 + .../interfaces/IfSetPlayerModelSelfEncoder.kt | 23 + .../codec/interfaces/IfSetPositionEncoder.kt | 23 + .../interfaces/IfSetRotateSpeedEncoder.kt | 23 + .../codec/interfaces/IfSetScrollPosEncoder.kt | 22 + .../codec/interfaces/IfSetTextEncoder.kt | 22 + .../codec/inv/UpdateInvFullEncoder.kt | 40 + .../codec/inv/UpdateInvPartialEncoder.kt | 40 + .../codec/inv/UpdateInvStopTransmitEncoder.kt | 20 + .../outgoing/codec/logout/LogoutEncoder.kt | 12 + .../codec/logout/LogoutTransferEncoder.kt | 24 + .../codec/logout/LogoutWithReasonEncoder.kt | 22 + .../codec/map/RebuildNormalEncoder.kt | 43 + .../codec/map/RebuildRegionEncoder.kt | 25 + .../codec/map/RebuildWorldEntityEncoder.kt | 36 + .../outgoing/codec/map/util/RegionEncoder.kt | 100 ++ .../codec/misc/client/HideLocOpsEncoder.kt | 22 + .../codec/misc/client/HideNpcOpsEncoder.kt | 22 + .../codec/misc/client/HideObjOpsEncoder.kt | 22 + .../codec/misc/client/HintArrowEncoder.kt | 43 + .../codec/misc/client/HiscoreReplyEncoder.kt | 45 + .../codec/misc/client/MinimapToggleEncoder.kt | 22 + .../misc/client/ReflectionCheckerEncoder.kt | 71 + .../codec/misc/client/ResetAnimsEncoder.kt | 12 + .../codec/misc/client/SendPingEncoder.kt | 23 + .../codec/misc/client/ServerTickEndEncoder.kt | 12 + .../misc/client/SetHeatmapEnabledEncoder.kt | 22 + .../codec/misc/client/SiteSettingsEncoder.kt | 22 + .../misc/client/UpdateRebootTimerEncoder.kt | 20 + .../codec/misc/client/UpdateUid192Encoder.kt | 24 + .../codec/misc/client/UrlOpenEncoder.kt | 28 + .../misc/player/ChatFilterSettingsEncoder.kt | 21 + .../ChatFilterSettingsPrivateChatEncoder.kt | 22 + .../codec/misc/player/MessageGameEncoder.kt | 30 + .../misc/player/RunClientScriptEncoder.kt | 35 + .../codec/misc/player/SetMapFlagEncoder.kt | 23 + .../codec/misc/player/SetPlayerOpEncoder.kt | 22 + .../player/TriggerOnDialogAbortEncoder.kt | 12 + .../misc/player/UpdateRunEnergyEncoder.kt | 22 + .../misc/player/UpdateRunWeightEncoder.kt | 22 + .../codec/misc/player/UpdateStatEncoder.kt | 23 + .../codec/misc/player/UpdateStatOldEncoder.kt | 22 + .../player/UpdateStockMarketSlotEncoder.kt | 36 + .../misc/player/UpdateTradingPostEncoder.kt | 42 + .../DesktopLowResolutionChangeEncoder.kt | 41 + .../codec/npcinfo/NpcInfoLargeEncoder.kt | 21 + .../codec/npcinfo/NpcInfoSmallEncoder.kt | 21 + .../npcinfo/SetNpcUpdateOriginEncoder.kt | 24 + .../NpcBaseAnimationSetEncoder.kt | 72 + .../NpcBodyCustomisationEncoder.kt | 81 + .../NpcCombatLevelChangeEncoder.kt | 23 + .../extendedinfo/NpcExactMoveEncoder.kt | 29 + .../extendedinfo/NpcFaceCoordEncoder.kt | 25 + .../NpcFacePathingEntityEncoder.kt | 24 + .../NpcHeadCustomisationEncoder.kt | 81 + .../NpcHeadIconCustomisationEncoder.kt | 34 + .../npcinfo/extendedinfo/NpcHitEncoder.kt | 103 ++ .../extendedinfo/NpcNameChangeEncoder.kt | 25 + .../npcinfo/extendedinfo/NpcSayEncoder.kt | 25 + .../extendedinfo/NpcSequenceEncoder.kt | 24 + .../extendedinfo/NpcSpotAnimEncoder.kt | 36 + .../npcinfo/extendedinfo/NpcTintingEncoder.kt | 29 + .../extendedinfo/NpcTransformationEncoder.kt | 23 + .../extendedinfo/NpcVisibleOpsEncoder.kt | 23 + .../NpcAvatarExtendedInfoDesktopWriter.kt | 194 +++ .../codec/playerinfo/PlayerInfoEncoder.kt | 21 + .../extendedinfo/PlayerAppearanceEncoder.kt | 210 +++ .../extendedinfo/PlayerChatEncoder.kt | 47 + .../extendedinfo/PlayerExactMoveEncoder.kt | 29 + .../extendedinfo/PlayerFaceAngleEncoder.kt | 23 + .../PlayerFacePathingEntityEncoder.kt | 24 + .../extendedinfo/PlayerHitEncoder.kt | 109 ++ .../extendedinfo/PlayerMoveSpeedEncoder.kt | 23 + .../extendedinfo/PlayerSayEncoder.kt | 25 + .../extendedinfo/PlayerSequenceEncoder.kt | 24 + .../extendedinfo/PlayerSpotAnimEncoder.kt | 36 + .../PlayerTemporaryMoveSpeedEncoder.kt | 23 + .../extendedinfo/PlayerTintingEncoder.kt | 22 + .../PlayerAvatarExtendedInfoDesktopWriter.kt | 161 ++ .../codec/social/FriendListLoadedEncoder.kt | 12 + .../codec/social/MessagePrivateEchoEncoder.kt | 27 + .../codec/social/MessagePrivateEncoder.kt | 30 + .../codec/social/UpdateFriendListEncoder.kt | 35 + .../codec/social/UpdateIgnoreListEncoder.kt | 35 + .../outgoing/codec/sound/MidiJingleEncoder.kt | 21 + .../outgoing/codec/sound/MidiSongEncoder.kt | 27 + .../codec/sound/MidiSongOldEncoder.kt | 20 + .../codec/sound/MidiSongStopEncoder.kt | 24 + .../sound/MidiSongWithSecondaryEncoder.kt | 28 + .../outgoing/codec/sound/MidiSwapEncoder.kt | 26 + .../outgoing/codec/sound/SynthSoundEncoder.kt | 24 + .../codec/specific/LocAnimSpecificEncoder.kt | 22 + .../codec/specific/MapAnimSpecificEncoder.kt | 23 + .../codec/specific/NpcAnimSpecificEncoder.kt | 22 + .../specific/NpcHeadIconSpecificEncoder.kt | 23 + .../specific/NpcSpotAnimSpecificEncoder.kt | 23 + .../specific/PlayerAnimSpecificEncoder.kt | 21 + .../specific/PlayerSpotAnimSpecificEncoder.kt | 23 + .../codec/specific/ProjAnimSpecificEncoder.kt | 31 + .../outgoing/codec/varp/VarpLargeEncoder.kt | 21 + .../outgoing/codec/varp/VarpResetEncoder.kt | 12 + .../outgoing/codec/varp/VarpSmallEncoder.kt | 21 + .../outgoing/codec/varp/VarpSyncEncoder.kt | 12 + .../codec/worldentity/ClearEntitiesEncoder.kt | 12 + .../worldentity/SetActiveWorldEncoder.kt | 36 + .../worldentity/WorldEntityInfoEncoder.kt | 21 + ...DesktopUpdateZonePartialEnclosedEncoder.kt | 157 ++ .../header/UpdateZoneFullFollowsEncoder.kt | 22 + .../header/UpdateZonePartialFollowsEncoder.kt | 22 + .../codec/zone/payload/LocAddChangeEncoder.kt | 24 + .../codec/zone/payload/LocAnimEncoder.kt | 24 + .../codec/zone/payload/LocDelEncoder.kt | 22 + .../codec/zone/payload/LocMergeEncoder.kt | 30 + .../codec/zone/payload/MapAnimEncoder.kt | 24 + .../codec/zone/payload/MapProjAnimEncoder.kt | 33 + .../codec/zone/payload/ObjAddEncoder.kt | 29 + .../codec/zone/payload/ObjCountEncoder.kt | 24 + .../codec/zone/payload/ObjDelEncoder.kt | 23 + .../zone/payload/ObjEnabledOpsEncoder.kt | 23 + .../codec/zone/payload/SoundAreaEncoder.kt | 26 + .../DesktopGameMessageEncoderRepository.kt | 305 ++++ .../game/outgoing/prot/GameServerProt.kt | 202 +++ .../game/outgoing/prot/GameServerProtId.kt | 141 ++ .../game/outgoing/info/NpcInfoClient.kt | 304 ++++ .../game/outgoing/info/NpcInfoTest.kt | 314 ++++ .../game/outgoing/info/PlayerInfoClient.kt | 560 ++++++ .../game/outgoing/info/PlayerInfoTest.kt | 196 +++ .../protocol/game/outgoing/info/huffman.dat | 17 + .../osrs-225-internal/build.gradle.kts | 20 + .../net/rsprot/protocol/common/LogLevel.kt | 10 + .../net/rsprot/protocol/common/RSProtFlags.kt | 142 ++ .../protocol/common/client/ClientTypeMap.kt | 74 + .../codec/zone/payload/OldSchoolZoneProt.kt | 15 + .../codec/zone/payload/ZoneProtEncoder.kt | 27 + .../game/outgoing/info/CachedExtendedInfo.kt | 14 + .../common/game/outgoing/info/CoordGrid.kt | 96 ++ .../common/game/outgoing/info/ExtendedInfo.kt | 95 ++ .../outgoing/info/TransientExtendedInfo.kt | 18 + .../info/encoder/ExtendedInfoEncoder.kt | 8 + .../encoder/OnDemandExtendedInfoEncoder.kt | 18 + .../encoder/PrecomputedExtendedInfoEncoder.kt | 19 + .../outgoing/info/npcinfo/NpcAvatarDetails.kt | 128 ++ .../encoder/NpcExtendedInfoEncoders.kt | 45 + .../encoder/NpcResolutionChangeEncoder.kt | 19 + .../npcinfo/extendedinfo/BaseAnimationSet.kt | 65 + .../npcinfo/extendedinfo/BodyCustomisation.kt | 16 + .../npcinfo/extendedinfo/CombatLevelChange.kt | 20 + .../info/npcinfo/extendedinfo/FaceCoord.kt | 20 + .../npcinfo/extendedinfo/HeadCustomisation.kt | 16 + .../extendedinfo/HeadIconCustomisation.kt | 30 + .../info/npcinfo/extendedinfo/NameChange.kt | 16 + .../info/npcinfo/extendedinfo/NpcTinting.kt | 21 + .../npcinfo/extendedinfo/Transformation.kt | 16 + .../npcinfo/extendedinfo/TypeCustomisation.kt | 8 + .../info/npcinfo/extendedinfo/VisibleOps.kt | 20 + .../encoder/PlayerExtendedInfoEncoders.kt | 37 + .../playerinfo/extendedinfo/Appearance.kt | 232 +++ .../info/playerinfo/extendedinfo/Chat.kt | 53 + .../info/playerinfo/extendedinfo/FaceAngle.kt | 44 + .../info/playerinfo/extendedinfo/MoveSpeed.kt | 34 + .../extendedinfo/ObjTypeCustomisation.kt | 28 + .../extendedinfo/PlayerTintingList.kt | 31 + .../extendedinfo/TemporaryMoveSpeed.kt | 27 + .../info/shared/extendedinfo/ExactMove.kt | 68 + .../shared/extendedinfo/FacePathingEntity.kt | 28 + .../outgoing/info/shared/extendedinfo/Hit.kt | 24 + .../outgoing/info/shared/extendedinfo/Say.kt | 24 + .../info/shared/extendedinfo/Sequence.kt | 29 + .../info/shared/extendedinfo/SpotAnimList.kt | 69 + .../info/shared/extendedinfo/Tinting.kt | 47 + .../info/shared/extendedinfo/util/HeadBar.kt | 38 + .../shared/extendedinfo/util/HeadBarList.kt | 20 + .../info/shared/extendedinfo/util/HitMark.kt | 67 + .../shared/extendedinfo/util/HitMarkList.kt | 15 + .../info/shared/extendedinfo/util/SpotAnim.kt | 39 + .../game/outgoing/inv/internal/Inventory.kt | 61 + .../outgoing/inv/internal/InventoryPool.kt | 51 + .../osrs-225/osrs-225-model/build.gradle.kts | 20 + .../game/incoming/GameClientProtCategory.kt | 11 + .../game/incoming/buttons/If1Button.kt | 43 + .../game/incoming/buttons/If3Button.kt | 83 + .../game/incoming/buttons/IfButtonD.kt | 113 ++ .../game/incoming/buttons/IfButtonT.kt | 110 ++ .../protocol/game/incoming/buttons/IfSubOp.kt | 89 + ...AffinedClanSettingsAddBannedFromChannel.kt | 67 + .../AffinedClanSettingsSetMutedFromChannel.kt | 74 + .../incoming/clan/ClanChannelFullRequest.kt | 33 + .../game/incoming/clan/ClanChannelKickUser.kt | 66 + .../incoming/clan/ClanSettingsFullRequest.kt | 34 + .../game/incoming/events/EventAppletFocus.kt | 30 + .../incoming/events/EventCameraPosition.kt | 56 + .../game/incoming/events/EventKeyboard.kt | 329 ++++ .../game/incoming/events/EventMouseClick.kt | 77 + .../game/incoming/events/EventMouseMove.kt | 58 + .../game/incoming/events/EventMouseScroll.kt | 31 + .../incoming/events/EventNativeMouseClick.kt | 78 + .../incoming/events/EventNativeMouseMove.kt | 57 + .../incoming/events/util/MouseMovements.kt | 79 + .../friendchat/FriendChatJoinLeave.kt | 31 + .../incoming/friendchat/FriendChatKick.kt | 30 + .../incoming/friendchat/FriendChatSetRank.kt | 54 + .../protocol/game/incoming/locs/OpLoc.kt | 81 + .../protocol/game/incoming/locs/OpLoc6.kt | 30 + .../protocol/game/incoming/locs/OpLocT.kt | 105 ++ .../game/incoming/messaging/MessagePrivate.kt | 44 + .../game/incoming/messaging/MessagePublic.kt | 250 +++ .../misc/client/ConnectionTelemetry.kt | 79 + .../misc/client/DetectModifiedClient.kt | 31 + .../game/incoming/misc/client/Idle.kt | 15 + .../incoming/misc/client/MapBuildComplete.kt | 16 + .../client/MembershipPromotionEligibility.kt | 60 + .../game/incoming/misc/client/NoTimeout.kt | 14 + .../misc/client/ReflectionCheckReply.kt | 376 ++++ .../incoming/misc/client/SendPingReply.kt | 70 + .../incoming/misc/client/SoundJingleEnd.kt | 30 + .../game/incoming/misc/client/WindowStatus.kt | 62 + .../game/incoming/misc/user/BugReport.kt | 66 + .../game/incoming/misc/user/ClickWorldMap.kt | 55 + .../game/incoming/misc/user/ClientCheat.kt | 29 + .../game/incoming/misc/user/CloseModal.kt | 15 + .../game/incoming/misc/user/HiscoreRequest.kt | 64 + .../game/incoming/misc/user/IfCrmViewClick.kt | 96 ++ .../game/incoming/misc/user/MoveGameClick.kt | 67 + .../incoming/misc/user/MoveMinimapClick.kt | 172 ++ .../game/incoming/misc/user/OculusLeave.kt | 14 + .../game/incoming/misc/user/SendSnapshot.kt | 92 + .../misc/user/SetChatFilterSettings.kt | 80 + .../game/incoming/misc/user/Teleport.kt | 82 + .../misc/user/UpdatePlayerModelOld.kt | 102 ++ .../misc/user/internal/MovementRequest.kt | 34 + .../protocol/game/incoming/npcs/OpNpc.kt | 64 + .../protocol/game/incoming/npcs/OpNpc6.kt | 29 + .../protocol/game/incoming/npcs/OpNpcT.kt | 91 + .../protocol/game/incoming/objs/OpObj.kt | 81 + .../protocol/game/incoming/objs/OpObj6.kt | 63 + .../protocol/game/incoming/objs/OpObjT.kt | 109 ++ .../game/incoming/players/OpPlayer.kt | 64 + .../game/incoming/players/OpPlayerT.kt | 91 + .../incoming/resumed/ResumePCountDialog.kt | 33 + .../incoming/resumed/ResumePNameDialog.kt | 31 + .../game/incoming/resumed/ResumePObjDialog.kt | 31 + .../incoming/resumed/ResumePStringDialog.kt | 30 + .../incoming/resumed/ResumePauseButton.kt | 64 + .../game/incoming/social/FriendListAdd.kt | 30 + .../game/incoming/social/FriendListDel.kt | 30 + .../game/incoming/social/IgnoreListAdd.kt | 30 + .../game/incoming/social/IgnoreListDel.kt | 30 + .../game/outgoing/GameServerProtCategory.kt | 15 + .../game/outgoing/camera/CamLookAt.kt | 88 + .../outgoing/camera/CamLookAtEasedCoord.kt | 84 + .../protocol/game/outgoing/camera/CamMode.kt | 31 + .../game/outgoing/camera/CamMoveTo.kt | 87 + .../game/outgoing/camera/CamMoveToArc.kt | 117 ++ .../game/outgoing/camera/CamMoveToCycles.kt | 94 + .../protocol/game/outgoing/camera/CamReset.kt | 15 + .../game/outgoing/camera/CamRotateBy.kt | 87 + .../game/outgoing/camera/CamRotateTo.kt | 84 + .../protocol/game/outgoing/camera/CamShake.kt | 97 ++ .../game/outgoing/camera/CamSmoothReset.kt | 78 + .../game/outgoing/camera/CamTarget.kt | 144 ++ .../game/outgoing/camera/CamTargetOld.kt | 135 ++ .../game/outgoing/camera/OculusSync.kt | 36 + .../camera/util/CameraEaseFunction.kt | 59 + .../game/outgoing/clan/ClanChannelDelta.kt | 337 ++++ .../game/outgoing/clan/ClanChannelFull.kt | 247 +++ .../game/outgoing/clan/ClanSettingsDelta.kt | 701 ++++++++ .../game/outgoing/clan/ClanSettingsFull.kt | 469 +++++ .../game/outgoing/clan/MessageClanChannel.kt | 104 ++ .../outgoing/clan/MessageClanChannelSystem.kt | 88 + .../protocol/game/outgoing/clan/VarClan.kt | 125 ++ .../game/outgoing/clan/VarClanDisable.kt | 14 + .../game/outgoing/clan/VarClanEnable.kt | 14 + .../friendchat/MessageFriendChannel.kt | 106 ++ .../friendchat/UpdateFriendChatChannelFull.kt | 69 + .../UpdateFriendChatChannelFullV1.kt | 98 ++ .../UpdateFriendChatChannelFullV2.kt | 95 ++ .../UpdateFriendChatChannelSingleUser.kt | 154 ++ .../outgoing/info/AvatarExtendedInfoWriter.kt | 85 + .../game/outgoing/info/InfoRepository.kt | 177 ++ .../info/ObserverExtendedInfoFlags.kt | 47 + .../info/exceptions/InfoProcessException.kt | 13 + .../info/filter/DefaultExtendedInfoFilter.kt | 33 + .../info/filter/ExtendedInfoFilter.kt | 40 + .../game/outgoing/info/npcinfo/NpcAvatar.kt | 411 +++++ .../info/npcinfo/NpcAvatarExceptionHandler.kt | 22 + .../info/npcinfo/NpcAvatarExtendedInfo.kt | 1255 ++++++++++++++ .../npcinfo/NpcAvatarExtendedInfoBlocks.kt | 78 + .../outgoing/info/npcinfo/NpcAvatarFactory.kt | 92 + .../info/npcinfo/NpcAvatarRepository.kt | 154 ++ .../outgoing/info/npcinfo/NpcIndexSupplier.kt | 55 + .../game/outgoing/info/npcinfo/NpcInfo.kt | 610 +++++++ .../outgoing/info/npcinfo/NpcInfoLarge.kt | 29 + .../outgoing/info/npcinfo/NpcInfoProtocol.kt | 268 +++ .../info/npcinfo/NpcInfoRepository.kt | 39 + .../outgoing/info/npcinfo/NpcInfoSmall.kt | 29 + .../info/npcinfo/NpcInfoWorldDetails.kt | 191 +++ .../npcinfo/NpcInfoWorldDetailsStorage.kt | 39 + .../info/npcinfo/SetNpcUpdateOrigin.kt | 62 + .../info/npcinfo/util/NpcCellOpcodes.kt | 64 + .../GlobalLowResolutionPositionRepository.kt | 69 + .../outgoing/info/playerinfo/PlayerAvatar.kt | 238 +++ .../playerinfo/PlayerAvatarExtendedInfo.kt | 1505 +++++++++++++++++ .../PlayerAvatarExtendedInfoBlocks.kt | 68 + .../info/playerinfo/PlayerAvatarFactory.kt | 25 + .../outgoing/info/playerinfo/PlayerInfo.kt | 1014 +++++++++++ .../info/playerinfo/PlayerInfoPacket.kt | 31 + .../info/playerinfo/PlayerInfoProtocol.kt | 258 +++ .../info/playerinfo/PlayerInfoRepository.kt | 48 + .../info/playerinfo/PlayerInfoWorldDetails.kt | 29 + .../info/playerinfo/util/CellOpcodes.kt | 147 ++ .../playerinfo/util/LowResolutionPosition.kt | 32 + .../game/outgoing/info/util/Avatar.kt | 10 + .../game/outgoing/info/util/BuildArea.kt | 124 ++ .../info/util/ReferencePooledObject.kt | 34 + .../info/worker/DefaultProtocolWorker.kt | 35 + .../ForkJoinMultiThreadProtocolWorker.kt | 14 + .../outgoing/info/worker/ProtocolWorker.kt | 17 + .../info/worker/SingleThreadProtocolWorker.kt | 14 + .../info/worldentityinfo/WorldEntityAvatar.kt | 109 ++ .../WorldEntityAvatarExceptionHandler.kt | 16 + .../WorldEntityAvatarFactory.kt | 59 + .../WorldEntityAvatarRepository.kt | 115 ++ .../WorldEntityIndexSupplier.kt | 25 + .../info/worldentityinfo/WorldEntityInfo.kt | 476 ++++++ .../WorldEntityInfoExtensions.kt | 23 + .../worldentityinfo/WorldEntityInfoPacket.kt | 21 + .../WorldEntityInfoRepository.kt | 33 + .../worldentityinfo/WorldEntityProtocol.kt | 169 ++ .../game/outgoing/interfaces/IfClearInv.kt | 51 + .../game/outgoing/interfaces/IfCloseSub.kt | 49 + .../game/outgoing/interfaces/IfMoveSub.kt | 78 + .../game/outgoing/interfaces/IfOpenSub.kt | 99 ++ .../game/outgoing/interfaces/IfOpenTop.kt | 31 + .../game/outgoing/interfaces/IfResync.kt | 222 +++ .../game/outgoing/interfaces/IfSetAngle.kt | 94 + .../game/outgoing/interfaces/IfSetAnim.kt | 72 + .../game/outgoing/interfaces/IfSetColour.kt | 191 +++ .../game/outgoing/interfaces/IfSetEvents.kt | 92 + .../game/outgoing/interfaces/IfSetHide.kt | 61 + .../game/outgoing/interfaces/IfSetModel.kt | 72 + .../game/outgoing/interfaces/IfSetNpcHead.kt | 73 + .../outgoing/interfaces/IfSetNpcHeadActive.kt | 75 + .../game/outgoing/interfaces/IfSetObject.kt | 83 + .../outgoing/interfaces/IfSetPlayerHead.kt | 50 + .../interfaces/IfSetPlayerModelBaseColour.kt | 85 + .../interfaces/IfSetPlayerModelBodyType.kt | 73 + .../interfaces/IfSetPlayerModelObj.kt | 63 + .../interfaces/IfSetPlayerModelSelf.kt | 62 + .../game/outgoing/interfaces/IfSetPosition.kt | 82 + .../outgoing/interfaces/IfSetRotateSpeed.kt | 89 + .../outgoing/interfaces/IfSetScrollPos.kt | 72 + .../game/outgoing/interfaces/IfSetText.kt | 62 + .../game/outgoing/inv/UpdateInvFull.kt | 178 ++ .../game/outgoing/inv/UpdateInvPartial.kt | 198 +++ .../outgoing/inv/UpdateInvStopTransmit.kt | 35 + .../protocol/game/outgoing/logout/Logout.kt | 15 + .../game/outgoing/logout/LogoutTransfer.kt | 99 ++ .../game/outgoing/logout/LogoutWithReason.kt | 41 + .../game/outgoing/map/RebuildLogin.kt | 102 ++ .../game/outgoing/map/RebuildNormal.kt | 72 + .../game/outgoing/map/RebuildRegion.kt | 144 ++ .../game/outgoing/map/RebuildWorldEntity.kt | 156 ++ .../game/outgoing/map/StaticRebuildMessage.kt | 11 + .../outgoing/map/util/RebuildRegionZone.kt | 99 ++ .../game/outgoing/map/util/XteaHelper.kt | 26 + .../game/outgoing/map/util/XteaProvider.kt | 12 + .../game/outgoing/misc/client/HideLocOps.kt | 29 + .../game/outgoing/misc/client/HideNpcOps.kt | 29 + .../game/outgoing/misc/client/HideObjOps.kt | 29 + .../game/outgoing/misc/client/HintArrow.kt | 194 +++ .../game/outgoing/misc/client/HiscoreReply.kt | 161 ++ .../outgoing/misc/client/MinimapToggle.kt | 43 + .../outgoing/misc/client/ReflectionChecker.kt | 273 +++ .../game/outgoing/misc/client/ResetAnims.kt | 16 + .../game/outgoing/misc/client/SendPing.kt | 47 + .../outgoing/misc/client/ServerTickEnd.kt | 16 + .../outgoing/misc/client/SetHeatmapEnabled.kt | 35 + .../game/outgoing/misc/client/SiteSettings.kt | 31 + .../outgoing/misc/client/UpdateRebootTimer.kt | 38 + .../game/outgoing/misc/client/UpdateUid192.kt | 33 + .../game/outgoing/misc/client/UrlOpen.kt | 30 + .../misc/player/ChatFilterSettings.kt | 69 + .../player/ChatFilterSettingsPrivateChat.kt | 40 + .../game/outgoing/misc/player/MessageGame.kt | 119 ++ .../outgoing/misc/player/RunClientScript.kt | 112 ++ .../game/outgoing/misc/player/SetMapFlag.kt | 49 + .../game/outgoing/misc/player/SetPlayerOp.kt | 62 + .../misc/player/TriggerOnDialogAbort.kt | 15 + .../outgoing/misc/player/UpdateRunEnergy.kt | 30 + .../outgoing/misc/player/UpdateRunWeight.kt | 29 + .../game/outgoing/misc/player/UpdateStat.kt | 74 + .../outgoing/misc/player/UpdateStatOld.kt | 65 + .../misc/player/UpdateStockMarketSlot.kt | 130 ++ .../outgoing/misc/player/UpdateTradingPost.kt | 155 ++ .../game/outgoing/social/FriendListLoaded.kt | 16 + .../game/outgoing/social/MessagePrivate.kt | 98 ++ .../outgoing/social/MessagePrivateEcho.kt | 45 + .../game/outgoing/social/UpdateFriendList.kt | 262 +++ .../game/outgoing/social/UpdateIgnoreList.kt | 107 ++ .../game/outgoing/sound/MidiJingle.kt | 65 + .../protocol/game/outgoing/sound/MidiSong.kt | 86 + .../game/outgoing/sound/MidiSongOld.kt | 30 + .../game/outgoing/sound/MidiSongStop.kt | 54 + .../outgoing/sound/MidiSongWithSecondary.kt | 100 ++ .../protocol/game/outgoing/sound/MidiSwap.kt | 76 + .../game/outgoing/sound/SynthSound.kt | 63 + .../game/outgoing/specific/LocAnimSpecific.kt | 123 ++ .../game/outgoing/specific/MapAnimSpecific.kt | 125 ++ .../game/outgoing/specific/NpcAnimSpecific.kt | 64 + .../outgoing/specific/NpcHeadIconSpecific.kt | 90 + .../outgoing/specific/NpcSpotAnimSpecific.kt | 83 + .../outgoing/specific/PlayerAnimSpecific.kt | 57 + .../specific/PlayerSpotAnimSpecific.kt | 83 + .../outgoing/specific/ProjAnimSpecific.kt | 300 ++++ .../protocol/game/outgoing/util/OpFlags.kt | 72 + .../protocol/game/outgoing/varp/VarpLarge.kt | 56 + .../protocol/game/outgoing/varp/VarpReset.kt | 18 + .../protocol/game/outgoing/varp/VarpSmall.kt | 57 + .../protocol/game/outgoing/varp/VarpSync.kt | 19 + .../outgoing/worldentity/ClearEntities.kt | 16 + .../outgoing/worldentity/SetActiveWorld.kt | 126 ++ .../zone/header/UpdateZoneFullFollows.kt | 77 + .../zone/header/UpdateZonePartialEnclosed.kt | 78 + .../zone/header/UpdateZonePartialFollows.kt | 76 + .../outgoing/zone/payload/LocAddChange.kt | 96 ++ .../game/outgoing/zone/payload/LocAnim.kt | 85 + .../game/outgoing/zone/payload/LocDel.kt | 76 + .../game/outgoing/zone/payload/LocMerge.kt | 159 ++ .../game/outgoing/zone/payload/MapAnim.kt | 86 + .../game/outgoing/zone/payload/MapProjAnim.kt | 216 +++ .../game/outgoing/zone/payload/ObjAdd.kt | 154 ++ .../game/outgoing/zone/payload/ObjCount.kt | 85 + .../game/outgoing/zone/payload/ObjDel.kt | 78 + .../outgoing/zone/payload/ObjEnabledOps.kt | 78 + .../game/outgoing/zone/payload/SoundArea.kt | 124 ++ .../zone/payload/util/CoordInBuildArea.kt | 65 + .../outgoing/zone/payload/util/CoordInZone.kt | 26 + .../zone/payload/util/LocProperties.kt | 26 + .../protocol/js5/incoming/Js5GroupRequest.kt | 9 + .../protocol/js5/incoming/PrefetchRequest.kt | 36 + .../js5/incoming/PriorityChangeHigh.kt | 5 + .../js5/incoming/PriorityChangeLow.kt | 5 + .../protocol/js5/incoming/UrgentRequest.kt | 36 + .../rsprot/protocol/js5/incoming/XorChange.kt | 20 + .../protocol/js5/outgoing/Js5GroupResponse.kt | 45 + .../protocol/loginprot/incoming/GameLogin.kt | 13 + .../loginprot/incoming/GameReconnect.kt | 13 + .../loginprot/incoming/InitGameConnection.kt | 5 + .../incoming/InitJs5RemoteConnection.kt | 32 + .../loginprot/incoming/ProofOfWorkReply.kt | 7 + .../incoming/RemainingBetaArchives.kt | 20 + .../incoming/pow/NopProofOfWorkProvider.kt | 24 + .../loginprot/incoming/pow/ProofOfWork.kt | 42 + .../incoming/pow/ProofOfWorkProvider.kt | 17 + .../pow/SingleTypeProofOfWorkProvider.kt | 30 + .../pow/challenges/ChallengeGenerator.kt | 13 + .../pow/challenges/ChallengeMetaData.kt | 6 + .../challenges/ChallengeMetaDataProvider.kt | 17 + .../incoming/pow/challenges/ChallengeType.kt | 23 + .../pow/challenges/ChallengeVerifier.kt | 22 + .../pow/challenges/ChallengeWorker.kt | 25 + .../pow/challenges/DefaultChallengeWorker.kt | 21 + .../sha256/DefaultSha256ChallengeGenerator.kt | 32 + .../sha256/DefaultSha256MetaDataProvider.kt | 15 + .../DefaultSha256ProofOfWorkProvider.kt | 24 + .../pow/challenges/sha256/Sha256Challenge.kt | 76 + .../sha256/Sha256ChallengeVerifier.kt | 78 + .../pow/challenges/sha256/Sha256MetaData.kt | 52 + .../DefaultSha256MessageDigestHashFunction.kt | 17 + .../sha256/hashing/Sha256HashFunction.kt | 13 + ...eadLocalSha256MessageDigestHashFunction.kt | 24 + .../incoming/util/AuthenticationType.kt | 32 + .../util/CyclicRedundancyCheckBlock.kt | 34 + .../incoming/util/HostPlatformStats.kt | 160 ++ .../loginprot/incoming/util/LoginBlock.kt | 152 ++ .../util/LoginBlockDecodingFunction.kt | 10 + .../incoming/util/LoginClientType.kt | 21 + .../incoming/util/LoginPlatformType.kt | 18 + .../incoming/util/OtpAuthenticationType.kt | 46 + .../loginprot/incoming/util/Password.kt | 27 + .../protocol/loginprot/incoming/util/Token.kt | 24 + .../loginprot/outgoing/LoginResponse.kt | 215 +++ .../outgoing/util/AuthenticatorResponse.kt | 9 + .../osrs-225/osrs-225-shared/build.gradle.kts | 20 + .../incoming/codec/PrefetchRequestDecoder.kt | 20 + .../codec/PriorityChangeHighDecoder.kt | 16 + .../codec/PriorityChangeLowDecoder.kt | 16 + .../incoming/codec/UrgentRequestDecoder.kt | 20 + .../js5/incoming/codec/XorChangeDecoder.kt | 17 + .../common/js5/incoming/prot/Js5ClientProt.kt | 14 + .../js5/incoming/prot/Js5ClientProtId.kt | 9 + .../prot/Js5MessageDecoderRepository.kt | 26 + .../outgoing/codec/Js5GroupResponseEncoder.kt | 34 + .../prot/Js5MessageEncoderRepository.kt | 20 + .../common/js5/outgoing/prot/Js5ServerProt.kt | 12 + .../incoming/codec/GameLoginDecoder.kt | 85 + .../incoming/codec/GameReconnectDecoder.kt | 35 + .../codec/InitGameConnectionDecoder.kt | 13 + .../codec/InitJs5RemoteConnectionDecoder.kt | 20 + .../incoming/codec/ProofOfWorkReplyDecoder.kt | 16 + .../codec/RemainingBetaArchivesDecoder.kt | 33 + .../codec/shared/LoginBlockDecoder.kt | 238 +++ .../incoming/prot/LoginClientProt.kt | 17 + .../incoming/prot/LoginClientProtId.kt | 12 + .../prot/LoginMessageDecoderRepository.kt | 34 + .../DisallowedByScriptLoginResponseEncoder.kt | 22 + .../codec/EmptyLoginResponseEncoder.kt | 18 + .../outgoing/codec/OkLoginResponseEncoder.kt | 41 + .../codec/ProofOfWorkResponseEncoder.kt | 22 + .../codec/ReconnectOkResponseEncoder.kt | 21 + .../codec/SuccessfulLoginResponseEncoder.kt | 21 + .../prot/LoginMessageEncoderRepository.kt | 84 + .../outgoing/prot/LoginServerProt.kt | 67 + .../outgoing/prot/LoginServerProtId.kt | 70 + 715 files changed, 47072 insertions(+) create mode 100644 protocol/osrs-225/build.gradle.kts create mode 100644 protocol/osrs-225/osrs-225-api/build.gradle.kts create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/AbstractNetworkServiceFactory.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/ChannelExceptionHandler.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/EntityInfoProtocols.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameConnectionHandler.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameMessageCounter.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameMessageCounterProvider.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/IncomingGameMessageConsumerExceptionHandler.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/InetAddressTracker.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/InetAddressValidator.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/LoginChannelInitializer.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/LoginDecoderService.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/MessageQueueProvider.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/NetworkService.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/Session.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/SessionIdGenerator.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/StreamCipherProvider.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/bootstrap/BootstrapFactory.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/channel/ChannelExtensions.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/decoder/IncomingMessageDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/encoder/OutgoingMessageEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageHandler.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/ExceptionHandlers.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/GameMessageHandlers.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/INetAddressHandlers.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/LoginHandlers.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultGameMessageCounter.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultGameMessageCounterProvider.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultIncomingGameMessageConsumerExceptionHandler.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultInetAddressTracker.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultInetAddressValidator.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultLoginDecoderService.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultMessageQueueProvider.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultSessionIdGenerator.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultStreamCipherProvider.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5ChannelHandler.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Client.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Configuration.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5GroupProvider.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5MessageDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5MessageEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Service.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/util/IntArrayDeque.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/util/UniqueQueue.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/logging/LoggingExt.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginChannelHandler.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginMessageDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginMessageEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/repositories/MessageDecoderRepositories.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/repositories/MessageEncoderRepositories.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/NpcInfoSupplier.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/PlayerInfoSupplier.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/WorldEntityInfoSupplier.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/util/FutureExtensions.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/util/ZonePartialEnclosedCacheBuffer.kt create mode 100644 protocol/osrs-225/osrs-225-api/src/test/kotlin/net/rsprot/protocol/api/util/ZonePartialEnclosedCacheBufferTest.kt create mode 100644 protocol/osrs-225/osrs-225-common/build.gradle.kts create mode 100644 protocol/osrs-225/osrs-225-common/src/main/kotlin/net/rsprot/protocol/common/client/OldSchoolClientType.kt create mode 100644 protocol/osrs-225/osrs-225-common/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/InventoryObject.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/build.gradle.kts create mode 100644 protocol/osrs-225/osrs-225-desktop/src/benchmarks/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoBenchmark.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/benchmarks/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoBenchmark.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/benchmarks/resources/net/rsprot/protocol/game/outgoing/info/huffman.dat create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/If1ButtonDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/If3ButtonDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfButtonDDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfButtonTDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfSubOpDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/AffinedClanSettingsAddBannedFromChannelDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/AffinedClanSettingsSetMutedFromChannelDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanChannelFullRequestDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanChannelKickUserDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanSettingsFullRequestDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventAppletFocusDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventCameraPositionDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventKeyboardDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventMouseClickDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventMouseMoveDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventMouseScrollDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventNativeMouseClickDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventNativeMouseMoveDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatJoinLeaveDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatKickDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatSetRankDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc1Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc2Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc3Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc4Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc5Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc6Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLocTDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/messaging/MessagePrivateDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/messaging/MessagePublicDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/ConnectionTelemetryDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/DetectModifiedClientDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/IdleDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/MapBuildCompleteDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/MembershipPromotionEligibilityDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/NoTimeoutDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/ReflectionCheckReplyDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/SendPingReplyDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/SoundJingleEndDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/WindowStatusDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/BugReportDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/ClickWorldMapDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/ClientCheatDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/CloseModalDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/HiscoreRequestDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/IfCrmViewClickDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/MoveGameClickDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/MoveMinimapClickDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/OculusLeaveDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/SendSnapshotDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/SetChatFilterSettingsDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/TeleportDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/UpdatePlayerModelDecoderOld.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc1Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc2Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc3Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc4Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc5Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc6Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpcTDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj1Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj2Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj3Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj4Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj5Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj6Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObjTDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer1Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer2Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer3Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer4Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer5Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer6Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer7Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer8Decoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayerTDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePCountDialogDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePNameDialogDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePObjDialogDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePStringDialogDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePauseButtonDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/FriendListAddDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/FriendListDelDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/IgnoreListAddDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/IgnoreListDelDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/DesktopGameMessageDecoderRepository.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/GameClientProt.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/GameClientProtId.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamLookAtEasedCoordEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamLookAtEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamModeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToArc.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToCyclesEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamResetEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamRotateByEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamRotateToEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamShakeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamSmoothResetEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamTargetEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamTargetOldEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/OculusSyncEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanChannelDeltaEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanChannelFullEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanSettingsDeltaEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanSettingsFullEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/MessageClanChannelEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/MessageClanChannelSystemEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanDisableEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanEnableEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/MessageFriendChannelEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelFullV1Encoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelFullV2Encoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelSingleUserEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfClearInvEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfCloseSubEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfMoveSubEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfOpenSubEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfOpenTopEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfResyncEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetAngleEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetAnimEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetColourEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetEventsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetHideEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetModelEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetNpcHeadActiveEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetNpcHeadEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetObjectEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerHeadEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelBaseColourEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelBodyTypeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelObjEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelSelfEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPositionEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetRotateSpeedEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetScrollPosEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetTextEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvFullEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvPartialEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvStopTransmitEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutTransferEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutWithReasonEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildNormalEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildRegionEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildWorldEntityEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/util/RegionEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HideLocOpsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HideNpcOpsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HideObjOpsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HintArrowEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HiscoreReplyEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/MinimapToggleEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ReflectionCheckerEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ResetAnimsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SendPingEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ServerTickEndEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SetHeatmapEnabledEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SiteSettingsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UpdateRebootTimerEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UpdateUid192Encoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UrlOpenEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/ChatFilterSettingsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/ChatFilterSettingsPrivateChatEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/MessageGameEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/RunClientScriptEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/SetMapFlagEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/SetPlayerOpEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/TriggerOnDialogAbortEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateRunEnergyEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateRunWeightEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStatEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStatOldEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStockMarketSlotEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateTradingPostEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/DesktopLowResolutionChangeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/NpcInfoLargeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/NpcInfoSmallEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/SetNpcUpdateOriginEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcBaseAnimationSetEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcBodyCustomisationEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcCombatLevelChangeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcExactMoveEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcFaceCoordEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcFacePathingEntityEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHeadCustomisationEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHeadIconCustomisationEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHitEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcNameChangeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSayEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSequenceEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSpotAnimEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcTintingEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcTransformationEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcVisibleOpsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/writer/NpcAvatarExtendedInfoDesktopWriter.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/PlayerInfoEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerAppearanceEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerChatEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerExactMoveEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerFaceAngleEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerFacePathingEntityEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerHitEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerMoveSpeedEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSayEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSequenceEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSpotAnimEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerTemporaryMoveSpeedEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerTintingEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/writer/PlayerAvatarExtendedInfoDesktopWriter.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/FriendListLoadedEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/MessagePrivateEchoEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/MessagePrivateEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/UpdateFriendListEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/UpdateIgnoreListEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiJingleEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongOldEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongStopEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongWithSecondaryEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSwapEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/SynthSoundEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/LocAnimSpecificEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/MapAnimSpecificEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcAnimSpecificEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcHeadIconSpecificEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcSpotAnimSpecificEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/PlayerAnimSpecificEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/PlayerSpotAnimSpecificEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/ProjAnimSpecificEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpLargeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpResetEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpSmallEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpSyncEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/ClearEntitiesEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/SetActiveWorldEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/WorldEntityInfoEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/DesktopUpdateZonePartialEnclosedEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/UpdateZoneFullFollowsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/UpdateZonePartialFollowsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocAddChangeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocAnimEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocDelEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocMergeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/MapAnimEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/MapProjAnimEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjAddEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjCountEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjDelEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjEnabledOpsEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/SoundAreaEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/DesktopGameMessageEncoderRepository.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/GameServerProt.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/GameServerProtId.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoClient.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoTest.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoClient.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoTest.kt create mode 100644 protocol/osrs-225/osrs-225-desktop/src/test/resources/net/rsprot/protocol/game/outgoing/info/huffman.dat create mode 100644 protocol/osrs-225/osrs-225-internal/build.gradle.kts create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/LogLevel.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/RSProtFlags.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/client/ClientTypeMap.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/codec/zone/payload/OldSchoolZoneProt.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/codec/zone/payload/ZoneProtEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/CachedExtendedInfo.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/CoordGrid.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/ExtendedInfo.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/TransientExtendedInfo.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/ExtendedInfoEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/OnDemandExtendedInfoEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/PrecomputedExtendedInfoEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/NpcAvatarDetails.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/encoder/NpcExtendedInfoEncoders.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/encoder/NpcResolutionChangeEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/BaseAnimationSet.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/BodyCustomisation.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/CombatLevelChange.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/FaceCoord.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/HeadCustomisation.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/HeadIconCustomisation.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/NameChange.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/NpcTinting.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/Transformation.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/TypeCustomisation.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/VisibleOps.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/encoder/PlayerExtendedInfoEncoders.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/Appearance.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/Chat.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/FaceAngle.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/MoveSpeed.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/ObjTypeCustomisation.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/PlayerTintingList.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/TemporaryMoveSpeed.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/ExactMove.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/FacePathingEntity.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Hit.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Say.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Sequence.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/SpotAnimList.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Tinting.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HeadBar.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HeadBarList.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HitMark.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HitMarkList.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/SpotAnim.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/internal/Inventory.kt create mode 100644 protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/internal/InventoryPool.kt create mode 100644 protocol/osrs-225/osrs-225-model/build.gradle.kts create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/GameClientProtCategory.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/If1Button.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/If3Button.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfButtonD.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfButtonT.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfSubOp.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/AffinedClanSettingsAddBannedFromChannel.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/AffinedClanSettingsSetMutedFromChannel.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanChannelFullRequest.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanChannelKickUser.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanSettingsFullRequest.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventAppletFocus.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventCameraPosition.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventKeyboard.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseClick.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseMove.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseScroll.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventNativeMouseClick.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventNativeMouseMove.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/util/MouseMovements.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatJoinLeave.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatKick.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatSetRank.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLoc.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLoc6.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLocT.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/messaging/MessagePrivate.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/messaging/MessagePublic.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/ConnectionTelemetry.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/DetectModifiedClient.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/Idle.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/MapBuildComplete.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/MembershipPromotionEligibility.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/NoTimeout.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/ReflectionCheckReply.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/SendPingReply.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/SoundJingleEnd.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/WindowStatus.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/BugReport.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/ClickWorldMap.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/ClientCheat.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/CloseModal.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/HiscoreRequest.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/IfCrmViewClick.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/MoveGameClick.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/MoveMinimapClick.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/OculusLeave.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/SendSnapshot.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/SetChatFilterSettings.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/Teleport.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/UpdatePlayerModelOld.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/internal/MovementRequest.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpc.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpc6.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpcT.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObj.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObj6.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObjT.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/players/OpPlayer.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/players/OpPlayerT.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePCountDialog.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePNameDialog.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePObjDialog.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePStringDialog.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePauseButton.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/FriendListAdd.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/FriendListDel.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/IgnoreListAdd.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/IgnoreListDel.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/GameServerProtCategory.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamLookAt.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamLookAtEasedCoord.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMode.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveTo.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveToArc.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveToCycles.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamReset.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamRotateBy.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamRotateTo.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamShake.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamSmoothReset.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamTarget.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamTargetOld.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/OculusSync.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/util/CameraEaseFunction.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanChannelDelta.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanChannelFull.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanSettingsDelta.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanSettingsFull.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/MessageClanChannel.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/MessageClanChannelSystem.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClan.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClanDisable.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClanEnable.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/MessageFriendChannel.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFull.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFullV1.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFullV2.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelSingleUser.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/AvatarExtendedInfoWriter.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/InfoRepository.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/ObserverExtendedInfoFlags.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/exceptions/InfoProcessException.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/filter/DefaultExtendedInfoFilter.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/filter/ExtendedInfoFilter.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatar.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExceptionHandler.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExtendedInfo.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExtendedInfoBlocks.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarFactory.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarRepository.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcIndexSupplier.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfo.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoLarge.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoProtocol.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoRepository.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoSmall.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoWorldDetails.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoWorldDetailsStorage.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/SetNpcUpdateOrigin.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/util/NpcCellOpcodes.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/GlobalLowResolutionPositionRepository.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatar.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarExtendedInfo.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarExtendedInfoBlocks.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarFactory.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfo.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoPacket.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoProtocol.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoRepository.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoWorldDetails.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/util/CellOpcodes.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/util/LowResolutionPosition.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/Avatar.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/BuildArea.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/ReferencePooledObject.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/DefaultProtocolWorker.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/ForkJoinMultiThreadProtocolWorker.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/ProtocolWorker.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/SingleThreadProtocolWorker.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatar.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarExceptionHandler.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarFactory.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarRepository.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityIndexSupplier.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfo.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoExtensions.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoPacket.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoRepository.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityProtocol.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfClearInv.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfCloseSub.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfMoveSub.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfOpenSub.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfOpenTop.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfResync.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetAngle.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetAnim.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetColour.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetEvents.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetHide.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetModel.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetNpcHead.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetNpcHeadActive.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetObject.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerHead.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelBaseColour.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelBodyType.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelObj.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelSelf.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPosition.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetRotateSpeed.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetScrollPos.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetText.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvFull.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvPartial.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvStopTransmit.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/Logout.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/LogoutTransfer.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/LogoutWithReason.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildLogin.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildNormal.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildRegion.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildWorldEntity.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/StaticRebuildMessage.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/RebuildRegionZone.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/XteaHelper.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/XteaProvider.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideLocOps.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideNpcOps.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideObjOps.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HintArrow.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HiscoreReply.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/MinimapToggle.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ReflectionChecker.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ResetAnims.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SendPing.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ServerTickEnd.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SetHeatmapEnabled.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SiteSettings.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UpdateRebootTimer.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UpdateUid192.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UrlOpen.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/ChatFilterSettings.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/ChatFilterSettingsPrivateChat.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/MessageGame.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/RunClientScript.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/SetMapFlag.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/SetPlayerOp.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/TriggerOnDialogAbort.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateRunEnergy.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateRunWeight.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStat.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStatOld.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStockMarketSlot.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateTradingPost.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/FriendListLoaded.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/MessagePrivate.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/MessagePrivateEcho.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/UpdateFriendList.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/UpdateIgnoreList.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiJingle.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSong.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongOld.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongStop.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongWithSecondary.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSwap.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/SynthSound.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/LocAnimSpecific.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/MapAnimSpecific.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcAnimSpecific.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcHeadIconSpecific.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcSpotAnimSpecific.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/PlayerAnimSpecific.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/PlayerSpotAnimSpecific.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/ProjAnimSpecific.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/util/OpFlags.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpLarge.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpReset.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpSmall.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpSync.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/worldentity/ClearEntities.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/worldentity/SetActiveWorld.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZoneFullFollows.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZonePartialEnclosed.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZonePartialFollows.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocAddChange.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocAnim.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocDel.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocMerge.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/MapAnim.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/MapProjAnim.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjAdd.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjCount.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjDel.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjEnabledOps.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/SoundArea.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/CoordInBuildArea.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/CoordInZone.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/LocProperties.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/Js5GroupRequest.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PrefetchRequest.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PriorityChangeHigh.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PriorityChangeLow.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/UrgentRequest.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/XorChange.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/outgoing/Js5GroupResponse.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/GameLogin.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/GameReconnect.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/InitGameConnection.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/InitJs5RemoteConnection.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/ProofOfWorkReply.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/RemainingBetaArchives.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/NopProofOfWorkProvider.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWork.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWorkProvider.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/SingleTypeProofOfWorkProvider.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeGenerator.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaData.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaDataProvider.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeType.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeVerifier.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeWorker.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/DefaultChallengeWorker.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ChallengeGenerator.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256MetaDataProvider.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ProofOfWorkProvider.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256Challenge.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256ChallengeVerifier.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256MetaData.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/DefaultSha256MessageDigestHashFunction.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/Sha256HashFunction.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/ThreadLocalSha256MessageDigestHashFunction.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/AuthenticationType.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/CyclicRedundancyCheckBlock.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/HostPlatformStats.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginBlock.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginBlockDecodingFunction.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginClientType.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginPlatformType.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/OtpAuthenticationType.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/Password.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/Token.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt create mode 100644 protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/util/AuthenticatorResponse.kt create mode 100644 protocol/osrs-225/osrs-225-shared/build.gradle.kts create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PrefetchRequestDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PriorityChangeHighDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PriorityChangeLowDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/UrgentRequestDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/XorChangeDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5ClientProt.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5ClientProtId.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5MessageDecoderRepository.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/outgoing/codec/Js5GroupResponseEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/outgoing/prot/Js5MessageEncoderRepository.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/outgoing/prot/Js5ServerProt.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/GameLoginDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/GameReconnectDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/InitGameConnectionDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/InitJs5RemoteConnectionDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/ProofOfWorkReplyDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/RemainingBetaArchivesDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/shared/LoginBlockDecoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/prot/LoginClientProt.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/prot/LoginClientProtId.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/prot/LoginMessageDecoderRepository.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/DisallowedByScriptLoginResponseEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/EmptyLoginResponseEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/OkLoginResponseEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/ProofOfWorkResponseEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/ReconnectOkResponseEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/SuccessfulLoginResponseEncoder.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginMessageEncoderRepository.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginServerProt.kt create mode 100644 protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginServerProtId.kt diff --git a/protocol/osrs-225/build.gradle.kts b/protocol/osrs-225/build.gradle.kts new file mode 100644 index 000000000..da2c48d10 --- /dev/null +++ b/protocol/osrs-225/build.gradle.kts @@ -0,0 +1 @@ +// No-op diff --git a/protocol/osrs-225/osrs-225-api/build.gradle.kts b/protocol/osrs-225/osrs-225-api/build.gradle.kts new file mode 100644 index 000000000..30179b461 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/build.gradle.kts @@ -0,0 +1,27 @@ +dependencies { + implementation(platform(rootProject.libs.netty.bom)) + api(rootProject.libs.netty.buffer) + implementation(rootProject.libs.netty.transport) + implementation(rootProject.libs.netty.handler) + implementation(rootProject.libs.netty.native.epoll) + implementation(rootProject.libs.netty.native.kqueue) + implementation(rootProject.libs.netty.incubator.iouring) + implementation(rootProject.libs.inline.logger) + api(projects.protocol) + api(projects.compression) + api(projects.crypto) + api(projects.protocol.osrs225.osrs225Common) + api(projects.protocol.osrs225.osrs225Model) + implementation(projects.protocol.osrs225.osrs225Internal) + implementation(projects.protocol.osrs225.osrs225Desktop) + implementation(projects.protocol.osrs225.osrs225Shared) + implementation(projects.buffer) +} + +mavenPublishing { + pom { + name = "RsProt OSRS 225 API" + description = "The API module for revision 225 OldSchool RuneScape networking, " + + "offering an all-in-one implementation." + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/AbstractNetworkServiceFactory.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/AbstractNetworkServiceFactory.kt new file mode 100644 index 000000000..320411b36 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/AbstractNetworkServiceFactory.kt @@ -0,0 +1,291 @@ +package net.rsprot.protocol.api + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBufAllocator +import io.netty.buffer.PooledByteBufAllocator +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.crypto.rsa.RsaKeyPair +import net.rsprot.protocol.api.bootstrap.BootstrapFactory +import net.rsprot.protocol.api.handlers.ExceptionHandlers +import net.rsprot.protocol.api.handlers.GameMessageHandlers +import net.rsprot.protocol.api.handlers.INetAddressHandlers +import net.rsprot.protocol.api.handlers.LoginHandlers +import net.rsprot.protocol.api.js5.Js5Configuration +import net.rsprot.protocol.api.js5.Js5GroupProvider +import net.rsprot.protocol.api.suppliers.NpcInfoSupplier +import net.rsprot.protocol.api.suppliers.PlayerInfoSupplier +import net.rsprot.protocol.api.suppliers.WorldEntityInfoSupplier +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.message.codec.incoming.provider.GameMessageConsumerRepositoryProvider + +/** + * The abstract network service factory is used to build the network service that is used + * as the entry point to this library, allowing one to bind the network and supply everything + * necessary network-wise from one spot. + * @param R the receiver type that will be consuming game messages, typically a Player + */ +@Suppress("MemberVisibilityCanBePrivate") +@ExperimentalUnsignedTypes +public abstract class AbstractNetworkServiceFactory { + /** + * The allocator that will be used for everything in this networking library. + * This will primarily be passed onto NPC and Player info objects, which will utilize + * this to precompute extended info blocks and the overall main buffer. + * It is HIGHLY recommended to use a pooled direct byte buffer if possible. + * Pooling in particular allows us to avoid allocating new expensive 40kb buffers + * per each player, for both player and npc infos, and direct buffers allow + * the Netty layer to skip one copy operation to move the data off of the heap. + */ + public open val allocator: ByteBufAllocator + get() = PooledByteBufAllocator.DEFAULT + + /** + * The list of ports to listen to. Typically, the server should listen to ports + * 43594 and 443 in this section, with the 43594 port being the primary one, + * and 443 being the fallback. 443 is additionally used for HTTP requests, should + * those be supported. + */ + public abstract val ports: List + + /** + * The list of client types to register within the network. + * If there are multiple client types implemented, one can supply multiple + * client types in this section. It is highly likely that only one will be + * offered though, as C++ clients are much harder to figure out. + * Furthermore, if multiple client types are offered, it is highly recommended + * to only register the ones you intend on supporting and using. This is because + * each client type that is registered here additionally means we have to precompute + * Player and NPC info extended info blocks for each of those client types, + * meaning all that work would go to a waste if no one is there to use the types. + */ + public abstract val supportedClientTypes: List + + /** + * Whether the client is connecting to this world under the beta world flag of + * 65536, or 0x10000. If this is the case, the login block that the client transmits + * differs from the usual one, as it ends up splitting CRCs up into two incomplete + * sections. This was intended to prevent people from downloading most of the + * beta cache without the access to the beta server, however the implementation + * is done incorrectly and all it ends up preventing is people who use the client + * to download the cache. The JS5 server is not notified of these constraints and + * will server every cache group as requested regardless of the status. + * Because of this, the implementation for the beta worlds is simplified to just + * support logging in via a beta world, which is accomplished by immediately + * requesting the remaining beta CRCs before passing the login request on to + * the server. By the time the server receives information about the request, + * all the CRCs have been obtained. + * It is worth noting that if the client flag status differs from the server, + * one of two possible scenarios will occur - either you will get an exception + * as the server tries to read more bytes than what the client wrote via the CRC + * block, or the server never receives enough bytes to consider the login block + * complete, which means the login request will hang and eventually time out. + */ + public open val betaWorld: Boolean + get() = false + + /** + * Gets the bootstrap factory used to register the network service. + * The bootstrap factory offers the initial socket and Netty configurations + * to be used within this library. These configurations are by default + * made to mirror the client as much as possible. + */ + public abstract fun getBootstrapFactory(): BootstrapFactory + + /** + * Gets the RSA key pair that will be used to decipher the login blocks + * sent by the client. If the keys aren't correct, the login block + * will fail to decode, and exceptions will be thrown. + */ + public abstract fun getRsaKeyPair(): RsaKeyPair + + /** + * Gets the Huffman codec provider. + * This is implemented in a provider format to allow the server to use + * blocking implementations, which allow lazy-loading Huffman. + * This is useful to allow binding the network early on in the boot + * cycle without having to wait behind Huffman. + * If a blocking implementation is used, and Huffman isn't ready when + * it is needed, the underlying Netty thread will be blocked until it + * is supplied. Because of this, it is recommended to use the + * non-blocking variant in production. + */ + public abstract fun getHuffmanCodecProvider(): HuffmanCodecProvider + + /** + * Gets the JS5 configuration settings to be used within the JS5 service. + * These settings allow a server to modify the frequency at which + * groups are served to the client, as well as the ratio between + * high priority logged in players and the low priority logged out ones, + * allowing the JS5 protocol to send more data to those logged in + * Furthermore, this allows defining the block size that is written + * per client per iteration. + * The default configuration is set to be fast enough to not show any + * client speed reduction via localhost. + */ + public open fun getJs5Configuration(): Js5Configuration = Js5Configuration() + + /** + * Gets the JS5 group provider, used to return the respective byte buffers + * or file regions from the server based on the incoming request. + * It is fine to use lazy-loading for development, but it is highly + * recommended to pre-compute the JS5 groups in the final form when + * used in development, to avoid instant no-delay responses. + */ + public abstract fun getJs5GroupProvider(): Js5GroupProvider + + /** + * Gets the consumer repository for incoming client game prots. + * This repository will be used to determine whether an incoming game packet + * needs decoding in the first place - if there is no consumer for the packet + * registered, we can simply skip the number of bytes that came in with that + * packet, avoiding any generation of garbage. Furthermore, the consumers + * will be automatically triggered for each incoming message when the server + * requests it via the [Session] object that is provided during login. + */ + public abstract fun getGameMessageConsumerRepositoryProvider(): GameMessageConsumerRepositoryProvider + + /** + * Gets the game connection handlers that are invoked whenever the client + * logs in or reconnects. These functions are only triggered after + * features such as Proof of Work and remaining beta archive CRCs + * have been obtained, furthermore additional library-sided checks, such as + * not too many connections from the same INetAddress must be met. + * Session id is also verified by the library before passing the request + * on to the implementing server. + * The server is responsible for doing all the login validation at this point, + * as well as CRC validations. + * It is worth noting that the connection handler will be invoked from whichever + * thread was used to decode the login block itself. + * By default, this will be one of the threads in the ForkJoinPool. + */ + public abstract fun getGameConnectionHandler(): GameConnectionHandler + + /** + * Gets the supplier for all the context that the NPC info protocol requires + * to function. The server must use the avatar factory to allocate + * avatars for each NPC that it spawned into the game. Furthermore, + * when the respective NPC is permanently remove from the game, + * the avatar MUST be de-allocated, otherwise that avatar will never be usable. + * For NPCs which are simply set to respawning mode, the avatar should be kept. + * For each player that logs into the world, one NPC info object should be + * allocated. Similarly to the avatar, this object MUST be deallocated + * when the player is removed from the game, be that normally or abnormally. + */ + public open fun getNpcInfoSupplier(): NpcInfoSupplier = NpcInfoSupplier() + + /** + * Gets the supplier for all the context that the Player info protocol requires + * to function. The server must allocate one Player info object per each + * player that logs in. Just like with NPC info, the object must be deallocated + * when the player is removed. + */ + public open fun getPlayerInfoSupplier(): PlayerInfoSupplier = PlayerInfoSupplier() + + public open fun getWorldEntityInfoSupplier(): WorldEntityInfoSupplier = WorldEntityInfoSupplier() + + /** + * Gets the exception handlers for channel exceptions as well as any incoming + * game message consumers that get processed in the library. + * Further implementations may be introduced in the future if the need arises. + */ + public open fun getExceptionHandlers(): ExceptionHandlers = + ExceptionHandlers( + { ctx, cause -> + if (ctx.channel().isActive) { + ctx.close() + } + logger.error(cause) { + "Exception in channel ${ctx.channel()}" + } + }, + ) + + /** + * Gets the handlers for anything related to INetAddresses. + * The default implementation will keep track of the number of concurrent + * game connections and JS5 connections separately. + * There is furthermore a validation implementation that will by default + * reject any connection to either service if there are 10 concurrent connections + * to that service from the same address already. + * The initial check is performed after the login connection, when either + * the game login or the JS5 connection is established. For game logins + * a secondary validation is performed right before the login block + * is passed onto the server to handle. + * It should be noted that the tracking mechanism is fairly straightforward + * and doesn't cost much in performance. + */ + public open fun getINetAddressHandlers(): INetAddressHandlers = INetAddressHandlers() + + /** + * Gets the handlers for game messages, which includes providing + * queue implementations for the incoming and outgoing messages, + * as well as a counter for incoming game messages, to avoid processing + * too many packets per cycle. + * The default implementation offers a ConcurrentLinkedQueue for both of + * the queues, and has a limit of 10 user packets, and 50 client packets. + * Only packets for which the server has registered a listener will be counted + * towards these limitations. Most packets will count towards the 10 user packets, + * as the client limitation is strictly for packets which cannot directly + * be influenced by the user. + * If either of the limitations is hit during message decoding, the decoding + * is halted immediately until after the game has polled the incoming messages. + * This means the TCP protocol is responsible for ensuring too much data cannot + * be passed onto us. + */ + public open fun getGameMessageHandlers(): GameMessageHandlers = GameMessageHandlers() + + /** + * Gets the handlers for the login related things. + * This includes an implementation for generating the initial session ids, + * which by default uses a secure random implementation. + * Additional configurations support modifying the stream cipher, proof of work + * logic, and a service for decoding logins, which is invoked using ForkJoinPool + * by default. + * The Proof of Work implementation uses the same inputs as used in the OldSchool + * as of writing this. It is possible to disable Proof of Work entirely, which + * can be useful for development environments. + */ + public open fun getLoginHandlers(): LoginHandlers = LoginHandlers() + + /** + * Builds a network service through this factoring, using all + * the information provided in here. + */ + public fun build(): NetworkService { + val allocator = this.allocator + val ports = this.ports + val supportedClientTypes = this.supportedClientTypes + val huffman = getHuffmanCodecProvider() + val entityInfoProtocols = + EntityInfoProtocols.initialize( + allocator, + supportedClientTypes, + huffman, + getPlayerInfoSupplier(), + getNpcInfoSupplier(), + getWorldEntityInfoSupplier(), + ) + return NetworkService( + allocator, + ports, + betaWorld, + getBootstrapFactory(), + entityInfoProtocols, + supportedClientTypes, + getGameConnectionHandler(), + getExceptionHandlers(), + getINetAddressHandlers(), + getGameMessageHandlers(), + getLoginHandlers(), + huffman, + getGameMessageConsumerRepositoryProvider(), + getRsaKeyPair(), + getJs5Configuration(), + getJs5GroupProvider(), + ) + } + + private companion object { + private val logger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/ChannelExceptionHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/ChannelExceptionHandler.kt new file mode 100644 index 000000000..93dac5dd6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/ChannelExceptionHandler.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.api + +import io.netty.channel.ChannelHandlerContext + +/** + * The channel exception handler is an interface that is invoked whenever Netty catches + * an exception in the channels. The server is expected to close the connection in any such case, + * if it is still open, and log the exception behind it. + */ +public fun interface ChannelExceptionHandler { + /** + * Invoked whenever a Netty handler catches an exception. + * @param ctx the channel handler context behind this connection + * @param cause the causation behind the exception + */ + public fun exceptionCaught( + ctx: ChannelHandlerContext, + cause: Throwable, + ) +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/EntityInfoProtocols.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/EntityInfoProtocols.kt new file mode 100644 index 000000000..463d8c644 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/EntityInfoProtocols.kt @@ -0,0 +1,197 @@ +package net.rsprot.protocol.api + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.api.suppliers.NpcInfoSupplier +import net.rsprot.protocol.api.suppliers.PlayerInfoSupplier +import net.rsprot.protocol.api.suppliers.WorldEntityInfoSupplier +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.encoder.NpcResolutionChangeEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.DesktopLowResolutionChangeEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.writer.NpcAvatarExtendedInfoDesktopWriter +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.writer.PlayerAvatarExtendedInfoDesktopWriter +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarExtendedInfoWriter +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarFactory +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcInfoProtocol +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerAvatarExtendedInfoWriter +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerAvatarFactory +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfoProtocol +import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityAvatarFactory +import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityProtocol + +/** + * The entity info protocols class brings together the relatively complex player and NPC info + * protocols. This is responsible for registering all the client types that are used by the user. + * @property playerAvatarFactory the avatar factory for players. Since players have a 1:1 player info + * to avatar ratio, the avatar is automatically included in the player info object that is requested. + * This is additionally strategically placed to improve cache locality and improve the performance + * of the player info protocol. + * @property playerInfoProtocol the main player info protocol responsible for computing the player + * info packet for all the players in the game. + * @property npcAvatarFactory the avatar factory for NPCs. Each NPC must allocate one avatar + * as they spawn, and that avatar must be deallocated when the NPC is fully removed from the game. + * @property npcInfoProtocol the main NPC info protocol responsible for computing the npc info packet + * for all the players in the game. + */ +public class EntityInfoProtocols + private constructor( + public val playerAvatarFactory: PlayerAvatarFactory, + public val playerInfoProtocol: PlayerInfoProtocol, + public val npcAvatarFactory: NpcAvatarFactory, + public val npcInfoProtocol: NpcInfoProtocol, + public val worldEntityAvatarFactory: WorldEntityAvatarFactory, + public val worldEntityInfoProtocol: WorldEntityProtocol, + ) { + internal companion object { + /** + * Initializes the player and NPC info avatar factories and protocols. + * @param allocator the byte buffer allocator used for player and NPC info main buffers, + * as well as any pre-computed extended info blocks. + * @param clientTypes the list of client types to register + * @param huffmanCodecProvider the Huffman codec provider that will be used to compute + * the chat extended info block, any others in the future that may require it. + * @param playerInfoSupplier the class wrapping the worker used to perform computations, + * as well as the filter for extended info blocks that ensures that the packet does not + * under any circumstances exceed the maximum packet limitations. + * @param npcInfoSupplier the class wrapping the worker used to perform computations, + * as well as the filter for extended info blocks that ensures that the packet does not + * under any circumstances exceed the maximum packet limitations. Furthermore, unlike + * player info, this will also provide an implementation for exceptions caught during + * pre-computations of NPC avatars. It is up to the server to decide how to handle + * any exceptions which are caught when computing information for avatars. The least + * destructive way is to remove the underlying NPC from the world when that happens, + * and log the exception in the process. This will still cause any observers to disconnect, + * however, but it ensures that anyone else that comes around the same area will not + * experience the same fate. There is also an implementation that is used to supply + * indices of nearby NPCs for the NPC info packet from the server's perspective, + * given a number of arguments necessary to determine it. The server is expected + * to return an iterator of all the indices of the NPCs that match the predicate, + * even if a NPC is already tracked by a given player. The protocol is responsible + * for ensuring no duplications will occur. + * @return a class wrapping all the protocols into one object. + */ + fun initialize( + allocator: ByteBufAllocator, + clientTypes: List, + huffmanCodecProvider: HuffmanCodecProvider, + playerInfoSupplier: PlayerInfoSupplier, + npcInfoSupplier: NpcInfoSupplier, + worldEntityInfoSupplier: WorldEntityInfoSupplier, + ): EntityInfoProtocols { + val playerWriters = mutableListOf() + val npcWriters = mutableListOf() + val npcResolutionChangeEncoders = mutableListOf() + if (OldSchoolClientType.DESKTOP in clientTypes) { + playerWriters += PlayerAvatarExtendedInfoDesktopWriter() + npcWriters += NpcAvatarExtendedInfoDesktopWriter() + npcResolutionChangeEncoders += DesktopLowResolutionChangeEncoder() + } + val worldEntityAvatarFactory = buildWorldEntityAvatarFactory(allocator) + val worldEntityProtocol = + buildWorldEntityInfoProtocol( + allocator, + worldEntityInfoSupplier, + worldEntityAvatarFactory, + ) + val playerAvatarFactory = + buildPlayerAvatarFactory(allocator, playerInfoSupplier, playerWriters, huffmanCodecProvider) + val playerInfoProtocol = + buildPlayerInfoProtocol( + allocator, + playerInfoSupplier, + playerAvatarFactory, + ) + val npcAvatarFactory = + buildNpcAvatarFactory(allocator, npcInfoSupplier, npcWriters, huffmanCodecProvider) + val npcInfoProtocol = + buildNpcInfoProtocol( + allocator, + npcInfoSupplier, + npcResolutionChangeEncoders, + npcAvatarFactory, + ) + + return EntityInfoProtocols( + playerAvatarFactory, + playerInfoProtocol, + npcAvatarFactory, + npcInfoProtocol, + worldEntityAvatarFactory, + worldEntityProtocol, + ) + } + + private fun buildNpcInfoProtocol( + allocator: ByteBufAllocator, + npcInfoSupplier: NpcInfoSupplier, + npcResolutionChangeEncoders: MutableList, + npcAvatarFactory: NpcAvatarFactory, + ) = NpcInfoProtocol( + allocator, + npcInfoSupplier.npcIndexSupplier, + ClientTypeMap.of( + npcResolutionChangeEncoders, + OldSchoolClientType.COUNT, + ) { + it.clientType + }, + npcAvatarFactory, + npcInfoSupplier.npcAvatarExceptionHandler, + npcInfoSupplier.npcInfoProtocolWorker, + ) + + private fun buildNpcAvatarFactory( + allocator: ByteBufAllocator, + npcInfoSupplier: NpcInfoSupplier, + npcWriters: MutableList, + huffmanCodecProvider: HuffmanCodecProvider, + ): NpcAvatarFactory = + NpcAvatarFactory( + allocator, + npcInfoSupplier.npcExtendedInfoFilter, + npcWriters, + huffmanCodecProvider, + ) + + private fun buildWorldEntityAvatarFactory(allocator: ByteBufAllocator): WorldEntityAvatarFactory = + WorldEntityAvatarFactory( + allocator, + ) + + private fun buildWorldEntityInfoProtocol( + allocator: ByteBufAllocator, + worldEntityInfoSupplier: WorldEntityInfoSupplier, + worldEntityAvatarFactory: WorldEntityAvatarFactory, + ) = WorldEntityProtocol( + allocator, + worldEntityInfoSupplier.worldEntityIndexSupplier, + worldEntityInfoSupplier.worldEntityAvatarExceptionHandler, + worldEntityAvatarFactory, + ) + + private fun buildPlayerInfoProtocol( + allocator: ByteBufAllocator, + playerInfoSupplier: PlayerInfoSupplier, + playerAvatarFactory: PlayerAvatarFactory, + ): PlayerInfoProtocol = + PlayerInfoProtocol( + allocator, + playerInfoSupplier.playerInfoProtocolWorker, + playerAvatarFactory, + ) + + private fun buildPlayerAvatarFactory( + allocator: ByteBufAllocator, + playerInfoSupplier: PlayerInfoSupplier, + playerWriters: MutableList, + huffmanCodecProvider: HuffmanCodecProvider, + ): PlayerAvatarFactory = + PlayerAvatarFactory( + allocator, + playerInfoSupplier.playerExtendedInfoFilter, + playerWriters, + huffmanCodecProvider, + ) + } + } diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameConnectionHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameConnectionHandler.kt new file mode 100644 index 000000000..c7aa026ff --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameConnectionHandler.kt @@ -0,0 +1,47 @@ +package net.rsprot.protocol.api + +import net.rsprot.crypto.xtea.XteaKey +import net.rsprot.protocol.api.login.GameLoginResponseHandler +import net.rsprot.protocol.loginprot.incoming.util.AuthenticationType +import net.rsprot.protocol.loginprot.incoming.util.LoginBlock + +/** + * A handler interface for any game logins and reconnections. + * @param R the receiver of the incoming game packets, typically a Player class. + */ +public interface GameConnectionHandler { + /** + * The onLogin function is triggered whenever a login request is received by the library, + * and it passes all the initial validation necessary. The server is responsible + * for doing most of the validation here, but preliminary things like max number of connections + * and session ids will have been pre-checked by us. + * @param responseHandler the handler used to write a successful or failed login response, + * depending on the decisions made by the server. + * @param block the login block sent by the client, containing all the information the server + * will need. + */ + public fun onLogin( + responseHandler: GameLoginResponseHandler, + block: LoginBlock>, + ) + + /** + * The onReconnect function is triggered whenever a reconnect request is received + * by the library. It is worth noting that Proof of Work will not be involved + * if this is the case, assuming it is enabled in the first place. + * Instead of transmitting the password, the client will transmit the seed used + * by the previous login connection. If the seed does not match with what the + * server knows, the request should be rejected. If the reconnect is successful, + * the server should replace the Session object in that player with the one + * provided by the response handler. The old session will close or time out shortly + * afterwards, if it already hasn't. + * @param responseHandler the handler used to write a successful or failed reconnect response, + * depending on the decisions made by the server. + * @param block the login block sent by the client, containing all the information the server + * will need. + */ + public fun onReconnect( + responseHandler: GameLoginResponseHandler, + block: LoginBlock, + ) +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameMessageCounter.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameMessageCounter.kt new file mode 100644 index 000000000..9c5b6d487 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameMessageCounter.kt @@ -0,0 +1,32 @@ +package net.rsprot.protocol.api + +import net.rsprot.protocol.ClientProtCategory + +/** + * An interface for tracking incoming game messages, in order to avoid + * decoding and consuming too many messages if the client is flooding us + * with them. + * This implementation must be thread safe in the sense that the + * increment and reset functions could be called concurrently from different + * threads. The default implementation uses an array for tracking the counts + * and thus does not need such thread safety here. + */ +public interface GameMessageCounter { + /** + * Increments the message counter for the provided client prot + * category. + * @param clientProtCategory the category of the incoming packet. + */ + public fun increment(clientProtCategory: ClientProtCategory) + + /** + * Whether any of the message categories have reached their limit + * for maximum number of decoded messages. + */ + public fun isFull(): Boolean + + /** + * Resets the tracked counts for the messages. + */ + public fun reset() +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameMessageCounterProvider.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameMessageCounterProvider.kt new file mode 100644 index 000000000..966e5b2ad --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/GameMessageCounterProvider.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.api + +/** + * Gets the message counter provider for incoming game messages. + * This is in a provider implementation as one instance is allocated + * for each session object. + */ +public fun interface GameMessageCounterProvider { + /** + * Provides a game message counter implementation. + * A new instance must be allocated with each request, + * as this is per session basis. + */ + public fun provide(): GameMessageCounter +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/IncomingGameMessageConsumerExceptionHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/IncomingGameMessageConsumerExceptionHandler.kt new file mode 100644 index 000000000..d37fcf405 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/IncomingGameMessageConsumerExceptionHandler.kt @@ -0,0 +1,28 @@ +package net.rsprot.protocol.api + +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * An exception handler for exceptions caught during the invocation of game message + * consumers for a given session. As the server only calls one function + * to process all the incoming messages, any one of them could throw an exception + * half-way through, thus we need a handler to safely deal with exceptions if that + * were to happen. + * @param R the receiver of the session object, typically a player. + */ +public fun interface IncomingGameMessageConsumerExceptionHandler { + /** + * Triggered whenever an throwable is caught when invoking the incoming + * game message consumers for all the packets that came in. + * @param session the session which triggered the exception + * @param packet the incoming game message that failed to be processed + * @param cause the throwable being caught. Note that because this catches + * throwables, it will also catch errors, which likely should be propagated + * further. + */ + public fun exceptionCaught( + session: Session, + packet: IncomingGameMessage, + cause: Throwable, + ) +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/InetAddressTracker.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/InetAddressTracker.kt new file mode 100644 index 000000000..8c7cc610b --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/InetAddressTracker.kt @@ -0,0 +1,28 @@ +package net.rsprot.protocol.api + +import java.net.InetAddress + +/** + * The tracker implementation for INetAddresses. + * This implementation must be thread safe, as it is triggered by all kinds + * of Netty threads! + */ +public interface InetAddressTracker { + /** + * The register function is invoked whenever a channel goes active + * @param address the address that connected + */ + public fun register(address: InetAddress) + + /** + * The deregister function is invoked whenever a channel goes inactive + * @param address the address that disconnected + */ + public fun deregister(address: InetAddress) + + /** + * Gets the number of active connections for a given address + * @param address the address to check + */ + public fun getCount(address: InetAddress): Int +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/InetAddressValidator.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/InetAddressValidator.kt new file mode 100644 index 000000000..26a4dc75d --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/InetAddressValidator.kt @@ -0,0 +1,40 @@ +package net.rsprot.protocol.api + +import java.net.InetAddress + +/** + * The validation service for [InetAddress]. + * This service is responsible for accepting of rejecting connections based + * on the number of active connections from said service. + * It is worth noting that game and JS5 are tracked separately, as each + * client opened will initiate a request to both. + * Any connections opened at the very start before either JS5 or + * game has been decided will not be validated, as it is unclear to which + * end point they wish to connect. Those sessions will time out after 30 seconds + * if no decision has been made. + */ +public interface InetAddressValidator { + /** + * Whether to accept a game connection from the provided [address] + * based on the current number of active game connections + * @param address the address attempting to establish a game connection + * @param activeGameConnections the number of currently active game connections from that address + */ + public fun acceptGameConnection( + address: InetAddress, + activeGameConnections: Int, + ): Boolean + + /** + * Whether to accept a JS5 connection from the provided [address] + * based on the current number of active Js5 connections + * @param address the address attempting to establish a JS5 connection + * @param activeJs5Connections the number of currently active JS5 connections from that address + * @param seed the seed used for reconnections and xtea block decryption. + */ + public fun acceptJs5Connection( + address: InetAddress, + activeJs5Connections: Int, + seed: IntArray, + ): Boolean +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/LoginChannelInitializer.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/LoginChannelInitializer.kt new file mode 100644 index 000000000..124907138 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/LoginChannelInitializer.kt @@ -0,0 +1,54 @@ +package net.rsprot.protocol.api + +import com.github.michaelbull.logging.InlineLogger +import io.netty.channel.Channel +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelInitializer +import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.protocol.api.logging.networkLog +import net.rsprot.protocol.api.login.LoginChannelHandler +import net.rsprot.protocol.api.login.LoginMessageDecoder +import net.rsprot.protocol.api.login.LoginMessageEncoder +import java.util.concurrent.TimeUnit + +/** + * The channel initializer for login blocks. + * This initializer will add the login channel handler as well as an + * idle state handler to ensure the connections are cut short if they go idle. + */ +public class LoginChannelInitializer( + private val networkService: NetworkService, +) : ChannelInitializer() { + override fun initChannel(ch: Channel) { + networkLog(logger) { + "Channel initialized: $ch" + } + ch.pipeline().addLast( + IdleStateHandler( + true, + NetworkService.LOGIN_TIMEOUT_SECONDS, + NetworkService.LOGIN_TIMEOUT_SECONDS, + NetworkService.LOGIN_TIMEOUT_SECONDS, + TimeUnit.SECONDS, + ), + LoginMessageDecoder(networkService), + LoginMessageEncoder(networkService), + LoginChannelHandler(networkService), + ) + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun exceptionCaught( + ctx: ChannelHandlerContext, + cause: Throwable, + ) { + networkService + .exceptionHandlers + .channelExceptionHandler + .exceptionCaught(ctx, cause) + } + + private companion object { + private val logger: InlineLogger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/LoginDecoderService.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/LoginDecoderService.kt new file mode 100644 index 000000000..d044b6f5a --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/LoginDecoderService.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.api + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.loginprot.incoming.util.LoginBlock +import net.rsprot.protocol.loginprot.incoming.util.LoginBlockDecodingFunction +import java.util.concurrent.CompletableFuture + +/** + * The service behind decoding login blocks. + * This is needed as the login blocks take a noticeable amount of time to decode, + * and it may not be ideal to block the Netty threads from doing any work during + * that time. Most of the time is taken up by the RSA deciphering, + * which can take around a millisecond for a secure key. + */ +public interface LoginDecoderService { + /** + * Decodes a login block buffer using the [decoder] implementation. + * The default implementation for login decoder utilizes a ForkJoinPool. + * @param buffer the buffer to decode + * @param betaWorld whether the login connection came from a beta world, + * which means the decoding is only partial + * @param decoder the decoder function used to turn the buffer into a login block + * @return a completable future instance that may be completed on a different thread, + * to avoid blocking Netty threads. + */ + public fun decode( + buffer: JagByteBuf, + betaWorld: Boolean, + decoder: LoginBlockDecodingFunction, + ): CompletableFuture> +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/MessageQueueProvider.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/MessageQueueProvider.kt new file mode 100644 index 000000000..22fc8b80c --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/MessageQueueProvider.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.api + +import net.rsprot.protocol.message.Message +import java.util.Queue + +/** + * The queue provider for any type of messages. + * The queue must be thread-safe! + * The default implementation is a ConcurrentLinkedQueue. + * @param T the type of the message that the queue handles, either incoming or outgoing. + */ +public fun interface MessageQueueProvider { + /** + * Provides a new instance of the message queue. This should always + * return a new instance of the queue implementation. + */ + public fun provide(): Queue +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/NetworkService.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/NetworkService.kt new file mode 100644 index 000000000..2c75e6aec --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/NetworkService.kt @@ -0,0 +1,184 @@ +package net.rsprot.protocol.api + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBufAllocator +import io.netty.channel.ChannelFuture +import io.netty.channel.EventLoopGroup +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.crypto.rsa.RsaKeyPair +import net.rsprot.protocol.api.bootstrap.BootstrapFactory +import net.rsprot.protocol.api.handlers.ExceptionHandlers +import net.rsprot.protocol.api.handlers.GameMessageHandlers +import net.rsprot.protocol.api.handlers.INetAddressHandlers +import net.rsprot.protocol.api.handlers.LoginHandlers +import net.rsprot.protocol.api.js5.Js5Configuration +import net.rsprot.protocol.api.js5.Js5GroupProvider +import net.rsprot.protocol.api.js5.Js5Service +import net.rsprot.protocol.api.repositories.MessageDecoderRepositories +import net.rsprot.protocol.api.repositories.MessageEncoderRepositories +import net.rsprot.protocol.api.util.asCompletableFuture +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarFactory +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcInfoProtocol +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfoProtocol +import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityAvatarFactory +import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityProtocol +import net.rsprot.protocol.message.codec.incoming.provider.GameMessageConsumerRepositoryProvider +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ScheduledFuture +import kotlin.time.measureTime + +/** + * The primary network service implementation that brings all the necessary components together + * in a single "god" object. + * @param R the receiver type for the incoming game message consumers, typically a player + * @property allocator the byte buffer allocator used throughout the library + * @property ports the list of ports that the service will connect to + * @property betaWorld whether this world is a beta world + * @property bootstrapFactory the bootstrap factory used to configure the socket and Netty + * @property entityInfoProtocols a wrapper object to bring together player and NPC info protocols + * @property clientTypes the list of client types that were registered + * @property gameConnectionHandler the handler for game logins and reconnections + * @property exceptionHandlers the wrapper object for any exception handlers that the server must provide + * @property iNetAddressHandlers the wrapper object to handle anything to do with tracking and rejecting + * network addresses trying to establish connections + * @property gameMessageHandlers the wrapper object for anything to do with game packets post-login + * @property huffmanCodecProvider the provider for Huffman codecs, used to compress the text + * in some packets + * @property gameMessageConsumerRepositoryProvider the consumer repository for game messages. + * This is made public in case a blocking implementation is used, in which case the + * repository may be lazy-initialized into this library. + * @param rsaKeyPair the key pair for RSA to decode login blocks + * @param js5Configuration the configuration used by the JS5 service to determine the exact conditions + * for serving any connected clients + * @param js5GroupProvider the provider for any JS5 requests that the client makes + * @property encoderRepositories the encoder repositories for all the connection types + * @property js5Service the service behind the JS5, serving all connected clients fairly + * @property js5ServiceExecutor the thread executing the JS5 service. Since the main JS5 + * service is fairly lightweight and doesn't actually process much, a single thread + * is more than sufficient here. Utilizing more threads makes implementing a fair JS5 + * service significantly more difficult. + * @property decoderRepositories the repositories for decoding all the incoming client packets + * @property playerInfoProtocol the protocol responsible for tracking and computing player info + * for all the players in the game + * @property npcAvatarFactory the avatar factory for NPCs responsible for tracking anything + * necessary to represent a NPC to the client + * @property npcInfoProtocol the protocol responsible for tracking and computing everything related + * to the NPC info packet for every player + */ +@Suppress("MemberVisibilityCanBePrivate") +public class NetworkService + internal constructor( + internal val allocator: ByteBufAllocator, + internal val ports: List, + internal val betaWorld: Boolean, + internal val bootstrapFactory: BootstrapFactory, + internal val entityInfoProtocols: EntityInfoProtocols, + internal val clientTypes: List, + internal val gameConnectionHandler: GameConnectionHandler, + internal val exceptionHandlers: ExceptionHandlers, + internal val iNetAddressHandlers: INetAddressHandlers, + internal val gameMessageHandlers: GameMessageHandlers, + internal val loginHandlers: LoginHandlers, + public val huffmanCodecProvider: HuffmanCodecProvider, + public val gameMessageConsumerRepositoryProvider: GameMessageConsumerRepositoryProvider, + rsaKeyPair: RsaKeyPair, + js5Configuration: Js5Configuration, + js5GroupProvider: Js5GroupProvider, + ) { + internal val encoderRepositories: MessageEncoderRepositories = MessageEncoderRepositories(huffmanCodecProvider) + internal val js5Service: Js5Service = Js5Service(js5Configuration, js5GroupProvider) + private val js5ServiceExecutor = Thread(js5Service) + internal val decoderRepositories: MessageDecoderRepositories = + MessageDecoderRepositories.initialize( + clientTypes, + rsaKeyPair, + huffmanCodecProvider, + ) + public val playerInfoProtocol: PlayerInfoProtocol + get() = entityInfoProtocols.playerInfoProtocol + public val npcAvatarFactory: NpcAvatarFactory + get() = entityInfoProtocols.npcAvatarFactory + public val npcInfoProtocol: NpcInfoProtocol + get() = entityInfoProtocols.npcInfoProtocol + public val worldEntityAvatarFactory: WorldEntityAvatarFactory + get() = entityInfoProtocols.worldEntityAvatarFactory + public val worldEntityInfoProtocol: WorldEntityProtocol + get() = entityInfoProtocols.worldEntityInfoProtocol + + private lateinit var bossGroup: EventLoopGroup + private lateinit var childGroup: EventLoopGroup + private lateinit var js5PrefetchFuture: ScheduledFuture<*> + + /** + * Starts the network service by binding the provided ports. + * If any of them fail, the service is shut down and the exception is propagated forward. + */ + @ExperimentalUnsignedTypes + @ExperimentalStdlibApi + public fun start() { + val time = + measureTime { + bossGroup = bootstrapFactory.createParentLoopGroup() + childGroup = bootstrapFactory.createChildLoopGroup() + val initializer = + bootstrapFactory + .createServerBootstrap(bossGroup, childGroup) + .childHandler( + LoginChannelInitializer(this), + ) + val futures = + ports + .map(initializer::bind) + .map>(ChannelFuture::asCompletableFuture) + val future = + CompletableFuture + .allOf(*futures.toTypedArray()) + .handle { _, exception -> + if (exception != null) { + bossGroup.shutdownGracefully() + childGroup.shutdownGracefully() + throw exception + } + } + js5ServiceExecutor.start() + js5PrefetchFuture = Js5Service.startPrefetching(js5Service) + try { + // join it, which will propagate any exceptions + future.join() + } catch (t: Throwable) { + js5Service.triggerShutdown() + throw t + } + } + logger.info { "Started in: $time" } + logger.info { "Bound to ports: ${ports.joinToString(", ")}" } + logger.info { "Revision: $REVISION" } + val clientTypeNames = + clientTypes.joinToString(", ") { + it.name.lowercase().replaceFirstChar(Char::uppercase) + } + logger.info { "Supported client types: $clientTypeNames" } + } + + public fun shutdown() { + logger.info { "Attempting to shut down network service." } + js5Service.triggerShutdown() + js5PrefetchFuture.cancel(true) + bossGroup.shutdownGracefully() + childGroup.shutdownGracefully() + logger.info { "Network service successfully shut down." } + } + + /** + * Checks whether the provided [clientType] is supported by the service. + */ + public fun isSupported(clientType: OldSchoolClientType): Boolean = clientType in clientTypes + + public companion object { + public const val REVISION: Int = 225 + public const val LOGIN_TIMEOUT_SECONDS: Long = 60 + public const val JS5_TIMEOUT_SECONDS: Long = 30 + private val logger = InlineLogger() + } + } diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/Session.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/Session.kt new file mode 100644 index 000000000..e087bf138 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/Session.kt @@ -0,0 +1,283 @@ +package net.rsprot.protocol.api + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBufHolder +import io.netty.channel.ChannelHandlerContext +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.api.channel.inetAddress +import net.rsprot.protocol.api.game.GameMessageDecoder +import net.rsprot.protocol.api.logging.networkLog +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.loginprot.incoming.util.LoginBlock +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.message.codec.incoming.MessageConsumer +import java.net.InetAddress +import java.util.Queue + +/** + * The session objects are used to link the player instances together with the respective + * session instances. + * @param R the receiver of the game message consumers, typically a player + * @property ctx the channel handler context behind this session, public in case the server + * needs to directly manage it + * @property incomingMessageQueue the message queue for incoming game messages + * @param outgoingMessageQueueProvider the provider for outgoing message queues. + * One queue is allocated for each possible game server prot category. + * @property counter the incoming message counter used to determine whether to stop + * decoding packets after too many have flooded in over a single cycle. + * @property consumers the map of incoming game message classes to the consumers of + * said messages + * @property loginBlock the login block that resulted in this session being constructed + * @property incomingGameMessageConsumerExceptionHandler the exception handler responsible + * for managing any exceptions during the game message processing + * @property outgoingMessageQueues the array of outgoing game messages, categorized based + * on the server prots. This is because some packets are given priority and written + * to the client first, despite often being computed near the end of the cycle. + * @property inetAddress the inet address behind this connection + * @property disconnectionHook the disconnection hook to trigger if the channel happens + * to disconnect. It should be noted that it is the server's responsibility to set + * the hook after a successful login. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class Session( + public val ctx: ChannelHandlerContext, + private val incomingMessageQueue: Queue, + outgoingMessageQueueProvider: MessageQueueProvider, + private val counter: GameMessageCounter, + private val consumers: Map, MessageConsumer>, + public val loginBlock: LoginBlock<*>, + private val incomingGameMessageConsumerExceptionHandler: IncomingGameMessageConsumerExceptionHandler, +) { + private val outgoingMessageQueues: Array> = + Array(GameServerProtCategory.COUNT) { + outgoingMessageQueueProvider.provide() + } + public val inetAddress: InetAddress = ctx.inetAddress() + internal var disconnectionHook: Runnable? = null + private set + + /** + * Queues a game message to be written to the client based on the message's defined + * category + * @param message the outgoing game message to queue in its respective message queue + */ + public fun queue(message: OutgoingGameMessage) { + queue(message, message.category) + } + + /** + * Queues a game message to be written to the client based on the provided message + * category, in case one wishes to override the categories defined by the library + * @param message the outgoing game message + * @param category the category of the queue to put the message in + */ + public fun queue( + message: OutgoingGameMessage, + category: ServerProtCategory, + ) { + val categoryId = category.id + val queue = outgoingMessageQueues[categoryId] + queue += message + } + + /** + * Iterates through all the incoming packets over the last cycle and invokes the + * consumer with the receiver on them. If an exception is caught, it is propagated + * forward to the respective exception handler. + * If too many messages were received from the server during the last cycle, + * the decoder will have stopped accepting any new packets. If that was the case + * at the end of this function, the packet decoding will resume. + * @param receiver the receiver on whom to invoke the message consumers, + * typically a player instance + * @return the number of consumers that was invoked, this is handy in case + * one wishes to manually track the idle status serverside and potentially + * log the player out earlier if no packets are received over a number of cycles. + */ + public fun processIncomingPackets(receiver: R): Int { + var count = 0 + while (true) { + val packet = pollIncomingMessage() ?: break + val consumer = consumers[packet::class.java] + checkNotNull(consumer) { + "Consumer for packet $packet does not exist." + } + networkLog(logger) { + "Processing incoming game packet from channel '${ctx.channel()}': $packet" + } + try { + consumer.consume(receiver, packet) + } catch (cause: Throwable) { + incomingGameMessageConsumerExceptionHandler.exceptionCaught(this, packet, cause) + } + count++ + } + onPollComplete() + return count + } + + /** + * Sets a disconnection hook to be triggered if the connection to this channel is lost, + * allowing one to safely log the player out in such case. + * @param hook the hook runnable to invoke if the connection is lost + * @throws IllegalStateException if a hook was already registered + */ + public fun setDisconnectionHook(hook: Runnable) { + val currentHook = this.disconnectionHook + if (currentHook != null) { + throw IllegalStateException("A disconnection hook has already been registered!") + } + this.disconnectionHook = hook + } + + /** + * Polls one incoming game message from the queue, or null if none exists. + */ + private fun pollIncomingMessage(): IncomingGameMessage? = incomingMessageQueue.poll() + + /** + * Resets the incoming message counter and resumes reading. + */ + private fun onPollComplete() { + counter.reset() + resumeReading() + } + + /** + * Flushes any queued messages to the client, if any exist. + * The flushing process takes place in the netty event loop, thus + * the calls to this function are non-blocking and fast. + * If not all packets were written due to writability constraints, + * this function will further be re-triggered when channel writability + * turns back to true, meaning this function can be called directly + * from the netty event loop, thus the check inside it. + */ + public fun flush() { + if (!ctx.channel().isActive || + outgoingMessageQueues.all(Queue::isEmpty) + ) { + return + } + val eventLoop = ctx.channel().eventLoop() + if (eventLoop.inEventLoop()) { + writeAndFlush() + } else { + eventLoop.execute { + writeAndFlush() + } + } + } + + /** + * Clears all the remaining outgoing messages, releasing any buffers that were wrapped + * in a byte buffer holder. + * This function should be called on logout and whenever a reconnection happens, in order + * to get rid of any messages that got written to the session, but couldn't be flushed + * out in time before the session became inactive. + */ + public fun clear() { + for (queue in outgoingMessageQueues) { + for (message in queue) { + if (message is ByteBufHolder) { + message.release() + } + } + queue.clear() + } + } + + /** + * Writes any messages that can be written based on writability to the channel. + * The message queues are processed in ascending order, with the highest + * priority being the first. If the writability turns false half-way through, + * no more messages are written out - this will be resumed when channel writability + * changes to true again. + * At the end of this function call, the channel is flushed. + */ + private fun writeAndFlush() { + val channel = ctx.channel() + queues@ for (queue in outgoingMessageQueues) { + packets@ while (true) { + if (!channel.isWritable) { + break@queues + } + val next = queue.poll() ?: break@packets + networkLog(logger) { + "Writing outgoing game packet to channel '${ctx.channel()}': $next" + } + channel.write(next, channel.voidPromise()) + } + } + channel.flush() + networkLog(logger) { + val leftoverPackets = outgoingMessageQueues.sumOf(Queue::size) + if (leftoverPackets > 0) { + "Flushing outgoing game packets to channel " + + "'${ctx.channel()}': $leftoverPackets leftover packets remaining" + } else { + "Flushing outgoing game packets to channel ${ctx.channel()}" + } + } + } + + /** + * Sets auto-read back to true and single decode back to false. + */ + internal fun resumeReading() { + setReadStatus(stopReading = false) + } + + /** + * Sets auto-read back to false and single decode back to true. + */ + internal fun stopReading() { + setReadStatus(stopReading = true) + } + + /** + * Sets the auto-read and single decode status based on the input, + * if the channel is still open. + * If the single decode is set to false, netty will IMMEDIATELY stop + * decoding any more packets. Just turning auto-read to false does not + * have the same behavior, as by default, netty will read until the ctx + * is empty. This ensures that we only decode exactly up to the packet limit + * and never any more beyond that. + * If auto-read is set back to true, netty automatically calls ctx.read(), so + * we do not need to manually call this. + * @param stopReading whether to stop reading more packets, or resume reading + */ + private fun setReadStatus(stopReading: Boolean) { + val channel = ctx.channel() + // The decoder will be null if the channel has closed + val decoder = + channel.pipeline()[GameMessageDecoder::class.java] + ?: return + decoder.isSingleDecode = stopReading + channel.config().isAutoRead = !stopReading + } + + /** + * Adds an incoming message to the incoming message queue + */ + internal fun addIncomingMessage(incomingGameMessage: IncomingGameMessage) { + incomingMessageQueue += incomingGameMessage + } + + /** + * Increment the message counter for the provided incoming game message, + * based on the message's category. + */ + internal fun incrementCounter(incomingGameMessage: IncomingGameMessage) { + counter.increment(incomingGameMessage.category) + } + + /** + * Whether any of the incoming message categories are full, meaning + * no more packets should be decoded. + */ + internal fun isFull(): Boolean = counter.isFull() + + private companion object { + private val logger: InlineLogger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/SessionIdGenerator.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/SessionIdGenerator.kt new file mode 100644 index 000000000..a7edf7eb5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/SessionIdGenerator.kt @@ -0,0 +1,17 @@ +package net.rsprot.protocol.api + +import java.net.InetAddress + +/** + * A session id generator for new connections. + * By default, a secure random implementation is used. + * This session id is further passed back in the login block, and the library + * will verify to make sure the session id matches. + */ +public interface SessionIdGenerator { + /** + * Generates a new session id + * @param address in case the session id should be based on the address + */ + public fun generate(address: InetAddress): Long +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/StreamCipherProvider.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/StreamCipherProvider.kt new file mode 100644 index 000000000..56df612e9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/StreamCipherProvider.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.api + +import net.rsprot.crypto.cipher.StreamCipher + +/** + * A provider for stream ciphers, where a new instance is allocated after each successful login. + */ +public fun interface StreamCipherProvider { + /** + * Provides a new stream cipher based on the input seed. + */ + public fun provide(seed: IntArray): StreamCipher +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/bootstrap/BootstrapFactory.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/bootstrap/BootstrapFactory.kt new file mode 100644 index 000000000..2d085f81b --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/bootstrap/BootstrapFactory.kt @@ -0,0 +1,94 @@ +package net.rsprot.protocol.api.bootstrap + +import com.github.michaelbull.logging.InlineLogger +import io.netty.bootstrap.ServerBootstrap +import io.netty.buffer.ByteBufAllocator +import io.netty.channel.ChannelOption +import io.netty.channel.EventLoopGroup +import io.netty.channel.WriteBufferWaterMark +import io.netty.channel.epoll.Epoll +import io.netty.channel.epoll.EpollChannelOption +import io.netty.channel.epoll.EpollEventLoopGroup +import io.netty.channel.epoll.EpollMode +import io.netty.channel.epoll.EpollServerSocketChannel +import io.netty.channel.kqueue.KQueue +import io.netty.channel.kqueue.KQueueEventLoopGroup +import io.netty.channel.kqueue.KQueueServerSocketChannel +import io.netty.channel.nio.NioEventLoopGroup +import io.netty.channel.socket.nio.NioServerSocketChannel +import io.netty.incubator.channel.uring.IOUring +import io.netty.incubator.channel.uring.IOUringEventLoopGroup +import io.netty.incubator.channel.uring.IOUringServerSocketChannel +import net.rsprot.protocol.api.logging.networkLog + +/** + * A bootstrap factory responsible for generating the Netty bootstrap + */ +public class BootstrapFactory( + private val alloc: ByteBufAllocator, +) { + /** + * Creates a parent loop group with a single thread behind it, based on the best + * available event loop group. + */ + public fun createParentLoopGroup(): EventLoopGroup = + when { + IOUring.isAvailable() -> IOUringEventLoopGroup(1) + Epoll.isAvailable() -> EpollEventLoopGroup(1) + KQueue.isAvailable() -> KQueueEventLoopGroup(1) + else -> NioEventLoopGroup(1) + } + + /** + * Creates a child loop group with a number of threads based on availableProcessors * 2, + * which is done at Netty level. + */ + public fun createChildLoopGroup(): EventLoopGroup = + when { + IOUring.isAvailable() -> IOUringEventLoopGroup() + Epoll.isAvailable() -> EpollEventLoopGroup() + KQueue.isAvailable() -> KQueueEventLoopGroup() + else -> NioEventLoopGroup() + } + + /** + * Creates a server bootstrap using the parent and child event loop groups with + * a configuration that closely resembles the values found in the client. + */ + public fun createServerBootstrap( + parentGroup: EventLoopGroup, + childGroup: EventLoopGroup, + ): ServerBootstrap { + val channel = + when (parentGroup) { + is IOUringEventLoopGroup -> IOUringServerSocketChannel::class.java + is EpollEventLoopGroup -> EpollServerSocketChannel::class.java + is KQueueEventLoopGroup -> KQueueServerSocketChannel::class.java + is NioEventLoopGroup -> NioServerSocketChannel::class.java + else -> throw IllegalArgumentException("Unknown EventLoopGroup type") + } + networkLog(logger) { + "Bootstrap event loop group: ${parentGroup.javaClass.simpleName}" + } + return ServerBootstrap() + .group(parentGroup, childGroup) + .channel(channel) + .option(ChannelOption.ALLOCATOR, alloc) + .childOption(ChannelOption.ALLOCATOR, alloc) + .childOption(ChannelOption.AUTO_READ, false) + .childOption(ChannelOption.TCP_NODELAY, true) + .childOption(ChannelOption.SO_RCVBUF, 65536) + .childOption(ChannelOption.SO_SNDBUF, 65536) + .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30_000) + .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, WriteBufferWaterMark(524_288, 2_097_152)) + .also { + if (parentGroup is EpollEventLoopGroup) { + it.childOption(EpollChannelOption.EPOLL_MODE, EpollMode.LEVEL_TRIGGERED) + } + } + } + + private companion object { + private val logger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/channel/ChannelExtensions.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/channel/ChannelExtensions.kt new file mode 100644 index 000000000..5e7dd7e29 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/channel/ChannelExtensions.kt @@ -0,0 +1,36 @@ +package net.rsprot.protocol.api.channel + +import io.netty.channel.Channel +import io.netty.channel.ChannelHandler +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelPipeline +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.SocketAddress + +/** + * Gets the INetAddress from a socket address. + * @throws UnsupportedOperationException if the socket address doesn't support an IP address. + */ +public fun SocketAddress.inetAddress(): InetAddress = + if (this is InetSocketAddress) { + address + } else { + throw UnsupportedOperationException("Only IP addresses supported") + } + +/** + * Gets the INetAddress from the given channel + */ +public fun Channel.inetAddress(): InetAddress = remoteAddress().inetAddress() + +/** + * Gets the INetAddress from the given channel handler context. + */ +public fun ChannelHandlerContext.inetAddress(): InetAddress = channel().inetAddress() + +/** + * Replaces a channel handler with a new variant. + */ +public inline fun ChannelPipeline.replace(newHandler: ChannelHandler): ChannelHandler = + replace(T::class.java, newHandler::class.qualifiedName, newHandler) diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/decoder/IncomingMessageDecoder.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/decoder/IncomingMessageDecoder.kt new file mode 100644 index 000000000..85293601c --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/decoder/IncomingMessageDecoder.kt @@ -0,0 +1,121 @@ +package net.rsprot.protocol.api.decoder + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.ByteToMessageDecoder +import io.netty.handler.codec.DecoderException +import net.rsprot.buffer.extensions.g1 +import net.rsprot.buffer.extensions.g2 +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.Prot +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.message.codec.incoming.MessageDecoderRepository + +/** + * A general-purpose incoming message decoder, responsible for decoding all types + * of messages, including login, JS5 and game. + * @property decoder the decoder repository containing all the decoders for each opcode + * @property streamCipher the stream cipher used to properly decode the encrypted opcodes + * @property state the current state in this decoder, following a typical + * opcode -> (optional) size -> payload structure. + * @property decoder the decoder of the current opcode + * @property opcode the current opcode + * @property length the current packet's real length + */ +public abstract class IncomingMessageDecoder : ByteToMessageDecoder() { + protected abstract val decoders: MessageDecoderRepository + protected abstract val streamCipher: StreamCipher + + protected enum class State { + READ_OPCODE, + READ_LENGTH, + READ_PAYLOAD, + } + + protected var state: State = State.READ_OPCODE + protected lateinit var decoder: MessageDecoder<*> + protected var opcode: Int = -1 + protected var length: Int = 0 + + protected open fun readOpcode( + ctx: ChannelHandlerContext, + input: ByteBuf, + ) { + this.opcode = (input.g1() - streamCipher.nextInt()) and 0xFF + this.decoder = decoders.getDecoder(opcode) + this.length = this.decoder.prot.size + state = + if (this.length >= 0) { + State.READ_PAYLOAD + } else { + State.READ_LENGTH + } + } + + override fun decode( + ctx: ChannelHandlerContext, + input: ByteBuf, + out: MutableList, + ) { + if (state == State.READ_OPCODE) { + if (!input.isReadable) { + return + } + readOpcode(ctx, input) + } + + if (state == State.READ_LENGTH) { + when (length) { + Prot.VAR_BYTE -> { + if (!input.isReadable(Byte.SIZE_BYTES)) { + return + } + this.length = input.g1() + } + + Prot.VAR_SHORT -> { + if (!input.isReadable(Short.SIZE_BYTES)) { + return + } + this.length = input.g2() + } + + else -> { + throw IllegalStateException("Invalid length: $length") + } + } + state = State.READ_PAYLOAD + } + + if (state == State.READ_PAYLOAD) { + if (!input.isReadable(length)) { + return + } + decodePayload(ctx, input, out) + + state = State.READ_OPCODE + } + } + + /** + * Decodes the payload of this packet. + * Open implementation as game messages have further count tracking and + * more complex logic to deal with pausing the decoding. + */ + protected open fun decodePayload( + ctx: ChannelHandlerContext, + input: ByteBuf, + out: MutableList, + ) { + val payload = input.readSlice(length) + out += decoder.decode(payload.toJagByteBuf()) + if (payload.isReadable) { + throw DecoderException( + "Decoder ${decoder.javaClass} did not read entire payload " + + "of opcode $opcode: ${payload.readableBytes()}", + ) + } + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/encoder/OutgoingMessageEncoder.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/encoder/OutgoingMessageEncoder.kt new file mode 100644 index 000000000..d7245c464 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/encoder/OutgoingMessageEncoder.kt @@ -0,0 +1,133 @@ +package net.rsprot.protocol.api.encoder + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.MessageToByteEncoder +import net.rsprot.buffer.extensions.p1 +import net.rsprot.buffer.extensions.p2 +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.Prot +import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import net.rsprot.protocol.message.OutgoingMessage +import net.rsprot.protocol.message.codec.outgoing.MessageEncoderRepository + +/** + * A generic message encoder for all outgoing messages, including login, JS5 and game. + * @property cipher the stream cipher used to encrypt the opcodes, and in the case of + * some packets, the entire payload + * @property repository the message encoder repository containing all the encoders + */ +public abstract class OutgoingMessageEncoder : MessageToByteEncoder(OutgoingMessage::class.java) { + protected abstract val cipher: StreamCipher + protected abstract val repository: MessageEncoderRepository<*> + protected abstract val validate: Boolean + + override fun encode( + ctx: ChannelHandlerContext, + msg: OutgoingMessage, + out: ByteBuf, + ) { + val encoder = repository.getEncoder(msg::class.java) + val prot = encoder.prot + val opcode = prot.opcode + pSmart1Or2Enc(out, opcode) + + val sizeMarker = + when (prot.size) { + Prot.VAR_BYTE -> { + out.p1(0) + out.writerIndex() - 1 + } + Prot.VAR_SHORT -> { + out.p2(0) + out.writerIndex() - 2 + } + else -> -1 + } + + val payloadMarker = out.writerIndex() + encoder.encode( + cipher, + out.toJagByteBuf(), + msg, + ) + + // Update the size based on the number of bytes written, if it's a var-* packet + if (sizeMarker != -1) { + val writerIndex = out.writerIndex() + var length = writerIndex - payloadMarker + + // Ok login response is a relatively special case that requires encoding the size + // as either 3 or 4 bytes bigger than it actually is. + // This is because it is intended to include the header of the first packet that + // will come after the login response, so the client knows how many bytes to expect + // right away with the login response. This _could've_ been done differently by + // Jagex, but it isn't, resulting in this slightly awkward code. + // If the opcode is > 127, two bytes are needed to encode the opcode, + // otherwise a single byte is needed. In either case, 2 more bytes are needed + // for the size of the rebuild login packet itself. + if (msg is LoginResponse.Ok) { + length += + if (opcode > MAX_OPCODE_VALUE_FOR_SINGLE_BYTE_OPCODE) { + Short.SIZE_BYTES + Short.SIZE_BYTES + } else { + Byte.SIZE_BYTES + Short.SIZE_BYTES + } + } + out.writerIndex(sizeMarker) + when (prot.size) { + Prot.VAR_BYTE -> { + if (validate) { + check(length in 0..UByte.MAX_VALUE.toInt()) { + "Server prot $prot length out of bounds; expected 0..255, received $length; message: $msg" + } + } + out.p1(length) + } + Prot.VAR_SHORT -> { + if (validate) { + check(length in 0..MAX_PAYLOAD_SIZE) { + "Server prot $prot length out of bounds; " + + "expected 0..40_000, received $length; message: $msg" + } + } + out.p2(length) + } + } + out.writerIndex(writerIndex) + } else if (validate) { + val writerIndex = out.writerIndex() + val length = writerIndex - payloadMarker + check(length == prot.size) { + "Server prot $prot length mismatch; expected ${prot.size}, received $length; message: $msg" + } + } + } + + /** + * Writes a byte or short for the opcode with all the bytes + * encrypted using the stream cipher provided. + * The name of this function is from a leak. + */ + private fun pSmart1Or2Enc( + out: ByteBuf, + opcode: Int, + ) { + if (opcode < 0x80) { + out.p1(opcode + cipher.nextInt()) + } else { + out.p1((opcode ushr 8 or 0x80) + cipher.nextInt()) + out.p1((opcode and 0xFF) + cipher.nextInt()) + } + } + + private companion object { + /** + * The highest possible value for an opcode that can still be written in a single byte + * using a 1 or 2 byte smart. + */ + private const val MAX_OPCODE_VALUE_FOR_SINGLE_BYTE_OPCODE: Int = 127 + private const val MAX_PAYLOAD_SIZE: Int = 40_000 + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageDecoder.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageDecoder.kt new file mode 100644 index 000000000..b96a219e1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageDecoder.kt @@ -0,0 +1,87 @@ +package net.rsprot.protocol.api.game + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import io.netty.handler.codec.DecoderException +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.Session +import net.rsprot.protocol.api.decoder.IncomingMessageDecoder +import net.rsprot.protocol.api.logging.networkLog +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.codec.incoming.MessageDecoderRepository + +/** + * A decoder for game messages, one that respects the limitations set in place + * for incoming game messages to stop decoding after a specific threshold. + * Furthermore, this will discard any payload of a packet if no consumer + * has been registered, avoiding the creation of further garbage in the form + * of decoded messages or buffer slices. + */ +public class GameMessageDecoder( + public val networkService: NetworkService, + private val session: Session, + override val streamCipher: StreamCipher, + oldSchoolClientType: OldSchoolClientType, +) : IncomingMessageDecoder() { + override val decoders: MessageDecoderRepository = + networkService + .decoderRepositories + .gameMessageDecoderRepositories[oldSchoolClientType] + + override fun decodePayload( + ctx: ChannelHandlerContext, + input: ByteBuf, + out: MutableList, + ) { + if (length > SINGLE_PACKET_MAX_ACCEPTED_LENGTH) { + throw DecoderException( + "Opcode $opcode exceeds the natural maximum allowed length in OldSchool: " + + "$length > $SINGLE_PACKET_MAX_ACCEPTED_LENGTH", + ) + } + val messageClass = decoders.getMessageClass(this.decoder.javaClass) + val consumerRepository = networkService.gameMessageConsumerRepositoryProvider.provide() + val consumer = consumerRepository.consumers[messageClass] + if (consumer == null) { + networkLog(logger) { + "Discarding incoming game packet from channel '${ctx.channel()}': ${messageClass.simpleName}" + } + input.skipBytes(length) + return + } + val payload = input.readSlice(length) + val message = decoder.decode(payload.toJagByteBuf()) + if (payload.isReadable) { + throw DecoderException( + "Decoder ${decoder.javaClass} did not read entire payload: ${payload.readableBytes()}", + ) + } + out += message + session.incrementCounter(message as IncomingGameMessage) + if (session.isFull()) { + networkLog(logger) { + "Incoming packet limit reached, no longer reading incoming game packets from channel ${ctx.channel()}" + } + session.stopReading() + } + } + + @Suppress("unused") + private companion object { + /** + * The maximum size that a single packet can have in the client. + */ + private const val SINGLE_PACKET_MAX_PAYLOAD_LENGTH: Int = 5_000 + + /** + * The maximum size a single packet can have that the server still accepts in OldSchool. + */ + private const val SINGLE_PACKET_MAX_ACCEPTED_LENGTH: Int = 1_600 + private val logger: InlineLogger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageEncoder.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageEncoder.kt new file mode 100644 index 000000000..f811aad12 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageEncoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.api.game + +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.encoder.OutgoingMessageEncoder +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.message.codec.outgoing.MessageEncoderRepository + +/** + * The game messages encoder, following the traditional outgoing message encoder. + */ +public class GameMessageEncoder( + public val networkService: NetworkService<*>, + override val cipher: StreamCipher, + client: OldSchoolClientType, +) : OutgoingMessageEncoder() { + override val repository: MessageEncoderRepository<*> = + networkService.encoderRepositories.gameMessageDecoderRepositories[client] + override val validate: Boolean = true +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageHandler.kt new file mode 100644 index 000000000..f90f5f5db --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/game/GameMessageHandler.kt @@ -0,0 +1,101 @@ +package net.rsprot.protocol.api.game + +import com.github.michaelbull.logging.InlineLogger +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleStateEvent +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.Session +import net.rsprot.protocol.api.channel.inetAddress +import net.rsprot.protocol.api.logging.networkLog +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * The handler for game messages. + */ +public class GameMessageHandler( + private val networkService: NetworkService, + private val session: Session, +) : SimpleChannelInboundHandler() { + override fun handlerAdded(ctx: ChannelHandlerContext) { + // As auto-read is false, immediately begin reading once this handler + // has been added post-login + ctx.read() + } + + override fun channelActive(ctx: ChannelHandlerContext) { + // Register this channel in the respective address tracker + networkService + .iNetAddressHandlers + .gameInetAddressTracker + .register(ctx.inetAddress()) + networkLog(logger) { + "Channel is now active: ${ctx.channel()}" + } + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + // Triggers the disconnection hook when the channel goes inactive. + // This is the earliest guaranteed known point of no return + try { + session.disconnectionHook?.run() + } finally { + // Must ensure both blocks of code get invoked, even if one throws an exception + networkService + .iNetAddressHandlers + .gameInetAddressTracker + .deregister(ctx.inetAddress()) + networkLog(logger) { + "Channel is now inactive: ${ctx.channel()}" + } + } + } + + override fun channelRead0( + ctx: ChannelHandlerContext, + msg: IncomingGameMessage, + ) { + networkLog(logger) { + "Incoming game message accepted from channel '${ctx.channel()}': $msg" + } + session.addIncomingMessage(msg) + } + + override fun channelWritabilityChanged(ctx: ChannelHandlerContext) { + if (ctx.channel().isWritable) { + networkLog(logger) { + "Channel '${ctx.channel()}' is now writable again, continuing to write game packets" + } + session.flush() + } + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun exceptionCaught( + ctx: ChannelHandlerContext, + cause: Throwable, + ) { + networkService + .exceptionHandlers + .channelExceptionHandler + .exceptionCaught(ctx, cause) + } + + override fun userEventTriggered( + ctx: ChannelHandlerContext, + evt: Any, + ) { + // Handle idle states by disconnecting the user if this is hit. This is normally reached + // after 60 seconds of idle status. + if (evt is IdleStateEvent) { + networkLog(logger) { + "Login connection has gone idle, closing channel ${ctx.channel()}" + } + ctx.close() + } + } + + private companion object { + private val logger: InlineLogger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/ExceptionHandlers.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/ExceptionHandlers.kt new file mode 100644 index 000000000..8e03a5a6f --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/ExceptionHandlers.kt @@ -0,0 +1,46 @@ +package net.rsprot.protocol.api.handlers + +import net.rsprot.protocol.api.ChannelExceptionHandler +import net.rsprot.protocol.api.IncomingGameMessageConsumerExceptionHandler +import net.rsprot.protocol.api.implementation.DefaultIncomingGameMessageConsumerExceptionHandler + +/** + * A wrapper class for all the exception handlers necessary to make this library function safely. + * @property channelExceptionHandler the exception handler for any exceptions caught by netty handlers + * @property incomingGameMessageConsumerExceptionHandler the exception handler for exceptions triggered + * via any message consumers, in order to allow the message processing to take place safely without + * the server needing to wrap each payload with its own exception handler + */ +public class ExceptionHandlers + @JvmOverloads + public constructor( + public val channelExceptionHandler: ChannelExceptionHandler, + public val incomingGameMessageConsumerExceptionHandler: IncomingGameMessageConsumerExceptionHandler = + DefaultIncomingGameMessageConsumerExceptionHandler(), + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ExceptionHandlers<*> + + if (channelExceptionHandler != other.channelExceptionHandler) return false + if (incomingGameMessageConsumerExceptionHandler != other.incomingGameMessageConsumerExceptionHandler) { + return false + } + + return true + } + + override fun hashCode(): Int { + var result = channelExceptionHandler.hashCode() + result = 31 * result + incomingGameMessageConsumerExceptionHandler.hashCode() + return result + } + + override fun toString(): String = + "ExceptionHandlers(" + + "channelExceptionHandler=$channelExceptionHandler, " + + "incomingGameMessageConsumerExceptionHandler=$incomingGameMessageConsumerExceptionHandler" + + ")" + } diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/GameMessageHandlers.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/GameMessageHandlers.kt new file mode 100644 index 000000000..ce870f612 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/GameMessageHandlers.kt @@ -0,0 +1,52 @@ +package net.rsprot.protocol.api.handlers + +import net.rsprot.protocol.api.GameMessageCounterProvider +import net.rsprot.protocol.api.MessageQueueProvider +import net.rsprot.protocol.api.implementation.DefaultGameMessageCounterProvider +import net.rsprot.protocol.api.implementation.DefaultMessageQueueProvider +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * The handlers for incoming game messages. + * @property incomingGameMessageQueueProvider the queue provider for any incoming game messages + * @property outgoingGameMessageQueueProvider the queue provider for any outgoing game messages + * @property gameMessageCounterProvider the message counter provider, used to track the number + * of incoming messages over the span of one game cycle. + */ +public class GameMessageHandlers + @JvmOverloads + public constructor( + public val incomingGameMessageQueueProvider: MessageQueueProvider = + DefaultMessageQueueProvider(), + public val outgoingGameMessageQueueProvider: MessageQueueProvider = + DefaultMessageQueueProvider(), + public val gameMessageCounterProvider: GameMessageCounterProvider = DefaultGameMessageCounterProvider(), + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GameMessageHandlers + + if (incomingGameMessageQueueProvider != other.incomingGameMessageQueueProvider) return false + if (outgoingGameMessageQueueProvider != other.outgoingGameMessageQueueProvider) return false + if (gameMessageCounterProvider != other.gameMessageCounterProvider) return false + + return true + } + + override fun hashCode(): Int { + var result = incomingGameMessageQueueProvider.hashCode() + result = 31 * result + outgoingGameMessageQueueProvider.hashCode() + result = 31 * result + gameMessageCounterProvider.hashCode() + return result + } + + override fun toString(): String = + "GameMessageHandlers(" + + "incomingGameMessageQueueProvider=$incomingGameMessageQueueProvider, " + + "outgoingGameMessageQueueProvider=$outgoingGameMessageQueueProvider, " + + "gameMessageCounterProvider=$gameMessageCounterProvider" + + ")" + } diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/INetAddressHandlers.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/INetAddressHandlers.kt new file mode 100644 index 000000000..74c6ab99f --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/INetAddressHandlers.kt @@ -0,0 +1,48 @@ +package net.rsprot.protocol.api.handlers + +import net.rsprot.protocol.api.InetAddressTracker +import net.rsprot.protocol.api.InetAddressValidator +import net.rsprot.protocol.api.implementation.DefaultInetAddressTracker +import net.rsprot.protocol.api.implementation.DefaultInetAddressValidator + +/** + * The handlers for anything to do with INet addresses. + * @property inetAddressValidator the validator for new connections, responsible for rejecting + * any connections after a limit has been reached. + * @property js5InetAddressTracker the tracker for active JS5 connections + * @property gameInetAddressTracker the tracker for active game connections + */ +public class INetAddressHandlers + @JvmOverloads + public constructor( + public val inetAddressValidator: InetAddressValidator = DefaultInetAddressValidator(), + public val js5InetAddressTracker: InetAddressTracker = DefaultInetAddressTracker(), + public val gameInetAddressTracker: InetAddressTracker = DefaultInetAddressTracker(), + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as INetAddressHandlers + + if (inetAddressValidator != other.inetAddressValidator) return false + if (js5InetAddressTracker != other.js5InetAddressTracker) return false + if (gameInetAddressTracker != other.gameInetAddressTracker) return false + + return true + } + + override fun hashCode(): Int { + var result = inetAddressValidator.hashCode() + result = 31 * result + js5InetAddressTracker.hashCode() + result = 31 * result + gameInetAddressTracker.hashCode() + return result + } + + override fun toString(): String = + "INetAddressHandlers(" + + "inetAddressValidator=$inetAddressValidator, " + + "js5InetAddressTracker=$js5InetAddressTracker, " + + "gameInetAddressTracker=$gameInetAddressTracker" + + ")" + } diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/LoginHandlers.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/LoginHandlers.kt new file mode 100644 index 000000000..4c7739378 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/handlers/LoginHandlers.kt @@ -0,0 +1,78 @@ +package net.rsprot.protocol.api.handlers + +import net.rsprot.protocol.api.LoginDecoderService +import net.rsprot.protocol.api.SessionIdGenerator +import net.rsprot.protocol.api.StreamCipherProvider +import net.rsprot.protocol.api.implementation.DefaultLoginDecoderService +import net.rsprot.protocol.api.implementation.DefaultSessionIdGenerator +import net.rsprot.protocol.api.implementation.DefaultStreamCipherProvider +import net.rsprot.protocol.loginprot.incoming.pow.ProofOfWorkProvider +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeWorker +import net.rsprot.protocol.loginprot.incoming.pow.challenges.DefaultChallengeWorker +import net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.DefaultSha256ProofOfWorkProvider + +/** + * The handlers for anything to do with the login procedure. + * @property sessionIdGenerator the generator for session ids which are initially made + * at the very beginning when the client establishes a connection. This session id is + * furthermore passed whenever a login occurs and validated by the library to ensure it matches. + * @property streamCipherProvider the provider for game stream ciphers, by default, the stream + * cipher uses the normal OldSchool client implementation. + * @property loginDecoderService the decoder service responsible for decoding login blocks, + * as the RSA deciphering is fairly expensive, allowing this to be done on a different thread. + * @property proofOfWorkProvider the provider for proof of work which must be completed + * before a login can take place. If the provider returns null, no proof of work is used. + * @property proofOfWorkChallengeWorker the worker used to verify the validity of the challenge, + * allowing servers to execute this off of another thread. By default, this will be + * executed via the calling thread, as this is extremely fast to check. + * @property suppressInvalidLoginProts whether to suppress and kill the channel whenever an invalid + * login prot is received. This can be useful if the server is susceptible to web crawlers and + * anything of such nature which could lead into a lot of useless errors being thrown. + * By default, this is off, and errors will be thrown whenever an invalid prot is received. + */ +public class LoginHandlers + @JvmOverloads + public constructor( + public val sessionIdGenerator: SessionIdGenerator = DefaultSessionIdGenerator(), + public val streamCipherProvider: StreamCipherProvider = DefaultStreamCipherProvider(), + public val loginDecoderService: LoginDecoderService = DefaultLoginDecoderService(), + public val proofOfWorkProvider: ProofOfWorkProvider<*, *> = DefaultSha256ProofOfWorkProvider(1), + public val proofOfWorkChallengeWorker: ChallengeWorker = DefaultChallengeWorker, + public val suppressInvalidLoginProts: Boolean = false, + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LoginHandlers + + if (sessionIdGenerator != other.sessionIdGenerator) return false + if (streamCipherProvider != other.streamCipherProvider) return false + if (loginDecoderService != other.loginDecoderService) return false + if (proofOfWorkProvider != other.proofOfWorkProvider) return false + if (proofOfWorkChallengeWorker != other.proofOfWorkChallengeWorker) return false + if (suppressInvalidLoginProts != other.suppressInvalidLoginProts) return false + + return true + } + + override fun hashCode(): Int { + var result = sessionIdGenerator.hashCode() + result = 31 * result + streamCipherProvider.hashCode() + result = 31 * result + loginDecoderService.hashCode() + result = 31 * result + proofOfWorkProvider.hashCode() + result = 31 * result + proofOfWorkChallengeWorker.hashCode() + result = 31 * result + suppressInvalidLoginProts.hashCode() + return result + } + + override fun toString(): String = + "LoginHandlers(" + + "sessionIdGenerator=$sessionIdGenerator, " + + "streamCipherProvider=$streamCipherProvider, " + + "loginDecoderService=$loginDecoderService, " + + "proofOfWorkProvider=$proofOfWorkProvider, " + + "proofOfWorkChallengeWorker=$proofOfWorkChallengeWorker, " + + "suppressInvalidLoginProts=$suppressInvalidLoginProts" + + ")" + } diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultGameMessageCounter.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultGameMessageCounter.kt new file mode 100644 index 000000000..5ae19e85b --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultGameMessageCounter.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.api.implementation + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.api.GameMessageCounter +import net.rsprot.protocol.game.incoming.GameClientProtCategory + +/** + * A default game message counter that follows the normal OldSchool limitations, + * allowing for up to 10 user events and up to 50 client events, stopping decoding + * whenever either of the limitations is reached. + */ +public class DefaultGameMessageCounter : GameMessageCounter { + private val counts: IntArray = IntArray(PROT_TYPES_COUNT) + + override fun increment(clientProtCategory: ClientProtCategory) { + counts[clientProtCategory.id]++ + } + + override fun isFull(): Boolean = + GameClientProtCategory.entries.any { entry -> + counts[entry.id] >= entry.limit + } + + override fun reset() { + counts.fill(0) + } + + private companion object { + private const val PROT_TYPES_COUNT: Int = 2 + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultGameMessageCounterProvider.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultGameMessageCounterProvider.kt new file mode 100644 index 000000000..43e03bcb9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultGameMessageCounterProvider.kt @@ -0,0 +1,11 @@ +package net.rsprot.protocol.api.implementation + +import net.rsprot.protocol.api.GameMessageCounter +import net.rsprot.protocol.api.GameMessageCounterProvider + +/** + * The provider for the default game messages + */ +public class DefaultGameMessageCounterProvider : GameMessageCounterProvider { + override fun provide(): GameMessageCounter = DefaultGameMessageCounter() +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultIncomingGameMessageConsumerExceptionHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultIncomingGameMessageConsumerExceptionHandler.kt new file mode 100644 index 000000000..db2b7bba8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultIncomingGameMessageConsumerExceptionHandler.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.api.implementation + +import com.github.michaelbull.logging.InlineLogger +import net.rsprot.protocol.api.IncomingGameMessageConsumerExceptionHandler +import net.rsprot.protocol.api.Session +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * The default handler for incoming game messages, which will simply log the exceptions + * and errors, and in the case of errors, propagate them further. For any exceptions, + * nothing besides logging is done. + */ +public class DefaultIncomingGameMessageConsumerExceptionHandler : IncomingGameMessageConsumerExceptionHandler { + override fun exceptionCaught( + session: Session, + packet: IncomingGameMessage, + cause: Throwable, + ) { + logger.error(cause) { + "Exception during consumption of $packet for channel ${session.ctx.channel()}" + } + // Propagate errors forward + if (cause is Error) { + throw cause + } + } + + private companion object { + private val logger: InlineLogger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultInetAddressTracker.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultInetAddressTracker.kt new file mode 100644 index 000000000..c0949dbe8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultInetAddressTracker.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.api.implementation + +import net.rsprot.protocol.api.InetAddressTracker +import java.net.InetAddress +import java.util.concurrent.ConcurrentHashMap + +/** + * The default tracker for INet addresses, utilizing a concurrent hash map. + */ +public class DefaultInetAddressTracker : InetAddressTracker { + private val counts: MutableMap = ConcurrentHashMap() + + override fun register(address: InetAddress) { + counts.compute(address) { _, value -> + (value ?: 0) + 1 + } + } + + override fun deregister(address: InetAddress) { + counts.compute(address) { _, value -> + if (value == null || value <= 1) { + null + } else { + value - 1 + } + } + } + + override fun getCount(address: InetAddress): Int = counts.getOrDefault(address, 0) +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultInetAddressValidator.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultInetAddressValidator.kt new file mode 100644 index 000000000..c4b768f8a --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultInetAddressValidator.kt @@ -0,0 +1,40 @@ +package net.rsprot.protocol.api.implementation + +import net.rsprot.protocol.api.InetAddressValidator +import java.net.InetAddress + +/** + * The default validation for a max number of concurrent active connections + * from a specific INet address, limited to 10 by default. + */ +public class DefaultInetAddressValidator( + public val limit: Int = MAX_CONNECTIONS, +) : InetAddressValidator { + override fun acceptGameConnection( + address: InetAddress, + activeGameConnections: Int, + ): Boolean = activeGameConnections < limit + + override fun acceptJs5Connection( + address: InetAddress, + activeJs5Connections: Int, + seed: IntArray, + ): Boolean = activeJs5Connections < limit + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DefaultInetAddressValidator + + return limit == other.limit + } + + override fun hashCode(): Int = limit + + override fun toString(): String = "DefaultInetAddressValidator(limit=$limit)" + + private companion object { + private const val MAX_CONNECTIONS: Int = 10 + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultLoginDecoderService.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultLoginDecoderService.kt new file mode 100644 index 000000000..4f7bb6847 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultLoginDecoderService.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.api.implementation + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.api.LoginDecoderService +import net.rsprot.protocol.loginprot.incoming.util.LoginBlock +import net.rsprot.protocol.loginprot.incoming.util.LoginBlockDecodingFunction +import java.util.concurrent.CompletableFuture + +/** + * The default login decoder utilizing a ForkJoinPool to decode the login block. + */ +public class DefaultLoginDecoderService : LoginDecoderService { + override fun decode( + buffer: JagByteBuf, + betaWorld: Boolean, + decoder: LoginBlockDecodingFunction, + ): CompletableFuture> = + CompletableFuture>().completeAsync { + decoder.decode(buffer, betaWorld) + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultMessageQueueProvider.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultMessageQueueProvider.kt new file mode 100644 index 000000000..7b3f28573 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultMessageQueueProvider.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.api.implementation + +import net.rsprot.protocol.api.MessageQueueProvider +import net.rsprot.protocol.message.Message +import java.util.Queue +import java.util.concurrent.ConcurrentLinkedQueue + +/** + * The default message queue provider, returning a concurrent linked queue. + */ +public class DefaultMessageQueueProvider : MessageQueueProvider { + override fun provide(): Queue = ConcurrentLinkedQueue() +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultSessionIdGenerator.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultSessionIdGenerator.kt new file mode 100644 index 000000000..6f7fe4200 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultSessionIdGenerator.kt @@ -0,0 +1,14 @@ +package net.rsprot.protocol.api.implementation + +import net.rsprot.protocol.api.SessionIdGenerator +import java.net.InetAddress +import java.security.SecureRandom + +/** + * The default session id generator, using a secure random to generate the ids. + */ +public class DefaultSessionIdGenerator : SessionIdGenerator { + private val random = SecureRandom() + + override fun generate(address: InetAddress): Long = random.nextLong() +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultStreamCipherProvider.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultStreamCipherProvider.kt new file mode 100644 index 000000000..04bf15f36 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/implementation/DefaultStreamCipherProvider.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.api.implementation + +import net.rsprot.crypto.cipher.IsaacRandom +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.api.StreamCipherProvider + +/** + * The default stream cipher provider, returning an instance of the ISAAC random + * stream cipher based on the input seed. + */ +public class DefaultStreamCipherProvider : StreamCipherProvider { + override fun provide(seed: IntArray): StreamCipher = IsaacRandom(seed) +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5ChannelHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5ChannelHandler.kt new file mode 100644 index 000000000..44511f77e --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5ChannelHandler.kt @@ -0,0 +1,139 @@ +package net.rsprot.protocol.api.js5 + +import com.github.michaelbull.logging.InlineLogger +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleStateEvent +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.channel.inetAddress +import net.rsprot.protocol.api.logging.js5Log +import net.rsprot.protocol.api.logging.networkLog +import net.rsprot.protocol.js5.incoming.Js5GroupRequest +import net.rsprot.protocol.js5.incoming.PriorityChangeHigh +import net.rsprot.protocol.js5.incoming.PriorityChangeLow +import net.rsprot.protocol.js5.incoming.XorChange +import net.rsprot.protocol.message.IncomingJs5Message + +/** + * A channel handler for the JS5 connections + */ +public class Js5ChannelHandler( + private val networkService: NetworkService<*>, +) : SimpleChannelInboundHandler(IncomingJs5Message::class.java) { + private lateinit var client: Js5Client + private val service: Js5Service + get() = networkService.js5Service + + override fun channelActive(ctx: ChannelHandlerContext) { + networkService + .iNetAddressHandlers + .js5InetAddressTracker + .register(ctx.inetAddress()) + networkLog(logger) { + "Js5 channel '${ctx.channel()}' is now active" + } + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + networkService + .iNetAddressHandlers + .js5InetAddressTracker + .deregister(ctx.inetAddress()) + networkLog(logger) { + "Js5 channel '${ctx.channel()}' is now inactive" + } + } + + override fun handlerAdded(ctx: ChannelHandlerContext) { + // Instantiate the client when the handler is added, additionally read from the ctx + client = Js5Client(ctx.read()) + service.onClientConnected(client) + } + + override fun handlerRemoved(ctx: ChannelHandlerContext) { + service.onClientDisconnected(client) + } + + override fun channelRead0( + ctx: ChannelHandlerContext, + msg: IncomingJs5Message, + ) { + // Directly handle all the possible message types in a descending order of + // probability of being sent + when (msg) { + is Js5GroupRequest -> { + js5Log(logger) { + "JS5 group request from channel '${ctx.channel()}' received: $msg" + } + service.push(client, msg) + } + PriorityChangeLow -> { + js5Log(logger) { + "Priority changed to low in channel ${ctx.channel()}" + } + client.setLowPriority() + service.readIfNotFull(client) + // Furthermore, notify the client as we might've transferred prefetch over + service.notifyIfNotEmpty(client) + } + PriorityChangeHigh -> { + js5Log(logger) { + "Priority changed to high in channel ${ctx.channel()}" + } + client.setHighPriority() + service.readIfNotFull(client) + } + is XorChange -> { + js5Log(logger) { + "Encryption key received from channel '${ctx.channel()}': $msg" + } + service.use { + client.setXorKey(msg.key) + service.readIfNotFull(client) + } + } + else -> throw IllegalStateException("Unknown JS5 message: $msg") + } + } + + override fun channelReadComplete(ctx: ChannelHandlerContext) { + // Read more from the context if we have space to read, when the read has completed + service.readIfNotFull(client) + } + + override fun channelWritabilityChanged(ctx: ChannelHandlerContext) { + // If the channel turns writable again, allow the service to continue + // serving to this client + if (ctx.channel().isWritable) { + service.notifyIfNotEmpty(client) + } + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun exceptionCaught( + ctx: ChannelHandlerContext, + cause: Throwable, + ) { + networkService + .exceptionHandlers + .channelExceptionHandler + .exceptionCaught(ctx, cause) + } + + override fun userEventTriggered( + ctx: ChannelHandlerContext, + evt: Any, + ) { + // Close the context if the channel goes idle + if (evt is IdleStateEvent) { + networkLog(logger) { + "JS5 channel has gone idle, closing channel ${ctx.channel()}" + } + ctx.close() + } + } + + private companion object { + private val logger: InlineLogger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Client.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Client.kt new file mode 100644 index 000000000..1f9f4c6c8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Client.kt @@ -0,0 +1,335 @@ +package net.rsprot.protocol.api.js5 + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import net.rsprot.protocol.api.js5.util.IntArrayDeque +import net.rsprot.protocol.api.logging.js5Log +import net.rsprot.protocol.js5.incoming.Js5GroupRequest +import net.rsprot.protocol.js5.incoming.UrgentRequest +import net.rsprot.protocol.js5.outgoing.Js5GroupResponse +import kotlin.math.min + +/** + * The JS5 client is responsible for keeping track of all the requests and state of + * a connected client. + * @property ctx the channel handler context behind this client + * @property urgent the array deque for any urgent requests - if any exist, these will be served + * before the prefetch requests + * @property prefetch the array deque for any prefetch requests + * @property currentRequest the current partial request, since the service allows fair serving, + * we may only write a sector of a group at a time, rather than the full thing + * @property priority the current priority of this client. If the client is logged in, + * they will have a higher priority and the service will by default write 3x as much data + * to that channel compared to any other client that is set to a low, logged-out priority. + * @property writtenByteCount the number of bytes written since the last flush + * @property writtenGroupCount the number of group writes that have completed since + * the last flush + */ +public class Js5Client( + public val ctx: ChannelHandlerContext, +) { + private val urgent: IntArrayDeque = IntArrayDeque(MAX_QUEUE_SIZE) + private val prefetch: IntArrayDeque = IntArrayDeque(MAX_QUEUE_SIZE) + private val awaitingPrefetch: IntArrayDeque = IntArrayDeque(MAX_QUEUE_SIZE) + private val currentRequest: PartialJs5GroupRequest = PartialJs5GroupRequest() + private var lowPriorityChangeCount: Int = 0 + public var priority: ClientPriority = ClientPriority.LOW + private set + + private var writtenByteCount: Int = 0 + private var writtenGroupCount: Int = 0 + private var xorKey: Int = 0 + + /** + * Gets the next block response for this channel, typically a section of a cache group. + * @param provider the provider for JS5 groups + * @param blockLength the maximum size of a block to write in a single call. + * If the number of bytes left in this group to write is less than the block, + * then less bytes are written than expected. + * @return a group response to write to the client, or null if none exists + */ + public fun getNextBlock( + provider: Js5GroupProvider, + blockLength: Int, + ): Js5GroupResponse? { + var block: ByteBuf? = currentRequest.block + if (block == null || currentRequest.isComplete()) { + val request = pop() + if (request == -1) { + return null + } + val archiveId = request ushr 16 + val groupId = request and 0xFFFF + js5Log(logger) { + "Assigned next request block: $archiveId:$groupId" + } + block = provider.provide(archiveId, groupId) + currentRequest.set(block) + } + val progress = currentRequest.progress + val length = currentRequest.getNextBlockLengthAndIncrementProgress(blockLength) + writtenByteCount += length + if (currentRequest.isComplete()) { + writtenGroupCount++ + } + return Js5GroupResponse( + block, + progress, + length, + xorKey, + ) + } + + /** + * Pushes a JS5 request to this client, adding it to the end of the respective queue. + * If this is an urgent request, the request itself is removed from the prefetch list, + * if it exists. This is a one-way operation, however, as the client by default + * can only request duplicate requests via this manner. Any modifications to the client + * can by-pass this, but that is a non issue since we offer a fair JS5 service where + * the actual request doesn't matter and the number of bytes written is all the same + * to everyone connected. + * @param request the request to add to this client + */ + public fun push(request: Js5GroupRequest) { + val bitpacked = request.bitpacked + if (request is UrgentRequest) { + prefetch.remove(bitpacked) + awaitingPrefetch.remove(bitpacked) + urgent.addLast(bitpacked) + } else { + // If on login screen (NOT pre-login screen), do not throttle prefetch + if (lowPriorityChangeCount >= 2 && priority == ClientPriority.LOW) { + this.prefetch.addLast(bitpacked) + } else { + awaitingPrefetch.addLast(bitpacked) + } + } + } + + /** + * Pops a request from this client, prioritizing urgent requests before prefetch. + * @return the bitpacked id of the request, or -1 if the queues are empty. + */ + private fun pop(): Int { + val urgent = urgent.removeFirstOrDefault(-1) + if (urgent != -1) { + return urgent + } + return prefetch.removeFirstOrDefault(-1) + } + + /** + * Transfers [threshold] worth of bytes of prefetch requests to be served + * to the client. + * @param groupProvider the provider for JS5 group sizes, allowing for proper throttling + * @param threshold the threshold at which the loop breaks, stopping any more bytes + * being transmitted via prefetch than described here + * @return whether any bytes were transferred to be served to the client. + */ + internal fun transferPrefetch( + groupProvider: Js5GroupProvider, + threshold: Int, + ): Boolean { + // Only transfer prefetch over if the connection is effectively idle + if (awaitingPrefetch.isEmpty() || + urgent.isNotEmpty() || + prefetch.isNotEmpty() || + !currentRequest.isComplete() || + !ctx.channel().isActive || + !ctx.channel().isWritable + ) { + return false + } + var transferredBytes = 0 + while (true) { + val next = awaitingPrefetch.removeFirstOrDefault(-1) + if (next == -1) { + break + } + val archiveId = next ushr 16 + val groupId = next and 0xFFFF + val size = groupProvider.provide(archiveId, groupId).readableBytes() + prefetch.addLast(next) + transferredBytes += size + // Do not clog the pipeline with more than threshold of prefetch data at a time, as this results + // in urgent requests being delayed + if (size >= threshold) { + break + } + } + return transferredBytes > 0 + } + + /** + * Transfers all prefetch requests from the throttled collection over to the + * non-throttled one. This is one whenever the client goes on login-screen. + */ + private fun transferAllPrefetch() { + while (true) { + val next = awaitingPrefetch.removeFirstOrDefault(-1) + if (next == -1) { + break + } + prefetch.addLast(next) + } + } + + /** + * Sets this client in a low priority mode, meaning it gets served less data + * than those that have a higher priority. + * This happens when a player logs out of the game. + */ + public fun setLowPriority() { + this.priority = ClientPriority.LOW + // A bit of a state machine, as client sets priority to low when it first connects, + // and once more when it reaches the login screen. + // Since our goal is to give urgent requests max priority before login screen to speed + // up load times, we use a boolean flag to indicate when to stop throttling prefetch requests. + // Once the second (or any furthermore) low priority response is received, the throttle is removed. + if (++lowPriorityChangeCount >= 2) { + transferAllPrefetch() + } + } + + /** + * Sets this client in a high priority state, meaning it gets served more data than + * those that have a lower priority. + * The client switches to high priority when the player logs into the game. + */ + public fun setHighPriority() { + this.priority = ClientPriority.HIGH + } + + /** + * Sets the pending encryption key for this client. + * Client sends this when it receives corrupt data for a group, in which case + * it will close the old socket first, allowing for a new one to be opened. + * In that new one, the encryption key is first sent out, followed by any requests + * it was previously waiting on, as those get transferred from the "awaiting response" + * over to the "awaiting to be requested" map. + * + * A potential theory for why this exist is network filters for HTTP traffic. + * The client can listen to port 443 which is commonly used for HTTP traffic, + * and some groups in the cache are not compressed at all. If said groups + * contain normal text that would fail any network filters, such as those + * set by schools, this could be a way to bypass these filters by re-requesting + * the data with it re-encrypted, meaning it won't get caught in the filters again. + * Besides compression, it is possible by pure chance that a sequence of bytes + * doesn't pass the filters, too. + * @param key the encryption key to use + */ + public fun setXorKey(key: Int) { + xorKey = key + } + + /** + * Checks that the JS5 client isn't full and can accept more requests in both queues. + */ + public fun isNotFull(): Boolean = + urgent.size < MAX_QUEUE_SIZE && (prefetch.size + awaitingPrefetch.size) < (MAX_QUEUE_SIZE + 1) + + /** + * Checks if the client is empty of any requests and has no pending request to still write. + */ + private fun isEmpty(): Boolean = currentRequest.isComplete() && urgent.isEmpty() && prefetch.isEmpty() + + /** + * Checks that the client is not empty, meaning it has some requests, or a group is half-written. + */ + public fun isNotEmpty(): Boolean = !isEmpty() + + /** + * Checks if the client is ready by ensuring it can be written to, and there is some data + * to be written to the client. + */ + public fun isReady(): Boolean = ctx.channel().isWritable && isNotEmpty() + + /** + * Checks if the client needs flushing based on the input thresholds. + * @param flushThresholdInBytes the number of bytes that must be written since the last flush, + * before a flush will occur. Note that the flush only occurs in this case if at least one + * full group has been finished, as there's no reason to flush an incomplete group, + * the client will not be able to continue anyhow. + * @param flushThresholdInGroups the number of full groups written to the client before + * a flush should occur. + */ + public fun needsFlushing( + flushThresholdInBytes: Int, + flushThresholdInGroups: Int, + ): Boolean = + writtenGroupCount >= flushThresholdInGroups || + (writtenGroupCount > 0 && writtenByteCount >= flushThresholdInBytes) || + (writtenByteCount > 0 && isEmpty()) || + !ctx.channel().isWritable + + /** + * Resets the number of bytes and groups written. + */ + public fun resetTracker() { + writtenByteCount = 0 + writtenGroupCount = 0 + } + + /** + * A class for tracking partial JS5 group requests, allowing us to feed the groups + * sections at a time, instead of the full thing - this is due to the groups + * varying greatly in size and naively sending a group could open it up for attacks. + * @property block the current block that is being written + * @property progress the current number of bytes written of this block + * @property length the total number of bytes of this block until it has been + * completely written over. + */ + public class PartialJs5GroupRequest { + public var block: ByteBuf? = null + private set + public var progress: Int = 0 + private set + private var length: Int = 0 + + /** + * Checks whether this group has been fully written over to the client + */ + public fun isComplete(): Boolean = progress >= length + + /** + * Gets the length of the next block, capped to [blockLength], + * and increments the current progress by that value. + */ + public fun getNextBlockLengthAndIncrementProgress(blockLength: Int): Int { + val progress = this.progress + this.progress = min(this.length, this.progress + blockLength) + return this.progress - progress + } + + /** + * Sets a new block to be written to the client, resetting the progress and + * updating the length to that of this block. + */ + public fun set(block: ByteBuf) { + this.block = block + this.progress = 0 + this.length = block.readableBytes() + } + } + + /** + * The possible client priority values. + * @property LOW is used when the client is logged out, e.g. in the loading + * screen or at the login screen + * @property HIGH is used when the client is logged into the game. + */ + public enum class ClientPriority { + LOW, + HIGH, + } + + private companion object { + private val logger: InlineLogger = InlineLogger() + + /** + * The maximum number of requests the client can send out per each group at a time. + */ + private const val MAX_QUEUE_SIZE: Int = 200 + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Configuration.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Configuration.kt new file mode 100644 index 000000000..1375f64da --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Configuration.kt @@ -0,0 +1,88 @@ +package net.rsprot.protocol.api.js5 + +/** + * The configuration for the JS5 service per client basis. + * @property blockSizeInBytes the maximum number of bytes written per client per iteration + * @property flushThresholdInBytes the minimum number of bytes that must be written into + * the channel before a flush occurs. Note that the flush will only occur if at least one + * group has been successfully written over, as there's no point in flushing a partial + * large group - the client cannot continue anyhow. Furthermore, flushing occurs if + * there's no more data to write, ignoring both the [flushThresholdInBytes] and + * [flushThresholdInRequests] thresholds in the process. + * @property flushThresholdInRequests the number of full requests that must be written + * into this channel since the last flush before another flush can trigger. As explained + * before, a flush will take place if no more data can be written out, ignoring this threshold. + * @property priorityRatio the ratio for how much more data to write to logged in clients + * compared to those logged out. A ratio of 3 means that per iteration, any low priority + * client will receive [blockSizeInBytes] number of bytes, however any logged in client + * will receive [blockSizeInBytes] * 3 number of bytes. This effectively gives priority to + * those logged in, as they are in more of a need for data than anyone sitting on the loading + * screen. + * @property prefetchTransferThresholdInBytes the number of bytes to transfer from the 'pending' + * prefetch collection over to the 'being served' prefetch collection. This is a soft cap, + * meaning it will keep transferring groups until the moment that the sum of all transferred + * is equal to or above this value. It is worth noting that this will be uncapped when + * the user reaches the login screen, allowing for fast downloads of the cache if one + * chooses to sit on the login screen. Before reaching the login screen, however, the + * thresholds are still applied. + * Below are some numbers of how long it takes to transfer + * the entire OldSchool cache over via localhost using various thresholds: + * Uncapped - 1 minute, 20 seconds + * 16,384 bytes: 3 minutes, 30 seconds + * 8,192 bytes: 5 minutes, 52 seconds + * 4,096 bytes: 15 minutes, 26 seconds + * + * It is worth noting that prefetch groups are NOT necessary, one could disable them altogether, + * however this would mean the users will experience small loading screens even days into the gameplay. + * Sweet spot is having as small delays as possible when the client requires urgent responses, but + * still downloading the entire thing as soon as possible. 8,192 bytes appears to be that sweet spot. + */ +public class Js5Configuration public constructor( + public val blockSizeInBytes: Int = 512, + public val flushThresholdInBytes: Int = 10240, + public val flushThresholdInRequests: Int = 10, + public val priorityRatio: Int = 3, + public val prefetchTransferThresholdInBytes: Int = 8192, +) { + init { + require(blockSizeInBytes >= 8) { + "Js5 block size must be at least 8 bytes" + } + require(priorityRatio >= 1) { + "Priority ratio must be at least 1" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Js5Configuration + + if (blockSizeInBytes != other.blockSizeInBytes) return false + if (flushThresholdInBytes != other.flushThresholdInBytes) return false + if (flushThresholdInRequests != other.flushThresholdInRequests) return false + if (priorityRatio != other.priorityRatio) return false + if (prefetchTransferThresholdInBytes != other.prefetchTransferThresholdInBytes) return false + + return true + } + + override fun hashCode(): Int { + var result = blockSizeInBytes + result = 31 * result + flushThresholdInBytes + result = 31 * result + flushThresholdInRequests + result = 31 * result + priorityRatio + result = 31 * result + prefetchTransferThresholdInBytes + return result + } + + override fun toString(): String = + "Js5Configuration(" + + "blockSizeInBytes=$blockSizeInBytes, " + + "flushThresholdInBytes=$flushThresholdInBytes, " + + "flushThresholdInRequests=$flushThresholdInRequests, " + + "priorityRatio=$priorityRatio, " + + "prefetchTransferThresholdInBytes=$prefetchTransferThresholdInBytes" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5GroupProvider.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5GroupProvider.kt new file mode 100644 index 000000000..df62a441b --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5GroupProvider.kt @@ -0,0 +1,19 @@ +package net.rsprot.protocol.api.js5 + +import io.netty.buffer.ByteBuf + +/** + * The group provider interface for JS5. + */ +public fun interface Js5GroupProvider { + /** + * Provides a JS5 group based on the input archive and group + * @param archive the archive id requested by the client + * @param group the group in that archive requested + * @return a full JS5 group to be written to the client + */ + public fun provide( + archive: Int, + group: Int, + ): ByteBuf +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5MessageDecoder.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5MessageDecoder.kt new file mode 100644 index 000000000..05bcfda53 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5MessageDecoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.api.js5 + +import net.rsprot.crypto.cipher.NopStreamCipher +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.decoder.IncomingMessageDecoder +import net.rsprot.protocol.message.codec.incoming.MessageDecoderRepository + +/** + * A message decoder for JS5 packets. + */ +public class Js5MessageDecoder( + public val networkService: NetworkService<*>, +) : IncomingMessageDecoder() { + override val decoders: MessageDecoderRepository = + networkService + .decoderRepositories + .js5MessageDecoderRepository + override val streamCipher: StreamCipher = NopStreamCipher +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5MessageEncoder.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5MessageEncoder.kt new file mode 100644 index 000000000..872838565 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5MessageEncoder.kt @@ -0,0 +1,38 @@ +package net.rsprot.protocol.api.js5 + +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.crypto.cipher.NopStreamCipher +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.encoder.OutgoingMessageEncoder +import net.rsprot.protocol.message.OutgoingMessage +import net.rsprot.protocol.message.codec.outgoing.MessageEncoderRepository + +/** + * A message encoder for JS5 requests. + */ +public class Js5MessageEncoder( + public val networkService: NetworkService<*>, +) : OutgoingMessageEncoder() { + override val cipher: StreamCipher = NopStreamCipher + override val repository: MessageEncoderRepository<*> = + networkService.encoderRepositories.js5MessageDecoderRepository + override val validate: Boolean = false + + override fun encode( + ctx: ChannelHandlerContext, + msg: OutgoingMessage, + out: ByteBuf, + ) { + // Unlike all the other encoders, JS5 does not use any opcode system + // It simply just writes the request ids followed by the payload itself. + val encoder = repository.getEncoder(msg::class.java) + encoder.encode( + cipher, + out.toJagByteBuf(), + msg, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Service.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Service.kt new file mode 100644 index 000000000..709893937 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/Js5Service.kt @@ -0,0 +1,286 @@ +package net.rsprot.protocol.api.js5 + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBuf +import net.rsprot.protocol.api.js5.util.UniqueQueue +import net.rsprot.protocol.api.logging.js5Log +import net.rsprot.protocol.js5.incoming.Js5GroupRequest +import net.rsprot.protocol.js5.outgoing.Js5GroupResponse +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import kotlin.math.min + +/** + * A single-threaded JS5 service implementation used to fairly feed + * all connected clients, with a priority on those in the logged in state. + * @property configuration the configuration to use for writing the data to clients + * @property provider the provider for JS5 groups to write over + */ +public class Js5Service( + private val configuration: Js5Configuration, + private val provider: Js5GroupProvider, +) : Runnable { + private val clients = UniqueQueue() + private val connectedClients = ArrayDeque() + + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + @PublishedApi + internal val lock: Object = Object() + + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + private val clientLock: Object = Object() + + @Volatile + private var isRunning: Boolean = true + + override fun run() { + while (true) { + var client: Js5Client + var response: Js5GroupResponse + var flush: Boolean + synchronized(lock) { + while (true) { + if (!isRunning) { + return + } + val next = clients.removeFirstOrNull() + if (next == null) { + lock.wait() + continue + } + client = next + if (!client.ctx.channel().isActive) { + continue + } + val priority = client.priority + val ratio = + if (priority == Js5Client.ClientPriority.HIGH) { + configuration.priorityRatio + } else { + 1 + } + response = client.getNextBlock( + provider, + configuration.blockSizeInBytes * ratio, + ) ?: continue + flush = + client.needsFlushing( + configuration.flushThresholdInBytes, + configuration.flushThresholdInRequests, + ) + if (flush) { + client.resetTracker() + } + break + } + } + + serveClient(client, response, flush) + } + } + + private fun prefetch(): Runnable = + Runnable { + // Ensure the connectedClients collection doesn't modify during it, as modifications + // during iteration may not occur + synchronized(clientLock) { + for (client in connectedClients) { + // Obtain a short-lived lock to avoid blocking the service for long periods + // This is to ensure we don't run into concurrency issues. + synchronized(lock) { + if (client.transferPrefetch( + provider, + configuration.prefetchTransferThresholdInBytes, + ) + ) { + clients.add(client) + lock.notifyAll() + if (client.isNotFull()) { + client.ctx.read() + } + } + } + } + } + } + + internal fun onClientConnected(client: Js5Client) { + synchronized(clientLock) { + this.connectedClients += client + } + } + + internal fun onClientDisconnected(client: Js5Client) { + synchronized(clientLock) { + this.connectedClients -= client + } + } + + /** + * Serves a client with a jS5 response which may only be a subsection of a full group. + * @param client the client to serve + * @param response the response to write to the client + * @param flush whether to flush the channel after writing this request + */ + private fun serveClient( + client: Js5Client, + response: Js5GroupResponse, + flush: Boolean, + ) { + val ctx = client.ctx + ctx.write(response) + js5Log(logger) { + "Serving channel '${ctx.channel()}' with response: $response" + } + if (flush) { + js5Log(logger) { + "Flushing channel ${ctx.channel()}" + } + ctx.flush() + } + synchronized(lock) { + if (!flush || client.isReady()) { + js5Log(logger) { + "Continuing to serve channel ${ctx.channel()}" + } + clients.add(client) + } else { + js5Log(logger) { + "No longer serving channel ${ctx.channel()}" + } + } + + val notFull = client.isNotFull() + if (notFull) { + ctx.read() + } + js5Log(logger) { + "Reading further JS5 requests from channel ${ctx.channel()}" + } + } + } + + /** + * Pushes a new JS5 request to this client + * @param client the client to push the request to + * @param request the request to push to this client + */ + public fun push( + client: Js5Client, + request: Js5GroupRequest, + ) { + synchronized(lock) { + client.push(request) + + if (client.isReady()) { + clients.add(client) + lock.notifyAll() + } + + if (client.isNotFull()) { + client.ctx.read() + } + } + } + + /** + * Requests a read from the given channel if it can receive more requests. + * @param client the client to check + */ + public fun readIfNotFull(client: Js5Client) { + synchronized(lock) { + if (client.isNotFull()) { + js5Log(logger) { + "Reading further JS5 requests from channel ${client.ctx.channel()}" + } + client.ctx.read() + } + } + } + + /** + * Notifies the lock if the list of clients is not empty, resuming the JS5 + * service in the process. + */ + public fun notifyIfNotEmpty(client: Js5Client) { + synchronized(lock) { + if (client.isNotEmpty()) { + js5Log(logger) { + "Channel '${client.ctx.channel()}' is now writable, continuing to serve JS5 requests." + } + clients.add(client) + lock.notifyAll() + } + } + } + + /** + * Executes the [block] in a synchronized manner as the rest of the JS5. + */ + @PublishedApi + internal inline fun use(block: () -> Unit) { + synchronized(lock) { + block() + } + } + + /** + * Triggers a shutdown. + */ + public fun triggerShutdown() { + isRunning = false + synchronized(lock) { + lock.notifyAll() + } + } + + public companion object { + /** + * The interval at which a terminator byte is expected in the client. + */ + private const val BLOCK_LENGTH: Int = 512 + private val logger: InlineLogger = InlineLogger() + + /** + * Prepares a JS5 buffer to be in a format read to be served to the clients, + * by splitting the payload up into chunks of 512 bytes, which each have a 0xFF + * terminator splitting them. + * @param archive the archive id, written as a byte at the start + * @param group the group id, written as a short at the start + * @param input the input byte buffer from the cache, with version information + * stripped off + * @param output the output byte buffer into which to write the split-up JS5 buffers. + */ + public fun prepareJs5Buffer( + archive: Int, + group: Int, + input: ByteBuf, + output: ByteBuf, + ) { + val readableBytes = input.readableBytes() + output.writeByte(archive) + output.writeShort(group) + // Block length - 3 as we already wrote 3 bytes at the start + val len = min(readableBytes, BLOCK_LENGTH - 3) + output.writeBytes(input, 0, len) + + var offset = len + while (offset < readableBytes) { + output.writeByte(0xFF) + // Block length - 1 as we already wrote the separator 0xFF + val nextBlockLength = min(readableBytes - offset, BLOCK_LENGTH - 1) + output.writeBytes(input, offset, nextBlockLength) + offset += nextBlockLength + } + } + + public fun startPrefetching(service: Js5Service): ScheduledFuture<*> = + Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay( + service.prefetch(), + 200, + 200, + TimeUnit.MILLISECONDS, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/util/IntArrayDeque.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/util/IntArrayDeque.kt new file mode 100644 index 000000000..0add87127 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/util/IntArrayDeque.kt @@ -0,0 +1,621 @@ +package net.rsprot.protocol.api.js5.util + +/** + * A specialized array deque for int types, allowing no-autoboxing implementation. + * This is effectively just the Kotlin stdlib ArrayDeque class copied over and adjusted + * to work off of the primitive int. + */ +@Suppress("ReplaceUntilWithRangeUntil", "NOTHING_TO_INLINE", "MemberVisibilityCanBePrivate") +public class IntArrayDeque { + private var head: Int = 0 + private var elementData: IntArray + + public var size: Int = 0 + private set + + public val lastIndex: Int + get() = elementData.size - 1 + + /** + * Constructs an empty deque with specified [initialCapacity], or throws [IllegalArgumentException] if [initialCapacity] is negative. + */ + public constructor( + initialCapacity: Int, + ) { + elementData = + when { + initialCapacity == 0 -> emptyElementData + initialCapacity > 0 -> IntArray(initialCapacity) + else -> throw IllegalArgumentException("Illegal Capacity: $initialCapacity") + } + } + + /** + * Constructs an empty deque. + */ + public constructor() { + elementData = emptyElementData + } + + /** + * Constructs a deque that contains the same elements as the specified [elements] collection in the same order. + */ + public constructor( + elements: Collection, + ) { + elementData = elements.toIntArray() + size = elementData.size + if (elementData.isEmpty()) elementData = emptyElementData + } + + /** + * Ensures that the capacity of this deque is at least equal to the specified [minCapacity]. + * + * If the current capacity is less than the [minCapacity], a new backing storage is allocated with greater capacity. + * Otherwise, this method takes no action and simply returns. + */ + private fun ensureCapacity(minCapacity: Int) { + if (minCapacity < 0) throw IllegalStateException("Deque is too big.") // overflow + if (minCapacity <= elementData.size) return + if (elementData === emptyElementData) { + elementData = IntArray(minCapacity.coerceAtLeast(DEFAULT_MIN_CAPACITY)) + return + } + + val newCapacity = newCapacity(elementData.size, minCapacity) + copyElements(newCapacity) + } + + /** + * Creates a new array with the specified [newCapacity] size and copies elements in the [elementData] array to it. + */ + private fun copyElements(newCapacity: Int) { + val newElements = IntArray(newCapacity) + elementData.copyInto(newElements, 0, head, elementData.size) + elementData.copyInto(newElements, elementData.size - head, 0, head) + head = 0 + elementData = newElements + } + + private inline fun internalGet(internalIndex: Int): Int = elementData[internalIndex] + + private fun positiveMod(index: Int): Int = if (index >= elementData.size) index - elementData.size else index + + private fun negativeMod(index: Int): Int = if (index < 0) index + elementData.size else index + + private inline fun internalIndex(index: Int): Int = positiveMod(head + index) + + private fun incremented(index: Int): Int = if (index == elementData.lastIndex) 0 else index + 1 + + private fun decremented(index: Int): Int = if (index == 0) elementData.lastIndex else index - 1 + + public fun isEmpty(): Boolean = size == 0 + + public inline fun isNotEmpty(): Boolean = !isEmpty() + + /** + * Returns the first element, or throws [NoSuchElementException] if this deque is empty. + */ + public fun first(): Int = if (isEmpty()) throw NoSuchElementException("ArrayDeque is empty.") else internalGet(head) + + /** + * Returns the first element, or `null` if this deque is empty. + */ + public fun firstOrNull(): Int? = if (isEmpty()) null else internalGet(head) + + /** + * Returns the last element, or throws [NoSuchElementException] if this deque is empty. + */ + public fun last(): Int = + if (isEmpty()) throw NoSuchElementException("ArrayDeque is empty.") else internalGet(internalIndex(lastIndex)) + + /** + * Returns the last element, or `null` if this deque is empty. + */ + public fun lastOrNull(): Int? = if (isEmpty()) null else internalGet(internalIndex(lastIndex)) + + /** + * Prepends the specified [element] to this deque. + */ + public fun addFirst(element: Int) { + ensureCapacity(size + 1) + + head = decremented(head) + elementData[head] = element + size += 1 + } + + /** + * Appends the specified [element] to this deque. + */ + public fun addLast(element: Int) { + ensureCapacity(size + 1) + + elementData[internalIndex(size)] = element + size += 1 + } + + /** + * Removes the first element from this deque and returns that removed element, or throws [NoSuchElementException] if this deque is empty. + */ + public fun removeFirst(): Int { + if (isEmpty()) throw NoSuchElementException("ArrayDeque is empty.") + + val element = internalGet(head) + elementData[head] = 0 + head = incremented(head) + size -= 1 + return element + } + + /** + * Removes the first element from this deque and returns that removed element, or returns `null` if this deque is empty. + */ + public fun removeFirstOrNull(): Int? = if (isEmpty()) null else removeFirst() + + /** + * Removes the first element from this deque and returns that removed element, + * or returns [default] if this deque is empty. + */ + public fun removeFirstOrDefault(default: Int): Int = if (isEmpty()) default else removeFirst() + + /** + * Removes the last element from this deque and returns that removed element, or throws [NoSuchElementException] if this deque is empty. + */ + public fun removeLast(): Int { + if (isEmpty()) throw NoSuchElementException("ArrayDeque is empty.") + + val internalLastIndex = internalIndex(lastIndex) + val element = internalGet(internalLastIndex) + elementData[internalLastIndex] = 0 + size -= 1 + return element + } + + /** + * Removes the last element from this deque and returns that removed element, or returns `null` if this deque is empty. + */ + public fun removeLastOrNull(): Int? = if (isEmpty()) null else removeLast() + + // MutableList, MutableCollection + public fun add(element: Int): Boolean { + addLast(element) + return true + } + + public fun add( + index: Int, + element: Int, + ) { + checkPositionIndex(index, size) + + if (index == size) { + addLast(element) + return + } else if (index == 0) { + addFirst(element) + return + } + + ensureCapacity(size + 1) + + // Elements in circular array lay in 2 ways: + // 1. `head` is less than `tail`: [#, #, e1, e2, e3, #] + // 2. `head` is greater than `tail`: [e3, #, #, #, e1, e2] + // where head is the index of the first element in the circular array, + // and tail is the index following the last element. + // + // At this point the insertion index is not equal to head or tail. + // Also the circular array can store at least one more element. + // + // Depending on where the given element must be inserted the preceding or the succeeding + // elements will be shifted to make room for the element to be inserted. + // + // In case the preceding elements are shifted: + // * if the insertion index is greater than the head (regardless of circular array form) + // -> shift the preceding elements + // * otherwise, the circular array has (2) form and the insertion index is less than tail + // -> shift all elements in the back of the array + // -> shift preceding elements in the front of the array + // In case the succeeding elements are shifted: + // * if the insertion index is less than the tail (regardless of circular array form) + // -> shift the succeeding elements + // * otherwise, the circular array has (2) form and the insertion index is greater than head + // -> shift all elements in the front of the array + // -> shift succeeding elements in the back of the array + + val internalIndex = internalIndex(index) + + if (index < (size + 1) shr 1) { + // closer to the first element -> shift preceding elements + val decrementedInternalIndex = decremented(internalIndex) + val decrementedHead = decremented(head) + + if (decrementedInternalIndex >= head) { + elementData[decrementedHead] = elementData[head] // head can be zero + elementData.copyInto(elementData, head, head + 1, decrementedInternalIndex + 1) + } else { // head > tail + elementData.copyInto(elementData, head - 1, head, elementData.size) // head can't be zero + elementData[elementData.size - 1] = elementData[0] + elementData.copyInto(elementData, 0, 1, decrementedInternalIndex + 1) + } + + elementData[decrementedInternalIndex] = element + head = decrementedHead + } else { + // closer to the last element -> shift succeeding elements + val tail = internalIndex(size) + + if (internalIndex < tail) { + elementData.copyInto(elementData, internalIndex + 1, internalIndex, tail) + } else { // head > tail + elementData.copyInto(elementData, 1, 0, tail) + elementData[0] = elementData[elementData.size - 1] + elementData.copyInto(elementData, internalIndex + 1, internalIndex, elementData.size - 1) + } + + elementData[internalIndex] = element + } + size += 1 + } + + private fun copyCollectionElements( + internalIndex: Int, + elements: Collection, + ) { + val iterator = elements.iterator() + + for (index in internalIndex until elementData.size) { + if (!iterator.hasNext()) break + elementData[index] = iterator.next() + } + for (index in 0 until head) { + if (!iterator.hasNext()) break + elementData[index] = iterator.next() + } + + size += elements.size + } + + public fun addAll(elements: Collection): Boolean { + if (elements.isEmpty()) return false + ensureCapacity(this.size + elements.size) + copyCollectionElements(internalIndex(size), elements) + return true + } + + public fun addAll( + index: Int, + elements: Collection, + ): Boolean { + checkPositionIndex(index, size) + + if (elements.isEmpty()) { + return false + } else if (index == size) { + return addAll(elements) + } + + ensureCapacity(this.size + elements.size) + + val tail = internalIndex(size) + val internalIndex = internalIndex(index) + val elementsSize = elements.size + + if (index < (size + 1) shr 1) { + // closer to the first element -> shift preceding elements + + var shiftedHead = head - elementsSize + + if (internalIndex >= head) { + if (shiftedHead >= 0) { + elementData.copyInto(elementData, shiftedHead, head, internalIndex) + } else { // head < tail, insertion leads to head >= tail + shiftedHead += elementData.size + val elementsToShift = internalIndex - head + val shiftToBack = elementData.size - shiftedHead + + if (shiftToBack >= elementsToShift) { + elementData.copyInto(elementData, shiftedHead, head, internalIndex) + } else { + elementData.copyInto(elementData, shiftedHead, head, head + shiftToBack) + elementData.copyInto(elementData, 0, head + shiftToBack, internalIndex) + } + } + } else { // head > tail, internalIndex < tail + elementData.copyInto(elementData, shiftedHead, head, elementData.size) + if (elementsSize >= internalIndex) { + elementData.copyInto(elementData, elementData.size - elementsSize, 0, internalIndex) + } else { + elementData.copyInto(elementData, elementData.size - elementsSize, 0, elementsSize) + elementData.copyInto(elementData, 0, elementsSize, internalIndex) + } + } + head = shiftedHead + copyCollectionElements(negativeMod(internalIndex - elementsSize), elements) + } else { + // closer to the last element -> shift succeeding elements + + val shiftedInternalIndex = internalIndex + elementsSize + + if (internalIndex < tail) { + if (tail + elementsSize <= elementData.size) { + elementData.copyInto(elementData, shiftedInternalIndex, internalIndex, tail) + } else { // head < tail, insertion leads to head >= tail + if (shiftedInternalIndex >= elementData.size) { + elementData.copyInto(elementData, shiftedInternalIndex - elementData.size, internalIndex, tail) + } else { + val shiftToFront = tail + elementsSize - elementData.size + elementData.copyInto(elementData, 0, tail - shiftToFront, tail) + elementData.copyInto(elementData, shiftedInternalIndex, internalIndex, tail - shiftToFront) + } + } + } else { // head > tail, internalIndex > head + elementData.copyInto(elementData, elementsSize, 0, tail) + if (shiftedInternalIndex >= elementData.size) { + elementData.copyInto( + elementData, + shiftedInternalIndex - elementData.size, + internalIndex, + elementData.size, + ) + } else { + elementData.copyInto(elementData, 0, elementData.size - elementsSize, elementData.size) + elementData.copyInto( + elementData, + shiftedInternalIndex, + internalIndex, + elementData.size - elementsSize, + ) + } + } + copyCollectionElements(internalIndex, elements) + } + + return true + } + + public fun get(index: Int): Int { + checkElementIndex(index, size) + + return internalGet(internalIndex(index)) + } + + public fun set( + index: Int, + element: Int, + ): Int { + checkElementIndex(index, size) + + val internalIndex = internalIndex(index) + val oldElement = internalGet(internalIndex) + elementData[internalIndex] = element + + return oldElement + } + + public fun contains(element: Int): Boolean = indexOf(element) != -1 + + public fun indexOf(element: Int): Int { + val tail = internalIndex(size) + + if (head < tail) { + for (index in head until tail) { + if (element == elementData[index]) return index - head + } + } else { + for (index in head until elementData.size) { + if (element == elementData[index]) return index - head + } + for (index in 0 until tail) { + if (element == elementData[index]) return index + elementData.size - head + } + } + + return -1 + } + + public fun lastIndexOf(element: Int): Int { + val tail = internalIndex(size) + + if (head < tail) { + for (index in tail - 1 downTo head) { + if (element == elementData[index]) return index - head + } + } else if (head > tail) { + for (index in tail - 1 downTo 0) { + if (element == elementData[index]) return index + elementData.size - head + } + for (index in elementData.lastIndex downTo head) { + if (element == elementData[index]) return index - head + } + } + + return -1 + } + + public fun remove(element: Int): Boolean { + val index = indexOf(element) + if (index == -1) return false + removeAt(index) + return true + } + + public fun removeAt(index: Int): Int { + checkElementIndex(index, size) + + if (index == lastIndex) { + return removeLast() + } else if (index == 0) { + return removeFirst() + } + + val internalIndex = internalIndex(index) + val element = internalGet(internalIndex) + + if (index < size shr 1) { + // closer to the first element -> shift preceding elements + if (internalIndex >= head) { + elementData.copyInto(elementData, head + 1, head, internalIndex) + } else { // head > tail, internalIndex < head + elementData.copyInto(elementData, 1, 0, internalIndex) + elementData[0] = elementData[elementData.size - 1] + elementData.copyInto(elementData, head + 1, head, elementData.size - 1) + } + + elementData[head] = 0 + head = incremented(head) + } else { + // closer to the last element -> shift succeeding elements + val internalLastIndex = internalIndex(lastIndex) + + if (internalIndex <= internalLastIndex) { + elementData.copyInto(elementData, internalIndex, internalIndex + 1, internalLastIndex + 1) + } else { // head > tail, internalIndex > head + elementData.copyInto(elementData, internalIndex, internalIndex + 1, elementData.size) + elementData[elementData.size - 1] = elementData[0] + elementData.copyInto(elementData, 0, 1, internalLastIndex + 1) + } + + elementData[internalLastIndex] = 0 + } + size -= 1 + + return element + } + + public fun removeAll(elements: Collection): Boolean = filterInPlace { !elements.contains(it) } + + public fun retainAll(elements: Collection): Boolean = filterInPlace { elements.contains(it) } + + private inline fun filterInPlace(predicate: (Int) -> Boolean): Boolean { + if (this.isEmpty() || elementData.isEmpty()) { + return false + } + + val tail = internalIndex(size) + var newTail = head + var modified = false + + if (head < tail) { + for (index in head until tail) { + val element = elementData[index] + + if (predicate(element)) { + elementData[newTail++] = element + } else { + modified = true + } + } + + elementData.fill(0, newTail, tail) + } else { + for (index in head until elementData.size) { + val element = elementData[index] + elementData[index] = 0 + + if (predicate(element)) { + elementData[newTail++] = element + } else { + modified = true + } + } + + newTail = positiveMod(newTail) + + for (index in 0 until tail) { + val element = elementData[index] + elementData[index] = 0 + + if (predicate(element)) { + elementData[newTail] = element + newTail = incremented(newTail) + } else { + modified = true + } + } + } + if (modified) { + size = negativeMod(newTail - head) + } + + return modified + } + + public fun clear() { + val tail = internalIndex(size) + if (head < tail) { + elementData.fill(0, head, tail) + } else if (isNotEmpty()) { + elementData.fill(0, head, elementData.size) + elementData.fill(0, 0, tail) + } + head = 0 + size = 0 + } + + @Suppress("NOTHING_TO_OVERRIDE") + public fun toArray(array: IntArray): IntArray { + val dest = ( + if (array.size >= size) { + array + } else { + IntArray(size) + } + ) + + val tail = internalIndex(size) + if (head < tail) { + elementData.copyInto(dest, startIndex = head, endIndex = tail) + } else if (isNotEmpty()) { + elementData.copyInto(dest, destinationOffset = 0, startIndex = head, endIndex = elementData.size) + elementData.copyInto(dest, destinationOffset = elementData.size - head, startIndex = 0, endIndex = tail) + } + + return array + } + + @Suppress("NOTHING_TO_OVERRIDE") + public fun toArray(): IntArray = toArray(IntArray(size)) + + internal companion object { + private val emptyElementData = IntArray(0) + private const val DEFAULT_MIN_CAPACITY = 10 + + internal fun checkElementIndex( + index: Int, + size: Int, + ) { + if (index < 0 || index >= size) { + throw IndexOutOfBoundsException("index: $index, size: $size") + } + } + + internal fun checkPositionIndex( + index: Int, + size: Int, + ) { + if (index < 0 || index > size) { + throw IndexOutOfBoundsException("index: $index, size: $size") + } + } + + private const val MAX_ARRAY_SIZE = Int.MAX_VALUE - 8 + + /** [oldCapacity] and [minCapacity] must be non-negative. */ + internal fun newCapacity( + oldCapacity: Int, + minCapacity: Int, + ): Int { + // overflow-conscious + var newCapacity = oldCapacity + (oldCapacity shr 1) + if (newCapacity - minCapacity < 0) { + newCapacity = minCapacity + } + if (newCapacity - MAX_ARRAY_SIZE > 0) { + newCapacity = if (minCapacity > MAX_ARRAY_SIZE) Int.MAX_VALUE else MAX_ARRAY_SIZE + } + return newCapacity + } + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/util/UniqueQueue.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/util/UniqueQueue.kt new file mode 100644 index 000000000..c082209c5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/js5/util/UniqueQueue.kt @@ -0,0 +1,51 @@ +package net.rsprot.protocol.api.js5.util + +/** + * A unique ArrayDeque that utilizes a hash set to check whether something already exists + * in the queue. + * This implementation is NOT thread-safe. + * @property queue the backing array deque + * @property set the hash set used to check if something exists in this queue + */ +public class UniqueQueue : Iterable { + private val queue = ArrayDeque() + private val set = HashSet() + + /** + * Adds the element [v] into this queue if it isn't already in the hash set. + * @return true if the element was added + */ + public fun add(v: T): Boolean { + if (set.add(v)) { + queue.addLast(v) + return true + } + + return false + } + + /** + * Removes the first element from this queue, or null if it doesn't exist. + * If it does exist, the element is furthermore removed from the backing hash set. + * @return element if it exists, else null + */ + public fun removeFirstOrNull(): T? { + val v = queue.removeFirstOrNull() + if (v != null) { + set.remove(v) + return v + } + + return null + } + + /** + * Clears both the queue and the set. + */ + public fun clear() { + queue.clear() + set.clear() + } + + override fun iterator(): Iterator = queue.iterator() +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/logging/LoggingExt.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/logging/LoggingExt.kt new file mode 100644 index 000000000..5ba24cd3b --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/logging/LoggingExt.kt @@ -0,0 +1,64 @@ +package net.rsprot.protocol.api.logging + +import com.github.michaelbull.logging.InlineLogger +import net.rsprot.protocol.common.LogLevel +import net.rsprot.protocol.common.RSProtFlags + +/** + * Performs a debug log if network logging flag is enabled. + * This effectively allows one to apply a second filter as network stuff + * is fairly high pressure. Furthermore, since the logger level checks + * actually consist of quite a lot of function calls and checks, + * they're not as cheap as one might expect, so running a preliminary + * boolean check on it beforehand avoids doing any of that work, meaning + * this should have virtually no effect in production if disabled. + */ +internal inline fun networkLog( + logger: InlineLogger, + block: () -> Any?, +) { + logBlock(logger, RSProtFlags.networkLogging, block) +} + +/** + * Performs a debug log if JS5 logging flag is enabled. + * This effectively allows one to apply a second filter as JS5 logging + * is extremely high pressure. Furthermore, since the logger level checks + * actually consist of quite a lot of function calls and checks, + * they're not as cheap as one might expect, so running a preliminary + * boolean check on it beforehand avoids doing any of that work, meaning + * this should have virtually no effect in production if disabled. + */ +internal inline fun js5Log( + logger: InlineLogger, + block: () -> Any?, +) { + logBlock(logger, RSProtFlags.js5Logging, block) +} + +private inline fun logBlock( + logger: InlineLogger, + level: LogLevel, + block: () -> Any?, +) { + when (level) { + LogLevel.OFF -> { + // no-op + } + LogLevel.TRACE -> { + logger.trace(block) + } + LogLevel.DEBUG -> { + logger.debug(block) + } + LogLevel.INFO -> { + logger.info(block) + } + LogLevel.WARN -> { + logger.warn(block) + } + LogLevel.ERROR -> { + logger.error(block) + } + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt new file mode 100644 index 000000000..86cfeb7f4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/GameLoginResponseHandler.kt @@ -0,0 +1,231 @@ +@file:Suppress("DuplicatedCode") + +package net.rsprot.protocol.api.login + +import com.github.michaelbull.logging.InlineLogger +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.ChannelPipeline +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.crypto.cipher.IsaacRandom +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.crypto.cipher.StreamCipherPair +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.Session +import net.rsprot.protocol.api.channel.inetAddress +import net.rsprot.protocol.api.channel.replace +import net.rsprot.protocol.api.game.GameMessageDecoder +import net.rsprot.protocol.api.game.GameMessageEncoder +import net.rsprot.protocol.api.game.GameMessageHandler +import net.rsprot.protocol.api.logging.networkLog +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.loginprot.incoming.util.LoginBlock +import net.rsprot.protocol.loginprot.incoming.util.LoginClientType +import net.rsprot.protocol.loginprot.outgoing.LoginResponse + +/** + * A response handler for login requests, allowing the server to write either + * a successful or a failed login response, depending on the server's decision. + * @property networkService the main network service god object + * @property ctx the channel handler context to write the response to + */ +public class GameLoginResponseHandler( + public val networkService: NetworkService, + public val ctx: ChannelHandlerContext, +) { + /** + * Writes a successful login response to the client. + * @param response the login response to write + * @param loginBlock the login request that the client initially made + * @return a session object if the login was successful, otherwise a null. + */ + public fun writeSuccessfulResponse( + response: LoginResponse.Ok, + loginBlock: LoginBlock<*>, + ): Session? { + val oldSchoolClientType = + getOldSchoolClientType(loginBlock) + if (oldSchoolClientType == null || !networkService.isSupported(oldSchoolClientType)) { + networkLog(logger) { + "Unsupported client type received from channel " + + "'${ctx.channel()}': $oldSchoolClientType, login block: $loginBlock" + } + ctx + .writeAndFlush(LoginResponse.InvalidLoginPacket) + .addListener(ChannelFutureListener.CLOSE) + return null + } + val address = ctx.inetAddress() + val count = + networkService + .iNetAddressHandlers + .gameInetAddressTracker + .getCount(address) + val accepted = + networkService + .iNetAddressHandlers + .inetAddressValidator + .acceptGameConnection(address, count) + // Secondary validation just before we allow the server to log the user in + if (!accepted) { + networkLog(logger) { + "INetAddressValidator rejected game login for channel ${ctx.channel()}" + } + ctx + .writeAndFlush(LoginResponse.TooManyAttempts) + .addListener(ChannelFutureListener.CLOSE) + return null + } + val cipher = createStreamCipherPair(loginBlock) + + if (networkService.betaWorld) { + val encoder = + networkService + .encoderRepositories + .loginMessageDecoderRepository + .getEncoder(response::class.java) + val buffer = ctx.alloc().buffer(37 + 1).toJagByteBuf() + buffer.p1(37) + encoder.encode(cipher.encoderCipher, buffer, response) + ctx.writeAndFlush(buffer.buffer) + } else { + ctx.writeAndFlush(response) + } + + val pipeline = ctx.channel().pipeline() + + val session = + createSession(loginBlock, pipeline, cipher.decodeCipher, oldSchoolClientType, cipher.encoderCipher) + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + + public fun writeSuccessfulResponse( + response: LoginResponse.ReconnectOk, + loginBlock: LoginBlock<*>, + ): Session? { + val oldSchoolClientType = + getOldSchoolClientType(loginBlock) + if (oldSchoolClientType == null || !networkService.isSupported(oldSchoolClientType)) { + networkLog(logger) { + "Unsupported client type received from channel " + + "'${ctx.channel()}': $oldSchoolClientType, login block: $loginBlock" + } + ctx.writeAndFlush(LoginResponse.InvalidLoginPacket) + return null + } + val (encodingCipher, decodingCipher) = createStreamCipherPair(loginBlock) + + // Unlike in the above case, we kind of have to assume it was successful + // as the player is already in the game and needs to continue on as normal + ctx.write(response, ctx.voidPromise()) + val pipeline = ctx.channel().pipeline() + + val session = + createSession(loginBlock, pipeline, decodingCipher, oldSchoolClientType, encodingCipher) + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $loginBlock" + } + return session + } + + private fun createStreamCipherPair(loginBlock: LoginBlock<*>): StreamCipherPair { + val encodeSeed = loginBlock.seed + val decodeSeed = + IntArray(encodeSeed.size) { index -> + encodeSeed[index] + DECODE_SEED_OFFSET + } + + val encodingCipher = IsaacRandom(decodeSeed) + val decodingCipher = IsaacRandom(encodeSeed) + return StreamCipherPair(encodingCipher, decodingCipher) + } + + private fun getOldSchoolClientType(loginBlock: LoginBlock<*>): OldSchoolClientType? { + val oldSchoolClientType = + when (loginBlock.clientType) { + LoginClientType.DESKTOP -> OldSchoolClientType.DESKTOP + LoginClientType.ENHANCED_WINDOWS -> OldSchoolClientType.DESKTOP + LoginClientType.ENHANCED_LINUX -> OldSchoolClientType.DESKTOP + LoginClientType.ENHANCED_MAC -> OldSchoolClientType.DESKTOP + else -> null + } + return oldSchoolClientType + } + + private fun createSession( + loginBlock: LoginBlock<*>, + pipeline: ChannelPipeline, + decodingCipher: StreamCipher, + oldSchoolClientType: OldSchoolClientType, + encodingCipher: StreamCipher, + ): Session { + val session = + Session( + ctx, + networkService + .gameMessageHandlers + .incomingGameMessageQueueProvider + .provide(), + networkService + .gameMessageHandlers + .outgoingGameMessageQueueProvider, + networkService + .gameMessageHandlers + .gameMessageCounterProvider + .provide(), + networkService + .gameMessageConsumerRepositoryProvider + .provide() + .consumers, + loginBlock, + networkService + .exceptionHandlers + .incomingGameMessageConsumerExceptionHandler, + ) + pipeline.replace( + GameMessageDecoder( + networkService, + session, + decodingCipher, + oldSchoolClientType, + ), + ) + pipeline.replace( + GameMessageEncoder(networkService, encodingCipher, oldSchoolClientType), + ) + pipeline.replace>(GameMessageHandler(networkService, session)) + return session + } + + /** + * Writes a failed response to the client. This is _all_ requests that aren't the ok + * response, even ones where technically it's correct - this is because the client + * always makes a new connection to re-request the login, nothing is kept open + * over long periods of time. + * @param response the response to write to the client - this cannot be the successful + * response or the proof of work response, as those are handled in a special manner. + */ + public fun writeFailedResponse(response: LoginResponse) { + if (response is LoginResponse.ProofOfWork) { + throw IllegalStateException("Proof of Work is handled at the engine level.") + } + if (response is LoginResponse.Successful) { + throw IllegalStateException("Successful login response is handled at the engine level.") + } + networkLog(logger) { + "Writing failed login response to channel '${ctx.channel()}': $response" + } + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE) + } + + private companion object { + /** + * The offset applied to the decode ISAAC stream cipher seed. + */ + private const val DECODE_SEED_OFFSET: Int = 50 + private val logger: InlineLogger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginChannelHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginChannelHandler.kt new file mode 100644 index 000000000..cb87c6da4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginChannelHandler.kt @@ -0,0 +1,221 @@ +package net.rsprot.protocol.api.login + +import com.github.michaelbull.logging.InlineLogger +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleStateEvent +import io.netty.handler.timeout.IdleStateHandler +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.channel.inetAddress +import net.rsprot.protocol.api.channel.replace +import net.rsprot.protocol.api.js5.Js5ChannelHandler +import net.rsprot.protocol.api.js5.Js5MessageDecoder +import net.rsprot.protocol.api.js5.Js5MessageEncoder +import net.rsprot.protocol.api.logging.networkLog +import net.rsprot.protocol.loginprot.incoming.InitGameConnection +import net.rsprot.protocol.loginprot.incoming.InitJs5RemoteConnection +import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import net.rsprot.protocol.message.IncomingLoginMessage +import java.text.NumberFormat +import java.util.concurrent.TimeUnit + +/** + * The channel handler for login channels, essentially the very first requests that will + * come in from the client, pointing to either JS5 or the game. + */ +@Suppress("DuplicatedCode") +public class LoginChannelHandler( + public val networkService: NetworkService<*>, +) : SimpleChannelInboundHandler(IncomingLoginMessage::class.java) { + override fun channelActive(ctx: ChannelHandlerContext) { + ctx.read() + networkLog(logger) { + "Channel is now active: ${ctx.channel()}" + } + } + + override fun channelRead0( + ctx: ChannelHandlerContext, + msg: IncomingLoginMessage, + ) { + networkLog(logger) { + "Login channel message in channel '${ctx.channel()}': $msg" + } + when (msg) { + InitGameConnection -> { + handleInitGameConnection(ctx) + } + is InitJs5RemoteConnection -> { + handleInitJs5RemoteConnection(ctx, msg.revision, msg.seed) + } + // TODO: Unknown, SSL web + else -> { + throw IllegalStateException("Unknown login channel message: $msg") + } + } + } + + private fun handleInitGameConnection(ctx: ChannelHandlerContext) { + val address = ctx.inetAddress() + val count = + networkService + .iNetAddressHandlers + .gameInetAddressTracker + .getCount(address) + val accepted = + networkService + .iNetAddressHandlers + .inetAddressValidator + .acceptGameConnection(address, count) + if (!accepted) { + networkLog(logger) { + "INetAddressValidator rejected game connection for channel ${ctx.channel()}" + } + ctx + .write(LoginResponse.TooManyAttempts) + .addListener(ChannelFutureListener.CLOSE) + return + } + val sessionId = + networkService + .loginHandlers + .sessionIdGenerator + .generate(address) + networkLog(logger) { + "Game connection accepted with session id: ${NumberFormat.getNumberInstance().format(sessionId)}" + } + ctx + .write(LoginResponse.Successful(sessionId)) + .addListener( + ChannelFutureListener { future -> + if (!future.isSuccess) { + networkLog(logger) { + "Failed to write a successful game connection response to channel ${ctx.channel()}" + } + future.channel().pipeline().fireExceptionCaught(future.cause()) + future.channel().close() + return@ChannelFutureListener + } + // Extra validation to ensure we don't get any weird scenarios where it's stuck in memory + if (ctx.channel().isActive) { + networkLog(logger) { + "Tracking game INetAddress for channel '${future.channel()}': $address" + } + networkService + .iNetAddressHandlers + .gameInetAddressTracker + .register(address) + } + future + .channel() + .pipeline() + .replace(LoginConnectionHandler(networkService, sessionId)) + }, + ) + } + + private fun handleInitJs5RemoteConnection( + ctx: ChannelHandlerContext, + revision: Int, + seed: IntArray, + ) { + if (revision != NetworkService.REVISION) { + networkLog(logger) { + "Invalid JS5 revision received from channel '${ctx.channel()}': $revision" + } + ctx + .write(LoginResponse.ClientOutOfDate) + .addListener(ChannelFutureListener.CLOSE) + return + } + val address = ctx.inetAddress() + val count = + networkService + .iNetAddressHandlers + .js5InetAddressTracker + .getCount(address) + val accepted = + networkService + .iNetAddressHandlers + .inetAddressValidator + .acceptJs5Connection(address, count, seed) + if (!accepted) { + networkLog(logger) { + "INetAddressValidator rejected JS5 connection for channel ${ctx.channel()}" + } + ctx + .write(LoginResponse.IPLimit) + .addListener(ChannelFutureListener.CLOSE) + return + } + ctx + .write(LoginResponse.Successful(null)) + .addListener( + ChannelFutureListener { future -> + if (!future.isSuccess) { + networkLog(logger) { + "Failed to write a successful JS5 connection response to channel ${ctx.channel()}" + } + future.channel().pipeline().fireExceptionCaught(future.cause()) + future.channel().close() + return@ChannelFutureListener + } + // Extra validation to ensure we don't get any weird scenarios where it's stuck in memory + if (ctx.channel().isActive) { + networkLog(logger) { + "Tracking JS5 INetAddress for channel '${future.channel()}': $address" + } + networkService + .iNetAddressHandlers + .js5InetAddressTracker + .register(address) + } + val pipeline = ctx.channel().pipeline() + pipeline.replace(Js5MessageDecoder(networkService)) + pipeline.replace(Js5MessageEncoder(networkService)) + pipeline.replace(Js5ChannelHandler(networkService)) + pipeline.replace( + IdleStateHandler( + true, + NetworkService.JS5_TIMEOUT_SECONDS, + NetworkService.JS5_TIMEOUT_SECONDS, + NetworkService.JS5_TIMEOUT_SECONDS, + TimeUnit.SECONDS, + ), + ) + }, + ) + } + + override fun channelReadComplete(ctx: ChannelHandlerContext) { + ctx.flush() + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun exceptionCaught( + ctx: ChannelHandlerContext, + cause: Throwable, + ) { + networkService + .exceptionHandlers + .channelExceptionHandler + .exceptionCaught(ctx, cause) + } + + override fun userEventTriggered( + ctx: ChannelHandlerContext, + evt: Any, + ) { + if (evt is IdleStateEvent) { + networkLog(logger) { + "Login channel has gone idle, closing channel ${ctx.channel()}" + } + ctx.close() + } + } + + private companion object { + private val logger: InlineLogger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt new file mode 100644 index 000000000..687035f14 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginConnectionHandler.kt @@ -0,0 +1,364 @@ +package net.rsprot.protocol.api.login + +import com.github.michaelbull.logging.InlineLogger +import io.netty.channel.ChannelFutureListener +import io.netty.channel.ChannelHandlerContext +import io.netty.channel.SimpleChannelInboundHandler +import io.netty.handler.timeout.IdleStateEvent +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.channel.inetAddress +import net.rsprot.protocol.api.logging.networkLog +import net.rsprot.protocol.loginprot.incoming.GameLogin +import net.rsprot.protocol.loginprot.incoming.GameReconnect +import net.rsprot.protocol.loginprot.incoming.ProofOfWorkReply +import net.rsprot.protocol.loginprot.incoming.RemainingBetaArchives +import net.rsprot.protocol.loginprot.incoming.pow.ProofOfWork +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaData +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeType +import net.rsprot.protocol.loginprot.incoming.util.LoginBlock +import net.rsprot.protocol.loginprot.incoming.util.LoginBlockDecodingFunction +import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import net.rsprot.protocol.message.IncomingLoginMessage +import java.text.NumberFormat +import java.util.concurrent.CompletableFuture + +/** + * The login connection handler, responsible for handling any game connections. + * @property sessionId the session id that was originally generated and written, + * expected to receive the same session id back from the client in the login block. + */ +@Suppress("DuplicatedCode") +public class LoginConnectionHandler( + private val networkService: NetworkService, + private val sessionId: Long, +) : SimpleChannelInboundHandler(IncomingLoginMessage::class.java) { + private var loginState: LoginState = LoginState.UNINITIALIZED + private lateinit var loginPacket: IncomingLoginMessage + private lateinit var proofOfWork: ProofOfWork<*, *> + + override fun handlerAdded(ctx: ChannelHandlerContext) { + ctx.read() + } + + override fun channelActive(ctx: ChannelHandlerContext) { + networkService + .iNetAddressHandlers + .gameInetAddressTracker + .register(ctx.inetAddress()) + networkLog(logger) { + "Channel is now active: ${ctx.channel()}" + } + } + + override fun channelInactive(ctx: ChannelHandlerContext) { + networkService + .iNetAddressHandlers + .gameInetAddressTracker + .deregister(ctx.inetAddress()) + networkLog(logger) { + "Channel is now inactive: ${ctx.channel()}" + } + } + + override fun channelUnregistered(ctx: ChannelHandlerContext) { + // If the channel is unregistered, we must release the login block buffer + if (this.loginState == LoginState.REQUESTED_PROOF_OF_WORK) { + releaseLoginBlock() + } + } + + /** + * Release the login block buffer that was supposed to be decoded after a successful + * proof of work response. + */ + private fun releaseLoginBlock() { + // If login block isn't initialized yet, do nothing + if (!this::loginPacket.isInitialized) { + return + } + val jagBuffer = + when (val packet = this.loginPacket) { + is GameLogin -> packet.buffer + is GameReconnect -> packet.buffer + else -> return + } + val buffer = jagBuffer.buffer + val refCnt = buffer.refCnt() + if (refCnt > 0) { + buffer.release(refCnt) + } + } + + override fun channelRead0( + ctx: ChannelHandlerContext, + msg: IncomingLoginMessage, + ) { + networkLog(logger) { + "Login connection message in channel '${ctx.channel()}': $msg" + } + when (msg) { + is RemainingBetaArchives -> { + if (this.loginState != LoginState.AWAITING_BETA_RESPONSE) { + ctx.close() + return + } + decodeLoginPacket(ctx, msg) + } + is GameLogin -> { + if (this.loginState != LoginState.UNINITIALIZED) { + ctx.close() + return + } + this.loginPacket = msg + requestProofOfWork(ctx) + } + + is GameReconnect -> { + this.loginPacket = msg + continueLogin(ctx) + } + + is ProofOfWorkReply -> { + if (loginState != LoginState.REQUESTED_PROOF_OF_WORK) { + ctx.close() + return + } + val pow = this.proofOfWork + verifyProofOfWork(pow, msg.result).handle { success, exception -> + if (success != true) { + networkLog(logger) { + "Incorrect proof of work response received from " + + "channel '${ctx.channel()}': ${msg.result}, challenge was: $pow" + } + ctx.writeAndFlush(LoginResponse.LoginFail1).addListener(ChannelFutureListener.CLOSE) + return@handle + } + if (exception != null) { + logger.error(exception) { + "Exception during proof of work verification " + + "from channel '${ctx.channel()}': $exception" + } + ctx.writeAndFlush(LoginResponse.LoginFail1).addListener(ChannelFutureListener.CLOSE) + } + networkLog(logger) { + "Correct proof of work response received from channel '${ctx.channel()}': ${msg.result}" + } + continueLogin(ctx) + } + } + else -> { + throw IllegalStateException("Unknown login connection handler") + } + } + } + + private fun requestProofOfWork(ctx: ChannelHandlerContext) { + val pow = + networkService + .loginHandlers + .proofOfWorkProvider + .provide(ctx.inetAddress()) + ?: return continueLogin(ctx) + loginState = LoginState.REQUESTED_PROOF_OF_WORK + this.proofOfWork = pow + ctx.writeAndFlush(LoginResponse.ProofOfWork(pow)).addListener( + ChannelFutureListener { future -> + if (!future.isSuccess) { + networkLog(logger) { + "Failed to write a successful proof of work request to channel ${ctx.channel()}" + } + future.channel().pipeline().fireExceptionCaught(future.cause()) + future.channel().close() + return@ChannelFutureListener + } + ctx.read() + }, + ) + } + + private fun continueLogin(ctx: ChannelHandlerContext) { + if (networkService.betaWorld) { + loginState = LoginState.AWAITING_BETA_RESPONSE + // Instantly request the remaining beta archives, as that feature + // is implemented incorrectly and serves no functional purpose + ctx + .writeAndFlush(ctx.alloc().buffer(1).writeByte(2)) + .addListener( + ChannelFutureListener { future -> + if (!future.isSuccess) { + networkLog(logger) { + "Failed to write beta crc request to channel ${ctx.channel()}" + } + future.channel().pipeline().fireExceptionCaught(future.cause()) + future.channel().close() + return@ChannelFutureListener + } + ctx.read() + }, + ) + } else { + decodeLoginPacket(ctx, null) + } + } + + override fun channelReadComplete(ctx: ChannelHandlerContext) { + ctx.flush() + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun exceptionCaught( + ctx: ChannelHandlerContext, + cause: Throwable, + ) { + networkService + .exceptionHandlers + .channelExceptionHandler + .exceptionCaught(ctx, cause) + } + + override fun userEventTriggered( + ctx: ChannelHandlerContext, + evt: Any, + ) { + if (evt is IdleStateEvent) { + networkLog(logger) { + "Login connection has gone idle, closing channel ${ctx.channel()}" + } + ctx.close() + } + } + + private fun decodeLoginPacket( + ctx: ChannelHandlerContext, + remainingBetaArchives: RemainingBetaArchives?, + ) { + val responseHandler = GameLoginResponseHandler(networkService, ctx) + when (val packet = loginPacket) { + is GameLogin -> { + decodeGameLoginBuffer(packet, ctx, remainingBetaArchives, responseHandler) + } + + is GameReconnect -> { + decodeGameReconnectBuffer(packet, ctx, remainingBetaArchives, responseHandler) + } + + else -> { + throw IllegalStateException("Unknown login packet: $packet") + } + } + } + + private fun decodeGameLoginBuffer( + packet: GameLogin, + ctx: ChannelHandlerContext, + remainingBetaArchives: RemainingBetaArchives?, + responseHandler: GameLoginResponseHandler, + ) { + decodeLogin( + packet.buffer, + networkService.betaWorld, + packet.decoder, + ).handle { block, exception -> + if (block == null || exception != null) { + networkService + .exceptionHandlers + .channelExceptionHandler + .exceptionCaught(ctx, exception) + return@handle + } + if (sessionId != block.sessionId) { + networkLog(logger) { + "Mismatching game login session id received from channel " + + "'${ctx.channel()}': ${NumberFormat.getNumberInstance().format(block.sessionId)}, " + + "expected value: ${NumberFormat.getNumberInstance().format(sessionId)}" + } + ctx + .writeAndFlush(LoginResponse.InvalidLoginPacket) + .addListener(ChannelFutureListener.CLOSE) + return@handle + } + if (remainingBetaArchives != null) { + block.mergeBetaCrcs(remainingBetaArchives) + } + networkLog(logger) { + "Successful game login from channel '${ctx.channel()}': $block" + } + networkService.gameConnectionHandler.onLogin(responseHandler, block) + } + } + + private fun decodeGameReconnectBuffer( + packet: GameReconnect, + ctx: ChannelHandlerContext, + remainingBetaArchives: RemainingBetaArchives?, + responseHandler: GameLoginResponseHandler, + ) { + decodeLogin( + packet.buffer, + networkService.betaWorld, + packet.decoder, + ).handle { block, exception -> + if (block == null || exception != null) { + logger.error(exception) { + "Failed to decode game reconnect block for channel ${ctx.channel()}" + } + ctx + .writeAndFlush(LoginResponse.LoginFail2) + .addListener(ChannelFutureListener.CLOSE) + return@handle + } + if (sessionId != block.sessionId) { + networkLog(logger) { + "Mismatching reconnect session id received from channel " + + "'${ctx.channel()}': ${NumberFormat.getNumberInstance().format(block.sessionId)}, " + + "expected value: ${NumberFormat.getNumberInstance().format(sessionId)}" + } + ctx + .writeAndFlush(LoginResponse.InvalidLoginPacket) + .addListener(ChannelFutureListener.CLOSE) + return@handle + } + if (remainingBetaArchives != null) { + block.mergeBetaCrcs(remainingBetaArchives) + } + networkLog(logger) { + "Successful game reconnection from channel '${ctx.channel()}': $block" + } + networkService.gameConnectionHandler.onReconnect(responseHandler, block) + } + } + + private fun decodeLogin( + buf: JagByteBuf, + betaWorld: Boolean, + function: LoginBlockDecodingFunction, + ): CompletableFuture> = + networkService + .loginHandlers + .loginDecoderService + .decode(buf, betaWorld, function) + + private fun , MetaData : ChallengeMetaData> verifyProofOfWork( + pow: ProofOfWork, + result: Long, + ): CompletableFuture = + networkService + .loginHandlers + .proofOfWorkChallengeWorker + .verify( + result, + pow.challengeType, + pow.challengeVerifier, + ) + + private enum class LoginState { + UNINITIALIZED, + REQUESTED_PROOF_OF_WORK, + AWAITING_BETA_RESPONSE, + } + + private companion object { + private val logger: InlineLogger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginMessageDecoder.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginMessageDecoder.kt new file mode 100644 index 000000000..fe3885f6e --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginMessageDecoder.kt @@ -0,0 +1,56 @@ +package net.rsprot.protocol.api.login + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBuf +import io.netty.channel.ChannelHandlerContext +import net.rsprot.buffer.extensions.g1 +import net.rsprot.crypto.cipher.NopStreamCipher +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.decoder.IncomingMessageDecoder +import net.rsprot.protocol.api.logging.networkLog +import net.rsprot.protocol.message.codec.incoming.MessageDecoderRepository + +/** + * The decoder for any login messages. + */ +public class LoginMessageDecoder( + public val networkService: NetworkService<*>, +) : IncomingMessageDecoder() { + override val decoders: MessageDecoderRepository = + networkService + .decoderRepositories + .loginMessageDecoderRepository + override val streamCipher: StreamCipher = NopStreamCipher + + override fun readOpcode( + ctx: ChannelHandlerContext, + input: ByteBuf, + ) { + if (!networkService.loginHandlers.suppressInvalidLoginProts) { + return super.readOpcode(ctx, input) + } + this.opcode = (input.g1() - streamCipher.nextInt()) and 0xFF + val decoder = decoders.getDecoderOrNull(opcode) + if (decoder == null) { + networkLog(logger) { + "Invalid login packet from channel ${ctx.channel()}': ${this.opcode}" + } + ctx.close() + return + } + this.decoder = decoder + this.length = this.decoder.prot.size + state = + if (this.length >= 0) { + State.READ_PAYLOAD + } else { + State.READ_LENGTH + } + } + + private companion object { + private val logger: InlineLogger = InlineLogger() + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginMessageEncoder.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginMessageEncoder.kt new file mode 100644 index 000000000..6ebeff334 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/login/LoginMessageEncoder.kt @@ -0,0 +1,19 @@ +package net.rsprot.protocol.api.login + +import net.rsprot.crypto.cipher.NopStreamCipher +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.api.NetworkService +import net.rsprot.protocol.api.encoder.OutgoingMessageEncoder +import net.rsprot.protocol.message.codec.outgoing.MessageEncoderRepository + +/** + * The encoder for any login messages. + */ +public class LoginMessageEncoder( + public val networkService: NetworkService<*>, +) : OutgoingMessageEncoder() { + override val cipher: StreamCipher = NopStreamCipher + override val repository: MessageEncoderRepository<*> = + networkService.encoderRepositories.loginMessageDecoderRepository + override val validate: Boolean = false +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/repositories/MessageDecoderRepositories.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/repositories/MessageDecoderRepositories.kt new file mode 100644 index 000000000..db87ef78b --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/repositories/MessageDecoderRepositories.kt @@ -0,0 +1,58 @@ +package net.rsprot.protocol.api.repositories + +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.crypto.rsa.RsaKeyPair +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.client.OldSchoolClientType.DESKTOP +import net.rsprot.protocol.common.js5.incoming.prot.Js5MessageDecoderRepository +import net.rsprot.protocol.common.loginprot.incoming.prot.LoginMessageDecoderRepository +import net.rsprot.protocol.game.incoming.prot.DesktopGameMessageDecoderRepository +import net.rsprot.protocol.message.codec.incoming.MessageDecoderRepository +import java.math.BigInteger + +/** + * The message decoder repositories for login, JS5 and game, all held in the same place. + */ +@OptIn(ExperimentalStdlibApi::class) +public class MessageDecoderRepositories private constructor( + public val loginMessageDecoderRepository: MessageDecoderRepository, + public val js5MessageDecoderRepository: MessageDecoderRepository, + public val gameMessageDecoderRepositories: ClientTypeMap>, +) { + public constructor( + exp: BigInteger, + mod: BigInteger, + gameMessageDecoderRepositories: ClientTypeMap>, + ) : this( + LoginMessageDecoderRepository.build(exp, mod), + Js5MessageDecoderRepository.build(), + gameMessageDecoderRepositories, + ) + + internal companion object { + fun initialize( + clientTypes: List, + rsaKeyPair: RsaKeyPair, + huffmanCodecProvider: HuffmanCodecProvider, + ): MessageDecoderRepositories { + val repositories = + buildList { + if (DESKTOP in clientTypes) { + add(DESKTOP to DesktopGameMessageDecoderRepository.build(huffmanCodecProvider)) + } + } + val clientTypeMap = + ClientTypeMap.of( + OldSchoolClientType.COUNT, + repositories, + ) + return MessageDecoderRepositories( + rsaKeyPair.exponent, + rsaKeyPair.modulus, + clientTypeMap, + ) + } + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/repositories/MessageEncoderRepositories.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/repositories/MessageEncoderRepositories.kt new file mode 100644 index 000000000..24516a4da --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/repositories/MessageEncoderRepositories.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.api.repositories + +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.js5.outgoing.prot.Js5MessageEncoderRepository +import net.rsprot.protocol.common.loginprot.outgoing.prot.LoginMessageEncoderRepository +import net.rsprot.protocol.game.outgoing.prot.DesktopGameMessageEncoderRepository +import net.rsprot.protocol.message.codec.outgoing.MessageEncoderRepository + +/** + * The message encoder repository for all outgoing messages, for JS5, login and game. + */ +@OptIn(ExperimentalStdlibApi::class) +public class MessageEncoderRepositories private constructor( + public val loginMessageDecoderRepository: MessageEncoderRepository, + public val js5MessageDecoderRepository: MessageEncoderRepository, + public val gameMessageDecoderRepositories: ClientTypeMap>, +) { + public constructor( + huffmanCodecProvider: HuffmanCodecProvider, + ) : this( + LoginMessageEncoderRepository.build(), + Js5MessageEncoderRepository.build(), + ClientTypeMap.of( + OldSchoolClientType.COUNT, + listOf(OldSchoolClientType.DESKTOP to DesktopGameMessageEncoderRepository.build(huffmanCodecProvider)), + ), + ) +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/NpcInfoSupplier.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/NpcInfoSupplier.kt new file mode 100644 index 000000000..bc2beb7d8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/NpcInfoSupplier.kt @@ -0,0 +1,74 @@ +package net.rsprot.protocol.api.suppliers + +import com.github.michaelbull.logging.InlineLogger +import net.rsprot.protocol.game.outgoing.info.filter.DefaultExtendedInfoFilter +import net.rsprot.protocol.game.outgoing.info.filter.ExtendedInfoFilter +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarExceptionHandler +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcIndexSupplier +import net.rsprot.protocol.game.outgoing.info.worker.DefaultProtocolWorker +import net.rsprot.protocol.game.outgoing.info.worker.ProtocolWorker + +/** + * The supplier for NPC info protocol, allowing the construction of the protocol and its + * correct use. + * @property npcIndexSupplier the supplier for NPC indices, allowing the protocol + * to determine what NPCs need to be added to the high resolution view. + * The server is expected to return all NPCs, even ones that are already tracked as + * the server has no way of determining what is already tracked. + * @property npcAvatarExceptionHandler the exception handler for NPC avatars, + * catching any exceptions that happen during pre-computations of NPC avatar blocks. + * @property npcExtendedInfoFilter the filter for NPC extended info blocks, responsible + * for ensuring that the NPC info packet never exceeds the 40 kilobyte limit. + * @property npcInfoProtocolWorker the worker behind the NPC info protocol, responsible + * for executing the underlying tasks, either on a single thread or a thread pool. + */ +public class NpcInfoSupplier + @JvmOverloads + public constructor( + public val npcIndexSupplier: NpcIndexSupplier = + NpcIndexSupplier { _, _, _, _, _ -> + emptySequence().iterator() + }, + public val npcAvatarExceptionHandler: NpcAvatarExceptionHandler = + NpcAvatarExceptionHandler { index, exception -> + logger.error(exception) { + "Exception in processing npc avatar for npc $index" + } + }, + public val npcExtendedInfoFilter: ExtendedInfoFilter = DefaultExtendedInfoFilter(), + public val npcInfoProtocolWorker: ProtocolWorker = DefaultProtocolWorker(), + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NpcInfoSupplier + + if (npcIndexSupplier != other.npcIndexSupplier) return false + if (npcExtendedInfoFilter != other.npcExtendedInfoFilter) return false + if (npcInfoProtocolWorker != other.npcInfoProtocolWorker) return false + if (npcAvatarExceptionHandler != other.npcAvatarExceptionHandler) return false + + return true + } + + override fun hashCode(): Int { + var result = npcIndexSupplier.hashCode() + result = 31 * result + npcExtendedInfoFilter.hashCode() + result = 31 * result + npcInfoProtocolWorker.hashCode() + result = 31 * result + npcAvatarExceptionHandler.hashCode() + return result + } + + override fun toString(): String = + "NpcInfoSupplier(" + + "npcIndexSupplier=$npcIndexSupplier, " + + "npcAvatarExceptionHandler=$npcAvatarExceptionHandler, " + + "npcExtendedInfoFilter=$npcExtendedInfoFilter, " + + "npcInfoProtocolWorker=$npcInfoProtocolWorker" + + ")" + + private companion object { + private val logger = InlineLogger() + } + } diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/PlayerInfoSupplier.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/PlayerInfoSupplier.kt new file mode 100644 index 000000000..570f780e5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/PlayerInfoSupplier.kt @@ -0,0 +1,43 @@ +package net.rsprot.protocol.api.suppliers + +import net.rsprot.protocol.game.outgoing.info.filter.DefaultExtendedInfoFilter +import net.rsprot.protocol.game.outgoing.info.filter.ExtendedInfoFilter +import net.rsprot.protocol.game.outgoing.info.worker.DefaultProtocolWorker +import net.rsprot.protocol.game.outgoing.info.worker.ProtocolWorker + +/** + * A supplier for the player info protocol. + * @property playerExtendedInfoFilter the extended info filter responsible for ensuring that + * the player info packet never exceeds the 40 kilobyte limitation. + * @property playerInfoProtocolWorker the worker behind the player info protocol. + */ +public class PlayerInfoSupplier + @JvmOverloads + public constructor( + public val playerExtendedInfoFilter: ExtendedInfoFilter = DefaultExtendedInfoFilter(), + public val playerInfoProtocolWorker: ProtocolWorker = DefaultProtocolWorker(), + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlayerInfoSupplier + + if (playerExtendedInfoFilter != other.playerExtendedInfoFilter) return false + if (playerInfoProtocolWorker != other.playerInfoProtocolWorker) return false + + return true + } + + override fun hashCode(): Int { + var result = playerExtendedInfoFilter.hashCode() + result = 31 * result + playerInfoProtocolWorker.hashCode() + return result + } + + override fun toString(): String = + "PlayerInfoSupplier(" + + "playerExtendedInfoFilter=$playerExtendedInfoFilter, " + + "playerInfoProtocolWorker=$playerInfoProtocolWorker" + + ")" + } diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/WorldEntityInfoSupplier.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/WorldEntityInfoSupplier.kt new file mode 100644 index 000000000..2aae06641 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/suppliers/WorldEntityInfoSupplier.kt @@ -0,0 +1,39 @@ +package net.rsprot.protocol.api.suppliers + +import com.github.michaelbull.logging.InlineLogger +import net.rsprot.protocol.game.outgoing.info.worker.DefaultProtocolWorker +import net.rsprot.protocol.game.outgoing.info.worker.ProtocolWorker +import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityAvatarExceptionHandler +import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityIndexSupplier + +/** + * The supplier for world entity info protocol, allowing the construction of the protocol and its + * correct use. + * @property worldEntityIndexSupplier the supplier for world entity indices, allowing the protocol + * to determine what world entities need to be added to the high resolution view. + * The server is expected to return all world entities, even ones that are already tracked as + * the server has no way of determining what is already tracked. + * @property worldEntityInfoProtocolWorker the worker behind the world entity info protocol, responsible + * for executing the underlying tasks, either on a single thread or a thread pool. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class WorldEntityInfoSupplier + @JvmOverloads + public constructor( + public val worldEntityIndexSupplier: WorldEntityIndexSupplier = + WorldEntityIndexSupplier { _, _, _, _, _ -> + emptySequence() + .iterator() + }, + public val worldEntityAvatarExceptionHandler: WorldEntityAvatarExceptionHandler = + WorldEntityAvatarExceptionHandler { index, exception -> + logger.error(exception) { + "Exception in world entity avatar processing for index $index" + } + }, + public val worldEntityInfoProtocolWorker: ProtocolWorker = DefaultProtocolWorker(), + ) { + private companion object { + private val logger = InlineLogger() + } + } diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/util/FutureExtensions.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/util/FutureExtensions.kt new file mode 100644 index 000000000..d01e8d00f --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/util/FutureExtensions.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.api.util + +import io.netty.util.concurrent.Future +import java.util.concurrent.CompletableFuture + +/** + * Turns a normal Netty future object into a completable future, allowing + * for easier use of it. + */ +public fun Future.asCompletableFuture(): CompletableFuture { + if (isDone) { + return if (isSuccess) { + CompletableFuture.completedFuture(now) + } else { + CompletableFuture.failedFuture(cause()) + } + } + + val future = CompletableFuture() + + addListener { + if (isSuccess) { + future.complete(now) + } else { + future.completeExceptionally(cause()) + } + } + + return future +} diff --git a/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/util/ZonePartialEnclosedCacheBuffer.kt b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/util/ZonePartialEnclosedCacheBuffer.kt new file mode 100644 index 000000000..6141e6d02 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/main/kotlin/net/rsprot/protocol/api/util/ZonePartialEnclosedCacheBuffer.kt @@ -0,0 +1,187 @@ +package net.rsprot.protocol.api.util + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import io.netty.buffer.PooledByteBufAllocator +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.game.outgoing.codec.zone.header.DesktopUpdateZonePartialEnclosedEncoder +import net.rsprot.protocol.message.ZoneProt +import net.rsprot.protocol.message.codec.UpdateZonePartialEnclosedCache +import java.util.EnumMap +import java.util.LinkedList + +public class ZonePartialEnclosedCacheBuffer( + public val supportedClients: List = OldSchoolClientType.entries, + private val byteBufAllocator: ByteBufAllocator = PooledByteBufAllocator.DEFAULT, + internal var activeCachedBuffers: LinkedList = LinkedList(), +) { + /** + * Contains lists of buffers from [computeZone] calls that were unable to be released due to their [ByteBuf.refCnt]. + */ + internal val retainedBufferReferences = ArrayDeque>() + + /** + * The amount of [computeZone] calls that have been made before calling [releaseBuffers]. In other words, tracks + * the amount of zones that have been computed in a single tick. (or multiple ticks if the consumer does not + * properly call [releaseBuffers]) + */ + internal var currentZoneComputationCount: Int = 0 + + /** + * Computes the expected [net.rsprot.protocol.game.outgoing.zone.header.UpdateZonePartialEnclosed.payload] for each + * client type in [supportedClients] and returns them in map with the [OldSchoolClientType] as key and payload byte + * buffer as its value. + * + * The [pendingTickProtList] parameter should be a list of zone prot events that occurred during the _current world + * cycle_. It **should not** include prots from previous cycles, such as `LocAddChange` and `ObjAdd` from previously + * spawned locs or objs, respectively. + * + * Zone partial enclosed can include the following, when applicable: + * - [net.rsprot.protocol.game.outgoing.zone.payload.LocAddChange] + * - [net.rsprot.protocol.game.outgoing.zone.payload.LocAnim] + * - [net.rsprot.protocol.game.outgoing.zone.payload.LocDel] + * - [net.rsprot.protocol.game.outgoing.zone.payload.LocMerge] + * - [net.rsprot.protocol.game.outgoing.zone.payload.MapAnim] + * - [net.rsprot.protocol.game.outgoing.zone.payload.MapProjAnim] + * - [net.rsprot.protocol.game.outgoing.zone.payload.ObjAdd]: *Only for "publicly-visible" objs* + * - [net.rsprot.protocol.game.outgoing.zone.payload.ObjDel]: *Only for "publicly-visible" objs* + * - [net.rsprot.protocol.game.outgoing.zone.payload.ObjEnabledOps] + * - [net.rsprot.protocol.game.outgoing.zone.payload.SoundArea] + */ + public fun computeZone(pendingTickProtList: List): EnumMap { + val clientBuffers = buildZoneProtBuffers(pendingTickProtList) + activeCachedBuffers += clientBuffers.values + incrementZoneComputationCount() + return clientBuffers + } + + private fun buildZoneProtBuffers(protList: List): EnumMap { + val map = createClientBufferEnumMap() + for (client in supportedClients) { + val encoder = supportedEncoders[client] + val buffer = encoder.buildCache(byteBufAllocator, protList) + map[client] = buffer + } + return map + } + + private fun incrementZoneComputationCount() { + currentZoneComputationCount++ + logPossibleLeak() + } + + private fun logPossibleLeak() { + if (currentZoneComputationCount < ZONE_COUNT_BEFORE_LEAK_WARNING) { + return + } + logger.warn { "Update zone partial enclosed buffers have not been correctly released!" } + } + + /** + * Releases all prebuilt zone partial enclosed buffers that no longer have active references, indicating that all + * Netty channels have finished writing these buffers. This method also handles buffers that could not be + * immediately released due to their reference count ([ByteBuf.refCnt]). + * + * Under typical conditions, the encoder should trigger the buffer release within a single cycle. However, if a + * buffer remains unreleased due to a session closing or other interruptions, this method ensures they are + * handled correctly. Implementation details on this mechanism can be seen in [releaseBuffersOnThreshold]. + * + * **Usage Note:** This function should be invoked **once** at the end of **every tick** to ensure proper buffer + * cleanup and prevent possible memory leaks. + */ + public fun releaseBuffers() { + resetComputationCount() + releaseBuffersOnThreshold() + retainActiveBufferReferences() + releaseRetainedBuffers() + clearEmptyRetainedBuffers() + } + + private fun resetComputationCount() { + currentZoneComputationCount = 0 + } + + /** + * Checks and forcibly releases retained buffers if the number of unreleased buffers exceeds a predefined threshold. + * + * - **Periodic Forcible Release**: If the total number of retained buffers that could not be released reaches + * 100 ([BUF_RETENTION_COUNT_BEFORE_RELEASE]), this method will begin to forcibly release these buffers during + * each [releaseBuffers] call to prevent memory leaks. + * + * This mechanism is a safeguard to ensure that buffers are eventually released even in cases where they were not + * properly released due to reference count issues. If this mechanism is triggered, it suggests a deeper + * underlying issue. + */ + private fun releaseBuffersOnThreshold() { + if (retainedBufferReferences.size >= BUF_RETENTION_COUNT_BEFORE_RELEASE) { + val releaseTarget = retainedBufferReferences.removeFirst() + releaseBuffers(releaseTarget, true) + } + } + + private fun retainActiveBufferReferences() { + if (activeCachedBuffers.isNotEmpty()) { + retainedBufferReferences.addLast(activeCachedBuffers) + activeCachedBuffers = LinkedList() + } + } + + private fun releaseRetainedBuffers() { + for (buffers in retainedBufferReferences) { + releaseBuffers(buffers, false) + } + } + + private fun clearEmptyRetainedBuffers() { + retainedBufferReferences.removeIf { it.isEmpty() } + } + + internal companion object { + private const val ZONE_COUNT_BEFORE_LEAK_WARNING: Int = 25_000 + internal const val BUF_RETENTION_COUNT_BEFORE_RELEASE: Int = 100 + + private val supportedEncoders = createEncoderMap() + + private val logger = InlineLogger() + + private fun createClientBufferEnumMap(): EnumMap = + EnumMap(OldSchoolClientType::class.java) + + internal fun createEncoderMap(): ClientTypeMap { + val list = mutableListOf>() + list += OldSchoolClientType.DESKTOP to DesktopUpdateZonePartialEnclosedEncoder + return ClientTypeMap.of(OldSchoolClientType.COUNT, list) + } + + private fun releaseBuffers( + buffers: LinkedList, + forceRelease: Boolean, + ) { + if (buffers.isEmpty()) { + return + } + val iterator = buffers.iterator() + while (iterator.hasNext()) { + val next = iterator.next() + val refCount = next.refCnt() + if (forceRelease) { + // Don't bother removing from the list if force removing, + // let the garbage collector deal with it + if (refCount > 0) { + next.release(refCount) + } + continue + } + if (refCount > 1) { + continue + } + if (refCount == 1) { + next.release() + } + iterator.remove() + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-api/src/test/kotlin/net/rsprot/protocol/api/util/ZonePartialEnclosedCacheBufferTest.kt b/protocol/osrs-225/osrs-225-api/src/test/kotlin/net/rsprot/protocol/api/util/ZonePartialEnclosedCacheBufferTest.kt new file mode 100644 index 000000000..1bcd62cd8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-api/src/test/kotlin/net/rsprot/protocol/api/util/ZonePartialEnclosedCacheBufferTest.kt @@ -0,0 +1,189 @@ +package net.rsprot.protocol.api.util + +import io.netty.buffer.Unpooled +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.game.outgoing.util.OpFlags +import net.rsprot.protocol.game.outgoing.zone.payload.LocAddChange +import net.rsprot.protocol.game.outgoing.zone.payload.LocAnim +import net.rsprot.protocol.game.outgoing.zone.payload.LocDel +import net.rsprot.protocol.game.outgoing.zone.payload.LocMerge +import net.rsprot.protocol.game.outgoing.zone.payload.MapAnim +import net.rsprot.protocol.game.outgoing.zone.payload.MapProjAnim +import net.rsprot.protocol.game.outgoing.zone.payload.ObjAdd +import net.rsprot.protocol.game.outgoing.zone.payload.ObjCount +import net.rsprot.protocol.game.outgoing.zone.payload.ObjDel +import net.rsprot.protocol.game.outgoing.zone.payload.ObjEnabledOps +import net.rsprot.protocol.game.outgoing.zone.payload.SoundArea +import net.rsprot.protocol.message.ZoneProt +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import kotlin.test.Test + +class ZonePartialEnclosedCacheBufferTest { + @Test + fun `every available oldschool client type has an associated encoder`() { + val encoders = ZonePartialEnclosedCacheBuffer.createEncoderMap() + assertEquals(OldSchoolClientType.entries.toSet(), encoders.toClientList().toSet()) + } + + @Test + fun `computeZone creates buffers for supported clients`() { + val cache = ZonePartialEnclosedCacheBuffer() + val buffers = cache.computeZone(emptyList()) + + assertEquals(buffers.keys.toSet(), cache.supportedClients.toSet()) + + // `computeZone` did not receive any zone prot to encode, so all buffers should be empty. + val expectedBuffers = buffers.map { Unpooled.wrappedBuffer(ByteArray(0)) } + assertEquals(expectedBuffers, buffers.values.toList()) + } + + @Test + fun `compute zone partial enclosed buffers`() { + val cache = ZonePartialEnclosedCacheBuffer() + + val zoneProt = createFullZoneProtList() + val buffers = cache.computeZone(zoneProt) + + // Each zone prot should _at minimum_ write their `indexedEncoder` id. (written as a byte) + // Ensuring every zone prot opcode/payload is written correctly falls out of scope for this test. + val expectedMinReadableBytes = zoneProt.size * Byte.SIZE_BYTES + for ((client, buffer) in buffers) { + assertTrue(buffer.readableBytes() >= expectedMinReadableBytes) { + "Expected `$expectedMinReadableBytes` readable bytes from " + + "buffer for client: $client. (bytes=${buffer.readableBytes()})" + } + } + + // Each supported client type should have added a buffer to `activeCachedBuffers`. + assertEquals(cache.supportedClients.size, buffers.size) + + // The leak-reference-counter should have been incremented by a single zone. + assertEquals(1, cache.currentZoneComputationCount) + } + + @Test + fun `releaseBuffers resets computation count and releases buffers correctly`() { + val cache = ZonePartialEnclosedCacheBuffer(listOf(OldSchoolClientType.DESKTOP)) + + val emptyBuffer = Unpooled.wrappedBuffer(ByteArray(0)) + cache.activeCachedBuffers += emptyBuffer + cache.currentZoneComputationCount = 1 + + cache.releaseBuffers() + + assertEquals(0, cache.activeCachedBuffers.size) + assertEquals(0, cache.currentZoneComputationCount) + assertEquals(0, cache.retainedBufferReferences.size) + } + + @Test + fun `retain buffers that cannot be released`() { + val cache = ZonePartialEnclosedCacheBuffer(listOf(OldSchoolClientType.DESKTOP)) + + val threshold = ZonePartialEnclosedCacheBuffer.BUF_RETENTION_COUNT_BEFORE_RELEASE + val retainedBuffers = (0.. + if (buffer.refCnt() > 0) { + buffer.release(buffer.refCnt()) + } + } + } + check(retainedBuffers.all { it.refCnt() == 0 }) + } + + private fun ClientTypeMap.toClientList(): List = + OldSchoolClientType.entries.filter { it in this } + + private fun createFullZoneProtList(): List = + listOf( + LocAddChange(id = 123, xInZone = 0, zInZone = 0, shape = 0, rotation = 0, OpFlags.ALL_SHOWN), + LocAnim(id = 123, xInZone = 0, zInZone = 0, shape = 0, rotation = 0), + LocDel(xInZone = 0, zInZone = 0, shape = 0, rotation = 0), + LocMerge( + index = 0, + id = 123, + xInZone = 0, + zInZone = 0, + shape = 0, + rotation = 0, + start = 0, + end = 0, + minX = 0, + minZ = 0, + maxX = 0, + maxZ = 0, + ), + MapAnim(id = 123, delay = 0, height = 0, xInZone = 0, zInZone = 0), + MapProjAnim( + id = 123, + startHeight = 0, + endHeight = 0, + startTime = 0, + endTime = 0, + angle = 0, + progress = 0, + sourceIndex = 1, + targetIndex = 1, + xInZone = 0, + zInZone = 0, + deltaX = 0, + deltaZ = 0, + ), + ObjAdd(id = 123, quantity = 0, xInZone = 0, zInZone = 0, opFlags = OpFlags.ALL_SHOWN), + ObjCount(id = 123, oldQuantity = 0, newQuantity = 0, xInZone = 0, zInZone = 0), + ObjDel(id = 123, quantity = 0, xInZone = 0, zInZone = 0), + ObjEnabledOps(id = 123, opFlags = OpFlags.ALL_SHOWN, xInZone = 0, zInZone = 0), + SoundArea(id = 123, delay = 0, loops = 0, radius = 0, size = 0, xInZone = 0, zInZone = 0), + ) +} diff --git a/protocol/osrs-225/osrs-225-common/build.gradle.kts b/protocol/osrs-225/osrs-225-common/build.gradle.kts new file mode 100644 index 000000000..b8c14e930 --- /dev/null +++ b/protocol/osrs-225/osrs-225-common/build.gradle.kts @@ -0,0 +1,13 @@ +dependencies { + api(platform(rootProject.libs.netty.bom)) + api(rootProject.libs.netty.buffer) + api(projects.protocol) +} + +mavenPublishing { + pom { + name = "RsProt OSRS 225 Common" + description = "The common module for revision 225 OldSchool RuneScape networking, offering " + + "common classes for all the modules to depend on." + } +} diff --git a/protocol/osrs-225/osrs-225-common/src/main/kotlin/net/rsprot/protocol/common/client/OldSchoolClientType.kt b/protocol/osrs-225/osrs-225-common/src/main/kotlin/net/rsprot/protocol/common/client/OldSchoolClientType.kt new file mode 100644 index 000000000..0243dc7f9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-common/src/main/kotlin/net/rsprot/protocol/common/client/OldSchoolClientType.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.common.client + +import net.rsprot.protocol.client.ClientType + +public enum class OldSchoolClientType( + override val id: Int, +) : ClientType { + /** + * The desktop clients. + * As the protocol is the same between the Java and C++ versions of desktop, + * we use the same client type for both here. + */ + DESKTOP(0), + ; + + public companion object { + /** + * The number of client types that exist. + * This number should be large enough to be used as array capacity, + * as our buffers are often cached per-client type, and we need to use + * client types as the array index. + */ + public const val COUNT: Int = 1 + } +} diff --git a/protocol/osrs-225/osrs-225-common/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/InventoryObject.kt b/protocol/osrs-225/osrs-225-common/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/InventoryObject.kt new file mode 100644 index 000000000..43e64fa02 --- /dev/null +++ b/protocol/osrs-225/osrs-225-common/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/InventoryObject.kt @@ -0,0 +1,63 @@ +package net.rsprot.protocol.common.game.outgoing.inv + +/** + * Inventory objects are a value class around the primitive 'long' + * to efficiently compress an inventory's contents into a long array. + * This allows us to avoid any garbage creation that would otherwise + * be created by making lists of objs repeatedly. + */ +@JvmInline +public value class InventoryObject( + public val packed: Long, +) { + public constructor( + slot: Int, + id: Int, + count: Int, + ) : this( + (slot.toLong() and 0xFFFF) + .or((id.toLong() and 0xFFFF) shl 16) + .or((count.toLong() and 0xFFFFFFFF) shl 32), + ) + + public constructor( + id: Int, + count: Int, + ) : this( + 0, + id, + count, + ) + + public val slot: Int + get() { + val value = (packed and 0xFFFF).toInt() + return if (value == 0xFFFF) { + -1 + } else { + value + } + } + public val id: Int + get() { + val value = (packed ushr 16 and 0xFFFF).toInt() + return if (value == 0xFFFF) { + -1 + } else { + value + } + } + public val count: Int + get() = (packed ushr 32).toInt() + + override fun toString(): String = + "InventoryObject(" + + "slot=$slot, " + + "id=$id, " + + "count=$count" + + ")" + + public companion object { + public val NULL: InventoryObject = InventoryObject(-1) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/build.gradle.kts b/protocol/osrs-225/osrs-225-desktop/build.gradle.kts new file mode 100644 index 000000000..b7271e439 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + alias(libs.plugins.jmh) + alias(libs.plugins.allopen) +} + +dependencies { + api(platform(rootProject.libs.netty.bom)) + api(rootProject.libs.netty.buffer) + api(rootProject.libs.netty.transport) + api(projects.buffer) + api(projects.compression) + api(projects.crypto) + api(projects.protocol) + api(projects.protocol.osrs225.osrs225Model) + api(projects.protocol.osrs225.osrs225Internal) + api(projects.protocol.osrs225.osrs225Common) +} + +allOpen { + annotation("org.openjdk.jmh.annotations.State") +} + +sourceSets.create("benchmarks") + +kotlin.sourceSets.getByName("benchmarks") { + dependencies { + implementation(rootProject.libs.jmh.runtime) + val mainSourceSet by sourceSets.main + val testSourceSet by sourceSets.test + val sourceSets = listOf(mainSourceSet, testSourceSet) + for (sourceSet in sourceSets) { + implementation(sourceSet.output) + implementation(sourceSet.runtimeClasspath) + } + } +} + +benchmark { + targets { + register("benchmarks") + } + + configurations { + register("PlayerInfoBenchmark") { + include("net.rsprot.protocol.game.outgoing.info.PlayerInfoBenchmark") + } + register("NpcInfoBenchmark") { + include("net.rsprot.protocol.game.outgoing.info.NpcInfoBenchmark") + } + } +} + +mavenPublishing { + pom { + name = "RsProt OSRS 225 Desktop" + description = "The desktop module for revision 225 OldSchool RuneScape networking, " + + "offering encoders and decoders for all packets." + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/benchmarks/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoBenchmark.kt b/protocol/osrs-225/osrs-225-desktop/src/benchmarks/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoBenchmark.kt new file mode 100644 index 000000000..466043d13 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/benchmarks/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoBenchmark.kt @@ -0,0 +1,184 @@ +package net.rsprot.protocol.game.outgoing.info + +import io.netty.buffer.PooledByteBufAllocator +import io.netty.buffer.Unpooled +import net.rsprot.compression.HuffmanCodec +import net.rsprot.compression.provider.DefaultHuffmanCodecProvider +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.outgoing.codec.npcinfo.DesktopLowResolutionChangeEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.writer.NpcAvatarExtendedInfoDesktopWriter +import net.rsprot.protocol.game.outgoing.info.filter.DefaultExtendedInfoFilter +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatar +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarExceptionHandler +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarFactory +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcIndexSupplier +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcInfo +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcInfoProtocol +import net.rsprot.protocol.game.outgoing.info.util.BuildArea +import net.rsprot.protocol.game.outgoing.info.worker.DefaultProtocolWorker +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Fork +import org.openjdk.jmh.annotations.Measurement +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.OutputTimeUnit +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State +import org.openjdk.jmh.annotations.Warmup +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.TimeUnit +import kotlin.random.Random + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 3, time = 10, timeUnit = TimeUnit.SECONDS) +@Fork(3) +class NpcInfoBenchmark { + private lateinit var protocol: NpcInfoProtocol + private val random: Random = Random(0) + private lateinit var serverNpcs: List + private lateinit var supplier: NpcIndexSupplier + private lateinit var localNpcInfo: NpcInfo + private lateinit var otherNpcInfos: List + private var localPlayerCoord = CoordGrid(0, 3207, 3207) + + @Setup + fun setup() { + val allocator = PooledByteBufAllocator.DEFAULT + val factory = + NpcAvatarFactory( + allocator, + DefaultExtendedInfoFilter(), + listOf(NpcAvatarExtendedInfoDesktopWriter()), + DefaultHuffmanCodecProvider(createHuffmanCodec()), + ) + this.serverNpcs = createPhantomNpcs(factory) + this.supplier = createNpcIndexSupplier() + + val encoders = + ClientTypeMap.of( + listOf(DesktopLowResolutionChangeEncoder()), + OldSchoolClientType.COUNT, + ) { + it.clientType + } + protocol = + NpcInfoProtocol( + allocator, + supplier, + encoders, + factory, + npcExceptionHandler(), + DefaultProtocolWorker(1, ForkJoinPool.commonPool()), + ) + this.localNpcInfo = protocol.alloc(1, OldSchoolClientType.DESKTOP) + otherNpcInfos = (2..2046).map { protocol.alloc(it, OldSchoolClientType.DESKTOP) } + val infos = otherNpcInfos + localNpcInfo + for (info in infos) { + info.updateCoord(NpcInfo.ROOT_WORLD, localPlayerCoord.level, localPlayerCoord.x, localPlayerCoord.z) + info.updateBuildArea( + NpcInfo.ROOT_WORLD, + BuildArea( + (localPlayerCoord.x ushr 3) - 6, + (localPlayerCoord.z ushr 3) - 6, + ), + ) + } + } + + private fun npcExceptionHandler(): NpcAvatarExceptionHandler = + NpcAvatarExceptionHandler { _, _ -> + // No-op + } + + @Benchmark + fun benchmark() { + tick() + } + + private fun tick() { + for (npc in serverNpcs) { + npc.avatar.extendedInfo.setSay("Neque porro quisquam est qui dolorem ipsum quia do") + npc.avatar.teleport( + 0, + random.nextInt(3200, 3213), + random.nextInt(3200, 3213), + true, + ) + } + protocol.update() + for (i in 1..2046) { + val info = protocol[i] + info.backingBuffer(NpcInfo.ROOT_WORLD).release() + } + for (npc in serverNpcs) { + npc.avatar.postUpdate() + } + } + + private fun createNpcIndexSupplier(): NpcIndexSupplier = + NpcIndexSupplier { _, level, x, z, viewDistance -> + serverNpcs + .asSequence() + .filter { it.coordGrid.inDistance(CoordGrid(level, x, z), viewDistance) } + .take(250) + .mapTo(ArrayList(250)) { it.index } + .iterator() + } + + private fun createPhantomNpcs(factory: NpcAvatarFactory): List { + val npcs = ArrayList(500) + for (index in 0..<500) { + val x = random.nextInt(3200, 3213) + val z = random.nextInt(3200, 3213) + val id = (index * x * z) and 0x3FFF + val coord = CoordGrid(0, x, z) + npcs += + Npc( + index, + id, + factory.alloc( + index, + id, + coord.level, + coord.x, + coord.z, + ), + ) + } + return npcs + } + + private data class Npc( + val index: Int, + val id: Int, + val avatar: NpcAvatar, + ) { + val coordGrid: CoordGrid + get() = avatar.getCoordGrid() + + override fun toString(): String = + "Npc(" + + "index=$index, " + + "id=$id, " + + "coordGrid=${avatar.getCoordGrid()}" + + ")" + } + + private companion object { + private fun NpcAvatar.getCoordGrid(): CoordGrid = CoordGrid(level(), x(), z()) + + private fun createHuffmanCodec(): HuffmanCodec { + val resource = PlayerInfoTest::class.java.getResourceAsStream("huffman.dat") + checkNotNull(resource) { + "huffman.dat could not be found" + } + return HuffmanCodec.create(Unpooled.wrappedBuffer(resource.readBytes())) + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/benchmarks/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoBenchmark.kt b/protocol/osrs-225/osrs-225-desktop/src/benchmarks/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoBenchmark.kt new file mode 100644 index 000000000..bf46960ad --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/benchmarks/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoBenchmark.kt @@ -0,0 +1,148 @@ +package net.rsprot.protocol.game.outgoing.info + +import io.netty.buffer.PooledByteBufAllocator +import io.netty.buffer.Unpooled +import net.rsprot.compression.HuffmanCodec +import net.rsprot.compression.provider.DefaultHuffmanCodecProvider +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.writer.PlayerAvatarExtendedInfoDesktopWriter +import net.rsprot.protocol.game.outgoing.info.filter.DefaultExtendedInfoFilter +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerAvatarFactory +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfo +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfoProtocol +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfoProtocol.Companion.PROTOCOL_CAPACITY +import net.rsprot.protocol.game.outgoing.info.util.BuildArea +import net.rsprot.protocol.game.outgoing.info.worker.DefaultProtocolWorker +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Fork +import org.openjdk.jmh.annotations.Measurement +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.OutputTimeUnit +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.Setup +import org.openjdk.jmh.annotations.State +import org.openjdk.jmh.annotations.Warmup +import java.util.concurrent.ForkJoinPool +import java.util.concurrent.TimeUnit +import kotlin.random.Random + +@State(Scope.Benchmark) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 3, time = 10, timeUnit = TimeUnit.SECONDS) +@Fork(3) +class PlayerInfoBenchmark { + private lateinit var protocol: PlayerInfoProtocol + private lateinit var players: Array + private val random: Random = Random(0) + + @Setup + fun setup() { + val allocator = PooledByteBufAllocator.DEFAULT + val factory = + PlayerAvatarFactory( + allocator, + DefaultExtendedInfoFilter(), + listOf(PlayerAvatarExtendedInfoDesktopWriter()), + DefaultHuffmanCodecProvider(createHuffmanCodec()), + ) + protocol = + PlayerInfoProtocol( + allocator, + DefaultProtocolWorker(Int.MAX_VALUE, ForkJoinPool.commonPool()), + factory, + ) + players = arrayOfNulls(PROTOCOL_CAPACITY) + for (i in 1.. { + override val prot: ClientProt = GameClientProt.IF_BUTTON + + override fun decode(buffer: JagByteBuf): If1Button { + val combinedId = buffer.gCombinedId() + return If1Button(combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/If3ButtonDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/If3ButtonDecoder.kt new file mode 100644 index 000000000..bb2977bc1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/If3ButtonDecoder.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.game.incoming.codec.buttons + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.game.incoming.buttons.If3Button +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent +import net.rsprot.protocol.util.gCombinedId + +@Consistent +public class If3ButtonDecoder( + override val prot: GameClientProt, + private val op: Int, +) : MessageDecoder { + override fun decode(buffer: JagByteBuf): If3Button { + val combinedId = buffer.gCombinedId() + val sub = buffer.g2() + val obj = buffer.g2() + return If3Button( + combinedId, + sub, + obj, + op, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfButtonDDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfButtonDDecoder.kt new file mode 100644 index 000000000..ae7510bd9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfButtonDDecoder.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.incoming.codec.buttons + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.buttons.IfButtonD +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.util.gCombinedIdAlt1 + +public class IfButtonDDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.IF_BUTTOND + + override fun decode(buffer: JagByteBuf): IfButtonD { + val selectedCombinedId = buffer.gCombinedIdAlt1() + val selectedObj = buffer.g2Alt1() + val selectedSub = buffer.g2Alt2() + val targetObj = buffer.g2() + val targetSub = buffer.g2Alt1() + val targetCombinedId = buffer.gCombinedIdAlt1() + return IfButtonD( + selectedCombinedId, + selectedSub, + selectedObj, + targetCombinedId, + targetSub, + targetObj, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfButtonTDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfButtonTDecoder.kt new file mode 100644 index 000000000..342dc95f0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfButtonTDecoder.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.codec.buttons + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.buttons.IfButtonT +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.util.gCombinedIdAlt2 +import net.rsprot.protocol.util.gCombinedIdAlt3 + +public class IfButtonTDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.IF_BUTTONT + + override fun decode(buffer: JagByteBuf): IfButtonT { + val targetSub = buffer.g2Alt3() + val selectedCombinedId = buffer.gCombinedIdAlt2() + val targetObj = buffer.g2Alt3() + val selectedSub = buffer.g2Alt3() + val selectedObj = buffer.g2Alt1() + val targetCombinedId = buffer.gCombinedIdAlt3() + return IfButtonT( + selectedCombinedId, + selectedSub, + selectedObj, + targetCombinedId, + targetSub, + targetObj, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfSubOpDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfSubOpDecoder.kt new file mode 100644 index 000000000..0a55cbcfd --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/buttons/IfSubOpDecoder.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.incoming.codec.buttons + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.buttons.IfSubOp +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent +import net.rsprot.protocol.util.gCombinedId + +@Consistent +public class IfSubOpDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.IF_SUBOP + + override fun decode(buffer: JagByteBuf): IfSubOp { + val combinedId = buffer.gCombinedId() + val sub = buffer.g2() + val obj = buffer.g2() + val subop = buffer.g1() + val op = buffer.g1() + return IfSubOp( + combinedId, + sub, + obj, + op + 1, + subop, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/AffinedClanSettingsAddBannedFromChannelDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/AffinedClanSettingsAddBannedFromChannelDecoder.kt new file mode 100644 index 000000000..9f5fd345f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/AffinedClanSettingsAddBannedFromChannelDecoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.clan.AffinedClanSettingsAddBannedFromChannel +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class AffinedClanSettingsAddBannedFromChannelDecoder : + MessageDecoder { + override val prot: ClientProt = GameClientProt.AFFINEDCLANSETTINGS_ADDBANNED_FROMCHANNEL + + override fun decode(buffer: JagByteBuf): AffinedClanSettingsAddBannedFromChannel { + val clanId = buffer.g1() + val memberIndex = buffer.g2() + val name = buffer.gjstr() + return AffinedClanSettingsAddBannedFromChannel( + name, + clanId, + memberIndex, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/AffinedClanSettingsSetMutedFromChannelDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/AffinedClanSettingsSetMutedFromChannelDecoder.kt new file mode 100644 index 000000000..8def581c3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/AffinedClanSettingsSetMutedFromChannelDecoder.kt @@ -0,0 +1,27 @@ +package net.rsprot.protocol.game.incoming.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.clan.AffinedClanSettingsSetMutedFromChannel +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class AffinedClanSettingsSetMutedFromChannelDecoder : + MessageDecoder { + override val prot: ClientProt = GameClientProt.AFFINEDCLANSETTINGS_SETMUTED_FROMCHANNEL + + override fun decode(buffer: JagByteBuf): AffinedClanSettingsSetMutedFromChannel { + val clanId = buffer.g1() + val memberIndex = buffer.g2() + val muted = buffer.g1() == 1 + val name = buffer.gjstr() + return AffinedClanSettingsSetMutedFromChannel( + name, + clanId, + memberIndex, + muted, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanChannelFullRequestDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanChannelFullRequestDecoder.kt new file mode 100644 index 000000000..baf2d5efe --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanChannelFullRequestDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.clan.ClanChannelFullRequest +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ClanChannelFullRequestDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.CLANCHANNEL_FULL_REQUEST + + override fun decode(buffer: JagByteBuf): ClanChannelFullRequest { + val clanId = buffer.g1s() + return ClanChannelFullRequest(clanId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanChannelKickUserDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanChannelKickUserDecoder.kt new file mode 100644 index 000000000..e2c1d2e63 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanChannelKickUserDecoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.incoming.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.clan.ClanChannelKickUser +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ClanChannelKickUserDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.CLANCHANNEL_KICKUSER + + override fun decode(buffer: JagByteBuf): ClanChannelKickUser { + val clanId = buffer.g1() + val memberIndex = buffer.g2() + val name = buffer.gjstr() + return ClanChannelKickUser( + name, + clanId, + memberIndex, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanSettingsFullRequestDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanSettingsFullRequestDecoder.kt new file mode 100644 index 000000000..548faf22a --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/clan/ClanSettingsFullRequestDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.clan.ClanSettingsFullRequest +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ClanSettingsFullRequestDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.CLANSETTINGS_FULL_REQUEST + + override fun decode(buffer: JagByteBuf): ClanSettingsFullRequest { + val clanId = buffer.g1s() + return ClanSettingsFullRequest(clanId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventAppletFocusDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventAppletFocusDecoder.kt new file mode 100644 index 000000000..2ebfd4fe1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventAppletFocusDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.events + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.events.EventAppletFocus +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class EventAppletFocusDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.EVENT_APPLET_FOCUS + + override fun decode(buffer: JagByteBuf): EventAppletFocus { + val inFocus = buffer.g1() == 1 + return EventAppletFocus(inFocus) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventCameraPositionDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventCameraPositionDecoder.kt new file mode 100644 index 000000000..7df055ae6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventCameraPositionDecoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.game.incoming.codec.events + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.events.EventCameraPosition +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class EventCameraPositionDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.EVENT_CAMERA_POSITION + + override fun decode(buffer: JagByteBuf): EventCameraPosition { + val angleX = buffer.g2Alt1() + val angleY = buffer.g2Alt1() + return EventCameraPosition( + angleX, + angleY, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventKeyboardDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventKeyboardDecoder.kt new file mode 100644 index 000000000..8157c74df --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventKeyboardDecoder.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.incoming.codec.events + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.events.EventKeyboard +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class EventKeyboardDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.EVENT_KEYBOARD + + override fun decode(buffer: JagByteBuf): EventKeyboard { + val count = buffer.readableBytes() / 4 + val keys = ByteArray(count) + var lastTransmittedKeyPress: Int = -1 + for (i in 0.. { + override val prot: ClientProt = GameClientProt.EVENT_MOUSE_CLICK + + override fun decode(buffer: JagByteBuf): EventMouseClick { + val packed = buffer.g2() + val rightClick = packed and 0x1 != 0 + val lastTransmittedMouseClick = packed ushr 1 + val x = buffer.g2() + val y = buffer.g2() + return EventMouseClick( + lastTransmittedMouseClick, + rightClick, + x, + y, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventMouseMoveDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventMouseMoveDecoder.kt new file mode 100644 index 000000000..6cc4ebbee --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventMouseMoveDecoder.kt @@ -0,0 +1,81 @@ +package net.rsprot.protocol.game.incoming.codec.events + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.events.EventMouseMove +import net.rsprot.protocol.game.incoming.events.util.MouseMovements +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Suppress("DuplicatedCode") +@Consistent +public class EventMouseMoveDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.EVENT_MOUSE_MOVE + + override fun decode(buffer: JagByteBuf): EventMouseMove { + val averageTime = buffer.g1() + val remainingTime = buffer.g1() + val array = threadLocalArray.get() + var count = 0 + while (buffer.isReadable) { + var packed = buffer.g1() + var deltaX: Int + var deltaY: Int + var timeSinceLastMovement: Int + if (packed and 0xE0 == 0xE0) { + timeSinceLastMovement = packed and 0x1f shl 8 or buffer.g1() + deltaX = buffer.g2s() + deltaY = buffer.g2s() + if (deltaY == 0 && deltaX == -0x8000) { + deltaX = -1 + deltaY = -1 + } + } else if (packed and 0xC0 == 0xC0) { + timeSinceLastMovement = packed and 0x3f + deltaX = buffer.g2s() + deltaY = buffer.g2s() + if (deltaY == 0 && deltaX == -0x8000) { + deltaX = -1 + deltaY = -1 + } + } else if (packed and 0x80 == 0x80) { + timeSinceLastMovement = packed and 0x7f + deltaX = buffer.g1() - 128 + deltaY = buffer.g1() - 128 + } else { + packed = (packed shl 8) or (buffer.g1()) + timeSinceLastMovement = (packed ushr 12) and 0x7 + deltaX = ((packed shr 6) and 0x3F) - 32 + deltaY = (packed and 0x3F) - 32 + } + val change = + MouseMovements.MousePosChange( + timeSinceLastMovement, + deltaX, + deltaY, + ) + array[count++] = change.packed + } + val slice = array.copyOf(count) + return EventMouseMove( + averageTime, + remainingTime, + MouseMovements(slice), + ) + } + + private companion object { + /** + * Utilizing a thread-local initial long array, as the number of + * mouse movements is unknown (relies on remaining bytes in buffer, + * which in turn uses compression methods so each entry can be 2-4 bytes). + * As Netty's threads decode this, a thread-local implementation is + * perfectly safe to utilize, and will save us some memory in return. + */ + private val threadLocalArray = + ThreadLocal.withInitial { + LongArray(128) + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventMouseScrollDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventMouseScrollDecoder.kt new file mode 100644 index 000000000..d0e4102e7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventMouseScrollDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.events + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.events.EventMouseScroll +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class EventMouseScrollDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.EVENT_MOUSE_SCROLL + + override fun decode(buffer: JagByteBuf): EventMouseScroll { + val rotation = buffer.g2s() + return EventMouseScroll(rotation) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventNativeMouseClickDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventNativeMouseClickDecoder.kt new file mode 100644 index 000000000..0a66d3d0d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventNativeMouseClickDecoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.incoming.codec.events + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.events.EventNativeMouseClick +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class EventNativeMouseClickDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.EVENT_NATIVE_MOUSE_CLICK + + override fun decode(buffer: JagByteBuf): EventNativeMouseClick { + val packedCoord = buffer.g4Alt2() + val code = buffer.g1Alt1() + val lastTransmittedMouseClick = buffer.g2Alt3() + return EventNativeMouseClick( + lastTransmittedMouseClick, + code, + packedCoord and 0xFFFF, + packedCoord ushr 16 and 0xFFFF, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventNativeMouseMoveDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventNativeMouseMoveDecoder.kt new file mode 100644 index 000000000..308176129 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/events/EventNativeMouseMoveDecoder.kt @@ -0,0 +1,81 @@ +package net.rsprot.protocol.game.incoming.codec.events + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.events.EventNativeMouseMove +import net.rsprot.protocol.game.incoming.events.util.MouseMovements +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Suppress("DuplicatedCode") +@Consistent +public class EventNativeMouseMoveDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.EVENT_NATIVE_MOUSE_MOVE + + override fun decode(buffer: JagByteBuf): EventNativeMouseMove { + val averageTime = buffer.g1() + val remainingTime = buffer.g1() + val array = threadLocalArray.get() + var count = 0 + while (buffer.isReadable) { + var packed = buffer.g1() + var deltaX: Int + var deltaY: Int + var timeSinceLastMovement: Int + if (packed and 0xE0 == 0xE0) { + timeSinceLastMovement = packed and 0x1f shl 8 or buffer.g1() + deltaX = buffer.g2s() + deltaY = buffer.g2s() + if (deltaY == 0 && deltaX == -0x8000) { + deltaX = -1 + deltaY = -1 + } + } else if (packed and 0xC0 == 0xC0) { + timeSinceLastMovement = packed and 0x3f + deltaX = buffer.g2s() + deltaY = buffer.g2s() + if (deltaY == 0 && deltaX == -0x8000) { + deltaX = -1 + deltaY = -1 + } + } else if (packed and 0x80 == 0x80) { + timeSinceLastMovement = packed and 0x7f + deltaX = buffer.g1() - 128 + deltaY = buffer.g1() - 128 + } else { + packed = (packed shl 8) or (buffer.g1()) + timeSinceLastMovement = (packed ushr 12) and 0x7 + deltaX = ((packed shr 6) and 0x3F) - 32 + deltaY = (packed and 0x3F) - 32 + } + val change = + MouseMovements.MousePosChange( + timeSinceLastMovement, + deltaX, + deltaY, + ) + array[count++] = change.packed + } + val slice = array.copyOf(count) + return EventNativeMouseMove( + averageTime, + remainingTime, + MouseMovements(slice), + ) + } + + private companion object { + /** + * Utilizing a thread-local initial long array, as the number of + * mouse movements is unknown (relies on remaining bytes in buffer, + * which in turn uses compression methods so each entry can be 2-4 bytes). + * As Netty's threads decode this, a thread-local implementation is + * perfectly safe to utilize, and will save us some memory in return. + */ + private val threadLocalArray = + ThreadLocal.withInitial { + LongArray(128) + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatJoinLeaveDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatJoinLeaveDecoder.kt new file mode 100644 index 000000000..44dc7c68a --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatJoinLeaveDecoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.incoming.codec.friendchat + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.friendchat.FriendChatJoinLeave +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class FriendChatJoinLeaveDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.FRIENDCHAT_JOIN_LEAVE + + override fun decode(buffer: JagByteBuf): FriendChatJoinLeave { + val name = + if (!buffer.isReadable) { + null + } else { + buffer.gjstr() + } + return FriendChatJoinLeave(name) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatKickDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatKickDecoder.kt new file mode 100644 index 000000000..c611d9631 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatKickDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.friendchat + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.friendchat.FriendChatKick +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class FriendChatKickDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.FRIENDCHAT_KICK + + override fun decode(buffer: JagByteBuf): FriendChatKick { + val name = buffer.gjstr() + return FriendChatKick(name) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatSetRankDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatSetRankDecoder.kt new file mode 100644 index 000000000..59e25f02d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/friendchat/FriendChatSetRankDecoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.game.incoming.codec.friendchat + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.friendchat.FriendChatSetRank +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class FriendChatSetRankDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.FRIENDCHAT_SETRANK + + override fun decode(buffer: JagByteBuf): FriendChatSetRank { + val name = buffer.gjstr() + val rank = buffer.g1Alt1() + return FriendChatSetRank( + name, + rank, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc1Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc1Decoder.kt new file mode 100644 index 000000000..e42b86e8e --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc1Decoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.locs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.locs.OpLoc +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpLoc1Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPLOC1 + + override fun decode(buffer: JagByteBuf): OpLoc { + val x = buffer.g2Alt2() + val id = buffer.g2() + val z = buffer.g2() + val controlKey = buffer.g2Alt3() == 1 + return OpLoc( + id, + x, + z, + controlKey, + 1, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc2Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc2Decoder.kt new file mode 100644 index 000000000..c69b0336e --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc2Decoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.locs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.locs.OpLoc +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpLoc2Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPLOC2 + + override fun decode(buffer: JagByteBuf): OpLoc { + val z = buffer.g2() + val id = buffer.g2Alt2() + val controlKey = buffer.g1Alt2() == 1 + val x = buffer.g2Alt1() + return OpLoc( + id, + x, + z, + controlKey, + 2, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc3Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc3Decoder.kt new file mode 100644 index 000000000..04c257e59 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc3Decoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.locs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.locs.OpLoc +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpLoc3Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPLOC3 + + override fun decode(buffer: JagByteBuf): OpLoc { + val z = buffer.g2() + val id = buffer.g2Alt1() + val controlKey = buffer.g1Alt3() == 1 + val x = buffer.g2Alt2() + return OpLoc( + id, + x, + z, + controlKey, + 3, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc4Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc4Decoder.kt new file mode 100644 index 000000000..bd911b7cb --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc4Decoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.locs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.locs.OpLoc +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpLoc4Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPLOC4 + + override fun decode(buffer: JagByteBuf): OpLoc { + val z = buffer.g2() + val x = buffer.g2Alt1() + val controlKey = buffer.g1Alt2() == 1 + val id = buffer.g2Alt2() + return OpLoc( + id, + x, + z, + controlKey, + 4, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc5Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc5Decoder.kt new file mode 100644 index 000000000..a9b5e6bdf --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc5Decoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.locs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.locs.OpLoc +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpLoc5Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPLOC5 + + override fun decode(buffer: JagByteBuf): OpLoc { + val z = buffer.g2Alt2() + val controlKey = buffer.g1() == 1 + val x = buffer.g2Alt1() + val id = buffer.g2Alt2() + return OpLoc( + id, + x, + z, + controlKey, + 5, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc6Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc6Decoder.kt new file mode 100644 index 000000000..07d4a9fc8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLoc6Decoder.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.game.incoming.codec.locs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.locs.OpLoc6 +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpLoc6Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPLOC6 + + override fun decode(buffer: JagByteBuf): OpLoc6 { + val id = buffer.g2() + return OpLoc6(id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLocTDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLocTDecoder.kt new file mode 100644 index 000000000..3b1f6b541 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/locs/OpLocTDecoder.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.incoming.codec.locs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.locs.OpLocT +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.util.gCombinedIdAlt1 + +public class OpLocTDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPLOCT + + override fun decode(buffer: JagByteBuf): OpLocT { + val controlKey = buffer.g1Alt1() == 1 + val id = buffer.g2Alt3() + val selectedObj = buffer.g2() + val z = buffer.g2Alt1() + val x = buffer.g2Alt1() + val selectedCombinedId = buffer.gCombinedIdAlt1() + val selectedSub = buffer.g2Alt1() + return OpLocT( + id, + x, + z, + controlKey, + selectedCombinedId, + selectedSub, + selectedObj, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/messaging/MessagePrivateDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/messaging/MessagePrivateDecoder.kt new file mode 100644 index 000000000..66ce13b36 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/messaging/MessagePrivateDecoder.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.game.incoming.codec.messaging + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.messaging.MessagePrivate +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MessagePrivateDecoder( + private val huffmanCodecProvider: HuffmanCodecProvider, +) : MessageDecoder { + override val prot: ClientProt = GameClientProt.MESSAGE_PRIVATE + + override fun decode(buffer: JagByteBuf): MessagePrivate { + val name = buffer.gjstr() + val huffman = huffmanCodecProvider.provide() + val message = huffman.decode(buffer) + return MessagePrivate( + name, + message, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/messaging/MessagePublicDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/messaging/MessagePublicDecoder.kt new file mode 100644 index 000000000..b476088e6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/messaging/MessagePublicDecoder.kt @@ -0,0 +1,63 @@ +package net.rsprot.protocol.game.incoming.codec.messaging + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.messaging.MessagePublic +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MessagePublicDecoder( + private val huffmanCodecProvider: HuffmanCodecProvider, +) : MessageDecoder { + override val prot: ClientProt = GameClientProt.MESSAGE_PUBLIC + + override fun decode(buffer: JagByteBuf): MessagePublic { + val type = buffer.g1() + val colour = buffer.g1() + val effect = buffer.g1() + val patternArray = + if (colour in 13..20) { + ByteArray(colour - 12) { + buffer.g1().toByte() + } + } else { + null + } + val huffman = huffmanCodecProvider.provide() + val hasTrailingByte = type == CLAN_MAIN_CHANNEL_TYPE + val huffmanSlice = + if (hasTrailingByte) { + buffer.buffer.readSlice(buffer.readableBytes() - 1) + } else { + buffer.buffer + } + val message = huffman.decode(huffmanSlice) + val clanType = + if (hasTrailingByte) { + buffer.g1() + } else { + -1 + } + val pattern = + if (patternArray != null) { + MessagePublic.MessageColourPattern(patternArray) + } else { + null + } + return MessagePublic( + type, + colour, + effect, + message, + pattern, + clanType, + ) + } + + private companion object { + private const val CLAN_MAIN_CHANNEL_TYPE: Int = 3 + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/ConnectionTelemetryDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/ConnectionTelemetryDecoder.kt new file mode 100644 index 000000000..1ff9efd68 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/ConnectionTelemetryDecoder.kt @@ -0,0 +1,38 @@ +package net.rsprot.protocol.game.incoming.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.client.ConnectionTelemetry +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ConnectionTelemetryDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.CONNECTION_TELEMETRY + + override fun decode(buffer: JagByteBuf): ConnectionTelemetry { + val connectionLostDuration = buffer.g2() + val loginDuration = buffer.g2() + val unusedDuration = buffer.g2() + check(unusedDuration == 0) { + "Unknown duration detected: $unusedDuration" + } + val clientState = buffer.g2() + val unused1 = buffer.g2() + check(unused1 == 0) { + "Unused1 property value detected: $unused1" + } + val loginCount = buffer.g2() + val unused2 = buffer.g2() + check(unused2 == 0) { + "Unused2 property value detected: $unused2" + } + return ConnectionTelemetry( + connectionLostDuration, + loginDuration, + clientState, + loginCount, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/DetectModifiedClientDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/DetectModifiedClientDecoder.kt new file mode 100644 index 000000000..c06a2e48a --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/DetectModifiedClientDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.client.DetectModifiedClient +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class DetectModifiedClientDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.DETECT_MODIFIED_CLIENT + + override fun decode(buffer: JagByteBuf): DetectModifiedClient { + val code = buffer.g4() + return DetectModifiedClient(code) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/IdleDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/IdleDecoder.kt new file mode 100644 index 000000000..2f9d93cfe --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/IdleDecoder.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.incoming.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.client.Idle +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class IdleDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.IDLE + + override fun decode(buffer: JagByteBuf): Idle = Idle +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/MapBuildCompleteDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/MapBuildCompleteDecoder.kt new file mode 100644 index 000000000..14fd735eb --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/MapBuildCompleteDecoder.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.incoming.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.client.MapBuildComplete +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MapBuildCompleteDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.MAP_BUILD_COMPLETE + + override fun decode(buffer: JagByteBuf): MapBuildComplete = MapBuildComplete +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/MembershipPromotionEligibilityDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/MembershipPromotionEligibilityDecoder.kt new file mode 100644 index 000000000..997ba3308 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/MembershipPromotionEligibilityDecoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.incoming.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.client.MembershipPromotionEligibility +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MembershipPromotionEligibilityDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.MEMBERSHIP_PROMOTION_ELIGIBILITY + + override fun decode(buffer: JagByteBuf): MembershipPromotionEligibility { + val eligibleForIntroductoryPrice = buffer.g1() + val eligibleForTrialPurchase = buffer.g1() + return MembershipPromotionEligibility( + eligibleForIntroductoryPrice, + eligibleForTrialPurchase, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/NoTimeoutDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/NoTimeoutDecoder.kt new file mode 100644 index 000000000..3e6fb0cf7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/NoTimeoutDecoder.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.incoming.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.client.NoTimeout +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class NoTimeoutDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.NO_TIMEOUT + + override fun decode(buffer: JagByteBuf): NoTimeout = NoTimeout +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/ReflectionCheckReplyDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/ReflectionCheckReplyDecoder.kt new file mode 100644 index 000000000..57d6ae15a --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/ReflectionCheckReplyDecoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.incoming.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.client.ReflectionCheckReply +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ReflectionCheckReplyDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.REFLECTION_CHECK_REPLY + + override fun decode(buffer: JagByteBuf): ReflectionCheckReply { + val copy = buffer.buffer.copy() + val id = buffer.g4() + // Mark the buffer as "read" as copy function doesn't do it automatically. + buffer.buffer.readerIndex(buffer.buffer.writerIndex()) + return ReflectionCheckReply( + id, + copy, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/SendPingReplyDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/SendPingReplyDecoder.kt new file mode 100644 index 000000000..3c3cb9415 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/SendPingReplyDecoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.incoming.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.client.SendPingReply +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class SendPingReplyDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.SEND_PING_REPLY + + override fun decode(buffer: JagByteBuf): SendPingReply { + val fps = buffer.g2Alt2() + val value1 = buffer.g4Alt2() + val value2 = buffer.g4() + val gcPercentTime = buffer.g1Alt2() + return SendPingReply( + fps, + gcPercentTime, + value1, + value2, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/SoundJingleEndDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/SoundJingleEndDecoder.kt new file mode 100644 index 000000000..98bc5e212 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/SoundJingleEndDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.client.SoundJingleEnd +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class SoundJingleEndDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.SOUND_JINGLEEND + + override fun decode(buffer: JagByteBuf): SoundJingleEnd { + val jingle = buffer.g4() + return SoundJingleEnd(jingle) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/WindowStatusDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/WindowStatusDecoder.kt new file mode 100644 index 000000000..f6c131a7a --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/client/WindowStatusDecoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.incoming.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.client.WindowStatus +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class WindowStatusDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.WINDOW_STATUS + + override fun decode(buffer: JagByteBuf): WindowStatus { + val windowMode = buffer.g1() + val frameWidth = buffer.g2() + val frameHeight = buffer.g2() + return WindowStatus( + windowMode, + frameWidth, + frameHeight, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/BugReportDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/BugReportDecoder.kt new file mode 100644 index 000000000..d237309ff --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/BugReportDecoder.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.BugReport +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class BugReportDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.BUG_REPORT + + override fun decode(buffer: JagByteBuf): BugReport { + val description = buffer.gjstr() + val type = buffer.g1Alt3() + val instructions = buffer.gjstr() + check(description.length <= 500) { + "Bug report description length cannot exceed 500 characters." + } + check(instructions.length <= 500) { + "Bug report instructions length cannot exceed 500 characters." + } + return BugReport( + type, + description, + instructions, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/ClickWorldMapDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/ClickWorldMapDecoder.kt new file mode 100644 index 000000000..39bc41d14 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/ClickWorldMapDecoder.kt @@ -0,0 +1,17 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.incoming.misc.user.ClickWorldMap +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class ClickWorldMapDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.CLICKWORLDMAP + + override fun decode(buffer: JagByteBuf): ClickWorldMap { + val packed = buffer.g4() + return ClickWorldMap(CoordGrid(packed)) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/ClientCheatDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/ClientCheatDecoder.kt new file mode 100644 index 000000000..d22f09ed7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/ClientCheatDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.ClientCheat +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ClientCheatDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.CLIENT_CHEAT + + override fun decode(buffer: JagByteBuf): ClientCheat { + val command = buffer.gjstr() + return ClientCheat(command) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/CloseModalDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/CloseModalDecoder.kt new file mode 100644 index 000000000..828e2ec57 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/CloseModalDecoder.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.CloseModal +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CloseModalDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.CLOSE_MODAL + + override fun decode(buffer: JagByteBuf): CloseModal = CloseModal +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/HiscoreRequestDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/HiscoreRequestDecoder.kt new file mode 100644 index 000000000..a4336a453 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/HiscoreRequestDecoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.HiscoreRequest +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class HiscoreRequestDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.HISCORE_REQUEST + + override fun decode(buffer: JagByteBuf): HiscoreRequest { + val type = buffer.g1() + val requestId = buffer.g1() + val name = buffer.gjstr() + return HiscoreRequest( + type, + requestId, + name, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/IfCrmViewClickDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/IfCrmViewClickDecoder.kt new file mode 100644 index 000000000..6c2476d93 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/IfCrmViewClickDecoder.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.IfCrmViewClick +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.util.gCombinedId + +public class IfCrmViewClickDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.IF_CRMVIEW_CLICK + + override fun decode(buffer: JagByteBuf): IfCrmViewClick { + val sub = buffer.g2() + val behaviour1 = buffer.g4() + val behaviour2 = buffer.g4Alt1() + val combinedId = buffer.gCombinedId() + val behaviour3 = buffer.g4() + val serverTarget = buffer.g4Alt1() + return IfCrmViewClick( + serverTarget, + combinedId, + sub, + behaviour1, + behaviour2, + behaviour3, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/MoveGameClickDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/MoveGameClickDecoder.kt new file mode 100644 index 000000000..f17e693d8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/MoveGameClickDecoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.MoveGameClick +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class MoveGameClickDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.MOVE_GAMECLICK + + override fun decode(buffer: JagByteBuf): MoveGameClick { + val z = buffer.g2() + val keyCombination = buffer.g1Alt3() + val x = buffer.g2Alt3() + return MoveGameClick( + x, + z, + keyCombination, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/MoveMinimapClickDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/MoveMinimapClickDecoder.kt new file mode 100644 index 000000000..a04048b97 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/MoveMinimapClickDecoder.kt @@ -0,0 +1,55 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.MoveMinimapClick +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class MoveMinimapClickDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.MOVE_MINIMAPCLICK + + override fun decode(buffer: JagByteBuf): MoveMinimapClick { + // The x, z and keyCombination get scrambled between revisions + val z = buffer.g2() + val keyCombination = buffer.g1Alt3() + val x = buffer.g2Alt3() + + // The arguments below are consistent across revisions + val minimapWidth = buffer.g1() + val minimapHeight = buffer.g1() + val cameraAngleY = buffer.g2() + val checkpoint1 = buffer.g1() + check(checkpoint1 == 57) { + "Invalid checkpoint 1: $checkpoint1" + } + val checkpoint2 = buffer.g1() + check(checkpoint2 == 0) { + "Invalid checkpoint 2: $checkpoint2" + } + val checkpoint3 = buffer.g1() + check(checkpoint3 == 0) { + "Invalid checkpoint 3: $checkpoint3" + } + val checkpoint4 = buffer.g1() + check(checkpoint4 == 89) { + "Invalid checkpoint 4: $checkpoint4" + } + val fineX = buffer.g2() + val fineZ = buffer.g2() + val checkpoint5 = buffer.g1() + check(checkpoint5 == 63) { + "Invalid checkpoint 5: $checkpoint5" + } + return MoveMinimapClick( + x, + z, + keyCombination, + minimapWidth, + minimapHeight, + cameraAngleY, + fineX, + fineZ, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/OculusLeaveDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/OculusLeaveDecoder.kt new file mode 100644 index 000000000..a6c01f1ed --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/OculusLeaveDecoder.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.OculusLeave +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class OculusLeaveDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OCULUS_LEAVE + + override fun decode(buffer: JagByteBuf): OculusLeave = OculusLeave +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/SendSnapshotDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/SendSnapshotDecoder.kt new file mode 100644 index 000000000..01aee4eaa --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/SendSnapshotDecoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.SendSnapshot +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class SendSnapshotDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.SEND_SNAPSHOT + + override fun decode(buffer: JagByteBuf): SendSnapshot { + val name = buffer.gjstr() + val ruleId = buffer.g1() + val mute = buffer.g1() == 1 + return SendSnapshot( + name, + ruleId, + mute, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/SetChatFilterSettingsDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/SetChatFilterSettingsDecoder.kt new file mode 100644 index 000000000..a4e3ebd1f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/SetChatFilterSettingsDecoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.SetChatFilterSettings +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class SetChatFilterSettingsDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.SET_CHATFILTERSETTINGS + + override fun decode(buffer: JagByteBuf): SetChatFilterSettings { + val publicChatFilter = buffer.g1() + val privateChatFilter = buffer.g1() + val tradeChatFilter = buffer.g1() + return SetChatFilterSettings( + publicChatFilter, + privateChatFilter, + tradeChatFilter, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/TeleportDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/TeleportDecoder.kt new file mode 100644 index 000000000..ab6b16c54 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/TeleportDecoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.Teleport +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class TeleportDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.TELEPORT + + override fun decode(buffer: JagByteBuf): Teleport { + val oculusSyncValue = buffer.g4Alt1() + val level = buffer.g1Alt1() + val x = buffer.g2Alt1() + val z = buffer.g2Alt1() + return Teleport( + oculusSyncValue, + x, + z, + level, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/UpdatePlayerModelDecoderOld.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/UpdatePlayerModelDecoderOld.kt new file mode 100644 index 000000000..1072e244d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/misc/user/UpdatePlayerModelDecoderOld.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.codec.misc.user + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.misc.user.UpdatePlayerModelOld +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdatePlayerModelDecoderOld : MessageDecoder { + override val prot: ClientProt = GameClientProt.UPDATE_PLAYER_MODEL_OLD + + override fun decode(buffer: JagByteBuf): UpdatePlayerModelOld { + val bodyType = buffer.g1() + val identKit = ByteArray(7) + for (i in identKit.indices) { + identKit[i] = buffer.g1().toByte() + } + val colours = ByteArray(5) + for (i in colours.indices) { + colours[i] = buffer.g1().toByte() + } + return UpdatePlayerModelOld( + bodyType, + identKit, + colours, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc1Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc1Decoder.kt new file mode 100644 index 000000000..eb79f063d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc1Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.npcs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.npcs.OpNpc +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpNpc1Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPNPC1 + + override fun decode(buffer: JagByteBuf): OpNpc { + val index = buffer.g2Alt3() + val controlKey = buffer.g1Alt2() == 1 + return OpNpc( + index, + controlKey, + 1, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc2Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc2Decoder.kt new file mode 100644 index 000000000..8f457974e --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc2Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.npcs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.npcs.OpNpc +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpNpc2Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPNPC2 + + override fun decode(buffer: JagByteBuf): OpNpc { + val controlKey = buffer.g1() == 1 + val index = buffer.g2Alt1() + return OpNpc( + index, + controlKey, + 2, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc3Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc3Decoder.kt new file mode 100644 index 000000000..26271c1d3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc3Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.npcs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.npcs.OpNpc +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpNpc3Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPNPC3 + + override fun decode(buffer: JagByteBuf): OpNpc { + val controlKey = buffer.g1Alt2() == 1 + val index = buffer.g2Alt3() + return OpNpc( + index, + controlKey, + 3, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc4Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc4Decoder.kt new file mode 100644 index 000000000..4363ea0ea --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc4Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.npcs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.npcs.OpNpc +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpNpc4Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPNPC4 + + override fun decode(buffer: JagByteBuf): OpNpc { + val controlKey = buffer.g1() == 1 + val index = buffer.g2() + return OpNpc( + index, + controlKey, + 4, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc5Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc5Decoder.kt new file mode 100644 index 000000000..910bd4509 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc5Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.npcs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.npcs.OpNpc +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpNpc5Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPNPC5 + + override fun decode(buffer: JagByteBuf): OpNpc { + val index = buffer.g2Alt2() + val controlKey = buffer.g1Alt1() == 1 + return OpNpc( + index, + controlKey, + 5, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc6Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc6Decoder.kt new file mode 100644 index 000000000..f97819c8c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpc6Decoder.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.game.incoming.codec.npcs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.npcs.OpNpc6 +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpNpc6Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPNPC6 + + override fun decode(buffer: JagByteBuf): OpNpc6 { + val id = buffer.g2Alt1() + return OpNpc6(id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpcTDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpcTDecoder.kt new file mode 100644 index 000000000..e0fd6e76a --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/npcs/OpNpcTDecoder.kt @@ -0,0 +1,27 @@ +package net.rsprot.protocol.game.incoming.codec.npcs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.npcs.OpNpcT +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.util.gCombinedIdAlt1 + +public class OpNpcTDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPNPCT + + override fun decode(buffer: JagByteBuf): OpNpcT { + val index = buffer.g2Alt1() + val selectedObj = buffer.g2Alt3() + val selectedCombinedId = buffer.gCombinedIdAlt1() + val selectedSub = buffer.g2Alt1() + val controlKey = buffer.g1Alt2() == 1 + return OpNpcT( + index, + controlKey, + selectedCombinedId, + selectedSub, + selectedObj, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj1Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj1Decoder.kt new file mode 100644 index 000000000..ee42180b0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj1Decoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.objs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.objs.OpObj +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpObj1Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPOBJ1 + + override fun decode(buffer: JagByteBuf): OpObj { + val x = buffer.g2Alt2() + val z = buffer.g2Alt1() + val id = buffer.g2Alt3() + val controlKey = buffer.g1Alt3() == 1 + return OpObj( + id, + x, + z, + controlKey, + 1, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj2Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj2Decoder.kt new file mode 100644 index 000000000..d07e02e44 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj2Decoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.objs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.objs.OpObj +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpObj2Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPOBJ2 + + override fun decode(buffer: JagByteBuf): OpObj { + val id = buffer.g2Alt1() + val z = buffer.g2Alt1() + val x = buffer.g2Alt1() + val controlKey = buffer.g1Alt2() == 1 + return OpObj( + id, + x, + z, + controlKey, + 2, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj3Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj3Decoder.kt new file mode 100644 index 000000000..6051c4094 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj3Decoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.objs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.objs.OpObj +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpObj3Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPOBJ3 + + override fun decode(buffer: JagByteBuf): OpObj { + val controlKey = buffer.g1Alt2() == 1 + val id = buffer.g2() + val x = buffer.g2Alt2() + val z = buffer.g2Alt3() + return OpObj( + id, + x, + z, + controlKey, + 3, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj4Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj4Decoder.kt new file mode 100644 index 000000000..90ccc4e0a --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj4Decoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.objs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.objs.OpObj +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpObj4Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPOBJ4 + + override fun decode(buffer: JagByteBuf): OpObj { + val controlKey = buffer.g1Alt1() == 1 + val x = buffer.g2Alt2() + val z = buffer.g2Alt2() + val id = buffer.g2Alt2() + return OpObj( + id, + x, + z, + controlKey, + 4, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj5Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj5Decoder.kt new file mode 100644 index 000000000..6a0cdc36f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj5Decoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.incoming.codec.objs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.objs.OpObj +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpObj5Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPOBJ5 + + override fun decode(buffer: JagByteBuf): OpObj { + val id = buffer.g2() + val x = buffer.g2() + val z = buffer.g2Alt3() + val controlKey = buffer.g1Alt3() == 1 + return OpObj( + id, + x, + z, + controlKey, + 5, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj6Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj6Decoder.kt new file mode 100644 index 000000000..f2bc9701d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObj6Decoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.incoming.codec.objs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.objs.OpObj6 +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpObj6Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPOBJ6 + + override fun decode(buffer: JagByteBuf): OpObj6 { + val z = buffer.g2() + val x = buffer.g2Alt2() + val id = buffer.g2Alt1() + return OpObj6( + id, + x, + z, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObjTDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObjTDecoder.kt new file mode 100644 index 000000000..27ff69a07 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/objs/OpObjTDecoder.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.incoming.codec.objs + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.objs.OpObjT +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.util.gCombinedIdAlt3 + +public class OpObjTDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPOBJT + + override fun decode(buffer: JagByteBuf): OpObjT { + val controlKey = buffer.g1() == 1 + val id = buffer.g2Alt2() + val selectedCombinedId = buffer.gCombinedIdAlt3() + val selectedObj = buffer.g2Alt1() + val x = buffer.g2Alt1() + val selectedSub = buffer.g2Alt3() + val z = buffer.g2Alt3() + return OpObjT( + id, + x, + z, + controlKey, + selectedCombinedId, + selectedSub, + selectedObj, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer1Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer1Decoder.kt new file mode 100644 index 000000000..140cdc1a8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer1Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.players + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.players.OpPlayer +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpPlayer1Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPPLAYER1 + + override fun decode(buffer: JagByteBuf): OpPlayer { + val index = buffer.g2Alt1() + val controlKey = buffer.g1() == 1 + return OpPlayer( + index, + controlKey, + 1, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer2Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer2Decoder.kt new file mode 100644 index 000000000..8a5694cc3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer2Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.players + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.players.OpPlayer +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpPlayer2Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPPLAYER2 + + override fun decode(buffer: JagByteBuf): OpPlayer { + val index = buffer.g2Alt3() + val controlKey = buffer.g1Alt1() == 1 + return OpPlayer( + index, + controlKey, + 2, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer3Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer3Decoder.kt new file mode 100644 index 000000000..d54094028 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer3Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.players + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.players.OpPlayer +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpPlayer3Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPPLAYER3 + + override fun decode(buffer: JagByteBuf): OpPlayer { + val controlKey = buffer.g1Alt1() == 1 + val index = buffer.g2Alt2() + return OpPlayer( + index, + controlKey, + 3, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer4Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer4Decoder.kt new file mode 100644 index 000000000..8af83470d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer4Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.players + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.players.OpPlayer +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpPlayer4Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPPLAYER4 + + override fun decode(buffer: JagByteBuf): OpPlayer { + val index = buffer.g2() + val controlKey = buffer.g1Alt3() == 1 + return OpPlayer( + index, + controlKey, + 4, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer5Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer5Decoder.kt new file mode 100644 index 000000000..8e373fa1f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer5Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.players + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.players.OpPlayer +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpPlayer5Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPPLAYER5 + + override fun decode(buffer: JagByteBuf): OpPlayer { + val controlKey = buffer.g1() == 1 + val index = buffer.g2Alt1() + return OpPlayer( + index, + controlKey, + 5, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer6Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer6Decoder.kt new file mode 100644 index 000000000..8701fb8ce --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer6Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.players + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.players.OpPlayer +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpPlayer6Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPPLAYER6 + + override fun decode(buffer: JagByteBuf): OpPlayer { + val index = buffer.g2Alt3() + val controlKey = buffer.g1Alt1() == 1 + return OpPlayer( + index, + controlKey, + 6, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer7Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer7Decoder.kt new file mode 100644 index 000000000..537e43920 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer7Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.players + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.players.OpPlayer +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpPlayer7Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPPLAYER7 + + override fun decode(buffer: JagByteBuf): OpPlayer { + val controlKey = buffer.g1() == 1 + val index = buffer.g2Alt3() + return OpPlayer( + index, + controlKey, + 7, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer8Decoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer8Decoder.kt new file mode 100644 index 000000000..fb80353fc --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayer8Decoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.players + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.players.OpPlayer +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder + +public class OpPlayer8Decoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPPLAYER8 + + override fun decode(buffer: JagByteBuf): OpPlayer { + val controlKey = buffer.g1() == 1 + val index = buffer.g2() + return OpPlayer( + index, + controlKey, + 8, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayerTDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayerTDecoder.kt new file mode 100644 index 000000000..fdda6b7c3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/players/OpPlayerTDecoder.kt @@ -0,0 +1,27 @@ +package net.rsprot.protocol.game.incoming.codec.players + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.players.OpPlayerT +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.util.gCombinedIdAlt1 + +public class OpPlayerTDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.OPPLAYERT + + override fun decode(buffer: JagByteBuf): OpPlayerT { + val combinedId = buffer.gCombinedIdAlt1() + val selectedSub = buffer.g2Alt2() + val index = buffer.g2Alt2() + val selectedObj = buffer.g2Alt3() + val controlKey = buffer.g1Alt1() == 1 + return OpPlayerT( + index, + controlKey, + combinedId, + selectedSub, + selectedObj, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePCountDialogDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePCountDialogDecoder.kt new file mode 100644 index 000000000..eed9d4a75 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePCountDialogDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.resumed + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.game.incoming.resumed.ResumePCountDialog +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ResumePCountDialogDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.RESUME_P_COUNTDIALOG + + override fun decode(buffer: JagByteBuf): ResumePCountDialog { + val count = buffer.g4() + return ResumePCountDialog(count) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePNameDialogDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePNameDialogDecoder.kt new file mode 100644 index 000000000..d8e842265 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePNameDialogDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.resumed + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.game.incoming.resumed.ResumePNameDialog +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ResumePNameDialogDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.RESUME_P_NAMEDIALOG + + override fun decode(buffer: JagByteBuf): ResumePNameDialog { + val name = buffer.gjstr() + return ResumePNameDialog(name) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePObjDialogDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePObjDialogDecoder.kt new file mode 100644 index 000000000..125a172ea --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePObjDialogDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.resumed + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.game.incoming.resumed.ResumePObjDialog +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ResumePObjDialogDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.RESUME_P_OBJDIALOG + + override fun decode(buffer: JagByteBuf): ResumePObjDialog { + val obj = buffer.g2() + return ResumePObjDialog(obj) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePStringDialogDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePStringDialogDecoder.kt new file mode 100644 index 000000000..a7f99855c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePStringDialogDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.resumed + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.game.incoming.resumed.ResumePStringDialog +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ResumePStringDialogDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.RESUME_P_STRINGDIALOG + + override fun decode(buffer: JagByteBuf): ResumePStringDialog { + val string = buffer.gjstr() + return ResumePStringDialog(string) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePauseButtonDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePauseButtonDecoder.kt new file mode 100644 index 000000000..590a1a11a --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/resumed/ResumePauseButtonDecoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.incoming.codec.resumed + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.game.incoming.resumed.ResumePauseButton +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.util.gCombinedIdAlt3 + +public class ResumePauseButtonDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.RESUME_PAUSEBUTTON + + override fun decode(buffer: JagByteBuf): ResumePauseButton { + val sub = buffer.g2Alt3() + val combinedId = buffer.gCombinedIdAlt3() + return ResumePauseButton( + combinedId, + sub, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/FriendListAddDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/FriendListAddDecoder.kt new file mode 100644 index 000000000..735306f85 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/FriendListAddDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.social + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.game.incoming.social.FriendListAdd +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class FriendListAddDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.FRIENDLIST_ADD + + override fun decode(buffer: JagByteBuf): FriendListAdd { + val name = buffer.gjstr() + return FriendListAdd(name) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/FriendListDelDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/FriendListDelDecoder.kt new file mode 100644 index 000000000..d147c7fde --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/FriendListDelDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.social + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.game.incoming.social.FriendListDel +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class FriendListDelDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.FRIENDLIST_DEL + + override fun decode(buffer: JagByteBuf): FriendListDel { + val name = buffer.gjstr() + return FriendListDel(name) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/IgnoreListAddDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/IgnoreListAddDecoder.kt new file mode 100644 index 000000000..e15848430 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/IgnoreListAddDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.social + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.game.incoming.social.IgnoreListAdd +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class IgnoreListAddDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.IGNORELIST_ADD + + override fun decode(buffer: JagByteBuf): IgnoreListAdd { + val name = buffer.gjstr() + return IgnoreListAdd(name) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/IgnoreListDelDecoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/IgnoreListDelDecoder.kt new file mode 100644 index 000000000..4b05c014c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/codec/social/IgnoreListDelDecoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.incoming.codec.social + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.game.incoming.prot.GameClientProt +import net.rsprot.protocol.game.incoming.social.IgnoreListDel +import net.rsprot.protocol.message.codec.MessageDecoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class IgnoreListDelDecoder : MessageDecoder { + override val prot: ClientProt = GameClientProt.IGNORELIST_DEL + + override fun decode(buffer: JagByteBuf): IgnoreListDel { + val name = buffer.gjstr() + return IgnoreListDel(name) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/DesktopGameMessageDecoderRepository.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/DesktopGameMessageDecoderRepository.kt new file mode 100644 index 000000000..b4b4f5e39 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/DesktopGameMessageDecoderRepository.kt @@ -0,0 +1,210 @@ +package net.rsprot.protocol.game.incoming.prot + +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.ProtRepository +import net.rsprot.protocol.game.incoming.codec.buttons.If1ButtonDecoder +import net.rsprot.protocol.game.incoming.codec.buttons.If3ButtonDecoder +import net.rsprot.protocol.game.incoming.codec.buttons.IfButtonDDecoder +import net.rsprot.protocol.game.incoming.codec.buttons.IfButtonTDecoder +import net.rsprot.protocol.game.incoming.codec.buttons.IfSubOpDecoder +import net.rsprot.protocol.game.incoming.codec.clan.AffinedClanSettingsAddBannedFromChannelDecoder +import net.rsprot.protocol.game.incoming.codec.clan.AffinedClanSettingsSetMutedFromChannelDecoder +import net.rsprot.protocol.game.incoming.codec.clan.ClanChannelFullRequestDecoder +import net.rsprot.protocol.game.incoming.codec.clan.ClanChannelKickUserDecoder +import net.rsprot.protocol.game.incoming.codec.clan.ClanSettingsFullRequestDecoder +import net.rsprot.protocol.game.incoming.codec.events.EventAppletFocusDecoder +import net.rsprot.protocol.game.incoming.codec.events.EventCameraPositionDecoder +import net.rsprot.protocol.game.incoming.codec.events.EventKeyboardDecoder +import net.rsprot.protocol.game.incoming.codec.events.EventMouseClickDecoder +import net.rsprot.protocol.game.incoming.codec.events.EventMouseMoveDecoder +import net.rsprot.protocol.game.incoming.codec.events.EventMouseScrollDecoder +import net.rsprot.protocol.game.incoming.codec.events.EventNativeMouseClickDecoder +import net.rsprot.protocol.game.incoming.codec.events.EventNativeMouseMoveDecoder +import net.rsprot.protocol.game.incoming.codec.friendchat.FriendChatJoinLeaveDecoder +import net.rsprot.protocol.game.incoming.codec.friendchat.FriendChatKickDecoder +import net.rsprot.protocol.game.incoming.codec.friendchat.FriendChatSetRankDecoder +import net.rsprot.protocol.game.incoming.codec.locs.OpLoc1Decoder +import net.rsprot.protocol.game.incoming.codec.locs.OpLoc2Decoder +import net.rsprot.protocol.game.incoming.codec.locs.OpLoc3Decoder +import net.rsprot.protocol.game.incoming.codec.locs.OpLoc4Decoder +import net.rsprot.protocol.game.incoming.codec.locs.OpLoc5Decoder +import net.rsprot.protocol.game.incoming.codec.locs.OpLoc6Decoder +import net.rsprot.protocol.game.incoming.codec.locs.OpLocTDecoder +import net.rsprot.protocol.game.incoming.codec.messaging.MessagePrivateDecoder +import net.rsprot.protocol.game.incoming.codec.messaging.MessagePublicDecoder +import net.rsprot.protocol.game.incoming.codec.misc.client.ConnectionTelemetryDecoder +import net.rsprot.protocol.game.incoming.codec.misc.client.DetectModifiedClientDecoder +import net.rsprot.protocol.game.incoming.codec.misc.client.IdleDecoder +import net.rsprot.protocol.game.incoming.codec.misc.client.MapBuildCompleteDecoder +import net.rsprot.protocol.game.incoming.codec.misc.client.MembershipPromotionEligibilityDecoder +import net.rsprot.protocol.game.incoming.codec.misc.client.NoTimeoutDecoder +import net.rsprot.protocol.game.incoming.codec.misc.client.ReflectionCheckReplyDecoder +import net.rsprot.protocol.game.incoming.codec.misc.client.SendPingReplyDecoder +import net.rsprot.protocol.game.incoming.codec.misc.client.SoundJingleEndDecoder +import net.rsprot.protocol.game.incoming.codec.misc.client.WindowStatusDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.BugReportDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.ClickWorldMapDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.ClientCheatDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.CloseModalDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.HiscoreRequestDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.IfCrmViewClickDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.MoveGameClickDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.MoveMinimapClickDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.OculusLeaveDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.SendSnapshotDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.SetChatFilterSettingsDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.TeleportDecoder +import net.rsprot.protocol.game.incoming.codec.misc.user.UpdatePlayerModelDecoderOld +import net.rsprot.protocol.game.incoming.codec.npcs.OpNpc1Decoder +import net.rsprot.protocol.game.incoming.codec.npcs.OpNpc2Decoder +import net.rsprot.protocol.game.incoming.codec.npcs.OpNpc3Decoder +import net.rsprot.protocol.game.incoming.codec.npcs.OpNpc4Decoder +import net.rsprot.protocol.game.incoming.codec.npcs.OpNpc5Decoder +import net.rsprot.protocol.game.incoming.codec.npcs.OpNpc6Decoder +import net.rsprot.protocol.game.incoming.codec.npcs.OpNpcTDecoder +import net.rsprot.protocol.game.incoming.codec.objs.OpObj1Decoder +import net.rsprot.protocol.game.incoming.codec.objs.OpObj2Decoder +import net.rsprot.protocol.game.incoming.codec.objs.OpObj3Decoder +import net.rsprot.protocol.game.incoming.codec.objs.OpObj4Decoder +import net.rsprot.protocol.game.incoming.codec.objs.OpObj5Decoder +import net.rsprot.protocol.game.incoming.codec.objs.OpObj6Decoder +import net.rsprot.protocol.game.incoming.codec.objs.OpObjTDecoder +import net.rsprot.protocol.game.incoming.codec.players.OpPlayer1Decoder +import net.rsprot.protocol.game.incoming.codec.players.OpPlayer2Decoder +import net.rsprot.protocol.game.incoming.codec.players.OpPlayer3Decoder +import net.rsprot.protocol.game.incoming.codec.players.OpPlayer4Decoder +import net.rsprot.protocol.game.incoming.codec.players.OpPlayer5Decoder +import net.rsprot.protocol.game.incoming.codec.players.OpPlayer6Decoder +import net.rsprot.protocol.game.incoming.codec.players.OpPlayer7Decoder +import net.rsprot.protocol.game.incoming.codec.players.OpPlayer8Decoder +import net.rsprot.protocol.game.incoming.codec.players.OpPlayerTDecoder +import net.rsprot.protocol.game.incoming.codec.resumed.ResumePCountDialogDecoder +import net.rsprot.protocol.game.incoming.codec.resumed.ResumePNameDialogDecoder +import net.rsprot.protocol.game.incoming.codec.resumed.ResumePObjDialogDecoder +import net.rsprot.protocol.game.incoming.codec.resumed.ResumePStringDialogDecoder +import net.rsprot.protocol.game.incoming.codec.resumed.ResumePauseButtonDecoder +import net.rsprot.protocol.game.incoming.codec.social.FriendListAddDecoder +import net.rsprot.protocol.game.incoming.codec.social.FriendListDelDecoder +import net.rsprot.protocol.game.incoming.codec.social.IgnoreListAddDecoder +import net.rsprot.protocol.game.incoming.codec.social.IgnoreListDelDecoder +import net.rsprot.protocol.message.codec.incoming.MessageDecoderRepository +import net.rsprot.protocol.message.codec.incoming.MessageDecoderRepositoryBuilder + +public object DesktopGameMessageDecoderRepository { + @ExperimentalStdlibApi + public fun build(huffmanCodecProvider: HuffmanCodecProvider): MessageDecoderRepository { + val protRepository = ProtRepository.of() + val builder = + MessageDecoderRepositoryBuilder( + protRepository, + ).apply { + bind(If1ButtonDecoder()) + bind(If3ButtonDecoder(GameClientProt.IF_BUTTON1, 1)) + bind(If3ButtonDecoder(GameClientProt.IF_BUTTON2, 2)) + bind(If3ButtonDecoder(GameClientProt.IF_BUTTON3, 3)) + bind(If3ButtonDecoder(GameClientProt.IF_BUTTON4, 4)) + bind(If3ButtonDecoder(GameClientProt.IF_BUTTON5, 5)) + bind(If3ButtonDecoder(GameClientProt.IF_BUTTON6, 6)) + bind(If3ButtonDecoder(GameClientProt.IF_BUTTON7, 7)) + bind(If3ButtonDecoder(GameClientProt.IF_BUTTON8, 8)) + bind(If3ButtonDecoder(GameClientProt.IF_BUTTON9, 9)) + bind(If3ButtonDecoder(GameClientProt.IF_BUTTON10, 10)) + bind(IfSubOpDecoder()) + bind(IfButtonDDecoder()) + bind(IfButtonTDecoder()) + + bind(OpNpc1Decoder()) + bind(OpNpc2Decoder()) + bind(OpNpc3Decoder()) + bind(OpNpc4Decoder()) + bind(OpNpc5Decoder()) + bind(OpNpc6Decoder()) + bind(OpNpcTDecoder()) + + bind(OpLoc1Decoder()) + bind(OpLoc2Decoder()) + bind(OpLoc3Decoder()) + bind(OpLoc4Decoder()) + bind(OpLoc5Decoder()) + bind(OpLoc6Decoder()) + bind(OpLocTDecoder()) + + bind(OpObj1Decoder()) + bind(OpObj2Decoder()) + bind(OpObj3Decoder()) + bind(OpObj4Decoder()) + bind(OpObj5Decoder()) + bind(OpObj6Decoder()) + bind(OpObjTDecoder()) + + bind(OpPlayer1Decoder()) + bind(OpPlayer2Decoder()) + bind(OpPlayer3Decoder()) + bind(OpPlayer4Decoder()) + bind(OpPlayer5Decoder()) + bind(OpPlayer6Decoder()) + bind(OpPlayer7Decoder()) + bind(OpPlayer8Decoder()) + bind(OpPlayerTDecoder()) + + bind(EventAppletFocusDecoder()) + bind(EventCameraPositionDecoder()) + bind(EventKeyboardDecoder()) + bind(EventMouseScrollDecoder()) + bind(EventMouseMoveDecoder()) + bind(EventNativeMouseMoveDecoder()) + bind(EventMouseClickDecoder()) + bind(EventNativeMouseClickDecoder()) + + bind(ResumePauseButtonDecoder()) + bind(ResumePNameDialogDecoder()) + bind(ResumePStringDialogDecoder()) + bind(ResumePCountDialogDecoder()) + bind(ResumePObjDialogDecoder()) + + bind(FriendChatKickDecoder()) + bind(FriendChatSetRankDecoder()) + bind(FriendChatJoinLeaveDecoder()) + + bind(ClanChannelFullRequestDecoder()) + bind(ClanSettingsFullRequestDecoder()) + bind(ClanChannelKickUserDecoder()) + bind(AffinedClanSettingsAddBannedFromChannelDecoder()) + bind(AffinedClanSettingsSetMutedFromChannelDecoder()) + + bind(FriendListAddDecoder()) + bind(FriendListDelDecoder()) + bind(IgnoreListAddDecoder()) + bind(IgnoreListDelDecoder()) + + bind(MessagePublicDecoder(huffmanCodecProvider)) + bind(MessagePrivateDecoder(huffmanCodecProvider)) + + bind(MoveGameClickDecoder()) + bind(MoveMinimapClickDecoder()) + bind(ClientCheatDecoder()) + bind(SetChatFilterSettingsDecoder()) + bind(ClickWorldMapDecoder()) + bind(OculusLeaveDecoder()) + bind(CloseModalDecoder()) + bind(TeleportDecoder()) + bind(BugReportDecoder()) + bind(SendSnapshotDecoder()) + bind(HiscoreRequestDecoder()) + bind(IfCrmViewClickDecoder()) + bind(UpdatePlayerModelDecoderOld()) + + bind(ConnectionTelemetryDecoder()) + bind(SendPingReplyDecoder()) + bind(DetectModifiedClientDecoder()) + bind(ReflectionCheckReplyDecoder()) + bind(NoTimeoutDecoder()) + bind(IdleDecoder()) + bind(MapBuildCompleteDecoder()) + bind(MembershipPromotionEligibilityDecoder()) + bind(SoundJingleEndDecoder()) + bind(WindowStatusDecoder()) + } + return builder.build() + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/GameClientProt.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/GameClientProt.kt new file mode 100644 index 000000000..d5e198c29 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/GameClientProt.kt @@ -0,0 +1,169 @@ +package net.rsprot.protocol.game.incoming.prot + +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.Prot + +public enum class GameClientProt( + override val opcode: Int, + override val size: Int, +) : ClientProt { + // If buttons + IF_BUTTON(GameClientProtId.IF_BUTTON, 4), + IF_BUTTON1(GameClientProtId.IF_BUTTON1, 8), + IF_BUTTON2(GameClientProtId.IF_BUTTON2, 8), + IF_BUTTON3(GameClientProtId.IF_BUTTON3, 8), + IF_BUTTON4(GameClientProtId.IF_BUTTON4, 8), + IF_BUTTON5(GameClientProtId.IF_BUTTON5, 8), + IF_BUTTON6(GameClientProtId.IF_BUTTON6, 8), + IF_BUTTON7(GameClientProtId.IF_BUTTON7, 8), + IF_BUTTON8(GameClientProtId.IF_BUTTON8, 8), + IF_BUTTON9(GameClientProtId.IF_BUTTON9, 8), + IF_BUTTON10(GameClientProtId.IF_BUTTON10, 8), + IF_SUBOP(GameClientProtId.IF_SUBOP, 10), + IF_BUTTOND(GameClientProtId.IF_BUTTOND, 16), + IF_BUTTONT(GameClientProtId.IF_BUTTONT, 16), + + // Op npc + OPNPC1(GameClientProtId.OPNPC1, 3), + OPNPC2(GameClientProtId.OPNPC2, 3), + OPNPC3(GameClientProtId.OPNPC3, 3), + OPNPC4(GameClientProtId.OPNPC4, 3), + OPNPC5(GameClientProtId.OPNPC5, 3), + OPNPC6(GameClientProtId.OPNPC6, 2), + OPNPCT(GameClientProtId.OPNPCT, 11), + + @Deprecated( + "Deprecated since inventory rework in revision 204, " + + "use all usages go through OPNPCT now.", + replaceWith = ReplaceWith("OPNPCT"), + ) + OPNPCU(GameClientProtId.OPNPCU, 11), + + // Op loc + OPLOC1(GameClientProtId.OPLOC1, 7), + OPLOC2(GameClientProtId.OPLOC2, 7), + OPLOC3(GameClientProtId.OPLOC3, 7), + OPLOC4(GameClientProtId.OPLOC4, 7), + OPLOC5(GameClientProtId.OPLOC5, 7), + OPLOC6(GameClientProtId.OPLOC6, 2), + OPLOCT(GameClientProtId.OPLOCT, 15), + + @Deprecated( + "Deprecated since inventory rework in revision 204, " + + "use all usages go through OPLOCT now.", + replaceWith = ReplaceWith("OPLOCT"), + ) + OPLOCU(GameClientProtId.OPLOCU, 15), + + // Op obj + OPOBJ1(GameClientProtId.OPOBJ1, 7), + OPOBJ2(GameClientProtId.OPOBJ2, 7), + OPOBJ3(GameClientProtId.OPOBJ3, 7), + OPOBJ4(GameClientProtId.OPOBJ4, 7), + OPOBJ5(GameClientProtId.OPOBJ5, 7), + OPOBJ6(GameClientProtId.OPOBJ6, 6), + OPOBJT(GameClientProtId.OPOBJT, 15), + + @Deprecated( + "Deprecated since inventory rework in revision 204, " + + "use all usages go through OPOBJT now.", + replaceWith = ReplaceWith("OPOBJT"), + ) + OPOBJU(GameClientProtId.OPOBJU, 15), + + // Op player + OPPLAYER1(GameClientProtId.OPPLAYER1, 3), + OPPLAYER2(GameClientProtId.OPPLAYER2, 3), + OPPLAYER3(GameClientProtId.OPPLAYER3, 3), + OPPLAYER4(GameClientProtId.OPPLAYER4, 3), + OPPLAYER5(GameClientProtId.OPPLAYER5, 3), + OPPLAYER6(GameClientProtId.OPPLAYER6, 3), + OPPLAYER7(GameClientProtId.OPPLAYER7, 3), + OPPLAYER8(GameClientProtId.OPPLAYER8, 3), + OPPLAYERT(GameClientProtId.OPPLAYERT, 11), + + @Deprecated( + "Deprecated since inventory rework in revision 204, " + + "use all usages go through OPPLAYERT now.", + replaceWith = ReplaceWith("OPPLAYERT"), + ) + OPPLAYERU(GameClientProtId.OPPLAYERU, 11), + + // Op held + @Deprecated( + "Deprecated since revision 211, when a new variant that transmits " + + "the absolute coordinates was introduced for objs on the ground.", + replaceWith = ReplaceWith("IF_BUTTON10"), + ) + OPHELD6(GameClientProtId.OPHELD6, 2), + + // Events + EVENT_APPLET_FOCUS(GameClientProtId.EVENT_APPLET_FOCUS, 1), + EVENT_CAMERA_POSITION(GameClientProtId.EVENT_CAMERA_POSITION, 4), + EVENT_KEYBOARD(GameClientProtId.EVENT_KEYBOARD, Prot.VAR_SHORT), + EVENT_MOUSE_SCROLL(GameClientProtId.EVENT_MOUSE_SCROLL, 2), + EVENT_MOUSE_MOVE(GameClientProtId.EVENT_MOUSE_MOVE, Prot.VAR_BYTE), + EVENT_NATIVE_MOUSE_MOVE(GameClientProtId.EVENT_NATIVE_MOUSE_MOVE, Prot.VAR_BYTE), + EVENT_MOUSE_CLICK(GameClientProtId.EVENT_MOUSE_CLICK, 6), + EVENT_NATIVE_MOUSE_CLICK(GameClientProtId.EVENT_NATIVE_MOUSE_CLICK, 7), + + // Resume events + RESUME_PAUSEBUTTON(GameClientProtId.RESUME_PAUSEBUTTON, 6), + RESUME_P_NAMEDIALOG(GameClientProtId.RESUME_P_NAMEDIALOG, Prot.VAR_BYTE), + RESUME_P_STRINGDIALOG(GameClientProtId.RESUME_P_STRINGDIALOG, Prot.VAR_BYTE), + RESUME_P_COUNTDIALOG(GameClientProtId.RESUME_P_COUNTDIALOG, 4), + RESUME_P_OBJDIALOG(GameClientProtId.RESUME_P_OBJDIALOG, 2), + + // Friend chat packets + FRIENDCHAT_KICK(GameClientProtId.FRIENDCHAT_KICK, Prot.VAR_BYTE), + FRIENDCHAT_SETRANK(GameClientProtId.FRIENDCHAT_SETRANK, Prot.VAR_BYTE), + FRIENDCHAT_JOIN_LEAVE(GameClientProtId.FRIENDCHAT_JOIN_LEAVE, Prot.VAR_BYTE), + + // Clan packets + CLANCHANNEL_FULL_REQUEST(GameClientProtId.CLANCHANNEL_FULL_REQUEST, 1), + CLANSETTINGS_FULL_REQUEST(GameClientProtId.CLANSETTINGS_FULL_REQUEST, 1), + CLANCHANNEL_KICKUSER(GameClientProtId.CLANCHANNEL_KICKUSER, Prot.VAR_BYTE), + AFFINEDCLANSETTINGS_ADDBANNED_FROMCHANNEL( + GameClientProtId.AFFINEDCLANSETTINGS_ADDBANNED_FROMCHANNEL, + Prot.VAR_BYTE, + ), + AFFINEDCLANSETTINGS_SETMUTED_FROMCHANNEL(GameClientProtId.AFFINEDCLANSETTINGS_SETMUTED_FROMCHANNEL, Prot.VAR_BYTE), + + // Socials + FRIENDLIST_ADD(GameClientProtId.FRIENDLIST_ADD, Prot.VAR_BYTE), + FRIENDLIST_DEL(GameClientProtId.FRIENDLIST_DEL, Prot.VAR_BYTE), + IGNORELIST_ADD(GameClientProtId.IGNORELIST_ADD, Prot.VAR_BYTE), + IGNORELIST_DEL(GameClientProtId.IGNORELIST_DEL, Prot.VAR_BYTE), + + // Messaging + MESSAGE_PUBLIC(GameClientProtId.MESSAGE_PUBLIC, Prot.VAR_BYTE), + MESSAGE_PRIVATE(GameClientProtId.MESSAGE_PRIVATE, Prot.VAR_SHORT), + + // Misc. user packets + MOVE_GAMECLICK(GameClientProtId.MOVE_GAMECLICK, Prot.VAR_BYTE), + MOVE_MINIMAPCLICK(GameClientProtId.MOVE_MINIMAPCLICK, Prot.VAR_BYTE), + CLIENT_CHEAT(GameClientProtId.CLIENT_CHEAT, Prot.VAR_BYTE), + SET_CHATFILTERSETTINGS(GameClientProtId.SET_CHATFILTERSETTINGS, 3), + CLICKWORLDMAP(GameClientProtId.CLICKWORLDMAP, 4), + OCULUS_LEAVE(GameClientProtId.OCULUS_LEAVE, 0), + CLOSE_MODAL(GameClientProtId.CLOSE_MODAL, 0), + TELEPORT(GameClientProtId.TELEPORT, 9), + BUG_REPORT(GameClientProtId.BUG_REPORT, Prot.VAR_SHORT), + SEND_SNAPSHOT(GameClientProtId.SEND_SNAPSHOT, Prot.VAR_BYTE), + HISCORE_REQUEST(GameClientProtId.HISCORE_REQUEST, Prot.VAR_BYTE), + IF_CRMVIEW_CLICK(GameClientProtId.IF_CRMVIEW_CLICK, 22), + UPDATE_PLAYER_MODEL(GameClientProtId.UPDATE_PLAYER_MODEL, 26), + UPDATE_PLAYER_MODEL_OLD(GameClientProtId.UPDATE_PLAYER_MODEL_OLD, 13), + + // Misc. client packets + CONNECTION_TELEMETRY(GameClientProtId.CONNECTION_TELEMETRY, Prot.VAR_BYTE), + SEND_PING_REPLY(GameClientProtId.SEND_PING_REPLY, 10), + DETECT_MODIFIED_CLIENT(GameClientProtId.DETECT_MODIFIED_CLIENT, 4), + REFLECTION_CHECK_REPLY(GameClientProtId.REFLECTION_CHECK_REPLY, Prot.VAR_BYTE), + NO_TIMEOUT(GameClientProtId.NO_TIMEOUT, 0), + IDLE(GameClientProtId.IDLE, 0), + MAP_BUILD_COMPLETE(GameClientProtId.MAP_BUILD_COMPLETE, 0), + MEMBERSHIP_PROMOTION_ELIGIBILITY(GameClientProtId.MEMBERSHIP_PROMOTION_ELIGIBILITY, 2), + SOUND_JINGLEEND(GameClientProtId.SOUND_JINGLEEND, 4), + WINDOW_STATUS(GameClientProtId.WINDOW_STATUS, 5), +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/GameClientProtId.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/GameClientProtId.kt new file mode 100644 index 000000000..f8076cfeb --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/incoming/prot/GameClientProtId.kt @@ -0,0 +1,104 @@ +package net.rsprot.protocol.game.incoming.prot + +internal object GameClientProtId { + const val IDLE = 0 + const val FRIENDCHAT_JOIN_LEAVE = 1 + const val IF_BUTTON3 = 2 + const val IF_BUTTON5 = 3 + const val OPLOC5 = 4 + const val EVENT_APPLET_FOCUS = 5 + const val OPLOC3 = 6 + const val IF_BUTTON7 = 7 + const val HISCORE_REQUEST = 8 + const val FRIENDCHAT_SETRANK = 9 + const val OPPLAYER2 = 10 + const val IF_BUTTON4 = 11 + const val WINDOW_STATUS = 12 + const val OPPLAYER7 = 13 + const val IF_BUTTOND = 14 + const val OPLOCU = 15 + const val OPOBJ3 = 16 + const val OPNPC6 = 17 + const val IF_BUTTON = 18 + const val OPOBJ6 = 19 + const val OPNPC4 = 20 + const val IF_BUTTON6 = 21 + const val SEND_PING_REPLY = 22 + const val IF_BUTTON8 = 23 + const val OPPLAYER6 = 24 + const val IGNORELIST_DEL = 25 + const val CONNECTION_TELEMETRY = 26 + const val MESSAGE_PRIVATE = 27 + const val CLOSE_MODAL = 28 + const val OCULUS_LEAVE = 29 + const val OPOBJ5 = 30 + const val AFFINEDCLANSETTINGS_SETMUTED_FROMCHANNEL = 31 + const val TELEPORT = 32 + const val OPNPCT = 33 + const val UPDATE_PLAYER_MODEL_OLD = 34 + const val RESUME_P_NAMEDIALOG = 35 + const val IF_SUBOP = 36 + const val IF_BUTTONT = 37 + const val EVENT_MOUSE_MOVE = 38 + const val EVENT_MOUSE_CLICK = 39 + const val EVENT_NATIVE_MOUSE_MOVE = 40 + const val OPLOC4 = 41 + const val OPLOC1 = 42 + const val OPLOC2 = 43 + const val OPNPC1 = 44 + const val REFLECTION_CHECK_REPLY = 45 + const val FRIENDLIST_DEL = 46 + const val OPNPC3 = 47 + const val SEND_SNAPSHOT = 48 + const val IF_BUTTON1 = 49 + const val NO_TIMEOUT = 50 + const val OPPLAYER8 = 51 + const val OPPLAYER5 = 52 + const val OPHELD6 = 53 + const val IF_BUTTON2 = 54 + const val OPPLAYERU = 55 + const val OPPLAYER1 = 56 + const val BUG_REPORT = 57 + const val SOUND_JINGLEEND = 58 + const val OPLOCT = 59 + const val FRIENDCHAT_KICK = 60 + const val MESSAGE_PUBLIC = 61 + const val IF_BUTTON10 = 62 + const val MOVE_GAMECLICK = 63 + const val OPNPC5 = 64 + const val OPNPCU = 65 + const val OPPLAYERT = 66 + const val EVENT_CAMERA_POSITION = 67 + const val CLANCHANNEL_FULL_REQUEST = 68 + const val OPOBJT = 69 + const val EVENT_MOUSE_SCROLL = 70 + const val RESUME_P_OBJDIALOG = 71 + const val EVENT_KEYBOARD = 72 + const val IF_BUTTON9 = 73 + const val IGNORELIST_ADD = 74 + const val RESUME_P_STRINGDIALOG = 75 + const val SET_CHATFILTERSETTINGS = 76 + const val OPOBJ2 = 77 + const val CLANSETTINGS_FULL_REQUEST = 78 + const val OPPLAYER3 = 79 + const val CLIENT_CHEAT = 80 + const val DETECT_MODIFIED_CLIENT = 81 + const val CLANCHANNEL_KICKUSER = 82 + const val OPNPC2 = 83 + const val MAP_BUILD_COMPLETE = 84 + const val OPOBJ4 = 85 + const val MEMBERSHIP_PROMOTION_ELIGIBILITY = 86 + const val UPDATE_PLAYER_MODEL = 87 + const val OPPLAYER4 = 88 + const val OPOBJU = 89 + const val OPOBJ1 = 90 + const val RESUME_P_COUNTDIALOG = 91 + const val FRIENDLIST_ADD = 92 + const val RESUME_PAUSEBUTTON = 93 + const val CLICKWORLDMAP = 94 + const val IF_CRMVIEW_CLICK = 95 + const val MOVE_MINIMAPCLICK = 96 + const val EVENT_NATIVE_MOUSE_CLICK = 97 + const val AFFINEDCLANSETTINGS_ADDBANNED_FROMCHANNEL = 98 + const val OPLOC6 = 99 +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamLookAtEasedCoordEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamLookAtEasedCoordEncoder.kt new file mode 100644 index 000000000..f7ce1f48b --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamLookAtEasedCoordEncoder.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamLookAtEasedCoord +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamLookAtEasedCoordEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_LOOKAT_EASED_COORD + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamLookAtEasedCoord, + ) { + buffer.p1(message.destinationXInBuildArea) + buffer.p1(message.destinationZInBuildArea) + buffer.p2(message.height) + buffer.p2(message.cycles) + buffer.p1(message.easing.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamLookAtEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamLookAtEncoder.kt new file mode 100644 index 000000000..16bceeafa --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamLookAtEncoder.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamLookAt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamLookAtEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_LOOKAT + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamLookAt, + ) { + buffer.p1(message.destinationXInBuildArea) + buffer.p1(message.destinationZInBuildArea) + buffer.p2(message.height) + buffer.p1(message.rate) + buffer.p1(message.rate2) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamModeEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamModeEncoder.kt new file mode 100644 index 000000000..39f750a54 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamModeEncoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamMode +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class CamModeEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_MODE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamMode, + ) { + buffer.p1(message.mode) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToArc.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToArc.kt new file mode 100644 index 000000000..028d1260f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToArc.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamMoveToArc +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamMoveToArc : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_MOVETO_ARC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamMoveToArc, + ) { + buffer.p1(message.destinationXInBuildArea) + buffer.p1(message.destinationZInBuildArea) + buffer.p2(message.height) + buffer.p1(message.centerXInBuildArea) + buffer.p1(message.centerZInBuildArea) + buffer.p2(message.cycles) + buffer.pboolean(message.ignoreTerrain) + buffer.p1(message.easing.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToCyclesEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToCyclesEncoder.kt new file mode 100644 index 000000000..8365a9e91 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToCyclesEncoder.kt @@ -0,0 +1,27 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamMoveToCycles +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamMoveToCyclesEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_MOVETO_CYCLES + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamMoveToCycles, + ) { + buffer.p1(message.destinationXInBuildArea) + buffer.p1(message.destinationZInBuildArea) + buffer.p2(message.height) + buffer.p2(message.cycles) + buffer.pboolean(message.ignoreTerrain) + buffer.p1(message.easing.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToEncoder.kt new file mode 100644 index 000000000..7cd0818b5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamMoveToEncoder.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamMoveTo +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamMoveToEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_MOVETO + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamMoveTo, + ) { + buffer.p1(message.destinationXInBuildArea) + buffer.p1(message.destinationZInBuildArea) + buffer.p2(message.height) + buffer.p1(message.rate) + buffer.p1(message.rate2) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamResetEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamResetEncoder.kt new file mode 100644 index 000000000..e16374e3d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamResetEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamReset +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamResetEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_RESET +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamRotateByEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamRotateByEncoder.kt new file mode 100644 index 000000000..99d8ca046 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamRotateByEncoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamRotateBy +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamRotateByEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_ROTATEBY + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamRotateBy, + ) { + buffer.p2(message.yaw) + buffer.p2(message.pitch) + buffer.p2(message.cycles) + buffer.p1(message.easing.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamRotateToEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamRotateToEncoder.kt new file mode 100644 index 000000000..b3b52cc15 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamRotateToEncoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamRotateTo +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamRotateToEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_ROTATETO + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamRotateTo, + ) { + buffer.p2(message.yaw) + buffer.p2(message.pitch) + buffer.p2(message.cycles) + buffer.p1(message.easing.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamShakeEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamShakeEncoder.kt new file mode 100644 index 000000000..6b451926b --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamShakeEncoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamShake +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamShakeEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_SHAKE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamShake, + ) { + buffer.p1(message.axis) + buffer.p1(message.random) + buffer.p1(message.amplitude) + buffer.p1(message.rate) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamSmoothResetEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamSmoothResetEncoder.kt new file mode 100644 index 000000000..74d9d63c3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamSmoothResetEncoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamSmoothReset +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamSmoothResetEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_SMOOTHRESET + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamSmoothReset, + ) { + buffer.p1(message.cameraMoveConstantSpeed) + buffer.p1(message.cameraMoveProportionalSpeed) + buffer.p1(message.cameraLookConstantSpeed) + buffer.p1(message.cameraLookProportionalSpeed) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamTargetEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamTargetEncoder.kt new file mode 100644 index 000000000..9a2ef8794 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamTargetEncoder.kt @@ -0,0 +1,38 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamTarget +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamTargetEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_TARGET + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamTarget, + ) { + when (val type = message.type) { + is CamTarget.PlayerCamTarget -> { + buffer.p1(0) + buffer.p2(type.index) + buffer.p2(-1) + } + is CamTarget.NpcCamTarget -> { + buffer.p1(1) + buffer.p2(type.index) + buffer.p2(-1) + } + is CamTarget.WorldEntityTarget -> { + buffer.p1(2) + buffer.p2(type.index) + buffer.p2(type.cameraLockedPlayerIndex) + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamTargetOldEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamTargetOldEncoder.kt new file mode 100644 index 000000000..96a51a3af --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/CamTargetOldEncoder.kt @@ -0,0 +1,35 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.CamTargetOld +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class CamTargetOldEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CAM_TARGET_OLD + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: CamTargetOld, + ) { + when (val type = message.type) { + is CamTargetOld.PlayerCamTarget -> { + buffer.p1(0) + buffer.p2(type.index) + } + is CamTargetOld.NpcCamTarget -> { + buffer.p1(1) + buffer.p2(type.index) + } + is CamTargetOld.WorldEntityTarget -> { + buffer.p1(2) + buffer.p2(type.index) + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/OculusSyncEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/OculusSyncEncoder.kt new file mode 100644 index 000000000..4a8bff0c7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/camera/OculusSyncEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.camera + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.camera.OculusSync +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class OculusSyncEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.OCULUS_SYNC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: OculusSync, + ) { + buffer.p4(message.value) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanChannelDeltaEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanChannelDeltaEncoder.kt new file mode 100644 index 000000000..b523f94f5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanChannelDeltaEncoder.kt @@ -0,0 +1,87 @@ +package net.rsprot.protocol.game.outgoing.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.clan.ClanChannelDelta +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ClanChannelDeltaEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CLANCHANNEL_DELTA + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: ClanChannelDelta, + ) { + buffer.p1(message.clanType) + buffer.p8(message.clanHash) + buffer.p8(message.updateNum) + for (event in message.events) { + when (event) { + is ClanChannelDelta.AddUserEvent -> { + buffer.p1(1) + buffer.p1(255) + buffer.pjstrnull(event.name) + buffer.p2(event.world) + buffer.p1(event.rank) + + // Unused in all clients, including RS3 + buffer.p8(0) + } + is ClanChannelDelta.DeleteUserEvent -> { + buffer.p1(3) + buffer.p2(event.index) + + // Unused in all clients, including RS3 + buffer.p1(0) + buffer.p1(255) + } + is ClanChannelDelta.UpdateBaseSettingsEvent -> { + buffer.p1(4) + val name = event.clanName + buffer.pjstrnull(name) + if (name != null) { + // Unused in all clients, including RS3 + buffer.p1(0) + + buffer.p1(event.talkRank) + buffer.p1(event.kickRank) + } + } + is ClanChannelDelta.UpdateUserDetailsEvent -> { + buffer.p1(2) + buffer.p2(event.index) + buffer.p1(event.rank) + buffer.p2(event.world) + + // Unused in all clients, including RS3 + buffer.p8(0) + + buffer.pjstr(event.name) + } + is ClanChannelDelta.UpdateUserDetailsV2Event -> { + buffer.p1(5) + + // Unused in all clients, including RS3 + buffer.p1(0) + buffer.p2(event.index) + buffer.p1(event.rank) + buffer.p2(event.world) + + // Unused in all clients, including RS3 + buffer.p8(0) + + buffer.pjstr(event.name) + + // Unused in all clients, including RS3 + buffer.p1(0) + } + } + } + buffer.p1(0) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanChannelFullEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanChannelFullEncoder.kt new file mode 100644 index 000000000..176818ce1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanChannelFullEncoder.kt @@ -0,0 +1,58 @@ +package net.rsprot.protocol.game.outgoing.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.Base37 +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.clan.ClanChannelFull +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ClanChannelFullEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CLANCHANNEL_FULL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: ClanChannelFull, + ) { + buffer.p1(message.clanType) + when (val update = message.update) { + is ClanChannelFull.JoinUpdate -> { + buffer.p1(update.flags) + val version = update.version + if (update.flags and ClanChannelFull.FLAG_HAS_VERSION != 0) { + buffer.p1(version) + } + buffer.p8(update.clanHash) + buffer.p8(update.updateNum) + buffer.pjstr(update.clanName) + buffer.pboolean(update.discardedBoolean) + buffer.p1(update.kickRank) + buffer.p1(update.talkRank) + val members = update.members + buffer.p2(members.size) + val base37 = update.flags and ClanChannelFull.FLAG_USE_BASE_37_NAMES != 0 + val displayNames = update.flags and ClanChannelFull.FLAG_USE_DISPLAY_NAMES != 0 + for (member in members) { + if (base37) { + buffer.p8(Base37.encode(member.name)) + } + if (displayNames) { + buffer.pjstr(member.name) + } + buffer.p1(member.rank) + buffer.p2(member.world) + if (version >= 3) { + buffer.pboolean(member.discardedBoolean) + } + } + } + ClanChannelFull.LeaveUpdate -> { + // No-op + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanSettingsDeltaEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanSettingsDeltaEncoder.kt new file mode 100644 index 000000000..a057334c7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanSettingsDeltaEncoder.kt @@ -0,0 +1,127 @@ +package net.rsprot.protocol.game.outgoing.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.clan.ClanSettingsDelta +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ClanSettingsDeltaEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CLANSETTINGS_DELTA + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: ClanSettingsDelta, + ) { + buffer.p1(message.clanType) + buffer.p8(message.owner) + buffer.p4(message.updateNum) + val updates = message.updates + for (update in updates) { + when (update) { + is ClanSettingsDelta.SetClanOwnerUpdate -> { + buffer.p1(15) + buffer.p2(update.index) + } + is ClanSettingsDelta.AddBannedUpdate -> { + buffer.p1(3) + val hash = update.hash + if (hash and 0xFF != 0xFF.toLong()) { + buffer.p8(hash) + } else { + buffer.p1(0xFF) + } + buffer.pjstrnull(update.name) + } + is ClanSettingsDelta.AddMemberV1Update -> { + buffer.p1(1) + val hash = update.hash + if (hash and 0xFF != 0xFF.toLong()) { + buffer.p8(hash) + } else { + buffer.p1(0xFF) + } + buffer.pjstrnull(update.name) + } + is ClanSettingsDelta.AddMemberV2Update -> { + buffer.p1(13) + val hash = update.hash + if (hash and 0xFF != 0xFF.toLong()) { + buffer.p8(hash) + } else { + buffer.p1(0xFF) + } + buffer.pjstrnull(update.name) + buffer.p2(update.joinRuneDay) + } + is ClanSettingsDelta.BaseSettingsUpdate -> { + buffer.p1(4) + buffer.p1(if (update.allowUnaffined) 1 else 0) + buffer.p1(update.talkRank) + buffer.p1(update.kickRank) + buffer.p1(update.lootshareRank) + buffer.p1(update.coinshareRank) + } + is ClanSettingsDelta.DeleteBannedUpdate -> { + buffer.p1(6) + buffer.p2(update.index) + } + is ClanSettingsDelta.DeleteMemberUpdate -> { + buffer.p1(5) + buffer.p2(update.index) + } + is ClanSettingsDelta.SetClanNameUpdate -> { + buffer.p1(12) + buffer.pjstr(update.clanName) + + // Unused in all clients, including RS3 + buffer.p4(0) + } + is ClanSettingsDelta.SetIntSettingUpdate -> { + buffer.p1(8) + buffer.p4(update.setting) + buffer.p4(update.value) + } + is ClanSettingsDelta.SetLongSettingUpdate -> { + buffer.p1(9) + buffer.p4(update.setting) + buffer.p8(update.value) + } + is ClanSettingsDelta.SetMemberExtraInfoUpdate -> { + buffer.p1(7) + buffer.p2(update.index) + buffer.p4(update.value) + buffer.p1(update.startBit) + buffer.p1(update.endBit) + } + is ClanSettingsDelta.SetMemberMutedUpdate -> { + buffer.p1(14) + buffer.p2(update.index) + buffer.p1(if (update.muted) 1 else 0) + } + is ClanSettingsDelta.SetMemberRankUpdate -> { + buffer.p1(2) + buffer.p2(update.index) + buffer.p1(update.rank) + } + is ClanSettingsDelta.SetStringSettingUpdate -> { + buffer.p1(10) + buffer.p4(update.setting) + buffer.pjstr(update.value) + } + is ClanSettingsDelta.SetVarbitSettingUpdate -> { + buffer.p1(11) + buffer.p4(update.setting) + buffer.p4(update.value) + buffer.p1(update.startBit) + buffer.p1(update.endBit) + } + } + } + buffer.p1(0) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanSettingsFullEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanSettingsFullEncoder.kt new file mode 100644 index 000000000..d11335ded --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/ClanSettingsFullEncoder.kt @@ -0,0 +1,85 @@ +package net.rsprot.protocol.game.outgoing.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.clan.ClanSettingsFull +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ClanSettingsFullEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CLANSETTINGS_FULL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: ClanSettingsFull, + ) { + buffer.p1(message.clanType) + val update = message.update + when (update) { + is ClanSettingsFull.JoinUpdate -> { + // Send version always as 6, as it contains the most information + buffer.p1(6) + buffer.p1(update.flags) + buffer.p4(update.updateNum) + buffer.p4(update.creationTime) + buffer.p2(update.affinedMembers.size) + buffer.p1(update.bannedMembers.size) + buffer.pjstr(update.clanName) + + // Unused in all clients, including RS3 + buffer.p4(0) + buffer.p1(if (update.allowUnaffined) 1 else 0) + buffer.p1(update.talkRank) + buffer.p1(update.kickRank) + buffer.p1(update.lootshareRank) + buffer.p1(update.coinshareRank) + val hasAffinedHashes = update.flags and ClanSettingsFull.FLAG_HAS_AFFINED_HASHES != 0 + val hasAffinedDisplayNames = update.flags and ClanSettingsFull.FLAG_HAS_AFFINED_DISPLAY_NAMES != 0 + for (affined in update.affinedMembers) { + if (hasAffinedHashes) { + buffer.p8(affined.hash) + } + if (hasAffinedDisplayNames) { + buffer.pjstrnull(affined.name) + } + buffer.p1(affined.rank) + buffer.p4(affined.extraInfo) + buffer.p2(affined.joinRuneDay) + buffer.p1(if (affined.muted) 1 else 0) + } + for (banned in update.bannedMembers) { + if (hasAffinedHashes) { + buffer.p8(banned.hash) + } + if (hasAffinedDisplayNames) { + buffer.pjstrnull(banned.name) + } + } + buffer.p2(update.settings.size) + for (setting in update.settings) { + when (setting) { + is ClanSettingsFull.IntClanSetting -> { + buffer.p4(setting.id) + buffer.p4(setting.value) + } + is ClanSettingsFull.LongClanSetting -> { + buffer.p4(setting.id or (1 shl 30)) + buffer.p8(setting.value) + } + is ClanSettingsFull.StringClanSetting -> { + buffer.p4(setting.id or (2 shl 30)) + buffer.pjstr(setting.value) + } + } + } + } + ClanSettingsFull.LeaveUpdate -> { + // No-op + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/MessageClanChannelEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/MessageClanChannelEncoder.kt new file mode 100644 index 000000000..5fe3fac05 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/MessageClanChannelEncoder.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.outgoing.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.clan.MessageClanChannel +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MessageClanChannelEncoder( + private val huffmanCodecProvider: HuffmanCodecProvider, +) : MessageEncoder { + override val prot: ServerProt = GameServerProt.MESSAGE_CLANCHANNEL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MessageClanChannel, + ) { + buffer.p1(message.clanType) + buffer.pjstr(message.name) + buffer.p2(message.worldId) + buffer.p3(message.worldMessageCounter) + buffer.p1(message.chatCrownType) + val huffman = huffmanCodecProvider.provide() + huffman.encode(buffer, message.message) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/MessageClanChannelSystemEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/MessageClanChannelSystemEncoder.kt new file mode 100644 index 000000000..9b7852a57 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/MessageClanChannelSystemEncoder.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.clan.MessageClanChannelSystem +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MessageClanChannelSystemEncoder( + private val huffmanCodecProvider: HuffmanCodecProvider, +) : MessageEncoder { + override val prot: ServerProt = GameServerProt.MESSAGE_CLANCHANNEL_SYSTEM + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MessageClanChannelSystem, + ) { + buffer.p1(message.clanType) + buffer.p2(message.worldId) + buffer.p3(message.worldMessageCounter) + val huffman = huffmanCodecProvider.provide() + huffman.encode(buffer, message.message) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanDisableEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanDisableEncoder.kt new file mode 100644 index 000000000..d34e49421 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanDisableEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.clan + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.clan.VarClanDisable +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class VarClanDisableEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.VARCLAN_DISABLE +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanEnableEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanEnableEncoder.kt new file mode 100644 index 000000000..f46165865 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanEnableEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.clan + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.clan.VarClanEnable +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class VarClanEnableEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.VARCLAN_ENABLE +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanEncoder.kt new file mode 100644 index 000000000..2418c28cd --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/clan/VarClanEncoder.kt @@ -0,0 +1,35 @@ +package net.rsprot.protocol.game.outgoing.codec.clan + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.clan.VarClan +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class VarClanEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.VARCLAN + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: VarClan, + ) { + buffer.p2(message.id) + // Note that there is another clause for 'serializable' types, + // however none currently exist. + when (val value = message.value) { + is VarClan.VarClanIntData -> { + buffer.p4(value.value) + } + is VarClan.VarClanLongData -> { + buffer.p8(value.value) + } + is VarClan.VarClanStringData -> { + buffer.pjstr2(value.value) + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/MessageFriendChannelEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/MessageFriendChannelEncoder.kt new file mode 100644 index 000000000..8864c589c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/MessageFriendChannelEncoder.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.outgoing.codec.friendchat + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.friendchat.MessageFriendChannel +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MessageFriendChannelEncoder( + private val huffmanCodecProvider: HuffmanCodecProvider, +) : MessageEncoder { + override val prot: ServerProt = GameServerProt.MESSAGE_FRIENDCHANNEL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MessageFriendChannel, + ) { + buffer.pjstr(message.sender) + buffer.p8(message.channelNameBase37) + buffer.p2(message.worldId) + buffer.p3(message.worldMessageCounter) + buffer.p1(message.chatCrownType) + val huffman = huffmanCodecProvider.provide() + huffman.encode(buffer, message.message) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelFullV1Encoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelFullV1Encoder.kt new file mode 100644 index 000000000..ec16b2e75 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelFullV1Encoder.kt @@ -0,0 +1,38 @@ +package net.rsprot.protocol.game.outgoing.codec.friendchat + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.friendchat.UpdateFriendChatChannelFullV1 +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdateFriendChatChannelFullV1Encoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_FRIENDCHAT_CHANNEL_FULL_V1 + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateFriendChatChannelFullV1, + ) { + when (val update = message.updateType) { + is UpdateFriendChatChannelFullV1.JoinUpdate -> { + buffer.pjstr(update.channelOwner) + buffer.p8(update.channelNameBase37) + buffer.p1(update.kickRank) + buffer.p1(update.entries.size) + for (entry in update.entries) { + buffer.pjstr(entry.name) + buffer.p2(entry.worldId) + buffer.p1(entry.rank) + buffer.pjstr(entry.worldName) + } + } + UpdateFriendChatChannelFullV1.LeaveUpdate -> { + // No-op, no updates + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelFullV2Encoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelFullV2Encoder.kt new file mode 100644 index 000000000..25db0dbb5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelFullV2Encoder.kt @@ -0,0 +1,38 @@ +package net.rsprot.protocol.game.outgoing.codec.friendchat + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.friendchat.UpdateFriendChatChannelFullV2 +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdateFriendChatChannelFullV2Encoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_FRIENDCHAT_CHANNEL_FULL_V2 + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateFriendChatChannelFullV2, + ) { + when (val update = message.updateType) { + is UpdateFriendChatChannelFullV2.JoinUpdate -> { + buffer.pjstr(update.channelOwner) + buffer.p8(update.channelNameBase37) + buffer.p1(update.kickRank) + buffer.pSmart1or2null(update.entries.size) + for (entry in update.entries) { + buffer.pjstr(entry.name) + buffer.p2(entry.worldId) + buffer.p1(entry.rank) + buffer.pjstr(entry.worldName) + } + } + UpdateFriendChatChannelFullV2.LeaveUpdate -> { + // No-op, empty packet + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelSingleUserEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelSingleUserEncoder.kt new file mode 100644 index 000000000..31cdd32a8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/friendchat/UpdateFriendChatChannelSingleUserEncoder.kt @@ -0,0 +1,28 @@ +package net.rsprot.protocol.game.outgoing.codec.friendchat + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.friendchat.UpdateFriendChatChannelSingleUser +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdateFriendChatChannelSingleUserEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_FRIENDCHAT_CHANNEL_SINGLEUSER + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateFriendChatChannelSingleUser, + ) { + val user = message.user + buffer.pjstr(user.name) + buffer.p2(user.worldId) + buffer.p1(user.rank) + if (user is UpdateFriendChatChannelSingleUser.AddedFriendChatUser) { + buffer.pjstr(user.worldName) + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfClearInvEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfClearInvEncoder.kt new file mode 100644 index 000000000..d5b12acd9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfClearInvEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfClearInv +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedId + +public class IfClearInvEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_CLEARINV + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfClearInv, + ) { + buffer.pCombinedId(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfCloseSubEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfCloseSubEncoder.kt new file mode 100644 index 000000000..4967ad624 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfCloseSubEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfCloseSub +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent +import net.rsprot.protocol.util.pCombinedId + +@Consistent +public class IfCloseSubEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_CLOSESUB + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfCloseSub, + ) { + buffer.pCombinedId(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfMoveSubEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfMoveSubEncoder.kt new file mode 100644 index 000000000..9b1604f21 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfMoveSubEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfMoveSub +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt1 +import net.rsprot.protocol.util.pCombinedIdAlt3 + +public class IfMoveSubEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_MOVESUB + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfMoveSub, + ) { + buffer.pCombinedIdAlt3(message.sourceCombinedId) + buffer.pCombinedIdAlt1(message.destinationCombinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfOpenSubEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfOpenSubEncoder.kt new file mode 100644 index 000000000..8f34c8185 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfOpenSubEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfOpenSub +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt2 + +public class IfOpenSubEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_OPENSUB + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfOpenSub, + ) { + buffer.p2Alt3(message.interfaceId) + buffer.pCombinedIdAlt2(message.destinationCombinedId) + buffer.p1Alt3(message.type) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfOpenTopEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfOpenTopEncoder.kt new file mode 100644 index 000000000..582f6dc91 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfOpenTopEncoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfOpenTop +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class IfOpenTopEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_OPENTOP + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfOpenTop, + ) { + buffer.p2(message.interfaceId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfResyncEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfResyncEncoder.kt new file mode 100644 index 000000000..bc49daa8c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfResyncEncoder.kt @@ -0,0 +1,35 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfResync +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent +import net.rsprot.protocol.util.pCombinedId + +@Consistent +public class IfResyncEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_RESYNC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfResync, + ) { + buffer.p2(message.topLevelInterface) + buffer.p2(message.subInterfaces.size) + for (subInterface in message.subInterfaces) { + buffer.pCombinedId(subInterface.destinationCombinedId) + buffer.p2(subInterface.interfaceId) + buffer.p1(subInterface.type) + } + for (events in message.events) { + buffer.pCombinedId(events.combinedId) + buffer.p2(events.start) + buffer.p2(events.end) + buffer.p4(events.events) + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetAngleEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetAngleEncoder.kt new file mode 100644 index 000000000..2c93cb29f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetAngleEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetAngle +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt2 + +public class IfSetAngleEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETANGLE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetAngle, + ) { + buffer.p2Alt2(message.angleY) + buffer.p2Alt1(message.zoom) + buffer.p2(message.angleX) + buffer.pCombinedIdAlt2(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetAnimEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetAnimEncoder.kt new file mode 100644 index 000000000..e4259b677 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetAnimEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetAnim +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt3 + +public class IfSetAnimEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETANIM + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetAnim, + ) { + buffer.p2(message.anim) + buffer.pCombinedIdAlt3(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetColourEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetColourEncoder.kt new file mode 100644 index 000000000..256992fe2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetColourEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetColour +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt1 + +public class IfSetColourEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETCOLOUR + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetColour, + ) { + buffer.p2(message.colour15BitPacked) + buffer.pCombinedIdAlt1(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetEventsEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetEventsEncoder.kt new file mode 100644 index 000000000..216c3ea1e --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetEventsEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetEvents +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt3 + +public class IfSetEventsEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETEVENTS + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetEvents, + ) { + buffer.p2(message.start) + buffer.p2Alt1(message.end) + buffer.pCombinedIdAlt3(message.combinedId) + buffer.p4(message.events) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetHideEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetHideEncoder.kt new file mode 100644 index 000000000..0a09addc5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetHideEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetHide +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt2 + +public class IfSetHideEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETHIDE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetHide, + ) { + buffer.p1Alt3(if (message.hidden) 1 else 0) + buffer.pCombinedIdAlt2(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetModelEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetModelEncoder.kt new file mode 100644 index 000000000..bdcbb82c9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetModelEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetModel +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedId + +public class IfSetModelEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETMODEL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetModel, + ) { + buffer.pCombinedId(message.combinedId) + buffer.p2Alt1(message.model) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetNpcHeadActiveEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetNpcHeadActiveEncoder.kt new file mode 100644 index 000000000..d7f4b03e7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetNpcHeadActiveEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetNpcHeadActive +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt2 + +public class IfSetNpcHeadActiveEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETNPCHEAD_ACTIVE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetNpcHeadActive, + ) { + buffer.pCombinedIdAlt2(message.combinedId) + buffer.p2Alt2(message.index) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetNpcHeadEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetNpcHeadEncoder.kt new file mode 100644 index 000000000..8d52dacd7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetNpcHeadEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetNpcHead +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt1 + +public class IfSetNpcHeadEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETNPCHEAD + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetNpcHead, + ) { + buffer.p2Alt1(message.npc) + buffer.pCombinedIdAlt1(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetObjectEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetObjectEncoder.kt new file mode 100644 index 000000000..3f7919197 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetObjectEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetObject +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt1 + +public class IfSetObjectEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETOBJECT + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetObject, + ) { + buffer.p2Alt1(message.obj) + buffer.p4Alt2(message.count) + buffer.pCombinedIdAlt1(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerHeadEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerHeadEncoder.kt new file mode 100644 index 000000000..78520c083 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerHeadEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetPlayerHead +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt2 + +public class IfSetPlayerHeadEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETPLAYERHEAD + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetPlayerHead, + ) { + buffer.pCombinedIdAlt2(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelBaseColourEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelBaseColourEncoder.kt new file mode 100644 index 000000000..baf71c8eb --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelBaseColourEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetPlayerModelBaseColour +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt1 + +public class IfSetPlayerModelBaseColourEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETPLAYERMODEL_BASECOLOUR + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetPlayerModelBaseColour, + ) { + buffer.p1Alt1(message.colour) + buffer.p1Alt1(message.index) + buffer.pCombinedIdAlt1(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelBodyTypeEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelBodyTypeEncoder.kt new file mode 100644 index 000000000..1c82899b1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelBodyTypeEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetPlayerModelBodyType +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt3 + +public class IfSetPlayerModelBodyTypeEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETPLAYERMODEL_BODYTYPE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetPlayerModelBodyType, + ) { + buffer.p1Alt3(message.bodyType) + buffer.pCombinedIdAlt3(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelObjEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelObjEncoder.kt new file mode 100644 index 000000000..0494bccbd --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelObjEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetPlayerModelObj +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt2 + +public class IfSetPlayerModelObjEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETPLAYERMODEL_OBJ + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetPlayerModelObj, + ) { + buffer.p4Alt2(message.obj) + buffer.pCombinedIdAlt2(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelSelfEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelSelfEncoder.kt new file mode 100644 index 000000000..c7ebdd13f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPlayerModelSelfEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetPlayerModelSelf +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt1 + +public class IfSetPlayerModelSelfEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETPLAYERMODEL_SELF + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetPlayerModelSelf, + ) { + // The boolean is inverted client-sided, it's more of a "skip copying" + buffer.pCombinedIdAlt1(message.combinedId) + buffer.p1Alt2(if (message.copyObjs) 0 else 1) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPositionEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPositionEncoder.kt new file mode 100644 index 000000000..fe971d33b --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetPositionEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetPosition +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt2 + +public class IfSetPositionEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETPOSITION + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetPosition, + ) { + buffer.p2Alt1(message.y) + buffer.pCombinedIdAlt2(message.combinedId) + buffer.p2Alt2(message.x) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetRotateSpeedEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetRotateSpeedEncoder.kt new file mode 100644 index 000000000..438752658 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetRotateSpeedEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetRotateSpeed +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedId + +public class IfSetRotateSpeedEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETROTATESPEED + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetRotateSpeed, + ) { + buffer.p2Alt2(message.ySpeed) + buffer.pCombinedId(message.combinedId) + buffer.p2Alt1(message.xSpeed) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetScrollPosEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetScrollPosEncoder.kt new file mode 100644 index 000000000..f52cd68c6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetScrollPosEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetScrollPos +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedIdAlt1 + +public class IfSetScrollPosEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETSCROLLPOS + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetScrollPos, + ) { + buffer.pCombinedIdAlt1(message.combinedId) + buffer.p2Alt2(message.scrollPos) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetTextEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetTextEncoder.kt new file mode 100644 index 000000000..c0fc7a261 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/interfaces/IfSetTextEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.interfaces + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.interfaces.IfSetText +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedId + +public class IfSetTextEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.IF_SETTEXT + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: IfSetText, + ) { + buffer.pjstr(message.text) + buffer.pCombinedId(message.combinedId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvFullEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvFullEncoder.kt new file mode 100644 index 000000000..dca48ea20 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvFullEncoder.kt @@ -0,0 +1,40 @@ +package net.rsprot.protocol.game.outgoing.codec.inv + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.inv.InventoryObject +import net.rsprot.protocol.game.outgoing.inv.UpdateInvFull +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.util.pCombinedId + +public class UpdateInvFullEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_INV_FULL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateInvFull, + ) { + buffer.pCombinedId(message.combinedId) + buffer.p2(message.inventoryId) + val capacity = message.capacity + buffer.p2(capacity) + for (i in 0..= 255) { + buffer.p4Alt2(count) + } + buffer.p2Alt3(obj.id + 1) + } + message.returnInventory() + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvPartialEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvPartialEncoder.kt new file mode 100644 index 000000000..3156f62b6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvPartialEncoder.kt @@ -0,0 +1,40 @@ +package net.rsprot.protocol.game.outgoing.codec.inv + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.inv.UpdateInvPartial +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent +import net.rsprot.protocol.util.pCombinedId + +@Consistent +public class UpdateInvPartialEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_INV_PARTIAL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateInvPartial, + ) { + buffer.pCombinedId(message.combinedId) + buffer.p2(message.inventoryId) + for (i in 0..= 0xFF) { + buffer.p4(count) + } + } + message.returnInventory() + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvStopTransmitEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvStopTransmitEncoder.kt new file mode 100644 index 000000000..b5bcde758 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/inv/UpdateInvStopTransmitEncoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.game.outgoing.codec.inv + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.inv.UpdateInvStopTransmit +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class UpdateInvStopTransmitEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_INV_STOPTRANSMIT + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateInvStopTransmit, + ) { + buffer.p2Alt2(message.inventoryId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutEncoder.kt new file mode 100644 index 000000000..8572b07d9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.logout + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.logout.Logout +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class LogoutEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.LOGOUT +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutTransferEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutTransferEncoder.kt new file mode 100644 index 000000000..b911c7732 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutTransferEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.logout + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.logout.LogoutTransfer +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class LogoutTransferEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.LOGOUT_TRANSFER + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: LogoutTransfer, + ) { + buffer.pjstr(message.host) + buffer.p2(message.id) + buffer.p4(message.properties) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutWithReasonEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutWithReasonEncoder.kt new file mode 100644 index 000000000..b20476615 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/logout/LogoutWithReasonEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.logout + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.logout.LogoutWithReason +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class LogoutWithReasonEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.LOGOUT_WITHREASON + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: LogoutWithReason, + ) { + buffer.p1(message.reason) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildNormalEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildNormalEncoder.kt new file mode 100644 index 000000000..22287dc81 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildNormalEncoder.kt @@ -0,0 +1,43 @@ +package net.rsprot.protocol.game.outgoing.codec.map + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.map.RebuildLogin +import net.rsprot.protocol.game.outgoing.map.StaticRebuildMessage +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class RebuildNormalEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.REBUILD_NORMAL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: StaticRebuildMessage, + ) { + // We have to use the same encoder as it relies on the prot + // under the hood to map the encoders down + if (message is RebuildLogin) { + val gpiInitBlock = message.gpiInitBlock + try { + buffer.buffer.writeBytes( + gpiInitBlock, + gpiInitBlock.readerIndex(), + gpiInitBlock.readableBytes(), + ) + } finally { + gpiInitBlock.release() + } + } + buffer.p2(message.zoneX) + buffer.p2Alt3(message.worldArea) + buffer.p2Alt1(message.zoneZ) + buffer.p2(message.keys.size) + for (xteaKey in message.keys) { + for (intKey in xteaKey.key) { + buffer.p4(intKey) + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildRegionEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildRegionEncoder.kt new file mode 100644 index 000000000..56ee3e9bd --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildRegionEncoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.codec.map + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.codec.map.util.encodeRegion +import net.rsprot.protocol.game.outgoing.map.RebuildRegion +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class RebuildRegionEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.REBUILD_REGION + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: RebuildRegion, + ) { + buffer.p2(message.zoneZ) + buffer.p2(message.zoneX) + buffer.p1Alt2(if (message.reload) 1 else 0) + + encodeRegion(buffer, message.zones) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildWorldEntityEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildWorldEntityEncoder.kt new file mode 100644 index 000000000..c40d66a66 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/RebuildWorldEntityEncoder.kt @@ -0,0 +1,36 @@ +package net.rsprot.protocol.game.outgoing.codec.map + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.codec.map.util.encodeRegion +import net.rsprot.protocol.game.outgoing.map.RebuildWorldEntity +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class RebuildWorldEntityEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.REBUILD_WORLDENTITY + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: RebuildWorldEntity, + ) { + buffer.p2(message.index) + buffer.p2(message.baseX) + buffer.p2(message.baseZ) + try { + buffer.buffer.writeBytes( + message.gpiInitBlock, + message.gpiInitBlock.readerIndex(), + message.gpiInitBlock.readableBytes(), + ) + } finally { + message.gpiInitBlock.release() + } + + encodeRegion(buffer, message.zones) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/util/RegionEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/util/RegionEncoder.kt new file mode 100644 index 000000000..578d3767c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/map/util/RegionEncoder.kt @@ -0,0 +1,100 @@ +package net.rsprot.protocol.game.outgoing.codec.map.util + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.bitbuffer.toBitBuf +import net.rsprot.crypto.xtea.XteaKey +import net.rsprot.protocol.game.outgoing.map.util.RebuildRegionZone + +/** + * The maximum theoretical number of mapsquares that can be sent in a single + * rebuild region packet. + */ +private const val MAX_POTENTIAL_MAPSQUARES = 4 * 13 * 13 + +/** + * A thread-local implementation of mapsquares and their keys. + * As we need to trim our data set down to distinct mapsquares, + * doing so with new lists all the time can be quite wasteful, especially + * knowing how volatile the actual counts can be. + * To minimize the garbage created (in this case, to none), + * we use thread-local implementations for distinct mapsquares. + */ +private val distinctMapsquares = + ThreadLocal.withInitial { + IntArray(MAX_POTENTIAL_MAPSQUARES) to + Array(4 * 13 * 13) { + XteaKey.ZERO + } + } + +internal fun encodeRegion( + buffer: JagByteBuf, + zones: List, +) { + // Xtea count, temporary value + val marker = buffer.writerIndex() + buffer.p2(0) + + var xteaCount = 0 + val (mapsquares, xteas) = distinctMapsquares.get() + val maxBitBufByteCount = ((27 * zones.size) + 32) ushr 5 + val maxXteaByteCount = 2 + (4 * 4 * zones.size) + // Ensure the correct number of writable bytes ahead of time for the worst case scenario + // This is due to our bit buffer implementation by default not ensuring this + buffer.buffer.ensureWritable(maxBitBufByteCount + maxXteaByteCount) + val bitbuf = buffer.buffer.toBitBuf() + bitbuf.use { + for (zone in zones) { + if (zone == null) { + bitbuf.pBits(1, 0) + continue + } + bitbuf.pBits(1, 1) + bitbuf.pBits(26, zone.referenceZone.packed) + val mapsquareId = zone.referenceZone.mapsquareId + if (contains(mapsquares, xteaCount, mapsquareId)) { + continue + } + mapsquares[xteaCount] = mapsquareId + xteas[xteaCount] = zone.key + xteaCount++ + } + } + // Write the real xtea count + val writerIndex = buffer.writerIndex() + buffer.writerIndex(marker) + buffer.p2(xteaCount) + buffer.writerIndex(writerIndex) + + for (i in 0.. { + override val prot: ServerProt = GameServerProt.HIDELOCOPS + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: HideLocOps, + ) { + buffer.pboolean(message.hidden) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HideNpcOpsEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HideNpcOpsEncoder.kt new file mode 100644 index 000000000..d1016e89e --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HideNpcOpsEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.HideNpcOps +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class HideNpcOpsEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.HIDENPCOPS + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: HideNpcOps, + ) { + buffer.pboolean(message.hidden) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HideObjOpsEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HideObjOpsEncoder.kt new file mode 100644 index 000000000..79e971493 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HideObjOpsEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.HideObjOps +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class HideObjOpsEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.HIDEOBJOPS + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: HideObjOps, + ) { + buffer.pboolean(message.hidden) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HintArrowEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HintArrowEncoder.kt new file mode 100644 index 000000000..c08a91c1c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HintArrowEncoder.kt @@ -0,0 +1,43 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.HintArrow +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class HintArrowEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.HINT_ARROW + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: HintArrow, + ) { + when (val type = message.type) { + is HintArrow.NpcHintArrow -> { + buffer.p1(1) + buffer.p2(type.index) + buffer.skipWrite(3) + } + is HintArrow.PlayerHintArrow -> { + buffer.p1(10) + buffer.p2(type.index) + buffer.skipWrite(3) + } + is HintArrow.TileHintArrow -> { + buffer.p1(type.positionId) + buffer.p2(type.x) + buffer.p2(type.z) + buffer.p1(type.height) + } + HintArrow.ResetHintArrow -> { + buffer.p1(0) + buffer.skipWrite(5) + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HiscoreReplyEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HiscoreReplyEncoder.kt new file mode 100644 index 000000000..003c770a9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/HiscoreReplyEncoder.kt @@ -0,0 +1,45 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.HiscoreReply +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class HiscoreReplyEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.HISCORE_REPLY + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: HiscoreReply, + ) { + buffer.p1(message.requestId) + when (val response = message.response) { + is HiscoreReply.FailedHiscoreReply -> { + buffer.p1(1) + buffer.pjstr(response.reason) + } + is HiscoreReply.SuccessfulHiscoreReply -> { + buffer.p1(0) + buffer.p1(response.statResults.size) + for (stat in response.statResults) { + buffer.p2(stat.id) + buffer.p4(stat.rank) + buffer.p4(stat.result) + } + buffer.p4(response.overallRank) + buffer.p8(response.overallExperience) + buffer.p2(response.activityResults.size) + for (activity in response.activityResults) { + buffer.p2(activity.id) + buffer.p4(activity.rank) + buffer.p4(activity.result) + } + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/MinimapToggleEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/MinimapToggleEncoder.kt new file mode 100644 index 000000000..881f00e37 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/MinimapToggleEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.MinimapToggle +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MinimapToggleEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.MINIMAP_TOGGLE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MinimapToggle, + ) { + buffer.p1(message.minimapState) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ReflectionCheckerEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ReflectionCheckerEncoder.kt new file mode 100644 index 000000000..25260eef6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ReflectionCheckerEncoder.kt @@ -0,0 +1,71 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.ReflectionChecker +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ReflectionCheckerEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.REFLECTION_CHECKER + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: ReflectionChecker, + ) { + val checks = message.checks + buffer.p1(checks.size) + buffer.p4(message.id) + for (check in checks) { + when (check) { + is ReflectionChecker.GetFieldValue -> { + buffer.p1(0) + buffer.pjstr(check.className) + buffer.pjstr(check.fieldName) + } + is ReflectionChecker.SetFieldValue -> { + buffer.p1(1) + buffer.pjstr(check.className) + buffer.pjstr(check.fieldName) + buffer.p4(check.value) + } + is ReflectionChecker.GetFieldModifiers -> { + buffer.p1(2) + buffer.pjstr(check.className) + buffer.pjstr(check.fieldName) + } + is ReflectionChecker.InvokeMethod -> { + buffer.p1(3) + buffer.pjstr(check.className) + buffer.pjstr(check.methodName) + val parameterClasses = check.parameterClasses + val parameterValues = check.parameterValues + buffer.p1(parameterClasses.size) + for (parameterClass in parameterClasses) { + buffer.pjstr(parameterClass) + } + buffer.pjstr(check.returnClass) + for (parameterValue in parameterValues) { + buffer.p4(parameterValue.size) + buffer.pdata(parameterValue) + } + } + is ReflectionChecker.GetMethodModifiers -> { + buffer.p1(4) + buffer.pjstr(check.className) + buffer.pjstr(check.methodName) + val parameterClasses = check.parameterClasses + buffer.p1(parameterClasses.size) + for (parameterClass in parameterClasses) { + buffer.pjstr(parameterClass) + } + buffer.pjstr(check.returnClass) + } + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ResetAnimsEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ResetAnimsEncoder.kt new file mode 100644 index 000000000..f8eba942a --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ResetAnimsEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.ResetAnims +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ResetAnimsEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.RESET_ANIMS +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SendPingEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SendPingEncoder.kt new file mode 100644 index 000000000..5e39ddea1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SendPingEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.SendPing +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class SendPingEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.SEND_PING + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: SendPing, + ) { + buffer.p4(message.value1) + buffer.p4(message.value2) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ServerTickEndEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ServerTickEndEncoder.kt new file mode 100644 index 000000000..c3fe0d485 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/ServerTickEndEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.ServerTickEnd +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ServerTickEndEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.SERVER_TICK_END +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SetHeatmapEnabledEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SetHeatmapEnabledEncoder.kt new file mode 100644 index 000000000..d6e695f92 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SetHeatmapEnabledEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.SetHeatmapEnabled +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class SetHeatmapEnabledEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.SET_HEATMAP_ENABLED + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: SetHeatmapEnabled, + ) { + buffer.pboolean(message.enabled) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SiteSettingsEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SiteSettingsEncoder.kt new file mode 100644 index 000000000..d89ac6342 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/SiteSettingsEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.SiteSettings +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class SiteSettingsEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.SITE_SETTINGS + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: SiteSettings, + ) { + buffer.pjstr(message.settings) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UpdateRebootTimerEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UpdateRebootTimerEncoder.kt new file mode 100644 index 000000000..e1366ef1e --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UpdateRebootTimerEncoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.UpdateRebootTimer +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class UpdateRebootTimerEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_REBOOT_TIMER + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateRebootTimer, + ) { + buffer.p2Alt2(message.gameCycles) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UpdateUid192Encoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UpdateUid192Encoder.kt new file mode 100644 index 000000000..a57a41259 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UpdateUid192Encoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.crypto.crc.CyclicRedundancyCheck +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.UpdateUid192 +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdateUid192Encoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_UID192 + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateUid192, + ) { + buffer.pdata(message.uid) + buffer.p4(CyclicRedundancyCheck.computeCrc32(message.uid)) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UrlOpenEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UrlOpenEncoder.kt new file mode 100644 index 000000000..fd00909c4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/client/UrlOpenEncoder.kt @@ -0,0 +1,28 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.client + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.client.UrlOpen +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UrlOpenEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.URL_OPEN + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UrlOpen, + ) { + val marker = buffer.writerIndex() + buffer.pjstr(message.url) + + // Encrypt the entire buffer with a stream cipher + for (i in marker.. { + override val prot: ServerProt = GameServerProt.CHAT_FILTER_SETTINGS + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: ChatFilterSettings, + ) { + buffer.p1Alt2(message.publicChatFilter) + buffer.p1Alt1(message.tradeChatFilter) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/ChatFilterSettingsPrivateChatEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/ChatFilterSettingsPrivateChatEncoder.kt new file mode 100644 index 000000000..63ef40873 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/ChatFilterSettingsPrivateChatEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.ChatFilterSettingsPrivateChat +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ChatFilterSettingsPrivateChatEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.CHAT_FILTER_SETTINGS_PRIVATECHAT + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: ChatFilterSettingsPrivateChat, + ) { + buffer.p1(message.privateChatFilter) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/MessageGameEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/MessageGameEncoder.kt new file mode 100644 index 000000000..5a4529a0f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/MessageGameEncoder.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.MessageGame +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MessageGameEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.MESSAGE_GAME + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MessageGame, + ) { + buffer.pSmart1or2(message.type) + val name = message.name + if (name != null) { + buffer.p1(1) + buffer.pjstr(name) + } else { + buffer.p1(0) + } + buffer.pjstr(message.message) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/RunClientScriptEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/RunClientScriptEncoder.kt new file mode 100644 index 000000000..5004f6218 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/RunClientScriptEncoder.kt @@ -0,0 +1,35 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.RunClientScript +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent +import java.nio.CharBuffer + +@Consistent +public class RunClientScriptEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.RUNCLIENTSCRIPT + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: RunClientScript, + ) { + val types = message.types + val values = message.values + buffer.pjstr(CharBuffer.wrap(types)) + val length = types.size + for (i in (length - 1) downTo 0) { + val type = types[i] + if (type == 's') { + buffer.pjstr(values[i] as String) + } else { + buffer.p4(values[i] as Int) + } + } + buffer.p4(message.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/SetMapFlagEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/SetMapFlagEncoder.kt new file mode 100644 index 000000000..dd3739e63 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/SetMapFlagEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.SetMapFlag +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class SetMapFlagEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.SET_MAP_FLAG + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: SetMapFlag, + ) { + buffer.p1(message.xInBuildArea) + buffer.p1(message.zInBuildArea) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/SetPlayerOpEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/SetPlayerOpEncoder.kt new file mode 100644 index 000000000..7fd3e1b63 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/SetPlayerOpEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.SetPlayerOp +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class SetPlayerOpEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.SET_PLAYER_OP + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: SetPlayerOp, + ) { + buffer.p1Alt2(message.id) + buffer.p1(if (message.priority) 1 else 0) + buffer.pjstr(message.op ?: "null") + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/TriggerOnDialogAbortEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/TriggerOnDialogAbortEncoder.kt new file mode 100644 index 000000000..4edad7e14 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/TriggerOnDialogAbortEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.TriggerOnDialogAbort +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class TriggerOnDialogAbortEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.TRIGGER_ONDIALOGABORT +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateRunEnergyEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateRunEnergyEncoder.kt new file mode 100644 index 000000000..1e36e76ea --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateRunEnergyEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.UpdateRunEnergy +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdateRunEnergyEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_RUNENERGY + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateRunEnergy, + ) { + buffer.p2(message.runenergy) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateRunWeightEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateRunWeightEncoder.kt new file mode 100644 index 000000000..a56ce8384 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateRunWeightEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.UpdateRunWeight +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdateRunWeightEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_RUNWEIGHT + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateRunWeight, + ) { + buffer.p2(message.runweight) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStatEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStatEncoder.kt new file mode 100644 index 000000000..a6cac9738 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStatEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.UpdateStat +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class UpdateStatEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_STAT + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateStat, + ) { + buffer.p4Alt3(message.experience) + buffer.p1Alt3(message.stat) + buffer.p1Alt2(message.invisibleBoostedLevel) + buffer.p1Alt1(message.currentLevel) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStatOldEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStatOldEncoder.kt new file mode 100644 index 000000000..555a76772 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStatOldEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.UpdateStatOld +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class UpdateStatOldEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_STAT_OLD + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateStatOld, + ) { + buffer.p4Alt2(message.experience) + buffer.p1Alt2(message.currentLevel) + buffer.p1(message.stat) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStockMarketSlotEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStockMarketSlotEncoder.kt new file mode 100644 index 000000000..8765c21af --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateStockMarketSlotEncoder.kt @@ -0,0 +1,36 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.UpdateStockMarketSlot +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdateStockMarketSlotEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_STOCKMARKET_SLOT + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateStockMarketSlot, + ) { + buffer.p1(message.slot) + when (val update = message.update) { + UpdateStockMarketSlot.ResetStockMarketSlot -> { + buffer.p1(0) + buffer.skipWrite(18) + } + is UpdateStockMarketSlot.SetStockMarketSlot -> { + buffer.p1(update.status) + buffer.p2(update.obj) + buffer.p4(update.price) + buffer.p4(update.count) + buffer.p4(update.completedCount) + buffer.p4(update.completedGold) + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateTradingPostEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateTradingPostEncoder.kt new file mode 100644 index 000000000..4fcac4f2d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/misc/player/UpdateTradingPostEncoder.kt @@ -0,0 +1,42 @@ +package net.rsprot.protocol.game.outgoing.codec.misc.player + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.misc.player.UpdateTradingPost +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdateTradingPostEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_TRADINGPOST + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateTradingPost, + ) { + when (val update = message.updateType) { + UpdateTradingPost.ResetTradingPost -> { + buffer.p1(0) + } + is UpdateTradingPost.SetTradingPostOfferList -> { + buffer.p1(1) + buffer.p8(update.age) + buffer.p2(update.obj) + buffer.p1(if (update.status) 1 else 0) + val offers = update.offers + buffer.p2(offers.size) + for (offer in offers) { + buffer.pjstr(offer.name) + buffer.pjstr(offer.previousName) + buffer.p2(offer.world) + buffer.p8(offer.time) + buffer.p4(offer.price) + buffer.p4(offer.count) + } + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/DesktopLowResolutionChangeEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/DesktopLowResolutionChangeEncoder.kt new file mode 100644 index 000000000..4f2e51047 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/DesktopLowResolutionChangeEncoder.kt @@ -0,0 +1,41 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo + +import net.rsprot.buffer.bitbuffer.BitBuf +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.NpcAvatarDetails +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.encoder.NpcResolutionChangeEncoder + +public class DesktopLowResolutionChangeEncoder : NpcResolutionChangeEncoder { + override val clientType: OldSchoolClientType = OldSchoolClientType.DESKTOP + + override fun encode( + bitBuffer: BitBuf, + details: NpcAvatarDetails, + extendedInfo: Boolean, + localPlayerCoordGrid: CoordGrid, + largeDistance: Boolean, + cycleCount: Int, + ) { + val numOfBitsUsed = if (largeDistance) 8 else 5 + val maximumDistanceTransmittableByBits = if (largeDistance) 0xFF else 0x1F + val deltaX = details.currentCoord.x - localPlayerCoordGrid.x + val deltaZ = details.currentCoord.z - localPlayerCoordGrid.z + + bitBuffer.pBits(16, details.index) + bitBuffer.pBits(1, if (extendedInfo) 1 else 0) + // New NPCs should always be marked as "jumping" unless they explicitly only teleported without a jump + val noJump = details.isTeleWithoutJump() && details.allocateCycle != cycleCount + bitBuffer.pBits(1, if (noJump) 0 else 1) + bitBuffer.pBits(numOfBitsUsed, deltaZ and maximumDistanceTransmittableByBits) + if (details.spawnCycle != 0) { + bitBuffer.pBits(1, 1) + bitBuffer.pBits(32, details.spawnCycle) + } else { + bitBuffer.pBits(1, 0) + } + bitBuffer.pBits(14, details.id) + bitBuffer.pBits(3, details.direction) + bitBuffer.pBits(numOfBitsUsed, deltaX and maximumDistanceTransmittableByBits) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/NpcInfoLargeEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/NpcInfoLargeEncoder.kt new file mode 100644 index 000000000..8eb0fb872 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/NpcInfoLargeEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcInfoLarge +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class NpcInfoLargeEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.NPC_INFO_LARGE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: NpcInfoLarge, + ) { + // Due to message extending byte buf holder, it is automatically released by the pipeline + buffer.buffer.writeBytes(message.content()) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/NpcInfoSmallEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/NpcInfoSmallEncoder.kt new file mode 100644 index 000000000..7413cc6ea --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/NpcInfoSmallEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcInfoSmall +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class NpcInfoSmallEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.NPC_INFO_SMALL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: NpcInfoSmall, + ) { + // Due to message extending byte buf holder, it is automatically released by the pipeline + buffer.buffer.writeBytes(message.content()) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/SetNpcUpdateOriginEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/SetNpcUpdateOriginEncoder.kt new file mode 100644 index 000000000..5e336f54e --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/SetNpcUpdateOriginEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.info.npcinfo.SetNpcUpdateOrigin +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class SetNpcUpdateOriginEncoder : MessageEncoder { + override val prot: ServerProt + get() = GameServerProt.SET_NPC_UPDATE_ORIGIN + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: SetNpcUpdateOrigin, + ) { + buffer.p1(message.originX) + buffer.p1(message.originZ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcBaseAnimationSetEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcBaseAnimationSetEncoder.kt new file mode 100644 index 000000000..52fca8487 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcBaseAnimationSetEncoder.kt @@ -0,0 +1,72 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.BaseAnimationSet + +public class NpcBaseAnimationSetEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: BaseAnimationSet, + ): JagByteBuf { + val flag = extendedInfo.overrides + val bitCount = flag.countOneBits() + val capacity = 4 + (bitCount * 2) + val buffer = + alloc + .buffer(capacity, capacity) + .toJagByteBuf() + buffer.p4Alt1(flag) + + if (flag and 0x1 != 0) { + buffer.p2Alt1(extendedInfo.turnLeftAnim.toInt()) + } + if (flag and 0x2 != 0) { + buffer.p2Alt3(extendedInfo.turnRightAnim.toInt()) + } + if (flag and 0x4 != 0) { + buffer.p2Alt2(extendedInfo.walkAnim.toInt()) + } + if (flag and 0x8 != 0) { + buffer.p2(extendedInfo.walkAnimBack.toInt()) + } + if (flag and 0x10 != 0) { + buffer.p2(extendedInfo.walkAnimLeft.toInt()) + } + if (flag and 0x20 != 0) { + buffer.p2Alt1(extendedInfo.walkAnimRight.toInt()) + } + if (flag and 0x40 != 0) { + buffer.p2Alt1(extendedInfo.runAnim.toInt()) + } + if (flag and 0x80 != 0) { + buffer.p2Alt3(extendedInfo.runAnimBack.toInt()) + } + if (flag and 0x100 != 0) { + buffer.p2Alt3(extendedInfo.runAnimLeft.toInt()) + } + if (flag and 0x200 != 0) { + buffer.p2Alt2(extendedInfo.runAnimRight.toInt()) + } + if (flag and 0x400 != 0) { + buffer.p2Alt2(extendedInfo.crawlAnim.toInt()) + } + if (flag and 0x800 != 0) { + buffer.p2Alt1(extendedInfo.crawlAnimBack.toInt()) + } + if (flag and 0x1000 != 0) { + buffer.p2Alt1(extendedInfo.crawlAnimLeft.toInt()) + } + if (flag and 0x2000 != 0) { + buffer.p2Alt2(extendedInfo.crawlAnimRight.toInt()) + } + if (flag and 0x4000 != 0) { + buffer.p2Alt1(extendedInfo.readyAnim.toInt()) + } + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcBodyCustomisationEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcBodyCustomisationEncoder.kt new file mode 100644 index 000000000..eaad6893c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcBodyCustomisationEncoder.kt @@ -0,0 +1,81 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.BodyCustomisation + +@Suppress("DuplicatedCode") +public class NpcBodyCustomisationEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: BodyCustomisation, + ): JagByteBuf { + val customisation = extendedInfo.customisation + if (customisation == null) { + val buffer = + alloc + .buffer(1, 1) + .toJagByteBuf() + buffer.pFlag(FLAG_RESET) + return buffer + } + val capacity = + 3 + (customisation.models.size * 2) + + (customisation.recolours.size * 2) + + (customisation.retexture.size * 2) + val buffer = + alloc + .buffer(capacity, capacity) + .toJagByteBuf() + var flag = 0 + if (customisation.models.isNotEmpty()) { + flag = flag or FLAG_REMODEL + } + if (customisation.recolours.isNotEmpty()) { + flag = flag or FLAG_RECOLOUR + } + if (customisation.retexture.isNotEmpty()) { + flag = flag or FLAG_RETEXTURE + } + if (customisation.mirror != null) { + flag = flag or FLAG_MIRROR_LOCAL_PLAYER + } + buffer.pFlag(flag) + if (flag and FLAG_REMODEL != 0) { + buffer.p1Alt1(customisation.models.size) + for (model in customisation.models) { + buffer.p2Alt1(model) + } + } + if (flag and FLAG_RECOLOUR != 0) { + for (recol in customisation.recolours) { + buffer.p2Alt3(recol) + } + } + if (flag and FLAG_RETEXTURE != 0) { + for (retex in customisation.retexture) { + buffer.p2Alt2(retex) + } + } + if (flag and FLAG_MIRROR_LOCAL_PLAYER != 0) { + buffer.p1Alt3(if (customisation.mirror == true) 1 else 0) + } + return buffer + } + + private fun JagByteBuf.pFlag(value: Int) { + p1Alt2(value) + } + + private companion object { + private const val FLAG_RESET: Int = 0x1 + private const val FLAG_REMODEL: Int = 0x2 + private const val FLAG_RECOLOUR: Int = 0x4 + private const val FLAG_RETEXTURE: Int = 0x8 + private const val FLAG_MIRROR_LOCAL_PLAYER: Int = 0x10 + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcCombatLevelChangeEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcCombatLevelChangeEncoder.kt new file mode 100644 index 000000000..a0d78c517 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcCombatLevelChangeEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.CombatLevelChange + +public class NpcCombatLevelChangeEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: CombatLevelChange, + ): JagByteBuf { + val buffer = + alloc + .buffer(4, 4) + .toJagByteBuf() + buffer.p4Alt1(extendedInfo.level) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcExactMoveEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcExactMoveEncoder.kt new file mode 100644 index 000000000..72083706c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcExactMoveEncoder.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.ExactMove + +public class NpcExactMoveEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: ExactMove, + ): JagByteBuf { + val buffer = + alloc + .buffer(10, 10) + .toJagByteBuf() + buffer.p1Alt3(extendedInfo.deltaX1.toInt()) + buffer.p1Alt1(extendedInfo.deltaZ1.toInt()) + buffer.p1(extendedInfo.deltaX2.toInt()) + buffer.p1Alt2(extendedInfo.deltaZ2.toInt()) + buffer.p2Alt1(extendedInfo.delay1.toInt()) + buffer.p2Alt1(extendedInfo.delay2.toInt()) + buffer.p2(extendedInfo.direction.toInt()) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcFaceCoordEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcFaceCoordEncoder.kt new file mode 100644 index 000000000..455fbbfe4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcFaceCoordEncoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.FaceCoord + +public class NpcFaceCoordEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: FaceCoord, + ): JagByteBuf { + val buffer = + alloc + .buffer(5, 5) + .toJagByteBuf() + buffer.p2(extendedInfo.x.toInt()) + buffer.p2Alt1(extendedInfo.z.toInt()) + buffer.p1Alt2(if (extendedInfo.instant) 1 else 0) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcFacePathingEntityEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcFacePathingEntityEncoder.kt new file mode 100644 index 000000000..e03adffa8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcFacePathingEntityEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.FacePathingEntity + +public class NpcFacePathingEntityEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: FacePathingEntity, + ): JagByteBuf { + val buffer = + alloc + .buffer(3, 3) + .toJagByteBuf() + buffer.p2Alt3(extendedInfo.index) + buffer.p1Alt2(extendedInfo.index shr 16) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHeadCustomisationEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHeadCustomisationEncoder.kt new file mode 100644 index 000000000..229b14193 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHeadCustomisationEncoder.kt @@ -0,0 +1,81 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.HeadCustomisation + +@Suppress("DuplicatedCode") +public class NpcHeadCustomisationEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: HeadCustomisation, + ): JagByteBuf { + val customisation = extendedInfo.customisation + if (customisation == null) { + val buffer = + alloc + .buffer(1, 1) + .toJagByteBuf() + buffer.pFlag(FLAG_RESET) + return buffer + } + val capacity = + 3 + (customisation.models.size * 2) + + (customisation.recolours.size * 2) + + (customisation.retexture.size * 2) + val buffer = + alloc + .buffer(capacity, capacity) + .toJagByteBuf() + var flag = 0 + if (customisation.models.isNotEmpty()) { + flag = flag or FLAG_REMODEL + } + if (customisation.recolours.isNotEmpty()) { + flag = flag or FLAG_RECOLOUR + } + if (customisation.retexture.isNotEmpty()) { + flag = flag or FLAG_RETEXTURE + } + if (customisation.mirror != null) { + flag = flag or FLAG_MIRROR_LOCAL_PLAYER + } + buffer.pFlag(flag) + if (flag and FLAG_REMODEL != 0) { + buffer.p1(customisation.models.size) + for (model in customisation.models) { + buffer.p2Alt3(model) + } + } + if (flag and FLAG_RECOLOUR != 0) { + for (recol in customisation.recolours) { + buffer.p2(recol) + } + } + if (flag and FLAG_RETEXTURE != 0) { + for (retex in customisation.retexture) { + buffer.p2Alt3(retex) + } + } + if (flag and FLAG_MIRROR_LOCAL_PLAYER != 0) { + buffer.p1Alt2(if (customisation.mirror == true) 1 else 0) + } + return buffer + } + + private fun JagByteBuf.pFlag(value: Int) { + p1Alt2(value) + } + + private companion object { + private const val FLAG_RESET: Int = 0x1 + private const val FLAG_REMODEL: Int = 0x2 + private const val FLAG_RECOLOUR: Int = 0x4 + private const val FLAG_RETEXTURE: Int = 0x8 + private const val FLAG_MIRROR_LOCAL_PLAYER: Int = 0x10 + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHeadIconCustomisationEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHeadIconCustomisationEncoder.kt new file mode 100644 index 000000000..182baff94 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHeadIconCustomisationEncoder.kt @@ -0,0 +1,34 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.HeadIconCustomisation + +public class NpcHeadIconCustomisationEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: HeadIconCustomisation, + ): JagByteBuf { + val capacity = 1 + 8 * 6 + val buffer = + alloc + .buffer(capacity, capacity) + .toJagByteBuf() + val flag = extendedInfo.flag + buffer.p1Alt1(flag) + for (i in extendedInfo.headIconGroups.indices) { + if (flag and (1 shl i) == 0) { + continue + } + val group = extendedInfo.headIconGroups[i] + val index = extendedInfo.headIconIndices[i].toInt() + buffer.pSmart2or4null(group) + buffer.pSmart1or2null(index) + } + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHitEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHitEncoder.kt new file mode 100644 index 000000000..06989a105 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcHitEncoder.kt @@ -0,0 +1,103 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.common.game.outgoing.info.encoder.OnDemandExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Hit +import net.rsprot.protocol.message.toIntOrMinusOne + +@Suppress("DuplicatedCode") +public class NpcHitEncoder : OnDemandExtendedInfoEncoder { + override fun encode( + buffer: JagByteBuf, + localPlayerIndex: Int, + updatedAvatarIndex: Int, + extendedInfo: Hit, + ) { + pHits(buffer, localPlayerIndex, extendedInfo) + pHeadBars(buffer, localPlayerIndex, extendedInfo) + } + + private fun pHits( + buffer: JagByteBuf, + localPlayerIndex: Int, + info: Hit, + ) { + val countMarker = buffer.writerIndex() + buffer.skipWrite(1) + var count = 0 + for (hit in info.hitMarkList) { + // If we were the source of the hit in the first place + val tinted = localPlayerIndex == (hit.sourceIndex - 0x10_000) + // Skip the hitsplat if it isn't meant to render to us + // Should be noted that we only check this on the main types, and not soak ones + if (hit.otherType == UShort.MAX_VALUE && !tinted) { + continue + } + val mainType = if (tinted) hit.selfType else hit.otherType + val soakType = if (tinted) hit.selfSoakType else hit.otherSoakType + if (mainType.toInt() == 0x7FFE) { + buffer.pSmart1or2(0x7FFE) + } else if (soakType != UShort.MAX_VALUE) { + buffer.pSmart1or2(0x7FFF) + buffer.pSmart1or2(mainType.toInt()) + buffer.pSmart1or2(hit.value.toInt()) + buffer.pSmart1or2(soakType.toInt()) + buffer.pSmart1or2(hit.soakValue.toInt()) + } else { + buffer.pSmart1or2(mainType.toInt()) + buffer.pSmart1or2(hit.value.toInt()) + } + buffer.pSmart1or2(hit.delay.toInt()) + // Exit out of the loop if there are more than 255 hits, + // as that's the highest count we can write + if (++count >= 0xFF) { + break + } + } + val writerIndex = buffer.writerIndex() + buffer.writerIndex(countMarker) + buffer.p1Alt3(count) + buffer.writerIndex(writerIndex) + } + + private fun pHeadBars( + buffer: JagByteBuf, + localPlayerIndex: Int, + info: Hit, + ) { + val countMarker = buffer.writerIndex() + buffer.skipWrite(1) + var count = 0 + for (headBar in info.headBarList) { + val selfType = headBar.selfType.toIntOrMinusOne() + val isSelf = localPlayerIndex == (headBar.sourceIndex - 0x10_000) + if (isSelf && selfType == -1) { + continue + } + val otherType = headBar.otherType.toIntOrMinusOne() + if (!isSelf && otherType == -1) { + continue + } + val type = if (isSelf) selfType else otherType + buffer.pSmart1or2(type) + val endTime = headBar.endTime.toInt() + buffer.pSmart1or2(endTime) + if (endTime != 0x7FFF) { + buffer.pSmart1or2(headBar.startTime.toInt()) + buffer.p1(headBar.startFill.toInt()) + if (endTime > 0) { + buffer.p1Alt1(headBar.endFill.toInt()) + } + } + // Exit out of the loop if there are more than 255 head bars, + // as that's the highest count we can write + if (++count >= 0xFF) { + break + } + } + val writerIndex = buffer.writerIndex() + buffer.writerIndex(countMarker) + buffer.p1Alt1(count) + buffer.writerIndex(writerIndex) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcNameChangeEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcNameChangeEncoder.kt new file mode 100644 index 000000000..3dea84119 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcNameChangeEncoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.NameChange + +public class NpcNameChangeEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: NameChange, + ): JagByteBuf { + val text = extendedInfo.name ?: "" + val capacity = text.length + 1 + val buffer = + alloc + .buffer(capacity, capacity) + .toJagByteBuf() + buffer.pjstr(text) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSayEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSayEncoder.kt new file mode 100644 index 000000000..3f5fd156d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSayEncoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Say + +public class NpcSayEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: Say, + ): JagByteBuf { + val text = extendedInfo.text ?: "" + val capacity = text.length + 1 + val buffer = + alloc + .buffer(capacity, capacity) + .toJagByteBuf() + buffer.pjstr(text) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSequenceEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSequenceEncoder.kt new file mode 100644 index 000000000..c371312f3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSequenceEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Sequence + +public class NpcSequenceEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: Sequence, + ): JagByteBuf { + val buffer = + alloc + .buffer(3, 3) + .toJagByteBuf() + buffer.p2Alt3(extendedInfo.id.toInt()) + buffer.p1Alt1(extendedInfo.delay.toInt()) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSpotAnimEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSpotAnimEncoder.kt new file mode 100644 index 000000000..9437c55d9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcSpotAnimEncoder.kt @@ -0,0 +1,36 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.SpotAnimList +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.SpotAnim + +public class NpcSpotAnimEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: SpotAnimList, + ): JagByteBuf { + val changelist = extendedInfo.changelist + val count = changelist.cardinality() + val capacity = 1 + count * 7 + val buffer = + alloc + .buffer(capacity, capacity) + .toJagByteBuf() + buffer.p1Alt1(count) + val spotanims = extendedInfo.spotanims + var slot = changelist.nextSetBit(0) + while (slot != -1) { + val spotanim = SpotAnim(spotanims[slot]) + buffer.p1Alt3(slot) + buffer.p2Alt2(spotanim.id) + buffer.p4(spotanim.delay or (spotanim.height shl 16)) + slot = changelist.nextSetBit(slot + 1) + } + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcTintingEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcTintingEncoder.kt new file mode 100644 index 000000000..dcfa5419e --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcTintingEncoder.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.NpcTinting + +public class NpcTintingEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: NpcTinting, + ): JagByteBuf { + val buffer = + alloc + .buffer(10, 10) + .toJagByteBuf() + val tinting = extendedInfo.global + buffer.p2Alt1(tinting.start.toInt()) + buffer.p2(tinting.end.toInt()) + buffer.p1Alt1(tinting.hue.toInt()) + buffer.p1Alt2(tinting.saturation.toInt()) + buffer.p1(tinting.lightness.toInt()) + buffer.p1Alt2(tinting.weight.toInt()) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcTransformationEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcTransformationEncoder.kt new file mode 100644 index 000000000..4d8b708d5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcTransformationEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.Transformation + +public class NpcTransformationEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: Transformation, + ): JagByteBuf { + val buffer = + alloc + .buffer(2, 2) + .toJagByteBuf() + buffer.p2(extendedInfo.id.toInt()) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcVisibleOpsEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcVisibleOpsEncoder.kt new file mode 100644 index 000000000..0095dfbb0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/NpcVisibleOpsEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.VisibleOps + +public class NpcVisibleOpsEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: VisibleOps, + ): JagByteBuf { + val buffer = + alloc + .buffer(1, 1) + .toJagByteBuf() + buffer.p1(extendedInfo.ops.toInt()) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/writer/NpcAvatarExtendedInfoDesktopWriter.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/writer/NpcAvatarExtendedInfoDesktopWriter.kt new file mode 100644 index 000000000..443203312 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/npcinfo/extendedinfo/writer/NpcAvatarExtendedInfoDesktopWriter.kt @@ -0,0 +1,194 @@ +package net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.writer + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.encoder.NpcExtendedInfoEncoders +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcBaseAnimationSetEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcBodyCustomisationEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcCombatLevelChangeEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcExactMoveEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcFaceCoordEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcFacePathingEntityEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcHeadCustomisationEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcHeadIconCustomisationEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcHitEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcNameChangeEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcSayEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcSequenceEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcSpotAnimEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcTintingEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcTransformationEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.NpcVisibleOpsEncoder +import net.rsprot.protocol.game.outgoing.info.AvatarExtendedInfoWriter +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarExtendedInfo +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarExtendedInfoBlocks + +@Suppress("DuplicatedCode") +public class NpcAvatarExtendedInfoDesktopWriter : + AvatarExtendedInfoWriter( + OldSchoolClientType.DESKTOP, + NpcExtendedInfoEncoders( + OldSchoolClientType.DESKTOP, + NpcSpotAnimEncoder(), + NpcSayEncoder(), + NpcVisibleOpsEncoder(), + NpcExactMoveEncoder(), + NpcSequenceEncoder(), + NpcTintingEncoder(), + NpcHeadIconCustomisationEncoder(), + NpcNameChangeEncoder(), + NpcHeadCustomisationEncoder(), + NpcBodyCustomisationEncoder(), + NpcTransformationEncoder(), + NpcCombatLevelChangeEncoder(), + NpcHitEncoder(), + NpcFaceCoordEncoder(), + NpcFacePathingEntityEncoder(), + NpcBaseAnimationSetEncoder(), + ), + ) { + private fun convertFlags(constantFlags: Int): Int { + var clientFlags = 0 + if (constantFlags and NpcAvatarExtendedInfo.FACE_PATHINGENTITY != 0) { + clientFlags = clientFlags or FACE_PATHINGENTITY + } + if (constantFlags and NpcAvatarExtendedInfo.TINTING != 0) { + clientFlags = clientFlags or TINTING + } + if (constantFlags and NpcAvatarExtendedInfo.SAY != 0) { + clientFlags = clientFlags or SAY + } + if (constantFlags and NpcAvatarExtendedInfo.HITS != 0) { + clientFlags = clientFlags or HITS + } + if (constantFlags and NpcAvatarExtendedInfo.SEQUENCE != 0) { + clientFlags = clientFlags or SEQUENCE + } + if (constantFlags and NpcAvatarExtendedInfo.EXACT_MOVE != 0) { + clientFlags = clientFlags or EXACT_MOVE + } + if (constantFlags and NpcAvatarExtendedInfo.SPOTANIM != 0) { + clientFlags = clientFlags or SPOTANIM + } + if (constantFlags and NpcAvatarExtendedInfo.FACE_COORD != 0) { + clientFlags = clientFlags or FACE_COORD + } + if (constantFlags and NpcAvatarExtendedInfo.TRANSFORMATION != 0) { + clientFlags = clientFlags or TRANSFORMATION + } + if (constantFlags and NpcAvatarExtendedInfo.BODY_CUSTOMISATION != 0) { + clientFlags = clientFlags or BODY_CUSTOMISATION + } + if (constantFlags and NpcAvatarExtendedInfo.HEAD_CUSTOMISATION != 0) { + clientFlags = clientFlags or HEAD_CUSTOMISATION + } + if (constantFlags and NpcAvatarExtendedInfo.LEVEL_CHANGE != 0) { + clientFlags = clientFlags or LEVEL_CHANGE + } + if (constantFlags and NpcAvatarExtendedInfo.OPS != 0) { + clientFlags = clientFlags or OPS + } + if (constantFlags and NpcAvatarExtendedInfo.NAME_CHANGE != 0) { + clientFlags = clientFlags or NAME_CHANGE + } + if (constantFlags and NpcAvatarExtendedInfo.HEADICON_CUSTOMISATION != 0) { + clientFlags = clientFlags or HEADICON_CUSTOMISATION + } + if (constantFlags and NpcAvatarExtendedInfo.BAS_CHANGE != 0) { + clientFlags = clientFlags or BAS_CHANGE + } + return clientFlags + } + + override fun pExtendedInfo( + buffer: JagByteBuf, + localIndex: Int, + observerIndex: Int, + flag: Int, + blocks: NpcAvatarExtendedInfoBlocks, + ) { + var clientFlag = convertFlags(flag) + if (clientFlag and 0xFF.inv() != 0) clientFlag = clientFlag or EXTENDED_SHORT + if (clientFlag and 0xFFFF.inv() != 0) clientFlag = clientFlag or EXTENDED_MEDIUM + buffer.p1(clientFlag) + if (clientFlag and EXTENDED_SHORT != 0) { + buffer.p1(clientFlag shr 8) + } + if (clientFlag and EXTENDED_MEDIUM != 0) { + buffer.p1(clientFlag shr 16) + } + + if (clientFlag and TRANSFORMATION != 0) { + pCachedData(buffer, blocks.transformation) + } + if (clientFlag and SAY != 0) { + pCachedData(buffer, blocks.say) + } + if (clientFlag and FACE_COORD != 0) { + pCachedData(buffer, blocks.faceCoord) + } + if (clientFlag and OPS != 0) { + pCachedData(buffer, blocks.visibleOps) + } + if (clientFlag and SEQUENCE != 0) { + pCachedData(buffer, blocks.sequence) + } + if (clientFlag and FACE_PATHINGENTITY != 0) { + pCachedData(buffer, blocks.facePathingEntity) + } + if (clientFlag and BODY_CUSTOMISATION != 0) { + pCachedData(buffer, blocks.bodyCustomisation) + } + if (clientFlag and LEVEL_CHANGE != 0) { + pCachedData(buffer, blocks.combatLevelChange) + } + if (clientFlag and NAME_CHANGE != 0) { + pCachedData(buffer, blocks.nameChange) + } + if (clientFlag and TINTING != 0) { + pCachedData(buffer, blocks.tinting) + } + if (clientFlag and HEADICON_CUSTOMISATION != 0) { + pCachedData(buffer, blocks.headIconCustomisation) + } + if (clientFlag and BAS_CHANGE != 0) { + pCachedData(buffer, blocks.baseAnimationSet) + } + if (clientFlag and HEAD_CUSTOMISATION != 0) { + pCachedData(buffer, blocks.headCustomisation) + } + // old spotanim + if (clientFlag and SPOTANIM != 0) { + pCachedData(buffer, blocks.spotAnims) + } + if (clientFlag and HITS != 0) { + pOnDemandData(buffer, localIndex, blocks.hit, observerIndex) + } + if (clientFlag and EXACT_MOVE != 0) { + pCachedData(buffer, blocks.exactMove) + } + } + + @Suppress("unused") + private companion object { + private const val SAY: Int = 0x1 + private const val SEQUENCE: Int = 0x2 + private const val OLD_SPOTANIM_UNUSED: Int = 0x4 + private const val FACE_COORD: Int = 0x8 + private const val FACE_PATHINGENTITY: Int = 0x10 + private const val EXTENDED_SHORT: Int = 0x20 + private const val TRANSFORMATION: Int = 0x40 + private const val HITS: Int = 0x80 + private const val NAME_CHANGE: Int = 0x100 + private const val EXTENDED_MEDIUM: Int = 0x200 + private const val TINTING: Int = 0x400 + private const val BODY_CUSTOMISATION: Int = 0x800 + private const val EXACT_MOVE: Int = 0x1000 + private const val HEAD_CUSTOMISATION: Int = 0x2000 + private const val LEVEL_CHANGE: Int = 0x4000 + private const val OPS: Int = 0x8000 + private const val HEADICON_CUSTOMISATION: Int = 0x10000 + private const val BAS_CHANGE: Int = 0x20000 + private const val SPOTANIM: Int = 0x40000 + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/PlayerInfoEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/PlayerInfoEncoder.kt new file mode 100644 index 000000000..fff82ba26 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/PlayerInfoEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfoPacket +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class PlayerInfoEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.PLAYER_INFO + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: PlayerInfoPacket, + ) { + // Due to message extending byte buf holder, it is automatically released by the pipeline + buffer.buffer.writeBytes(message.content()) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerAppearanceEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerAppearanceEncoder.kt new file mode 100644 index 000000000..1730f78ad --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerAppearanceEncoder.kt @@ -0,0 +1,210 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.Appearance +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.ObjTypeCustomisation + +@Suppress("DuplicatedCode") +public class PlayerAppearanceEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: Appearance, + ): JagByteBuf { + val intermediate = alloc.buffer(100).toJagByteBuf() + intermediate.p1(if (extendedInfo.male) 0 else 1) + intermediate.p1(extendedInfo.skullIcon.toInt()) + intermediate.p1(extendedInfo.overheadIcon.toInt()) + if (extendedInfo.transformedNpcId != UShort.MAX_VALUE) { + pTransmog(intermediate, extendedInfo) + } else { + pEquipment(intermediate, extendedInfo) + } + pIdentKits(intermediate, extendedInfo) + pColours(intermediate, extendedInfo) + pBaseAnimationSet(intermediate, extendedInfo) + intermediate.pjstr(extendedInfo.name) + intermediate.p1(extendedInfo.combatLevel.toInt()) + intermediate.p2(extendedInfo.skillLevel.toInt()) + intermediate.p1(if (extendedInfo.hidden) 1 else 0) + pObjTypeCustomisations(intermediate, extendedInfo) + intermediate.pjstr(extendedInfo.beforeName) + intermediate.pjstr(extendedInfo.afterName) + intermediate.pjstr(extendedInfo.afterCombatLevel) + intermediate.p1(extendedInfo.textGender.toInt()) + val capacity = intermediate.readableBytes() + 1 + val buffer = alloc.buffer(capacity, capacity).toJagByteBuf() + buffer.p1Alt2(capacity - 1) + try { + buffer.pdataAlt2(intermediate.buffer) + } finally { + intermediate.buffer.release() + } + return buffer + } + + private fun pTransmog( + intermediate: JagByteBuf, + extendedInfo: Appearance, + ) { + intermediate.p2(-1) + intermediate.p2(extendedInfo.transformedNpcId.toInt()) + } + + private fun buildHiddenWearposFlag(hidden: ByteArray): Int { + var hiddenWearposFlag = 0 + for (i in 0..<12) { + val pos = hidden[i].toInt() + val wearpos2 = pos and 0xF + if (wearpos2 != 0xF) { + hiddenWearposFlag = hiddenWearposFlag or (1 shl wearpos2) + } + val wearpos3 = pos ushr 4 and 0xF + if (wearpos3 != 0xF) { + hiddenWearposFlag = hiddenWearposFlag or (1 shl wearpos3) + } + } + return hiddenWearposFlag + } + + private fun pEquipment( + intermediate: JagByteBuf, + extendedInfo: Appearance, + ) { + val identKit = extendedInfo.identKit + val objs = extendedInfo.wornObjs + val hiddenWearposFlag = buildHiddenWearposFlag(extendedInfo.hiddenWearPos) + for (wearpos in 0..<12) { + if (hiddenWearposFlag and (1 shl wearpos) != 0) { + intermediate.p1(0) + continue + } + val obj = objs[wearpos].toInt() and 0xFFFF + if (obj != 0xFFFF) { + intermediate.p2(obj + 0x800) + continue + } + val identKitSlot = Appearance.identKitSlotList[wearpos] + if (identKitSlot == -1) { + intermediate.p1(0) + continue + } + val identKitValue = identKit[identKitSlot].toInt() and 0xFFFF + if (identKitValue == 0xFFFF) { + intermediate.p1(0) + } else { + intermediate.p2(identKitValue + 0x100) + } + } + } + + private fun pIdentKits( + intermediate: JagByteBuf, + extendedInfo: Appearance, + ) { + val identKit = extendedInfo.identKit + for (wearpos in 0..<12) { + val identKitSlot = Appearance.identKitSlotList[wearpos] + if (identKitSlot == -1) { + intermediate.p1(0) + continue + } + val identKitValue = identKit[identKitSlot].toInt() and 0xFFFF + if (identKitValue == 0xFFFF) { + intermediate.p1(0) + } else { + intermediate.p2(identKitValue + 0x100) + } + } + } + + private fun pColours( + intermediate: JagByteBuf, + extendedInfo: Appearance, + ) { + val colours = extendedInfo.colours + for (i in colours.indices) { + intermediate.p1(colours[i].toInt()) + } + } + + private fun pBaseAnimationSet( + intermediate: JagByteBuf, + extendedInfo: Appearance, + ) { + intermediate.p2(extendedInfo.readyAnim.toInt()) + intermediate.p2(extendedInfo.turnAnim.toInt()) + intermediate.p2(extendedInfo.walkAnim.toInt()) + intermediate.p2(extendedInfo.walkAnimBack.toInt()) + intermediate.p2(extendedInfo.walkAnimLeft.toInt()) + intermediate.p2(extendedInfo.walkAnimRight.toInt()) + intermediate.p2(extendedInfo.runAnim.toInt()) + } + + private fun pObjTypeCustomisations( + intermediate: JagByteBuf, + extendedInfo: Appearance, + ) { + val marker = intermediate.writerIndex() + intermediate.skipWrite(2) + val objTypeCustomisations = extendedInfo.objTypeCustomisation + var flag = 0 + for (wearpos in objTypeCustomisations.indices) { + val objTypeCustomisation = objTypeCustomisations[wearpos] ?: continue + pObjTypeCustomisation(intermediate, objTypeCustomisation) + flag = flag or (1 shl (12 - wearpos)) + } + if (extendedInfo.forceModelRefresh) flag = flag or 0x8000 + val pos = intermediate.writerIndex() + intermediate.writerIndex(marker) + intermediate.p2(flag) + intermediate.writerIndex(pos) + } + + private fun pObjTypeCustomisation( + intermediate: JagByteBuf, + customisation: ObjTypeCustomisation, + ) { + val recolIndices = customisation.recolIndices.toInt() + val retexIndices = customisation.retexIndices.toInt() + var flag = 0 + if (recolIndices != 0xFF) { + flag = flag or 0x1 + } + if (retexIndices != 0xFF) { + flag = flag or 0x2 + } + intermediate.p1(flag) + pObjTypeCustomisation( + intermediate, + recolIndices, + customisation.recol1.toInt(), + customisation.recol2.toInt(), + ) + pObjTypeCustomisation( + intermediate, + retexIndices, + customisation.retex1.toInt(), + customisation.retex2.toInt(), + ) + } + + private fun pObjTypeCustomisation( + intermediate: JagByteBuf, + flag: Int, + value1: Int, + value2: Int, + ) { + intermediate.p1(flag) + if (flag and 0xF != 0xF) { + intermediate.p2(value1) + } + if (flag and 0xF0 != 0xF0) { + intermediate.p2(value2) + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerChatEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerChatEncoder.kt new file mode 100644 index 000000000..36b7e2f22 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerChatEncoder.kt @@ -0,0 +1,47 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.Chat + +public class PlayerChatEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: Chat, + ): JagByteBuf { + val codec = huffmanCodecProvider.provide() + val text = extendedInfo.text ?: "" + val colour = extendedInfo.colour.toInt() + val patternLength = if (colour in 13..20) colour - 12 else 0 + val capacity = 5 + text.length + patternLength + val buffer = + alloc + .buffer(capacity) + .toJagByteBuf() + buffer.p2(colour shl 8 or extendedInfo.effects.toInt()) + buffer.p1Alt2(extendedInfo.modicon.toInt()) + buffer.p1Alt1(if (extendedInfo.autotyper) 1 else 0) + val huffmanBuffer = + alloc + .buffer(text.length) + .toJagByteBuf() + codec.encode(huffmanBuffer, text) + buffer.p1Alt3(huffmanBuffer.readableBytes()) + try { + buffer.pdataAlt3(huffmanBuffer.buffer) + } finally { + huffmanBuffer.buffer.release() + } + if (patternLength in 1..8) { + val pattern = checkNotNull(extendedInfo.pattern) + for (i in 0.. { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: ExactMove, + ): JagByteBuf { + val buffer = + alloc + .buffer(10, 10) + .toJagByteBuf() + buffer.p1Alt1(extendedInfo.deltaX1.toInt()) + buffer.p1Alt3(extendedInfo.deltaZ1.toInt()) + buffer.p1Alt3(extendedInfo.deltaX2.toInt()) + buffer.p1Alt2(extendedInfo.deltaZ2.toInt()) + buffer.p2Alt1(extendedInfo.delay1.toInt()) + buffer.p2Alt1(extendedInfo.delay2.toInt()) + buffer.p2Alt3(extendedInfo.direction.toInt()) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerFaceAngleEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerFaceAngleEncoder.kt new file mode 100644 index 000000000..e2d2e826e --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerFaceAngleEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.FaceAngle + +public class PlayerFaceAngleEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: FaceAngle, + ): JagByteBuf { + val buffer = + alloc + .buffer(2, 2) + .toJagByteBuf() + buffer.p2Alt2(extendedInfo.angle.toInt()) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerFacePathingEntityEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerFacePathingEntityEncoder.kt new file mode 100644 index 000000000..651bda93b --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerFacePathingEntityEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.FacePathingEntity + +public class PlayerFacePathingEntityEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: FacePathingEntity, + ): JagByteBuf { + val buffer = + alloc + .buffer(3, 3) + .toJagByteBuf() + buffer.p2Alt3(extendedInfo.index) + buffer.p1Alt2(extendedInfo.index shr 16) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerHitEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerHitEncoder.kt new file mode 100644 index 000000000..5e4439b23 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerHitEncoder.kt @@ -0,0 +1,109 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.common.game.outgoing.info.encoder.OnDemandExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Hit +import net.rsprot.protocol.message.toIntOrMinusOne + +@Suppress("DuplicatedCode") +public class PlayerHitEncoder : OnDemandExtendedInfoEncoder { + override fun encode( + buffer: JagByteBuf, + localPlayerIndex: Int, + updatedAvatarIndex: Int, + extendedInfo: Hit, + ) { + pHits(buffer, localPlayerIndex, updatedAvatarIndex, extendedInfo) + pHeadBars(buffer, localPlayerIndex, updatedAvatarIndex, extendedInfo) + } + + private fun pHits( + buffer: JagByteBuf, + localPlayerIndex: Int, + updatedPlayerIndex: Int, + info: Hit, + ) { + val countMarker = buffer.writerIndex() + buffer.skipWrite(1) + var count = 0 + for (hit in info.hitMarkList) { + // If the hit appears on us, or we were the source of the hit in the first place + val tinted = + localPlayerIndex == updatedPlayerIndex || + localPlayerIndex == (hit.sourceIndex - 0x10_000) + // Skip the hitsplat if it isn't meant to render to us + // Should be noted that we only check this on the main types, and not soak ones + if (hit.otherType == UShort.MAX_VALUE && !tinted) { + continue + } + val mainType = if (tinted) hit.selfType else hit.otherType + val soakType = if (tinted) hit.selfSoakType else hit.otherSoakType + if (mainType.toInt() == 0x7FFE) { + buffer.pSmart1or2(0x7FFE) + } else if (soakType != UShort.MAX_VALUE) { + buffer.pSmart1or2(0x7FFF) + buffer.pSmart1or2(mainType.toInt()) + buffer.pSmart1or2(hit.value.toInt()) + buffer.pSmart1or2(soakType.toInt()) + buffer.pSmart1or2(hit.soakValue.toInt()) + } else { + buffer.pSmart1or2(mainType.toInt()) + buffer.pSmart1or2(hit.value.toInt()) + } + buffer.pSmart1or2(hit.delay.toInt()) + // Exit out of the loop if there are more than 255 hits, + // as that's the highest count we can write + if (++count >= 0xFF) { + break + } + } + val writerIndex = buffer.writerIndex() + buffer.writerIndex(countMarker) + buffer.p1Alt2(count) + buffer.writerIndex(writerIndex) + } + + private fun pHeadBars( + buffer: JagByteBuf, + localPlayerIndex: Int, + updatedPlayerIndex: Int, + info: Hit, + ) { + val countMarker = buffer.writerIndex() + buffer.skipWrite(1) + var count = 0 + for (headBar in info.headBarList) { + val selfType = headBar.selfType.toIntOrMinusOne() + val isSelf = + localPlayerIndex == updatedPlayerIndex || + localPlayerIndex == (headBar.sourceIndex - 0x10_000) + if (isSelf && selfType == -1) { + continue + } + val otherType = headBar.otherType.toIntOrMinusOne() + if (!isSelf && otherType == -1) { + continue + } + val type = if (isSelf) selfType else otherType + buffer.pSmart1or2(type) + val endTime = headBar.endTime.toInt() + buffer.pSmart1or2(endTime) + if (endTime != 0x7FFF) { + buffer.pSmart1or2(headBar.startTime.toInt()) + buffer.p1(headBar.startFill.toInt()) + if (endTime > 0) { + buffer.p1Alt3(headBar.endFill.toInt()) + } + } + // Exit out of the loop if there are more than 255 head bars, + // as that's the highest count we can write + if (++count >= 0xFF) { + break + } + } + val writerIndex = buffer.writerIndex() + buffer.writerIndex(countMarker) + buffer.p1Alt1(count) + buffer.writerIndex(writerIndex) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerMoveSpeedEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerMoveSpeedEncoder.kt new file mode 100644 index 000000000..fd5641680 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerMoveSpeedEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.MoveSpeed + +public class PlayerMoveSpeedEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: MoveSpeed, + ): JagByteBuf { + val buffer = + alloc + .buffer(1, 1) + .toJagByteBuf() + buffer.p1Alt2(extendedInfo.value) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSayEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSayEncoder.kt new file mode 100644 index 000000000..b39ccffaf --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSayEncoder.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Say + +public class PlayerSayEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: Say, + ): JagByteBuf { + val text = extendedInfo.text ?: "" + val capacity = text.length + 1 + val buffer = + alloc + .buffer(capacity, capacity) + .toJagByteBuf() + buffer.pjstr(text) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSequenceEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSequenceEncoder.kt new file mode 100644 index 000000000..37a5c6099 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSequenceEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Sequence + +public class PlayerSequenceEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: Sequence, + ): JagByteBuf { + val buffer = + alloc + .buffer(3, 3) + .toJagByteBuf() + buffer.p2Alt1(extendedInfo.id.toInt()) + buffer.p1Alt2(extendedInfo.delay.toInt()) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSpotAnimEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSpotAnimEncoder.kt new file mode 100644 index 000000000..e928a532f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerSpotAnimEncoder.kt @@ -0,0 +1,36 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.SpotAnimList +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.SpotAnim + +public class PlayerSpotAnimEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: SpotAnimList, + ): JagByteBuf { + val changelist = extendedInfo.changelist + val count = changelist.cardinality() + val capacity = 1 + count * 7 + val buffer = + alloc + .buffer(capacity, capacity) + .toJagByteBuf() + buffer.p1Alt3(count) + val spotanims = extendedInfo.spotanims + var slot = changelist.nextSetBit(0) + while (slot != -1) { + val spotanim = SpotAnim(spotanims[slot]) + buffer.p1Alt2(slot) + buffer.p2(spotanim.id) + buffer.p4Alt2(spotanim.delay or (spotanim.height shl 16)) + slot = changelist.nextSetBit(slot + 1) + } + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerTemporaryMoveSpeedEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerTemporaryMoveSpeedEncoder.kt new file mode 100644 index 000000000..dbf5e3b24 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerTemporaryMoveSpeedEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.TemporaryMoveSpeed + +public class PlayerTemporaryMoveSpeedEncoder : PrecomputedExtendedInfoEncoder { + override fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: TemporaryMoveSpeed, + ): JagByteBuf { + val buffer = + alloc + .buffer(1, 1) + .toJagByteBuf() + buffer.p1Alt3(extendedInfo.value) + return buffer + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerTintingEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerTintingEncoder.kt new file mode 100644 index 000000000..63e2e9754 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/PlayerTintingEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.common.game.outgoing.info.encoder.OnDemandExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.PlayerTintingList + +public class PlayerTintingEncoder : OnDemandExtendedInfoEncoder { + override fun encode( + buffer: JagByteBuf, + localPlayerIndex: Int, + updatedAvatarIndex: Int, + extendedInfo: PlayerTintingList, + ) { + val tinting = extendedInfo[localPlayerIndex] + buffer.p2(tinting.start.toInt()) + buffer.p2(tinting.end.toInt()) + buffer.p1Alt2(tinting.hue.toInt()) + buffer.p1(tinting.saturation.toInt()) + buffer.p1Alt2(tinting.lightness.toInt()) + buffer.p1Alt3(tinting.weight.toInt()) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/writer/PlayerAvatarExtendedInfoDesktopWriter.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/writer/PlayerAvatarExtendedInfoDesktopWriter.kt new file mode 100644 index 000000000..00048c681 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/playerinfo/extendedinfo/writer/PlayerAvatarExtendedInfoDesktopWriter.kt @@ -0,0 +1,161 @@ +package net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.writer + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.encoder.PlayerExtendedInfoEncoders +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerAppearanceEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerChatEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerExactMoveEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerFaceAngleEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerFacePathingEntityEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerHitEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerMoveSpeedEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerSayEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerSequenceEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerSpotAnimEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerTemporaryMoveSpeedEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.PlayerTintingEncoder +import net.rsprot.protocol.game.outgoing.info.AvatarExtendedInfoWriter +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerAvatarExtendedInfo +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerAvatarExtendedInfoBlocks + +public class PlayerAvatarExtendedInfoDesktopWriter : + AvatarExtendedInfoWriter( + OldSchoolClientType.DESKTOP, + PlayerExtendedInfoEncoders( + OldSchoolClientType.DESKTOP, + PlayerAppearanceEncoder(), + PlayerChatEncoder(), + PlayerExactMoveEncoder(), + PlayerFaceAngleEncoder(), + PlayerFacePathingEntityEncoder(), + PlayerHitEncoder(), + PlayerMoveSpeedEncoder(), + PlayerSayEncoder(), + PlayerSequenceEncoder(), + PlayerSpotAnimEncoder(), + PlayerTemporaryMoveSpeedEncoder(), + PlayerTintingEncoder(), + ), + ) { + private fun convertFlags(constantFlags: Int): Int { + var clientFlags = 0 + if (constantFlags and PlayerAvatarExtendedInfo.APPEARANCE != 0) { + clientFlags = clientFlags or APPEARANCE + } + if (constantFlags and PlayerAvatarExtendedInfo.MOVE_SPEED != 0) { + clientFlags = clientFlags or MOVE_SPEED + } + if (constantFlags and PlayerAvatarExtendedInfo.FACE_PATHINGENTITY != 0) { + clientFlags = clientFlags or FACE_PATHINGENTITY + } + if (constantFlags and PlayerAvatarExtendedInfo.TINTING != 0) { + clientFlags = clientFlags or TINTING + } + if (constantFlags and PlayerAvatarExtendedInfo.FACE_ANGLE != 0) { + clientFlags = clientFlags or FACE_ANGLE + } + if (constantFlags and PlayerAvatarExtendedInfo.SAY != 0) { + clientFlags = clientFlags or SAY + } + if (constantFlags and PlayerAvatarExtendedInfo.HITS != 0) { + clientFlags = clientFlags or HITS + } + if (constantFlags and PlayerAvatarExtendedInfo.SEQUENCE != 0) { + clientFlags = clientFlags or SEQUENCE + } + if (constantFlags and PlayerAvatarExtendedInfo.CHAT != 0) { + clientFlags = clientFlags or CHAT + } + if (constantFlags and PlayerAvatarExtendedInfo.TEMP_MOVE_SPEED != 0) { + clientFlags = clientFlags or TEMP_MOVE_SPEED + } + if (constantFlags and PlayerAvatarExtendedInfo.EXACT_MOVE != 0) { + clientFlags = clientFlags or EXACT_MOVE + } + if (constantFlags and PlayerAvatarExtendedInfo.SPOTANIM != 0) { + clientFlags = clientFlags or SPOTANIM + } + return clientFlags + } + + override fun pExtendedInfo( + buffer: JagByteBuf, + localIndex: Int, + observerIndex: Int, + flag: Int, + blocks: PlayerAvatarExtendedInfoBlocks, + ) { + var clientFlag = convertFlags(flag) + if (clientFlag and 0xFF.inv() != 0) clientFlag = clientFlag or EXTENDED_SHORT + if (clientFlag and 0xFFFF.inv() != 0) clientFlag = clientFlag or EXTENDED_MEDIUM + buffer.p1(clientFlag) + if (clientFlag and EXTENDED_SHORT != 0) { + buffer.p1(clientFlag shr 8) + } + if (clientFlag and EXTENDED_MEDIUM != 0) { + buffer.p1(clientFlag shr 16) + } + + if (clientFlag and MOVE_SPEED != 0) { + pCachedData(buffer, blocks.moveSpeed) + } + // Name extras + if (clientFlag and SPOTANIM != 0) { + pCachedData(buffer, blocks.spotAnims) + } + if (clientFlag and TEMP_MOVE_SPEED != 0) { + pCachedData(buffer, blocks.temporaryMoveSpeed) + } + if (clientFlag and APPEARANCE != 0) { + pCachedData(buffer, blocks.appearance) + } + if (clientFlag and HITS != 0) { + pOnDemandData(buffer, localIndex, blocks.hit, observerIndex) + } + if (clientFlag and CHAT != 0) { + pCachedData(buffer, blocks.chat) + } + if (clientFlag and EXACT_MOVE != 0) { + pCachedData(buffer, blocks.exactMove) + } + if (clientFlag and FACE_PATHINGENTITY != 0) { + pCachedData(buffer, blocks.facePathingEntity) + } + if (clientFlag and SEQUENCE != 0) { + pCachedData(buffer, blocks.sequence) + } + if (clientFlag and SAY != 0) { + pCachedData(buffer, blocks.say) + } + // Old chat + if (clientFlag and FACE_ANGLE != 0) { + pCachedData(buffer, blocks.faceAngle) + } + if (clientFlag and TINTING != 0) { + pOnDemandData(buffer, localIndex, blocks.tinting, observerIndex) + } + } + + @Suppress("unused") + private companion object { + private const val SAY = 0x1 + private const val FACE_ANGLE = 0x2 + private const val CHAT_OLD = 0x4 + private const val EXTENDED_SHORT = 0x8 + private const val SEQUENCE = 0x10 + private const val APPEARANCE = 0x20 + private const val FACE_PATHINGENTITY = 0x40 + private const val HITS = 0x80 + private const val MOVE_SPEED = 0x100 + private const val TINTING = 0x200 + private const val EXTENDED_MEDIUM = 0x400 + private const val TEMP_MOVE_SPEED = 0x800 + private const val CHAT = 0x2000 + private const val EXACT_MOVE = 0x4000 + private const val SPOTANIM = 0x10000 + + // Name extras are part of appearance nowadays, and thus will not be used on their own + private const val NAME_EXTRAS = 0x8000 + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/FriendListLoadedEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/FriendListLoadedEncoder.kt new file mode 100644 index 000000000..63c4a6e9f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/FriendListLoadedEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.social + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.social.FriendListLoaded +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class FriendListLoadedEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.FRIENDLIST_LOADED +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/MessagePrivateEchoEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/MessagePrivateEchoEncoder.kt new file mode 100644 index 000000000..135439f51 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/MessagePrivateEchoEncoder.kt @@ -0,0 +1,27 @@ +package net.rsprot.protocol.game.outgoing.codec.social + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.social.MessagePrivateEcho +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MessagePrivateEchoEncoder( + private val huffmanCodecProvider: HuffmanCodecProvider, +) : MessageEncoder { + override val prot: ServerProt = GameServerProt.MESSAGE_PRIVATE_ECHO + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MessagePrivateEcho, + ) { + buffer.pjstr(message.recipient) + val huffman = huffmanCodecProvider.provide() + huffman.encode(buffer, message.message) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/MessagePrivateEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/MessagePrivateEncoder.kt new file mode 100644 index 000000000..aab9ae3af --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/MessagePrivateEncoder.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.outgoing.codec.social + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.social.MessagePrivate +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class MessagePrivateEncoder( + private val huffmanCodecProvider: HuffmanCodecProvider, +) : MessageEncoder { + override val prot: ServerProt = GameServerProt.MESSAGE_PRIVATE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MessagePrivate, + ) { + buffer.pjstr(message.sender) + buffer.p2(message.worldId) + buffer.p3(message.worldMessageCounter) + buffer.p1(message.chatCrownType) + val huffmanCodec = huffmanCodecProvider.provide() + huffmanCodec.encode(buffer, message.message) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/UpdateFriendListEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/UpdateFriendListEncoder.kt new file mode 100644 index 000000000..98c5440a2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/UpdateFriendListEncoder.kt @@ -0,0 +1,35 @@ +package net.rsprot.protocol.game.outgoing.codec.social + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.social.UpdateFriendList +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdateFriendListEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_FRIENDLIST + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateFriendList, + ) { + for (friend in message.friends) { + buffer.p1(if (friend.added) 1 else 0) + buffer.pjstr(friend.name) + buffer.pjstr(friend.previousName ?: "") + buffer.p2(friend.worldId) + buffer.p1(friend.rank) + buffer.p1(friend.properties) + if (friend is UpdateFriendList.OnlineFriend) { + buffer.pjstr(friend.worldName) + buffer.p1(friend.platform) + buffer.p4(friend.worldFlags) + } + buffer.pjstr(friend.notes) + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/UpdateIgnoreListEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/UpdateIgnoreListEncoder.kt new file mode 100644 index 000000000..ffca81015 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/social/UpdateIgnoreListEncoder.kt @@ -0,0 +1,35 @@ +package net.rsprot.protocol.game.outgoing.codec.social + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.social.UpdateIgnoreList +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class UpdateIgnoreListEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_IGNORELIST + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateIgnoreList, + ) { + for (ignore in message.ignores) { + when (ignore) { + is UpdateIgnoreList.AddedIgnoredEntry -> { + buffer.p1(if (ignore.added) 0x1 else 0) + buffer.pjstr(ignore.name) + buffer.pjstr(ignore.previousName ?: "") + buffer.pjstr(ignore.note) + } + is UpdateIgnoreList.RemovedIgnoredEntry -> { + buffer.p1(0x4) + buffer.pjstr(ignore.name) + } + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiJingleEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiJingleEncoder.kt new file mode 100644 index 000000000..f2c377ad8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiJingleEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.codec.sound + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.sound.MidiJingle +import net.rsprot.protocol.message.codec.MessageEncoder + +public class MidiJingleEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.MIDI_JINGLE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MidiJingle, + ) { + buffer.p2(message.id) + buffer.p3Alt1(message.lengthInMillis) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongEncoder.kt new file mode 100644 index 000000000..859d5f00d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongEncoder.kt @@ -0,0 +1,27 @@ +package net.rsprot.protocol.game.outgoing.codec.sound + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.sound.MidiSong +import net.rsprot.protocol.message.codec.MessageEncoder + +public class MidiSongEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.MIDI_SONG + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MidiSong, + ) { + // The order in the client remains the same for the function call at the end + // of the packet, as: + // playSongList(ids, fadeOutDelay, fadeOutSpeed, fadeInDelay, fadeInSpeed) + buffer.p2Alt2(message.fadeOutDelay) + buffer.p2Alt3(message.fadeOutSpeed) + buffer.p2Alt1(message.fadeInDelay) + buffer.p2Alt3(message.id) + buffer.p2Alt2(message.fadeInSpeed) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongOldEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongOldEncoder.kt new file mode 100644 index 000000000..a259054dd --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongOldEncoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.game.outgoing.codec.sound + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.sound.MidiSongOld +import net.rsprot.protocol.message.codec.MessageEncoder + +public class MidiSongOldEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.MIDI_SONG_OLD + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MidiSongOld, + ) { + buffer.p2(message.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongStopEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongStopEncoder.kt new file mode 100644 index 000000000..2d2ca5050 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongStopEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.sound + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.sound.MidiSongStop +import net.rsprot.protocol.message.codec.MessageEncoder + +public class MidiSongStopEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.MIDI_SONG_STOP + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MidiSongStop, + ) { + // The order in the client remains the same for the function call at the end + // of the packet, as: + // fadeOut(fadeOutDelay, fadeOutSpeed) + buffer.p2Alt2(message.fadeOutSpeed) + buffer.p2(message.fadeOutDelay) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongWithSecondaryEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongWithSecondaryEncoder.kt new file mode 100644 index 000000000..f94540ce7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSongWithSecondaryEncoder.kt @@ -0,0 +1,28 @@ +package net.rsprot.protocol.game.outgoing.codec.sound + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.sound.MidiSongWithSecondary +import net.rsprot.protocol.message.codec.MessageEncoder + +public class MidiSongWithSecondaryEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.MIDI_SONG_WITHSECONDARY + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MidiSongWithSecondary, + ) { + // The order in the client remains the same for the function call at the end + // of the packet, as (the ids list has primary id as the first song): + // playSongList(ids, fadeOutDelay, fadeOutSpeed, fadeInDelay, fadeInSpeed) + buffer.p2(message.fadeOutSpeed) + buffer.p2Alt3(message.fadeInSpeed) + buffer.p2Alt1(message.fadeOutDelay) + buffer.p2Alt3(message.secondaryId) + buffer.p2Alt2(message.fadeInDelay) + buffer.p2(message.primaryId) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSwapEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSwapEncoder.kt new file mode 100644 index 000000000..ec84e0e1d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/MidiSwapEncoder.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.game.outgoing.codec.sound + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.sound.MidiSwap +import net.rsprot.protocol.message.codec.MessageEncoder + +public class MidiSwapEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.MIDI_SWAP + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MidiSwap, + ) { + // The order in the client remains the same for the function call at the end + // of the packet, as: + // swap(fadeOutDelay, fadeOutSpeed, fadeInDelay, fadeInSpeed) + buffer.p2Alt2(message.fadeOutDelay) + buffer.p2(message.fadeInSpeed) + buffer.p2Alt2(message.fadeOutSpeed) + buffer.p2Alt2(message.fadeInDelay) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/SynthSoundEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/SynthSoundEncoder.kt new file mode 100644 index 000000000..f4f35cf65 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/sound/SynthSoundEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.sound + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.sound.SynthSound +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class SynthSoundEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.SYNTH_SOUND + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: SynthSound, + ) { + buffer.p2(message.id) + buffer.p1(message.loops) + buffer.p2(message.delay) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/LocAnimSpecificEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/LocAnimSpecificEncoder.kt new file mode 100644 index 000000000..99093256c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/LocAnimSpecificEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.specific + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.specific.LocAnimSpecific +import net.rsprot.protocol.message.codec.MessageEncoder + +public class LocAnimSpecificEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.LOC_ANIM_SPECIFIC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: LocAnimSpecific, + ) { + buffer.p2Alt2(message.id) + buffer.p3(message.coordInBuildAreaPacked) + buffer.p1Alt1(message.locPropertiesPacked) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/MapAnimSpecificEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/MapAnimSpecificEncoder.kt new file mode 100644 index 000000000..1255df30f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/MapAnimSpecificEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.specific + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.specific.MapAnimSpecific +import net.rsprot.protocol.message.codec.MessageEncoder + +public class MapAnimSpecificEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.MAP_ANIM_SPECIFIC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: MapAnimSpecific, + ) { + buffer.p3Alt3(message.coordInBuildAreaPacked) + buffer.p2Alt3(message.id) + buffer.p1Alt2(message.height) + buffer.p2Alt1(message.delay) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcAnimSpecificEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcAnimSpecificEncoder.kt new file mode 100644 index 000000000..23c147068 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcAnimSpecificEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.specific + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.specific.NpcAnimSpecific +import net.rsprot.protocol.message.codec.MessageEncoder + +public class NpcAnimSpecificEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.NPC_ANIM_SPECIFIC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: NpcAnimSpecific, + ) { + buffer.p2Alt3(message.id) + buffer.p2Alt1(message.index) + buffer.p1(message.delay) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcHeadIconSpecificEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcHeadIconSpecificEncoder.kt new file mode 100644 index 000000000..201d3fb6c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcHeadIconSpecificEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.specific + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.specific.NpcHeadIconSpecific +import net.rsprot.protocol.message.codec.MessageEncoder + +public class NpcHeadIconSpecificEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.NPC_HEADICON_SPECIFIC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: NpcHeadIconSpecific, + ) { + buffer.p4Alt3(message.spriteGroup) + buffer.p2(message.index) + buffer.p2Alt2(message.spriteIndex) + buffer.p1Alt1(message.headIconSlot) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcSpotAnimSpecificEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcSpotAnimSpecificEncoder.kt new file mode 100644 index 000000000..e93657f3d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/NpcSpotAnimSpecificEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.specific + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.specific.NpcSpotAnimSpecific +import net.rsprot.protocol.message.codec.MessageEncoder + +public class NpcSpotAnimSpecificEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.NPC_SPOTANIM_SPECIFIC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: NpcSpotAnimSpecific, + ) { + buffer.p4Alt2((message.height shl 16) or message.delay) + buffer.p2Alt2(message.index) + buffer.p1Alt3(message.slot) + buffer.p2(message.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/PlayerAnimSpecificEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/PlayerAnimSpecificEncoder.kt new file mode 100644 index 000000000..4f08eec25 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/PlayerAnimSpecificEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.codec.specific + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.specific.PlayerAnimSpecific +import net.rsprot.protocol.message.codec.MessageEncoder + +public class PlayerAnimSpecificEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.PLAYER_ANIM_SPECIFIC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: PlayerAnimSpecific, + ) { + buffer.p1Alt2(message.delay) + buffer.p2(message.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/PlayerSpotAnimSpecificEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/PlayerSpotAnimSpecificEncoder.kt new file mode 100644 index 000000000..a7833d69f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/PlayerSpotAnimSpecificEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.specific + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.specific.PlayerSpotAnimSpecific +import net.rsprot.protocol.message.codec.MessageEncoder + +public class PlayerSpotAnimSpecificEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.PLAYER_SPOTANIM_SPECIFIC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: PlayerSpotAnimSpecific, + ) { + buffer.p2Alt1(message.id) + buffer.p4((message.height shl 16) or message.delay) + buffer.p1Alt1(message.slot) + buffer.p2Alt2(message.index) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/ProjAnimSpecificEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/ProjAnimSpecificEncoder.kt new file mode 100644 index 000000000..8e77d8aa5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/specific/ProjAnimSpecificEncoder.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.outgoing.codec.specific + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.specific.ProjAnimSpecific +import net.rsprot.protocol.message.codec.MessageEncoder + +public class ProjAnimSpecificEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.PROJANIM_SPECIFIC + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: ProjAnimSpecific, + ) { + buffer.p1Alt1(message.angle) + buffer.p1Alt1(message.deltaZ) + buffer.p1(message.deltaX) + buffer.p3Alt2(message.sourceIndex) + buffer.p2(message.progress) + buffer.p2Alt1(message.endTime) + buffer.p1Alt1(message.endHeight) + buffer.p3Alt1(message.targetIndex) + buffer.p2Alt1(message.id) + buffer.p1(message.startHeight) + buffer.p3(message.coordInBuildAreaPacked) + buffer.p2Alt3(message.startTime) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpLargeEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpLargeEncoder.kt new file mode 100644 index 000000000..a4ac3233a --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpLargeEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.codec.varp + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.varp.VarpLarge +import net.rsprot.protocol.message.codec.MessageEncoder + +public class VarpLargeEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.VARP_LARGE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: VarpLarge, + ) { + buffer.p2Alt2(message.id) + buffer.p4Alt2(message.value) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpResetEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpResetEncoder.kt new file mode 100644 index 000000000..82ab99e15 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpResetEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.varp + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.varp.VarpReset +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class VarpResetEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.VARP_RESET +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpSmallEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpSmallEncoder.kt new file mode 100644 index 000000000..338a87cf0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpSmallEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.codec.varp + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.varp.VarpSmall +import net.rsprot.protocol.message.codec.MessageEncoder + +public class VarpSmallEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.VARP_SMALL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: VarpSmall, + ) { + buffer.p1Alt3(message.value) + buffer.p2Alt3(message.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpSyncEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpSyncEncoder.kt new file mode 100644 index 000000000..106de0f48 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/varp/VarpSyncEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.varp + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.varp.VarpSync +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class VarpSyncEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.VARP_SYNC +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/ClearEntitiesEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/ClearEntitiesEncoder.kt new file mode 100644 index 000000000..f52ed904d --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/ClearEntitiesEncoder.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.codec.worldentity + +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.worldentity.ClearEntities +import net.rsprot.protocol.message.codec.NoOpMessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class ClearEntitiesEncoder : NoOpMessageEncoder { + override val prot: ServerProt = GameServerProt.CLEAR_ENTITIES +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/SetActiveWorldEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/SetActiveWorldEncoder.kt new file mode 100644 index 000000000..92e9114fb --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/SetActiveWorldEncoder.kt @@ -0,0 +1,36 @@ +package net.rsprot.protocol.game.outgoing.codec.worldentity + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.worldentity.SetActiveWorld +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.metadata.Consistent + +@Consistent +public class SetActiveWorldEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.SET_ACTIVE_WORLD + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: SetActiveWorld, + ) { + when (val type = message.worldType) { + is SetActiveWorld.RootWorldType -> { + // Prefix 0 implies a root world update + buffer.p1(0) + // The slot is ignored for root world updates + buffer.p2(0) + buffer.p1(type.activeLevel) + } + is SetActiveWorld.DynamicWorldType -> { + // Prefix 1 implies a dynamic world update + buffer.p1(1) + buffer.p2(type.index) + buffer.p1(type.activeLevel) + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/WorldEntityInfoEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/WorldEntityInfoEncoder.kt new file mode 100644 index 000000000..52035be73 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/worldentity/WorldEntityInfoEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.codec.worldentity + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.info.worldentityinfo.WorldEntityInfoPacket +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.message.codec.MessageEncoder + +public class WorldEntityInfoEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.WORLDENTITY_INFO + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: WorldEntityInfoPacket, + ) { + // Due to message extending byte buf holder, it is automatically released by the pipeline + buffer.buffer.writeBytes(message.content()) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/DesktopUpdateZonePartialEnclosedEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/DesktopUpdateZonePartialEnclosedEncoder.kt new file mode 100644 index 000000000..79e106963 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/DesktopUpdateZonePartialEnclosedEncoder.kt @@ -0,0 +1,157 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.header + +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocAddChangeEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocAnimEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocDelEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocMergeEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.MapAnimEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.MapProjAnimEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjAddEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjCountEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjDelEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjEnabledOpsEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.SoundAreaEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.header.UpdateZonePartialEnclosed +import net.rsprot.protocol.message.ZoneProt +import net.rsprot.protocol.message.codec.MessageEncoder +import net.rsprot.protocol.message.codec.UpdateZonePartialEnclosedCache +import kotlin.math.min + +public class DesktopUpdateZonePartialEnclosedEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_ZONE_PARTIAL_ENCLOSED + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateZonePartialEnclosed, + ) { + buffer.p1(message.zoneZ) + buffer.p1Alt2(message.zoneX) + buffer.p1Alt1(message.level) + buffer.buffer.writeBytes( + message.payload, + message.payload.readerIndex(), + message.payload.readableBytes(), + ) + message.payload.release() + } + + public companion object : UpdateZonePartialEnclosedCache { + private const val MAX_PARTIAL_ENCLOSED_SIZE = 40_000 - 3 + + /** + * Builds a cache of a given zone's list of zone prots. + * This is intended so the server only requests one cache per zone per game cycle, + * rather than re-building the same buffer N times, where N is the number of players + * observing the zone. With this in mind however, zone prots which are player-specific, + * such as OBJ_ADD cannot be grouped together and must be sent separately, as they also + * are in OldSchool RuneScape. + * @param allocator the byte buffer allocator used for the cached buffer. + * Note that it is the server's responsibility to release the buffer once the cycle has ended. + * The individual writes of [UpdateZonePartialEnclosed] do not modify the reference count + * in any way. + * @param messages the list of zone prot messages to be encoded. + */ + override fun buildCache( + allocator: ByteBufAllocator, + messages: List, + ): ByteBuf { + val buffer = + allocator + .buffer( + min(IndexedZoneProtEncoder.maxZoneProtSize * messages.size, MAX_PARTIAL_ENCLOSED_SIZE), + MAX_PARTIAL_ENCLOSED_SIZE, + ).toJagByteBuf() + for (message in messages) { + val indexedEncoder = IndexedZoneProtEncoder.indexedEncoders[message.protId] + buffer.p1(indexedEncoder.ordinal) + encodeMessage( + buffer, + message, + indexedEncoder.encoder, + ) + } + return buffer.buffer + } + + /** + * Encodes the [message] into the [buffer] using the [encoder] as the encoder for it. + * @param buffer the buffer to encode into + * @param message the message to be encoded + * @param encoder the encoder to use for encoding the message. + * Note that the type of the encoder is not compile-time known as we acquire it dynamically + * based on the message itself. + */ + private fun encodeMessage( + buffer: JagByteBuf, + message: T, + encoder: ZoneProtEncoder<*>, + ) { + @Suppress("UNCHECKED_CAST") + encoder as ZoneProtEncoder + encoder.encode(buffer, message) + } + + /** + * Zone prot encoders here are used specifically by the [UpdateZonePartialEnclosed] + * packet, as this packet has its own sub-system of the zone prots, with the ability + * to send a batch of zone packets in one go with its own internal indexing. + * + * WARNING: This enum's order MUST match the order in the client, as the + * [IndexedZoneProtEncoder.ordinal] function is used for indexing! + * + * @property protId the respective [ZoneProt.protId] of each message, used for + * quick indexing of respective messages. + * @property encoder the zone prot encoder responsible for encoding the respective message + * into a byte buffer. + */ + private enum class IndexedZoneProtEncoder( + private val protId: Int, + val encoder: ZoneProtEncoder<*>, + ) { + LOC_ADD_CHANGE(OldSchoolZoneProt.LOC_ADD_CHANGE, LocAddChangeEncoder()), + OBJ_ADD(OldSchoolZoneProt.OBJ_ADD, ObjAddEncoder()), + LOC_DEL(OldSchoolZoneProt.LOC_DEL, LocDelEncoder()), + SOUND_AREA(OldSchoolZoneProt.SOUND_AREA, SoundAreaEncoder()), + OBJ_ENABLED_OPS(OldSchoolZoneProt.OBJ_ENABLED_OPS, ObjEnabledOpsEncoder()), + MAP_ANIM(OldSchoolZoneProt.MAP_ANIM, MapAnimEncoder()), + LOC_MERGE(OldSchoolZoneProt.LOC_MERGE, LocMergeEncoder()), + MAP_PROJANIM(OldSchoolZoneProt.MAP_PROJANIM, MapProjAnimEncoder()), + OBJ_COUNT(OldSchoolZoneProt.OBJ_COUNT, ObjCountEncoder()), + OBJ_DEL(OldSchoolZoneProt.OBJ_DEL, ObjDelEncoder()), + LOC_ANIM(OldSchoolZoneProt.LOC_ANIM, LocAnimEncoder()), + ; + + companion object { + /** + * The maximum possible size of a single zone prot. + * This constant is used to determine the maximum initial possible buffer capacity. + */ + val maxZoneProtSize = + entries.maxOf { + it.encoder.prot.size + } + + /** + * The zone prot encoders indexed by their prot ids, allowing for fast access based + * on the respective [ZoneProt.protId] through the array. + */ + val indexedEncoders = + Array(entries.size) { index -> + entries.first { prot -> + index == prot.protId + } + } + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/UpdateZoneFullFollowsEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/UpdateZoneFullFollowsEncoder.kt new file mode 100644 index 000000000..706c19130 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/UpdateZoneFullFollowsEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.header + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.header.UpdateZoneFullFollows +import net.rsprot.protocol.message.codec.MessageEncoder + +public class UpdateZoneFullFollowsEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_ZONE_FULL_FOLLOWS + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateZoneFullFollows, + ) { + buffer.p1Alt1(message.zoneX) + buffer.p1Alt3(message.level) + buffer.p1Alt3(message.zoneZ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/UpdateZonePartialFollowsEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/UpdateZonePartialFollowsEncoder.kt new file mode 100644 index 000000000..5f72709b0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/header/UpdateZonePartialFollowsEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.header + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.header.UpdateZonePartialFollows +import net.rsprot.protocol.message.codec.MessageEncoder + +public class UpdateZonePartialFollowsEncoder : MessageEncoder { + override val prot: ServerProt = GameServerProt.UPDATE_ZONE_PARTIAL_FOLLOWS + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: UpdateZonePartialFollows, + ) { + buffer.p1(message.zoneX) + buffer.p1Alt1(message.level) + buffer.p1Alt1(message.zoneZ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocAddChangeEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocAddChangeEncoder.kt new file mode 100644 index 000000000..ab6d8b9b7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocAddChangeEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.LocAddChange + +public class LocAddChangeEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.LOC_ADD_CHANGE + + override fun encode( + buffer: JagByteBuf, + message: LocAddChange, + ) { + // The function at the bottom of the LOC_ADD_CHANGE has a consistent order, + // making it easy to identify all the properties of this packet: + // loc_add_change_del(world, level, x, z, layer, id, shape, rotation, opFlags, 0, -1) + buffer.p2Alt2(message.id) + buffer.p1Alt2(message.opFlags.value) + buffer.p1Alt2(message.locPropertiesPacked) + buffer.p1Alt2(message.coordInZonePacked) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocAnimEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocAnimEncoder.kt new file mode 100644 index 000000000..53332d389 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocAnimEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.LocAnim + +public class LocAnimEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.LOC_ANIM + + override fun encode( + buffer: JagByteBuf, + message: LocAnim, + ) { + // The function + // at the bottom of the LOC_ANIM has a consistent order, + // making it easy to identify all the properties of this packet: + // loc_anim(level, x, z, shape, rotation, layer, id) + buffer.p1Alt3(message.locPropertiesPacked) + buffer.p1(message.coordInZonePacked) + buffer.p2Alt1(message.id) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocDelEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocDelEncoder.kt new file mode 100644 index 000000000..9f636b331 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocDelEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.LocDel + +public class LocDelEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.LOC_DEL + + override fun encode( + buffer: JagByteBuf, + message: LocDel, + ) { + // The function at the bottom of the LOC_DEL has a consistent order, + // making it easy to identify all the properties of this packet: + // loc_add_change_del(world, level, x, z, layer, -1, shape, rotation, 31, 0, -1) + buffer.p1(message.coordInZonePacked) + buffer.p1(message.locPropertiesPacked) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocMergeEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocMergeEncoder.kt new file mode 100644 index 000000000..dd743a342 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/LocMergeEncoder.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.LocMerge + +public class LocMergeEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.LOC_MERGE + + override fun encode( + buffer: JagByteBuf, + message: LocMerge, + ) { + // The function at the bottom of the LOC_MERGE has a consistent order, + // making it easy to identify all the properties of this packet: + // loc_merge(level, x, z, shape, rotation, layer, id, start, end, minX, minZ, maxX, maxZ, player) + buffer.p2Alt1(message.start) + buffer.p2Alt1(message.end) + buffer.p1Alt1(message.coordInZonePacked) + buffer.p2Alt2(message.index) + buffer.p1Alt2(message.minX) + buffer.p2(message.id) + buffer.p1Alt1(message.maxX) + buffer.p1(message.minZ) + buffer.p1Alt2(message.maxZ) + buffer.p1Alt3(message.locPropertiesPacked) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/MapAnimEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/MapAnimEncoder.kt new file mode 100644 index 000000000..dbdb53aef --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/MapAnimEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.MapAnim + +public class MapAnimEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.MAP_ANIM + + override fun encode( + buffer: JagByteBuf, + message: MapAnim, + ) { + // While MAP_ANIM does not have a common function like the rest, + // the constructor for the SpotAnimation object itself has the following order: + // SpotAnimation(id, level, fineX, fineZ, getGroundHeight(fineX, fineZ, level) - height, delay, cycle) + buffer.p1Alt3(message.coordInZonePacked) + buffer.p2Alt2(message.id) + buffer.p1Alt3(message.height) + buffer.p2Alt2(message.delay) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/MapProjAnimEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/MapProjAnimEncoder.kt new file mode 100644 index 000000000..c38312b99 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/MapProjAnimEncoder.kt @@ -0,0 +1,33 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.MapProjAnim + +public class MapProjAnimEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.MAP_PROJANIM + + override fun encode( + buffer: JagByteBuf, + message: MapProjAnim, + ) { + // The function at the bottom of the MAP_PROJANIM has a consistent order, + // making it easy to identify all the properties of this packet: + // map_projanim(level, startX, startZ, endX, endZ, targetIndex, id, + // startHeight, endHeight, startTime, endTime, angle, progress, sourceIndex) + buffer.p2(message.id) + buffer.p1Alt2(message.endHeight) + buffer.p3Alt1(message.targetIndex) + buffer.p2Alt2(message.progress) + buffer.p2(message.endTime) + buffer.p1(message.coordInZonePacked) + buffer.p3Alt2(message.sourceIndex) + buffer.p1Alt1(message.angle) + buffer.p1(message.startHeight) + buffer.p1Alt3(message.deltaX) + buffer.p2Alt2(message.startTime) + buffer.p1(message.deltaZ) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjAddEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjAddEncoder.kt new file mode 100644 index 000000000..c40b3a887 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjAddEncoder.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.ObjAdd + +public class ObjAddEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.OBJ_ADD + + override fun encode( + buffer: JagByteBuf, + message: ObjAdd, + ) { + // The function at the bottom of the OBJ_ADD has a consistent order, + // making it easy to identify all the properties of this packet: + // obj_add(level, x, z, id, quantity, opFlags, + // timeUntilPublic, timeUntilDespawn, ownershipType, neverBecomesPublic) + buffer.p2Alt1(message.timeUntilDespawn) + buffer.p1Alt3(message.coordInZonePacked) + buffer.p2Alt3(message.id) + buffer.p2Alt1(message.timeUntilPublic) + buffer.p1(if (message.neverBecomesPublic) 1 else 0) + buffer.p1(message.opFlags.value) + buffer.p1Alt3(message.ownershipType) + buffer.p4Alt1(message.quantity) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjCountEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjCountEncoder.kt new file mode 100644 index 000000000..31f89002c --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjCountEncoder.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.ObjCount + +public class ObjCountEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.OBJ_COUNT + + override fun encode( + buffer: JagByteBuf, + message: ObjCount, + ) { + // The function at the bottom of the OBJ_COUNT has a consistent order, + // making it easy to identify all the properties of this packet: + // obj_count(level, x, z, id, oldQuantity, newQuantity) + buffer.p1Alt3(message.coordInZonePacked) + buffer.p4(message.newQuantity) + buffer.p2Alt3(message.id) + buffer.p4Alt2(message.oldQuantity) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjDelEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjDelEncoder.kt new file mode 100644 index 000000000..de55f67a9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjDelEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.ObjDel + +public class ObjDelEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.OBJ_DEL + + override fun encode( + buffer: JagByteBuf, + message: ObjDel, + ) { + // The function at the bottom of the OBJ_DEL has a consistent order, + // making it easy to identify all the properties of this packet: + // obj_del(level, x, z, id, quantity) + buffer.p2(message.id) + buffer.p1Alt3(message.coordInZonePacked) + buffer.p4Alt2(message.quantity) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjEnabledOpsEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjEnabledOpsEncoder.kt new file mode 100644 index 000000000..355aa94b9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/ObjEnabledOpsEncoder.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.ObjEnabledOps + +public class ObjEnabledOpsEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.OBJ_ENABLED_OPS + + override fun encode( + buffer: JagByteBuf, + message: ObjEnabledOps, + ) { + // The function at the bottom of the OBJ_OPFILTER has a consistent order, + // making it easy to identify all the properties of this packet: + // obj_opfilter(level, x, z, id, opFlags) + buffer.p1Alt1(message.coordInZonePacked) + buffer.p2Alt2(message.id) + buffer.p1Alt2(message.opFlags.value) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/SoundAreaEncoder.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/SoundAreaEncoder.kt new file mode 100644 index 000000000..7deba196b --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/codec/zone/payload/SoundAreaEncoder.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.ZoneProtEncoder +import net.rsprot.protocol.game.outgoing.prot.GameServerProt +import net.rsprot.protocol.game.outgoing.zone.payload.SoundArea + +public class SoundAreaEncoder : ZoneProtEncoder { + override val prot: ServerProt = GameServerProt.SOUND_AREA + + override fun encode( + buffer: JagByteBuf, + message: SoundArea, + ) { + // While the sound area packet doesn't have a static function call like + // most of these other packets, one can still identify it with relative ease + // using the screenshot below: https://media.z-kris.com/2024/04/0QX3RtlJF9.png + buffer.p1(message.range) + buffer.p2Alt2(message.id) + buffer.p1Alt3(message.dropOffRange) + buffer.p1Alt3(message.loops) + buffer.p1(message.delay) + buffer.p1Alt1(message.coordInZonePacked) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/DesktopGameMessageEncoderRepository.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/DesktopGameMessageEncoderRepository.kt new file mode 100644 index 000000000..14c6875e9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/DesktopGameMessageEncoderRepository.kt @@ -0,0 +1,305 @@ +package net.rsprot.protocol.game.outgoing.prot + +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.ProtRepository +import net.rsprot.protocol.game.outgoing.codec.camera.CamLookAtEasedCoordEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamLookAtEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamModeEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamMoveToArc +import net.rsprot.protocol.game.outgoing.codec.camera.CamMoveToCyclesEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamMoveToEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamResetEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamRotateByEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamRotateToEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamShakeEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamSmoothResetEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamTargetEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.CamTargetOldEncoder +import net.rsprot.protocol.game.outgoing.codec.camera.OculusSyncEncoder +import net.rsprot.protocol.game.outgoing.codec.clan.ClanChannelDeltaEncoder +import net.rsprot.protocol.game.outgoing.codec.clan.ClanChannelFullEncoder +import net.rsprot.protocol.game.outgoing.codec.clan.ClanSettingsDeltaEncoder +import net.rsprot.protocol.game.outgoing.codec.clan.ClanSettingsFullEncoder +import net.rsprot.protocol.game.outgoing.codec.clan.MessageClanChannelEncoder +import net.rsprot.protocol.game.outgoing.codec.clan.MessageClanChannelSystemEncoder +import net.rsprot.protocol.game.outgoing.codec.clan.VarClanDisableEncoder +import net.rsprot.protocol.game.outgoing.codec.clan.VarClanEnableEncoder +import net.rsprot.protocol.game.outgoing.codec.clan.VarClanEncoder +import net.rsprot.protocol.game.outgoing.codec.friendchat.MessageFriendChannelEncoder +import net.rsprot.protocol.game.outgoing.codec.friendchat.UpdateFriendChatChannelFullV1Encoder +import net.rsprot.protocol.game.outgoing.codec.friendchat.UpdateFriendChatChannelFullV2Encoder +import net.rsprot.protocol.game.outgoing.codec.friendchat.UpdateFriendChatChannelSingleUserEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfClearInvEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfCloseSubEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfMoveSubEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfOpenSubEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfOpenTopEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfResyncEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetAngleEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetAnimEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetColourEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetEventsEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetHideEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetModelEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetNpcHeadActiveEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetNpcHeadEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetObjectEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetPlayerHeadEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetPlayerModelBaseColourEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetPlayerModelBodyTypeEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetPlayerModelObjEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetPlayerModelSelfEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetPositionEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetRotateSpeedEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetScrollPosEncoder +import net.rsprot.protocol.game.outgoing.codec.interfaces.IfSetTextEncoder +import net.rsprot.protocol.game.outgoing.codec.inv.UpdateInvFullEncoder +import net.rsprot.protocol.game.outgoing.codec.inv.UpdateInvPartialEncoder +import net.rsprot.protocol.game.outgoing.codec.inv.UpdateInvStopTransmitEncoder +import net.rsprot.protocol.game.outgoing.codec.logout.LogoutEncoder +import net.rsprot.protocol.game.outgoing.codec.logout.LogoutTransferEncoder +import net.rsprot.protocol.game.outgoing.codec.logout.LogoutWithReasonEncoder +import net.rsprot.protocol.game.outgoing.codec.map.RebuildNormalEncoder +import net.rsprot.protocol.game.outgoing.codec.map.RebuildRegionEncoder +import net.rsprot.protocol.game.outgoing.codec.map.RebuildWorldEntityEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.HideLocOpsEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.HideNpcOpsEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.HideObjOpsEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.HintArrowEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.HiscoreReplyEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.MinimapToggleEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.ReflectionCheckerEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.ResetAnimsEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.SendPingEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.ServerTickEndEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.SetHeatmapEnabledEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.SiteSettingsEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.UpdateRebootTimerEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.UpdateUid192Encoder +import net.rsprot.protocol.game.outgoing.codec.misc.client.UrlOpenEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.ChatFilterSettingsEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.ChatFilterSettingsPrivateChatEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.MessageGameEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.RunClientScriptEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.SetMapFlagEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.SetPlayerOpEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.TriggerOnDialogAbortEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.UpdateRunEnergyEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.UpdateRunWeightEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.UpdateStatEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.UpdateStatOldEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.UpdateStockMarketSlotEncoder +import net.rsprot.protocol.game.outgoing.codec.misc.player.UpdateTradingPostEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.NpcInfoLargeEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.NpcInfoSmallEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.SetNpcUpdateOriginEncoder +import net.rsprot.protocol.game.outgoing.codec.playerinfo.PlayerInfoEncoder +import net.rsprot.protocol.game.outgoing.codec.social.FriendListLoadedEncoder +import net.rsprot.protocol.game.outgoing.codec.social.MessagePrivateEchoEncoder +import net.rsprot.protocol.game.outgoing.codec.social.MessagePrivateEncoder +import net.rsprot.protocol.game.outgoing.codec.social.UpdateFriendListEncoder +import net.rsprot.protocol.game.outgoing.codec.social.UpdateIgnoreListEncoder +import net.rsprot.protocol.game.outgoing.codec.sound.MidiJingleEncoder +import net.rsprot.protocol.game.outgoing.codec.sound.MidiSongEncoder +import net.rsprot.protocol.game.outgoing.codec.sound.MidiSongOldEncoder +import net.rsprot.protocol.game.outgoing.codec.sound.MidiSongStopEncoder +import net.rsprot.protocol.game.outgoing.codec.sound.MidiSongWithSecondaryEncoder +import net.rsprot.protocol.game.outgoing.codec.sound.MidiSwapEncoder +import net.rsprot.protocol.game.outgoing.codec.sound.SynthSoundEncoder +import net.rsprot.protocol.game.outgoing.codec.specific.LocAnimSpecificEncoder +import net.rsprot.protocol.game.outgoing.codec.specific.MapAnimSpecificEncoder +import net.rsprot.protocol.game.outgoing.codec.specific.NpcAnimSpecificEncoder +import net.rsprot.protocol.game.outgoing.codec.specific.NpcHeadIconSpecificEncoder +import net.rsprot.protocol.game.outgoing.codec.specific.NpcSpotAnimSpecificEncoder +import net.rsprot.protocol.game.outgoing.codec.specific.PlayerAnimSpecificEncoder +import net.rsprot.protocol.game.outgoing.codec.specific.PlayerSpotAnimSpecificEncoder +import net.rsprot.protocol.game.outgoing.codec.specific.ProjAnimSpecificEncoder +import net.rsprot.protocol.game.outgoing.codec.varp.VarpLargeEncoder +import net.rsprot.protocol.game.outgoing.codec.varp.VarpResetEncoder +import net.rsprot.protocol.game.outgoing.codec.varp.VarpSmallEncoder +import net.rsprot.protocol.game.outgoing.codec.varp.VarpSyncEncoder +import net.rsprot.protocol.game.outgoing.codec.worldentity.ClearEntitiesEncoder +import net.rsprot.protocol.game.outgoing.codec.worldentity.SetActiveWorldEncoder +import net.rsprot.protocol.game.outgoing.codec.worldentity.WorldEntityInfoEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.header.DesktopUpdateZonePartialEnclosedEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.header.UpdateZoneFullFollowsEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.header.UpdateZonePartialFollowsEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocAddChangeEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocAnimEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocDelEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.LocMergeEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.MapAnimEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.MapProjAnimEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjAddEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjCountEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjDelEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.ObjEnabledOpsEncoder +import net.rsprot.protocol.game.outgoing.codec.zone.payload.SoundAreaEncoder +import net.rsprot.protocol.game.outgoing.map.RebuildLogin +import net.rsprot.protocol.game.outgoing.map.RebuildNormal +import net.rsprot.protocol.message.codec.outgoing.MessageEncoderRepository +import net.rsprot.protocol.message.codec.outgoing.MessageEncoderRepositoryBuilder + +public object DesktopGameMessageEncoderRepository { + @ExperimentalStdlibApi + public fun build(huffmanCodecProvider: HuffmanCodecProvider): MessageEncoderRepository { + val protRepository = ProtRepository.of() + val builder = + MessageEncoderRepositoryBuilder( + protRepository, + ).apply { + bind(IfResyncEncoder()) + bind(IfOpenTopEncoder()) + bind(IfOpenSubEncoder()) + bind(IfCloseSubEncoder()) + bind(IfMoveSubEncoder()) + bind(IfClearInvEncoder()) + bind(IfSetEventsEncoder()) + bind(IfSetPositionEncoder()) + bind(IfSetScrollPosEncoder()) + bind(IfSetRotateSpeedEncoder()) + bind(IfSetTextEncoder()) + bind(IfSetHideEncoder()) + bind(IfSetAngleEncoder()) + bind(IfSetObjectEncoder()) + bind(IfSetColourEncoder()) + bind(IfSetAnimEncoder()) + bind(IfSetNpcHeadEncoder()) + bind(IfSetNpcHeadActiveEncoder()) + bind(IfSetPlayerHeadEncoder()) + bind(IfSetModelEncoder()) + bind(IfSetPlayerModelBaseColourEncoder()) + bind(IfSetPlayerModelBodyTypeEncoder()) + bind(IfSetPlayerModelObjEncoder()) + bind(IfSetPlayerModelSelfEncoder()) + + bind(MidiSongEncoder()) + bind(MidiSongWithSecondaryEncoder()) + bind(MidiSwapEncoder()) + bind(MidiSongStopEncoder()) + bind(MidiSongOldEncoder()) + bind(MidiJingleEncoder()) + bind(SynthSoundEncoder()) + + bind(UpdateZoneFullFollowsEncoder()) + bind(UpdateZonePartialFollowsEncoder()) + bind(DesktopUpdateZonePartialEnclosedEncoder()) + + bind(LocAddChangeEncoder()) + bind(LocDelEncoder()) + bind(LocAnimEncoder()) + bind(LocMergeEncoder()) + bind(ObjAddEncoder()) + bind(ObjDelEncoder()) + bind(ObjCountEncoder()) + bind(ObjEnabledOpsEncoder()) + bind(MapAnimEncoder()) + bind(MapProjAnimEncoder()) + bind(SoundAreaEncoder()) + + bind(ProjAnimSpecificEncoder()) + bind(MapAnimSpecificEncoder()) + bind(LocAnimSpecificEncoder()) + bind(NpcHeadIconSpecificEncoder()) + bind(NpcSpotAnimSpecificEncoder()) + bind(NpcAnimSpecificEncoder()) + bind(PlayerAnimSpecificEncoder()) + bind(PlayerSpotAnimSpecificEncoder()) + + bind(PlayerInfoEncoder()) + bind(NpcInfoSmallEncoder()) + bind(NpcInfoLargeEncoder()) + bind(SetNpcUpdateOriginEncoder()) + + bind(ClearEntitiesEncoder()) + bind(SetActiveWorldEncoder()) + bind(WorldEntityInfoEncoder()) + + bindWithAlts(RebuildNormalEncoder(), RebuildLogin::class.java, RebuildNormal::class.java) + bind(RebuildRegionEncoder()) + bind(RebuildWorldEntityEncoder()) + + bind(VarpSmallEncoder()) + bind(VarpLargeEncoder()) + bind(VarpResetEncoder()) + bind(VarpSyncEncoder()) + + bind(CamShakeEncoder()) + bind(CamResetEncoder()) + bind(CamSmoothResetEncoder()) + bind(CamMoveToEncoder()) + bind(CamMoveToCyclesEncoder()) + bind(CamMoveToArc()) + bind(CamLookAtEncoder()) + bind(CamLookAtEasedCoordEncoder()) + bind(CamRotateByEncoder()) + bind(CamRotateToEncoder()) + bind(CamModeEncoder()) + bind(CamTargetEncoder()) + bind(CamTargetOldEncoder()) + bind(OculusSyncEncoder()) + + bind(UpdateInvFullEncoder()) + bind(UpdateInvPartialEncoder()) + bind(UpdateInvStopTransmitEncoder()) + + bind(MessagePrivateEncoder(huffmanCodecProvider)) + bind(MessagePrivateEchoEncoder(huffmanCodecProvider)) + bind(FriendListLoadedEncoder()) + bind(UpdateFriendListEncoder()) + bind(UpdateIgnoreListEncoder()) + + bind(UpdateFriendChatChannelFullV1Encoder()) + bind(UpdateFriendChatChannelFullV2Encoder()) + bind(UpdateFriendChatChannelSingleUserEncoder()) + bind(MessageFriendChannelEncoder(huffmanCodecProvider)) + + bind(VarClanEncoder()) + bind(VarClanEnableEncoder()) + bind(VarClanDisableEncoder()) + bind(ClanChannelFullEncoder()) + bind(ClanChannelDeltaEncoder()) + bind(ClanSettingsFullEncoder()) + bind(ClanSettingsDeltaEncoder()) + bind(MessageClanChannelEncoder(huffmanCodecProvider)) + bind(MessageClanChannelSystemEncoder(huffmanCodecProvider)) + + bind(LogoutEncoder()) + bind(LogoutWithReasonEncoder()) + bind(LogoutTransferEncoder()) + + bind(UpdateRunWeightEncoder()) + bind(UpdateRunEnergyEncoder()) + bind(SetMapFlagEncoder()) + bind(SetPlayerOpEncoder()) + bind(UpdateStatEncoder()) + bind(UpdateStatOldEncoder()) + + bind(RunClientScriptEncoder()) + bind(TriggerOnDialogAbortEncoder()) + bind(MessageGameEncoder()) + bind(ChatFilterSettingsEncoder()) + bind(ChatFilterSettingsPrivateChatEncoder()) + bind(UpdateTradingPostEncoder()) + bind(UpdateStockMarketSlotEncoder()) + + bind(HintArrowEncoder()) + bind(ResetAnimsEncoder()) + bind(UpdateRebootTimerEncoder()) + bind(SetHeatmapEnabledEncoder()) + bind(MinimapToggleEncoder()) + bind(ServerTickEndEncoder()) + bind(HideNpcOpsEncoder()) + bind(HideObjOpsEncoder()) + bind(HideLocOpsEncoder()) + + bind(UrlOpenEncoder()) + bind(SiteSettingsEncoder()) + bind(UpdateUid192Encoder()) + bind(ReflectionCheckerEncoder()) + bind(SendPingEncoder()) + bind(HiscoreReplyEncoder()) + } + return builder.build() + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/GameServerProt.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/GameServerProt.kt new file mode 100644 index 000000000..78777646f --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/GameServerProt.kt @@ -0,0 +1,202 @@ +package net.rsprot.protocol.game.outgoing.prot + +import net.rsprot.protocol.Prot +import net.rsprot.protocol.ServerProt + +public enum class GameServerProt( + override val opcode: Int, + override val size: Int, +) : ServerProt { + // Interface related packets + IF_RESYNC(GameServerProtId.IF_RESYNC, Prot.VAR_SHORT), + IF_OPENTOP(GameServerProtId.IF_OPENTOP, 2), + IF_OPENSUB(GameServerProtId.IF_OPENSUB, 7), + IF_CLOSESUB(GameServerProtId.IF_CLOSESUB, 4), + IF_MOVESUB(GameServerProtId.IF_MOVESUB, 8), + IF_CLEARINV(GameServerProtId.IF_CLEARINV, 4), + IF_SETEVENTS(GameServerProtId.IF_SETEVENTS, 12), + IF_SETPOSITION(GameServerProtId.IF_SETPOSITION, 8), + IF_SETSCROLLPOS(GameServerProtId.IF_SETSCROLLPOS, 6), + IF_SETROTATESPEED(GameServerProtId.IF_SETROTATESPEED, 8), + IF_SETTEXT(GameServerProtId.IF_SETTEXT, Prot.VAR_SHORT), + IF_SETHIDE(GameServerProtId.IF_SETHIDE, 5), + IF_SETANGLE(GameServerProtId.IF_SETANGLE, 10), + IF_SETOBJECT(GameServerProtId.IF_SETOBJECT, 10), + IF_SETCOLOUR(GameServerProtId.IF_SETCOLOUR, 6), + IF_SETANIM(GameServerProtId.IF_SETANIM, 6), + IF_SETNPCHEAD(GameServerProtId.IF_SETNPCHEAD, 6), + IF_SETNPCHEAD_ACTIVE(GameServerProtId.IF_SETNPCHEAD_ACTIVE, 6), + IF_SETPLAYERHEAD(GameServerProtId.IF_SETPLAYERHEAD, 4), + IF_SETMODEL(GameServerProtId.IF_SETMODEL, 6), + IF_SETPLAYERMODEL_BASECOLOUR(GameServerProtId.IF_SETPLAYERMODEL_BASECOLOUR, 6), + IF_SETPLAYERMODEL_BODYTYPE(GameServerProtId.IF_SETPLAYERMODEL_BODYTYPE, 5), + IF_SETPLAYERMODEL_OBJ(GameServerProtId.IF_SETPLAYERMODEL_OBJ, 8), + IF_SETPLAYERMODEL_SELF(GameServerProtId.IF_SETPLAYERMODEL_SELF, 5), + + // Music-system related packets (excl. zone ones) + MIDI_SONG(GameServerProtId.MIDI_SONG, 10), + MIDI_SONG_WITHSECONDARY(GameServerProtId.MIDI_SONG_WITHSECONDARY, 12), + MIDI_SWAP(GameServerProtId.MIDI_SWAP, 8), + MIDI_SONG_STOP(GameServerProtId.MIDI_SONG_STOP, 4), + MIDI_SONG_OLD(GameServerProtId.MIDI_SONG_OLD, 2), + MIDI_JINGLE(GameServerProtId.MIDI_JINGLE, 5), + SYNTH_SOUND(GameServerProtId.SYNTH_SOUND, 5), + + // Zone header packets + UPDATE_ZONE_FULL_FOLLOWS(GameServerProtId.UPDATE_ZONE_FULL_FOLLOWS, 3), + UPDATE_ZONE_PARTIAL_FOLLOWS(GameServerProtId.UPDATE_ZONE_PARTIAL_FOLLOWS, 3), + UPDATE_ZONE_PARTIAL_ENCLOSED(GameServerProtId.UPDATE_ZONE_PARTIAL_ENCLOSED, Prot.VAR_SHORT), + + // Zone payload packets + LOC_ADD_CHANGE(GameServerProtId.LOC_ADD_CHANGE, 5), + LOC_DEL(GameServerProtId.LOC_DEL, 2), + LOC_ANIM(GameServerProtId.LOC_ANIM, 4), + LOC_MERGE(GameServerProtId.LOC_MERGE, 14), + OBJ_ADD(GameServerProtId.OBJ_ADD, 14), + OBJ_DEL(GameServerProtId.OBJ_DEL, 7), + OBJ_COUNT(GameServerProtId.OBJ_COUNT, 11), + OBJ_ENABLED_OPS(GameServerProtId.OBJ_ENABLED_OPS, 4), + MAP_ANIM(GameServerProtId.MAP_ANIM, 6), + MAP_PROJANIM(GameServerProtId.MAP_PROJANIM, 20), + SOUND_AREA(GameServerProtId.SOUND_AREA, 7), + + // Specific packets + PROJANIM_SPECIFIC(GameServerProtId.PROJANIM_SPECIFIC, 22), + + @Deprecated( + "Deprecated as a new variant that supports source index was introduced.", + replaceWith = ReplaceWith("PROJANIM_SPECIFIC"), + ) + PROJANIM_SPECIFIC_OLD(GameServerProtId.PROJANIM_SPECIFIC_OLD, 19), + + @Deprecated( + "Deprecated as it is bugged(size: 17; payload: 18) and " + + "a newer variant with greater property ranges is introduced", + replaceWith = ReplaceWith("PROJANIM_SPECIFIC"), + ) + PROJANIM_SPECIFIC_OLD_OLD(GameServerProtId.PROJANIM_SPECIFIC_OLD_OLD, 17), + MAP_ANIM_SPECIFIC(GameServerProtId.MAP_ANIM_SPECIFIC, 8), + LOC_ANIM_SPECIFIC(GameServerProtId.LOC_ANIM_SPECIFIC, 6), + NPC_HEADICON_SPECIFIC(GameServerProtId.NPC_HEADICON_SPECIFIC, 9), + NPC_SPOTANIM_SPECIFIC(GameServerProtId.NPC_SPOTANIM_SPECIFIC, 9), + NPC_ANIM_SPECIFIC(GameServerProtId.NPC_ANIM_SPECIFIC, 5), + PLAYER_ANIM_SPECIFIC(GameServerProtId.PLAYER_ANIM_SPECIFIC, 3), + PLAYER_SPOTANIM_SPECIFIC(GameServerProtId.PLAYER_SPOTANIM_SPECIFIC, 9), + + // Info packets + PLAYER_INFO(GameServerProtId.PLAYER_INFO, Prot.VAR_SHORT), + NPC_INFO_SMALL(GameServerProtId.NPC_INFO_SMALL, Prot.VAR_SHORT), + NPC_INFO_LARGE(GameServerProtId.NPC_INFO_LARGE, Prot.VAR_SHORT), + SET_NPC_UPDATE_ORIGIN(GameServerProtId.SET_NPC_UPDATE_ORIGIN, 2), + + // World entity packets + CLEAR_ENTITIES(GameServerProtId.CLEAR_ENTITIES, 0), + SET_ACTIVE_WORLD(GameServerProtId.SET_ACTIVE_WORLD, 4), + WORLDENTITY_INFO(GameServerProtId.WORLDENTITY_INFO, Prot.VAR_SHORT), + + @Deprecated( + "Deprecated as a new variant that supports fine height was introduced.", + replaceWith = ReplaceWith("WORLDENTITY_INFO"), + ) + WORLDENTITY_INFO_OLD(GameServerProtId.WORLDENTITY_INFO_OLD, Prot.VAR_SHORT), + + // Map packets + REBUILD_NORMAL(GameServerProtId.REBUILD_NORMAL, Prot.VAR_SHORT), + REBUILD_REGION(GameServerProtId.REBUILD_REGION, Prot.VAR_SHORT), + REBUILD_WORLDENTITY(GameServerProtId.REBUILD_WORLDENTITY, Prot.VAR_SHORT), + + // Varp packets + VARP_SMALL(GameServerProtId.VARP_SMALL, 3), + VARP_LARGE(GameServerProtId.VARP_LARGE, 6), + VARP_RESET(GameServerProtId.VARP_RESET, 0), + VARP_SYNC(GameServerProtId.VARP_SYNC, 0), + + // Camera packets + CAM_SHAKE(GameServerProtId.CAM_SHAKE, 4), + CAM_RESET(GameServerProtId.CAM_RESET, 0), + CAM_SMOOTHRESET(GameServerProtId.CAM_SMOOTHRESET, 4), + CAM_MOVETO(GameServerProtId.CAM_MOVETO, 6), + CAM_MOVETO_CYCLES(GameServerProtId.CAM_MOVETO_CYCLES, 8), + CAM_MOVETO_ARC(GameServerProtId.CAM_MOVETO_ARC, 10), + CAM_LOOKAT(GameServerProtId.CAM_LOOKAT, 6), + CAM_LOOKAT_EASED_COORD(GameServerProtId.CAM_LOOKAT_EASED_COORD, 7), + CAM_ROTATEBY(GameServerProtId.CAM_ROTATEBY, 7), + CAM_ROTATETO(GameServerProtId.CAM_ROTATETO, 7), + CAM_MODE(GameServerProtId.CAM_MODE, 1), + CAM_TARGET(GameServerProtId.CAM_TARGET, 5), + CAM_TARGET_OLD(GameServerProtId.CAM_TARGET_OLD, 3), + OCULUS_SYNC(GameServerProtId.OCULUS_SYNC, 4), + + // Inventory packets + UPDATE_INV_FULL(GameServerProtId.UPDATE_INV_FULL, Prot.VAR_SHORT), + UPDATE_INV_PARTIAL(GameServerProtId.UPDATE_INV_PARTIAL, Prot.VAR_SHORT), + UPDATE_INV_STOPTRANSMIT(GameServerProtId.UPDATE_INV_STOPTRANSMIT, 2), + + // Social packets + MESSAGE_PRIVATE(GameServerProtId.MESSAGE_PRIVATE, Prot.VAR_SHORT), + MESSAGE_PRIVATE_ECHO(GameServerProtId.MESSAGE_PRIVATE_ECHO, Prot.VAR_SHORT), + FRIENDLIST_LOADED(GameServerProtId.FRIENDLIST_LOADED, 0), + UPDATE_FRIENDLIST(GameServerProtId.UPDATE_FRIENDLIST, Prot.VAR_SHORT), + UPDATE_IGNORELIST(GameServerProtId.UPDATE_IGNORELIST, Prot.VAR_SHORT), + + // Friend chat (old "clans") packets + UPDATE_FRIENDCHAT_CHANNEL_FULL_V1(GameServerProtId.UPDATE_FRIENDCHAT_CHANNEL_FULL_V1, Prot.VAR_SHORT), + UPDATE_FRIENDCHAT_CHANNEL_FULL_V2(GameServerProtId.UPDATE_FRIENDCHAT_CHANNEL_FULL_V2, Prot.VAR_SHORT), + UPDATE_FRIENDCHAT_CHANNEL_SINGLEUSER(GameServerProtId.UPDATE_FRIENDCHAT_CHANNEL_SINGLEUSER, Prot.VAR_BYTE), + MESSAGE_FRIENDCHANNEL(GameServerProtId.MESSAGE_FRIENDCHANNEL, Prot.VAR_BYTE), + + // Clan chat packets + VARCLAN(GameServerProtId.VARCLAN, Prot.VAR_BYTE), + VARCLAN_ENABLE(GameServerProtId.VARCLAN_ENABLE, 0), + VARCLAN_DISABLE(GameServerProtId.VARCLAN_DISABLE, 0), + CLANCHANNEL_FULL(GameServerProtId.CLANCHANNEL_FULL, Prot.VAR_SHORT), + CLANCHANNEL_DELTA(GameServerProtId.CLANCHANNEL_DELTA, Prot.VAR_SHORT), + CLANSETTINGS_FULL(GameServerProtId.CLANSETTINGS_FULL, Prot.VAR_SHORT), + CLANSETTINGS_DELTA(GameServerProtId.CLANSETTINGS_DELTA, Prot.VAR_SHORT), + MESSAGE_CLANCHANNEL(GameServerProtId.MESSAGE_CLANCHANNEL, Prot.VAR_BYTE), + MESSAGE_CLANCHANNEL_SYSTEM(GameServerProtId.MESSAGE_CLANCHANNEL_SYSTEM, Prot.VAR_BYTE), + + // Log out packets + LOGOUT(GameServerProtId.LOGOUT, 0), + LOGOUT_WITHREASON(GameServerProtId.LOGOUT_WITHREASON, 1), + LOGOUT_TRANSFER(GameServerProtId.LOGOUT_TRANSFER, Prot.VAR_BYTE), + + // Misc. player state packets + UPDATE_RUNWEIGHT(GameServerProtId.UPDATE_RUNWEIGHT, 2), + UPDATE_RUNENERGY(GameServerProtId.UPDATE_RUNENERGY, 2), + SET_MAP_FLAG(GameServerProtId.SET_MAP_FLAG, 2), + SET_PLAYER_OP(GameServerProtId.SET_PLAYER_OP, Prot.VAR_BYTE), + UPDATE_STAT(GameServerProtId.UPDATE_STAT, 7), + UPDATE_STAT_OLD(GameServerProtId.UPDATE_STAT_OLD, 6), + + // Misc. player packets + RUNCLIENTSCRIPT(GameServerProtId.RUNCLIENTSCRIPT, Prot.VAR_SHORT), + TRIGGER_ONDIALOGABORT(GameServerProtId.TRIGGER_ONDIALOGABORT, 0), + MESSAGE_GAME(GameServerProtId.MESSAGE_GAME, Prot.VAR_BYTE), + CHAT_FILTER_SETTINGS(GameServerProtId.CHAT_FILTER_SETTINGS, 2), + CHAT_FILTER_SETTINGS_PRIVATECHAT(GameServerProtId.CHAT_FILTER_SETTINGS_PRIVATECHAT, 1), + UPDATE_TRADINGPOST(GameServerProtId.UPDATE_TRADINGPOST, Prot.VAR_SHORT), + UPDATE_STOCKMARKET_SLOT(GameServerProtId.UPDATE_STOCKMARKET_SLOT, 20), + + // Misc. client state packets + HINT_ARROW(GameServerProtId.HINT_ARROW, 6), + RESET_ANIMS(GameServerProtId.RESET_ANIMS, 0), + UPDATE_REBOOT_TIMER(GameServerProtId.UPDATE_REBOOT_TIMER, 2), + SET_HEATMAP_ENABLED(GameServerProtId.SET_HEATMAP_ENABLED, 1), + MINIMAP_TOGGLE(GameServerProtId.MINIMAP_TOGGLE, 1), + SERVER_TICK_END(GameServerProtId.SERVER_TICK_END, 0), + HIDENPCOPS(GameServerProtId.HIDENPCOPS, 1), + HIDEOBJOPS(GameServerProtId.HIDEOBJOPS, 1), + HIDELOCOPS(GameServerProtId.HIDELOCOPS, 1), + + // Misc. client packets + URL_OPEN(GameServerProtId.URL_OPEN, Prot.VAR_SHORT), + SITE_SETTINGS(GameServerProtId.SITE_SETTINGS, Prot.VAR_BYTE), + UPDATE_UID192(GameServerProtId.UPDATE_UID192, 28), + REFLECTION_CHECKER(GameServerProtId.REFLECTION_CHECKER, Prot.VAR_SHORT), + SEND_PING(GameServerProtId.SEND_PING, 8), + HISCORE_REPLY(GameServerProtId.HISCORE_REPLY, Prot.VAR_SHORT), + + // Unknown packets + UNKNOWN_STRING(GameServerProtId.UNKNOWN_STRING, Prot.VAR_BYTE), +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/GameServerProtId.kt b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/GameServerProtId.kt new file mode 100644 index 000000000..a75d309ec --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/main/kotlin/net/rsprot/protocol/game/outgoing/prot/GameServerProtId.kt @@ -0,0 +1,141 @@ +package net.rsprot.protocol.game.outgoing.prot + +internal object GameServerProtId { + const val UPDATE_FRIENDCHAT_CHANNEL_SINGLEUSER = 0 + const val SET_PLAYER_OP = 1 + const val PLAYER_INFO = 2 + const val MIDI_SONG_STOP = 3 + const val IF_CLEARINV = 4 + const val UPDATE_ZONE_PARTIAL_ENCLOSED = 5 + const val IF_SETNPCHEAD_ACTIVE = 6 + const val LOC_ANIM = 7 + const val UPDATE_STAT = 8 + const val CAM_SHAKE = 9 + const val UPDATE_ZONE_FULL_FOLLOWS = 10 + const val UPDATE_ZONE_PARTIAL_FOLLOWS = 11 + const val VARCLAN_ENABLE = 12 + const val PROJANIM_SPECIFIC_OLD_OLD = 13 + const val IF_SETROTATESPEED = 14 + const val SET_MAP_FLAG = 15 + const val MESSAGE_FRIENDCHANNEL = 16 + const val SOUND_AREA = 17 + const val OBJ_COUNT = 18 + const val IF_SETCOLOUR = 19 + const val MESSAGE_PRIVATE = 20 + const val PROJANIM_SPECIFIC_OLD = 21 + const val CHAT_FILTER_SETTINGS = 22 + const val CAM_MOVETO = 23 + const val CAM_ROTATEBY = 24 + const val VARP_SMALL = 25 + const val UPDATE_INV_FULL = 26 + const val IF_CLOSESUB = 27 + const val IF_SETOBJECT = 28 + const val IF_SETPLAYERMODEL_BASECOLOUR = 29 + const val CAM_SMOOTHRESET = 30 + const val MAP_ANIM_SPECIFIC = 31 + const val CAM_MODE = 32 + const val IF_SETPLAYERMODEL_BODYTYPE = 33 + const val MESSAGE_CLANCHANNEL = 34 + const val CAM_LOOKAT_EASED_COORD = 35 + const val IF_RESYNC = 36 + const val CAM_TARGET_OLD = 37 + const val IF_SETPLAYERHEAD = 38 + const val OBJ_ENABLED_OPS = 39 + const val REBUILD_NORMAL = 40 + const val LOC_DEL = 41 + const val VARP_RESET = 42 + const val LOC_ANIM_SPECIFIC = 43 + const val MAP_ANIM = 44 + const val SERVER_TICK_END = 45 + const val TRIGGER_ONDIALOGABORT = 46 + const val UPDATE_REBOOT_TIMER = 47 + const val VARCLAN = 48 + const val MESSAGE_PRIVATE_ECHO = 49 + const val UPDATE_FRIENDCHAT_CHANNEL_FULL_V1 = 50 + const val OBJ_ADD = 51 + const val SYNTH_SOUND = 52 + const val VARP_LARGE = 53 + const val RESET_ANIMS = 54 + const val OBJ_DEL = 55 + const val IF_SETPLAYERMODEL_OBJ = 56 + const val IF_SETHIDE = 57 + const val UPDATE_TRADINGPOST = 58 + const val LOGOUT = 59 + const val PLAYER_ANIM_SPECIFIC = 60 + const val UPDATE_FRIENDLIST = 61 + const val CAM_LOOKAT = 62 + const val FRIENDLIST_LOADED = 63 + const val IF_SETANGLE = 64 + const val IF_SETANIM = 65 + const val CAM_RESET = 66 + const val LOGOUT_WITHREASON = 67 + const val UPDATE_RUNENERGY = 68 + const val REFLECTION_CHECKER = 69 + const val CAM_MOVETO_CYCLES = 70 + const val UPDATE_INV_PARTIAL = 71 + const val LOC_ADD_CHANGE = 72 + const val CAM_ROTATETO = 73 + const val UPDATE_INV_STOPTRANSMIT = 74 + const val WORLDENTITY_INFO_OLD = 75 + const val MIDI_SONG_WITHSECONDARY = 76 + const val UPDATE_STOCKMARKET_SLOT = 77 + const val RUNCLIENTSCRIPT = 78 + const val VARP_SYNC = 79 + const val MESSAGE_GAME = 80 + const val CLANSETTINGS_FULL = 81 + const val CHAT_FILTER_SETTINGS_PRIVATECHAT = 82 + const val MIDI_JINGLE = 83 + const val URL_OPEN = 84 + const val SET_HEATMAP_ENABLED = 85 + const val MESSAGE_CLANCHANNEL_SYSTEM = 86 + const val IF_SETPOSITION = 87 + const val VARCLAN_DISABLE = 88 + const val IF_SETPLAYERMODEL_SELF = 89 + const val IF_SETMODEL = 90 + const val IF_SETSCROLLPOS = 91 + const val CLANCHANNEL_FULL = 92 + const val CLANCHANNEL_DELTA = 93 + const val UPDATE_RUNWEIGHT = 94 + const val REBUILD_REGION = 95 + const val LOC_MERGE = 96 + const val IF_SETNPCHEAD = 97 + const val IF_OPENTOP = 98 + const val IF_SETTEXT = 99 + const val HISCORE_REPLY = 100 + const val MINIMAP_TOGGLE = 101 + const val IF_SETEVENTS = 102 + const val HINT_ARROW = 103 + const val OCULUS_SYNC = 104 + const val NPC_INFO_SMALL = 105 + const val IF_OPENSUB = 106 + const val MIDI_SONG = 107 + const val UPDATE_IGNORELIST = 108 + const val NPC_INFO_LARGE = 109 + const val CAM_MOVETO_ARC = 110 + const val NPC_ANIM_SPECIFIC = 111 + const val SEND_PING = 112 + const val UPDATE_STAT_OLD = 113 + const val NPC_HEADICON_SPECIFIC = 114 + const val CLANSETTINGS_DELTA = 115 + const val PLAYER_SPOTANIM_SPECIFIC = 116 + const val IF_MOVESUB = 117 + const val UPDATE_FRIENDCHAT_CHANNEL_FULL_V2 = 118 + const val NPC_SPOTANIM_SPECIFIC = 119 + const val MIDI_SWAP = 120 + const val SITE_SETTINGS = 121 + const val LOGOUT_TRANSFER = 122 + const val MAP_PROJANIM = 123 + const val MIDI_SONG_OLD = 124 + const val CAM_TARGET = 125 + const val SET_NPC_UPDATE_ORIGIN = 126 + const val UPDATE_UID192 = 127 + const val CLEAR_ENTITIES = 128 + const val SET_ACTIVE_WORLD = 129 + const val HIDEOBJOPS = 130 + const val REBUILD_WORLDENTITY = 131 + const val HIDENPCOPS = 132 + const val HIDELOCOPS = 133 + const val WORLDENTITY_INFO = 134 + const val PROJANIM_SPECIFIC = 135 + const val UNKNOWN_STRING = 136 +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoClient.kt b/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoClient.kt new file mode 100644 index 000000000..260371fad --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoClient.kt @@ -0,0 +1,304 @@ +package net.rsprot.protocol.game.outgoing.info + +import io.netty.buffer.ByteBuf +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.bitbuffer.BitBuf +import net.rsprot.buffer.bitbuffer.toBitBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid + +@Suppress("MemberVisibilityCanBePrivate") +class NpcInfoClient { + var deletedNpcCount: Int = 0 + var deletedNpcSlot = IntArray(1000) + var cachedNpcs = arrayOfNulls(65536) + var npcSlotCount = 0 + var npcSlot = IntArray(65536) + var updatedNpcSlotCount: Int = 0 + var updatedNpcSlot: IntArray = IntArray(250) + + var cycle = 0 + + fun decode( + buffer: ByteBuf, + large: Boolean, + localPlayerCoord: CoordGrid, + ) { + deletedNpcCount = 0 + updatedNpcSlotCount = 0 + buffer.toBitBuf().use { bitBuffer -> + processHighResolution(bitBuffer) + processLowResolution(large, bitBuffer, localPlayerCoord) + } + processExtendedInfo(buffer.toJagByteBuf()) + for (i in 0..= indexBitCount + 12) { + val index = buffer.gBits(indexBitCount) + if (capacity - 1 != index) { + var isNew = false + if (cachedNpcs[index] == null) { + cachedNpcs[index] = Npc(index, -1, CoordGrid.INVALID) + isNew = true + } + val npc = checkNotNull(cachedNpcs[index]) + npcSlot[npcSlotCount++] = index + npc.lastUpdateCycle = cycle + val jump = buffer.gBits(1) + val hasSpawnCycle = buffer.gBits(1) == 1 + if (hasSpawnCycle) { + npc.spawnCycle = buffer.gBits(32) + } + val angle = NPC_TURN_ANGLES[buffer.gBits(3)] + if (isNew) { + npc.turnAngle = angle + npc.angle = angle + } + val deltaX = decodeDelta(large, buffer) + val deltaZ = decodeDelta(large, buffer) + npc.id = buffer.gBits(14) + val extendedInfo = buffer.gBits(1) + if (extendedInfo == 1) { + updatedNpcSlot[updatedNpcSlotCount++] = index + } + // reset bas + if (npc.turnSpeed == 0) { + npc.angle = 0 + } + npc.addRouteWaypoint( + localPlayerCoord, + deltaX, + deltaZ, + jump == 1, + ) + continue + } + } + return + } + } + + private fun decodeDelta( + large: Boolean, + buffer: BitBuf, + ): Int = + if (large) { + var delta = buffer.gBits(8) + if (delta > 127) { + delta -= 256 + } + delta + } else { + var delta = buffer.gBits(5) + if (delta > 15) { + delta -= 32 + } + delta + } + + class Npc( + val index: Int, + var id: Int, + var coord: CoordGrid, + ) { + var lastUpdateCycle: Int = 0 + var moveSpeed: MoveSpeed = MoveSpeed.STATIONARY + var turnAngle = 0 + var angle = 0 + var spawnCycle = 0 + var turnSpeed = 32 + var jump: Boolean = false + var overheadChat: String? = null + + fun addRouteWaypoint( + localPlayerCoord: CoordGrid, + relativeX: Int, + relativeZ: Int, + jump: Boolean, + ) { + coord = + CoordGrid( + localPlayerCoord.level, + localPlayerCoord.x + relativeX, + localPlayerCoord.z + relativeZ, + ) + moveSpeed = MoveSpeed.STATIONARY + this.jump = jump + } + + fun addRouteWaypointAdjacent( + opcode: Int, + speed: MoveSpeed, + ) { + var x: Int = coord.x + var z: Int = coord.z + if (opcode == 0) { + --x + ++z + } + + if (opcode == 1) { + ++z + } + + if (opcode == 2) { + ++x + ++z + } + + if (opcode == 3) { + --x + } + + if (opcode == 4) { + ++x + } + + if (opcode == 5) { + --x + --z + } + + if (opcode == 6) { + --z + } + + if (opcode == 7) { + ++x + --z + } + + coord = CoordGrid(coord.level, x, z) + moveSpeed = speed + } + + override fun toString(): String = + "Npc(" + + "index=$index, " + + "id=$id, " + + "coord=$coord, " + + "lastUpdateCycle=$lastUpdateCycle, " + + "moveSpeed=$moveSpeed, " + + "turnAngle=$turnAngle, " + + "angle=$angle, " + + "spawnCycle=$spawnCycle, " + + "turnSpeed=$turnSpeed, " + + "jump=$jump" + + ")" + } + + enum class MoveSpeed( + val id: Int, + ) { + STATIONARY(-1), + CRAWL(0), + WALK(1), + RUN(2), + } + + private companion object { + private val NPC_TURN_ANGLES = intArrayOf(768, 1024, 1280, 512, 1536, 256, 0, 1792) + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoTest.kt b/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoTest.kt new file mode 100644 index 000000000..a08340169 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/NpcInfoTest.kt @@ -0,0 +1,314 @@ +package net.rsprot.protocol.game.outgoing.info + +import io.netty.buffer.PooledByteBufAllocator +import io.netty.buffer.Unpooled +import net.rsprot.compression.HuffmanCodec +import net.rsprot.compression.provider.DefaultHuffmanCodecProvider +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.outgoing.codec.npcinfo.DesktopLowResolutionChangeEncoder +import net.rsprot.protocol.game.outgoing.codec.npcinfo.extendedinfo.writer.NpcAvatarExtendedInfoDesktopWriter +import net.rsprot.protocol.game.outgoing.info.filter.DefaultExtendedInfoFilter +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatar +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarExceptionHandler +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcAvatarFactory +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcIndexSupplier +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcInfo +import net.rsprot.protocol.game.outgoing.info.npcinfo.NpcInfoProtocol +import net.rsprot.protocol.game.outgoing.info.util.BuildArea +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.random.Random +import kotlin.test.assertEquals + +class NpcInfoTest { + private lateinit var protocol: NpcInfoProtocol + private lateinit var client: NpcInfoClient + private val random: Random = Random(0) + private lateinit var serverNpcs: List + private lateinit var supplier: NpcIndexSupplier + private lateinit var localNpcInfo: NpcInfo + private var localPlayerCoord = CoordGrid(0, 3207, 3207) + + @BeforeEach + fun initialize() { + val allocator = PooledByteBufAllocator.DEFAULT + val factory = + NpcAvatarFactory( + allocator, + DefaultExtendedInfoFilter(), + listOf(NpcAvatarExtendedInfoDesktopWriter()), + DefaultHuffmanCodecProvider(createHuffmanCodec()), + ) + this.serverNpcs = createPhantomNpcs(factory) + this.supplier = createNpcIndexSupplier() + + val encoders = + ClientTypeMap.of( + listOf(DesktopLowResolutionChangeEncoder()), + OldSchoolClientType.COUNT, + ) { + it.clientType + } + protocol = + NpcInfoProtocol( + allocator, + supplier, + encoders, + factory, + npcExceptionHandler(), + ) + this.client = NpcInfoClient() + this.localNpcInfo = protocol.alloc(500, OldSchoolClientType.DESKTOP) + } + + private fun npcExceptionHandler(): NpcAvatarExceptionHandler = + NpcAvatarExceptionHandler { _, _ -> + // No-op + } + + private fun tick() { + localNpcInfo.updateCoord(NpcInfo.ROOT_WORLD, localPlayerCoord.level, localPlayerCoord.x, localPlayerCoord.z) + localNpcInfo.updateBuildArea( + NpcInfo.ROOT_WORLD, + BuildArea( + (localPlayerCoord.x ushr 3) - 6, + (localPlayerCoord.z ushr 3) - 6, + ), + ) + protocol.update() + } + + @Test + fun `adding npcs to high resolution`() { + tick() + val buffer = this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD) + client.decode(buffer, false, localPlayerCoord) + for (index in client.cachedNpcs.indices) { + val clientNpc = client.cachedNpcs[index] ?: continue + val serverNpc = this.serverNpcs[index] + assertEquals(serverNpc.coordGrid, clientNpc.coord) + assertEquals(serverNpc.index, clientNpc.index) + assertEquals(serverNpc.id, clientNpc.id) + } + } + + @Test + fun `removing npcs from high resolution`() { + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + + this.localPlayerCoord = CoordGrid(0, 2000, 2000) + this.localNpcInfo.updateCoord( + NpcInfo.ROOT_WORLD, + localPlayerCoord.level, + localPlayerCoord.x, + localPlayerCoord.z, + ) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(0, client.npcSlotCount) + } + + @Test + fun `single npc walking`() { + val npc = serverNpcs.first() + // Skip everyone but the first entry + serverNpcs = listOf(npc) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(1, client.npcSlotCount) + val clientNpc = checkNotNull(client.cachedNpcs[client.npcSlot[0]]) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + + npc.avatar.walk(0, 1) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + } + + @Test + fun `single npc crawling`() { + val npc = serverNpcs.first() + // Skip everyone but the first entry + serverNpcs = listOf(npc) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(1, client.npcSlotCount) + val clientNpc = checkNotNull(client.cachedNpcs[client.npcSlot[0]]) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + + npc.avatar.crawl(0, 1) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + } + + @Test + fun `single npc running`() { + val npc = serverNpcs.first() + // Skip everyone but the first entry + serverNpcs = listOf(npc) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(1, client.npcSlotCount) + val clientNpc = checkNotNull(client.cachedNpcs[client.npcSlot[0]]) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + + npc.avatar.walk(0, 1) + npc.avatar.walk(0, 1) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + } + + @Test + fun `single npc telejumping`() { + val npc = serverNpcs.first() + // Skip everyone but the first entry + serverNpcs = listOf(npc) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(1, client.npcSlotCount) + var clientNpc = checkNotNull(client.cachedNpcs[client.npcSlot[0]]) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + + npc.avatar.teleport( + localPlayerCoord.level, + localPlayerCoord.x + 10, + localPlayerCoord.z + 10, + true, + ) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + // Re-obtain the instance as teleporting is equal to removal + adding + clientNpc = checkNotNull(client.cachedNpcs[client.npcSlot[0]]) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + } + + @Test + fun `single npc teleporting`() { + val npc = serverNpcs.first() + // Skip everyone but the first entry + serverNpcs = listOf(npc) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(1, client.npcSlotCount) + var clientNpc = checkNotNull(client.cachedNpcs[client.npcSlot[0]]) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + + npc.avatar.teleport( + localPlayerCoord.level, + localPlayerCoord.x + 10, + localPlayerCoord.z + 10, + false, + ) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + // Re-obtain the instance as teleporting is equal to removal + adding + clientNpc = checkNotNull(client.cachedNpcs[client.npcSlot[0]]) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + } + + @Test + fun `single npc overhead chat`() { + val npc = serverNpcs.first() + // Skip everyone but the first entry + serverNpcs = listOf(npc) + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(1, client.npcSlotCount) + val clientNpc = checkNotNull(client.cachedNpcs[client.npcSlot[0]]) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + + npc.avatar.extendedInfo.setSay("Hello world") + tick() + client.decode(this.localNpcInfo.backingBuffer(NpcInfo.ROOT_WORLD), false, localPlayerCoord) + assertEquals(npc.id, clientNpc.id) + assertEquals(npc.index, clientNpc.index) + assertEquals(npc.coordGrid, clientNpc.coord) + assertEquals("Hello world", clientNpc.overheadChat) + } + + private fun createNpcIndexSupplier(): NpcIndexSupplier = + NpcIndexSupplier { _, level, x, z, viewDistance -> + serverNpcs + .filter { it.coordGrid.inDistance(CoordGrid(level, x, z), viewDistance) } + .map { it.index } + .iterator() + } + + private fun createPhantomNpcs(factory: NpcAvatarFactory): List { + val npcs = ArrayList(500) + for (index in 0..<500) { + val x = random.nextInt(3200, 3213) + val z = random.nextInt(3200, 3213) + val id = (index * x * z) and 0x3FFF + val coord = CoordGrid(0, x, z) + npcs += + Npc( + index, + id, + factory.alloc( + index, + id, + coord.level, + coord.x, + coord.z, + ), + ) + } + return npcs + } + + private data class Npc( + val index: Int, + val id: Int, + val avatar: NpcAvatar, + ) { + val coordGrid: CoordGrid + get() = avatar.getCoordGrid() + + override fun toString(): String = + "Npc(" + + "index=$index, " + + "id=$id, " + + "coordGrid=${avatar.getCoordGrid()}" + + ")" + } + + private companion object { + private fun NpcAvatar.getCoordGrid(): CoordGrid = CoordGrid(level(), x(), z()) + + private fun createHuffmanCodec(): HuffmanCodec { + val resource = PlayerInfoTest::class.java.getResourceAsStream("huffman.dat") + checkNotNull(resource) { + "huffman.dat could not be found" + } + return HuffmanCodec.create(Unpooled.wrappedBuffer(resource.readBytes())) + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoClient.kt b/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoClient.kt new file mode 100644 index 000000000..49c787981 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoClient.kt @@ -0,0 +1,560 @@ +package net.rsprot.protocol.game.outgoing.info + +import io.netty.buffer.ByteBuf +import io.netty.buffer.Unpooled +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.bitbuffer.BitBuf +import net.rsprot.buffer.bitbuffer.toBitBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid + +@Suppress("MemberVisibilityCanBePrivate", "CascadeIf") +class PlayerInfoClient { + var localIndex: Int = -1 + var extendedInfoCount: Int = 0 + val extendedInfoIndices: IntArray = IntArray(2048) + var highResolutionCount: Int = 0 + val highResolutionIndices: IntArray = IntArray(2048) + var lowResolutionCount: Int = 0 + val lowResolutionIndices: IntArray = IntArray(2048) + val unmodifiedFlags: ByteArray = ByteArray(2048) + val cachedPlayers: Array = arrayOfNulls(2048) + val lowResolutionPositions: IntArray = IntArray(2048) + + fun gpiInit( + localIndex: Int, + bytebuf: ByteBuf, + ) { + this.localIndex = localIndex + bytebuf.toBitBuf().use { buffer -> + val localPlayer = Player(localIndex) + cachedPlayers[localIndex] = localPlayer + val coord = CoordGrid(buffer.gBits(30)) + localPlayer.coord = coord + highResolutionCount = 0 + highResolutionIndices[highResolutionCount++] = localIndex + unmodifiedFlags[localIndex] = 0 + lowResolutionCount = 0 + for (idx in 1..<2048) { + if (idx != localIndex) { + val lowResolutionPositionBitpacked = buffer.gBits(18) + val level = lowResolutionPositionBitpacked shr 16 + // Note: In osrs, the 0xFF is actually 0x255, a mixture between hexadecimal and decimal numbering. + // This is likely just an oversight, but due to only the first bit being utilized, + // this never causes problems in OSRS + val x = lowResolutionPositionBitpacked shr 8 and 0xFF + val z = lowResolutionPositionBitpacked and 0xFF + lowResolutionPositions[idx] = (x shl 14) + z + (level shl 28) + lowResolutionIndices[lowResolutionCount++] = idx + unmodifiedFlags[idx] = 0 + } + } + } + } + + fun decode(buffer: ByteBuf) { + extendedInfoCount = 0 + decodeBitCodes(buffer) + } + + private fun decodeBitCodes(byteBuf: ByteBuf) { + byteBuf.toBitBuf().use { buffer -> + var skipped = 0 + for (i in 0.. 0) { + --skipped + unmodifiedFlags[idx] = (unmodifiedFlags[idx].toInt() or NEXT_CYCLE_INACTIVE).toByte() + } else { + val active = buffer.gBits(1) + if (active == 0) { + skipped = readStationary(buffer) + unmodifiedFlags[idx] = (unmodifiedFlags[idx].toInt() or NEXT_CYCLE_INACTIVE).toByte() + } else { + getHighResolutionPlayerPosition(buffer, idx) + } + } + } + } + if (skipped != 0) { + throw RuntimeException() + } + } + byteBuf.toBitBuf().use { buffer -> + var skipped = 0 + for (i in 0.. 0) { + --skipped + unmodifiedFlags[idx] = (unmodifiedFlags[idx].toInt() or NEXT_CYCLE_INACTIVE).toByte() + } else { + val active = buffer.gBits(1) + if (active == 0) { + skipped = readStationary(buffer) + unmodifiedFlags[idx] = (unmodifiedFlags[idx].toInt() or NEXT_CYCLE_INACTIVE).toByte() + } else { + getHighResolutionPlayerPosition(buffer, idx) + } + } + } + } + if (skipped != 0) { + throw RuntimeException() + } + } + + byteBuf.toBitBuf().use { buffer -> + var skipped = 0 + for (i in 0.. 0) { + --skipped + unmodifiedFlags[idx] = (unmodifiedFlags[idx].toInt() or NEXT_CYCLE_INACTIVE).toByte() + } else { + val active = buffer.gBits(1) + if (active == 0) { + skipped = readStationary(buffer) + unmodifiedFlags[idx] = (unmodifiedFlags[idx].toInt() or NEXT_CYCLE_INACTIVE).toByte() + } else if (getLowResolutionPlayerPosition(buffer, idx)) { + unmodifiedFlags[idx] = (unmodifiedFlags[idx].toInt() or NEXT_CYCLE_INACTIVE).toByte() + } + } + } + } + if (skipped != 0) { + throw RuntimeException() + } + } + byteBuf.toBitBuf().use { buffer -> + var skipped = 0 + for (i in 0.. 0) { + --skipped + unmodifiedFlags[idx] = (unmodifiedFlags[idx].toInt() or NEXT_CYCLE_INACTIVE).toByte() + } else { + val active = buffer.gBits(1) + if (active == 0) { + skipped = readStationary(buffer) + unmodifiedFlags[idx] = (unmodifiedFlags[idx].toInt() or NEXT_CYCLE_INACTIVE).toByte() + } else if (getLowResolutionPlayerPosition(buffer, idx)) { + unmodifiedFlags[idx] = (unmodifiedFlags[idx].toInt() or NEXT_CYCLE_INACTIVE).toByte() + } + } + } + } + if (skipped != 0) { + throw RuntimeException() + } + } + lowResolutionCount = 0 + highResolutionCount = 0 + for (i in 1..<2048) { + unmodifiedFlags[i] = (unmodifiedFlags[i].toInt() shr 1).toByte() + val cachedPlayer = cachedPlayers[i] + if (cachedPlayer != null) { + highResolutionIndices[highResolutionCount++] = i + } else { + lowResolutionIndices[lowResolutionCount++] = i + } + } + decodeExtendedInfo(byteBuf.toJagByteBuf()) + } + + private fun decodeExtendedInfo(buffer: JagByteBuf) { + for (i in 0.. 15) { + deltaX -= 32 + } + var deltaZ = coord and 31 + if (deltaZ > 15) { + deltaZ -= 32 + } + var curLevel = cachedPlayer.coord.level + var curX = cachedPlayer.coord.x + var curZ = cachedPlayer.coord.z + curX += deltaX + curZ += deltaZ + curLevel = (curLevel + deltaLevel) and 0x3 + cachedPlayer.coord = CoordGrid(curLevel, curX, curZ) + cachedPlayer.queuedMove = extendedInfo + } else { + val coord = buffer.gBits(30) + val deltaLevel = coord shr 28 + val deltaX = coord shr 14 and 16383 + val deltaZ = coord and 16383 + var curLevel = cachedPlayer.coord.level + var curX = cachedPlayer.coord.x + var curZ = cachedPlayer.coord.z + curX = (curX + deltaX) and 16383 + curZ = (curZ + deltaZ) and 16383 + curLevel = (curLevel + deltaLevel) and 0x3 + cachedPlayer.coord = CoordGrid(curLevel, curX, curZ) + cachedPlayer.queuedMove = extendedInfo + } + } + } + + private fun getLowResolutionPlayerPosition( + buffer: BitBuf, + idx: Int, + ): Boolean { + val opcode = buffer.gBits(2) + if (opcode == 0) { + if (buffer.gBits(1) != 0) { + getLowResolutionPlayerPosition(buffer, idx) + } + val x = buffer.gBits(13) + val z = buffer.gBits(13) + val extendedInfo = buffer.gBits(1) == 1 + if (extendedInfo) { + this.extendedInfoIndices[extendedInfoCount++] = idx + } + if (cachedPlayers[idx] != null) { + throw RuntimeException() + } + val player = Player(idx) + cachedPlayers[idx] = player + // cached appearance decoding + val lowResolutionPosition = lowResolutionPositions[idx] + val level = lowResolutionPosition shr 28 + val lowResX = lowResolutionPosition shr 14 and 0xFF + val lowResZ = lowResolutionPosition and 0xFF + player.coord = CoordGrid(level, (lowResX shl 13) + x, (lowResZ shl 13) + z) + player.queuedMove = false + return true + } else if (opcode == 1) { + val levelDelta = buffer.gBits(2) + val lowResPosition = lowResolutionPositions[idx] + lowResolutionPositions[idx] = + ((lowResPosition shr 28) + levelDelta and 3 shl 28) + .plus(lowResPosition and 268435455) + return false + } else if (opcode == 2) { + val bitpacked = buffer.gBits(5) + val levelDelta = bitpacked shr 3 + val movementCode = bitpacked and 7 + val lowResPosition = lowResolutionPositions[idx] + val level = (lowResPosition shr 28) + levelDelta and 3 + var x = lowResPosition shr 14 and 255 + var z = lowResPosition and 255 + if (movementCode == 0) { + --x + --z + } + + if (movementCode == 1) { + --z + } + + if (movementCode == 2) { + ++x + --z + } + + if (movementCode == 3) { + --x + } + + if (movementCode == 4) { + ++x + } + + if (movementCode == 5) { + --x + ++z + } + + if (movementCode == 6) { + ++z + } + + if (movementCode == 7) { + ++x + ++z + } + lowResolutionPositions[idx] = (x shl 14) + z + (level shl 28) + return false + } else { + val bitpacked = buffer.gBits(18) + val levelDelta = bitpacked shr 16 + val xDelta = bitpacked shr 8 and 255 + val zDelta = bitpacked and 255 + val lowResPosition = lowResolutionPositions[idx] + val level = (lowResPosition shr 28) + levelDelta and 3 + val x = xDelta + (lowResPosition shr 14) and 255 + val z = zDelta + lowResPosition and 255 + lowResolutionPositions[idx] = (x shl 14) + z + (level shl 28) + return false + } + } + + private fun readStationary(buffer: BitBuf): Int { + val type = buffer.gBits(2) + return when (type) { + 0 -> 0 + 1 -> buffer.gBits(5) + 2 -> buffer.gBits(8) + else -> buffer.gBits(11) + } + } + + companion object { + private const val CUR_CYCLE_INACTIVE = 0x1 + private const val NEXT_CYCLE_INACTIVE = 0x2 + + class Player( + val playerId: Int, + ) { + var queuedMove: Boolean = false + var coord: CoordGrid = CoordGrid.INVALID + var skullIcon: Int = -1 + var headIcon: Int = -1 + var npcId: Int = -1 + var readyAnim: Int = -1 + var turnAnim: Int = -1 + var walkAnim: Int = -1 + var walkAnimBack: Int = -1 + var walkAnimLeft: Int = -1 + var walkAnimRight: Int = -1 + var runAnim: Int = -1 + var name: String? = null + var combatLevel: Int = 0 + var skillLevel: Int = 0 + var hidden: Boolean = false + var nameExtras: Array = Array(3) { "" } + var textGender: Int = 0 + var gender: Int = 0 + var equipment: IntArray = IntArray(12) + var identKit: IntArray = IntArray(12) + var colours: IntArray = IntArray(5) + + override fun toString(): String = + "Player(" + + "playerId=$playerId, " + + "queuedMove=$queuedMove, " + + "coord=$coord, " + + "skullIcon=$skullIcon, " + + "headIcon=$headIcon, " + + "npcId=$npcId, " + + "readyAnim=$readyAnim, " + + "turnAnim=$turnAnim, " + + "walkAnim=$walkAnim, " + + "walkAnimBack=$walkAnimBack, " + + "walkAnimLeft=$walkAnimLeft, " + + "walkAnimRight=$walkAnimRight, " + + "runAnim=$runAnim, " + + "name=$name, " + + "combatLevel=$combatLevel, " + + "skillLevel=$skillLevel, " + + "hidden=$hidden, " + + "nameExtras=${nameExtras.contentToString()}, " + + "textGender=$textGender, " + + "gender=$gender, " + + "equipment=${equipment.contentToString()}, " + + "identKit=${identKit.contentToString()}, " + + "colours=${colours.contentToString()}" + + ")" + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoTest.kt b/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoTest.kt new file mode 100644 index 000000000..fb0d66459 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/test/kotlin/net/rsprot/protocol/game/outgoing/info/PlayerInfoTest.kt @@ -0,0 +1,196 @@ +package net.rsprot.protocol.game.outgoing.info + +import io.netty.buffer.PooledByteBufAllocator +import io.netty.buffer.Unpooled +import net.rsprot.compression.HuffmanCodec +import net.rsprot.compression.provider.DefaultHuffmanCodecProvider +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.game.outgoing.codec.playerinfo.extendedinfo.writer.PlayerAvatarExtendedInfoDesktopWriter +import net.rsprot.protocol.game.outgoing.info.filter.DefaultExtendedInfoFilter +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerAvatarFactory +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfo +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfoProtocol +import net.rsprot.protocol.game.outgoing.info.util.BuildArea +import net.rsprot.protocol.game.outgoing.info.worker.DefaultProtocolWorker +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull + +class PlayerInfoTest { + private lateinit var protocol: PlayerInfoProtocol + private lateinit var localPlayerInfo: PlayerInfo + private lateinit var client: PlayerInfoClient + private lateinit var clientLocalPlayer: PlayerInfoClient.Companion.Player + + @BeforeEach + fun initialize() { + val allocator = PooledByteBufAllocator.DEFAULT + val factory = + PlayerAvatarFactory( + allocator, + DefaultExtendedInfoFilter(), + listOf(PlayerAvatarExtendedInfoDesktopWriter()), + DefaultHuffmanCodecProvider(createHuffmanCodec()), + ) + protocol = + PlayerInfoProtocol( + allocator, + DefaultProtocolWorker(), + factory, + ) + localPlayerInfo = protocol.alloc(LOCAL_PLAYER_INDEX, OldSchoolClientType.DESKTOP) + updateCoord(0, 3200, 3220) + localPlayerInfo.avatar.postUpdate() + client = PlayerInfoClient() + gpiInit() + } + + private fun gpiInit() { + val gpiBuffer = Unpooled.buffer(5000) + localPlayerInfo.handleAbsolutePlayerPositions(PlayerInfo.ROOT_WORLD, gpiBuffer) + client.gpiInit(LOCAL_PLAYER_INDEX, gpiBuffer) + clientLocalPlayer = checkNotNull(client.cachedPlayers[client.localIndex]) + } + + @Test + fun `test gpi init`() { + assertCoordEquals() + } + + private fun tick() { + protocol.update() + val buffer = localPlayerInfo.backingBuffer(PlayerInfo.ROOT_WORLD) + client.decode(buffer) + assertFalse(buffer.isReadable) + } + + @Test + fun `test single player consecutive movements`() { + // For local player, the coord we send in init should always be the real, high resolution + // As such, we must call tick() to for any future changes to take effect + tick() + + updateCoord(1, 3210, 3225) + tick() + assertCoordEquals() + + updateCoord(0, 512, 512) + tick() + assertCoordEquals() + + updateCoord(0, 513, 512) + tick() + assertCoordEquals() + + updateCoord(0, 3205, 3220) + tick() + assertCoordEquals() + } + + private fun updateCoord( + level: Int, + x: Int, + z: Int, + ) { + localPlayerInfo.updateCoord(level, x, z) + localPlayerInfo.updateRenderCoord(PlayerInfo.ROOT_WORLD, level, x, z) + localPlayerInfo.updateBuildArea(PlayerInfo.ROOT_WORLD, BuildArea((x ushr 3) - 6, (z ushr 3) - 6)) + } + + @Test + fun `test multi player movements`() { + val otherPlayerIndices = (1..280) + val otherPlayers = arrayOfNulls(2048) + for (index in otherPlayerIndices) { + val otherPlayer = protocol.alloc(index, OldSchoolClientType.DESKTOP) + otherPlayers[index] = otherPlayer + otherPlayer.updateCoord(0, 3205, 3220) + } + tick() + assertAllCoordsEqual(otherPlayers) + for (player in otherPlayers.filterNotNull()) { + player.updateCoord(0, 3204, 3220) + } + tick() + assertAllCoordsEqual(otherPlayers) + } + + @Test + fun `test single player appearance extended info`() { + localPlayerInfo.avatar.extendedInfo.setName("Local Player") + localPlayerInfo.avatar.extendedInfo.setCombatLevel(126) + localPlayerInfo.avatar.extendedInfo.setSkillLevel(1258) + localPlayerInfo.avatar.extendedInfo.setHidden(false) + localPlayerInfo.avatar.extendedInfo.setMale(false) + localPlayerInfo.avatar.extendedInfo.setTextGender(2) + localPlayerInfo.avatar.extendedInfo.setSkullIcon(-1) + localPlayerInfo.avatar.extendedInfo.setOverheadIcon(-1) + tick() + assertEquals("Local Player", clientLocalPlayer.name) + assertEquals(126, clientLocalPlayer.combatLevel) + assertEquals(1258, clientLocalPlayer.skillLevel) + assertEquals(false, clientLocalPlayer.hidden) + assertEquals(1, clientLocalPlayer.gender) + assertEquals(2, clientLocalPlayer.textGender) + assertEquals(-1, clientLocalPlayer.skullIcon) + assertEquals(-1, clientLocalPlayer.headIcon) + } + + @Test + fun `test multi player appearance extended info`() { + val otherPlayerIndices = (1..280) + val otherPlayers = arrayOfNulls(2048) + for (index in otherPlayerIndices) { + val otherPlayer = protocol.alloc(index, OldSchoolClientType.DESKTOP) + otherPlayers[index] = otherPlayer + otherPlayer.updateCoord(0, 3205, 3220) + otherPlayer.avatar.extendedInfo.setName("Player $index") + otherPlayer.avatar.extendedInfo.setCombatLevel(126) + otherPlayer.avatar.extendedInfo.setSkillLevel(index) + otherPlayer.avatar.extendedInfo.setHidden(false) + otherPlayer.avatar.extendedInfo.setMale(false) + otherPlayer.avatar.extendedInfo.setTextGender(2) + otherPlayer.avatar.extendedInfo.setSkullIcon(-1) + otherPlayer.avatar.extendedInfo.setOverheadIcon(-1) + } + tick() + for (index in otherPlayerIndices) { + val clientPlayer = client.cachedPlayers[index] + assertNotNull(clientPlayer) + assertEquals("Player $index", clientPlayer.name) + assertEquals(126, clientPlayer.combatLevel) + assertEquals(index, clientPlayer.skillLevel) + assertEquals(false, clientPlayer.hidden) + assertEquals(1, clientPlayer.gender) + assertEquals(2, clientPlayer.textGender) + assertEquals(-1, clientPlayer.skullIcon) + assertEquals(-1, clientPlayer.headIcon) + } + } + + private fun assertAllCoordsEqual(otherPlayers: Array) { + for (i in otherPlayers.indices) { + val otherPlayer = otherPlayers[i] ?: continue + val clientPlayer = client.cachedPlayers[i]!! + assertEquals(otherPlayer.avatar.currentCoord, clientPlayer.coord) + } + } + + private fun assertCoordEquals() { + assertEquals(localPlayerInfo.avatar.currentCoord, clientLocalPlayer.coord) + } + + private companion object { + private const val LOCAL_PLAYER_INDEX: Int = 499 + + private fun createHuffmanCodec(): HuffmanCodec { + val resource = PlayerInfoTest::class.java.getResourceAsStream("huffman.dat") + checkNotNull(resource) { + "huffman.dat could not be found" + } + return HuffmanCodec.create(Unpooled.wrappedBuffer(resource.readBytes())) + } + } +} diff --git a/protocol/osrs-225/osrs-225-desktop/src/test/resources/net/rsprot/protocol/game/outgoing/info/huffman.dat b/protocol/osrs-225/osrs-225-desktop/src/test/resources/net/rsprot/protocol/game/outgoing/info/huffman.dat new file mode 100644 index 000000000..98eab4bd4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-desktop/src/test/resources/net/rsprot/protocol/game/outgoing/info/huffman.dat @@ -0,0 +1,17 @@ +  + +   + + + + +  + + + + +     + + + + \ No newline at end of file diff --git a/protocol/osrs-225/osrs-225-internal/build.gradle.kts b/protocol/osrs-225/osrs-225-internal/build.gradle.kts new file mode 100644 index 000000000..5fc5236f5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/build.gradle.kts @@ -0,0 +1,20 @@ +dependencies { + api(platform(rootProject.libs.netty.bom)) + api(rootProject.libs.netty.buffer) + api(rootProject.libs.netty.transport) + api(rootProject.libs.commons.pool2) + implementation(rootProject.libs.inline.logger) + api(projects.buffer) + api(projects.compression) + api(projects.crypto) + api(projects.protocol) + api(projects.protocol.osrs225.osrs225Common) +} + +mavenPublishing { + pom { + name = "RsProt OSRS 225 Internal" + description = "The internal module for revision 225 OldSchool RuneScape networking, " + + "offering internal hidden implementations behind the library." + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/LogLevel.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/LogLevel.kt new file mode 100644 index 000000000..c77e1faf9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/LogLevel.kt @@ -0,0 +1,10 @@ +package net.rsprot.protocol.common + +public enum class LogLevel { + OFF, + TRACE, + DEBUG, + INFO, + WARN, + ERROR, +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/RSProtFlags.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/RSProtFlags.kt new file mode 100644 index 000000000..b3c487a17 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/RSProtFlags.kt @@ -0,0 +1,142 @@ +package net.rsprot.protocol.common + +import com.github.michaelbull.logging.InlineLogger +import io.netty.util.internal.SystemPropertyUtil + +/** + * An internal object that provides easy access to various error-checking flags. + * The purpose of this object is to avoid scattering these checks throughout + * the codebase, making it difficult for users to find any. + * Additionally, requires duplication of code to re-create all this. + */ +public object RSProtFlags { + private val logger: InlineLogger = InlineLogger() + private const val PREFIX = "net.rsprot.protocol.internal." + + /** + * Whether the server is in 'development' mode. + * Development mode is effectively a mode where all + * checks are performed to ensure all inputs are validated. + * Users are expected to turn development mode off when + * putting the server into production, as these checks + * end up taking a considerable amount of time. + */ + @JvmStatic + private val development: Boolean = + getBoolean( + "development", + true, + ) + + /** + * Whether to check that obj ids in inventory packets are all positive. + */ + @JvmStatic + public val inventoryObjCheck: Boolean = + getBoolean( + "inventoryObjCheck", + development, + ) + + /** + * Whether to validate extended info block inputs. + */ + @JvmStatic + public val extendedInfoInputVerification: Boolean = + getBoolean( + "extendedInfoInputVerification", + development, + ) + + @JvmStatic + public val clientscriptVerification: Boolean = + getBoolean( + "clientscriptVerification", + development, + ) + + private val networkLoggingString: String = + getString( + "networkLogging", + "off", + ) + + private val js5LoggingString: String = + getString( + "js5Logging", + "off", + ) + + @JvmStatic + public val networkLogging: LogLevel = + when (networkLoggingString) { + "off" -> LogLevel.OFF + "trace" -> LogLevel.TRACE + "debug" -> LogLevel.DEBUG + "info" -> LogLevel.INFO + "warn" -> LogLevel.WARN + "error" -> LogLevel.ERROR + else -> { + logger.warn { + "Unknown network logging option: $networkLoggingString, " + + "expected values: [off, trace, debug, info, warn, error]" + } + LogLevel.OFF + } + } + + @JvmStatic + public val js5Logging: LogLevel = + when (js5LoggingString) { + "off" -> LogLevel.OFF + "trace" -> LogLevel.TRACE + "debug" -> LogLevel.DEBUG + "info" -> LogLevel.INFO + "warn" -> LogLevel.WARN + "error" -> LogLevel.ERROR + else -> { + logger.warn { + "Unknown js5 logging option: $networkLoggingString, " + + "expected values: [off, trace, debug, info, warn, error]" + } + LogLevel.OFF + } + } + + init { + log("development", development) + log("inventoryObjCheck", inventoryObjCheck) + log("extendedInfoInputVerification", extendedInfoInputVerification) + log("clientscriptVerification", clientscriptVerification) + log("networkLogging", networkLoggingString) + log("js5Logging", js5LoggingString) + } + + private fun getBoolean( + propertyName: String, + defaultValue: Boolean, + ): Boolean = + SystemPropertyUtil.getBoolean( + PREFIX + propertyName, + defaultValue, + ) + + @Suppress("SameParameterValue") + private fun getString( + propertyName: String, + defaultValue: String, + ): String = + SystemPropertyUtil.get( + PREFIX + propertyName, + defaultValue, + ) + + private fun log( + name: String, + value: Any, + ) { + logger.debug { + "-D${PREFIX}$name: $value" + } + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/client/ClientTypeMap.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/client/ClientTypeMap.kt new file mode 100644 index 000000000..ebb80fad2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/client/ClientTypeMap.kt @@ -0,0 +1,74 @@ +package net.rsprot.protocol.common.client + +import net.rsprot.protocol.client.ClientType + +public class ClientTypeMap + @PublishedApi + internal constructor( + private val array: Array, + ) { + public val size: Int + get() = array.size + + public val notNullSize: Int + get() = array.count { it != null } + + public operator fun get(clientType: ClientType): T = + requireNotNull(array[clientType.id]) { + "Client type $clientType not initialized!" + } + + public fun getOrNull(clientType: ClientType): T? = array[clientType.id] + + public fun getOrNull(clientId: Int): T? = array[clientId] + + public operator fun contains(clientType: ClientType): Boolean = array[clientType.id] != null + + public companion object { + public inline fun of( + elements: List, + clientCapacity: Int, + clientTypeSelector: (T) -> ClientType, + ): ClientTypeMap { + val array = arrayOfNulls(clientCapacity) + for (element in elements) { + val clientType = clientTypeSelector(element) + check(array[clientType.id] == null) { + "A client is registered more than once: $elements" + } + array[clientType.id] = element + } + return ClientTypeMap(array) + } + + public inline fun of( + clientCapacity: Int, + elements: List>, + ): ClientTypeMap { + val array = arrayOfNulls(clientCapacity) + for ((clientType, element) in elements) { + check(array[clientType.id] == null) { + "A client is registered more than once: $elements" + } + array[clientType.id] = element + } + return ClientTypeMap(array) + } + + public inline fun ofType( + elements: List, + clientCapacity: Int, + clientTypeSelector: (T) -> Pair, + ): ClientTypeMap { + val array = arrayOfNulls(clientCapacity) + for (pair in elements) { + val (clientType, element) = clientTypeSelector(pair) + check(array[clientType.id] == null) { + "A client is registered more than once: $elements" + } + array[clientType.id] = element + } + return ClientTypeMap(array) + } + } + } diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/codec/zone/payload/OldSchoolZoneProt.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/codec/zone/payload/OldSchoolZoneProt.kt new file mode 100644 index 000000000..b7027c9a4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/codec/zone/payload/OldSchoolZoneProt.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.common.game.outgoing.codec.zone.payload + +public object OldSchoolZoneProt { + public const val LOC_ADD_CHANGE: Int = 0 + public const val LOC_DEL: Int = 1 + public const val LOC_ANIM: Int = 2 + public const val LOC_MERGE: Int = 3 + public const val OBJ_ADD: Int = 4 + public const val OBJ_DEL: Int = 5 + public const val OBJ_COUNT: Int = 6 + public const val OBJ_ENABLED_OPS: Int = 7 + public const val MAP_ANIM: Int = 8 + public const val MAP_PROJANIM: Int = 9 + public const val SOUND_AREA: Int = 10 +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/codec/zone/payload/ZoneProtEncoder.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/codec/zone/payload/ZoneProtEncoder.kt new file mode 100644 index 000000000..6f74424de --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/codec/zone/payload/ZoneProtEncoder.kt @@ -0,0 +1,27 @@ +package net.rsprot.protocol.common.game.outgoing.codec.zone.payload + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.message.ZoneProt +import net.rsprot.protocol.message.codec.MessageEncoder + +/** + * Zone prot encoder is an extension on message encoders, with the intent being + * that this encoder can be passed on-to + * [net.rsprot.protocol.game.outgoing.codec.zone.header.DesktopUpdateZonePartialEnclosedEncoder], + * as that packet combines multiple zone payloads into a single packet. + */ +public interface ZoneProtEncoder : MessageEncoder { + public fun encode( + buffer: JagByteBuf, + message: T, + ) + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: T, + ) { + encode(buffer, message) + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/CachedExtendedInfo.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/CachedExtendedInfo.kt new file mode 100644 index 000000000..60826646c --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/CachedExtendedInfo.kt @@ -0,0 +1,14 @@ +package net.rsprot.protocol.common.game.outgoing.info + +import net.rsprot.protocol.common.game.outgoing.info.encoder.ExtendedInfoEncoder + +/** + * Extended info blocks which get cached by the client, meaning if + * an avatar goes from low resolution to high resolution, and the client has a + * cached buffer of them, unless the server writes a new variant (in the case of a + * de-synchronization), the client will use the old buffer to restore that block. + * @param T the extended info block + * @param E the encoder for that extended info block + */ +public abstract class CachedExtendedInfo, E : ExtendedInfoEncoder> : + ExtendedInfo() diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/CoordGrid.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/CoordGrid.kt new file mode 100644 index 000000000..8b2de98a8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/CoordGrid.kt @@ -0,0 +1,96 @@ +package net.rsprot.protocol.common.game.outgoing.info + +/** + * Coord grid, commonly referred to just as Coordinate or Location, + * is responsible for tracking absolute positions of avatars in the game. + * @param packed the 30-bit bitpacked integer representing the coord grid. + */ +@JvmInline +public value class CoordGrid( + public val packed: Int, +) { + /** + * @param level the height level of the avatar. + * @param x the absolute x coordinate of the avatar. + * @param z the absolute z coordinate of the avatar. + */ + @Suppress("ConvertTwoComparisonsToRangeCheck") + public constructor( + level: Int, + x: Int, + z: Int, + ) : this( + (level shl 28) + .or(x shl 14) + .or(z), + ) { + // https://youtrack.jetbrains.com/issue/KT-62798/in-range-checks-are-not-intrinsified-in-kotlin-stdlib + // Using traditional checks to avoid generating range objects (seen by decompiling this class) + require(level >= 0 && level < 4) { + "Level must be in range of 0..<4: $level" + } + require(x >= 0 && x <= 16384) { + "X coordinate must be in range of 0..<16384: $x" + } + require(z >= 0 && z <= 16384) { + "Z coordinate must be in range of 0..<16384, $z" + } + } + + public val level: Int + get() = packed ushr 28 + public val x: Int + get() = packed ushr 14 and 0x3FFF + public val z: Int + get() = packed and 0x3FFF + + /** + * Checks whether this coord grid is within [distance] of the [other] coord grid. + * If the coord grids are on different levels, this function will always return false. + * @param other the other coord grid to check against. + * @param distance the distance to check (inclusive). A distance of 0 implies same coordinate. + * @return true if the [other] coord grid is within [distance] of this coord grid. + */ + public fun inDistance( + other: CoordGrid, + distance: Int, + ): Boolean { + if (level != other.level) { + return false + } + val deltaX = x - other.x + if (deltaX !in -distance..distance) { + return false + } + val deltaZ = z - other.z + return deltaZ in -distance..distance + } + + /** + * Checks if this coord grid is uninitialized. + * Uninitialized coord grids are determined by checking if all 32 bits of + * the [packed] property are enabled (including sign bit, which would be the opposite). + * As the main constructor of this class only takes in the components that build a coord grid, + * it is impossible to make an instance of this that matches the invalid value, + * unless directly using the single-argument constructor. + */ + @Suppress("NOTHING_TO_INLINE") + public inline fun invalid(): Boolean = this == INVALID + + public operator fun component1(): Int = level + + public operator fun component2(): Int = x + + public operator fun component3(): Int = z + + override fun toString(): String = + "CoordGrid(" + + "level=$level, " + + "x=$x, " + + "z=$z" + + ")" + + public companion object { + public val INVALID: CoordGrid = CoordGrid(-1) + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/ExtendedInfo.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/ExtendedInfo.kt new file mode 100644 index 000000000..456da9ff2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/ExtendedInfo.kt @@ -0,0 +1,95 @@ +package net.rsprot.protocol.common.game.outgoing.info + +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.encoder.ExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +/** + * The abstract extended info class, responsible for holding some + * information about a specific avatar. + * @param T the extended info block type. + * @param E the encoder for the given extended info block [T]. + */ +public abstract class ExtendedInfo, E : ExtendedInfoEncoder> { + public abstract val encoders: ClientTypeMap + + /** + * An array of client-specific pre-computed buffers of this extended info block. + * These buffers get pre-computed during player info building process, + * and the pre-computed buffers will be natively copied over to the main buffer + * by all the observers. + * For extended info blocks which cannot be pre-computed, the building happens on-demand. + */ + private val buffers: Array = arrayOfNulls(OldSchoolClientType.COUNT) + + /** + * Sets the client-specific [buffer] at index [clientTypeId]. + * @param clientTypeId the id of the client, additionally used as the key to the [buffers] array. + * @param buffer the pre-computed buffer for this extended info block. + */ + public fun setBuffer( + clientTypeId: Int, + buffer: ByteBuf, + ) { + buffers[clientTypeId] = buffer + } + + /** + * Gets the latest pre-computed buffer for the given [oldSchoolClientType]. + * @param oldSchoolClientType the client for which to obtain the buffer. + * @return the pre-computed buffer, or null if it does not exist. + */ + public fun getBuffer(oldSchoolClientType: OldSchoolClientType): ByteBuf? = buffers[oldSchoolClientType.id] + + /** + * Gets the encoder for a given [oldSchoolClientType]. + * @param oldSchoolClientType the client type for which to obtain the encoder. + * @return the client-specific encoder of this extended info block, or null + * if one has not been registered. + */ + public fun getEncoder(oldSchoolClientType: OldSchoolClientType): E? = encoders.getOrNull(oldSchoolClientType) + + /** + * Releases all the client-specific buffers of this extended info block, + * which will either be garbage-collected or returned into the bytebuf pool. + */ + internal fun releaseBuffers() { + for (i in 0.., E : PrecomputedExtendedInfoEncoder> T.precompute( + allocator: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, +) { + // Release any old buffers before overwriting with new ones + releaseBuffers() + for (id in 0.., E : ExtendedInfoEncoder> : + ExtendedInfo() diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/ExtendedInfoEncoder.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/ExtendedInfoEncoder.kt new file mode 100644 index 000000000..69ba8f523 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/ExtendedInfoEncoder.kt @@ -0,0 +1,8 @@ +package net.rsprot.protocol.common.game.outgoing.info.encoder + +import net.rsprot.protocol.common.game.outgoing.info.ExtendedInfo + +/** + * Extended info encoders are responsible for turning [T] into a byte buffer. + */ +public interface ExtendedInfoEncoder> diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/OnDemandExtendedInfoEncoder.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/OnDemandExtendedInfoEncoder.kt new file mode 100644 index 000000000..e96d4653f --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/OnDemandExtendedInfoEncoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.common.game.outgoing.info.encoder + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.common.game.outgoing.info.ExtendedInfo + +/** + * On-demand extended info encoders are invoked on every observer whenever information must be written. + * These differ from [PrecomputedExtendedInfoEncoder] in that they cannot be pre-computed, as the + * data in the buffer is dependent on the observer. + */ +public interface OnDemandExtendedInfoEncoder> : ExtendedInfoEncoder { + public fun encode( + buffer: JagByteBuf, + localPlayerIndex: Int, + updatedAvatarIndex: Int, + extendedInfo: T, + ) +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/PrecomputedExtendedInfoEncoder.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/PrecomputedExtendedInfoEncoder.kt new file mode 100644 index 000000000..1e9bfd9d1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/encoder/PrecomputedExtendedInfoEncoder.kt @@ -0,0 +1,19 @@ +package net.rsprot.protocol.common.game.outgoing.info.encoder + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.ExtendedInfo + +/** + * Pre-computed extended info encoders encode the data for all necessary extended info blocks + * early on in the process. This allows us to do a simple native buffer copy to transfer the data over, + * and avoids us having to re-calculate all the little properties that end up being encoded. + */ +public interface PrecomputedExtendedInfoEncoder> : ExtendedInfoEncoder { + public fun precompute( + alloc: ByteBufAllocator, + huffmanCodecProvider: HuffmanCodecProvider, + extendedInfo: T, + ): JagByteBuf +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/NpcAvatarDetails.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/NpcAvatarDetails.kt new file mode 100644 index 000000000..fad53210b --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/NpcAvatarDetails.kt @@ -0,0 +1,128 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo + +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid + +/** + * An internal class holding the state of NPC's avatar, containing everything + * that is sent during the movement from low to high resolution. + * @property index the index of the npc in the world + * @property id the id of the npc in the world + * @property currentCoord the current coordinate of the npc + * @property stepCount the number of steps the npc has taken this cycle + * @property firstStep the direction of the first step that the npc has taken this cycle, or -1 + * @property secondStep the direction of the second step that the npc has taken this cycle, or -2 + * @property movementType the bitpacked flag of all the movement typed the npc has utilized this cycle + * @property spawnCycle the game cycle on which the npc was originally spawned into the world + * @property direction the direction that the npc is facing when it is first added to high resolution view + * @property inaccessible whether the npc is inaccessible to all players, meaning it will not be + * added to high resolution for anyone, even though it is still within the zone. This is intended + * to be used with static npcs that respawn. After death, inaccessible should be set to true, and + * when the npc respawns, it should be set back to false. This allows us to not re-allocate avatars + * which furthermore requires cleanup and micromanaging. + */ +public class NpcAvatarDetails internal constructor( + public var index: Int, + public var id: Int, + public var currentCoord: CoordGrid = CoordGrid.INVALID, + public var stepCount: Int = 0, + public var firstStep: Int = -1, + public var secondStep: Int = -1, + public var movementType: Int = 0, + public var spawnCycle: Int = 0, + public var direction: Int = 0, + public var inaccessible: Boolean = false, + public var allocateCycle: Int, +) { + public constructor( + index: Int, + id: Int, + level: Int, + x: Int, + z: Int, + spawnCycle: Int = 0, + direction: Int = 0, + allocateCycle: Int, + ) : this( + index, + id, + CoordGrid(level, x, z), + spawnCycle = spawnCycle, + direction = direction, + allocateCycle = allocateCycle, + ) + + /** + * Whether the npc is tele jumping, meaning it will jump over to the destination + * coord, even if it is just one tile away. + */ + public fun isJumping(): Boolean = movementType and TELEJUMP != 0 + + /** + * Whether the npc is teleporting, but not explicitly jumping. + */ + public fun isTeleWithoutJump(): Boolean = movementType and TELE != 0 + + /** + * Whether the npc is teleporting. This means the npc will render as jumping + * if the destination is > 2 tiles away, and normal walk/run/in-between if the + * distance is 2 tiles or less. + */ + public fun isTeleporting(): Boolean = movementType and (TELE or TELEJUMP) != 0 + + /** + * Updates the current direction of the npc, allowing the server to sync up + * the current faced coordinate of npcs during movement, face angle and such. + */ + public fun updateDirection(direction: Int) { + this.direction = direction + } + + override fun toString(): String = + "NpcAvatarDetails(" + + "index=$index, " + + "id=$id, " + + "currentCoord=$currentCoord, " + + "stepCount=$stepCount, " + + "firstStep=$firstStep, " + + "secondStep=$secondStep, " + + "movementType=$movementType, " + + "spawnCycle=$spawnCycle, " + + "direction=$direction, " + + "inaccessible=$inaccessible, " + + "allocateCycle=$allocateCycle" + + ")" + + public companion object { + /** + * The constant flag movement type indicating the npc did crawl. + */ + public const val CRAWL: Int = 0x1 + + /** + * The constant flag movement type indicating the npc did walk. + */ + public const val WALK: Int = 0x2 + + /** + * The constant flag movement type indicating the npc did run. + * Run state is additionally reached if two walks, two crawls or a mix of + * a crawl and walk was used in one cycle. More than two walks/crawls will + * however turn into a telejump. + * This flag has a higher priority than crawl or walk, but is surpassed by both teleports. + */ + public const val RUN: Int = 0x4 + + /** + * The constant flag indicating the npc is teleporting without a jump. + * The jump condition is automatically included if the npc moves more than 2 tiles. + * This flag has the highest priority out of all above, only surpassed by telejump. + */ + public const val TELE: Int = 0x8 + + /** + * The constant flag indicating the npc is jumping regardless of distance. + * This flag has the highest priority out of all above. + */ + public const val TELEJUMP: Int = 0x10 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/encoder/NpcExtendedInfoEncoders.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/encoder/NpcExtendedInfoEncoders.kt new file mode 100644 index 000000000..d30c0362d --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/encoder/NpcExtendedInfoEncoders.kt @@ -0,0 +1,45 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.encoder + +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.encoder.OnDemandExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.BaseAnimationSet +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.BodyCustomisation +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.CombatLevelChange +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.FaceCoord +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.HeadCustomisation +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.HeadIconCustomisation +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.NameChange +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.NpcTinting +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.Transformation +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.VisibleOps +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.ExactMove +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.FacePathingEntity +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Hit +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Say +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Sequence +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.SpotAnimList + +/** + * A data class to bring all the extended info encoders for a given client together. + * @param oldSchoolClientType the client for which these encoders are created. + */ +public data class NpcExtendedInfoEncoders( + public val oldSchoolClientType: OldSchoolClientType, + public val spotAnim: PrecomputedExtendedInfoEncoder, + public val say: PrecomputedExtendedInfoEncoder, + public val visibleOps: PrecomputedExtendedInfoEncoder, + public val exactMove: PrecomputedExtendedInfoEncoder, + public val sequence: PrecomputedExtendedInfoEncoder, + public val tinting: PrecomputedExtendedInfoEncoder, + public val headIconCustomisation: PrecomputedExtendedInfoEncoder, + public val nameChange: PrecomputedExtendedInfoEncoder, + public val headCustomisation: PrecomputedExtendedInfoEncoder, + public val bodyCustomisation: PrecomputedExtendedInfoEncoder, + public val transformation: PrecomputedExtendedInfoEncoder, + public val combatLevelChange: PrecomputedExtendedInfoEncoder, + public val hit: OnDemandExtendedInfoEncoder, + public val faceCoord: PrecomputedExtendedInfoEncoder, + public val facePathingEntity: PrecomputedExtendedInfoEncoder, + public val baseAnimationSet: PrecomputedExtendedInfoEncoder, +) diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/encoder/NpcResolutionChangeEncoder.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/encoder/NpcResolutionChangeEncoder.kt new file mode 100644 index 000000000..cb0ed73ae --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/encoder/NpcResolutionChangeEncoder.kt @@ -0,0 +1,19 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.encoder + +import net.rsprot.buffer.bitbuffer.BitBuf +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.NpcAvatarDetails + +public interface NpcResolutionChangeEncoder { + public val clientType: OldSchoolClientType + + public fun encode( + bitBuffer: BitBuf, + details: NpcAvatarDetails, + extendedInfo: Boolean, + localPlayerCoordGrid: CoordGrid, + largeDistance: Boolean, + cycleCount: Int, + ) +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/BaseAnimationSet.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/BaseAnimationSet.kt new file mode 100644 index 000000000..bd69a83e2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/BaseAnimationSet.kt @@ -0,0 +1,65 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +public class BaseAnimationSet( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public var overrides: Int = DEFAULT_OVERRIDES_FLAG + public var turnLeftAnim: UShort = 0xFFFFu + public var turnRightAnim: UShort = 0xFFFFu + public var walkAnim: UShort = 0xFFFFu + public var walkAnimBack: UShort = 0xFFFFu + public var walkAnimLeft: UShort = 0xFFFFu + public var walkAnimRight: UShort = 0xFFFFu + public var runAnim: UShort = 0xFFFFu + public var runAnimBack: UShort = 0xFFFFu + public var runAnimLeft: UShort = 0xFFFFu + public var runAnimRight: UShort = 0xFFFFu + public var crawlAnim: UShort = 0xFFFFu + public var crawlAnimBack: UShort = 0xFFFFu + public var crawlAnimLeft: UShort = 0xFFFFu + public var crawlAnimRight: UShort = 0xFFFFu + public var readyAnim: UShort = 0xFFFFu + + override fun clear() { + releaseBuffers() + overrides = DEFAULT_OVERRIDES_FLAG + turnLeftAnim = 0xFFFFu + turnRightAnim = 0xFFFFu + walkAnim = 0xFFFFu + walkAnimBack = 0xFFFFu + walkAnimLeft = 0xFFFFu + walkAnimRight = 0xFFFFu + runAnim = 0xFFFFu + runAnimBack = 0xFFFFu + runAnimLeft = 0xFFFFu + runAnimRight = 0xFFFFu + crawlAnim = 0xFFFFu + crawlAnimBack = 0xFFFFu + crawlAnimLeft = 0xFFFFu + crawlAnimRight = 0xFFFFu + readyAnim = 0xFFFFu + } + + public companion object { + public const val DEFAULT_OVERRIDES_FLAG: Int = 0 + public const val TURN_LEFT_ANIM_FLAG: Int = 0x1 + public const val TURN_RIGHT_ANIM_FLAG: Int = 0x2 + public const val WALK_ANIM_FLAG: Int = 0x4 + public const val WALK_ANIM_BACK_FLAG: Int = 0x8 + public const val WALK_ANIM_LEFT_FLAG: Int = 0x10 + public const val WALK_ANIM_RIGHT_FLAG: Int = 0x20 + public const val RUN_ANIM_FLAG: Int = 0x40 + public const val RUN_ANIM_BACK_FLAG: Int = 0x80 + public const val RUN_ANIM_LEFT_FLAG: Int = 0x100 + public const val RUN_ANIM_RIGHT_FLAG: Int = 0x200 + public const val CRAWL_ANIM_FLAG: Int = 0x400 + public const val CRAWL_ANIM_BACK_FLAG: Int = 0x800 + public const val CRAWL_ANIM_LEFT_FLAG: Int = 0x1000 + public const val CRAWL_ANIM_RIGHT_FLAG: Int = 0x2000 + public const val READY_ANIM_FLAG: Int = 0x4000 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/BodyCustomisation.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/BodyCustomisation.kt new file mode 100644 index 000000000..8450cc714 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/BodyCustomisation.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +public class BodyCustomisation( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public var customisation: TypeCustomisation? = null + + override fun clear() { + releaseBuffers() + this.customisation = null + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/CombatLevelChange.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/CombatLevelChange.kt new file mode 100644 index 000000000..ff241256b --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/CombatLevelChange.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +public class CombatLevelChange( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public var level: Int = DEFAULT_LEVEL_OVERRIDE + + override fun clear() { + releaseBuffers() + this.level = DEFAULT_LEVEL_OVERRIDE + } + + public companion object { + public const val DEFAULT_LEVEL_OVERRIDE: Int = -1 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/FaceCoord.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/FaceCoord.kt new file mode 100644 index 000000000..3ff0df456 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/FaceCoord.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +public class FaceCoord( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public var instant: Boolean = false + public var x: UShort = 0xFFFFu + public var z: UShort = 0xFFFFu + + override fun clear() { + releaseBuffers() + this.instant = false + this.x = 0xFFFFu + this.z = 0xFFFFu + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/HeadCustomisation.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/HeadCustomisation.kt new file mode 100644 index 000000000..e0777cff6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/HeadCustomisation.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +public class HeadCustomisation( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public var customisation: TypeCustomisation? = null + + override fun clear() { + releaseBuffers() + this.customisation = null + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/HeadIconCustomisation.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/HeadIconCustomisation.kt new file mode 100644 index 000000000..dafb0e44c --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/HeadIconCustomisation.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +public class HeadIconCustomisation( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public var flag: Int = DEFAULT_FLAG + public val headIconGroups: IntArray = + IntArray(8) { + -1 + } + public val headIconIndices: ShortArray = + ShortArray(8) { + -1 + } + + override fun clear() { + releaseBuffers() + flag = DEFAULT_FLAG + headIconGroups.fill(-1) + headIconIndices.fill(-1) + } + + public companion object { + public const val DEFAULT_FLAG: Int = 0 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/NameChange.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/NameChange.kt new file mode 100644 index 000000000..482d057bd --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/NameChange.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +public class NameChange( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public var name: String? = null + + override fun clear() { + releaseBuffers() + this.name = null + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/NpcTinting.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/NpcTinting.kt new file mode 100644 index 000000000..b4f30e50f --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/NpcTinting.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Tinting + +/** + * The tinting extended info block. + * @param encoders the array of client-specific encoders for tinting. + */ +public class NpcTinting( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public val global: Tinting = Tinting() + + override fun clear() { + releaseBuffers() + global.reset() + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/Transformation.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/Transformation.kt new file mode 100644 index 000000000..5206b26f7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/Transformation.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +public class Transformation( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public var id: UShort = 0xFFFFu + + override fun clear() { + releaseBuffers() + this.id = 0xFFFFu + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/TypeCustomisation.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/TypeCustomisation.kt new file mode 100644 index 000000000..f1195b173 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/TypeCustomisation.kt @@ -0,0 +1,8 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +public class TypeCustomisation( + public val models: List, + public val recolours: List, + public val retexture: List, + public val mirror: Boolean?, +) diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/VisibleOps.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/VisibleOps.kt new file mode 100644 index 000000000..51f4b030a --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/npcinfo/extendedinfo/VisibleOps.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +public class VisibleOps( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public var ops: UByte = DEFAULT_OPS + + override fun clear() { + releaseBuffers() + ops = DEFAULT_OPS + } + + public companion object { + public const val DEFAULT_OPS: UByte = 0b11111u + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/encoder/PlayerExtendedInfoEncoders.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/encoder/PlayerExtendedInfoEncoders.kt new file mode 100644 index 000000000..9e913a886 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/encoder/PlayerExtendedInfoEncoders.kt @@ -0,0 +1,37 @@ +package net.rsprot.protocol.common.game.outgoing.info.playerinfo.encoder + +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.encoder.OnDemandExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.Appearance +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.Chat +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.FaceAngle +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.MoveSpeed +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.PlayerTintingList +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.TemporaryMoveSpeed +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.ExactMove +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.FacePathingEntity +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Hit +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Say +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Sequence +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.SpotAnimList + +/** + * A data class to bring all the extended info encoders for a given client together. + * @param oldSchoolClientType the client for which these encoders are created. + */ +public data class PlayerExtendedInfoEncoders( + public val oldSchoolClientType: OldSchoolClientType, + public val appearance: PrecomputedExtendedInfoEncoder, + public val chat: PrecomputedExtendedInfoEncoder, + public val exactMove: PrecomputedExtendedInfoEncoder, + public val faceAngle: PrecomputedExtendedInfoEncoder, + public val facePathingEntity: PrecomputedExtendedInfoEncoder, + public val hit: OnDemandExtendedInfoEncoder, + public val moveSpeed: PrecomputedExtendedInfoEncoder, + public val say: PrecomputedExtendedInfoEncoder, + public val sequence: PrecomputedExtendedInfoEncoder, + public val spotAnim: PrecomputedExtendedInfoEncoder, + public val temporaryMoveSpeed: PrecomputedExtendedInfoEncoder, + public val tinting: OnDemandExtendedInfoEncoder, +) diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/Appearance.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/Appearance.kt new file mode 100644 index 000000000..c7cc37a00 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/Appearance.kt @@ -0,0 +1,232 @@ +package net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.CachedExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +/** + * The appearance extended info block. + * This is an unusually-large extended info block that is also the only extended info block + * which gets cached client-side. + * The library utilizes that caching through a counter which increments with each modification + * done to the appearance. When an avatar goes from low resolution to high resolution, + * a comparison is done against the cache, if the counters match, no extended info block is written. + * If an avatar logs out, every observer will have their counter set back to -1. + * @param encoders the array of client-specific encoders for appearance. + */ +public class Appearance( + override val encoders: ClientTypeMap>, +) : CachedExtendedInfo>() { + /** + * The name of this avatar. + */ + public var name: String = "" + + /** + * The combat level of this avatar. + */ + public var combatLevel: UByte = 0u + + /** + * The skill level of this avatar, shown on the right-click menu as "skill-number". + * This is utilized within Burthorpe's games' room. + */ + public var skillLevel: UShort = 0u + + /** + * Whether this avatar is soft-hidden, meaning client will not render the model itself + * for anyone except J-Mods. Clients such as RuneLite will ignore this property within + * any plugins. + */ + public var hidden: Boolean = false + + /** + * Whether this avatar is using the male ident kit. + */ + public var male: Boolean = true + + /** + * The type of pronoun to utilize within clientscripts. + */ + public var textGender: UByte = MAX_UNSIGNED_BYTE + + /** + * The skull icon that appears over-head, mostly in PvP scenarios. + */ + public var skullIcon: UByte = MAX_UNSIGNED_BYTE + + /** + * The overhead icon that's utilized with prayers. + */ + public var overheadIcon: UByte = MAX_UNSIGNED_BYTE + + /** + * The id of the npc to which this avatar has transformed. + */ + public var transformedNpcId: UShort = MAX_UNSIGNED_SHORT + + /** + * An array of ident kit ids, indexed by the respective wearpos. + */ + public val identKit: ShortArray = ShortArray(7) { -1 } + + /** + * The worn obj ids, indexed by the respective wearpos. + */ + public val wornObjs: ShortArray = ShortArray(SLOT_COUNT) { -1 } + + /** + * The secondary and tertiary wearpos that the primary wearpos + * ends up hiding. The secondary and tertiary values are bitpacked + * into a single byte. We track this separately, so we can always + * get the full idea of what the avatar is built up out of. + */ + public val hiddenWearPos: ByteArray = ByteArray(SLOT_COUNT) { -1 } + + /** + * The colours the avatar's model is made up of. + */ + public var colours: ByteArray = ByteArray(COLOUR_COUNT) + + /** + * The animation used when the avatar is standing still. + */ + public var readyAnim: UShort = MAX_UNSIGNED_SHORT + + /** + * The animation used when the avatar is turning on-spot without movement. + */ + public var turnAnim: UShort = MAX_UNSIGNED_SHORT + + /** + * The animation used when the avatar is walking forward. + */ + public var walkAnim: UShort = MAX_UNSIGNED_SHORT + + /** + * The animation used when the avatar is walking backwards. + */ + public var walkAnimBack: UShort = MAX_UNSIGNED_SHORT + + /** + * The animation used when the avatar is walking to the left. + */ + public var walkAnimLeft: UShort = MAX_UNSIGNED_SHORT + + /** + * The animation used when the avatar is walking to the right. + */ + public var walkAnimRight: UShort = MAX_UNSIGNED_SHORT + + /** + * The animation used when the avatar is running. + */ + public var runAnim: UShort = MAX_UNSIGNED_SHORT + + /** + * Whether to force a model refresh client-side, removing the cached model of the player + * even if the worn objects + base colour + gender have not changed. + * This is important to flag when setting or removing an obj type customization. + */ + public var forceModelRefresh: Boolean = false + + /** + * The customisations applied to worn objs, indexed by the respective obj's primary wearpos. + */ + public val objTypeCustomisation: Array = arrayOfNulls(12) + + /** + * The string to render before an avatar's name in the right-click menu, + * used within the Burthorpe games' room. + */ + public var beforeName: String = "" + + /** + * The string to render after an avatar's name in the right-click menu, + * used within the Burthorpe games' room. + */ + public var afterName: String = "" + + /** + * The string to render after an avatar's combat level in the right-click menu, + * used within the Burthorpe games' room. + */ + public var afterCombatLevel: String = "" + + override fun clear() { + releaseBuffers() + name = "" + combatLevel = 0u + skillLevel = 0u + hidden = false + male = true + textGender = MAX_UNSIGNED_BYTE + skullIcon = MAX_UNSIGNED_BYTE + overheadIcon = MAX_UNSIGNED_BYTE + transformedNpcId = MAX_UNSIGNED_SHORT + identKit.fill(-1) + wornObjs.fill(-1) + hiddenWearPos.fill(-1) + colours.fill(0) + forceModelRefresh = false + objTypeCustomisation.fill(null) + readyAnim = MAX_UNSIGNED_SHORT + turnAnim = MAX_UNSIGNED_SHORT + walkAnim = MAX_UNSIGNED_SHORT + walkAnimBack = MAX_UNSIGNED_SHORT + walkAnimLeft = MAX_UNSIGNED_SHORT + walkAnimRight = MAX_UNSIGNED_SHORT + runAnim = MAX_UNSIGNED_SHORT + } + + public companion object { + /** + * The number of wearpos that the client will track. + */ + private const val SLOT_COUNT: Int = 12 + + /** + * The number of colours that the client tracks. + */ + private const val COLOUR_COUNT: Int = 5 + + /** + * A constant for max unsigned byte, frequently used as the "default, not initialized" value. + */ + private const val MAX_UNSIGNED_BYTE: UByte = 0xFFu + + /** + * A constant for max unsigned short, frequently used as the "default, not initialized" value. + */ + private const val MAX_UNSIGNED_SHORT: UShort = 0xFFFFu + + private const val HAIR_IDENTKIT: Int = 0 + private const val BEARD_IDENTKIT: Int = 1 + private const val BODY_IDENTKIT: Int = 2 + private const val ARMS_IDENTKIT: Int = 3 + private const val GLOVES_IDENTKIT: Int = 4 + private const val LEGS_IDENTKIT: Int = 5 + private const val BOOTS_IDENTKIT: Int = 6 + + /** + * An array of wearpos -> ident kit slot, indexed by wearpos. + */ + public val identKitSlotList: List = + listOf( + -1, + -1, + -1, + -1, + BODY_IDENTKIT, + -1, + ARMS_IDENTKIT, + LEGS_IDENTKIT, + HAIR_IDENTKIT, + GLOVES_IDENTKIT, + BOOTS_IDENTKIT, + BEARD_IDENTKIT, + -1, + -1, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/Chat.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/Chat.kt new file mode 100644 index 000000000..8100aaec9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/Chat.kt @@ -0,0 +1,53 @@ +package net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +/** + * The chat extended info block, responsible for any public messages. + * @param encoders the array of client-specific encoders for chat. + */ +public class Chat( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + /** + * The colour to apply to this chat message. + */ + public var colour: UByte = 0u + + /** + * The effect to apply to this chat message. + */ + public var effects: UByte = 0u + + /** + * The mod icon to render next to the name of the avatar who said this message. + */ + public var modicon: UByte = 0u + + /** + * Whether this avatar is using the built-in autotyper mechanic. + */ + public var autotyper: Boolean = false + + /** + * The text itself to render. This will be compressed using the [net.rsprot.compression.HuffmanCodec]. + */ + public var text: String? = null + + /** + * The colour pattern for specialized chat message colours, + */ + public var pattern: ByteArray? = null + + override fun clear() { + releaseBuffers() + colour = 0u + effects = 0u + modicon = 0u + autotyper = false + text = null + pattern = null + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/FaceAngle.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/FaceAngle.kt new file mode 100644 index 000000000..92c291583 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/FaceAngle.kt @@ -0,0 +1,44 @@ +package net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +/** + * The extended info block responsible for making an avatar turn towards a specific + * angle. + * @param encoders the array of client-specific encoders for face angle. + */ +public class FaceAngle( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + /** + * The value of the angle for this avatar to turn towards. + */ + public var angle: UShort = UShort.MAX_VALUE + public var outOfDate: Boolean = false + private set + + public fun markUpToDate() { + if (!outOfDate) { + return + } + outOfDate = false + releaseBuffers() + } + + public fun syncAngle(angle: Int) { + this.outOfDate = true + this.angle = angle.toUShort() + } + + override fun clear() { + releaseBuffers() + angle = UShort.MAX_VALUE + outOfDate = false + } + + public companion object { + public val DEFAULT_VALUE: UShort = UShort.MAX_VALUE + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/MoveSpeed.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/MoveSpeed.kt new file mode 100644 index 000000000..d92949e34 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/MoveSpeed.kt @@ -0,0 +1,34 @@ +package net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +/** + * The movement speed extended info block. + * Unlike most extended info blocks, the [value] will last as long as the server tells it to. + * The client will also temporarily cache it for the duration that it sees an avatar in high resolution. + * Whenever an avatar moves, unless the move speed has been overwritten, this is the speed + * that it will use for the movement, barring any special mechanics. + * If an avatar goes from high resolution to low resolution, the client **will not** cache this, + * and a new status update must be written when the opposite transition occurs. + * This move speed status should typically be synchronized with the state of the "Run orb". + * @param encoders the array of client-specific encoders for move speed. + */ +public class MoveSpeed( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + /** + * The current movement speed of this avatar. + */ + public var value: Int = DEFAULT_MOVESPEED + + override fun clear() { + releaseBuffers() + value = DEFAULT_MOVESPEED + } + + public companion object { + public const val DEFAULT_MOVESPEED: Int = 0 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/ObjTypeCustomisation.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/ObjTypeCustomisation.kt new file mode 100644 index 000000000..f83ad7044 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/ObjTypeCustomisation.kt @@ -0,0 +1,28 @@ +package net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo + +/** + * A class to track modifications done to a specific worn obj. + * @param recolIndices the bitpacked indices of the source colour to overwrite. + * @param recol1 the colour value to overwrite the source colour at the first index with. + * @param recol2 the colour value to overwrite the source colour at the second index with. + * @param retexIndices the bitpacked indices of the source texture to overwrite. + * @param retex1 the texture id to overwrite the source texture at the first index with. + * @param retex2 the texture id to overwrite the source texture at the second index with. + */ +public class ObjTypeCustomisation( + public var recolIndices: UByte, + public var recol1: UShort, + public var recol2: UShort, + public var retexIndices: UByte, + public var retex1: UShort, + public var retex2: UShort, +) { + public constructor() : this( + recolIndices = 0xFFu, + recol1 = 0u, + recol2 = 0u, + retexIndices = 0xFFu, + retex1 = 0u, + retex2 = 0u, + ) +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/PlayerTintingList.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/PlayerTintingList.kt new file mode 100644 index 000000000..127ffbedf --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/PlayerTintingList.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.OnDemandExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Tinting + +/** + * The tinting extended info block. + * This is a rather special case as tinting is one of the two observer-dependent extended info blocks, + * along with [net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Hit]. + * It is possible for the server to mark tinting for only a single avatar to see. + * In order to achieve this, we utilize [observerDependent] tinting, indexed by the observer's id. + * @param encoders the array of client-specific encoders for tinting. + */ +public class PlayerTintingList( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public val global: Tinting = Tinting() + public val observerDependent: MutableMap = HashMap() + + public operator fun get(index: Int): Tinting = observerDependent.getOrDefault(index, global) + + override fun clear() { + releaseBuffers() + global.reset() + if (observerDependent.isNotEmpty()) { + observerDependent.clear() + } + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/TemporaryMoveSpeed.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/TemporaryMoveSpeed.kt new file mode 100644 index 000000000..6f6615495 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/playerinfo/extendedinfo/TemporaryMoveSpeed.kt @@ -0,0 +1,27 @@ +package net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +/** + * The temporary move speed is used to set a move speed for a single cycle, commonly done + * when the player has run enabled through the orb, but decides to only walk a single tile instead. + * Rather than to switch the main mode over to walking, it utilizes the temporary move speed + * so the primary one will remain as running after this one cycle, as they are far more likely + * to utilize the move speed described by their run orb. + * @param encoders the array of client-specific encoders for temporary move speed. + */ +public class TemporaryMoveSpeed( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + /** + * The movement speed of this avatar for a single cycle. + */ + public var value: Int = -1 + + override fun clear() { + releaseBuffers() + value = -1 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/ExactMove.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/ExactMove.kt new file mode 100644 index 000000000..c566aa81c --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/ExactMove.kt @@ -0,0 +1,68 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +/** + * The exactmove extended info block is used to provide precise fine-tuned visual movement + * of an avatar. + * @param encoders the array of client-specific encoders for exact move. + */ +public class ExactMove( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + /** + * The coordinate delta between the current absolute + * x coordinate and where the avatar is going. + */ + public var deltaX1: UByte = 0u + + /** + * The coordinate delta between the current absolute + * z coordinate and where the avatar is going. + */ + public var deltaZ1: UByte = 0u + + /** + * Delay1 defines how many client cycles (20ms/cc) until the avatar arrives + * at x/z 1 coordinate. + */ + public var delay1: UShort = 0u + + /** + * The coordinate delta between the current absolute + * x coordinate and where the avatar is going. + */ + public var deltaX2: UByte = 0u + + /** + * The coordinate delta between the current absolute + * z coordinate and where the avatar is going. + */ + public var deltaZ2: UByte = 0u + + /** + * Delay2 defines how many client cycles (20ms/cc) until the avatar arrives + * at x/z 2 coordinate. + */ + public var delay2: UShort = 0u + + /** + * The angle the avatar will be facing throughout the exact movement, + * with 0 implying south, 512 west, 1024 north and 1536 east; interpolate + * between to get finer directions. + */ + public var direction: UShort = 0u + + override fun clear() { + releaseBuffers() + deltaX1 = 0u + deltaZ1 = 0u + delay1 = 0u + deltaX2 = 0u + deltaZ2 = 0u + delay2 = 0u + direction = 0u + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/FacePathingEntity.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/FacePathingEntity.kt new file mode 100644 index 000000000..c48829437 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/FacePathingEntity.kt @@ -0,0 +1,28 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +/** + * The extended info block to make avatars face-lock onto another avatar, be it a NPC or a player. + * @param encoders the array of client-specific encoders for face pathing entity. + */ +public class FacePathingEntity( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + /** + * The index of the avatar to face-lock onto. For player avatars, + * a value of 0x10000 is added onto the index to differentiate it. + */ + public var index: Int = DEFAULT_VALUE + + override fun clear() { + releaseBuffers() + index = DEFAULT_VALUE + } + + public companion object { + public const val DEFAULT_VALUE: Int = -1 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Hit.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Hit.kt new file mode 100644 index 000000000..493cadfc6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Hit.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.OnDemandExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.HeadBarList +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.HitMarkList + +/** + * The hit extended info, responsible for tracking all hitmarks and headbars for a given avatar. + * @param encoders the array of client-specific encoders for hits. + */ +public class Hit( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + public val headBarList: HeadBarList = HeadBarList() + public val hitMarkList: HitMarkList = HitMarkList() + + override fun clear() { + releaseBuffers() + headBarList.clear() + hitMarkList.clear() + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Say.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Say.kt new file mode 100644 index 000000000..77ff490bb --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Say.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +/** + * The say extended info block tracks any overhead chat set by the server, + * through content. Public chat will not utilize this. + * @param encoders the array of client-specific encoders for say. + */ +public class Say( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + /** + * The text to render over the avatar. + */ + public var text: String? = null + + override fun clear() { + releaseBuffers() + text = null + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Sequence.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Sequence.kt new file mode 100644 index 000000000..bcf6950ce --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Sequence.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder + +/** + * The sequence mask defines what animation an avatar is playing. + * @param encoders the array of client-specific encoders for sequence. + */ +public class Sequence( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + /** + * The id of the animation to play. + */ + public var id: UShort = 0xFFFFu + + /** + * The delay in client cycles (20ms/cc) until the given animation begins playing. + */ + public var delay: UShort = 0u + + override fun clear() { + releaseBuffers() + id = 0xFFFFu + delay = 0u + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/SpotAnimList.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/SpotAnimList.kt new file mode 100644 index 000000000..942af49d2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/SpotAnimList.kt @@ -0,0 +1,69 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.game.outgoing.info.TransientExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.PrecomputedExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.SpotAnim +import java.util.BitSet + +/** + * The spotanim list is a specialized extended info block with compression logic built into it, + * as the theoretical possibilities of this block are rather large. + * This extended info block will track the modified slots using a bitset. + * Instead of traversing the entire list at the end of a cycle to reset the properties, + * it will follow the bitset's enabled bits to identify which slots to reset, if any. + * As in most cases the answer is none - this should outperform array fills by quite a bit. + * @param encoders the array of client-specific encoders for spotanims. + */ +public class SpotAnimList( + override val encoders: ClientTypeMap>, +) : TransientExtendedInfo>() { + /** + * The changelist that tracks all the slots which have been flagged for a spotanim update. + */ + public val changelist: BitSet = BitSet(MAX_SPOTANIM_COUNT) + + /** + * The array of spotanims on this avatar. + * This array utilizes the bitpacked representation of a [SpotAnim]. + */ + public val spotanims: LongArray = + LongArray(MAX_SPOTANIM_COUNT) { + UNINITIALIZED_SPOTANIM + } + + /** + * Sets the spotanim in slot [slot]. + * This function will also flag the given slot for a change. + * @param slot the slot of the spotanim to set. + * @param spotAnim the spotanim to set. + */ + public fun set( + slot: Int, + spotAnim: SpotAnim, + ) { + spotanims[slot] = spotAnim.packed + changelist.set(slot) + } + + /** + * Traverses the bit set to determine which spotanims to clear out. + */ + override fun clear() { + releaseBuffers() + var nextSetBit = changelist.nextSetBit(0) + if (nextSetBit == -1) { + return + } + do { + spotanims[nextSetBit] = UNINITIALIZED_SPOTANIM + nextSetBit = changelist.nextSetBit(nextSetBit + 1) + } while (nextSetBit != -1) + changelist.clear() + } + + public companion object { + private const val UNINITIALIZED_SPOTANIM = -1L + private const val MAX_SPOTANIM_COUNT = 256 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Tinting.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Tinting.kt new file mode 100644 index 000000000..61f5515a2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/Tinting.kt @@ -0,0 +1,47 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo + +/** + * The tinting class is used to apply a specific tint onto the non-textured parts of + * an avatar. As the engine does not support modifying textures this way, they remain + * in their original form. + */ +public class Tinting { + /** + * The delay in client cycles (20ms/cc) until the tinting is applied. + */ + public var start: UShort = 0u + + /** + * The timestamp in client cycles (20ms/cc) until the tinting finishes. + */ + public var end: UShort = 0u + + /** + * The hue of the tint. + */ + public var hue: UByte = 0u + + /** + * The saturation of the tint. + */ + public var saturation: UByte = 0u + + /** + * The lightness of the tint. + */ + public var lightness: UByte = 0u + + /** + * The weight (or opacity) of the tint. + */ + public var weight: UByte = 0u + + public fun reset() { + start = 0u + end = 0u + hue = 0u + saturation = 0u + lightness = 0u + weight = 0u + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HeadBar.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HeadBar.kt new file mode 100644 index 000000000..8dfcd78c3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HeadBar.kt @@ -0,0 +1,38 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util + +/** + * A class to hold the values of a given head bar. + * @param sourceIndex the index of the entity that dealt the hit that resulted in this headbar. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * If there is no source, set the index to -1. + * The index will be used for rendering purposes, as both the player who dealt + * the hit, and the recipient will see the [selfType] variant, and everyone else + * will see the [otherType] variant, which, if set to -1 will be skipped altogether. + * @param selfType the id of the headbar to render to the entity on which the headbar appears, + * as well as the source who resulted in the creation of the headbar. + * @param otherType the id of the headbar to render to everyone that doesn't fit the [selfType] + * criteria. If set to -1, the headbar will not be rendered to these individuals. + * @param startFill the number of pixels to render of this headbar at in the start. + * The number of pixels that a headbar supports is defined in its respective headbar config. + * @param endFill the number of pixels to render of this headbar at in the end, + * if a [startTime] and [endTime] are defined. + * @param startTime the delay in client cycles (20ms/cc) until the headbar renders at [startFill] + * @param endTime the delay in client cycles (20ms/cc) until the headbar arrives at [endFill]. + */ +public data class HeadBar( + public var sourceIndex: Int, + public var selfType: UShort, + public var otherType: UShort, + public val startFill: UByte, + public val endFill: UByte, + public val startTime: UShort, + public val endTime: UShort, +) { + public companion object { + /** + * A constant that informs the client to remove the headbar by the given id. + */ + public const val REMOVED: UShort = 0x7FFFu + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HeadBarList.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HeadBarList.kt new file mode 100644 index 000000000..2d66cefa9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HeadBarList.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util + +/** + * The headbar list will contain all the headbars for this given avatar for a single cycle, + * backed by an [ArrayList]. + */ +public class HeadBarList( + private val elements: MutableList, +) : MutableList by elements { + public constructor(capacity: Int = DEFAULT_CAPACITY) : this(ArrayList(capacity)) + + private companion object { + /** + * The default capacity for the hits. + * As most avatars will not be getting hit much, there isn't much reason to + * allocate a large capacity list ahead of time. + */ + private const val DEFAULT_CAPACITY = 10 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HitMark.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HitMark.kt new file mode 100644 index 000000000..30d29963b --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HitMark.kt @@ -0,0 +1,67 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util + +/** + * A class for holding all the state of a given hitmark. + * @param sourceIndex the index of the character that dealt the hit. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * If there is no source, set the index to -1. + * The index will be used for tinting purposes, as both the player who dealt + * the hit, and the recipient will see a tinted variant. + * Everyone else, however, will see a regular darkened hit mark. + * @param selfType the multi hitmark id that supports tinted and darkened variants. + * @param otherType the hitmark id to render to anyone that isn't the recipient, + * or the one who dealt the hit. This will generally be a darkened variant. + * If the hitmark should only render to the local player, set the [otherType] + * value to -1, forcing it to only render to the recipient (and in the case of + * a [sourceIndex] being defined, the one who dealt the hit) + * @param value the value to show over the hitmark. + * @param selfSoakType the multi hitmark id that supports tinted and darkened variants, + * shown as soaking next to the normal hitmark. + * @param otherSoakType the hitmark id to render to anyone that isn't the recipient, + * or the one who dealt the hit. This will generally be a darkened variant. + * Unlike the [otherType], this does not support -1, as it is not possible to show partial + * soaked hitmarks. + * @param delay the delay in client cycles (20ms/cc) until the hitmark renders. + */ +public class HitMark( + public var sourceIndex: Int, + public var selfType: UShort, + public var otherType: UShort, + public var value: UShort, + public var selfSoakType: UShort, + public var otherSoakType: UShort, + public var soakValue: UShort, + public var delay: UShort, +) { + public constructor( + sourceIndex: Int, + selfType: UShort, + otherType: UShort, + value: UShort, + delay: UShort, + ) : this( + sourceIndex = sourceIndex, + selfType = selfType, + otherType = otherType, + value = value, + selfSoakType = UShort.MAX_VALUE, + otherSoakType = UShort.MAX_VALUE, + soakValue = UShort.MAX_VALUE, + delay = delay, + ) + + public constructor( + selfType: UShort, + delay: UShort, + ) : this( + sourceIndex = -1, + selfType = selfType, + otherType = selfType, + value = UShort.MAX_VALUE, + selfSoakType = UShort.MAX_VALUE, + otherSoakType = UShort.MAX_VALUE, + soakValue = UShort.MAX_VALUE, + delay = delay, + ) +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HitMarkList.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HitMarkList.kt new file mode 100644 index 000000000..848ce6b14 --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/HitMarkList.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util + +/** + * The hitmark list will contain all the hitmarks for this given avatar for a single cycle, + * backed by an [ArrayList]. + */ +public class HitMarkList( + private val elements: MutableList, +) : MutableList by elements { + public constructor(capacity: Int = DEFAULT_CAPACITY) : this(ArrayList(capacity)) + + private companion object { + private const val DEFAULT_CAPACITY = 10 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/SpotAnim.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/SpotAnim.kt new file mode 100644 index 000000000..3878e66fe --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/info/shared/extendedinfo/util/SpotAnim.kt @@ -0,0 +1,39 @@ +package net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util + +/** + * A value class to represent a spotanim in a primitive 'long'. + * @param packed the bitpacked long value of this spotanim. + */ +@JvmInline +public value class SpotAnim( + internal val packed: Long, +) { + /** + * @param id the id of the spotanim. + * @param delay the delay in client cycles (20ms/cc) until the given spotanim begins rendering. + * @param height the height at which to render the spotanim. + */ + public constructor( + id: Int, + delay: Int, + height: Int, + ) : this( + (id.toLong() and 0xFFFF) + .or(delay.toLong() and 0xFFFF shl 16) + .or(height.toLong() and 0xFFFF shl 32), + ) + + public val id: Int + get() = (packed and 0xFFFF).toInt() + public val delay: Int + get() = (packed ushr 16 and 0xFFFF).toInt() + public val height: Int + get() = (packed ushr 32 and 0xFFFF).toInt() + + public companion object { + /** + * The default value to initialize spotanim extended info as. + */ + public val INVALID: SpotAnim = SpotAnim(-1L) + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/internal/Inventory.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/internal/Inventory.kt new file mode 100644 index 000000000..409da922a --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/internal/Inventory.kt @@ -0,0 +1,61 @@ +package net.rsprot.protocol.common.game.outgoing.inv.internal + +import net.rsprot.protocol.common.game.outgoing.inv.InventoryObject +import kotlin.jvm.Throws + +/** + * A compressed internal representation of an inventory, to be transmitted + * with the various inventory update packets. + * Rather than use a List, we pool these [Inventory] instances to avoid + * generating significant amounts of garbage. + * For a popular server, it is perfectly reasonable to expect north of a gigabyte + * of memory to be wasted through List instances in the span of an hour. + * We eliminate all garbage generation by using soft-reference pooled inventory + * objects. While this does result in a small hit due to the synchronization involved, + * it is nothing compared to the hit caused by garbage collection and memory allocation + * involved with inventories. + * + * @property count the current count of objs in this inventory + * @property contents the array of contents of this inventory. + * The contents array is initialized at the maximum theoretical size + * of the full inv update packet. + */ +public class Inventory private constructor( + public var count: Int, + private val contents: LongArray, +) { + public constructor( + capacity: Int, + ) : this( + 0, + LongArray(capacity), + ) + + /** + * Adds an obj into this inventory + * @param obj the obj to be added to this inventory + * @throws ArrayIndexOutOfBoundsException if the inventory is full + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public fun add(obj: InventoryObject) { + contents[count++] = obj.packed + } + + /** + * Gets the obj in [slot]. + * @return the obj in the respective slot, or [InventoryObject.NULL] + * if no object exists in that slot. + * @throws ArrayIndexOutOfBoundsException if the index is out of bounds + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public operator fun get(slot: Int): InventoryObject = InventoryObject(contents[slot]) + + /** + * Clears the inventory by setting the count to zero. + * The actual backing long array can remain filled with values, + * as those will be overridden by real usages whenever necessary. + */ + public fun clear() { + count = 0 + } +} diff --git a/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/internal/InventoryPool.kt b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/internal/InventoryPool.kt new file mode 100644 index 000000000..f3bc323bb --- /dev/null +++ b/protocol/osrs-225/osrs-225-internal/src/main/kotlin/net/rsprot/protocol/common/game/outgoing/inv/internal/InventoryPool.kt @@ -0,0 +1,51 @@ +package net.rsprot.protocol.common.game.outgoing.inv.internal + +import org.apache.commons.pool2.BasePooledObjectFactory +import org.apache.commons.pool2.ObjectPool +import org.apache.commons.pool2.PooledObject +import org.apache.commons.pool2.PooledObjectFactory +import org.apache.commons.pool2.impl.DefaultPooledObject +import org.apache.commons.pool2.impl.SoftReferenceObjectPool + +/** + * A soft-reference based pool of [Inventory] objects, with the primary + * intent being to avoid re-creating lists of objs which end up wasting + * as much as 137kb of memory for a single inventory that's up to 5713 objs + * in capacity. While it is unlikely that any inventory would get near that, + * servers do commonly expand inventory capacities to numbers like 2,000 or 2,500, + * which would still consume up 48-60kb of memory as a result in any traditional manner. + * + * Breakdown of the above statements: + * Assuming an implementation where List is provided to the respective packets, + * where Obj is a class of three properties: + * + * ``` + * Slot: Int (necessary for partial inv updates) + * Id: Int + * Count: Int + * ``` + * + * The resulting memory requirement would be `(12 + (3 * 4))` bytes per obj. + * While this does coincide with the memory alignment, + * it still ends up consuming 24 bytes per obj, all of which would be discarded shortly after. + * Given the assumption that 1,000 players log in at once, and they all have a bank + * of 1000 objs - which is a fairly conservative estimate -, the resulting waste of memory + * is 24 megabytes alone. All of this can be avoided through the use of an object pool, + * as done below. + */ +public data object InventoryPool { + public val pool: ObjectPool = SoftReferenceObjectPool(createFactory()) + + private fun createFactory(): PooledObjectFactory { + return object : BasePooledObjectFactory() { + override fun create(): Inventory { + // 5713 is the maximum theoretical number of objs an inventory can carry + // before the 40kb limitation could get hit + // This assumes each obj sends a quantity of >= 255 + return Inventory(5713) + } + + override fun wrap(p0: Inventory): PooledObject = DefaultPooledObject(p0) + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/build.gradle.kts b/protocol/osrs-225/osrs-225-model/build.gradle.kts new file mode 100644 index 000000000..9ce232f18 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/build.gradle.kts @@ -0,0 +1,20 @@ +dependencies { + api(platform(rootProject.libs.netty.bom)) + api(rootProject.libs.netty.buffer) + implementation(rootProject.libs.inline.logger) + api(rootProject.libs.commons.pool2) + api(projects.buffer) + api(projects.compression) + api(projects.crypto) + api(projects.protocol) + api(projects.protocol.osrs225.osrs225Internal) + api(projects.protocol.osrs225.osrs225Common) +} + +mavenPublishing { + pom { + name = "RsProt OSRS 225 Model" + description = "The model module for revision 225 OldSchool RuneScape networking, " + + "offering all the model classes to be used by the implementing server." + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/GameClientProtCategory.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/GameClientProtCategory.kt new file mode 100644 index 000000000..ef72fd1c3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/GameClientProtCategory.kt @@ -0,0 +1,11 @@ +package net.rsprot.protocol.game.incoming + +import net.rsprot.protocol.ClientProtCategory + +public enum class GameClientProtCategory( + override val id: Int, + override val limit: Int, +) : ClientProtCategory { + CLIENT_EVENT(0, 50), + USER_EVENT(1, 10), +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/If1Button.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/If1Button.kt new file mode 100644 index 000000000..f29bd3463 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/If1Button.kt @@ -0,0 +1,43 @@ +package net.rsprot.protocol.game.incoming.buttons + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If1 button messages are sent whenever a player clicks on an older + * if1-type component. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id the player interacted with + * @property componentId the component id on that interface the player interacted with + */ +public class If1Button( + private val _combinedId: CombinedId, +) : IncomingGameMessage { + public val combinedId: Int + get() = _combinedId.combinedId + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as If1Button + + return _combinedId == other._combinedId + } + + override fun hashCode(): Int = _combinedId.hashCode() + + override fun toString(): String = + "If1Button(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/If3Button.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/If3Button.kt new file mode 100644 index 000000000..0edc04162 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/If3Button.kt @@ -0,0 +1,83 @@ +package net.rsprot.protocol.game.incoming.buttons + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * If3 button messages are sent whenever a player clicks on a newer + * if3-type component. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id the player interacted with + * @property componentId the component id on that interface the player interacted with + * @property sub the subcomponent within that component if it has one, otherwise -1 + * @property obj the obj in that subcomponent, or -1 + * @property op the option clicked, ranging from 1 to 10 + */ +@Suppress("MemberVisibilityCanBePrivate") +public class If3Button private constructor( + private val _combinedId: CombinedId, + private val _sub: UShort, + private val _obj: UShort, + private val _op: UByte, +) : IncomingGameMessage { + public constructor( + combinedId: CombinedId, + sub: Int, + obj: Int, + op: Int, + ) : this( + combinedId, + sub.toUShort(), + obj.toUShort(), + op.toUByte(), + ) + + public val combinedId: Int + get() = _combinedId.combinedId + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val sub: Int + get() = _sub.toIntOrMinusOne() + public val obj: Int + get() = _obj.toIntOrMinusOne() + public val op: Int + get() = _op.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as If3Button + + if (_combinedId != other._combinedId) return false + if (_sub != other._sub) return false + if (_obj != other._obj) return false + if (_op != other._op) return false + + return true + } + + override fun hashCode(): Int { + var result = _combinedId.hashCode() + result = 31 * result + _sub.hashCode() + result = 31 * result + _obj.hashCode() + result = 31 * result + _op.hashCode() + return result + } + + override fun toString(): String = + "If3Button(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "sub=$sub, " + + "obj=$obj, " + + "op=$op" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfButtonD.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfButtonD.kt new file mode 100644 index 000000000..8b5c01cef --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfButtonD.kt @@ -0,0 +1,113 @@ +package net.rsprot.protocol.game.incoming.buttons + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * If button drag messages are sent whenever an obj is dragged from one subcomponent + * to another. + * @property selectedCombinedId the bitpacked combination of [selectedInterfaceId] and [selectedComponentId]. + * @property selectedInterfaceId the interface id from which the obj is dragged + * @property selectedComponentId the component on that selected interface from which + * the obj is dragged + * @property selectedSub the subcomponent from which the obj is dragged, + * or -1 if none exists + * @property selectedObj the obj that is being dragged, or -1 if none exists + * @property targetCombinedId the bitpacked combination of [targetInterfaceId] and [targetComponentId]. + * @property targetInterfaceId the interface id to which the obj is being dragged + * @property targetComponentId the component of the target interface to which + * the obj is being dragged + * @property targetSub the subcomponent of the target to which the obj is being dragged, + * or -1 if none exists + * @property targetObj the obj in that subcomponent which is being dragged on, + * or -1 if there is no obj in the target position + */ +@Suppress("DuplicatedCode", "MemberVisibilityCanBePrivate") +public class IfButtonD private constructor( + private val _selectedCombinedId: CombinedId, + private val _selectedSub: UShort, + private val _selectedObj: UShort, + private val _targetCombinedId: CombinedId, + private val _targetSub: UShort, + private val _targetObj: UShort, +) : IncomingGameMessage { + public constructor( + selectedCombinedId: CombinedId, + selectedSub: Int, + selectedObj: Int, + targetCombinedId: CombinedId, + targetSub: Int, + targetObj: Int, + ) : this( + selectedCombinedId, + selectedSub.toUShort(), + selectedObj.toUShort(), + targetCombinedId, + targetSub.toUShort(), + targetObj.toUShort(), + ) + + public val selectedCombinedId: Int + get() = _selectedCombinedId.combinedId + public val selectedInterfaceId: Int + get() = _selectedCombinedId.interfaceId + public val selectedComponentId: Int + get() = _selectedCombinedId.componentId + public val selectedSub: Int + get() = _selectedSub.toIntOrMinusOne() + public val selectedObj: Int + get() = _selectedObj.toIntOrMinusOne() + public val targetCombinedId: Int + get() = _targetCombinedId.combinedId + public val targetInterfaceId: Int + get() = _targetCombinedId.interfaceId + public val targetComponentId: Int + get() = _targetCombinedId.componentId + public val targetSub: Int + get() = _targetSub.toIntOrMinusOne() + public val targetObj: Int + get() = _targetObj.toIntOrMinusOne() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfButtonD + + if (_selectedCombinedId != other._selectedCombinedId) return false + if (_selectedSub != other._selectedSub) return false + if (_selectedObj != other._selectedObj) return false + if (_targetCombinedId != other._targetCombinedId) return false + if (_targetSub != other._targetSub) return false + if (_targetObj != other._targetObj) return false + + return true + } + + override fun hashCode(): Int { + var result = _selectedCombinedId.hashCode() + result = 31 * result + _selectedSub.hashCode() + result = 31 * result + _selectedObj.hashCode() + result = 31 * result + _targetCombinedId.hashCode() + result = 31 * result + _targetSub.hashCode() + result = 31 * result + _targetObj.hashCode() + return result + } + + override fun toString(): String = + "IfButtonD(" + + "selectedInterfaceId=$selectedInterfaceId, " + + "selectedComponentId=$selectedComponentId, " + + "selectedSub=$selectedSub, " + + "selectedObj=$selectedObj, " + + "targetInterfaceId=$targetInterfaceId, " + + "targetComponentId=$targetComponentId, " + + "targetSub=$targetSub, " + + "targetObj=$targetObj" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfButtonT.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfButtonT.kt new file mode 100644 index 000000000..899f56ec5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfButtonT.kt @@ -0,0 +1,110 @@ +package net.rsprot.protocol.game.incoming.buttons + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * If button target messages are used whenever one button is targeted against another. + * @property selectedCombinedId the bitpacked combination of [selectedInterfaceId] and [selectedComponentId]. + * @property selectedInterfaceId the selected interface id of the component that is being used + * @property selectedComponentId the selected component id that is being used + * @property selectedSub the subcomponent id of the selected, or -1 if none exists + * @property selectedObj the obj in the selected subcomponent, or -1 if none exists + * @property targetCombinedId the bitpacked combination of [targetInterfaceId] and [targetComponentId]. + * @property targetInterfaceId the target interface id on which the selected component + * is being used + * @property targetComponentId the target component id on which the selected component + * is being used + * @property targetSub the target subcomponent id on which the selected component is + * being used, or -1 if none exists + * @property targetObj the obj within the target subcomponent, or -1 if none exists. + */ +@Suppress("DuplicatedCode", "MemberVisibilityCanBePrivate") +public class IfButtonT private constructor( + private val _selectedCombinedId: CombinedId, + private val _selectedSub: UShort, + private val _selectedObj: UShort, + private val _targetCombinedId: CombinedId, + private val _targetSub: UShort, + private val _targetObj: UShort, +) : IncomingGameMessage { + public constructor( + selectedCombinedId: CombinedId, + selectedSub: Int, + selectedObj: Int, + targetCombinedId: CombinedId, + targetSub: Int, + targetObj: Int, + ) : this( + selectedCombinedId, + selectedSub.toUShort(), + selectedObj.toUShort(), + targetCombinedId, + targetSub.toUShort(), + targetObj.toUShort(), + ) + + public val selectedCombinedId: Int + get() = _selectedCombinedId.combinedId + public val selectedInterfaceId: Int + get() = _selectedCombinedId.interfaceId + public val selectedComponentId: Int + get() = _selectedCombinedId.componentId + public val selectedSub: Int + get() = _selectedSub.toIntOrMinusOne() + public val selectedObj: Int + get() = _selectedObj.toIntOrMinusOne() + public val targetCombinedId: Int + get() = _targetCombinedId.combinedId + public val targetInterfaceId: Int + get() = _targetCombinedId.interfaceId + public val targetComponentId: Int + get() = _targetCombinedId.componentId + public val targetSub: Int + get() = _targetSub.toIntOrMinusOne() + public val targetObj: Int + get() = _targetObj.toIntOrMinusOne() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfButtonT + + if (_selectedCombinedId != other._selectedCombinedId) return false + if (_selectedSub != other._selectedSub) return false + if (_selectedObj != other._selectedObj) return false + if (_targetCombinedId != other._targetCombinedId) return false + if (_targetSub != other._targetSub) return false + if (_targetObj != other._targetObj) return false + + return true + } + + override fun hashCode(): Int { + var result = _selectedCombinedId.hashCode() + result = 31 * result + _selectedSub.hashCode() + result = 31 * result + _selectedObj.hashCode() + result = 31 * result + _targetCombinedId.hashCode() + result = 31 * result + _targetSub.hashCode() + result = 31 * result + _targetObj.hashCode() + return result + } + + override fun toString(): String = + "IfButtonT(" + + "selectedInterfaceId=$selectedInterfaceId, " + + "selectedComponentId=$selectedComponentId, " + + "selectedSub=$selectedSub, " + + "selectedObj=$selectedObj, " + + "targetInterfaceId=$targetInterfaceId, " + + "targetComponentId=$targetComponentId, " + + "targetSub=$targetSub, " + + "targetObj=$targetObj" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfSubOp.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfSubOp.kt new file mode 100644 index 000000000..0fb98217a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/buttons/IfSubOp.kt @@ -0,0 +1,89 @@ +package net.rsprot.protocol.game.incoming.buttons + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * Ifsubop messages are sent when the player clicks on a submenu option. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id the player interacted with + * @property componentId the component id on that interface the player interacted with + * @property sub the subcomponent within that component if it has one, otherwise -1 + * @property obj the obj in that subcomponent, or -1 + * @property op the option clicked, ranging from 1 to 10 + * @property subop the submenu option clicked + */ +@Suppress("MemberVisibilityCanBePrivate") +public class IfSubOp private constructor( + private val _combinedId: CombinedId, + private val _sub: UShort, + private val _obj: UShort, + private val _op: UByte, + private val _subop: UByte, +) : IncomingGameMessage { + public constructor( + combinedId: CombinedId, + sub: Int, + obj: Int, + op: Int, + subop: Int, + ) : this( + combinedId, + sub.toUShort(), + obj.toUShort(), + op.toUByte(), + subop.toUByte(), + ) + + public val combinedId: Int + get() = _combinedId.combinedId + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val sub: Int + get() = _sub.toIntOrMinusOne() + public val obj: Int + get() = _obj.toIntOrMinusOne() + public val op: Int + get() = _op.toInt() + public val subop: Int + get() = _subop.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSubOp + + if (_combinedId != other._combinedId) return false + if (_sub != other._sub) return false + if (_obj != other._obj) return false + if (_op != other._op) return false + + return true + } + + override fun hashCode(): Int { + var result = _combinedId.hashCode() + result = 31 * result + _sub.hashCode() + result = 31 * result + _obj.hashCode() + result = 31 * result + _op.hashCode() + return result + } + + override fun toString(): String = + "If3Button(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "sub=$sub, " + + "obj=$obj, " + + "op=$op, " + + "subop=$subop" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/AffinedClanSettingsAddBannedFromChannel.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/AffinedClanSettingsAddBannedFromChannel.kt new file mode 100644 index 000000000..ba9052da0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/AffinedClanSettingsAddBannedFromChannel.kt @@ -0,0 +1,67 @@ +package net.rsprot.protocol.game.incoming.clan + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Clan ban messages are sent when a player with sufficient rank + * in the clan requests to ban another member within the clan. + * @property name the name of the player to ban + * @property clanId the id of the clan, ranging from 0 to 3 (inclusive). + * Negative values are not supported for bans - it is not possible to + * ban others while you are in a clan as a guest. + * @property memberIndex the index of the member in the clan who's being banned. + * Note that the index isn't the player's absolute index in the world, but rather + * the index within this clan. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class AffinedClanSettingsAddBannedFromChannel private constructor( + public val name: String, + private val _clanId: UByte, + private val _memberIndex: UShort, +) : IncomingGameMessage { + public constructor( + name: String, + clanId: Int, + memberIndex: Int, + ) : this( + name, + clanId.toUByte(), + memberIndex.toUShort(), + ) + + public val clanId: Int + get() = _clanId.toInt() + public val memberIndex: Int + get() = _memberIndex.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AffinedClanSettingsAddBannedFromChannel + + if (name != other.name) return false + if (_clanId != other._clanId) return false + if (_memberIndex != other._memberIndex) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _clanId.hashCode() + result = 31 * result + _memberIndex.hashCode() + return result + } + + override fun toString(): String = + "AffinedClanSettingsAddBannedFromChannel(" + + "name='$name', " + + "clanId=$clanId, " + + "memberIndex=$memberIndex" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/AffinedClanSettingsSetMutedFromChannel.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/AffinedClanSettingsSetMutedFromChannel.kt new file mode 100644 index 000000000..eee57da14 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/AffinedClanSettingsSetMutedFromChannel.kt @@ -0,0 +1,74 @@ +package net.rsprot.protocol.game.incoming.clan + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Clan ban messages are sent when a player with sufficient rank + * in the clan requests to mute another member within the clan. + * @property name the name of the player to mute + * @property clanId the id of the clan, ranging from 0 to 3 (inclusive). + * Negative values are not supported for mutes - it is not possible to + * mute others while you are in a clan as a guest. + * @property memberIndex the index of the member in the clan who's being muted. + * Note that the index isn't the player's absolute index in the world, but rather + * the index within this clan. + * @property muted whether to mute or unmute this player + */ +@Suppress("MemberVisibilityCanBePrivate") +public class AffinedClanSettingsSetMutedFromChannel private constructor( + public val name: String, + private val _clanId: UByte, + private val _memberIndex: UShort, + public val muted: Boolean, +) : IncomingGameMessage { + public constructor( + name: String, + clanId: Int, + memberIndex: Int, + muted: Boolean, + ) : this( + name, + clanId.toUByte(), + memberIndex.toUShort(), + muted, + ) + + public val clanId: Int + get() = _clanId.toInt() + public val memberIndex: Int + get() = _memberIndex.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AffinedClanSettingsSetMutedFromChannel + + if (name != other.name) return false + if (_clanId != other._clanId) return false + if (_memberIndex != other._memberIndex) return false + if (muted != other.muted) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _clanId.hashCode() + result = 31 * result + _memberIndex.hashCode() + result = 31 * result + muted.hashCode() + return result + } + + override fun toString(): String = + "AffinedClanSettingsSetMutedFromChannel(" + + "name='$name', " + + "clanId=$clanId, " + + "memberIndex=$memberIndex, " + + "muted=$muted" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanChannelFullRequest.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanChannelFullRequest.kt new file mode 100644 index 000000000..af6bfc09c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanChannelFullRequest.kt @@ -0,0 +1,33 @@ +package net.rsprot.protocol.game.incoming.clan + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Clan channel requests are made whenever the server sends a clanchannel + * delta update, but the client does not have a clan defined at that id. + * In order to fix the problem, the client will then request for a full + * clan update for that clan id. + * @property clanId the id of the clan to request, ranging from 0 to 3 (inclusive), + * or a negative value if the request is for a guest-clan + */ +public class ClanChannelFullRequest( + public val clanId: Int, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ClanChannelFullRequest + + return clanId == other.clanId + } + + override fun hashCode(): Int = clanId + + override fun toString(): String = "ClanChannelFullRequest(clanId=$clanId)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanChannelKickUser.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanChannelKickUser.kt new file mode 100644 index 000000000..401d98fe8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanChannelKickUser.kt @@ -0,0 +1,66 @@ +package net.rsprot.protocol.game.incoming.clan + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Clan kick messages are sent when a player with sufficient privileges + * requests to kick another player within the clan out of it. + * @property name the name of the player to kick + * @property clanId the id of the clan the player is in, ranging from 0 to 3 (inclusive), + * or negative values if referring to a guest clan + * @property memberIndex the index of the member in the clan who's being kicked. + * Note that the index isn't the player's absolute index in the world, but rather + * the index within this clan. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class ClanChannelKickUser private constructor( + public val name: String, + private val _clanId: Byte, + private val _memberIndex: UShort, +) : IncomingGameMessage { + public constructor( + name: String, + clanId: Int, + memberIndex: Int, + ) : this( + name, + clanId.toByte(), + memberIndex.toUShort(), + ) + + public val clanId: Int + get() = _clanId.toInt() + public val memberIndex: Int + get() = _memberIndex.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ClanChannelKickUser + + if (name != other.name) return false + if (_clanId != other._clanId) return false + if (_memberIndex != other._memberIndex) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _clanId + result = 31 * result + _memberIndex.hashCode() + return result + } + + override fun toString(): String = + "ClanChannelKickUser(" + + "name='$name', " + + "clanId=$clanId, " + + "memberIndex=$memberIndex" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanSettingsFullRequest.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanSettingsFullRequest.kt new file mode 100644 index 000000000..2966ab1e9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/clan/ClanSettingsFullRequest.kt @@ -0,0 +1,34 @@ +package net.rsprot.protocol.game.incoming.clan + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Clan settings requests are made whenever the server sends a clansettings + * delta update, but the update counter in the clan settings message + * is greater than that of the clan itself. In order to avoid problems, + * the client requests for a full clan settings update from the server, + * to re-synchronize all the values. + * @property clanId the id of the clan to request, ranging from 0 to 3 (inclusive), + * or a negative value if the request is for a guest-clan + */ +public class ClanSettingsFullRequest( + public val clanId: Int, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ClanSettingsFullRequest + + return clanId == other.clanId + } + + override fun hashCode(): Int = clanId + + override fun toString(): String = "ClanSettingsFullRequest(clanId=$clanId)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventAppletFocus.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventAppletFocus.kt new file mode 100644 index 000000000..a94fccb59 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventAppletFocus.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.events + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Applet focus events are sent whenever the client either loses or gains focus. + * This can be seen by minimizing and maximizing the clients. + * @property inFocus whether the client was put into focus or out of focus + */ +public class EventAppletFocus( + public val inFocus: Boolean, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EventAppletFocus + + return inFocus == other.inFocus + } + + override fun hashCode(): Int = inFocus.hashCode() + + override fun toString(): String = "EventAppletFocus(inFocus=$inFocus)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventCameraPosition.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventCameraPosition.kt new file mode 100644 index 000000000..19fff0bd6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventCameraPosition.kt @@ -0,0 +1,56 @@ +package net.rsprot.protocol.game.incoming.events + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Camera position events are sent whenever the client's camera changes position, + * at a maximum frequency of 20 client cycles (20ms/cc). + * @property angleX the x angle of the camera, in range of 128 to 383 (inclusive) + * @property angleY the y angle of the camera, in range of 0 to 2047 (inclusive) + */ +@Suppress("MemberVisibilityCanBePrivate") +public class EventCameraPosition private constructor( + private val _angleX: UShort, + private val _angleY: UShort, +) : IncomingGameMessage { + public constructor( + angleX: Int, + angleY: Int, + ) : this( + angleX.toUShort(), + angleY.toUShort(), + ) + + public val angleX: Int + get() = _angleX.toInt() + public val angleY: Int + get() = _angleY.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EventCameraPosition + + if (_angleX != other._angleX) return false + if (_angleY != other._angleY) return false + + return true + } + + override fun hashCode(): Int { + var result = _angleX.hashCode() + result = 31 * result + _angleY.hashCode() + return result + } + + override fun toString(): String = + "EventCameraPosition(" + + "angleX=$angleX, " + + "angleY=$angleY" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventKeyboard.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventKeyboard.kt new file mode 100644 index 000000000..4174bdeba --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventKeyboard.kt @@ -0,0 +1,329 @@ +package net.rsprot.protocol.game.incoming.events + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import java.awt.event.KeyEvent + +/** + * Keyboard events are transmitted at a maximum frequency of every 20 milliseconds. + * This means that - almost always - a single key is only sent in each packet, + * as it is very unlikely to get more than one key pressed within a 20-millisecond + * window, even when trying. + * While the packet does send the [lastTransmittedKeyPress] per key pressed, + * there is a flaw in the logic and any subsequent keys after the first will + * always write a value of 0. For this reason, in order to reduce the memory + * footprint of this message, we omit any subsequent timestamps and reduce + * our keys to a byte array value class for even further compression. + * If the time delta is greater than 16,777,215 milliseconds since the last + * key transmission, the [lastTransmittedKeyPress] value will be 16,777,215. + */ +public class EventKeyboard( + public val lastTransmittedKeyPress: Int, + public val keysPressed: KeySequence, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EventKeyboard + + if (lastTransmittedKeyPress != other.lastTransmittedKeyPress) return false + if (keysPressed != other.keysPressed) return false + + return true + } + + override fun hashCode(): Int { + var result = lastTransmittedKeyPress + result = 31 * result + keysPressed.hashCode() + return result + } + + override fun toString(): String = + "EventKeyboard(" + + "lastTransmittedKeyPress=$lastTransmittedKeyPress, " + + "keysPressed=$keysPressed" + + ")" + + /** + * KeySequence class represents a sequence of keys pressed in a byte array. + * This class provides helpful functionality to convert keys from the Jagex + * format back into the normalized [java.awt.event.KeyEvent] format. + * @property length the length of the key sequence + */ + @JvmInline + public value class KeySequence( + private val array: ByteArray, + ) { + public val length: Int + get() = array.size + + /** + * Returns the backing byte array of this key sequence, in Jagex format. + * It is worth noting that changes done to this array will directly + * modify this key sequence. + * All valid keys will be positive byte values. + */ + public fun asByteArray(): ByteArray = array + + /** + * Copies this backing key array into an int array, normalizing the + * values in the process - all keys will be either positive integers, + * or -1. + */ + public fun toIntArray(): IntArray = + IntArray(length) { index -> + getJagexKey(index) + } + + /** + * Transforms the backing key array into an int array with + * [java.awt.event.KeyEvent] key codes instead of the compressed + * Jagex format. Any invalid key will be represented as -1. + */ + public fun toAwtKeyCodeIntArray(): IntArray = + IntArray(length) { index -> + getAwtKey(index) + } + + /** + * Gets the Jagex key code at the provided [index]. + * @param index the index of the key code to obtain. + * @return Jagex compressed key code, or -1 if the key isn't valid. + * @throws ArrayIndexOutOfBoundsException if the index is below 0, or >= [length]. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public fun getJagexKey(index: Int): Int { + val code = array[index].toInt() and 0xFF + return if (code == 0xFF) { + -1 + } else { + code + } + } + + /** + * Gets the [java.awt.event.KeyEvent] key code at the provided [index]. + * @param index the index of the key code to obtain. + * @return [java.awt.event.KeyEvent] key code, or -1 if the key isn't valid. + * @throws ArrayIndexOutOfBoundsException if the index is below 0, or >= [length]. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public fun getAwtKey(index: Int): Int { + val jagexKey = getJagexKey(index) + return if (jagexKey == -1) { + -1 + } else { + jagexToAwtKeyCodes[jagexKey] + } + } + + /** + * Gets the [java.awt.event.KeyEvent] key code text at the provided [index]. + * @param index the index of the key code text to obtain. + * @return [java.awt.event.KeyEvent] key code text, or null if the key isn't valid. + * @throws ArrayIndexOutOfBoundsException if the index is below 0, or >= [length]. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public fun getAwtKeyText(index: Int): String? { + val keyCode = getAwtKey(index) + return if (keyCode == -1) { + null + } else { + KeyEvent.getKeyText(keyCode) + } + } + + private companion object { + /** + * The key code translation array found in the client. + * The trailing -1s have been omitted in this array to shorten the data structure. + */ + private val awtToJagexKeyCodes = + intArrayOf( + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 85, + 80, + 84, + -1, + 91, + -1, + -1, + -1, + 81, + 82, + 86, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 13, + -1, + -1, + -1, + -1, + 83, + 104, + 105, + 103, + 102, + 96, + 98, + 97, + 99, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 25, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 48, + 68, + 66, + 50, + 34, + 51, + 52, + 53, + 39, + 54, + 55, + 56, + 70, + 69, + 40, + 41, + 32, + 35, + 49, + 36, + 38, + 67, + 33, + 65, + 37, + 64, + -1, + -1, + -1, + -1, + -1, + 228, + 231, + 227, + 233, + 224, + 219, + 225, + 230, + 226, + 232, + 89, + 87, + -1, + 88, + 229, + 90, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + -1, + -1, + -1, + 101, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 100, + -1, + 87, + ) + + /** + * The Jagex key codes to AWT key codes translation array. + */ + private val jagexToAwtKeyCodes = buildJagexToAwtKeyCodesArray() + + /** + * Builds a Jagex keycode to AWT key code translation array, + * used to normalize the keycode events into traditional values. + */ + private fun buildJagexToAwtKeyCodesArray(): IntArray { + val keys = IntArray(256) { -1 } + for ((index, keycode) in awtToJagexKeyCodes.withIndex()) { + if (keycode == -1) { + continue + } + keys[keycode] = index + } + return keys + } + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseClick.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseClick.kt new file mode 100644 index 000000000..da3bc9b4e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseClick.kt @@ -0,0 +1,77 @@ +package net.rsprot.protocol.game.incoming.events + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Mouse click messages are sent whenever the user clicks with the + * right or left mouse button, and if the "Middle mouse button controls camera" + * is disabled, middle buttons (the scroll wheel itself). + * @property lastTransmittedMouseClick how many milliseconds since the last mouse + * click event was transmitted + * @property rightClick whether a right mouse click was performed, or left/middle. + * There is no distinction between left and middle transmitted to the server. + * @property x the x coordinate clicked, always a positive integer, capped to the + * client frame width. + * @property y the y coordinate clicked, always a positive integer, capped to the + * client frame height. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class EventMouseClick private constructor( + private val _lastTransmittedMouseClick: UShort, + public val rightClick: Boolean, + private val _x: UShort, + private val _y: UShort, +) : IncomingGameMessage { + public constructor( + lastTransmittedMouseClick: Int, + rightClick: Boolean, + x: Int, + y: Int, + ) : this( + lastTransmittedMouseClick.toUShort(), + rightClick, + x.toUShort(), + y.toUShort(), + ) + + public val lastTransmittedMouseClick: Int + get() = _lastTransmittedMouseClick.toInt() + public val x: Int + get() = _x.toInt() + public val y: Int + get() = _y.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EventMouseClick + + if (_lastTransmittedMouseClick != other._lastTransmittedMouseClick) return false + if (rightClick != other.rightClick) return false + if (_x != other._x) return false + if (_y != other._y) return false + + return true + } + + override fun hashCode(): Int { + var result = _lastTransmittedMouseClick.hashCode() + result = 31 * result + rightClick.hashCode() + result = 31 * result + _x.hashCode() + result = 31 * result + _y.hashCode() + return result + } + + override fun toString(): String = + "EventMouseClick(" + + "lastTransmittedMouseClick=$lastTransmittedMouseClick, " + + "rightClick=$rightClick, " + + "x=$x, " + + "y=$y" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseMove.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseMove.kt new file mode 100644 index 000000000..dfbd8d2f6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseMove.kt @@ -0,0 +1,58 @@ +package net.rsprot.protocol.game.incoming.events + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.game.incoming.events.util.MouseMovements +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Mouse move messages are sent when the user moves their mouse across + * the client. + * @property totalTime the total time in milliseconds that all the movements + * inside this event span across + * @property averageTime the average time in milliseconds between each movement. + * The average time is truncated according to integer division rules in the JVM. + * This is equal to `totalTime / count`. + * @property remainingTime the remaining time from the [averageTime] integer + * division. This is equal to `totalTime % count`. + * @property movements all the recorded mouse movements within this message. + * Mouse movements are recorded by the client at a 50 millisecond interval, + * meaning any movements within that 50 milliseconds are discarded, and + * only the position changes of the mouse at each 50 millisecond interval + * are sent. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class EventMouseMove private constructor( + private val _averageTime: UByte, + private val _remainingTime: UByte, + public val movements: MouseMovements, +) : IncomingGameMessage { + public constructor( + averageTime: Int, + remainingTime: Int, + movements: MouseMovements, + ) : this( + averageTime.toUByte(), + remainingTime.toUByte(), + movements, + ) + + public val totalTime: Int + get() = (_averageTime.toInt() * movements.length) + _remainingTime.toInt() + + public val averageTime: Int + get() = _averageTime.toInt() + + public val remainingTime: Int + get() = _remainingTime.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT + + override fun toString(): String = + "EventMouseMove(" + + "movements=$movements, " + + "totalTime=$totalTime, " + + "averageTime=$averageTime, " + + "remainingTime=$remainingTime" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseScroll.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseScroll.kt new file mode 100644 index 000000000..e44280c41 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventMouseScroll.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.incoming.events + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Mouse scroll message is sent whenever the user scrolls using their mouse. + * @property mouseWheelRotation the number of "clicks" the mouse wheel has rotated. + * If the mouse wheel was rotated up/away from the user, negative value is sent, + * and if the wheel was rotated down/towards the user, a positive value is sent. + */ +public class EventMouseScroll( + public val mouseWheelRotation: Int, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EventMouseScroll + + return mouseWheelRotation == other.mouseWheelRotation + } + + override fun hashCode(): Int = mouseWheelRotation + + override fun toString(): String = "EventMouseScroll(mouseWheelRotation=$mouseWheelRotation)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventNativeMouseClick.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventNativeMouseClick.kt new file mode 100644 index 000000000..ecfe773c0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventNativeMouseClick.kt @@ -0,0 +1,78 @@ +package net.rsprot.protocol.game.incoming.events + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Mouse click messages are sent whenever the user clicks with the + * right or left mouse button, and if the "Middle mouse button controls camera" + * is disabled, middle buttons (the scroll wheel itself). + * @property lastTransmittedMouseClick how many milliseconds since the last mouse + * click event was transmitted + * @property code the hook code from windows. + * See [link here](https://learn.microsoft.com/en-us/windows/win32/winmsg/about-hooks?redirectedfrom=MSDN). + * @property x the x coordinate clicked, always a positive integer, capped to the + * client frame width. + * @property y the y coordinate clicked, always a positive integer, capped to the + * client frame height. + */ +public class EventNativeMouseClick private constructor( + private val _lastTransmittedMouseClick: UShort, + private val _code: UByte, + private val _x: UShort, + private val _y: UShort, +) : IncomingGameMessage { + public constructor( + lastTransmittedMouseClick: Int, + code: Int, + x: Int, + y: Int, + ) : this( + lastTransmittedMouseClick.toUShort(), + code.toUByte(), + x.toUShort(), + y.toUShort(), + ) + + public val lastTransmittedMouseClick: Int + get() = _lastTransmittedMouseClick.toInt() + public val code: Int + get() = _code.toInt() + public val x: Int + get() = _x.toInt() + public val y: Int + get() = _y.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as EventNativeMouseClick + + if (_lastTransmittedMouseClick != other._lastTransmittedMouseClick) return false + if (_code != other._code) return false + if (_x != other._x) return false + if (_y != other._y) return false + + return true + } + + override fun hashCode(): Int { + var result = _lastTransmittedMouseClick.hashCode() + result = 31 * result + _code.hashCode() + result = 31 * result + _x.hashCode() + result = 31 * result + _y.hashCode() + return result + } + + override fun toString(): String = + "EventNativeMouseClick(" + + "lastTransmittedMouseClick=$lastTransmittedMouseClick, " + + "code=$code, " + + "x=$x, " + + "y=$y" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventNativeMouseMove.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventNativeMouseMove.kt new file mode 100644 index 000000000..b77cb2d33 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/EventNativeMouseMove.kt @@ -0,0 +1,57 @@ +package net.rsprot.protocol.game.incoming.events + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.game.incoming.events.util.MouseMovements +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Mouse move messages are sent when the user moves their mouse across + * the client, in this case, on the enhanced C++ clients. + * @property totalTime the total time in milliseconds that all the movements + * inside this event span across + * @property averageTime the average time in milliseconds between each movement. + * The average time is truncated according to integer division rules in the JVM. + * This is equal to `totalTime / count`. + * @property remainingTime the remaining time from the [averageTime] integer + * division. This is equal to `totalTime % count`. + * @property movements all the recorded mouse movements within this message. + * Mouse movements are recorded by the client at a 50 millisecond interval, + * meaning any movements within that 50 milliseconds are discarded, and + * only the position changes of the mouse at each 50 millisecond interval + * are sent. + */ +public class EventNativeMouseMove private constructor( + private val _averageTime: UByte, + private val _remainingTime: UByte, + public val movements: MouseMovements, +) : IncomingGameMessage { + public constructor( + averageTime: Int, + remainingTime: Int, + movements: MouseMovements, + ) : this( + averageTime.toUByte(), + remainingTime.toUByte(), + movements, + ) + + public val totalTime: Int + get() = (_averageTime.toInt() * movements.length) + _remainingTime.toInt() + + public val averageTime: Int + get() = _averageTime.toInt() + + public val remainingTime: Int + get() = _remainingTime.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT + + override fun toString(): String = + "EventNativeMouseMove(" + + "movements=$movements, " + + "totalTime=$totalTime, " + + "averageTime=$averageTime, " + + "remainingTime=$remainingTime" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/util/MouseMovements.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/util/MouseMovements.kt new file mode 100644 index 000000000..9591854b8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/events/util/MouseMovements.kt @@ -0,0 +1,79 @@ +package net.rsprot.protocol.game.incoming.events.util + +/** + * A value class that wraps around an array of mouse movements, + * with the encoding specified by [MousePosChange]. + * @property length the number of mouse movements in this packet. + */ +@Suppress("MemberVisibilityCanBePrivate") +@JvmInline +public value class MouseMovements( + private val movements: LongArray, +) { + public val length: Int + get() = movements.size + + /** + * @return the mouse movements data structure as a long array, + * with encoding as specified by [MousePosChange]. + * It is worth noting the encoding does not match up with the client. + * However, if people wish to store the mouse movements for later usage, + * this function provides the backing array which can be reconstructed + * at a later date. + * Changes to the backing array will directly reflect on this class. + */ + public fun asLongArray(): LongArray = movements + + /** + * Gets the mouse position change at the specified [index] + * @param index the index at which to obtain the mouse pos change. + * @return the mouse position change that occurred at that index. + * @throws ArrayIndexOutOfBoundsException if the index is below 0, or >= [length] + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public fun getMousePosChange(index: Int): MousePosChange = MousePosChange(movements[index]) + + /** + * A value class for mouse position changes, packed into a primitive long. + * We utilize bitpacking in order to use primitive long arrays for space + * constraints. + * @property packed the bitpacked long value, exposed as servers may wish + * to re-compose the position changes at a later date. + * @property timeDelta the time difference in milliseconds since the last + * transmitted mouse movement. + * @property xDelta the x coordinate delta of the mouse, in pixels. If the + * mouse goes outside the client window, the value will be -1. + * @property yDelta the y coordinate delta of the mouse, in pixels. If the + * mouse goes outside the client window, the value will be -1. + */ + @Suppress("MemberVisibilityCanBePrivate") + @JvmInline + public value class MousePosChange( + public val packed: Long, + ) { + public constructor( + timeDelta: Int, + xDelta: Int, + yDelta: Int, + ) : this( + (timeDelta and 0xFFFF) + .toLong() + .or(xDelta.toLong() and 0xFFFF shl 16) + .or(yDelta.toLong() and 0xFFFF shl 32), + ) + + public val timeDelta: Int + get() = (packed and 0xFFFF).toInt() + public val xDelta: Int + get() = (packed ushr 16 and 0xFFFF).toShort().toInt() + public val yDelta: Int + get() = (packed ushr 32 and 0xFFFF).toShort().toInt() + + override fun toString(): String = + "MousePosChange(" + + "timeDelta=$timeDelta, " + + "xDelta=$xDelta, " + + "yDelta=$yDelta" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatJoinLeave.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatJoinLeave.kt new file mode 100644 index 000000000..dbdc3c7b7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatJoinLeave.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.incoming.friendchat + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Friend chat join-leave message is sent when the player joins or leaves + * a friend chat channel. + * @property name the name of the player whose friend chat channel to join, + * or null if the player is leaving a friend chat channel + */ +public class FriendChatJoinLeave( + public val name: String?, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FriendChatJoinLeave + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() + + override fun toString(): String = "FriendChatJoinLeave(name='$name')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatKick.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatKick.kt new file mode 100644 index 000000000..848b8c839 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatKick.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.friendchat + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Friend chat kick is sent when the owner requests to click another + * player from their friend chat. + * @property name the name of the player to kick + */ +public class FriendChatKick( + public val name: String, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FriendChatKick + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() + + override fun toString(): String = "FriendChatKick(name='$name')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatSetRank.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatSetRank.kt new file mode 100644 index 000000000..efc9c3cce --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/friendchat/FriendChatSetRank.kt @@ -0,0 +1,54 @@ +package net.rsprot.protocol.game.incoming.friendchat + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Friend chat set rank message is sent when the owner of a friend chat + * channel changes the rank of another player who is on their friendlist. + * @property name the name of the player whose rank to change + * @property rank the id of the new rank to set to that player + */ +@Suppress("MemberVisibilityCanBePrivate") +public class FriendChatSetRank private constructor( + public val name: String, + private val _rank: UByte, +) : IncomingGameMessage { + public constructor( + name: String, + rank: Int, + ) : this( + name, + rank.toUByte(), + ) + + public val rank: Int + get() = _rank.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FriendChatSetRank + + if (name != other.name) return false + if (_rank != other._rank) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _rank.hashCode() + return result + } + + override fun toString(): String = + "FriendChatSetRank(" + + "name='$name', " + + "rank=$rank" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLoc.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLoc.kt new file mode 100644 index 000000000..a1ad3385d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLoc.kt @@ -0,0 +1,81 @@ +package net.rsprot.protocol.game.incoming.locs + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * OpLoc messages are fired when a player clicks one of the five (excluding oploc6) + * options on a loc in the game. + * @property id the base(non-multi-transformed) id of the loc the player clicked on + * @property x the absolute x coordinate of the south-western corner of the loc + * @property z the absolute z coordinate of the south-western corner of the loc + * @property controlKey whether the control key was held down, used to invert movement speed + * @property op the option clicked, ranging from 1 to 5 (inclusive) + */ +@Suppress("DuplicatedCode", "MemberVisibilityCanBePrivate") +public class OpLoc private constructor( + private val _id: UShort, + private val _x: UShort, + private val _z: UShort, + public val controlKey: Boolean, + private val _op: UByte, +) : IncomingGameMessage { + public constructor( + id: Int, + x: Int, + z: Int, + controlKey: Boolean, + op: Int, + ) : this( + id.toUShort(), + x.toUShort(), + z.toUShort(), + controlKey, + op.toUByte(), + ) + + public val id: Int + get() = _id.toInt() + public val x: Int + get() = _x.toInt() + public val z: Int + get() = _z.toInt() + public val op: Int + get() = _op.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpLoc + + if (_id != other._id) return false + if (_x != other._x) return false + if (_z != other._z) return false + if (controlKey != other.controlKey) return false + if (_op != other._op) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _x.hashCode() + result = 31 * result + _z.hashCode() + result = 31 * result + controlKey.hashCode() + result = 31 * result + _op.hashCode() + return result + } + + override fun toString(): String = + "OpLoc(" + + "id=$id, " + + "x=$x, " + + "z=$z, " + + "controlKey=$controlKey, " + + "op=$op" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLoc6.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLoc6.kt new file mode 100644 index 000000000..c039a3284 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLoc6.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.locs + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * OpLoc6 message is fired whenever a player clicks examine on a loc. + * @property id the id of the loc (if multiloc, transformed to the + * currently visible variant) + */ +public class OpLoc6( + public val id: Int, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpLoc6 + + return id == other.id + } + + override fun hashCode(): Int = id + + override fun toString(): String = "OpLoc6(id=$id)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLocT.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLocT.kt new file mode 100644 index 000000000..c6e256e35 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/locs/OpLocT.kt @@ -0,0 +1,105 @@ +package net.rsprot.protocol.game.incoming.locs + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * OpLocT messages are fired whenever an interface component is targeted + * on a loc, which, as of revision 204, includes using items from + * the player's inventory on locs - the OpLocU message was deprecated. + * @property id the base(non-multi-transformed) id of the loc the component was used on + * @property x the absolute x coordinate of the south-western corner of the loc + * @property z the absolute z coordinate of the south-western corner of the loc + * @property controlKey whether the control key was held down, used to invert movement speed + * @property selectedCombinedId the bitpacked combination of [selectedInterfaceId] and [selectedComponentId]. + * @property selectedInterfaceId the interface id of the selected component + * @property selectedComponentId the component id being used on the loc + * @property selectedSub the subcomponent of the selected component, or -1 of none exists + * @property selectedObj the obj on the selected subcomponent, or -1 if none exists + */ +@Suppress("DuplicatedCode", "MemberVisibilityCanBePrivate") +public class OpLocT private constructor( + private val _id: UShort, + private val _x: UShort, + private val _z: UShort, + public val controlKey: Boolean, + private val _selectedCombinedId: CombinedId, + private val _selectedSub: UShort, + private val _selectedObj: UShort, +) : IncomingGameMessage { + public constructor( + id: Int, + x: Int, + z: Int, + controlKey: Boolean, + selectedCombinedId: CombinedId, + selectedSub: Int, + selectedObj: Int, + ) : this( + id.toUShort(), + x.toUShort(), + z.toUShort(), + controlKey, + selectedCombinedId, + selectedSub.toUShort(), + selectedObj.toUShort(), + ) + + public val id: Int + get() = _id.toInt() + public val x: Int + get() = _x.toInt() + public val z: Int + get() = _z.toInt() + public val selectedCombinedId: Int + get() = _selectedCombinedId.combinedId + public val selectedInterfaceId: Int + get() = _selectedCombinedId.interfaceId + public val selectedComponentId: Int + get() = _selectedCombinedId.componentId + public val selectedSub: Int + get() = _selectedSub.toIntOrMinusOne() + public val selectedObj: Int + get() = _selectedObj.toIntOrMinusOne() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpLocT + + if (_id != other._id) return false + if (controlKey != other.controlKey) return false + if (_selectedCombinedId != other._selectedCombinedId) return false + if (_selectedSub != other._selectedSub) return false + if (_selectedObj != other._selectedObj) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + controlKey.hashCode() + result = 31 * result + _selectedCombinedId.hashCode() + result = 31 * result + _selectedSub.hashCode() + result = 31 * result + _selectedObj.hashCode() + return result + } + + override fun toString(): String = + "OpLocT(" + + "id=$id, " + + "x=$x, " + + "z=$z, " + + "controlKey=$controlKey, " + + "selectedInterfaceId=$selectedInterfaceId, " + + "selectedComponentId=$selectedComponentId, " + + "selectedSub=$selectedSub, " + + "selectedObj=$selectedObj" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/messaging/MessagePrivate.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/messaging/MessagePrivate.kt new file mode 100644 index 000000000..c91196715 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/messaging/MessagePrivate.kt @@ -0,0 +1,44 @@ +package net.rsprot.protocol.game.incoming.messaging + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Message private events are sent when a player writes a private + * message to the target player. The server is responsible for looking + * up the target player and forwarding the message to them, if possible. + * @property name the name of the recipient of this private message + * @property message the message forwarded to the recipient + */ +public class MessagePrivate( + public val name: String, + public val message: String, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessagePrivate + + if (name != other.name) return false + if (message != other.message) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + message.hashCode() + return result + } + + override fun toString(): String = + "MessagePrivate(" + + "name='$name', " + + "message='$message'" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/messaging/MessagePublic.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/messaging/MessagePublic.kt new file mode 100644 index 000000000..e6a3493e6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/messaging/MessagePublic.kt @@ -0,0 +1,250 @@ +package net.rsprot.protocol.game.incoming.messaging + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Message public events are sent when the player talks in public. + * + * Chat types table: + * ``` + * | Id | Type | + * |----|:------------------:| + * | 0 | Normal | + * | 1 | Autotyper | + * | 2 | Friend channel | + * | 3 | Clan main channel | + * | 4 | Clan guest channel | + * ``` + * + * Colour table: + * ``` + * | Id | Prefix | Hex Value | + * |-------|-----------|:--------------------------:| + * | 0 | yellow: | 0xFFFF00 | + * | 1 | red: | 0xFF0000 | + * | 2 | green: | 0x00FF00 | + * | 3 | cyan: | 0x00FFFF | + * | 4 | purple: | 0xFF00FF | + * | 5 | white: | 0xFFFFFF | + * | 6 | flash1: | 0xFF0000/0xFFFF00 | + * | 7 | flash2: | 0x0000FF/0x00FFFF | + * | 8 | flash3: | 0x00B000/0x80FF80 | + * | 9 | glow1: | 0xFF0000-0xFFFF00-0x00FFFF | + * | 10 | glow2: | 0xFF0000-0x00FF00-0x0000FF | + * | 11 | glow3: | 0xFFFFFF-0x00FF00-0x00FFFF | + * | 12 | rainbow: | N/A | + * | 13-20 | pattern*: | N/A | + * ``` + * + * Effects table: + * ``` + * | Id | Prefix | + * |----|---------| + * | 1 | wave: | + * | 2 | wave2: | + * | 3 | shake: | + * | 4 | scroll: | + * | 5 | slide: | + * ``` + * + * Clan types table: + * ``` + * | Id | Type | + * |----|:-------------:| + * | 0 | Normal clan | + * | 1 | Group ironman | + * | 2 | PvP Arena | + * ``` + * + * @property type the type of the message, ranging from 0 to 4 (inclusive) (see above) + * @property colour the colour of the message, ranging from 0 to 20 (inclusive) (see above) + * @property effect the effect of the message, ranging from 0 to 5 (inclusive) (see above) + * @property message the message typed + * @property pattern the colour pattern attached to the message, if the [colour] value is + * in range of 13-20 (inclusive), otherwise null + * @property clanType the clan type, if the [type] is the main clan channel, + * a value of 0 to 2 (inclusive) is provided. If the clan type is not defined, + * the value of -1 is given. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class MessagePublic private constructor( + private val _type: UByte, + private val _colour: UByte, + private val _effect: UByte, + public val message: String, + public val pattern: MessageColourPattern?, + private val _clanType: Byte, +) : IncomingGameMessage { + public constructor( + type: Int, + colour: Int, + effect: Int, + message: String, + pattern: MessageColourPattern?, + clanType: Int, + ) : this( + type.toUByte(), + colour.toUByte(), + effect.toUByte(), + message, + pattern, + clanType.toByte(), + ) + + public val type: Int + get() = _type.toInt() + public val colour: Int + get() = _colour.toInt() + public val effect: Int + get() = _effect.toInt() + public val clanType: Int + get() = _clanType.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessagePublic + + if (_type != other._type) return false + if (_colour != other._colour) return false + if (_effect != other._effect) return false + if (message != other.message) return false + if (pattern != other.pattern) return false + if (_clanType != other._clanType) return false + + return true + } + + override fun hashCode(): Int { + var result = _type.hashCode() + result = 31 * result + _colour.hashCode() + result = 31 * result + _effect.hashCode() + result = 31 * result + message.hashCode() + result = 31 * result + (pattern?.hashCode() ?: 0) + result = 31 * result + _clanType + return result + } + + override fun toString(): String = + "MessagePublicEvent(" + + "message='$message', " + + "pattern=$pattern, " + + "type=$type, " + + "colour=$colour, " + + "effect=$effect, " + + "clanType=$clanType" + + ")" + + /** + * A value class for message colour patterns, allowing easy + * conversion from the byte array to the respective 24-bit RGB colours. + * This wrapper class additionally provides a helpful [isValid] function, + * as it is possible to otherwise send bad data from the client and + * crash the players in vicinity. + */ + @JvmInline + public value class MessageColourPattern( + private val bytes: ByteArray, + ) { + public val length: Int + get() = bytes.size + + /** + * @return the backing byte array for the pattern. + * Changes done to this array will reflect on the pattern itself. + */ + public fun asByteArray(): ByteArray = bytes + + /** + * @return a copy of the backing pattern byte array. + */ + public fun toByteArray(): ByteArray = bytes.copyOf() + + /** + * Checks if the pattern itself is valid (as in, will not crash the client). + * The client's own checks are currently slightly flawed and allow for + * crashes to occur in one particular manner. + * @return whether the pattern is valid + */ + public fun isValid(): Boolean { + if (length !in 1..8) { + return false + } + for (i in bytes.indices) { + val value = bytes[i].toInt() + if (value < 0 || value >= colourCodes.size) { + return false + } + } + return true + } + + /** + * Turns the pattern into a 24-bit RGB colour array, if it is valid. + * @return 24-bit RGB colour array of this pattern, or null if the pattern + * is corrupt. + */ + public fun to24BitRgbOrNull(): IntArray? { + if (length !in 1..8) { + return null + } + val colours = IntArray(length) + for (i in bytes.indices) { + val colourCode = + colourCodes.getOrNull(bytes[i].toInt()) + ?: return null + colours[i] = colourCode + } + return colours + } + + override fun toString(): String = "MessageColourPattern(bytes=${bytes.contentToString()})" + + private companion object { + private val colourCodes = + intArrayOf( + 0xffffff, + 0xe40303, + 0xff8c00, + 0xffed00, + 0x8026, + 0x24408e, + 0x732982, + 0xff218c, + 0xb55690, + 0x5049cc, + 0xa3a3a3, + 0xd52d00, + 0xef7627, + 0xfcf434, + 0x78d70, + 0x21b1ff, + 0x9b4f96, + 0xffafc7, + 0xd162a4, + 0x7bade3, + 0xff9a56, + 0x26ceaa, + 0x73d7ee, + 0x9c59d1, + 0x98e8c1, + 0xb57edc, + 0x2c2c2c, + 0x940202, + 0x613915, + 0xd0c100, + 0x4a8123, + 0x38a8, + 0x800080, + 0xd60270, + 0xa30262, + 0x3d1a78, + ) + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/ConnectionTelemetry.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/ConnectionTelemetry.kt new file mode 100644 index 000000000..e3a001707 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/ConnectionTelemetry.kt @@ -0,0 +1,79 @@ +package net.rsprot.protocol.game.incoming.misc.client + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Connection telemetry is sent as part of the first packets during login, + * written during the part of login that handles login rebuild messages + * and player info initialization. + * While the packet sends more properties than the four listed here, + * they are never assigned a value, so they're just dummy zeros. + * @property connectionLostDuration how long the connection was lost for. + * Each unit here equals 10 milliseconds. The value is coerced in 0..65535 + * @property loginDuration how long the login took to complete. + * Each unit here equals 10 milliseconds. The value is coerced in 0..65535 + * @property clientState the state the client is in + * @property loginCount how many login attempts have occurred. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class ConnectionTelemetry private constructor( + private val _connectionLostDuration: UShort, + private val _loginDuration: UShort, + private val _clientState: UShort, + private val _loginCount: UShort, +) : IncomingGameMessage { + public constructor( + connectionLostDuration: Int, + loginDuration: Int, + clientState: Int, + loginCount: Int, + ) : this( + connectionLostDuration.toUShort(), + loginDuration.toUShort(), + clientState.toUShort(), + loginCount.toUShort(), + ) + + public val connectionLostDuration: Int + get() = _connectionLostDuration.toInt() + public val loginDuration: Int + get() = _loginDuration.toInt() + public val clientState: Int + get() = _clientState.toInt() + public val loginCount: Int + get() = _loginCount.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ConnectionTelemetry + + if (_connectionLostDuration != other._connectionLostDuration) return false + if (_loginDuration != other._loginDuration) return false + if (_clientState != other._clientState) return false + if (_loginCount != other._loginCount) return false + + return true + } + + override fun hashCode(): Int { + var result = _connectionLostDuration.hashCode() + result = 31 * result + _loginDuration.hashCode() + result = 31 * result + _clientState.hashCode() + result = 31 * result + _loginCount.hashCode() + return result + } + + override fun toString(): String = + "ConnectionTelemetry(" + + "connectionLostDuration=$connectionLostDuration, " + + "loginDuration=$loginDuration, " + + "clientState=$clientState, " + + "loginCount=$loginCount" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/DetectModifiedClient.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/DetectModifiedClient.kt new file mode 100644 index 000000000..b4dbe0adb --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/DetectModifiedClient.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.incoming.misc.client + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Detect modified client is sent by the client right before a map load + * if the client has been given a frame. For simple deobs, this is generally + * not the case. + * In OSRS, the code is consistently sent as '1,057,001,181'. + */ +public class DetectModifiedClient( + public val code: Int, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DetectModifiedClient + + return code == other.code + } + + override fun hashCode(): Int = code + + override fun toString(): String = "DetectModifiedClient(code=$code)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/Idle.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/Idle.kt new file mode 100644 index 000000000..bd940ac95 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/Idle.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.incoming.misc.client + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Idle messages are sent if the user hasn't interacted with their + * mouse nor their keyboard for 15,000 client cycles (20ms/cc) in a row, + * meaning continuous inactivity for five minutes in a row. + */ +public data object Idle : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/MapBuildComplete.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/MapBuildComplete.kt new file mode 100644 index 000000000..51e03a4a8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/MapBuildComplete.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.game.incoming.misc.client + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Map build complete is sent when the client finishes building map after + * a map reload. This packet is primarily used by the server for `p_loaddelay;` + * procs, which delay current active script until the client has finished loading + * the map, with a 10-game-cycle timeout. + */ +public data object MapBuildComplete : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/MembershipPromotionEligibility.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/MembershipPromotionEligibility.kt new file mode 100644 index 000000000..909244c65 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/MembershipPromotionEligibility.kt @@ -0,0 +1,60 @@ +package net.rsprot.protocol.game.incoming.misc.client + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * An enhanced-client-only packet to inform the server of the status of + * membership eligibility. + * @property eligibleForIntroductoryPrice whether the player is eligible for + * an introductory price, kept in an integer form in case there are more values + * than just yes/no. + * @property eligibleForTrialPurchase whether the player is eligible + * for a trial purchase, kept int an integer form in case there are more values + * than just yes/no. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class MembershipPromotionEligibility private constructor( + private val _eligibleForIntroductoryPrice: UByte, + private val _eligibleForTrialPurchase: UByte, +) : IncomingGameMessage { + public constructor( + eligibleForIntroductoryPrice: Int, + eligibleForTrialPurchase: Int, + ) : this( + eligibleForIntroductoryPrice.toUByte(), + eligibleForTrialPurchase.toUByte(), + ) + + public val eligibleForIntroductoryPrice: Int + get() = _eligibleForIntroductoryPrice.toInt() + public val eligibleForTrialPurchase: Int + get() = _eligibleForTrialPurchase.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MembershipPromotionEligibility + + if (_eligibleForIntroductoryPrice != other._eligibleForIntroductoryPrice) return false + if (_eligibleForTrialPurchase != other._eligibleForTrialPurchase) return false + + return true + } + + override fun hashCode(): Int { + var result = _eligibleForIntroductoryPrice.hashCode() + result = 31 * result + _eligibleForTrialPurchase.hashCode() + return result + } + + override fun toString(): String = + "MembershipPromotionEligibility(" + + "eligibleForIntroductoryPrice=$eligibleForIntroductoryPrice, " + + "eligibleForTrialPurchase=$eligibleForTrialPurchase" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/NoTimeout.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/NoTimeout.kt new file mode 100644 index 000000000..41088c8c3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/NoTimeout.kt @@ -0,0 +1,14 @@ +package net.rsprot.protocol.game.incoming.misc.client + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * No timeout packets are sent every 50 client cycles (20ms/cc) + * to ensure the server doesn't disconnect the client due to inactivity. + */ +public data object NoTimeout : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/ReflectionCheckReply.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/ReflectionCheckReply.kt new file mode 100644 index 000000000..c30e1874e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/ReflectionCheckReply.kt @@ -0,0 +1,376 @@ +package net.rsprot.protocol.game.incoming.misc.client + +import io.netty.buffer.ByteBuf +import net.rsprot.buffer.extensions.checkCRC32 +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.game.outgoing.misc.client.ReflectionChecker +import net.rsprot.protocol.message.IncomingGameMessage +import java.io.IOException +import java.io.InvalidClassException +import java.io.OptionalDataException +import java.io.StreamCorruptedException +import java.lang.reflect.InvocationTargetException +import kotlin.IllegalArgumentException + +/** + * A reflection check reply is sent by the client whenever a server requests + * a reflection checker to be performed. + * @property id the original request id sent by the server. + * @property result the resulting byte buffer slice. + * As decoding reflection checks requires knowing the original request that was made, + * we have to defer the decoding of the payload until the original request is + * provided to us, thus, using [decode] we can obtain the real results. + */ +public class ReflectionCheckReply( + public val id: Int, + public val result: ByteBuf, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.CLIENT_EVENT + + /** + * Decodes the reply using the original [request] that the server put in. + * It is worth noting that the [result] buffer will always be released + * after the decoding function call, so it may only be called once. + */ + public fun decode(request: ReflectionChecker): List> { + try { + val buffer = result.toJagByteBuf() + // Skip the id, it is necessary for CRC verification though. + buffer.skipRead(4) + val results = ArrayList>(request.checks.size) + for (check in request.checks) { + val opcode = buffer.g1s() + if (opcode < 0) { + val exception = getExceptionClass(opcode) + results += ErrorResult(check, exception) + continue + } + when (check) { + is ReflectionChecker.GetFieldValue -> { + val result = buffer.g4() + results += GetFieldValueResult(check, result) + } + is ReflectionChecker.SetFieldValue -> { + results += SetFieldValueResult(check) + } + is ReflectionChecker.GetFieldModifiers -> { + val modifiers = buffer.g4() + results += GetFieldModifiersResult(check, modifiers) + } + is ReflectionChecker.InvokeMethod -> { + results += + when (opcode) { + 0 -> InvokeMethodResult(check, NullReturnValue) + 1 -> InvokeMethodResult(check, NumberReturnValue(buffer.g8())) + 2 -> InvokeMethodResult(check, StringReturnValue(buffer.gjstr())) + 4 -> InvokeMethodResult(check, UnknownReturnValue) + else -> throw IllegalStateException("Unknown opcode for method invocation: $opcode") + } + } + is ReflectionChecker.GetMethodModifiers -> { + val modifiers = buffer.g4() + results += GetMethodModifiersResult(check, modifiers) + } + } + } + result.readerIndex(result.writerIndex()) + if (!result.checkCRC32()) { + throw IllegalStateException("CRC mismatch!") + } + return results + } finally { + result.release() + } + } + + /** + * Gets the exception class corresponding to each opcode. + * @param opcode the opcode value + * @return the exception class corresponding to that opcode + */ + private fun getExceptionClass(opcode: Int): Class<*> = + when (opcode) { + -10 -> ClassNotFoundException::class.java + -11 -> InvalidClassException::class.java + -12 -> StreamCorruptedException::class.java + -13 -> OptionalDataException::class.java + -14 -> IllegalAccessException::class.java + -15 -> IllegalArgumentException::class.java + -16 -> InvocationTargetException::class.java + -17 -> SecurityException::class.java + -18 -> IOException::class.java + -19 -> NullPointerException::class.java + -20 -> Exception::class.java + -21 -> Throwable::class.java + else -> throw IllegalArgumentException("Unknown exception opcode: $opcode") + } + + override fun toString(): String = + "ReflectionCheckReply(" + + "id=$id, " + + "result=$result" + + ")" + + public sealed interface ReflectionCheckResult { + public val check: T + } + + /** + * Any error result will be in its own class, as there will not be any + * return values included in this lot. + * @property check the reflection check requested by the server + * @property exceptionClass the exception class that the client received + */ + public class ErrorResult>( + override val check: T, + public val exceptionClass: E, + ) : ReflectionCheckResult { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ErrorResult<*, *> + + if (check != other.check) return false + if (exceptionClass != other.exceptionClass) return false + + return true + } + + override fun hashCode(): Int { + var result = check.hashCode() + result = 31 * result + exceptionClass.hashCode() + return result + } + + override fun toString(): String = + "ErrorResult(" + + "check=$check, " + + "exceptionClass=$exceptionClass" + + ")" + } + + /** + * Get field value result provides a successful result for retrieving a + * value of a field in the client. + * @property check the reflection check requested by the server + * @property value the value that the client received after invoking reflection + */ + public class GetFieldValueResult( + override val check: ReflectionChecker.GetFieldValue, + public val value: Int, + ) : ReflectionCheckResult { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GetFieldValueResult + + if (check != other.check) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = check.hashCode() + result = 31 * result + value + return result + } + + override fun toString(): String = + "GetFieldValueResult(" + + "check=$check, " + + "value=$value" + + ")" + } + + /** + * Set field value results will only ever be successful if a value was + * successfully assigned, in which case nothing gets returned. + * @property check the reflection check requested by the server + */ + public class SetFieldValueResult( + override val check: ReflectionChecker.SetFieldValue, + ) : ReflectionCheckResult { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetFieldValueResult + + return check == other.check + } + + override fun hashCode(): Int = check.hashCode() + + override fun toString(): String = "SetFieldValueResult(check=$check)" + } + + /** + * Get field modifiers result will attempt to look up the modifiers + * of a field. + * @property check the reflection check requested by the server + * @property modifiers the bitpacked modifier values as assigned by the JVM + */ + public class GetFieldModifiersResult( + override val check: ReflectionChecker.GetFieldModifiers, + public val modifiers: Int, + ) : ReflectionCheckResult { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GetFieldModifiersResult + + if (check != other.check) return false + if (modifiers != other.modifiers) return false + + return true + } + + override fun hashCode(): Int { + var result = check.hashCode() + result = 31 * result + modifiers + return result + } + + override fun toString(): String = + "GetFieldModifiersResult(" + + "check=$check, " + + "modifiers=$modifiers" + + ")" + } + + /** + * Invoke method result is sent when a method invocation was successfully + * performed with the provided arguments and return type. + * @property check the reflection check requested by the server + * @property result the result of invoking the method + */ + public class InvokeMethodResult( + override val check: ReflectionChecker.InvokeMethod, + public val result: T, + ) : ReflectionCheckResult { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as InvokeMethodResult<*> + + if (check != other.check) return false + if (result != other.result) return false + + return true + } + + override fun hashCode(): Int { + var result1 = check.hashCode() + result1 = 31 * result1 + result.hashCode() + return result1 + } + + override fun toString(): String = + "InvokeMethodResult(" + + "check=$check, " + + "result=$result" + + ")" + } + + /** + * Get method modifiers will attempt to look up the modifiers of a method + * using reflection. + * @property check the reflection check requested by the server + * @property modifiers the bitpacked modifier values as assigned by the JVM + */ + public class GetMethodModifiersResult( + override val check: ReflectionChecker.GetMethodModifiers, + public val modifiers: Int, + ) : ReflectionCheckResult { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GetMethodModifiersResult + + if (check != other.check) return false + if (modifiers != other.modifiers) return false + + return true + } + + override fun hashCode(): Int { + var result = check.hashCode() + result = 31 * result + modifiers + return result + } + + override fun toString(): String = + "GetMethodModifiersResult(" + + "check=$check, " + + "modifiers=$modifiers" + + ")" + } + + public sealed interface MethodInvocationReturnValue + + /** + * A null return value is sent if a method invocation returned a null value. + */ + public data object NullReturnValue : MethodInvocationReturnValue + + /** + * A number return value is sent if a method returns any [Number] type, + * in which case the client will call [java.lang.Number.longValue] + * to retrieve the long representation of the value. + * @property longValue the long representation of the numeric value. + */ + public class NumberReturnValue( + public val longValue: Long, + ) : MethodInvocationReturnValue { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NumberReturnValue + + return longValue == other.longValue + } + + override fun hashCode(): Int = longValue.hashCode() + + override fun toString(): String = "NumberReturnValue(longValue=$longValue)" + } + + /** + * A string return value is provided if a method invocation results + * in a string value. + * @property stringValue the string value returned by the method. + */ + public class StringReturnValue( + public val stringValue: String, + ) : MethodInvocationReturnValue { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as StringReturnValue + + return stringValue == other.stringValue + } + + override fun hashCode(): Int = stringValue.hashCode() + + override fun toString(): String = "StringReturnValue(stringValue='$stringValue')" + } + + /** + * An unknown return value is provided when a method returns a value, + * but that value is not a null, a number of a string - essentially + * the 'else' case if all else falls through. + */ + public data object UnknownReturnValue : MethodInvocationReturnValue +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/SendPingReply.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/SendPingReply.kt new file mode 100644 index 000000000..03e237bc5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/SendPingReply.kt @@ -0,0 +1,70 @@ +package net.rsprot.protocol.game.incoming.misc.client + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Sends a ping reply to the server whenever the server requests it. + * @property fps the current fps of the client at the time of the message + * @property gcPercentTime the approximate percentage of the time spent + * garbage collecting + * @property value1 the first integer value sent by the serer + * @property value2 the second integer value sent by the server + */ +@Suppress("MemberVisibilityCanBePrivate") +public class SendPingReply private constructor( + private val _fps: UByte, + private val _gcPercentTime: UByte, + public val value1: Int, + public val value2: Int, +) : IncomingGameMessage { + public constructor( + fps: Int, + gcPercentTime: Int, + value1: Int, + value2: Int, + ) : this( + fps.toUByte(), + gcPercentTime.toUByte(), + value1, + value2, + ) + + public val fps: Int + get() = _fps.toInt() + public val gcPercentTime: Int + get() = _gcPercentTime.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SendPingReply + + if (_fps != other._fps) return false + if (_gcPercentTime != other._gcPercentTime) return false + if (value1 != other.value1) return false + if (value2 != other.value2) return false + + return true + } + + override fun hashCode(): Int { + var result = _fps.hashCode() + result = 31 * result + _gcPercentTime.hashCode() + result = 31 * result + value1 + result = 31 * result + value2 + return result + } + + override fun toString(): String = + "SendPingReply(" + + "fps=$fps, " + + "gcPercentTime=$gcPercentTime, " + + "value1=$value1, " + + "value2=$value2" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/SoundJingleEnd.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/SoundJingleEnd.kt new file mode 100644 index 000000000..6bf404035 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/SoundJingleEnd.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.misc.client + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Sound jingle end packet is sent when a jingle finishes playing in the client, + * used to resume normal music from the start again (basically informs the server + * that it needs to reset its internal play-time counter back to zero). + */ +public class SoundJingleEnd( + public val jingleId: Int, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SoundJingleEnd + + return jingleId == other.jingleId + } + + override fun hashCode(): Int = jingleId + + override fun toString(): String = "SoundJingleEnd(jingleId=$jingleId)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/WindowStatus.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/WindowStatus.kt new file mode 100644 index 000000000..e86b7c08c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/client/WindowStatus.kt @@ -0,0 +1,62 @@ +package net.rsprot.protocol.game.incoming.misc.client + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Window status is sent first on login, and afterwards whenever + * the client changes window status. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class WindowStatus private constructor( + private val _windowMode: UByte, + private val _frameWidth: UShort, + private val _frameHeight: UShort, +) : IncomingGameMessage { + public constructor( + windowMode: Int, + frameWidth: Int, + frameHeight: Int, + ) : this( + windowMode.toUByte(), + frameWidth.toUShort(), + frameHeight.toUShort(), + ) + + public val windowMode: Int + get() = _windowMode.toInt() + public val frameWidth: Int + get() = _frameWidth.toInt() + public val frameHeight: Int + get() = _frameHeight.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as WindowStatus + + if (_windowMode != other._windowMode) return false + if (_frameWidth != other._frameWidth) return false + if (_frameHeight != other._frameHeight) return false + + return true + } + + override fun hashCode(): Int { + var result = _windowMode.hashCode() + result = 31 * result + _frameWidth.hashCode() + result = 31 * result + _frameHeight.hashCode() + return result + } + + override fun toString(): String = + "WindowStatus(" + + "windowMode=$windowMode, " + + "frameWidth=$frameWidth, " + + "frameHeight=$frameHeight" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/BugReport.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/BugReport.kt new file mode 100644 index 000000000..e2a97ac6f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/BugReport.kt @@ -0,0 +1,66 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Bug report packets are sent when players submit a bug report + * using the bug report interface. + * @property type the type of the report. The only known value of this is 0. + * @property description the description of the bug, how it happened etc. + * The maximum length of this form is 500 characters, as the client prevents + * sending anything beyond that. + * @property instructions instructions on how to reproduce the bug. + * The maximum length of this form is also 500 characters, as the client + * prevents sending anything beyond that. + * The decoder will throw an exception if the length of the message exceeds + * the 500 length constraint, so no validation needs to be done on the user's end. + */ +public class BugReport private constructor( + private val _type: UByte, + public val description: String, + public val instructions: String, +) : IncomingGameMessage { + public constructor( + type: Int, + description: String, + instructions: String, + ) : this( + type.toUByte(), + description, + instructions, + ) + + public val type: Int + get() = _type.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BugReport + + if (_type != other._type) return false + if (description != other.description) return false + if (instructions != other.instructions) return false + + return true + } + + override fun hashCode(): Int { + var result = _type.hashCode() + result = 31 * result + description.hashCode() + result = 31 * result + instructions.hashCode() + return result + } + + override fun toString(): String = + "BugReport(" + + "description='$description', " + + "instructions='$instructions', " + + "type=$type" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/ClickWorldMap.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/ClickWorldMap.kt new file mode 100644 index 000000000..3447f58d0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/ClickWorldMap.kt @@ -0,0 +1,55 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Click world map events are transmitted when the user double-clicks + * on the world map. If the user has J-Mod privileges and holds the + * 'Control' and 'Shift' keys down as they do the click, a different + * packet is transmitted instead. + * This packet is intended for a feature that never released - world + * map hints. In the pre-eoc days, players could double-click on their + * world map to set a 'Destination marker' which had a blue arrow to it, + * allowing them easier navigation to the given destination. + * In OldSchool RuneScape, there is a RuneLite plugin that accomplishes + * the same thing. Additionally, the double-clicking is fairly broken + * in the C++ client, and only sends this packet in some extreme cases + * when dragging the world map around, not through the traditional + * double-clicking. + * @property x the absolute x coordinate to set the destination to + * @property z the absolute z coordinate to set the destination to + * @property level the level to set the destination to + */ +public class ClickWorldMap( + private val coordGrid: CoordGrid, +) : IncomingGameMessage { + public val x: Int + get() = coordGrid.x + public val z: Int + get() = coordGrid.z + public val level: Int + get() = coordGrid.level + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ClickWorldMap + + return coordGrid == other.coordGrid + } + + override fun hashCode(): Int = coordGrid.hashCode() + + override fun toString(): String = + "ClickWorldMap(" + + "x=$x, " + + "z=$z, " + + "level=$level" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/ClientCheat.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/ClientCheat.kt new file mode 100644 index 000000000..b8f891591 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/ClientCheat.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Client cheats are commands sent in chat using the :: prefix, + * or through the console on the C++ client. + */ +public class ClientCheat( + public val command: String, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ClientCheat + + return command == other.command + } + + override fun hashCode(): Int = command.hashCode() + + override fun toString(): String = "ClientCheat(command='$command')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/CloseModal.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/CloseModal.kt new file mode 100644 index 000000000..90a3ec906 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/CloseModal.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Close modal messages are sent when the player clicks on the 'x' button + * of a modal interface, or if they press the 'Esc' key while having the + * "Esc to close interfaces" setting enabled. + */ +public data object CloseModal : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/HiscoreRequest.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/HiscoreRequest.kt new file mode 100644 index 000000000..dff67c03b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/HiscoreRequest.kt @@ -0,0 +1,64 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * A hiscore request message is sent when a player does a lookup of another + * player on the C++ clients. This functionality is currently not used in any way. + * @property type the type of the request (main, ironman, group ironman etc) + * The exact values are not yet known. + * @property requestId the id of the request + * @property name the name of the player whom to look up + */ +@Suppress("MemberVisibilityCanBePrivate") +public class HiscoreRequest( + private val _type: UByte, + private val _requestId: UByte, + public val name: String, +) : IncomingGameMessage { + public constructor( + type: Int, + requestId: Int, + name: String, + ) : this( + type.toUByte(), + requestId.toUByte(), + name, + ) + + public val type: Int + get() = _type.toInt() + public val requestId: Int + get() = _requestId.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HiscoreRequest + + if (_type != other._type) return false + if (_requestId != other._requestId) return false + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + var result = _type.hashCode() + result = 31 * result + _requestId.hashCode() + result = 31 * result + name.hashCode() + return result + } + + override fun toString(): String = + "HiscoreRequest(" + + "name='$name', " + + "type=$type, " + + "requestId=$requestId" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/IfCrmViewClick.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/IfCrmViewClick.kt new file mode 100644 index 000000000..551605220 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/IfCrmViewClick.kt @@ -0,0 +1,96 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * Content recommendation interface clicks happen when a player + * clicks a component on the CRM interface, which is currently only used + * in the form of the lobby interface, where user-specific advertisements + * are shown. + * Worth noting that the properties here are rough guesses at their naming + * and the real usage has not been tested in-game. + * @property crmServerTarget the server target, an integer + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id clicked on + * @property componentId the component id clicked on + * @property sub the subcomponent clicked on, or -1 if none exists + * @property behaviour1 the first CRM behaviour, an integer + * @property behaviour2 the second CRM behaviour, an integer + * @property behaviour3 the third CRM behaviour, an integer + */ +public class IfCrmViewClick private constructor( + public val crmServerTarget: Int, + private val _combinedId: CombinedId, + private val _sub: UShort, + public val behaviour1: Int, + public val behaviour2: Int, + public val behaviour3: Int, +) : IncomingGameMessage { + public constructor( + crmServerTarget: Int, + combinedId: CombinedId, + sub: Int, + behaviour1: Int, + behaviour2: Int, + behaviour3: Int, + ) : this( + crmServerTarget, + combinedId, + sub.toUShort(), + behaviour1, + behaviour2, + behaviour3, + ) + + public val combinedId: Int + get() = _combinedId.combinedId + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val sub: Int + get() = _sub.toIntOrMinusOne() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfCrmViewClick + + if (crmServerTarget != other.crmServerTarget) return false + if (_combinedId != other._combinedId) return false + if (_sub != other._sub) return false + if (behaviour1 != other.behaviour1) return false + if (behaviour2 != other.behaviour2) return false + if (behaviour3 != other.behaviour3) return false + + return true + } + + override fun hashCode(): Int { + var result = crmServerTarget + result = 31 * result + _combinedId.hashCode() + result = 31 * result + _sub.hashCode() + result = 31 * result + behaviour1 + result = 31 * result + behaviour2 + result = 31 * result + behaviour3 + return result + } + + override fun toString(): String = + "IfCrmViewClick(" + + "crmServerTarget=$crmServerTarget, " + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "sub=$sub, " + + "behaviour1=$behaviour1, " + + "behaviour2=$behaviour2, " + + "behaviour3=$behaviour3" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/MoveGameClick.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/MoveGameClick.kt new file mode 100644 index 000000000..92e4d33a7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/MoveGameClick.kt @@ -0,0 +1,67 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.game.incoming.misc.user.internal.MovementRequest +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Move gameclick packets are sent when the user clicks to walk within their + * main game window (not minimap). + * @property x the absolute x coordinate to walk to + * @property z the absolute z coordinate to walk to + * @property keyCombination the combination of keys held down to move there. + * Possible values include 0, 1 and 2, where: + * A value of 2 is sent if the user is holding down the 'Control' and 'Shift' keys + * simultaneously. + * A value of 1 is sent if the user is holding down the 'Control' key without + * the 'Shift' key. + * In any other scenario, a value of 0 is sent. + * The 'Control' key is used to invert move speed for the single movement request, + * and the 'Control' + 'Shift' combination is presumably for J-Mods to teleport + * around - although there are no validations for J-Mod privileges in the client, + * it will send the value of 2 even for regular users. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class MoveGameClick private constructor( + private val movementRequest: MovementRequest, +) : IncomingGameMessage { + public constructor( + x: Int, + z: Int, + keyCombination: Int, + ) : this( + MovementRequest( + x, + z, + keyCombination, + ), + ) + + public val x: Int + get() = movementRequest.x + public val z: Int + get() = movementRequest.z + public val keyCombination: Int + get() = movementRequest.keyCombination + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MoveGameClick + + return movementRequest == other.movementRequest + } + + override fun hashCode(): Int = movementRequest.hashCode() + + override fun toString(): String = + "MoveGameClick(" + + "x=$x, " + + "z=$z, " + + "keyCombination=$keyCombination" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/MoveMinimapClick.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/MoveMinimapClick.kt new file mode 100644 index 000000000..dec9e9566 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/MoveMinimapClick.kt @@ -0,0 +1,172 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.game.incoming.misc.user.internal.MovementRequest +import net.rsprot.protocol.message.IncomingGameMessage +import kotlin.math.cos +import kotlin.math.sin + +/** + * Move minimap click is sent when the player requests to walk somewhere + * through their minimap. + * While the packet itself sends additional constant values to the server, + * we do not store those values as they are expected to always be the same. + * The decoder will verify the values and throw an exception in decoding + * if those values do not align up. + * @property x the absolute x coordinate the player is walking to + * @property z the absolute z coordinate the player is walking to + * @property keyCombination the combination of keys held down to move there. + * Possible values include 0, 1 and 2, where: + * A value of 2 is sent if the user is holding down the 'Control' and 'Shift' keys + * simultaneously. + * A value of 1 is sent if the user is holding down the 'Control' key without + * the 'Shift' key. + * In any other scenario, a value of 0 is sent. + * The 'Control' key is used to invert move speed for the single movement request, + * and the 'Control' + 'Shift' combination is presumably for J-Mods to teleport + * around - although there are no validations for J-Mod privileges in the client, + * it will send the value of 2 even for regular users. + * @property minimapWidth the width of the minimap component in pixels + * @property minimapHeight the height of the minimap component in pixels + * @property cameraAngleY the angle of the camera + * @property fineX the fine x coordinate of the local player + * @property fineZ the fine z coordinate of the local player + */ +@Suppress("DuplicatedCode", "MemberVisibilityCanBePrivate") +public class MoveMinimapClick private constructor( + private val movementRequest: MovementRequest, + private val _minimapWidth: UByte, + private val _minimapHeight: UByte, + private val _cameraAngleY: UShort, + private val _fineX: UShort, + private val _fineZ: UShort, +) : IncomingGameMessage { + public constructor( + x: Int, + z: Int, + keyCombination: Int, + minimapWidth: Int, + minimapHeight: Int, + cameraAngleY: Int, + fineX: Int, + fineZ: Int, + ) : this( + MovementRequest( + x, + z, + keyCombination, + ), + minimapWidth.toUByte(), + minimapHeight.toUByte(), + cameraAngleY.toUShort(), + fineX.toUShort(), + fineZ.toUShort(), + ) + + public val x: Int + get() = movementRequest.x + public val z: Int + get() = movementRequest.z + public val keyCombination: Int + get() = movementRequest.keyCombination + public val minimapWidth: Int + get() = _minimapWidth.toInt() + public val minimapHeight: Int + get() = _minimapHeight.toInt() + public val cameraAngleY: Int + get() = _cameraAngleY.toInt() + public val fineX: Int + get() = _fineX.toInt() + public val fineZ: Int + get() = _fineZ.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + /** + * Checks if the provided arguments are valid (as in produce the same + * end coordinates if re-calculated). + * It is worth noting that the C++ client has zoom functionality for + * which the server does not get information, so it is not possible + * to verify this on the C++ clients. Additionally, clients such as + * RuneLite have their own built-in zoom and also do not correct the + * packet itself. For these reasons, the verification is not done by + * the library, but it could provide a useful bit of information for + * complete vanilla builds. + * @param baseZoneX the south-western zone x coordinate of the build area + * @param baseZoneZ the south-western zone z coordinate of the build area + * The base zone coordinates are relative to the build-area that the client + * builds around. If the player logs in at absolute coordinates 3200, 3220, + * their baseZoneX would be ((3200 - (6 * 8)) / 8), and the baseZoneZ + * would be ((3220 - (6 * 8)) / 8), resulting in base zone coordinates of + * 394, 396. The (6 * 8) is the normal subtraction to go from the center + * of the build-area to the south-western corner, as a value of 48 is + * subtracted in the case of a size 104 build-area. + */ + public fun isValid( + baseZoneX: Int, + baseZoneZ: Int, + ): Boolean { + val minimapAngle = cameraAngleY and 0x7FF + val sine = sine[minimapAngle] + val cosine = cosine[minimapAngle] + val minimapX = ((cosine * minimapWidth) + (sine * minimapHeight)) shr 11 + val minimapY = ((cosine * minimapHeight) - (sine * minimapWidth)) shr 11 + val localX = (minimapX + fineX) shr 7 + val localY = (fineZ - minimapY) shr 7 + val calculatedDestX = (baseZoneX shl 3) + localX + val calculatedDestZ = (baseZoneZ shl 3) + localY + return calculatedDestX == x && calculatedDestZ == z + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MoveMinimapClick + + if (movementRequest != other.movementRequest) return false + if (_minimapWidth != other._minimapWidth) return false + if (_minimapHeight != other._minimapHeight) return false + if (_cameraAngleY != other._cameraAngleY) return false + if (_fineX != other._fineX) return false + if (_fineZ != other._fineZ) return false + + return true + } + + override fun hashCode(): Int { + var result = movementRequest.hashCode() + result = 31 * result + _minimapWidth.hashCode() + result = 31 * result + _minimapHeight.hashCode() + result = 31 * result + _cameraAngleY.hashCode() + result = 31 * result + _fineX.hashCode() + result = 31 * result + _fineZ.hashCode() + return result + } + + override fun toString(): String = + "MoveMinimapClick(" + + "x=$x, " + + "z=$z, " + + "keyCombination=$keyCombination, " + + "width=$minimapWidth, " + + "height=$minimapHeight, " + + "cameraAngleY=$cameraAngleY, " + + "fineX=$fineX, " + + "fineZ=$fineZ" + + ")" + + private companion object { + private const val MAX_ANGLE = 65536.0 + private const val CONSTANT = 0.0030679615 + private val sine: IntArray = + IntArray(2048) { + (MAX_ANGLE * sin(it * CONSTANT)).toInt() + } + private val cosine: IntArray = + IntArray(2048) { + (MAX_ANGLE * cos(it * CONSTANT)).toInt() + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/OculusLeave.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/OculusLeave.kt new file mode 100644 index 000000000..79f517656 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/OculusLeave.kt @@ -0,0 +1,14 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Oculus leave message is sent when the player presses the 'Esc' key + * to exit the orb of oculus view. + */ +public data object OculusLeave : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/SendSnapshot.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/SendSnapshot.kt new file mode 100644 index 000000000..179dd4804 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/SendSnapshot.kt @@ -0,0 +1,92 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Send snapshot message is sent when a player reports another player. + * + * Rules table: + * ``` + * | Id | Rule | + * |----|:-------------------------------------------:| + * | 3 | Exploiting a bug | + * | 4 | Staff impersonation | + * | 5 | Buying/selling accounts and services | + * | 6 | Macroing or use of bots | + * | 7 | Boxing in the Deadman Tournament | + * | 8 | Encouraging rule breaking | + * | 10 | Advertising websites | + * | 11 | Muling in the Deadman Tournament | + * | 12 | Asking for or providing contact information | + * | 14 | Scamming | + * | 15 | Seriously offensive language | + * | 16 | Solicitation | + * | 17 | Disruptive behaviour | + * | 18 | Offensive account name | + * | 19 | Real-life threats | + * | 20 | Breaking real-world laws | + * | 21 | Player-run Games of Chance | + * ``` + * + * @property name the name of the player that is being reported + * @property ruleId the rule that the player broke (see table above). + * Note that the rule ids are internal and not what one sees on the interface, + * as the rule ids must be persistent across years of usage. Additionally, + * the "Boxing in Deadman Tournament" and "Muling in the Deadman Tournament" + * rules can only be selected if the player is logged into a Deadman world. + * Additionally worth noting that the rule ids are 1 less than what is shown + * in clientscripts, as the clientscript command behind sending the snapshot + * decrements 1 from the value prior to submitting it to the server. + * @property mute whether to mute the player. This option is only possible + * by Player and Jagex moderators. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class SendSnapshot private constructor( + public val name: String, + private val _ruleId: UByte, + public val mute: Boolean, +) : IncomingGameMessage { + public constructor( + name: String, + ruleId: Int, + mute: Boolean, + ) : this( + name, + ruleId.toUByte(), + mute, + ) + + public val ruleId: Int + get() = _ruleId.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SendSnapshot + + if (name != other.name) return false + if (_ruleId != other._ruleId) return false + if (mute != other.mute) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _ruleId.hashCode() + result = 31 * result + mute.hashCode() + return result + } + + override fun toString(): String = + "SendSnapshot(" + + "name='$name', " + + "ruleId=$ruleId, " + + "mute=$mute" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/SetChatFilterSettings.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/SetChatFilterSettings.kt new file mode 100644 index 000000000..4b780ad6f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/SetChatFilterSettings.kt @@ -0,0 +1,80 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Set chat filter settings is sent when the player changes either their + * public, private or trade filters, in order to synchronize the status + * with the server. + * + * Chat filters table: + * ``` + * | Id | Type | + * |----|:--------:| + * | 0 | On | + * | 1 | Friends | + * | 2 | Off | + * | 3 | Hide | + * | 4 | Autochat | + * ``` + * + * @property publicChatFilter the public chat filter status, any value in the above table + * @property privateChatFilter the private chat filter status, allowed values include + * 'On', 'Friends' and 'Off' (see table above) + * @property tradeChatFilter the trade chat filter status, allowed values include + * 'On', 'Friends' and 'Off' (see table above) + */ +@Suppress("MemberVisibilityCanBePrivate") +public class SetChatFilterSettings private constructor( + private val _publicChatFilter: UByte, + private val _privateChatFilter: UByte, + private val _tradeChatFilter: UByte, +) : IncomingGameMessage { + public constructor( + publicChatFilter: Int, + privateChatFilter: Int, + tradeChatFilter: Int, + ) : this( + publicChatFilter.toUByte(), + privateChatFilter.toUByte(), + tradeChatFilter.toUByte(), + ) + + public val publicChatFilter: Int + get() = _publicChatFilter.toInt() + public val privateChatFilter: Int + get() = _privateChatFilter.toInt() + public val tradeChatFilter: Int + get() = _tradeChatFilter.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetChatFilterSettings + + if (_publicChatFilter != other._publicChatFilter) return false + if (_privateChatFilter != other._privateChatFilter) return false + if (_tradeChatFilter != other._tradeChatFilter) return false + + return true + } + + override fun hashCode(): Int { + var result = _publicChatFilter.hashCode() + result = 31 * result + _privateChatFilter.hashCode() + result = 31 * result + _tradeChatFilter.hashCode() + return result + } + + override fun toString(): String = + "SetChatFilterSettings(" + + "publicChatFilter=$publicChatFilter, " + + "privateChatFilter=$privateChatFilter, " + + "tradeChatFilter=$tradeChatFilter" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/Teleport.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/Teleport.kt new file mode 100644 index 000000000..ddc1070d1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/Teleport.kt @@ -0,0 +1,82 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Teleport packets are sent in multiple possible scenarios: + * 1. The player is a J-Mod and has the 'Control' and 'Shift' keys held down + * while scrolling with their mouse wheel - the player will be teleported + * up or down a level. + * 2. The player is a J-Mod using an Orb of Oculus - the teleport packet + * will be sent repeatedly every 50 client cycles (20ms/cc) while the player's + * coordinate doesn't align up with the oculus camera center coordinate. + * 3. The player is a J-Mod and has the 'Control' and 'Shift' keys held down + * while clicking on the world map - the player will teleport to the coordinate + * they clicked on in the world map. + * @property oculusSyncValue if the player is in orb of oculus (scenario 2 above), + * this value is equal to the last value the server transmitted with the + * [net.rsprot.protocol.game.outgoing.prot.GameServerProt.OCULUS_SYNC] packet, + * or 0 if the packet was never transmitted/player is not using orb of oculus. + * @property x the absolute x coordinate to teleport to + * @property z the absolute z coordinate to teleport to + * @property level the height level to teleport to + */ +public class Teleport private constructor( + public val oculusSyncValue: Int, + private val _x: UShort, + private val _z: UShort, + private val _level: UByte, +) : IncomingGameMessage { + public constructor( + oculusSyncValue: Int, + x: Int, + z: Int, + level: Int, + ) : this( + oculusSyncValue, + x.toUShort(), + z.toUShort(), + level.toUByte(), + ) + + public val x: Int + get() = _x.toInt() + public val z: Int + get() = _z.toInt() + public val level: Int + get() = _level.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Teleport + + if (oculusSyncValue != other.oculusSyncValue) return false + if (_x != other._x) return false + if (_z != other._z) return false + if (_level != other._level) return false + + return true + } + + override fun hashCode(): Int { + var result = oculusSyncValue + result = 31 * result + _x.hashCode() + result = 31 * result + _z.hashCode() + result = 31 * result + _level.hashCode() + return result + } + + override fun toString(): String = + "Teleport(" + + "oculusSyncValue=$oculusSyncValue, " + + "x=$x, " + + "z=$z, " + + "level=$level" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/UpdatePlayerModelOld.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/UpdatePlayerModelOld.kt new file mode 100644 index 000000000..dc92b2f02 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/UpdatePlayerModelOld.kt @@ -0,0 +1,102 @@ +package net.rsprot.protocol.game.incoming.misc.user + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import kotlin.jvm.Throws + +/** + * Update player model packet is sent for the old make-over interface, + * when the player finishes designing their character. It should be noted, + * that this is no longer in use in OldSchool RuneScape, as a newer interface + * uses traditional buttons to manage it. However, as this is still a valid + * packet that can be sent by the server, we've implemented it. + * @property bodyType the body type of the player + * @property identKits the ident kits the player can customize + * @property colours the colours the player can customize + */ +@Suppress("MemberVisibilityCanBePrivate") +public class UpdatePlayerModelOld private constructor( + private val _bodyType: UByte, + private val identKits: ByteArray, + private val colours: ByteArray, +) : IncomingGameMessage { + public constructor( + bodyType: Int, + identKits: ByteArray, + colours: ByteArray, + ) : this( + bodyType.toUByte(), + identKits, + colours, + ) + + public val bodyType: Int + get() = _bodyType.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + /** + * Gets the backing ident kits byte array. + * Changes done to this byte array reflect on this packet. + */ + public fun getIdentKitsByteArray(): ByteArray = identKits + + /** + * Gets the backing colours byte array. + * Changes done to this byte array reflect on the packet. + */ + public fun getColoursByteArray(): ByteArray = colours + + /** + * Gets the ident kit at index [index], or -1 if it doesn't exist. + * @param index the index of the body part + * @return ident kit at that body part, or -1 if it doesn't exist + * @throws ArrayIndexOutOfBoundsException if the index is below 0, or >= 7 + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public fun getIdentKit(index: Int): Int { + val value = identKits[index].toInt() + return if (value == 0xFF) { + -1 + } else { + value + } + } + + /** + * Gets the colour at index [index], or -1 if it doesn't exist. + * @param index the index of the colour + * @return colour at that index, or -1 if it doesn't exist + * @throws ArrayIndexOutOfBoundsException if the index is below 0, or >= 5 + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public fun getColour(index: Int): Int = colours[index].toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdatePlayerModelOld + + if (_bodyType != other._bodyType) return false + if (!identKits.contentEquals(other.identKits)) return false + if (!colours.contentEquals(other.colours)) return false + + return true + } + + override fun hashCode(): Int { + var result = _bodyType.hashCode() + result = 31 * result + identKits.contentHashCode() + result = 31 * result + colours.contentHashCode() + return result + } + + override fun toString(): String = + "UpdatePlayerModelOld(" + + "bodyType=$bodyType, " + + "identKits=${identKits.contentToString()}, " + + "colours=${colours.contentToString()}" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/internal/MovementRequest.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/internal/MovementRequest.kt new file mode 100644 index 000000000..b70b28283 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/misc/user/internal/MovementRequest.kt @@ -0,0 +1,34 @@ +package net.rsprot.protocol.game.incoming.misc.user.internal + +/** + * A value class around an int to bitpack all the properties of move gameclick + * into a single integer. This is primarily to constrain our class to a payload + * of 4 bytes, as going above it means being subject to increased memory alignment. + * As mentioned in documentation before, empty objects allocate 12 bytes, but + * get aligned to a multiple of 8 bytes - so they will consume 16 bytes. + * Putting an int inside the class would allocate 16 bytes, and remain as 16 + * after padding. Allocating 17 bytes (ref: 12, x: 2, y: 2, key: 1), + * however, would mean the class is subject to being padded to 24 bytes, with 7 of + * them being completely wasted in the process. + */ +@JvmInline +internal value class MovementRequest private constructor( + private val packed: Int, +) { + internal constructor( + x: Int, + z: Int, + keyCombination: Int, + ) : this( + (z and 0x3FFF) + .or(x and 0x3FFF shl 14) + .or(keyCombination and 0x3 shl 28), + ) + + val x: Int + get() = packed ushr 14 and 0x3FFF + val z: Int + get() = packed and 0x3FFF + val keyCombination: Int + get() = packed ushr 28 and 0x3 +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpc.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpc.kt new file mode 100644 index 000000000..a6bb11d61 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpc.kt @@ -0,0 +1,64 @@ +package net.rsprot.protocol.game.incoming.npcs + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * OpNpc messages are sent when a player clicks one of the five primary options on a NPC. + * It should be noted that this message will not handle 'OPNPC6', as that message requires + * different arguments. + * @property index the index of the npc that was clicked + * @property controlKey whether the control key was held down, used to invert movement speed + * @property op the option clicked, ranging from 1 to 5(inclusive). + */ +@Suppress("MemberVisibilityCanBePrivate") +public class OpNpc private constructor( + private val _index: UShort, + public val controlKey: Boolean, + private val _op: UByte, +) : IncomingGameMessage { + public constructor( + index: Int, + controlKey: Boolean, + op: Int, + ) : this( + index.toUShort(), + controlKey, + op.toUByte(), + ) + + public val index: Int + get() = _index.toInt() + public val op: Int + get() = _op.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpNpc + + if (_index != other._index) return false + if (controlKey != other.controlKey) return false + if (_op != other._op) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + controlKey.hashCode() + result = 31 * result + _op.hashCode() + return result + } + + override fun toString(): String = + "OpNpc(" + + "index=$index, " + + "controlKey=$controlKey, " + + "op=$op" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpc6.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpc6.kt new file mode 100644 index 000000000..0e9c32eda --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpc6.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.incoming.npcs + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * OpNpc6 message is fired when a player clicks the 'Examine' option on a npc. + * @property id the config id of the npc clicked + */ +public class OpNpc6( + public val id: Int, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpNpc6 + + return id == other.id + } + + override fun hashCode(): Int = id + + override fun toString(): String = "OpNpc6(id=$id)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpcT.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpcT.kt new file mode 100644 index 000000000..2eca06796 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/npcs/OpNpcT.kt @@ -0,0 +1,91 @@ +package net.rsprot.protocol.game.incoming.npcs + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * OpNpcT messages are fired whenever an interface component is targeted + * on a NPC, which, as of revision 204, includes using items from + * the player's inventory on NPCs - the OpNpcU message was deprecated. + * @property index the index of the npc the component was used on + * @property controlKey whether the control key was held down, used to invert movement speed + * @property selectedCombinedId the bitpacked combination of [selectedInterfaceId] and [selectedComponentId]. + * @property selectedInterfaceId the interface id of the selected component + * @property selectedComponentId the component id being used on the npc + * @property selectedSub the subcomponent of the selected component, or -1 of none exists + * @property selectedObj the obj on the selected subcomponent, or -1 if none exists + */ +@Suppress("DuplicatedCode", "MemberVisibilityCanBePrivate") +public class OpNpcT private constructor( + private val _index: UShort, + public val controlKey: Boolean, + private val _selectedCombinedId: CombinedId, + private val _selectedSub: UShort, + private val _selectedObj: UShort, +) : IncomingGameMessage { + public constructor( + index: Int, + controlKey: Boolean, + selectedCombinedId: CombinedId, + selectedSub: Int, + selectedObj: Int, + ) : this( + index.toUShort(), + controlKey, + selectedCombinedId, + selectedSub.toUShort(), + selectedObj.toUShort(), + ) + + public val index: Int + get() = _index.toInt() + public val selectedCombinedId: Int + get() = _selectedCombinedId.combinedId + public val selectedInterfaceId: Int + get() = _selectedCombinedId.interfaceId + public val selectedComponentId: Int + get() = _selectedCombinedId.componentId + public val selectedSub: Int + get() = _selectedSub.toIntOrMinusOne() + public val selectedObj: Int + get() = _selectedObj.toIntOrMinusOne() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpNpcT + + if (_index != other._index) return false + if (controlKey != other.controlKey) return false + if (_selectedCombinedId != other._selectedCombinedId) return false + if (_selectedSub != other._selectedSub) return false + if (_selectedObj != other._selectedObj) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + controlKey.hashCode() + result = 31 * result + _selectedCombinedId.hashCode() + result = 31 * result + _selectedSub.hashCode() + result = 31 * result + _selectedObj.hashCode() + return result + } + + override fun toString(): String = + "OpNpcT(" + + "index=$index, " + + "controlKey=$controlKey, " + + "selectedInterfaceId=$selectedInterfaceId, " + + "selectedComponentId=$selectedComponentId, " + + "selectedSub=$selectedSub, " + + "selectedObj=$selectedObj" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObj.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObj.kt new file mode 100644 index 000000000..a6d8b286e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObj.kt @@ -0,0 +1,81 @@ +package net.rsprot.protocol.game.incoming.objs + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * OpObj messages are fired when the player interacts with an obj on the ground, + * e.g. picking an obj up off the ground. This does not include examining objs. + * @property id the id of the obj interacted with + * @property x the absolute x coordinate of the obj on the ground + * @property z the absolute z coordinate of the obj on the ground + * @property controlKey whether the control key was held down, used to invert movement speed + * @property op the option clicked, ranging from 1 to 5 (inclusive) + */ +@Suppress("DuplicatedCode", "MemberVisibilityCanBePrivate") +public class OpObj private constructor( + private val _id: UShort, + private val _x: UShort, + private val _z: UShort, + public val controlKey: Boolean, + private val _op: UByte, +) : IncomingGameMessage { + public constructor( + id: Int, + x: Int, + z: Int, + controlKey: Boolean, + op: Int, + ) : this( + id.toUShort(), + x.toUShort(), + z.toUShort(), + controlKey, + op.toUByte(), + ) + + public val id: Int + get() = _id.toInt() + public val x: Int + get() = _x.toInt() + public val z: Int + get() = _z.toInt() + public val op: Int + get() = _op.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpObj + + if (_id != other._id) return false + if (_x != other._x) return false + if (_z != other._z) return false + if (controlKey != other.controlKey) return false + if (_op != other._op) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _x.hashCode() + result = 31 * result + _z.hashCode() + result = 31 * result + controlKey.hashCode() + result = 31 * result + _op.hashCode() + return result + } + + override fun toString(): String = + "OpObj(" + + "id=$id, " + + "x=$x, " + + "z=$z, " + + "controlKey=$controlKey, " + + "op=$op" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObj6.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObj6.kt new file mode 100644 index 000000000..47d6ad3d5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObj6.kt @@ -0,0 +1,63 @@ +package net.rsprot.protocol.game.incoming.objs + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * OpObj6 messages are fired whenever a player examines an obj on the ground. + * @property id the id of the obj examined + * @property x the absolute x coordinate of the obj on the ground + * @property z the absolute z coordinate of the obj on the ground + */ +public class OpObj6 private constructor( + private val _id: UShort, + private val _x: UShort, + private val _z: UShort, +) : IncomingGameMessage { + public constructor( + id: Int, + x: Int, + z: Int, + ) : this( + id.toUShort(), + x.toUShort(), + z.toUShort(), + ) + + public val id: Int + get() = _id.toInt() + public val x: Int + get() = _x.toInt() + public val z: Int + get() = _z.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpObj6 + + if (_id != other._id) return false + if (_x != other._x) return false + if (_z != other._z) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _x.hashCode() + result = 31 * result + _z.hashCode() + return result + } + + override fun toString(): String = + "OpObj6(" + + "id=$id, " + + "x=$x, " + + "z=$z" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObjT.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObjT.kt new file mode 100644 index 000000000..61ac1ae60 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/objs/OpObjT.kt @@ -0,0 +1,109 @@ +package net.rsprot.protocol.game.incoming.objs + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * OpObjT messages are fired whenever an interface component is targeted + * on an obj on the ground, which, as of revision 204, includes using items from + * the player's inventory on objs - the OpObjU message was deprecated. + * @property id the id of the obj the component was used on + * @property x the absolute x coordinate of the obj + * @property z the absolute z coordinate of the obj + * @property controlKey whether the control key was held down, used to invert movement speed + * @property selectedCombinedId the bitpacked combination of [selectedInterfaceId] and [selectedComponentId]. + * @property selectedInterfaceId the interface id of the selected component + * @property selectedComponentId the component id being used on the obj + * @property selectedSub the subcomponent of the selected component, or -1 of none exists + * @property selectedObj the obj on the selected subcomponent, or -1 if none exists + */ +@Suppress("DuplicatedCode", "MemberVisibilityCanBePrivate") +public class OpObjT private constructor( + private val _id: UShort, + private val _x: UShort, + private val _z: UShort, + public val controlKey: Boolean, + private val _selectedCombinedId: CombinedId, + private val _selectedSub: UShort, + private val _selectedObj: UShort, +) : IncomingGameMessage { + public constructor( + id: Int, + x: Int, + z: Int, + controlKey: Boolean, + selectedCombinedId: CombinedId, + selectedSub: Int, + selectedObj: Int, + ) : this( + id.toUShort(), + x.toUShort(), + z.toUShort(), + controlKey, + selectedCombinedId, + selectedSub.toUShort(), + selectedObj.toUShort(), + ) + + public val id: Int + get() = _id.toInt() + public val x: Int + get() = _x.toInt() + public val z: Int + get() = _z.toInt() + public val selectedCombinedId: Int + get() = _selectedCombinedId.combinedId + public val selectedInterfaceId: Int + get() = _selectedCombinedId.interfaceId + public val selectedComponentId: Int + get() = _selectedCombinedId.componentId + public val selectedSub: Int + get() = _selectedSub.toIntOrMinusOne() + public val selectedObj: Int + get() = _selectedObj.toIntOrMinusOne() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpObjT + + if (_id != other._id) return false + if (_x != other._x) return false + if (_z != other._z) return false + if (controlKey != other.controlKey) return false + if (_selectedCombinedId != other._selectedCombinedId) return false + if (_selectedSub != other._selectedSub) return false + if (_selectedObj != other._selectedObj) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _x.hashCode() + result = 31 * result + _z.hashCode() + result = 31 * result + controlKey.hashCode() + result = 31 * result + _selectedCombinedId.hashCode() + result = 31 * result + _selectedSub.hashCode() + result = 31 * result + _selectedObj.hashCode() + return result + } + + override fun toString(): String = + "OpObjT(" + + "id=$id, " + + "x=$x, " + + "z=$z, " + + "controlKey=$controlKey, " + + "selectedInterfaceId=$selectedInterfaceId, " + + "selectedComponentId=$selectedComponentId, " + + "selectedSub=$selectedSub, " + + "selectedObj=$selectedObj" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/players/OpPlayer.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/players/OpPlayer.kt new file mode 100644 index 000000000..9584e3b1c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/players/OpPlayer.kt @@ -0,0 +1,64 @@ +package net.rsprot.protocol.game.incoming.players + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Opplayer messages are fired whenever a player clicks an option on another player, + * or if messages such as "* wishes to trade with you." are clicked. + * In the case of latter, only ops 1, 4, 6 and 7 will fire the packet. + * @property index the index of the player who was interacted with + * @property controlKey whether the control key was held down, used to invert movement speed + * @property op the option clicked, ranging from 1 to 8 (inclusive) + */ +@Suppress("MemberVisibilityCanBePrivate") +public class OpPlayer private constructor( + private val _index: UShort, + public val controlKey: Boolean, + private val _op: UByte, +) : IncomingGameMessage { + public constructor( + index: Int, + controlKey: Boolean, + op: Int, + ) : this( + index.toUShort(), + controlKey, + op.toUByte(), + ) + + public val index: Int + get() = _index.toInt() + public val op: Int + get() = _op.toInt() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpPlayer + + if (_index != other._index) return false + if (controlKey != other.controlKey) return false + if (_op != other._op) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + controlKey.hashCode() + result = 31 * result + _op.hashCode() + return result + } + + override fun toString(): String = + "OpPlayer(" + + "index=$index, " + + "controlKey=$controlKey, " + + "op=$op" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/players/OpPlayerT.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/players/OpPlayerT.kt new file mode 100644 index 000000000..b388b5952 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/players/OpPlayerT.kt @@ -0,0 +1,91 @@ +package net.rsprot.protocol.game.incoming.players + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * OpPlayerT messages are fired whenever an interface component is targeted + * on another player, which, as of revision 204, includes using items from + * the player's inventory on players - the OpPlayerU message was deprecated. + * @property index the index of the player clicked on + * @property controlKey whether the control key was held down, used to invert movement speed + * @property selectedCombinedId the bitpacked combination of [selectedInterfaceId] and [selectedComponentId]. + * @property selectedInterfaceId the interface id of the selected component + * @property selectedComponentId the component id being used on the player + * @property selectedSub the subcomponent of the selected component, or -1 of none exists + * @property selectedObj the obj on the selected subcomponent, or -1 if none exists + */ +@Suppress("MemberVisibilityCanBePrivate", "DuplicatedCode") +public class OpPlayerT private constructor( + private val _index: UShort, + public val controlKey: Boolean, + private val _selectedCombinedId: CombinedId, + private val _selectedSub: UShort, + private val _selectedObj: UShort, +) : IncomingGameMessage { + public constructor( + index: Int, + controlKey: Boolean, + selectedCombinedId: CombinedId, + selectedSub: Int, + selectedObj: Int, + ) : this( + index.toUShort(), + controlKey, + selectedCombinedId, + selectedSub.toUShort(), + selectedObj.toUShort(), + ) + + public val index: Int + get() = _index.toInt() + public val selectedCombinedId: Int + get() = _selectedCombinedId.combinedId + public val selectedInterfaceId: Int + get() = _selectedCombinedId.interfaceId + public val selectedComponentId: Int + get() = _selectedCombinedId.componentId + public val selectedSub: Int + get() = _selectedSub.toIntOrMinusOne() + public val selectedObj: Int + get() = _selectedObj.toIntOrMinusOne() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OpPlayerT + + if (_index != other._index) return false + if (controlKey != other.controlKey) return false + if (_selectedCombinedId != other._selectedCombinedId) return false + if (_selectedSub != other._selectedSub) return false + if (_selectedObj != other._selectedObj) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + controlKey.hashCode() + result = 31 * result + _selectedCombinedId.hashCode() + result = 31 * result + _selectedSub.hashCode() + result = 31 * result + _selectedObj.hashCode() + return result + } + + override fun toString(): String = + "OpPlayerT(" + + "index=$index, " + + "controlKey=$controlKey, " + + "selectedInterfaceId=$selectedInterfaceId, " + + "selectedComponentId=$selectedComponentId, " + + "selectedSub=$selectedSub, " + + "selectedObj=$selectedObj" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePCountDialog.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePCountDialog.kt new file mode 100644 index 000000000..52fbde079 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePCountDialog.kt @@ -0,0 +1,33 @@ +package net.rsprot.protocol.game.incoming.resumed + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Resume p count dialogue is sent whenever a player enters an + * integer to the input box, e.g. to withdraw an item in x-quantity. + * @property count the count entered. While this can only be a positive + * integer for manually-entered inputs, it is **not** guaranteed to always + * be positive. Clientscripts can invoke this event with negative values to + * represent various potential response codes. + */ +public class ResumePCountDialog( + public val count: Int, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ResumePCountDialog + + return count == other.count + } + + override fun hashCode(): Int = count + + override fun toString(): String = "ResumePCountDialog(count=$count)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePNameDialog.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePNameDialog.kt new file mode 100644 index 000000000..c3bf7c4a3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePNameDialog.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.incoming.resumed + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Name dialogs are sent whenever a player enters the name of a player + * into the chatbox input box, e.g. to enter someone else's player-owned + * house. + * @property name the name of the player entered + */ +public class ResumePNameDialog( + public val name: String, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ResumePNameDialog + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() + + override fun toString(): String = "ResumePNameDialog(name='$name')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePObjDialog.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePObjDialog.kt new file mode 100644 index 000000000..771f042e6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePObjDialog.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.incoming.resumed + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Resume p obj dialogue is sent when the user selects an obj from the + * Grand Exchange item search box, however this packet is not necessarily + * exclusive to that feature, and can be used in other pieces of content. + * @property obj the id of the obj selected + */ +public class ResumePObjDialog( + public val obj: Int, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ResumePObjDialog + + return obj == other.obj + } + + override fun hashCode(): Int = obj + + override fun toString(): String = "ResumePObjDialog(obj=$obj)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePStringDialog.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePStringDialog.kt new file mode 100644 index 000000000..5de817c90 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePStringDialog.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.resumed + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * String dialogs are sent whenever a player enters a string into + * the input box, e.g. for wiki search or diango's item code service. + * @property string the string entered + */ +public class ResumePStringDialog( + public val string: String, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ResumePStringDialog + + return string == other.string + } + + override fun hashCode(): Int = string.hashCode() + + override fun toString(): String = "ResumePStringDialog(string='$string')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePauseButton.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePauseButton.kt new file mode 100644 index 000000000..041d2dc54 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/resumed/ResumePauseButton.kt @@ -0,0 +1,64 @@ +package net.rsprot.protocol.game.incoming.resumed + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * Resume pausebutton messages are sent when the player continues + * a dialogue through the "Click to continue" button + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface on which the component exists + * @property componentId the component id clicked + * @property sub the subcomponent id, or -1 if it doesn't exist + */ +public class ResumePauseButton private constructor( + private val _combinedId: CombinedId, + private val _sub: UShort, +) : IncomingGameMessage { + public constructor( + combinedId: CombinedId, + sub: Int, + ) : this( + combinedId, + sub.toUShort(), + ) + + public val combinedId: Int + get() = _combinedId.combinedId + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val sub: Int + get() = _sub.toIntOrMinusOne() + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ResumePauseButton + + if (_combinedId != other._combinedId) return false + if (_sub != other._sub) return false + + return true + } + + override fun hashCode(): Int { + var result = _combinedId.hashCode() + result = 31 * result + _sub.hashCode() + return result + } + + override fun toString(): String = + "ResumePauseButton(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "sub=$sub" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/FriendListAdd.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/FriendListAdd.kt new file mode 100644 index 000000000..108428658 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/FriendListAdd.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.social + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Friend list add messages are sent when the player requests + * to add another player to their friend list + * @property name the name of the player to add to the friend list + */ +public class FriendListAdd( + public val name: String, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FriendListAdd + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() + + override fun toString(): String = "FriendListAdd(name='$name')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/FriendListDel.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/FriendListDel.kt new file mode 100644 index 000000000..307809a89 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/FriendListDel.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.social + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Friend list deletion messages are sent whenever the player + * requests to delete another user from their friend list. + * @property name the name of the player to delete + */ +public class FriendListDel( + public val name: String, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FriendListDel + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() + + override fun toString(): String = "FriendListDel(name='$name')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/IgnoreListAdd.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/IgnoreListAdd.kt new file mode 100644 index 000000000..5c25ee265 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/IgnoreListAdd.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.social + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Ignore list addition events are sent whenever the player + * requests to add another player to their ignorelist + * @property name the name of the player to add to their ignorelist + */ +public class IgnoreListAdd( + public val name: String, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IgnoreListAdd + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() + + override fun toString(): String = "IgnoreListAdd(name='$name')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/IgnoreListDel.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/IgnoreListDel.kt new file mode 100644 index 000000000..6dc6ffd45 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/incoming/social/IgnoreListDel.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.incoming.social + +import net.rsprot.protocol.ClientProtCategory +import net.rsprot.protocol.game.incoming.GameClientProtCategory +import net.rsprot.protocol.message.IncomingGameMessage + +/** + * Ignore list deletion messages are sent whenever the player + * requests to delete another player from their ignorelist + * @property name the name of the player to delete from their ignorelist + */ +public class IgnoreListDel( + public val name: String, +) : IncomingGameMessage { + override val category: ClientProtCategory + get() = GameClientProtCategory.USER_EVENT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IgnoreListDel + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() + + override fun toString(): String = "IgnoreListDel(name='$name')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/GameServerProtCategory.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/GameServerProtCategory.kt new file mode 100644 index 000000000..1cf993719 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/GameServerProtCategory.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.outgoing + +import net.rsprot.protocol.ServerProtCategory + +public enum class GameServerProtCategory( + override val id: Int, +) : ServerProtCategory { + HIGH_PRIORITY_PROT(0), + LOW_PRIORITY_PROT(1), + ; + + public companion object { + public const val COUNT: Int = 2 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamLookAt.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamLookAt.kt new file mode 100644 index 000000000..f0bf8806e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamLookAt.kt @@ -0,0 +1,88 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInBuildArea +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Cam lookat packet is used to make the camera look towards + * a certain coordinate in the build area. + * It is important to note that if this is sent together with + * a map reload, whether this packet comes before or after the + * map reload makes a difference - as the build area itself changes. + * + * @property destinationXInBuildArea the dest x coordinate within the build area, + * in range of 0 to 103 (inclusive) + * @property destinationZInBuildArea the dest z coordinate within the build area, + * in range of 0 to 103 (inclusive) + * @property height the height of the camera + * @property rate the constant speed at which the camera looks towards + * to the new coordinate + * @property rate2 the speed increase as the camera looks + * towards the end coordinate. + */ +public class CamLookAt private constructor( + private val destinationCoordInBuildArea: CoordInBuildArea, + private val _height: UShort, + private val _rate: UByte, + private val _rate2: UByte, +) : OutgoingGameMessage { + public constructor( + xInBuildArea: Int, + zInBuildArea: Int, + height: Int, + rate: Int, + rate2: Int, + ) : this( + CoordInBuildArea(xInBuildArea, zInBuildArea), + height.toUShort(), + rate.toUByte(), + rate2.toUByte(), + ) + + public val destinationXInBuildArea: Int + get() = destinationCoordInBuildArea.xInBuildArea + public val destinationZInBuildArea: Int + get() = destinationCoordInBuildArea.zInBuildArea + public val height: Int + get() = _height.toInt() + public val rate: Int + get() = _rate.toInt() + public val rate2: Int + get() = _rate2.toInt() + + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamLookAt + + if (destinationCoordInBuildArea != other.destinationCoordInBuildArea) return false + if (_height != other._height) return false + if (_rate != other._rate) return false + if (_rate2 != other._rate2) return false + + return true + } + + override fun hashCode(): Int { + var result = destinationCoordInBuildArea.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + _rate.hashCode() + result = 31 * result + _rate2.hashCode() + return result + } + + override fun toString(): String = + "CamLookAt(" + + "destinationXInBuildArea=$destinationXInBuildArea, " + + "destinationZInBuildArea=$destinationZInBuildArea, " + + "height=$height, " + + "rate=$rate, " + + "rate2=$rate2" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamLookAtEasedCoord.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamLookAtEasedCoord.kt new file mode 100644 index 000000000..c378bd17d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamLookAtEasedCoord.kt @@ -0,0 +1,84 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.camera.util.CameraEaseFunction +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInBuildArea +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Cam look at eased coord is used to make the camera look towards + * a certain coordinate with various easing functions. + * + * @property destinationXInBuildArea the dest x coordinate within the build area, + * in range of 0 to 103 (inclusive) + * @property destinationZInBuildArea the dest z coordinate within the build area, + * in range of 0 to 103 (inclusive) + * @property height the height of the camera + * @property cycles the duration of the movement in client cycles (20ms/cc) + * @property easing the camera easing function, allowing for finer + * control over the way it moves from the start coordinate to the end. + */ +public class CamLookAtEasedCoord private constructor( + private val destinationCoordInBuildArea: CoordInBuildArea, + private val _height: UShort, + private val _cycles: UShort, + private val _easing: UByte, +) : OutgoingGameMessage { + public constructor( + xInBuildArea: Int, + zInBuildArea: Int, + height: Int, + cycles: Int, + easing: Int, + ) : this( + CoordInBuildArea(xInBuildArea, zInBuildArea), + height.toUShort(), + cycles.toUShort(), + easing.toUByte(), + ) + + public val destinationXInBuildArea: Int + get() = destinationCoordInBuildArea.xInBuildArea + public val destinationZInBuildArea: Int + get() = destinationCoordInBuildArea.zInBuildArea + public val height: Int + get() = _height.toInt() + public val cycles: Int + get() = _cycles.toInt() + public val easing: CameraEaseFunction + get() = CameraEaseFunction[_easing.toInt()] + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamLookAtEasedCoord + + if (destinationCoordInBuildArea != other.destinationCoordInBuildArea) return false + if (_height != other._height) return false + if (_cycles != other._cycles) return false + if (_easing != other._easing) return false + + return true + } + + override fun hashCode(): Int { + var result = destinationCoordInBuildArea.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + _cycles.hashCode() + result = 31 * result + _easing.hashCode() + return result + } + + override fun toString(): String = + "CamLookAtEasedCoord(" + + "destinationXInBuildArea=$destinationXInBuildArea, " + + "destinationZInBuildArea=$destinationZInBuildArea, " + + "height=$height, " + + "cycles=$cycles, " + + "easing=$easing" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMode.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMode.kt new file mode 100644 index 000000000..557047764 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMode.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Cam mode is used to set the camera into an orb-of-oculus mode, + * or out of it. + * @property mode the mode to set in, with the only valid values being + * 0 for "out of oculus" and 1 for "into oculus". + */ +public class CamMode( + public val mode: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamMode + + return mode == other.mode + } + + override fun hashCode(): Int = mode + + override fun toString(): String = "CamMode(mode=$mode)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveTo.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveTo.kt new file mode 100644 index 000000000..8354f8641 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveTo.kt @@ -0,0 +1,87 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInBuildArea +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Cam move to packet is used to move the position of the camera + * to a specific coordinate within the current build area. + * It is important to note that if this is sent together with + * a map reload, whether this packet comes before or after the + * map reload makes a difference - as the build area itself changes. + * + * @property destinationXInBuildArea the dest x coordinate within the build area, + * in range of 0 to 103 (inclusive) + * @property destinationZInBuildArea the dest z coordinate within the build area, + * in range of 0 to 103 (inclusive) + * @property height the height of the camera + * @property rate the constant speed at which the camera moves + * to the new coordinate + * @property rate2 the speed increase as the camera moves + * towards the end coordinate. + */ +public class CamMoveTo private constructor( + private val destinationCoordInBuildArea: CoordInBuildArea, + private val _height: UShort, + private val _rate: UByte, + private val _rate2: UByte, +) : OutgoingGameMessage { + public constructor( + xInBuildArea: Int, + zInBuildArea: Int, + height: Int, + rate: Int, + rate2: Int, + ) : this( + CoordInBuildArea(xInBuildArea, zInBuildArea), + height.toUShort(), + rate.toUByte(), + rate2.toUByte(), + ) + + public val destinationXInBuildArea: Int + get() = destinationCoordInBuildArea.xInBuildArea + public val destinationZInBuildArea: Int + get() = destinationCoordInBuildArea.zInBuildArea + public val height: Int + get() = _height.toInt() + public val rate: Int + get() = _rate.toInt() + public val rate2: Int + get() = _rate2.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamMoveTo + + if (destinationCoordInBuildArea != other.destinationCoordInBuildArea) return false + if (_height != other._height) return false + if (_rate != other._rate) return false + if (_rate2 != other._rate2) return false + + return true + } + + override fun hashCode(): Int { + var result = destinationCoordInBuildArea.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + _rate.hashCode() + result = 31 * result + _rate2.hashCode() + return result + } + + override fun toString(): String = + "CamMoveTo(" + + "destinationXInBuildArea=$destinationXInBuildArea, " + + "destinationZInBuildArea=$destinationZInBuildArea, " + + "height=$height, " + + "rate=$rate, " + + "rate2=$rate2" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveToArc.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveToArc.kt new file mode 100644 index 000000000..d1f0c00fe --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveToArc.kt @@ -0,0 +1,117 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.camera.util.CameraEaseFunction +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInBuildArea +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Camera move to arc packet is used to move camera + * to a new coordinate with finer control behind it. + * This packet differs from [CamMoveToCycles] in that it will first + * move through a center coordinate before going towards the destination, + * creating a `)`-shape movement. An example image of this can be seen + * [here](https://media.z-kris.com/2024/04/cam%20move%20eased%20circular.png) + * + * @property centerXInBuildArea the center x coordinate within the build area, + * in range of 0 to 103 (inclusive). This marks the middle point between the + * camera movement through which the camera has to go. + * @property centerZInBuildArea the center z coordinate within the build area, + * in range of 0 to 103 (inclusive). This marks the middle point between the + * camera movement through which the camera has to go. + * @property destinationXInBuildArea the dest x coordinate within the build area, + * in range of 0 to 103 (inclusive) + * @property destinationZInBuildArea the dest z coordinate within the build area, + * in range of 0 to 103 (inclusive) + * @property height the height of the camera once it arrives at the destination + * @property cycles the duration of the movement in client cycles (20ms/cc) + * @property ignoreTerrain whether the camera moves along the terrain, + * moving up and down according to bumps in the terrain. + * If true, the camera will move in a straight line from the starting position + * towards the end position, ignoring any changes in the terrain. + * @property easing the camera easing function, allowing for finer + * control over the way it moves from the start coordinate to the end. + */ +@Suppress("DuplicatedCode") +public class CamMoveToArc private constructor( + private val centerCoordInBuildArea: CoordInBuildArea, + private val destinationCoordInBuildArea: CoordInBuildArea, + private val _height: UShort, + private val _cycles: UShort, + public val ignoreTerrain: Boolean, + private val _easing: UByte, +) : OutgoingGameMessage { + public constructor( + centerXInBuildArea: Int, + centerZInBuildArea: Int, + destinationXInBuildArea: Int, + destinationZInBuildArea: Int, + height: Int, + cycles: Int, + ignoreTerrain: Boolean, + easing: Int, + ) : this( + CoordInBuildArea(centerXInBuildArea, centerZInBuildArea), + CoordInBuildArea(destinationXInBuildArea, destinationZInBuildArea), + height.toUShort(), + cycles.toUShort(), + ignoreTerrain, + easing.toUByte(), + ) + + public val centerXInBuildArea: Int + get() = centerCoordInBuildArea.xInBuildArea + public val centerZInBuildArea: Int + get() = centerCoordInBuildArea.zInBuildArea + public val destinationXInBuildArea: Int + get() = destinationCoordInBuildArea.xInBuildArea + public val destinationZInBuildArea: Int + get() = destinationCoordInBuildArea.zInBuildArea + public val height: Int + get() = _height.toInt() + public val cycles: Int + get() = _cycles.toInt() + public val easing: CameraEaseFunction + get() = CameraEaseFunction[_easing.toInt()] + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamMoveToArc + + if (centerCoordInBuildArea != other.centerCoordInBuildArea) return false + if (destinationCoordInBuildArea != other.destinationCoordInBuildArea) return false + if (_height != other._height) return false + if (_cycles != other._cycles) return false + if (ignoreTerrain != other.ignoreTerrain) return false + if (_easing != other._easing) return false + + return true + } + + override fun hashCode(): Int { + var result = centerCoordInBuildArea.hashCode() + result = 31 * result + destinationCoordInBuildArea.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + _cycles.hashCode() + result = 31 * result + ignoreTerrain.hashCode() + result = 31 * result + _easing.hashCode() + return result + } + + override fun toString(): String = + "CamMoveToArc(" + + "centerXInBuildArea=$centerXInBuildArea, " + + "centerZInBuildArea=$centerZInBuildArea, " + + "destinationXInBuildArea=$destinationXInBuildArea, " + + "destinationZInBuildArea=$destinationZInBuildArea, " + + "height=$height, " + + "cycles=$cycles, " + + "ignoreTerrain=$ignoreTerrain, " + + "easing=$easing" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveToCycles.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveToCycles.kt new file mode 100644 index 000000000..3662f3d0e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamMoveToCycles.kt @@ -0,0 +1,94 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.camera.util.CameraEaseFunction +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInBuildArea +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Camera move to cycles packet is used to move camera + * to a new coordinate with finer control behind it. + * @property destinationXInBuildArea the dest x coordinate within the build area, + * in range of 0 to 103 (inclusive) + * @property destinationZInBuildArea the dest z coordinate within the build area, + * in range of 0 to 103 (inclusive) + * @property height the height of the camera once it arrives at the destination + * @property cycles the duration of the movement in client cycles (20ms/cc) + * @property ignoreTerrain whether the camera moves along the terrain, + * moving up and down according to bumps in the terrain. + * If true, the camera will move in a straight line from the starting position + * towards the end position, ignoring any changes in the terrain. + * @property easing the camera easing function, allowing for finer + * control over the way it moves from the start coordinate to the end. + */ +@Suppress("DuplicatedCode") +public class CamMoveToCycles private constructor( + private val destinationCoordInBuildArea: CoordInBuildArea, + private val _height: UShort, + private val _cycles: UShort, + public val ignoreTerrain: Boolean, + private val _easing: UByte, +) : OutgoingGameMessage { + public constructor( + xInBuildArea: Int, + zInBuildArea: Int, + height: Int, + cycles: Int, + ignoreTerrain: Boolean, + easing: Int, + ) : this( + CoordInBuildArea(xInBuildArea, zInBuildArea), + height.toUShort(), + cycles.toUShort(), + ignoreTerrain, + easing.toUByte(), + ) + + public val destinationXInBuildArea: Int + get() = destinationCoordInBuildArea.xInBuildArea + public val destinationZInBuildArea: Int + get() = destinationCoordInBuildArea.zInBuildArea + public val height: Int + get() = _height.toInt() + public val cycles: Int + get() = _cycles.toInt() + public val easing: CameraEaseFunction + get() = CameraEaseFunction[_easing.toInt()] + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamMoveToCycles + + if (destinationCoordInBuildArea != other.destinationCoordInBuildArea) return false + if (_height != other._height) return false + if (_cycles != other._cycles) return false + if (ignoreTerrain != other.ignoreTerrain) return false + if (_easing != other._easing) return false + + return true + } + + override fun hashCode(): Int { + var result = destinationCoordInBuildArea.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + _cycles.hashCode() + result = 31 * result + ignoreTerrain.hashCode() + result = 31 * result + _easing.hashCode() + return result + } + + override fun toString(): String = + "CamMoveToCycles(" + + "destinationXInBuildArea=$destinationXInBuildArea, " + + "destinationZInBuildArea=$destinationZInBuildArea, " + + "height=$height, " + + "cycles=$cycles, " + + "ignoreTerrain=$ignoreTerrain, " + + "easing=$easing" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamReset.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamReset.kt new file mode 100644 index 000000000..82c333ace --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamReset.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Cam reset is used to clear out any camera shaking or + * any sort of movements that might've been previously set. + * Additionally, unlocks the camera if it has been locked in place. + */ +public data object CamReset : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamRotateBy.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamRotateBy.kt new file mode 100644 index 000000000..16475342c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamRotateBy.kt @@ -0,0 +1,87 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.camera.util.CameraEaseFunction +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Cam rotate by is used to make the camera look towards + * an angle relative to the current camera angle. + * One way to think of this packet is that it **adds** values to the + * x and y angles of the camera. + * + * @property pitch the additional angle to add to the x-axis of the camera. + * It's worth noting that the x angle of the camera ranges between 128 and + * 383 (inclusive), and the resulting value is coerced in that range. + * Negative values are also accepted. + * Additionally, there is currently a bug in the client that causes the + * third and the fifth least significant bits of the resulting angle to + * be discarded due to the code doing (cameraXAngle + [pitch] & 2027), + * which is further coerced into the 128-383 range. + * @property yaw the additional angle to add to the y-axis of the camera. + * Unlike the x-axis angle, this one ranges from 0 to 2047 (inclusive), + * and does not get coerced - instead it will just roll over (e.g. 2047 -> 0). + * @property cycles the duration of the movement in client cycles (20ms/cc) + * @property easing the camera easing function, allowing for finer + * control over the way it moves from the start coordinate to the end. + */ +public class CamRotateBy private constructor( + private val _pitch: Short, + private val _yaw: Short, + private val _cycles: UShort, + private val _easing: UByte, +) : OutgoingGameMessage { + public constructor( + pitch: Int, + yaw: Int, + cycles: Int, + easing: Int, + ) : this( + pitch.toShort(), + yaw.toShort(), + cycles.toUShort(), + easing.toUByte(), + ) + + public val pitch: Int + get() = _pitch.toInt() + public val yaw: Int + get() = _yaw.toInt() + public val cycles: Int + get() = _cycles.toInt() + public val easing: CameraEaseFunction + get() = CameraEaseFunction[_easing.toInt()] + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamRotateBy + + if (_pitch != other._pitch) return false + if (_yaw != other._yaw) return false + if (_cycles != other._cycles) return false + if (_easing != other._easing) return false + + return true + } + + override fun hashCode(): Int { + var result = _pitch.toInt() + result = 31 * result + _yaw + result = 31 * result + _cycles.hashCode() + result = 31 * result + _easing.hashCode() + return result + } + + override fun toString(): String = + "CamRotateBy(" + + "pitch=$pitch, " + + "yaw=$yaw, " + + "cycles=$cycles, " + + "easing=$easing" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamRotateTo.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamRotateTo.kt new file mode 100644 index 000000000..0f8ec4c1a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamRotateTo.kt @@ -0,0 +1,84 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.camera.util.CameraEaseFunction +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Cam rotate to is used to make the camera look towards + * an angle relative to the current camera angle. + * One way to think of this packet is that it **adds** values to the + * x and y angles of the camera. + * + * @property pitch the x angle of the camera to set to. + * Note that the angle is coerced into a range of 128..383, + * and incorrectly excludes the third and fifth least significant bits + * before doing so (by doing [pitch] & 2027, rather than 2047). + * @property yaw the x angle of the camera to set to. + * Note that the angle incorrectly excludes the third and fifth least significant bits + * (by doing [pitch] & 2027, rather than 2047). + * @property cycles the duration of the movement in client cycles (20ms/cc) + * @property easing the camera easing function, allowing for finer + * control over the way it moves from the start coordinate to the end. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class CamRotateTo private constructor( + private val _pitch: Short, + private val _yaw: Short, + private val _cycles: UShort, + private val _easing: UByte, +) : OutgoingGameMessage { + public constructor( + pitch: Int, + yaw: Int, + cycles: Int, + easing: Int, + ) : this( + pitch.toShort(), + yaw.toShort(), + cycles.toUShort(), + easing.toUByte(), + ) + + public val pitch: Int + get() = _pitch.toInt() + public val yaw: Int + get() = _yaw.toInt() + public val cycles: Int + get() = _cycles.toInt() + public val easing: CameraEaseFunction + get() = CameraEaseFunction[_easing.toInt()] + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamRotateTo + + if (_pitch != other._pitch) return false + if (_yaw != other._yaw) return false + if (_cycles != other._cycles) return false + if (_easing != other._easing) return false + + return true + } + + override fun hashCode(): Int { + var result = _pitch.toInt() + result = 31 * result + _yaw + result = 31 * result + _cycles.hashCode() + result = 31 * result + _easing.hashCode() + return result + } + + override fun toString(): String = + "CamRotateTo(" + + "pitch=$pitch, " + + "yaw=$yaw, " + + "cycles=$cycles, " + + "easing=$easing" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamShake.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamShake.kt new file mode 100644 index 000000000..c674d97be --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamShake.kt @@ -0,0 +1,97 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Cam shake packet is used to make the camera shake around. + * It is worth noting that multiple different types of shakes + * can be executed simultaneously, making the camera more and + * more volatile as a result. + * + * The properties of this class are in the exact order as + * the client reads them, which is consistent across revisions! + * + * Camera movements table: + * ``` + * | Id | Type | Observed Movement | + * |----|:-------:|:----------------------:| + * | 0 | X-axis | Left and right | + * | 1 | Y-axis | Up and down | + * | 2 | Z-axis | Forwards and backwards | + * | 3 | Y-angle | Panning left and right | + * | 4 | X-angle | Panning up and down | + * ``` + * + * @property axis the type of the shake (see table above) + * @property random the amount of randomness involved. + * The client will generate a random double from 0.0 to 1.0 + * and multiply it with the [random] as part of the shaking. + * This property is called 'shakeIntensity' in the event inspector. + * @property amplitude the amount of randomness generated by the + * sine. Unlike [random], this is multiplied against the + * [rate]. + * This property is called 'movementIntensity' in the event inspector. + * @property rate the sine frequency. + * This property is called 'speed' in the event inspector. + */ +public class CamShake private constructor( + private val _axis: UByte, + private val _random: UByte, + private val _amplitude: UByte, + private val _rate: UByte, +) : OutgoingGameMessage { + public constructor( + axis: Int, + random: Int, + amplitude: Int, + rate: Int, + ) : this( + axis.toUByte(), + random.toUByte(), + amplitude.toUByte(), + rate.toUByte(), + ) + + public val axis: Int + get() = _axis.toInt() + public val random: Int + get() = _random.toInt() + public val amplitude: Int + get() = _amplitude.toInt() + public val rate: Int + get() = _rate.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamShake + + if (_axis != other._axis) return false + if (_random != other._random) return false + if (_amplitude != other._amplitude) return false + if (_rate != other._rate) return false + + return true + } + + override fun hashCode(): Int { + var result = _axis.hashCode() + result = 31 * result + _random.hashCode() + result = 31 * result + _amplitude.hashCode() + result = 31 * result + _rate.hashCode() + return result + } + + override fun toString(): String = + "CamShake(" + + "axis=$axis, " + + "random=$random, " + + "amplitude=$amplitude, " + + "rate=$rate" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamSmoothReset.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamSmoothReset.kt new file mode 100644 index 000000000..dae1c988f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamSmoothReset.kt @@ -0,0 +1,78 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Cam smooth reset is used to smoothly reset camera back to the + * state where the user is in control, instead of it happening + * instantaneously. + * + * Note that the properties of this packet are unused in the Java client. + * + * **WARNING:** The client code __requires__ that the camera is in + * a locked state for this packet's code to be executed in **Java**. + * If the camera isn't in a locked state, an error condition is hit + * at the bottom of the function and the player will be kicked out of + * the game! + */ +public class CamSmoothReset private constructor( + private val _cameraMoveConstantSpeed: UByte, + private val _cameraMoveProportionalSpeed: UByte, + private val _cameraLookConstantSpeed: UByte, + private val _cameraLookProportionalSpeed: UByte, +) : OutgoingGameMessage { + public constructor( + cameraMoveConstantSpeed: Int, + cameraMoveProportionalSpeed: Int, + cameraLookConstantSpeed: Int, + cameraLookProportionalSpeed: Int, + ) : this( + cameraMoveConstantSpeed.toUByte(), + cameraMoveProportionalSpeed.toUByte(), + cameraLookConstantSpeed.toUByte(), + cameraLookProportionalSpeed.toUByte(), + ) + + public val cameraMoveConstantSpeed: Int + get() = _cameraMoveConstantSpeed.toInt() + public val cameraMoveProportionalSpeed: Int + get() = _cameraMoveProportionalSpeed.toInt() + public val cameraLookConstantSpeed: Int + get() = _cameraLookConstantSpeed.toInt() + public val cameraLookProportionalSpeed: Int + get() = _cameraLookProportionalSpeed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamSmoothReset + + if (_cameraMoveConstantSpeed != other._cameraMoveConstantSpeed) return false + if (_cameraMoveProportionalSpeed != other._cameraMoveProportionalSpeed) return false + if (_cameraLookConstantSpeed != other._cameraLookConstantSpeed) return false + if (_cameraLookProportionalSpeed != other._cameraLookProportionalSpeed) return false + + return true + } + + override fun hashCode(): Int { + var result = _cameraMoveConstantSpeed.hashCode() + result = 31 * result + _cameraMoveProportionalSpeed.hashCode() + result = 31 * result + _cameraLookConstantSpeed.hashCode() + result = 31 * result + _cameraLookProportionalSpeed.hashCode() + return result + } + + override fun toString(): String = + "CamSmoothReset(" + + "cameraMoveConstantSpeed=$cameraMoveConstantSpeed, " + + "cameraMoveProportionalSpeed=$cameraMoveProportionalSpeed, " + + "cameraLookConstantSpeed=$cameraLookConstantSpeed, " + + "cameraLookProportionalSpeed=$cameraLookProportionalSpeed" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamTarget.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamTarget.kt new file mode 100644 index 000000000..3ee2e57db --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamTarget.kt @@ -0,0 +1,144 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Camera target packet is used to attach to camera on another entity in the scene. + * If the entity by the specified index cannot be found in the client, the camera + * will always be focused back on the local player. + * Furthermore, depth buffering (z-buffer) will be enabled if the [WorldEntityTarget] type + * is used. Other types will use the traditional priority system. + * @property type the camera target type to focus on. + */ +public class CamTarget( + public val type: CamTargetType, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CamTarget + + return type == other.type + } + + override fun hashCode(): Int = type.hashCode() + + override fun toString(): String = "CamTargetOld(type=$type)" + + /** + * A sealed interface for various camera target types. + */ + public sealed interface CamTargetType + + /** + * Camera target type for players. This will focus the camera on a specific player. + * If the player by the specified [index] cannot be found, the camera will be set back on + * local player. + * @property index the index of the player who to set the camera on. + */ + public class PlayerCamTarget( + public val index: Int, + ) : CamTargetType { + init { + require(index in 0..<2048) { + "Index must be in range of 0..<2048" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlayerCamTarget + + return index == other.index + } + + override fun hashCode(): Int = index + + override fun toString(): String = "PlayerCamTarget(index=$index)" + } + + /** + * Camera target type for NPCs. This will focus the camera on a specific NPC. + * If the NPC by the specified [index] cannot be found, the camera will be set back on + * local player. + * @property index the index of the NPC who to set the camera on. + */ + public class NpcCamTarget( + public val index: Int, + ) : CamTargetType { + init { + require(index in 0..<65536) { + "Index must be in range of 0..<65536" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NpcCamTarget + + return index == other.index + } + + override fun hashCode(): Int = index + + override fun toString(): String = "NpcCamTarget(index=$index)" + } + + /** + * Camera target type for world entities. This will focus the camera on a specific world entity. + * If the world entity by the specified [index] cannot be found, the camera will be set back on + * local player. If a player index is provided, the client will try to look up that player in the + * root player's current world entity and lock the camera onto them. + * Additionally, depth buffering (z-buffer) will be enabled when this type of camera target is used. + * @property index the index of the world entity who to set the camera on. + * @property cameraLockedPlayerIndex the index of the player on the local player's world entity whom + * to lock the camera onto. + */ + public class WorldEntityTarget( + public val index: Int, + public val cameraLockedPlayerIndex: Int, + ) : CamTargetType { + init { + require(index in 0..<2048) { + "Index must be in range of 0..<2048" + } + require(cameraLockedPlayerIndex == -1 || cameraLockedPlayerIndex in 0..<2048) { + "Player index must be -1, or in range of 0..<2048" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as WorldEntityTarget + + if (index != other.index) return false + if (cameraLockedPlayerIndex != other.cameraLockedPlayerIndex) return false + + return true + } + + override fun hashCode(): Int { + var result = index + result = 31 * result + cameraLockedPlayerIndex + return result + } + + override fun toString(): String = + "WorldEntityTarget(" + + "index=$index, " + + "cameraLockedPlayerIndex=$cameraLockedPlayerIndex" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamTargetOld.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamTargetOld.kt new file mode 100644 index 000000000..6453ff3cf --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/CamTargetOld.kt @@ -0,0 +1,135 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Camera target packet is used to attach to camera on another entity in the scene. + * If the entity by the specified index cannot be found in the client, the camera + * will always be focused back on the local player. + * Furthermore, depth buffering (z-buffer) will be enabled if the [WorldEntityTarget] type + * is used. Other types will use the traditional priority system. + * @property type the camera target type to focus on. + */ +@Deprecated( + "Deprecated in revision 223.", + replaceWith = + ReplaceWith( + "CamTarget", + imports = arrayOf("net.rsprot.protocol.game.outgoing.camera.CamTarget"), + ), +) +public class CamTargetOld( + public val type: CamTargetType, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + @Suppress("DEPRECATION") + other as CamTargetOld + + return type == other.type + } + + override fun hashCode(): Int = type.hashCode() + + override fun toString(): String = "CamTargetOld(type=$type)" + + /** + * A sealed interface for various camera target types. + */ + public sealed interface CamTargetType + + /** + * Camera target type for players. This will focus the camera on a specific player. + * If the player by the specified [index] cannot be found, the camera will be set back on + * local player. + * @property index the index of the player who to set the camera on. + */ + public class PlayerCamTarget( + public val index: Int, + ) : CamTargetType { + init { + require(index in 0..<2048) { + "Index must be in range of 0..<2048" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlayerCamTarget + + return index == other.index + } + + override fun hashCode(): Int = index + + override fun toString(): String = "PlayerCamTarget(index=$index)" + } + + /** + * Camera target type for NPCs. This will focus the camera on a specific NPC. + * If the NPC by the specified [index] cannot be found, the camera will be set back on + * local player. + * @property index the index of the NPC who to set the camera on. + */ + public class NpcCamTarget( + public val index: Int, + ) : CamTargetType { + init { + require(index in 0..<65536) { + "Index must be in range of 0..<65536" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NpcCamTarget + + return index == other.index + } + + override fun hashCode(): Int = index + + override fun toString(): String = "NpcCamTarget(index=$index)" + } + + /** + * Camera target type for world entities. This will focus the camera on a specific world entity. + * If the world entity by the specified [index] cannot be found, the camera will be set back on + * local player. + * Additionally, depth buffering (z-buffer) will be enabled when this type of camera target is used. + * @property index the index of the world entity who to set the camera on. + */ + public class WorldEntityTarget( + public val index: Int, + ) : CamTargetType { + init { + require(index in 0..<2048) { + "Index must be in range of 0..<2048" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as WorldEntityTarget + + return index == other.index + } + + override fun hashCode(): Int = index + + override fun toString(): String = "WorldEntityTarget(index=$index)" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/OculusSync.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/OculusSync.kt new file mode 100644 index 000000000..c87569fd5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/OculusSync.kt @@ -0,0 +1,36 @@ +package net.rsprot.protocol.game.outgoing.camera + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Oculus sync is used to re-synchronize the orb of oculus + * camera to the local player in the client, if the value + * does not match up with the client's value. + * The client initializes this property as zero. + * @property value the synchronization value, if the client's + * value is different, oculus camera is moved to the client's local player. + * Additionally, this value is sent by the client in the + * [net.rsprot.protocol.game.incoming.misc.user.Teleport] packet whenever + * the oculus causes the player to teleport. + */ +public class OculusSync( + public val value: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OculusSync + + return value == other.value + } + + override fun hashCode(): Int = value + + override fun toString(): String = "OculusSync(value=$value)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/util/CameraEaseFunction.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/util/CameraEaseFunction.kt new file mode 100644 index 000000000..750e836c6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/camera/util/CameraEaseFunction.kt @@ -0,0 +1,59 @@ +package net.rsprot.protocol.game.outgoing.camera.util + +import kotlin.jvm.Throws + +/** + * Camera functions for eased movement. + * These functions are used together with various 'eased' camera + * packets to alter how the camera movement happens between the + * coordinates provided. + * + * @property id the respective id of the camera function, + * as expected by the client. + */ +public enum class CameraEaseFunction( + public val id: Int, +) { + LINEAR(0), + EASE_IN_SINE(1), + EASE_OUT_SINE(2), + EASE_IN_OUT_SINE(3), + EASE_IN_QUAD(4), + EASE_OUT_QUAD(5), + EASE_IN_OUT_QUAD(6), + EASE_IN_CUBIC(7), + EASE_OUT_CUBIC(8), + EASE_IN_OUT_CUBIC(9), + EASE_IN_QUART(10), + EASE_OUT_QUART(11), + EASE_IN_OUT_QUART(12), + EASE_IN_QUINT(13), + EASE_OUT_QUINT(14), + EASE_IN_OUT_QUINT(15), + EASE_IN_EXPO(16), + EASE_OUT_EXPO(17), + EASE_IN_OUT_EXPO(18), + EASE_IN_CIRC(19), + EASE_OUT_CIRC(20), + EASE_IN_OUT_CIRC(21), + EASE_IN_BACK(22), + EASE_OUT_BACK(23), + EASE_IN_OUT_BACK(24), + EASE_IN_ELASTIC(25), + EASE_OUT_ELASTIC(26), + EASE_IN_OUT_ELASTIC(27), + ; + + public companion object { + /** + * Gets the camera easing function based on the [id] provided. + * @throws IndexOutOfBoundsException if the id is below 0 or above 27 + * @return camera ease function + */ + @Throws(IndexOutOfBoundsException::class) + public operator fun get(id: Int): CameraEaseFunction { + // Relying on ordinal here as ordinal aligns with the id values. + return entries[id] + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanChannelDelta.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanChannelDelta.kt new file mode 100644 index 000000000..d7635196a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanChannelDelta.kt @@ -0,0 +1,337 @@ +package net.rsprot.protocol.game.outgoing.clan + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Clan channel delta is a packet used to transmit partial updates + * to an existing clan channel. This prevents sending a full update for everything + * as that can get rather wasteful. + * @property clanType the type of the clan the player is in + * @property clanHash the 64-bit hash of the clan + * @property updateNum the update counter/timestamp for the clan. + * The exact behaviours behind this are not known, but the value appears to be + * an epoch time millis, with each minor change resulting in the value incrementing + * by +1; e.g. each member joining seems to increment the value by 1. + * @property events the list of channel delta events to perform in this update + */ +public class ClanChannelDelta private constructor( + private val _clanType: Byte, + public val clanHash: Long, + public val updateNum: Long, + public val events: List, +) : OutgoingGameMessage { + public constructor( + clanType: Int, + key: Long, + updateNum: Long, + events: List, + ) : this( + clanType.toByte(), + key, + updateNum, + events, + ) + + public val clanType: Int + get() = _clanType.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ClanChannelDelta + + if (_clanType != other._clanType) return false + if (clanHash != other.clanHash) return false + if (updateNum != other.updateNum) return false + if (events != other.events) return false + + return true + } + + override fun hashCode(): Int { + var result = _clanType.toInt() + result = 31 * result + clanHash.hashCode() + result = 31 * result + updateNum.hashCode() + result = 31 * result + events.hashCode() + return result + } + + override fun toString(): String = + "ClanChannelDelta(" + + "clanType=$clanType, " + + "clanHash=$clanHash, " + + "updateNum=$updateNum, " + + "events=$events" + + ")" + + public sealed interface Event + + /** + * Clan channel delta adduser event is used to add a new user + * into the clan. + * @property name the name of the player to add to the clan + * @property world the id of the world in which the player resides + * @property rank the rank of the player within the clan + */ + public class AddUserEvent private constructor( + public val name: String, + private val _world: UShort, + private val _rank: Byte, + ) : Event { + public constructor( + name: String, + world: Int, + rank: Int, + ) : this( + name, + world.toUShort(), + rank.toByte(), + ) + + public val world: Int + get() = _world.toInt() + public val rank: Int + get() = _rank.toInt() + + override fun toString(): String = + "AddUserEvent(" + + "name='$name', " + + "world=$world, " + + "rank=$rank" + + ")" + } + + /** + * Clan channel delta update base settings event is used to modify the base + * settings of a clan. + * @property clanName the clan name to set + * @property talkRank the minimum rank needed to talk + * @property kickRank the minimum rank needed to kick other members + */ + public class UpdateBaseSettingsEvent private constructor( + public val clanName: String?, + private val _talkRank: Byte, + private val _kickRank: Byte, + ) : Event { + public constructor() : this( + null, + 0, + 0, + ) + + public constructor( + clanName: String, + talkRank: Int, + kickRank: Int, + ) : this( + clanName, + talkRank.toByte(), + kickRank.toByte(), + ) + + public val talkRank: Int + get() = _talkRank.toInt() + public val kickRank: Int + get() = _kickRank.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateBaseSettingsEvent + + if (clanName != other.clanName) return false + if (_talkRank != other._talkRank) return false + if (_kickRank != other._kickRank) return false + + return true + } + + override fun hashCode(): Int { + var result = clanName?.hashCode() ?: 0 + result = 31 * result + _talkRank + result = 31 * result + _kickRank + return result + } + + override fun toString(): String = + "UpdateBaseSettingsEvent(" + + "clanName=$clanName, " + + "talkRank=$talkRank, " + + "kickRank=$kickRank" + + ")" + } + + /** + * Clan channel delta delete user event is used to delete an existing + * member from the clan. + * @property index the index of the player within the clan. + * Note that this index is the index within this clan, and not a global + * index of the player. + */ + public class DeleteUserEvent private constructor( + private val _index: UShort, + ) : Event { + public constructor( + index: Int, + ) : this( + index.toUShort(), + ) + + public val index: Int + get() = _index.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DeleteUserEvent + + return _index == other._index + } + + override fun hashCode(): Int = _index.hashCode() + + override fun toString(): String = "DeleteUserEvent(index=$index)" + } + + /** + * Clan channel delta update user details event is used to modify + * the details of a user in the clan. + * @property index the index of the player whom to update within the clan. + * Note that this is the index within the clan's list of members and not + * the world-global indexed player list. + * @property name the new name of this player within the clan + * @property rank the new rank of this player within the clan + * @property world the new world of this player within the clan + */ + public class UpdateUserDetailsEvent private constructor( + private val _index: UShort, + public val name: String, + private val _rank: Byte, + private val _world: UShort, + ) : Event { + public constructor( + index: Int, + name: String, + rank: Int, + world: Int, + ) : this( + index.toUShort(), + name, + rank.toByte(), + world.toUShort(), + ) + + public val index: Int + get() = _index.toInt() + public val rank: Int + get() = _rank.toInt() + public val world: Int + get() = _world.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateUserDetailsEvent + + if (_index != other._index) return false + if (name != other.name) return false + if (_rank != other._rank) return false + if (_world != other._world) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _index.hashCode() + result = 31 * result + _rank + result = 31 * result + _world.hashCode() + return result + } + + override fun toString(): String = + "UpdateUserDetailsEvent(" + + "index=$index, " + + "name='$name', " + + "rank=$rank, " + + "world=$world" + + ")" + } + + /** + * Clan channel delta update user details v2 event is used to modify + * the details of a user in the clan. + * Note that this class is identical to the [UpdateUserDetailsEvent], + * with the only exception being that more bandwidth is used to transmit this update, + * as there are multiple unused properties being sent on-top. + * @property index the index of the player whom to update within the clan. + * Note that this is the index within the clan's list of members and not + * the world-global indexed player list. + * @property name the new name of this player within the clan + * @property rank the new rank of this player within the clan + * @property world the new world of this player within the clan + */ + public class UpdateUserDetailsV2Event private constructor( + private val _index: UShort, + public val name: String, + private val _rank: Byte, + private val _world: UShort, + ) : Event { + public constructor( + index: Int, + name: String, + rank: Int, + world: Int, + ) : this( + index.toUShort(), + name, + rank.toByte(), + world.toUShort(), + ) + + public val index: Int + get() = _index.toInt() + public val rank: Int + get() = _rank.toInt() + public val world: Int + get() = _world.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateUserDetailsV2Event + + if (_index != other._index) return false + if (name != other.name) return false + if (_rank != other._rank) return false + if (_world != other._world) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _index.hashCode() + result = 31 * result + _rank + result = 31 * result + _world.hashCode() + return result + } + + override fun toString(): String = + "UpdateUserDetailsV2Event(" + + "index=$index, " + + "name='$name', " + + "rank=$rank, " + + "world=$world" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanChannelFull.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanChannelFull.kt new file mode 100644 index 000000000..3b31ee5ce --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanChannelFull.kt @@ -0,0 +1,247 @@ +package net.rsprot.protocol.game.outgoing.clan + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Clan channel full packets are used to update + * the state of a clan upon first joining it, or when the player is leaving it. + * @property clanType the type of the clan the player is joining or leaving, + * such as guest or normal. + * @property update the type of update to perform, either [JoinUpdate] + * or [LeaveUpdate]. + */ +public class ClanChannelFull private constructor( + private val _clanType: Byte, + public val update: Update, +) : OutgoingGameMessage { + public constructor( + clanType: Int, + update: Update, + ) : this( + clanType.toByte(), + update, + ) + + public val clanType: Int + get() = _clanType.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun toString(): String = + "ClanChannelFull(" + + "update=$update, " + + "clanType=$clanType" + + ")" + + public sealed interface Update + + /** + * Clan channel full join update implies the user is joining + * a new clan. + * @property useBase37Names whether to send the names of players + * in a base-37 encoding. In OldSchool RuneScape, this option is unused. + * @property useDisplayNames whether to use display names for encoding. + * In OldSchool RuneScape, this is always the case and cannot be opted out of. + * @property hasVersion whether a custom version id is provided. + * It is unclear what the purpose behind this is, as the values are discarded. + * @property version the version id, defaulting to 2 in OldSchool RuneScape. + * @property clanHash the 64-bit hash of the clan + * @property updateNum the update counter/timestamp for the clan. + * The exact behaviours behind this are not known, but the value appears to be + * an epoch time millis, with each minor change resulting in the value incrementing + * by +1; e.g. each member joining seems to increment the value by 1. + * @property clanName the name of the clan + * @property discardedBoolean currently unknown as the client discards this value + * @property kickRank the minimum rank needed to kick other players from the clan + * @property talkRank the minimum rank needed to talk in the clan + * @property members the list of members within this clan. + */ + public class JoinUpdate private constructor( + private val _flags: UByte, + private val _version: UByte, + public val clanHash: Long, + public val updateNum: Long, + public val clanName: String, + public val discardedBoolean: Boolean, + private val _kickRank: Byte, + private val _talkRank: Byte, + public val members: List, + ) : Update { + public constructor( + key: Long, + updateNum: Long, + clanName: String, + discardedBoolean: Boolean, + kickRank: Int, + talkRank: Int, + members: List, + version: Int = DEFAULT_OLDSCHOOL_VERSION, + base37Names: Boolean = false, + ) : this( + ( + FLAG_USE_DISPLAY_NAMES + .or(if (base37Names) FLAG_USE_BASE_37_NAMES else 0) + .or(if (version != DEFAULT_OLDSCHOOL_VERSION) FLAG_HAS_VERSION else 0) + ).toUByte(), + version.toUByte(), + key, + updateNum, + clanName, + discardedBoolean, + kickRank.toByte(), + talkRank.toByte(), + members, + ) + + public val useBase37Names: Boolean + get() = _flags.toInt() and FLAG_USE_BASE_37_NAMES != 0 + public val useDisplayNames: Boolean + get() = _flags.toInt() and FLAG_USE_DISPLAY_NAMES != 0 + public val hasVersion: Boolean + get() = _flags.toInt() and FLAG_HAS_VERSION != 0 + public val version: Int + get() = _version.toInt() + public val flags: Int + get() = _flags.toInt() + public val kickRank: Int + get() = _kickRank.toInt() + public val talkRank: Int + get() = _talkRank.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as JoinUpdate + + if (_flags != other._flags) return false + if (_version != other._version) return false + if (clanHash != other.clanHash) return false + if (updateNum != other.updateNum) return false + if (clanName != other.clanName) return false + if (discardedBoolean != other.discardedBoolean) return false + if (_kickRank != other._kickRank) return false + if (_talkRank != other._talkRank) return false + if (members != other.members) return false + + return true + } + + override fun hashCode(): Int { + var result = _flags.toInt() + result = 31 * result + _version.hashCode() + result = 31 * result + clanHash.hashCode() + result = 31 * result + updateNum.hashCode() + result = 31 * result + clanName.hashCode() + result = 31 * result + discardedBoolean.hashCode() + result = 31 * result + _kickRank + result = 31 * result + _talkRank + result = 31 * result + members.hashCode() + return result + } + + override fun toString(): String = + "JoinUpdate(" + + "useBase37Names=$useBase37Names, " + + "useDisplayNames=$useDisplayNames, " + + "hasVersion=$hasVersion, " + + "version=$version, " + + "key=$clanHash, " + + "updateNum=$updateNum, " + + "clanName='$clanName', " + + "discardedBoolean=$discardedBoolean, " + + "kickRank=$kickRank, " + + "talkRank=$talkRank, " + + "members=$members" + + ")" + } + + /** + * Clan channel full leave update implies the user is leaving an existing + * clan of theirs. + */ + public data object LeaveUpdate : Update + + /** + * Clan member classes are used to wrap all the properties shown in the clan + * interface about each player in the clan. + * @property name the display name of the clan member + * @property rank the rank of the clan member in the clan, + * for guest members, the rank is set to -1 + * @property world the world in which the player resides + * @property discardedBoolean unknown boolean (not used by the client) + */ + public class ClanMember private constructor( + public val name: String, + private val _rank: Byte, + private val _world: UShort, + public val discardedBoolean: Boolean, + ) { + public constructor( + name: String, + rank: Int, + world: Int, + discardedBoolean: Boolean, + ) : this( + name, + rank.toByte(), + world.toUShort(), + discardedBoolean, + ) + + public constructor( + name: String, + rank: Int, + world: Int, + ) : this( + name, + rank.toByte(), + world.toUShort(), + false, + ) + + public val rank: Int + get() = _rank.toInt() + public val world: Int + get() = _world.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ClanMember + + if (name != other.name) return false + if (_rank != other._rank) return false + if (_world != other._world) return false + if (discardedBoolean != other.discardedBoolean) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _rank + result = 31 * result + _world.hashCode() + result = 31 * result + discardedBoolean.hashCode() + return result + } + + override fun toString(): String = + "ClanMember(" + + "name='$name', " + + "rank=$rank, " + + "world=$world, " + + "discardedBoolean=$discardedBoolean" + + ")" + } + + public companion object { + public const val FLAG_USE_BASE_37_NAMES: Int = 0x1 + public const val FLAG_USE_DISPLAY_NAMES: Int = 0x2 + public const val FLAG_HAS_VERSION: Int = 0x4 + public const val DEFAULT_OLDSCHOOL_VERSION: Int = 2 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanSettingsDelta.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanSettingsDelta.kt new file mode 100644 index 000000000..d5a435abf --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanSettingsDelta.kt @@ -0,0 +1,701 @@ +package net.rsprot.protocol.game.outgoing.clan + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Clan settings delta updates are used to modify a sub-set of this clan's settings. + * @property clanType the type of the clan to modify, e.g. guest or normal, + * @property owner the hash of the owner. + * As the value of this property is never assigned in the client, but it is compared, + * this property should always be assigned the value 0. + * @property updateNum the number of updates this clans settings has had. + * If the value does not match up, the client will throw an exception! + */ +public class ClanSettingsDelta private constructor( + private val _clanType: Byte, + public val owner: Long, + public val updateNum: Int, + public val updates: List, +) : OutgoingGameMessage { + public constructor( + clanType: Int, + owner: Long, + updateNum: Int, + updates: List, + ) : this( + clanType.toByte(), + owner, + updateNum, + updates, + ) + + public val clanType: Int + get() = _clanType.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ClanSettingsDelta + + if (_clanType != other._clanType) return false + if (owner != other.owner) return false + if (updateNum != other.updateNum) return false + if (updates != other.updates) return false + + return true + } + + override fun hashCode(): Int { + var result = _clanType.toInt() + result = 31 * result + owner.hashCode() + result = 31 * result + updateNum + return result + } + + override fun toString(): String = + "ClanSettingsDelta(" + + "clanType=$clanType, " + + "owner=$owner, " + + "updateNum=$updateNum, " + + "updates=$updates" + + ")" + + public sealed interface Update + + /** + * Add banned updates are used to add a member to the banned members list. + * @property hash the hash of the member, or 0 if this clan does not use hashes. + * @property name the name of the member. + */ + public class AddBannedUpdate( + public val hash: Long, + public val name: String?, + ) : Update { + /** + * A secondary constructor for when the clan does not support hashes. + */ + public constructor( + name: String, + ) : this( + 0, + name, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AddBannedUpdate + + if (hash != other.hash) return false + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + var result = hash.hashCode() + result = 31 * result + (name?.hashCode() ?: 0) + return result + } + + override fun toString(): String = + "AddBannedUpdate(" + + "hash=$hash, " + + "name=$name" + + ")" + } + + /** + * Older add-member update for clans. + * @property hash the hash of the member, or 0 if this clan does not use hashes. + * @property name the name of the member. + */ + public class AddMemberV1Update( + public val hash: Long, + public val name: String?, + ) : Update { + /** + * A secondary constructor for when the clan does not support hashes. + */ + public constructor( + name: String, + ) : this( + 0, + name, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AddMemberV1Update + + if (hash != other.hash) return false + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + var result = hash.hashCode() + result = 31 * result + (name?.hashCode() ?: 0) + return result + } + + override fun toString(): String = + "AddMemberV1Update(" + + "hash=$hash, " + + "name=$name" + + ")" + } + + /** + * Newer add-member update for clans. + * @property hash the hash of the member, or 0 if this clan does not use hashes. + * @property name the name of the member. + * @property joinRuneDay the rune day when this user joined the clan + */ + public class AddMemberV2Update private constructor( + public val hash: Long, + public val name: String?, + private val _joinRuneDay: UShort, + ) : Update { + public constructor( + hash: Long, + name: String?, + joinRuneDay: Int, + ) : this( + hash, + name, + joinRuneDay.toUShort(), + ) + + public constructor( + name: String?, + joinRuneDay: Int, + ) : this( + 0, + name, + joinRuneDay.toUShort(), + ) + + public val joinRuneDay: Int + get() = _joinRuneDay.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AddMemberV2Update + + if (hash != other.hash) return false + if (name != other.name) return false + if (_joinRuneDay != other._joinRuneDay) return false + + return true + } + + override fun hashCode(): Int { + var result = hash.hashCode() + result = 31 * result + (name?.hashCode() ?: 0) + result = 31 * result + _joinRuneDay.hashCode() + return result + } + + override fun toString(): String = + "AddMemberV2Update(" + + "hash=$hash, " + + "name=$name, " + + "joinRuneDay=$joinRuneDay" + + ")" + } + + /** + * Base settings updates are used to manage global clan settings, + * such as privileges to use various aspects of this clan. + * @property allowUnaffined whether guest members are allowed to join this clan + * @property talkRank the minimum rank needed to talk within this clan + * @property kickRank the minimum rank needed to kick other members in this clan + * @property lootshareRank the minimum rank needed to toggle lootshare, unused in OldSchool + * @property coinshareRank the minimum rank needed to toggle coinshare, unused in OldSchool + */ + public class BaseSettingsUpdate private constructor( + public val allowUnaffined: Boolean, + private val _talkRank: Byte, + private val _kickRank: Byte, + private val _lootshareRank: Byte, + private val _coinshareRank: Byte, + ) : Update { + public constructor( + allowUnaffined: Boolean, + talkRank: Int, + kickRank: Int, + lootshareRank: Int, + coinshareRank: Int, + ) : this( + allowUnaffined, + talkRank.toByte(), + kickRank.toByte(), + lootshareRank.toByte(), + coinshareRank.toByte(), + ) + + public val talkRank: Int + get() = _talkRank.toInt() + public val kickRank: Int + get() = _kickRank.toInt() + public val lootshareRank: Int + get() = _lootshareRank.toInt() + public val coinshareRank: Int + get() = _coinshareRank.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BaseSettingsUpdate + + if (allowUnaffined != other.allowUnaffined) return false + if (_talkRank != other._talkRank) return false + if (_kickRank != other._kickRank) return false + if (_lootshareRank != other._lootshareRank) return false + if (_coinshareRank != other._coinshareRank) return false + + return true + } + + override fun hashCode(): Int { + var result = allowUnaffined.hashCode() + result = 31 * result + _talkRank + result = 31 * result + _kickRank + result = 31 * result + _lootshareRank + result = 31 * result + _coinshareRank + return result + } + + override fun toString(): String = + "BaseSettingsUpdate(" + + "allowUnaffined=$allowUnaffined, " + + "talkRank=$talkRank, " + + "kickRank=$kickRank, " + + "lootshareRank=$lootshareRank, " + + "coinshareRank=$coinshareRank" + + ")" + } + + /** + * Delete banned member updates are used to remove existing banned members + * from the list of banned users. + * @property index the index of the user in the banned members list. + */ + public class DeleteBannedUpdate( + public val index: Int, + ) : Update { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DeleteBannedUpdate + + return index == other.index + } + + override fun hashCode(): Int = index + + override fun toString(): String = "DeleteBannedUpdate(index=$index)" + } + + /** + * Delete member updates are used to remove members from this clan. + * @property index the index of this member within the clan's member list. + */ + public class DeleteMemberUpdate( + public val index: Int, + ) : Update { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DeleteMemberUpdate + + return index == other.index + } + + override fun hashCode(): Int = index + + override fun toString(): String = "DeleteMemberUpdate(index=$index)" + } + + /** + * Set member rank update is used to modify a given clan member's privileges + * within the clan. + * @property index the index of this member within the clan's member list. + * @property rank the new rank to assign to that member. + */ + public class SetMemberRankUpdate private constructor( + private val _index: UShort, + private val _rank: Byte, + ) : Update { + public constructor( + index: Int, + rank: Int, + ) : this( + index.toUShort(), + rank.toByte(), + ) + + public val index: Int + get() = _index.toInt() + public val rank: Int + get() = _rank.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetMemberRankUpdate + + if (_index != other._index) return false + if (_rank != other._rank) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + _rank + return result + } + + override fun toString(): String = + "SetMemberRankUpdate(" + + "index=$index, " + + "rank=$rank" + + ")" + } + + /** + * Set member extra info is used to modify extra info about a member in the clan, + * by modifying the provided bit range of the 32-bit integer that each + * member has. + * @property index the index of this member in the clan's members list. + * @property value the value to assign to the provided bit range + * @property startBit the start bit of the bit range to update + * @property endBit the end bit of the bit range to update + */ + public class SetMemberExtraInfoUpdate private constructor( + private val _index: UShort, + public val value: Int, + private val _startBit: UByte, + private val _endBit: UByte, + ) : Update { + public constructor( + index: Int, + value: Int, + startBit: Int, + endBit: Int, + ) : this( + index.toUShort(), + value, + startBit.toUByte(), + endBit.toUByte(), + ) + + public val index: Int + get() = _index.toInt() + public val startBit: Int + get() = _startBit.toInt() + public val endBit: Int + get() = _endBit.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetMemberExtraInfoUpdate + + if (_index != other._index) return false + if (value != other.value) return false + if (_startBit != other._startBit) return false + if (_endBit != other._endBit) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + value + result = 31 * result + _startBit.hashCode() + result = 31 * result + _endBit.hashCode() + return result + } + + override fun toString(): String = + "SetMemberExtraInfoUpdate(" + + "index=$index, " + + "value=$value, " + + "startBit=$startBit, " + + "endBit=$endBit" + + ")" + } + + /** + * Set member muted updates are used to mute or unmute members of this clan. + * @property index the index of this member within the clan's member list. + * @property muted whether to set the member muted or unmuted. + */ + public class SetMemberMutedUpdate private constructor( + private val _index: UShort, + public val muted: Boolean, + ) : Update { + public constructor( + index: Int, + muted: Boolean, + ) : this( + index.toUShort(), + muted, + ) + + public val index: Int + get() = _index.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetMemberMutedUpdate + + if (_index != other._index) return false + if (muted != other.muted) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + muted.hashCode() + return result + } + + override fun toString(): String = + "SetMemberMutedUpdate(" + + "index=$index, " + + "muted=$muted" + + ")" + } + + /** + * Int setting updates are used to modify the value of an integer-based + * setting of this clan. + * @property setting the id of the setting to modify, a 30-bit integer. + * @property value the 32-bit integer value to assign to that setting. + */ + public class SetIntSettingUpdate( + public val setting: Int, + public val value: Int, + ) : Update { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetIntSettingUpdate + + if (setting != other.setting) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = setting + result = 31 * result + value + return result + } + + override fun toString(): String = + "SetIntSettingUpdate(" + + "setting=$setting, " + + "value=$value" + + ")" + } + + /** + * Long setting updates are used to modify the value of a long-based + * setting of this clan. + * @property setting the id of the setting to modify, a 30-bit integer. + * @property value the 64-bit long value to assign to that setting. + */ + public class SetLongSettingUpdate( + public val setting: Int, + public val value: Long, + ) : Update { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetLongSettingUpdate + + if (setting != other.setting) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = setting + result = 31 * result + value.hashCode() + return result + } + + override fun toString(): String = + "SetLongSettingUpdate(" + + "setting=$setting, " + + "value=$value" + + ")" + } + + /** + * String setting updates are used to modify the values of string settings + * within the clan. + * @property setting the id of the setting to modify, a 30-bit integer. + * @property value the string value to assign to that setting. + */ + public class SetStringSettingUpdate( + public val setting: Int, + public val value: String, + ) : Update { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetStringSettingUpdate + + if (setting != other.setting) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = setting + result = 31 * result + value.hashCode() + return result + } + + override fun toString(): String = + "SetStringSettingUpdate(" + + "setting=$setting, " + + "value='$value'" + + ")" + } + + /** + * Varbit setting updates are used to modify a bit-range of an integer-based + * setting of this clan. + * @property setting the id of the setting to modify, a 30-bit integer. + * @property value the new value to assign to the provided bit range. + * @property startBit the start bit of the bit range to modify + * @property endBit the end bit of the bit range ot modify + */ + public class SetVarbitSettingUpdate private constructor( + public val setting: Int, + public val value: Int, + private val _startBit: UByte, + private val _endBit: UByte, + ) : Update { + public constructor( + setting: Int, + value: Int, + startBit: Int, + endBit: Int, + ) : this( + setting, + value, + startBit.toUByte(), + endBit.toUByte(), + ) + + public val startBit: Int + get() = _startBit.toInt() + public val endBit: Int + get() = _endBit.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetVarbitSettingUpdate + + if (setting != other.setting) return false + if (value != other.value) return false + if (_startBit != other._startBit) return false + if (_endBit != other._endBit) return false + + return true + } + + override fun hashCode(): Int { + var result = setting + result = 31 * result + value + result = 31 * result + _startBit.hashCode() + result = 31 * result + _endBit.hashCode() + return result + } + + override fun toString(): String = + "SetVarbitSettingUpdate(" + + "setting=$setting, " + + "value=$value, " + + "startBit=$startBit, " + + "endBit=$endBit" + + ")" + } + + /** + * Clan name updates are used to modify the name of the clan. + * @property clanName the new clan name to assign to this clan. + */ + public class SetClanNameUpdate( + public val clanName: String, + ) : Update { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetClanNameUpdate + + return clanName == other.clanName + } + + override fun hashCode(): Int = clanName.hashCode() + + override fun toString(): String = "SetClanNameUpdate(clanName='$clanName')" + } + + /** + * Clan owner updates are used to assign a new owner to this clan. + * @property index the index of the new owner in the clan's members list. + */ + public class SetClanOwnerUpdate( + public val index: Int, + ) : Update { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetClanOwnerUpdate + + return index == other.index + } + + override fun hashCode(): Int = index + + override fun toString(): String = "SetClanOwnerUpdate(index=$index)" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanSettingsFull.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanSettingsFull.kt new file mode 100644 index 000000000..faf60158f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/ClanSettingsFull.kt @@ -0,0 +1,469 @@ +package net.rsprot.protocol.game.outgoing.clan + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Clan settings full packet is used to update the clan's primary settings. + * @property clanType the clan being updated + * @property update the clan settings update to be performed + */ +public class ClanSettingsFull private constructor( + private val _clanType: Byte, + public val update: Update, +) : OutgoingGameMessage { + public constructor( + clanType: Int, + update: Update, + ) : this( + clanType.toByte(), + update, + ) + + public val clanType: Int + get() = _clanType.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun toString(): String = + "ClanSettingsFull(" + + "update=$update, " + + "clanType=$clanType" + + ")" + + public sealed interface Update + + /** + * Clan settings full join update is used to make the player join a clan. + * @property updateNum the number of changes done to this clan's settings + * @property creationTime the epoch time minute when the clan was created + * @property clanName the name of the clan + * @property allowUnaffined whether to allow guests to join the clan + * @property talkRank the minimum rank needed to talk in this clan chat + * @property kickRank the minimum rank needed to kick members from this clan + * @property lootshareRank the minimum rank needed to toggle lootshare, unused in OldSchool. + * @property coinshareRank the minimum rank needed to toggle coinshare, unused in OldSchool. + * @property affinedMembers the list of affined members in this clan + * @property bannedMembers the list of banned members in this clan + * @property settings the list of settings to apply to this clan + */ + public class JoinUpdate private constructor( + private val _flags: UByte, + public val updateNum: Int, + public val creationTime: Int, + public val clanName: String, + public val allowUnaffined: Boolean, + private val _talkRank: Byte, + private val _kickRank: Byte, + private val _lootshareRank: Byte, + private val _coinshareRank: Byte, + public val affinedMembers: List, + public val bannedMembers: List, + public val settings: List, + ) : Update { + public constructor( + updateNum: Int, + creationTime: Int, + clanName: String, + allowUnaffined: Boolean, + talkRank: Int, + kickRank: Int, + lootshareRank: Int, + coinshareRank: Int, + affinedMembers: List, + bannedMembers: List, + settings: List, + hasAffinedHashes: Boolean = false, + hasAffinedDisplayNames: Boolean = true, + ) : this( + (if (hasAffinedHashes) FLAG_HAS_AFFINED_HASHES else 0) + .or(if (hasAffinedDisplayNames) FLAG_HAS_AFFINED_DISPLAY_NAMES else 0) + .toUByte(), + updateNum, + creationTime, + clanName, + allowUnaffined, + talkRank.toByte(), + kickRank.toByte(), + lootshareRank.toByte(), + coinshareRank.toByte(), + affinedMembers, + bannedMembers, + settings, + ) + + init { + require(affinedMembers.size <= 0xFFFF) { + "Affined member count cannot exceed 65535" + } + require(bannedMembers.size <= 0xFF) { + "Banned member count cannot exceed 255" + } + } + + public val flags: Int + get() = _flags.toInt() + public val talkRank: Int + get() = _talkRank.toInt() + public val kickRank: Int + get() = _kickRank.toInt() + public val lootshareRank: Int + get() = _lootshareRank.toInt() + public val coinshareRank: Int + get() = _coinshareRank.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as JoinUpdate + + if (_flags != other._flags) return false + if (updateNum != other.updateNum) return false + if (creationTime != other.creationTime) return false + if (clanName != other.clanName) return false + if (allowUnaffined != other.allowUnaffined) return false + if (_talkRank != other._talkRank) return false + if (_kickRank != other._kickRank) return false + if (_lootshareRank != other._lootshareRank) return false + if (_coinshareRank != other._coinshareRank) return false + if (affinedMembers != other.affinedMembers) return false + if (bannedMembers != other.bannedMembers) return false + if (settings != other.settings) return false + + return true + } + + override fun hashCode(): Int { + var result = _flags.hashCode() + result = 31 * result + updateNum + result = 31 * result + creationTime + result = 31 * result + clanName.hashCode() + result = 31 * result + allowUnaffined.hashCode() + result = 31 * result + _talkRank + result = 31 * result + _kickRank + result = 31 * result + _lootshareRank + result = 31 * result + _coinshareRank + result = 31 * result + affinedMembers.hashCode() + result = 31 * result + bannedMembers.hashCode() + result = 31 * result + settings.hashCode() + return result + } + + override fun toString(): String = + "JoinUpdate(" + + "flags=$flags, " + + "updateNum=$updateNum, " + + "creationTime=$creationTime, " + + "clanName='$clanName', " + + "allowUnaffined=$allowUnaffined, " + + "talkRank=$talkRank, " + + "kickRank=$kickRank, " + + "lootshareRank=$lootshareRank, " + + "coinshareRank=$coinshareRank, " + + "affinedMembers=$affinedMembers, " + + "bannedMembers=$bannedMembers, " + + "settings=$settings" + + ")" + } + + public data object LeaveUpdate : Update + + public sealed interface ClanMember + + /** + * An affined clan member is someone who has joined the clan permanently, + * e.g. not as a guest. + * @property hash the 64-bit hash of this member. + * @property name the name of this member. + * @property rank this member's rank in this clan + * @property extraInfo extra information bitpacked into an integer, to be read and used + * within clientscripts. + * @property joinRuneDay the rune day when the member joined this clan + * @property muted whether this member is muted in this clan. + */ + public class AffinedClanMember private constructor( + public val hash: Long, + public val name: String?, + private val _rank: Byte, + public val extraInfo: Int, + private val _joinRuneDay: UShort, + public val muted: Boolean, + ) : ClanMember { + /** + * Constructor for when the hash and name are both being transmitted. + */ + public constructor( + hash: Long, + name: String, + rank: Int, + extraInfo: Int, + joinRuneDay: Int, + muted: Boolean, + ) : this( + hash, + name, + rank.toByte(), + extraInfo, + joinRuneDay.toUShort(), + muted, + ) + + /** + * Constructor for when only the name, and no hashes are being transmitted. + */ + public constructor( + name: String, + rank: Int, + extraInfo: Int, + joinRuneDay: Int, + muted: Boolean, + ) : this( + 0, + name, + rank.toByte(), + extraInfo, + joinRuneDay.toUShort(), + muted, + ) + + /** + * Constructor for when only the hash and no name is being transmitted. + */ + public constructor( + hash: Long, + rank: Int, + extraInfo: Int, + joinRuneDay: Int, + muted: Boolean, + ) : this( + hash, + null, + rank.toByte(), + extraInfo, + joinRuneDay.toUShort(), + muted, + ) + + public val rank: Int + get() = _rank.toInt() + public val joinRuneDay: Int + get() = _joinRuneDay.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AffinedClanMember + + if (hash != other.hash) return false + if (name != other.name) return false + if (_rank != other._rank) return false + if (extraInfo != other.extraInfo) return false + if (_joinRuneDay != other._joinRuneDay) return false + if (muted != other.muted) return false + + return true + } + + override fun hashCode(): Int { + var result = hash.hashCode() + result = 31 * result + (name?.hashCode() ?: 0) + result = 31 * result + _rank + result = 31 * result + extraInfo + result = 31 * result + _joinRuneDay.hashCode() + result = 31 * result + muted.hashCode() + return result + } + + override fun toString(): String = + "AffinedClanMember(" + + "hash=$hash, " + + "name=$name, " + + "rank=$rank, " + + "extraInfo=$extraInfo, " + + "joinRuneDay=$joinRuneDay, " + + "muted=$muted" + + ")" + } + + /** + * A banned clan member is someone who has joined the clan permanently, + * but has been banned from it. + * @property hash the 64-bit hash of this member. + * @property name the name of this member. + */ + public class BannedClanMember( + public val hash: Long, + public val name: String?, + ) : ClanMember { + public constructor( + hash: Long, + ) : this( + hash, + null, + ) + + public constructor( + name: String, + ) : this( + 0, + name, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as BannedClanMember + + if (hash != other.hash) return false + if (name != other.name) return false + + return true + } + + override fun hashCode(): Int { + var result = hash.hashCode() + result = 31 * result + (name?.hashCode() ?: 0) + return result + } + + override fun toString(): String = + "BannedClanMember(" + + "hash=$hash, " + + "name=$name" + + ")" + } + + public sealed interface ClanSetting + + /** + * Integer-value based clan setting + * @property id the id the of clan setting. + * Note that the last two bits(including the sign bit) may not be used. + * @property value the value of this setting, a 32-bit integer. + */ + public class IntClanSetting( + public val id: Int, + public val value: Int, + ) : ClanSetting { + init { + require(id and 0x3FFFFFFF.inv() == 0) { + "Id cannot be larger than 30 bits" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IntClanSetting + + if (id != other.id) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = id + result = 31 * result + value + return result + } + + override fun toString(): String = + "IntClanSetting(" + + "id=$id, " + + "value=$value" + + ")" + } + + /** + * Long-value based clan setting + * @property id the id the of clan setting. + * Note that the last two bits(including the sign bit) may not be used. + * @property value the value of this setting, a 64-bit long. + */ + public class LongClanSetting( + public val id: Int, + public val value: Long, + ) : ClanSetting { + init { + require(id and 0x3FFFFFFF.inv() == 0) { + "Id cannot be larger than 30 bits" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LongClanSetting + + if (id != other.id) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = id + result = 31 * result + value.hashCode() + return result + } + + override fun toString(): String = + "LongClanSetting(" + + "id=$id, " + + "value=$value" + + ")" + } + + /** + * String-value based clan setting + * @property id the id the of clan setting. + * Note that the last two bits(including the sign bit) may not be used. + * @property value the value of this setting, a string. + */ + public class StringClanSetting( + public val id: Int, + public val value: String, + ) : ClanSetting { + init { + require(id and 0x3FFFFFFF.inv() == 0) { + "Id cannot be larger than 30 bits" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as StringClanSetting + + if (id != other.id) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = id + result = 31 * result + value.hashCode() + return result + } + + override fun toString(): String = + "StringClanSetting(" + + "id=$id, " + + "value='$value'" + + ")" + } + + public companion object { + public const val FLAG_HAS_AFFINED_HASHES: Int = 0x1 + public const val FLAG_HAS_AFFINED_DISPLAY_NAMES: Int = 0x2 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/MessageClanChannel.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/MessageClanChannel.kt new file mode 100644 index 000000000..581c3a33e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/MessageClanChannel.kt @@ -0,0 +1,104 @@ +package net.rsprot.protocol.game.outgoing.clan + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Message clan channel is used to send messages within a clan channel + * that the player is in. + * @property clanType the type of the clan the player is in + * @property name the name of the player sending the message + * @property worldId the id of the world from which the message is sent + * @property worldMessageCounter the world-local message counter. + * Each world must have its own message counter which is used to create + * a unique id for each message. This message counter must be + * incrementing with each message that is sent out. + * If two messages share the same unique id (which is a combination of + * the [worldId] and the [worldMessageCounter] properties), + * the client will not render the second message if it already has one + * received in the last 100 messages. + * It is additionally worth noting that servers with low population + * should probably not start the counter at the same value with each + * game boot, as the probability of multiple messages coinciding + * is relatively high in that scenario, given the low quantity of + * messages sent out to begin with. + * Additionally, only the first 24 bits of the counter are utilized, + * meaning a value from 0 to 16,777,215 (inclusive). + * A good starting point for message counting would be to take the + * hour of the year and multiply it by 50,000 when the server boots + * up. This means the roll-over happens roughly after every two weeks. + * Fine-tuning may be used to make it more granular, but the overall + * idea remains the same. + * @property chatCrownType the chat crown type to be rendered next to the name + * @property message the message to send + */ +public class MessageClanChannel private constructor( + private val _clanType: Byte, + public val name: String, + private val _worldId: UShort, + public val worldMessageCounter: Int, + private val _chatCrownType: UByte, + public val message: String, +) : OutgoingGameMessage { + public constructor( + clanType: Int, + name: String, + worldId: Int, + worldMessageCounter: Int, + chatCrownType: Int, + message: String, + ) : this( + clanType.toByte(), + name, + worldId.toUShort(), + worldMessageCounter, + chatCrownType.toUByte(), + message, + ) + + public val clanType: Int + get() = _clanType.toInt() + public val worldId: Int + get() = _worldId.toInt() + public val chatCrownType: Int + get() = _chatCrownType.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessageClanChannel + + if (_clanType != other._clanType) return false + if (name != other.name) return false + if (_worldId != other._worldId) return false + if (worldMessageCounter != other.worldMessageCounter) return false + if (_chatCrownType != other._chatCrownType) return false + if (message != other.message) return false + + return true + } + + override fun hashCode(): Int { + var result = _clanType.toInt() + result = 31 * result + name.hashCode() + result = 31 * result + _worldId.hashCode() + result = 31 * result + worldMessageCounter + result = 31 * result + _chatCrownType.hashCode() + result = 31 * result + message.hashCode() + return result + } + + override fun toString(): String = + "MessageClanChannel(" + + "clanType=$clanType, " + + "name='$name', " + + "worldId=$worldId, " + + "worldLocalCounter=$worldMessageCounter, " + + "chatCrownType=$chatCrownType, " + + "message='$message'" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/MessageClanChannelSystem.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/MessageClanChannelSystem.kt new file mode 100644 index 000000000..0e77a5632 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/MessageClanChannelSystem.kt @@ -0,0 +1,88 @@ +package net.rsprot.protocol.game.outgoing.clan + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Message clan channel system is used to send system messages + * within a clan channel that the player is in + * @property clanType the type of the clan the player is in + * @property worldId the id of the world from which the message is sent + * @property worldMessageCounter the world-local message counter. + * Each world must have its own message counter which is used to create + * a unique id for each message. This message counter must be + * incrementing with each message that is sent out. + * If two messages share the same unique id (which is a combination of + * the [worldId] and the [worldMessageCounter] properties), + * the client will not render the second message if it already has one + * received in the last 100 messages. + * It is additionally worth noting that servers with low population + * should probably not start the counter at the same value with each + * game boot, as the probability of multiple messages coinciding + * is relatively high in that scenario, given the low quantity of + * messages sent out to begin with. + * Additionally, only the first 24 bits of the counter are utilized, + * meaning a value from 0 to 16,777,215 (inclusive). + * A good starting point for message counting would be to take the + * hour of the year and multiply it by 50,000 when the server boots + * up. This means the roll-over happens roughly after every two weeks. + * Fine-tuning may be used to make it more granular, but the overall + * idea remains the same. + * @property message the message to send + */ +public class MessageClanChannelSystem private constructor( + private val _clanType: Byte, + private val _worldId: UShort, + public val worldMessageCounter: Int, + public val message: String, +) : OutgoingGameMessage { + public constructor( + clanType: Int, + worldId: Int, + worldMessageCounter: Int, + message: String, + ) : this( + clanType.toByte(), + worldId.toUShort(), + worldMessageCounter, + message, + ) + + public val clanType: Int + get() = _clanType.toInt() + public val worldId: Int + get() = _worldId.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessageClanChannelSystem + + if (_clanType != other._clanType) return false + if (_worldId != other._worldId) return false + if (worldMessageCounter != other.worldMessageCounter) return false + if (message != other.message) return false + + return true + } + + override fun hashCode(): Int { + var result = _clanType.toInt() + result = 31 * result + _worldId.hashCode() + result = 31 * result + worldMessageCounter + result = 31 * result + message.hashCode() + return result + } + + override fun toString(): String = + "MessageClanChannelSystem(" + + "clanType=$clanType, " + + "worldId=$worldId, " + + "worldLocalCounter=$worldMessageCounter, " + + "message='$message'" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClan.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClan.kt new file mode 100644 index 000000000..e17e2dd56 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClan.kt @@ -0,0 +1,125 @@ +package net.rsprot.protocol.game.outgoing.clan + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Var clans are used to transmit a variable of a clan to the user. + * It is important to note that the data type must align with what + * is defined in the cache, or the client will not be decoding it + * correctly, which will most likely lead to a disconnection. + * @property id the id of the varclan + * @property value the varclan data value. + * Use [VarClanIntData], [VarClanLongData] or [VarClanStringData] to + * transmit the payload, depending on the defined type in the cache. + */ +public class VarClan private constructor( + private val _id: UShort, + public val value: VarClanData, +) : OutgoingGameMessage { + public constructor( + id: Int, + value: VarClanData, + ) : this( + id.toUShort(), + value, + ) + + public val id: Int + get() = _id.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as VarClan + + if (_id != other._id) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + value.hashCode() + return result + } + + override fun toString(): String = + "VarClan(" + + "id=$id, " + + "value=$value" + + ")" + + public sealed interface VarClanData + + /** + * Var clan int data is used to transmit a 32-bit integer as a varclan + * value. + * @property value the 32-bit integer value for this varclan. + */ + public class VarClanIntData( + public val value: Int, + ) : VarClanData { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as VarClanIntData + + return value == other.value + } + + override fun hashCode(): Int = value + + override fun toString(): String = "VarClanIntData(value=$value)" + } + + /** + * Var clan int data is used to transmit a 64-bit long as a varclan + * value. + * @property value the 64-bit long value for this varclan. + */ + public class VarClanLongData( + public val value: Long, + ) : VarClanData { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as VarClanLongData + + return value == other.value + } + + override fun hashCode(): Int = value.hashCode() + + override fun toString(): String = "VarClanLongData(value=$value)" + } + + /** + * Var clan int data is used to transmit a string as a varclan + * value. + * @property value the string for this varclan. + */ + public class VarClanStringData( + public val value: String, + ) : VarClanData { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as VarClanStringData + + return value == other.value + } + + override fun hashCode(): Int = value.hashCode() + + override fun toString(): String = "VarClanStringData(value='$value')" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClanDisable.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClanDisable.kt new file mode 100644 index 000000000..2a3eee6da --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClanDisable.kt @@ -0,0 +1,14 @@ +package net.rsprot.protocol.game.outgoing.clan + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Var clan disable packet is used to clear out a var domain + * in the client, intended to be sent as the player leaves a clan. + */ +public data object VarClanDisable : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClanEnable.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClanEnable.kt new file mode 100644 index 000000000..ed10bd113 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/clan/VarClanEnable.kt @@ -0,0 +1,14 @@ +package net.rsprot.protocol.game.outgoing.clan + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Var clan enable packet is used to initialize a new var domain + * in the client, intended to be sent as the player joins a clan. + */ +public data object VarClanEnable : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/MessageFriendChannel.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/MessageFriendChannel.kt new file mode 100644 index 000000000..126468045 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/MessageFriendChannel.kt @@ -0,0 +1,106 @@ +package net.rsprot.protocol.game.outgoing.friendchat + +import net.rsprot.compression.Base37 +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Message friendchannel is used to transmit messages within a friend + * chat channel. + * @property sender the name of the player who is sending the message + * @property channelName the name of the friend chat channel + * @property worldMessageCounter the world-local message counter. + * Each world must have its own message counter which is used to create + * a unique id for each message. This message counter must be + * incrementing with each message that is sent out. + * If two messages share the same unique id (which is a combination of + * the [worldId] and the [worldMessageCounter] properties), + * the client will not render the second message if it already has one + * received in the last 100 messages. + * It is additionally worth noting that servers with low population + * should probably not start the counter at the same value with each + * game boot, as the probability of multiple messages coinciding + * is relatively high in that scenario, given the low quantity of + * messages sent out to begin with. + * Additionally, only the first 24 bits of the counter are utilized, + * meaning a value from 0 to 16,777,215 (inclusive). + * A good starting point for message counting would be to take the + * hour of the year and multiply it by 50,000 when the server boots + * up. This means the roll-over happens roughly after every two weeks. + * Fine-tuning may be used to make it more granular, but the overall + * idea remains the same. + * @property chatCrownType the id of the crown to render next to the + * name of the sender. + * @property message the message to be sent in the friend chat + * channel. + */ +public class MessageFriendChannel private constructor( + public val sender: String, + public val channelNameBase37: Long, + private val _worldId: UShort, + public val worldMessageCounter: Int, + private val _chatCrownType: UByte, + public val message: String, +) : OutgoingGameMessage { + public constructor( + sender: String, + channelName: String, + worldId: Int, + worldMessageCounter: Int, + chatCrownType: Int, + message: String, + ) : this( + sender, + Base37.encode(channelName), + worldId.toUShort(), + worldMessageCounter, + chatCrownType.toUByte(), + message, + ) + + public val channelName: String + get() = Base37.decodeWithCase(channelNameBase37) + public val worldId: Int + get() = _worldId.toInt() + public val chatCrownType: Int + get() = _chatCrownType.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessageFriendChannel + + if (sender != other.sender) return false + if (channelNameBase37 != other.channelNameBase37) return false + if (_worldId != other._worldId) return false + if (worldMessageCounter != other.worldMessageCounter) return false + if (_chatCrownType != other._chatCrownType) return false + if (message != other.message) return false + + return true + } + + override fun hashCode(): Int { + var result = sender.hashCode() + result = 31 * result + channelNameBase37.hashCode() + result = 31 * result + _worldId.hashCode() + result = 31 * result + worldMessageCounter + result = 31 * result + _chatCrownType.hashCode() + result = 31 * result + message.hashCode() + return result + } + + override fun toString(): String = + "MessageFriendChannel(" + + "sender='$sender', " + + "channelName='$channelName', " + + "worldId=$worldId, " + + "worldMessageCounter=$worldMessageCounter, " + + "chatCrownType=$chatCrownType, " + + "message='$message'" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFull.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFull.kt new file mode 100644 index 000000000..e2ad83af0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFull.kt @@ -0,0 +1,69 @@ +package net.rsprot.protocol.game.outgoing.friendchat + +public sealed class UpdateFriendChatChannelFull { + public abstract val channelOwner: String + public abstract val channelName: String + public abstract val kickRank: Int + public abstract val entries: List + + /** + * A class to contain all the properties of a player in a friend chat. + * @property name the name of the player that is in the friend chat + * @property worldId the id of the world in which the given user is + * @property rank the rank of the given used in this friend chat + * @property worldName world name, unused in OldSchool RuneScape. + */ + public class FriendChatEntry private constructor( + public val name: String, + private val _worldId: UShort, + private val _rank: Byte, + public val worldName: String, + ) { + public constructor( + name: String, + worldId: Int, + rank: Int, + worldName: String, + ) : this( + name, + worldId.toUShort(), + rank.toByte(), + worldName, + ) + + public val worldId: Int + get() = _worldId.toInt() + public val rank: Int + get() = _rank.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FriendChatEntry + + if (name != other.name) return false + if (_worldId != other._worldId) return false + if (_rank != other._rank) return false + if (worldName != other.worldName) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _worldId.hashCode() + result = 31 * result + _rank.hashCode() + result = 31 * result + worldName.hashCode() + return result + } + + override fun toString(): String = + "FriendChatEntry(" + + "name='$name', " + + "worldId=$worldId, " + + "rank=$rank, " + + "worldName='$worldName'" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFullV1.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFullV1.kt new file mode 100644 index 000000000..78bf8fd56 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFullV1.kt @@ -0,0 +1,98 @@ +package net.rsprot.protocol.game.outgoing.friendchat + +import net.rsprot.compression.Base37 +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update friendchat channel full V1 is used to send full channel updates + * where the list of entries has a size of 255 or less, as that is + * the maximum theoretical limitation. + */ +public class UpdateFriendChatChannelFullV1( + public val updateType: UpdateType, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun toString(): String = "UpdateFriendChatChannelFullV1(updateType=$updateType)" + + public sealed interface UpdateType + + /** + * Join updates are used to enter a friendchat channel. + * @property channelOwner the name of the player who owns this channel + * @property channelName the name of the friend chat channel. + * This name must be compatible with base-37 encoding, meaning + * it cannot have special symbols, and it must be 12 characters of less. + * @property kickRank the minimum rank id to kick another player from + * the friend chat. + * @property entries the list of friend chat entries to be added. + */ + public class JoinUpdate( + override val channelOwner: String, + public val channelNameBase37: Long, + private val _kickRank: Byte, + override val entries: List, + ) : UpdateFriendChatChannelFull(), + UpdateType { + public constructor( + channelOwner: String, + channelName: String, + kickRank: Int, + entries: List, + ) : this( + channelOwner, + Base37.encode(channelName), + kickRank.toByte(), + entries, + ) { + require(entries.size <= 255) { + "Cannot send more than 255 entries in a channel full V1 update." + } + } + + override val channelName: String + get() = Base37.decode(channelNameBase37) + override val kickRank: Int + get() = _kickRank.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as JoinUpdate + + if (channelOwner != other.channelOwner) return false + if (channelNameBase37 != other.channelNameBase37) return false + if (_kickRank != other._kickRank) return false + if (entries != other.entries) return false + + return true + } + + override fun hashCode(): Int { + var result = channelOwner.hashCode() + result = 31 * result + channelNameBase37.hashCode() + result = 31 * result + _kickRank.hashCode() + result = 31 * result + entries.hashCode() + return result + } + + override fun toString(): String = + "UpdateFriendChatChannelFullV1.JoinUpdate(" + + "channelOwner='$channelOwner', " + + "channelName='$channelName', " + + "kickRank=$kickRank, " + + "entries=$entries" + + ")" + } + + /** + * Leave updates are used to leave a friendchat channel. + */ + public data object LeaveUpdate : UpdateType { + override fun toString(): String = "UpdateFriendChatChannelFullV1.LeaveUpdate()" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFullV2.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFullV2.kt new file mode 100644 index 000000000..48b04b801 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelFullV2.kt @@ -0,0 +1,95 @@ +package net.rsprot.protocol.game.outgoing.friendchat + +import net.rsprot.compression.Base37 +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update friendchat channel full V2 is used to send full channel updates + * where the list of entries has a size of more than 255. + * It can also support sizes below that, but for sizes in range of 128..255, + * it is more efficient by 1 byte to use V1 of this packet. + */ +public class UpdateFriendChatChannelFullV2( + public val updateType: UpdateType, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun toString(): String = "UpdateFriendChatChannelFullV2(updateType=$updateType)" + + public sealed interface UpdateType + + /** + * Join updates are used to enter a friendchat channel. + * @property channelOwner the name of the player who owns this channel + * @property channelName the name of the friend chat channel. + * This name must be compatible with base-37 encoding, meaning + * it cannot have special symbols, and it must be 12 characters of less. + * @property kickRank the minimum rank id to kick another player from + * the friend chat. + * @property entries the list of friend chat entries to be added. + */ + public class JoinUpdate( + override val channelOwner: String, + public val channelNameBase37: Long, + private val _kickRank: Byte, + override val entries: List, + ) : UpdateFriendChatChannelFull(), + UpdateType { + public constructor( + channelOwner: String, + channelName: String, + kickRank: Int, + entries: List, + ) : this( + channelOwner, + Base37.encode(channelName), + kickRank.toByte(), + entries, + ) + + override val channelName: String + get() = Base37.decode(channelNameBase37) + override val kickRank: Int + get() = _kickRank.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as JoinUpdate + + if (channelOwner != other.channelOwner) return false + if (channelNameBase37 != other.channelNameBase37) return false + if (_kickRank != other._kickRank) return false + if (entries != other.entries) return false + + return true + } + + override fun hashCode(): Int { + var result = channelOwner.hashCode() + result = 31 * result + channelNameBase37.hashCode() + result = 31 * result + _kickRank.hashCode() + result = 31 * result + entries.hashCode() + return result + } + + override fun toString(): String = + "UpdateFriendChatChannelFullV2.JoinUpdate(" + + "channelOwner='$channelOwner', " + + "channelName='$channelName', " + + "kickRank=$kickRank, " + + "entries=$entries" + + ")" + } + + /** + * Leave updates are used to leave a friendchat channel. + */ + public data object LeaveUpdate : UpdateType { + override fun toString(): String = "UpdateFriendChatChannelFullV2.LeaveUpdate()" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelSingleUser.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelSingleUser.kt new file mode 100644 index 000000000..dba370bf8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/friendchat/UpdateFriendChatChannelSingleUser.kt @@ -0,0 +1,154 @@ +package net.rsprot.protocol.game.outgoing.friendchat + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update friendchat singleuser is used to perform a change + * to a friend chat for a single user, whether that be + * adding the user to the friend chat, or removing them. + * @property user the user entry being removed or added. + * Use [AddedFriendChatUser] and [RemovedFriendChatUser] + * respectively to perform different updates. + */ +public class UpdateFriendChatChannelSingleUser( + public val user: FriendChatUser, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateFriendChatChannelSingleUser + + return user == other.user + } + + override fun hashCode(): Int = user.hashCode() + + override fun toString(): String = "UpdateFriendChatChannelSingleUser(user=$user)" + + public sealed interface FriendChatUser { + public val name: String + public val worldId: Int + public val rank: Int + } + + /** + * Added friendchat user indicates a single player + * that is being added to the given friend chat channel. + * @property name the name of the player being added to the friend chat + * @property worldId the id of the world in which that player resides + * @property rank the rank of that player in the friend chat + * @property worldName world name, unused in OldSchool RuneScape. + */ + public class AddedFriendChatUser private constructor( + override val name: String, + private val _worldId: UShort, + private val _rank: Byte, + public val worldName: String, + ) : FriendChatUser { + public constructor( + name: String, + worldId: Int, + rank: Int, + string: String, + ) : this( + name, + worldId.toUShort(), + rank.toByte(), + string, + ) { + require(rank != -128) { + "Rank cannot be -128 as that is used to indicate a removed entry." + } + } + + override val worldId: Int + get() = _worldId.toInt() + override val rank: Int + get() = _rank.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AddedFriendChatUser + + if (name != other.name) return false + if (_worldId != other._worldId) return false + if (_rank != other._rank) return false + if (worldName != other.worldName) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _worldId.hashCode() + result = 31 * result + _rank + result = 31 * result + worldName.hashCode() + return result + } + + override fun toString(): String = + "AddedFriendChatUser(" + + "name='$name', " + + "worldId=$worldId, " + + "rank=$rank, " + + "worldName='$worldName'" + + ")" + } + + /** + * Removed friendchat user indicates that a player + * is leaving a friend chat channel. + * @property name the name of the player leaving this friend chat channel + * @property worldId the id of the world in which the player resided. + * Note that the world id must match up or the user will not be removed. + */ + public class RemovedFriendChatUser private constructor( + override val name: String, + private val _worldId: UShort, + ) : FriendChatUser { + public constructor( + name: String, + worldId: Int, + ) : this( + name, + worldId.toUShort(), + ) + + override val worldId: Int + get() = _worldId.toInt() + override val rank: Int + get() = -128 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RemovedFriendChatUser + + if (name != other.name) return false + if (_worldId != other._worldId) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + _worldId.hashCode() + return result + } + + override fun toString(): String = + "RemovedFriendChatUser(" + + "name='$name', " + + "worldId=$worldId" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/AvatarExtendedInfoWriter.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/AvatarExtendedInfoWriter.kt new file mode 100644 index 000000000..daf7fdae4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/AvatarExtendedInfoWriter.kt @@ -0,0 +1,85 @@ +package net.rsprot.protocol.game.outgoing.info + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.ExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.OnDemandExtendedInfoEncoder + +/** + * A base class for client-specific extended info writers. + * @param oldSchoolClientType the client for which the encoders are created. + * @param encoders the set of extended info encoders for the given [oldSchoolClientType]. + */ +public abstract class AvatarExtendedInfoWriter( + public val oldSchoolClientType: OldSchoolClientType, + public val encoders: E, +) { + /** + * Main function to write all the extended info blocks over. + * The extended info blocks must be in the exact order as they are + * read within the client, and this function is responsible + * for converting library-specific-constants to client-specific-flags. + * + * @param buffer the buffer into which to write the extended info block. + * @param localIndex the index of the avatar that owns these extended info blocks. + * @param observerIndex the index of the player observing this avatar. + * @param flag the constant-flag of all the extended info blocks which must be + * translated and written to the buffer. + * @param blocks the wrapper class around all the extended info blocks. + * The blocks which are flagged will be written over. + */ + public abstract fun pExtendedInfo( + buffer: JagByteBuf, + localIndex: Int, + observerIndex: Int, + flag: Int, + blocks: B, + ) + + /** + * Natively copies cached data from the pre-computed extended info buffer over + * into the primary player info buffer. + * @param buffer the primary player info buffer. + * @param block the extended info block which to copy over. + * @throws IllegalStateException if the given buffer has not been precomputed + * for the given client type. + */ + protected fun pCachedData( + buffer: JagByteBuf, + block: ExtendedInfo<*, *>, + ) { + val precomputed = + checkNotNull(block.getBuffer(oldSchoolClientType)) { + "Buffer has not been computed on client $oldSchoolClientType, ${block.javaClass.name}" + } + buffer.buffer.writeBytes(precomputed, precomputed.readerIndex(), precomputed.readableBytes()) + } + + /** + * Writes on-demand extended info block. This is for extended info blocks which + * cannot be pre-computed as they depend on the observer for information, + * such as tinted hitmarks. + * @param buffer the primary player info buffer. + * @param localIndex the index of the avatar that owns this extended info block. + * @param block the extended info block to compute and write into the primary buffer. + * @param observerIndex the index of the avatar observing the avatar who owns this + * extended info block. + */ + protected fun , E : OnDemandExtendedInfoEncoder> pOnDemandData( + buffer: JagByteBuf, + localIndex: Int, + block: T, + observerIndex: Int, + ) { + val encoder = + checkNotNull(block.getEncoder(oldSchoolClientType)) { + "Encoder has not been set for client $oldSchoolClientType" + } + encoder.encode( + buffer, + observerIndex, + localIndex, + block, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/InfoRepository.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/InfoRepository.kt new file mode 100644 index 000000000..24d6bf64d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/InfoRepository.kt @@ -0,0 +1,177 @@ +package net.rsprot.protocol.game.outgoing.info + +import net.rsprot.protocol.common.client.OldSchoolClientType +import java.lang.ref.ReferenceQueue +import java.lang.ref.SoftReference + +/** + * The info repository class is responsible for allocating and re-using various info implementations. + */ +@Suppress("DuplicatedCode") +internal abstract class InfoRepository( + private val allocator: (index: Int, oldSchoolClientType: OldSchoolClientType) -> T, +) { + /** + * The backing elements array used to store currently-in-use objects. + */ + protected abstract val elements: Array + + /** + * The reference queue used to store soft references of the objects after they have been + * returned to this structure. The references may release their object if the JVM + * requires that memory, but only as a last resort, before having to throw an + * out of memory exception. + */ + private val queue: ReferenceQueue = ReferenceQueue() + + /** + * Gets the current element at index [idx], or null if it doesn't exist. + * @param idx the index of the player info object to obtain + * @throws ArrayIndexOutOfBoundsException if the index is below zero, or above [capacity]. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + fun getOrNull(idx: Int): T? = elements[idx] + + /** + * Gets the current element at index [idx]. + * @param idx the index of the player info object to obtain + * @throws ArrayIndexOutOfBoundsException if the index is below zero, or above [capacity]. + * @throws IllegalStateException if the element at index [idx] is null. + */ + @Throws( + ArrayIndexOutOfBoundsException::class, + IllegalStateException::class, + ) + operator fun get(idx: Int): T = checkNotNull(elements[idx]) + + /** + * Gets the maximum capacity of this object array. + */ + fun capacity(): Int = elements.size + + /** + * Allocates a new element at the specified [idx]. + * This function will first check if there are any unused objects + * left in the [queue]. If there are, obtains the reference and executes + * [net.rsprot.protocol.game.outgoing.info.util.ReferencePooledObject.onAlloc] in it, + * which is responsible for cleaning the object so that it can be re-used again. + * This is preferably done on allocations, rather than de-allocations, + * as there's a chance the JVM will just garbage collect + * the object without it ever being re-allocated. + * + * @param idx the index of the element to obtain. + * @throws ArrayIndexOutOfBoundsException if the [idx] is below zero, or above [capacity]. + * @throws IllegalStateException if the element at index [idx] is already in use. + */ + @Throws( + ArrayIndexOutOfBoundsException::class, + IllegalStateException::class, + ) + fun alloc( + idx: Int, + oldSchoolClientType: OldSchoolClientType, + ): T { + val element = elements[idx] + check(element == null) { + "Overriding existing element: $idx" + } + val cached = queue.poll()?.get() + if (cached != null) { + onAlloc(cached, idx, oldSchoolClientType, false) + elements[idx] = cached + return cached + } + val new = allocator(idx, oldSchoolClientType) + onAlloc(new, idx, oldSchoolClientType, true) + elements[idx] = new + return new + } + + /** + * The onAlloc function is called when a new element is allocated, necessary to clean up the + * object before it may be used. + * @param element the element being allocated + * @param idx the index of the element + * @param oldSchoolClientType the client on which the info is being allocated. + */ + protected abstract fun onAlloc( + element: T, + idx: Int, + oldSchoolClientType: OldSchoolClientType, + newInstance: Boolean, + ) + + /** + * Deallocates the element at [idx], if there is one. + * If an object was found, [net.rsprot.protocol.game.outgoing.info.util.ReferencePooledObject.onDealloc] + * function is called on it. + * This is to clean up any potential memory leaks for objects which may incur such. + * It should not reset indices and other properties, that should be left to be done + * during [alloc]. + * @param idx the index of the element to deallocate. + * @throws ArrayIndexOutOfBoundsException if the [idx] is below zero, or above [capacity]. + * @return true if the object was deallocated, false if there was nothing to deallocate. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + fun dealloc(idx: Int): Boolean { + require(idx in elements.indices) { + "Index out of boundaries: $idx, ${elements.indices}" + } + val element = + elements[idx] + ?: return false + try { + onDealloc(element) + } finally { + elements[idx] = null + } + informDeallocation(idx) + val reference = SoftReference(element, queue) + reference.enqueue() + return true + } + + /** + * Destroys the element at [idx], if there is one. + * If an object was found, [net.rsprot.protocol.game.outgoing.info.util.ReferencePooledObject.onDealloc] + * function is called on it. + * This is to clean up any potential memory leaks for objects which may incur such. + * It should not reset indices and other properties, that should be left to be done + * during [alloc]. + * Unlike the [dealloc] function, this function will not put the object back into the pool. + * This is important in case we catch an exception mid-processing, as that will immediately + * destroy the object, which technically means it could be picked up by another player right + * away in an unsafe manner. As such, these objects which threw exceptions must be garbage-collected. + * @param idx the index of the element to deallocate. + * @throws ArrayIndexOutOfBoundsException if the [idx] is below zero, or above [capacity]. + * @return true if the object was deallocated, false if there was nothing to deallocate. + */ + fun destroy(idx: Int): Boolean { + require(idx in elements.indices) { + "Index out of boundaries: $idx, ${elements.indices}" + } + val element = + elements[idx] + ?: return false + try { + onDealloc(element) + } finally { + elements[idx] = null + } + informDeallocation(idx) + return true + } + + /** + * The onDealloc function is called when an element is being deallocated. + * @param element the element being deallocated. + */ + protected abstract fun onDealloc(element: T) + + /** + * Informs all the other avatars of a given avatar being deallocated. + * This is necessary to reset our cached properties (such as the appearance cache) + * of other players. + */ + protected abstract fun informDeallocation(idx: Int) +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/ObserverExtendedInfoFlags.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/ObserverExtendedInfoFlags.kt new file mode 100644 index 000000000..f5a40d86b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/ObserverExtendedInfoFlags.kt @@ -0,0 +1,47 @@ +package net.rsprot.protocol.game.outgoing.info + +/** + * A data structure holding information about observer-dependent extended info flags. + * An example of this would be any extended info blocks that get written when an avatar is + * moved from low resolution to high resolution, in which case we need to synchronize any + * data that was set in the past, such as their appearance, the move speed and + * the face pathingentity status. This additionally includes any extended info blocks which + * were flagged for a specific observer alone, such as tinting utilized in Tombs of Amascut, + * where a single user will see tinting applied to all the other members of the party. + * When setting up the tinting, rather than flagging tinting on the recipient, + * we flag the observer-dependent flag on the receiver of the given extended info block. + */ +internal class ObserverExtendedInfoFlags( + capacity: Int, +) { + /** + * The observer-dependent flags. This array will not include "static" flags. + */ + private val flags: ByteArray = ByteArray(capacity) + + /** + * Resets the observer-dependent flags by filling the array with zeros. + */ + fun reset() { + flags.fill(0) + } + + /** + * Appends the given [flag] for avatar at index [index]. + * @param index the index of the recipient player or npc + * @param flag the bit flag to enable + */ + fun addFlag( + index: Int, + flag: Int, + ) { + flags[index] = (flags[index].toInt() or flag).toByte() + } + + /** + * Gets the observer-dependent flag of the avatar at index [index] + * @param index the index of the recipient player or npc + * @return the observer-dependent flag value + */ + fun getFlag(index: Int): Int = flags[index].toInt() +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/exceptions/InfoProcessException.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/exceptions/InfoProcessException.kt new file mode 100644 index 000000000..feb9b8f8c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/exceptions/InfoProcessException.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.game.outgoing.info.exceptions + +/** + * An exception that is sent whenever an info packet gets built by the server. + * This exception wraps around another exception that was generated during the processing of + * the respective info packet, allowing servers to properly observe and handle the exception. + * @param message the message associated with the exception + * @param throwable the throwable that was thrown during info processing + */ +public class InfoProcessException( + message: String, + throwable: Throwable, +) : RuntimeException(message, throwable) diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/filter/DefaultExtendedInfoFilter.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/filter/DefaultExtendedInfoFilter.kt new file mode 100644 index 000000000..3db4edbb4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/filter/DefaultExtendedInfoFilter.kt @@ -0,0 +1,33 @@ +package net.rsprot.protocol.game.outgoing.info.filter + +/** + * A default naive extended info filter. This filter will stop accepting + * any avatars once 30kb of data has been written in the buffer. + * As a result of this, it is guaranteed that the packet capacity will + * never be exceeded under any circumstances, as the remaining 10kb + * is more than enough to write every extended info block set to the + * theoretical maximums. + */ +public class DefaultExtendedInfoFilter : ExtendedInfoFilter { + override fun accept( + writableBytes: Int, + constantFlag: Int, + remainingAvatars: Int, + previouslyObserved: Boolean, + ): Boolean = (writableBytes - remainingAvatars) >= THEORETICAL_HIGHEST_EXTENDED_INFO_BLOCK_SIZE + + public companion object { + /** + * The theoretical highest is a rough approximation if a player had every extended + * info block flagged to the maximum, meaning 256 worst case hitmarks, headbars, spotanims, + * and so on. The real maximum comes to somewhere in the 7,000-8,000 range, + * however for some head-room and not having to recompute this all the time, + * we stick with a 10 kilobyte limitation. + * This limitation is more than enough in just about every scenario in real life. + * For the longest time, the actual total limitation was only 5 kilobytes for + * the entire player info packet, so limiting most of it to be 30kb or less + * is still a huge improvement and should cover almost all realistic scenarios. + */ + public const val THEORETICAL_HIGHEST_EXTENDED_INFO_BLOCK_SIZE: Int = 10_000 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/filter/ExtendedInfoFilter.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/filter/ExtendedInfoFilter.kt new file mode 100644 index 000000000..8e4771e13 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/filter/ExtendedInfoFilter.kt @@ -0,0 +1,40 @@ +package net.rsprot.protocol.game.outgoing.info.filter + +/** + * Extended info filter provides the protocol with a strategy for how to handle + * the packet capacity limitations, as it is all too easy to fly past the 40kb + * limitation in extreme scenarios and benchmarks. This interface is responsible + * for ensuring that the extended info blocks do not exceed the 40kb limitation. + * In order to achieve this, all necessary information is provided within the + * [accept] function. It should be noted that at least 1 byte of space is + * necessary per each remaining avatar at the very least, as we write the flag + * as zero in those extreme scenarios. + */ +public fun interface ExtendedInfoFilter { + /** + * Whether to accept writing the extended info blocks for the next avatar. + * @param writableBytes the amount of bytes that can still be written into the buffer + * before reaching its absolute capacity. 1 byte of space is required as a minimum + * per each [remainingAvatars]. + * @param constantFlag the bitpacked flag of all the extended info blocks flagged + * for this avatar. This function utilizes the constant flags found in + * [net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerAvatarExtendedInfo], + * rather than the client-specific variants. + * @param remainingAvatars the number of avatars for whom we need to still write + * extended info blocks. This includes the current avatar on whom we are checking + * the accept function. Per each avatar, at least one byte must be writable. + * @param previouslyObserved whether the protocol has previously observed this + * avatar. This is done by checking if our appearance cache has previously tracked + * an avatar by that index. While the exact acceptation mechanics are unknown, + * in times of high pressure, OldSchool RuneScape seems to always send extended info + * about the avatars whom you've already observed in the past. However, it is very + * strict with whom it newly accepts, often only rendering 16 or 32 players + * when there's high resolution information sent about a thousand of them. + */ + public fun accept( + writableBytes: Int, + constantFlag: Int, + remainingAvatars: Int, + previouslyObserved: Boolean, + ): Boolean +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatar.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatar.kt new file mode 100644 index 000000000..f4c16ba40 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatar.kt @@ -0,0 +1,411 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import net.rsprot.buffer.bitbuffer.UnsafeLongBackedBitBuf +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.NpcAvatarDetails +import net.rsprot.protocol.game.outgoing.info.npcinfo.util.NpcCellOpcodes +import net.rsprot.protocol.game.outgoing.info.util.Avatar +import java.util.concurrent.atomic.AtomicInteger + +/** + * The npc avatar class represents an NPC as shown by the client. + * This class contains all the properties necessary to put a NPC into high resolution. + * + * Npc direction table: + * ``` + * | Id | Client Angle | Direction | + * |:--:|:------------:|:----------:| + * | 0 | 768 | North-West | + * | 1 | 1024 | North | + * | 2 | 1280 | North-East | + * | 3 | 512 | West | + * | 4 | 1536 | East | + * | 5 | 256 | South-West | + * | 6 | 0 | South | + * | 7 | 1792 | South-East | + * ``` + * + * @param index the index of the npc in the world + * @param id the id of the npc in the world, limited to range of 0..16383 + * @param level the height level of the npc + * @param x the absolute x coordinate of the npc + * @param z the absolute z coordinate of the npc + * @param spawnCycle the game cycle on which the npc spawned into the world; + * for static NPCs, this would always be zero. This is only used by the C++ clients. + * @param direction the direction that the npc will face on spawn (see table above) + */ +public class NpcAvatar internal constructor( + index: Int, + id: Int, + level: Int, + x: Int, + z: Int, + spawnCycle: Int = 0, + direction: Int = 0, + allocateCycle: Int, + /** + * Extended info repository, commonly referred to as "masks", will track everything relevant + * inside itself. Setting properties such as a spotanim would be done through this. + * The [extendedInfo] is also responsible for caching the non-temporary blocks, + * such as appearance and move speed. + */ + public val extendedInfo: NpcAvatarExtendedInfo, +) : Avatar { + /** + * Npc avatar details class wraps all the client properties of a NPC in its own + * data structure. + */ + internal val details: NpcAvatarDetails = + NpcAvatarDetails( + index, + id, + level, + x, + z, + spawnCycle, + direction, + allocateCycle, + ) + + /** + * The number of player avatars observing this NPC avatar. + * We utilize the count tracking to determine what NPCs require precomputation. + * As the game has circa 25,000 NPCs, and even at max world capacity, only 2,000 players, + * the majority of NPCs in the game will at all times __not__ be observed by any players. + * This means computing their high resolution blocks is unnecessary, as that is strictly + * only for players who are already observing a NPC - moving from low resolution to high + * resolution has its own set of code. + * Additionally, this is used to skip computing extended info blocks later on in the cycle, + * given the assumption that no player added this NPC to their high resolution view. + * Furthermore, this observer count must be an atomic integer, as certain parts of NPC info + * are multithreaded, including the parts which modify this count. + */ + private val observerCount: AtomicInteger = AtomicInteger() + + /** + * The high resolution movement buffer, used to avoid re-calculating the movement information + * for each observer of a given NPC, in cases where there are multiple. It is additionally + * more efficient to just do a single bulk pBits() call, than to call it multiple times, which + * this accomplishes. + */ + internal var highResMovementBuffer: UnsafeLongBackedBitBuf? = null + + /** + * Adds an observer to this avatar by incrementing the observer count. + * Note that it is necessary for servers to de-register npc info when the player is logging off, + * or the protocol will run into issues on multiple levels. + */ + internal fun addObserver() { + observerCount.incrementAndGet() + } + + /** + * Removes an observer from this avatar by decrementing the observer count. + * This function must be called when a player logs off for each NPC they were observing. + */ + internal fun removeObserver() { + // If the allocation cycle is the same as current cycle count, + // a "hotswap" has occurred. + // This means that a npc was deallocated and another allocated the same index + // in the same cycle. + // Due to the new one being allocated, the observer count is already reset + // to zero, and we cannot decrement the observer count further - it would go negative. + if (details.allocateCycle == NpcInfoProtocol.cycleCount) { + return + } + observerCount.decrementAndGet() + } + + /** + * Checks if this NPC has any observers, necessary to determine whether cached information + * must be computed for this NPC. + */ + internal fun hasObservers(): Boolean = observerCount.get() > 0 + + /** + * Resets the observer count. + */ + internal fun resetObservers() { + observerCount.set(0) + } + + /** + * Updates the spawn direction of the NPC. + * + * Table of possible direction values: + * ``` + * | Id | Direction | Angle | + * |:--:|:----------:|:-----:| + * | 0 | North-West | 768 | + * | 1 | North | 1024 | + * | 2 | North-East | 1280 | + * | 3 | West | 512 | + * | 4 | East | 1536 | + * | 5 | South-West | 256 | + * | 6 | South | 0 | + * | 7 | South-East | 1792 | + * ``` + * + * @param direction the direction for the NPC to face. + */ + public fun updateDirection(direction: Int) { + require(direction in 0..7) { + "Direction must be a value in range of 0..7. " + + "See the table in documentation. Value: $direction" + } + this.details.updateDirection(direction) + } + + /** + * Sets the id of the avatar - any new observers of this NPC will receive the new id. + * This should be used in tandem with the transformation extended info block. + * @param id the id of the npc to set to - any new observers will see that id instead. + */ + public fun setId(id: Int) { + require(id in 0..16383) { + "Id must be a value in range of 0..16383. Value: $id" + } + this.details.id = id + } + + /** + * A helper function to teleport the NPC to a new coordinate. + * This will furthermore mark the movement type as teleport, meaning no matter what other + * coordinate changes are applied, as teleport has the highest priority, teleportation + * will be how it is rendered on the client's end. + * @param level the new height level of the NPC + * @param x the new absolute x coordinate of the NPC + * @param z the new absolute z coordinate of the NPC + * @param jump whether to "jump" the NPC to the new coordinate, or to treat it as a + * regular walk/run type movement. While this should __almost__ always be true, there are + * certain NPCs, such as Sarachnis in OldSchool, that utilize teleporting without jumping. + * This effectively makes the NPC appear as it is walking towards the destination. If the + * NPC falls visually behind, the client will begin increasing its movement speed, to a + * maximum of run speed, until it has caught up visually. + */ + public fun teleport( + level: Int, + x: Int, + z: Int, + jump: Boolean, + ) { + details.currentCoord = CoordGrid(level, x, z) + details.movementType = details.movementType or (if (jump) NpcAvatarDetails.TELEJUMP else NpcAvatarDetails.TELE) + } + + /** + * Marks the NPC as moved with the crawl movement type. + * If more than one crawl/walks are sent in one cycle, it will instead be treated as run. + * If more than two crawl/walks are sent in one cycle, it will be treated as a teleport. + * @param deltaX the x coordinate delta that the NPC moved. + * @param deltaZ the z coordinate delta that the npc moved. + * @throws ArrayIndexOutOfBoundsException if either of the deltas is not in range of -1..1, + * or both are 0s. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public fun crawl( + deltaX: Int, + deltaZ: Int, + ) { + singleStepMovement( + deltaX, + deltaZ, + NpcAvatarDetails.CRAWL, + ) + } + + /** + * Marks the NPC as moved with the walk movement type. + * If more than one crawl/walks are sent in one cycle, it will instead be treated as run. + * If more than two crawl/walks are sent in one cycle, it will be treated as a teleport. + * @param deltaX the x coordinate delta that the NPC moved. + * @param deltaZ the z coordinate delta that the npc moved. + * @throws ArrayIndexOutOfBoundsException if either of the deltas is not in range of -1..1, + * or both are 0s. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public fun walk( + deltaX: Int, + deltaZ: Int, + ) { + singleStepMovement( + deltaX, + deltaZ, + NpcAvatarDetails.WALK, + ) + } + + /** + * Determines the movement opcode for the NPC, adjusting the NPC's underlying coordinate afterwards, + * and defines the movement speed based on previous movements in this cycle, as well as the + * [flag] requested by the movement. + * @param deltaX the x coordinate delta that the NPC moved. + * @param deltaZ the z coordinate delta that the npc moved. + * @param flag the movement speed flag, used to determine what movement speeds have been used + * in one cycle, given it is possible to move a NPC more than one in one cycle, should the + * server request it. + * @throws ArrayIndexOutOfBoundsException if either of the deltas is not in range of -1..1, + * or both are 0s. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + private fun singleStepMovement( + deltaX: Int, + deltaZ: Int, + flag: Int, + ) { + val opcode = NpcCellOpcodes.singleCellMovementOpcode(deltaX, deltaZ) + val (level, x, z) = details.currentCoord + details.currentCoord = CoordGrid(level, x + deltaX, z + deltaZ) + when (++details.stepCount) { + 1 -> { + details.firstStep = opcode + details.movementType = details.movementType or flag + } + 2 -> { + details.secondStep = opcode + details.movementType = details.movementType or NpcAvatarDetails.RUN + } + else -> { + details.movementType = details.movementType or NpcAvatarDetails.TELE + } + } + } + + /** + * Prepares the bitcodes of a NPC given the assumption it has at least one player observing it, + * and the NPC is not teleporting (or tele-jumping), as both of those cause it to be treated as + * remove + re-add client-side, meaning no normal block is used. + * While it is possible to additionally make NPC removal as part of this function, + * because part of the responsibility is at the NPC info protocol level (coordinate checks, + * state checks), it is not possible to fully cover it, so best to leave that for the protocol + * to handle. + */ + internal fun prepareBitcodes() { + val movementType = details.movementType + // If teleporting, or if there are no observers, there's no need to compute this + if (movementType and (NpcAvatarDetails.TELE or NpcAvatarDetails.TELEJUMP) != 0 || observerCount.get() == 0) { + return + } + val buffer = UnsafeLongBackedBitBuf() + this.highResMovementBuffer = buffer + val extendedInfo = this.extendedInfo.flags != 0 + if (movementType and NpcAvatarDetails.RUN != 0) { + pRun(buffer, extendedInfo) + } else if (movementType and NpcAvatarDetails.WALK != 0) { + pWalk(buffer, extendedInfo) + } else if (movementType and NpcAvatarDetails.CRAWL != 0) { + pCrawl(buffer, extendedInfo) + } else if (extendedInfo) { + pExtendedInfo(buffer) + } else { + pNoUpdate(buffer) + } + } + + /** + * Informs the client that there will be no movement or extended info update for this NPC. + * @param buffer the pre-computed buffer into which to write the bitcodes. + */ + private fun pNoUpdate(buffer: UnsafeLongBackedBitBuf) { + buffer.pBits(1, 0) + } + + /** + * Informs the client that there is no movement occurring for this NPC, but it does have + * extended info blocks encoded. + * @param buffer the pre-computed buffer into which to write the bitcodes. + */ + private fun pExtendedInfo(buffer: UnsafeLongBackedBitBuf) { + buffer.pBits(1, 1) + buffer.pBits(2, 0) + } + + /** + * Informs the client that there is a crawl-speed movement occurring for this NPC. + * @param buffer the pre-computed buffer into which to write the bitcodes. + * @param extendedInfo whether this NPC additionally has extended info updates coming. + */ + private fun pCrawl( + buffer: UnsafeLongBackedBitBuf, + extendedInfo: Boolean, + ) { + buffer.pBits(1, 1) + buffer.pBits(2, 2) + buffer.pBits(1, 0) + buffer.pBits(3, details.firstStep) + buffer.pBits(1, if (extendedInfo) 1 else 0) + } + + /** + * Informs the client that there is a walk-speed movement occurring for this NPC. + * @param buffer the pre-computed buffer into which to write the bitcodes. + * @param extendedInfo whether this NPC additionally has extended info updates coming. + */ + private fun pWalk( + buffer: UnsafeLongBackedBitBuf, + extendedInfo: Boolean, + ) { + buffer.pBits(1, 1) + buffer.pBits(2, 1) + buffer.pBits(3, details.firstStep) + buffer.pBits(1, if (extendedInfo) 1 else 0) + } + + /** + * Informs the client that there is a run-speed movement occurring for this NPC. + * @param buffer the pre-computed buffer into which to write the bitcodes. + * @param extendedInfo whether this NPC additionally has extended info updates coming. + */ + private fun pRun( + buffer: UnsafeLongBackedBitBuf, + extendedInfo: Boolean, + ) { + buffer.pBits(1, 1) + buffer.pBits(2, 2) + buffer.pBits(1, 1) + buffer.pBits(3, details.firstStep) + buffer.pBits(3, details.secondStep) + buffer.pBits(1, if (extendedInfo) 1 else 0) + } + + /** + * The current height level of this avatar. + */ + public fun level(): Int = details.currentCoord.level + + /** + * The current absolute x coordinate of this avatar. + */ + public fun x(): Int = details.currentCoord.x + + /** + * The current absolute z coordinate of this avatar. + */ + public fun z(): Int = details.currentCoord.z + + /** + * Sets this avatar inaccessible, meaning no player can observe this NPC, + * but they are still in the world. This is how NPCs in the 'dead' state + * will be handled. + * @param inaccessible whether the npc is inaccessible to all players (not rendered) + */ + public fun setInaccessible(inaccessible: Boolean) { + details.inaccessible = inaccessible + } + + override fun postUpdate() { + details.stepCount = 0 + details.firstStep = -1 + details.secondStep = -1 + details.movementType = 0 + extendedInfo.postUpdate() + } + + override fun toString(): String = + "NpcAvatar(" + + "extendedInfo=$extendedInfo, " + + "details=$details, " + + "observerCount=$observerCount, " + + "highResMovementBuffer=$highResMovementBuffer" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExceptionHandler.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExceptionHandler.kt new file mode 100644 index 000000000..36dc5002d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExceptionHandler.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import java.lang.Exception + +/** + * An exception handler for npc avatar processing. + * This is necessary as we might run into hiccups during computations of a specific npc, + * in which case we need to propagate the exceptions to the server, which will ideally remove said npcs + * from the world as a result of it. + */ +public fun interface NpcAvatarExceptionHandler { + /** + * This function is triggered whenever there's an exception caught during npc + * avatar processing. + * @param index the index of the npc that had an exception during its processing. + * @param exception the exception that was caught during a npc's avatar processing + */ + public fun exceptionCaught( + index: Int, + exception: Exception, + ) +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExtendedInfo.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExtendedInfo.kt new file mode 100644 index 000000000..eb99c6042 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExtendedInfo.kt @@ -0,0 +1,1255 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.RSProtFlags +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.encoder.NpcExtendedInfoEncoders +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.BaseAnimationSet +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.CombatLevelChange +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.HeadIconCustomisation +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.TypeCustomisation +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.VisibleOps +import net.rsprot.protocol.common.game.outgoing.info.precompute +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.FacePathingEntity +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.HeadBar +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.HitMark +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.SpotAnim +import net.rsprot.protocol.game.outgoing.info.AvatarExtendedInfoWriter +import net.rsprot.protocol.game.outgoing.info.filter.ExtendedInfoFilter + +public typealias NpcAvatarExtendedInfoWriter = + AvatarExtendedInfoWriter + +/** + * Npc avatar extended info is a data structure used to keep track of all the extended info + * properties of the given avatar. + * @property avatarIndex the index of the avatar npc + * @property filter the filter used to ensure that the buffer does not exceed the 40kb limit. + * @param extendedInfoWriters the list of client-specific extended info writers. + * @property allocator the byte buffer allocator used to pre-compute extended info blocks. + * @property huffmanCodec the huffman codec is used to compress chat messages, though + * none are used for NPCs, the writer function still expects it. + */ +@Suppress("DuplicatedCode") +public class NpcAvatarExtendedInfo( + private var avatarIndex: Int, + private val filter: ExtendedInfoFilter, + extendedInfoWriters: List, + private val allocator: ByteBufAllocator, + private val huffmanCodec: HuffmanCodecProvider, +) { + /** + * The extended info blocks enabled on this NPC in a given cycle. + */ + internal var flags: Int = 0 + + /** + * Extended info blocks used to transmit changes to the client, + * wrapped in its own class as we must pass this onto the client-specific + * implementations. + */ + private val blocks: NpcAvatarExtendedInfoBlocks = NpcAvatarExtendedInfoBlocks(extendedInfoWriters) + + /** + * The client-specific extended info writers, indexed by the respective [OldSchoolClientType]'s id. + * All clients in use must be registered, or an exception will occur during player info encoding. + */ + private val writers: Array = + buildClientWriterArray(extendedInfoWriters) + + /** + * Sets the sequence for this avatar to play. + * @param id the id of the sequence to play, or -1 to stop playing current sequence. + * @param delay the delay in client cycles (20ms/cc) until the avatar starts playing this sequence. + */ + public fun setSequence( + id: Int, + delay: Int, + ) { + verify { + require(id == -1 || id in UNSIGNED_SHORT_RANGE) { + "Unexpected sequence id: $id, expected value -1 or in range $UNSIGNED_SHORT_RANGE" + } + require(delay in UNSIGNED_SHORT_RANGE) { + "Unexpected sequence delay: $delay, expected range: $UNSIGNED_SHORT_RANGE" + } + } + blocks.sequence.id = id.toUShort() + blocks.sequence.delay = delay.toUShort() + flags = flags or SEQUENCE + } + + /** + * Sets the face-locking onto the avatar with index [index]. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * In order to stop facing an entity, set the index value to -1. + * @param index the index of the target to face-lock onto (read above) + */ + public fun setFacePathingEntity(index: Int) { + verify { + require(index == -1 || index in 0..0x107FF) { + "Unexpected pathing entity index: $index, expected values: -1 to reset, " + + "0-65535 for NPCs, 65536-67583 for players" + } + } + blocks.facePathingEntity.index = index + flags = flags or FACE_PATHINGENTITY + } + + /** + * Sets the overhead chat of this avatar. + * If the [text] starts with the character `~`, the message will additionally + * also be rendered in the chatbox of everyone nearby, although no chat icons + * will appear alongside. The first `~` character itself will not be rendered + * in that scenario. + * @param text the text to render overhead. + */ + public fun setSay(text: String) { + verify { + require(text.length <= 80) { + "Unexpected say input; expected value 80 characters or less, " + + "input len: ${text.length}, input: $text" + } + } + blocks.say.text = text + flags = flags or SAY + } + + /** + * Sets an exact movement for this avatar. It should be noted + * that this is done in conjunction with actual movement, as the + * exact move extended info block is only responsible for visualizing + * precise movement, and will synchronize to the real coordinate once + * the exact movement has finished. + * + * @param deltaX1 the coordinate delta between the current absolute + * x coordinate and where the avatar is going. + * @param deltaZ1 the coordinate delta between the current absolute + * z coordinate and where the avatar is going. + * @param delay1 how many client cycles (20ms/cc) until the avatar arrives + * at x/z 1 coordinate. + * @param deltaX2 the coordinate delta between the current absolute + * x coordinate and where the avatar is going. + * @param deltaZ2 the coordinate delta between the current absolute + * z coordinate and where the avatar is going. + * @param delay2 how many client cycles (20ms/cc) until the avatar arrives + * at x/z 2 coordinate. + * @param angle the angle the avatar will be facing throughout the exact movement, + * with 0 implying south, 512 west, 1024 north and 1536 east; interpolate + * between to get finer directions. + */ + public fun setExactMove( + deltaX1: Int, + deltaZ1: Int, + delay1: Int, + deltaX2: Int, + deltaZ2: Int, + delay2: Int, + angle: Int, + ) { + verify { + require(delay1 >= 0) { + "First delay cannot be negative: $delay1" + } + require(delay2 >= 0) { + "Second delay cannot be negative: $delay2" + } + require(delay2 > delay1) { + "Second delay must be greater than the first: $delay1 > $delay2" + } + require(angle in 0..2047) { + "Unexpected angle value: $angle, expected range: 0..2047" + } + require(deltaX1 in SIGNED_BYTE_RANGE) { + "Unexpected deltaX1: $deltaX1, expected range: $SIGNED_BYTE_RANGE" + } + require(deltaZ1 in SIGNED_BYTE_RANGE) { + "Unexpected deltaZ1: $deltaZ1, expected range: $SIGNED_BYTE_RANGE" + } + require(deltaX2 in SIGNED_BYTE_RANGE) { + "Unexpected deltaX1: $deltaX2, expected range: $SIGNED_BYTE_RANGE" + } + require(deltaZ2 in SIGNED_BYTE_RANGE) { + "Unexpected deltaZ1: $deltaZ2, expected range: $SIGNED_BYTE_RANGE" + } + } + blocks.exactMove.deltaX1 = deltaX1.toUByte() + blocks.exactMove.deltaZ1 = deltaZ1.toUByte() + blocks.exactMove.delay1 = delay1.toUShort() + blocks.exactMove.deltaX2 = deltaX2.toUByte() + blocks.exactMove.deltaZ2 = deltaZ2.toUByte() + blocks.exactMove.delay2 = delay2.toUShort() + blocks.exactMove.direction = angle.toUShort() + flags = flags or EXACT_MOVE + } + + /** + * Sets the spotanim in slot [slot], overriding any previous spotanim + * in that slot in doing so. + * @param slot the slot of the spotanim. + * @param id the id of the spotanim. + * @param delay the delay in client cycles (20ms/cc) until the given spotanim begins rendering. + * @param height the height at which to render the spotanim. + */ + public fun setSpotAnim( + slot: Int, + id: Int, + delay: Int, + height: Int, + ) { + verify { + require(slot in UNSIGNED_BYTE_RANGE) { + "Unexpected slot: $slot, expected range: $UNSIGNED_BYTE_RANGE" + } + require(id == -1 || id in UNSIGNED_SHORT_RANGE) { + "Unexpected id: $id, expected value -1 or in range: $UNSIGNED_SHORT_RANGE" + } + require(delay in UNSIGNED_SHORT_RANGE) { + "Unexpected delay: $delay, expected range: $UNSIGNED_SHORT_RANGE" + } + require(height in UNSIGNED_SHORT_RANGE) { + "Unexpected delay: $height, expected range: $UNSIGNED_SHORT_RANGE" + } + } + blocks.spotAnims.set(slot, SpotAnim(id, delay, height)) + flags = flags or SPOTANIM + } + + /** + * Adds a simple hitmark on this avatar. + * @param sourceIndex the index of the character that dealt the hit. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * If there is no source, set the index to -1. + * The index will be used for tinting purposes, as both the player who dealt + * the hit, and the recipient will see a tinted variant. + * Everyone else, however, will see a regular darkened hit mark. + * @param selfType the multi hitmark id that supports tinted and darkened variants. + * @param otherType the hitmark id to render to anyone that isn't the recipient, + * or the one who dealt the hit. This will generally be a darkened variant. + * If the hitmark should only render to the local player, set the [otherType] + * value to -1, forcing it to only render to the recipient (and in the case of + * a [sourceIndex] being defined, the one who dealt the hit) + * @param value the value to show over the hitmark. + * @param delay the delay in client cycles (20ms/cc) until the hitmark renders. + */ + public fun addHitMark( + sourceIndex: Int, + selfType: Int, + otherType: Int = selfType, + value: Int, + delay: Int = 0, + ) { + if (blocks.hit.hitMarkList.size >= 0xFF) { + return + } + verify { + require(sourceIndex == -1 || sourceIndex in 0..0x107FF) { + "Unexpected source index: $sourceIndex, expected values: -1 to reset, " + + "0-65535 for NPCs, 65536-67583 for players" + } + require(selfType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected selfType: $selfType, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(otherType == -1 || otherType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected otherType: $otherType, expected value -1 or range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(value in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected value: $value, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(delay in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected delay: $delay, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + } + blocks.hit.hitMarkList += + HitMark( + sourceIndex, + selfType.toUShort(), + otherType.toUShort(), + value.toUShort(), + delay.toUShort(), + ) + flags = flags or HITS + } + + /** + * Removes the oldest currently showing hitmark on this avatar, + * if one exists. + * @param delay the delay in client cycles (20ms/cc) until the hitmark is removed. + */ + public fun removeHitMark(delay: Int = 0) { + if (blocks.hit.hitMarkList.size >= 0xFF) { + return + } + verify { + require(delay in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected delay: $delay, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + } + blocks.hit.hitMarkList += HitMark(0x7FFEu, delay.toUShort()) + flags = flags or HITS + } + + /** + * Adds a simple hitmark on this avatar. + * @param sourceIndex the index of the character that dealt the hit. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * If there is no source, set the index to -1. + * The index will be used for tinting purposes, as both the player who dealt + * the hit, and the recipient will see a tinted variant. + * Everyone else, however, will see a regular darkened hit mark. + * @param selfType the multi hitmark id that supports tinted and darkened variants. + * @param otherType the hitmark id to render to anyone that isn't the recipient, + * or the one who dealt the hit. This will generally be a darkened variant. + * If the hitmark should only render to the local player, set the [otherType] + * value to -1, forcing it to only render to the recipient (and in the case of + * a [sourceIndex] being defined, the one who dealt the hit) + * @param value the value to show over the hitmark. + * @param selfSoakType the multi hitmark id that supports tinted and darkened variants, + * shown as soaking next to the normal hitmark. + * @param otherSoakType the hitmark id to render to anyone that isn't the recipient, + * or the one who dealt the hit. This will generally be a darkened variant. + * Unlike the [otherType], this does not support -1, as it is not possible to show partial + * soaked hitmarks. + * @param delay the delay in client cycles (20ms/cc) until the hitmark renders. + */ + @JvmOverloads + public fun addSoakedHitMark( + sourceIndex: Int, + selfType: Int, + otherType: Int = selfType, + value: Int, + selfSoakType: Int, + otherSoakType: Int = selfSoakType, + soakValue: Int, + delay: Int = 0, + ) { + if (blocks.hit.hitMarkList.size >= 0xFF) { + return + } + verify { + require(sourceIndex == -1 || sourceIndex in 0..0x107FF) { + "Unexpected source index: $sourceIndex, expected values: -1 to reset, " + + "0-65535 for NPCs, 65536-67583 for players" + } + require(selfType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected selfType: $selfType, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(otherType == -1 || otherType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected otherType: $otherType, expected value -1 or in range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(value in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected value: $value, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(selfSoakType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected selfType: $selfSoakType, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(otherSoakType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected otherType: $otherSoakType, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(soakValue in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected value: $soakValue, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(delay in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected delay: $delay, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + } + blocks.hit.hitMarkList += + HitMark( + sourceIndex, + selfType.toUShort(), + otherType.toUShort(), + value.toUShort(), + selfSoakType.toUShort(), + otherSoakType.toUShort(), + soakValue.toUShort(), + delay.toUShort(), + ) + flags = flags or HITS + } + + /** + * Adds a headbar onto the avatar. + * If a headbar by the same id already exists, updates the status of the old one. + * Up to four distinct headbars can be rendered simultaneously. + * + * @param sourceIndex the index of the entity that dealt the hit that resulted in this headbar. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * If there is no source, set the index to -1. + * The index will be used for rendering purposes, as both the player who dealt + * the hit, and the recipient will see the [selfType] variant, and everyone else + * will see the [otherType] variant, which, if set to -1 will be skipped altogether. + * @param selfType the id of the headbar to render to the entity on which the headbar appears, + * as well as the source who resulted in the creation of the headbar. + * @param otherType the id of the headbar to render to everyone that doesn't fit the [selfType] + * criteria. If set to -1, the headbar will not be rendered to these individuals. + * @param startFill the number of pixels to render of this headbar at in the start. + * The number of pixels that a headbar supports is defined in its respective headbar config. + * @param endFill the number of pixels to render of this headbar at in the end, + * if a [startTime] and [endTime] are defined. + * @param startTime the delay in client cycles (20ms/cc) until the headbar renders at [startFill] + * @param endTime the delay in client cycles (20ms/cc) until the headbar arrives at [endFill]. + */ + @JvmOverloads + public fun addHeadBar( + sourceIndex: Int, + selfType: Int, + otherType: Int = selfType, + startFill: Int, + endFill: Int = startFill, + startTime: Int = 0, + endTime: Int = 0, + ) { + if (blocks.hit.headBarList.size >= 0xFF) { + return + } + verify { + require(sourceIndex == -1 || sourceIndex in 0..0x107FF) { + "Unexpected source index: $sourceIndex, expected values: -1 to reset, " + + "0-65535 for NPCs, 65536-67583 for players" + } + require(selfType == -1 || selfType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected id: $selfType, expected value -1 or in range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(otherType == -1 || otherType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected id: $otherType, expected value -1 or in range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(startFill in UNSIGNED_BYTE_RANGE) { + "Unexpected startFill: $startFill, expected range $UNSIGNED_BYTE_RANGE" + } + require(endFill in UNSIGNED_BYTE_RANGE) { + "Unexpected endFill: $endFill, expected range $UNSIGNED_BYTE_RANGE" + } + require(startTime in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected startTime: $startTime, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(endTime in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected endTime: $endTime, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(endTime >= startTime) { + "End time must be greater than or equal to start time: $startTime <= $endTime" + } + } + blocks.hit.headBarList += + HeadBar( + sourceIndex, + selfType.toUShort(), + otherType.toUShort(), + startFill.toUByte(), + endFill.toUByte(), + startTime.toUShort(), + endTime.toUShort(), + ) + flags = flags or HITS + } + + /** + * Removes a headbar on this avatar by the id of [id], if one renders. + * @param id the id of the head bar to remove. + */ + public fun removeHeadBar(id: Int) { + addHeadBar( + -1, + id, + startFill = 0, + endTime = HeadBar.REMOVED.toInt(), + ) + } + + /** + * Applies a tint over the non-textured parts of the character. + * @param startTime the delay in client cycles (20ms/cc) until the tinting is applied. + * @param endTime the timestamp in client cycles (20ms/cc) until the tinting finishes. + * @param hue the hue of the tint. + * @param saturation the saturation of the tint. + * @param lightness the lightness of the tint. + * @param weight the weight (or opacity) of the tint. + */ + public fun tinting( + startTime: Int, + endTime: Int, + hue: Int, + saturation: Int, + lightness: Int, + weight: Int, + ) { + verify { + require(startTime in UNSIGNED_SHORT_RANGE) { + "Unexpected startTime: $startTime, expected range $UNSIGNED_SHORT_RANGE" + } + require(endTime in UNSIGNED_SHORT_RANGE) { + "Unexpected endTime: $endTime, expected range $UNSIGNED_SHORT_RANGE" + } + require(endTime >= startTime) { + "End time should be equal to or greater than start time: $endTime > $startTime" + } + require(hue in UNSIGNED_BYTE_RANGE) { + "Unexpected hue: $hue, expected range $UNSIGNED_BYTE_RANGE" + } + require(saturation in UNSIGNED_BYTE_RANGE) { + "Unexpected saturation: $saturation, expected range $UNSIGNED_BYTE_RANGE" + } + require(lightness in UNSIGNED_BYTE_RANGE) { + "Unexpected lightness: $lightness, expected range $UNSIGNED_BYTE_RANGE" + } + require(weight in UNSIGNED_BYTE_RANGE) { + "Unexpected weight: $weight, expected range $UNSIGNED_BYTE_RANGE" + } + } + val tint = blocks.tinting.global + tint.start = startTime.toUShort() + tint.end = endTime.toUShort() + tint.hue = hue.toUByte() + tint.saturation = saturation.toUByte() + tint.lightness = lightness.toUByte() + tint.weight = weight.toUByte() + flags = flags or TINTING + } + + /** + * Faces the center of the absolute coordinate provided. + * @param x the absolute x coordinate to turn towards + * @param z the absolute z coordinate to turn towards + * @param instant whether to turn towards the coord instantly without any turn anim, + * or gradually. The instant property is typically used when spawning in NPCs; + * While the low to high resolution change does support a direction, it only supports + * in increments of 45 degrees - so utilizing this extended info blocks allows for + * more precise control over it. + */ + @JvmOverloads + public fun faceCoord( + x: Int, + z: Int, + instant: Boolean = false, + ) { + verify { + require(x in 0..<16384) { + "Unexpected x coord: $x, expected range: 0..<16384" + } + require(z in 0..<16384) { + "Unexpected z coord: $z, expected range: 0..<16384" + } + } + val faceCoord = blocks.faceCoord + faceCoord.x = x.toUShort() + faceCoord.z = z.toUShort() + faceCoord.instant = instant + flags = flags or FACE_COORD + } + + /** + * Transforms this NPC into the [id] provided. + * It should be noted that this extended info block is transient and only applies to one cycle. + * The server is expected to additionally change the id of the avatar itself, otherwise + * any new observers will get the old variant. + * @param id the new id of the npc to transform to. + */ + public fun transformation(id: Int) { + verify { + require(id in UNSIGNED_SHORT_RANGE) { + "Unexpected id: $id, expected in range: $UNSIGNED_SHORT_RANGE" + } + } + blocks.transformation.id = id.toUShort() + flags = flags or TRANSFORMATION + } + + /** + * Overrides the combat level of this NPC with the provided level. + * @param level the combat leve to render, or -1 to remove the combat level override. + */ + public fun combatLevelChange(level: Int) { + blocks.combatLevelChange.level = level + flags = flags or LEVEL_CHANGE + } + + /** + * Overrides the name of this NPC with the provided [name]. + * @param name the name to override with, or null to reset an existing override. + */ + public fun nameChange(name: String?) { + blocks.nameChange.name = name + flags = flags or NAME_CHANGE + } + + /** + * Sets the visible ops flag of this NPC to the provided value. + * @param flag the bit flag to set. Only the 5 lowest bits are used, + * and an enabled bit implies the option at that index should render. + * Note that this extended info block is not transient and will be transmitted to + * future players as well. + */ + @Suppress("MemberVisibilityCanBePrivate") + public fun visibleOps(flag: Int) { + blocks.visibleOps.ops = flag.toUByte() + flags = flags or OPS + } + + /** + * Marks the provided right-click options as visible or invisible. + * @param op1 whether to render op1 + * @param op2 whether to render op2 + * @param op3 whether to render op3 + * @param op4 whether to render op4 + * @param op5 whether to render op5 + */ + public fun visibleOps( + op1: Boolean, + op2: Boolean, + op3: Boolean, + op4: Boolean, + op5: Boolean, + ) { + var flag = 0 + if (op1) flag = flag or 0x1 + if (op2) flag = flag or 0x2 + if (op3) flag = flag or 0x4 + if (op4) flag = flag or 0x8 + if (op5) flag = flag or 0x10 + visibleOps(flag) + } + + /** + * Sets all the right-click options invisible on this NPC. + */ + public fun allOpsInvisible() { + visibleOps(0) + } + + /** + * Sets all the right-click options as visible on this NPC. + */ + public fun allOpsVisible() { + visibleOps(0b11111) + } + + /** + * Sets the base animation set of this NPC with the provided values. + * If the value is equal to [Int.MIN_VALUE], the animation will not be overwritten. + * Only the 16 lowest bits of the animation ids are used. + * @param turnLeftAnim the animation used when the NPC turns to the left + * @param turnRightAnim the animation used when the NPC turns to the right + * @param walkAnim the animation used when the NPC walks forward + * @param walkAnimLeft the animation used when the NPC walks to the left + * @param walkAnimRight the animation used when the NPC walks to the right + * @param walkAnimBack the animation used when the NPC walks backwards + * @param runAnim the animation used when the NPC runs forward + * @param runAnimLeft the animation used when the NPC runs to the left + * @param runAnimRight the animation used when the NPC runs to the right + * @param runAnimBack the animation used when the NPC runs backwards + * @param crawlAnim the animation used when the NPC crawls forward + * @param crawlAnimLeft the animation used when the NPC crawls to the left + * @param crawlAnimRight the animation used when the NPC crawls to the right + * @param crawlAnimBack the animation used when the NPC crawls backwards + * @param readyAnim the default stance animation of this NPC when it is not moving + */ + @JvmSynthetic + public fun baseAnimationSet( + turnLeftAnim: Int = Int.MIN_VALUE, + turnRightAnim: Int = Int.MIN_VALUE, + walkAnim: Int = Int.MIN_VALUE, + walkAnimBack: Int = Int.MIN_VALUE, + walkAnimLeft: Int = Int.MIN_VALUE, + walkAnimRight: Int = Int.MIN_VALUE, + runAnim: Int = Int.MIN_VALUE, + runAnimBack: Int = Int.MIN_VALUE, + runAnimLeft: Int = Int.MIN_VALUE, + runAnimRight: Int = Int.MIN_VALUE, + crawlAnim: Int = Int.MIN_VALUE, + crawlAnimBack: Int = Int.MIN_VALUE, + crawlAnimLeft: Int = Int.MIN_VALUE, + crawlAnimRight: Int = Int.MIN_VALUE, + readyAnim: Int = Int.MIN_VALUE, + ) { + var flag = 0 + val bas = blocks.baseAnimationSet + if (turnLeftAnim != Int.MIN_VALUE) { + bas.turnLeftAnim = turnLeftAnim.toUShort() + flag = BaseAnimationSet.TURN_LEFT_ANIM_FLAG + } + if (turnRightAnim != Int.MIN_VALUE) { + bas.turnRightAnim = turnRightAnim.toUShort() + flag = BaseAnimationSet.TURN_RIGHT_ANIM_FLAG + } + if (walkAnim != Int.MIN_VALUE) { + bas.walkAnim = walkAnim.toUShort() + flag = BaseAnimationSet.WALK_ANIM_FLAG + } + if (walkAnimBack != Int.MIN_VALUE) { + bas.walkAnimBack = walkAnimBack.toUShort() + flag = BaseAnimationSet.WALK_ANIM_BACK_FLAG + } + if (walkAnimLeft != Int.MIN_VALUE) { + bas.walkAnimLeft = walkAnimLeft.toUShort() + flag = BaseAnimationSet.WALK_ANIM_LEFT_FLAG + } + if (walkAnimRight != Int.MIN_VALUE) { + bas.walkAnimRight = walkAnimRight.toUShort() + flag = BaseAnimationSet.WALK_ANIM_RIGHT_FLAG + } + if (runAnim != Int.MIN_VALUE) { + bas.runAnim = runAnim.toUShort() + flag = BaseAnimationSet.RUN_ANIM_FLAG + } + if (runAnimBack != Int.MIN_VALUE) { + bas.runAnimBack = runAnimBack.toUShort() + flag = BaseAnimationSet.RUN_ANIM_BACK_FLAG + } + if (runAnimLeft != Int.MIN_VALUE) { + bas.runAnimLeft = runAnimLeft.toUShort() + flag = BaseAnimationSet.RUN_ANIM_LEFT_FLAG + } + if (runAnimRight != Int.MIN_VALUE) { + bas.runAnimRight = runAnimRight.toUShort() + flag = BaseAnimationSet.RUN_ANIM_RIGHT_FLAG + } + if (crawlAnim != Int.MIN_VALUE) { + bas.crawlAnim = crawlAnim.toUShort() + flag = BaseAnimationSet.CRAWL_ANIM_FLAG + } + if (crawlAnimBack != Int.MIN_VALUE) { + bas.crawlAnimBack = crawlAnimBack.toUShort() + flag = BaseAnimationSet.CRAWL_ANIM_BACK_FLAG + } + if (crawlAnimLeft != Int.MIN_VALUE) { + bas.crawlAnimLeft = crawlAnimLeft.toUShort() + flag = BaseAnimationSet.CRAWL_ANIM_LEFT_FLAG + } + if (crawlAnimRight != Int.MIN_VALUE) { + bas.crawlAnimRight = crawlAnimRight.toUShort() + flag = BaseAnimationSet.CRAWL_ANIM_RIGHT_FLAG + } + if (readyAnim != Int.MIN_VALUE) { + bas.readyAnim = readyAnim.toUShort() + flag = BaseAnimationSet.READY_ANIM_FLAG + } + bas.overrides = flag + flags = flags or BAS_CHANGE + } + + /** + * Sets the ready animation of this NPC to the provided [id]. + * @param id the ready animation id + */ + public fun setReadyAnim(id: Int) { + baseAnimationSet(readyAnim = id) + } + + /** + * Sets the turn left and turn right animations of this NPC. + * @param turnLeftAnim the animation used when the NPC turns to the left, or null if + * turn left animation should be skipped + * @param turnRightAnim the animation used when the NPC turns to the right, or null if + * turn right animation should be skipped. + */ + public fun setTurnAnims( + turnLeftAnim: Int?, + turnRightAnim: Int?, + ) { + baseAnimationSet( + turnLeftAnim = turnLeftAnim ?: Int.MIN_VALUE, + turnRightAnim = turnRightAnim ?: Int.MIN_VALUE, + ) + } + + /** + * Sets the walk animations of this NPC. If any of the animations is null, that animation + * will not be overwritten by the client, allowing a subset of the below animations + * to be overridden. + * @param walkAnim the animation used when the NPC walks forward + * @param walkAnimBack the animation used when the NPC walks backwards + * @param walkAnimLeft the animation used when the NPC walks to the left + * @param walkAnimRight the animation used when the NPC walks to the right + */ + public fun setWalkAnims( + walkAnim: Int?, + walkAnimBack: Int?, + walkAnimLeft: Int?, + walkAnimRight: Int?, + ) { + baseAnimationSet( + walkAnim = walkAnim ?: Int.MIN_VALUE, + walkAnimBack = walkAnimBack ?: Int.MIN_VALUE, + walkAnimLeft = walkAnimLeft ?: Int.MIN_VALUE, + walkAnimRight = walkAnimRight ?: Int.MIN_VALUE, + ) + } + + /** + * Sets the run animations of this NPC. If any of the animations is null, that animation + * will not be overwritten by the client, allowing a subset of the below animations + * to be overridden. + * @param runAnim the animation used when the NPC runs forward + * @param runAnimBack the animation used when the NPC runs backwards + * @param runAnimLeft the animation used when the NPC runs to the left + * @param runAnimRight the animation used when the NPC runs to the right + */ + public fun setRunAnims( + runAnim: Int?, + runAnimBack: Int?, + runAnimLeft: Int?, + runAnimRight: Int?, + ) { + baseAnimationSet( + runAnim = runAnim ?: Int.MIN_VALUE, + runAnimBack = runAnimBack ?: Int.MIN_VALUE, + runAnimLeft = runAnimLeft ?: Int.MIN_VALUE, + runAnimRight = runAnimRight ?: Int.MIN_VALUE, + ) + } + + /** + * Sets the crawl animations of this NPC. If any of the animations is null, that animation + * will not be overwritten by the client, allowing a subset of the below animations + * to be overridden. + * @param crawlAnim the animation used when the NPC crawls forward + * @param crawlAnimBack the animation used when the NPC crawls backwards + * @param crawlAnimLeft the animation used when the NPC crawls to the left + * @param crawlAnimRight the animation used when the NPC crawls to the right + */ + public fun setCrawlAnims( + crawlAnim: Int?, + crawlAnimBack: Int?, + crawlAnimLeft: Int?, + crawlAnimRight: Int?, + ) { + baseAnimationSet( + crawlAnim = crawlAnim ?: Int.MIN_VALUE, + crawlAnimBack = crawlAnimBack ?: Int.MIN_VALUE, + crawlAnimLeft = crawlAnimLeft ?: Int.MIN_VALUE, + crawlAnimRight = crawlAnimRight ?: Int.MIN_VALUE, + ) + } + + /** + * Changes the head icon of a NPC to the sprite at the provided group and sprite index. + * @param slot the slot of the headicon, a value of 0-8 (exclusive) + * @param group the sprite group id in the cache. + * @param index the index of the sprite in that sprite file, as sprite files contain + * multiple sprites together. + */ + public fun headIconChange( + slot: Int, + group: Int, + index: Int, + ) { + verify { + require(slot in 0..<8) { + "Unexpected headicon slot: $slot, expected slot range: 0..<8" + } + require(index == -1 || index in UNSIGNED_SHORT_RANGE) { + "Unexpected headicon index: $index, expected value -1 or in range $UNSIGNED_SHORT_RANGE" + } + } + val headIcons = blocks.headIconCustomisation + headIcons.headIconGroups[slot] = group + headIcons.headIconIndices[slot] = index.toShort() + headIcons.flag = (1 shl slot) + flags = flags or HEADICON_CUSTOMISATION + } + + /** + * Resets the head icon at the specified [slot]. + * @param slot the slot of the head icon to reset. + */ + public fun resetHeadIcon(slot: Int) { + verify { + require(slot in 0..<8) { + "Unexpected headicon slot: $slot, expected slot range: 0..<8" + } + } + val headIcons = blocks.headIconCustomisation + headIcons.headIconGroups[slot] = -1 + headIcons.headIconIndices[slot] = -1 + headIcons.flag = (1 shl slot) + flags = flags or HEADICON_CUSTOMISATION + } + + /** + * Resets any chathead customisations applied to this NPC. + */ + public fun resetHeadCustomisations() { + blocks.headCustomisation.customisation = null + flags = flags or HEAD_CUSTOMISATION + } + + /** + * Sets the chathead of the NPC to be a mirror of the local player's own chathead. + */ + public fun setHeadCustomisationMirrored() { + blocks.headCustomisation.customisation = + TypeCustomisation( + emptyList(), + emptyList(), + emptyList(), + true, + ) + flags = flags or HEAD_CUSTOMISATION + } + + /** + * Sets the chat head customisation for this NPC. + * @param models the list of models to override; if the list is empty, models are not overridden. + * @param recolours the list of recolours to apply to this NPC; if the list is empty, + * recolours are not applied. If recolours are provided, the server MUST ensure that the number of recolours + * matches the number of source colours defined on the NPC in the cache, as the client reads based on the + * cache configuration. + * @param retextures the list of retextures to apply to this NPC; if the list is empty, + * retextures are not applied. If retextures are provided, the server MUST ensure that the number of retextures + * matches the number of source textures defined on the NPC in the cache, as the client reads based on the + * cache configuration. + */ + public fun setHeadCustomisation( + models: List, + recolours: List, + retextures: List, + ) { + blocks.headCustomisation.customisation = + TypeCustomisation( + models, + recolours, + retextures, + false, + ) + flags = flags or HEAD_CUSTOMISATION + } + + /** + * Resets any NPC body customisations applied. + */ + public fun resetBodyCustomisations() { + blocks.bodyCustomisation.customisation = null + flags = flags or BODY_CUSTOMISATION + } + + /** + * Sets the NPC to mirror the body of the local player in its entirety, including any worn gear. + */ + public fun setBodyCustomisationMirrored() { + blocks.bodyCustomisation.customisation = + TypeCustomisation( + emptyList(), + emptyList(), + emptyList(), + true, + ) + flags = flags or BODY_CUSTOMISATION + } + + /** + * Sets the NPC body customisation for this NPC. + * @param models the list of models to override; if the list is empty, models are not overridden. + * @param recolours the list of recolours to apply to this NPC; if the list is empty, + * recolours are not applied. If recolours are provided, the server MUST ensure that the number of recolours + * matches the number of source colours defined on the NPC in the cache, as the client reads based on the + * cache configuration. + * @param retextures the list of retextures to apply to this NPC; if the list is empty, + * retextures are not applied. If retextures are provided, the server MUST ensure that the number of retextures + * matches the number of source textures defined on the NPC in the cache, as the client reads based on the + * cache configuration. + */ + public fun setBodyCustomisation( + models: List, + recolours: List, + retextures: List, + ) { + blocks.bodyCustomisation.customisation = + TypeCustomisation( + models, + recolours, + retextures, + false, + ) + flags = flags or BODY_CUSTOMISATION + } + + /** + * Clears any transient information and resets the flag to zero at the end of the cycle. + */ + internal fun postUpdate() { + clearTransientExtendedInformation() + flags = 0 + } + + /** + * Resets all the properties of this extended info object, making it ready for use + * by another avatar. + */ + internal fun reset() { + flags = 0 + blocks.sequence.clear() + blocks.facePathingEntity.clear() + blocks.say.clear() + blocks.exactMove.clear() + blocks.spotAnims.clear() + blocks.hit.clear() + blocks.tinting.clear() + blocks.faceCoord.clear() + blocks.transformation.clear() + blocks.bodyCustomisation.clear() + blocks.headCustomisation.clear() + blocks.combatLevelChange.clear() + blocks.visibleOps.clear() + blocks.nameChange.clear() + blocks.headIconCustomisation.clear() + blocks.baseAnimationSet.clear() + } + + /** + * Pre-computes all the buffers for this avatar. + * Pre-computation is done, so we don't have to calculate these extended info blocks + * for every avatar that observes us. Instead, we can do more performance-efficient + * operations of native memory copying to get the latest extended info blocks. + */ + internal fun precompute() { + // Hits and tinting do not get precomputed + + precomputeCached() + if (flags and SEQUENCE != 0) { + blocks.sequence.precompute(allocator, huffmanCodec) + } + if (flags and SAY != 0) { + blocks.say.precompute(allocator, huffmanCodec) + } + if (flags and EXACT_MOVE != 0) { + blocks.exactMove.precompute(allocator, huffmanCodec) + } + if (flags and SPOTANIM != 0) { + blocks.spotAnims.precompute(allocator, huffmanCodec) + } + if (flags and TINTING != 0) { + blocks.tinting.precompute(allocator, huffmanCodec) + } + if (flags and FACE_COORD != 0) { + blocks.faceCoord.precompute(allocator, huffmanCodec) + } + if (flags and TRANSFORMATION != 0) { + blocks.transformation.precompute(allocator, huffmanCodec) + } + } + + /** + * Precomputes the extended info blocks which are cached and potentially transmitted + * to any players who newly observe this npc. The full list of extended info blocks + * which must be placed in here is seen in [getLowToHighResChangeExtendedInfoFlags]. + * Every condition there must be among this function, else it is possible to run into + * scenarios where a block isn't computed but is required in the future. + */ + internal fun precomputeCached() { + if (flags and OPS != 0) { + blocks.visibleOps.precompute(allocator, huffmanCodec) + } + if (flags and HEADICON_CUSTOMISATION != 0) { + blocks.headIconCustomisation.precompute(allocator, huffmanCodec) + } + if (flags and NAME_CHANGE != 0) { + blocks.nameChange.precompute(allocator, huffmanCodec) + } + if (flags and HEAD_CUSTOMISATION != 0) { + blocks.headCustomisation.precompute(allocator, huffmanCodec) + } + if (flags and BODY_CUSTOMISATION != 0) { + blocks.bodyCustomisation.precompute(allocator, huffmanCodec) + } + if (flags and LEVEL_CHANGE != 0) { + blocks.combatLevelChange.precompute(allocator, huffmanCodec) + } + if (flags and FACE_PATHINGENTITY != 0) { + blocks.facePathingEntity.precompute(allocator, huffmanCodec) + } + if (flags and BAS_CHANGE != 0) { + blocks.baseAnimationSet.precompute(allocator, huffmanCodec) + } + } + + /** + * Writes the extended info block of this avatar for the given observer. + * @param oldSchoolClientType the client that the observer is using. + * @param buffer the buffer into which the extended info block should be written. + * @param observerIndex index of the player avatar that is observing us. + * @param remainingAvatars the number of avatars that must still be updated for + * the given [observerIndex], necessary to avoid memory overflow. + */ + internal fun pExtendedInfo( + oldSchoolClientType: OldSchoolClientType, + buffer: JagByteBuf, + observerIndex: Int, + remainingAvatars: Int, + extraFlag: Int, + ) { + val flag = this.flags or extraFlag + if (!filter.accept( + buffer.writableBytes(), + flag, + remainingAvatars, + false, + ) + ) { + buffer.p1(0) + return + } + val writer = + requireNotNull(writers[oldSchoolClientType.id]) { + "Extended info writer missing for client $oldSchoolClientType" + } + + writer.pExtendedInfo( + buffer, + avatarIndex, + observerIndex, + flag, + blocks, + ) + } + + /** + * Gets the set of extended info blocks that were previously set but also + * need to be transmitted to any new users. + * @return the bit flag of all the non-transient extended info blocks that were previously flagged. + */ + internal fun getLowToHighResChangeExtendedInfoFlags(): Int { + var flag = 0 + if (this.flags and OPS == 0 && + blocks.visibleOps.ops != VisibleOps.DEFAULT_OPS + ) { + flag = flag or OPS + } + if (this.flags and HEADICON_CUSTOMISATION == 0 && + blocks.headIconCustomisation.flag != HeadIconCustomisation.DEFAULT_FLAG + ) { + flag = flag or HEADICON_CUSTOMISATION + } + if (this.flags and NAME_CHANGE == 0 && + blocks.nameChange.name != null + ) { + flag = flag or NAME_CHANGE + } + if (this.flags and HEAD_CUSTOMISATION == 0 && + blocks.headCustomisation.customisation != null + ) { + flag = flag or HEAD_CUSTOMISATION + } + if (this.flags and BODY_CUSTOMISATION == 0 && + blocks.bodyCustomisation.customisation != null + ) { + flag = flag or BODY_CUSTOMISATION + } + if (this.flags and LEVEL_CHANGE == 0 && + blocks.combatLevelChange.level != CombatLevelChange.DEFAULT_LEVEL_OVERRIDE + ) { + flag = flag or LEVEL_CHANGE + } + if (this.flags and FACE_PATHINGENTITY == 0 && + blocks.facePathingEntity.index != FacePathingEntity.DEFAULT_VALUE + ) { + flag = flag or FACE_PATHINGENTITY + } + if (this.flags and BAS_CHANGE == 0 && + blocks.baseAnimationSet.overrides != BaseAnimationSet.DEFAULT_OVERRIDES_FLAG + ) { + flag = flag or BAS_CHANGE + } + return flag + } + + /** + * Clears any transient extended info that was flagged in this cycle. + */ + private fun clearTransientExtendedInformation() { + if (flags and SEQUENCE != 0) { + blocks.sequence.clear() + } + if (flags and SAY != 0) { + blocks.say.clear() + } + if (flags and EXACT_MOVE != 0) { + blocks.exactMove.clear() + } + if (flags and SPOTANIM != 0) { + blocks.spotAnims.clear() + } + if (flags and HITS != 0) { + blocks.hit.clear() + } + if (flags and TINTING != 0) { + blocks.tinting.clear() + } + if (flags and FACE_COORD != 0) { + blocks.faceCoord.clear() + } + if (flags and TRANSFORMATION != 0) { + blocks.transformation.clear() + } + } + + override fun toString(): String = + "NpcAvatarExtendedInfo(" + + "avatarIndex=$avatarIndex, " + + "flags=$flags" + + ")" + + public companion object { + private val SIGNED_BYTE_RANGE: IntRange = Byte.MIN_VALUE.toInt()..Byte.MAX_VALUE.toInt() + private val UNSIGNED_BYTE_RANGE: IntRange = UByte.MIN_VALUE.toInt()..UByte.MAX_VALUE.toInt() + private val UNSIGNED_SHORT_RANGE: IntRange = UShort.MIN_VALUE.toInt()..UShort.MAX_VALUE.toInt() + private val UNSIGNED_SMART_1_OR_2_RANGE: IntRange = 0..0x7FFF + + // Observer-dependent flags, utilizing the lowest bits as we store observer flags in a byte array + // IMPORTANT: As we store it in a byte array, we currently only support 8 blocks + // all of which are currently filled. If more are needed, the data structure needs + // to be updated to a short array. + public const val OPS: Int = 0x1 + public const val HEADICON_CUSTOMISATION: Int = 0x2 + public const val NAME_CHANGE: Int = 0x4 + public const val HEAD_CUSTOMISATION: Int = 0x8 + public const val BODY_CUSTOMISATION: Int = 0x10 + public const val LEVEL_CHANGE: Int = 0x20 + public const val FACE_PATHINGENTITY: Int = 0x40 + public const val BAS_CHANGE: Int = 0x80 + + // "Static" flags, the bit values here are irrelevant + public const val TINTING: Int = 0x100 + public const val SAY: Int = 0x200 + public const val HITS: Int = 0x400 + public const val FACE_COORD: Int = 0x800 + public const val TRANSFORMATION: Int = 0x1000 + public const val SEQUENCE: Int = 0x2000 + public const val EXACT_MOVE: Int = 0x4000 + public const val SPOTANIM: Int = 0x8000 + + /** + * Executes the [block] if input verification is enabled, + * otherwise does nothing. Verification should be enabled for + * development environments, to catch problems mid-development. + * In production, or during benchmarking, verification should be disabled, + * as there is still some overhead to running verifications. + */ + private inline fun verify(crossinline block: () -> Unit) { + if (RSProtFlags.extendedInfoInputVerification) { + block() + } + } + + /** + * Builds an extended info writer array indexed by provided client types. + * All client types which are utilized must be registered to avoid runtime errors. + */ + private fun buildClientWriterArray( + extendedInfoWriters: List, + ): Array { + val array = + arrayOfNulls( + OldSchoolClientType.COUNT, + ) + for (writer in extendedInfoWriters) { + array[writer.oldSchoolClientType.id] = writer + } + return array + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExtendedInfoBlocks.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExtendedInfoBlocks.kt new file mode 100644 index 000000000..a7093cdd1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarExtendedInfoBlocks.kt @@ -0,0 +1,78 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.ExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.ExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.encoder.NpcExtendedInfoEncoders +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.BaseAnimationSet +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.BodyCustomisation +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.CombatLevelChange +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.FaceCoord +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.HeadCustomisation +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.HeadIconCustomisation +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.NameChange +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.NpcTinting +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.Transformation +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.extendedinfo.VisibleOps +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.ExactMove +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.FacePathingEntity +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Hit +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Say +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Sequence +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.SpotAnimList +import net.rsprot.protocol.game.outgoing.info.AvatarExtendedInfoWriter + +private typealias NEnc = NpcExtendedInfoEncoders +private typealias HeadIcon = HeadIconCustomisation +private typealias NpcExtendedInfoWriters = + List> + +/** + * A data structure to bring all the extended info blocks together, + * so the information can be passed onto various client-specific encoders. + * @param writers the list of client-specific writers. + * The writers must be client-specific too, not just encoders, as + * the order in which the extended info blocks get written must follow + * the exact order described by the client. + */ +public class NpcAvatarExtendedInfoBlocks( + writers: NpcExtendedInfoWriters, +) { + public val spotAnims: SpotAnimList = SpotAnimList(encoders(writers, NEnc::spotAnim)) + public val say: Say = Say(encoders(writers, NEnc::say)) + public val visibleOps: VisibleOps = VisibleOps(encoders(writers, NEnc::visibleOps)) + public val exactMove: ExactMove = ExactMove(encoders(writers, NEnc::exactMove)) + public val sequence: Sequence = Sequence(encoders(writers, NEnc::sequence)) + public val tinting: NpcTinting = NpcTinting(encoders(writers, NEnc::tinting)) + public val headIconCustomisation: HeadIcon = HeadIcon(encoders(writers, NEnc::headIconCustomisation)) + public val nameChange: NameChange = NameChange(encoders(writers, NEnc::nameChange)) + public val headCustomisation: HeadCustomisation = HeadCustomisation(encoders(writers, NEnc::headCustomisation)) + public val bodyCustomisation: BodyCustomisation = BodyCustomisation(encoders(writers, NEnc::bodyCustomisation)) + public val transformation: Transformation = Transformation(encoders(writers, NEnc::transformation)) + public val combatLevelChange: CombatLevelChange = CombatLevelChange(encoders(writers, NEnc::combatLevelChange)) + public val hit: Hit = Hit(encoders(writers, NEnc::hit)) + public val faceCoord: FaceCoord = FaceCoord(encoders(writers, NEnc::faceCoord)) + public val facePathingEntity: FacePathingEntity = FacePathingEntity(encoders(writers, NEnc::facePathingEntity)) + public val baseAnimationSet: BaseAnimationSet = BaseAnimationSet(encoders(writers, NEnc::baseAnimationSet)) + + private companion object { + /** + * Builds a client-specific map of encoders for a specific extended info block, + * keyed by [OldSchoolClientType.id]. + * If a client hasn't been registered, the encoder at that index will be null. + * @param allEncoders all the client-specific extended info writers for the given type. + * @param selector a higher order function to retrieve a specific extended info block from + * the full structure of all the extended info blocks. + * @return a map of client-specific encoders of the given extended info block, + * keyed by [OldSchoolClientType.id]. + */ + private inline fun , reified E : ExtendedInfoEncoder> encoders( + allEncoders: NpcExtendedInfoWriters, + selector: (NpcExtendedInfoEncoders) -> E, + ): ClientTypeMap = + ClientTypeMap.ofType(allEncoders, OldSchoolClientType.COUNT) { + it.encoders.oldSchoolClientType to selector(it.encoders) + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarFactory.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarFactory.kt new file mode 100644 index 000000000..4116b5298 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarFactory.kt @@ -0,0 +1,92 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.game.outgoing.info.filter.ExtendedInfoFilter + +/** + * NPC avatar factor is responsible for allocating new avatars for NPCs, + * or, if possible, re-using old ones that are no longer in use, to avoid generating + * mass amounts of garbage. + * @param allocator the byte buffer allocator used to pre-compute bitcodes for this avatar. + * @param extendedInfoFilter the filter used to determine whether the given NPC can still + * have extended info blocks written to it, or if we have to utilize a fall-back and tell + * the client that despite extended info having been flagged, we cannot write it (by writing + * the flag itself as a zero, so the client reads no further information). + * @param extendedInfoWriter the client-specific extended info writers for NPC information. + * @param huffmanCodec the huffman codec is used to compress chat extended info. + * While NPCs do not currently have any such extended info blocks, the interface requires + * it be passed in, so we must still provide it. + */ +public class NpcAvatarFactory( + allocator: ByteBufAllocator, + extendedInfoFilter: ExtendedInfoFilter, + extendedInfoWriter: List, + huffmanCodec: HuffmanCodecProvider, +) { + /** + * The avatar repository is responsible for keeping track of all avatars, including ones + * which are no longer in use - but can be used in the future. + */ + internal val avatarRepository: NpcAvatarRepository = + NpcAvatarRepository( + allocator, + extendedInfoFilter, + extendedInfoWriter, + huffmanCodec, + ) + + /** + * Allocates a new NPC avatar, or re-uses an older cached one if possible. + * + * Npc direction table: + * ``` + * | Id | Client Angle | Direction | + * |:--:|:------------:|:----------:| + * | 0 | 768 | North-West | + * | 1 | 1024 | North | + * | 2 | 1280 | North-East | + * | 3 | 512 | West | + * | 4 | 1536 | East | + * | 5 | 256 | South-West | + * | 6 | 0 | South | + * | 7 | 1792 | South-East | + * ``` + * + * @param index the index of the npc in the world + * @param id the id of the npc in the world, limited to range of 0..16383 + * @param level the height level of the npc + * @param x the absolute x coordinate of the npc + * @param z the absolute z coordinate of the npc + * @param spawnCycle the game cycle on which the npc spawned into the world; + * for static NPCs, this would always be zero. This is only used by the C++ clients. + * @param direction the direction that the npc will face on spawn (see table above) + * @return a npc avatar with the above provided details. + */ + public fun alloc( + index: Int, + id: Int, + level: Int, + x: Int, + z: Int, + spawnCycle: Int = 0, + direction: Int = 0, + ): NpcAvatar = + avatarRepository.getOrAlloc( + index, + id, + level, + x, + z, + spawnCycle, + direction, + ) + + /** + * Releases the avatar back into the repository to be used by other NPCs. + * @param avatar the avatar to release. + */ + public fun release(avatar: NpcAvatar) { + avatarRepository.release(avatar) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarRepository.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarRepository.kt new file mode 100644 index 000000000..0098560d4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcAvatarRepository.kt @@ -0,0 +1,154 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.NpcAvatarDetails +import net.rsprot.protocol.game.outgoing.info.filter.ExtendedInfoFilter +import java.lang.ref.ReferenceQueue +import java.lang.ref.SoftReference + +/** + * The NPC avatar repository is a class responsible for keeping track of all the avatars + * in the game, as well as allocating/re-using new instances if needed. + * @param allocator the byte buffer allocator used to pre-compute bitcodes for an avatar. + * @param extendedInfoFilter the filter used to determine whether the given NPC can still + * have extended info blocks written to it, or if we have to utilize a fall-back and tell + * the client that despite extended info having been flagged, we cannot write it (by writing + * the flag itself as a zero, so the client reads no further information). + * @param extendedInfoWriter the client-specific extended info writers for NPC information. + * @param huffmanCodec the huffman codec is used to compress chat extended info. + * While NPCs do not currently have any such extended info blocks, the interface requires + * it be passed in, so we must still provide it. + */ +internal class NpcAvatarRepository( + private val allocator: ByteBufAllocator, + private val extendedInfoFilter: ExtendedInfoFilter, + private val extendedInfoWriter: List, + private val huffmanCodec: HuffmanCodecProvider, +) { + /** + * The array of npc avatars that currently exist in the game. + */ + private val elements: Array = arrayOfNulls(AVATAR_CAPACITY) + + /** + * A soft-reference queue of avatars that are no longer in use. + * If the server requires the memory, these references will be freed up, but this is + * only as a last resort. Other than that, these instances should remain available + * for a long period of time - rightfully so as extended info blocks primarily + * are the heavy part. + */ + private val queue: ReferenceQueue = ReferenceQueue() + + /** + * Gets a npc avatar at the provided index, or null if it doesn't exist yet. + * @param idx the index of the avatar to obtain + * @return the npc avatar, or null if it doesn't exist + * @throws ArrayIndexOutOfBoundsException if the [idx] is below 0, or >= [AVATAR_CAPACITY] + */ + fun getOrNull(idx: Int): NpcAvatar? = elements[idx] + + /** + * Gets an older avatar, or makes a new one depending on the circumstances. + * If using an older one, this function is responsible for sanitizing the older avatar + * so that it is equal to a new instance. + * + * Npc direction table: + * ``` + * | Id | Client Angle | Direction | + * |:--:|:------------:|:----------:| + * | 0 | 768 | North-West | + * | 1 | 1024 | North | + * | 2 | 1280 | North-East | + * | 3 | 512 | West | + * | 4 | 1536 | East | + * | 5 | 256 | South-West | + * | 6 | 0 | South | + * | 7 | 1792 | South-East | + * ``` + * + * @param index the index of the npc in the world + * @param id the id of the npc in the world, limited to range of 0..16383 + * @param level the height level of the npc + * @param x the absolute x coordinate of the npc + * @param z the absolute z coordinate of the npc + * @param spawnCycle the game cycle on which the npc spawned into the world; + * for static NPCs, this would always be zero. This is only used by the C++ clients. + * @param direction the direction that the npc will face on spawn (see table above) + * @return a npc avatar with the above provided details. + */ + fun getOrAlloc( + index: Int, + id: Int, + level: Int, + x: Int, + z: Int, + spawnCycle: Int = 0, + direction: Int = 0, + ): NpcAvatar { + val existing = queue.poll()?.get() + if (existing != null) { + existing.resetObservers() + val details = existing.details + resetTransientDetails(details) + details.index = index + details.id = id + details.currentCoord = CoordGrid(level, x, z) + details.spawnCycle = spawnCycle + details.direction = direction + details.allocateCycle = NpcInfoProtocol.cycleCount + elements[index] = existing + return existing + } + val extendedInfo = + NpcAvatarExtendedInfo( + index, + extendedInfoFilter, + extendedInfoWriter, + allocator, + huffmanCodec, + ) + val avatar = + NpcAvatar( + index, + id, + level, + x, + z, + spawnCycle, + direction, + NpcInfoProtocol.cycleCount, + extendedInfo, + ) + elements[index] = avatar + return avatar + } + + /** + * Releases avatar back into the pool for it to be used later in the future, if possible. + * @param avatar the avatar to release. + */ + fun release(avatar: NpcAvatar) { + this.elements[avatar.details.index] = null + avatar.extendedInfo.reset() + val reference = SoftReference(avatar, queue) + reference.enqueue() + } + + /** + * Resets all the transient properties with the default values. + * @param details the npc avatar details class holding all the properties of a NPC. + */ + private fun resetTransientDetails(details: NpcAvatarDetails) { + details.stepCount = 0 + details.firstStep = -1 + details.secondStep = -1 + details.movementType = 0 + details.inaccessible = false + } + + internal companion object { + internal const val AVATAR_CAPACITY = 65536 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcIndexSupplier.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcIndexSupplier.kt new file mode 100644 index 000000000..e5cb45896 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcIndexSupplier.kt @@ -0,0 +1,55 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +/** + * NPC index supplier is an interface used to yield NPCs near a player from the server's + * perspective. This is necessary as the protocol cannot directly communicate with the server, + * so an interface is needed to provide the indices from the server's end. + */ +public fun interface NpcIndexSupplier { + /** + * The supply function should yield **all** npc indices that are + * in range of the local player and should be rendered to them. + * It is important to note that the server will be unaware of the indices + * that are already tracked by a given player's npc info; the npc info protocol + * is responsible for ignoring NPCs it already tracks in such cases. + * Additionally, the protocol is responsible for taking as many indices as it + * can realistically process. This means that the iterator may be left in + * a partially-consumed state. + * + * One additional side note, because all the indices will be in range of 0..<65535, + * setting the VM flag `-XX:AutoBoxCacheMax=65535` will help reduce garbage creation, + * as all the indices will perfectly fit into the integer autobox cache. + * + * @param localPlayerIndex the index of the local player, in case further checks + * are needed to be executed for that player. + * @param level the height level at which the local player is + * @param x the x coordinate at which the local player is + * @param z the z coordinate at which the local player is + * @param viewDistance the radius how far the local player should be able to see + * other NPCs, inclusive. + * @return an iterator that provides all the NPC indices within [viewDistance] range + * of the local player. For emulation purposes, the iteration should begin with the + * south-westernmost zone, going north, then east, ie this pattern (in ascending order): + * ``` + * - - - - - + * | 3 6 9 | + * | 2 5 8 | + * | 1 4 7 | + * - - - - - + * ``` + * + * As for indexing within each zone, the oldest NPCs should be returned first, meaning + * a natural ascending order. + * Furthermore, as `& 65535` is performed on each index, values of -1/65535 will be skipped + * in processing, if there is a need to yield an invalid index for whatever reason. + * It is important to note that indices which are out of bounds will have the higher + * order bits ignored, and will likely result in a crash within the protocol. + */ + public fun supply( + localPlayerIndex: Int, + level: Int, + x: Int, + z: Int, + viewDistance: Int, + ): Iterator +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfo.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfo.kt new file mode 100644 index 000000000..2a3006b1d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfo.kt @@ -0,0 +1,610 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.bitbuffer.BitBuf +import net.rsprot.buffer.bitbuffer.toBitBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.common.game.outgoing.info.npcinfo.encoder.NpcResolutionChangeEncoder +import net.rsprot.protocol.game.outgoing.info.exceptions.InfoProcessException +import net.rsprot.protocol.game.outgoing.info.util.BuildArea +import net.rsprot.protocol.game.outgoing.info.util.ReferencePooledObject +import net.rsprot.protocol.message.OutgoingGameMessage +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +/** + * An implementation of the npc info packet. + * This class is responsible for bringing together all the bits of the npc info protocol, + * including copying all the pre-built buffers that were made beforehand. + * @property allocator the byte buffer allocator used to allocate new buffers to be used + * for the npc info packet, as well as the pre-built extended info buffers. + * @property repository the npc avatar repository, keeping track of every npc avatar that exists + * in the game. + * @property oldSchoolClientType the client the player owning this npc info packet is on + * @property localPlayerIndex the index of the local player that owns this npc info packet. + * @property indexSupplier a supplier-style interface responsible for yielding npc indices + * which are within vicinity of the player. This is the primary way the server will be providing + * information about nearby NPCs to a player, as well as whether to render the NPC in the first place, + * as some NPCs are meant to only render to a given player if certain conditions are met. + * @property lowResolutionToHighResolutionEncoders a client map of low resolution to high resolution + * change encoders, used to move a npc into high resolution for the given player. + * As this is scrambled, a separate client-specific implementation is required. + */ +@OptIn(ExperimentalUnsignedTypes::class) +@Suppress("ReplaceUntilWithRangeUntil") +public class NpcInfo internal constructor( + private val allocator: ByteBufAllocator, + private val repository: NpcAvatarRepository, + private var oldSchoolClientType: OldSchoolClientType, + internal var localPlayerIndex: Int, + private val indexSupplier: NpcIndexSupplier, + private val lowResolutionToHighResolutionEncoders: ClientTypeMap, + private val detailsStorage: NpcInfoWorldDetailsStorage, +) : ReferencePooledObject { + /** + * The maximum view distance how far a player will see other NPCs. + * Unlike with player info, this does not automatically resize to accommodate for nearby NPCs, + * as it is almost impossible for such a scenario to happen in the first place. + * It is confirmed that OldSchool RuneScape does not do it either. + */ + private var viewDistance: Int = MAX_SMALL_PACKET_DISTANCE + + /** + * The exception that was caught during the processing of this player's npc info packet. + * This exception will be propagated further during the [toNpcInfoPacket] function call, + * allowing the server to handle it properly at a per-player basis. + */ + internal var exception: Exception? = null + + /** + * An array of world details, containing all the player info properties specific to a single world. + * The root world is placed at the end of this array, however id -1 will be treated as the root. + */ + internal val details: Array = arrayOfNulls(WORLD_ENTITY_CAPACITY + 1) + + /** + * Updates the build area of a given world to the specified one. + * This will ensure that no NPCs outside of this box will be + * added to high resolution view. + * @param worldId the id of the world to set the build area of, + * with -1 being the root world. + * @param buildArea the build area to assign. + */ + public fun updateBuildArea( + worldId: Int, + buildArea: BuildArea, + ) { + require(worldId == ROOT_WORLD || worldId in 0..<2048) { + "World id must be -1 or in range of 0..<2048" + } + val details = getDetails(worldId) + details.buildArea = buildArea + } + + /** + * Allocates a new NPC info tracking object for the respective [worldId], + * keeping track of everyone that's within this new world entity. + * @param worldId the new world entity id + */ + public fun allocateWorld(worldId: Int) { + require(worldId in 0.. MAX_SMALL_PACKET_DISTANCE) { + NpcInfoLarge(backingBuffer(worldId)) + } else { + NpcInfoSmall(backingBuffer(worldId)) + } + } + + /** + * Allocates a new buffer from the [allocator] with a capacity of [BUF_CAPACITY]. + * The old [NpcInfoWorldDetails.buffer] will not be released, as that is the duty of the encoder class. + */ + @Suppress("DuplicatedCode") + private fun allocBuffer(worldId: Int): ByteBuf { + val details = getDetails(worldId) + // If a given player's packet was never sent out, we need to release the old buffer + if (!details.builtIntoPacket) { + val oldBuf = details.buffer + if (oldBuf != null && oldBuf.refCnt() > 0) { + oldBuf.release() + } + } + // Acquire a new buffer with each cycle, in case the previous one isn't fully written out yet + val buffer = allocator.buffer(BUF_CAPACITY, BUF_CAPACITY) + details.buffer = buffer + details.builtIntoPacket = false + return buffer + } + + /** + * Updates the coordinate of the local player, as this is necessary to know + * how far NPCs nearby are to the player, which allows us to remove NPCs that + * have gone too far out, and add NPCs that are within certain distance. + * @param level the height level of the local player + * @param x the x coordinate of the local player + * @param z the z coordinate of the local player + */ + public fun updateCoord( + worldId: Int, + level: Int, + x: Int, + z: Int, + ) { + val details = getDetails(worldId) + details.localPlayerCurrentCoord = + CoordGrid( + level, + x, + z, + ) + } + + /** + * Computes the high resolution and low resolution bitcodes for this given player, + * additionally marks down which NPCs need to furthermore send their extended info + * updates. + */ + internal fun compute(details: NpcInfoWorldDetails) { + val viewDistance = this.viewDistance + val buffer = allocBuffer(details.worldId) + buffer.toBitBuf().use { bitBuffer -> + val fragmented = processHighResolution(details, bitBuffer, viewDistance) + if (fragmented) { + details.defragmentIndices() + } + processLowResolution(details, bitBuffer, viewDistance) + // Terminate the low-resolution processing block if there are extended info + // blocks after that; if not, the loop ends naturally due to not enough + // readable bits remaining (at most would have 7 bits remaining due to + // the bit writer closing, which "finishes" the current byte). + if (details.extendedInfoCount > 0) { + bitBuffer.pBits(16, 0xFFFF) + } + } + } + + /** + * Synchronizes the last coordinate of the local player with the current coordinate + * set previously in this cycle. This is simply to help make removal of all NPCs + * in high resolution more efficient, as we can avoid distance checks against every + * NPC, and only do so against the player's last coordinate. + */ + public fun afterUpdate() { + for (details in this.details) { + if (details == null) { + continue + } + details.localPlayerLastCoord = details.localPlayerCurrentCoord + details.extendedInfoCount = 0 + details.observerExtendedInfoFlags.reset() + } + } + + /** + * Writes the extended info blocks over to the backing buffer, based on the indices + * of the NPCs from whom we requested extended info updates prior in this cycle. + */ + internal fun putExtendedInfo(details: NpcInfoWorldDetails) { + val jagBuffer = backingBuffer(details).toJagByteBuf() + for (i in 0 until details.extendedInfoCount) { + val index = details.extendedInfoIndices[i].toInt() + val other = checkNotNull(repository.getOrNull(index)) + val observerFlag = other.extendedInfo.flags or details.observerExtendedInfoFlags.getFlag(i) + other.extendedInfo.pExtendedInfo( + oldSchoolClientType, + jagBuffer, + localPlayerIndex, + details.extendedInfoCount - i, + observerFlag, + ) + } + } + + /** + * Processes high resolution, existing, NPCs by writing their movements/extended info updates, + * or removes them altogether if need be. + * @param buffer the buffer into which to write the bitcode information. + * @param viewDistance the maximum view distance how far a NPC can be seen. + * If the npc is farther away from the local player than the provided view distance, + * they will be removed from high resolution view. + * @return whether any high resolution npcs were removed in the middle of the + * array. This does not include the npcs dropped off at the end. + * This is necessary to determine whether we need to defragment the array (ie remove any + * gaps that were produced by removing npcs in the middle of the array). + */ + private fun processHighResolution( + details: NpcInfoWorldDetails, + buffer: BitBuf, + viewDistance: Int, + ): Boolean { + // If no one to process, skip + if (details.highResolutionNpcIndexCount == 0) { + buffer.pBits(8, 0) + return false + } + // If our coordinate compared to last cycle changed more than 'viewDistance' + // tiles, every NPC in our local view would be removed anyhow, + // so by sending the count as 0, client automatically removes everyone + if (isTooFar(details, viewDistance)) { + buffer.pBits(8, 0) + // While it would be more efficient to just... not do this block below, + // the reality is there are ~25k static npcs in the game alone, + // and by tracking the observer counts we can omit computing + // extended info as well as high resolution movement blocks + // for any npc that doesn't have a player near them, + // which, even at full world, will be the majority of npcs. + for (i in 0..= MAX_HIGH_RESOLUTION_NPCS) { + return + } + val encoder = lowResolutionToHighResolutionEncoders[oldSchoolClientType] + val largeDistance = viewDistance > MAX_SMALL_PACKET_DISTANCE + val npcs = + this.indexSupplier.supply( + localPlayerIndex, + details.localPlayerCurrentCoord.level, + details.localPlayerCurrentCoord.x, + details.localPlayerCurrentCoord.z, + viewDistance, + ) + while (npcs.hasNext()) { + val index = npcs.next() and NPC_INFO_CAPACITY + if (index == NPC_INFO_CAPACITY || isHighResolution(details, index)) { + continue + } + if (details.highResolutionNpcIndexCount >= MAX_HIGH_RESOLUTION_NPCS) { + break + } + val avatar = repository.getOrNull(index) ?: continue + if (avatar.details.inaccessible) { + continue + } + if (!isInBuildArea(details, avatar)) { + continue + } + avatar.addObserver() + val i = details.highResolutionNpcIndexCount++ + details.highResolutionNpcIndices[i] = index.toUShort() + val observerFlags = avatar.extendedInfo.getLowToHighResChangeExtendedInfoFlags() + details.observerExtendedInfoFlags.addFlag(details.extendedInfoCount, observerFlags) + val extendedInfo = (avatar.extendedInfo.flags or observerFlags) != 0 + if (extendedInfo) { + details.extendedInfoIndices[details.extendedInfoCount++] = index.toUShort() + } + encoder.encode( + buffer, + avatar.details, + extendedInfo, + details.localPlayerCurrentCoord, + largeDistance, + NpcInfoProtocol.cycleCount, + ) + } + } + + /** + * Checks whether a npc by the index of [index] is already within our high resolution + * view. + * @param index the index of the npc to check + * @return whether the npc at the given index is already in high resolution. + */ + private fun isHighResolution( + details: NpcInfoWorldDetails, + index: Int, + ): Boolean { + // NOTE: Perhaps it's more efficient to just allocate 65535 bits and do a bit check? + // Would cost ~16.76mb at max world capacity + for (i in 0.., + avatarFactory: NpcAvatarFactory, + private val exceptionHandler: NpcAvatarExceptionHandler, + private val worker: ProtocolWorker = DefaultProtocolWorker(), +) { + private val detailsStorage: NpcInfoWorldDetailsStorage = NpcInfoWorldDetailsStorage() + + /** + * The avatar repository keeps track of all the avatars currently in the game. + */ + private val avatarRepository = avatarFactory.avatarRepository + + /** + * Npc info repository keeps track of the main npc info objects which are allocated + * by players at a 1:1 ratio. + */ + private val npcInfoRepository: NpcInfoRepository = + NpcInfoRepository { localIndex, clientType -> + NpcInfo( + allocator, + avatarRepository, + clientType, + localIndex, + npcIndexSupplier, + resolutionChangeEncoders, + detailsStorage, + ) + } + + /** + * The list of [Callable] instances which perform the jobs for player info. + * This list itself is re-used throughout the lifespan of the application, + * but the [Callable] instances themselves are generated for every job. + */ + private val callables: MutableList> = ArrayList(PROTOCOL_CAPACITY) + + /** + * Allocates a new npc info object, or re-uses an older one if possible. + * @param idx the index of the player allocating the npc info object. + * @param oldSchoolClientType the client on which the player has logged into. + */ + public fun alloc( + idx: Int, + oldSchoolClientType: OldSchoolClientType, + ): NpcInfo = npcInfoRepository.alloc(idx, oldSchoolClientType) + + /** + * Deallocates the provided npc info object, allowing it to be used up + * by another player in the future. + * @param info the npc info object to deallocate + */ + public fun dealloc(info: NpcInfo) { + npcInfoRepository.dealloc(info.localPlayerIndex) + } + + /** + * Gets the npc info at the provided index. + * @param idx the index of the npc info + * @return npc info object at that index + * @throws IllegalStateException if the npc info is null at that index + * @throws ArrayIndexOutOfBoundsException if the index is out of bounds + */ + public operator fun get(idx: Int): NpcInfo = npcInfoRepository[idx] + + /** + * Updates the npc info protocol for this cycle. + * The jobs here will be executed according to the [worker] specified, + * allowing multithreaded execution if selected. + */ + public fun update() { + prepareBitcodes() + putBitcodes() + prepareExtendedInfo() + putExtendedInfo() + postUpdate() + cycleCount++ + } + + /** + * Prepares the high resolution bitcodes of all the NPC avatars which have + * at least one observer. + */ + private fun prepareBitcodes() { + for (i in 0.. high resolution changes + if (!avatar.hasObservers()) { + avatar.extendedInfo.precomputeCached() + } else { + avatar.extendedInfo.precompute() + } + } catch (e: Exception) { + exceptionHandler.exceptionCaught(i, e) + } catch (t: Throwable) { + logger.error(t) { + "Error during npc extended info preparation" + } + throw t + } + } + } + + /** + * Writes the bitcodes of npc info objects over into the buffer. + * The work is split across according to the [worker] specified. + */ + private fun putBitcodes() { + execute { + for (details in this.details) { + if (details == null) { + continue + } + compute(details) + } + } + } + + /** + * Writes the extended info blocks over into the buffer. + * The work is split across according to the [worker] specified. + */ + private fun putExtendedInfo() { + execute { + for (details in this.details) { + if (details == null) { + continue + } + putExtendedInfo(details) + } + } + } + + /** + * Cleans up any single-cycle temporary information for npc info protocol. + */ + private fun postUpdate() { + for (i in 1.. Unit) { + for (i in 1.. NpcInfo, +) : InfoRepository(allocator) { + override val elements: Array = arrayOfNulls(NpcInfoProtocol.PROTOCOL_CAPACITY) + + override fun informDeallocation(idx: Int) { + // No-op + } + + override fun onDealloc(element: NpcInfo) { + element.onDealloc() + } + + override fun onAlloc( + element: NpcInfo, + idx: Int, + oldSchoolClientType: OldSchoolClientType, + newInstance: Boolean, + ) { + element.onAlloc(idx, oldSchoolClientType, newInstance) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoSmall.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoSmall.kt new file mode 100644 index 000000000..af83b2c77 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoSmall.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import io.netty.buffer.ByteBuf +import io.netty.buffer.DefaultByteBufHolder +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * A small npc info wrapper packet, used to wrap the pre-built buffer from the npc info class. + */ +public class NpcInfoSmall( + buffer: ByteBuf, +) : DefaultByteBufHolder(buffer), + OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + return true + } + + override fun hashCode(): Int = super.hashCode() + + override fun toString(): String = super.toString() +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoWorldDetails.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoWorldDetails.kt new file mode 100644 index 000000000..f2abf32f3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoWorldDetails.kt @@ -0,0 +1,191 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import io.netty.buffer.ByteBuf +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.outgoing.info.ObserverExtendedInfoFlags +import net.rsprot.protocol.game.outgoing.info.util.BuildArea + +/** + * A world detail implementation for NPC info, tracking local NPCs in a specific world. + * @property worldId the id of the world in which the NPCs exist. + */ +@OptIn(ExperimentalUnsignedTypes::class) +internal class NpcInfoWorldDetails( + internal var worldId: Int, +) { + /** + * The last cycle's coordinate of the local player, used to perform faster npc removal. + * If the player moves a greater distance than the [NpcInfo.viewDistance], we can make the assumption + * that all the existing high-resolution NPCs need to be removed, and thus remove them + * in a simplified manner, rather than applying a coordinate check on each one. This commonly + * occurs whenever a player teleports far away. + */ + internal var localPlayerLastCoord: CoordGrid = CoordGrid.INVALID + + /** + * The current coordinate of the local player used for the calculations of this npc info + * packet. This will be cross-referenced against NPCs to ensure they are within distance. + */ + internal var localPlayerCurrentCoord: CoordGrid = CoordGrid.INVALID + + /** + * The entire build area of this world - this effectively caps what we can see + * to be within this block of land. Anything outside will be excluded. + */ + internal var buildArea: BuildArea = BuildArea.INVALID + + /** + * The indices of the high resolution NPCs, in the order as they came in. + * This is a replica of how the client keeps track of NPCs. + */ + internal var highResolutionNpcIndices: UShortArray = + UShortArray(MAX_HIGH_RESOLUTION_NPCS) { + NPC_INDEX_TERMINATOR + } + + /** + * A secondary array for high resolution NPCs. + * After each cycle, the [highResolutionNpcIndices] gets swapped with this property, + * and the indices will be appended one by one. As a result of it, we can get away + * with significantly fewer operations to defragment the array, as we don't have to + * shift every entry over, we only need to fill in the ones that still exist. + */ + private var temporaryHighResolutionNpcIndices: UShortArray = + UShortArray(MAX_HIGH_RESOLUTION_NPCS) { + NPC_INDEX_TERMINATOR + } + + /** + * A counter for how many high resolution NPCs are currently being tracked. + * This count cannot exceed [MAX_HIGH_RESOLUTION_NPCS], as the client + * only supports that many extended info updates. + */ + internal var highResolutionNpcIndexCount: Int = 0 + + /** + * The extended info indices contain pointers to all the npcs for whom we need to + * write an extended info block. We do this rather than directly writing them as this + * improves CPU cache locality and allows us to batch extended info blocks together. + */ + internal val extendedInfoIndices: UShortArray = UShortArray(MAX_HIGH_RESOLUTION_NPCS) + + /** + * The number of npcs for whom we need to write extended info blocks this cycle. + */ + internal var extendedInfoCount: Int = 0 + + /** + * The observer extended info flags are a means to track which extended info blocks + * we need to transmit when moving a NPC from low resolution to high resolution, + * as there are numerous extended info blocks which hold state over a long period + * of time, such as head icon changes - if we didn't do this, anyone that observes + * a NPC after the cycle during which the head icons were set, would not see these + * head icons. + */ + internal val observerExtendedInfoFlags: ObserverExtendedInfoFlags = + ObserverExtendedInfoFlags(MAX_HIGH_RESOLUTION_NPCS) + + /** + * The primary npc info buffer, holding all the bitcodes and extended info blocks. + */ + internal var buffer: ByteBuf? = null + + /** + * Whether the buffer allocated by this NPC info object has been built + * into a packet message. If this returns false, but NPC info was in fact built, + * we have an allocated buffer that needs releasing. If the NPC info itself + * is released but isn't built into packet, we make sure to release it, to avoid + * any memory leaks. + */ + internal var builtIntoPacket: Boolean = false + + /** + * Performs an index defragmentation on the [highResolutionNpcIndices] array. + * This function will effectively take all indices that are NOT [NPC_INDEX_TERMINATOR] + * and put them into the [temporaryHighResolutionNpcIndices] in a consecutive order, + * without gaps. Afterwards, the [temporaryHighResolutionNpcIndices] and [highResolutionNpcIndices] + * arrays get swapped out, so our [highResolutionNpcIndices] becomes a defragmented array. + * This process occurs every cycle, after high resolution indices are processed, in order to + * get rid of any gaps that were produced as a result of it. + * + * A breakdown of this process: + * At the start of a cycle, we might have indices as `[1, 7, 5, 3, 8, 65535, ...]` + * If we make the assumption that NPCs at indices 7 and 8 are being removed from our high resolution, + * during the high resolution processing, npc at index 8 is dropped naturally - this is because + * the client will automatically trim off any NPCs at the end which don't fit into the transmitted + * count. So, npc at index 8 does not count towards fragmentation, as we just decrement the index count. + * However, index 7, because it is in the middle of this array of indices, causes the array + * to fragment. So in order to resolve this, we will iterate the fragmented indices + * until we have collected [highResolutionNpcIndexCount] worth of valid indices into the + * [temporaryHighResolutionNpcIndices] array. + * After defragmenting, our array will look as `[1, 5, 3, 65535, ...]`. + * While it is possible to do this with a single array, it requires one to shift every element + * in the array after the first fragmentation occurs. As the arrays are relatively small, it's + * better simply to use two arrays that get swapped every cycle, so we simply swap + * the [temporaryHighResolutionNpcIndices] and [highResolutionNpcIndices] arrays between one another, + * rather than needing to shift everything over. + */ + internal fun defragmentIndices() { + var count = 0 + for (i in highResolutionNpcIndices.indices) { + if (count >= highResolutionNpcIndexCount) { + break + } + val index = highResolutionNpcIndices[i] + if (index != NPC_INDEX_TERMINATOR) { + temporaryHighResolutionNpcIndices[count++] = index + } + } + val uncompressed = this.highResolutionNpcIndices + this.highResolutionNpcIndices = this.temporaryHighResolutionNpcIndices + this.temporaryHighResolutionNpcIndices = uncompressed + } + + /** + * Resets all the properties of this world details implementation, allowing + * it to be re-used for another player. + * @param worldId the new world id to be used for these details. + */ + internal fun onAlloc(worldId: Int) { + this.worldId = worldId + this.localPlayerCurrentCoord = CoordGrid.INVALID + this.localPlayerLastCoord = localPlayerCurrentCoord + this.buildArea = BuildArea.INVALID + this.highResolutionNpcIndexCount = 0 + this.highResolutionNpcIndices.fill(0u) + this.temporaryHighResolutionNpcIndices.fill(0u) + this.extendedInfoCount = 0 + this.extendedInfoIndices.fill(0u) + this.observerExtendedInfoFlags.reset() + this.builtIntoPacket = false + val buffer = this.buffer + if (buffer != null && buffer.refCnt() > 0) { + buffer.release(buffer.refCnt()) + this.buffer = null + } + } + + internal fun onDealloc() { + val buffer = this.buffer + if (buffer != null) { + if (!builtIntoPacket && buffer.refCnt() > 0) { + buffer.release(buffer.refCnt()) + } + this.buffer = null + } + this.builtIntoPacket = false + } + + private companion object { + /** + * The maximum number of high resolution NPCs that the client supports, limited by the + * client's array of extended info updates being a size-250 int array. + */ + private const val MAX_HIGH_RESOLUTION_NPCS: Int = 250 + + /** + * The terminator value used to indicate that no NPC is here. + */ + private const val NPC_INDEX_TERMINATOR: UShort = 0xFFFFu + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoWorldDetailsStorage.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoWorldDetailsStorage.kt new file mode 100644 index 000000000..4d3eb8136 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/NpcInfoWorldDetailsStorage.kt @@ -0,0 +1,39 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import java.lang.ref.ReferenceQueue +import java.lang.ref.SoftReference + +/** + * A storage object for npc info world details. + * As these detail objects are fairly large, with each one making several arrays + * that are thousands in length, it is preferred to pool and re-use these whenever possible. + * @property queue the soft reference queue holding these objects. + */ +internal class NpcInfoWorldDetailsStorage { + private val queue: ReferenceQueue = ReferenceQueue() + + /** + * Polls a world from the queue, or creates a new one. + * @param worldId the id of the world to assign to the details. + * @return an unused world details implementation. + */ + internal fun poll(worldId: Int): NpcInfoWorldDetails { + val next = queue.poll()?.get() + if (next != null) { + next.onAlloc(worldId) + return next + } + return NpcInfoWorldDetails(worldId) + } + + /** + * Pushes a world that's now unused back into the queue, allowing it to be re-used + * by someone else in the future. + * @param details the object containing the implementation details. + */ + internal fun push(details: NpcInfoWorldDetails) { + details.onDealloc() + val reference = SoftReference(details, queue) + reference.enqueue() + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/SetNpcUpdateOrigin.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/SetNpcUpdateOrigin.kt new file mode 100644 index 000000000..de0a5517a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/SetNpcUpdateOrigin.kt @@ -0,0 +1,62 @@ +package net.rsprot.protocol.game.outgoing.info.npcinfo + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * The set npc update origin packet is used to set the relative coordinate for npc info packet. + * As of revision 222, with the introduction of world entities, it is no longer viable to solely + * rely on the local player's coordinate, as it may be impacted by a specific world entity. + * As such, npc info updates should now be prefaced with the origin update to mark the relative coord. + * For no-world-entity use cases, just pass the player's coordinate in the current build area to + * get the old behavior. + * + * @property originX the x coordinate within the current build area of the player relative + * to which NPCs will be placed within NPC info packet. + * @property originZ the z coordinate within the current build area of the player relative + * to which NPCs will be placed within NPC info packet. + */ +public class SetNpcUpdateOrigin private constructor( + private val _originX: UByte, + private val _originZ: UByte, +) : OutgoingGameMessage { + public constructor( + originX: Int, + originZ: Int, + ) : this( + originX.toUByte(), + originZ.toUByte(), + ) + + public val originX: Int + get() = _originX.toInt() + public val originZ: Int + get() = _originZ.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetNpcUpdateOrigin + + if (_originX != other._originX) return false + if (_originZ != other._originZ) return false + + return true + } + + override fun hashCode(): Int { + var result = _originX.hashCode() + result = 31 * result + _originZ.hashCode() + return result + } + + override fun toString(): String = + "SetNpcUpdateOrigin(" + + "originX=$originX, " + + "originZ=$originZ" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/util/NpcCellOpcodes.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/util/NpcCellOpcodes.kt new file mode 100644 index 000000000..dcafced12 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/npcinfo/util/NpcCellOpcodes.kt @@ -0,0 +1,64 @@ +@file:Suppress("DuplicatedCode") + +package net.rsprot.protocol.game.outgoing.info.npcinfo.util + +internal object NpcCellOpcodes { + private const val NW: Int = 0 + private const val N: Int = 1 + private const val NE: Int = 2 + private const val W: Int = 3 + private const val E: Int = 4 + private const val SW: Int = 5 + private const val S: Int = 6 + private const val SE: Int = 7 + + /** + * Single cell movement opcodes in a len-16 array. + */ + private val singleCellMovementOpcodes: IntArray = buildSingleCellMovementOpcodes() + + /** + * Gets the index for a single cell movement opcode based on the deltas, + * where the deltas are expected to be either -1, 0 or 1. + * @param deltaX the x-coordinate delta + * @param deltaZ the z-coordinate delta + * @return the index of the single cell opcode stored in [singleCellMovementOpcodes] + */ + private fun singleCellIndex( + deltaX: Int, + deltaZ: Int, + ): Int = (deltaX + 1).or((deltaZ + 1) shl 2) + + /** + * Gets the single cell movement opcode value for the provided deltas. + * @param deltaX the x-coordinate delta + * @param deltaZ the z-coordinate delta + * @return the movement opcode as expected by the client, or -1 if the deltas are in range, + * but the deltas do not result in any movement. + * @throws ArrayIndexOutOfBoundsException if either of the deltas is not in range of -1..1. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + internal fun singleCellMovementOpcode( + deltaX: Int, + deltaZ: Int, + ): Int = singleCellMovementOpcodes[singleCellIndex(deltaX, deltaZ)] + + /** + * Builds a simple bitpacked array of the bit codes for all the possible deltas. + * This is simply a more efficient variant of the normal if-else chain of checking + * the different delta combinations, as we are skipping a lot of branch prediction. + * In a benchmark, the results showed ~603% increased performance. + */ + private fun buildSingleCellMovementOpcodes(): IntArray { + val array = IntArray(16) { -1 } + array[singleCellIndex(-1, -1)] = SW + array[singleCellIndex(0, -1)] = S + array[singleCellIndex(1, -1)] = SE + array[singleCellIndex(-1, 0)] = W + array[singleCellIndex(1, 0)] = E + array[singleCellIndex(-1, 1)] = NW + array[singleCellIndex(0, 1)] = N + array[singleCellIndex(1, 1)] = NE + return array + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/GlobalLowResolutionPositionRepository.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/GlobalLowResolutionPositionRepository.kt new file mode 100644 index 000000000..0eefa05c1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/GlobalLowResolutionPositionRepository.kt @@ -0,0 +1,69 @@ +package net.rsprot.protocol.game.outgoing.info.playerinfo + +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfoProtocol.Companion.PROTOCOL_CAPACITY +import net.rsprot.protocol.game.outgoing.info.playerinfo.util.LowResolutionPosition + +/** + * A repository used to track the low resolution positions of all the player avatars in the world. + * These low resolution positions will be synchronized to all the players, as part of the protocol. + * As all observers receive the same information, rather than allocating this per-player basis, + * we do it once, globally, for the entire world. + */ +internal class GlobalLowResolutionPositionRepository { + /** + * The low resolution positions of all the players in the previous cycle. + */ + private val previousLowResPositions: IntArray = IntArray(PROTOCOL_CAPACITY) + + /** + * The low resolution positions of all the players in the current cycle. + */ + private val currentLowResPositions: IntArray = IntArray(PROTOCOL_CAPACITY) + + /** + * Updates the current low resolution position of the player at index [idx]. + * @param idx the index of the player whose low resolution position to update. + * @param coordGrid the new absolute coordinate of that player. The low resolution + * coordinate will be calculated out of it. + */ + internal fun update( + idx: Int, + coordGrid: CoordGrid, + ) { + val lowResolutionPosition = LowResolutionPosition(coordGrid) + currentLowResPositions[idx] = lowResolutionPosition.packed + } + + /** + * Marks the player at index [idx] as unused. This should be done whenever a player logs out. + * @param idx the index of the player. + */ + internal fun markUnused(idx: Int) { + currentLowResPositions[idx] = 0 + } + + /** + * Gets the previous cycle's low resolution position of the player at index [index]. + * @param index the index of the player + * @return the low resolution position of that player in the last cycle. + */ + internal fun getPreviousLowResolutionPosition(index: Int): LowResolutionPosition = + LowResolutionPosition(previousLowResPositions[index]) + + /** + * Gets the current cycle's low resolution position of the player at index [index]. + * @param index the index of the player + * @return the low resolution position of that player in the current cycle. + */ + internal fun getCurrentLowResolutionPosition(index: Int): LowResolutionPosition = + LowResolutionPosition(currentLowResPositions[index]) + + /** + * Synchronize the low resolution positions at the end of the cycle. + * This function will move all current positions over to the previous cycle. + */ + internal fun postUpdate() { + currentLowResPositions.copyInto(previousLowResPositions) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatar.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatar.kt new file mode 100644 index 000000000..8c196f49e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatar.kt @@ -0,0 +1,238 @@ +package net.rsprot.protocol.game.outgoing.info.playerinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.encoder.PlayerExtendedInfoEncoders +import net.rsprot.protocol.game.outgoing.info.AvatarExtendedInfoWriter +import net.rsprot.protocol.game.outgoing.info.filter.ExtendedInfoFilter +import net.rsprot.protocol.game.outgoing.info.util.Avatar + +/** + * The player avatar class represents an avatar for the purposes of player information packet. + * Every player will have a respective avatar that contains basic information about that player, + * such as their coordinates and how far to render other players. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class PlayerAvatar internal constructor( + allocator: ByteBufAllocator, + localIndex: Int, + extendedInfoFilter: ExtendedInfoFilter, + extendedInfoWriters: List>, + huffmanCodec: HuffmanCodecProvider, +) : Avatar { + /** + * The preferred resize range. The player information protocol will attempt to + * add everyone within [preferredResizeRange] tiles to high resolution. + * If [preferredResizeRange] is equal to [Int.MAX_VALUE], resizing will be disabled + * and everyone will be put to high resolution. The extended information may be + * disabled for these players as a result, to avoid buffer overflows. + */ + private var preferredResizeRange: Int = DEFAULT_RESIZE_RANGE + + /** + * The current range at which other players can be observed. + * By default, this value is equal to 15 game squares, however, it may dynamically + * decrease if there are too many high resolution players nearby. It will naturally + * restore back to the default size when the pressure starts to decrease. + */ + internal var resizeRange: Int = preferredResizeRange + + /** + * The current cycle counter for resizing logic. + * Resizing by default will occur after every ten cycles. Once the + * protocol begins decrementing the range, it will continue to do so + * every cycle until it reaches a low enough pressure point. + * Every 11th cycle from thereafter, it will attempt to increase it back. + * If it succeeds, it will continue to do so every cycle, similarly to decreasing. + * If it however fails, it will set the range lower by one tile and remain there + * for the next ten cycles. + */ + private var resizeCounter: Int = DEFAULT_RESIZE_INTERVAL + + /** + * The current known coordinate of the given player. + * The coordinate property will need to be updated for all players prior to computing + * player info packet for any of them. + */ + public var currentCoord: CoordGrid = CoordGrid.INVALID + private set + + /** + * The current world that the player is on, by default the root world. + * When a player moves onto a world entity (a ship), this value must be updated. + */ + public var worldId: Int = PlayerInfo.ROOT_WORLD + private set + + /** + * The last known coordinate of this player. This property will be used in conjunction + * with [currentCoord] to determine the coordinate delta, which is then transmitted + * to the clients. + */ + internal var lastCoord: CoordGrid = CoordGrid.INVALID + + /** + * Extended info repository, commonly referred to as "masks", will track everything relevant + * inside itself. Setting properties such as a spotanim would be done through this. + * The [extendedInfo] is also responsible for caching the non-temporary blocks, + * such as appearance and move speed. + */ + public val extendedInfo: PlayerAvatarExtendedInfo = + PlayerAvatarExtendedInfo( + localIndex, + extendedInfoFilter, + extendedInfoWriters, + allocator, + huffmanCodec, + ) + + internal var hidden: Boolean = false + + public fun setHidden(hidden: Boolean) { + this.hidden = hidden + } + + /** + * Resets all the properties of the given avatar to their default values. + */ + internal fun reset() { + preferredResizeRange = DEFAULT_RESIZE_RANGE + resizeRange = preferredResizeRange + resizeCounter = DEFAULT_RESIZE_INTERVAL + currentCoord = CoordGrid.INVALID + lastCoord = CoordGrid.INVALID + worldId = PlayerInfo.ROOT_WORLD + } + + /** + * Updates the current known coordinate of the given [PlayerAvatar]. + * This function must be called on each avatar before player info is computed. + * @param level the current height level of the avatar. + * @param x the x coordinate of the avatar. + * @param z the z coordinate of the avatar (this is commonly referred to as 'y' coordinate). + * @throws IllegalArgumentException if [level] is not in range of 0..<4, or [x]/[z] are + * not in range of 0..<16384. + */ + public fun updateCoord( + level: Int, + x: Int, + z: Int, + ) { + this.currentCoord = CoordGrid(level, x, z) + } + + /** + * Updates the world id for a given player. Whether a player renders to you is determined + * based on the player's distance to that world's render coord, as defined by [PlayerInfo]. + * @param worldId the new world that the player is on. + */ + public fun updateWorld(worldId: Int) { + require(worldId == PlayerInfo.ROOT_WORLD || worldId in 0..= PREFERRED_PLAYER_COUNT) { + if (resizeRange > 0) { + resizeRange-- + } + resizeCounter = 0 + return + } + // If our resize counter gets high enough, the protocol will + // try to increment the range by 1 if it's less than 15 + // otherwise, resets the counter. + if (++resizeCounter >= DEFAULT_RESIZE_INTERVAL) { + if (resizeRange < preferredResizeRange) { + resizeRange++ + } else { + resizeCounter = 0 + } + } + } + + private companion object { + /** + * The default range of visibility of other players, in game tiles. + */ + private const val DEFAULT_RESIZE_RANGE = 15 + + /** + * The default interval at which resizing will be checked, in game cycles. + */ + private const val DEFAULT_RESIZE_INTERVAL = 10 + + /** + * The maximum preferred number of players in high resolution. + * Exceeding this count will cause the view range to start lowering. + */ + private const val PREFERRED_PLAYER_COUNT = 250 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarExtendedInfo.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarExtendedInfo.kt new file mode 100644 index 000000000..be944e020 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarExtendedInfo.kt @@ -0,0 +1,1505 @@ +@file:Suppress("DuplicatedCode") + +package net.rsprot.protocol.game.outgoing.info.playerinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.common.RSProtFlags +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.encoder.PlayerExtendedInfoEncoders +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.FaceAngle +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.MoveSpeed +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.ObjTypeCustomisation +import net.rsprot.protocol.common.game.outgoing.info.precompute +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.FacePathingEntity +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Tinting +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.HeadBar +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.HitMark +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.util.SpotAnim +import net.rsprot.protocol.game.outgoing.info.AvatarExtendedInfoWriter +import net.rsprot.protocol.game.outgoing.info.filter.ExtendedInfoFilter + +public typealias PlayerAvatarExtendedInfoWriter = + AvatarExtendedInfoWriter + +/** + * This data structure keeps track of all the extended info blocks for a given player avatar. + * @param localIndex the index of the avatar who owns this extended info block. + * @param filter the filter responsible for ensuring the total packet size constraint + * is not broken in any way. If this filter does not conform to the contract correctly, + * crashes are likely to happen during encoding. + * @param extendedInfoWriters the list of client-specific writers & encoders of all extended + * info blocks. During caching procedure, all registered client buffers will be built + * concurrently among players. + * @param allocator the byte buffer allocator used to allocate buffers during the caching procedure. + * Any extended info block which is built on-demand is written directly into the main buffer. + * @param huffmanCodec the Huffman codec is used to compress public chat extended info blocks. + */ +public class PlayerAvatarExtendedInfo( + internal var localIndex: Int, + private val filter: ExtendedInfoFilter, + extendedInfoWriters: List, + private val allocator: ByteBufAllocator, + private val huffmanCodec: HuffmanCodecProvider, +) { + /** + * The flags currently enabled for this avatar. + * When an update is requested, the respective flag of that update is appended + * onto this flag. At the end of each cycle, the flag is reset. + * Worth noting, however, that this flag only contains constants within + * the [Companion] of this class. For client-specific encoders, a translation + * occurs to turn these constants into a client-specific flag. + */ + internal var flags: Int = 0 + + /** + * Extended info blocks used to transmit changes to the client, + * wrapped in its own class as we must pass this onto the client-specific + * implementations. + */ + private val blocks: PlayerAvatarExtendedInfoBlocks = PlayerAvatarExtendedInfoBlocks(extendedInfoWriters) + + /** + * The client-specific extended info writers, indexed by the respective [OldSchoolClientType]'s id. + * All clients in use must be registered, or an exception will occur during player info encoding. + */ + private val writers: Array = + buildClientWriterArray(extendedInfoWriters) + + /** + * An int array to keep track of the number of times we've seen someone modify their appearance. + * During low to high resolution transition, if our counter of their changes does not align + * with their own counter of their appearance changes, their appearance will be re-transmitted + * to our client, in order to synchronize it. If the values align, the client will utilize its + * previously cached variant. + */ + private val otherAppearanceChangesCounter: IntArray = IntArray(PlayerInfoProtocol.PROTOCOL_CAPACITY) + + /** + * The number of times our appearance has changed. + */ + private var appearanceChangesCounter: Int = 0 + + /** + * Invalidates the appearance cache. + */ + internal fun invalidateAppearanceCache() { + otherAppearanceChangesCounter.fill(0) + } + + /** + * Sets the movement speed for this avatar. This move speed will be used whenever + * the player moves, unless a temporary move speed is utilized, which will take priority. + * The known values are: + * + * ``` + * | Type | Id | + * |------------|----| + * | Stationary | -1 | + * | Crawl | 0 | + * | Walk | 1 | + * | Run | 2 | + * ``` + * @param value the move speed value. + */ + public fun setMoveSpeed(value: Int) { + verify { + require(value in -1..2) { + "Unexpected move speed: $value, expected values: -1, 0, 1, 2" + } + } + blocks.moveSpeed.value = value + flags = flags or MOVE_SPEED + } + + /** + * Sets the temporary movement speed for this avatar - this move speed will only + * apply for a single game cycle. + * The known values are: + * ``` + * | Type | Id | + * |-----------------|-----| + * | Stationary | -1 | + * | Crawl | 0 | + * | Walk | 1 | + * | Run | 2 | + * | Teleport | 127 | + * ``` + * @param value the temporary move speed value. + */ + public fun setTempMoveSpeed(value: Int) { + verify { + require(value in -1..2 || value == 127) { + "Unexpected temporary move speed: $value, expected values: -1, 0, 1, 2, 127" + } + } + blocks.temporaryMoveSpeed.value = value + flags = flags or TEMP_MOVE_SPEED + } + + /** + * Sets the sequence for this avatar to play. + * @param id the id of the sequence to play, or -1 to stop playing current sequence. + * @param delay the delay in client cycles (20ms/cc) until the avatar starts playing this sequence. + */ + public fun setSequence( + id: Int, + delay: Int, + ) { + verify { + require(id == -1 || id in UNSIGNED_SHORT_RANGE) { + "Unexpected sequence id: $id, expected value -1 or in range $UNSIGNED_SHORT_RANGE" + } + require(delay in UNSIGNED_SHORT_RANGE) { + "Unexpected sequence delay: $delay, expected range: $UNSIGNED_SHORT_RANGE" + } + } + blocks.sequence.id = id.toUShort() + blocks.sequence.delay = delay.toUShort() + flags = flags or SEQUENCE + } + + /** + * Sets the face-locking onto the avatar with index [index]. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * In order to stop facing an entity, set the index value to -1. + * @param index the index of the target to face-lock onto (read above) + */ + public fun setFacePathingEntity(index: Int) { + verify { + require(index == -1 || index in 0..0x107FF) { + "Unexpected pathing entity index: $index, expected values: -1 to reset, " + + "0-65535 for NPCs, 65536-67583 for players" + } + } + blocks.facePathingEntity.index = index + flags = flags or FACE_PATHINGENTITY + } + + /** + * Sets the angle for this avatar to face. + * @param angle the angle to face, value range is 0..<2048, + * with 0 implying south, 512 west, 1024 north and 1536 east; interpolate + * between to get finer directions. + */ + public fun setFaceAngle(angle: Int) { + verify { + require(angle in 0..2047) { + "Unexpected angle: $angle, expected range: 0-2047" + } + } + blocks.faceAngle.angle = angle.toUShort() + flags = flags or FACE_ANGLE + } + + /** + * Sets the overhead chat of this avatar. + * If the [text] starts with the character `~`, the message will additionally + * also be rendered in the chatbox of everyone nearby, although no chat icons + * will appear alongside. The first `~` character itself will not be rendered + * in that scenario. + * @param text the text to render overhead. + */ + public fun setSay(text: String) { + verify { + require(text.length <= 80) { + "Unexpected say input; expected value 80 characters or less, " + + "input len: ${text.length}, input: $text" + } + } + blocks.say.text = text + flags = flags or SAY + } + + /** + * Sets the public chat of this avatar. + * + * Colour table: + * ``` + * | Id | Prefix | Hex Value | + * |-------|-----------|:--------------------------:| + * | 0 | yellow: | 0xFFFF00 | + * | 1 | red: | 0xFF0000 | + * | 2 | green: | 0x00FF00 | + * | 3 | cyan: | 0x00FFFF | + * | 4 | purple: | 0xFF00FF | + * | 5 | white: | 0xFFFFFF | + * | 6 | flash1: | 0xFF0000/0xFFFF00 | + * | 7 | flash2: | 0x0000FF/0x00FFFF | + * | 8 | flash3: | 0x00B000/0x80FF80 | + * | 9 | glow1: | 0xFF0000-0xFFFF00-0x00FFFF | + * | 10 | glow2: | 0xFF0000-0x00FF00-0x0000FF | + * | 11 | glow3: | 0xFFFFFF-0x00FF00-0x00FFFF | + * | 12 | rainbow: | N/A | + * | 13-20 | pattern*: | N/A | + * ``` + * + * Effects table: + * ``` + * | Id | Prefix | + * |----|---------| + * | 1 | wave: | + * | 2 | wave2: | + * | 3 | shake: | + * | 4 | scroll: | + * | 5 | slide: | + * ``` + * + * @param colour the colour id to render (see above) + * @param effects the effects to apply to the text (see above) + * @param modicon the index of the sprite in the modicons group to render before the name + * @param autotyper whether the avatar is using built-in autotyper + * @param text the text to render overhead and in chat + * @param pattern the pattern description if the user is using the pattern colour type + */ + public fun setChat( + colour: Int, + effects: Int, + modicon: Int, + autotyper: Boolean, + text: String, + pattern: ByteArray?, + ) { + verify { + require(text.length <= 80) { + "Unexpected chat input; expected value 80 characters or less, " + + "input len: ${text.length}, input: $text" + } + require(colour in 0..20) { + "Unexpected colour value: $colour, expected range: 0-20" + } + // No verification for mod icons, as servers often create custom ranks + } + val patternLength = if (colour in 13..20) colour - 12 else 0 + // Unlike most inputs, these are necessary to avoid crashes, so these can't be turned off. + if (patternLength in 1..8) { + requireNotNull(pattern) { + "Pattern cannot be null if pattern length is defined." + } + require(pattern.size == patternLength) { + "Pattern length does not match the size configured in the colour property." + } + } + blocks.chat.colour = colour.toUByte() + blocks.chat.effects = effects.toUByte() + blocks.chat.modicon = modicon.toUByte() + blocks.chat.autotyper = autotyper + blocks.chat.text = text + blocks.chat.pattern = pattern + flags = flags or CHAT + } + + /** + * Sets an exact movement for this avatar. It should be noted + * that this is done in conjunction with actual movement, as the + * exact move extended info block is only responsible for visualizing + * precise movement, and will synchronize to the real coordinate once + * the exact movement has finished. + * + * @param deltaX1 the coordinate delta between the current absolute + * x coordinate and where the avatar is going. + * @param deltaZ1 the coordinate delta between the current absolute + * z coordinate and where the avatar is going. + * @param delay1 how many client cycles (20ms/cc) until the avatar arrives + * at x/z 1 coordinate. + * @param deltaX2 the coordinate delta between the current absolute + * x coordinate and where the avatar is going. + * @param deltaZ2 the coordinate delta between the current absolute + * z coordinate and where the avatar is going. + * @param delay2 how many client cycles (20ms/cc) until the avatar arrives + * at x/z 2 coordinate. + * @param angle the angle the avatar will be facing throughout the exact movement, + * with 0 implying south, 512 west, 1024 north and 1536 east; interpolate + * between to get finer directions. + */ + public fun setExactMove( + deltaX1: Int, + deltaZ1: Int, + delay1: Int, + deltaX2: Int, + deltaZ2: Int, + delay2: Int, + angle: Int, + ) { + verify { + require(delay1 >= 0) { + "First delay cannot be negative: $delay1" + } + require(delay2 >= 0) { + "Second delay cannot be negative: $delay2" + } + require(delay2 > delay1) { + "Second delay must be greater than the first: $delay1 > $delay2" + } + require(angle in 0..2047) { + "Unexpected angle value: $angle, expected range: 0..2047" + } + require(deltaX1 in SIGNED_BYTE_RANGE) { + "Unexpected deltaX1: $deltaX1, expected range: $SIGNED_BYTE_RANGE" + } + require(deltaZ1 in SIGNED_BYTE_RANGE) { + "Unexpected deltaZ1: $deltaZ1, expected range: $SIGNED_BYTE_RANGE" + } + require(deltaX2 in SIGNED_BYTE_RANGE) { + "Unexpected deltaX1: $deltaX2, expected range: $SIGNED_BYTE_RANGE" + } + require(deltaZ2 in SIGNED_BYTE_RANGE) { + "Unexpected deltaZ1: $deltaZ2, expected range: $SIGNED_BYTE_RANGE" + } + } + blocks.exactMove.deltaX1 = deltaX1.toUByte() + blocks.exactMove.deltaZ1 = deltaZ1.toUByte() + blocks.exactMove.delay1 = delay1.toUShort() + blocks.exactMove.deltaX2 = deltaX2.toUByte() + blocks.exactMove.deltaZ2 = deltaZ2.toUByte() + blocks.exactMove.delay2 = delay2.toUShort() + blocks.exactMove.direction = angle.toUShort() + flags = flags or EXACT_MOVE + } + + /** + * Sets the spotanim in slot [slot], overriding any previous spotanim + * in that slot in doing so. + * @param slot the slot of the spotanim. + * @param id the id of the spotanim. + * @param delay the delay in client cycles (20ms/cc) until the given spotanim begins rendering. + * @param height the height at which to render the spotanim. + */ + public fun setSpotAnim( + slot: Int, + id: Int, + delay: Int, + height: Int, + ) { + verify { + require(slot in UNSIGNED_BYTE_RANGE) { + "Unexpected slot: $slot, expected range: $UNSIGNED_BYTE_RANGE" + } + require(id == -1 || id in UNSIGNED_SHORT_RANGE) { + "Unexpected id: $id, expected value -1 or in range: $UNSIGNED_SHORT_RANGE" + } + require(delay in UNSIGNED_SHORT_RANGE) { + "Unexpected delay: $delay, expected range: $UNSIGNED_SHORT_RANGE" + } + require(height in UNSIGNED_SHORT_RANGE) { + "Unexpected delay: $height, expected range: $UNSIGNED_SHORT_RANGE" + } + } + blocks.spotAnims.set(slot, SpotAnim(id, delay, height)) + flags = flags or SPOTANIM + } + + /** + * Adds a simple hitmark on this avatar. + * @param sourceIndex the index of the character that dealt the hit. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * If there is no source, set the index to -1. + * The index will be used for tinting purposes, as both the player who dealt + * the hit, and the recipient will see a tinted variant. + * Everyone else, however, will see a regular darkened hit mark. + * @param selfType the multi hitmark id that supports tinted and darkened variants. + * @param otherType the hitmark id to render to anyone that isn't the recipient, + * or the one who dealt the hit. This will generally be a darkened variant. + * If the hitmark should only render to the local player, set the [otherType] + * value to -1, forcing it to only render to the recipient (and in the case of + * a [sourceIndex] being defined, the one who dealt the hit) + * @param value the value to show over the hitmark. + * @param delay the delay in client cycles (20ms/cc) until the hitmark renders. + */ + public fun addHitMark( + sourceIndex: Int, + selfType: Int, + otherType: Int = selfType, + value: Int, + delay: Int = 0, + ) { + if (blocks.hit.hitMarkList.size >= 0xFF) { + return + } + verify { + require(sourceIndex == -1 || sourceIndex in 0..0x107FF) { + "Unexpected source index: $sourceIndex, expected values: -1 to reset, " + + "0-65535 for NPCs, 65536-67583 for players" + } + require(selfType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected selfType: $selfType, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(otherType == -1 || otherType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected otherType: $otherType, expected value -1 or range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(value in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected value: $value, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(delay in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected delay: $delay, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + } + blocks.hit.hitMarkList += + HitMark( + sourceIndex, + selfType.toUShort(), + otherType.toUShort(), + value.toUShort(), + delay.toUShort(), + ) + flags = flags or HITS + } + + /** + * Removes the oldest currently showing hitmark on this avatar, + * if one exists. + * @param delay the delay in client cycles (20ms/cc) until the hitmark is removed. + */ + public fun removeHitMark(delay: Int = 0) { + if (blocks.hit.hitMarkList.size >= 0xFF) { + return + } + verify { + require(delay in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected delay: $delay, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + } + blocks.hit.hitMarkList += HitMark(0x7FFEu, delay.toUShort()) + flags = flags or HITS + } + + /** + * Adds a simple hitmark on this avatar. + * @param sourceIndex the index of the character that dealt the hit. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * If there is no source, set the index to -1. + * The index will be used for tinting purposes, as both the player who dealt + * the hit, and the recipient will see a tinted variant. + * Everyone else, however, will see a regular darkened hit mark. + * @param selfType the multi hitmark id that supports tinted and darkened variants. + * @param otherType the hitmark id to render to anyone that isn't the recipient, + * or the one who dealt the hit. This will generally be a darkened variant. + * If the hitmark should only render to the local player, set the [otherType] + * value to -1, forcing it to only render to the recipient (and in the case of + * a [sourceIndex] being defined, the one who dealt the hit) + * @param value the value to show over the hitmark. + * @param selfSoakType the multi hitmark id that supports tinted and darkened variants, + * shown as soaking next to the normal hitmark. + * @param otherSoakType the hitmark id to render to anyone that isn't the recipient, + * or the one who dealt the hit. This will generally be a darkened variant. + * Unlike the [otherType], this does not support -1, as it is not possible to show partial + * soaked hitmarks. + * @param delay the delay in client cycles (20ms/cc) until the hitmark renders. + */ + @JvmOverloads + public fun addSoakedHitMark( + sourceIndex: Int, + selfType: Int, + otherType: Int = selfType, + value: Int, + selfSoakType: Int, + otherSoakType: Int = selfSoakType, + soakValue: Int, + delay: Int = 0, + ) { + if (blocks.hit.hitMarkList.size >= 0xFF) { + return + } + verify { + require(sourceIndex == -1 || sourceIndex in 0..0x107FF) { + "Unexpected source index: $sourceIndex, expected values: -1 to reset, " + + "0-65535 for NPCs, 65536-67583 for players" + } + require(selfType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected selfType: $selfType, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(otherType == -1 || otherType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected otherType: $otherType, expected value -1 or in range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(value in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected value: $value, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(selfSoakType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected selfType: $selfSoakType, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(otherSoakType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected otherType: $otherSoakType, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(soakValue in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected value: $soakValue, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(delay in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected delay: $delay, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + } + blocks.hit.hitMarkList += + HitMark( + sourceIndex, + selfType.toUShort(), + otherType.toUShort(), + value.toUShort(), + selfSoakType.toUShort(), + otherSoakType.toUShort(), + soakValue.toUShort(), + delay.toUShort(), + ) + flags = flags or HITS + } + + /** + * Adds a headbar onto the avatar. + * If a headbar by the same id already exists, updates the status of the old one. + * Up to four distinct headbars can be rendered simultaneously. + * + * @param sourceIndex the index of the entity that dealt the hit that resulted in this headbar. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * If there is no source, set the index to -1. + * The index will be used for rendering purposes, as both the player who dealt + * the hit, and the recipient will see the [selfType] variant, and everyone else + * will see the [otherType] variant, which, if set to -1 will be skipped altogether. + * @param selfType the id of the headbar to render to the entity on which the headbar appears, + * as well as the source who resulted in the creation of the headbar. + * @param otherType the id of the headbar to render to everyone that doesn't fit the [selfType] + * criteria. If set to -1, the headbar will not be rendered to these individuals. + * @param startFill the number of pixels to render of this headbar at in the start. + * The number of pixels that a headbar supports is defined in its respective headbar config. + * @param endFill the number of pixels to render of this headbar at in the end, + * if a [startTime] and [endTime] are defined. + * @param startTime the delay in client cycles (20ms/cc) until the headbar renders at [startFill] + * @param endTime the delay in client cycles (20ms/cc) until the headbar arrives at [endFill]. + */ + @JvmOverloads + public fun addHeadBar( + sourceIndex: Int, + selfType: Int, + otherType: Int = selfType, + startFill: Int, + endFill: Int = startFill, + startTime: Int = 0, + endTime: Int = 0, + ) { + if (blocks.hit.headBarList.size >= 0xFF) { + return + } + verify { + require(sourceIndex == -1 || sourceIndex in 0..0x107FF) { + "Unexpected source index: $sourceIndex, expected values: -1 to reset, " + + "0-65535 for NPCs, 65536-67583 for players" + } + require(selfType == -1 || selfType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected id: $selfType, expected value -1 or in range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(otherType == -1 || otherType in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected id: $otherType, expected value -1 or in range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(startFill in UNSIGNED_BYTE_RANGE) { + "Unexpected startFill: $startFill, expected range $UNSIGNED_BYTE_RANGE" + } + require(endFill in UNSIGNED_BYTE_RANGE) { + "Unexpected endFill: $endFill, expected range $UNSIGNED_BYTE_RANGE" + } + require(startTime in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected startTime: $startTime, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(endTime in UNSIGNED_SMART_1_OR_2_RANGE) { + "Unexpected endTime: $endTime, expected range $UNSIGNED_SMART_1_OR_2_RANGE" + } + require(endTime >= startTime) { + "End time must be greater than or equal to start time: $startTime <= $endTime" + } + } + blocks.hit.headBarList += + HeadBar( + sourceIndex, + selfType.toUShort(), + otherType.toUShort(), + startFill.toUByte(), + endFill.toUByte(), + startTime.toUShort(), + endTime.toUShort(), + ) + flags = flags or HITS + } + + /** + * Removes a headbar on this avatar by the id of [id], if one renders. + * @param id the id of the head bar to remove. + */ + public fun removeHeadBar(id: Int) { + addHeadBar( + -1, + id, + startFill = 0, + endTime = HeadBar.REMOVED.toInt(), + ) + } + + /** + * Applies a tint over the non-textured parts of the character. + * @param startTime the delay in client cycles (20ms/cc) until the tinting is applied. + * @param endTime the timestamp in client cycles (20ms/cc) until the tinting finishes. + * @param hue the hue of the tint. + * @param saturation the saturation of the tint. + * @param lightness the lightness of the tint. + * @param weight the weight (or opacity) of the tint. + */ + public fun tinting( + startTime: Int, + endTime: Int, + hue: Int, + saturation: Int, + lightness: Int, + weight: Int, + ) { + verify { + require(startTime in UNSIGNED_SHORT_RANGE) { + "Unexpected startTime: $startTime, expected range $UNSIGNED_SHORT_RANGE" + } + require(endTime in UNSIGNED_SHORT_RANGE) { + "Unexpected endTime: $endTime, expected range $UNSIGNED_SHORT_RANGE" + } + require(endTime >= startTime) { + "End time should be equal to or greater than start time: $endTime > $startTime" + } + require(hue in UNSIGNED_BYTE_RANGE) { + "Unexpected hue: $hue, expected range $UNSIGNED_BYTE_RANGE" + } + require(saturation in UNSIGNED_BYTE_RANGE) { + "Unexpected saturation: $saturation, expected range $UNSIGNED_BYTE_RANGE" + } + require(lightness in UNSIGNED_BYTE_RANGE) { + "Unexpected lightness: $lightness, expected range $UNSIGNED_BYTE_RANGE" + } + require(weight in UNSIGNED_BYTE_RANGE) { + "Unexpected weight: $weight, expected range $UNSIGNED_BYTE_RANGE" + } + } + val tint = blocks.tinting.global + tint.start = startTime.toUShort() + tint.end = endTime.toUShort() + tint.hue = hue.toUByte() + tint.saturation = saturation.toUByte() + tint.lightness = lightness.toUByte() + tint.weight = weight.toUByte() + flags = flags or TINTING + } + + /** + * Applies a tint over the non-textured parts of the character. + * @param startTime the delay in client cycles (20ms/cc) until the tinting is applied. + * @param endTime the timestamp in client cycles (20ms/cc) until the tinting finishes. + * @param hue the hue of the tint. + * @param saturation the saturation of the tint. + * @param lightness the lightness of the tint. + * @param weight the weight (or opacity) of the tint. + * @param visibleTo the player who will see the tint applied. + * Note that this only accepts player indices, and not NPC ones like many other extended info blocks. + */ + public fun specificTinting( + startTime: Int, + endTime: Int, + hue: Int, + saturation: Int, + lightness: Int, + weight: Int, + visibleTo: PlayerInfo, + ) { + verify { + require(startTime in UNSIGNED_SHORT_RANGE) { + "Unexpected startTime: $startTime, expected range $UNSIGNED_SHORT_RANGE" + } + require(endTime in UNSIGNED_SHORT_RANGE) { + "Unexpected endTime: $endTime, expected range $UNSIGNED_SHORT_RANGE" + } + require(endTime >= startTime) { + "End time should be equal to or greater than start time: $endTime > $startTime" + } + require(hue in UNSIGNED_BYTE_RANGE) { + "Unexpected hue: $hue, expected range $UNSIGNED_BYTE_RANGE" + } + require(saturation in UNSIGNED_BYTE_RANGE) { + "Unexpected saturation: $saturation, expected range $UNSIGNED_BYTE_RANGE" + } + require(lightness in UNSIGNED_BYTE_RANGE) { + "Unexpected lightness: $lightness, expected range $UNSIGNED_BYTE_RANGE" + } + require(weight in UNSIGNED_BYTE_RANGE) { + "Unexpected weight: $weight, expected range $UNSIGNED_BYTE_RANGE" + } + } + val tint = Tinting() + blocks.tinting.observerDependent[visibleTo.avatar.extendedInfo.localIndex] = tint + tint.start = startTime.toUShort() + tint.end = endTime.toUShort() + tint.hue = hue.toUByte() + tint.saturation = saturation.toUByte() + tint.lightness = lightness.toUByte() + tint.weight = weight.toUByte() + visibleTo.observerExtendedInfoFlags.addFlag( + localIndex, + TINTING, + ) + } + + /** + * Sets the name of the avatar. + * @param name the name to assign. + */ + public fun setName(name: String) { + verify { + require(name.length in 1..12) { + "Unexpected name length, expected range 1..12" + } + } + if (blocks.appearance.name == name) { + return + } + blocks.appearance.name = name + flagAppearance() + } + + /** + * Sets the combat level of the avatar. + * @param combatLevel the level to assign. + */ + public fun setCombatLevel(combatLevel: Int) { + verify { + require(combatLevel in UNSIGNED_BYTE_RANGE) { + "Unexpected combatLevel $combatLevel, expected range $UNSIGNED_BYTE_RANGE" + } + } + val level = combatLevel.toUByte() + if (blocks.appearance.combatLevel == level) { + return + } + blocks.appearance.combatLevel = level + flagAppearance() + } + + /** + * Sets the skill level of the avatar, seen when right-clicking players as "skill: value", + * instead of the usual combat level. Set to 0 to render combat level instead. + * @param skillLevel the level to render + */ + public fun setSkillLevel(skillLevel: Int) { + verify { + require(skillLevel in UNSIGNED_SHORT_RANGE) { + "Unexpected skill level $skillLevel, expected range $UNSIGNED_SHORT_RANGE" + } + } + val level = skillLevel.toUShort() + if (blocks.appearance.skillLevel == level) { + return + } + blocks.appearance.skillLevel = level + flagAppearance() + } + + /** + * Sets this avatar hidden (or un-hidden) client-sided. + * If the observer is a J-Mod or above, the character will render regardless. + * It is worth noting that plugin clients such as RuneLite will render information + * about these avatars regardless of their hidden status. + * @param hidden whether to hide the avatar. + */ + public fun setHidden(hidden: Boolean) { + if (blocks.appearance.hidden == hidden) { + return + } + blocks.appearance.hidden = hidden + flagAppearance() + } + + /** + * Sets the character male or female. + * @param isMale whether to set the character male (or female, if false) + */ + public fun setMale(isMale: Boolean) { + if (blocks.appearance.male == isMale) { + return + } + blocks.appearance.male = isMale + flagAppearance() + } + + /** + * Sets the text gender of this avatar. + * @param num the number to set, with the value 0 being male, 1 being female, + * and 2 being 'other'. + */ + public fun setTextGender(num: Int) { + verify { + require(num in UNSIGNED_BYTE_RANGE) { + "Unexpected textGender $num, expected range $UNSIGNED_BYTE_RANGE" + } + } + val textGender = num.toUByte() + if (blocks.appearance.textGender == textGender) { + return + } + blocks.appearance.textGender = textGender + flagAppearance() + } + + /** + * Sets the skull icon over this avatar. + * @param icon the id of the icon to render, or -1 to not show any. + */ + public fun setSkullIcon(icon: Int) { + verify { + require(icon == -1 || icon in UNSIGNED_BYTE_RANGE) { + "Unexpected skullIcon $icon, expected value -1 or in range $UNSIGNED_BYTE_RANGE" + } + } + val skullIcon = icon.toUByte() + if (blocks.appearance.skullIcon == skullIcon) { + return + } + blocks.appearance.skullIcon = skullIcon + flagAppearance() + } + + /** + * Sets the overhead icon over this avatar (e.g. prayer icons) + * @param icon the id of the icon to render, or -1 to not show any. + */ + public fun setOverheadIcon(icon: Int) { + verify { + require(icon == -1 || icon in UNSIGNED_BYTE_RANGE) { + "Unexpected overheadIcon $icon, expected value -1 or in range $UNSIGNED_BYTE_RANGE" + } + } + val overheadIcon = icon.toUByte() + if (blocks.appearance.overheadIcon == overheadIcon) { + return + } + blocks.appearance.overheadIcon = overheadIcon + flagAppearance() + } + + /** + * Transforms this avatar to the respective NPC, or back to player if the [id] is -1. + * @param id the id of the NPC to transform to, or -1 if resetting. + */ + public fun transformToNpc(id: Int) { + verify { + require(id == -1 || id in UNSIGNED_SHORT_RANGE) { + "Unexpected id $id, expected value -1 or in range $UNSIGNED_SHORT_RANGE" + } + } + val npcId = id.toUShort() + if (blocks.appearance.transformedNpcId == npcId) { + return + } + blocks.appearance.transformedNpcId = npcId + flagAppearance() + } + + /** + * Sets an ident kit. Note that this function does not rely on wearpos values, + * as those range from 0 to 11. Ident kit values only range from 0 to 6, which would + * result in some wasted memory. + * A list of wearpos to ident kit can also be found in + * [net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.Appearance.identKitSlotList] + * + * Ident kit table: + * ```kt + * | Id | Slot | + * |:--:|:------:| + * | 0 | Hair | + * | 1 | Beard | + * | 2 | Body | + * | 3 | Arms | + * | 4 | Gloves | + * | 5 | Legs | + * | 6 | Boots | + * ``` + * + * @param identKitSlot the position in which to set this ident kit. + * @param value the value of the ident kit config, or -1 if hidden. + */ + public fun setIdentKit( + identKitSlot: Int, + value: Int, + ) { + verify { + require(identKitSlot in 0..6) { + "Unexpected wearPos $identKitSlot, expected range 0..6" + } + require(value == -1 || value in 0..<2048) { + "Unexpected value $value, expected value -1 or in range 0..<2048" + } + } + val valueAsShort = value.toShort() + val cur = blocks.appearance.identKit[identKitSlot] + if (cur == valueAsShort) { + return + } + blocks.appearance.identKit[identKitSlot] = valueAsShort + flagAppearance() + } + + /** + * Sets a worn object in the given [wearpos]. + * @param wearpos the main wearpos in which the obj equips. + * @param id the obj id to set in that wearpos, or -1 to not have anything. + * @param wearpos2 the secondary wearpos that this obj utilizes, hiding whatever + * ident kit was in that specific wearpos (e.g. hair, beard), or -1 to not use any. + * @param wearpos3 the tertiary wearpos that this obj utilizes, hiding whatever + * ident kit was in that specific wearpos (e.g. hair, beard), or -1 to not use any. + */ + public fun setWornObj( + wearpos: Int, + id: Int, + wearpos2: Int, + wearpos3: Int, + ) { + verify { + require(wearpos in 0..11) { + "Unexpected wearPos $wearpos, expected range 0..11" + } + require(id == -1 || id in UNSIGNED_SHORT_RANGE) { + "Unexpected id $id, expected value -1 or range $UNSIGNED_SHORT_RANGE" + } + require(wearpos2 == -1 || wearpos2 in 0..11) { + "Unexpected wearpos2 $wearpos2, expected value -1 or in range 0..11" + } + require(wearpos3 == -1 || wearpos3 in 0..11) { + "Unexpected wearpos3 $wearpos3, expected value -1 or in range 0..11" + } + } + val valueAsShort = id.toShort() + val cur = blocks.appearance.wornObjs[wearpos] + if (cur == valueAsShort) { + return + } + blocks.appearance.wornObjs[wearpos] = valueAsShort + val hiddenSlotsBitpacked = (wearpos2 and 0xF shl 4) or (wearpos3 and 0xF) + blocks.appearance.hiddenWearPos[wearpos] = hiddenSlotsBitpacked.toByte() + flagAppearance() + } + + /** + * Sets the colour of this avatar's appearance. + * @param slot the slot of the element to colour + * @param value the 16-bit HSL colour value + */ + public fun setColour( + slot: Int, + value: Int, + ) { + verify { + require(slot in 0..<5) { + "Unexpected slot $slot, expected range 0..<5" + } + require(value in UNSIGNED_BYTE_RANGE) { + "Unexpected value $value, expected range $UNSIGNED_BYTE_RANGE" + } + } + val valueAsByte = value.toByte() + val cur = blocks.appearance.colours[slot] + if (cur == valueAsByte) { + return + } + blocks.appearance.colours[slot] = valueAsByte + flagAppearance() + } + + /** + * Sets the base animations of this avatar. + * @param readyAnim the animation used when the avatar is standing still. + * @param turnAnim the animation used when the avatar is turning on-spot without movement. + * @param walkAnim the animation used when the avatar is walking forward. + * @param walkAnimBack the animation used when the avatar is walking backwards. + * @param walkAnimLeft the animation used when the avatar is walking to the left. + * @param walkAnimRight the animation used when the avatar is walking to the right. + * @param runAnim the animation used when the avatar is running. + */ + public fun setBaseAnimationSet( + readyAnim: Int, + turnAnim: Int, + walkAnim: Int, + walkAnimBack: Int, + walkAnimLeft: Int, + walkAnimRight: Int, + runAnim: Int, + ) { + verify { + require(readyAnim == -1 || readyAnim in UNSIGNED_SHORT_RANGE) { + "Unexpected readyAnim $readyAnim, expected value -1 or range $UNSIGNED_SHORT_RANGE" + } + require(turnAnim == -1 || turnAnim in UNSIGNED_SHORT_RANGE) { + "Unexpected turnAnim $turnAnim, expected value -1 or range $UNSIGNED_SHORT_RANGE" + } + require(walkAnim == -1 || walkAnim in UNSIGNED_SHORT_RANGE) { + "Unexpected walkAnim $walkAnim, expected value -1 or range $UNSIGNED_SHORT_RANGE" + } + require(walkAnimBack == -1 || walkAnimBack in UNSIGNED_SHORT_RANGE) { + "Unexpected walkAnimBack $walkAnimBack, expected value -1 or range $UNSIGNED_SHORT_RANGE" + } + require(walkAnimLeft == -1 || walkAnimLeft in UNSIGNED_SHORT_RANGE) { + "Unexpected walkAnimLeft $walkAnimLeft, expected value -1 or range $UNSIGNED_SHORT_RANGE" + } + require(walkAnimRight == -1 || walkAnimRight in UNSIGNED_SHORT_RANGE) { + "Unexpected walkAnimRight $walkAnimRight, expected value -1 or range $UNSIGNED_SHORT_RANGE" + } + require(runAnim == -1 || runAnim in UNSIGNED_SHORT_RANGE) { + "Unexpected runAnim $runAnim, expected value -1 or range $UNSIGNED_SHORT_RANGE" + } + } + blocks.appearance.readyAnim = readyAnim.toUShort() + blocks.appearance.turnAnim = turnAnim.toUShort() + blocks.appearance.walkAnim = walkAnim.toUShort() + blocks.appearance.walkAnimBack = walkAnimBack.toUShort() + blocks.appearance.walkAnimLeft = walkAnimLeft.toUShort() + blocks.appearance.walkAnimRight = walkAnimRight.toUShort() + blocks.appearance.runAnim = runAnim.toUShort() + flagAppearance() + } + + /** + * Sets the name extras of this avatar, rendered when right-clicking users. + * @param beforeName the text to render before this avatar's name. + * @param afterName the text to render after this avatar's name, but before the combat level. + * @param afterCombatLevel the text to render after this avatar's combat level. + */ + public fun nameExtras( + beforeName: String, + afterName: String, + afterCombatLevel: String, + ) { + verify { + require(beforeName.length in UNSIGNED_BYTE_RANGE) { + "Unexpected beforeName length ${beforeName.length}, expected range $UNSIGNED_BYTE_RANGE" + } + require(afterName.length in UNSIGNED_BYTE_RANGE) { + "Unexpected afterName length ${afterName.length}, expected range $UNSIGNED_BYTE_RANGE" + } + require(afterCombatLevel.length in UNSIGNED_BYTE_RANGE) { + "Unexpected afterCombatLevel length ${afterCombatLevel.length}, expected range $UNSIGNED_BYTE_RANGE" + } + } + blocks.appearance.beforeName = beforeName + blocks.appearance.afterName = afterName + blocks.appearance.afterCombatLevel = afterCombatLevel + flagAppearance() + } + + /** + * Forces a model refresh client-side even if the worn objects + base colour + gender have not changed. + * This is particularly important to enable when setting or clearing any obj type customisations, + * as those are not considered when calculating the hash code. + */ + public fun forceModelRefresh(enabled: Boolean) { + blocks.appearance.forceModelRefresh = enabled + } + + /** + * Clears any obj type customisations applied to [wearpos]. + * @param wearpos the worn item slot. + */ + public fun clearObjTypeCustomisation(wearpos: Int) { + verify { + require(wearpos in 0..11) { + "Unexpected wearpos $wearpos, expected range 0..11" + } + } + if (blocks.appearance.objTypeCustomisation[wearpos] == null) { + return + } + blocks.appearance.objTypeCustomisation[wearpos] = null + flagAppearance() + } + + /** + * Allocates an obj type customisation in [wearpos] if it doesn't already exist. + * @param wearpos the wearpos in which a customisation is being made. + * @return the customisation class holding the state overrides of this obj. + */ + private fun allocObjCustomisation(wearpos: Int): ObjTypeCustomisation { + var customisation = blocks.appearance.objTypeCustomisation[wearpos] + if (customisation == null) { + customisation = ObjTypeCustomisation() + blocks.appearance.objTypeCustomisation[wearpos] = customisation + } + return customisation + } + + /** + * Recolours part of an obj in the first slot (out of two). + * @param wearpos the position in which the obj is worn. + * @param index the source index of the colour to override. + * @param value the 16 bit HSL colour to override with. + */ + public fun objRecol1( + wearpos: Int, + index: Int, + value: Int, + ) { + verify { + require(wearpos in 0..11) { + "Unexpected wearpos $wearpos, expected range 0..11" + } + require(index in 0..14) { + "Unexpected recol index $index, expected range 0..14" + } + require(value in UNSIGNED_SHORT_RANGE) { + "Unexpected value $value, expected range $UNSIGNED_SHORT_RANGE" + } + } + val customisation = allocObjCustomisation(wearpos) + customisation.recolIndices = ((customisation.recolIndices.toInt() and 0xF0) or (index and 0xF)).toUByte() + customisation.recol1 = value.toUShort() + flagAppearance() + } + + /** + * Recolours part of an obj in the second slot (out of two). + * @param wearpos the position in which the obj is worn. + * @param index the source index of the colour to override. + * @param value the 16 bit HSL colour to override with. + */ + public fun objRecol2( + wearpos: Int, + index: Int, + value: Int, + ) { + verify { + require(wearpos in 0..11) { + "Unexpected wearpos $wearpos, expected range 0..11" + } + require(index in 0..14) { + "Unexpected recol index $index, expected range 0..14" + } + require(value in UNSIGNED_SHORT_RANGE) { + "Unexpected value $value, expected range $UNSIGNED_SHORT_RANGE" + } + } + val customisation = allocObjCustomisation(wearpos) + customisation.recolIndices = ((customisation.recolIndices.toInt() and 0xF) or ((index and 0xF) shl 4)).toUByte() + customisation.recol2 = value.toUShort() + flagAppearance() + } + + /** + * Retextures part of an obj in the first slot (out of two). + * @param wearpos the position in which the obj is worn. + * @param index the source index of the texture to override. + * @param value the id of the texture to override with. + */ + public fun objRetex1( + wearpos: Int, + index: Int, + value: Int, + ) { + verify { + require(wearpos in 0..11) { + "Unexpected wearpos $wearpos, expected range 0..11" + } + require(index in 0..14) { + "Unexpected retex index $index, expected range 0..14" + } + require(value in UNSIGNED_SHORT_RANGE) { + "Unexpected value $value, expected range $UNSIGNED_SHORT_RANGE" + } + } + val customisation = allocObjCustomisation(wearpos) + customisation.retexIndices = ((customisation.retexIndices.toInt() and 0xF0) or (index and 0xF)).toUByte() + customisation.retex1 = value.toUShort() + flagAppearance() + } + + /** + * Retextures part of an obj in the second slot (out of two). + * @param wearpos the position in which the obj is worn. + * @param index the source index of the texture to override. + * @param value the id of the texture to override with. + */ + public fun objRetex2( + wearpos: Int, + index: Int, + value: Int, + ) { + verify { + require(wearpos in 0..11) { + "Unexpected wearpos $wearpos, expected range 0..11" + } + require(index in 0..14) { + "Unexpected retex index $index, expected range 0..14" + } + require(value in UNSIGNED_SHORT_RANGE) { + "Unexpected value $value, expected range $UNSIGNED_SHORT_RANGE" + } + } + val customisation = allocObjCustomisation(wearpos) + customisation.retexIndices = ((customisation.retexIndices.toInt() and 0xF) or ((index and 0xF) shl 4)).toUByte() + customisation.retex2 = value.toUShort() + flagAppearance() + } + + /** + * Flags appearance to have changed, in order for it to be synchronized to all observers. + */ + private fun flagAppearance() { + flags = flags or APPEARANCE + appearanceChangesCounter++ + } + + /** + * Clears any transient extended info blocks which only applied for this cycle, + * making it ready for the next. + */ + internal fun postUpdate() { + clearTransientExtendedInformation() + flags = 0 + } + + /** + * Resets all the properties of this extended info object, making it ready for use + * by another avatar. + */ + internal fun reset() { + flags = 0 + this.appearanceChangesCounter = 0 + this.otherAppearanceChangesCounter.fill(0) + blocks.appearance.clear() + blocks.moveSpeed.clear() + blocks.temporaryMoveSpeed.clear() + blocks.sequence.clear() + blocks.facePathingEntity.clear() + blocks.faceAngle.clear() + blocks.say.clear() + blocks.chat.clear() + blocks.exactMove.clear() + blocks.spotAnims.clear() + blocks.hit.clear() + blocks.tinting.clear() + } + + /** + * Gets all the extended info flags which must be updated for the given [observer], + * based on what is out of date with what they last saw (if they saw the player before). + * @param observer the avatar observing us. + * @return the flags that need updating. + */ + internal fun getLowToHighResChangeExtendedInfoFlags(observer: PlayerAvatarExtendedInfo): Int { + var flag = 0 + if (this.flags and APPEARANCE == 0 && + checkOutOfDate(observer) + ) { + flag = flag or APPEARANCE + } + if (this.flags and MOVE_SPEED == 0 && + blocks.moveSpeed.value != MoveSpeed.DEFAULT_MOVESPEED + ) { + flag = flag or MOVE_SPEED + } + if (this.flags and FACE_PATHINGENTITY == 0 && + blocks.facePathingEntity.index != FacePathingEntity.DEFAULT_VALUE + ) { + flag = flag or FACE_PATHINGENTITY + } + if (this.flags and FACE_ANGLE == 0 && + blocks.faceAngle.angle != FaceAngle.DEFAULT_VALUE + ) { + flag = flag or FACE_ANGLE + } + return flag + } + + /** + * Checks if the cached version of our appearance is out for date for the [observer]. + * @param observer the avatar observing us. + * @return true if the [observer] needs an updated version of our avatar, false if the cached + * variant is still up-to-date. + */ + private fun checkOutOfDate(observer: PlayerAvatarExtendedInfo): Boolean = + observer.otherAppearanceChangesCounter[localIndex] != appearanceChangesCounter + + /** + * Silently synchronizes the angle of the avatar, meaning any new observers will see them + * at this specific angle. + * @param angle the angle to render them under. + */ + public fun syncAngle(angle: Int) { + this.blocks.faceAngle.syncAngle(angle) + } + + /** + * Pre-computes all the buffers for this avatar. + * Pre-computation is done, so we don't have to calculate these extended info blocks + * for every avatar that observes us. Instead, we can do more performance-efficient + * operations of native memory copying to get the latest extended info blocks. + */ + internal fun precompute() { + // Hits and tinting do not get precomputed + if (flags and APPEARANCE != 0) { + blocks.appearance.precompute(allocator, huffmanCodec) + } + if (flags and TEMP_MOVE_SPEED != 0) { + blocks.temporaryMoveSpeed.precompute(allocator, huffmanCodec) + } + if (flags and SEQUENCE != 0) { + blocks.sequence.precompute(allocator, huffmanCodec) + } + if (flags and FACE_ANGLE != 0 || blocks.faceAngle.outOfDate) { + blocks.faceAngle.markUpToDate() + blocks.faceAngle.precompute(allocator, huffmanCodec) + } + if (flags and SAY != 0) { + blocks.say.precompute(allocator, huffmanCodec) + } + if (flags and CHAT != 0) { + blocks.chat.precompute(allocator, huffmanCodec) + } + if (flags and EXACT_MOVE != 0) { + blocks.exactMove.precompute(allocator, huffmanCodec) + } + if (flags and SPOTANIM != 0) { + blocks.spotAnims.precompute(allocator, huffmanCodec) + } + if (flags and FACE_PATHINGENTITY != 0) { + blocks.facePathingEntity.precompute(allocator, huffmanCodec) + } + if (flags and MOVE_SPEED != 0) { + blocks.moveSpeed.precompute(allocator, huffmanCodec) + } + } + + /** + * Writes the extended info block of this avatar for the given observer. + * @param oldSchoolClientType the client that the observer is using. + * @param buffer the buffer into which the extended info block should be written. + * @param observerFlag the dynamic out-of-date flags that we must send to the observer + * on-top of everything that was pre-computed earlier. + * @param observer the avatar that is observing us. + * @param remainingAvatars the number of avatars that must still be updated for + * the given [observer], necessary to avoid memory overflow. + */ + internal fun pExtendedInfo( + oldSchoolClientType: OldSchoolClientType, + buffer: JagByteBuf, + observerFlag: Int, + observer: PlayerAvatarExtendedInfo, + remainingAvatars: Int, + ) { + val flag = this.flags or observerFlag + if (!filter.accept( + buffer.writableBytes(), + flag, + remainingAvatars, + observer.otherAppearanceChangesCounter[localIndex] != 0, + ) + ) { + buffer.p1(0) + return + } + val writer = + requireNotNull(writers[oldSchoolClientType.id]) { + "Extended info writer missing for client $oldSchoolClientType" + } + + // If appearance is flagged, ensure we synchronize the changes counter + if (flag and APPEARANCE != 0) { + observer.otherAppearanceChangesCounter[localIndex] = appearanceChangesCounter + } + writer.pExtendedInfo( + buffer, + localIndex, + observer.localIndex, + flag, + blocks, + ) + } + + /** + * Clears any flagged transient extended information blocks from this cycle. + */ + private fun clearTransientExtendedInformation() { + if (flags and TEMP_MOVE_SPEED != 0) { + blocks.temporaryMoveSpeed.clear() + } + if (flags and SEQUENCE != 0) { + blocks.sequence.clear() + } + if (flags and SAY != 0) { + blocks.say.clear() + } + if (flags and CHAT != 0) { + blocks.chat.clear() + } + if (flags and EXACT_MOVE != 0) { + blocks.exactMove.clear() + } + if (flags and SPOTANIM != 0) { + blocks.spotAnims.clear() + } + if (flags and HITS != 0) { + blocks.hit.clear() + } + if (flags and TINTING != 0) { + blocks.tinting.clear() + } + } + + /** + * Resets our tracked version of the target's appearance, + * so it will be updated whenever someone else takes their index. + */ + public fun onOtherAvatarDeallocated(idx: Int) { + otherAppearanceChangesCounter[idx] = -1 + } + + public companion object { + // Observer-dependent flags, utilizing the lowest bits as we store observer flags in a byte array + public const val APPEARANCE: Int = 0x1 + public const val MOVE_SPEED: Int = 0x2 + public const val FACE_PATHINGENTITY: Int = 0x4 + public const val TINTING: Int = 0x8 + public const val FACE_ANGLE: Int = 0x10 + + // "Static" flags, the bit values here are irrelevant + public const val SAY: Int = 0x20 + public const val HITS: Int = 0x40 + public const val SEQUENCE: Int = 0x80 + public const val CHAT: Int = 0x100 + public const val TEMP_MOVE_SPEED: Int = 0x200 + public const val EXACT_MOVE: Int = 0x400 + public const val SPOTANIM: Int = 0x800 + + private val SIGNED_BYTE_RANGE: IntRange = Byte.MIN_VALUE.toInt()..Byte.MAX_VALUE.toInt() + private val UNSIGNED_BYTE_RANGE: IntRange = UByte.MIN_VALUE.toInt()..UByte.MAX_VALUE.toInt() + private val UNSIGNED_SHORT_RANGE: IntRange = UShort.MIN_VALUE.toInt()..UShort.MAX_VALUE.toInt() + private val UNSIGNED_SMART_1_OR_2_RANGE: IntRange = 0..0x7FFF + + /** + * Executes the [block] if input verification is enabled, + * otherwise does nothing. Verification should be enabled for + * development environments, to catch problems mid-development. + * In production, or during benchmarking, verification should be disabled, + * as there is still some overhead to running verifications. + */ + private inline fun verify(crossinline block: () -> Unit) { + if (RSProtFlags.extendedInfoInputVerification) { + block() + } + } + + /** + * Builds an extended info writer array indexed by provided client types. + * All client types which are utilized must be registered to avoid runtime errors. + */ + private fun buildClientWriterArray( + extendedInfoWriters: List, + ): Array { + val array = + arrayOfNulls( + OldSchoolClientType.COUNT, + ) + for (writer in extendedInfoWriters) { + array[writer.oldSchoolClientType.id] = writer + } + return array + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarExtendedInfoBlocks.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarExtendedInfoBlocks.kt new file mode 100644 index 000000000..b3c97cd7d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarExtendedInfoBlocks.kt @@ -0,0 +1,68 @@ +package net.rsprot.protocol.game.outgoing.info.playerinfo + +import net.rsprot.protocol.common.client.ClientTypeMap +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.ExtendedInfo +import net.rsprot.protocol.common.game.outgoing.info.encoder.ExtendedInfoEncoder +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.encoder.PlayerExtendedInfoEncoders +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.Appearance +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.Chat +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.FaceAngle +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.MoveSpeed +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.PlayerTintingList +import net.rsprot.protocol.common.game.outgoing.info.playerinfo.extendedinfo.TemporaryMoveSpeed +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.ExactMove +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.FacePathingEntity +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Hit +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Say +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.Sequence +import net.rsprot.protocol.common.game.outgoing.info.shared.extendedinfo.SpotAnimList +import net.rsprot.protocol.game.outgoing.info.AvatarExtendedInfoWriter + +private typealias PEnc = PlayerExtendedInfoEncoders +private typealias TempMoveSpeed = TemporaryMoveSpeed + +/** + * A data structure to bring all the extended info blocks together, + * so the information can be passed onto various client-specific encoders. + * @param writers the list of client-specific writers. + * The writers must be client-specific too, not just encoders, as + * the order in which the extended info blocks get written must follow + * the exact order described by the client. + */ +public class PlayerAvatarExtendedInfoBlocks( + writers: List>, +) { + public val appearance: Appearance = Appearance(encoders(writers, PEnc::appearance)) + public val moveSpeed: MoveSpeed = MoveSpeed(encoders(writers, PEnc::moveSpeed)) + public val temporaryMoveSpeed: TempMoveSpeed = TempMoveSpeed(encoders(writers, PEnc::temporaryMoveSpeed)) + public val sequence: Sequence = Sequence(encoders(writers, PEnc::sequence)) + public val facePathingEntity: FacePathingEntity = FacePathingEntity(encoders(writers, PEnc::facePathingEntity)) + public val faceAngle: FaceAngle = FaceAngle(encoders(writers, PEnc::faceAngle)) + public val say: Say = Say(encoders(writers, PEnc::say)) + public val chat: Chat = Chat(encoders(writers, PEnc::chat)) + public val exactMove: ExactMove = ExactMove(encoders(writers, PEnc::exactMove)) + public val spotAnims: SpotAnimList = SpotAnimList(encoders(writers, PEnc::spotAnim)) + public val hit: Hit = Hit(encoders(writers, PEnc::hit)) + public val tinting: PlayerTintingList = PlayerTintingList(encoders(writers, PEnc::tinting)) + + private companion object { + /** + * Builds a client-specific map of encoders for a specific extended info block, + * keyed by [OldSchoolClientType.id]. + * If a client hasn't been registered, the encoder at that index will be null. + * @param allEncoders all the client-specific extended info writers for the given type. + * @param selector a higher order function to retrieve a specific extended info block from + * the full structure of all the extended info blocks. + * @return a map of client-specific encoders of the given extended info block, + * keyed by [OldSchoolClientType.id]. + */ + private inline fun , reified E : ExtendedInfoEncoder> encoders( + allEncoders: List>, + selector: (PlayerExtendedInfoEncoders) -> E, + ): ClientTypeMap = + ClientTypeMap.ofType(allEncoders, OldSchoolClientType.COUNT) { + it.encoders.oldSchoolClientType to selector(it.encoders) + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarFactory.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarFactory.kt new file mode 100644 index 000000000..5c51b8751 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerAvatarFactory.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.info.playerinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.compression.provider.HuffmanCodecProvider +import net.rsprot.protocol.game.outgoing.info.filter.ExtendedInfoFilter + +public class PlayerAvatarFactory( + private val allocator: ByteBufAllocator, + private val extendedInfoFilter: ExtendedInfoFilter, + private val extendedInfoWriter: List, + private val huffmanCodec: HuffmanCodecProvider, +) { + internal fun alloc(index: Int): PlayerAvatar { + // It is possible to just pass in the extended info from here, but based on benchmarks, + // due to the field order changing, the performance will absolutely tank in doing so, + // going from ~160ms in the benchmark to around 200ms + return PlayerAvatar( + allocator, + index, + extendedInfoFilter, + extendedInfoWriter, + huffmanCodec, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfo.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfo.kt new file mode 100644 index 000000000..b81db8db3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfo.kt @@ -0,0 +1,1014 @@ +package net.rsprot.protocol.game.outgoing.info.playerinfo + +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.bitbuffer.BitBuf +import net.rsprot.buffer.bitbuffer.UnsafeLongBackedBitBuf +import net.rsprot.buffer.bitbuffer.toBitBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.outgoing.info.ObserverExtendedInfoFlags +import net.rsprot.protocol.game.outgoing.info.exceptions.InfoProcessException +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfoProtocol.Companion.PROTOCOL_CAPACITY +import net.rsprot.protocol.game.outgoing.info.playerinfo.util.CellOpcodes +import net.rsprot.protocol.game.outgoing.info.util.Avatar +import net.rsprot.protocol.game.outgoing.info.util.BuildArea +import net.rsprot.protocol.game.outgoing.info.util.ReferencePooledObject +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract +import kotlin.math.abs + +/** + * An implementation of the player info packet. + * This class is responsible for tracking and building the packets each cycle. + * This class utilizes [ReferencePooledObject], meaning instances of it will be pooled + * and re-used as needed, as the data stored within them is relatively memory-heavy. + * + * @param protocol the repository of all the [PlayerInfo] objects, + * as well as a source global information about everyone in the game. + * As the packet is responsible for tracking everyone in the game, + * we need to provide access to this. + * @param localIndex the index of this local player. The index corresponds to the player's slot + * in the world. The index will not change throughout the lifespan of a player, + * but can change within allocations in the reference pool. + * @param allocator the [ByteBuf] allocator responsible for allocating the primary buffer + * the is written out to the pipeline, as well as any intermediate buffers used by extended + * info blocks. The allocator should ideally be pooled, as we acquire a new instance with each + * cycle. This is because there isn't necessarily a guarantee that Netty threads have fully + * written the information out to the network by the time the next cycle comes along and starts + * writing into this buffer. A direct implementation is also preferred, as this avoids unnecessary + * copying from and to the heap. + * @param oldSchoolClientType the client on which the player is logging into. This is utilized + * to determine what encoders to use for extended info blocks. + */ +@Suppress("DuplicatedCode", "ReplaceUntilWithRangeUntil", "MemberVisibilityCanBePrivate") +public class PlayerInfo internal constructor( + private val protocol: PlayerInfoProtocol, + internal var localIndex: Int, + internal val allocator: ByteBufAllocator, + private var oldSchoolClientType: OldSchoolClientType, + public val avatar: PlayerAvatar, +) : ReferencePooledObject { + /** + * Low resolution indices are tracked together with [lowResolutionCount]. + * Whenever a player enters the low resolution view, their index + * is added into this [lowResolutionIndices] array, and the [lowResolutionCount] + * is incremented by one. + * At the end of each cycle, the [lowResolutionIndices] are rebuilt to sort the indices. + */ + private val lowResolutionIndices: ShortArray = ShortArray(PROTOCOL_CAPACITY) + + /** + * The number of players in low resolution according to the protocol. + */ + private var lowResolutionCount: Int = 0 + + /** + * The tracked high resolution players by their indices. + * If a player enters our high resolution, the bit at their index is set to true. + * We do not need to use references to players as we can then refer to the [PlayerInfoRepository] + * to find the actual [PlayerInfo] implementation. + */ + private val highResolutionPlayers: LongArray = LongArray(PROTOCOL_CAPACITY ushr 6) + + /** + * High resolution indices are tracked together with [highResolutionCount]. + * Whenever an external player enters the high resolution view, their index + * is added into this [highResolutionIndices] array, and the [highResolutionCount] + * is incremented by one. + * At the end of each cycle, the [highResolutionIndices] are rebuilt to sort the indices. + */ + private val highResolutionIndices: ShortArray = ShortArray(PROTOCOL_CAPACITY) + + /** + * The number of players in high resolution according to the protocol. + */ + private var highResolutionCount: Int = 0 + + /** + * The extended info indices contain pointers to all the players for whom we need to + * write an extended info block. We do this rather than directly writing them as this + * improves CPU cache locality and allows us to batch extended info blocks together. + */ + private val extendedInfoIndices: ShortArray = ShortArray(PROTOCOL_CAPACITY) + + /** + * The number of players for whom we need to write extended info blocks this cycle. + */ + private var extendedInfoCount: Int = 0 + + /** + * The flags indicating the status of the players in the previous and current cycles. + * This is used to categorize players who are 'stationary', which implies they did not + * move, nor did they have any extended info blocks written for them. By batching + * players up this way, the protocol is able to skip a larger number of players + * with each skip block, as players are far more likely to be in the same state + * as they were in the last cycle. + */ + private val stationary = ByteArray(PROTOCOL_CAPACITY) + + /** + * The observer info flags are used for us to track extended info blocks which weren't necessarily + * flagged on the target player. This can happen during the transitioning from low resolution + * to high resolution, in which case appearance, move speed and face pathingentity may be transmitted, + * despite not having been flagged. Additionally, some extended info blocks, such as hits and tinting, + * will sometimes be observer-dependent. This means each observer will receive a different variant + * of the extended info buffer. A simple example of this is the red circle hitmark ironmen will + * see on NPCs whenever they attack a NPC that has already received damage from another player. + * Only the ironman will receive information about that hitmark in this case, and no one else. + */ + internal val observerExtendedInfoFlags: ObserverExtendedInfoFlags = ObserverExtendedInfoFlags(PROTOCOL_CAPACITY) + + /** + * High resolution bit buffers are cached to avoid small computations for each observer, + * and it allows us to reduce the number of [BitBuf.pBits] calls, which are quite expensive. + * This implementation will store all the information inside a 'long' primitive, as the maximum + * data size will always fit in under 50 bits. + */ + private var highResMovementBuffer: UnsafeLongBackedBitBuf? = null + + /** + * Low resolution bit buffers are cached to avoid small computations for each observer, + * and it allows us to reduce the number of [BitBuf.pBits] calls, which are quite expensive. + * This implementation will store all the information inside a 'long' primitive, as the maximum + * data size will always fit in under 50 bits. + */ + private var lowResMovementBuffer: UnsafeLongBackedBitBuf? = null + + /** + * The buffer into which all the information is written in this cycle. + * It should be noted that this buffer is constantly changing, as we reallocate + * a new buffer instance through the [allocator] each cycle. This is to ensure that + * we do not start overwriting a buffer before it has been fully written into the pipeline. + * Thus, a pooled [allocator] implementation should be preferred to avoid expensive re-allocations. + */ + private var buffer: ByteBuf? = null + + /** + * The exception that was caught during the processing of this player's playerinfo packet. + * This exception will be propagated further during the [toPacket] function call, + * allowing the server to handle it properly at a per-player basis. + */ + internal var exception: Exception? = null + + /** + * Whether the buffer allocated by this player info object has been built + * into a packet message. If this returns false, but player info was in fact built, + * we have an allocated buffer that needs releasing. If the NPC info itself + * is released but isn't built into packet, we make sure to release it, to avoid + * any memory leaks. + */ + private var builtIntoPacket: Boolean = false + + /** + * An array of world details, containing all the player info properties specific to a single world. + * The root world is placed at the end of this array, however id -1 will be treated as the root. + */ + internal val details: Array = arrayOfNulls(PROTOCOL_CAPACITY + 1) + + /** + * Returns the backing buffer for this cycle. + * @throws IllegalStateException if the buffer has not been allocated yet. + */ + @Throws(IllegalStateException::class) + public fun backingBuffer(): ByteBuf = checkNotNull(buffer) + + /** + * Updates the render coordinate for the provided world id. + * This coordinate is what will be used to perform distance checks between the player + * and everyone else. + * @param worldId the id of the world in which the player resides + * @param level the height level at which the player resides + * @param x the absolute x coordinate where the player resides + * @param z the absolute z coordinate where the player resides + */ + public fun updateRenderCoord( + worldId: Int, + level: Int, + x: Int, + z: Int, + ) { + require(worldId == ROOT_WORLD || worldId in 0..<2048) { + "World id must be -1 or in range of 0..<2048" + } + val details = getDetails(worldId) + details.renderCoord = CoordGrid(level, x, z) + } + + /** + * Updates the build area of a given world to the specified one. + * This will ensure that no players outside of this box will be + * added to high resolution view. + * @param worldId the id of the world to set the build area of, + * with -1 being the root world. + * @param buildArea the build area to assign. + */ + public fun updateBuildArea( + worldId: Int, + buildArea: BuildArea, + ) { + require(worldId == ROOT_WORLD || worldId in 0..<2048) { + "World id must be -1 or in range of 0..<2048" + } + val details = getDetails(worldId) + details.buildArea = buildArea + } + + /** + * Allocates a new player info tracking object for the respective [worldId], + * keeping track of everyone that's within this new world entity. + * @param worldId the new world entity id + */ + public fun allocateWorld(worldId: Int) { + require(worldId in 0.. + buffer.pBits(30, avatar.currentCoord.packed) + setHighResolution(localIndex) + highResolutionIndices[highResolutionCount++] = localIndex.toShort() + for (i in 1 until PROTOCOL_CAPACITY) { + if (i == localIndex) { + continue + } + val lowResolutionPosition = protocol.getLowResolutionPosition(i) + buffer.pBits(18, lowResolutionPosition.packed) + lowResolutionIndices[lowResolutionCount++] = i.toShort() + } + } + // Sync the coordinate delta here! + // Meaning if a player info is sent afterwards, it will not re-send the delta + // which often results in the coordinate being 2x'd at the client + avatar.postUpdate() + } + + /** + * Resets any existing state. + * Cached state should be re-assigned from the server as a result of this. + */ + public fun onReconnect() { + // If player info was constructed, but it was not built into a packet object + // it implies the packet is never being written to Netty, which means + // a memory leak is occurring - if that is the case, release the buffer here + if (!builtIntoPacket) { + val buffer = this.buffer + if (buffer != null && buffer.refCnt() > 0) { + buffer.release(buffer.refCnt()) + } + } + this.buffer = null + highResMovementBuffer = null + lowResMovementBuffer = null + + lowResolutionIndices.fill(0) + lowResolutionCount = 0 + highResolutionIndices.fill(0) + highResolutionCount = 0 + highResolutionPlayers.fill(0L) + extendedInfoCount = 0 + extendedInfoIndices.fill(0) + stationary.fill(0) + observerExtendedInfoFlags.reset() + avatar.postUpdate() + } + + /** + * Precalculates all the bitcodes for this player, for both low-resolution and high-resolution updates. + * This function will be thread-safe relative to other players and can be calculated concurrently for all players. + */ + internal fun prepareBitcodes(globalLowResolutionPositionRepository: GlobalLowResolutionPositionRepository) { + this.highResMovementBuffer = prepareHighResMovement() + this.lowResMovementBuffer = prepareLowResMovement(globalLowResolutionPositionRepository) + } + + /** + * Pre-computes extended info blocks for this player. Only extended info blocks + * which were flagged during this cycle will be pre-computed, with any on-demand + * extended info blocks excluded in pre-computations altogether. + */ + internal fun precomputeExtendedInfo() { + avatar.extendedInfo.precompute() + } + + /** + * Writes the extended info blocks of everyone who were marked + * during [pBitcodes] to the [buffer]. This will utilize fast native memory copying for any + * pre-computed extended info blocks. For any observer-dependent info blocks, + * a new [ByteBuf] instance is allocated from the [allocator], which is then written + * the information, followed by a fast native copy, which is further followed by releasing + * this temporary buffer back. As mentioned before, it is highly suggested to use a pooled + * implementation of the [allocator]. + * This function is thread-safe relative to other players and can be computed for all players + * concurrently. + */ + internal fun putExtendedInfo() { + val jagBuffer = backingBuffer().toJagByteBuf() + for (i in 0 until extendedInfoCount) { + val index = extendedInfoIndices[i].toInt() + val other = checkNotNull(protocol.getPlayerInfo(index)) + val observerFlag = observerExtendedInfoFlags.getFlag(index) + other.avatar.extendedInfo.pExtendedInfo( + oldSchoolClientType, + jagBuffer, + observerFlag, + avatar.extendedInfo, + extendedInfoCount - i, + ) + } + } + + /** + * Writes to the actual buffers the prepared bitcodes and extended information. + * This function will be thread-safe relative to other players and can be calculated concurrently for all players. + */ + internal fun pBitcodes() { + avatar.resize(highResolutionCount) + val buffer = allocBuffer() + val bitBuf = buffer.toBitBuf() + bitBuf.use { processHighResolution(it, skipStationary = true) } + bitBuf.use { processHighResolution(it, skipStationary = false) } + bitBuf.use { processLowResolution(it, skipStationary = false) } + bitBuf.use { processLowResolution(it, skipStationary = true) } + } + + /** + * Processes low resolution updates for all the players who are currently + * in our low resolution view. + * @param buffer the buffer into which to write the bitcodes regarding each player. + * @param skipStationary whether to skip any players who were marked as stationary last cycle. + */ + private fun processLowResolution( + buffer: BitBuf, + skipStationary: Boolean, + ) { + var skips = -1 + for (i in 0 until lowResolutionCount) { + val index = lowResolutionIndices[i].toInt() + val wasStationary = stationary[index].toInt() and WAS_STATIONARY != 0 + if (skipStationary == wasStationary) { + continue + } + val other = protocol.getPlayerInfo(index) + if (other == null) { + skips++ + stationary[index] = (stationary[index].toInt() or IS_STATIONARY).toByte() + continue + } + val visible = shouldMoveToHighResolution(other) + if (!visible && other.lowResMovementBuffer == null) { + skips++ + stationary[index] = (stationary[index].toInt() or IS_STATIONARY).toByte() + continue + } + if (skips > -1) { + pStationary(buffer, skips) + skips = -1 + } + if (!visible) { + buffer.pBits(1, 1) + buffer.pBits(other.lowResMovementBuffer!!) + continue + } + pLowResToHighRes(buffer, other) + } + if (skips > -1) { + pStationary(buffer, skips) + } + } + + /** + * Writes a transition from low resolution to high resolution for the given player. + * @param buffer the buffer into which to write the transition. + * @param other the player who is being moved from low resolution to high resolution. + */ + private fun pLowResToHighRes( + buffer: BitBuf, + other: PlayerInfo, + ) { + val index = other.localIndex + // The above one-liner pBits is equal to this comment: + // buffer.pBits(1, 1) + // buffer.pBits(2, 0) + buffer.pBits(3, 1 shl 2) + val lowResBuf = other.lowResMovementBuffer + if (lowResBuf != null) { + buffer.pBits(1, 1) + buffer.pBits(lowResBuf) + } else { + buffer.pBits(1, 0) + } + val (_, x, z) = other.avatar.currentCoord + + buffer.pBits(13, x) + buffer.pBits(13, z) + + // Get a flags of all the extended info blocks that are 'outdated' to us and must be sent again. + val extraFlags = other.avatar.extendedInfo.getLowToHighResChangeExtendedInfoFlags(avatar.extendedInfo) + // Mark those flags as observer-dependent. + observerExtendedInfoFlags.addFlag(index, extraFlags) + stationary[index] = (stationary[index].toInt() or IS_STATIONARY).toByte() + setHighResolution(index) + val flag = other.avatar.extendedInfo.flags or observerExtendedInfoFlags.getFlag(index) + val hasExtendedInfoBlock = flag != 0 + if (hasExtendedInfoBlock) { + extendedInfoIndices[extendedInfoCount++] = index.toShort() + buffer.pBits(1, 1) + } else { + buffer.pBits(1, 0) + } + } + + /** + * Processes high resolution updates for all the players who are currently + * in our high resolution view. + * @param buffer the buffer into which to write the bitcodes regarding each player. + * @param skipStationary whether to skip any players who were marked as stationary last cycle. + */ + private fun processHighResolution( + buffer: BitBuf, + skipStationary: Boolean, + ) { + var skips = -1 + for (i in 0 until highResolutionCount) { + val index = highResolutionIndices[i].toInt() + val wasStationary = (stationary[index].toInt() and WAS_STATIONARY) != 0 + if (skipStationary == wasStationary) { + continue + } + val other = protocol.getPlayerInfo(index) + if (!shouldStayInHighResolution(other)) { + if (skips > -1) { + pStationary(buffer, skips) + skips = -1 + } + pHighToLowResChange(buffer, index, other) + continue + } + + val flag = other.avatar.extendedInfo.flags or observerExtendedInfoFlags.getFlag(index) + val hasExtendedInfoBlock = flag != 0 + val highResBuf = other.highResMovementBuffer + val skipped = !hasExtendedInfoBlock && highResBuf == null + if (!skipped) { + if (skips > -1) { + pStationary(buffer, skips) + skips = -1 + } + pHighRes(buffer, index, hasExtendedInfoBlock, highResBuf) + continue + } + skips++ + stationary[index] = (stationary[index].toInt() or IS_STATIONARY).toByte() + } + if (skips > -1) { + pStationary(buffer, skips) + } + } + + /** + * Writes the [count] of consecutive stationary players + * using [run-length encoding](https://en.wikipedia.org/wiki/Run-length_encoding). + * @param buffer the buffer into which to write the encoded count. + * @param count the count of players that were skipped. + * The actual number that is written will always be 1 less, as the client automatically + * includes 1 in the total value through the presence of a stationary block in the first place. + */ + private fun pStationary( + buffer: BitBuf, + count: Int, + ) { + // The below code is a branchless variant of this: + // buffer.pBits(1, 0) + // when { + // count == 0 -> buffer.pBits(2, 0) + // count <= 0x1F -> { + // buffer.pBits(2, 1) + // buffer.pBits(5, count) + // } + // count <= 0xFF -> { + // buffer.pBits(2, 2) + // buffer.pBits(8, count) + // } + // else -> { + // buffer.pBits(2, 3) + // buffer.pBits(11, count) + // } + // } + // + // The branching causes a significant (~15-20%) performance loss in the extreme + // end-case benchmarks, so it's best to eliminate it. + + // (Special thanks to Greg for figuring out the magic below!) + // Positive signum the bits proceeding the 1st, 5th and 8th bit to give a value 1 - 3 to + // represent > 0, > 31 and > 255 respectively. + val lowerBits = (-count ushr 31) + val higherBits = (-(count shr 5) ushr 31) + (-(count shr 8) ushr 31) + val bitCountOpcode = lowerBits + higherBits + val valueBitCount = (lowerBits * 5) + (higherBits * 3) + buffer.pBits(3 + valueBitCount, count or (bitCountOpcode shl valueBitCount)) + } + + /** + * Writes high resolution information about a player into the [buffer]. + * @param buffer the buffer into which to write the bitcodes. + * @param index the index of the player whose information we are writing. + * @param extendedInfo whether this player also had extended info block changes. + * @param highResBuf the pre-computed bit buffer regarding this player's movement. + */ + private fun pHighRes( + buffer: BitBuf, + index: Int, + extendedInfo: Boolean, + highResBuf: UnsafeLongBackedBitBuf?, + ) { + buffer.pBits(1, 1) + if (extendedInfo) { + extendedInfoIndices[extendedInfoCount++] = index.toShort() + buffer.pBits(1, 1) + } else { + buffer.pBits(1, 0) + } + if (highResBuf != null) { + buffer.pBits(highResBuf) + } else { + buffer.pBits(2, 0) + } + } + + /** + * Writes a high resolution to low resolution change for the player. + * @param buffer the buffer into which to write the bitcodes. + * @param index the index of the player that is being moved to low resolution. + */ + private fun pHighToLowResChange( + buffer: BitBuf, + index: Int, + other: PlayerInfo?, + ) { + unsetHighResolution(index) + // The one-liner pBits is equal to the below comment: + // buffer.pBits(1, 1) + // buffer.pBits(1, 0) + // buffer.pBits(2, 0) + buffer.pBits(4, 1 shl 3) + val buf = other?.lowResMovementBuffer + if (buf != null) { + buffer.pBits(1, 1) + buffer.pBits(buf) + } else { + buffer.pBits(1, 0) + } + } + + /** + * Checks if [other] is visible to us considering our [PlayerAvatar.resizeRange]. + * This function utilizes experimental contracts to avoid an unnecessary null-check, + * as if the function returns true, the parameter cannot ever be null. + * @param other the player whom to check. + * @return true if the other should be moved to low resolution. + */ + @OptIn(ExperimentalContracts::class) + private fun shouldStayInHighResolution(other: PlayerInfo?): Boolean { + contract { + returns(true) implies (other != null) + } + // If the avatar is no longer logged in, remove it + if (other == null) { + return false + } + // Do not add or remove local player + if (other.localIndex == localIndex) { + return true + } + if (other.avatar.hidden) { + return false + } + val worldId = other.avatar.worldId + val details = getDetailsOrNull(worldId) ?: return false + val coord = other.avatar.currentCoord + if (!coord.inDistance(details.renderCoord, this.avatar.resizeRange)) { + return false + } + if (coord !in details.buildArea) { + return false + } + return true + } + + /** + * Checks if [other] is visible to us considering our [PlayerAvatar.resizeRange]. + * This function utilizes experimental contracts to avoid an unnecessary null-check, + * as if the function returns true, the parameter cannot ever be null. + * @param other the player whom to check. + * @return true if the other player should be moved to high resolution. + */ + @OptIn(ExperimentalContracts::class) + private fun shouldMoveToHighResolution(other: PlayerInfo?): Boolean { + contract { + returns(true) implies (other != null) + } + // If the avatar is no longer logged in, remove it + if (other == null || other.localIndex == localIndex) { + return false + } + if (other.avatar.hidden) { + return false + } + val worldId = other.avatar.worldId + val details = getDetailsOrNull(worldId) ?: return false + val coord = other.avatar.currentCoord + if (!coord.inDistance(details.renderCoord, this.avatar.resizeRange)) { + return false + } + if (coord !in details.buildArea) { + return false + } + return true + } + + /** + * Allocates a new buffer from the [allocator] with a capacity of [BUF_CAPACITY]. + * The old [buffer] will not be released, as that is the duty of the encoder class. + */ + private fun allocBuffer(): ByteBuf { + // If a given player's packet was never sent out, we need to release the old buffer + if (!builtIntoPacket) { + val oldBuf = buffer + if (oldBuf != null && oldBuf.refCnt() > 0) { + oldBuf.release() + } + } + // Acquire a new buffer with each cycle, in case the previous one isn't fully written out yet + val buffer = allocator.buffer(BUF_CAPACITY, BUF_CAPACITY) + this.buffer = buffer + this.builtIntoPacket = false + return buffer + } + + /** + * Reset any temporary properties from this cycle. + */ + internal fun postUpdate() { + this.avatar.postUpdate() + avatar.extendedInfo.postUpdate() + lowResolutionCount = 0 + highResolutionCount = 0 + // Only need to reset the count here, the actual numbers don't matter. + extendedInfoCount = 0 + for (i in 1 until PROTOCOL_CAPACITY) { + stationary[i] = (stationary[i].toInt() shr 1).toByte() + if (isHighResolution(i)) { + highResolutionIndices[highResolutionCount++] = i.toShort() + } else { + lowResolutionIndices[lowResolutionCount++] = i.toShort() + } + } + observerExtendedInfoFlags.reset() + avatar.extendedInfo.postUpdate() + } + + /** + * Resets all the primitive properties of this class which can be lazy-reset. + * We utilize lazy resetting here as there's no guarantee that a given [PlayerInfo] + * object will ever be re-used. Due to the nature of soft references, it is possible + * for the garbage collector to collect it when it truly needs it. In order to reduce processing + * time, we skip resetting these properties on de-allocation. + * @param index the index of the new player who will be utilizing this player info object. + * @param oldSchoolClientType the client the new player is utilizing. + */ + override fun onAlloc( + index: Int, + oldSchoolClientType: OldSchoolClientType, + newInstance: Boolean, + ) { + this.localIndex = index + avatar.extendedInfo.localIndex = index + this.oldSchoolClientType = oldSchoolClientType + avatar.reset() + lowResolutionIndices.fill(0) + lowResolutionCount = 0 + highResolutionIndices.fill(0) + highResolutionCount = 0 + highResolutionPlayers.fill(0L) + extendedInfoCount = 0 + extendedInfoIndices.fill(0) + stationary.fill(0) + observerExtendedInfoFlags.reset() + allocateWorld(ROOT_WORLD) + } + + /** + * Clears any references to temporary buffers on de-allocation, as we don't want these + * to stick around for extended periods of time. Any primitive properties will remain untouched. + */ + override fun onDealloc() { + // If player info was constructed, but it was not built into a packet object + // it implies the packet is never being written to Netty, which means + // a memory leak is occurring - if that is the case, release the buffer here + if (!builtIntoPacket) { + val buffer = this.buffer + if (buffer != null && buffer.refCnt() > 0) { + buffer.release(buffer.refCnt()) + } + } + this.buffer = null + avatar.extendedInfo.reset() + highResMovementBuffer = null + lowResMovementBuffer = null + for (i in this.details.indices) { + val world = this.details[i] + if (world != null) { + this.details[i] = null + } + } + } + + /** + * Prepares the low resolution movement block using global information about all players' + * low resolution coordinates. + * @param globalLowResolutionPositionRepository the global repository tracking everyone's + * low resolution coordinate. + * @return unsafe long-backed bit buffer that encodes the information into a 'long' primitive, + * rather than a real byte buffer, in order to reduce unnecessary computations. + */ + private fun prepareLowResMovement( + globalLowResolutionPositionRepository: GlobalLowResolutionPositionRepository, + ): UnsafeLongBackedBitBuf? { + val old = globalLowResolutionPositionRepository.getPreviousLowResolutionPosition(localIndex) + val cur = globalLowResolutionPositionRepository.getCurrentLowResolutionPosition(localIndex) + if (old == cur) { + return null + } + val buffer = UnsafeLongBackedBitBuf() + val deltaX = cur.x - old.x + val deltaZ = cur.z - old.z + val deltaLevel = cur.level - old.level + if (deltaX == 0 && deltaZ == 0) { + buffer.pBits(2, 1) + buffer.pBits(2, deltaLevel) + } else if (abs(deltaX) <= 1 && abs(deltaZ) <= 1) { + buffer.pBits(2, 2) + buffer.pBits(2, deltaLevel) + buffer.pBits(3, CellOpcodes.singleCellMovementOpcode(deltaX, deltaZ)) + } else { + buffer.pBits(2, 3) + buffer.pBits(2, deltaLevel) + buffer.pBits(8, deltaX and 0xFF) + buffer.pBits(8, deltaZ and 0xFF) + } + return buffer + } + + /** + * Prepares the high resolution movement block by checking the player's absolute coordinate + * differences. + * @return unsafe long-backed bit buffer that encodes the information into a 'long' primitive, + * rather than a real byte buffer, in order to reduce unnecessary computations. + */ + private fun prepareHighResMovement(): UnsafeLongBackedBitBuf? { + val oldCoord = avatar.lastCoord + val newCoord = avatar.currentCoord + if (oldCoord == newCoord) { + return null + } + val buffer = UnsafeLongBackedBitBuf() + val deltaX = newCoord.x - oldCoord.x + val deltaZ = newCoord.z - oldCoord.z + val deltaLevel = newCoord.level - oldCoord.level + val absX = abs(deltaX) + val absZ = abs(deltaZ) + if (deltaLevel != 0 || absX > 2 || absZ > 2) { + if (absX >= 16 || absZ >= 16) { + pLargeTeleport(buffer, deltaX, deltaZ, deltaLevel) + } else { + pSmallTeleport(buffer, deltaX, deltaZ, deltaLevel) + } + } else if (absX == 2 || absZ == 2) { + pRun(buffer, deltaX, deltaZ) + } else { + // Guaranteed to be walking here, as our 'oldCoord == newCoord' covers the stationary condition. + pWalk(buffer, deltaX, deltaZ) + } + return buffer + } + + /** + * Writes a single cell movement bitcode. + * @param buffer the buffer into which to write the bitcode. + * @param deltaX the x-coordinate delta the player moved. + * @param deltaZ the z-coordinate delta the player moved. + * @throws ArrayIndexOutOfBoundsException if the provided deltas do not result in a + * one-cell movement. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + private fun pWalk( + buffer: UnsafeLongBackedBitBuf, + deltaX: Int, + deltaZ: Int, + ) { + buffer.pBits(2, 1) + buffer.pBits(3, CellOpcodes.singleCellMovementOpcode(deltaX, deltaZ)) + } + + /** + * Writes a dual cell movement bitcode. + * @param buffer the buffer into which to write the bitcode. + * @param deltaX the x-coordinate delta the player moved. + * @param deltaZ the z-coordinate delta the player moved. + * @throws ArrayIndexOutOfBoundsException if the provided deltas do not result in a + * dual-cell movement. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + private fun pRun( + buffer: UnsafeLongBackedBitBuf, + deltaX: Int, + deltaZ: Int, + ) { + buffer.pBits(2, 2) + buffer.pBits(4, CellOpcodes.dualCellMovementOpcode(deltaX, deltaZ)) + } + + /** + * Writes a low-distance movement block, capped to a maximum delta of 15 coordinates + * as well as any level changes. + * @param buffer the buffer into which to write the bitcode. + * @param deltaX the x-coordinate delta the player moved. + * @param deltaZ the z-coordinate delta the player moved. + * @param deltaLevel the level-coordinate delta the player moved. + */ + private fun pSmallTeleport( + buffer: UnsafeLongBackedBitBuf, + deltaX: Int, + deltaZ: Int, + deltaLevel: Int, + ) { + buffer.pBits(2, 3) + buffer.pBits(1, 0) + buffer.pBits(2, deltaLevel and 0x3) + buffer.pBits(5, deltaX and 0x1F) + buffer.pBits(5, deltaZ and 0x1F) + } + + /** + * Writes a long-distance movement block, completely uncapped for the game world. + * @param buffer the buffer into which to write the bitcode. + * @param deltaX the x-coordinate delta the player moved. + * @param deltaZ the z-coordinate delta the player moved. + * @param deltaLevel the level-coordinate delta the player moved. + */ + private fun pLargeTeleport( + buffer: UnsafeLongBackedBitBuf, + deltaX: Int, + deltaZ: Int, + deltaLevel: Int, + ) { + buffer.pBits(2, 3) + buffer.pBits(1, 1) + buffer.pBits(2, deltaLevel and 0x3) + buffer.pBits(14, deltaX and 0x3FFF) + buffer.pBits(14, deltaZ and 0x3FFF) + } + + public companion object { + /** + * The default capacity of the backing byte buffer into which all player info is written. + */ + private const val BUF_CAPACITY: Int = 40_000 + + /** + * The flag indicating that a player was stationary in the previous cycle. + */ + private const val WAS_STATIONARY: Int = 0x1 + + /** + * The flag indicating that a player is stationary in the current cycle. + */ + private const val IS_STATIONARY: Int = 0x2 + + /** + * The constant id for the root world. + */ + public const val ROOT_WORLD: Int = -1 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoPacket.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoPacket.kt new file mode 100644 index 000000000..a8493c51e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoPacket.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.outgoing.info.playerinfo + +import io.netty.buffer.ByteBuf +import io.netty.buffer.DefaultByteBufHolder +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * A npc info packet class, wrapped in its own byte buf holder as the packet encoder is only + * invoked through Netty threads, therefore it is not safe to strictly pass the reference + * from player info itself. + */ +public class PlayerInfoPacket( + buffer: ByteBuf, +) : DefaultByteBufHolder(buffer), + OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + return true + } + + override fun hashCode(): Int = super.hashCode() + + override fun toString(): String = super.toString() +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoProtocol.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoProtocol.kt new file mode 100644 index 000000000..a3f5a5d04 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoProtocol.kt @@ -0,0 +1,258 @@ +package net.rsprot.protocol.game.outgoing.info.playerinfo + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.game.outgoing.info.playerinfo.util.LowResolutionPosition +import net.rsprot.protocol.game.outgoing.info.worker.DefaultProtocolWorker +import net.rsprot.protocol.game.outgoing.info.worker.ProtocolWorker +import java.util.concurrent.Callable +import java.util.concurrent.ForkJoinPool +import kotlin.Exception + +/** + * The player info protocol is responsible for tracking everything player info related + * within the given world. This class holds every avatar, their state, and provides + * means to allocate new player info instances. + * @param allocator the [ByteBuf] allocator responsible for allocating the primary buffer + * the is written out to the pipeline, as well as any intermediate buffers used by extended + * info blocks. The allocator should ideally be pooled, as we acquire a new instance with each + * cycle. This is because there isn't necessarily a guarantee that Netty threads have fully + * written the information out to the network by the time the next cycle comes along and starts + * writing into this buffer. A direct implementation is also preferred, as this avoids unnecessary + * copying from and to the heap. + * @param worker the worker responsible for executing the blocks of code found in player info. + * The default worker will remain single-threaded if there are less than `coreCount * 4` players + * in the world. Otherwise, it will use [ForkJoinPool] to execute these jobs. Both of these + * are configurable within the [DefaultProtocolWorker] constructor. + */ +public class PlayerInfoProtocol( + private val allocator: ByteBufAllocator, + private val worker: ProtocolWorker = DefaultProtocolWorker(), + private val avatarFactory: PlayerAvatarFactory, +) { + /** + * The repository responsible for keeping track of all the players' low resolution + * position within the world. + */ + private val lowResolutionPositionRepository: GlobalLowResolutionPositionRepository = + GlobalLowResolutionPositionRepository() + + /** + * The repository responsible for allocating and storing player info instances of + * all the avatars that exist. + */ + private val playerInfoRepository: PlayerInfoRepository = + PlayerInfoRepository { localIndex, clientType -> + PlayerInfo( + this, + localIndex, + allocator, + clientType, + avatarFactory.alloc(localIndex), + ) + } + + /** + * The list of [Callable] instances which perform the jobs for player info. + * This list itself is re-used throughout the lifespan of the application, + * but the [Callable] instances themselves are generated for every job. + */ + private val callables: MutableList> = ArrayList(PROTOCOL_CAPACITY) + + /** + * Gets the current element at index [idx], or null if it doesn't exist. + * @param idx the index of the player info object to obtain + * @throws ArrayIndexOutOfBoundsException if the index is below zero, + * or above [PlayerInfoProtocol.PROTOCOL_CAPACITY]. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + internal fun getPlayerInfo(idx: Int): PlayerInfo? = playerInfoRepository.getOrNull(idx) + + /** + * Allocates a new player info instance at index [idx] + * @param idx the index of the player to allocate + * @param oldSchoolClientType the client on which this player is. + * The client type is used to determine which extended info encoders to utilize + * when building the buffers for this packet. + * @throws ArrayIndexOutOfBoundsException if the [idx] is below 1, or above 2047. + * @throws IllegalStateException if the element at index [idx] is already in use. + */ + @Throws( + ArrayIndexOutOfBoundsException::class, + IllegalStateException::class, + ) + public fun alloc( + idx: Int, + oldSchoolClientType: OldSchoolClientType, + ): PlayerInfo { + // Only handle index 0 as a special case, as the protocol + // does not allow putting an avatar at index 0. + // Other index exceptions are handled by the alloc function. + if (idx == 0) { + throw ArrayIndexOutOfBoundsException("Index 0 is not valid for player info protocol.") + } + return playerInfoRepository.alloc(idx, oldSchoolClientType) + } + + /** + * Deallocates the player info object, releasing it back into the pool to be used by another player. + * @param info the player info object + */ + public fun dealloc(info: PlayerInfo) { + playerInfoRepository.dealloc(info.localIndex) + } + + /** + * Gets the current cycle's low resolution position of the player at index [idx]. + * @param idx the index of the player + * @return the low resolution position of that player in the current cycle. + */ + internal fun getLowResolutionPosition(idx: Int): LowResolutionPosition = + lowResolutionPositionRepository.getCurrentLowResolutionPosition(idx) + + public fun update() { + prepare() + putBitcodes() + prepareExtendedInfo() + putExtendedInfo() + postUpdate() + } + + /** + * Prepares the player info protocol for every player in the world. + * First it will synchronize the low resolution positions of all the avatars in the world. + * Afterwards, according to the implementation defined by the [worker], + * this will cache the low and high resolution movement bit buffers + * for every avatar in the world. + */ + private fun prepare() { + // Synchronize the known low res positions of everyone for this cycle + for (i in 1.. Unit) { + for (i in 1.. PlayerInfo, +) : InfoRepository(allocator) { + /** + * The backing elements array used to store currently-in-use objects. + */ + override val elements: Array = arrayOfNulls(PlayerInfoProtocol.PROTOCOL_CAPACITY) + + override fun informDeallocation(idx: Int) { + for (element in elements) { + if (element == null) { + continue + } + element.avatar.extendedInfo.onOtherAvatarDeallocated(idx) + } + } + + override fun onDealloc(element: PlayerInfo) { + element.onDealloc() + } + + override fun onAlloc( + element: PlayerInfo, + idx: Int, + oldSchoolClientType: OldSchoolClientType, + newInstance: Boolean, + ) { + element.onAlloc(idx, oldSchoolClientType, newInstance) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoWorldDetails.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoWorldDetails.kt new file mode 100644 index 000000000..e8c8192ea --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/PlayerInfoWorldDetails.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.info.playerinfo + +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.outgoing.info.util.BuildArea + +/** + * A class which wraps the details of a player info implementation in a specific world. + * @property worldId the id of the world this info object is tracking. + */ +internal class PlayerInfoWorldDetails( + internal var worldId: Int, +) { + /** + * The coordinate from which distance checks are done against other players. + */ + internal var renderCoord: CoordGrid = CoordGrid.INVALID + + /** + * The entire build area of this world - this effectively caps what we can see + * to be within this block of land. Anything outside will be excluded. + */ + internal var buildArea: BuildArea = BuildArea.INVALID + + internal fun onAlloc(worldId: Int) { + this.worldId = worldId + this.renderCoord = CoordGrid.INVALID + this.buildArea = BuildArea.INVALID + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/util/CellOpcodes.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/util/CellOpcodes.kt new file mode 100644 index 000000000..142f2d3a2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/util/CellOpcodes.kt @@ -0,0 +1,147 @@ +@file:Suppress("DuplicatedCode") + +package net.rsprot.protocol.game.outgoing.info.playerinfo.util + +/** + * An object containing all the opcodes used for transmission in player info protocol. + * These opcodes are used as a means to compress short-distance movement deltas sent to the client. + */ +internal object CellOpcodes { + // Single cell opcodes + private const val SW: Int = 0 + private const val S: Int = 1 + private const val SE: Int = 2 + private const val W: Int = 3 + private const val E: Int = 4 + private const val NW: Int = 5 + private const val N: Int = 6 + private const val NE: Int = 7 + + // Dual cell opcodes (each letter stands for 1 cell in that direction, SSWW = 2 tiles south, 2 tiles west) + private const val SSWW: Int = 0 + private const val SSW: Int = 1 + private const val SS: Int = 2 + private const val SSE: Int = 3 + private const val SSEE: Int = 4 + private const val SWW: Int = 5 + private const val SEE: Int = 6 + private const val WW: Int = 7 + private const val EE: Int = 8 + private const val NWW: Int = 9 + private const val NEE: Int = 10 + private const val NNWW: Int = 11 + private const val NNW: Int = 12 + private const val NN: Int = 13 + private const val NNE: Int = 14 + private const val NNEE: Int = 15 + + /** + * Single cell movement opcodes in a len-16 array. + */ + private val singleCellMovementOpcodes: IntArray = buildSingleCellMovementOpcodes() + + /** + * Dual cell movement opcodes in a len-64 array. + */ + private val dualCellMovementOpcodes: IntArray = buildDualCellMovementOpcodes() + + /** + * Gets the index for a single cell movement opcode based on the deltas, + * where the deltas are expected to be either -1, 0 or 1. + * @param deltaX the x-coordinate delta + * @param deltaZ the z-coordinate delta + * @return the index of the single cell opcode stored in [singleCellMovementOpcodes] + */ + private fun singleCellIndex( + deltaX: Int, + deltaZ: Int, + ): Int = (deltaX + 1).or((deltaZ + 1) shl 2) + + /** + * Gets the index of the dual cell movement opcode based on the deltas, + * where the deltas are expected to be in range of -2..2. + * @param deltaX the x-coordinate delta + * @param deltaZ the z-coordinate delta + * @return the index of the dual cell opcode stored in [dualCellMovementOpcodes] + */ + private fun dualCellIndex( + deltaX: Int, + deltaZ: Int, + ): Int = (deltaX + 2).or((deltaZ + 2) shl 3) + + /** + * Gets the single cell movement opcode value for the provided deltas. + * @param deltaX the x-coordinate delta + * @param deltaZ the z-coordinate delta + * @return the movement opcode as expected by the client, or -1 if the deltas are in range, + * but the deltas do not result in any movement. + * @throws ArrayIndexOutOfBoundsException if either of the deltas is not in range of -1..1. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + internal fun singleCellMovementOpcode( + deltaX: Int, + deltaZ: Int, + ): Int = singleCellMovementOpcodes[singleCellIndex(deltaX, deltaZ)] + + /** + * Gets the dual cell movement opcode value for the provided deltas. + * @param deltaX the x-coordinate delta + * @param deltaZ the z-coordinate delta + * @return the movement opcode as expected by the client, or -1 if the deltas are in range, + * but the deltas do not result in any movement. + * @throws ArrayIndexOutOfBoundsException if either of the deltas is not in range of -2..2. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + internal fun dualCellMovementOpcode( + deltaX: Int, + deltaZ: Int, + ): Int = dualCellMovementOpcodes[dualCellIndex(deltaX, deltaZ)] + + /** + * Builds a simple bitpacked array of the bit codes for all the possible deltas. + * This is simply a more efficient variant of the normal if-else chain of checking + * the different delta combinations, as we are skipping a lot of branch prediction. + * In a benchmark, the results showed ~603% increased performance. + */ + private fun buildSingleCellMovementOpcodes(): IntArray { + val array = IntArray(16) { -1 } + array[singleCellIndex(-1, -1)] = SW + array[singleCellIndex(0, -1)] = S + array[singleCellIndex(1, -1)] = SE + array[singleCellIndex(-1, 0)] = W + array[singleCellIndex(1, 0)] = E + array[singleCellIndex(-1, 1)] = NW + array[singleCellIndex(0, 1)] = N + array[singleCellIndex(1, 1)] = NE + return array + } + + /** + * Similarly to [buildSingleCellMovementOpcodes], this is significantly more efficient + * than chained if-else statements. + * In this case, as there are more branches, the benchmark showed a 891% performance increase. + * It is worth noting that the benchmark in question also included reading deltas from + * a pre-computed array and thus, the real gain would actually be even more significant if only + * comparing the raw time taken by reading the opcode alone. + */ + private fun buildDualCellMovementOpcodes(): IntArray { + val array = IntArray(64) { -1 } + array[dualCellIndex(-2, -2)] = SSWW + array[dualCellIndex(-1, -2)] = SSW + array[dualCellIndex(0, -2)] = SS + array[dualCellIndex(1, -2)] = SSE + array[dualCellIndex(2, -2)] = SSEE + array[dualCellIndex(-2, -1)] = SWW + array[dualCellIndex(2, -1)] = SEE + array[dualCellIndex(-2, 0)] = WW + array[dualCellIndex(2, 0)] = EE + array[dualCellIndex(-2, 1)] = NWW + array[dualCellIndex(2, 1)] = NEE + array[dualCellIndex(-2, 2)] = NNWW + array[dualCellIndex(-1, 2)] = NNW + array[dualCellIndex(0, 2)] = NN + array[dualCellIndex(1, 2)] = NNE + array[dualCellIndex(2, 2)] = NNEE + return array + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/util/LowResolutionPosition.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/util/LowResolutionPosition.kt new file mode 100644 index 000000000..30d01c60b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/playerinfo/util/LowResolutionPosition.kt @@ -0,0 +1,32 @@ +package net.rsprot.protocol.game.outgoing.info.playerinfo.util + +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid + +/** + * A value class for holding low resolution position information in a primitive int. + * @param packed the bitpacked representation of the low resolution position + */ +@JvmInline +internal value class LowResolutionPosition( + val packed: Int, +) { + val x: Int + get() = packed ushr 8 and 0xFF + val z: Int + get() = packed and 0xFF + val level: Int + get() = packed ushr 16 and 0x3 +} + +/** + * A fake constructor for the low resolution position value class, as the JVM signature + * matches that of the primary constructor. + * @param coordGrid the absolute coordinate to turn into a low resolution position. + * @return the low resolution representation of the given [coordGrid] + */ +internal fun LowResolutionPosition(coordGrid: CoordGrid): LowResolutionPosition = + LowResolutionPosition( + (coordGrid.z ushr 13) + .or((coordGrid.x ushr 13) shl 8) + .or((coordGrid.level shl 16)), + ) diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/Avatar.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/Avatar.kt new file mode 100644 index 000000000..984cab65a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/Avatar.kt @@ -0,0 +1,10 @@ +package net.rsprot.protocol.game.outgoing.info.util + +public interface Avatar { + /** + * Handles any changes to be done to the avatar post its update. + * This will clean up any extended info blocks and update the last coordinate to + * match up with the current (set earlier in the cycle). + */ + public fun postUpdate() +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/BuildArea.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/BuildArea.kt new file mode 100644 index 000000000..1424865fa --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/BuildArea.kt @@ -0,0 +1,124 @@ +package net.rsprot.protocol.game.outgoing.info.util + +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInBuildArea + +/** + * The build area class is responsible for tracking the currently-rendered + * map of a given player. Everything sent via world entity info is tracked + * as relative to the build area. + * @property zoneX the south-western zone x coordinate of the build area + * @property zoneZ the south-western zone z coordinate of the build area + * @property widthInZones the build area width in zones (typically 13, meaning 104 tiles) + * @property heightInZones the build area height in zones (typically 13, meaning 104 tiles) + */ +@Suppress("MemberVisibilityCanBePrivate") +@JvmInline +public value class BuildArea private constructor( + private val packed: Long, +) { + public constructor( + zoneX: Int, + zoneZ: Int, + widthInZones: Int = DEFAULT_BUILD_AREA_SIZE, + heightInZones: Int = DEFAULT_BUILD_AREA_SIZE, + ) : this( + (zoneX and 0xFFFF) + .toLong() + .or((zoneZ and 0xFFFF).toLong() shl 16) + .or((widthInZones and 0xFFFF).toLong() shl 32) + .or((heightInZones and 0xFFFF).toLong() shl 48), + ) { + require(zoneX in 0..<2048) { + "ZoneX must be in range of 0..<2048: $zoneX" + } + require(zoneZ in 0..<2048) { + "ZoneZ must be in range of 0..<2048: $zoneZ" + } + require(widthInZones >= 0) { + "Width in zones cannot be negative: $widthInZones" + } + require(heightInZones >= 0) { + "Height in zones cannot be negative: $heightInZones" + } + } + + public val zoneX: Int + get() = (packed and 0xFFFF).toInt() + public val zoneZ: Int + get() = (packed ushr 16 and 0xFFFF).toInt() + public val widthInZones: Int + get() = (packed ushr 32 and 0xFFFF).toInt() + public val heightInZones: Int + get() = (packed ushr 48 and 0xFFFF).toInt() + + /** + * Localizes a specific absolute coordinate to be relative to the south-western + * corner of this build area. + * @param coordGrid the coordinate to localize. + * @return a coordinate local to the build area. + */ + internal fun localize(coordGrid: CoordGrid): CoordInBuildArea { + val (_, x, z) = coordGrid + val buildAreaX = zoneX shl 3 + val buildAreaZ = zoneZ shl 3 + val dx = x - buildAreaX + val dz = z - buildAreaZ + val maxDeltaX = this.widthInZones shl 3 + val maxDeltaZ = this.heightInZones shl 3 + check(dx in 0..= (maxDeltaX - 2)) { + return false + } + val maxDeltaZ = this.heightInZones shl 3 + return dz < (maxDeltaZ - 2) + } + + public companion object { + /** + * The default build area size in zones. + */ + public const val DEFAULT_BUILD_AREA_SIZE: Int = 104 ushr 3 + + /** + * An uninitialized build area. + */ + public val INVALID: BuildArea = BuildArea(-1) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/ReferencePooledObject.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/ReferencePooledObject.kt new file mode 100644 index 000000000..01cc120f8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/util/ReferencePooledObject.kt @@ -0,0 +1,34 @@ +package net.rsprot.protocol.game.outgoing.info.util + +import net.rsprot.protocol.common.client.OldSchoolClientType + +/** + * An interface used for info protocols for the purpose of re-using an object. + * This is handy in cases where the objects themselves are heavy, and deallocating and reallocating + * the object itself might become too costly, so we utilize a soft reference pool to retrieve older + * objects and re-use them in the future. + */ +public interface ReferencePooledObject { + /** + * Invoked whenever a previously pooled object is re-allocated. + * This function will be responsible for restoring state to be equivalent to newly + * instantiated object. + * @param index the index of the new element to allocate. + * @param oldSchoolClientType the client type used by the new owner. + */ + public fun onAlloc( + index: Int, + oldSchoolClientType: OldSchoolClientType, + newInstance: Boolean, + ) + + /** + * Invoked whenever a pooled object is no longer in use. + * This function is primarily used to clear out any sensitive information or potential memory leaks + * regarding byte buffers. This function should not fully reset objects, particularly primitives, + * as there is a chance a given pooled object never gets re-utilized and the garbage collector + * ends up picking it up. In such cases, it is more beneficial to do the resetting of properties + * during the [onAlloc], to ensure no work is 'wasted'. + */ + public fun onDealloc() +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/DefaultProtocolWorker.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/DefaultProtocolWorker.kt new file mode 100644 index 000000000..1286ff705 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/DefaultProtocolWorker.kt @@ -0,0 +1,35 @@ +package net.rsprot.protocol.game.outgoing.info.worker + +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.ForkJoinPool + +/** + * The default protocol worker, utilizing the calling thread + * if there are less than [asynchronousThreshold] callables to execute. + * Otherwise, utilizes the [executorService] to process the callables in parallel. + */ +public class DefaultProtocolWorker( + private val asynchronousThreshold: Int, + private val executorService: ExecutorService, +) : ProtocolWorker { + /** + * A default implementation that switches to parallel processing using [ForkJoinPool] + * if there are at least `coreCount * 4` callables to execute. + * Otherwise, utilizes the calling thread. + */ + public constructor() : this( + Runtime.getRuntime().availableProcessors() * 4, + ForkJoinPool.commonPool(), + ) + + override fun execute(callables: List>) { + if (callables.size < asynchronousThreshold) { + for (callable in callables) { + callable.call() + } + } else { + executorService.invokeAll(callables) + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/ForkJoinMultiThreadProtocolWorker.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/ForkJoinMultiThreadProtocolWorker.kt new file mode 100644 index 000000000..e6d4179a0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/ForkJoinMultiThreadProtocolWorker.kt @@ -0,0 +1,14 @@ +package net.rsprot.protocol.game.outgoing.info.worker + +import java.util.concurrent.Callable +import java.util.concurrent.ForkJoinPool + +/** + * A simple single-threaded info worker, executing all the callables using [ForkJoinPool]. + * The pool will be used even if there are very few callables to execute. + */ +public class ForkJoinMultiThreadProtocolWorker : ProtocolWorker { + override fun execute(callables: List>) { + ForkJoinPool.commonPool().invokeAll(callables) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/ProtocolWorker.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/ProtocolWorker.kt new file mode 100644 index 000000000..779eef945 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/ProtocolWorker.kt @@ -0,0 +1,17 @@ +package net.rsprot.protocol.game.outgoing.info.worker + +import java.util.concurrent.Callable + +/** + * Provides an API to processing info protocols. + */ +public interface ProtocolWorker { + /** + * Executes the [callables] collection as defined by the given worker's behavior. + * The callables may be executed asynchronously and are guaranteed to be thread-safe. + * It should be noted that _all_ the callables must be called upon, or the protocol breaks. + * + * @param callables the list of callables that must be executed. + */ + public fun execute(callables: List>) +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/SingleThreadProtocolWorker.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/SingleThreadProtocolWorker.kt new file mode 100644 index 000000000..32b96331c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worker/SingleThreadProtocolWorker.kt @@ -0,0 +1,14 @@ +package net.rsprot.protocol.game.outgoing.info.worker + +import java.util.concurrent.Callable + +/** + * A simple single-threaded info worker, executing all the callables in order on the calling thread. + */ +public class SingleThreadProtocolWorker : ProtocolWorker { + override fun execute(callables: List>) { + for (callable in callables) { + callable.call() + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatar.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatar.kt new file mode 100644 index 000000000..66de0a43c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatar.kt @@ -0,0 +1,109 @@ +package net.rsprot.protocol.game.outgoing.info.worldentityinfo + +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.extensions.p1 +import net.rsprot.buffer.extensions.p2 +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.outgoing.info.util.Avatar + +/** + * A world entity avatar represents a dynamic world entity as a single unit. + * + * Movement speed table: + * ```kt + * | Id | Speed (Tiles/Cycle) | + * |:--:|:-------------------:| + * | -1 | Instantaneous | + * | 0 | 0.5 | + * | 1 | 1.0 | + * | 2 | 1.5 | + * | 3 | 2.0 | + * | 4 | 2.5 | + * | 5 | 3.0 | + * | 6 | 3.5 | + * | 7 | 4.0 | + * ``` + * + * @property allocator the byte buffer allocator to be used for the high resolution + * movement buffer of this world entity. + * @property index the index of this world entity. + * @property sizeX the width of the world entity in zones. + * @property sizeZ the height of the world entity in zones. + * @property currentCoord the coordinate that this world entity is being rendered at. + * @property angle the current angle of this world entity. + * @property moveSpeed the current movement speed of this world entity. See the table above + * for a full description of every possible move speed. + * @property lastCoord the last known coordinate of the world entity by the client. + * @property highResolutionBuffer the buffer which contains the pre-computed high resolution + * movement of this avatar. + */ +public class WorldEntityAvatar( + internal val allocator: ByteBufAllocator, + internal var index: Int, + internal var sizeX: Int, + internal var sizeZ: Int, + internal var currentCoord: CoordGrid = CoordGrid.INVALID, + internal var angle: Int, +) : Avatar { + private var moveSpeed: Int = -1 + internal var lastCoord: CoordGrid = currentCoord + + internal var highResolutionBuffer: ByteBuf? = null + + /** + * Precomputes the high resolution buffer of this world entity. + */ + internal fun precompute() { + val buffer = allocator.buffer(MAX_HIGH_RES_BUF_SIZE, MAX_HIGH_RES_BUF_SIZE) + this.highResolutionBuffer = buffer + val dx = currentCoord.x - lastCoord.x + val dz = currentCoord.z - lastCoord.z + buffer.p1(currentCoord.level) + buffer.p1(dx) + buffer.p1(dz) + buffer.p2(angle) + buffer.p1(moveSpeed) + } + + /** + * Updates the current coordinate of this world entity, along with a move speed + * to reach that coordinate, if applicable. + * @param level the current level of this world entity. + * @param x the current absolute x coordinate of this world entity. + * @param z the current absolute z coordinate of this world entity. + * @param moveSpeed the movement speed of this world entity. See the table within + * the main class documentation for the possible move speed values. + */ + @Throws(IllegalArgumentException::class) + public fun updateCoord( + level: Int, + x: Int, + z: Int, + moveSpeed: Int, + ) { + this.currentCoord = CoordGrid(level, x, z) + this.moveSpeed = moveSpeed + } + + /** + * Updates the current angle of this world entity. + * It should be noted that the client is only made to rotate by a maximum of 22.5 degrees (128/2048 units) + * per game cycle, so it may take multiple seconds for it to finish the turn. + */ + public fun updateAngle(angle: Int) { + this.angle = angle + } + + override fun postUpdate() { + this.lastCoord = this.currentCoord + this.highResolutionBuffer?.release() + } + + private companion object { + /** + * The maximum buffer size for the high resolution precomputed buffer. + */ + private const val MAX_HIGH_RES_BUF_SIZE: Int = 5 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarExceptionHandler.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarExceptionHandler.kt new file mode 100644 index 000000000..93447eb60 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarExceptionHandler.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.game.outgoing.info.worldentityinfo + +import java.lang.Exception + +public fun interface WorldEntityAvatarExceptionHandler { + /** + * This function is triggered whenever there's an exception caught during world entity + * avatar processing. + * @param index the index of the world entity that had an exception during its processing. + * @param exception the exception that was caught during a world entity's avatar processing + */ + public fun exceptionCaught( + index: Int, + exception: Exception, + ) +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarFactory.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarFactory.kt new file mode 100644 index 000000000..3d4c06bb9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarFactory.kt @@ -0,0 +1,59 @@ +package net.rsprot.protocol.game.outgoing.info.worldentityinfo + +import io.netty.buffer.ByteBufAllocator + +/** + * An avatar factory for world entities. + * This class will be responsible for allocating and releasing world entity avatars, + * allowing them to be pooled and re-used, if needed. + * @property avatarRepository the repository keeping track of existing and past world + * entity avatars. + */ +public class WorldEntityAvatarFactory( + allocator: ByteBufAllocator, +) { + public val avatarRepository: WorldEntityAvatarRepository = + WorldEntityAvatarRepository( + allocator, + ) + + /** + * Allocates a new world entity with the provided arguments. + * @param index the index of the world entity + * @param sizeX the width of the world entity in zones (8 tiles/zone) + * @param sizeZ the height of the world entity in zones (8 tiles/zone) + * @param x the absolute x coordinate of the world entity where + * it is being portrayed in the root world. + * @param z the absolute z coordinate of the world entity where + * it is being portrayed in the root world. + * @param level the height level of the world entity. + * @return either a new world entity avatar, or a pooled one that has been + * updated to contain the provided params. + */ + public fun alloc( + index: Int, + sizeX: Int, + sizeZ: Int, + x: Int, + z: Int, + level: Int, + angle: Int, + ): WorldEntityAvatar = + avatarRepository.getOrAlloc( + index, + sizeX, + sizeZ, + x, + z, + level, + angle, + ) + + /** + * Releases a world entity avatar back into the pool, allowing it to be re-used in the future. + * @param avatar the world entity avatar to be released. + */ + public fun release(avatar: WorldEntityAvatar) { + avatarRepository.release(avatar) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarRepository.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarRepository.kt new file mode 100644 index 000000000..c1c654f4f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityAvatarRepository.kt @@ -0,0 +1,115 @@ +package net.rsprot.protocol.game.outgoing.info.worldentityinfo + +import io.netty.buffer.ByteBufAllocator +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import java.lang.ref.ReferenceQueue +import java.lang.ref.SoftReference + +/** + * An avatar repository for world entities, keeping track of every current avatar, + * as well as any avatars that were previously used but now released. + * @property allocator an allocator for the world entity avatars, to be used for + * precomputed high resolution blocks. + * @property elements the array of existing world entity avatars, currently in use. + * @property queue the soft reference queue of world avatars that were previously in use. + * As a soft reference queue, it will hold on-to the unused references until the JVM + * absolutely needs the memory - before that, these can be reused, making it a perfect + * use case for the pooling mechanism. + * @property releasedAvatarQueue the avatars that were released within this cycle. + * These avatars are initially put into a different structure as we cannot immediately + * release them - they could be picked up by something else the same cycle, which could + * lead to some weird bugs occurring. Instead, we wait for one cycle to pass before + * pushing them to the queue to be re-usable. This ensures no one is relying on this + * same instance still. + */ +public class WorldEntityAvatarRepository internal constructor( + private val allocator: ByteBufAllocator, +) { + private val elements: Array = arrayOfNulls(AVATAR_CAPACITY) + private val queue: ReferenceQueue = ReferenceQueue() + private val releasedAvatarQueue: ArrayDeque = ArrayDeque() + + /** + * Gets a world entity at the provided [idx], or null if it doesn't exist. + * @throws ArrayIndexOutOfBoundsException if the [idx] is < 0, or >= [AVATAR_CAPACITY]. + */ + @Throws(ArrayIndexOutOfBoundsException::class) + public fun getOrNull(idx: Int): WorldEntityAvatar? = elements[idx] + + /** + * Gets an existing world entity avatar from the queue if one is ready, or constructs + * a new avatar if not. + * @param index the index of the world entity + * @param sizeX the width of the world entity in zones (8 tiles/zone) + * @param sizeZ the height of the world entity in zones (8 tiles/zone) + * @param x the absolute x coordinate of the world entity where + * it is being portrayed in the root world. + * @param z the absolute z coordinate of the world entity where + * it is being portrayed in the root world. + * @param level the height level of the world entity. + * @return either a new world entity avatar, or a pooled one that has been + * updated to contain the provided params. + */ + public fun getOrAlloc( + index: Int, + sizeX: Int, + sizeZ: Int, + x: Int, + z: Int, + level: Int, + angle: Int, + ): WorldEntityAvatar { + val existing = queue.poll()?.get() + if (existing != null) { + existing.index = index + existing.sizeX = sizeX + existing.sizeZ = sizeZ + existing.currentCoord = CoordGrid(level, x, z) + existing.lastCoord = existing.currentCoord + existing.angle = angle + elements[index] = existing + return existing + } + val avatar = + WorldEntityAvatar( + allocator, + index, + sizeX, + sizeZ, + CoordGrid(level, x, z), + angle, + ) + elements[index] = avatar + return avatar + } + + /** + * Releases avatar back into the pool for it to be used later in the future, if possible. + * @param avatar the avatar to release. + */ + public fun release(avatar: WorldEntityAvatar) { + this.elements[avatar.index] = null + releasedAvatarQueue += avatar + } + + /** + * Transfers the recently released avatars over to the pool, so they can be re-used. + */ + internal fun transferAvatars() { + if (releasedAvatarQueue.isEmpty()) { + return + } + while (releasedAvatarQueue.isNotEmpty()) { + val avatar = releasedAvatarQueue.removeFirst() + val reference = SoftReference(avatar, queue) + reference.enqueue() + } + } + + internal companion object { + /** + * The maximum number of world entity avatars. + */ + internal const val AVATAR_CAPACITY = 2048 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityIndexSupplier.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityIndexSupplier.kt new file mode 100644 index 000000000..5f42d53b0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityIndexSupplier.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.game.outgoing.info.worldentityinfo + +/** + * An index supplier for world entities. + */ +public fun interface WorldEntityIndexSupplier { + /** + * Supplies an iterator of world entity indices that will be added to high resolution, + * if they are not already in there. Furthermore, a secondary build-area check is performed + * before such world entities may be added, as a safety precaution. + * @param localPlayerIndex the index of the local player for whom the indices are + * being supplied. + * @param level the height level of the coordinate where to look up world entities. + * @param x the absolute x coordinate around which to find world entities. + * @param z the absolute z coordinate around which to find world entities. + * @param viewDistance the current view distance in tiles. + */ + public fun supply( + localPlayerIndex: Int, + level: Int, + x: Int, + z: Int, + viewDistance: Int, + ): Iterator +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfo.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfo.kt new file mode 100644 index 000000000..78370ea5b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfo.kt @@ -0,0 +1,476 @@ +package net.rsprot.protocol.game.outgoing.info.worldentityinfo + +import io.netty.buffer.ByteBuf +import io.netty.buffer.ByteBufAllocator +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.common.game.outgoing.info.CoordGrid +import net.rsprot.protocol.game.outgoing.info.exceptions.InfoProcessException +import net.rsprot.protocol.game.outgoing.info.util.BuildArea +import net.rsprot.protocol.game.outgoing.info.util.ReferencePooledObject + +/** + * The world entity info class tracks everything about the world entities that + * are near this player. + * @property localIndex the index of the local player that owns this world entity info. + * @property allocator the byte buffer allocator used to build the buffer for the packet. + * @property oldSchoolClientType the client type on which the player has logged in. + * @property avatarRepository the avatar repository keeping track of every known + * world entity in the root world. + * @property indexSupplier the implementation returning all the indices of the world + * entities near the local player. + * @property renderDistance the render distance in tiles, effectively how far to render + * world entities from the local player (or the camera pov) + * @property currentWorldEntityId the id of the world entity on which the local player + * currently resides. + * @property currentCoord the current real coordinate of the local player. + * @property buildArea the current build area of the player, this is the root base + * map that's being rendered to the player. + * @property highResolutionIndicesCount the number of high resolution world entity avatars. + * @property highResolutionIndices the indices of all the high resolution avatars currently + * being tracked. + * @property temporaryHighResolutionIndices a temporary array of high resolution avatar indices, + * allowing for more efficient defragmentation of the indices when indices get removed + * from the middle of the array. + * @property allWorldEntities the indices of all the world entities currently in high resolution, + * provided in a list format as the server must know everything currently rendered, so it can + * perform accurate updates to player info, npc info and zones. + * @property addedWorldEntities the indices of all the world entities that were added within + * this cycle after it has been computed. The server can use this information to build the + * REBUILD_WORLDENTITY packet, which is used to actually render the instance itself. + * @property removedWorldEntities the indices of all the world entities that were removed + * within this cycle after it has been computed. These are only removed from the high resolution, + * and not necessarily the world itself. This allows the server to inform NPC and player info + * protocol to deallocate the instances. + * @property buffer the buffer for this world entity info packet. + * @property builtIntoPacket whether the buffer has been built into a packet, + * which means the responsibility of releasing the buffer falls to the server. + * If false, the protocol will release the buffer when this instance is being + * deallocated. + * @property exception the exception that was caught during the computations + * of this world entity info, if any. This will be thrown as the [toPacket] + * function is called, allowing the server to handle it from the correct + * perspective of the caller, as the protocol itself is computed in for the + * entire server in one go. + * @property renderCoord if the player is currently on a world entity, this marks the coordinate + * at which the world entity is being rendered on the root world. This allows the protocol + * to still see other world entities nearby despite the player being in an instance. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class WorldEntityInfo internal constructor( + internal var localIndex: Int, + internal val allocator: ByteBufAllocator, + private var oldSchoolClientType: OldSchoolClientType, + private val avatarRepository: WorldEntityAvatarRepository, + private val indexSupplier: WorldEntityIndexSupplier, +) : ReferencePooledObject { + private var renderDistance: Int = DEFAULT_RENDER_DISTANCE + private var currentWorldEntityId: Int = ROOT_WORLD + private var currentCoord: CoordGrid = CoordGrid.INVALID + private var buildArea: BuildArea = BuildArea.INVALID + private var highResolutionIndicesCount: Int = 0 + private var highResolutionIndices: ShortArray = + ShortArray(WorldEntityProtocol.CAPACITY) { + INDEX_TERMINATOR + } + private var temporaryHighResolutionIndices: ShortArray = + ShortArray(WorldEntityProtocol.CAPACITY) { + INDEX_TERMINATOR + } + private val allWorldEntities = ArrayList() + private val addedWorldEntities = ArrayList() + private val removedWorldEntities = ArrayList() + private var buffer: ByteBuf? = null + internal var exception: Exception? = null + private var builtIntoPacket: Boolean = false + private var renderCoord: CoordGrid = CoordGrid.INVALID + + /** + * Updates the render distance for this player, potentially allowing + * the world entities to be rendered from further away. All instances + * will however still be constrained to within the build area, in their + * entirety - if they cannot fulfill that constraint, they will not be + * put into high resolution view. + * @param distance the distance in tiles how far the world entities should + * be rendered from the player (or the camera's POV) + */ + public fun updateRenderDistance(distance: Int) { + this.renderDistance = distance + } + + /** + * Updates the build area for this player. This should always perfectly correspond to + * the actual build area that is sent via REBUILD_NORMAL or REBUILD_REGION packets. + * @param buildArea the build area in which everything is rendered. + */ + public fun updateBuildArea(buildArea: BuildArea) { + this.buildArea = buildArea + } + + /** + * Gets a list of all the world entity indices that are currently in high resolution, + * allowing for correct functionality for player and npc infos, as well as zone updates. + * @return a list of indices of the world entities currently in high resolution. + */ + public fun getAllWorldEntityIndices(): List = this.allWorldEntities + + /** + * Gets the indices of all the world entities that were added to high resolution in this cycle, + * allowing the server to allocate new player and npc info instances, and sync the state of the + * zones in those world entities. + * @return a list of all the world entity indices added to the high resolution view in this + * cycle. + */ + public fun getAddedWorldEntityIndices(): List = this.addedWorldEntities + + /** + * Gets the indices of all the world entities that were removed from the high resolution in + * this cycle, allowing the server to destroy the player and npc info instances corresponding + * to them, and to clear the zones that were being tracked due to it. + * @return a list of all the indices of the world entities that were removed from the high + * resolution view this cycle. + */ + public fun getRemovedWorldEntityIndices(): List = this.removedWorldEntities + + /** + * Updates the current real absolute coordinate of the local player in the world. + * @param worldId the id of the world in which the player currently resides, + * if they are inside a world entity, this would be that index. If they are in the + * root world, this should be [ROOT_WORLD]. + * @param level the current height level of the player + * @param x the current absolute x coordinate of the player + * @param z the current absolute z coordinate of the player + */ + public fun updateCoord( + worldId: Int, + level: Int, + x: Int, + z: Int, + ) { + this.currentWorldEntityId = worldId + this.currentCoord = CoordGrid(level, x, z) + } + + /** + * Sets the render coordinate of this player. This function should only be used + * when the player is inside one of the world entities. The value should correspond + * to the coordinate at which the world entity in which the player resides, in the + * root world - not in the instance land. + * @param level the level of the render coordinate + * @param x the absolute x value of the render coordinate + * @param z the absolute z value of the render coordinate + */ + public fun setRenderCoord( + level: Int, + x: Int, + z: Int, + ) { + this.renderCoord = CoordGrid(level, x, z) + } + + /** + * Resets the render coordinate. This function should be called when the player + * leaves one of the dynamic world entities and moves back onto the root world. + */ + public fun resetRenderCoord() { + this.renderCoord = CoordGrid.INVALID + } + + /** + * Returns the backing byte buffer for this world entity info instance. + * @return the byte buffer instance into which all the world entity info + * is being written. + * @throws IllegalStateException if the buffer has not yet been allocated. + */ + @Throws(IllegalStateException::class) + public fun backingBuffer(): ByteBuf = checkNotNull(buffer) + + /** + * Turns the previously-computed world entity info into a packet instance + * which can be flushed to the client. + * If an exception was caught during the computation of this world entity info, + * it will be thrown in here, allowing the server to properly handle exceptions + * in a per-player perspective. + * @return the world entity packet instance. + */ + public fun toPacket(): WorldEntityInfoPacket { + val exception = this.exception + if (exception != null) { + throw InfoProcessException( + "Exception occurred during player info processing for index $localIndex", + exception, + ) + } + builtIntoPacket = true + return WorldEntityInfoPacket(backingBuffer()) + } + + /** + * Allocates a new buffer for the next world entity info packet. + * Furthermore, resets some temporary properties from the last cycle. + * @return the buffer into which everything is written about this packet. + */ + private fun allocBuffer(): ByteBuf { + // If a given player's packet was never sent out, we need to release the old buffer + if (!builtIntoPacket) { + val oldBuf = buffer + if (oldBuf != null && oldBuf.refCnt() > 0) { + oldBuf.release() + } + } + // Acquire a new buffer with each cycle, in case the previous one isn't fully written out yet + val buffer = allocator.buffer(BUF_CAPACITY, BUF_CAPACITY) + this.buffer = buffer + this.builtIntoPacket = false + this.addedWorldEntities.clear() + this.removedWorldEntities.clear() + return buffer + } + + /** + * Defragments the indices of the high resolution world entities. + * This is done only if world entities were removed in the middle of + * the array. + */ + private fun defragmentIndices() { + var count = 0 + for (i in highResolutionIndices.indices) { + if (count >= highResolutionIndicesCount) { + break + } + val index = highResolutionIndices[i] + if (index != INDEX_TERMINATOR) { + temporaryHighResolutionIndices[count++] = index + } + } + val uncompressed = this.highResolutionIndices + this.highResolutionIndices = this.temporaryHighResolutionIndices + this.temporaryHighResolutionIndices = uncompressed + } + + /** + * Performs the full world entity info update for the given player. + */ + internal fun updateWorldEntities() { + val buffer = allocBuffer().toJagByteBuf() + val fragmented = processHighResolution(buffer) + if (fragmented) { + defragmentIndices() + } + processLowResolution(buffer) + } + + /** + * Processes all the currently tracked high resolution world entities. + * @param buffer the buffer into which to write the high resolution updates. + * @return whether any world entities were removed from high resolution, meaning + * a defragmentation process is necessary. + */ + private fun processHighResolution(buffer: JagByteBuf): Boolean { + val count = this.highResolutionIndicesCount + buffer.p1(count) + for (i in 0..= MAX_HIGH_RES_COUNT) { + return + } + val currentWorld = this.currentWorldEntityId + val (level, x, z) = + if (currentWorld == ROOT_WORLD) { + this.currentCoord + } else { + val worldEntity = checkNotNull(avatarRepository.getOrNull(currentWorld)) + // Perhaps center coord instead? + worldEntity.currentCoord + } + val entities = + indexSupplier.supply( + this.localIndex, + level, + x, + z, + this.renderDistance, + ) + while (entities.hasNext()) { + val index = entities.next() and 0xFFFF + if (index == 0xFFFF || isHighResolution(index)) { + continue + } + if (this.highResolutionIndicesCount >= MAX_HIGH_RES_COUNT) { + break + } + val avatar = avatarRepository.getOrNull(index) ?: continue + // Secondary build-area distance check + if (!inRange(avatar)) { + continue + } + addedWorldEntities += index + allWorldEntities += index + val i = highResolutionIndicesCount++ + highResolutionIndices[i] = index.toShort() + buffer.p2(avatar.index) + buffer.p1(avatar.sizeX) + buffer.p1(avatar.sizeZ) + val buildAreaCoord = buildArea.localize(avatar.currentCoord) + buffer.p1(buildAreaCoord.xInBuildArea) + buffer.p1(buildAreaCoord.zInBuildArea) + buffer.p2(avatar.angle) + // The zero is a currently unassigned property on all clients + buffer.p2(0) + } + } + + /** + * Checks if the world entity at [index] is in high resolution indices. + * @return whether the world entity is within high resolution. + */ + private fun isHighResolution(index: Int): Boolean { + for (i in 0.. 0) { + buffer.release(buffer.refCnt()) + } + this.builtIntoPacket = false + this.buffer = null + this.exception = null + } + this.highResolutionIndicesCount = 0 + this.highResolutionIndices.fill(0) + this.temporaryHighResolutionIndices.fill(0) + this.allWorldEntities.clear() + this.addedWorldEntities.clear() + this.removedWorldEntities.clear() + } + + override fun onDealloc() { + if (!builtIntoPacket) { + val buffer = this.buffer + if (buffer != null && buffer.refCnt() > 0) { + buffer.release(buffer.refCnt()) + } + } + } + + private companion object { + /** + * The index value that marks a termination for high resolution indices. + */ + private const val INDEX_TERMINATOR: Short = -1 + + /** + * The maximum number of high resolution world entities that could be sent. + */ + private const val MAX_HIGH_RES_COUNT: Int = 255 + + /** + * The id of the root world. + */ + private const val ROOT_WORLD: Int = -1 + + /** + * The default render distance for world entities. + */ + private const val DEFAULT_RENDER_DISTANCE: Int = 15 + + /** + * The default capacity of the backing byte buffer into which all world info is written. + * The size here is calculated by taking the high res count byte + (max removals) + (max additions), + * creating the maximum theoretically possible buffer as a result of it. + * If the packet ever changes, this MUST be adjusted accordingly. + */ + private const val BUF_CAPACITY: Int = 1 + (MAX_HIGH_RES_COUNT * 1) + (MAX_HIGH_RES_COUNT * 10) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoExtensions.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoExtensions.kt new file mode 100644 index 000000000..1a6d2d542 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoExtensions.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.game.outgoing.info.worldentityinfo + +import net.rsprot.protocol.game.outgoing.info.util.BuildArea + +/** + * Checks if the [avatar] is inside the specified build area. + * @return whether this build area fully contains the [avatar]. + */ +internal operator fun BuildArea.contains(avatar: WorldEntityAvatar): Boolean { + val minBuildAreaZoneX = this.zoneX + val minBuildAreaZoneZ = this.zoneZ + val coord = avatar.currentCoord + val minAvatarZoneX = coord.x ushr 3 + val minAvatarZoneZ = coord.z ushr 3 + if (minAvatarZoneX < minBuildAreaZoneX || minAvatarZoneZ < minBuildAreaZoneZ) { + return false + } + val maxBuildAreaZoneX = minBuildAreaZoneX + this.widthInZones + val maxBuildAreaZoneZ = minBuildAreaZoneZ + this.heightInZones + val maxAvatarZoneX = minAvatarZoneX + avatar.sizeX + val maxAvatarZoneZ = minAvatarZoneZ + avatar.sizeZ + return !(maxAvatarZoneX > maxBuildAreaZoneX || maxAvatarZoneZ > maxBuildAreaZoneZ) +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoPacket.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoPacket.kt new file mode 100644 index 000000000..9d13d2104 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoPacket.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.game.outgoing.info.worldentityinfo + +import io.netty.buffer.ByteBuf +import io.netty.buffer.DefaultByteBufHolder +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * World entity info packet is used to update the coordinate, angle and move speed of all + * the world entities near a player. + */ +public class WorldEntityInfoPacket( + buffer: ByteBuf, +) : DefaultByteBufHolder(buffer), + OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun toString(): String = "WorldEntityInfoPacket()" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoRepository.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoRepository.kt new file mode 100644 index 000000000..b9add2bb5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityInfoRepository.kt @@ -0,0 +1,33 @@ +package net.rsprot.protocol.game.outgoing.info.worldentityinfo + +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.game.outgoing.info.InfoRepository + +/** + * A repository for world entity info instances. + * @param allocator the allocator used to return a new or re-used world entity info + * instance, based on the provided player's index and client type. + * @property elements the array of currently in used world entity info objects. + */ +internal class WorldEntityInfoRepository( + allocator: (index: Int, oldSchoolClientType: OldSchoolClientType) -> WorldEntityInfo, +) : InfoRepository(allocator) { + override val elements: Array = arrayOfNulls(WorldEntityProtocol.CAPACITY) + + override fun informDeallocation(idx: Int) { + // No-op + } + + override fun onDealloc(element: WorldEntityInfo) { + element.onDealloc() + } + + override fun onAlloc( + element: WorldEntityInfo, + idx: Int, + oldSchoolClientType: OldSchoolClientType, + newInstance: Boolean, + ) { + element.onAlloc(idx, oldSchoolClientType, newInstance) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityProtocol.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityProtocol.kt new file mode 100644 index 000000000..86dfaeb07 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/info/worldentityinfo/WorldEntityProtocol.kt @@ -0,0 +1,169 @@ +package net.rsprot.protocol.game.outgoing.info.worldentityinfo + +import com.github.michaelbull.logging.InlineLogger +import io.netty.buffer.ByteBufAllocator +import net.rsprot.protocol.common.client.OldSchoolClientType +import net.rsprot.protocol.game.outgoing.info.worker.DefaultProtocolWorker +import net.rsprot.protocol.game.outgoing.info.worker.ProtocolWorker +import java.util.concurrent.Callable + +/** + * The world entity protocol class will track everything related to world entities. + * @property allocator the byte buffer allocator used for world entity buffers. + * @property indexSupplier the index supplier implementation that yields indices of + * the world entities which are near a specific coordinate. + * @property exceptionHandler the exception handler which will be notified whenever + * there is an exception caught in world entity avatar pre-computation. + * @param factory the avatar factory used to provide instances of world entity avatars. + * @property worker the protocol worker that will be executing the computation + * of avatar and info buffers on the thread(s) specified by the implementation. + * @property avatarRepository the repository containing all the world entity avatars. + * @property worldEntityInfoRepository the repository containing all the currently + * in used world entity info instances. + */ +public class WorldEntityProtocol( + private val allocator: ByteBufAllocator, + private val indexSupplier: WorldEntityIndexSupplier, + private val exceptionHandler: WorldEntityAvatarExceptionHandler, + factory: WorldEntityAvatarFactory, + private val worker: ProtocolWorker = DefaultProtocolWorker(), +) { + private val avatarRepository = factory.avatarRepository + private val worldEntityInfoRepository: WorldEntityInfoRepository = + WorldEntityInfoRepository { localIndex, clientType -> + WorldEntityInfo( + localIndex, + allocator, + clientType, + factory.avatarRepository, + indexSupplier, + ) + } + + /** + * The list of [Callable] instances which perform the jobs for player info. + * This list itself is re-used throughout the lifespan of the application, + * but the [Callable] instances themselves are generated for every job. + */ + private val callables: MutableList> = ArrayList(CAPACITY) + + /** + * Allocates a new instance of world entity info. + * @param idx the index of the player who is requesting a world entity info. + * @param oldSchoolClientType the client type on which the player has logged in. + * @return an instance of the world entity info. + */ + public fun alloc( + idx: Int, + oldSchoolClientType: OldSchoolClientType, + ): WorldEntityInfo = worldEntityInfoRepository.alloc(idx, oldSchoolClientType) + + /** + * Deallocates the world entity info, allowing for it to be re-used in the future. + * @param info the world entity info to be deallocated. + */ + public fun dealloc(info: WorldEntityInfo) { + worldEntityInfoRepository.dealloc(info.localIndex) + } + + /** + * Updates all the world entities that exist in one go. + */ + public fun update() { + prepareHighResolutionBuffers() + updateInfos() + postUpdate() + } + + /** + * Pre-computes the high resolution block of world entities that exist. + */ + private fun prepareHighResolutionBuffers() { + for (i in 0.. Unit) { + for (i in 1.., + public val events: List, +) : OutgoingGameMessage { + public constructor( + topLevelInterface: Int, + subInterfaces: List, + events: List, + ) : this( + topLevelInterface.toUShort(), + subInterfaces, + events, + ) + + public val topLevelInterface: Int + get() = _topLevelInterface.toIntOrMinusOne() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfResync + + if (_topLevelInterface != other._topLevelInterface) return false + if (subInterfaces != other.subInterfaces) return false + if (events != other.events) return false + + return true + } + + override fun hashCode(): Int { + var result = _topLevelInterface.hashCode() + result = 31 * result + subInterfaces.hashCode() + result = 31 * result + events.hashCode() + return result + } + + override fun toString(): String = + "IfResync(" + + "topLevelInterface=$topLevelInterface, " + + "subInterfaces=$subInterfaces, " + + "events=$events" + + ")" + + /** + * Sub interface holds state about a sub interface to be opened. + * @property destinationCombinedId the bitpacked combination of [destinationInterfaceId] and [destinationComponentId]. + * @property destinationInterfaceId the destination interface on which the sub + * interface is being opened + * @property destinationComponentId the component on the destination interface + * on which the sub interface is being opened + * @property interfaceId the sub interface id + * @property type the type of the interface to be opened as (modal, overlay, client) + */ + @Suppress("MemberVisibilityCanBePrivate") + public class SubInterfaceMessage private constructor( + public val destinationCombinedId: Int, + private val _interfaceId: UShort, + private val _type: UByte, + ) { + public constructor( + destinationInterfaceId: Int, + destinationComponentId: Int, + interfaceId: Int, + type: Int, + ) : this( + CombinedId(destinationInterfaceId, destinationComponentId).combinedId, + interfaceId.toUShort(), + type.toUByte(), + ) + + public constructor( + destinationCombinedId: Int, + interfaceId: Int, + type: Int, + ) : this( + destinationCombinedId, + interfaceId.toUShort(), + type.toUByte(), + ) + + private val _destinationCombinedId: CombinedId + get() = CombinedId(destinationCombinedId) + public val destinationInterfaceId: Int + get() = _destinationCombinedId.interfaceId + public val destinationComponentId: Int + get() = _destinationCombinedId.componentId + public val interfaceId: Int + get() = _interfaceId.toIntOrMinusOne() + public val type: Int + get() = _type.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SubInterfaceMessage + + if (destinationCombinedId != other.destinationCombinedId) return false + if (_interfaceId != other._interfaceId) return false + if (_type != other._type) return false + + return true + } + + override fun hashCode(): Int { + var result = destinationCombinedId.hashCode() + result = 31 * result + _interfaceId.hashCode() + result = 31 * result + _type.hashCode() + return result + } + + override fun toString(): String = + "SubInterfaceMessage(" + + "destinationInterfaceId=$destinationInterfaceId, " + + "destinationComponentId=$destinationComponentId, " + + "interfaceId=$interfaceId, " + + "type=$type" + + ")" + } + + /** + * Interface events compress the IF_SETEVENTS packet's payload + * into a helper class. + * @property interfaceId the interface id on which to set the events + * @property componentId the component on that interface to set the events on + * @property start the start subcomponent id + * @property end the end subcomponent id (inclusive) + * @property events the bitpacked events + */ + public class InterfaceEventsMessage private constructor( + public val combinedId: Int, + private val _start: UShort, + private val _end: UShort, + public val events: Int, + ) { + public constructor( + interfaceId: Int, + componentId: Int, + start: Int, + end: Int, + events: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + start.toUShort(), + end.toUShort(), + events, + ) + + public constructor( + combinedId: Int, + start: Int, + end: Int, + events: Int, + ) : this( + combinedId, + start.toUShort(), + end.toUShort(), + events, + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val start: Int + get() = _start.toInt() + public val end: Int + get() = _end.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as InterfaceEventsMessage + + if (combinedId != other.combinedId) return false + if (_start != other._start) return false + if (_end != other._end) return false + if (events != other.events) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _start.hashCode() + result = 31 * result + _end.hashCode() + result = 31 * result + events + return result + } + + override fun toString(): String = + "InterfaceEvents(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "start=$start, " + + "end=$end, " + + "events=$events" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetAngle.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetAngle.kt new file mode 100644 index 000000000..7b083d9c8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetAngle.kt @@ -0,0 +1,94 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If set-angle is used to change the angle of a model on an interface component. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id on which the component resides + * @property componentId the component id on which the model resides + * @property angleX the new x model angle to set to, a value from 0 to 2047 (inclusive) + * @property angleY the new y model angle to set to, a value from 0 to 2047 (inclusive) + * @property zoom the zoom of the model, defaults to a value of 100 in the client. + * The greater the [zoom] value, the smaller the model will appear - it is inverted. + */ +public class IfSetAngle private constructor( + public val combinedId: Int, + private val _angleX: UShort, + private val _angleY: UShort, + private val _zoom: UShort, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + angleX: Int, + angleY: Int, + zoom: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + angleX.toUShort(), + angleY.toUShort(), + zoom.toUShort(), + ) + + public constructor( + combinedId: Int, + angleX: Int, + angleY: Int, + zoom: Int, + ) : this( + combinedId, + angleX.toUShort(), + angleY.toUShort(), + zoom.toUShort(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val angleX: Int + get() = _angleX.toInt() + public val angleY: Int + get() = _angleY.toInt() + public val zoom: Int + get() = _zoom.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetAngle + + if (combinedId != other.combinedId) return false + if (_angleX != other._angleX) return false + if (_angleY != other._angleY) return false + if (_zoom != other._zoom) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _angleX.hashCode() + result = 31 * result + _angleY.hashCode() + result = 31 * result + _zoom.hashCode() + return result + } + + override fun toString(): String = + "IfSetAngle(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "angleX=$angleX, " + + "angleY=$angleY, " + + "zoom=$zoom" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetAnim.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetAnim.kt new file mode 100644 index 000000000..367a9bd8d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetAnim.kt @@ -0,0 +1,72 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * If set-anim is used to make a model animate on a component. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the id of the interface on which the model resides + * @property componentId the id of the component on which the model resides + * @property anim the id of the animation to play, or -1 to reset the animation + */ +public class IfSetAnim private constructor( + public val combinedId: Int, + private val _anim: UShort, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + anim: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + anim.toUShort(), + ) + + public constructor( + combinedId: Int, + anim: Int, + ) : this( + combinedId, + anim.toUShort(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val anim: Int + get() = _anim.toIntOrMinusOne() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetAnim + + if (combinedId != other.combinedId) return false + if (_anim != other._anim) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _anim.hashCode() + return result + } + + override fun toString(): String = + "IfSetAnim(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "anim=$anim" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetColour.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetColour.kt new file mode 100644 index 000000000..495abd045 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetColour.kt @@ -0,0 +1,191 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId +import java.awt.Color + +/** + * If set-colour is used to set the colour of a text component. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the id of the interface on which the text resides + * @property componentId the id of the component on which the text resides + * @property red the value of the red colour, ranging from 0 to 31 (inclusive) + * @property green the value of the green colour, ranging from 0 to 31 (inclusive) + * @property blue the value of the blue colour, ranging from 0 to 31 (inclusive) + */ +@Suppress("MemberVisibilityCanBePrivate") +public class IfSetColour private constructor( + public val combinedId: Int, + private val colour: Rs15BitColour, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + red: Int, + green: Int, + blue: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + Rs15BitColour( + red, + green, + blue, + ), + ) + + public constructor( + combinedId: Int, + red: Int, + green: Int, + blue: Int, + ) : this( + combinedId, + Rs15BitColour( + red, + green, + blue, + ), + ) + + public constructor( + combinedId: Int, + colour15BitPacked: Int, + ) : this( + combinedId, + Rs15BitColour(colour15BitPacked.toUShort()), + ) + + /** + * A secondary constructor to build a colour from [Color]. + * This can be useful to avoid manual colour conversions, + * as 8-bit colours are typically used. + * This function will strip away the 3 least significant + * bits from the colours, as Jagex's colour format only expects + * 5 bits per colour, so small changes in tone may occur. + */ + public constructor( + interfaceId: Int, + componentId: Int, + color: Color, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + Rs15BitColour( + color.red ushr 3, + color.green ushr 3, + color.blue ushr 3, + ), + ) + + /** + * A secondary constructor to build a colour from [Color]. + * This can be useful to avoid manual colour conversions, + * as 8-bit colours are typically used. + * This function will strip away the 3 least significant + * bits from the colours, as Jagex's colour format only expects + * 5 bits per colour, so small changes in tone may occur. + */ + public constructor( + combinedId: Int, + color: Color, + ) : this( + combinedId, + Rs15BitColour( + color.red ushr 3, + color.green ushr 3, + color.blue ushr 3, + ), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val red: Int + get() = colour.red + public val green: Int + get() = colour.green + public val blue: Int + get() = colour.blue + public val colour15BitPacked: Int + get() = colour.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + /** + * Turns the 15-bit RS RGB colour into a 24-bit normalized RGB colour. + */ + public fun toAwtColor(): Color = + Color( + (red shl 19) + .or(green shl 11) + .or(blue shl 3), + ) + + @JvmInline + private value class Rs15BitColour( + val packed: UShort, + ) { + constructor( + red: Int, + green: Int, + blue: Int, + ) : this( + (red and 0x1F shl 10) + .or(green and 0x1F shl 5) + .or(blue and 0x1F) + .toUShort(), + ) + + val red: Int + get() = packed.toInt() ushr 10 and 0x1F + val green: Int + get() = packed.toInt() ushr 5 and 0x1F + val blue: Int + get() = packed.toInt() and 0x1F + + override fun toString(): String = + "Rs15BitColour(" + + "red=$red, " + + "green=$green, " + + "blue=$blue" + + ")" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetColour + + if (combinedId != other.combinedId) return false + if (colour != other.colour) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + colour.hashCode() + return result + } + + @OptIn(ExperimentalStdlibApi::class) + override fun toString(): String { + val packed = + (red shl 19) + .or(green shl 11) + .or(blue shl 3) + return "IfSetColour(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "red=$red/31, " + + "green=$green/31, " + + "blue=$blue/31, " + + "24-bit RGB colour=${packed.toHexString(HexFormat.UpperCase)}" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetEvents.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetEvents.kt new file mode 100644 index 000000000..fc7b70b9f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetEvents.kt @@ -0,0 +1,92 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * Interface events are sent to set/unlock various options on a component, + * such as button clicks and dragging. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id on which to set the events + * @property componentId the component on that interface to set the events on + * @property start the start subcomponent id + * @property end the end subcomponent id (inclusive) + * @property events the bitpacked events + */ +public class IfSetEvents private constructor( + public val combinedId: Int, + private val _start: UShort, + private val _end: UShort, + public val events: Int, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + start: Int, + end: Int, + events: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + start.toUShort(), + end.toUShort(), + events, + ) + + public constructor( + combinedId: Int, + start: Int, + end: Int, + events: Int, + ) : this( + combinedId, + start.toUShort(), + end.toUShort(), + events, + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val start: Int + get() = _start.toInt() + public val end: Int + get() = _end.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetEvents + + if (combinedId != other.combinedId) return false + if (_start != other._start) return false + if (_end != other._end) return false + if (events != other.events) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _start.hashCode() + result = 31 * result + _end.hashCode() + result = 31 * result + events + return result + } + + override fun toString(): String = + "IfSetEvents(" + + "events=$events, " + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "start=$start, " + + "end=$end" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetHide.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetHide.kt new file mode 100644 index 000000000..547f5a105 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetHide.kt @@ -0,0 +1,61 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If set-hide is used to hide or unhide a component and its children on an interface. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id on which the component to hide or unhide resides on + * @property componentId the component on the [interfaceId] to hide or unhide + * @property hidden whether to hide or unhide the component + */ +public class IfSetHide( + public val combinedId: Int, + public val hidden: Boolean, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + hidden: Boolean, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + hidden, + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetHide + + if (combinedId != other.combinedId) return false + if (hidden != other.hidden) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + hidden.hashCode() + return result + } + + override fun toString(): String = + "IfSetHide(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "hidden=$hidden" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetModel.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetModel.kt new file mode 100644 index 000000000..1d5456c05 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetModel.kt @@ -0,0 +1,72 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If set model packet is used to set a model to render on an interface. + * The component must be of model type for this to succeed. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id on which to set the events + * @property componentId the component on that interface to set the events on + * @property model the id of the model to render. + */ +public class IfSetModel private constructor( + public val combinedId: Int, + private val _model: UShort, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + model: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + model.toUShort(), + ) + + public constructor( + combinedId: Int, + model: Int, + ) : this( + combinedId, + model.toUShort(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val model: Int + get() = _model.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetModel + + if (combinedId != other.combinedId) return false + if (_model != other._model) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _model.hashCode() + return result + } + + override fun toString(): String = + "IfSetModel(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "model=$model" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetNpcHead.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetNpcHead.kt new file mode 100644 index 000000000..11b057669 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetNpcHead.kt @@ -0,0 +1,73 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.message.toIntOrMinusOne +import net.rsprot.protocol.util.CombinedId + +/** + * If set-npc-head is used to set a npc's chathead on an interface, commonly + * in dialogues. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id on which the model resides + * @property componentId the component id on which the model resides + * @property npc the id of the npc config whose head to set as the model + */ +public class IfSetNpcHead private constructor( + public val combinedId: Int, + private val _npc: UShort, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + npc: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + npc.toUShort(), + ) + + public constructor( + combinedId: Int, + npc: Int, + ) : this( + combinedId, + npc.toUShort(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val npc: Int + get() = _npc.toIntOrMinusOne() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetNpcHead + + if (combinedId != other.combinedId) return false + if (_npc != other._npc) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _npc.hashCode() + return result + } + + override fun toString(): String = + "IfSetNpcHead(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "npc=$npc" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetNpcHeadActive.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetNpcHeadActive.kt new file mode 100644 index 000000000..107ad455a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetNpcHeadActive.kt @@ -0,0 +1,75 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If set-npc-head-active is used to set a npc's chathead on an interface, commonly + * in dialogues. Rather than taking the id of the npc config, this function + * takes the index of the npc in the world. Npc's model is looked up from the + * client through npc info, allowing for the chatbox to render a custom-built + * npc with completely dynamic models, rather than the pre-defined configs. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id on which the model resides + * @property componentId the component id on which the model resides + * @property index the index of the npc in the world + */ +public class IfSetNpcHeadActive private constructor( + public val combinedId: Int, + private val _index: UShort, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + index: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + index.toUShort(), + ) + + public constructor( + combinedId: Int, + index: Int, + ) : this( + combinedId, + index.toUShort(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val index: Int + get() = _index.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetNpcHeadActive + + if (combinedId != other.combinedId) return false + if (_index != other._index) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _index.hashCode() + return result + } + + override fun toString(): String = + "IfSetNpcHead(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "index=$index" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetObject.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetObject.kt new file mode 100644 index 000000000..705f5a391 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetObject.kt @@ -0,0 +1,83 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * Sets an object on an interface component. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface on which the component resides + * @property componentId the component on which the obj resides + * @property obj the id of the obj to set on the component + * @property count the count of the obj, used to obtain a different variant + * of the model of the obj + */ +public class IfSetObject private constructor( + public val combinedId: Int, + private val _obj: UShort, + private val _count: UShort, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + obj: Int, + count: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + obj.toUShort(), + count.toUShort(), + ) + + public constructor( + combinedId: Int, + obj: Int, + count: Int, + ) : this( + combinedId, + obj.toUShort(), + count.toUShort(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val obj: Int + get() = _obj.toInt() + public val count: Int + get() = _count.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetObject + + if (combinedId != other.combinedId) return false + if (_obj != other._obj) return false + if (_count != other._count) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _obj.hashCode() + result = 31 * result + _count.hashCode() + return result + } + + override fun toString(): String = + "IfSetObject(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "obj=$obj, " + + "count=$count" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerHead.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerHead.kt new file mode 100644 index 000000000..51ec9c71a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerHead.kt @@ -0,0 +1,50 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If set-player-head is used to set the local player's chathead on an interface, + * commonly used for dialogues. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the id of the interface on which the chathead model resides + * @property componentId the id of the component on which the chathead model resides + */ +public class IfSetPlayerHead( + public val combinedId: Int, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetPlayerHead + + return combinedId == other.combinedId + } + + override fun hashCode(): Int = combinedId.hashCode() + + override fun toString(): String = + "IfSetPlayerHead(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelBaseColour.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelBaseColour.kt new file mode 100644 index 000000000..ad43ac9c0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelBaseColour.kt @@ -0,0 +1,85 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If set-player-model basecolour packet is used to set the ident kit colour + * of a customized player model on an interface. This allows one to build + * a completely unique player model up without using anyone as reference. + * The colouring logic is identical to that found within Appearance for players. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the id of the interface on which the model resides + * @property componentId the id of the component on which the model resides + * @property index the index of the colour, ranging from 0 to 4 (inclusive) + * @property colour the value of the colour, ranging from 0 to 255 (inclusive) + */ +public class IfSetPlayerModelBaseColour private constructor( + public val combinedId: Int, + private val _index: UByte, + private val _colour: UByte, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + index: Int, + colour: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + index.toUByte(), + colour.toUByte(), + ) + + public constructor( + combinedId: Int, + index: Int, + colour: Int, + ) : this( + combinedId, + index.toUByte(), + colour.toUByte(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val index: Int + get() = _index.toInt() + public val colour: Int + get() = _colour.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetPlayerModelBaseColour + + if (combinedId != other.combinedId) return false + if (_index != other._index) return false + if (_colour != other._colour) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _index.hashCode() + result = 31 * result + _colour.hashCode() + return result + } + + override fun toString(): String = + "IfSetPlayerModelBaseColour(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "index=$index, " + + "colour=$colour" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelBodyType.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelBodyType.kt new file mode 100644 index 000000000..abaf672d7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelBodyType.kt @@ -0,0 +1,73 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If setplayermodel bodytype is used to change the current body-type of + * a player model on an interface, making the client prefer swap out + * the models for the respective type. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the id of the interface on which the model resides + * @property componentId the id of the component on which the model resides + * @property bodyType the new body-type to set to the player model + */ +public class IfSetPlayerModelBodyType private constructor( + public val combinedId: Int, + private val _bodyType: UByte, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + bodyType: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + bodyType.toUByte(), + ) + + public constructor( + combinedId: Int, + bodyType: Int, + ) : this( + combinedId, + bodyType.toUByte(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val bodyType: Int + get() = _bodyType.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetPlayerModelBodyType + + if (combinedId != other.combinedId) return false + if (_bodyType != other._bodyType) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _bodyType.hashCode() + return result + } + + override fun toString(): String = + "IfSetPlayerModelBodyType(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "bodyType=$bodyType" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelObj.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelObj.kt new file mode 100644 index 000000000..082bcf339 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelObj.kt @@ -0,0 +1,63 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If setplayermodel obj is used to set a worn obj on a player model. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the id of the interface on which the model resides + * @property componentId the id of the component on which the model resides + * @property obj the id of the obj. Interestingly, the client reads a 32-bit int + * for the obj, even though configs having a strict 32767/65535 limitation elsewhere + * in the client. + */ +public class IfSetPlayerModelObj( + public val combinedId: Int, + public val obj: Int, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + obj: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + obj, + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetPlayerModelObj + + if (combinedId != other.combinedId) return false + if (obj != other.obj) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + obj + return result + } + + override fun toString(): String = + "IfSetPlayerModelObj(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "obj=$obj" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelSelf.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelSelf.kt new file mode 100644 index 000000000..a21cfba40 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPlayerModelSelf.kt @@ -0,0 +1,62 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If setplayermodel self is used to set the player model on an interface + * to that of the local player. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the id of the interface on which the model resides + * @property componentId the id of the component on which the model resides + * @property copyObjs whether to copy all the worn objs over as well + */ +public class IfSetPlayerModelSelf( + public val combinedId: Int, + public val copyObjs: Boolean, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + copyObjs: Boolean, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + copyObjs, + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetPlayerModelSelf + + if (combinedId != other.combinedId) return false + if (copyObjs != other.copyObjs) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + copyObjs.hashCode() + return result + } + + override fun toString(): String = + "IfSetPlayerModelSelf(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "copyObjs=$copyObjs" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPosition.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPosition.kt new file mode 100644 index 000000000..ac241c47c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetPosition.kt @@ -0,0 +1,82 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If set-position events are used to move a component on an interface. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface on which the component to move exists + * @property componentId the component id to move + * @property x the x coordinate to move to + * @property y the y coordinate to move to + */ +public class IfSetPosition private constructor( + public val combinedId: Int, + private val _x: UShort, + private val _y: UShort, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + x: Int, + y: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + x.toUShort(), + y.toUShort(), + ) + + public constructor( + combinedId: Int, + x: Int, + y: Int, + ) : this( + combinedId, + x.toUShort(), + y.toUShort(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val x: Int + get() = _x.toInt() + public val y: Int + get() = _y.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetPosition + + if (combinedId != other.combinedId) return false + if (_x != other._x) return false + if (_y != other._y) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _x.hashCode() + result = 31 * result + _y.hashCode() + return result + } + + override fun toString(): String = + "IfSetPosition(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "x=$x, " + + "y=$y" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetRotateSpeed.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetRotateSpeed.kt new file mode 100644 index 000000000..b431c7503 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetRotateSpeed.kt @@ -0,0 +1,89 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If set-rotate-speed packet is used to make a model rotate + * according to the client's update counter. This only has an effect + * on model-type components. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the id of the interface on which the component to rotate + * lives. + * @property componentId the component on which the model to rotate lives + * @property xSpeed the speed of the x angle of the model to rotate by + * each client cycle (20ms/cc), with a value of 1 being equal to 1/2048th of a + * full circle + * @property ySpeed the speed of the y angle of the model to rotate by + * each client cycle (20ms/cc), with a value of 1 being equal to 1/2048th of a + * full circle + */ +public class IfSetRotateSpeed private constructor( + public val combinedId: Int, + private val _xSpeed: UShort, + private val _ySpeed: UShort, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + xSpeed: Int, + ySpeed: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + xSpeed.toUShort(), + ySpeed.toUShort(), + ) + + public constructor( + combinedId: Int, + xSpeed: Int, + ySpeed: Int, + ) : this( + combinedId, + xSpeed.toUShort(), + ySpeed.toUShort(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val xSpeed: Int + get() = _xSpeed.toInt() + public val ySpeed: Int + get() = _ySpeed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetRotateSpeed + + if (combinedId != other.combinedId) return false + if (_xSpeed != other._xSpeed) return false + if (_ySpeed != other._ySpeed) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _xSpeed.hashCode() + result = 31 * result + _ySpeed.hashCode() + return result + } + + override fun toString(): String = + "IfSetRotateSpeed(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "xSpeed=$xSpeed, " + + "ySpeed=$ySpeed" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetScrollPos.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetScrollPos.kt new file mode 100644 index 000000000..5d8885d84 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetScrollPos.kt @@ -0,0 +1,72 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If set scroll pos messages are used to force the scroll position + * of a layer component. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface on which the scroll layer exists + * @property componentId the component id of the scroll layer + * @property scrollPos the scroll position to set to + */ +public class IfSetScrollPos private constructor( + public val combinedId: Int, + private val _scrollPos: UShort, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + scrollPos: Int, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + scrollPos.toUShort(), + ) + + public constructor( + combinedId: Int, + scrollPos: Int, + ) : this( + combinedId, + scrollPos.toUShort(), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val scrollPos: Int + get() = _scrollPos.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetScrollPos + + if (combinedId != other.combinedId) return false + if (_scrollPos != other._scrollPos) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _scrollPos.hashCode() + return result + } + + override fun toString(): String = + "IfSetScrollPos(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "scrollPos=$scrollPos" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetText.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetText.kt new file mode 100644 index 000000000..a92cf6500 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/interfaces/IfSetText.kt @@ -0,0 +1,62 @@ +package net.rsprot.protocol.game.outgoing.interfaces + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * If set-text packet is used to set the text on a text component. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the interface id on which the text resides + * @property componentId the component id on the interface on which the text + * resides + * @property text the text to assign + */ +public class IfSetText( + public val combinedId: Int, + public val text: String, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + text: String, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + text, + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IfSetText + + if (combinedId != other.combinedId) return false + if (text != other.text) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + text.hashCode() + return result + } + + override fun toString(): String = + "IfSetText(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "text='$text'" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvFull.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvFull.kt new file mode 100644 index 000000000..4d2d655f7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvFull.kt @@ -0,0 +1,178 @@ +package net.rsprot.protocol.game.outgoing.inv + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.RSProtFlags +import net.rsprot.protocol.common.game.outgoing.inv.InventoryObject +import net.rsprot.protocol.common.game.outgoing.inv.internal.Inventory +import net.rsprot.protocol.common.game.outgoing.inv.internal.InventoryPool +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * Update inv full is used to perform a full synchronization of an inventory's + * contents to the client. + * The client will wipe any existing cache of this inventory prior to performing + * an update. + * While not very well known, it is possible to send less objs than the inventory's + * respective capacity in the cache. As an example, if the inventory's capacity + * in the cache is 500, but the inv only has a single object at the first slot, + * a simple compression method is to send the capacity as 1 to the client, + * and only inform of the single object that does exist - all others would be + * presumed non-existent. There is no need to transmit all 500 slots when + * the remaining 499 are not filled, saving considerable amount of space in the + * process. + * + * @property combinedId the combined id of the interface and the component id. + * For IF3-type interfaces, only negative values are allowed. + * If one wishes to make the inventory a "mirror", e.g. for trading, + * how both the player's own and the partner's inventory share the id, + * a value of < -70000 is expected, this tells the client that the respective + * inventory is a "mirrored" one. + * For normal IF3 interfaces, a value of -1 is perfectly acceptable. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the IF1 interface on which the inventory lies. + * For IF3 interfaces, no [interfaceId] should be provided. + * @property componentId the component on which the inventory lies + * @property inventoryId the id of the inventory to update + * @property capacity the capacity of the inventory being transmitted in this + * update. + */ +public class UpdateInvFull private constructor( + public val combinedId: Int, + private val _inventoryId: UShort, + private val inventory: Inventory, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + inventoryId: Int, + capacity: Int, + provider: ObjectProvider, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + inventoryId.toUShort(), + buildInventory(capacity, provider), + ) + + public constructor( + combinedId: Int, + inventoryId: Int, + capacity: Int, + provider: ObjectProvider, + ) : this( + CombinedId(combinedId).combinedId, + inventoryId.toUShort(), + buildInventory(capacity, provider), + ) + + public constructor( + inventoryId: Int, + capacity: Int, + provider: ObjectProvider, + ) : this( + -1, + inventoryId.toUShort(), + buildInventory(capacity, provider), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val inventoryId: Int + get() = _inventoryId.toInt() + public val capacity: Int + get() = inventory.count + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + /** + * Gets the obj in the [slot] provided. + * @param slot the slot in the inventory. + * @return the inventory object that's in that slot, + * or [InventoryObject.NULL] if there's no object. + * @throws IndexOutOfBoundsException if the [slot] is outside + * the inventory's boundaries. + */ + public fun getObject(slot: Int): InventoryObject = inventory[slot] + + public fun returnInventory() { + inventory.clear() + InventoryPool.pool.returnObject(inventory) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateInvFull + + if (combinedId != other.combinedId) return false + if (_inventoryId != other._inventoryId) return false + if (inventory != other.inventory) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _inventoryId.hashCode() + result = 31 * result + inventory.hashCode() + return result + } + + override fun toString(): String = + "UpdateInvFull(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "inventoryId=$inventoryId, " + + "capacity=$capacity" + + ")" + + /** + * An object provider interface is used to acquire the objs + * that exist in different inventories. These objs are bit-packed + * into a long, which gets further placed into a long array. + * This is all in order to avoid garbage creation with inventories, + * as this can be a considerable hot-spot for that. + */ + public fun interface ObjectProvider { + /** + * Provides an [InventoryObject] instance for a given slot + * in inventory. If there is no object in that slot, + * use [InventoryObject.NULL] as an indicator of it. + */ + public fun provide(slot: Int): InventoryObject + } + + private companion object { + /** + * Builds an inventory based on a [provider]. + * @param capacity the capacity of the inventory, this is how far + * the function will iterate to slots wise. + * @param provider the object provider, used to return information + * about an object in a slot of an inventory. + * @return an inventory object, which is a compressed representation + * of a list of [InventoryObject]s, backed by a long array. + */ + private fun buildInventory( + capacity: Int, + provider: ObjectProvider, + ): Inventory { + val inventory = InventoryPool.pool.borrowObject() + for (i in 0..= 0) { + "Obj count cannot be below zero: $obj @ $i" + } + } + inventory.add(obj) + } + return inventory + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvPartial.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvPartial.kt new file mode 100644 index 000000000..cdb7876ea --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvPartial.kt @@ -0,0 +1,198 @@ +package net.rsprot.protocol.game.outgoing.inv + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.RSProtFlags +import net.rsprot.protocol.common.game.outgoing.inv.InventoryObject +import net.rsprot.protocol.common.game.outgoing.inv.internal.Inventory +import net.rsprot.protocol.common.game.outgoing.inv.internal.InventoryPool +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage +import net.rsprot.protocol.util.CombinedId + +/** + * Update inv partial is used to send an update of an inventory + * that doesn't include the entire inventory. + * This is typically used after the first [UpdateInvFull] + * update has been performed, as subsequent updates tend to + * be smaller, such as picking up an object - only + * a single slot in the player's inventory would change, not warranting + * the full 28-slot update as a result. + * + * A general rule of thumb for when to use partial updates is + * if the percentage of modified objects is less than two thirds + * of that of the highest modified index. + * So, if our inventory has a capacity of 1,000, and we have sparsely + * modified 500 indices throughout that, including the 999th index, + * it is more beneficial to use the partial inventory update, + * as the total bandwidth used by it is generally going to be less + * than what the full update would require. + * If, however, there is a continuous sequence of indices that + * have been modified, such as everything from indices 0 to 100, + * it is more beneficial to use the full update and set the + * capacity to 100 in that. + * + * Below is a percentage-breakdown of how much more bandwidth the partial update + * requires per object basis given different criteria, compared to the full update: + * ``` + * 14.3% if slot < 128 && count >= 255 + * 28.6% if slot >= 128 && count >= 255 + * 33.3% if slot < 128 && count < 255 + * 66.6% if slot >= 128 && count < 255 + * ``` + * + * While it is impossible to truly estimate what the exact threshold is, + * this provides a good general idea of when to use either packet. + * + * @property combinedId the combined id of the interface and the component id. + * For IF3-type interfaces, only negative values are allowed. + * If one wishes to make the inventory a "mirror", e.g. for trading, + * how both the player's own and the partner's inventory share the id, + * a value of < -70000 is expected, this tells the client that the respective + * inventory is a "mirrored" one. + * For normal IF3 interfaces, a value of -1 is perfectly acceptable. + * @property combinedId the bitpacked combination of [interfaceId] and [componentId]. + * @property interfaceId the IF1 interface on which the inventory lies. + * For IF3 interfaces, no [interfaceId] should be provided. + * @property componentId the component on which the inventory lies + * @property inventoryId the id of the inventory to update + * @property count the number of items added into this partial update. + */ +public class UpdateInvPartial private constructor( + public val combinedId: Int, + private val _inventoryId: UShort, + private val inventory: Inventory, +) : OutgoingGameMessage { + public constructor( + interfaceId: Int, + componentId: Int, + inventoryId: Int, + provider: IndexedObjectProvider, + ) : this( + CombinedId(interfaceId, componentId).combinedId, + inventoryId.toUShort(), + buildInventory(provider), + ) + + public constructor( + combinedId: Int, + inventoryId: Int, + provider: IndexedObjectProvider, + ) : this( + CombinedId(combinedId).combinedId, + inventoryId.toUShort(), + buildInventory(provider), + ) + + public constructor( + inventoryId: Int, + provider: IndexedObjectProvider, + ) : this( + -1, + inventoryId.toUShort(), + buildInventory(provider), + ) + + private val _combinedId: CombinedId + get() = CombinedId(combinedId) + public val interfaceId: Int + get() = _combinedId.interfaceId + public val componentId: Int + get() = _combinedId.componentId + public val inventoryId: Int + get() = _inventoryId.toInt() + public val count: Int + get() = inventory.count + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + /** + * Gets the obj in the [slot] provided. + * @param slot the slot in the inventory. + * @return the inventory object that's in that slot, + * or [InventoryObject.NULL] if there's no object. + * @throws IndexOutOfBoundsException if the [slot] is outside + * the inventory's boundaries. + */ + public fun getObject(slot: Int): InventoryObject = inventory[slot] + + public fun returnInventory() { + inventory.clear() + InventoryPool.pool.returnObject(inventory) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateInvPartial + + if (combinedId != other.combinedId) return false + if (_inventoryId != other._inventoryId) return false + if (inventory != other.inventory) return false + + return true + } + + override fun hashCode(): Int { + var result = combinedId.hashCode() + result = 31 * result + _inventoryId.hashCode() + result = 31 * result + inventory.hashCode() + return result + } + + override fun toString(): String = + "UpdateInvPartial(" + + "interfaceId=$interfaceId, " + + "componentId=$componentId, " + + "inventoryId=$inventoryId, " + + "count=$count" + + ")" + + /** + * An object provider interface is used to acquire the objs + * that exist in different inventories. These objs are bit-packed + * into a long, which gets further placed into a long array. + * This is all in order to avoid garbage creation with inventories, + * as this can be a considerable hot-spot for that. + */ + public abstract class IndexedObjectProvider( + internal val indices: Iterator, + ) { + /** + * Provides an [InventoryObject] instance for a given slot + * in inventory. If there is no object in that slot, + * use [InventoryObject.NULL] as an indicator of it. + */ + public abstract fun provide(slot: Int): InventoryObject + } + + private companion object { + /** + * Builds an inventory based on a [provider]. + * @param provider the object provider, used to return information + * about an object in a slot of an inventory. + * @return an inventory object, which is a compressed representation + * of a list of [InventoryObject]s, backed by a long array. + */ + private fun buildInventory(provider: IndexedObjectProvider): Inventory { + val inventory = InventoryPool.pool.borrowObject() + for (index in provider.indices) { + val obj = provider.provide(index) + if (RSProtFlags.inventoryObjCheck) { + check(obj != InventoryObject.NULL) { + "Obj cannot be InventoryObject.NULL for partial updates. Use InventoryObject(slot, -1, -1) " + + "instead." + } + check(obj.slot >= 0) { + "Obj slot cannot be below zero: $obj $ $index" + } + check(obj.id == -1 || obj.count >= 0) { + "Obj count cannot be below zero: $obj @ $index" + } + } + inventory.add(obj) + } + return inventory + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvStopTransmit.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvStopTransmit.kt new file mode 100644 index 000000000..a855c4c77 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/inv/UpdateInvStopTransmit.kt @@ -0,0 +1,35 @@ +package net.rsprot.protocol.game.outgoing.inv + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update inv stop transmit is used by the server to inform the client + * that no more updates for a given inventory are expected. + * In OldSchool RuneScape, this is sent whenever an interface that's + * linked to the inventory is sent. + * In doing so, the client will wipe its cache of the given inventory. + * There is no technical reason to send this, however, as it doesn't + * prevent anything from functioning as normal. + * @property inventoryId the id of the inventory to stop listening to + */ +public class UpdateInvStopTransmit( + public val inventoryId: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateInvStopTransmit + + return inventoryId == other.inventoryId + } + + override fun hashCode(): Int = inventoryId + + override fun toString(): String = "UpdateInvStopTransmit(inventoryId=$inventoryId)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/Logout.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/Logout.kt new file mode 100644 index 000000000..09b1e7581 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/Logout.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.outgoing.logout + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Log out messages are used to tell the client the player + * has finished playing, which then causes the client to close + * the socket, and reset a lot of properties as a result. + */ +public data object Logout : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/LogoutTransfer.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/LogoutTransfer.kt new file mode 100644 index 000000000..0bd538daf --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/LogoutTransfer.kt @@ -0,0 +1,99 @@ +package net.rsprot.protocol.game.outgoing.logout + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Logout transfer packet is used for world-hopping purposes, + * making the client connect to a different world instead. + * + * World properties table: + * ``` + * | Flag | Type | + * |------------|:-----------------------:| + * | 0x1 | Members | + * | 0x2 | Quick chat | + * | 0x4 | PvP world | + * | 0x8 | Lootshare | + * | 0x10 | Dedicated activity | + * | 0x20 | Bounty world | + * | 0x40 | PvP Arena | + * | 0x80 | High level only - 1500+ | + * | 0x100 | Speedrun | + * | 0x200 | Existing players only | + * | 0x400 | Extra-hard wilderness | + * | 0x800 | Dungeoneering | + * | 0x1000 | Instance shard | + * | 0x2000 | Rentable | + * | 0x4000 | Last man standing | + * | 0x8000 | New players | + * | 0x10000 | Beta world | + * | 0x20000 | Staff IP only | + * | 0x40000 | High level only - 2000+ | + * | 0x80000 | High level only - 2400+ | + * | 0x100000 | VIPs only | + * | 0x200000 | Hidden world | + * | 0x400000 | Legacy only | + * | 0x800000 | EoC only | + * | 0x1000000 | Behind proxy | + * | 0x2000000 | No save mode | + * | 0x4000000 | Tournament world | + * | 0x8000000 | Fresh start world | + * | 0x10000000 | High level only - 1750+ | + * | 0x20000000 | Deadman world | + * | 0x40000000 | Seasonal world | + * | 0x80000000 | External partner only | + * ``` + * + * @property host the ip address of the new world + * @property id the id of the new world + * @property properties the flags of the new world + */ +public class LogoutTransfer private constructor( + public val host: String, + private val _id: UShort, + public val properties: Int, +) : OutgoingGameMessage { + public constructor( + host: String, + id: Int, + properties: Int, + ) : this( + host, + id.toUShort(), + properties, + ) + + public val id: Int + get() = _id.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LogoutTransfer + + if (host != other.host) return false + if (_id != other._id) return false + if (properties != other.properties) return false + + return true + } + + override fun hashCode(): Int { + var result = host.hashCode() + result = 31 * result + _id.hashCode() + result = 31 * result + properties + return result + } + + override fun toString(): String = + "LogoutTransfer(" + + "host='$host', " + + "id=$id, " + + "properties=$properties" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/LogoutWithReason.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/LogoutWithReason.kt new file mode 100644 index 000000000..5e4466655 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/logout/LogoutWithReason.kt @@ -0,0 +1,41 @@ +package net.rsprot.protocol.game.outgoing.logout + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Logout with reason, much like [Logout], is used to + * log the player out of the game. The only difference here + * is that the user will be given a reason for why they were + * logged out of the game, e.g. inactive for too long. + * + * Logout reasons table: + * ``` + * | Id | Type | + * |----|:--------:| + * | 1 | Kicked | + * | 2 | Updating | + * ``` + * + * @property reason the id of the reason to display (see table above) + */ +public class LogoutWithReason( + public val reason: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LogoutWithReason + + return reason == other.reason + } + + override fun hashCode(): Int = reason + + override fun toString(): String = "LogoutWithReason(reason=$reason)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildLogin.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildLogin.kt new file mode 100644 index 000000000..b3d357530 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildLogin.kt @@ -0,0 +1,102 @@ +package net.rsprot.protocol.game.outgoing.map + +import io.netty.buffer.ByteBuf +import net.rsprot.crypto.xtea.XteaKey +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfo +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfo.Companion.ROOT_WORLD +import net.rsprot.protocol.game.outgoing.map.util.XteaProvider +import net.rsprot.protocol.game.outgoing.map.util.buildXteaKeyList + +/** + * Rebuild login is sent as part of the login procedure as the very first packet, + * as this one contains information about everyone's low resolution position, allowing + * the player information packet to be initialized properly. + * @property zoneX the x coordinate of the local player's current zone. + * @property zoneZ the z coordinate of the local player's current zone. + * @property worldArea the current world area in which the player resides. + * @property keys the list of xtea keys needed to decrypt the map. + * @property gpiInitBlock the initialization block of the player info protocol, + * used to inform the client of all the low resolution coordinates of everyone in the game. + */ +public class RebuildLogin private constructor( + private val _zoneX: UShort, + private val _zoneZ: UShort, + private val _worldArea: UShort, + override val keys: List, + public val gpiInitBlock: ByteBuf, +) : StaticRebuildMessage { + public constructor( + zoneX: Int, + zoneZ: Int, + worldArea: Int, + keyProvider: XteaProvider, + playerInfo: PlayerInfo, + ) : this( + zoneX.toUShort(), + zoneZ.toUShort(), + worldArea.toUShort(), + buildXteaKeyList(zoneX, zoneZ, keyProvider), + initializePlayerInfo(playerInfo), + ) + + override val zoneX: Int + get() = _zoneX.toInt() + override val zoneZ: Int + get() = _zoneZ.toInt() + override val worldArea: Int + get() = _worldArea.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RebuildLogin + + if (_zoneX != other._zoneX) return false + if (_zoneZ != other._zoneZ) return false + if (_worldArea != other._worldArea) return false + if (keys != other.keys) return false + if (gpiInitBlock != other.gpiInitBlock) return false + + return true + } + + override fun hashCode(): Int { + var result = _zoneX.hashCode() + result = 31 * result + _zoneZ.hashCode() + result = 31 * result + _worldArea.hashCode() + result = 31 * result + keys.hashCode() + result = 31 * result + gpiInitBlock.hashCode() + return result + } + + override fun toString(): String = + "RebuildLogin(" + + "keys=$keys, " + + "gpiInitBlock=$gpiInitBlock, " + + "zoneX=$zoneX, " + + "zoneZ=$zoneZ, " + + "worldArea=$worldArea" + + ")" + + private companion object { + private const val REBUILD_NORMAL_MAXIMUM_SIZE: Int = 44 + private const val PLAYER_INFO_BLOCK_SIZE = ((30 + (2046 * 18)) + Byte.SIZE_BITS - 1) ushr 3 + + /** + * Initializes the player info block into a buffer provided by allocator in the playerinfo object + * @param playerInfo the player info protocol of this player to be initialized + * @return a buffer containing the initialization block of the player info protocol + */ + private fun initializePlayerInfo(playerInfo: PlayerInfo): ByteBuf { + val allocator = playerInfo.allocator + val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE + REBUILD_NORMAL_MAXIMUM_SIZE) + playerInfo.handleAbsolutePlayerPositions(ROOT_WORLD, buffer) + return buffer + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildNormal.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildNormal.kt new file mode 100644 index 000000000..53a697ad4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildNormal.kt @@ -0,0 +1,72 @@ +package net.rsprot.protocol.game.outgoing.map + +import net.rsprot.crypto.xtea.XteaKey +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.map.util.XteaProvider +import net.rsprot.protocol.game.outgoing.map.util.buildXteaKeyList + +/** + * Rebuild normal is sent when the game requires a map reload without being in instances. + * @property zoneX the x coordinate of the local player's current zone. + * @property zoneZ the z coordinate of the local player's current zone. + * @property worldArea the current world area in which the player resides. + * @property keys the list of xtea keys needed to decrypt the map. + */ +public class RebuildNormal private constructor( + private val _zoneX: UShort, + private val _zoneZ: UShort, + private val _worldArea: UShort, + override val keys: List, +) : StaticRebuildMessage { + public constructor( + zoneX: Int, + zoneZ: Int, + worldArea: Int, + keyProvider: XteaProvider, + ) : this( + zoneX.toUShort(), + zoneZ.toUShort(), + worldArea.toUShort(), + buildXteaKeyList(zoneX, zoneZ, keyProvider), + ) + + override val zoneX: Int + get() = _zoneX.toInt() + override val zoneZ: Int + get() = _zoneZ.toInt() + override val worldArea: Int + get() = _worldArea.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RebuildNormal + + if (_zoneX != other._zoneX) return false + if (_zoneZ != other._zoneZ) return false + if (_worldArea != other._worldArea) return false + if (keys != other.keys) return false + + return true + } + + override fun hashCode(): Int { + var result = _zoneX.hashCode() + result = 31 * result + _zoneZ.hashCode() + result = 31 * result + _worldArea.hashCode() + result = 31 * result + keys.hashCode() + return result + } + + override fun toString(): String = + "RebuildNormal(" + + "keys=$keys, " + + "zoneX=$zoneX, " + + "zoneZ=$zoneZ, " + + "worldArea=$worldArea" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildRegion.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildRegion.kt new file mode 100644 index 000000000..9ead9f84f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildRegion.kt @@ -0,0 +1,144 @@ +package net.rsprot.protocol.game.outgoing.map + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.map.util.RebuildRegionZone +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Rebuild region is used to send a dynamic map to the client, + * built up out of zones (8x8x1 tiles), allowing for any kind + * of unique instancing to occur. + * @property zoneX the x coordinate of the center zone around + * which the build area is built + * @property zoneZ the z coordinate of the center zone around + * which the build area is built + * @property reload whether to forcibly reload the map client-sided. + * If this property is false, the client will only reload if + * the last rebuild had difference [zoneX] or [zoneZ] coordinates + * than this one. + * @property zones the list of zones to build, in a specific order. + */ +public class RebuildRegion private constructor( + private val _zoneX: UShort, + private val _zoneZ: UShort, + public val reload: Boolean, + public val zones: List, +) : OutgoingGameMessage { + public constructor( + zoneX: Int, + zoneZ: Int, + reload: Boolean, + zoneProvider: RebuildRegionZoneProvider, + ) : this( + zoneX.toUShort(), + zoneZ.toUShort(), + reload, + buildRebuildRegionZones( + zoneX, + zoneZ, + zoneProvider, + ), + ) + + public val zoneX: Int + get() = _zoneX.toInt() + public val zoneZ: Int + get() = _zoneZ.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RebuildRegion + + if (_zoneX != other._zoneX) return false + if (_zoneZ != other._zoneZ) return false + if (reload != other.reload) return false + if (zones != other.zones) return false + + return true + } + + override fun hashCode(): Int { + var result = _zoneX.hashCode() + result = 31 * result + _zoneZ.hashCode() + result = 31 * result + reload.hashCode() + result = 31 * result + zones.hashCode() + return result + } + + override fun toString(): String = + "RebuildRegion(" + + "zoneX=$zoneX, " + + "zoneZ=$zoneZ, " + + "reload=$reload, " + + "zones=$zones" + + ")" + + /** + * Zone provider acts as a function to provide all the necessary information + * needed for rebuild region to function, in the order the client + * expects it in. + */ + public fun interface RebuildRegionZoneProvider { + /** + * Provides a zone that the client must copy based on the parameters. + * In order to calculate the mapsquare id for xtea keys, use [getMapsquareId]. + * + * @param zoneX the x coordinate of the static zone to be copied + * @param zoneZ the z coordinate of the static zone to be copied + * @param level the level of the static zone to be copied + * @return the zone to be copied, or null if there's no zone to be copied there. + */ + public fun provide( + zoneX: Int, + zoneZ: Int, + level: Int, + ): RebuildRegionZone? + + /** + * Calculates the mapsquare id based on the zone coordinates. + * @param zoneX the x coordinate of the zone + * @param zoneZ the z coordinate of the zone + */ + public fun getMapsquareId( + zoneX: Int, + zoneZ: Int, + ): Int = (zoneX and 0x7FF ushr 3 shl 8) or (zoneZ and 0x7FF ushr 3) + } + + private companion object { + /** + * Builds a list of rebuild region zones to be written to the client, + * in order as the client expects them. + * @param centerZoneX the center zone x coordinate around which the build area is built + * @param centerZoneZ the center zone z coordinate around which the build area is built + * @param zoneProvider the functional interface providing the necessary information + * to be written to the client + * @return a list of rebuild region zones (or nulls) for each zone in the build area. + */ + private fun buildRebuildRegionZones( + centerZoneX: Int, + centerZoneZ: Int, + zoneProvider: RebuildRegionZoneProvider, + ): List { + val zones = ArrayList(4 * 13 * 13) + for (level in 0..<4) { + for (zoneX in (centerZoneX - 6)..(centerZoneX + 6)) { + for (zoneZ in (centerZoneZ - 6)..(centerZoneZ + 6)) { + zones += + zoneProvider.provide( + zoneX, + zoneZ, + level, + ) + } + } + } + return zones + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildWorldEntity.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildWorldEntity.kt new file mode 100644 index 000000000..72a42315a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/RebuildWorldEntity.kt @@ -0,0 +1,156 @@ +package net.rsprot.protocol.game.outgoing.map + +import io.netty.buffer.ByteBuf +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfo +import net.rsprot.protocol.game.outgoing.map.util.RebuildRegionZone +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Rebuild worldentity packet is used to build a new world entity block, + * which will be rendered in the root world for the player. + * @property index the index of the world entity (0-2048) + * @property baseX the absolute base x coordinate of the world entity in the instance land + * @property baseZ the absolute base z coordinate of the world entity in the instance land + * @property zones the list of zones that will be built into the root world + * @property gpiInitBlock the player info initialization block for the world entity + */ +public class RebuildWorldEntity private constructor( + private val _index: UShort, + private val _baseX: UShort, + private val _baseZ: UShort, + public val zones: List, + public val gpiInitBlock: ByteBuf, +) : OutgoingGameMessage { + public constructor( + index: Int, + baseX: Int, + baseZ: Int, + sizeX: Int, + sizeZ: Int, + zoneProvider: RebuildWorldEntityZoneProvider, + playerInfo: PlayerInfo, + ) : this( + index.toUShort(), + baseX.toUShort(), + baseZ.toUShort(), + buildRebuildWorldEntityZones(index, sizeX, sizeZ, zoneProvider), + initializePlayerInfo(playerInfo, index), + ) { + require(sizeX in 0..<13) { + "Size x must be in range of 0..<13: $sizeX" + } + require(sizeZ in 0..<13) { + "Size z must be in range of 0..<13: $sizeZ" + } + require(index in 0..<2048) { + "Index must be in range of 0..<2048" + } + require(baseX in 0..<16384) { + "Base x must be in range of 0..<16384" + } + require(baseZ in 0..<16384) { + "Base z must be in range of 0..<16384" + } + } + + public val index: Int + get() = _index.toInt() + public val baseX: Int + get() = _baseX.toInt() + public val baseZ: Int + get() = _baseZ.toInt() + + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + /** + * Zone provider acts as a function to provide all the necessary information + * needed for rebuild worldentity to function, in the order the client + * expects it in. + */ + public fun interface RebuildWorldEntityZoneProvider { + /** + * Provides a zone that the client must copy based on the parameters. + * This 'provide' function will be called with the relative-to-worldentity zone coordinates, + * so starting with 0,0 and ending before sizeX,sizeZ. The server is responsible for + * looking up the actual zone that was copied for that world entity. + * In order to calculate the mapsquare id for xtea keys, use [getMapsquareId]. + * + * @param zoneX the x coordinate of the static zone to be copied + * @param zoneZ the z coordinate of the static zone to be copied + * @param level the level of the static zone to be copied + * @return the zone to be copied, or null if there's no zone to be copied there. + */ + public fun provide( + index: Int, + zoneX: Int, + zoneZ: Int, + level: Int, + ): RebuildRegionZone? + + /** + * Calculates the mapsquare id based on the **absolute** zone coordinates, + * not the relative ones to the worldentity. + * @param zoneX the x coordinate of the zone + * @param zoneZ the z coordinate of the zone + */ + public fun getMapsquareId( + zoneX: Int, + zoneZ: Int, + ): Int = (zoneX and 0x7FF ushr 3 shl 8) or (zoneZ and 0x7FF ushr 3) + } + + private companion object { + /** + * Builds a list of rebuild region zones to be written to the client, + * in order as the client expects them. + * @param index the index of the world entity that is being built. + * @param sizeX the width of the worldentity + * @param sizeZ the length of the worldentity + * @param zoneProvider the functional interface providing the necessary information + * to be written to the client + * @return a list of rebuild region zones (or nulls) for each zone in the build area. + */ + private fun buildRebuildWorldEntityZones( + index: Int, + sizeX: Int, + sizeZ: Int, + zoneProvider: RebuildWorldEntityZoneProvider, + ): List { + val zones = ArrayList(4 * sizeX * sizeZ) + for (level in 0..<4) { + for (zoneX in 0.. +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/RebuildRegionZone.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/RebuildRegionZone.kt new file mode 100644 index 000000000..f98623dbc --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/RebuildRegionZone.kt @@ -0,0 +1,99 @@ +package net.rsprot.protocol.game.outgoing.map.util + +import net.rsprot.crypto.xtea.XteaKey + +/** + * This class wraps a reference zone to be copied together with the respective + * xtea key needed to decrypt the backing mapsquare. + * @property referenceZone the zone to be copied from the static map + * @property key the xtea key needed to decrypt the locs file in the cache of that respective mapsquare + */ +public class RebuildRegionZone private constructor( + public val referenceZone: ReferenceZone, + public val key: XteaKey, +) { + public constructor( + zoneX: Int, + zoneZ: Int, + level: Int, + rotation: Int, + key: XteaKey, + ) : this( + ReferenceZone( + zoneX, + zoneZ, + level, + rotation, + ), + key, + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RebuildRegionZone + + if (referenceZone != other.referenceZone) return false + if (key != other.key) return false + + return true + } + + override fun hashCode(): Int { + var result = referenceZone.hashCode() + result = 31 * result + key.hashCode() + return result + } + + override fun toString(): String = + "RebuildRegionZone(" + + "referenceZone=$referenceZone, " + + "key=$key" + + ")" + + /** + * A value class around zone objects that bitpacks all the properties into a single + * integer to be written to the client as the client expects it. + * @property rotation the rotation of the zone to be copied + * @property zoneX the x coordinate of the static zone to be copied + * @property zoneZ the z coordinate of the static zone to be copied + * @property level the level of the static zone to be copied + */ + @JvmInline + public value class ReferenceZone private constructor( + public val packed: Int, + ) { + public constructor( + zoneX: Int, + zoneZ: Int, + level: Int, + rotation: Int, + ) : this( + ((rotation and 0x3) shl 1) + .or((zoneZ and 0x7FF) shl 3) + .or((zoneX and 0x3FF) shl 14) + .or((level and 0x3) shl 24), + ) + + public val rotation: Int + get() = packed ushr 1 and 0x3 + public val zoneX: Int + get() = packed ushr 14 and 0x3FF + public val zoneZ: Int + get() = packed ushr 3 and 0x7FF + public val level: Int + get() = packed ushr 24 and 0x3 + + public val mapsquareId: Int + get() = ((zoneX ushr 3) shl 8) or (zoneZ ushr 3) + + override fun toString(): String = + "ReferenceZone(" + + "zoneX=$zoneX, " + + "zoneZ=$zoneZ, " + + "level=$level, " + + "rotation=$rotation" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/XteaHelper.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/XteaHelper.kt new file mode 100644 index 000000000..75224e890 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/XteaHelper.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.game.outgoing.map.util + +import net.rsprot.crypto.xtea.XteaKey + +/** + * A helper function to build the mapsquare key list the same way the client does, + * as the keys must be in the same specific order as the client reads it. + */ +internal fun buildXteaKeyList( + zoneX: Int, + zoneZ: Int, + keyProvider: XteaProvider, +): List { + val minMapsquareX = (zoneX - 6).coerceAtLeast(0) ushr 3 + val maxMapsquareX = (zoneX + 6).coerceAtMost(2047) ushr 3 + val minMapsquareZ = (zoneZ - 6).coerceAtLeast(0) ushr 3 + val maxMapsquareZ = (zoneZ + 6).coerceAtMost(2047) ushr 3 + val count = (maxMapsquareX - minMapsquareX + 1) * (maxMapsquareZ - minMapsquareZ + 1) + val keys = ArrayList(count.coerceIn(4, 9)) + for (mapsquareX in minMapsquareX..maxMapsquareX) { + for (mapsquareZ in minMapsquareZ..maxMapsquareZ) { + keys += keyProvider.provide((mapsquareX shl 8) or mapsquareZ) + } + } + return keys +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/XteaProvider.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/XteaProvider.kt new file mode 100644 index 000000000..75bc106ee --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/map/util/XteaProvider.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.game.outgoing.map.util + +import net.rsprot.crypto.xtea.XteaKey + +public fun interface XteaProvider { + public fun provide(mapsquareId: Int): XteaKey + + public companion object { + @JvmStatic + public val ZERO_XTEA_KEY_PROVIDER: XteaProvider = XteaProvider { XteaKey.ZERO } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideLocOps.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideLocOps.kt new file mode 100644 index 000000000..98ba4d34b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideLocOps.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Hide loc ops packet is used to hide the right-click menu of all locs across the game. + * @property hidden whether to hide all the click options of locs. + */ +public class HideLocOps( + public val hidden: Boolean, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HideLocOps + + return hidden == other.hidden + } + + override fun hashCode(): Int = hidden.hashCode() + + override fun toString(): String = "HideLocOps(hidden=$hidden)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideNpcOps.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideNpcOps.kt new file mode 100644 index 000000000..911786e4f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideNpcOps.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Hide npc ops packet is used to hide the right-click menu of all NPCs across the game. + * @property hidden whether to hide all the click options of NPCs. + */ +public class HideNpcOps( + public val hidden: Boolean, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HideNpcOps + + return hidden == other.hidden + } + + override fun hashCode(): Int = hidden.hashCode() + + override fun toString(): String = "HideNpcOps(hidden=$hidden)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideObjOps.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideObjOps.kt new file mode 100644 index 000000000..01a63972e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HideObjOps.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Hide obj ops packet is used to hide the right-click menu of all objs on the ground. + * @property hidden whether to hide all the click options of objs. + */ +public class HideObjOps( + public val hidden: Boolean, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HideObjOps + + return hidden == other.hidden + } + + override fun hashCode(): Int = hidden.hashCode() + + override fun toString(): String = "HideObjOps(hidden=$hidden)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HintArrow.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HintArrow.kt new file mode 100644 index 000000000..264fdb111 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HintArrow.kt @@ -0,0 +1,194 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Hint arrow packets are used to render a hint arrow + * at a specific player, NPC or a tile. + * Only a single hint arrow can exist at a time in OldSchool. + * @property type the hint arrow type to render. + */ +public class HintArrow( + public val type: HintArrowType, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HintArrow + + return type == other.type + } + + override fun hashCode(): Int = type.hashCode() + + override fun toString(): String = "HintArrow(type=$type)" + + public sealed interface HintArrowType + + /** + * Reset hint arrow message is used to clear out any + * existing hint arrows. + */ + public data object ResetHintArrow : HintArrowType + + /** + * NPC hint arrows are used to render a hint arrow + * on-top of a specific NPC. + * @property index the index of the NPC who is receiving + * the hint arrow. Note that this is the real index without + * any offsets or additions. + */ + public class NpcHintArrow( + public val index: Int, + ) : HintArrowType { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NpcHintArrow + + return index == other.index + } + + override fun hashCode(): Int = index + + override fun toString(): String = "NpcHintArrow(index=$index)" + } + + /** + * Player hint arrows are used to render a hint arrow + * on-top of a specific player. + * @property index the index of the player who is receiving + * the hint arrow. Note that this is the real index without + * any offsets or additions. + */ + public class PlayerHintArrow( + public val index: Int, + ) : HintArrowType { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlayerHintArrow + + return index == other.index + } + + override fun hashCode(): Int = index + + override fun toString(): String = "PlayerHintArrow(index=$index)" + } + + /** + * Tile hint arrows are used to render a hint arrow at + * a specific coordinate. + * @property x the absolute x coordinate of the hint arrow. + * @property z the absolute z coordinate of the hint arrow. + * @property height the height of the hint arrow, + * with the expected range being 0 to 255 (inclusive). + * @property position the position of the hint arrow within + * the target tile. + */ + public class TileHintArrow private constructor( + private val _x: UShort, + private val _z: UShort, + private val _height: UByte, + private val _position: UByte, + ) : HintArrowType { + public constructor( + x: Int, + z: Int, + height: Int, + tilePosition: HintArrowTilePosition, + ) : this( + x.toUShort(), + z.toUShort(), + height.toUByte(), + tilePosition.id.toUByte(), + ) + + public constructor( + x: Int, + z: Int, + height: Int, + tilePosition: Int, + ) : this( + x.toUShort(), + z.toUShort(), + height.toUByte(), + tilePosition.toUByte(), + ) + + public val x: Int + get() = _x.toInt() + public val z: Int + get() = _z.toInt() + public val height: Int + get() = _height.toInt() + public val position: HintArrowTilePosition + get() = HintArrowTilePosition[_position.toInt()] + public val positionId: Int + get() = _position.toInt() + + /** + * Hint arrow tile positions define where within a tile + * the given hint arrow will render. All the options here + * are centered on the tile, e.g. [WEST] will be at the + * western section of the tile, whilst being centered + * on the z-axis. + * + * @property id the id of the hint arrow position, + * as expected by the client. + */ + public enum class HintArrowTilePosition( + public val id: Int, + ) { + CENTER(2), + WEST(3), + EAST(4), + SOUTH(5), + NORTH(6), + ; + + internal companion object { + operator fun get(id: Int): HintArrowTilePosition = entries.first { it.id == id } + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TileHintArrow + + if (_x != other._x) return false + if (_z != other._z) return false + if (_height != other._height) return false + if (_position != other._position) return false + + return true + } + + override fun hashCode(): Int { + var result = _x.hashCode() + result = 31 * result + _z.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + _position.hashCode() + return result + } + + override fun toString(): String = + "TileHintArrow(" + + "x=$x, " + + "z=$z, " + + "height=$height, " + + "position=$position" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HiscoreReply.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HiscoreReply.kt new file mode 100644 index 000000000..2bfd38040 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/HiscoreReply.kt @@ -0,0 +1,161 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Hiscore reply is a packet used in the enhanced clients to do + * lookups of nearby players, to find out their stats and rankings + * on the high scores. + * This packet is sent as a response to the hiscore request packet. + * @property requestId the id of the request that was made. + * @property response the response to be written to the client. + */ +public class HiscoreReply private constructor( + private val _requestId: UByte, + public val response: HiscoreReplyResponse, +) : OutgoingGameMessage { + public constructor( + requestId: Int, + response: HiscoreReplyResponse, + ) : this( + requestId.toUByte(), + response, + ) + + public val requestId: Int + get() = _requestId.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HiscoreReply + + if (_requestId != other._requestId) return false + if (response != other.response) return false + + return true + } + + override fun hashCode(): Int { + var result = _requestId.hashCode() + result = 31 * result + response.hashCode() + return result + } + + override fun toString(): String = + "HiscoreReply(" + + "requestId=$requestId, " + + "response=$response" + + ")" + + public sealed interface HiscoreReplyResponse + + /** + * A successful hiscore reply, transmitting all the stat and activity results. + * It is worth noting that because the packet isn't used, it is not entirely + * certain that the naming of these properties is accurate. These are merely + * a guess based on the hiscore json syntax. + * @property statResults the list of stats to transmit + * @property overallRank the overall rank of this player based on the total level + * @property overallExperience the overall experience of this player + * @property activityResults the list of activity results to transmit. + */ + public class SuccessfulHiscoreReply( + public val statResults: List, + public val overallRank: Int, + public val overallExperience: Long, + public val activityResults: List, + ) : HiscoreReplyResponse { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SuccessfulHiscoreReply + + if (statResults != other.statResults) return false + if (overallRank != other.overallRank) return false + if (overallExperience != other.overallExperience) return false + if (activityResults != other.activityResults) return false + + return true + } + + override fun hashCode(): Int { + var result = statResults.hashCode() + result = 31 * result + overallRank + result = 31 * result + overallExperience.hashCode() + result = 31 * result + activityResults.hashCode() + return result + } + + override fun toString(): String = + "SuccessfulHiscoreReply(" + + "statResults=$statResults, " + + "overallRank=$overallRank, " + + "overallExperience=$overallExperience, " + + "activityResults=$activityResults" + + ")" + } + + /** + * A failed hiscore reply would be sent when a lookup could not be + * performed successfully. The client will read a string for a reason + * when this occurs. + * @property reason the reason to give to the player for why + * the lookup could not be done. + */ + public class FailedHiscoreReply( + public val reason: String, + ) : HiscoreReplyResponse { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as FailedHiscoreReply + + return reason == other.reason + } + + override fun hashCode(): Int = reason.hashCode() + + override fun toString(): String = "FailedHiscoreReply(reason='$reason')" + } + + public class HiscoreResult( + public val id: Int, + public val rank: Int, + public val result: Int, + ) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HiscoreResult + + if (id != other.id) return false + if (rank != other.rank) return false + if (result != other.result) return false + + return true + } + + override fun hashCode(): Int { + var result1 = id + result1 = 31 * result1 + rank + result1 = 31 * result1 + result + return result1 + } + + override fun toString(): String = + "HiscoreResult(" + + "id=$id, " + + "rank=$rank, " + + "result=$result" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/MinimapToggle.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/MinimapToggle.kt new file mode 100644 index 000000000..f744a5765 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/MinimapToggle.kt @@ -0,0 +1,43 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Minimap toggle is used to modify the state of the minimap + * and the attached compass. + * + * Minimap states table: + * ``` + * | Id | Description | + * |----|:-------------------------------:| + * | 0 | Enabled | + * | 1 | Minimap unclickable | + * | 2 | Minimap hidden | + * | 3 | Compass hidden | + * | 4 | Map unclickable, compass hidden | + * | 5 | Disabled | + * ``` + * + * @property minimapState the minimap state to set (see table above) + */ +public class MinimapToggle( + public val minimapState: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MinimapToggle + + return minimapState == other.minimapState + } + + override fun hashCode(): Int = minimapState + + override fun toString(): String = "MinimapToggle(minimapState=$minimapState)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ReflectionChecker.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ReflectionChecker.kt new file mode 100644 index 000000000..fa8d02161 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ReflectionChecker.kt @@ -0,0 +1,273 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Reflection checker packet will attempt to use [java.lang.reflect] to + * perform a lookup or invocation on a method or field in the client, + * using information provided in this packet. + * These invocations/lookups may fail completely, which is fully supported, + * as various exceptions get caught and special return codes are provided + * in such cases. + * An important thing to note, however, is that the server is responsible + * for not requesting too much, as the client's reply packet has a var-byte + * size, meaning the entire reply for a reflection check must fit into 255 + * bytes or fewer. There is no protection against this. + * Additionally worth noting that the [InvokeMethod] variant, while very + * powerful, is not utilized in OldSchool, and is rather dangerous to + * invoke due to the aforementioned size limitation. + * + * @property id the id of the reflection check, sent back in the reply and + * used to link together the request and reply, which is needed to fully + * decode the respective replies. + * @property checks the list of reflection checks to perform. + */ +public class ReflectionChecker( + public val id: Int, + public val checks: List, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ReflectionChecker + + if (id != other.id) return false + if (checks != other.checks) return false + + return true + } + + override fun hashCode(): Int { + var result = id + result = 31 * result + checks.hashCode() + return result + } + + override fun toString(): String = + "ReflectionChecker(" + + "id=$id, " + + "checks=$checks" + + ")" + + public sealed interface ReflectionCheck + + /** + * Get field value is a reflection check which will aim to call the + * [java.lang.reflect.Field.getInt] function on the respective field. + * The value is submitted back in the reply, if a value was obtained. + * @property className the full class name in which the field exists. + * @property fieldName the name of the field in that class to look up. + */ + public class GetFieldValue( + public val className: String, + public val fieldName: String, + ) : ReflectionCheck { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GetFieldValue + + if (className != other.className) return false + if (fieldName != other.fieldName) return false + + return true + } + + override fun hashCode(): Int { + var result = className.hashCode() + result = 31 * result + fieldName.hashCode() + return result + } + + override fun toString(): String = + "GetFieldValue(" + + "className='$className', " + + "fieldName='$fieldName'" + + ")" + } + + /** + * Set field value aims to try to assign the provided int [value] to + * a field in the class. + * @property className the full class name in which the field exists. + * @property fieldName the name of the field in that class to look up. + * @property value the value to try to assign to the field. + */ + public class SetFieldValue( + public val className: String, + public val fieldName: String, + public val value: Int, + ) : ReflectionCheck { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetFieldValue + + if (className != other.className) return false + if (fieldName != other.fieldName) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = className.hashCode() + result = 31 * result + fieldName.hashCode() + result = 31 * result + value + return result + } + + override fun toString(): String = + "SetFieldValue(" + + "className='$className', " + + "fieldName='$fieldName', " + + "value=$value" + + ")" + } + + /** + * Get field modifiers aims to try to look up a given field's modifiers, + * if possible. + * @property className the full class name in which the field exists. + * @property fieldName the name of the field in that class to look up. + */ + public class GetFieldModifiers( + public val className: String, + public val fieldName: String, + ) : ReflectionCheck { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GetFieldModifiers + + if (className != other.className) return false + if (fieldName != other.fieldName) return false + + return true + } + + override fun hashCode(): Int { + var result = className.hashCode() + result = 31 * result + fieldName.hashCode() + return result + } + + override fun toString(): String = + "GetFieldModifiers(" + + "className='$className', " + + "fieldName='$fieldName'" + + ")" + } + + /** + * Invoke method check aims to try to invoke a function in a class + * with the provided parameters. The [parameterValues] are turned + * into an object using [java.io.ObjectInputStream.readObject] function. + * @property className the full name of the class in which the function lies. + * @property methodName the name of the function to invoke. + * @property parameterClasses the types of the parameters that the function takes. + * @property parameterValues the values to pass into the function, + * represented as a serialized byte array. + * @property returnClass the full name of the return type class + */ + public class InvokeMethod( + public val className: String, + public val methodName: String, + public val parameterClasses: List, + public val parameterValues: List, + public val returnClass: String, + ) : ReflectionCheck { + init { + require(parameterClasses.size == parameterValues.size) { + "Parameter classes and values must have an equal length: " + + "${parameterClasses.size}, ${parameterValues.size}" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as InvokeMethod + + if (className != other.className) return false + if (methodName != other.methodName) return false + if (parameterClasses != other.parameterClasses) return false + if (parameterValues != other.parameterValues) return false + if (returnClass != other.returnClass) return false + + return true + } + + override fun hashCode(): Int { + var result = className.hashCode() + result = 31 * result + methodName.hashCode() + result = 31 * result + parameterClasses.hashCode() + result = 31 * result + parameterValues.hashCode() + result = 31 * result + returnClass.hashCode() + return result + } + + override fun toString(): String = + "InvokeMethod(" + + "className='$className', " + + "methodName='$methodName', " + + "parameterClasses=$parameterClasses, " + + "parameterValues=$parameterValues, " + + "returnClass=$returnClass" + + ")" + } + + /** + * Get method modifiers will aim to try and look up a method's modifiers. + * @property className the full name of the class in which the function lies. + * @property methodName the name of the function to invoke. + * @property parameterClasses the types of the parameters that the function takes. + * @property returnClass the full name of the return type class + */ + public class GetMethodModifiers( + public val className: String, + public val methodName: String, + public val parameterClasses: List, + public val returnClass: String, + ) : ReflectionCheck { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GetMethodModifiers + + if (className != other.className) return false + if (methodName != other.methodName) return false + if (parameterClasses != other.parameterClasses) return false + if (returnClass != other.returnClass) return false + + return true + } + + override fun hashCode(): Int { + var result = className.hashCode() + result = 31 * result + methodName.hashCode() + result = 31 * result + parameterClasses.hashCode() + result = 31 * result + returnClass.hashCode() + return result + } + + override fun toString(): String = + "GetMethodModifiers(" + + "className='$className', " + + "methodName='$methodName', " + + "parameterClasses=$parameterClasses, " + + "returnClass=$returnClass" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ResetAnims.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ResetAnims.kt new file mode 100644 index 000000000..0d001538a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ResetAnims.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Reset anims message is used to reset the currently playing + * animation of all NPCs and players. This does not impact + * base animations (e.g. standing, walking). + * It is unclear what the purpose of this packet actually is. + */ +public data object ResetAnims : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SendPing.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SendPing.kt new file mode 100644 index 000000000..dd821be20 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SendPing.kt @@ -0,0 +1,47 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Send ping packet is used to request a ping response from the client. + * The client will send these [value1] and [value2] variables back to the + * server in exchange. + * These integer identifiers do not appear to have any known structure to + * them - they are not epoch time in any form. Seemingly random as the value + * can change drastically between different logins. + * @property value1 the first 32-bit integer identifier. + * @property value2 the second 32-bit integer identifier. + */ +public class SendPing( + public val value1: Int, + public val value2: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SendPing + + if (value1 != other.value1) return false + if (value2 != other.value2) return false + + return true + } + + override fun hashCode(): Int { + var result = value1 + result = 31 * result + value2 + return result + } + + override fun toString(): String = + "SendPing(" + + "value1=$value1, " + + "value2=$value2" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ServerTickEnd.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ServerTickEnd.kt new file mode 100644 index 000000000..ed9438ada --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/ServerTickEnd.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Server tick end packets are used by the C++ client + * for ground item settings, in order to decrement + * visible ground item's timers. Without it, all ground + * items' timers will remain frozen once dropped. + */ +public data object ServerTickEnd : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SetHeatmapEnabled.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SetHeatmapEnabled.kt new file mode 100644 index 000000000..443791c4a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SetHeatmapEnabled.kt @@ -0,0 +1,35 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Set heatmap enabled packet is used to either enable or + * disabled the heatmap, which is rendered over the + * world map in OldSchool. + * This packet utilizes high resolution coordinate info + * about all the players of the game through player info + * packet, so in order for it to properly function, + * high resolution information must be sent for everyone + * in the game. + */ +public class SetHeatmapEnabled( + public val enabled: Boolean, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetHeatmapEnabled + + return enabled == other.enabled + } + + override fun hashCode(): Int = enabled.hashCode() + + override fun toString(): String = "SetHeatmapEnabled(enabled=$enabled)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SiteSettings.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SiteSettings.kt new file mode 100644 index 000000000..e0ee09b4d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/SiteSettings.kt @@ -0,0 +1,31 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Site settings packet is used to identify the given client. + * The settings are sent as part of the URL when connecting to services + * or secure RuneScape URLs. + * @property settings the settings string to assign + */ +public class SiteSettings( + public val settings: String, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SiteSettings + + return settings == other.settings + } + + override fun hashCode(): Int = settings.hashCode() + + override fun toString(): String = "SiteSettings(settings='$settings')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UpdateRebootTimer.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UpdateRebootTimer.kt new file mode 100644 index 000000000..4af0f85c2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UpdateRebootTimer.kt @@ -0,0 +1,38 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update reboot timer is used to start the shut-down timer + * in preparation of an update. + * @property gameCycles the number of game cycles (600ms/gc) + * until the shut-down is complete. + * If the number is set to zero, any existing reboot timers + * will be cleared out. + * The maximum possible value is 65535, which is equal to just + * below 11 hours. + */ +public class UpdateRebootTimer( + public val gameCycles: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateRebootTimer + + return gameCycles == other.gameCycles + } + + override fun hashCode(): Int = gameCycles + + override fun toString(): String = + "UpdateRebootTimer(" + + "gameCycles=$gameCycles" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UpdateUid192.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UpdateUid192.kt new file mode 100644 index 000000000..0a9521171 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UpdateUid192.kt @@ -0,0 +1,33 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update UID 192 packed is used to update the random 192-bit + * id that is found in the random.dat file within the player's + * cache directory. + * The 192-bit UID will be accompanied by a 32-bit CRC of the + * block, which the client will verify before changing the + * contents of the random.dat file. + */ +public class UpdateUid192( + public val uid: ByteArray, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateUid192 + + return uid.contentEquals(other.uid) + } + + override fun hashCode(): Int = uid.contentHashCode() + + override fun toString(): String = "UpdateUid192(uid=${uid.contentToString()})" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UrlOpen.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UrlOpen.kt new file mode 100644 index 000000000..32dc8b5ca --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/client/UrlOpen.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.outgoing.misc.client + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * URL open packets are used to open a site on the target's default + * browser. + * @property url the url to connect to + */ +public class UrlOpen( + public val url: String, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UrlOpen + + return url == other.url + } + + override fun hashCode(): Int = url.hashCode() + + override fun toString(): String = "UrlOpen(url='$url')" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/ChatFilterSettings.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/ChatFilterSettings.kt new file mode 100644 index 000000000..21dfcd0f9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/ChatFilterSettings.kt @@ -0,0 +1,69 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Chat filter settings packed is used to set the public and + * trade chat filters to the specified values. + * + * Chat filters table: + * ``` + * | Id | Type | + * |----|:--------:| + * | 0 | On | + * | 1 | Friends | + * | 2 | Off | + * | 3 | Hide | + * | 4 | Autochat | + * ``` + * + * @property publicChatFilter the public chat filter value, allowed values + * include everything in the table above. + * @property tradeChatFilter the trade chat filter value, allowed values include + * 'On', 'Friends' and 'Off' (see table above) + */ +public class ChatFilterSettings private constructor( + private val _publicChatFilter: UByte, + private val _tradeChatFilter: UByte, +) : OutgoingGameMessage { + public constructor( + publicChatFilter: Int, + tradeChatFilter: Int, + ) : this( + publicChatFilter.toUByte(), + tradeChatFilter.toUByte(), + ) + + public val publicChatFilter: Int + get() = _publicChatFilter.toInt() + public val tradeChatFilter: Int + get() = _tradeChatFilter.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ChatFilterSettings + + if (_publicChatFilter != other._publicChatFilter) return false + if (_tradeChatFilter != other._tradeChatFilter) return false + + return true + } + + override fun hashCode(): Int { + var result = _publicChatFilter.hashCode() + result = 31 * result + _tradeChatFilter.hashCode() + return result + } + + override fun toString(): String = + "ChatFilterSettings(" + + "publicChatFilter=$publicChatFilter, " + + "tradeChatFilter=$tradeChatFilter" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/ChatFilterSettingsPrivateChat.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/ChatFilterSettingsPrivateChat.kt new file mode 100644 index 000000000..02c2e2b3b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/ChatFilterSettingsPrivateChat.kt @@ -0,0 +1,40 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Chat filter settings packed is used to set the private + * chat filter. + * + * Chat filters table: + * ``` + * | Id | Type | + * |----|:--------:| + * | 0 | On | + * | 1 | Friends | + * | 2 | Off | + * ``` + * + * @property privateChatFilter the private chat filter value. + */ +public class ChatFilterSettingsPrivateChat( + public val privateChatFilter: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ChatFilterSettingsPrivateChat + + return privateChatFilter == other.privateChatFilter + } + + override fun hashCode(): Int = privateChatFilter + + override fun toString(): String = "ChatFilterSettingsPrivateChat(privateChatFilter=$privateChatFilter)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/MessageGame.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/MessageGame.kt new file mode 100644 index 000000000..f64782c4e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/MessageGame.kt @@ -0,0 +1,119 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Message game packet is used to send a normal game message in + * the player's chatbox. + * + * Game message types (note: names without asterisk are official from a leak): + * ``` + * | Id | Type | + * |-----|:----------------------------------:| + * | 0 | chattype_gamemessage | + * | 1 | chattype_modchat | + * | 2 | chattype_publicchat | + * | 3 | chattype_privatechat | + * | 4 | chattype_engine | + * | 5 | chattype_loginlogoutnotification | + * | 6 | chattype_privatechatout | + * | 7 | chattype_modprivatechat | + * | 9 | chattype_friendschat | + * | 11 | chattype_friendschatnotification | + * | 14 | chattype_broadcast | + * | 26 | chattype_snapshotfeedback | + * | 27 | chattype_obj_examine | + * | 28 | chattype_npc_examine | + * | 29 | chattype_loc_examine | + * | 30 | chattype_friendnotification | + * | 31 | chattype_ignorenotification | + * | 41 | chattype_clan* | + * | 43 | chattype_clan_system* | + * | 44 | chattype_clan_guest* | + * | 46 | chattype_clan_guest_system* | + * | 90 | chattype_autotyper | + * | 91 | chattype_modautotyper | + * | 99 | chattype_console | + * | 101 | chattype_tradereq | + * | 102 | chattype_trade | + * | 103 | chattype_chalreq_trade | + * | 104 | chattype_chalreq_friendschat | + * | 105 | chattype_spam | + * | 106 | chattype_playerrelated | + * | 107 | chattype_10sectimeout | + * | 108 | chattype_welcome* | + * | 109 | chattype_clan_creation_invitation* | + * | 110 | chattype_clan_wars_challenge* | + * | 111 | chattype_gim_form_group* | + * | 112 | chattype_gim_group_with* | + * ``` + * + * @property type the type of the message to send (see table above) + * @property name the name of the target player who is making a request. + * This property is only for messages such as "X wishes to trade with you.", + * where there is a player at the other end that is making some sort of request. + * Upon interacting with these chat messages, the client will invoke the respective + * op-player packet if it can find that player in local player's high resolution + * list of players. + * It is important to note, however, that only opplayer 1, 4, 6 and 7 will ever + * be fired in this manner. + * @property message the message itself to render in the chatbox + */ +public class MessageGame private constructor( + private val _type: UShort, + public val name: String?, + public val message: String, +) : OutgoingGameMessage { + public constructor( + type: Int, + name: String?, + message: String, + ) : this( + type.toUShort(), + name, + message, + ) + + public constructor( + type: Int, + message: String, + ) : this( + type.toUShort(), + null, + message, + ) + + public val type: Int + get() = _type.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessageGame + + if (_type != other._type) return false + if (name != other.name) return false + if (message != other.message) return false + + return true + } + + override fun hashCode(): Int { + var result = _type.hashCode() + result = 31 * result + (name?.hashCode() ?: 0) + result = 31 * result + message.hashCode() + return result + } + + override fun toString(): String = + "MessageGame(" + + "type=$type, " + + "name=$name, " + + "message='$message'" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/RunClientScript.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/RunClientScript.kt new file mode 100644 index 000000000..23426d160 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/RunClientScript.kt @@ -0,0 +1,112 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.RSProtFlags +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Run clientscript packet is used to execute a clientscript in the client + * with the provided arguments. + * @property id the id of the script to invoke + * @property types the array of characters representing the clientscript types + * to send to the client. It is important to remember that all types which + * aren't `'s'` will be integer-based, with `'s'` being the only string-type. + * If the given value element cannot be cast to string/int respective + * to its type, an exception is thrown. + * @property values the list of int or string values to be sent to the + * client script. + */ +public class RunClientScript : OutgoingGameMessage { + public val id: Int + public val types: CharArray + public val values: List + + /** + * A primary constructor that allows ones to pass in the types from the server, in case one wishes + * to provide accurate types. The client however only cares whether the type is a string, or isn't a string, + * and the exact type values get discarded. + */ + public constructor( + id: Int, + types: CharArray, + values: List, + ) { + this.id = id + this.types = types + this.values = values + if (RSProtFlags.clientscriptVerification) { + require(types.size == values.size) { + "Types and values sizes must match: ${types.size}, ${values.size}" + } + for (i in types.indices) { + val type = types[i] + val value = values[i] + if (type == 's') { + require(value is String) { + "Expected string value at index $i for char $type, got: $value" + } + } else { + require(value is Int) { + "Expected int value at index $i for char $type, got: $value" + } + } + } + } + } + + /** + * A secondary constructor that allows one to only pass in the values and infer the types from the + * values. All values must be integer or string types, both of which can be mixed too. + * As client discards the actual types, there's no value in providing the exact type values to the + * client, and we can simply infer this the same way client reverses it. + */ + public constructor( + id: Int, + values: List, + ) { + this.id = id + this.values = values + this.types = + CharArray(values.size) { index -> + when (val value = values[index]) { + is Int -> 'i' + is String -> 's' + else -> throw IllegalArgumentException( + "Unknown clientscript value type: " + + "${value.javaClass} @ $value, accepted types only include integers and strings.", + ) + } + } + } + + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RunClientScript + + if (id != other.id) return false + if (!types.contentEquals(other.types)) return false + if (values != other.values) return false + + return true + } + + override fun hashCode(): Int { + var result = id + result = 31 * result + types.contentHashCode() + result = 31 * result + values.hashCode() + return result + } + + override fun toString(): String = + "RunClientScript(" + + "id=$id, " + + "types=${types.contentToString()}, " + + "values=$values" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/SetMapFlag.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/SetMapFlag.kt new file mode 100644 index 000000000..046a00714 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/SetMapFlag.kt @@ -0,0 +1,49 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInBuildArea +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Set map flag is used to set the red map flag on the minimap. + * Use values 255, 255 to remove the map flag. + * @property xInBuildArea the x coordinate within the build area + * to render the map flag at. + * @property zInBuildArea the z coordinate within the build area + * to render the map flag at. + */ +public class SetMapFlag private constructor( + private val coordInBuildArea: CoordInBuildArea, +) : OutgoingGameMessage { + public constructor( + xInBuildArea: Int, + zInBuildArea: Int, + ) : this( + CoordInBuildArea(xInBuildArea, zInBuildArea), + ) + + public val xInBuildArea: Int + get() = coordInBuildArea.xInBuildArea + public val zInBuildArea: Int + get() = coordInBuildArea.zInBuildArea + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetMapFlag + + return coordInBuildArea == other.coordInBuildArea + } + + override fun hashCode(): Int = coordInBuildArea.hashCode() + + override fun toString(): String = + "SetMapFlag(" + + "xInBuildArea=$xInBuildArea, " + + "zInBuildArea=$zInBuildArea" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/SetPlayerOp.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/SetPlayerOp.kt new file mode 100644 index 000000000..44219ae3a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/SetPlayerOp.kt @@ -0,0 +1,62 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Set player op packet is used to set the right-click + * option on all players to a specific option. + * @property id the id of the option to change, a value in range of + * 1 to 8 (inclusive) + * @property priority whether the option should get priority + * over the 'Walk here' option. + * @property op the option string to set, or null if removing an op. + */ +public class SetPlayerOp private constructor( + private val _id: UByte, + public val priority: Boolean, + public val op: String?, +) : OutgoingGameMessage { + public constructor( + id: Int, + priority: Boolean, + op: String?, + ) : this( + id.toUByte(), + priority, + op, + ) + + public val id: Int + get() = _id.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetPlayerOp + + if (_id != other._id) return false + if (priority != other.priority) return false + if (op != other.op) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + priority.hashCode() + result = 31 * result + op.hashCode() + return result + } + + override fun toString(): String = + "SetPlayerOp(" + + "id=$id, " + + "priority=$priority, " + + "op='$op'" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/TriggerOnDialogAbort.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/TriggerOnDialogAbort.kt new file mode 100644 index 000000000..1977bbdb7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/TriggerOnDialogAbort.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Trigger on dialog abort is used to invoke any ondialogabort + * scripts that have been set up on interfaces, typically to close + * any dialogues. + */ +public data object TriggerOnDialogAbort : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateRunEnergy.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateRunEnergy.kt new file mode 100644 index 000000000..10b5aef5b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateRunEnergy.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update runenergy packet is used to modify the player's current + * run energy. 100 units equals one percentage on the run orb, + * meaning a value of 10,000 is equal to 100% run energy. + */ +public class UpdateRunEnergy( + public val runenergy: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateRunEnergy + + return runenergy == other.runenergy + } + + override fun hashCode(): Int = runenergy + + override fun toString(): String = "UpdateRunEnergy(runenergy=$runenergy)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateRunWeight.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateRunWeight.kt new file mode 100644 index 000000000..cc7167fae --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateRunWeight.kt @@ -0,0 +1,29 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update runweight packet is used to modify the player's current + * equipment and inventory weight, in grams. + */ +public class UpdateRunWeight( + public val runweight: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateRunWeight + + return runweight == other.runweight + } + + override fun hashCode(): Int = runweight + + override fun toString(): String = "UpdateRunWeight(runweight=$runweight)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStat.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStat.kt new file mode 100644 index 000000000..7f82f3b18 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStat.kt @@ -0,0 +1,74 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update stat packet is used to set the current experience + * and levels of a skill for a given player. + * @property stat the id of the stat to update + * @property currentLevel player's current level in that stat, + * e.g. boosted or drained. + * @property invisibleBoostedLevel player's level in the stat + * with invisible boosts included + * @property experience player's experience in the skill, + * in its integer form - expected value range 0 to 200,000,000. + */ +public class UpdateStat private constructor( + private val _stat: UByte, + private val _currentLevel: UByte, + private val _invisibleBoostedLevel: UByte, + public val experience: Int, +) : OutgoingGameMessage { + public constructor( + stat: Int, + currentLevel: Int, + invisibleBoostedLevel: Int, + experience: Int, + ) : this( + stat.toUByte(), + currentLevel.toUByte(), + invisibleBoostedLevel.toUByte(), + experience, + ) + + public val stat: Int + get() = _stat.toInt() + public val currentLevel: Int + get() = _currentLevel.toInt() + public val invisibleBoostedLevel: Int + get() = _invisibleBoostedLevel.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateStat + + if (_stat != other._stat) return false + if (_currentLevel != other._currentLevel) return false + if (_invisibleBoostedLevel != other._invisibleBoostedLevel) return false + if (experience != other.experience) return false + + return true + } + + override fun hashCode(): Int { + var result = _stat.hashCode() + result = 31 * result + _currentLevel.hashCode() + result = 31 * result + _invisibleBoostedLevel.hashCode() + result = 31 * result + experience + return result + } + + override fun toString(): String = + "UpdateStat(" + + "stat=$stat, " + + "currentLevel=$currentLevel, " + + "invisibleBoostedLevel=$invisibleBoostedLevel, " + + "experience=$experience" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStatOld.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStatOld.kt new file mode 100644 index 000000000..ec973177d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStatOld.kt @@ -0,0 +1,65 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update stat old packet is used to set the current experience + * and levels of a skill for a given player, excluding the new + * invisible boosted level property. + * @property stat the id of the stat to update + * @property currentLevel player's current level in that stat, + * e.g. boosted or drained. + * @property experience player's experience in the skill, + * in its integer form - expected value range 0 to 200,000,000. + */ +public class UpdateStatOld private constructor( + private val _stat: UByte, + private val _currentLevel: UByte, + public val experience: Int, +) : OutgoingGameMessage { + public constructor( + stat: Int, + currentLevel: Int, + experience: Int, + ) : this( + stat.toUByte(), + currentLevel.toUByte(), + experience, + ) + + public val stat: Int + get() = _stat.toInt() + public val currentLevel: Int + get() = _currentLevel.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateStatOld + + if (_stat != other._stat) return false + if (_currentLevel != other._currentLevel) return false + if (experience != other.experience) return false + + return true + } + + override fun hashCode(): Int { + var result = _stat.hashCode() + result = 31 * result + _currentLevel.hashCode() + result = 31 * result + experience + return result + } + + override fun toString(): String = + "UpdateStat(" + + "stat=$stat, " + + "currentLevel=$currentLevel, " + + "experience=$experience" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStockMarketSlot.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStockMarketSlot.kt new file mode 100644 index 000000000..5d4d1233b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateStockMarketSlot.kt @@ -0,0 +1,130 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update stockmarket slot packet is used to set up + * an offer on the Grand Exchange, or to clear out an + * offer. + * @property update the update type to perform, either + * [ResetStockMarketSlot] or [SetStockMarketSlot]. + */ +public class UpdateStockMarketSlot private constructor( + private val _slot: UByte, + public val update: StockMarketUpdateType, +) : OutgoingGameMessage { + public constructor( + slot: Int, + update: StockMarketUpdateType, + ) : this( + slot.toUByte(), + update, + ) + + public val slot: Int + get() = _slot.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateStockMarketSlot + + return update == other.update + } + + override fun hashCode(): Int = update.hashCode() + + override fun toString(): String = + "UpdateStockMarketSlot(" + + "slot=$slot, " + + "update=$update" + + ")" + + public sealed interface StockMarketUpdateType + + public data object ResetStockMarketSlot : StockMarketUpdateType + + /** + * Set stockmarket slot update creates an offer + * on the Grand Exchange. + * @property status the status of the offer to create. + * Note that if the status value is 0, it will be treated + * as a request to clear out the slot and all the remaining + * data will be ignored in the process. + * @property obj the obj to set in the specified slot + * @property price the price per item + * @property count the count to buy or sell + * @property completedCount the amount already bought or sold + * @property completedGold the amount of gold received + */ + public class SetStockMarketSlot private constructor( + private val _status: Byte, + private val _obj: UShort, + public val price: Int, + public val count: Int, + public val completedCount: Int, + public val completedGold: Int, + ) : StockMarketUpdateType { + public constructor( + status: Int, + obj: Int, + price: Int, + count: Int, + completedCount: Int, + completedGold: Int, + ) : this( + status.toByte(), + obj.toUShort(), + price, + count, + completedCount, + completedGold, + ) + + public val status: Int + get() = _status.toInt() + public val obj: Int + get() = _obj.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetStockMarketSlot + + if (_status != other._status) return false + if (_obj != other._obj) return false + if (price != other.price) return false + if (count != other.count) return false + if (completedCount != other.completedCount) return false + if (completedGold != other.completedGold) return false + + return true + } + + override fun hashCode(): Int { + var result = _status.toInt() + result = 31 * result + _obj.hashCode() + result = 31 * result + price + result = 31 * result + count + result = 31 * result + completedCount + result = 31 * result + completedGold + return result + } + + override fun toString(): String = + "SetStockMarketSlot(" + + "status=$status, " + + "obj=$obj, " + + "price=$price, " + + "count=$count, " + + "completedCount=$completedCount, " + + "completedGold=$completedGold" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateTradingPost.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateTradingPost.kt new file mode 100644 index 000000000..77f443f2b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/misc/player/UpdateTradingPost.kt @@ -0,0 +1,155 @@ +package net.rsprot.protocol.game.outgoing.misc.player + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update trading post packed was used to create + * a list of offers of a specific obj in the trading + * post interface back when it still existed, in circa + * 2014. This packet has not had a use since then, however. + */ +public class UpdateTradingPost( + public val updateType: TradingPostUpdateType, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateTradingPost + + return updateType == other.updateType + } + + override fun hashCode(): Int = updateType.hashCode() + + override fun toString(): String = "UpdateTradingPost(updateType=$updateType)" + + public sealed interface TradingPostUpdateType + + public data object ResetTradingPost : TradingPostUpdateType + + public class SetTradingPostOfferList private constructor( + public val age: Long, + private val _obj: UShort, + public val status: Boolean, + public val offers: List, + ) : TradingPostUpdateType { + public constructor( + age: Long, + obj: Int, + status: Boolean, + offers: List, + ) : this( + age, + obj.toUShort(), + status, + offers, + ) { + require(offers.size <= 65535) { + "Offers size must fit in an unsigned short" + } + } + + public val obj: Int + get() = _obj.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetTradingPostOfferList + + if (age != other.age) return false + if (_obj != other._obj) return false + if (status != other.status) return false + if (offers != other.offers) return false + + return true + } + + override fun hashCode(): Int { + var result = age.hashCode() + result = 31 * result + _obj.hashCode() + result = 31 * result + status.hashCode() + result = 31 * result + offers.hashCode() + return result + } + + override fun toString(): String = + "SetTradingPostOfferList(" + + "age=$age, " + + "obj=$obj, " + + "status=$status, " + + "offers=$offers" + + ")" + } + + public class TradingPostOffer private constructor( + public val name: String, + public val previousName: String, + private val _world: UShort, + public val time: Long, + public val price: Int, + public val count: Int, + ) { + public constructor( + name: String, + previousName: String, + world: Int, + time: Long, + price: Int, + count: Int, + ) : this( + name, + previousName, + world.toUShort(), + time, + price, + count, + ) + + public val world: Int + get() = _world.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TradingPostOffer + + if (name != other.name) return false + if (previousName != other.previousName) return false + if (_world != other._world) return false + if (time != other.time) return false + if (price != other.price) return false + if (count != other.count) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + previousName.hashCode() + result = 31 * result + _world.hashCode() + result = 31 * result + time.hashCode() + result = 31 * result + price + result = 31 * result + count + return result + } + + override fun toString(): String = + "TradingPostOffer(" + + "name='$name', " + + "previousName='$previousName', " + + "world=$world, " + + "time=$time, " + + "price=$price, " + + "count=$count" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/FriendListLoaded.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/FriendListLoaded.kt new file mode 100644 index 000000000..0c49ace6e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/FriendListLoaded.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.game.outgoing.social + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Friend list loaded is used to mark the friend list + * as loaded if there are no friends to be sent. + * If there are friends to be sent, use the [UpdateFriendList] + * packet instead without this. + */ +public data object FriendListLoaded : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/MessagePrivate.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/MessagePrivate.kt new file mode 100644 index 000000000..1084eca69 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/MessagePrivate.kt @@ -0,0 +1,98 @@ +package net.rsprot.protocol.game.outgoing.social + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Message private packets are used to send private messages between + * players across multiple worlds. + * This specific packet results in the `From name: message` being shown + * on the target's client. + * @property sender name of the player who is sending the message + * @property worldId the id of the world from which the message is sent + * @property worldMessageCounter the world-local message counter. + * Each world must have its own message counter which is used to create + * a unique id for each message. This message counter must be + * incrementing with each message that is sent out. + * If two messages share the same unique id (which is a combination of + * the [worldId] and the [worldMessageCounter] properties), + * the client will not render the second message if it already has one + * received in the last 100 messages. + * It is additionally worth noting that servers with low population + * should probably not start the counter at the same value with each + * game boot, as the probability of multiple messages coinciding + * is relatively high in that scenario, given the low quantity of + * messages sent out to begin with. + * Additionally, only the first 24 bits of the counter are utilized, + * meaning a value from 0 to 16,777,215 (inclusive). + * A good starting point for message counting would be to take the + * hour of the year and multiply it by 50,000 when the server boots + * up. This means the roll-over happens roughly after every two weeks. + * Fine-tuning may be used to make it more granular, but the overall + * idea remains the same. + * @property chatCrownType the id of the crown to render next to the + * name of the sender. + * @property message the message to be forwarded to the recipient. + */ +public class MessagePrivate private constructor( + public val sender: String, + private val _worldId: UShort, + public val worldMessageCounter: Int, + private val _chatCrownType: UByte, + public val message: String, +) : OutgoingGameMessage { + public constructor( + sender: String, + worldId: Int, + worldMessageCounter: Int, + chatCrownType: Int, + message: String, + ) : this( + sender, + worldId.toUShort(), + worldMessageCounter, + chatCrownType.toUByte(), + message, + ) + + public val worldId: Int + get() = _worldId.toInt() + public val chatCrownType: Int + get() = _chatCrownType.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessagePrivate + + if (sender != other.sender) return false + if (_worldId != other._worldId) return false + if (worldMessageCounter != other.worldMessageCounter) return false + if (_chatCrownType != other._chatCrownType) return false + if (message != other.message) return false + + return true + } + + override fun hashCode(): Int { + var result = sender.hashCode() + result = 31 * result + _worldId.hashCode() + result = 31 * result + worldMessageCounter + result = 31 * result + _chatCrownType.hashCode() + result = 31 * result + message.hashCode() + return result + } + + override fun toString(): String = + "MessagePrivate(" + + "sender='$sender', " + + "worldId=$worldId, " + + "worldMessageCounter=$worldMessageCounter, " + + "chatCrownType=$chatCrownType, " + + "message='$message'" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/MessagePrivateEcho.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/MessagePrivateEcho.kt new file mode 100644 index 000000000..e3072f682 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/MessagePrivateEcho.kt @@ -0,0 +1,45 @@ +package net.rsprot.protocol.game.outgoing.social + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Message private echo is used to show the messages + * the given player has sent out to others, + * in a "To name: message" format. + * @property recipient the name of the player who received + * the private message. + * @property message the message to be forwarded. + */ +public class MessagePrivateEcho( + public val recipient: String, + public val message: String, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MessagePrivateEcho + + if (recipient != other.recipient) return false + if (message != other.message) return false + + return true + } + + override fun hashCode(): Int { + var result = recipient.hashCode() + result = 31 * result + message.hashCode() + return result + } + + override fun toString(): String = + "MessagePrivateEcho(" + + "recipient='$recipient', " + + "message='$message'" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/UpdateFriendList.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/UpdateFriendList.kt new file mode 100644 index 000000000..3818977f9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/UpdateFriendList.kt @@ -0,0 +1,262 @@ +package net.rsprot.protocol.game.outgoing.social + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update friendlist is used to send the initial friend list on login, + * as well as any additions to the friend list over time. + * @property friends the list of friends to be added/set to this friend list. + * For instances of this class, use [OnlineFriend] and [OfflineFriend] + * respectively. + */ +@Suppress("MemberVisibilityCanBePrivate") +public class UpdateFriendList( + public val friends: List, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateFriendList + + return friends == other.friends + } + + override fun hashCode(): Int = friends.hashCode() + + override fun toString(): String = "UpdateFriendList(friends=$friends)" + + public sealed interface Friend { + public val added: Boolean + public val name: String + public val previousName: String? + public val worldId: Int + public val rank: Int + public val properties: Int + public val notes: String + } + + /** + * Online friends are friends who are currently logged into the game. + * @property added whether the friend was just added to the friend list, + * or if it's an initial load. For initial loads, the client skips existing + * friend checks. + * @property name the display name of the friend + * @property previousName the previous display name of the friend, + * if they had one. If not, set it to null. + * @property worldId the world that the friend is logged into + * @property rank the friend's current rank, used to determine the chat icon + * @property properties a set of bitpacked properties; currently, the client + * only checks for two properties - [PROPERTY_REFERRED] and [PROPERTY_REFERRER]. + * These properties only affect the ordering of friends in the player's friend list. + * @property notes the notes on that friend. None of the clients use this value. + * @property worldName the name of the world the player is logged into, + * e.g. "Old School 35" for world 335 in OldSchool RuneScape. + * @property platform the id of the client the friend is logged into. + * Current known values include 0 for RuneScape 3, 4 for RS3's lobby (presumably), + * and 8 for OldSchool RuneScape. The OldSchool clients do not utilize this, + * its purpose is to prevent sending quick-chat messages from RuneScape 3 over + * to OldSchool RuneScape, as it does not support quick chat functionality. + * @property worldFlags the flags of the world the friend is logged into. + */ + public class OnlineFriend private constructor( + override val added: Boolean, + override val name: String, + override val previousName: String?, + private val _worldId: UShort, + private val _rank: UByte, + private val _properties: UByte, + override val notes: String, + public val worldName: String, + private val _platform: UByte, + public val worldFlags: Int, + ) : Friend { + public constructor( + added: Boolean, + name: String, + previousName: String?, + worldId: Int, + rank: Int, + properties: Int, + notes: String, + worldName: String, + platform: Int, + worldFlags: Int, + ) : this( + added, + name, + previousName, + worldId.toUShort(), + rank.toUByte(), + properties.toUByte(), + notes, + worldName, + platform.toUByte(), + worldFlags, + ) { + require(worldId > 0) { + "World id must be greater than 0 for online friends" + } + } + + override val worldId: Int + get() = _worldId.toInt() + override val rank: Int + get() = _rank.toInt() + override val properties: Int + get() = _properties.toInt() + public val platform: Int + get() = _platform.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OnlineFriend + + if (added != other.added) return false + if (name != other.name) return false + if (previousName != other.previousName) return false + if (_worldId != other._worldId) return false + if (_rank != other._rank) return false + if (_properties != other._properties) return false + if (notes != other.notes) return false + if (worldName != other.worldName) return false + if (_platform != other._platform) return false + if (worldFlags != other.worldFlags) return false + + return true + } + + override fun hashCode(): Int { + var result = added.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + (previousName?.hashCode() ?: 0) + result = 31 * result + _worldId.hashCode() + result = 31 * result + _rank.hashCode() + result = 31 * result + _properties.hashCode() + result = 31 * result + notes.hashCode() + result = 31 * result + worldName.hashCode() + result = 31 * result + _platform.hashCode() + result = 31 * result + worldFlags + return result + } + + override fun toString(): String = + "OnlineFriend(" + + "added=$added, " + + "name='$name', " + + "previousName=$previousName, " + + "worldId=$worldId, " + + "rank=$rank, " + + "properties=$properties, " + + "worldName='$worldName', " + + "platform=$platform, " + + "worldFlags=$worldFlags, " + + "notes='$notes'" + + ")" + } + + /** + * Offline friends are friends who either aren't logged in, or cannot be + * seen as online due to preferences chosen. + * @property added whether the friend was just added to the friend list, + * or if it's an initial load. For initial loads, the client skips existing + * friend checks. + * @property name the display name of the friend + * @property previousName the previous display name of the friend, + * if they had one. If not, set it to null. + * @property worldId the world that the friend is logged into + * @property rank the friend's current rank, used to determine the chat icon + * @property properties a set of bitpacked properties; currently, the client + * only checks for two properties - [PROPERTY_REFERRED] and [PROPERTY_REFERRER]. + * These properties only affect the ordering of friends in the player's friend list. + * @property notes the notes on that friend. None of the clients use this value. + */ + public class OfflineFriend private constructor( + override val added: Boolean, + override val name: String, + override val previousName: String?, + private val _rank: UByte, + private val _properties: UByte, + override val notes: String, + ) : Friend { + public constructor( + added: Boolean, + name: String, + previousName: String?, + rank: Int, + properties: Int, + notes: String, + ) : this( + added, + name, + previousName, + rank.toUByte(), + properties.toUByte(), + notes, + ) + + override val rank: Int + get() = _rank.toInt() + override val properties: Int + get() = _properties.toInt() + override val worldId: Int + get() = 0 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as OfflineFriend + + if (added != other.added) return false + if (name != other.name) return false + if (previousName != other.previousName) return false + if (_rank != other._rank) return false + if (_properties != other._properties) return false + if (notes != other.notes) return false + + return true + } + + override fun hashCode(): Int { + var result = added.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + (previousName?.hashCode() ?: 0) + result = 31 * result + _rank.hashCode() + result = 31 * result + _properties.hashCode() + result = 31 * result + notes.hashCode() + return result + } + + override fun toString(): String = + "OfflineFriend(" + + "added=$added, " + + "name='$name', " + + "previousName=$previousName, " + + "rank=$rank, " + + "properties=$properties, " + + "notes='$notes'" + + ")" + } + + public companion object { + /** + * Referred property is used to assign a higher priority to a friend + * in the friend list. + */ + public const val PROPERTY_REFERRED: Int = 0x1 + + /** + * Referred property is used to assign a higher priority to a friend + * in the friend list. Referrers have a higher priority than [PROPERTY_REFERRED]. + */ + public const val PROPERTY_REFERRER: Int = 0x2 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/UpdateIgnoreList.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/UpdateIgnoreList.kt new file mode 100644 index 000000000..d83046e75 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/social/UpdateIgnoreList.kt @@ -0,0 +1,107 @@ +package net.rsprot.protocol.game.outgoing.social + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update ignorelist is used to perform changes to the ignore list. + * Unlike friend list, it is possible to delete ignore list entries + * from the server's perspective. + * @property ignores the list of ignores to add or remove. + */ +public class UpdateIgnoreList( + public val ignores: List, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateIgnoreList + + return ignores == other.ignores + } + + override fun hashCode(): Int = ignores.hashCode() + + override fun toString(): String = "UpdateIgnoreList(ignores=$ignores)" + + public sealed interface IgnoredPlayer { + public val name: String + } + + /** + * Removed ignored entry is an ignored entry that is requested to be + * deleted from the ignore list of this player. + * @property name the name of the ignored player + */ + public class RemovedIgnoredEntry( + override val name: String, + ) : IgnoredPlayer { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RemovedIgnoredEntry + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() + + override fun toString(): String = "RemovedIgnoredEntry(name='$name')" + } + + /** + * Added ignore entry encompasses all the ignore list entries + * which are added to the ignore list, be that during login or + * individual additions of new entries. + * @property name the name of the player to be added to the ignore list + * @property previousName the previous name of that player, if they had any. + * Set to null if there is no previous name associated. + * @property note the note attached to this player. + * This property is not used in any of the OldSchool RuneScape clients. + * @property added whether the ignore list entry was just added, or if it's + * a historic entry sent during login. + * If the property is false, the client skips any existing name checks. + */ + public class AddedIgnoredEntry( + override val name: String, + public val previousName: String?, + public val note: String, + public val added: Boolean, + ) : IgnoredPlayer { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AddedIgnoredEntry + + if (name != other.name) return false + if (previousName != other.previousName) return false + if (note != other.note) return false + if (added != other.added) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + (previousName?.hashCode() ?: 0) + result = 31 * result + note.hashCode() + result = 31 * result + added.hashCode() + return result + } + + override fun toString(): String = + "AddedIgnoredEntry(" + + "name='$name', " + + "previousName=$previousName, " + + "note='$note', " + + "added=$added" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiJingle.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiJingle.kt new file mode 100644 index 000000000..ea4a8470f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiJingle.kt @@ -0,0 +1,65 @@ +package net.rsprot.protocol.game.outgoing.sound + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Midi jingle packet is used to play a short midi song, typically when + * the player accomplishes something. The normal song that was playing + * will be resumed after the jingle finishes playing. + * In the old days, the [lengthInMillis] property was used to tell the client + * how long the jingle lasts, so it knows when to resume the normal midi song. + * It has long since been removed, however - while the client expects a 24-bit + * integer for the length, it does not use this value in any way. + * @property id the id of the jingle to play + * @property lengthInMillis the length in milliseconds of the jingle, now unused. + */ +public class MidiJingle private constructor( + private val _id: UShort, + public val lengthInMillis: Int, +) : OutgoingGameMessage { + public constructor( + id: Int, + ) : this( + id.toUShort(), + 0, + ) + + public constructor( + id: Int, + lengthInMillis: Int, + ) : this( + id.toUShort(), + lengthInMillis, + ) + + public val id: Int + get() = _id.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MidiJingle + + if (_id != other._id) return false + if (lengthInMillis != other.lengthInMillis) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + lengthInMillis + return result + } + + override fun toString(): String = + "MidiJingle(" + + "id=$id, " + + "lengthInMillis=$lengthInMillis" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSong.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSong.kt new file mode 100644 index 000000000..dddbc4cac --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSong.kt @@ -0,0 +1,86 @@ +package net.rsprot.protocol.game.outgoing.sound + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Midi song packets are used to play songs through the music player. + * @property id the id of the midi song + * @property fadeOutDelay the delay in client cycles (20ms/cc) until the old song + * begins fading out. The default value for this, based on the old midi song packet, is 0. + * @property fadeOutSpeed the speed at which the old song fades out in client cycles (20ms/cc). + * The default value for this, based on the old midi song packet, is 60. + * @property fadeInDelay the delay until the new song begins playing, in client cycles (20ms/cc). + * The default value for this, based on the old midi song packet is 60. + * @property fadeInSpeed the speed at which the new song fades in, in client cycles (20ms/cc). + * The default value for this, based on the old midi song packet is 0. + */ +@Suppress("DuplicatedCode") +public class MidiSong private constructor( + private val _id: UShort, + private val _fadeOutDelay: UShort, + private val _fadeOutSpeed: UShort, + private val _fadeInDelay: UShort, + private val _fadeInSpeed: UShort, +) : OutgoingGameMessage { + public constructor( + id: Int, + fadeOutDelay: Int, + fadeOutSpeed: Int, + fadeInDelay: Int, + fadeInSpeed: Int, + ) : this( + id.toUShort(), + fadeOutDelay.toUShort(), + fadeOutSpeed.toUShort(), + fadeInDelay.toUShort(), + fadeInSpeed.toUShort(), + ) + + public val id: Int + get() = _id.toInt() + public val fadeOutDelay: Int + get() = _fadeOutDelay.toInt() + public val fadeOutSpeed: Int + get() = _fadeOutSpeed.toInt() + public val fadeInDelay: Int + get() = _fadeInDelay.toInt() + public val fadeInSpeed: Int + get() = _fadeInSpeed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MidiSong + + if (_id != other._id) return false + if (_fadeOutDelay != other._fadeOutDelay) return false + if (_fadeOutSpeed != other._fadeOutSpeed) return false + if (_fadeInDelay != other._fadeInDelay) return false + if (_fadeInSpeed != other._fadeInSpeed) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _fadeOutDelay.hashCode() + result = 31 * result + _fadeOutSpeed.hashCode() + result = 31 * result + _fadeInDelay.hashCode() + result = 31 * result + _fadeInSpeed.hashCode() + return result + } + + override fun toString(): String = + "MidiSong(" + + "id=$id, " + + "fadeOutDelay=$fadeOutDelay, " + + "fadeOutSpeed=$fadeOutSpeed, " + + "fadeInDelay=$fadeInDelay, " + + "fadeInSpeed=$fadeInSpeed" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongOld.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongOld.kt new file mode 100644 index 000000000..09c47fd54 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongOld.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.game.outgoing.sound + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Midi song old packet is used to play a midi song, in the old format. + * This is equal to playing [MidiSong] with the arguments of `id, 0, 60, 60, 0`. + * @property id the id of the song to play + */ +public class MidiSongOld( + public val id: Int, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MidiSongOld + + return id == other.id + } + + override fun hashCode(): Int = id + + override fun toString(): String = "MidiSongOld(id=$id)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongStop.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongStop.kt new file mode 100644 index 000000000..620731a78 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongStop.kt @@ -0,0 +1,54 @@ +package net.rsprot.protocol.game.outgoing.sound + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Midi song stop is used to stop playing an existing midi song. + * @property fadeOutDelay the delay in client cycles (20ms/cc) until the song begins fading out. + * @property fadeOutSpeed the speed at which the song fades out in client cycles (20ms/cc). + */ +public class MidiSongStop private constructor( + private val _fadeOutDelay: UShort, + private val _fadeOutSpeed: UShort, +) : OutgoingGameMessage { + public constructor( + fadeOutDelay: Int, + fadeOutSpeed: Int, + ) : this( + fadeOutDelay.toUShort(), + fadeOutSpeed.toUShort(), + ) + + public val fadeOutDelay: Int + get() = _fadeOutDelay.toInt() + public val fadeOutSpeed: Int + get() = _fadeOutSpeed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MidiSongStop + + if (_fadeOutDelay != other._fadeOutDelay) return false + if (_fadeOutSpeed != other._fadeOutSpeed) return false + + return true + } + + override fun hashCode(): Int { + var result = _fadeOutDelay.hashCode() + result = 31 * result + _fadeOutSpeed.hashCode() + return result + } + + override fun toString(): String = + "MidiSongStop(" + + "fadeOutDelay=$fadeOutDelay, " + + "fadeOutSpeed=$fadeOutSpeed" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongWithSecondary.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongWithSecondary.kt new file mode 100644 index 000000000..a3fdd7fb4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSongWithSecondary.kt @@ -0,0 +1,100 @@ +package net.rsprot.protocol.game.outgoing.sound + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Midi song packets are used to play songs through the music player. + * This packet pre-queues a secondary song which can be hot-swapped at any point. + * The intended use case here is to swap the song out mid-playing between identical + * songs that have different tones playing, e.g. a more up-beat vs a more somber song, + * while letting the song play on from where it was, rather than re-starting the song. + * @property primaryId the primary id of the song that will be playing + * @property secondaryId the secondary id that will play if the `MIDI_SWAP` packet + * is sent. + * @property fadeOutDelay the delay in client cycles (20ms/cc) until the old song + * begins fading out. The default value for this, based on the old midi song packet, is 0. + * @property fadeOutSpeed the speed at which the old song fades out in client cycles (20ms/cc). + * The default value for this, based on the old midi song packet, is 60. + * @property fadeInDelay the delay until the new song begins playing, in client cycles (20ms/cc). + * The default value for this, based on the old midi song packet is 60. + * @property fadeInSpeed the speed at which the new song fades in, in client cycles (20ms/cc). + * The default value for this, based on the old midi song packet is 0. + */ +@Suppress("DuplicatedCode") +public class MidiSongWithSecondary private constructor( + private val _primaryId: UShort, + private val _secondaryId: UShort, + private val _fadeOutDelay: UShort, + private val _fadeOutSpeed: UShort, + private val _fadeInDelay: UShort, + private val _fadeInSpeed: UShort, +) : OutgoingGameMessage { + public constructor( + primaryId: Int, + secondaryId: Int, + fadeOutDelay: Int, + fadeOutSpeed: Int, + fadeInDelay: Int, + fadeInSpeed: Int, + ) : this( + primaryId.toUShort(), + secondaryId.toUShort(), + fadeOutDelay.toUShort(), + fadeOutSpeed.toUShort(), + fadeInDelay.toUShort(), + fadeInSpeed.toUShort(), + ) + + public val primaryId: Int + get() = _primaryId.toInt() + public val secondaryId: Int + get() = _secondaryId.toInt() + public val fadeOutDelay: Int + get() = _fadeOutDelay.toInt() + public val fadeOutSpeed: Int + get() = _fadeOutSpeed.toInt() + public val fadeInDelay: Int + get() = _fadeInDelay.toInt() + public val fadeInSpeed: Int + get() = _fadeInSpeed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MidiSongWithSecondary + + if (_primaryId != other._primaryId) return false + if (_secondaryId != other._secondaryId) return false + if (_fadeOutDelay != other._fadeOutDelay) return false + if (_fadeOutSpeed != other._fadeOutSpeed) return false + if (_fadeInDelay != other._fadeInDelay) return false + if (_fadeInSpeed != other._fadeInSpeed) return false + + return true + } + + override fun hashCode(): Int { + var result = _primaryId.hashCode() + result = 31 * result + _secondaryId.hashCode() + result = 31 * result + _fadeOutDelay.hashCode() + result = 31 * result + _fadeOutSpeed.hashCode() + result = 31 * result + _fadeInDelay.hashCode() + result = 31 * result + _fadeInSpeed.hashCode() + return result + } + + override fun toString(): String = + "MidiSongWithSecondary(" + + "primaryId=$primaryId, " + + "secondaryId=$secondaryId, " + + "fadeOutSpeed=$fadeOutSpeed, " + + "fadeOutDelay=$fadeOutDelay, " + + "fadeInDelay=$fadeInDelay, " + + "fadeInSpeed=$fadeInSpeed" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSwap.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSwap.kt new file mode 100644 index 000000000..c1bf42ebd --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/MidiSwap.kt @@ -0,0 +1,76 @@ +package net.rsprot.protocol.game.outgoing.sound + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Midi swap packet allows one to hot-swap a song mid-playing with a different one + * that was pre-queued with the [MidiSongWithSecondary] packet. + * This hot-swapping only works if the secondary packet was used, as that defines + * the id of the secondary song to swap to. + * @property fadeOutDelay the delay in client cycles (20ms/cc) until the old song + * begins fading out. + * @property fadeOutSpeed the speed at which the old song fades out in client cycles (20ms/cc). + * @property fadeInDelay the delay until the new song begins playing, in client cycles (20ms/cc). + * @property fadeInSpeed the speed at which the new song fades in, in client cycles (20ms/cc). + */ +public class MidiSwap private constructor( + private val _fadeOutDelay: UShort, + private val _fadeOutSpeed: UShort, + private val _fadeInDelay: UShort, + private val _fadeInSpeed: UShort, +) : OutgoingGameMessage { + public constructor( + fadeOutDelay: Int, + fadeOutSpeed: Int, + fadeInDelay: Int, + fadeInSpeed: Int, + ) : this( + fadeOutDelay.toUShort(), + fadeOutSpeed.toUShort(), + fadeInDelay.toUShort(), + fadeInSpeed.toUShort(), + ) + + public val fadeOutDelay: Int + get() = _fadeOutDelay.toInt() + public val fadeOutSpeed: Int + get() = _fadeOutSpeed.toInt() + public val fadeInDelay: Int + get() = _fadeInDelay.toInt() + public val fadeInSpeed: Int + get() = _fadeInSpeed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MidiSwap + + if (_fadeOutDelay != other._fadeOutDelay) return false + if (_fadeOutSpeed != other._fadeOutSpeed) return false + if (_fadeInDelay != other._fadeInDelay) return false + if (_fadeInSpeed != other._fadeInSpeed) return false + + return true + } + + override fun hashCode(): Int { + var result = _fadeOutDelay.hashCode() + result = 31 * result + _fadeOutSpeed.hashCode() + result = 31 * result + _fadeInDelay.hashCode() + result = 31 * result + _fadeInSpeed.hashCode() + return result + } + + override fun toString(): String = + "MidiSwap(" + + "fadeOutDelay=$fadeOutDelay, " + + "fadeInDelay=$fadeInDelay, " + + "fadeInSpeed=$fadeInSpeed, " + + "fadeOutSpeed=$fadeOutSpeed" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/SynthSound.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/SynthSound.kt new file mode 100644 index 000000000..49975c72c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/sound/SynthSound.kt @@ -0,0 +1,63 @@ +package net.rsprot.protocol.game.outgoing.sound + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Synth sound is used to play a short sound effect locally for the given player. + * @property id the id of the sound effect to play + * @property loops the number of times to loop the sound effect + * @property delay the delay in client cycles (20ms/cc) until the sound effect begins playing + */ +public class SynthSound private constructor( + private val _id: UShort, + private val _loops: UByte, + private val _delay: UShort, +) : OutgoingGameMessage { + public constructor( + id: Int, + loops: Int, + delay: Int, + ) : this( + id.toUShort(), + loops.toUByte(), + delay.toUShort(), + ) + + public val id: Int + get() = _id.toInt() + public val loops: Int + get() = _loops.toInt() + public val delay: Int + get() = _delay.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SynthSound + + if (_id != other._id) return false + if (_loops != other._loops) return false + if (_delay != other._delay) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _loops.hashCode() + result = 31 * result + _delay.hashCode() + return result + } + + override fun toString(): String = + "SynthSound(" + + "id=$id, " + + "loops=$loops, " + + "delay=$delay" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/LocAnimSpecific.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/LocAnimSpecific.kt new file mode 100644 index 000000000..32768d869 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/LocAnimSpecific.kt @@ -0,0 +1,123 @@ +package net.rsprot.protocol.game.outgoing.specific + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInBuildArea +import net.rsprot.protocol.game.outgoing.zone.payload.util.LocProperties +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Loc anim specific packets are used to make a loc play an animation, + * specific to one player and not the entire world. + * @property id the id of the animation to play + * @property zoneX the x coordinate of the zone's south-western corner in the + * build area. + * @property xInZone the x coordinate of the loc within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zoneZ the z coordinate of the zone's south-western corner in the + * build area. + * @property zInZone the z coordinate of the loc within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property shape the shape of the loc, a value of 0 to 22 (inclusive) is expected. + * @property rotation the rotation of the loc, a value of 0 to 3 (inclusive) is expected. + * + * It should be noted that the [zoneX] and [zoneZ] coordinates are relative + * to the build area in their absolute form, not in their shifted zone form. + * If the player is at an absolute coordinate of 50, 40 within the build area(104x104), + * the expected coordinates to transmit here would be 48, 40, as that would + * point to the south-western corner of the zone in which the player is standing in. + * The client will add up the respective [zoneX] + [xInZone] properties together, + * along with [zoneZ] + [zInZone] to re-create the effects of a normal zone packet. + */ +public class LocAnimSpecific private constructor( + private val _id: UShort, + private val coordInBuildArea: CoordInBuildArea, + private val locProperties: LocProperties, +) : OutgoingGameMessage { + public constructor( + id: Int, + zoneX: Int, + xInZone: Int, + zoneZ: Int, + zInZone: Int, + shape: Int, + rotation: Int, + ) : this( + id.toUShort(), + CoordInBuildArea( + zoneX, + xInZone, + zoneZ, + zInZone, + ), + LocProperties(shape, rotation), + ) + + public constructor( + id: Int, + xInBuildArea: Int, + zInBuildArea: Int, + shape: Int, + rotation: Int, + ) : this( + id.toUShort(), + CoordInBuildArea( + xInBuildArea, + zInBuildArea, + ), + LocProperties(shape, rotation), + ) + + public val id: Int + get() = _id.toInt() + public val zoneX: Int + get() = coordInBuildArea.zoneX + public val xInZone: Int + get() = coordInBuildArea.xInZone + public val zoneZ: Int + get() = coordInBuildArea.zoneZ + public val zInZone: Int + get() = coordInBuildArea.zInZone + public val shape: Int + get() = locProperties.shape + public val rotation: Int + get() = locProperties.rotation + + public val coordInBuildAreaPacked: Int + get() = coordInBuildArea.packedMedium + public val locPropertiesPacked: Int + get() = locProperties.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LocAnimSpecific + + if (_id != other._id) return false + if (coordInBuildArea != other.coordInBuildArea) return false + if (locProperties != other.locProperties) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + coordInBuildArea.hashCode() + result = 31 * result + locProperties.hashCode() + return result + } + + override fun toString(): String = + "LocAnimSpecific(" + + "id=$id, " + + "zoneX=$zoneX, " + + "xInZone=$xInZone, " + + "zoneZ=$zoneZ, " + + "zInZone=$zInZone, " + + "shape=$shape, " + + "rotation=$rotation" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/MapAnimSpecific.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/MapAnimSpecific.kt new file mode 100644 index 000000000..7aadf54e3 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/MapAnimSpecific.kt @@ -0,0 +1,125 @@ +package net.rsprot.protocol.game.outgoing.specific + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInBuildArea +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Map anim specific is sent to play a graphical effect/spotanim on a tile, + * local to a single user, and not the entire world. + * @property id the id of the spotanim + * @property delay the delay in client cycles (20ms/cc) until the spotanim begins playing + * @property height the height at which the spotanim will play + * @property zoneX the x coordinate of the zone's south-western corner in the + * build area. + * @property xInZone the x coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zoneZ the z coordinate of the zone's south-western corner in the + * build area. + * @property zInZone the z coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * + * It should be noted that the [zoneX] and [zoneZ] coordinates are relative + * to the build area in their absolute form, not in their shifted zone form. + * If the player is at an absolute coordinate of 50, 40 within the build area(104x104), + * the expected coordinates to transmit here would be 48, 40, as that would + * point to the south-western corner of the zone in which the player is standing in. + * The client will add up the respective [zoneX] + [xInZone] properties together, + * along with [zoneZ] + [zInZone] to re-create the effects of a normal zone packet. + */ +public class MapAnimSpecific private constructor( + private val _id: UShort, + private val _delay: UShort, + private val _height: UByte, + private val coordInBuildArea: CoordInBuildArea, +) : OutgoingGameMessage { + public constructor( + id: Int, + delay: Int, + height: Int, + zoneX: Int, + xInZone: Int, + zoneZ: Int, + zInZone: Int, + ) : this( + id.toUShort(), + delay.toUShort(), + height.toUByte(), + CoordInBuildArea( + zoneX, + xInZone, + zoneZ, + zInZone, + ), + ) + + public constructor( + id: Int, + delay: Int, + height: Int, + xInBuildArea: Int, + zInBuildArea: Int, + ) : this( + id.toUShort(), + delay.toUShort(), + height.toUByte(), + CoordInBuildArea( + xInBuildArea, + zInBuildArea, + ), + ) + + public val id: Int + get() = _id.toInt() + public val delay: Int + get() = _delay.toInt() + public val height: Int + get() = _height.toInt() + public val zoneX: Int + get() = coordInBuildArea.zoneX + public val xInZone: Int + get() = coordInBuildArea.xInZone + public val zoneZ: Int + get() = coordInBuildArea.zoneZ + public val zInZone: Int + get() = coordInBuildArea.zInZone + + public val coordInBuildAreaPacked: Int + get() = coordInBuildArea.packedMedium + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MapAnimSpecific + + if (_id != other._id) return false + if (_delay != other._delay) return false + if (_height != other._height) return false + if (coordInBuildArea != other.coordInBuildArea) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _delay.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + coordInBuildArea.hashCode() + return result + } + + override fun toString(): String = + "MapAnimSpecific(" + + "id=$id, " + + "delay=$delay, " + + "height=$height, " + + "zoneX=$zoneX, " + + "xInZone=$xInZone, " + + "zoneZ=$zoneZ, " + + "zInZone=$zInZone" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcAnimSpecific.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcAnimSpecific.kt new file mode 100644 index 000000000..2fb8094b5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcAnimSpecific.kt @@ -0,0 +1,64 @@ +package net.rsprot.protocol.game.outgoing.specific + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Npc anim specifics are used to play an animation on a NPC for a specific player, + * and not the entire world. + * @property index the index of the npc in the world + * @property id the id of the animation + * @property delay the delay of the animation before it begins playing in client cycles (20ms/cc) + */ +public class NpcAnimSpecific private constructor( + private val _index: UShort, + private val _id: UShort, + private val _delay: UByte, +) : OutgoingGameMessage { + public constructor( + index: Int, + id: Int, + delay: Int, + ) : this( + index.toUShort(), + id.toUShort(), + delay.toUByte(), + ) + + public val index: Int + get() = _index.toInt() + public val id: Int + get() = _id.toInt() + public val delay: Int + get() = _delay.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NpcAnimSpecific + + if (_index != other._index) return false + if (_id != other._id) return false + if (_delay != other._delay) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + _id.hashCode() + result = 31 * result + _delay.hashCode() + return result + } + + override fun toString(): String = + "NpcAnimSpecific(" + + "index=$index, " + + "id=$id, " + + "delay=$delay" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcHeadIconSpecific.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcHeadIconSpecific.kt new file mode 100644 index 000000000..37dd9ec9b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcHeadIconSpecific.kt @@ -0,0 +1,90 @@ +package net.rsprot.protocol.game.outgoing.specific + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Npc head-icon specific packets are used to render a head icon over + * a given NPC to one user alone, and not the rest of the world. + * It is worth noting, however, that the head icon will only be set + * if the given NPC was already registered by the NPC INFO packet. + * If a given NPC is removed from the local view through NPC INFO, + * the head icon goes alongside, and will not be automatically + * restored should that NPC re-enter the local view. + * @property index the index of the npc in the world + * @property headIconSlot the slot of the head icon, a value of 0 to 7 (inclusive) + * @property spriteGroup the cache group id of the sprite. + * While the client reads a 32-bit integer for this value, the client + * does not allow for a value greater than 65535 to be used due to cache limitations, + * thus, in order to compress the packet even further, we also limit the id to a maximum + * of 65535. + * @property spriteIndex the index of the sprite within the sprite file in the cache. + * Note that this is not the id of the file in the cache group, as for sprites, this is always + * zero. Each sprite file itself defines a number of sprites - this is the index in that list + * of sprites. + * @throws IllegalArgumentException if the [headIconSlot] is not in range of 0 to 7 (inclusive) + */ +public class NpcHeadIconSpecific private constructor( + private val _index: UShort, + private val _headIconSlot: UByte, + private val _spriteGroup: UShort, + private val _spriteIndex: UShort, +) : OutgoingGameMessage { + public constructor( + index: Int, + headIconSlot: Int, + spriteGroup: Int, + spriteIndex: Int, + ) : this( + index.toUShort(), + headIconSlot.toUByte(), + spriteGroup.toUShort(), + spriteIndex.toUShort(), + ) { + require(headIconSlot in 0..<8) { + "Head icon slot must be in range of 0 to 7 (inclusive)" + } + } + + public val index: Int + get() = _index.toInt() + public val headIconSlot: Int + get() = _headIconSlot.toInt() + public val spriteGroup: Int + get() = _spriteGroup.toInt() + public val spriteIndex: Int + get() = _spriteIndex.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NpcHeadIconSpecific + + if (_index != other._index) return false + if (_headIconSlot != other._headIconSlot) return false + if (_spriteGroup != other._spriteGroup) return false + if (_spriteIndex != other._spriteIndex) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + _headIconSlot.hashCode() + result = 31 * result + _spriteGroup.hashCode() + result = 31 * result + _spriteIndex.hashCode() + return result + } + + override fun toString(): String = + "NpcHeadIconSpecific(" + + "index=$index, " + + "headIconSlot=$headIconSlot, " + + "spriteGroup=$spriteGroup, " + + "spriteIndex=$spriteIndex" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcSpotAnimSpecific.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcSpotAnimSpecific.kt new file mode 100644 index 000000000..d322c9ff8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/NpcSpotAnimSpecific.kt @@ -0,0 +1,83 @@ +package net.rsprot.protocol.game.outgoing.specific + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Npc spot-anim specific packets are used to play a spotanim on a NPC + * for a specific player and not the entire world. + * @property index the index of the NPC in the world + * @property id the id of the spotanim to play + * @property slot the slot of the spotanim + * @property height the height of the spotanim + * @property delay the delay of the spotanim in client cycles (20ms/cc) + */ +@Suppress("DuplicatedCode") +public class NpcSpotAnimSpecific private constructor( + private val _index: UShort, + private val _id: UShort, + private val _slot: UByte, + private val _height: UShort, + private val _delay: UShort, +) : OutgoingGameMessage { + public constructor( + index: Int, + id: Int, + slot: Int, + height: Int, + delay: Int, + ) : this( + index.toUShort(), + id.toUShort(), + slot.toUByte(), + height.toUShort(), + delay.toUShort(), + ) + + public val index: Int + get() = _index.toInt() + public val id: Int + get() = _id.toInt() + public val slot: Int + get() = _slot.toInt() + public val height: Int + get() = _height.toInt() + public val delay: Int + get() = _delay.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NpcSpotAnimSpecific + + if (_index != other._index) return false + if (_id != other._id) return false + if (_slot != other._slot) return false + if (_height != other._height) return false + if (_delay != other._delay) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + _id.hashCode() + result = 31 * result + _slot.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + _delay.hashCode() + return result + } + + override fun toString(): String = + "NpcSpotAnimSpecific(" + + "index=$index, " + + "id=$id, " + + "slot=$slot, " + + "height=$height, " + + "delay=$delay" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/PlayerAnimSpecific.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/PlayerAnimSpecific.kt new file mode 100644 index 000000000..e0cbb6ff6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/PlayerAnimSpecific.kt @@ -0,0 +1,57 @@ +package net.rsprot.protocol.game.outgoing.specific + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Player anim specifics are used to play an animation on the local player for the local player, + * not the entire world. + * Note that unlike most other packets, this one does not provide the index, so it can only + * be played on the local player and no one else. + * @property id the id of the animation + * @property delay the delay of the animation before it begins playing in client cycles (20ms/cc) + */ +public class PlayerAnimSpecific private constructor( + private val _id: UShort, + private val _delay: UByte, +) : OutgoingGameMessage { + public constructor( + id: Int, + delay: Int, + ) : this( + id.toUShort(), + delay.toUByte(), + ) + + public val id: Int + get() = _id.toInt() + public val delay: Int + get() = _delay.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlayerAnimSpecific + + if (_id != other._id) return false + if (_delay != other._delay) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _delay.hashCode() + return result + } + + override fun toString(): String = + "PlayerAnimSpecific(" + + "id=$id, " + + "delay=$delay" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/PlayerSpotAnimSpecific.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/PlayerSpotAnimSpecific.kt new file mode 100644 index 000000000..68eb20ed1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/PlayerSpotAnimSpecific.kt @@ -0,0 +1,83 @@ +package net.rsprot.protocol.game.outgoing.specific + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Npc spot-anim specific packets are used to play a spotanim on a player + * for a specific player and not the entire world. + * @property index the index of the player in the world + * @property id the id of the spotanim to play + * @property slot the slot of the spotanim + * @property height the height of the spotanim + * @property delay the delay of the spotanim in client cycles (20ms/cc) + */ +@Suppress("DuplicatedCode") +public class PlayerSpotAnimSpecific private constructor( + private val _index: UShort, + private val _id: UShort, + private val _slot: UByte, + private val _height: UShort, + private val _delay: UShort, +) : OutgoingGameMessage { + public constructor( + index: Int, + id: Int, + slot: Int, + height: Int, + delay: Int, + ) : this( + index.toUShort(), + id.toUShort(), + slot.toUByte(), + height.toUShort(), + delay.toUShort(), + ) + + public val index: Int + get() = _index.toInt() + public val id: Int + get() = _id.toInt() + public val slot: Int + get() = _slot.toInt() + public val height: Int + get() = _height.toInt() + public val delay: Int + get() = _delay.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PlayerSpotAnimSpecific + + if (_index != other._index) return false + if (_id != other._id) return false + if (_slot != other._slot) return false + if (_height != other._height) return false + if (_delay != other._delay) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + _id.hashCode() + result = 31 * result + _slot.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + _delay.hashCode() + return result + } + + override fun toString(): String = + "PlayerSpotAnimSpecific(" + + "index=$index, " + + "id=$id, " + + "slot=$slot, " + + "height=$height, " + + "delay=$delay" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/ProjAnimSpecific.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/ProjAnimSpecific.kt new file mode 100644 index 000000000..866214c91 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/specific/ProjAnimSpecific.kt @@ -0,0 +1,300 @@ +package net.rsprot.protocol.game.outgoing.specific + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInBuildArea +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Proj anim specific packets are used to send a projectile for a specific user, + * without anyone else in the world seeing it. + * Unlike the regular [net.rsprot.protocol.game.outgoing.zone.payload.MapProjAnim] + * zone packet, this packet does not support transmitting the source index. + + * @property id the id of the spotanim that is this projectile + * @property startHeight the height of the projectile as it begins flying + * @property endHeight the height of the projectile as it finishes flying + * @property startTime the start time in client cycles (20ms/cc) until the + * projectile begins moving + * @property endTime the end time in client cycles (20ms/cc) until the + * projectile arrives at its destination + * @property angle the angle that the projectile takes during its flight + * @property progress the fine coord progress that the projectile + * has made before it begins flying. If the value is 0, the projectile begins flying + * at the defined start coordinate. For every 128 units of value, the projectile + * is moved 1 game square towards the end position. Interpolate between 0-128 for + * units smaller than 1 game square. + * This is commonly set to 128 to make a projectile appear as if it's flying + * straight down, as the projectile will not render if its defined start and + * end coords are equal. So, in order to avoid that, one solution is to put the + * end coordinate 1 game square away from the start in a cardinal direction, + * and set the value of this property to 128 - ensuring that the projectile + * will appear to fly completely vertically, with no horizontal movement whatsoever. + * In the event inspector, this property is called 'distanceOffset'. + * @property sourceIndex the index of the pathing entity from whom the projectile is shot. + * If the value is 0, the projectile will not be locked to any source entity. + * If the source avatar is a player, add 0x10000 to the real index value (0-2048). + * If the source avatar is a NPC, set the index as it is. + * @property targetIndex the index of the pathing entity at whom the projectile is shot. + * If the value is 0, the projectile will not be locked to any target entity. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * @property zoneX the x coordinate of the zone's south-western corner in the + * build area. + * @property xInZone the start x coordinate of the projectile within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zoneZ the z coordinate of the zone's south-western corner in the + * build area. + * @property zInZone the start z coordinate of the projectile within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property deltaX the x coordinate delta that the projectile will move to + * relative to the starting position. + * @property deltaZ the z coordinate delta that the projectile will move to + * relative to the starting position. + * + * It should be noted that the [zoneX] and [zoneZ] coordinates are relative + * to the build area in their absolute form, not in their shifted zone form. + * If the player is at an absolute coordinate of 50, 40 within the build area(104x104), + * the expected coordinates to transmit here would be 48, 40, as that would + * point to the south-western corner of the zone in which the player is standing in. + * The client will add up the respective [zoneX] + [xInZone] properties together, + * along with [zoneZ] + [zInZone] to re-create the effects of a normal zone packet. + */ +@Suppress("DuplicatedCode") +public class ProjAnimSpecific private constructor( + private val _id: UShort, + private val _startHeight: UByte, + private val _endHeight: UByte, + private val _startTime: UShort, + private val _endTime: UShort, + private val _angle: UByte, + private val _progress: UShort, + public val sourceIndex: Int, + public val targetIndex: Int, + private val coordInBuildArea: CoordInBuildArea, + private val _deltaX: Byte, + private val _deltaZ: Byte, +) : OutgoingGameMessage { + public constructor( + id: Int, + startHeight: Int, + endHeight: Int, + startTime: Int, + endTime: Int, + angle: Int, + progress: Int, + targetIndex: Int, + zoneX: Int, + xInZone: Int, + zoneZ: Int, + zInZone: Int, + deltaX: Int, + deltaZ: Int, + ) : this( + id.toUShort(), + startHeight.toUByte(), + endHeight.toUByte(), + startTime.toUShort(), + endTime.toUShort(), + angle.toUByte(), + progress.toUShort(), + 0, + targetIndex, + CoordInBuildArea( + zoneX, + xInZone, + zoneZ, + zInZone, + ), + deltaX.toByte(), + deltaZ.toByte(), + ) + + public constructor( + id: Int, + startHeight: Int, + endHeight: Int, + startTime: Int, + endTime: Int, + angle: Int, + progress: Int, + sourceIndex: Int, + targetIndex: Int, + zoneX: Int, + xInZone: Int, + zoneZ: Int, + zInZone: Int, + deltaX: Int, + deltaZ: Int, + ) : this( + id.toUShort(), + startHeight.toUByte(), + endHeight.toUByte(), + startTime.toUShort(), + endTime.toUShort(), + angle.toUByte(), + progress.toUShort(), + sourceIndex, + targetIndex, + CoordInBuildArea( + zoneX, + xInZone, + zoneZ, + zInZone, + ), + deltaX.toByte(), + deltaZ.toByte(), + ) + + public constructor( + id: Int, + startHeight: Int, + endHeight: Int, + startTime: Int, + endTime: Int, + angle: Int, + progress: Int, + targetIndex: Int, + xInBuildArea: Int, + zInBuildArea: Int, + deltaX: Int, + deltaZ: Int, + ) : this( + id.toUShort(), + startHeight.toUByte(), + endHeight.toUByte(), + startTime.toUShort(), + endTime.toUShort(), + angle.toUByte(), + progress.toUShort(), + 0, + targetIndex, + CoordInBuildArea( + xInBuildArea, + zInBuildArea, + ), + deltaX.toByte(), + deltaZ.toByte(), + ) + + public constructor( + id: Int, + startHeight: Int, + endHeight: Int, + startTime: Int, + endTime: Int, + angle: Int, + progress: Int, + sourceIndex: Int, + targetIndex: Int, + xInBuildArea: Int, + zInBuildArea: Int, + deltaX: Int, + deltaZ: Int, + ) : this( + id.toUShort(), + startHeight.toUByte(), + endHeight.toUByte(), + startTime.toUShort(), + endTime.toUShort(), + angle.toUByte(), + progress.toUShort(), + sourceIndex, + targetIndex, + CoordInBuildArea( + xInBuildArea, + zInBuildArea, + ), + deltaX.toByte(), + deltaZ.toByte(), + ) + + public val id: Int + get() = _id.toInt() + public val startHeight: Int + get() = _startHeight.toInt() + public val endHeight: Int + get() = _endHeight.toInt() + public val startTime: Int + get() = _startTime.toInt() + public val endTime: Int + get() = _endTime.toInt() + public val angle: Int + get() = _angle.toInt() + public val progress: Int + get() = _progress.toInt() + public val zoneX: Int + get() = coordInBuildArea.zoneX + public val xInZone: Int + get() = coordInBuildArea.xInZone + public val zoneZ: Int + get() = coordInBuildArea.zoneZ + public val zInZone: Int + get() = coordInBuildArea.zInZone + public val deltaX: Int + get() = _deltaX.toInt() + public val deltaZ: Int + get() = _deltaZ.toInt() + + public val coordInBuildAreaPacked: Int + get() = coordInBuildArea.packedMedium + override val category: ServerProtCategory + get() = GameServerProtCategory.LOW_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ProjAnimSpecific + + if (_id != other._id) return false + if (_startHeight != other._startHeight) return false + if (_endHeight != other._endHeight) return false + if (_startTime != other._startTime) return false + if (_endTime != other._endTime) return false + if (_angle != other._angle) return false + if (_progress != other._progress) return false + if (sourceIndex != other.sourceIndex) return false + if (targetIndex != other.targetIndex) return false + if (coordInBuildArea != other.coordInBuildArea) return false + if (_deltaX != other._deltaX) return false + if (_deltaZ != other._deltaZ) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _startHeight.hashCode() + result = 31 * result + _endHeight.hashCode() + result = 31 * result + _startTime.hashCode() + result = 31 * result + _endTime.hashCode() + result = 31 * result + _angle.hashCode() + result = 31 * result + _progress.hashCode() + result = 31 * result + sourceIndex + result = 31 * result + targetIndex + result = 31 * result + coordInBuildArea.hashCode() + result = 31 * result + _deltaX + result = 31 * result + _deltaZ + return result + } + + override fun toString(): String = + "ProjAnimSpecific(" + + "id=$id, " + + "startHeight=$startHeight, " + + "endHeight=$endHeight, " + + "startTime=$startTime, " + + "endTime=$endTime, " + + "angle=$angle, " + + "progress=$progress, " + + "sourceIndex=$sourceIndex, " + + "targetIndex=$targetIndex, " + + "zoneX=$zoneX, " + + "xInZone=$xInZone, " + + "zoneZ=$zoneZ, " + + "zInZone=$zInZone, " + + "deltaX=$deltaX, " + + "deltaZ=$deltaZ" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/util/OpFlags.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/util/OpFlags.kt new file mode 100644 index 000000000..38054107a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/util/OpFlags.kt @@ -0,0 +1,72 @@ +package net.rsprot.protocol.game.outgoing.util + +/** + * Op flags are used to hide or show certain right-click options on various + * interactable entities. + * This is a helper class to create + */ +@Suppress("MemberVisibilityCanBePrivate") +@JvmInline +public value class OpFlags( + private val packed: Byte, +) { + public constructor( + op1: Boolean, + op2: Boolean, + op3: Boolean, + op4: Boolean, + op5: Boolean, + ) : this( + toInt(op1) + .or(toInt(op2) shl 1) + .or(toInt(op3) shl 2) + .or(toInt(op4) shl 3) + .or(toInt(op5) shl 4) + .toByte(), + ) + + public val value: Int + get() = packed.toInt() + + /** + * Checks if an option is enabled. + * @param op the id of the option, in range of 1 to 5 (inclusive). + * @return whether the given option is enabled. + * @throws IllegalArgumentException if the option is out of bounds (not in range of 1..5) + */ + public fun isEnabled(op: Int): Boolean { + require(op in 1..5) { + "Unexpected op value: $op" + } + val index = op - 1 + val flag = 1 shl index + return packed.toInt() and flag != 0 + } + + override fun toString(): String = + "OpFlags(" + + "op1=${isEnabled(1)}, " + + "op2=${isEnabled(2)}, " + + "op3=${isEnabled(3)}, " + + "op4=${isEnabled(4)}, " + + "op5=${isEnabled(5)}" + + ")" + + public companion object { + /** + * A constant flag for 'show all options' on an entity. + */ + public val ALL_SHOWN: OpFlags = OpFlags(-1) + + /** + * A constant flag for 'show no options' on an entity. + */ + public val NONE_SHOWN: OpFlags = OpFlags(0) + + /** + * Turns the boolean to an integer. + * @return 1 if the boolean is enabled, 0 otherwise. + */ + private fun toInt(value: Boolean): Int = if (value) 1 else 0 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpLarge.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpLarge.kt new file mode 100644 index 000000000..7e639fd74 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpLarge.kt @@ -0,0 +1,56 @@ +package net.rsprot.protocol.game.outgoing.varp + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Varp small messages are used to send a varp to the client that + * has a value which does not fit in the range of a byte, being -128..127. + * For values which do fit in the aforementioned range, the [VarpSmall] + * packed is preferred as it takes up less bandwidth, although nothing + * prevents one from sending all varps using this variant. + * @property id the id of the varp + * @property value the value of the varp + */ +public class VarpLarge private constructor( + private val _id: UShort, + public val value: Int, +) : OutgoingGameMessage { + public constructor( + id: Int, + value: Int, + ) : this( + id.toUShort(), + value, + ) + + public val id: Int + get() = _id.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as VarpLarge + + if (_id != other._id) return false + if (value != other.value) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + value + return result + } + + override fun toString(): String = + "VarpLarge(" + + "id=$id, " + + "value=$value" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpReset.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpReset.kt new file mode 100644 index 000000000..df573e40a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpReset.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.game.outgoing.varp + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * The varp reset packet is used to set the values of every + * varplayer type to 0. + * It is worth noting that the client will only reset the varps + * up until the last one which has a respective cache config. + * So if the varps array is extended, but respective configs + * are not made, the extended ones will not be zero'd out. + */ +public data object VarpReset : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpSmall.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpSmall.kt new file mode 100644 index 000000000..ba75ba8f1 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpSmall.kt @@ -0,0 +1,57 @@ +package net.rsprot.protocol.game.outgoing.varp + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Varp small messages are used to send a varp to the client that + * has a value which fits in the range of a byte, being -128..127. + * Note that this class does not verify that the value is in the correct + * range - instead any bits beyond the range of a byte get ignored. + * @property id the id of the varp + * @property value the value of the varp, in range of -128 to 127 (inclusive) + */ +public class VarpSmall private constructor( + private val _id: UShort, + private val _value: Byte, +) : OutgoingGameMessage { + public constructor( + id: Int, + value: Int, + ) : this( + id.toUShort(), + value.toByte(), + ) + + public val id: Int + get() = _id.toInt() + public val value: Int + get() = _value.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as VarpSmall + + if (_id != other._id) return false + if (_value != other._value) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _value + return result + } + + override fun toString(): String = + "VarpSmall(" + + "id=$id, " + + "value=$value" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpSync.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpSync.kt new file mode 100644 index 000000000..9b4235dad --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/varp/VarpSync.kt @@ -0,0 +1,19 @@ +package net.rsprot.protocol.game.outgoing.varp + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * The varp sync packet is used to synchronize the client's cache + * of varps back up with the server's version. + * + * The client keeps two int arrays for varps one that it modifies, + * and one that is a perfect replica of what the server has sent. + * This packet provides a means to sync the modified variant up + * with what the server has sent. + */ +public data object VarpSync : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/worldentity/ClearEntities.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/worldentity/ClearEntities.kt new file mode 100644 index 000000000..08468a7b8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/worldentity/ClearEntities.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.game.outgoing.worldentity + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Clear entities packet is used to clear any NPCs and world entities from the currently + * active world. This furthermore sets the active world back to root. + * This packet will not clear out any players, so the player info related to that world must + * still be used to transfer players over. + */ +public data object ClearEntities : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/worldentity/SetActiveWorld.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/worldentity/SetActiveWorld.kt new file mode 100644 index 000000000..80ea73b36 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/worldentity/SetActiveWorld.kt @@ -0,0 +1,126 @@ +package net.rsprot.protocol.game.outgoing.worldentity + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Set active world packet is used to set the currently active world in the client, + * allowing for various world-specific packets to perform changes to a different world + * than the usual root. + * Packets such as zone updates, player info, NPC info are a few examples of what may be sent afterwards. + * @property worldType the world type to update next. + */ +public class SetActiveWorld( + public val worldType: WorldType, +) : OutgoingGameMessage { + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SetActiveWorld + + return worldType == other.worldType + } + + override fun hashCode(): Int = worldType.hashCode() + + override fun toString(): String = "SetActiveWorld(worldType=$worldType)" + + /** + * A world type to set as the currently active world, allowing for updates + * to be done to that specific world. + */ + public sealed interface WorldType + + /** + * The root world type, resetting currently world to the main one. + * @property activeLevel the level at which various events will take place, such as + * zone updates. + */ + public class RootWorldType private constructor( + private val _activeLevel: UByte, + ) : WorldType { + public constructor(activeLevel: Int) : this(activeLevel.toUByte()) { + require(activeLevel in 0..<4) { + "Active level must be in range of 0..<4" + } + } + + public val activeLevel: Int + get() = _activeLevel.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RootWorldType + + return _activeLevel == other._activeLevel + } + + override fun hashCode(): Int = _activeLevel.hashCode() + + override fun toString(): String = "RootWorldType(activeLevel=$activeLevel)" + } + + /** + * A dynamic world type is used to mark one of the world entities' worlds as + * the active world, allowing for changes to be sent to that world entity. + * @property index the index of the world entity whose world is about to be updated, + * in range of 0..<2048. + * @property activeLevel the level at which various events will take place, such as + * zone updates. + */ + public class DynamicWorldType private constructor( + private val _index: UShort, + private val _activeLevel: UByte, + ) : WorldType { + public constructor( + index: Int, + activeLevel: Int, + ) : this( + index.toUShort(), + activeLevel.toUByte(), + ) { + require(index in 0..<2048) { + "Index must be in range of 0..<2048" + } + require(activeLevel in 0..<4) { + "Active level must be in range of 0..<4" + } + } + + public val index: Int + get() = _index.toInt() + public val activeLevel: Int + get() = _activeLevel.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DynamicWorldType + + if (_index != other._index) return false + if (_activeLevel != other._activeLevel) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + _activeLevel.hashCode() + return result + } + + override fun toString(): String = + "DynamicWorldType(" + + "index=$index, " + + "activeLevel=$activeLevel" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZoneFullFollows.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZoneFullFollows.kt new file mode 100644 index 000000000..538b1c3fe --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZoneFullFollows.kt @@ -0,0 +1,77 @@ +package net.rsprot.protocol.game.outgoing.zone.header + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update zone full-follows packets are used to clear a zone (8x8x1 tiles space) + * from any modifications done to it prior, wiping any obj and loc changes in + * the process. This packet additionally sets the 'current zone pointer' to this + * zone, allowing one to follow it with any other zone payload packet, commonly + * used to synchronize the zone to the observer (restoring all the objs in it, + * loc changes and so on). + * @property zoneX the x coordinate of the zone's south-western corner in the + * build area. + * @property zoneZ the z coordinate of the zone's south-western corner in the + * build area. + * @property level the height level of the zone, typically equal to the player's + * own height level. + * + * It should be noted that the [zoneX] and [zoneZ] coordinates are relative + * to the build area in their absolute form, not in their shifted zone form. + * If the player is at an absolute coordinate of 50, 40 within the build area(104x104), + * the expected coordinates to transmit here would be 48, 40, as that would + * point to the south-western corner of the zone in which the player is standing in. + */ +public class UpdateZoneFullFollows private constructor( + private val _zoneX: UByte, + private val _zoneZ: UByte, + private val _level: UByte, +) : OutgoingGameMessage { + public constructor( + zoneX: Int, + zoneZ: Int, + level: Int, + ) : this( + zoneX.toUByte(), + zoneZ.toUByte(), + level.toUByte(), + ) + + public val zoneX: Int + get() = _zoneX.toInt() + public val zoneZ: Int + get() = _zoneZ.toInt() + public val level: Int + get() = _level.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateZoneFullFollows + + if (_zoneX != other._zoneX) return false + if (_zoneZ != other._zoneZ) return false + if (_level != other._level) return false + + return true + } + + override fun hashCode(): Int { + var result = _zoneX.hashCode() + result = 31 * result + _zoneZ.hashCode() + result = 31 * result + _level.hashCode() + return result + } + + override fun toString(): String = + "UpdateZoneFullFollows(" + + "zoneX=$zoneX, " + + "zoneZ=$zoneZ, " + + "level=$level" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZonePartialEnclosed.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZonePartialEnclosed.kt new file mode 100644 index 000000000..5117a1d10 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZonePartialEnclosed.kt @@ -0,0 +1,78 @@ +package net.rsprot.protocol.game.outgoing.zone.header + +import io.netty.buffer.ByteBuf +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update zone partial-enclosed is used to send a batch of updates for a given + * zone all in one packet. This results in less bandwidth being used, as well as + * avoiding the client limitations of 100 packets/client cycle (20ms/cc). + * @property zoneX the x coordinate of the zone's south-western corner in the + * build area. + * @property zoneZ the z coordinate of the zone's south-western corner in the + * build area. + * @property level the height level of the zone, typically equal to the player's + * own height level. + * + * It should be noted that the [zoneX] and [zoneZ] coordinates are relative + * to the build area in their absolute form, not in their shifted zone form. + * If the player is at an absolute coordinate of 50, 40 within the build area(104x104), + * the expected coordinates to transmit here would be 48, 40, as that would + * point to the south-western corner of the zone in which the player is standing in. + */ +public class UpdateZonePartialEnclosed private constructor( + private val _zoneX: UByte, + private val _zoneZ: UByte, + private val _level: UByte, + public val payload: ByteBuf, +) : OutgoingGameMessage { + public constructor( + zoneX: Int, + zoneZ: Int, + level: Int, + payload: ByteBuf, + ) : this( + zoneX.toUByte(), + zoneZ.toUByte(), + level.toUByte(), + payload.retain(), + ) + + public val zoneX: Int + get() = _zoneX.toInt() + public val zoneZ: Int + get() = _zoneZ.toInt() + public val level: Int + get() = _level.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateZonePartialEnclosed + + if (_zoneX != other._zoneX) return false + if (_zoneZ != other._zoneZ) return false + if (_level != other._level) return false + + return true + } + + override fun hashCode(): Int { + var result = _zoneX.hashCode() + result = 31 * result + _zoneZ.hashCode() + result = 31 * result + _level.hashCode() + return result + } + + override fun toString(): String = + "UpdateZonePartialEnclosed(" + + "zoneX=$zoneX, " + + "zoneZ=$zoneZ, " + + "level=$level" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZonePartialFollows.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZonePartialFollows.kt new file mode 100644 index 000000000..c6b3f4187 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/header/UpdateZonePartialFollows.kt @@ -0,0 +1,76 @@ +package net.rsprot.protocol.game.outgoing.zone.header + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.message.OutgoingGameMessage + +/** + * Update zone partial follows packets are used to set the 'current zone pointer' to this + * zone, allowing one to follow it with any other zone payload packet. + * This packet is more efficient to use over the partial-enclosed variant + * when there is only a single zone packet following it, in any other scenario, + * it is more bandwidth-friendly to use the enclosed packet. + * @property zoneX the x coordinate of the zone's south-western corner in the + * build area. + * @property zoneZ the z coordinate of the zone's south-western corner in the + * build area. + * @property level the height level of the zone, typically equal to the player's + * own height level. + * + * It should be noted that the [zoneX] and [zoneZ] coordinates are relative + * to the build area in their absolute form, not in their shifted zone form. + * If the player is at an absolute coordinate of 50, 40 within the build area(104x104), + * the expected coordinates to transmit here would be 48, 40, as that would + * point to the south-western corner of the zone in which the player is standing in. + */ +public class UpdateZonePartialFollows private constructor( + private val _zoneX: UByte, + private val _zoneZ: UByte, + private val _level: UByte, +) : OutgoingGameMessage { + public constructor( + zoneX: Int, + zoneZ: Int, + level: Int, + ) : this( + zoneX.toUByte(), + zoneZ.toUByte(), + level.toUByte(), + ) + + public val zoneX: Int + get() = _zoneX.toInt() + public val zoneZ: Int + get() = _zoneZ.toInt() + public val level: Int + get() = _level.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UpdateZonePartialFollows + + if (_zoneX != other._zoneX) return false + if (_zoneZ != other._zoneZ) return false + if (_level != other._level) return false + + return true + } + + override fun hashCode(): Int { + var result = _zoneX.hashCode() + result = 31 * result + _zoneZ.hashCode() + result = 31 * result + _level.hashCode() + return result + } + + override fun toString(): String = + "UpdateZonePartialFollows(" + + "zoneX=$zoneX, " + + "zoneZ=$zoneZ, " + + "level=$level" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocAddChange.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocAddChange.kt new file mode 100644 index 000000000..56691e94e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocAddChange.kt @@ -0,0 +1,96 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.util.OpFlags +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.game.outgoing.zone.payload.util.LocProperties +import net.rsprot.protocol.message.ZoneProt + +/** + * Loc add-change packed is used to either add or change a loc in the world. + * The client will add a new loc if none exists by this description, + * or overwrites an old one with the same layer (layer is obtained through the [shape] + * property of the loc). + * @property id the id of the loc to add + * @property xInZone the x coordinate of the loc within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the z coordinate of the loc within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property shape the shape of the loc, a value of 0 to 22 (inclusive) is expected. + * @property rotation the rotation of the loc, a value of 0 to 3 (inclusive) is expected. + * @property opFlags the right-click options enabled on this loc. + */ +public class LocAddChange private constructor( + private val _id: UShort, + private val coordInZone: CoordInZone, + private val locProperties: LocProperties, + public val opFlags: OpFlags, +) : ZoneProt { + public constructor( + id: Int, + xInZone: Int, + zInZone: Int, + shape: Int, + rotation: Int, + opFlags: OpFlags, + ) : this( + id.toUShort(), + CoordInZone(xInZone, zInZone), + LocProperties(shape, rotation), + opFlags, + ) + + public val id: Int + get() = _id.toInt() + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + public val shape: Int + get() = locProperties.shape + public val rotation: Int + get() = locProperties.rotation + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + public val locPropertiesPacked: Int + get() = locProperties.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override val protId: Int = OldSchoolZoneProt.LOC_ADD_CHANGE + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LocAddChange + + if (_id != other._id) return false + if (coordInZone != other.coordInZone) return false + if (locProperties != other.locProperties) return false + if (opFlags != other.opFlags) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + coordInZone.hashCode() + result = 31 * result + locProperties.hashCode() + result = 31 * result + opFlags.hashCode() + return result + } + + override fun toString(): String = + "LocAddChange(" + + "id=$id, " + + "xInZone=$xInZone, " + + "zInZone=$zInZone, " + + "shape=$shape, " + + "rotation=$rotation, " + + "opFlags=$opFlags" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocAnim.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocAnim.kt new file mode 100644 index 000000000..61da7cfc0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocAnim.kt @@ -0,0 +1,85 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.game.outgoing.zone.payload.util.LocProperties +import net.rsprot.protocol.message.ZoneProt + +/** + * Loc anim packets are used to make a loc play an animation. + * @property id the id of the animation to play + * @property xInZone the x coordinate of the loc within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the z coordinate of the loc within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property shape the shape of the loc, a value of 0 to 22 (inclusive) is expected. + * @property rotation the rotation of the loc, a value of 0 to 3 (inclusive) is expected. + */ +public class LocAnim private constructor( + private val _id: UShort, + private val coordInZone: CoordInZone, + private val locProperties: LocProperties, +) : ZoneProt { + public constructor( + id: Int, + xInZone: Int, + zInZone: Int, + shape: Int, + rotation: Int, + ) : this( + id.toUShort(), + CoordInZone(xInZone, zInZone), + LocProperties(shape, rotation), + ) + + public val id: Int + get() = _id.toInt() + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + public val shape: Int + get() = locProperties.shape + public val rotation: Int + get() = locProperties.rotation + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + public val locPropertiesPacked: Int + get() = locProperties.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override val protId: Int = OldSchoolZoneProt.LOC_ANIM + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LocAnim + + if (_id != other._id) return false + if (coordInZone != other.coordInZone) return false + if (locProperties != other.locProperties) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + coordInZone.hashCode() + result = 31 * result + locProperties.hashCode() + return result + } + + override fun toString(): String = + "LocAnim(" + + "id=$id, " + + "xInZone=$xInZone, " + + "zInZone=$zInZone, " + + "shape=$shape, " + + "rotation=$rotation" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocDel.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocDel.kt new file mode 100644 index 000000000..34a80665c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocDel.kt @@ -0,0 +1,76 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.game.outgoing.zone.payload.util.LocProperties +import net.rsprot.protocol.message.ZoneProt + +/** + * Loc del packets are used to delete locs from the world. + * @property xInZone the x coordinate of the loc within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the z coordinate of the loc within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property shape the shape of the loc, a value of 0 to 22 (inclusive) is expected. + * @property rotation the rotation of the loc, a value of 0 to 3 (inclusive) is expected. + */ +public class LocDel private constructor( + private val coordInZone: CoordInZone, + private val locProperties: LocProperties, +) : ZoneProt { + public constructor( + xInZone: Int, + zInZone: Int, + shape: Int, + rotation: Int, + ) : this( + CoordInZone(xInZone, zInZone), + LocProperties(shape, rotation), + ) + + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + public val shape: Int + get() = locProperties.shape + public val rotation: Int + get() = locProperties.rotation + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + public val locPropertiesPacked: Int + get() = locProperties.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + + override val protId: Int = OldSchoolZoneProt.LOC_DEL + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LocDel + + if (coordInZone != other.coordInZone) return false + if (locProperties != other.locProperties) return false + + return true + } + + override fun hashCode(): Int { + var result = coordInZone.hashCode() + result = 31 * result + locProperties.hashCode() + return result + } + + override fun toString(): String = + "LocDel(" + + "xInZone=$xInZone, " + + "zInZone=$zInZone, " + + "shape=$shape, " + + "rotation=$rotation" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocMerge.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocMerge.kt new file mode 100644 index 000000000..11ab4b2d8 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/LocMerge.kt @@ -0,0 +1,159 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.game.outgoing.zone.payload.util.LocProperties +import net.rsprot.protocol.message.ZoneProt + +/** + * Loc merge packets are used to merge a given loc's model with the player's + * own model, preventing any visual clipping problems in the process. + * This is commonly done with obstacle pipes in agility courses, as + * the player model will otherwise render through the pipes. + * + * The merge will cover a rectangle defined by the [minX], [minZ], [maxX] and [maxZ] + * properties, relative to the player who is being merged. It should be noted + * that the client adds an extra 1 to the total width/height values here, + * so having all these properties at zero would still create a single + * tile square to be merged. + * + * @property index the index of the player who is being merged + * @property id the id of the loc that is being merged with the player + * @property xInZone the x coordinate of the loc within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the z coordinate of the loc within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property shape the shape of the loc, a value of 0 to 22 (inclusive) is expected. + * @property rotation the rotation of the loc, a value of 0 to 3 (inclusive) is expected. + * @property start the delay until the loc merging begins, in client cycles (20ms/cc). + * @property end the client cycle (20ms/cc) at which the merging ends. + * @property minX the min x coordinate at which the merge occurs (see explanation above) + * @property minZ the min z coordinate at which the merge occurs (see explanation above) + * @property maxX the max x coordinate at which the merge occurs (see explanation above) + * @property maxZ the max z coordinate at which the merge occurs (see explanation above) + */ +@Suppress("DuplicatedCode") +public class LocMerge private constructor( + private val _index: UShort, + private val _id: UShort, + private val coordInZone: CoordInZone, + private val locProperties: LocProperties, + private val _start: UShort, + private val _end: UShort, + private val _minX: Byte, + private val _minZ: Byte, + private val _maxX: Byte, + private val _maxZ: Byte, +) : ZoneProt { + public constructor( + index: Int, + id: Int, + xInZone: Int, + zInZone: Int, + shape: Int, + rotation: Int, + start: Int, + end: Int, + minX: Int, + minZ: Int, + maxX: Int, + maxZ: Int, + ) : this( + index.toUShort(), + id.toUShort(), + CoordInZone(xInZone, zInZone), + LocProperties(shape, rotation), + start.toUShort(), + end.toUShort(), + minX.toByte(), + minZ.toByte(), + maxX.toByte(), + maxZ.toByte(), + ) + + public val index: Int + get() = _index.toInt() + public val id: Int + get() = _id.toInt() + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + public val shape: Int + get() = locProperties.shape + public val rotation: Int + get() = locProperties.rotation + public val start: Int + get() = _start.toInt() + public val end: Int + get() = _end.toInt() + public val minX: Int + get() = _minX.toInt() + public val minZ: Int + get() = _minZ.toInt() + public val maxX: Int + get() = _maxX.toInt() + public val maxZ: Int + get() = _maxZ.toInt() + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + public val locPropertiesPacked: Int + get() = locProperties.packed.toInt() + + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + override val protId: Int = OldSchoolZoneProt.LOC_MERGE + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LocMerge + + if (_index != other._index) return false + if (_id != other._id) return false + if (coordInZone != other.coordInZone) return false + if (locProperties != other.locProperties) return false + if (_start != other._start) return false + if (_end != other._end) return false + if (_minX != other._minX) return false + if (_minZ != other._minZ) return false + if (_maxX != other._maxX) return false + if (_maxZ != other._maxZ) return false + + return true + } + + override fun hashCode(): Int { + var result = _index.hashCode() + result = 31 * result + _id.hashCode() + result = 31 * result + coordInZone.hashCode() + result = 31 * result + locProperties.hashCode() + result = 31 * result + _start.hashCode() + result = 31 * result + _end.hashCode() + result = 31 * result + _minX.hashCode() + result = 31 * result + _minZ.hashCode() + result = 31 * result + _maxX.hashCode() + result = 31 * result + _maxZ.hashCode() + return result + } + + override fun toString(): String = + "LocMerge(" + + "index=$index, " + + "id=$id, " + + "xInZone=$xInZone, " + + "zInZone=$zInZone, " + + "shape=$shape, " + + "rotation=$rotation, " + + "start=$start, " + + "end=$end, " + + "minX=$minX, " + + "minZ=$minZ, " + + "maxX=$maxX, " + + "maxZ=$maxZ" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/MapAnim.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/MapAnim.kt new file mode 100644 index 000000000..45e672c82 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/MapAnim.kt @@ -0,0 +1,86 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.message.ZoneProt + +/** + * Map anim is sent to play a graphical effect/spotanim on a tile. + * @property id the id of the spotanim + * @property delay the delay in client cycles (20ms/cc) until the spotanim begins playing + * @property height the height at which the spotanim will play + * @property xInZone the x coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the z coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + */ +public class MapAnim private constructor( + private val _id: UShort, + private val _delay: UShort, + private val _height: UByte, + private val coordInZone: CoordInZone, +) : ZoneProt { + public constructor( + id: Int, + delay: Int, + height: Int, + xInZone: Int, + zInZone: Int, + ) : this( + id.toUShort(), + delay.toUShort(), + height.toUByte(), + CoordInZone(xInZone, zInZone), + ) + + public val id: Int + get() = _id.toInt() + public val delay: Int + get() = _delay.toInt() + public val height: Int + get() = _height.toInt() + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + override val protId: Int = OldSchoolZoneProt.MAP_ANIM + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MapAnim + + if (_id != other._id) return false + if (_delay != other._delay) return false + if (_height != other._height) return false + if (coordInZone != other.coordInZone) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _delay.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + coordInZone.hashCode() + return result + } + + override fun toString(): String = + "MapAnim(" + + "id=$id, " + + "delay=$delay, " + + "height=$height, " + + "xInZone=$xInZone, " + + "zInZone=$zInZone" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/MapProjAnim.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/MapProjAnim.kt new file mode 100644 index 000000000..263f44ea4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/MapProjAnim.kt @@ -0,0 +1,216 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.message.ZoneProt + +/** + * Map projectile anim packets are sent to render projectiles + * from one coord to another. + * @property id the id of the spotanim that is this projectile + * @property startHeight the height of the projectile as it begins flying + * @property endHeight the height of the projectile as it finishes flying + * @property startTime the start time in client cycles (20ms/cc) until the + * projectile begins moving + * @property endTime the end time in client cycles (20ms/cc) until the + * projectile arrives at its destination + * @property angle the angle that the projectile takes during its flight + * @property progress the fine coord distance offset that the projectile + * begins flying at. If the value is 0, the projectile begins flying + * at the defined start coordinate. For every 128 units of value, the projectile + * is moved 1 game square towards the end position. Interpolate between 0-128 for + * units smaller than 1 game square. + * This is commonly set to 128 to make a projectile appear as if it's flying + * straight down, as the projectile will not render if its defined start and + * end coords are equal. So, in order to avoid that, one solution is to put the + * end coordinate 1 game square away from the start in a cardinal direction, + * and set the value of this property to 128 - ensuring that the projectile + * will appear to fly completely vertically, with no horizontal movement whatsoever. + * @property sourceIndex the index of the pathing entity from whom the projectile comes. + * If the value is 0, the projectile will not be locked to any source entity. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * @property targetIndex the index of the pathing entity at whom the projectile is shot. + * If the value is 0, the projectile will not be locked to any target entity. + * If the target avatar is a player, add 0x10000 to the real index value (0-2048). + * If the target avatar is a NPC, set the index as it is. + * @property xInZone the start x coordinate of the projectile within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the start z coordinate of the projectile within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property deltaX the x coordinate delta that the projectile will move to + * relative to the starting position. + * @property deltaZ the z coordinate delta that the projectile will move to + * relative to the starting position. + */ +@Suppress("DuplicatedCode") +public class MapProjAnim private constructor( + private val _id: UShort, + private val _startTime: UShort, + private val _endTime: UShort, + private val _angle: UByte, + private val _progress: UShort, + private val compressedInfo: CompressedMapProjAnimInfo, + private val coordInZone: CoordInZone, + private val _deltaX: Byte, + private val _deltaZ: Byte, +) : ZoneProt { + public constructor( + id: Int, + startHeight: Int, + endHeight: Int, + startTime: Int, + endTime: Int, + angle: Int, + progress: Int, + sourceIndex: Int, + targetIndex: Int, + xInZone: Int, + zInZone: Int, + deltaX: Int, + deltaZ: Int, + ) : this( + id.toUShort(), + startTime.toUShort(), + endTime.toUShort(), + angle.toUByte(), + progress.toUShort(), + CompressedMapProjAnimInfo( + sourceIndex, + targetIndex, + startHeight.toUByte(), + endHeight.toUByte(), + ), + CoordInZone(xInZone, zInZone), + deltaX.toByte(), + deltaZ.toByte(), + ) + + public val id: Int + get() = _id.toInt() + public val startHeight: Int + get() = compressedInfo.startHeight + public val endHeight: Int + get() = compressedInfo.endHeight + public val startTime: Int + get() = _startTime.toInt() + public val endTime: Int + get() = _endTime.toInt() + public val angle: Int + get() = _angle.toInt() + public val progress: Int + get() = _progress.toInt() + public val sourceIndex: Int + get() = compressedInfo.sourceIndex + public val targetIndex: Int + get() = compressedInfo.targetIndex + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + public val deltaX: Int + get() = _deltaX.toInt() + public val deltaZ: Int + get() = _deltaZ.toInt() + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + override val protId: Int = OldSchoolZoneProt.MAP_PROJANIM + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MapProjAnim + + if (_id != other._id) return false + if (_startTime != other._startTime) return false + if (_endTime != other._endTime) return false + if (_angle != other._angle) return false + if (_progress != other._progress) return false + if (compressedInfo != other.compressedInfo) return false + if (coordInZone != other.coordInZone) return false + if (_deltaX != other._deltaX) return false + if (_deltaZ != other._deltaZ) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _startTime.hashCode() + result = 31 * result + _endTime.hashCode() + result = 31 * result + _angle.hashCode() + result = 31 * result + _progress.hashCode() + result = 31 * result + compressedInfo.hashCode() + result = 31 * result + coordInZone.hashCode() + result = 31 * result + _deltaX + result = 31 * result + _deltaZ + return result + } + + override fun toString(): String = + "MapProjAnim(" + + "id=$id, " + + "startHeight=$startHeight, " + + "endHeight=$endHeight, " + + "startTime=$startTime, " + + "endTime=$endTime, " + + "angle=$angle, " + + "progress=$progress, " + + "sourceIndex=$sourceIndex, " + + "targetIndex=$targetIndex, " + + "xInZone=$xInZone, " + + "zInZone=$zInZone, " + + "deltaX=$deltaX, " + + "deltaZ=$deltaZ" + + ")" + + /** + * A value class to compress several properties into one. + * This is primarily done so the entire class comes to a sum of 20 bytes. + * The [sourceIndex] and [targetIndex] properties are 24-bit integers, for + * which there are no backing types in the JVM. Treating them as 32-bit + * integers would push the total sum of all the payload to 22 bytes, which, + * due to memory alignment would cause the entire thing to take 28 bytes + * instead of the usual 20. + */ + @JvmInline + private value class CompressedMapProjAnimInfo private constructor( + private val packed: Long, + ) { + constructor( + sourceIndex: Int, + targetIndex: Int, + startHeight: UByte, + endHeight: UByte, + ) : this( + (sourceIndex and 0xFFFFFF) + .toLong() + .or((targetIndex and 0xFFFFFF).toLong() shl 24) + .or((startHeight.toLong() and 0xFF) shl 48) + .or((endHeight.toLong() and 0xFF) shl 56), + ) + + val sourceIndex: Int + get() = (packed and 0xFFFFFF).toInt() + val targetIndex: Int + get() = (packed ushr 24 and 0xFFFFFF).toInt() + val startHeight: Int + get() = (packed ushr 48 and 0xFF).toInt() + val endHeight: Int + get() = (packed ushr 56 and 0xFF).toInt() + + override fun toString(): String = + "MapProjAnimInfo(" + + "sourceIndex=$sourceIndex, " + + "targetIndex=$targetIndex, " + + "startHeight=$startHeight, " + + "endHeight=$endHeight" + + ")" + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjAdd.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjAdd.kt new file mode 100644 index 000000000..7c4d94a6f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjAdd.kt @@ -0,0 +1,154 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.util.OpFlags +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.message.ZoneProt + +/** + * Obj add packets are used to spawn an obj on the ground. + * + * Ownership table: + * ``` + * | Id | Ownership Type | + * |----|:--------------:| + * | 0 | None | + * | 1 | Self Player | + * | 2 | Other Player | + * | 3 | Group Ironman | + * ``` + * + * @property id the id of the obj config + * @property quantity the quantity of the obj to be spawned + * @property xInZone the x coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the z coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property opFlags the right-click options enabled on this obj. + * @property timeUntilPublic how many game cycles until the obj turns public. + * This property is only used on the C++-based clients. + * @property timeUntilDespawn how many game cycles until the obj disappears. + * This property is only used on the C++-based clients. + * @property ownershipType the type of ownership of this obj (see table above). + * This property is only used on the C++-based clients. + * @property neverBecomesPublic whether the item turns public in the future. + * This property is only used on the c++-based clients. + */ +@Suppress("DuplicatedCode") +public class ObjAdd private constructor( + private val _id: UShort, + public val quantity: Int, + private val coordInZone: CoordInZone, + public val opFlags: OpFlags, + private val _timeUntilPublic: UShort, + private val _timeUntilDespawn: UShort, + private val _ownershipType: UByte, + public val neverBecomesPublic: Boolean, +) : ZoneProt { + public constructor( + id: Int, + quantity: Int, + xInZone: Int, + zInZone: Int, + opFlags: OpFlags, + timeUntilPublic: Int, + timeUntilDespawn: Int, + ownershipType: Int, + neverBecomesPublic: Boolean, + ) : this( + id.toUShort(), + quantity, + CoordInZone(xInZone, zInZone), + opFlags, + timeUntilPublic.toUShort(), + timeUntilDespawn.toUShort(), + ownershipType.toUByte(), + neverBecomesPublic, + ) + + /** + * A helper constructor for the JVM-based clients, as these clients + * do not utilize the [timeUntilPublic], [timeUntilDespawn], [ownershipType] and + * [neverBecomesPublic] properties. + */ + public constructor( + id: Int, + quantity: Int, + xInZone: Int, + zInZone: Int, + opFlags: OpFlags, + ) : this( + id, + quantity, + xInZone, + zInZone, + opFlags, + timeUntilPublic = 0, + timeUntilDespawn = 0, + ownershipType = 0, + neverBecomesPublic = false, + ) + + public val id: Int + get() = _id.toInt() + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + public val timeUntilPublic: Int + get() = _timeUntilPublic.toInt() + public val timeUntilDespawn: Int + get() = _timeUntilDespawn.toInt() + public val ownershipType: Int + get() = _ownershipType.toInt() + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + override val protId: Int = OldSchoolZoneProt.OBJ_ADD + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ObjAdd + + if (_id != other._id) return false + if (quantity != other.quantity) return false + if (coordInZone != other.coordInZone) return false + if (opFlags != other.opFlags) return false + if (_timeUntilPublic != other._timeUntilPublic) return false + if (_timeUntilDespawn != other._timeUntilDespawn) return false + if (_ownershipType != other._ownershipType) return false + if (neverBecomesPublic != other.neverBecomesPublic) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + quantity + result = 31 * result + coordInZone.hashCode() + result = 31 * result + opFlags.hashCode() + result = 31 * result + _timeUntilPublic.hashCode() + result = 31 * result + _timeUntilDespawn.hashCode() + result = 31 * result + _ownershipType.hashCode() + result = 31 * result + neverBecomesPublic.hashCode() + return result + } + + override fun toString(): String = + "ObjAdd(" + + "id=$id, " + + "quantity=$quantity, " + + "xInZone=$xInZone, " + + "zInZone=$zInZone, " + + "opFlags=$opFlags, " + + "timeUntilPublic=$timeUntilPublic, " + + "timeUntilDespawn=$timeUntilDespawn, " + + "ownershipType=$ownershipType" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjCount.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjCount.kt new file mode 100644 index 000000000..a2768621a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjCount.kt @@ -0,0 +1,85 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.message.ZoneProt + +/** + * Obj count is a packet used to update the quantity of an obj that's already + * spawned into the build area. This is only done for objs which are private + * to a specific user - doing so merges the stacks together into one rather + * than having two distinct stacks of the same item. + * @property id the id of the obj to merge + * @property oldQuantity the old quantity of the obj to find, if no obj + * by this quantity is found, this packet has no effect client-side + * @property newQuantity the new quantity to be set to this obj + * @property xInZone the x coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the z coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + */ +public class ObjCount private constructor( + private val _id: UShort, + public val oldQuantity: Int, + public val newQuantity: Int, + private val coordInZone: CoordInZone, +) : ZoneProt { + public constructor( + id: Int, + oldQuantity: Int, + newQuantity: Int, + xInZone: Int, + zInZone: Int, + ) : this( + id.toUShort(), + oldQuantity, + newQuantity, + CoordInZone(xInZone, zInZone), + ) + + public val id: Int + get() = _id.toInt() + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + override val protId: Int = OldSchoolZoneProt.OBJ_COUNT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ObjCount + + if (_id != other._id) return false + if (oldQuantity != other.oldQuantity) return false + if (newQuantity != other.newQuantity) return false + if (coordInZone != other.coordInZone) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + oldQuantity + result = 31 * result + newQuantity + result = 31 * result + coordInZone.hashCode() + return result + } + + override fun toString(): String = + "ObjCount(" + + "id=$id, " + + "oldQuantity=$oldQuantity, " + + "newQuantity=$newQuantity, " + + "xInZone=$xInZone, " + + "zInZone=$zInZone" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjDel.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjDel.kt new file mode 100644 index 000000000..8a2d695fa --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjDel.kt @@ -0,0 +1,78 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.message.ZoneProt + +/** + * Obj del packets are used to delete an existing obj from the build area, + * assuming it exists in the first place. + * @property id the id of the obj to delete. Note that the client does bitwise-and + * on the id to cap it to the lowest 15 bits, meaning the maximum id that can be + * transmitted is 32767. + * @property quantity the quantity of the obj to be deleted. If there is no obj + * with this quantity, nothing will be deleted. + * @property xInZone the x coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the z coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + */ +public class ObjDel private constructor( + private val _id: UShort, + public val quantity: Int, + private val coordInZone: CoordInZone, +) : ZoneProt { + public constructor( + id: Int, + quantity: Int, + xInZone: Int, + zInZone: Int, + ) : this( + id.toUShort(), + quantity, + CoordInZone(xInZone, zInZone), + ) + + public val id: Int + get() = _id.toInt() + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + override val protId: Int = OldSchoolZoneProt.OBJ_DEL + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ObjDel + + if (_id != other._id) return false + if (quantity != other.quantity) return false + if (coordInZone != other.coordInZone) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + quantity + result = 31 * result + coordInZone.hashCode() + return result + } + + override fun toString(): String = + "ObjDel(" + + "id=$id, " + + "quantity=$quantity, " + + "xInZone=$xInZone, " + + "zInZone=$zInZone" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjEnabledOps.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjEnabledOps.kt new file mode 100644 index 000000000..2ac338f78 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/ObjEnabledOps.kt @@ -0,0 +1,78 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.util.OpFlags +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.message.ZoneProt + +/** + * Obj enabled ops is used to change the right-click options on an obj + * on the ground. This packet is currently unused in OldSchool RuneScape. + * It works by finding the first obj in the stack with the provided [id], + * and modifying the right-click ops on that. It does not verify quantity. + * @property id the id of the obj that needs to get its ops changed + * @property opFlags the right-click options to set enabled on that obj + * @property xInZone the x coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the z coordinate of the obj within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + */ +public class ObjEnabledOps private constructor( + private val _id: UShort, + public val opFlags: OpFlags, + private val coordInZone: CoordInZone, +) : ZoneProt { + public constructor( + id: Int, + opFlags: OpFlags, + xInZone: Int, + zInZone: Int, + ) : this( + id.toUShort(), + opFlags, + CoordInZone(xInZone, zInZone), + ) + + public val id: Int + get() = _id.toInt() + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + override val protId: Int = OldSchoolZoneProt.OBJ_ENABLED_OPS + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ObjEnabledOps + + if (_id != other._id) return false + if (opFlags != other.opFlags) return false + if (coordInZone != other.coordInZone) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + opFlags.hashCode() + result = 31 * result + coordInZone.hashCode() + return result + } + + override fun toString(): String = + "ObjEnabledOps(" + + "id=$id, " + + "opFlags=$opFlags, " + + "xInZone=$xInZone, " + + "zInZone=$zInZone" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/SoundArea.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/SoundArea.kt new file mode 100644 index 000000000..7fab8ae40 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/SoundArea.kt @@ -0,0 +1,124 @@ +package net.rsprot.protocol.game.outgoing.zone.payload + +import net.rsprot.protocol.ServerProtCategory +import net.rsprot.protocol.common.game.outgoing.codec.zone.payload.OldSchoolZoneProt +import net.rsprot.protocol.game.outgoing.GameServerProtCategory +import net.rsprot.protocol.game.outgoing.zone.payload.util.CoordInZone +import net.rsprot.protocol.message.ZoneProt + +/** + * Sound area packed is sent to play a sound effect at a specific coord. + * Any players within [range] tiles of the destination coord will + * hear this sound effect played, if they have sound effects enabled. + * The volume will change according to the player's distance to the + * origin coord of the sound effect itself. + * It is worth noting there is a maximum quantity of 50 area sound effects + * that can play concurrently in the client across all the zones. + * Therefore, a potential optimization one can do is prevent appending + * any more area sound effects once the quantity has reached 50 in a zone. + * @property id the id of the sound effect to play + * @property delay the delay in client cycles (20ms/cc) until the + * sound effect starts playing + * @property loops how many loops the sound effect should do. + * If the [loops] property is 0, the sound effect will not play. + * @property range the radius from the originating coord how far the sound + * effect can be heard. Note that the client ignores the 4 higher bits of + * this value, meaning the maximum radius is 31 tiles - anything above has + * no effect. + * @property dropOffRange the size of the origin. In most cases, this should be + * a value of 1. However, if a larger value is provided, it means the + * client will treat the south-western coord provided here as the + * south-western corner of the 'box' that is made with this size in mind, + * for the purpose of having an evenly-spreading volume around this + * element. + * This size property is primarily used for larger NPCs, to make their + * sound effects flow out smoothly from all sides. + * @property xInZone the x coordinate of the sound effect within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + * @property zInZone the z coordinate of the sound effect within the zone it is in, + * a value in range of 0 to 7 (inclusive) is expected. Any bits outside that are ignored. + */ +@Suppress("DuplicatedCode") +public class SoundArea private constructor( + private val _id: UShort, + private val _delay: UByte, + private val _loops: UByte, + private val _range: UByte, + private val _dropOffRange: UByte, + private val coordInZone: CoordInZone, +) : ZoneProt { + public constructor( + id: Int, + delay: Int, + loops: Int, + radius: Int, + size: Int, + xInZone: Int, + zInZone: Int, + ) : this( + id.toUShort(), + delay.toUByte(), + loops.toUByte(), + radius.toUByte(), + size.toUByte(), + CoordInZone(xInZone, zInZone), + ) + + public val id: Int + get() = _id.toInt() + public val delay: Int + get() = _delay.toInt() + public val loops: Int + get() = _loops.toInt() + public val range: Int + get() = _range.toInt() + public val dropOffRange: Int + get() = _dropOffRange.toInt() + public val xInZone: Int + get() = coordInZone.xInZone + public val zInZone: Int + get() = coordInZone.zInZone + + public val coordInZonePacked: Int + get() = coordInZone.packed.toInt() + override val category: ServerProtCategory + get() = GameServerProtCategory.HIGH_PRIORITY_PROT + override val protId: Int = OldSchoolZoneProt.SOUND_AREA + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SoundArea + + if (_id != other._id) return false + if (_delay != other._delay) return false + if (_loops != other._loops) return false + if (_range != other._range) return false + if (_dropOffRange != other._dropOffRange) return false + if (coordInZone != other.coordInZone) return false + + return true + } + + override fun hashCode(): Int { + var result = _id.hashCode() + result = 31 * result + _delay.hashCode() + result = 31 * result + _loops.hashCode() + result = 31 * result + _range.hashCode() + result = 31 * result + _dropOffRange.hashCode() + result = 31 * result + coordInZone.hashCode() + return result + } + + override fun toString(): String = + "SoundArea(" + + "id=$id, " + + "delay=$delay, " + + "loops=$loops, " + + "range=$range, " + + "dropOffRange=$dropOffRange, " + + "xInZone=$xInZone, " + + "zInZone=$zInZone" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/CoordInBuildArea.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/CoordInBuildArea.kt new file mode 100644 index 000000000..26bbe99f4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/CoordInBuildArea.kt @@ -0,0 +1,65 @@ +package net.rsprot.protocol.game.outgoing.zone.payload.util + +/** + * Coord in build-area is a helper class to compress the data used to transmit + * build-area coords to the client, primarily in *-specific packets. + * These packets will separate the south-western zone X/Z coordinates, + * and the x/z in-zone coordinates into separate properties. + * @property zoneX the south-western x coordinate of the zone (multiples of 8 value) + * @property xInZone the x coordinate within the zone (0-7 value) + * @property zoneZ the south-western z coordinate of the zone (multiples of 8 value) + * @property zInZone the z coordinate within the zone (0-7 value) + * @property packedMedium the coordinates bitpacked into a 24-bit integer, + * as this is how they tend to be transmitted to the client. + */ +@JvmInline +internal value class CoordInBuildArea private constructor( + private val packedShort: UShort, +) { + constructor( + zoneX: Int, + xInZone: Int, + zoneZ: Int, + zInZone: Int, + ) : this( + ((zoneX and 0x7.inv() or (xInZone and 0x7)) shl 8) + .or((zoneZ and 0x7.inv()) or (zInZone and 0x7)) + .toUShort(), + ) + + constructor( + xinBuildArea: Int, + zInBuildArea: Int, + ) : this( + ((xinBuildArea and 0xFF) shl 8) + .or(zInBuildArea) + .toUShort(), + ) + + val zoneX: Int + get() = packedShort.toInt() ushr 8 and 0xF8 + val xInZone: Int + get() = packedShort.toInt() ushr 8 and 0x7 + val zoneZ: Int + get() = packedShort.toInt() and 0xF8 + val zInZone: Int + get() = packedShort.toInt() and 0x7 + + val xInBuildArea: Int + get() = packedShort.toInt() ushr 8 + val zInBuildArea: Int + get() = packedShort.toInt() and 0xFF + + val packedMedium: Int + get() = + (zoneX shl 16) + .or(zoneZ shl 8) + .or(xInZone shl 4) + .or(zInZone) + + override fun toString(): String = + "CoordInBuildArea(" + + "xInBuildArea=$xInBuildArea, " + + "zInBuildArea=$zInBuildArea" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/CoordInZone.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/CoordInZone.kt new file mode 100644 index 000000000..2dabd5d54 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/CoordInZone.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.game.outgoing.zone.payload.util + +@JvmInline +internal value class CoordInZone private constructor( + val packed: UByte, +) { + constructor( + xInZone: Int, + zInZone: Int, + ) : this( + (xInZone and 0x7 shl 4) + .or(zInZone and 0x7) + .toUByte(), + ) + + val xInZone: Int + get() = packed.toInt() ushr 4 and 0x7 + val zInZone: Int + get() = packed.toInt() and 0x7 + + override fun toString(): String = + "CoordInZone(" + + "xInZone=$xInZone, " + + "zInZone=$zInZone" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/LocProperties.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/LocProperties.kt new file mode 100644 index 000000000..df638ba7e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/game/outgoing/zone/payload/util/LocProperties.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.game.outgoing.zone.payload.util + +@JvmInline +internal value class LocProperties private constructor( + val packed: UByte, +) { + constructor( + shape: Int, + rotation: Int, + ) : this( + (shape and 0x1F shl 2) + .or(rotation and 0x3) + .toUByte(), + ) + + val shape: Int + get() = packed.toInt() ushr 2 and 0x1F + val rotation: Int + get() = packed.toInt() and 0x3 + + override fun toString(): String = + "LocProperties(" + + "shape=$shape, " + + "rotation=$rotation" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/Js5GroupRequest.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/Js5GroupRequest.kt new file mode 100644 index 000000000..9c8513987 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/Js5GroupRequest.kt @@ -0,0 +1,9 @@ +package net.rsprot.protocol.js5.incoming + +public sealed interface Js5GroupRequest { + public val archiveId: Int + public val groupId: Int + + public val bitpacked: Int + get() = groupId or (archiveId shl 16) +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PrefetchRequest.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PrefetchRequest.kt new file mode 100644 index 000000000..1a477454e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PrefetchRequest.kt @@ -0,0 +1,36 @@ +package net.rsprot.protocol.js5.incoming + +import net.rsprot.protocol.message.IncomingJs5Message + +public class PrefetchRequest( + private val _archiveId: UByte, + private val _groupId: UShort, +) : IncomingJs5Message, + Js5GroupRequest { + override val archiveId: Int + get() = _archiveId.toInt() + override val groupId: Int + get() = _groupId.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is PrefetchRequest) return false + + if (_archiveId != other._archiveId) return false + if (_groupId != other._groupId) return false + + return true + } + + override fun hashCode(): Int { + var result = _archiveId.hashCode() + result = 31 * result + _groupId.hashCode() + return result + } + + override fun toString(): String = + "PrefetchRequest(" + + "archiveId=$archiveId, " + + "groupId=$groupId" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PriorityChangeHigh.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PriorityChangeHigh.kt new file mode 100644 index 000000000..8d565121e --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PriorityChangeHigh.kt @@ -0,0 +1,5 @@ +package net.rsprot.protocol.js5.incoming + +import net.rsprot.protocol.message.IncomingJs5Message + +public data object PriorityChangeHigh : IncomingJs5Message diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PriorityChangeLow.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PriorityChangeLow.kt new file mode 100644 index 000000000..c3fbf790a --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/PriorityChangeLow.kt @@ -0,0 +1,5 @@ +package net.rsprot.protocol.js5.incoming + +import net.rsprot.protocol.message.IncomingJs5Message + +public data object PriorityChangeLow : IncomingJs5Message diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/UrgentRequest.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/UrgentRequest.kt new file mode 100644 index 000000000..bd0388981 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/UrgentRequest.kt @@ -0,0 +1,36 @@ +package net.rsprot.protocol.js5.incoming + +import net.rsprot.protocol.message.IncomingJs5Message + +public class UrgentRequest( + private val _archiveId: UByte, + private val _groupId: UShort, +) : IncomingJs5Message, + Js5GroupRequest { + override val archiveId: Int + get() = _archiveId.toInt() + override val groupId: Int + get() = _groupId.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is UrgentRequest) return false + + if (_archiveId != other._archiveId) return false + if (_groupId != other._groupId) return false + + return true + } + + override fun hashCode(): Int { + var result = _archiveId.hashCode() + result = 31 * result + _groupId.hashCode() + return result + } + + override fun toString(): String = + "UrgentRequest(" + + "archiveId=$archiveId, " + + "groupId=$groupId" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/XorChange.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/XorChange.kt new file mode 100644 index 000000000..2005b312d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/incoming/XorChange.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.js5.incoming + +import net.rsprot.protocol.message.IncomingJs5Message + +public class XorChange( + public val key: Int, +) : IncomingJs5Message { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is XorChange) return false + + if (key != other.key) return false + + return true + } + + override fun hashCode(): Int = key + + override fun toString(): String = "XorChange(key=$key)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/outgoing/Js5GroupResponse.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/outgoing/Js5GroupResponse.kt new file mode 100644 index 000000000..aa9446e71 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/js5/outgoing/Js5GroupResponse.kt @@ -0,0 +1,45 @@ +package net.rsprot.protocol.js5.outgoing + +import io.netty.buffer.ByteBuf +import io.netty.buffer.DefaultByteBufHolder +import net.rsprot.protocol.message.OutgoingJs5Message + +/** + * Js5 group responses are used to feed the cache to the client through the server. + * @param buffer the byte buffer that is used for the response + * @property offset the starting index from which the response is written + * @property limit the ending index until which the response is written + */ +public class Js5GroupResponse( + buffer: ByteBuf, + public val offset: Int, + public val limit: Int, + public val key: Int, +) : DefaultByteBufHolder(buffer), + OutgoingJs5Message { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as Js5GroupResponse + + if (offset != other.offset) return false + if (limit != other.limit) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + offset + result = 31 * result + limit + return result + } + + override fun toString(): String = + "Js5GroupResponse(" + + "offset=$offset, " + + "limit=$limit" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/GameLogin.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/GameLogin.kt new file mode 100644 index 000000000..8f549e0b6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/GameLogin.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.loginprot.incoming + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.loginprot.incoming.util.AuthenticationType +import net.rsprot.protocol.loginprot.incoming.util.LoginBlockDecodingFunction +import net.rsprot.protocol.message.IncomingLoginMessage + +public typealias LoginDecodingFunction = LoginBlockDecodingFunction> + +public data class GameLogin( + public val buffer: JagByteBuf, + public val decoder: LoginDecodingFunction, +) : IncomingLoginMessage diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/GameReconnect.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/GameReconnect.kt new file mode 100644 index 000000000..c87606be5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/GameReconnect.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.loginprot.incoming + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.xtea.XteaKey +import net.rsprot.protocol.loginprot.incoming.util.LoginBlockDecodingFunction +import net.rsprot.protocol.message.IncomingLoginMessage + +public typealias ReconnectDecodingFunction = LoginBlockDecodingFunction + +public data class GameReconnect( + public val buffer: JagByteBuf, + public val decoder: ReconnectDecodingFunction, +) : IncomingLoginMessage diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/InitGameConnection.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/InitGameConnection.kt new file mode 100644 index 000000000..49e985cbb --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/InitGameConnection.kt @@ -0,0 +1,5 @@ +package net.rsprot.protocol.loginprot.incoming + +import net.rsprot.protocol.message.IncomingLoginMessage + +public data object InitGameConnection : IncomingLoginMessage diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/InitJs5RemoteConnection.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/InitJs5RemoteConnection.kt new file mode 100644 index 000000000..b6db6be09 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/InitJs5RemoteConnection.kt @@ -0,0 +1,32 @@ +package net.rsprot.protocol.loginprot.incoming + +import net.rsprot.protocol.message.IncomingLoginMessage + +public class InitJs5RemoteConnection( + public val revision: Int, + public val seed: IntArray, +) : IncomingLoginMessage { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as InitJs5RemoteConnection + + if (revision != other.revision) return false + if (!seed.contentEquals(other.seed)) return false + + return true + } + + override fun hashCode(): Int { + var result = revision + result = 31 * result + seed.contentHashCode() + return result + } + + override fun toString(): String = + "InitJs5RemoteConnection(" + + "revision=$revision, " + + "seed=${seed.contentToString()}" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/ProofOfWorkReply.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/ProofOfWorkReply.kt new file mode 100644 index 000000000..6b03e2653 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/ProofOfWorkReply.kt @@ -0,0 +1,7 @@ +package net.rsprot.protocol.loginprot.incoming + +import net.rsprot.protocol.message.IncomingLoginMessage + +public data class ProofOfWorkReply( + public val result: Long, +) : IncomingLoginMessage diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/RemainingBetaArchives.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/RemainingBetaArchives.kt new file mode 100644 index 000000000..80963a257 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/RemainingBetaArchives.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.loginprot.incoming + +import net.rsprot.protocol.message.IncomingLoginMessage + +public class RemainingBetaArchives( + internal val crc: IntArray, +) : IncomingLoginMessage { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RemainingBetaArchives + + return crc.contentEquals(other.crc) + } + + override fun hashCode(): Int = crc.contentHashCode() + + override fun toString(): String = "RemainingBetaArchives(crc=${crc.contentToString()})" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/NopProofOfWorkProvider.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/NopProofOfWorkProvider.kt new file mode 100644 index 000000000..8cf1056df --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/NopProofOfWorkProvider.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.loginprot.incoming.pow + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaData +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeType +import java.net.InetAddress + +/** + * A no-operation proof of work provider, allowing one to skip proof of work entirely. + */ +public object NopProofOfWorkProvider : + ProofOfWorkProvider { + override fun provide(inetAddress: InetAddress): ProofOfWork? = null + + public object NopChallengeType : ChallengeType { + override val id: Int = 0 + + override fun encode(buffer: JagByteBuf) { + // nop + } + } + + public object NopChallengeMetaData : ChallengeMetaData +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWork.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWork.kt new file mode 100644 index 000000000..0934ff2cd --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWork.kt @@ -0,0 +1,42 @@ +package net.rsprot.protocol.loginprot.incoming.pow + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaData +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeType +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeVerifier + +/** + * Proof of work is a procedure during login to attempt to throttle login requests from a single source, + * by requiring them to do CPU-bound work before accepting the login. + * @property challengeType the type of the challenge to require the client to solve + * @property challengeVerifier the verifier of that challenge, to ensure the client did complete + * the world successfully + */ +@Suppress("MemberVisibilityCanBePrivate") +public class ProofOfWork, in MetaData : ChallengeMetaData>( + public val challengeType: T, + public val challengeVerifier: ChallengeVerifier, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ProofOfWork<*, *> + + if (challengeType != other.challengeType) return false + if (challengeVerifier != other.challengeVerifier) return false + + return true + } + + override fun hashCode(): Int { + var result = challengeType.hashCode() + result = 31 * result + challengeVerifier.hashCode() + return result + } + + override fun toString(): String = + "ProofOfWork(" + + "challengeType=$challengeType, " + + "challengeVerifier=$challengeVerifier" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWorkProvider.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWorkProvider.kt new file mode 100644 index 000000000..fa8a0b16b --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/ProofOfWorkProvider.kt @@ -0,0 +1,17 @@ +package net.rsprot.protocol.loginprot.incoming.pow + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaData +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeType +import java.net.InetAddress + +/** + * An interface to return proof of work implementations based on the input ip. + */ +public fun interface ProofOfWorkProvider, in MetaData : ChallengeMetaData> { + /** + * Provides a proof of work instance for a given [inetAddress]. + * @param inetAddress the IP from which the client is connecting. + * @return a proof of work instance that the client needs to solve, or null if it should be skipped + */ + public fun provide(inetAddress: InetAddress): ProofOfWork? +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/SingleTypeProofOfWorkProvider.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/SingleTypeProofOfWorkProvider.kt new file mode 100644 index 000000000..91f3acfa6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/SingleTypeProofOfWorkProvider.kt @@ -0,0 +1,30 @@ +package net.rsprot.protocol.loginprot.incoming.pow + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeGenerator +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaData +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaDataProvider +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeType +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeVerifier +import java.net.InetAddress + +/** + * A single type proof of work provider is used to always return proof of work instances + * of a single specific type. + * @property metaDataProvider the provider used to return instances of metadata for the + * challenges. + * @property challengeGenerator the generator that will create a new proof of work challenge + * based on the input metadata. + * @property challengeVerifier the verifier that will check if the answer sent by the client + * is correct. + */ +public class SingleTypeProofOfWorkProvider, in MetaData : ChallengeMetaData>( + private val metaDataProvider: ChallengeMetaDataProvider, + private val challengeGenerator: ChallengeGenerator, + private val challengeVerifier: ChallengeVerifier, +) : ProofOfWorkProvider { + override fun provide(inetAddress: InetAddress): ProofOfWork { + val metadata = metaDataProvider.provide(inetAddress) + val challenge = challengeGenerator.generate(metadata) + return ProofOfWork(challenge, challengeVerifier) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeGenerator.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeGenerator.kt new file mode 100644 index 000000000..3533b70e7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeGenerator.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +/** + * A challenge generator used to construct a challenge out of the provided metadata. + */ +public fun interface ChallengeGenerator> { + /** + * A function to generate a challenge out of the provided metadata. + * @param input the metadata input necessary to generate a challenge + * @return a challenge generated out of the metadata + */ + public fun generate(input: MetaData): Type +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaData.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaData.kt new file mode 100644 index 000000000..388bb05c0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaData.kt @@ -0,0 +1,6 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +/** + * A common binding interface for metadata necessary to pass into the challenge constructors. + */ +public interface ChallengeMetaData diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaDataProvider.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaDataProvider.kt new file mode 100644 index 000000000..eb8a12fa6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeMetaDataProvider.kt @@ -0,0 +1,17 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +import java.net.InetAddress + +/** + * A challenge metadata provider is used to generate a metadata necessary to construct a challenge. + */ +public fun interface ChallengeMetaDataProvider { + /** + * Provides a metadata instance for a challenge, using the ip as the input parameter. + * @param inetAddress the IP from which the user is connecting to the server. + * This is provided in case an implementation which scales with the number of requests + * from a given host is desired. + * @return the metadata object necessary to construct a challenge. + */ + public fun provide(inetAddress: InetAddress): T +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeType.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeType.kt new file mode 100644 index 000000000..c8c497362 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeType.kt @@ -0,0 +1,23 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +import net.rsprot.buffer.JagByteBuf + +/** + * A common binding interface for challenge types. + * Currently, the client only supports SHA-256 as a challenge, but it is set up to + * support other types with ease. + * @param MetaData the metadata necessary to construct a challenge of this type. + * @property id the id of the challenge, used by the client to identify what challenge + * solver to use. + */ +public interface ChallengeType { + public val id: Int + + /** + * A function to encode the given challenge into the byte buffer that the client expects. + * The role of encoding is moved over to the implementation as the server can provide + * its own implementations, should the client support any. + * @param buffer the buffer into which to encode the challenge + */ + public fun encode(buffer: JagByteBuf) +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeVerifier.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeVerifier.kt new file mode 100644 index 000000000..e47bf85ff --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeVerifier.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +/** + * A challenge verifier is used to check the work that the client did. + * The general idea here is that the client has to perform the work N times, where N is + * pseudo-random, while the server only has to do that same work one time - to verify the + * result that the client sent. The complexity of the work to perform is configurable by the + * server. + * @param T the challenge type to verify + */ +public interface ChallengeVerifier> { + /** + * Verifies the work performed by the client. + * @param result the 64-bit long response value from the client + * @param challenge the challenge to verify using the [result] provided. + * @return whether the challenge is solved using the [result] provided. + */ + public fun verify( + result: Long, + challenge: T, + ): Boolean +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeWorker.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeWorker.kt new file mode 100644 index 000000000..f6c070f20 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/ChallengeWorker.kt @@ -0,0 +1,25 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +import java.util.concurrent.CompletableFuture + +/** + * A worker is used to perform the verifications of the data sent by the client for our + * proof of work requests. While the work itself is relatively cheap, servers may wish + * to perform the work on other threads - this interface allows doing that. + */ +public interface ChallengeWorker { + /** + * Verifies the result sent by the client. + * @param result the byte buffer containing the result data sent by the client + * @param challenge the challenge the client had to solve + * @param verifier the verifier used to check the work done by the client for out challenge + * @return a future object containing the result of the work, or an exception. + * If the future doesn't return immediately, there will be a 30-second timeout applied to it, + * after which the work will be concluded failed. + */ + public fun , V : ChallengeVerifier> verify( + result: Long, + challenge: T, + verifier: V, + ): CompletableFuture +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/DefaultChallengeWorker.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/DefaultChallengeWorker.kt new file mode 100644 index 000000000..2ae28c7b7 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/DefaultChallengeWorker.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges + +import java.util.concurrent.CompletableFuture + +/** + * The default challenge worker will perform the work on the calling thread. + * The SHA-256 challenges are fairly inexpensive and the overhead of switching threads + * is similar to the work itself done. + */ +public data object DefaultChallengeWorker : ChallengeWorker { + public override fun , V : ChallengeVerifier> verify( + result: Long, + challenge: T, + verifier: V, + ): CompletableFuture = + try { + CompletableFuture.completedFuture(verifier.verify(result, challenge)) + } catch (t: Throwable) { + CompletableFuture.failedFuture(t) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ChallengeGenerator.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ChallengeGenerator.kt new file mode 100644 index 000000000..3ef4fa218 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ChallengeGenerator.kt @@ -0,0 +1,32 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeGenerator +import java.math.BigInteger +import kotlin.random.Random + +/** + * The default SHA-256 challenge generator is used to generate challenges which align + * up with what OldSchool RuneScape is generating, which is a combination of epoch time millis, + * the world id and a 495-byte [BigInteger] that is turned into a hexadecimal string, + * which will have a length of 1004 or 1005 characters, depending on if the [BigInteger] was negative. + */ +public class DefaultSha256ChallengeGenerator : ChallengeGenerator { + override fun generate(input: Sha256MetaData): Sha256Challenge { + val randomData = Random.Default.nextBytes(RANDOM_DATA_LENGTH) + val hexSalt = BigInteger(randomData).toString(HEX_RADIX) + val salt = + java.lang.Long.toHexString(input.epochTimeMillis) + + Integer.toHexString(input.world) + + hexSalt + return Sha256Challenge( + input.unknown, + input.difficulty, + salt, + ) + } + + private companion object { + private const val RANDOM_DATA_LENGTH: Int = 495 + private const val HEX_RADIX: Int = 16 + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256MetaDataProvider.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256MetaDataProvider.kt new file mode 100644 index 000000000..fda5ea645 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256MetaDataProvider.kt @@ -0,0 +1,15 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaDataProvider +import java.net.InetAddress + +/** + * The default SHA-256 metadata provider will return a metadata object + * that matches what OldSchool RuneScape sends. + * @property world the world that the client is connecting to. + */ +public class DefaultSha256MetaDataProvider( + private val world: Int, +) : ChallengeMetaDataProvider { + override fun provide(inetAddress: InetAddress): Sha256MetaData = Sha256MetaData(world) +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ProofOfWorkProvider.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ProofOfWorkProvider.kt new file mode 100644 index 000000000..c9119b4ea --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/DefaultSha256ProofOfWorkProvider.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.protocol.loginprot.incoming.pow.ProofOfWorkProvider +import net.rsprot.protocol.loginprot.incoming.pow.SingleTypeProofOfWorkProvider + +/** + * A value class to wrap the properties of a SHA-256 into a single instance. + * @property provider the SHA-256 proof of work provider. + */ +@Suppress("MemberVisibilityCanBePrivate") +@JvmInline +public value class DefaultSha256ProofOfWorkProvider private constructor( + public val provider: SingleTypeProofOfWorkProvider, +) : ProofOfWorkProvider by provider { + public constructor( + world: Int, + ) : this( + SingleTypeProofOfWorkProvider( + DefaultSha256MetaDataProvider(world), + DefaultSha256ChallengeGenerator(), + Sha256ChallengeVerifier(), + ), + ) +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256Challenge.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256Challenge.kt new file mode 100644 index 000000000..6975822e2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256Challenge.kt @@ -0,0 +1,76 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeType + +/** + * A SHA-256 challenge is a challenge which forces the client to find a hash which + * has at least [difficulty] number of leading zero bits in the hash. + * As hashing returns pseudo-random results, as a general rule of thumb, the work + * needed to solve a challenge doubles with each difficulty increase, since each + * bit can be either true or false, and the solution must have at least [difficulty] + * amount of false (zero) bits. + * Since the requirement is that there are at least [difficulty] amount of leading + * zero bits, these challenges aren't constrained to only having a single successful + * answer. + * @property unknown an unknown byte value that is appended to the start of each + * base string that needs hashing. The value of this byte is **always** one in OldSchool + * RuneScape. + * @property difficulty the difficulty of the challenge, as explained above, is the number + * of leading zero bits the hash must have for it to be considered successful. + * The default difficulty in OldSchool RuneScape is 18 as of writing this. + * When Proof of Work was first introduced, this value was 16. + * It is possible that the value gets dynamically increased as the pressure increases, + * or if there are a lot of requests from a single IP. + * @property salt the salt string that is the bulk of the input to hash. + * @property id the id of the challenge as identified by the client. + */ +public class Sha256Challenge( + public val unknown: Int, + public val difficulty: Int, + public val salt: String, +) : ChallengeType { + override val id: Int + get() = 0 + + override fun encode(buffer: JagByteBuf) { + buffer.p1(unknown) + buffer.p1(difficulty) + buffer.pjstr(salt, Charsets.UTF_8) + } + + /** + * Gets the base string that is part of the input for the hash. + * A long will be appended to this base string at the end, which will additionally + * be the solution to the challenge. The full string of baseString + the long is what + * must result in [difficulty] number of leading zero bits after having been hashed. + * @return the base string used for the hashing input. + */ + public fun getBaseString(): String = + Integer.toHexString(this.unknown) + Integer.toHexString(this.difficulty) + this.salt + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Sha256Challenge) return false + + if (unknown != other.unknown) return false + if (difficulty != other.difficulty) return false + if (salt != other.salt) return false + + return true + } + + override fun hashCode(): Int { + var result = unknown + result = 31 * result + difficulty + result = 31 * result + salt.hashCode() + return result + } + + override fun toString(): String = + "Sha256Challenge(" + + "unknown=$unknown, " + + "difficulty=$difficulty, " + + "salt='$salt'" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256ChallengeVerifier.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256ChallengeVerifier.kt new file mode 100644 index 000000000..f0e1897b0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256ChallengeVerifier.kt @@ -0,0 +1,78 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeVerifier +import net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.hashing.DefaultSha256MessageDigestHashFunction +import net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.hashing.Sha256HashFunction + +/** + * The SHA-256 challenge verifier is a replica of the client's implementation of the + * SHA-256 proof of work verifier. + * @property hashFunction the function used to hash the input bytes, with the default + * implementation being the same as the client - making a new MessageDigest object + * for each hash. These objects are fairly cheap, though. + */ +public class Sha256ChallengeVerifier( + private val hashFunction: Sha256HashFunction = DefaultSha256MessageDigestHashFunction, +) : ChallengeVerifier { + override fun verify( + result: Long, + challenge: Sha256Challenge, + ): Boolean { + val baseString = challenge.getBaseString() + val builder = StringBuilder(baseString) + builder.append(java.lang.Long.toHexString(result)) + val utf8ByteArray = + builder + .toString() + .toByteArray(Charsets.UTF_8) + val hash = hashFunction.hash(utf8ByteArray) + return leadingZeros(hash) >= challenge.difficulty + } + + /** + * Counts the number of leading zero bits in the [byteArray]. + * @param byteArray the byte array to check for leading zero bits. + * @return the number of leading zero bits in the byte array. + */ + private fun leadingZeros(byteArray: ByteArray): Int { + var numBits = 0 + for (byte in byteArray) { + val bitCount = leadingZeros(byte) + numBits += bitCount + if (bitCount != Byte.SIZE_BITS) { + break + } + } + return numBits + } + + /** + * Gets the number of leading zero bits in the provided [byte]. + * @return the number of leading zero bits in the byte. + */ + private fun leadingZeros(byte: Byte): Int { + var value = byte.toInt() and 0xFF + if (value == 0) { + return Byte.SIZE_BITS + } + var numBits = 0 + while (value and 0x80 == 0) { + numBits++ + value = value shl 1 + } + return numBits + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Sha256ChallengeVerifier) return false + + if (hashFunction != other.hashFunction) return false + + return true + } + + override fun hashCode(): Int = hashFunction.hashCode() + + override fun toString(): String = "Sha256ChallengeVerifier(hashFunction=$hashFunction)" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256MetaData.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256MetaData.kt new file mode 100644 index 000000000..7953e1c6c --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/Sha256MetaData.kt @@ -0,0 +1,52 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256 + +import net.rsprot.protocol.loginprot.incoming.pow.challenges.ChallengeMetaData + +/** + * The SHA-256 metadata is what the default SHA-256 implementation requires in order + * to construct new challenges. + * @property world the world that the client is connecting to. The world id is the second argument + * to the string that will be hashed. + * @property difficulty the difficulty of the challenge, which is the number of leading zero bits + * that the hash must have for it to be considered successful. + * @property epochTimeMillis the epoch time milliseconds when the request was made. + * This value is the very first section of the hash input. + * @property unknown an unknown byte value - this value is always one in OldSchool RuneScape; it is + * unclear what it is meant to represent. + */ +public class Sha256MetaData + @JvmOverloads + public constructor( + public val world: Int, + public val difficulty: Int = 18, + public val epochTimeMillis: Long = System.currentTimeMillis(), + public val unknown: Int = 1, + ) : ChallengeMetaData { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Sha256MetaData) return false + + if (world != other.world) return false + if (difficulty != other.difficulty) return false + if (epochTimeMillis != other.epochTimeMillis) return false + if (unknown != other.unknown) return false + + return true + } + + override fun hashCode(): Int { + var result = world + result = 31 * result + difficulty + result = 31 * result + epochTimeMillis.hashCode() + result = 31 * result + unknown + return result + } + + override fun toString(): String = + "Sha256MetaData(" + + "world=$world, " + + "difficulty=$difficulty, " + + "epochTimeMillis=$epochTimeMillis, " + + "unknown=$unknown" + + ")" + } diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/DefaultSha256MessageDigestHashFunction.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/DefaultSha256MessageDigestHashFunction.kt new file mode 100644 index 000000000..6f7862b9f --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/DefaultSha256MessageDigestHashFunction.kt @@ -0,0 +1,17 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.hashing + +import java.security.MessageDigest + +/** + * The default SHA-256 hash function using the [MessageDigest] implementation. + * Each hash request will generate a new instance of the [MessageDigest] object, + * as these implementations are not thread safe. + * These [MessageDigest] instances however are relatively cheap to construct. + */ +public data object DefaultSha256MessageDigestHashFunction : Sha256HashFunction { + override fun hash(input: ByteArray): ByteArray { + val messageDigest = MessageDigest.getInstance("SHA-256") + messageDigest.update(input) + return messageDigest.digest() + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/Sha256HashFunction.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/Sha256HashFunction.kt new file mode 100644 index 000000000..3bd9bf6fe --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/Sha256HashFunction.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.hashing + +/** + * A SHA-256 hash function is a function used to turn the input bytes into a valid SHA-256 hash. + */ +public interface Sha256HashFunction { + /** + * The hash function takes an [input] byte array and turns it into a valid SHA-256 hash. + * @param input the input byte array to be hashed. + * @return the SHA-256 hash. + */ + public fun hash(input: ByteArray): ByteArray +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/ThreadLocalSha256MessageDigestHashFunction.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/ThreadLocalSha256MessageDigestHashFunction.kt new file mode 100644 index 000000000..b8b5d01ee --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/pow/challenges/sha256/hashing/ThreadLocalSha256MessageDigestHashFunction.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.loginprot.incoming.pow.challenges.sha256.hashing + +import java.security.MessageDigest + +/** + * A SHA-256 hash function using the [MessageDigest] implementation. + * Unlike the default implementation, this one will utilize a thread-local implementation + * of the [MessageDigest] instances, which are all reset before use. + * + * @property digesters the thread-local message digest instances of SHA-256. + */ +public data object ThreadLocalSha256MessageDigestHashFunction : Sha256HashFunction { + private val digesters = + ThreadLocal.withInitial { + MessageDigest.getInstance("SHA-256") + } + + override fun hash(input: ByteArray): ByteArray { + val messageDigest = digesters.get() + messageDigest.reset() + messageDigest.update(input) + return messageDigest.digest() + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/AuthenticationType.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/AuthenticationType.kt new file mode 100644 index 000000000..18f1b2fce --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/AuthenticationType.kt @@ -0,0 +1,32 @@ +package net.rsprot.protocol.loginprot.incoming.util + +public sealed interface AuthenticationType { + public val otpAuthentication: T + + /** + * Clears all data stored in this [AuthenticationType]. + * OTP codes will all be set to [Int.MIN_VALUE], + * password and token fields will be filled with zeros. + */ + public fun clear() + + public data class PasswordAuthentication( + public val password: Password, + override val otpAuthentication: T, + ) : AuthenticationType { + override fun clear() { + otpAuthentication.clear() + password.clear() + } + } + + public data class TokenAuthentication( + public val token: Token, + override val otpAuthentication: T, + ) : AuthenticationType { + override fun clear() { + otpAuthentication.clear() + token.clear() + } + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/CyclicRedundancyCheckBlock.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/CyclicRedundancyCheckBlock.kt new file mode 100644 index 000000000..58edab1bb --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/CyclicRedundancyCheckBlock.kt @@ -0,0 +1,34 @@ +package net.rsprot.protocol.loginprot.incoming.util + +/** + * CRC blocks are helper structures used for the server to verify that the CRC is up-to-date. + * As the client transmits less CRCs than there are cache indices, we provide validation methods + * through this abstract class at the respective revision's decoder level, so we can perform checks + * that correspond to the information received from the client, and not what the server fully knows of. + * @property clientCrc the int array of client CRCs, indexed by the cache archives. + */ +public abstract class CyclicRedundancyCheckBlock( + protected val clientCrc: IntArray, +) { + public abstract fun validate(serverCrc: IntArray): Boolean + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CyclicRedundancyCheckBlock) return false + + if (!clientCrc.contentEquals(other.clientCrc)) return false + + return true + } + + internal fun set( + index: Int, + value: Int, + ) { + this.clientCrc[index] = value + } + + override fun hashCode(): Int = clientCrc.contentHashCode() + + override fun toString(): String = "CyclicRedundancyCheckBlock(clientCrc=${clientCrc.contentToString()})" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/HostPlatformStats.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/HostPlatformStats.kt new file mode 100644 index 000000000..b7bd63872 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/HostPlatformStats.kt @@ -0,0 +1,160 @@ +package net.rsprot.protocol.loginprot.incoming.util + +@Suppress("DuplicatedCode", "MemberVisibilityCanBePrivate") +public class HostPlatformStats( + private val _version: UByte, + private val _osType: UByte, + public val os64Bit: Boolean, + private val _osVersion: UShort, + private val _javaVendor: UByte, + private val _javaVersionMajor: UByte, + private val _javaVersionMinor: UByte, + private val _javaVersionPatch: UByte, + public val applet: Boolean, + private val _javaMaxMemoryMb: UShort, + private val _javaAvailableProcessors: UByte, + public val systemMemory: Int, + private val _systemSpeed: UShort, + public val gpuDxName: String, + public val gpuGlName: String, + public val gpuDxVersion: String, + public val gpuGlVersion: String, + private val _gpuDriverMonth: UByte, + private val _gpuDriverYear: UShort, + public val cpuManufacturer: String, + public val cpuBrand: String, + private val _cpuCount1: UByte, + private val _cpuCount2: UByte, + public val cpuFeatures: IntArray, + public val cpuSignature: Int, + public val clientName: String, + public val deviceName: String, +) { + public val version: Int + get() = _version.toInt() + public val osType: Int + get() = _osType.toInt() + public val osVersion: Int + get() = _osVersion.toInt() + public val javaVendor: Int + get() = _javaVendor.toInt() + public val javaVersionMajor: Int + get() = _javaVersionMajor.toInt() + public val javaVersionMinor: Int + get() = _javaVersionMinor.toInt() + public val javaVersionPatch: Int + get() = _javaVersionPatch.toInt() + public val javaMaxMemoryMb: Int + get() = _javaMaxMemoryMb.toInt() + public val javaAvailableProcessors: Int + get() = _javaAvailableProcessors.toInt() + public val systemSpeed: Int + get() = _systemSpeed.toInt() + public val gpuDriverMonth: Int + get() = _gpuDriverMonth.toInt() + public val gpuDriverYear: Int + get() = _gpuDriverYear.toInt() + public val cpuCount1: Int + get() = _cpuCount1.toInt() + public val cpuCount2: Int + get() = _cpuCount2.toInt() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as HostPlatformStats + + if (_version != other._version) return false + if (_osType != other._osType) return false + if (os64Bit != other.os64Bit) return false + if (_osVersion != other._osVersion) return false + if (_javaVendor != other._javaVendor) return false + if (_javaVersionMajor != other._javaVersionMajor) return false + if (_javaVersionMinor != other._javaVersionMinor) return false + if (_javaVersionPatch != other._javaVersionPatch) return false + if (applet != other.applet) return false + if (_javaMaxMemoryMb != other._javaMaxMemoryMb) return false + if (_javaAvailableProcessors != other._javaAvailableProcessors) return false + if (systemMemory != other.systemMemory) return false + if (_systemSpeed != other._systemSpeed) return false + if (gpuDxName != other.gpuDxName) return false + if (gpuGlName != other.gpuGlName) return false + if (gpuDxVersion != other.gpuDxVersion) return false + if (gpuGlVersion != other.gpuGlVersion) return false + if (_gpuDriverMonth != other._gpuDriverMonth) return false + if (_gpuDriverYear != other._gpuDriverYear) return false + if (cpuManufacturer != other.cpuManufacturer) return false + if (cpuBrand != other.cpuBrand) return false + if (_cpuCount1 != other._cpuCount1) return false + if (_cpuCount2 != other._cpuCount2) return false + if (!cpuFeatures.contentEquals(other.cpuFeatures)) return false + if (cpuSignature != other.cpuSignature) return false + if (clientName != other.clientName) return false + if (deviceName != other.deviceName) return false + + return true + } + + override fun hashCode(): Int { + var result = _version.hashCode() + result = 31 * result + _osType.hashCode() + result = 31 * result + os64Bit.hashCode() + result = 31 * result + _osVersion.hashCode() + result = 31 * result + _javaVendor.hashCode() + result = 31 * result + _javaVersionMajor.hashCode() + result = 31 * result + _javaVersionMinor.hashCode() + result = 31 * result + _javaVersionPatch.hashCode() + result = 31 * result + applet.hashCode() + result = 31 * result + _javaMaxMemoryMb.hashCode() + result = 31 * result + _javaAvailableProcessors.hashCode() + result = 31 * result + systemMemory + result = 31 * result + _systemSpeed.hashCode() + result = 31 * result + gpuDxName.hashCode() + result = 31 * result + gpuGlName.hashCode() + result = 31 * result + gpuDxVersion.hashCode() + result = 31 * result + gpuGlVersion.hashCode() + result = 31 * result + _gpuDriverMonth.hashCode() + result = 31 * result + _gpuDriverYear.hashCode() + result = 31 * result + cpuManufacturer.hashCode() + result = 31 * result + cpuBrand.hashCode() + result = 31 * result + _cpuCount1.hashCode() + result = 31 * result + _cpuCount2.hashCode() + result = 31 * result + cpuFeatures.contentHashCode() + result = 31 * result + cpuSignature + result = 31 * result + clientName.hashCode() + result = 31 * result + deviceName.hashCode() + return result + } + + override fun toString(): String = + "HostPlatformStats(" + + "os64Bit=$os64Bit, " + + "systemMemory=$systemMemory, " + + "gpuDxName='$gpuDxName', " + + "gpuGlName='$gpuGlName', " + + "gpuDxVersion='$gpuDxVersion', " + + "gpuGlVersion='$gpuGlVersion', " + + "cpuManufacturer='$cpuManufacturer', " + + "cpuBrand='$cpuBrand', " + + "cpuFeatures=${cpuFeatures.contentToString()}, " + + "cpuSignature=$cpuSignature, " + + "clientName='$clientName', " + + "deviceName='$deviceName', " + + "version=$version, " + + "osType=$osType, " + + "osVersion=$osVersion, " + + "javaVendor=$javaVendor, " + + "javaVersionMajor=$javaVersionMajor, " + + "javaVersionMinor=$javaVersionMinor, " + + "javaVersionPatch=$javaVersionPatch, " + + "applet=$applet, " + + "javaMaxMemoryMb=$javaMaxMemoryMb, " + + "javaAvailableProcessors=$javaAvailableProcessors, " + + "systemSpeed=$systemSpeed, " + + "gpuDriverMonth=$gpuDriverMonth, " + + "gpuDriverYear=$gpuDriverYear, " + + "cpuCount1=$cpuCount1, " + + "cpuCount2=$cpuCount2" + + ")" +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginBlock.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginBlock.kt new file mode 100644 index 000000000..ff0da3ac0 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginBlock.kt @@ -0,0 +1,152 @@ +package net.rsprot.protocol.loginprot.incoming.util + +import net.rsprot.protocol.loginprot.incoming.RemainingBetaArchives + +@Suppress("MemberVisibilityCanBePrivate", "DuplicatedCode") +public class LoginBlock( + public val version: Int, + public val subVersion: Int, + private val _clientType: UByte, + private val _platformType: UByte, + private val _constZero1: UByte, + public val seed: IntArray, + public val sessionId: Long, + public val username: String, + public val lowDetail: Boolean, + public val resizable: Boolean, + private val _width: UShort, + private val _height: UShort, + public val uuid: ByteArray, + public val siteSettings: String, + public val affiliate: Int, + private val _constZero2: UByte, + public val hostPlatformStats: HostPlatformStats, + private val _validationClientType: UByte, + private val _crcBlockHeader: UByte, + public val crc: CyclicRedundancyCheckBlock, + public val authentication: T, +) { + public val clientType: LoginClientType + get() = LoginClientType[_clientType.toInt()] + public val platformType: LoginPlatformType + get() = LoginPlatformType[_platformType.toInt()] + public val constZero1: Int + get() = _constZero1.toInt() + public val width: Int + get() = _width.toInt() + public val height: Int + get() = _height.toInt() + public val constZero2: Int + get() = _constZero2.toInt() + public val validationClientType: LoginClientType + get() = LoginClientType[_validationClientType.toInt()] + public val crcBlockHeader: Int + get() = _crcBlockHeader.toInt() + + public fun mergeBetaCrcs(remainingBetaArchives: RemainingBetaArchives) { + for (i in remainingBetaArchiveIndices) { + this.crc.set(i, remainingBetaArchives.crc[i]) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as LoginBlock<*> + + if (version != other.version) return false + if (subVersion != other.subVersion) return false + if (_clientType != other._clientType) return false + if (_platformType != other._platformType) return false + if (_constZero1 != other._constZero1) return false + if (!seed.contentEquals(other.seed)) return false + if (sessionId != other.sessionId) return false + if (username != other.username) return false + if (lowDetail != other.lowDetail) return false + if (resizable != other.resizable) return false + if (_width != other._width) return false + if (_height != other._height) return false + if (!uuid.contentEquals(other.uuid)) return false + if (siteSettings != other.siteSettings) return false + if (affiliate != other.affiliate) return false + if (_constZero2 != other._constZero2) return false + if (hostPlatformStats != other.hostPlatformStats) return false + if (_validationClientType != other._validationClientType) return false + if (_crcBlockHeader != other._crcBlockHeader) return false + if (crc != other.crc) return false + if (authentication != other.authentication) return false + + return true + } + + override fun hashCode(): Int { + var result = version + result = 31 * result + subVersion + result = 31 * result + _clientType.hashCode() + result = 31 * result + _platformType.hashCode() + result = 31 * result + _constZero1.hashCode() + result = 31 * result + seed.contentHashCode() + result = 31 * result + sessionId.hashCode() + result = 31 * result + username.hashCode() + result = 31 * result + lowDetail.hashCode() + result = 31 * result + resizable.hashCode() + result = 31 * result + _width.hashCode() + result = 31 * result + _height.hashCode() + result = 31 * result + uuid.contentHashCode() + result = 31 * result + siteSettings.hashCode() + result = 31 * result + affiliate + result = 31 * result + _constZero2.hashCode() + result = 31 * result + hostPlatformStats.hashCode() + result = 31 * result + _validationClientType.hashCode() + result = 31 * result + _crcBlockHeader.hashCode() + result = 31 * result + crc.hashCode() + result = 31 * result + authentication.hashCode() + return result + } + + override fun toString(): String = + "LoginBlock(" + + "version=$version, " + + "subVersion=$subVersion, " + + "seed=${seed.contentToString()}, " + + "sessionId=$sessionId, " + + "username='$username', " + + "lowDetail=$lowDetail, " + + "resizable=$resizable, " + + "uuid=${uuid.contentToString()}, " + + "siteSettings='$siteSettings', " + + "affiliate=$affiliate, " + + "hostPlatformStats=$hostPlatformStats, " + + "crc=$crc, " + + "clientType=$clientType, " + + "platformType=$platformType, " + + "constZero1=$constZero1, " + + "constZero2=$constZero2, " + + "width=$width, " + + "height=$height, " + + "validationClientType=$validationClientType, " + + "crcBlockHeader=$crcBlockHeader, " + + "authentication=$authentication" + + ")" + + private companion object { + private val remainingBetaArchiveIndices = + listOf( + 0, + 1, + 2, + 3, + 5, + 7, + 9, + 11, + 12, + 16, + 17, + 18, + 19, + 20, + ) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginBlockDecodingFunction.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginBlockDecodingFunction.kt new file mode 100644 index 000000000..4f1d16ae5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginBlockDecodingFunction.kt @@ -0,0 +1,10 @@ +package net.rsprot.protocol.loginprot.incoming.util + +import net.rsprot.buffer.JagByteBuf + +public fun interface LoginBlockDecodingFunction { + public fun decode( + buffer: JagByteBuf, + betaWorld: Boolean, + ): LoginBlock +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginClientType.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginClientType.kt new file mode 100644 index 000000000..02faec6a9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginClientType.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.loginprot.incoming.util + +public enum class LoginClientType( + public val id: Int, +) { + DESKTOP(1), + ANDROID(2), + IOS(3), + ENHANCED_WINDOWS(4), + ENHANCED_MAC(5), + ENHANCED_ANDROID(7), + ENHANCED_IOS(8), + ENHANCED_LINUX(10), + ; + + public companion object { + public operator fun get(id: Int): LoginClientType = + entries.firstOrNull { it.id == id } + ?: throw IllegalArgumentException("Unknown client type: $id") + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginPlatformType.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginPlatformType.kt new file mode 100644 index 000000000..5afb8df78 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/LoginPlatformType.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.loginprot.incoming.util + +public enum class LoginPlatformType( + public val id: Int, +) { + DEFAULT(0), + STEAM(1), + ANDROID(2), + APPLE(3), + JAGEX(5), + ; + + public companion object { + public operator fun get(id: Int): LoginPlatformType = + entries.firstOrNull { it.id == id } + ?: throw IllegalArgumentException("Unknown platform type: $id") + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/OtpAuthenticationType.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/OtpAuthenticationType.kt new file mode 100644 index 000000000..120ea8d98 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/OtpAuthenticationType.kt @@ -0,0 +1,46 @@ +package net.rsprot.protocol.loginprot.incoming.util + +public sealed interface OtpAuthenticationType { + /** + * Clear any data stored in the one-time passwords by setting the + * payload to [Int.MIN_VALUE], if there is a payload attached. + */ + public fun clear() + + public data class TrustedComputer( + public var identifier: Int, + ) : OtpAuthenticationType { + override fun clear() { + identifier = Int.MIN_VALUE + } + } + + public data class TrustedAuthenticator( + override var otp: Int, + ) : OtpAuthentication { + override fun clear() { + otp = Int.MIN_VALUE + } + } + + public data object NoMultiFactorAuthentication : OtpAuthenticationType { + override fun clear() { + } + } + + public data class UntrustedAuthentication( + override var otp: Int, + ) : OtpAuthentication { + override fun clear() { + otp = Int.MIN_VALUE + } + } + + public sealed interface OtpAuthentication : OtpAuthenticationType { + /** + * One-time password, typically referred to as authentication code. + * This is the 6-digit 24-bit code used by two-factor authenticators. + */ + public var otp: Int + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/Password.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/Password.kt new file mode 100644 index 000000000..09fd86f6d --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/Password.kt @@ -0,0 +1,27 @@ +package net.rsprot.protocol.loginprot.incoming.util + +/** + * A value class to hold the password. + * This class offers additional functionality to clear the data from memory, + * to avoid any potential memory attacks. + */ +@JvmInline +public value class Password( + public val data: ByteArray, +) { + /** + * Returns the string representation of the password. + */ + @Suppress("MemberVisibilityCanBePrivate") + public fun asString(): String = String(data) + + override fun toString(): String = "Password(password=${asString()})" + + /** + * Clears the data, setting all bytes to 0. + * Once [clear] has been called, the password will no longer be in memory. + */ + public fun clear() { + data.fill(0) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/Token.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/Token.kt new file mode 100644 index 000000000..80f81c1ca --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/incoming/util/Token.kt @@ -0,0 +1,24 @@ +package net.rsprot.protocol.loginprot.incoming.util + +/** + * A value class to hold the login token. + * This class offers additional functionality to clear the data from memory, + * to avoid any potential memory attacks. + */ +@JvmInline +public value class Token( + public val data: ByteArray, +) { + /** + * Returns the string representation of the token. + */ + public fun asString(): String = String(data) + + /** + * Clears the data, setting all bytes to 0. + * Once [clear] has been called, the token will no longer be in memory. + */ + public fun clear() { + data.fill(0) + } +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt new file mode 100644 index 000000000..f8970fa56 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/LoginResponse.kt @@ -0,0 +1,215 @@ +package net.rsprot.protocol.loginprot.outgoing + +import io.netty.buffer.ByteBuf +import io.netty.buffer.DefaultByteBufHolder +import net.rsprot.protocol.game.outgoing.info.playerinfo.PlayerInfo +import net.rsprot.protocol.loginprot.outgoing.util.AuthenticatorResponse +import net.rsprot.protocol.message.OutgoingLoginMessage + +public sealed interface LoginResponse : OutgoingLoginMessage { + public data class Successful( + public val sessionId: Long?, + ) : LoginResponse + + @Suppress("DataClassPrivateConstructor") + public data class Ok private constructor( + public val authenticatorResponse: AuthenticatorResponse, + private val _staffModLevel: UByte, + public val playerMod: Boolean, + private val _index: UShort, + public val member: Boolean, + public val accountHash: Long, + public val userId: Long, + public val userHash: Long, + ) : LoginResponse { + public constructor( + authenticatorResponse: AuthenticatorResponse, + staffModLevel: Int, + playerMod: Boolean, + index: Int, + member: Boolean, + accountHash: Long, + userId: Long, + userHash: Long, + ) : this( + authenticatorResponse, + staffModLevel.toUByte(), + playerMod, + index.toUShort(), + member, + accountHash, + userId, + userHash, + ) + + public val staffModLevel: Int + get() = _staffModLevel.toInt() + public val index: Int + get() = _index.toInt() + + override fun toString(): String = + "Ok(" + + "authenticatorResponse=$authenticatorResponse, " + + "playerMod=$playerMod, " + + "member=$member, " + + "accountHash=$accountHash, " + + "userId=$userId, " + + "userHash=$userHash, " + + "staffModLevel=$staffModLevel, " + + "index=$index" + + ")" + } + + public data object InvalidUsernameOrPassword : LoginResponse + + public data object Banned : LoginResponse + + public data object Duplicate : LoginResponse + + public data object ClientOutOfDate : LoginResponse + + public data object ServerFull : LoginResponse + + public data object LoginServerOffline : LoginResponse + + public data object IPLimit : LoginResponse + + public data object BadSessionId : LoginResponse + + public data object ForcePasswordChange : LoginResponse + + public data object NeedMembersAccount : LoginResponse + + public data object InvalidSave : LoginResponse + + public data object UpdateInProgress : LoginResponse + + public class ReconnectOk( + buffer: ByteBuf, + ) : DefaultByteBufHolder(buffer), + LoginResponse { + public constructor(worldId: Int, playerInfo: PlayerInfo) : this( + initializePlayerInfo(worldId, playerInfo), + ) + + private companion object { + private const val PLAYER_INFO_BLOCK_SIZE = ((30 + (2046 * 18)) + Byte.SIZE_BITS - 1) ushr 3 + + /** + * Initializes the player info block into a buffer provided by allocator in the playerinfo object + * @param playerInfo the player info protocol of this player to be initialized + * @return a buffer containing the initialization block of the player info protocol + */ + private fun initializePlayerInfo( + worldId: Int, + playerInfo: PlayerInfo, + ): ByteBuf { + val allocator = playerInfo.allocator + val buffer = allocator.buffer(PLAYER_INFO_BLOCK_SIZE) + playerInfo.handleAbsolutePlayerPositions(worldId, buffer) + return buffer + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + return true + } + + override fun hashCode(): Int = super.hashCode() + + override fun toString(): String = "ReconnectOk()" + } + + public data object TooManyAttempts : LoginResponse + + public data object InMembersArea : LoginResponse + + public data object Locked : LoginResponse + + public data object ClosedBetaInvitedOnly : LoginResponse + + public data object InvalidLoginServer : LoginResponse + + public data object HopBlocked : LoginResponse + + public data object InvalidLoginPacket : LoginResponse + + public data object LoginServerNoReply : LoginResponse + + public data object LoginServerLoadError : LoginResponse + + public data object UnknownReplyFromLoginServer : LoginResponse + + public data object IPBlocked : LoginResponse + + public data object ServiceUnavailable : LoginResponse + + public data class DisallowedByScript( + public val line1: String, + public val line2: String, + public val line3: String, + ) : LoginResponse + + public data object DisplayNameRequired : LoginResponse + + public data object NegativeCredit : LoginResponse + + public data object InvalidSingleSignOn : LoginResponse + + public data object NoReplyFromSingleSignOn : LoginResponse + + public data object ProfileBeingEdited : LoginResponse + + public data object NoBetaAccess : LoginResponse + + public data object InstanceInvalid : LoginResponse + + public data object InstanceNotSpecified : LoginResponse + + public data object InstanceFull : LoginResponse + + public data object InQueue : LoginResponse + + public data object AlreadyInQueue : LoginResponse + + public data object BillingTimeout : LoginResponse + + public data object NotAgreedToNda : LoginResponse + + public data object EmailNotValidated : LoginResponse + + public data object ConnectFail : LoginResponse + + public data object PrivacyPolicy : LoginResponse + + public data object Authenticator : LoginResponse + + public data object InvalidAuthenticatorCode : LoginResponse + + public data object UpdateDob : LoginResponse + + public data object Timeout : LoginResponse + + public data object Kick : LoginResponse + + public data object Retry : LoginResponse + + public data object LoginFail1 : LoginResponse + + public data object LoginFail2 : LoginResponse + + public data object OutOfDateReload : LoginResponse + + public class ProofOfWork( + public val proofOfWork: net.rsprot.protocol.loginprot.incoming.pow.ProofOfWork<*, *>, + ) : LoginResponse + + public data object DobError : LoginResponse + + public data object DobReview : LoginResponse + + public data object ClosedBeta : LoginResponse +} diff --git a/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/util/AuthenticatorResponse.kt b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/util/AuthenticatorResponse.kt new file mode 100644 index 000000000..545d827b2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-model/src/main/kotlin/net/rsprot/protocol/loginprot/outgoing/util/AuthenticatorResponse.kt @@ -0,0 +1,9 @@ +package net.rsprot.protocol.loginprot.outgoing.util + +public sealed interface AuthenticatorResponse { + public data object NoAuthenticator : AuthenticatorResponse + + public data class AuthenticatorCode( + public val code: Int, + ) : AuthenticatorResponse +} diff --git a/protocol/osrs-225/osrs-225-shared/build.gradle.kts b/protocol/osrs-225/osrs-225-shared/build.gradle.kts new file mode 100644 index 000000000..56bc85f0d --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/build.gradle.kts @@ -0,0 +1,20 @@ +dependencies { + api(platform(rootProject.libs.netty.bom)) + api(rootProject.libs.netty.buffer) + api(rootProject.libs.netty.transport) + api(projects.buffer) + api(projects.compression) + api(projects.crypto) + api(projects.protocol) + api(projects.protocol.osrs225.osrs225Model) + api(projects.protocol.osrs225.osrs225Internal) + api(projects.protocol.osrs225.osrs225Common) +} + +mavenPublishing { + pom { + name = "RsProt OSRS 225 Shared" + description = "The shared module for revision 225 OldSchool RuneScape networking, " + + "offering a set of shared classes that do not depend on a specific client." + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PrefetchRequestDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PrefetchRequestDecoder.kt new file mode 100644 index 000000000..5fd7166a6 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PrefetchRequestDecoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.common.js5.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.js5.incoming.prot.Js5ClientProt +import net.rsprot.protocol.js5.incoming.PrefetchRequest +import net.rsprot.protocol.message.codec.MessageDecoder + +public class PrefetchRequestDecoder : MessageDecoder { + override val prot: ClientProt = Js5ClientProt.PREFETCH_REQUEST + + override fun decode(buffer: JagByteBuf): PrefetchRequest { + val archiveId = buffer.g1() + val groupId = buffer.g2() + return PrefetchRequest( + archiveId.toUByte(), + groupId.toUShort(), + ) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PriorityChangeHighDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PriorityChangeHighDecoder.kt new file mode 100644 index 000000000..930278700 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PriorityChangeHighDecoder.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.common.js5.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.js5.incoming.prot.Js5ClientProt +import net.rsprot.protocol.js5.incoming.PriorityChangeHigh +import net.rsprot.protocol.message.codec.MessageDecoder + +public class PriorityChangeHighDecoder : MessageDecoder { + override val prot: ClientProt = Js5ClientProt.PRIORITY_CHANGE_HIGH + + override fun decode(buffer: JagByteBuf): PriorityChangeHigh { + buffer.skipRead(3) + return PriorityChangeHigh + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PriorityChangeLowDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PriorityChangeLowDecoder.kt new file mode 100644 index 000000000..44c6132f9 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/PriorityChangeLowDecoder.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.common.js5.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.js5.incoming.prot.Js5ClientProt +import net.rsprot.protocol.js5.incoming.PriorityChangeLow +import net.rsprot.protocol.message.codec.MessageDecoder + +public class PriorityChangeLowDecoder : MessageDecoder { + override val prot: ClientProt = Js5ClientProt.PRIORITY_CHANGE_LOW + + override fun decode(buffer: JagByteBuf): PriorityChangeLow { + buffer.skipRead(3) + return PriorityChangeLow + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/UrgentRequestDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/UrgentRequestDecoder.kt new file mode 100644 index 000000000..b55e28045 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/UrgentRequestDecoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.common.js5.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.js5.incoming.prot.Js5ClientProt +import net.rsprot.protocol.js5.incoming.UrgentRequest +import net.rsprot.protocol.message.codec.MessageDecoder + +public class UrgentRequestDecoder : MessageDecoder { + override val prot: ClientProt = Js5ClientProt.URGENT_REQUEST + + override fun decode(buffer: JagByteBuf): UrgentRequest { + val archiveId = buffer.g1() + val groupId = buffer.g2() + return UrgentRequest( + archiveId.toUByte(), + groupId.toUShort(), + ) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/XorChangeDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/XorChangeDecoder.kt new file mode 100644 index 000000000..a01e8de94 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/codec/XorChangeDecoder.kt @@ -0,0 +1,17 @@ +package net.rsprot.protocol.common.js5.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.js5.incoming.prot.Js5ClientProt +import net.rsprot.protocol.js5.incoming.XorChange +import net.rsprot.protocol.message.codec.MessageDecoder + +public class XorChangeDecoder : MessageDecoder { + override val prot: ClientProt = Js5ClientProt.XOR_CHANGE + + override fun decode(buffer: JagByteBuf): XorChange { + val key = buffer.g1() + buffer.skipRead(2) + return XorChange(key) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5ClientProt.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5ClientProt.kt new file mode 100644 index 000000000..a7bcfd2d2 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5ClientProt.kt @@ -0,0 +1,14 @@ +package net.rsprot.protocol.common.js5.incoming.prot + +import net.rsprot.protocol.ClientProt + +public enum class Js5ClientProt( + override val opcode: Int, + override val size: Int, +) : ClientProt { + PREFETCH_REQUEST(Js5ClientProtId.PREFETCH_REQUEST, 3), + URGENT_REQUEST(Js5ClientProtId.URGENT_REQUEST, 3), + PRIORITY_CHANGE_HIGH(Js5ClientProtId.PRIORITY_CHANGE_HIGH, 3), + PRIORITY_CHANGE_LOW(Js5ClientProtId.PRIORITY_CHANGE_LOW, 3), + XOR_CHANGE(Js5ClientProtId.XOR_CHANGE, 3), +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5ClientProtId.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5ClientProtId.kt new file mode 100644 index 000000000..b1e4ef29c --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5ClientProtId.kt @@ -0,0 +1,9 @@ +package net.rsprot.protocol.common.js5.incoming.prot + +internal object Js5ClientProtId { + const val PREFETCH_REQUEST = 0 + const val URGENT_REQUEST = 1 + const val PRIORITY_CHANGE_HIGH = 2 + const val PRIORITY_CHANGE_LOW = 3 + const val XOR_CHANGE = 4 +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5MessageDecoderRepository.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5MessageDecoderRepository.kt new file mode 100644 index 000000000..9c2ea833d --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/incoming/prot/Js5MessageDecoderRepository.kt @@ -0,0 +1,26 @@ +package net.rsprot.protocol.common.js5.incoming.prot + +import net.rsprot.protocol.ProtRepository +import net.rsprot.protocol.common.js5.incoming.codec.PrefetchRequestDecoder +import net.rsprot.protocol.common.js5.incoming.codec.PriorityChangeHighDecoder +import net.rsprot.protocol.common.js5.incoming.codec.PriorityChangeLowDecoder +import net.rsprot.protocol.common.js5.incoming.codec.UrgentRequestDecoder +import net.rsprot.protocol.common.js5.incoming.codec.XorChangeDecoder +import net.rsprot.protocol.message.codec.incoming.MessageDecoderRepository +import net.rsprot.protocol.message.codec.incoming.MessageDecoderRepositoryBuilder + +public object Js5MessageDecoderRepository { + @ExperimentalStdlibApi + public fun build(): MessageDecoderRepository { + val protRepository = ProtRepository.of() + val builder = + MessageDecoderRepositoryBuilder(protRepository).apply { + bind(PrefetchRequestDecoder()) + bind(PriorityChangeHighDecoder()) + bind(PriorityChangeLowDecoder()) + bind(UrgentRequestDecoder()) + bind(XorChangeDecoder()) + } + return builder.build() + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/outgoing/codec/Js5GroupResponseEncoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/outgoing/codec/Js5GroupResponseEncoder.kt new file mode 100644 index 000000000..77fa74e58 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/outgoing/codec/Js5GroupResponseEncoder.kt @@ -0,0 +1,34 @@ +package net.rsprot.protocol.common.js5.outgoing.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.js5.outgoing.prot.Js5ServerProt +import net.rsprot.protocol.js5.outgoing.Js5GroupResponse +import net.rsprot.protocol.message.codec.MessageEncoder + +public class Js5GroupResponseEncoder : MessageEncoder { + override val prot: ServerProt = Js5ServerProt.JS5_GROUP_RESPONSE + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: Js5GroupResponse, + ) { + val offset = message.offset + val limit = message.limit + val messageBuf = message.content() + if (message.key != 0) { + val out = buffer.buffer + for (i in offset.. { + val protRepository = ProtRepository.of() + val builder = + MessageEncoderRepositoryBuilder( + protRepository, + ).apply { + bind(Js5GroupResponseEncoder()) + } + return builder.build() + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/outgoing/prot/Js5ServerProt.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/outgoing/prot/Js5ServerProt.kt new file mode 100644 index 000000000..80a798752 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/js5/outgoing/prot/Js5ServerProt.kt @@ -0,0 +1,12 @@ +package net.rsprot.protocol.common.js5.outgoing.prot + +import net.rsprot.protocol.ServerProt + +public enum class Js5ServerProt( + override val opcode: Int, + override val size: Int, +) : ServerProt { + // Js5 responses have no actual opcode, + // but we do need to have something to identify it by within the lib + JS5_GROUP_RESPONSE(0, -2), +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/GameLoginDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/GameLoginDecoder.kt new file mode 100644 index 000000000..32c969479 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/GameLoginDecoder.kt @@ -0,0 +1,85 @@ +package net.rsprot.protocol.common.loginprot.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.loginprot.incoming.codec.shared.LoginBlockDecoder +import net.rsprot.protocol.common.loginprot.incoming.prot.LoginClientProt +import net.rsprot.protocol.loginprot.incoming.GameLogin +import net.rsprot.protocol.loginprot.incoming.util.AuthenticationType +import net.rsprot.protocol.loginprot.incoming.util.OtpAuthenticationType +import net.rsprot.protocol.loginprot.incoming.util.Password +import net.rsprot.protocol.loginprot.incoming.util.Token +import net.rsprot.protocol.message.codec.MessageDecoder +import java.math.BigInteger + +public class GameLoginDecoder( + exp: BigInteger, + mod: BigInteger, +) : LoginBlockDecoder>(exp, mod), + MessageDecoder { + override val prot: ClientProt = LoginClientProt.GAMELOGIN + + override fun decode(buffer: JagByteBuf): GameLogin { + val copy = buffer.buffer.copy() + // Mark the buffer as "read" as copy function doesn't do it automatically. + buffer.buffer.readerIndex(buffer.buffer.writerIndex()) + return GameLogin(copy.toJagByteBuf()) { jagByteBuf, betaWorld -> + decodeLoginBlock(jagByteBuf, betaWorld) + } + } + + override fun decodeAuthentication(buffer: JagByteBuf): AuthenticationType<*> { + val otp = decodeOtpAuthentication(buffer) + return when (val authenticationType = buffer.g1()) { + PASSWORD_AUTHENTICATION -> + AuthenticationType.PasswordAuthentication( + Password(buffer.gjstr().toByteArray()), + otp, + ) + TOKEN_AUTHENTICATION -> + AuthenticationType.TokenAuthentication( + Token(buffer.gjstr().toByteArray()), + otp, + ) + else -> { + throw IllegalStateException("Unknown authentication type: $authenticationType") + } + } + } + + private fun decodeOtpAuthentication(buffer: JagByteBuf): OtpAuthenticationType = + when (val otpType = buffer.g1()) { + OTP_TOKEN -> { + val identifier = buffer.g4() + OtpAuthenticationType.TrustedComputer(identifier) + } + OTP_REMEMBER -> { + val otpKey = buffer.g3() + buffer.skipRead(1) + OtpAuthenticationType.TrustedAuthenticator(otpKey) + } + OTP_NONE -> { + buffer.skipRead(4) + OtpAuthenticationType.NoMultiFactorAuthentication + } + OTP_FORGET -> { + val otpKey = buffer.g3() + buffer.skipRead(1) + OtpAuthenticationType.UntrustedAuthentication(otpKey) + } + else -> { + throw IllegalStateException("Unknown authentication type: $otpType") + } + } + + private companion object { + private const val OTP_TOKEN = 0 + private const val OTP_REMEMBER = 1 + private const val OTP_NONE = 2 + private const val OTP_FORGET = 3 + + private const val PASSWORD_AUTHENTICATION = 0 + private const val TOKEN_AUTHENTICATION = 2 + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/GameReconnectDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/GameReconnectDecoder.kt new file mode 100644 index 000000000..7c4dad8f5 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/GameReconnectDecoder.kt @@ -0,0 +1,35 @@ +package net.rsprot.protocol.common.loginprot.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.crypto.xtea.XteaKey +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.loginprot.incoming.codec.shared.LoginBlockDecoder +import net.rsprot.protocol.common.loginprot.incoming.prot.LoginClientProt +import net.rsprot.protocol.loginprot.incoming.GameReconnect +import net.rsprot.protocol.message.codec.MessageDecoder +import java.math.BigInteger + +public class GameReconnectDecoder( + exp: BigInteger, + mod: BigInteger, +) : LoginBlockDecoder(exp, mod), + MessageDecoder { + override val prot: ClientProt = LoginClientProt.GAMERECONNECT + + override fun decode(buffer: JagByteBuf): GameReconnect { + val copy = buffer.buffer.copy() + // Mark the buffer as "read" as copy function doesn't do it automatically. + buffer.buffer.readerIndex(buffer.buffer.writerIndex()) + return GameReconnect(copy.toJagByteBuf()) { jagByteBuf, betaWorld -> + decodeLoginBlock(jagByteBuf, betaWorld) + } + } + + override fun decodeAuthentication(buffer: JagByteBuf): XteaKey = + XteaKey( + IntArray(4) { + buffer.g4() + }, + ) +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/InitGameConnectionDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/InitGameConnectionDecoder.kt new file mode 100644 index 000000000..5079cd25c --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/InitGameConnectionDecoder.kt @@ -0,0 +1,13 @@ +package net.rsprot.protocol.common.loginprot.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.loginprot.incoming.prot.LoginClientProt +import net.rsprot.protocol.loginprot.incoming.InitGameConnection +import net.rsprot.protocol.message.codec.MessageDecoder + +public class InitGameConnectionDecoder : MessageDecoder { + override val prot: ClientProt = LoginClientProt.INIT_GAME_CONNECTION + + override fun decode(buffer: JagByteBuf): InitGameConnection = InitGameConnection +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/InitJs5RemoteConnectionDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/InitJs5RemoteConnectionDecoder.kt new file mode 100644 index 000000000..8a041466b --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/InitJs5RemoteConnectionDecoder.kt @@ -0,0 +1,20 @@ +package net.rsprot.protocol.common.loginprot.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.loginprot.incoming.prot.LoginClientProt +import net.rsprot.protocol.loginprot.incoming.InitJs5RemoteConnection +import net.rsprot.protocol.message.codec.MessageDecoder + +public class InitJs5RemoteConnectionDecoder : MessageDecoder { + override val prot: ClientProt = LoginClientProt.INIT_JS5REMOTE_CONNECTION + + override fun decode(buffer: JagByteBuf): InitJs5RemoteConnection { + val revision = buffer.g4() + val seed = + IntArray(4) { + buffer.g4() + } + return InitJs5RemoteConnection(revision, seed) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/ProofOfWorkReplyDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/ProofOfWorkReplyDecoder.kt new file mode 100644 index 000000000..71bc9b9f4 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/ProofOfWorkReplyDecoder.kt @@ -0,0 +1,16 @@ +package net.rsprot.protocol.common.loginprot.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.loginprot.incoming.prot.LoginClientProt +import net.rsprot.protocol.loginprot.incoming.ProofOfWorkReply +import net.rsprot.protocol.message.codec.MessageDecoder + +public class ProofOfWorkReplyDecoder : MessageDecoder { + override val prot: ClientProt = LoginClientProt.POW_REPLY + + override fun decode(buffer: JagByteBuf): ProofOfWorkReply { + val result = buffer.g8() + return ProofOfWorkReply(result) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/RemainingBetaArchivesDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/RemainingBetaArchivesDecoder.kt new file mode 100644 index 000000000..44d4eaa5b --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/RemainingBetaArchivesDecoder.kt @@ -0,0 +1,33 @@ +package net.rsprot.protocol.common.loginprot.incoming.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.protocol.ClientProt +import net.rsprot.protocol.common.loginprot.incoming.prot.LoginClientProt +import net.rsprot.protocol.loginprot.incoming.RemainingBetaArchives +import net.rsprot.protocol.message.codec.MessageDecoder + +public class RemainingBetaArchivesDecoder : MessageDecoder { + override val prot: ClientProt = LoginClientProt.REMAINING_BETA_ARCHIVE_CRCS + + override fun decode(buffer: JagByteBuf): RemainingBetaArchives { + check(buffer.g2() == 58) { + "Expected remaining beta archives size of 58" + } + val crc = IntArray(21) + crc[19] = buffer.g4() + crc[2] = buffer.g4() + crc[0] = buffer.g4Alt3() + crc[7] = buffer.g4Alt3() + crc[5] = buffer.g4Alt3() + crc[12] = buffer.g4() + crc[17] = buffer.g4Alt2() + crc[11] = buffer.g4() + crc[3] = buffer.g4Alt2() + crc[9] = buffer.g4Alt2() + crc[16] = buffer.g4Alt2() + crc[1] = buffer.g4Alt3() + crc[20] = buffer.g4Alt2() + crc[18] = buffer.g4Alt3() + return RemainingBetaArchives(crc) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/shared/LoginBlockDecoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/shared/LoginBlockDecoder.kt new file mode 100644 index 000000000..90606f9cc --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/incoming/codec/shared/LoginBlockDecoder.kt @@ -0,0 +1,238 @@ +package net.rsprot.protocol.common.loginprot.incoming.codec.shared + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.buffer.extensions.toJagByteBuf +import net.rsprot.crypto.rsa.decipherRsa +import net.rsprot.crypto.xtea.xteaDecrypt +import net.rsprot.protocol.loginprot.incoming.util.CyclicRedundancyCheckBlock +import net.rsprot.protocol.loginprot.incoming.util.HostPlatformStats +import net.rsprot.protocol.loginprot.incoming.util.LoginBlock +import java.math.BigInteger + +@Suppress("DuplicatedCode") +public abstract class LoginBlockDecoder( + private val exp: BigInteger, + private val mod: BigInteger, +) { + protected abstract fun decodeAuthentication(buffer: JagByteBuf): T + + protected fun decodeLoginBlock( + buffer: JagByteBuf, + betaWorld: Boolean, + ): LoginBlock { + try { + val version = buffer.g4() + val subVersion = buffer.g4() + val firstClientType = buffer.g1() + val platformType = buffer.g1() + val constZero1 = buffer.g1() + val rsaSize = buffer.g2() + if (!buffer.isReadable(rsaSize)) { + throw IllegalStateException("RSA buffer not readable: $rsaSize, ${buffer.readableBytes()}") + } + val rsaBuffer = + buffer.buffer + .decipherRsa( + exp, + mod, + rsaSize, + ).toJagByteBuf() + try { + val encryptionCheck = rsaBuffer.g1() + check(encryptionCheck == 1) { + "Invalid RSA check '$encryptionCheck'. " + + "This typically means the RSA in the client does not match up with the server." + } + val seed = + IntArray(4) { + rsaBuffer.g4() + } + val sessionId = rsaBuffer.g8() + val authentication = decodeAuthentication(rsaBuffer) + val xteaBuffer = buffer.buffer.xteaDecrypt(seed).toJagByteBuf() + try { + val username = xteaBuffer.gjstr() + val packedClientSettings = xteaBuffer.g1() + val lowDetail = packedClientSettings and 0x1 != 0 + val resizable = packedClientSettings and 0x2 != 0 + val width = xteaBuffer.g2() + val height = xteaBuffer.g2() + val uuid = + ByteArray(24) { + xteaBuffer.g1().toByte() + } + val siteSettings = xteaBuffer.gjstr() + val affiliate = xteaBuffer.g4() + val constZero2 = xteaBuffer.g1() + val hostPlatformStats = decodeHostPlatformStats(xteaBuffer) + val secondClientType = xteaBuffer.g1() + val crcBlockHeader = xteaBuffer.g4() + val crc = + if (betaWorld) { + decodeBetaCrc(xteaBuffer) + } else { + decodeCrc(xteaBuffer) + } + return LoginBlock( + version, + subVersion, + firstClientType.toUByte(), + platformType.toUByte(), + constZero1.toUByte(), + seed, + sessionId, + username, + lowDetail, + resizable, + width.toUShort(), + height.toUShort(), + uuid, + siteSettings, + affiliate, + constZero2.toUByte(), + hostPlatformStats, + secondClientType.toUByte(), + crcBlockHeader.toUByte(), + crc, + authentication, + ) + } finally { + xteaBuffer.buffer.release() + } + } finally { + rsaBuffer.buffer.release() + } + } finally { + buffer.buffer.release() + } + } + + private fun decodeCrc(buffer: JagByteBuf): CyclicRedundancyCheckBlock { + val crc = IntArray(TRANSMITTED_CRC_COUNT) + crc[19] = buffer.g4Alt3() + crc[6] = buffer.g4() + crc[14] = buffer.g4Alt3() + crc[2] = buffer.g4() + crc[16] = buffer.g4() + crc[0] = buffer.g4Alt3() + crc[15] = buffer.g4Alt3() + crc[10] = buffer.g4Alt1() + crc[20] = buffer.g4Alt1() + crc[1] = buffer.g4Alt2() + crc[12] = buffer.g4() + crc[17] = buffer.g4Alt2() + crc[3] = buffer.g4Alt1() + crc[18] = buffer.g4Alt3() + crc[8] = buffer.g4Alt1() + crc[5] = buffer.g4Alt3() + crc[7] = buffer.g4Alt2() + crc[11] = buffer.g4Alt3() + crc[4] = buffer.g4Alt2() + crc[13] = buffer.g4Alt1() + crc[9] = buffer.g4() + + return object : CyclicRedundancyCheckBlock(crc) { + override fun validate(serverCrc: IntArray): Boolean { + require(serverCrc.size >= TRANSMITTED_CRC_COUNT) { + "Server CRC length less than expected: ${serverCrc.size}, expected >= $TRANSMITTED_CRC_COUNT" + } + for (i in 0..= TRANSMITTED_CRC_COUNT) { + "Server CRC length less than expected: ${serverCrc.size}, expected >= $TRANSMITTED_CRC_COUNT" + } + for (i in 0.. { + val protRepository = ProtRepository.of() + val builder = + MessageDecoderRepositoryBuilder( + protRepository, + ).apply { + bind(InitGameConnectionDecoder()) + bind(InitJs5RemoteConnectionDecoder()) + bind(GameLoginDecoder(exp, mod)) + bind(GameReconnectDecoder(exp, mod)) + bind(ProofOfWorkReplyDecoder()) + bind(RemainingBetaArchivesDecoder()) + } + return builder.build() + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/DisallowedByScriptLoginResponseEncoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/DisallowedByScriptLoginResponseEncoder.kt new file mode 100644 index 000000000..782a7c917 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/DisallowedByScriptLoginResponseEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.common.loginprot.outgoing.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.loginprot.outgoing.prot.LoginServerProt +import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import net.rsprot.protocol.message.codec.MessageEncoder + +public class DisallowedByScriptLoginResponseEncoder : MessageEncoder { + override val prot: ServerProt = LoginServerProt.DISALLOWED_BY_SCRIPT + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: LoginResponse.DisallowedByScript, + ) { + buffer.pjstr(message.line1) + buffer.pjstr(message.line2) + buffer.pjstr(message.line3) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/EmptyLoginResponseEncoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/EmptyLoginResponseEncoder.kt new file mode 100644 index 000000000..2eca36712 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/EmptyLoginResponseEncoder.kt @@ -0,0 +1,18 @@ +package net.rsprot.protocol.common.loginprot.outgoing.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.message.OutgoingMessage +import net.rsprot.protocol.message.codec.MessageEncoder + +public class EmptyLoginResponseEncoder( + override val prot: ServerProt, +) : MessageEncoder { + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: T, + ) { + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/OkLoginResponseEncoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/OkLoginResponseEncoder.kt new file mode 100644 index 000000000..0adf7f420 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/OkLoginResponseEncoder.kt @@ -0,0 +1,41 @@ +package net.rsprot.protocol.common.loginprot.outgoing.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.loginprot.outgoing.prot.LoginServerProt +import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import net.rsprot.protocol.loginprot.outgoing.util.AuthenticatorResponse +import net.rsprot.protocol.message.codec.MessageEncoder + +public class OkLoginResponseEncoder : MessageEncoder { + override val prot: ServerProt = LoginServerProt.OK + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: LoginResponse.Ok, + ) { + when (val response = message.authenticatorResponse) { + is AuthenticatorResponse.AuthenticatorCode -> { + val code = response.code + buffer.p1(1) + buffer.p1((code ushr 24 and 0xFF) + streamCipher.nextInt()) + buffer.p1((code ushr 16 and 0xFF) + streamCipher.nextInt()) + buffer.p1((code ushr 8 and 0xFF) + streamCipher.nextInt()) + buffer.p1((code and 0xFF) + streamCipher.nextInt()) + } + AuthenticatorResponse.NoAuthenticator -> { + buffer.p1(0) + buffer.p4(0) + } + } + buffer.p1(message.staffModLevel) + buffer.pboolean(message.playerMod) + buffer.p2(message.index) + buffer.pboolean(message.member) + buffer.p8(message.accountHash) + buffer.p8(message.userId) + buffer.p8(message.userHash) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/ProofOfWorkResponseEncoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/ProofOfWorkResponseEncoder.kt new file mode 100644 index 000000000..1fe191684 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/ProofOfWorkResponseEncoder.kt @@ -0,0 +1,22 @@ +package net.rsprot.protocol.common.loginprot.outgoing.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.loginprot.outgoing.prot.LoginServerProt +import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import net.rsprot.protocol.message.codec.MessageEncoder + +public class ProofOfWorkResponseEncoder : MessageEncoder { + override val prot: ServerProt = LoginServerProt.PROOF_OF_WORK + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: LoginResponse.ProofOfWork, + ) { + val challenge = message.proofOfWork.challengeType + buffer.p1(message.proofOfWork.challengeType.id) + challenge.encode(buffer) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/ReconnectOkResponseEncoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/ReconnectOkResponseEncoder.kt new file mode 100644 index 000000000..4936d0767 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/ReconnectOkResponseEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.common.loginprot.outgoing.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.loginprot.outgoing.prot.LoginServerProt +import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import net.rsprot.protocol.message.codec.MessageEncoder + +public class ReconnectOkResponseEncoder : MessageEncoder { + override val prot: ServerProt = LoginServerProt.RECONNECT_OK + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: LoginResponse.ReconnectOk, + ) { + // Due to message extending byte buf holder, it is automatically released by the pipeline + buffer.buffer.writeBytes(message.content()) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/SuccessfulLoginResponseEncoder.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/SuccessfulLoginResponseEncoder.kt new file mode 100644 index 000000000..2b18b6d43 --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/codec/SuccessfulLoginResponseEncoder.kt @@ -0,0 +1,21 @@ +package net.rsprot.protocol.common.loginprot.outgoing.codec + +import net.rsprot.buffer.JagByteBuf +import net.rsprot.crypto.cipher.StreamCipher +import net.rsprot.protocol.ServerProt +import net.rsprot.protocol.common.loginprot.outgoing.prot.LoginServerProt +import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import net.rsprot.protocol.message.codec.MessageEncoder + +public class SuccessfulLoginResponseEncoder : MessageEncoder { + override val prot: ServerProt = LoginServerProt.SUCCESSFUL + + override fun encode( + streamCipher: StreamCipher, + buffer: JagByteBuf, + message: LoginResponse.Successful, + ) { + val sessionId = message.sessionId ?: return + buffer.p8(sessionId) + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginMessageEncoderRepository.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginMessageEncoderRepository.kt new file mode 100644 index 000000000..2ac7e801f --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginMessageEncoderRepository.kt @@ -0,0 +1,84 @@ +package net.rsprot.protocol.common.loginprot.outgoing.prot + +import net.rsprot.protocol.ProtRepository +import net.rsprot.protocol.common.loginprot.outgoing.codec.DisallowedByScriptLoginResponseEncoder +import net.rsprot.protocol.common.loginprot.outgoing.codec.EmptyLoginResponseEncoder +import net.rsprot.protocol.common.loginprot.outgoing.codec.OkLoginResponseEncoder +import net.rsprot.protocol.common.loginprot.outgoing.codec.ProofOfWorkResponseEncoder +import net.rsprot.protocol.common.loginprot.outgoing.codec.ReconnectOkResponseEncoder +import net.rsprot.protocol.common.loginprot.outgoing.codec.SuccessfulLoginResponseEncoder +import net.rsprot.protocol.loginprot.outgoing.LoginResponse +import net.rsprot.protocol.message.codec.outgoing.MessageEncoderRepository +import net.rsprot.protocol.message.codec.outgoing.MessageEncoderRepositoryBuilder + +private typealias Encoder = EmptyLoginResponseEncoder + +public object LoginMessageEncoderRepository { + @ExperimentalStdlibApi + public fun build(): MessageEncoderRepository { + val protRepository = ProtRepository.of() + val builder = + MessageEncoderRepositoryBuilder( + protRepository, + ).apply { + bind(OkLoginResponseEncoder()) + bind(DisallowedByScriptLoginResponseEncoder()) + bind(ProofOfWorkResponseEncoder()) + bind(SuccessfulLoginResponseEncoder()) + bind(ReconnectOkResponseEncoder()) + bind(Encoder(LoginServerProt.INVALID_USERNAME_OR_PASSWORD)) + bind(Encoder(LoginServerProt.BANNED)) + bind(Encoder(LoginServerProt.DUPLICATE)) + bind(Encoder(LoginServerProt.CLIENT_OUT_OF_DATE)) + bind(Encoder(LoginServerProt.SERVER_FULL)) + bind(Encoder(LoginServerProt.LOGINSERVER_OFFLINE)) + bind(Encoder(LoginServerProt.IP_LIMIT)) + bind(Encoder(LoginServerProt.BAD_SESSION_ID)) + bind(Encoder(LoginServerProt.FORCE_PASSWORD_CHANGE)) + bind(Encoder(LoginServerProt.NEED_MEMBERS_ACCOUNT)) + bind(Encoder(LoginServerProt.INVALID_SAVE)) + bind(Encoder(LoginServerProt.UPDATE_IN_PROGRESS)) + bind(Encoder(LoginServerProt.TOO_MANY_ATTEMPTS)) + bind(Encoder(LoginServerProt.IN_MEMBERS_AREA)) + bind(Encoder(LoginServerProt.LOCKED)) + bind(Encoder(LoginServerProt.CLOSED_BETA_INVITED_ONLY)) + bind(Encoder(LoginServerProt.INVALID_LOGINSERVER)) + bind(Encoder(LoginServerProt.HOP_BLOCKED)) + bind(Encoder(LoginServerProt.INVALID_LOGIN_PACKET)) + bind(Encoder(LoginServerProt.LOGINSERVER_NO_REPLY)) + bind(Encoder(LoginServerProt.LOGINSERVER_LOAD_ERROR)) + bind(Encoder(LoginServerProt.UNKNOWN_REPLY_FROM_LOGINSERVER)) + bind(Encoder(LoginServerProt.IP_BLOCKED)) + bind(Encoder(LoginServerProt.SERVICE_UNAVAILABLE)) + bind(Encoder(LoginServerProt.DISPLAYNAME_REQUIRED)) + bind(Encoder(LoginServerProt.NEGATIVE_CREDIT)) + bind(Encoder(LoginServerProt.INVALID_SINGLE_SIGNON)) + bind(Encoder(LoginServerProt.NO_REPLY_FROM_SINGLE_SIGNON)) + bind(Encoder(LoginServerProt.PROFILE_BEING_EDITED)) + bind(Encoder(LoginServerProt.NO_BETA_ACCESS)) + bind(Encoder(LoginServerProt.INSTANCE_INVALID)) + bind(Encoder(LoginServerProt.INSTANCE_NOT_SPECIFIED)) + bind(Encoder(LoginServerProt.INSTANCE_FULL)) + bind(Encoder(LoginServerProt.IN_QUEUE)) + bind(Encoder(LoginServerProt.ALREADY_IN_QUEUE)) + bind(Encoder(LoginServerProt.BILLING_TIMEOUT)) + bind(Encoder(LoginServerProt.NOT_AGREED_TO_NDA)) + bind(Encoder(LoginServerProt.EMAIL_NOT_VALIDATED)) + bind(Encoder(LoginServerProt.CONNECT_FAIL)) + bind(Encoder(LoginServerProt.PRIVACY_POLICY)) + bind(Encoder(LoginServerProt.AUTHENTICATOR)) + bind(Encoder(LoginServerProt.INVALID_AUTHENTICATOR_CODE)) + bind(Encoder(LoginServerProt.UPDATE_DOB)) + bind(Encoder(LoginServerProt.TIMEOUT)) + bind(Encoder(LoginServerProt.KICK)) + bind(Encoder(LoginServerProt.RETRY)) + bind(Encoder(LoginServerProt.LOGIN_FAIL_1)) + bind(Encoder(LoginServerProt.LOGIN_FAIL_2)) + bind(Encoder(LoginServerProt.OUT_OF_DATE_RELOAD)) + bind(Encoder(LoginServerProt.DOB_ERROR)) + bind(Encoder(LoginServerProt.DOB_REVIEW)) + bind(Encoder(LoginServerProt.CLOSED_BETA)) + } + return builder.build() + } +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginServerProt.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginServerProt.kt new file mode 100644 index 000000000..c8badfbec --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginServerProt.kt @@ -0,0 +1,67 @@ +package net.rsprot.protocol.common.loginprot.outgoing.prot + +import net.rsprot.protocol.ServerProt + +public enum class LoginServerProt( + override val opcode: Int, + override val size: Int, +) : ServerProt { + SUCCESSFUL(LoginServerProtId.SUCCESSFUL, 0), + OK(LoginServerProtId.OK, -1), + INVALID_USERNAME_OR_PASSWORD(LoginServerProtId.INVALID_USERNAME_OR_PASSWORD, 0), + BANNED(LoginServerProtId.BANNED, 0), + DUPLICATE(LoginServerProtId.DUPLICATE, 0), + CLIENT_OUT_OF_DATE(LoginServerProtId.CLIENT_OUT_OF_DATE, 0), + SERVER_FULL(LoginServerProtId.SERVER_FULL, 0), + LOGINSERVER_OFFLINE(LoginServerProtId.LOGINSERVER_OFFLINE, 0), + IP_LIMIT(LoginServerProtId.IP_LIMIT, 0), + BAD_SESSION_ID(LoginServerProtId.BAD_SESSION_ID, 0), + FORCE_PASSWORD_CHANGE(LoginServerProtId.FORCE_PASSWORD_CHANGE, 0), + NEED_MEMBERS_ACCOUNT(LoginServerProtId.NEED_MEMBERS_ACCOUNT, 0), + INVALID_SAVE(LoginServerProtId.INVALID_SAVE, 0), + UPDATE_IN_PROGRESS(LoginServerProtId.UPDATE_IN_PROGRESS, 0), + RECONNECT_OK(LoginServerProtId.RECONNECT_OK, -2), + TOO_MANY_ATTEMPTS(LoginServerProtId.TOO_MANY_ATTEMPTS, 0), + IN_MEMBERS_AREA(LoginServerProtId.IN_MEMBERS_AREA, 0), + LOCKED(LoginServerProtId.LOCKED, 0), + CLOSED_BETA_INVITED_ONLY(LoginServerProtId.CLOSED_BETA_INVITED_ONLY, 0), + INVALID_LOGINSERVER(LoginServerProtId.INVALID_LOGINSERVER, 0), + HOP_BLOCKED(LoginServerProtId.HOP_BLOCKED, 0), + INVALID_LOGIN_PACKET(LoginServerProtId.INVALID_LOGIN_PACKET, 0), + LOGINSERVER_NO_REPLY(LoginServerProtId.LOGINSERVER_NO_REPLY, 0), + LOGINSERVER_LOAD_ERROR(LoginServerProtId.LOGINSERVER_LOAD_ERROR, 0), + UNKNOWN_REPLY_FROM_LOGINSERVER(LoginServerProtId.UNKNOWN_REPLY_FROM_LOGINSERVER, 0), + IP_BLOCKED(LoginServerProtId.IP_BLOCKED, 0), + SERVICE_UNAVAILABLE(LoginServerProtId.SERVICE_UNAVAILABLE, 0), + DISALLOWED_BY_SCRIPT(LoginServerProtId.DISALLOWED_BY_SCRIPT, -2), + DISPLAYNAME_REQUIRED(LoginServerProtId.DISPLAYNAME_REQUIRED, 0), + NEGATIVE_CREDIT(LoginServerProtId.NEGATIVE_CREDIT, 0), + INVALID_SINGLE_SIGNON(LoginServerProtId.INVALID_SINGLE_SIGNON, 0), + NO_REPLY_FROM_SINGLE_SIGNON(LoginServerProtId.NO_REPLY_FROM_SINGLE_SIGNON, 0), + PROFILE_BEING_EDITED(LoginServerProtId.PROFILE_BEING_EDITED, 0), + NO_BETA_ACCESS(LoginServerProtId.NO_BETA_ACCESS, 0), + INSTANCE_INVALID(LoginServerProtId.INSTANCE_INVALID, 0), + INSTANCE_NOT_SPECIFIED(LoginServerProtId.INSTANCE_NOT_SPECIFIED, 0), + INSTANCE_FULL(LoginServerProtId.INSTANCE_FULL, 0), + IN_QUEUE(LoginServerProtId.IN_QUEUE, 0), + ALREADY_IN_QUEUE(LoginServerProtId.ALREADY_IN_QUEUE, 0), + BILLING_TIMEOUT(LoginServerProtId.BILLING_TIMEOUT, 0), + NOT_AGREED_TO_NDA(LoginServerProtId.NOT_AGREED_TO_NDA, 0), + EMAIL_NOT_VALIDATED(LoginServerProtId.EMAIL_NOT_VALIDATED, 0), + CONNECT_FAIL(LoginServerProtId.CONNECT_FAIL, 0), + PRIVACY_POLICY(LoginServerProtId.PRIVACY_POLICY, 0), + AUTHENTICATOR(LoginServerProtId.AUTHENTICATOR, 0), + INVALID_AUTHENTICATOR_CODE(LoginServerProtId.INVALID_AUTHENTICATOR_CODE, 0), + UPDATE_DOB(LoginServerProtId.UPDATE_DOB, 0), + TIMEOUT(LoginServerProtId.TIMEOUT, 0), + KICK(LoginServerProtId.KICK, 0), + RETRY(LoginServerProtId.RETRY, 0), + LOGIN_FAIL_1(LoginServerProtId.LOGIN_FAIL_1, 0), + LOGIN_FAIL_2(LoginServerProtId.LOGIN_FAIL_2, 0), + OUT_OF_DATE_RELOAD(LoginServerProtId.OUT_OF_DATE_RELOAD, 0), + PROOF_OF_WORK(LoginServerProtId.PROOF_OF_WORK, -2), + DOB_ERROR(LoginServerProtId.DOB_ERROR, 0), + WEBSITE_DOB(LoginServerProtId.WEBSITE_DOB, 0), + DOB_REVIEW(LoginServerProtId.DOB_REVIEW, 0), + CLOSED_BETA(LoginServerProtId.CLOSED_BETA, 0), +} diff --git a/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginServerProtId.kt b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginServerProtId.kt new file mode 100644 index 000000000..db8d1eb0c --- /dev/null +++ b/protocol/osrs-225/osrs-225-shared/src/main/kotlin/net/rsprot/protocol/common/loginprot/outgoing/prot/LoginServerProtId.kt @@ -0,0 +1,70 @@ +package net.rsprot.protocol.common.loginprot.outgoing.prot + +internal object LoginServerProtId { + /** + * TFU responses + */ + const val OK = 2 + const val INVALID_USERNAME_OR_PASSWORD = 3 + const val BANNED = 4 + const val DUPLICATE = 5 + const val CLIENT_OUT_OF_DATE = 6 + const val SERVER_FULL = 7 + const val LOGINSERVER_OFFLINE = 8 + const val IP_LIMIT = 9 + const val FORCE_PASSWORD_CHANGE = 11 + const val NEED_MEMBERS_ACCOUNT = 12 + const val INVALID_SAVE = 13 + const val UPDATE_IN_PROGRESS = 14 + const val RECONNECT_OK = 15 + const val TOO_MANY_ATTEMPTS = 16 + const val LOCKED = 18 + const val HOP_BLOCKED = 21 + const val INVALID_LOGIN_PACKET = 22 + const val LOGINSERVER_LOAD_ERROR = 24 + const val UNKNOWN_REPLY_FROM_LOGINSERVER = 25 + const val IP_BLOCKED = 26 + const val DISALLOWED_BY_SCRIPT = 29 + const val NEGATIVE_CREDIT = 32 + const val INVALID_SINGLE_SIGNON = 35 + const val NO_REPLY_FROM_SINGLE_SIGNON = 36 + const val PROFILE_BEING_EDITED = 37 + const val NO_BETA_ACCESS = 38 + const val INSTANCE_INVALID = 39 + const val INSTANCE_NOT_SPECIFIED = 40 + const val INSTANCE_FULL = 41 + const val IN_QUEUE = 42 + const val ALREADY_IN_QUEUE = 43 + const val BILLING_TIMEOUT = 44 + const val NOT_AGREED_TO_NDA = 45 + const val EMAIL_NOT_VALIDATED = 47 + const val CONNECT_FAIL = 50 + + /** + * Responses from the OSRS client. + * Namings here are guessed. + */ + const val SUCCESSFUL = 0 + const val BAD_SESSION_ID = 10 + const val IN_MEMBERS_AREA = 17 + const val CLOSED_BETA_INVITED_ONLY = 19 + const val INVALID_LOGINSERVER = 20 + const val LOGINSERVER_NO_REPLY = 23 + const val SERVICE_UNAVAILABLE = 27 + const val DISPLAYNAME_REQUIRED = 31 + const val PRIVACY_POLICY = 55 + const val AUTHENTICATOR = 56 + const val INVALID_AUTHENTICATOR_CODE = 57 + const val UPDATE_DOB = 61 + const val TIMEOUT = 62 + const val KICK = 63 + const val RETRY = 64 + const val LOGIN_FAIL_1 = 65 + const val LOGIN_FAIL_2 = 67 + const val OUT_OF_DATE_RELOAD = 68 + const val PROOF_OF_WORK = 69 + const val DOB_ERROR = 71 + const val WEBSITE_DOB = 72 + const val DOB_REVIEW = 73 + const val CLOSED_BETA = 74 +}