Showing preview only (1,346K chars total). Download the full file or copy to clipboard to get everything.
Repository: Sorapointa/Sorapointa
Branch: master
Commit: 31578cb9e460
Files: 479
Total size: 1.2 MB
Directory structure:
gitextract_y0piscv0/
├── .editorconfig
├── .git-hooks/
│ ├── commit-msg
│ └── pre-commit
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── api_check.yml
│ └── test.yml
├── .gitignore
├── .idea/
│ └── encodings.xml
├── LICENSE
├── build.gradle.kts
├── buildSrc/
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ └── src/
│ └── main/
│ └── kotlin/
│ ├── BuildConfigExtension.kt
│ ├── GitHook.kt
│ ├── JniHeader.kt
│ ├── OptInAnnotations.kt
│ ├── Properties.kt
│ ├── ResourcesCopy.kt
│ ├── Test.kt
│ ├── sorapointa-conventions.gradle.kts
│ └── sorapointa-publish.gradle.kts
├── docs/
│ ├── CONTRIBUTING.md
│ ├── CONTRIBUTING.zh-CN.md
│ ├── README.md
│ ├── README.zh-CN.md
│ └── guides/
│ ├── concurrency.md
│ ├── concurrency.zh-CN.md
│ ├── database.md
│ ├── database.zh-CN.md
│ ├── kotlin-atomicfu.md
│ ├── kotlin-atomicfu.zh-CN.md
│ ├── unit-test.md
│ └── unit-test.zh-CN.md
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── renovate.json
├── settings.gradle.kts
├── sorapointa-core/
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── kotlin/
│ │ │ └── org/
│ │ │ └── sorapointa/
│ │ │ ├── CoreBundle.kt
│ │ │ ├── Main.kt
│ │ │ ├── Sorapointa.kt
│ │ │ ├── SorapointaConfig.kt
│ │ │ ├── command/
│ │ │ │ ├── Command.kt
│ │ │ │ ├── CommandLocalization.kt
│ │ │ │ ├── CommandManager.kt
│ │ │ │ ├── CommandSender.kt
│ │ │ │ ├── ConsoleCommandSender.kt
│ │ │ │ ├── defaults/
│ │ │ │ │ ├── Defaults.kt
│ │ │ │ │ ├── console/
│ │ │ │ │ │ ├── ConsoleUser.kt
│ │ │ │ │ │ └── Quit.kt
│ │ │ │ │ └── general/
│ │ │ │ │ ├── Help.kt
│ │ │ │ │ ├── ListPlayer.kt
│ │ │ │ │ ├── LocaleCommand.kt
│ │ │ │ │ └── Version.kt
│ │ │ │ └── utils/
│ │ │ │ └── Options.kt
│ │ │ ├── console/
│ │ │ │ ├── Completer.kt
│ │ │ │ ├── Console.kt
│ │ │ │ ├── JLineRedirector.kt
│ │ │ │ ├── SoraHighlighter.kt
│ │ │ │ └── WebSocketConsole.kt
│ │ │ ├── events/
│ │ │ │ └── PlayerEvent.kt
│ │ │ ├── game/
│ │ │ │ ├── AvatarEntity.kt
│ │ │ │ ├── Player.kt
│ │ │ │ ├── PlayerAvatarComp.kt
│ │ │ │ ├── PlayerComp.kt
│ │ │ │ ├── PlayerItemComp.kt
│ │ │ │ ├── Scene.kt
│ │ │ │ ├── SceneEntity.kt
│ │ │ │ ├── World.kt
│ │ │ │ └── data/
│ │ │ │ ├── GameConstants.kt
│ │ │ │ ├── PlayerData.kt
│ │ │ │ ├── Position.kt
│ │ │ │ └── SorapointaStoreEntry.kt
│ │ │ ├── server/
│ │ │ │ ├── ServerNetwork.kt
│ │ │ │ └── network/
│ │ │ │ ├── NetworkHandler.kt
│ │ │ │ ├── OutgoingPacket.kt
│ │ │ │ ├── PacketHandler.kt
│ │ │ │ ├── PacketHandlerImpl.kt
│ │ │ │ └── SoraPacket.kt
│ │ │ └── utils/
│ │ │ ├── Console.kt
│ │ │ ├── GameUtils.kt
│ │ │ ├── NetworkUtils.kt
│ │ │ ├── OptionalContainer.kt
│ │ │ ├── PropDelegate.kt
│ │ │ └── TypoSuggestor.kt
│ │ └── resources/
│ │ ├── logback.xml
│ │ └── messages/
│ │ ├── CoreBundle.properties
│ │ └── CoreBundle_zh_CN.properties
│ └── test/
│ ├── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ ├── command/
│ │ │ └── defaults/
│ │ │ └── HelpTest.kt
│ │ └── logger/
│ │ └── LogTest.kt
│ └── resources/
│ └── logback-test.xml
├── sorapointa-crypto/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── crypto/
│ └── Crypto.kt
├── sorapointa-dataloader/
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── dataloader/
│ │ ├── DataLoader.kt
│ │ ├── common/
│ │ │ ├── AddProp.kt
│ │ │ ├── CurveInfo.kt
│ │ │ ├── Enum.kt
│ │ │ ├── ItemParamData.kt
│ │ │ ├── ItemParamStringData.kt
│ │ │ ├── OpenCondData.kt
│ │ │ ├── PointData.kt
│ │ │ ├── PropGrowCurve.kt
│ │ │ ├── RewardItemData.kt
│ │ │ └── ScenePointConfig.kt
│ │ └── def/
│ │ ├── AvatarExcelData.kt
│ │ ├── AvatarSkillData.kt
│ │ ├── AvatarSkillDepotData.kt
│ │ ├── MaterialData.kt
│ │ ├── ReliquaryAffixData.kt
│ │ ├── ReliquaryData.kt
│ │ ├── ReliquaryLevelData.kt
│ │ ├── ReliquaryMainPropData.kt
│ │ ├── ReliquarySetData.kt
│ │ ├── SceneData.kt
│ │ └── WeaponData.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── dataloader/
│ └── DataLoaderTest.kt
├── sorapointa-dataprovider/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── data/
│ │ └── provider/
│ │ ├── AutoLoadFilePersist.kt
│ │ ├── AutoSaveFilePersist.kt
│ │ ├── DataFilePersist.kt
│ │ ├── DatabaseConfig.kt
│ │ ├── DatabaseManager.kt
│ │ ├── FilePersist.kt
│ │ └── sql/
│ │ ├── SQLJson.kt
│ │ ├── SQLMap.kt
│ │ └── SQLSet.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── data/
│ └── provider/
│ ├── DatabaseProviderTest.kt
│ ├── FileProviderTest.kt
│ └── Init.kt
├── sorapointa-dispatch/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── kotlin/
│ │ │ └── org/
│ │ │ └── sorapointa/
│ │ │ └── dispatch/
│ │ │ ├── DispatchBundle.kt
│ │ │ ├── DispatchServer.kt
│ │ │ ├── data/
│ │ │ │ ├── AccountData.kt
│ │ │ │ ├── DispatchData.kt
│ │ │ │ ├── DispatchKeyData.kt
│ │ │ │ └── SwitchData.kt
│ │ │ ├── events/
│ │ │ │ └── DispatchEvent.kt
│ │ │ ├── plugins/
│ │ │ │ ├── HTTP.kt
│ │ │ │ ├── Monitoring.kt
│ │ │ │ ├── RouteHandler.kt
│ │ │ │ ├── Routing.kt
│ │ │ │ ├── Serialization.kt
│ │ │ │ └── StatusPage.kt
│ │ │ └── utils/
│ │ │ ├── CertBuilder.kt
│ │ │ ├── Certificates.kt
│ │ │ ├── KeyProvider.kt
│ │ │ └── Route.kt
│ │ └── resources/
│ │ └── messages/
│ │ ├── DispatchBundle.properties
│ │ └── DispatchBundle_zh_CN.properties
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── dispatch/
│ ├── AccountTest.kt
│ ├── CertTest.kt
│ └── DispatchServerTest.kt
├── sorapointa-event/
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── event/
│ │ ├── Event.kt
│ │ ├── EventManager.kt
│ │ └── StateController.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── event/
│ ├── EventPipelineTest.kt
│ └── StateControllerTest.kt
├── sorapointa-i18n/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── utils/
│ │ ├── I18n.kt
│ │ └── MessageBundle.kt
│ └── test/
│ ├── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── utils/
│ │ ├── I18nTest.kt
│ │ ├── LocalSerializerTest.kt
│ │ └── TestBundle.kt
│ └── resources/
│ └── messages/
│ ├── TestBundle.properties
│ └── TestBundle_nl.properties
├── sorapointa-native/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── build.gradle.kts
│ └── src/
│ ├── jnienv.rs
│ ├── lib.rs
│ └── logger.rs
├── sorapointa-native-wrapper/
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── rust/
│ │ ├── Setup.kt
│ │ └── logging/
│ │ └── RustLogger.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── rust/
│ └── logging/
│ └── LoggerTest.kt
├── sorapointa-proto/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── proto/
│ │ ├── PacketUtils.kt
│ │ └── ProtoInfo.kt
│ ├── proto/
│ │ ├── AbilityAppliedAbility.proto
│ │ ├── AbilityAppliedModifier.proto
│ │ ├── AbilityAttachedModifier.proto
│ │ ├── AbilityControlBlock.proto
│ │ ├── AbilityEmbryo.proto
│ │ ├── AbilityGadgetInfo.proto
│ │ ├── AbilityMixinRecoverInfo.proto
│ │ ├── AbilityScalarType.proto
│ │ ├── AbilityScalarValueEntry.proto
│ │ ├── AbilityString.proto
│ │ ├── AbilitySyncStateInfo.proto
│ │ ├── AdjustTrackingInfo.proto
│ │ ├── AnimatorParameterValueInfo.proto
│ │ ├── AnimatorParameterValueInfoPair.proto
│ │ ├── AvatarDataNotify.proto
│ │ ├── AvatarEnterSceneInfo.proto
│ │ ├── AvatarEquipAffixInfo.proto
│ │ ├── AvatarExcelInfo.proto
│ │ ├── AvatarExpeditionState.proto
│ │ ├── AvatarFetterInfo.proto
│ │ ├── AvatarFightPropNotify.proto
│ │ ├── AvatarFightPropUpdateNotify.proto
│ │ ├── AvatarInfo.proto
│ │ ├── AvatarLifeStateChangeNotify.proto
│ │ ├── AvatarPropChangeReasonNotify.proto
│ │ ├── AvatarPropNotify.proto
│ │ ├── AvatarRenameInfo.proto
│ │ ├── AvatarSkillInfo.proto
│ │ ├── AvatarTeam.proto
│ │ ├── AvatarTeamUpdateNotify.proto
│ │ ├── AvatarUpgradeRsp.proto
│ │ ├── Birthday.proto
│ │ ├── BlockInfo.proto
│ │ ├── BlossomChestInfo.proto
│ │ ├── BossChestInfo.proto
│ │ ├── BreakoutAction.proto
│ │ ├── BreakoutBrickInfo.proto
│ │ ├── BreakoutElementReactionCounter.proto
│ │ ├── BreakoutPhysicalObject.proto
│ │ ├── BreakoutPhysicalObjectModifier.proto
│ │ ├── BreakoutSnapShot.proto
│ │ ├── BreakoutSpawnPoint.proto
│ │ ├── BreakoutSyncAction.proto
│ │ ├── BreakoutSyncConnectUidInfo.proto
│ │ ├── BreakoutSyncCreateConnect.proto
│ │ ├── BreakoutSyncFinishGame.proto
│ │ ├── BreakoutSyncPing.proto
│ │ ├── BreakoutSyncSnapShot.proto
│ │ ├── BreakoutVector2.proto
│ │ ├── ChangeGameTimeReq.proto
│ │ ├── ChangeGameTimeRsp.proto
│ │ ├── ClientGadgetInfo.proto
│ │ ├── CoinCollectOperatorInfo.proto
│ │ ├── CurVehicleInfo.proto
│ │ ├── CustomCommonNodeInfo.proto
│ │ ├── CustomGadgetTreeInfo.proto
│ │ ├── DeshretObeliskGadgetInfo.proto
│ │ ├── DoSetPlayerBornDataNotify.proto
│ │ ├── EchoShellInfo.proto
│ │ ├── EnterSceneDoneReq.proto
│ │ ├── EnterSceneDoneRsp.proto
│ │ ├── EnterScenePeerNotify.proto
│ │ ├── EnterSceneReadyReq.proto
│ │ ├── EnterSceneReadyRsp.proto
│ │ ├── EnterType.proto
│ │ ├── EntityAuthorityInfo.proto
│ │ ├── EntityClientData.proto
│ │ ├── EntityClientExtraInfo.proto
│ │ ├── EntityEnvironmentInfo.proto
│ │ ├── EntityRendererChangedInfo.proto
│ │ ├── Equip.proto
│ │ ├── FeatureBlockInfo.proto
│ │ ├── FetterData.proto
│ │ ├── FightPropPair.proto
│ │ ├── FishPoolInfo.proto
│ │ ├── FishtankFishInfo.proto
│ │ ├── ForceUpdateInfo.proto
│ │ ├── FoundationInfo.proto
│ │ ├── FoundationStatus.proto
│ │ ├── FriendEnterHomeOption.proto
│ │ ├── FriendOnlineState.proto
│ │ ├── Furniture.proto
│ │ ├── GadgetBornType.proto
│ │ ├── GadgetCrucibleInfo.proto
│ │ ├── GadgetGeneralRewardInfo.proto
│ │ ├── GadgetPlayInfo.proto
│ │ ├── GatherGadgetInfo.proto
│ │ ├── GetPlayerSocialDetailReq.proto
│ │ ├── GetPlayerSocialDetailRsp.proto
│ │ ├── GetPlayerTokenReq.proto
│ │ ├── GetPlayerTokenRsp.proto
│ │ ├── HostPlayerNotify.proto
│ │ ├── Item.proto
│ │ ├── ItemParam.proto
│ │ ├── LifeStateChangeNotify.proto
│ │ ├── MPLevelEntityInfo.proto
│ │ ├── MassivePropParam.proto
│ │ ├── MassivePropSyncInfo.proto
│ │ ├── Material.proto
│ │ ├── MaterialDeleteInfo.proto
│ │ ├── MathQuaternion.proto
│ │ ├── ModifierDurability.proto
│ │ ├── MonsterBornType.proto
│ │ ├── MonsterRoute.proto
│ │ ├── MotionInfo.proto
│ │ ├── MotionState.proto
│ │ ├── MovingPlatformType.proto
│ │ ├── MpPlayRewardInfo.proto
│ │ ├── MpSettingType.proto
│ │ ├── NightCrowGadgetInfo.proto
│ │ ├── OfferingInfo.proto
│ │ ├── OnlinePlayerInfo.proto
│ │ ├── OpenStateUpdateNotify.proto
│ │ ├── PacketHead.proto
│ │ ├── PingReq.proto
│ │ ├── PingRsp.proto
│ │ ├── PlatformInfo.proto
│ │ ├── PlayTeamEntityInfo.proto
│ │ ├── PlayerDataNotify.proto
│ │ ├── PlayerDieOption.proto
│ │ ├── PlayerDieType.proto
│ │ ├── PlayerEnterSceneInfoNotify.proto
│ │ ├── PlayerEnterSceneNotify.proto
│ │ ├── PlayerGameTimeNotify.proto
│ │ ├── PlayerLocationInfo.proto
│ │ ├── PlayerLoginReq.proto
│ │ ├── PlayerLoginRsp.proto
│ │ ├── PlayerPropChangeNotify.proto
│ │ ├── PlayerPropChangeReasonNotify.proto
│ │ ├── PlayerPropNotify.proto
│ │ ├── PlayerRTTInfo.proto
│ │ ├── PlayerSetPauseReq.proto
│ │ ├── PlayerSetPauseRsp.proto
│ │ ├── PlayerStoreNotify.proto
│ │ ├── PlayerWidgetInfo.proto
│ │ ├── PlayerWorldLocationInfo.proto
│ │ ├── PlayerWorldSceneInfo.proto
│ │ ├── PlayerWorldSceneInfoListNotify.proto
│ │ ├── PostEnterSceneReq.proto
│ │ ├── PostEnterSceneRsp.proto
│ │ ├── ProfilePicture.proto
│ │ ├── PropChangeReason.proto
│ │ ├── PropPair.proto
│ │ ├── PropValue.proto
│ │ ├── ProtEntityType.proto
│ │ ├── QueryCurrRegionHttpRsp.proto
│ │ ├── QueryRegionListHttpRsp.proto
│ │ ├── RegionInfo.proto
│ │ ├── RegionSimpleInfo.proto
│ │ ├── Reliquary.proto
│ │ ├── ResVersionConfig.proto
│ │ ├── Retcode.proto
│ │ ├── RoguelikeGadgetInfo.proto
│ │ ├── Route.proto
│ │ ├── RoutePoint.proto
│ │ ├── SceneAvatarInfo.proto
│ │ ├── SceneDataNotify.proto
│ │ ├── SceneEntityAiInfo.proto
│ │ ├── SceneEntityAppearNotify.proto
│ │ ├── SceneEntityInfo.proto
│ │ ├── SceneFishInfo.proto
│ │ ├── SceneGadgetInfo.proto
│ │ ├── SceneInitFinishReq.proto
│ │ ├── SceneInitFinishRsp.proto
│ │ ├── SceneMonsterInfo.proto
│ │ ├── SceneNpcInfo.proto
│ │ ├── ScenePlayerInfo.proto
│ │ ├── ScenePlayerInfoNotify.proto
│ │ ├── ScenePlayerLocationNotify.proto
│ │ ├── SceneReliquaryInfo.proto
│ │ ├── SceneTeamAvatar.proto
│ │ ├── SceneTeamUpdateNotify.proto
│ │ ├── SceneTimeNotify.proto
│ │ ├── SceneWeaponInfo.proto
│ │ ├── ScreenInfo.proto
│ │ ├── ServantInfo.proto
│ │ ├── ServerBuff.proto
│ │ ├── ServerDisconnectClientNotify.proto
│ │ ├── ServerTimeNotify.proto
│ │ ├── SetPlayerBornDataReq.proto
│ │ ├── SetPlayerBornDataRsp.proto
│ │ ├── SetPlayerPropReq.proto
│ │ ├── SetPlayerPropRsp.proto
│ │ ├── ShortAbilityHashPair.proto
│ │ ├── SocialDetail.proto
│ │ ├── SocialShowAvatarInfo.proto
│ │ ├── StatueGadgetInfo.proto
│ │ ├── StopServerInfo.proto
│ │ ├── StoreType.proto
│ │ ├── StoreWeightLimitNotify.proto
│ │ ├── SyncScenePlayTeamEntityNotify.proto
│ │ ├── SyncTeamEntityNotify.proto
│ │ ├── TeamEnterSceneInfo.proto
│ │ ├── TeamEntityInfo.proto
│ │ ├── TrackingIOInfo.proto
│ │ ├── TrialAvatarGrantRecord.proto
│ │ ├── TrialAvatarInfo.proto
│ │ ├── UnionCmd.proto
│ │ ├── UnionCmdNotify.proto
│ │ ├── Vector.proto
│ │ ├── VehicleInfo.proto
│ │ ├── VehicleLocationInfo.proto
│ │ ├── VehicleMember.proto
│ │ ├── VisionType.proto
│ │ ├── Weapon.proto
│ │ ├── WeatherInfo.proto
│ │ ├── WeeklyBossResinDiscountInfo.proto
│ │ ├── WidgetSlotData.proto
│ │ ├── WidgetSlotTag.proto
│ │ ├── WorktopInfo.proto
│ │ ├── WorldDataNotify.proto
│ │ ├── WorldPlayerDieNotify.proto
│ │ ├── WorldPlayerInfoNotify.proto
│ │ ├── WorldPlayerLocationNotify.proto
│ │ ├── WorldPlayerRTTNotify.proto
│ │ ├── WorldPlayerReviveReq.proto
│ │ ├── WorldPlayerReviveRsp.proto
│ │ └── server_side/
│ │ ├── bin.block.proto
│ │ ├── bin.home.proto
│ │ ├── bin.server.proto
│ │ ├── bin_common.server.proto
│ │ ├── cmd_activity.server.proto
│ │ ├── cmd_id_config.proto
│ │ ├── cmd_match.server.proto
│ │ ├── cmd_misc.server.proto
│ │ ├── cmd_mp.server.proto
│ │ ├── cmd_muip.server.proto
│ │ ├── cmd_offline_op.server.proto
│ │ ├── cmd_player.server.proto
│ │ ├── config.server.proto
│ │ ├── define.proto
│ │ └── enum.server.proto
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── proto/
│ └── ProtoTest.kt
├── sorapointa-task/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── task/
│ │ ├── Cron.kt
│ │ ├── CronTask.kt
│ │ └── TaskManager.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── task/
│ └── TaskManagerTest.kt
└── sorapointa-utils/
├── build.gradle.kts
├── sorapointa-utils-all/
│ └── build.gradle.kts
├── sorapointa-utils-core/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── utils/
│ │ ├── Annotations.kt
│ │ ├── ByteUtils.kt
│ │ ├── Cast.kt
│ │ ├── Collection.kt
│ │ ├── Environment.kt
│ │ ├── File.kt
│ │ ├── Files.kt
│ │ ├── JVM.kt
│ │ ├── Locks.kt
│ │ ├── ModuleScope.kt
│ │ ├── Optional.kt
│ │ ├── Random.kt
│ │ ├── Reflection.kt
│ │ ├── String.kt
│ │ ├── Test.kt
│ │ ├── XML.kt
│ │ ├── encoding/
│ │ │ ├── Base64Provider.kt
│ │ │ ├── Digest.kt
│ │ │ ├── Hex.kt
│ │ │ └── RSAProvider.kt
│ │ └── logging/
│ │ └── PatternLayoutNoLambda.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── utils/
│ ├── FilesTest.kt
│ ├── LocksTest.kt
│ ├── ScopeTest.kt
│ ├── StringTest.kt
│ └── encoding/
│ ├── Base64.kt
│ └── HexTest.kt
├── sorapointa-utils-crypto/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── utils/
│ │ ├── ByteReadUtils.kt
│ │ └── crypto/
│ │ ├── Ec2b.kt
│ │ ├── Ec2bAes.kt
│ │ ├── MT19937.kt
│ │ ├── Magic.kt
│ │ └── RSA.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── utils/
│ ├── ScopeTest.kt
│ └── crypto/
│ ├── Ec2bTest.kt
│ ├── MT64Test.kt
│ └── RSAKeyTest.kt
├── sorapointa-utils-serialization/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── utils/
│ │ ├── Json.kt
│ │ └── Yaml.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── utils/
│ └── YamlCompatible.kt
└── sorapointa-utils-time/
├── build.gradle.kts
└── src/
└── main/
└── kotlin/
└── org/
└── sorapointa/
└── utils/
└── Time.kt
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
[*]
insert_final_newline = true
charset = utf-8
indent_style = space
end_of_line = lf
[{*.kt,*.kts}]
indent_size = 4
max_line_length = 120
[*.rs]
indent_size = 4
max_line_length = 120
[{*.yml,*.yaml}]
indent_size = 2
[*.md]
indent_size = 2
================================================
FILE: .git-hooks/commit-msg
================================================
#!/usr/bin/env bash
INPUT_FILE=$1
START_LINE=$(head -n1 "$INPUT_FILE")
PATTERN='^(feat(ure)?|fix|docs|style|refactor|ci|chore|perf|build|test|revert)(\(.+\))?(!)?: .+$'
if [ "${#START_LINE}" -gt "72" ]; then
echo -e "Message too long! Assert length <= 72."
exit
fi
if ! [[ "$START_LINE" =~ $PATTERN ]]; then
echo -e "$START_LINE"
echo
echo -e "↑ Bad commit message, it does not meet the Conventional Commit standard."
echo -e "See more: https://www.conventionalcommits.org/en/v1.0.0/"
exit 1
fi
================================================
FILE: .git-hooks/pre-commit
================================================
#!/bin/bash
echo "[pre-commit check]"
if ! [ -x "$(command -v cargo)" ]; then
echo -e 'Rust toolchains are not installed!'
echo
echo 'If you are running on Unix-like platform, you can follow the on-screen instructions:'
echo
echo " curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
echo
echo 'If you are running on Windows, or you want to install'
echo 'Rust toolchains via package managers, you can see:'
echo
echo '- [Install Rust - rust-lang.org](https://www.rust-lang.org/tools/install)'
echo '- [Other Rust Installation Methods](https://forge.rust-lang.org/infra/other-installation-methods.html)'
exit 1
fi
kotlin() {
if [ ! -e "./gradlew" ]; then
return 0
fi
CHANGED_FILES="$(git --no-pager diff --name-status --no-color --cached | awk '$1 != "D" && $NF ~ /\.kts?$/ { print $NF }')"
if [ -z "$CHANGED_FILES" ]; then
echo "No Kotlin staged files."
return 0
fi
echo '[pre-commit] Executing Gradle spotlessCheck before commit'
git stash --quiet --keep-index
./gradlew spotlessCheck --daemon
RESULT=$?
git stash pop -q
if [ "$RESULT" -ne "0" ]; then
echo -e "spotlessCheck failed..."
echo -e 'You can try "./gradlew spotlessApply" to apply auto-fixes.'
fi
return $RESULT
}
rust() {
cd sorapointa-native || return 0
CHANGED_FILES="$(git --no-pager diff --name-status --no-color --cached | awk '$1 != "D" && $NF ~ /\.rs$/ { print $NF }')"
if [ -z "$CHANGED_FILES" ]; then
echo "No Rust staged files."
return 0
fi
if ! cargo clippy -- -D warnings; then
echo -e "cargo clippy failed..."
return 1
fi
if ! cargo fmt --all -- --check; then
echo -e "cargo fmt failed..."
echo -e "You can manually run 'cargo fmt' at './sorapointa-native' for auto format"
return 1
fi
}
if ! kotlin; then
exit 1
fi
if ! rust; then
exit 1
fi
================================================
FILE: .gitattributes
================================================
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf
================================================
FILE: .github/workflows/api_check.yml
================================================
name: API Check
on:
workflow_dispatch:
push:
branches: [ master ]
paths:
- '**.kt'
- '**.kts'
- '**.proto'
- '.github/workflows/*.yml'
pull_request:
branches:
- '*'
paths:
- '**.kt'
- '**.kts'
- '**.proto'
- '.github/workflows/*.yml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
- uses: burrunan/gradle-cache-action@v1
name: Checker
with:
job-id: api-checker
arguments: apiCheck
gradle-version: wrapper
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
workflow_dispatch:
push:
branches: [ master ]
paths:
- '**.kt'
- '**.kts'
- '**.rs'
- '**.proto'
- 'Cargo.toml'
- 'gradle-wrapper.properties'
- 'gradle/libs.versions.toml'
- '.github/workflows/*.yml'
pull_request:
branches:
- '*'
paths:
- '**.kt'
- '**.kts'
- '**.rs'
- '**.proto'
- 'Cargo.toml'
- 'gradle-wrapper.properties'
- 'gradle/libs.versions.toml'
- '.github/workflows/*.yml'
jobs:
clippy_rustfmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Rust Toolchains
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: rustfmt, clippy
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
prefix-key: "v0-rust"
workspaces: "sorapointa-native -> target"
- name: Clippy
uses: giraffate/clippy-action@v1
with:
workdir: ./sorapointa-native
reporter: 'github-pr-review'
github_token: ${{ secrets.GITHUB_TOKEN }}
clippy_flags: -- -Dwarnings
- name: rustfmt
working-directory: ./sorapointa-native
run: cargo fmt --all -- --check
spotlessCheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
- name: Set up Rust Toolchains
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
prefix-key: "v0-rust"
workspaces: "sorapointa-native -> target"
- uses: burrunan/gradle-cache-action@v1
name: Checker
with:
job-id: checker
arguments: spotlessCheck
gradle-version: wrapper
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
- name: Set up Rust Toolchains
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
prefix-key: "v0-rust"
workspaces: "sorapointa-native -> target"
- uses: burrunan/gradle-cache-action@v1
name: Checker
with:
job-id: checker
arguments: test
gradle-version: wrapper
================================================
FILE: .gitignore
================================================
### Gradle ###
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### Cargo ###
target/
Cargo.lock
### IntelliJ IDEA ###
/.idea/**/*
!/.idea/encodings.xml
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
### Proto ###
generated/
### Tmp ###
**/tmp
### Logs ###
**/logs/*
*.log
**/src/main/resources/logback.xml
**/src/test/resources/logback-test.xml
**/src/main/resources/langs/
!sorapointa-core/src/main/resources/logback.xml
!sorapointa-core/src/test/resources/logback-test.xml
### Resources ###
/resources
### Build Config ###
local.properties
### Sorapointa Config & Generated Files ###
sorapointa-core/langs/
sorapointa-dispatch/langs/
**/config/
/cache
### Database ###
**/*.db
*.db-journal
================================================
FILE: .idea/encodings.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 Sorapointa Organization and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: build.gradle.kts
================================================
import com.diffplug.gradle.spotless.FormatExtension
plugins {
kotlin("jvm") apply false
java
// NOT AN ERROR, see: https://youtrack.jetbrains.com/issue/KTIJ-19369
// You can install a plugin to suppress it:
// https://plugins.jetbrains.com/plugin/18949-gradle-libs-error-suppressor
alias(libs.plugins.spotless)
alias(libs.plugins.licensee)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ktlint)
alias(libs.plugins.abi.validator)
alias(libs.plugins.wire) apply false
alias(libs.plugins.rust.wrapper) apply false
}
subprojects {
if (!arrayOf("sorapointa-native", "sorapointa-utils").contains(project.name)) {
apply(plugin = "app.cash.licensee")
configureLicensee()
}
apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
afterEvaluate {
configureLogbackCopy()
}
}
allprojects {
repositories {
mavenCentral()
}
group = "moe.sdl.sorapointa"
version = "0.1.0-SNAPSHOT"
}
installGitHooks()
spotless {
fun FormatExtension.excludes() {
targetExclude("**/build/", "**/generated/", "**/resources/")
}
fun FormatExtension.common() {
trimTrailingWhitespace()
lineEndings = com.diffplug.spotless.LineEnding.UNIX
endWithNewline()
}
val ktlintConfig = mapOf(
"ij_kotlin_allow_trailing_comma" to "true",
"ij_kotlin_allow_trailing_comma_on_call_site" to "true",
"trailing-comma-on-declaration-site" to "true",
"trailing-comma-on-call-site" to "true",
"ktlint_standard_no-wildcard-imports" to "disabled",
"ktlint_disabled_import-ordering" to "disabled",
)
kotlin {
target("**/*.kt")
excludes()
common()
ktlint(libs.versions.ktlint.get()).editorConfigOverride(ktlintConfig)
}
kotlinGradle {
target("**/*.gradle.kts")
excludes()
common()
ktlint(libs.versions.ktlint.get()).editorConfigOverride(ktlintConfig)
}
}
fun Project.configureLicensee() = this.configure<app.cash.licensee.LicenseeExtension>() {
val allowedLicenses = arrayOf(
"Apache-2.0",
"MIT",
"ISC",
"BSD-2-Clause",
"BSD-3-Clause",
"CC0-1.0",
"EPL-1.0",
"GPL-2.0-with-classpath-exception",
)
allowedLicenses.forEach { allow(it) }
ignoreDependencies("org.postgresql", "postgresql") {
because("BSD-2-Clause, but typo in license URL")
}
}
================================================
FILE: buildSrc/build.gradle.kts
================================================
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
mavenCentral()
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
dependencies {
implementation(libs.kotlin.gradle.plugin)
implementation(libs.build.kotlinpoet)
implementation(libs.build.buildconfig)
implementation(libs.build.shadow)
}
sourceSets {
main {
groovy {
setSrcDirs(emptySet<File>()) // No Groovy
}
java {
setSrcDirs(setOf("kotlin")) // No Java
}
}
test {
groovy {
setSrcDirs(emptySet<File>())
}
java {
setSrcDirs(setOf("kotlin"))
}
}
}
kotlin {
jvmToolchain {
this.languageVersion.set(JavaLanguageVersion.of(17))
}
}
================================================
FILE: buildSrc/settings.gradle.kts
================================================
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
================================================
FILE: buildSrc/src/main/kotlin/BuildConfigExtension.kt
================================================
import com.github.gmazzo.gradle.plugins.BuildConfigSourceSet
fun BuildConfigSourceSet.string(name: String, value: String) = buildConfigField("String", name, "\"$value\"")
fun BuildConfigSourceSet.stringNullable(name: String, value: String?) =
buildConfigField("String?", name, value?.let { "\"$value\"" } ?: "null")
fun BuildConfigSourceSet.long(name: String, value: Long) = buildConfigField("long", name, value.toString())
fun BuildConfigSourceSet.longNullable(name: String, value: Long?) =
buildConfigField("Long?", name, value?.let { "$value" } ?: "null")
fun BuildConfigSourceSet.int(name: String, value: Int) = buildConfigField("int", name, value.toString())
fun BuildConfigSourceSet.intNullable(name: String, value: Int?) =
buildConfigField("int", name, value?.let { "$value" } ?: "null")
fun BuildConfigSourceSet.boolean(name: String, value: Boolean) = buildConfigField("boolean", name, value.toString())
================================================
FILE: buildSrc/src/main/kotlin/GitHook.kt
================================================
import org.gradle.api.Project
import org.gradle.internal.os.OperatingSystem
import java.io.File
import java.nio.file.Files
fun Project.installGitHooks() {
val target = File(project.rootProject.rootDir, ".git/hooks")
val source = File(project.rootProject.rootDir, ".git-hooks")
if (target.canonicalFile == source) return
target.deleteRecursively()
if (OperatingSystem.current().isWindows) {
source.copyRecursively(target)
} else {
Files.createSymbolicLink(target.toPath(), source.toPath())
}
}
================================================
FILE: buildSrc/src/main/kotlin/JniHeader.kt
================================================
import org.gradle.api.Project
import org.gradle.api.tasks.TaskContainer
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import java.io.ByteArrayOutputStream
val bodyExtractingRegex = Regex("""^.+\Rpublic \w* ?class ([^\s]+).*\{\R((?s:.+))\}\R$""")
val nativeMethodExtractingRegex = Regex(""".*\bnative\b.*""")
fun Project.jniHeaderTask(tasks: TaskContainer) = tasks.create("generateJniHeaders") {
group = "build"
dependsOn(tasks.getByName("compileKotlin"))
project.kotlinExtension.sourceSets.getByName("main").kotlin.srcDirs.filter {
it.exists()
}.forEach {
inputs.dir(it)
}
outputs.dir("src/main/generated/jni")
doLast {
val javaHome = org.gradle.internal.jvm.Jvm.current().javaHome
val javap = javaHome.resolve("bin").walk()
.firstOrNull { it.name.startsWith("javap") }
?.absolutePath ?: error("javap not found")
val javac = javaHome.resolve("bin").walk()
.firstOrNull { it.name.startsWith("javac") }
?.absolutePath ?: error("javac not found")
val buildDir = file("build/classes/kotlin/main")
val tmpDir = file("build/tmp/jvmJni")
tmpDir.mkdirs()
buildDir.walk()
.asSequence()
.filter { "META" !in it.absolutePath }
.filter { it.isFile }
.filter { it.extension == "class" }
.forEach { file ->
val output = ByteArrayOutputStream().use {
project.exec {
commandLine(javap, "-private", "-cp", buildDir.absolutePath, file.absolutePath)
standardOutput = it
}.assertNormalExitValue()
it.toString()
}
val (qualifiedName, methodInfo) =
bodyExtractingRegex
.find(output)?.destructured
?: return@forEach
val lastDot = qualifiedName.lastIndexOf('.')
val packageName = qualifiedName.substring(0, lastDot)
val className = qualifiedName.substring(lastDot + 1, qualifiedName.length)
val nativeMethods =
nativeMethodExtractingRegex.findAll(methodInfo).map { it.groups }
.flatMap { it.asSequence().mapNotNull { group -> group?.value } }.toList()
if (nativeMethods.isEmpty()) return@forEach
val generatedCode = buildString {
appendLine("package $packageName;")
appendLine("public class $className {")
nativeMethods.forEach { method ->
val newMethod = if (method.contains("()")) {
method
} else {
buildString {
append(method)
var count = 0
var i = 0
while (i < length) {
if (this[i] == ',' || this[i] == ')') {
count++
insert(i, " arg$count".also { i += it.length + 1 })
} else {
i++
}
}
}
}
appendLine(newMethod)
}
appendLine("}")
}
val javaFile = tmpDir
.resolve(packageName.replace(".", "/"))
.resolve("$className.java")
javaFile.parentFile.mkdirs()
if (javaFile.exists()) delete()
javaFile.createNewFile()
javaFile.writeText(generatedCode)
project.exec {
commandLine(javac, "-h", "src/main/generated/jni", javaFile.absolutePath)
}.assertNormalExitValue()
}
}
}
================================================
FILE: buildSrc/src/main/kotlin/OptInAnnotations.kt
================================================
object OptInAnnotations {
val list = listOf(
"kotlin.ExperimentalUnsignedTypes",
"kotlin.contracts.ExperimentalContracts",
"org.sorapointa.utils.SorapointaInternal",
)
}
================================================
FILE: buildSrc/src/main/kotlin/Properties.kt
================================================
import org.gradle.api.Project
import org.gradle.kotlin.dsl.extra
import java.util.*
fun Project.getRootProjectLocalProps(): Map<String, String> {
val file = project.rootProject.file("local.properties")
return if (file.exists()) {
file.reader().use {
Properties().apply {
load(it)
}
}.toMap().map {
it.key.toString() to it.value.toString()
}.toMap()
} else {
emptyMap()
}
}
fun Project.getExtraString(name: String) = runCatching { this.extra[name]?.toString() }.getOrNull()
fun Project.getExtraBoolean(name: String) = runCatching { this.extra[name] as Boolean }.getOrNull()
================================================
FILE: buildSrc/src/main/kotlin/ResourcesCopy.kt
================================================
import org.gradle.api.Project
import org.gradle.api.tasks.Copy
import org.gradle.kotlin.dsl.register
internal fun Project.resourceTaskDep(task: String) {
tasks.named("classes") {
dependsOn(task)
}
tasks.named("processResources") {
dependsOn(task)
}
}
internal fun Project.testResourceTaskDep(task: String) {
tasks.named("testClasses") {
dependsOn(task)
}
tasks.named("processTestResources") {
dependsOn(task)
}
}
fun Project.configureLogbackCopy() {
if (!pluginManager.hasPlugin("org.jetbrains.kotlin.jvm")) return
if (name == "sorapointa-core") return
fun registerCopyPath(name: String, source: String, dest: String) {
tasks.register(name, Copy::class) {
group = "resources"
from(
rootProject.subprojects.first { it.name == "sorapointa-core" }
.layout.projectDirectory.dir(source),
)
into(project.layout.projectDirectory.dir(dest))
}
}
registerCopyPath(
name = "copyLogbackXml",
source = "./src/main/resources/logback.xml",
dest = "./src/main/resources/",
)
registerCopyPath(
name = "copyLogbackTestXml",
source = "./src/test/resources/logback-test.xml",
dest = "./src/test/resources/",
)
afterEvaluate {
resourceTaskDep("copyLogbackXml")
testResourceTaskDep("copyLogbackTestXml")
}
}
================================================
FILE: buildSrc/src/main/kotlin/Test.kt
================================================
val isCI = System.getenv("CI") != null
================================================
FILE: buildSrc/src/main/kotlin/sorapointa-conventions.gradle.kts
================================================
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
id("com.github.gmazzo.buildconfig")
id("com.github.johnrengelman.shadow")
java
}
repositories {
mavenCentral()
maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
maven("https://plugins.gradle.org/m2/")
}
dependencies {
constraints {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation(kotlin("test"))
}
sourceSets {
main {
java {
setSrcDirs(setOf("kotlin")) // No Java, and Kotlin Only
}
}
test {
java {
setSrcDirs(setOf("kotlin")) // No Java, and Kotlin Only
}
}
}
tasks.test {
dependsOn("generateTestBuildConfig")
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions.apply {
jvmTarget = "17"
OptInAnnotations.list.forEach {
freeCompilerArgs = freeCompilerArgs + "-opt-in=$it"
}
}
}
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
configurations {
create("test")
}
tasks.register<Jar>("testArchive") {
archiveBaseName.set("${project.name}-test")
from(project.the<SourceSetContainer>()["test"].output)
}
artifacts {
add("test", tasks["testArchive"])
}
tasks.withType<Jar>() {
exclude("main") // duplicated jar root main, very confusing
exclude("logback-test.xml")
exclude("*.proto")
}
tasks.shadowJar {
exclude("checkstyle.xml")
exclude("**/*.html")
exclude("CronUtilsI18N*.properties")
exclude("DebugProbesKt.bin")
exclude("custom.config.*")
// SQLite
exclude("org/sqlite/native/FreeBSD/**/*")
exclude("org/sqlite/native/Linux-Android/**/*")
exclude("org/sqlite/native/Linux-Musl/**/*")
arrayOf("arm", "armv6", "armv7", "ppc64", "x86").forEach {
exclude("org/sqlite/native/Linux/$it/**/*")
exclude("org/sqlite/native/Windows/$it/**/*")
}
arrayOf("freebsd32", "freebsd64", "linux32", "windows32").forEach {
exclude("META-INF/native/$it/**/*")
}
// JNA
arrayOf("aix", "freebsd", "openbsd", "sunos").forEach {
exclude("com/sun/jna/$it*/**/*")
}
arrayOf("arm", "armel", "loongarch64", "mips64el", "ppc", "ppc64le", "riscv64", "s390x", "x86").forEach {
exclude("com/sun/jna/linux-$it/**/*")
exclude("com/sun/jna/win32-$it/**/*")
}
// Jansi Native Lib
exclude("org/fusesource/jansi/internal/native/FreeBSD")
arrayOf("arm", "armv6", "armv7", "ppc64", "x86").forEach {
exclude("org/fusesource/jansi/internal/native/Linux/$it/**/*")
exclude("org/fusesource/jansi/internal/native/Mac/$it/**/*")
exclude("org/fusesource/jansi/internal/native/Windows/$it/**/*")
}
}
================================================
FILE: buildSrc/src/main/kotlin/sorapointa-publish.gradle.kts
================================================
plugins {
`java-library`
`maven-publish`
signing
}
java {
withJavadocJar()
withSourcesJar()
}
val secretPropsFile: File = project.rootProject.file("local.properties")
if (secretPropsFile.exists()) {
val props = getRootProjectLocalProps()
props.forEach { t, u -> ext[t] = u }
} else {
ext["signing.keyId"] = System.getenv("SIGNING_KEY_ID")
ext["signing.password"] = System.getenv("SIGNING_PASSWORD")
ext["signing.secretKeyRingFile"] = System.getenv("SIGNING_SECRET_KEY_RING_FILE")
ext["ossrhUsername"] = System.getenv("OSSRH_USERNAME")
ext["ossrhPassword"] = System.getenv("OSSRH_PASSWORD")
}
publishing {
publications {
create<MavenPublication>("mavenKotlin") {
artifactId = project.name
from(components["java"])
versionMapping {
usage("java-api") {
fromResolutionOf("runtimeClasspath")
}
usage("java-runtime") {
fromResolutionResult()
}
}
pom {
name.set("Sorapointa")
description.set("A server software implementation for a certain anime game, and avoid sorapointa")
url.set("https://github.com/Sorapointa/Sorapointa")
licenses {
license {
name.set("Apache License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
}
}
developers {
developer {
id.set("WetABQ")
}
developer {
id.set("Colerar")
}
}
scm {
connection.set("scm:git:git://github.com/Sorapointa/Sorapointa.git")
developerConnection.set("scm:git:ssh://github.com/Sorapointa/Sorapointa.git")
url.set("https://github.com/Sorapointa/Sorapointa")
}
}
}
repositories {
maven {
name = "sonatype"
val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
val url = if (version.toString().contains("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl
setUrl(url)
credentials {
username = getExtraString("ossrhUsername")
password = getExtraString("ossrhPassword")
}
}
}
}
}
signing {
sign(publishing.publications["mavenKotlin"])
}
tasks.javadoc {
exclude("org.sorapointa.proto")
if (JavaVersion.current().isJava9Compatible) {
(options as StandardJavadocDocletOptions).addBooleanOption("html5", true)
}
}
================================================
FILE: docs/CONTRIBUTING.md
================================================
# Contributing Guideline
[简体中文](CONTRIBUTING.zh-CN.md)
## Code Style
- [Kotlin Official Code Style](https://kotlinlang.org/docs/coding-conventions.html)
- Indent is 4 spaces, the `.editorconfig` in our project will help your IDE to automatically set it
- Star import is allowed
- We require **all PRs to pass the `ktlint` check** before they merge into the active branch
- We recommended you to format your code using `Ktlint` before committing.
- You can run Gradle task `spotlessCheck` via `./gradlew spotlessCheck` command.
- About Rust code style, see: [sorapointa-native/README.md](../sorapointa-native/README.md)
## Git
### Branch
- **Active branch** refers to the `dev`, the development branch
- All commits or changes that want to merge into the **active branch**
will be required to submit PR and pass all CI check.
### Push
- If you want to submit your commits or changes into **active branch**,
you **must submit those through PR** and **pass all CI check**.
### Merge Branch
- Please **don't** pull any upstream updates when you open a new branch (or fork),
which are used for submitting your changes or commits,
except those updates are required for your changes or commits.
Even though, you need to update your branch following [this rule](#incompatible-changes-and-sync-upstream-updates)
- You must turn on the `rebase` option to pull upstream updates
- It shouldn't occur any conflicts, except you had pulled upstream updates after your committed your own code.
- Merge PR with different methods determined by different situations
- If the PR contains big changes, we often merge it into active branch with `merge` or `squash`
- If the PR contains small changes, we often merge it into active branch with `rebase`
### Incompatible Changes and Sync Upstream Updates
- Please **don't** pull any upstream updates when you open a new branch (or fork),
but if these updates are necessary for you, please follow this process.
- Create a new branch `xxx-update` from the current latest upstream branch to the local
- Merge your commits into the `xxx-update` branch
through `rebase`(if there are no conflicts) or `cherrypick`
- Resolve all conflicts and fix all compatibility errors
- Submit PR to make `xxx-update` merge into active branch
- When you have made any incompatibility changes,
please follow the same process as above and make a new branch, like `xxx-premerge`,
with all incompatibility issues fixed
- Note: If there are other PRs or branches that are also affected by your incompatible update,
set the merge target of the other PRs to `xxx-premerge`, which is equivalent to a staging branch
- When all affected PRs or branches have been merged into `xxx-premerge`
and all compatibility issues have been fixed,
submit a PR to make it merge into the active branch
### Commit
- Write commit messages in English
- Short message template: `type(scope): message`. For example, `feat(network): impl KCP protocol`
- For `type` field, abbreviated and qualified name are both acceptable.
- Use `#issue_number` to mention related issue for easy tracking
You can try [IDEA Conventional Commits](https://plugins.jetbrains.com/plugin/13389-conventional-commit)
plugin for smart completion.
[](https://plugins.jetbrains.com/plugin/13389-conventional-commit)
For reference: [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
## More Documentations
- [Unit Test](guides/unit-test.md)
- Please write unit tests to ensure the reliability of the code.
- All commits merged into the master branch must pass unit tests.
- Concurrent Safety
- [Kotlin AtomicFU Guideline](guides/kotlin-atomicfu.md)
- [Concurrent Safety](guides/concurrency.md)
- [Database Operation Safety](guides/database.md)
- Others: [guides](./guides)
================================================
FILE: docs/CONTRIBUTING.zh-CN.md
================================================
# 贡献指南
[English](CONTRIBUTING.zh-CN.md)
## 代码风格
- Kotlin 官方代码风格 - [中文站参考](https://book.kotlincn.net/text/coding-conventions.html)
- 缩进 4 空格,项目中的 `.editorconfig` 配置会帮助您的 IDE 自动设置
- 可以使用 `*` 导入
- 我们会要求**所有并入活跃分支**的 PR 通过 `ktlint` 检查
- 可使用命令 `./gradlew spotlessCheck` 运行 Gradle 任务
- 关于 Rust 代码风格,参见:[sorapointa-native/README.md](../sorapointa-native/README.zh-CN.md)
## Git 规范
### 关于分支
- 一般而言**活跃分支**指的是 `dev` 开发分支
- 在**活跃分支**上进行修改必须通过其他分支提交 PR 并通过所有的 CI 检查
### Push 规范
- 若您希望在**活跃分支**上提交您的修改,则**必须通过其他分支**提交 PR 并**通过所有的 CI 检查**
可参考:[Git 使用规范流程](https://www.ruanyifeng.com/blog/2015/08/git-use-process.html)
### 关于合并
- 在当您开启了一个新分支(或者 `fork`),用于提交您的代码修改时,请在 `fork` 后**不要拉取任何上游的更新**,
除非这些更新对于你的开发是必须的,如果是必须的,请按照 [关于不兼容性修改与同步上游更新](#关于不兼容性修改与同步上游更新)
- 从远程拉取到本地时,必须使用 `rebase`
- 不应该存在冲突,若存在冲突一般情况下是您在提交修改的同时拉取了上游的更新
- 将 PR 合并时,应该酌情使用不同的合并方式
- 如大修改,一般考虑 `merge` 或者 `squash`
- 小修改一般考虑 `rebase`
### 关于不兼容性修改与同步上游更新
- 一般而言,最好不要在 `fork` 后拉取任何上游的更新,但是若这些更新对您来说是必须地请按照以下流程进行更新:
- 从当前最新的上游分支中新建分支 `xxx-update` 到本地
- 将您已经在其他分支提交的更改 `rebase`(若无冲突) 或者 `cherrypick` 进入 `xxx-update` 分支
- 解决所有冲突,并修正所有的兼容性错误
- 将 `xxx-update` 提交 PR 并入活跃分支
- 当您进行了任何的不兼容性修改,请同样按照上述流程,将修复了所有不兼容性问题的分支作为新分支,如 `xxx-premerge`
- 注意,若此时同时存在其他 PR 或者分支,并同样受到您的不兼容性更新影响,
请将其他 PR 的合并目标设置为 `xxx-premerge` 也就是相当于一个暂存分支
- 当所有受到影响 PR 或者分支,都合并进入了 `xxx-premerge` 并解决了所有兼容性问题后,提交 PR 申请并入活跃分支
### Commit 规范
- `commit` 中的信息请使用英语
- 短消息格式满足:`类型(作用域): 消息`,例如 `feat(network): impl KCP protocol`
- `类型` 字段无论用缩写或全称都可行。
- 用 `#issue 编号` 提及相关的 issue,便于跟踪
可以使用 [IDEA Conventional Commits](https://plugins.jetbrains.com/plugin/13389-conventional-commit)
插件智能补全:
[](https://plugins.jetbrains.com/plugin/13389-conventional-commit)
另可参考:
- [约定式提交 v1.0.0](https://www.conventionalcommits.org/zh-hans/v1.0.0/)
- [Commit Message 和 Change Log 编写指南](https://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html)
## 更多文档
- [关于单元测试](guides/unit-test.zh-CN.md)
- 尽量写单测保证代码可靠性。
- 与此同时,如果你提交的 PR 无法通过 CI 中的单元测试,将无法并入活跃分支
- 关于并发安全
- [AtomicFU 指南](guides/kotlin-atomicfu.zh-CN.md)
- [关于并发安全](guides/concurrency.zh-CN.md)
- [关于数据库操作安全](guides/database.zh-CN.md)
- 其他: 请参见 [guides](./guides)
================================================
FILE: docs/README.md
================================================
<!--Logo-->

<!--Badges-->
<p align="center">
<a href="https://kotlinlang.org"><img
src="https://img.shields.io/badge/kotlin-%230095D5.svg?style=for-the-badge&logo=kotlin&logoColor=white"
alt="Kotlin"/></a><a href="https://www.rust-lang.org/"><img
src="https://img.shields.io/badge/rust-%23704b34.svg?style=for-the-badge&logo=rust&logoColor=white"
alt="Rust"/></a><a
href="https://gradle.org/"><img
src="https://img.shields.io/badge/Gradle-02303A.svg?style=for-the-badge&logo=Gradle&logoColor=white"
alt="Gradle"/></a><a
href="https://www.jetbrains.com/idea/"><img
src="https://img.shields.io/badge/IDEA-000000.svg?style=for-the-badge&logo=intellij-idea&logoColor=white"
alt="IntelliJ IDEA"/></a>
</p>
<p align="center">
<a
href="https://www.apache.org/licenses/LICENSE-2.0"><img
src="https://img.shields.io/badge/License-Apache2.0-lightgreen?style=for-the-badge&logo=opensourceinitiative&logoColor=white"
alt="Apache 2.0 Open Source License"/></a><a
href="https://s01.oss.sonatype.org/content/repositories/snapshots/moe/sdl/sorapointa/"><img
src="https://img.shields.io/nexus/s/moe.sdl.sorapointa/sorapointa-core?logo=apache-maven&label=Maven%20Dev&server=https%3A%2F%2Fs01.oss.sonatype.org&style=for-the-badge"
alt="Maven Developer"/></a>
<div align="center"><a href="https://discord.gg/MRadGNhqce"><img alt="Discord - Sorapointa" src="https://img.shields.io/discord/976764233029140550?label=Discord&logo=discord&style=for-the-badge"></a></div>
<!--Content-->
English | [简体中文](README.zh-CN.md)
**WIP**: This project is under active development, you can take a part in contributing, but most of the features is
unavailable.
## Name
As you see, our project name is **Sorapointa**. This name was inspired from Java well-known `NullPointerException`.
We translated `NullPointer` to Japanese and phoneticized it in English.
Sorapointa is aim to reduce runtime-error, write readable, easy-to-maintain code. So, **Sorapointa avoid Sorapointa**.
Sorapointa can be written as the Chinese equivalent of "空想家", but in any case, please read it as <ruby>Sorapointa<rt>
ソラポインタ</rt></ruby>
## Build
Requirement:
- JDK 17
- Rust Toolchains, see:[sorapointa-native/README.md](../sorapointa-native/README.md)
```shell
./gradlew shadowJar
# if you want to run test
./gradlew test
```
### Available Build Configs
Create `local.properties` in project root and edit it. Config uses Java `.properties` format.
| key | description | available value |
|------------------------|---------------------------------|------------------------|
| `database.default` | default database for new config | `SQLITE`, `POSTGRESQL` |
| `database.driver.list` | database drivers to build | `SQLITE`, `POSTGRESQL` |
Example:
```properties
database.default=SQLITE
database.driver.list=SQLITE,POSTGRESQL
```
## Thanks
### Person
- [HolographicHat](https://github.com/HolographicHat) - Supports a lot on algorithms and computer security.
### Project
- [JVM](https://openjdk.org/) - The best programming language VM
- [Kotlin](https://github.com/JetBrains/kotlin) - A modern programming language that makes developers happier.
- [Rust](https://github.com/rust-lang/rust) - A language empowering everyone to build reliable and efficient software.
- [IDEA](https://www.jetbrains.com/idea/) - Capable and Ergonomic IDE for JVM
- [Grasscutter](https://github.com/Grasscutters/Grasscutter)
- [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) - Kotlin multiplatform / multi-format
**reflectionless** serialization
- [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) - A rich library for coroutines developed by
JetBrains
- [kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) - The idiomatic way to use atomic operations in Kotlin
- [Ktor](https://github.com/ktorio/ktor) - An asynchronous framework for creating microservices, web applications and
more.
- [Netty](https://netty.io/) - Netty is an asynchronous event-driven network application framework
- [Exposed](https://github.com/JetBrains/Exposed) - Kotlin SQL Framework
- [Clikt](https://github.com/ajalt/clikt/tree/master/clikt) - Multiplatform command line interface parsing for Kotlin
- [Kotlin-Logging](https://github.com/MicroUtils/kotlin-logging) - Lightweight logging framework for Kotlin
- [Password4j](https://github.com/Password4j/password4j) - Password4j is a user-friendly cryptographic library that
supports Argon2 and so on
- [JLine](https://github.com/jline/jline3) - JLine is a Java library for handling console input.
- [kaml](https://github.com/charleskorn/kaml) - YAML support for kotlinx.serialization
- [Protobuf](https://developers.google.com/protocol-buffers) - Protocol buffers are a language-neutral, platform-neutral
extensible mechanism for serializing structured data
================================================
FILE: docs/README.zh-CN.md
================================================
<!--Logo-->

<!--Badges-->
<p align="center">
<a href="https://kotlinlang.org"><img
src="https://img.shields.io/badge/kotlin-%230095D5.svg?style=for-the-badge&logo=kotlin&logoColor=white"
alt="Kotlin"/></a><a href="https://www.rust-lang.org/zh-CN/"><img
src="https://img.shields.io/badge/rust-%23704b34.svg?style=for-the-badge&logo=rust&logoColor=white"
alt="Rust"/></a><a
href="https://gradle.org/"><img
src="https://img.shields.io/badge/Gradle-02303A.svg?style=for-the-badge&logo=Gradle&logoColor=white"
alt="Gradle"/></a><a
href="https://www.jetbrains.com/idea/"><img
src="https://img.shields.io/badge/IDEA-000000.svg?style=for-the-badge&logo=intellij-idea&logoColor=white"
alt="IntelliJ IDEA"/></a>
</p>
<p align="center">
<a
href="https://www.apache.org/licenses/LICENSE-2.0"><img
src="https://img.shields.io/badge/License-Apache2.0-lightgreen?style=for-the-badge&logo=opensourceinitiative&logoColor=white"
alt="Apache 2.0 Open Source License"/></a><a
href="https://s01.oss.sonatype.org/content/repositories/snapshots/moe/sdl/sorapointa/"><img
src="https://img.shields.io/nexus/s/moe.sdl.sorapointa/sorapointa-core?logo=apache-maven&label=Maven%20Dev&server=https%3A%2F%2Fs01.oss.sonatype.org&style=for-the-badge"
alt="Maven Developer"/></a>
</p>
<!--Content-->
[English](README.md) | 简体中文
**WIP**: 该项目正被积极开发,你可以参与贡献,但大多数功能不可用。
## 名称
如你所见,我们的项目名为 **Sorapointa**。这来自于 `Java` 中闻名的 `NullPointerException`。
我们将 `NullPointer` 日语化再以英语拟音,得到了这个名字。
Sorapointa 旨在减少运行时错误,编写可读性高、易于维护的代码。因此,**Sorapointa 避免 Sorapointa**。
Sorapointa 可以写成对应的汉字「空想家」,但无论如何,请你读作 <ruby>Sorapointa<rt>ソラポインタ</rt></ruby>
## Build
需要:
- JDK 17
- Rust 工具链,详情参见:[sorapointa-native/README.md](../sorapointa-native/README.zh-CN.md)
```shell
./gradlew shadowJar
# 如果你想运行测试
./gradlew test
```
### 可用的构建选项
在项目根目录创建 `local.properties` 并编辑。配置使用 Java `.properties` 格式。
| key | 描述 | 可用值 |
|------------------------|------------|------------------------|
| `database.default` | 默认配置使用的数据库 | `SQLITE`, `POSTGRESQL` |
| `database.driver.list` | 编译时打包哪些数据库 | `SQLITE`, `POSTGRESQL` |
示例:
```properties
database.default=SQLITE
database.driver.list=SQLITE,POSTGRESQL
```
## Contributing
参见:[CONTRIBUTING](CONTRIBUTING.zh-CN.md),以及对应模块的 README。
## Thanks
### Person
- [HolographicHat](https://github.com/HolographicHat) - Supports a lot on algorithms and computer security.
### Project
- [JVM](https://openjdk.org/) - The best programming language VM
- [Kotlin](https://github.com/JetBrains/kotlin) - A modern programming language that makes developers happier.
- [Rust](https://github.com/rust-lang/rust) - A language empowering everyone to build reliable and efficient software.
- [IDEA](https://www.jetbrains.com/idea/) - Capable and Ergonomic IDE for JVM
- [Grasscutter](https://github.com/Grasscutters/Grasscutter)
- [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) - Kotlin multiplatform / multi-format
**reflectionless** serialization
- [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) - A rich library for coroutines developed by
JetBrains
- [kotlinx.atomicfu](https://github.com/Kotlin/kotlinx.atomicfu) - The idiomatic way to use atomic operations in Kotlin
- [Ktor](https://github.com/ktorio/ktor) - An asynchronous framework for creating microservices, web applications and
more.
- [Netty](https://netty.io/) - Netty is an asynchronous event-driven network application framework
- [Exposed](https://github.com/JetBrains/Exposed) - Kotlin SQL Framework
- [Clikt](https://github.com/ajalt/clikt/tree/master/clikt) - Multiplatform command line interface parsing for Kotlin
- [Kotlin-Logging](https://github.com/MicroUtils/kotlin-logging) - Lightweight logging framework for Kotlin
- [Password4j](https://github.com/Password4j/password4j) - Password4j is a user-friendly cryptographic library that
supports Argon2 and so on
- [JLine](https://github.com/jline/jline3) - JLine is a Java library for handling console input.
- [kaml](https://github.com/charleskorn/kaml) - YAML support for kotlinx.serialization
- [Protobuf](https://developers.google.com/protocol-buffers) - Protocol buffers are a language-neutral, platform-neutral
extensible mechanism for serializing structured data
================================================
FILE: docs/guides/concurrency.md
================================================
# Concurrency Safety
The Sorapointa project has applied a lot of coroutine and multi-thread techniques,
so please to be very careful with thread-safe and
the code performance in the multi-thread and high volume situation.
## Shared Mutable State
During development, it is best to expose **unmodifiable** variables and collections
such as `val`, `List`, `Map` to avoid thread safety issues caused by sharing mutable data
If you must share mutable data,
use a thread-safe data structure such as Atomic,
Sorapointa has already used the AtomicFU framework.
About AtomicFU,please refers to [AtomicFU Guideline](kotlin-atomicfu.md)
But for collections,
we usually use `ConcurrentHashMap` or some similar thread-safe data structures
However, using thread-safe data structures does not guarantee are safe;
atomicity is not magic or comes from void, it needs to be carefully maintained
For example, for `ConcurrentHashMap`,
you must use the provided methods such as `get`, `put`, `getOrPut` to ensure thread safety.
A thread-safe data structure can only guarantee the atomicity of the methods it provides.
So, when using thread-safe data structures, combining their atomic operations
can lead to a loss of atomicity and to be thread-unsafe, for example
```kotlin
val map = ConcurrentHashMap<Int, String>()
if (!map.containsKey(123)) {
map.put(123,"foobar")
}
```
This is thread-unsafe because when multiple threads use this method at the same time,
they will both arrive at `containsKey` and both find there is not `123`
and it will repeatedly execute following code
So, that's why you must use the methods `getOrPut()` or `putIfCompute()`
provided by `ConcurrentHashMap` to ensure thread safety.
Refers to [Shared Mutable State](https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html)
## Thread Safety Review
For every object, if any properties is variable
and will be access by multiple threads, you have to use Atomic.
You cannot leak the atomic reference to public,
and must use the provided update functions.
If there are any methods in the object that use Atomic objects,
ensure that every operation accesses it
is completely independent (no branches and local variables).
If there is a dependent operation,
you must ensure the atomicity of that dependent operation.
(As mentioned above, atomic methods cannot be combined,
or locks can be used to ensure atomicity)
If this object itself already guarantees atomicity for all operations,
it is necessary ensure that the operations of the object is atomic.
(and the outer methods must not combine atomic methods, and so on).
## Requirements for Thread Safety
Fix as many thread-safety issues as possible that can be fixed easily,
such as using the atomic delegation, ConcurrentHashMap
and other built-in atomic objects.
If all built-in atomic methods are no longer enough for your needs,
try using the simple Mutex.
(Please follow the guideline for properly using the Mutex)
But before using Mutex or more complex thread-safe mechanism,
first think about whether I can accept the risk of problems occurring
(e.g., the [Primogem](https://genshin-impact.fandom.com/wiki/Primogem)-related operation is very sensitive to
But repeatedly rewriting insertDefault
or repeatedly starting some coroutines because of high concurrency.
This cost is affordable.)
Acceptable cost means that
the resulting data changes are acceptable,
the resulting performance loss is acceptable,
and the program will not crash with errors.
## Lock
In Kotlin Coroutine, some types of lock (such as Mutex) that is thread-bind,
would easily cause the deadlock issue.
We highly recommend you to use `withReentrantLock` method located in `sorapointa-utils` module,
to keep the mutex lock consistency in the coroutine context.
Please refer to [Phantom of the Coroutine](https://elizarov.medium.com/phantom-of-the-coroutine-afc63b03a131)
## Structured Concurrency
The code is cooperative and use structured concurrency to
ensure that all concurrent processes do not leak and are manageable.
In Sorapointa, we use ModuleScope to
ensure that the task structure between concurrent processes is proper.
In general,please don't implement `CoroutineScope` interface to make coroutine to be structured concurrency,
and also don't add context in the parameter of `launch` method.
For specific reasons, please refer to, [Kotlin CoroutineScope Documentation](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/),
[Why your class probably should not implement CoroutineScope](https://proandroiddev.com/why-your-class-probably-shouldnt-implement-coroutinescope-eb34f722e510),
[Structured Concurrency Anniversary](https://elizarov.medium.com/structured-concurrency-anniversary-f2cc748b2401),
[Legacy Convention of CoroutineScope](https://maxkim.eu/things-every-kotlin-developer-should-know-about-coroutines-part-2-coroutinescope)
In short, it's an outdated approach.
You can write cooperative code by referring to TaskManager, EventManager, etc.
================================================
FILE: docs/guides/concurrency.zh-CN.md
================================================
# 关于并发安全
Sorapointa 项目大量使用了协程与多线程技术,请时刻注意检查代码在多线程以及高并发环境下的运行状况
## 共享可修改数据
在开发期间,最好暴露不可修改的变量和集合如 `val`, `List`, `Map`,以避免共享可修改数据造成的线程安全问题
如果一定要共享可修改数据,请使用线程安全的数据结构 ,如 Atomic,Sorapointa 使用了 AtomicFU 框架封装了 Java 的原子方法
关于 AtomicFU,参考 [AtomicFU 指南](/docs/guides/kotlin-atomicfu.zh-CN.md)
但是对于集合,我们一般使用 `ConcurrentHashMap` 或同样类似的线程安全的数据结构
但使用线程安全的数据结构,并不代表一定安全,原子性并不凭空产生,需要小心维护
比如对于 `ConcurrentHashMap`,必须使用其自身提供的方法如 `get`,`put` 才能保证线程安全,
线程安全的数据结构也只能保证它提供的方法的线程安全和原子性。
在使用线程安全的数据结构时,组合其原子操作会导致原子性的丢失,比如
```kotlin
val map = ConcurrentHashMap<Int, String>()
if (!map.containsKey(123)) {
map.put(123,"foobar")
}
```
就是线程不安全的,因为当多线程同时使用这个方法时,
他们会同时抵达 `containsKey` 并都发现没有 `123`,并重复执行了下面的代码
你必须使用 `ConcurrentHashMap` 提供的方法 `getOrPut()` 或 `putIfCompute()` 以确保线程安全
了解更多,关于 [线程间共享可修改数据](https://kotlinlang.org/docs/shared-mutable-state-and-concurrency.html)
## 线程安全分析
对于每个对象,如果对象中的任何成员变量是可修改的,且存在被多线程访问的情况,就需要使用 Atomic ,
并且不可以外泄 Atomic 的赋值权,必须使用 Atomic 自带的更新方法。
如果对象中的任何方法中存在使用 Atomic 对象,保证每次访问 Atomic 对象的操作是完全独立的(不产生分支和引用),
如果是存在依赖性操作,必须保证该依赖性操作的原子性。(也就是上文提到的,不可组合原子方法,或使用锁保证原子性)
如果这个对象本身已经保证所有操作的原子性,就需要保证调用这个对象的对象操作的原子性(外层也不可组合原子方法,以此类推)。
## 对于线程安全的要求
对能简单修复的线程安全问题尽量予以修复,比如 使用 `atomic` 代理,
使用 `ConcurrentHashMap` 以及其内置的其他原子方法,
如果内置的所有原子方法已经不足以满足你的需求,可以尝试使用简单的 `Mutex`
(请遵循指导在协程下正确使用 `Mutex`)
但是在使用 `Mutex` 或更复杂的线程安全机制前,
首先思考,我是否能接受发生问题的风险(如原石操作是很敏感的,
但是重复复写入 `insertDefault` 或者是因为高并发重复启动一些协程,
这个成本是可以承受的)
成本可接受指的是,造成的数据变更可接受,造成的性能损耗可接受,程序不会报错崩溃
## 关于锁
在 Kotlin 协程中,与线程绑定的锁会容易造成死锁问题(比如 `Mutex`),
建议使用 `sorapointa-utils` 模块中拓展的 `withReentrantLock` 方法,以确保锁的上下文一致性。
可以参考,[Phantom of the Coroutine](https://elizarov.medium.com/phantom-of-the-coroutine-afc63b03a131)
## 结构化并发
代码应该是合作式的,并使用结构化并发确保所有的协程不会泄漏与可被管理
在 Sorapointa 中,我们使用了 `ModuleScope` 以确保正确建立协程之间的任务结构
你可以参照 `TaskManager`,`EventManager` 等,写出合作式的代码
通常情况下,请不要实现 `CoroutineScope` 接口以实现结构化并发,也不要往 `launch` 方法中添加上下文。
具体原因可以参考,[Kotlin CoroutineScope 文档](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/),
[为什么你不应该实现 CoroutineScope 接口](https://proandroiddev.com/why-your-class-probably-shouldnt-implement-coroutinescope-eb34f722e510),
[结构化并发周年庆 - Roman Elizarov](https://elizarov.medium.com/structured-concurrency-anniversary-f2cc748b2401),
[CoroutineScope 的 Legacy Convention](https://maxkim.eu/things-every-kotlin-developer-should-know-about-coroutines-part-2-coroutinescope)
简而言之,这是一种过时了的方法。
了解更多,关于 [结构化并发](https://kotlinlang.org/docs/composing-suspending-functions.html#structured-concurrency-with-async)
================================================
FILE: docs/guides/database.md
================================================
# Database Operation Safety
## Transaction
Nested use of `transaction` is prohibited when it is not required,
which would result in sessions started at `READ_COMMITTED` and above isolation levels
not being able to read any changes within the nested transaction,
resulting in various unintended and unpredictable outcomes,
so transactions cannot be created within API methods,
and transactions must be guaranteed to be invoked
by the outermost code, not by the api layer.
Any error during the execution of the transaction
will cause the entire operation of the transaction to be rolled back.
### Transaction Isolation
The reason for the above requirement is that the Repeatable Read isolation level
only sees data committed before the transaction began;
it never sees either uncommitted data or changes committed during transaction execution by concurrent transactions.
(However, the query does see the effects of previous updates executed within its own transaction,
even though they are not yet committed.)
This level is different from Read Committed in that a query in a repeatable read transaction
sees a snapshot as of the start of the first non-transaction-control statement in the transaction,
not as of the start of the current statement within the transaction.
Thus, successive `SELECT` commands within a single transaction see the same data,
i.e., they do not see changes made by other transactions
that committed after their own transaction started.
So if you use nested transactions,
it will make outer transaction could not see updates from the inner transaction,
and further cause unintended results.
Refers to [PostgreSQL Manual - Transaction Isolation](https://www.postgresql.org/docs/current/transaction-iso.html)
### Asynchronous Transactions
Asynchronous transactions are allowed,
but it is not allowed to switch threads / coroutine in the same transaction
to operate on the database.
This will cause deadlocks during high concurrency.
All database operations within the same transaction must be
executed synchronously under the same thread.
## Table Structure
Once the table structure is defined,
it is best not to make any changes (including deleting fields and modifying field properties).
Because it will require user to manually sync new table structure,
only adding fields can be updated automatically.
## API Method
Any database operations API provided to the upper layer should not include any transaction block.
It will cause nested transactions and raises the issues mentioned above due to transaction isolation.
================================================
FILE: docs/guides/database.zh-CN.md
================================================
# 数据库操作安全
## 事务
在非必须时禁止使用嵌套使用 `transaction`,这会导致 `READ_COMMITTED`
及以上的事物隔离级别启动的会话**无法读取到嵌套事务内的任何变更**,
以造成各种不符合预期和难以预测的结果,
所以**不能在 API 方法内创建事务**,
事务**必须保证是被最外层应用端调用**,而非底层。
事务执行过程中发生任何错误都会使得整个事务的操作被回滚。
### 事务隔离
对于上述要求的原因是,`READ_COMMITTED` 隔离级别在 `PostgreSQL` 中,
在 `READ_COMMITTED` 事务中的查询看到的是事务开始时的快照,
而不是该事务内部当前查询开始时的快照,这样,单个事务内部的 `SELECT` 命令总是看到同样的数据,
也就是说,它们看不到它们自身事务开始之后提交的其他事务所做出的改变。
所以如果使用嵌套事务,将会使得外层事务无法看到内层事务的更新,并进一步造成不符合预期的结果。
请参考 [PostgreSQL 手册 - 事务隔离](http://www.postgres.cn/docs/9.3/transaction-iso.html)
### 事务的异步
在任何一个事务中都禁止使用任何异步方法来操作数据库,
异步在事务是允许的,但是不允许在同一事务中切线程 / 协程操作数据库,
这会在高并发时造成死锁,在同一事务内所有数据库操作要求必须在同一线程下同步执行。
## 表结构安全
表结构确定了之后最好不要进行任何修改(包括删除字段,修改字段的属性),
因为这需要用户手动同步,只有增加字段的操作可以被自动更新。
## API 安全
任何提供给上层的封装后的数据库操作都**不要自己启动事务**,
这会造成事务的嵌套,并引发在上面提到由于事务隔离所产生的问题。
================================================
FILE: docs/guides/kotlin-atomicfu.md
================================================
# Kotlin AtomicFU Guideline
[简体中文](kotlin-atomicfu.zh-CN.md)
## Setup
```kotlin
dependencies {
implementation("org.jetbrains.kotlinx:atomicfu:_")
}
```
## Usage
```kotlin
val atomicInt = atomic(123)
atomicInt.value // read
atomicInt.value = 1234 // write
```
Must ensure all operations are atomic. You can only operate value with provided methods.
If you only need read and write operation, use kotlin delegation:
```kotlin
var delegatedAtomic by atomic(1231)
delegatedAtomic // read
delegatedAtomic = 123 // write
```
**BUT, please use the methods `atomicfu` provided only. Atomicity requires careful maintenance. The snippet below
is completely wrong:**
**Multiple atomic methods cannot be combined, combining atomic methods will lose atomicity**
```kotlin
var delegatedAtomic by atomic(123)
if (delegatedAtomic == 1) delegatedAtomic = 1000
```
Should be:
```kotlin
val atomicInt = atomic(123)
atomicInt.compareAndSet(expect = 1, update = 1000)
// or... using high-order function
atomicInt.getAndUpdate { if (it == 1) 1000 else it }
```
Idiomatic lock-free methods:
```kotlin
fun push(v: Value) = top.update { cur -> Node(v, cur) }
fun pop(): Value? = top.getAndUpdate { cur -> cur?.next }?.value
```
Int and Long atomics provide all the usual `getAndIncrement`, `incrementAndGet`, `getAndAdd`, `addAndGet`, etc. They can
be also atomically modified via `+=` and `-=` operators.
## Notice
### Avoid using unprovided API
Notice again, atomicity needs careful maintenance. You must use provided API.
- Do not read references on atomic variables into local variables
- Do not introduce complex data flow in parameters to atomic variable operations, please use function
like `atomicValue.update` instead
### Hide internal implementation
As you can see, it's hard to maintenance thread-safe, so do not leak reference to other modules.
Use the following convention if you need to expose the value of atomic property to the public:
```kotlin
private val _foo = atomic(100) // private atomic, starts with underscore
public var foo: Int by _foo // public delegated property (val/var)
```
================================================
FILE: docs/guides/kotlin-atomicfu.zh-CN.md
================================================
# Kotlin AtomicFU 使用指南
[English](kotlin-atomicfu.md)
## Setup
```kotlin
dependencies {
implementation("org.jetbrains.kotlinx:atomicfu:_")
}
```
## 用法
```kotlin
val atomicInt = atomic(123)
atomicInt.value // 读
atomicInt.value = 1234 // 写
```
必须确保所有的操作都是原子的。亦即只能通过 `atomicfu` 已经提供的方法操作。
如果只需要读/写操作,可以使用代理:
```kotlin
var delegatedAtomic by atomic(1231)
delegatedAtomic // 读
delegatedAtomic = 123 // 写
```
**但切记只能通过 `atomicfu` 已经提供的方法操作,原子性需要小心维护,以下的写法是完全错误的:**
**多个原子方法不可组合使用,组合原子方法会失去原子性**
```kotlin
var delegatedAtomic by atomic(123)
if (delegatedAtomic == 1) delegatedAtomic = 1000
```
正确的写法:
```kotlin
val atomicInt = atomic(123)
atomicInt.compareAndSet(expect = 1, update = 1000)
// 或... 使用高阶函数
atomicInt.getAndUpdate { if (it == 1) 1000 else it }
```
无需加锁的函数式写法:
```kotlin
fun push(v: Value) = top.update { cur -> Node(v, cur) }
fun pop(): Value? = top.getAndUpdate { cur -> cur?.next }?.value
```
`Int` 和 `Long` 也有 `getAndIncrement`, `incrementAndGet`, `getAndAdd`, `addAndGet` 方法,以及对 `+=` `-=` 的操作符重载。
## 注意事项
### 避免使用未提供的操作
再次强调,原子性并不凭空产生。你需要使用已经提供的原子性 API。
- 不要使用局部变量存储 atomic 变量的引用
- 不要使用复杂的表达式,任何有分支的语句都会造成问题,需要时请使用诸如 `atomicValue.update` 的方法。
### 隐藏内部实现
如你所见,维护线程安全并不容易,所以不要向外泄漏引用。
需要向外公开API时,请像这样:
```kotlin
private val _foo = atomic(100) // 内部原子变量,以 _ 开头
public var foo: Int by _foo // 公开代理属性 (val/var)
```
================================================
FILE: docs/guides/unit-test.md
================================================
# JUnit Guideline
[简体中文](unit-test.zh-CN.md)
[JUnit Official User Guide](https://junit.org/junit5/docs/current/user-guide/)
## Basic Usage
You can create unit test in `test` source set,
the package structure should keep the same as the `main` source set.
For example, you're going to write unit test for `org.sorapointa.db.Account`,
you can create class `org.sorapointa.db.AccountTest`:
```kotlin
// org.sorapointa.db.Account
data class Account(
val name: String,
val level: Int,
)
// org.sorapointa.db.AccountTest
class AccountTest {
@Test
fun `account must be equals`() {
val var1 = Account(name = "foo", level = 20)
val var2 = var1.copy()
assertEquals(var1, var2)
}
}
```
By annotating test function with `@Test`, JUnit and IDEA can recognize it.
You may have noticed the test name is surrounded by backticks.
It's for readability, you can use natural language in test name.
(By the way, under_score_style is popular in Java and Android test.)
See [Kotlin documentation](https://kotlinlang.org/docs/coding-conventions.html#names-for-test-methods).
Assertion is a way for test by describing the program **MUST** or **MUST NOT** do something.
If condition equals to false, assertion throw exception, then test fails.
Following method is frequently used:
- `assert(condition)`
- `assertEquals(excepted, actual)`
- `assertContentEquals(excepted, actual)` for arrays
- `assertTrue { block }`
## BeforeAll / BeforeEach
1.
```kotlin
@TestInstance(Lifecycle.PER_CLASS)
class DatabaseTest {
@BeforeAll
fun initDatabase() {
// do init
}
@Test
fun test1() {
// do test...
}
}
```
2.
```kotlin
class DatabaseTest {
companion object {
@BeforeAll
@JvmStatic
fun initDatabase() {
// do init
}
}
@Test
fun test1() {
// do test...
}
}
```
Both 1 and 2 are correct. The function annotated by `@BeforeAll` will be invoked before other function.
`@BeforeEach` is similar, but the function will be invoked before each function.
## Test Dependency
If a dependency is test-only, add it in `build.gradle.kts` in this way:
```kotlin
dependencies {
testImplementation("com.example.artifact:example:version")
// add test dependency from subprojects:
testImplementation(project(":sorapointa-event", "test"))
}
```
## Sorapointa Test Util
### Properties
- `TEST_DIR` is the Gradle root project directory
- `IS_CI`, is run in CI
### Functions
- `runTest` provide a suspendable block, and SKIP_OPTIONs
```kotlin
// SKIP_CI: skip test if run in CI
runTest(TestOption.SKIP_CI) {
val atomicInt = atomic(0)
(1..10).map {
// suspend and CoroutineScope extension function is applicable
launch {
repeat(1000) {
atomicInt.getAndIncrement()
}
}
}.joinAll()
assertEquals(10000, 10000)
}
```
### High Volume Test
- Do not run high volume test in the IDE's `Debug` mode, which will cause `OutOfMemory` and deadlocks for unknown reasons
================================================
FILE: docs/guides/unit-test.zh-CN.md
================================================
# JUnit 使用指南
[English](unit-test.md)
[JUnit 官方文档](https://junit.org/junit5/docs/current/user-guide/)
## 基本用法
你可以在 `test` 源集中创建单元测试,包结构应该和 `main` 源集保持一致。
例如,要为 `org.sorapointa.db.Account` 编写单元测试,
就可以创建 `org.sorapointa.db.AccountTest`。
```kotlin
// org.sorapointa.db.Account
data class Account(
val name: String,
val level: Int,
)
// org.sorapointa.db.AccountTest
class AccountTest {
@Test
fun `account must be equals`() {
val var1 = Account(name = "foo", level = 20)
val var2 = var1.copy()
assertEquals(var1, var2)
}
}
```
通过添加 `@Test` 注解,JUnit 和 IDEA 就可以识别出单元测试。
你或许已经注意到,测试方法名被反引号包裹。
这是为了可读性,你可以在测试名中使用自然语言。
(另外,下划线命名法在 Java 和 Android 测试中也很流行。)
参见:[Kotlin 文档](https://kotlinlang.org/docs/coding-conventions.html#names-for-test-methods).
断言通过描述**必须**(MUST)和**一定不能**(MUST NOT)做的事,以测试程序。
如果条件为 false,断言抛出异常,同时测试也失败。
以下方法经常用到:
- `assert(条件)`
- `assertEquals(预期值, 实际值)`
- `assertContentEquals(预期值, 实际值)` 用于数组
- `assertTrue { 代码块 }`
## BeforeAll / BeforeEach
1.
```kotlin
@TestInstance(Lifecycle.PER_CLASS)
class DatabaseTest {
@BeforeAll
fun initDatabase() {
// 初始化
}
@Test
fun test1() {
// 测试...
}
}
```
2.
```kotlin
class DatabaseTest {
companion object {
@BeforeAll
@JvmStatic
fun initDatabase() {
// 初始化
}
}
@Test
fun test1() {
// 测试...
}
}
```
1 和 2 都是正确的。被 `@BeforeAll` 注解的方法将会在所有方法之前被调用。
`@BeforeEach` 类似,但函数在每个方法之前被调用。
## 测试依赖
如果一个依赖仅用于测试,在 `build.gradle.kts` 中这样添加:
```kotlin
dependencies {
testImplementation("com.example.artifact:example:version")
// 添加子模块的依赖:
testImplementation(project(":sorapointa-event", "test"))
}
```
## Sorapointa 测试工具
### 属性
- `TEST_DIR` is the Gradle root project directory
- `IS_CI`, is run in CI
### 方法
- `runTest` 提供 suspend 块和 SKIP_OPTION
```kotlin
// SKIP_CI: 如果运行在 CI,跳过该测试
runTest(TestOption.SKIP_CI) {
val atomicInt = atomic(0)
(1..10).map {
// 可以使用 suspend 函数和 CoroutineScope 的拓展方法
launch {
repeat(1000) {
atomicInt.getAndIncrement()
}
}
}.joinAll()
assertEquals(10000, 10000)
}
```
### 关于高并发测试
- 请不要在 IDE 的 `Debug` 模式下运行高并发测试,这容易导致 `OutOfMemory` 和未知原因的死锁
================================================
FILE: gradle/libs.versions.toml
================================================
[versions]
kotlin = "1.8.10"
ktor = "2.1.2"
ktlint = "0.48.1"
exposed = "0.41.1"
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "10.3.0" }
abi-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.12.1" }
wire = { id = "com.squareup.wire", version = "4.4.3" }
rust-wrapper = { id = "fr.stardustenterprises.rust.wrapper", version = "3.2.5" }
spotless = { id = "com.diffplug.spotless", version = "6.15.0" }
licensee = { id = "app.cash.licensee", version = "1.6.0" }
[libraries]
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinx-serialization-core = "org.jetbrains.kotlinx:kotlinx-serialization-core:1.4.1"
kotlinx-serialization-json = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
kaml = "com.charleskorn.kaml:kaml:0.49.0"
moshi = "com.squareup.moshi:moshi:1.14.0"
wire-moshi-adapter = "com.squareup.wire:wire-moshi-adapter:4.4.3"
kotlinx-coroutines-core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
kotlinx-datetime = "org.jetbrains.kotlinx:kotlinx-datetime:0.4.0"
microutils-logging = "io.github.microutils:kotlin-logging-jvm:3.0.4"
logback = "ch.qos.logback:logback-classic:1.4.5"
atomicfu = "org.jetbrains.kotlinx:atomicfu:0.18.3"
netty = "io.netty:netty-handler:4.1.86.Final"
kcp = "moe.sdl.kcp:grasskcpper:2.0.0"
yac = "moe.sdl.yac:core:1.0.1"
jline = "org.jline:jline:3.22.0"
password4j = "com.password4j:password4j:1.6.3"
classgraph = "io.github.classgraph:classgraph:4.8.154"
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
hikaricp = "com.zaxxer:HikariCP:5.0.1"
sqlite = "org.xerial:sqlite-jdbc:3.40.0.0"
postgresql = "org.postgresql:postgresql:42.5.1"
ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" }
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktor" }
ktor-server-logging = { module = "io.ktor:ktor-server-call-logging-jvm", version.ref = "ktor" }
ktor-server-compression = { module = "io.ktor:ktor-server-compression-jvm", version.ref = "ktor" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" }
ktor-server-status-page = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" }
ktor-server-html = { module = "io.ktor:ktor-server-html-builder", version.ref = "ktor" }
ktor-server-wss = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-network-cert = { module = "io.ktor:ktor-network-tls-certificates", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktor" }
ktor-utils = { module = "io.ktor:ktor-utils", version.ref = "ktor" }
cron-utils = "com.cronutils:cron-utils:9.2.0"
wire-runtime = "com.squareup.wire:wire-runtime:4.4.3"
okio = "com.squareup.okio:okio:3.3.0"
build-kotlinpoet = "com.squareup:kotlinpoet:1.12.0"
build-buildconfig = "com.github.gmazzo:gradle-buildconfig-plugin:3.1.0"
build-shadow = "gradle.plugin.com.github.johnrengelman:shadow:7.1.2"
build-atomicfu = "org.jetbrains.kotlinx:atomicfu-gradle-plugin:0.18.5"
yanl = "fr.stardustenterprises:yanl:0.8.1"
plat4k = "fr.stardustenterprises:plat4k:1.6.3"
[bundles]
log = ["microutils-logging", "logback"]
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
kotlin.code.style=official
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.warning.mode=summary
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: renovate.json
================================================
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
]
}
================================================
FILE: settings.gradle.kts
================================================
rootProject.name = "Sorapointa"
include("sorapointa-core")
include("sorapointa-crypto")
include("sorapointa-dataloader")
include("sorapointa-dataprovider")
include("sorapointa-dispatch")
include("sorapointa-event")
include("sorapointa-i18n")
include("sorapointa-native")
include("sorapointa-native-wrapper")
include("sorapointa-proto")
include("sorapointa-task")
include("sorapointa-utils")
include("sorapointa-utils:sorapointa-utils-all")
include("sorapointa-utils:sorapointa-utils-core")
include("sorapointa-utils:sorapointa-utils-crypto")
include("sorapointa-utils:sorapointa-utils-serialization")
include("sorapointa-utils:sorapointa-utils-time")
pluginManagement {
repositories {
google()
gradlePluginPortal()
}
}
================================================
FILE: sorapointa-core/README.md
================================================
# Core Module
[简体中文](README.zh-CN.md)
## Command System
The command system is based on [**Yac**](https://githubfast.com/Colerar/Yac)
(A variety of [**clikt**](https://ajalt.github.io/clikt/)),
usage is basically the same as **clikt**.
The main components are: `Command`, `CommandSender`, `CommandManager`
- `Command`: The command object, with `run` function and parser provided by **Yac**
- `CommandSender`: The sender who invoke the command
- `CommandManager`: A manager to register, invoke `Command` asynchronously and thread-safely.
### Create
```kotlin
class ExampleCommand(
private val sender: CommandSender,
) : Command(sender, ExampleCommand) {
companion object : Entry(
name = "example",
help = "An example command",
alias = listOf("alias")
)
val option by option("--option").flag()
override suspend fun run() {
sender.sendMessage("Hello Command! Option: $option")
}
}
```
There is a little trick, properties of command are store in `Entry` object,
so we can declare a companion object.
Then we can access it with class name directly.
For more information, see [Clikt Documentation](https://ajalt.github.io/clikt/).
### Register
Register a command:
```kotlin
val command = CommandNode(ExampleCommand) { sender -> ExampleCommand(sender) }
CommandManager.registerCommand(command)
```
The first parameter of `CommandNode` is `CommandEntry`, the companion object we declared before.
The second parameter is a high-order function, used to construct a command instance.
Register a list of commands:
```kotlin
val commands = listOf(
CommandNode(ExampleCommand) { sender -> ExampleCommand(sender) },
CommandNode(Foo) { sender -> Foo(sender) },
CommandNode(Bar) { sender -> Bar(sender) },
CommandNode(Help) { sender -> Help(sender) },
)
CommandManager.registerCommands(commands)
```
### Invoke
At most of the time, you do not need to invoke command manually, core will invoke for you.
But in some cases, you may want to call command manually in your code,
or invoke command when player trigger event.
```kotlin
// just an example for invoking command,
// related module still in progress,
// so it may be not applicable now
// current this: Player
CommandManager.invokeCommand(this.asSender(), ExampleCommand.name)
```
================================================
FILE: sorapointa-core/README.zh-CN.md
================================================
# Core 模块
[English](README.zh-CN.md)
## 命令系统
命令系统基于 [**Yac**](https://githubfast.com/Colerar/Yac) ([**clikt**](https://ajalt.github.io/clikt/) 的变体),
用法基本和 **clikt** 相同。
主要组成有: `Command`, `CommandSender`, `CommandManager`
- `Command`: 命令对象,具有 `run` 函数和由 **Yac** 提供的命令解析功能
- `CommandSender`: 命令发送者
- `CommandManager`: 命令管理器,提供并发、线程安全的命令注册和调用
### 创建
```kotlin
class ExampleCommand(
private val sender: CommandSender,
) : Command(sender, ExampleCommand) {
companion object : Entry(
name = "example",
help = "示例指令",
alias = listOf("示例")
)
val option by option("--option").flag()
override suspend fun run() {
sender.sendMessage("你好!选项:$option")
}
}
```
请注意这里的小技巧,命令的属性用 `Entry` 对象存储,我们可以将其定义为伴生对象(companion object),以便于用类名直接获取。
关于命令解析,请参见:[Clikt 文档](https://ajalt.github.io/clikt/)
### 注册
注册单个命令:
```kotlin
val command = CommandNode(ExampleCommand) { sender -> ExampleCommand(sender) }
CommandManager.registerCommand(command)
```
`CommandNode`的第一个参数是 `CommandEntry`,之前我们将其定义为伴生对象;
第二个参数是一个高阶函数,用于构造新的命令实例。
注册命令清单:
```kotlin
val commands = listOf(
CommandNode(ExampleCommand) { sender -> ExampleCommand(sender) },
CommandNode(Foo) { sender -> Foo(sender) },
CommandNode(Bar) { sender -> Bar(sender) },
CommandNode(Help) { sender -> Help(sender) },
)
CommandManager.registerCommands(commands)
```
### 调用
多数情况下你不需要手动调用命令,core 会帮你调用。
但在有些情况,你可能想在代码里手动调用命令,或在玩家触发某些事件时调用。
```kotlin
// 只是一个示例,代码可能并不可用,因为相关模块尚未完成
// 上下文中的 this: Player
CommandManager.invokeCommand(this.asSender(), ExampleCommand.name)
```
================================================
FILE: sorapointa-core/build.gradle.kts
================================================
@file:Suppress("GradlePackageUpdate")
plugins {
`sorapointa-conventions`
`sorapointa-publish`
application
}
dependencies {
// Project submodules
implementation(project(":sorapointa-dataloader"))
implementation(project(":sorapointa-dispatch"))
api(project(":sorapointa-dataprovider"))
api(project(":sorapointa-event"))
api(project(":sorapointa-i18n"))
api(project(":sorapointa-proto"))
api(project(":sorapointa-task"))
api(project(":sorapointa-crypto"))
api(project(":sorapointa-utils:sorapointa-utils-all"))
// KotlinX
implementation(libs.atomicfu)
// network
implementation(libs.netty)
implementation(libs.kcp)
// Ktor
implementation(libs.ktor.server.wss)
// Command
api(libs.yac)
// Console
implementation(libs.jline)
implementation(libs.password4j)
implementation(libs.okio)
testImplementation(project(":sorapointa-dispatch", "test"))
testImplementation(project(":sorapointa-dataprovider", "test"))
}
configurations.all {
resolutionStrategy.cacheChangingModulesFor(0, "seconds")
}
application {
applicationName = "sorapointa"
mainClass.set("org.sorapointa.MainKt")
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/CoreBundle.kt
================================================
package org.sorapointa
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.PropertyKey
import org.sorapointa.utils.MessageBundle
import java.util.*
internal const val BUNDLE = "messages.CoreBundle"
object CoreBundle : MessageBundle(BUNDLE) {
@Nls
@JvmStatic
fun message(
@PropertyKey(resourceBundle = BUNDLE) key: String,
vararg params: Any?,
locale: Locale? = null,
): String = getString(key, *params, locale = locale)
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/Main.kt
================================================
package org.sorapointa
import io.ktor.server.application.*
import kotlinx.coroutines.*
import moe.sdl.yac.core.CliktCommand
import moe.sdl.yac.core.CommandResult
import moe.sdl.yac.parameters.groups.OptionGroup
import moe.sdl.yac.parameters.groups.defaultByName
import moe.sdl.yac.parameters.groups.groupSwitch
import moe.sdl.yac.parameters.options.*
import mu.KotlinLogging
import org.jline.reader.EndOfFileException
import org.jline.reader.UserInterruptException
import org.sorapointa.SorapointaMain.Mode.*
import org.sorapointa.command.CommandManager
import org.sorapointa.command.ConsoleCommandSender
import org.sorapointa.command.defaults.defaultsCommand
import org.sorapointa.config.*
import org.sorapointa.config.registeredConfig
import org.sorapointa.config.registeredDatabaseTable
import org.sorapointa.console.Console
import org.sorapointa.console.setupConsoleClient
import org.sorapointa.console.setupWebConsoleServer
import org.sorapointa.data.provider.DatabaseManager
import org.sorapointa.dataloader.ResourceHolder
import org.sorapointa.event.EventManager
import org.sorapointa.game.data.SorapointaStore
import org.sorapointa.task.TaskManager
import org.sorapointa.utils.ModuleScope
import org.sorapointa.utils.absPath
import org.sorapointa.utils.addShutdownHook
import org.sorapointa.utils.globalWorkDirectory
import java.io.File
import kotlin.system.exitProcess
import kotlin.system.measureTimeMillis
private val logger = KotlinLogging.logger {}
class SorapointaMain : CliktCommand(name = "sorapointa") {
private val workingDirectory by option("-D", "--working-directory", help = "Set working directory")
.convert { File(it) }
.check("File must be directory") { (it.exists() && it.isDirectory) || (!it.exists()) }
private val noOut by option("-N", "--no-out", help = "stdout and stderr will be disable")
.flag(default = false)
private val noRedirect by option("-R", "--no-redirect", help = "Whether redirect to JLine's printAbove")
.flag(default = false)
private sealed class Mode : OptionGroup() {
class Server : Mode()
class Client : Mode() {
val username by option("--username", "--usr", "-u").required()
val password by option("--password", "--pwd", "-p").default("")
val wssUrl by option("--wss-url", "--url", "-I").default("wss://localhost:443/webconsole")
}
class Mixed : Mode()
class Local : Mode()
}
private val mode by option().groupSwitch(
"--server" to Server(),
"--client" to Client(),
"--local" to Local(),
"--mixed" to Mixed(),
).defaultByName("--local")
override suspend fun run(): Unit = scope.launch {
setupShutdownHook()
logger.info { "Version: $VERSION-$BUILD_BRANCH+$COMMIT_HASH" }
workingDirectory?.let { System.setProperty("user.dir", it.absPath) }
logger.info { "Sorapointa is working in $globalWorkDirectory" }
redirectPrint()
when (val m = mode) {
is Server -> {
val server = setupServer {
it.setupWebConsoleServer()
}
server.join()
}
is Mixed -> {
val server = setupServer {
it.setupWebConsoleServer()
}
setupLocalConsole()
server.join()
}
is Local -> {
val server = setupServer()
setupLocalConsole()
server.join()
}
is Client -> {
setupConsoleClient(m.username, m.password, m.wssUrl)
}
}
}.join()
private fun setupShutdownHook() {
addShutdownHook {
closeAll()
println("\nExiting Sorapointa...")
Console.redirectToNull()
}
}
private fun setupServer(config: (Application) -> Unit = {}) = scope.launch {
setupRegisteredConfigs().join()
setupDefaultsCommand()
Console.setupCompletion()
EventManager.init(scope.coroutineContext)
TaskManager.init(scope.coroutineContext)
setupDatabase().join()
setupDataloader().join()
Sorapointa.init(this, scope.coroutineContext, config)
}
private fun setupLocalConsole() = scope.launch {
val consoleSender = ConsoleCommandSender()
while (isActive) {
try {
CommandManager.invokeCommand(consoleSender, Console.readln()).join()
} catch (e: UserInterruptException) { // Ctrl + C
println("<Interrupted> use 'quit' command to exit process")
} catch (e: EndOfFileException) { // Ctrl + D
exitProcess(0)
}
}
}
private fun setupRegisteredConfigs(): Job =
scope.launch {
logger.info { "Loading Sorapointa configs..." }
val ms = measureTimeMillis {
registeredConfig.map {
launch {
it.init()
it.save()
}
}.joinAll()
}
logger.info { "Costed $ms ms for loading all configs" }
}
private fun setupDataloader(): Job {
return scope.launch {
logger.info { "Loading Sorapointa excel data..." }
runCatching {
val count: Int
val time = measureTimeMillis {
count = ResourceHolder.findAndRegister()
ResourceHolder.loadAll(scope.coroutineContext)
}
logger.info { "Loaded $count excel data in $time ms" }
}.onFailure {
logger.error(it) { "Could not load sorapointa resources data" }
exitProcess(1)
}
}
}
private fun setupDatabase(): Job =
scope.launch {
logger.info { "Loading Sorapointa database..." }
val time = measureTimeMillis {
DatabaseManager.loadDatabase()
DatabaseManager.loadTables(registeredDatabaseTable)
}
logger.info { "Loaded ${registeredDatabaseTable.size} tables in $time ms" }
SorapointaStore.initDefaultEntry()
}
private fun setupDefaultsCommand() {
CommandManager.registerCommands(defaultsCommand)
val registered = defaultsCommand.joinToString(", ") { it.entry.name }
logger.info { "Registered defaults command, total ${defaultsCommand.size}: $registered" }
}
private fun redirectPrint() {
Console.initReader()
when {
noOut -> Console.redirectToNull()
!noOut && !noRedirect -> Console.redirectToJLine()
else -> {
// keep origin
}
}
}
companion object {
private val scope = ModuleScope("SorapointaRootScope")
internal fun closeAll() {
scope.dispose()
scope.cancel()
}
}
}
suspend fun main(args: Array<String>) {
when (val result = SorapointaMain().main(args)) {
is CommandResult.Success -> {
exitProcess(0)
}
is CommandResult.Error -> {
println(result.userMessage)
exitProcess(1)
}
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/Sorapointa.kt
================================================
package org.sorapointa
import io.ktor.server.application.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.sorapointa.command.CommandManager
import org.sorapointa.dispatch.DispatchServer
import org.sorapointa.dispatch.plugins.getCurrentRegionHttpRsp
import org.sorapointa.dispatch.plugins.saveCache
import org.sorapointa.game.Player
import org.sorapointa.server.ServerNetwork
import org.sorapointa.utils.ModuleScope
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
object Sorapointa {
private var scope = ModuleScope("Sorapointa")
private val playerMap = ConcurrentHashMap<Int, Player>()
internal fun init(
serverScope: CoroutineScope,
parentContext: CoroutineContext = EmptyCoroutineContext,
config: (Application) -> Unit = {},
): Job {
scope = ModuleScope("Sorapointa", parentContext)
CommandManager.init(scope.coroutineContext)
ServerNetwork.boot(scope.coroutineContext)
return if (SorapointaConfig.data.startWithDispatch) {
DispatchServer.startDispatch(serverScope, config = config)
} else {
// If we don't start with dispatch server,
// we will need some CurRegHttpRsp data for future processing.
// And since we don't have original request from player client, or sth,
// so we can only use the hardcode URL in
scope.launch {
getCurrentRegionHttpRsp().saveCache()
}
}
}
suspend fun addPlayer(player: Player) {
player.init()
playerMap[player.uid] = player
}
fun removePlayer(uid: Int) {
playerMap.remove(uid)
}
fun getPlayerList(): List<Player> {
return playerMap.values.toList()
}
fun findPlayerById(id: Int) =
playerMap[id] ?: error("Cannot find player with uid $id")
fun findPlayerByIdOrNull(id: Int) =
playerMap[id]
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/SorapointaConfig.kt
================================================
package org.sorapointa
import com.charleskorn.kaml.YamlComment
import kotlinx.datetime.TimeZone
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.sorapointa.data.provider.DataFilePersist
import org.sorapointa.utils.configDirectory
import org.sorapointa.utils.lenientYaml
import java.io.File
import kotlin.time.Duration
object SorapointaConfig : DataFilePersist<SorapointaConfig.Data>(
File(configDirectory, "sorapointaConfig.yaml"),
Data(),
Data.serializer(),
lenientYaml,
) {
@Serializable
data class Data(
@YamlComment("Run sorapointa with dispatch server, sorapointa allow you to run them separately")
val startWithDispatch: Boolean = true,
@YamlComment("Use current region info for login rsp")
val useCurrentRegionForLoginRsp: Boolean = true,
val offsetHours: Int = 4,
@SerialName("timeZone")
private val _timeZone: String = TimeZone.currentSystemDefault().toString(),
@YamlComment("Game server network setting")
val networkSetting: NetworkSetting = NetworkSetting(),
@YamlComment("Player inventory store limits")
val inventoryLimits: InventoryLimits = InventoryLimits(),
@YamlComment(
"" +
"Debug setting for developers",
"Notice: if you want to enable debug log, " +
"YOU SHOULD ENABLE IT BY `-Dlogback.configurationFile=path/to/logback.xml`",
)
val debugSetting: DebugSetting = DebugSetting(),
) {
val timeZone by lazy {
TimeZone.of(_timeZone)
}
}
@Serializable
data class InventoryLimits(
val weapon: Int = 2000,
val reliquary: Int = 2000,
val material: Int = 2000,
val furniture: Int = 2000,
val allWeight: Int = 30000,
)
@Serializable
data class NetworkSetting(
@YamlComment("Game server bind port")
val bindPort: Int = 22101,
@YamlComment("Auto disconnect session if client dosen't send `PingReq` in specified time")
@SerialName("pingTimeout")
private val _pingTimeout: String = "20s",
@YamlComment(
"Game server kcp setting, don't change those settings if you don't know about KCP",
"See more: https://github.com/skywind3000/kcp",
)
val uKcpSetting: UKcpSetting = UKcpSetting(),
) {
val pingTimeout: Duration
get() = Duration.parse(_pingTimeout)
}
@Serializable
data class UKcpSetting(
val noDelay: Boolean = true,
val interval: Int = 40,
val fastResend: Int = 2,
val noCongestionWindow: Boolean = true,
val MTU: Int = 1400,
val sendWindow: Int = 256,
val receiveWindow: Int = 256,
val timeoutMillis: Long = 30 * 1000, // KCP Timeout > Protocol Ping Timeout
val ackNoDelay: Boolean = false,
)
@Serializable
data class DebugSetting(
@YamlComment("Use CamelCase rather than SNAKE_CASE for packet name")
val camelCasePacketName: Boolean = true,
@YamlComment("Turn on means use blocklist to filter packet, off means use allowlist to filter packet")
val blockListPacketWatcher: Boolean = true,
@YamlComment("Skip born cutscene and auto choose name and avatar")
val skipBornCutscene: Boolean = false,
@YamlComment("Blocklist of Packet Watcher")
val blocklist: List<String> = listOf(
"PingReq",
"PingRsp",
"UnionCmdNotify",
"PlayerSetPauseReq",
"PlayerSetPauseRsp",
),
@YamlComment("Allowlist of Packet Watcher")
val allowlist: List<String> = listOf(),
)
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/Command.kt
================================================
package org.sorapointa.command
import moe.sdl.yac.core.CliktCommand
import moe.sdl.yac.core.context
import org.jetbrains.annotations.PropertyKey
import org.sorapointa.BUNDLE
import org.sorapointa.CoreBundle
import org.sorapointa.game.Player
abstract class Command(
sender: CommandSender,
entry: Entry,
option: Option = Option(),
) : CliktCommand(
name = entry.name,
help = CoreBundle.message(entry.helpKey, locale = sender.locale),
invokeWithoutSubcommand = option.invokeWithoutSubCommand,
printHelpOnEmptyArgs = option.printHelpOnEmptyArgs,
allowMultipleSubcommands = option.allowMultipleSubcommands,
treatUnknownOptionsAsArgs = option.treatUnknownOptionsAsArgs,
) {
class Option(
val invokeWithoutSubCommand: Boolean = false,
val printHelpOnEmptyArgs: Boolean = false,
val allowMultipleSubcommands: Boolean = false,
val treatUnknownOptionsAsArgs: Boolean = false,
)
/**
* @param helpKey, will be invoked with i18n
*/
open class Entry(
val name: String,
@PropertyKey(resourceBundle = BUNDLE) val helpKey: String,
val alias: List<String> = emptyList(),
val permissionRequired: Int = 0,
)
init {
context { localization = CommandLocalization }
}
}
abstract class ConsoleCommand(
sender: ConsoleCommandSender,
entry: Entry,
option: Option = Option(),
) : Command(sender, entry, option)
abstract class PlayerCommand(
sender: Player,
entry: Entry,
option: Option = Option(),
) : Command(sender, entry, option)
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/CommandLocalization.kt
================================================
package org.sorapointa.command
import moe.sdl.yac.core.*
import moe.sdl.yac.output.HelpFormatter
import moe.sdl.yac.output.Localization
import moe.sdl.yac.parameters.groups.ChoiceGroup
import moe.sdl.yac.parameters.groups.MutuallyExclusiveOptions
import org.sorapointa.CoreBundle
/** An object to let clikt use i18n */
object CommandLocalization : Localization {
/** [Abort] was thrown */
override fun aborted() = CoreBundle.message("clikt.aborted")
/** Prefix for any [UsageError] */
override fun usageError(message: String) = CoreBundle.message("clikt.usage.error", message)
/** Message for [BadParameterValue] */
override fun badParameter() = CoreBundle.message("clikt.bad.parameter")
/** Message for [BadParameterValue] */
override fun badParameterWithMessage(message: String) =
CoreBundle.message("clikt.bad.parameter.with.message", message)
/** Message for [BadParameterValue] */
override fun badParameterWithParam(paramName: String) =
CoreBundle.message("clikt.bad.parameter.with.param", paramName)
/** Message for [BadParameterValue] */
override fun badParameterWithMessageAndParam(paramName: String, message: String) =
CoreBundle.message("clikt.bad.parameter.with.message.param", paramName, message)
/** Message for [MissingOption] */
override fun missingOption(paramName: String) = CoreBundle.message("clikt.missing.option", paramName)
/** Message for [MissingArgument] */
override fun missingArgument(paramName: String) = CoreBundle.message("clikt.missing.argument", paramName)
/** Message for [NoSuchSubcommand] */
override fun noSuchSubcommand(name: String, possibilities: List<String>): String {
return CoreBundle.message("clikt.no.such.subcommand") + when (possibilities.size) {
0 -> ""
1 -> CoreBundle.message("clikt.no.such.subcommand.one", possibilities.first())
else -> possibilities.joinToString(
prefix = CoreBundle.message("clikt.no.such.subcommand.else.prefix"),
postfix = ")",
)
}
}
/** Message for [NoSuchOption] */
override fun noSuchOption(name: String, possibilities: List<String>): String {
return CoreBundle.message("clikt.no.such.option") + when (possibilities.size) {
0 -> ""
1 -> CoreBundle.message("clikt.no.such.option.one", possibilities.first())
else -> possibilities.joinToString(
prefix = CoreBundle.message("clikt.no.such.option.else.prefix"),
postfix = ")",
)
}
}
/**
* Message for [IncorrectOptionValueCount]
*
* @param count non-negative count of required values
*/
override fun incorrectOptionValueCount(name: String, count: Int): String {
return when (count) {
0 -> CoreBundle.message("clikt.incorrect.option.value.count.zero", name)
1 -> CoreBundle.message("clikt.incorrect.option.value.count.one", name)
else -> CoreBundle.message("clikt.incorrect.option.value.count.else", name, count)
}
}
/**
* Message for [IncorrectArgumentValueCount]
*
* @param count non-negative count of required values
*/
override fun incorrectArgumentValueCount(name: String, count: Int): String {
return when (count) {
0 -> CoreBundle.message("clikt.incorrect.argument.value.count.zero", name)
1 -> CoreBundle.message("clikt.incorrect.argument.value.count.one", name)
else -> CoreBundle.message("clikt.incorrect.argument.value.count.else", name, count)
}
}
/**
* Message for [MutuallyExclusiveGroupException]
*
* @param others non-empty list of other options in the group
*/
override fun mutexGroupException(name: String, others: List<String>): String {
return CoreBundle.message(
"clikt.mutex.group.exception",
name,
others.joinToString(
CoreBundle.message("clikt.mutex.group.exception.separator"),
),
)
}
/** Message for [FileNotFound] */
override fun fileNotFound(filename: String) = CoreBundle.message("clikt.file.not.found", filename)
/** Message for [InvalidFileFormat]*/
override fun invalidFileFormat(filename: String, message: String) =
CoreBundle.message("clikt.invalid.file.format", filename, message)
/** Message for [InvalidFileFormat]*/
override fun invalidFileFormat(filename: String, lineNumber: Int, message: String) =
CoreBundle.message("clikt.invalid.file.format.with.line.number", filename, lineNumber, message)
/** Error in message for [InvalidFileFormat] */
override fun unclosedQuote() = CoreBundle.message("clikt.unclosed.quote")
/** Error in message for [InvalidFileFormat] */
override fun fileEndsWithSlash() = CoreBundle.message("clikt.file.ends.with.slash")
/** One extra argument is present */
override fun extraArgumentOne(name: String) = CoreBundle.message("clikt.extra.argument.one")
/** More than one extra argument is present */
override fun extraArgumentMany(name: String, count: Int) = CoreBundle.message("clikt.extra.argument.many")
/** Error message when reading flag option from a file */
override fun invalidFlagValueInFile(name: String) =
CoreBundle.message("clikt.invalid.flag.value.in.file")
/** Error message when reading switch option from environment variable */
override fun switchOptionEnvvar() = CoreBundle.message("clikt.switch.option.envvar")
/** Required [MutuallyExclusiveOptions] was not provided */
override fun requiredMutexOption(options: String) = CoreBundle.message("clikt.required.mutex.option", options)
/**
* [ChoiceGroup] value was invalid
*
* @param choices non-empty list of possible choices
*/
override fun invalidGroupChoice(value: String, choices: List<String>): String {
return CoreBundle.message("clikt.invalid.group.choice", value, choices.joinToString())
}
/** Invalid value for a parameter of type [Double] or [Float] */
override fun floatConversionError(value: String) = CoreBundle.message("clikt.conversion.error.float", value)
/** Invalid value for a parameter of type [Int] or [Long] */
override fun intConversionError(value: String) = CoreBundle.message("clikt.conversion.error.int", value)
/** Invalid value for a parameter of type [Boolean] */
override fun boolConversionError(value: String) = CoreBundle.message("clikt.conversion.error.bool", value)
/** Invalid value falls outside range */
override fun rangeExceededMax(value: String, limit: String) =
CoreBundle.message("clikt.range.exceeded.max", value, limit)
/** Invalid value falls outside range */
override fun rangeExceededMin(value: String, limit: String) =
CoreBundle.message("clikt.range.exceeded.min", value, limit)
/** Invalid value falls outside range */
override fun rangeExceededBoth(value: String, min: String, max: String) =
CoreBundle.message("clikt.range.exceeded.both", value, min, max)
/**
* Invalid value for `choice` parameter
*
* @param choices non-empty list of possible choices
*/
override fun invalidChoice(choice: String, choices: List<String>): String =
CoreBundle.message("clikt.invalid.choice", choice, choices.joinToString())
/** The `pathType` parameter to [pathDoesNotExist] and other `path*` errors */
override fun pathTypeFile() = CoreBundle.message("clikt.path.type.file")
/** The `pathType` parameter to [pathDoesNotExist] and other `path*` errors */
override fun pathTypeDirectory() = CoreBundle.message("clikt.path.type.directory")
/** The `pathType` parameter to [pathDoesNotExist] and other `path*` errors */
override fun pathTypeOther() = CoreBundle.message("clikt.path.type.other")
/** Invalid path type */
override fun pathDoesNotExist(pathType: String, path: String) =
CoreBundle.message("clikt.path.does.not.exist", pathType, path)
/** Invalid path type */
override fun pathIsFile(pathType: String, path: String) = CoreBundle.message("clikt.path.is.file", pathType, path)
/** Invalid path type */
override fun pathIsDirectory(pathType: String, path: String) =
CoreBundle.message("clikt.path.is.directory", pathType, path)
/** Invalid path type */
override fun pathIsNotWritable(pathType: String, path: String) =
CoreBundle.message("clikt.path.is.not.writable", pathType, path)
/** Invalid path type */
override fun pathIsNotReadable(pathType: String, path: String) =
CoreBundle.message("clikt.path.is.not.readable", pathType, path)
/** Invalid path type */
override fun pathIsSymlink(pathType: String, path: String) =
CoreBundle.message("clikt.path.is.symlink", pathType, path)
/** Metavar used for options with unspecified value type */
override fun defaultMetavar() = CoreBundle.message("clikt.meta.var.default")
/** Metavar used for options that take [String] values */
override fun stringMetavar() = CoreBundle.message("clikt.meta.var.string")
/** Metavar used for options that take [Float] or [Double] values */
override fun floatMetavar() = CoreBundle.message("clikt.meta.var.float")
/** Metavar used for options that take [Int] or [Long] values */
override fun intMetavar() = CoreBundle.message("clikt.meta.var.int")
/** Metavar used for options that take `File` or `Path` values */
override fun pathMetavar() = CoreBundle.message("clikt.meta.var.path")
/** Metavar used for options that take `InputStream` or `OutputStream` values */
override fun fileMetavar() = CoreBundle.message("clikt.meta.var.file")
/** The title for the usage section of help output */
override fun usageTitle(): String = CoreBundle.message("clikt.title.usage")
/** The title for the options' section of help output */
override fun optionsTitle(): String = CoreBundle.message("clikt.title.options")
/** The title for the arguments' section of help output */
override fun argumentsTitle(): String = CoreBundle.message("clikt.title.arguments")
/** The title for the subcommands section of help output */
override fun commandsTitle(): String = CoreBundle.message("clikt.title.commands")
/** The that indicates where options may be present in the usage help output */
override fun optionsMetavar(): String = CoreBundle.message("clikt.meta.var.options")
/** The that indicates where subcommands may be present in the usage help output */
override fun commandMetavar(): String = CoreBundle.message("clikt.meta.var.command")
/** Text rendered for parameters tagged with [HelpFormatter.Tags.DEFAULT] */
override fun helpTagDefault(): String = CoreBundle.message("clikt.help.tag.default")
/** Text rendered for parameters tagged with [HelpFormatter.Tags.REQUIRED] */
override fun helpTagRequired(): String = CoreBundle.message("clikt.help.tag.required")
/** The default message for the `--help` option. */
override fun helpOptionMessage(): String = CoreBundle.message("clikt.help.option.message")
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/CommandManager.kt
================================================
package org.sorapointa.command
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import moe.sdl.yac.core.CommandResult
import moe.sdl.yac.core.CommandResult.Error
import moe.sdl.yac.core.CommandResult.Success
import moe.sdl.yac.core.PrintHelpMessage
import moe.sdl.yac.core.parseToArgs
import org.sorapointa.CoreBundle
import org.sorapointa.game.Player
import org.sorapointa.utils.ModuleScope
import org.sorapointa.utils.suggestTypo
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
private val logger = mu.KotlinLogging.logger {}
abstract class AbstractCommandNode<TSender : CommandSender>(
val entry: Command.Entry,
val creator: (sender: TSender) -> Command,
)
class CommandNode(
entry: Command.Entry,
creator: (CommandSender) -> Command,
) : AbstractCommandNode<CommandSender>(entry, creator)
class ConsoleCommandNode(
entry: Command.Entry,
creator: (ConsoleCommandSender) -> Command,
) : AbstractCommandNode<ConsoleCommandSender>(entry, creator)
class PlayerCommandNode(
entry: Command.Entry,
creator: (Player) -> Command,
) : AbstractCommandNode<Player>(entry, creator)
object CommandManager {
val commandMap: Map<String, AbstractCommandNode<*>>
get() = cmdMap
private val cmdMap: MutableMap<String, AbstractCommandNode<*>> = ConcurrentHashMap()
// A map to save the registered commands with alias.
private val aliasMap: MutableMap<String, AbstractCommandNode<*>> = ConcurrentHashMap()
val commandEntries: List<Command.Entry> get() = cmdMap.entries.map { it.value.entry }
private var commandScope = ModuleScope("CommandManager")
internal fun init(parentContext: CoroutineContext = EmptyCoroutineContext) {
commandScope = ModuleScope("CommandManager", parentContext)
}
@Suppress("unused")
fun registerCommand(entry: Command.Entry, creator: (CommandSender) -> Command) {
registerCommand(CommandNode(entry, creator))
}
@Suppress("MemberVisibilityCanBePrivate")
fun registerCommand(commandNode: AbstractCommandNode<*>) {
val name = commandNode.entry.name
val alias = commandNode.entry.alias
cmdMap.putIfAbsent(name, commandNode)?.also {
logger.warn { "Command name '$name' conflict." }
}
alias.forEach {
aliasMap.putIfAbsent(it, commandNode)?.also {
logger.warn { "Alias name '$alias' conflict." }
}
}
}
fun registerCommands(collection: Collection<AbstractCommandNode<*>>): Unit =
collection.forEach { registerCommand(it) }
fun invokeCommand(
sender: CommandSender,
rawMsg: String,
): Job = commandScope.launch {
if (rawMsg.isBlank()) {
sender.sendMessage(
CoreBundle.message("sora.cmd.manager.invoke.empty", locale = sender.locale),
)
return@launch
}
val args = rawMsg.parseToArgs()
val mainCommand = args[0]
val cmd = cmdMap[mainCommand] ?: aliasMap[mainCommand] ?: run {
val mainTypo = suggestTypo(mainCommand, cmdMap.keys.toList())
if (mainTypo == null) {
sender.sendMessage(
CoreBundle.message("sora.cmd.manager.invoke.error", mainCommand, locale = sender.locale),
)
} else {
sender.sendMessage(
CoreBundle.message(
"sora.cmd.manager.invoke.typo.suggest",
mainCommand,
mainTypo,
locale = sender.locale,
),
)
}
return@launch
}
val result: CommandResult = run {
if (sender is Player && sender.account.permissionLevel < cmd.entry.permissionRequired) {
return@run Error(
null,
userMessage = CoreBundle.message("sora.cmd.no.permission", locale = sender.locale),
)
}
when (cmd) {
is CommandNode -> {
cmd.creator(sender).execute(args)
}
is ConsoleCommandNode -> {
if (sender is ConsoleCommandSender) {
cmd.creator(sender).execute(args)
} else {
Error(null, userMessage = CoreBundle.message("sora.cmd.no.permission", locale = sender.locale))
}
}
is PlayerCommandNode -> {
if (sender is Player) {
cmd.creator(sender).execute(args)
} else {
Error(null, userMessage = CoreBundle.message("sora.cmd.is.not.player", locale = sender.locale))
}
}
else -> Error(null, userMessage = CoreBundle.message("server.error", locale = sender.locale))
}
}
when (result) {
is Error -> {
val msg = buildString {
append(result.userMessage)
if (result.cause is PrintHelpMessage && cmd.entry.alias.isNotEmpty()) {
append(
CoreBundle.message(
"sora.cmd.manager.alias",
cmd.entry.alias.joinToString(),
locale = sender.locale,
),
)
}
}
sender.sendMessage(msg)
}
is Success -> {
// pass
}
}
}
/**
* @param mainCommand main command or alias
* @return [Boolean] has or not
*/
fun hasCommand(mainCommand: String): Boolean =
cmdMap.containsKey(mainCommand) || aliasMap.containsKey(mainCommand)
}
private suspend fun Command.execute(args: List<String>) = this.main(args.drop(1))
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/CommandSender.kt
================================================
package org.sorapointa.command
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.PropertyKey
import org.sorapointa.BUNDLE
import org.sorapointa.CoreBundle
import org.sorapointa.utils.LocaleAble
import java.util.*
interface CommandSender : LocaleAble {
/**
* An abstract function to send a message to the sender.
*
* @param msg The message to be sent.
*/
suspend fun sendMessage(msg: String)
}
@Nls
internal suspend inline fun CommandSender.sendLocaled(
@PropertyKey(resourceBundle = BUNDLE) key: String,
vararg params: Any?,
) = sendMessage(CoreBundle.message(key, *params, locale = this.locale))
@Nls
@Suppress("NOTHING_TO_INLINE")
internal inline fun CommandSender.localed(
@PropertyKey(resourceBundle = BUNDLE) key: String,
vararg params: Any?,
): String = CoreBundle.message(key, *params, locale = this.locale)
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/ConsoleCommandSender.kt
================================================
package org.sorapointa.command
import io.ktor.server.websocket.*
import kotlinx.coroutines.isActive
import org.sorapointa.console.MessageNotify
import org.sorapointa.console.WebConsolePacket
import java.util.*
open class ConsoleCommandSender(
override val locale: Locale? = null,
) : CommandSender {
override suspend fun sendMessage(msg: String): Unit = println(msg)
}
class RemoteCommandSender(
private val session: DefaultWebSocketServerSession,
override val locale: Locale?,
) : ConsoleCommandSender() {
val isActive
get() = session.isActive
override suspend fun sendMessage(msg: String) {
session.sendSerialized<WebConsolePacket>(MessageNotify(msg))
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/Defaults.kt
================================================
package org.sorapointa.command.defaults
import org.sorapointa.command.AbstractCommandNode
import org.sorapointa.command.CommandNode
import org.sorapointa.command.ConsoleCommandNode
import org.sorapointa.command.defaults.console.ConsoleUser
import org.sorapointa.command.defaults.console.Quit
import org.sorapointa.command.defaults.general.Help
import org.sorapointa.command.defaults.general.ListPlayer
import org.sorapointa.command.defaults.general.LocaleCommand
import org.sorapointa.command.defaults.general.Version
val defaultsCommand: List<AbstractCommandNode<*>> = listOf(
CommandNode(Help) { sender -> Help(sender) },
CommandNode(ListPlayer) { sender -> ListPlayer(sender) },
CommandNode(LocaleCommand) { sender -> LocaleCommand(sender) },
CommandNode(Version) { sender -> Version(sender) },
ConsoleCommandNode(Quit) { sender -> Quit(sender) },
ConsoleCommandNode(ConsoleUser) { sender -> ConsoleUser(sender) },
)
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/console/ConsoleUser.kt
================================================
package org.sorapointa.command.defaults.console
import moe.sdl.yac.core.PrintMessage
import moe.sdl.yac.parameters.options.default
import moe.sdl.yac.parameters.options.option
import org.sorapointa.command.*
import org.sorapointa.command.utils.switchSet
import org.sorapointa.console.ConsoleUsers
class ConsoleUser(val sender: ConsoleCommandSender) : ConsoleCommand(
sender,
ConsoleUser,
Option(printHelpOnEmptyArgs = true),
) {
companion object : Entry(
name = "consoleuser",
helpKey = "sora.cmd.console.user.desc",
alias = listOf("cslusr"),
)
private val username by option(
names = arrayOf("--username", "-u"),
help = sender.localed("sora.cmd.console.user.opt.user"),
)
private val password by option(
names = arrayOf("--password", "--pwd", "-p"),
help = sender.localed("sora.cmd.console.user.opt.pwd"),
)
enum class Operation {
ADD, UPDATE, DELETE, LIST
}
private val operation by option(
help = sender.localed("sora.cmd.console.user.opt.operation"),
).switchSet(
setOf("--add", "-a") to Operation.ADD,
setOf("--update", "--upd", "-U") to Operation.UPDATE,
setOf("--delete", "--del", "-d") to Operation.DELETE,
setOf("--list", "--ls", "-l") to Operation.LIST,
).default(Operation.ADD)
private suspend fun addOrUpdate(username: String?) {
if (username == null) throw PrintMessage(CommandLocalization.missingOption("--username"))
val sender = this@ConsoleUser.sender
ConsoleUsers.addOrUpdate(
username,
password ?: run {
sender.sendLocaled("sora.cmd.console.user.msg.empty.pwd")
""
},
)
}
override suspend fun run() {
when (operation) {
Operation.UPDATE -> {
addOrUpdate(username)
sender.sendLocaled("sora.cmd.console.user.msg.success.update", username)
ConsoleUsers.save()
}
Operation.ADD -> {
if (ConsoleUsers.data.users.contains(username)) {
sender.sendLocaled("sora.cmd.console.user.msg.duplicate", username)
return
}
addOrUpdate(username)
sender.sendLocaled("sora.cmd.console.user.msg.success.add", username)
ConsoleUsers.save()
}
Operation.DELETE -> {
val removed = ConsoleUsers.data.users.remove(username) != null
if (removed) {
sender.sendLocaled("sora.cmd.console.user.msg.success.remove", username)
} else {
sender.sendLocaled("sora.cmd.console.user.msg.nosuch", username)
}
}
Operation.LIST -> {
val usrs = ConsoleUsers.data.users.keys
sender.sendLocaled(
"sora.cmd.console.user.msg.list",
usrs.size,
usrs.joinToString(),
)
}
}
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/console/Quit.kt
================================================
package org.sorapointa.command.defaults.console
import org.sorapointa.command.ConsoleCommand
import org.sorapointa.command.ConsoleCommandSender
import kotlin.system.exitProcess
class Quit(sender: ConsoleCommandSender) : ConsoleCommand(sender, Quit) {
companion object : Entry(
name = "quit",
helpKey = "sora.cmd.quit.desc",
alias = listOf("exit"),
)
override suspend fun run() {
exitProcess(0)
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/general/Help.kt
================================================
package org.sorapointa.command.defaults.general
import moe.sdl.yac.core.UsageError
import moe.sdl.yac.parameters.arguments.argument
import moe.sdl.yac.parameters.arguments.default
import moe.sdl.yac.parameters.options.convert
import moe.sdl.yac.parameters.options.default
import moe.sdl.yac.parameters.options.option
import moe.sdl.yac.parameters.types.int
import org.sorapointa.command.Command
import org.sorapointa.command.CommandManager
import org.sorapointa.command.CommandSender
import org.sorapointa.command.localed
class Help(private val sender: CommandSender) : Command(sender, Help) {
companion object : Entry(
name = "help",
helpKey = "sora.cmd.help.desc",
alias = listOf("?"),
)
private val pageNum by argument(
name = sender.localed("sora.cmd.help.arg.page.num.name"),
help = sender.localed("sora.cmd.help.arg.page.num.desc"),
).int().default(1)
private val pageSize by option(
"--page-size",
"-s",
help = sender.localed("sora.cmd.help.opt.page.size.desc"),
).int().convert {
it.coerceIn(1..50)
}.default(10)
override suspend fun run() {
val cmdList = CommandManager.commandEntries
// Take out the page items from command list.
val pageItems = cmdList.chunked(pageSize)
// Throw exception when the page number exceed.
if (pageNum !in 1..pageItems.size) {
throw UsageError(sender.localed("sora.cmd.help.arg.page.num.exceed"))
}
// Build the message and send to the sender
sender.sendMessage(
buildString {
appendLine(sender.localed("sora.cmd.help.msg.page", pageNum, pageItems.size))
val entries = pageItems[pageNum - 1]
val maxLen = entries.map { it.name }.maxOf { it.length }
entries.forEach {
append(it.name.padEnd(maxLen, ' '))
append(" >> ")
if (it.helpKey.isNotBlank()) {
append(sender.localed(it.helpKey))
} else {
append(sender.localed("sora.cmd.help.msg.empty.desc"))
}
appendLine()
}
append(sender.localed("sora.cmd.help.msg.more"))
},
)
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/general/ListPlayer.kt
================================================
package org.sorapointa.command.defaults.general
import org.sorapointa.Sorapointa
import org.sorapointa.command.Command
import org.sorapointa.command.CommandSender
import org.sorapointa.command.sendLocaled
class ListPlayer(private val sender: CommandSender) : Command(sender, ListPlayer) {
companion object : Entry(
name = "listplayer",
helpKey = "sora.cmd.list.player.desc",
alias = listOf("list"),
permissionRequired = 1,
)
override suspend fun run() {
val list = Sorapointa.getPlayerList()
sender.sendLocaled(
"sora.cmd.list.player.msg",
list.size,
list.joinToString { "${it.account.userName} (${it.basicComp.nickname}, UID:${it.uid})" },
)
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/general/LocaleCommand.kt
================================================
package org.sorapointa.command.defaults.general
import moe.sdl.yac.parameters.arguments.argument
import moe.sdl.yac.parameters.arguments.default
import moe.sdl.yac.parameters.arguments.optional
import moe.sdl.yac.parameters.options.flag
import moe.sdl.yac.parameters.options.option
import moe.sdl.yac.parameters.types.enum
import org.sorapointa.CoreBundle
import org.sorapointa.command.*
import org.sorapointa.game.Player
import org.sorapointa.utils.I18nConfig
import java.util.Locale as JavaLocale
class LocaleCommand(private val sender: CommandSender) : Command(sender, LocaleCommand) {
companion object : Entry(
name = "locale",
helpKey = "sora.cmd.locale.desc",
alias = listOf("lang"),
)
enum class Operation {
VIEW, SET, LIST,
}
private val defaultOp = Operation.VIEW
private val operation by argument(
CoreBundle.message("sora.cmd.locale.arg.operation.name", locale = sender.locale),
help = CoreBundle.message(
"sora.cmd.locale.arg.operation.desc",
Operation.values().joinToString { it.name },
defaultOp.name,
locale = sender.locale,
),
).enum<Operation>(ignoreCase = true).default(defaultOp)
private val newValue by argument(
sender.localed("sora.cmd.locale.arg.new.value.name"),
help = sender.localed("sora.cmd.locale.arg.new.value.desc"),
).optional()
private val force by option(
"--force",
"-F",
help = sender.localed("sora.cmd.locale.opt.force.desc"),
).flag(default = false)
private suspend fun sendLocaleInfo(locale: JavaLocale?) {
val langTag = locale?.toLanguageTag() ?: "NONE"
sender.sendLocaled("sora.cmd.locale.msg.view", langTag)
}
private suspend fun modifyLocaleInfo(modifyValue: suspend (JavaLocale) -> Unit) {
val newValue = this.newValue // For smart cast
if (newValue == null) {
sender.sendLocaled("sora.cmd.locale.msg.new.value.missing")
return
}
if (newValue.length > 20) {
sender.sendLocaled("sora.cmd.locale.msg.new.value.toolong", 20)
return
}
val locale = JavaLocale.forLanguageTag(newValue)
val found = CoreBundle.availableLocales().contains(locale)
if (found && !force) {
sender.sendLocaled("sora.cmd.locale.msg.new.value.notfound")
return
}
modifyValue(locale)
sender.sendLocaled("sora.cmd.locale.msg.success", locale.toLanguageTag())
}
private suspend inline fun sendAvailableList() =
sender.sendMessage(CoreBundle.availableLocales().joinToString())
override suspend fun run() {
when (sender) {
is Player -> when (operation) {
Operation.VIEW -> sendLocaleInfo(sender.locale)
Operation.SET -> modifyLocaleInfo { sender.locale = it }
Operation.LIST -> sendAvailableList()
}
is ConsoleCommandSender -> when (operation) {
Operation.VIEW -> sendLocaleInfo(I18nConfig.data.globalLocale)
Operation.SET -> modifyLocaleInfo {
I18nConfig.data.globalLocale = it
I18nConfig.save()
}
Operation.LIST -> sendAvailableList()
}
else -> sender.sendLocaled("sora.cmd.locale.msg.unsupported")
}
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/general/Version.kt
================================================
package org.sorapointa.command.defaults.general
import org.sorapointa.command.Command
import org.sorapointa.command.CommandSender
import org.sorapointa.config.BUILD_BRANCH
import org.sorapointa.config.COMMIT_HASH
import org.sorapointa.config.VERSION
class Version(private val sender: CommandSender) : Command(sender, Version) {
companion object : Entry(
name = "version",
helpKey = "sora.cmd.version.desc",
)
override suspend fun run() {
sender.sendMessage("Sorapointa v$VERSION-$BUILD_BRANCH+$COMMIT_HASH")
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/command/utils/Options.kt
================================================
package org.sorapointa.command.utils
import moe.sdl.yac.parameters.groups.ChoiceGroup
import moe.sdl.yac.parameters.groups.OptionGroup
import moe.sdl.yac.parameters.groups.groupSwitch
import moe.sdl.yac.parameters.options.FlagOption
import moe.sdl.yac.parameters.options.RawOption
import moe.sdl.yac.parameters.options.switch
/**
* Make a multikey pair to group option
*
* ### Example:
*
* ```
* option().switch(
* setOf("--foo", "-F") to Foo(),
* listOf("-b", "--bar") to Bar(),
* )
* ```
*/
fun <T : Any> RawOption.switchSet(vararg choices: Pair<Collection<String>, T>): FlagOption<T?> =
switch(
choices.flatMap { (keys, value) ->
keys.map { it to value }
}.toMap(),
)
/**
* Convert the option into a set of flags that each map to an option group.
* Make a multikey pair to option group
* ### Example:
*
* ```
* option().groupSwitch(
* setOf("--foo", "-F") to FooOptionGroup(),
* setOf("--bar", "-b") to BarOptionGroup()
* )
* ```
*/
fun <T : OptionGroup> RawOption.groupSwitchSet(vararg choices: Pair<Collection<String>, T>): ChoiceGroup<T, T?> =
groupSwitch(
choices.flatMap { (keys, value) ->
keys.map { it to value }
}.toMap(),
)
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/console/Completer.kt
================================================
@file:Suppress("unused")
package org.sorapointa.console
import moe.sdl.yac.core.CliktCommand
import moe.sdl.yac.parameters.arguments.Argument
import moe.sdl.yac.parameters.groups.ParameterGroup
import moe.sdl.yac.parameters.options.Option
import org.jline.builtins.Completers.TreeCompleter.Node
import org.jline.builtins.Completers.TreeCompleter.node
import org.sorapointa.utils.uncheckedCast
private val cliktClazz = CliktCommand::class.java
@Suppress("NOTHING_TO_INLINE")
private inline fun field(name: String) = cliktClazz.getDeclaredField(name).apply {
trySetAccessible()
} ?: throw NoSuchFieldException(name)
private val subcommands = field("_subcommands")
private val options = field("_options")
private val arguments = field("_arguments")
private val groups = field("_groups")
private fun CliktCommand.subcommands(): List<CliktCommand> = subcommands.get(this).uncheckedCast()
private fun CliktCommand.options(): List<Option> = options.get(this).uncheckedCast()
private fun CliktCommand.arguments(): List<Argument> = arguments.get(this).uncheckedCast()
private fun CliktCommand.groups(): List<ParameterGroup> = groups.get(this).uncheckedCast()
internal fun CliktCommand.toCompleterNodes(): List<Node> =
subcommands().map { node(it.commandName, it.toCompleterNodes()) } +
options().flatMap { it.names + it.secondaryNames }.map { node(it) } +
groups().mapNotNull { it.groupName }.map { node(it) }
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/console/Console.kt
================================================
package org.sorapointa.console
import io.ktor.util.collections.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.jline.builtins.Completers.TreeCompleter
import org.jline.builtins.Completers.TreeCompleter.node
import org.jline.reader.LineReader
import org.jline.reader.LineReaderBuilder
import org.jline.reader.impl.LineReaderImpl
import org.jline.reader.impl.history.DefaultHistory
import org.jline.terminal.Terminal
import org.jline.terminal.TerminalBuilder
import org.jline.widget.AutosuggestionWidgets
import org.sorapointa.command.*
import org.sorapointa.utils.*
import java.io.OutputStream
import java.io.PrintStream
import java.util.*
import kotlin.reflect.jvm.ExperimentalReflectionOnLambdas
import kotlin.reflect.jvm.reflect
import kotlin.reflect.typeOf
@Suppress("MemberVisibilityCanBePrivate")
internal object Console {
internal val terminal: Terminal = TerminalBuilder.terminal()
private var reader: LineReader? = null
private object FakeSender : ConsoleCommandSender() {
override suspend fun sendMessage(msg: String) {}
override val locale: Locale = Locale.ENGLISH
}
internal fun initReader() {
if (reader != null) return
reader = LineReaderBuilder.builder()
.appName("Sorapointa")
.terminal(terminal)
.highlighter(SoraHighlighter)
.build().apply {
AutosuggestionWidgets(this).enable()
initHistory()
}
}
@OptIn(ExperimentalReflectionOnLambdas::class)
internal fun setupCompletion() {
val completions by lazy {
CommandManager.commandMap.filter { (_, node) ->
val type = node.creator.reflect()?.parameters?.firstOrNull()?.type
type == typeOf<CommandSender>() || type == typeOf<ConsoleCommandSender>()
}.flatMap { (_, node) ->
val creator = node.creator.uncheckedCast<(sender: CommandSender) -> Command>()
val nodes = creator(FakeSender).toCompleterNodes()
buildList {
add(node.entry.name)
addAll(node.entry.alias)
}.map {
node(it, *nodes.toTypedArray())
}
}.let { TreeCompleter(it) }
}
(reader as? LineReaderImpl)?.completer = completions
}
private const val HISTORY_FILE = ".sorapointa_history"
private fun LineReader.initHistory() {
setVariable(
LineReader.HISTORY_FILE,
resolveHome(HISTORY_FILE)
?: resolveWorkDirectory(HISTORY_FILE),
)
DefaultHistory(this).apply {
addShutdownHook {
save()
}
}
}
private val scope = ModuleScope("RemoteConsole", dispatcher = Dispatchers.IO)
internal val consoleUsers = ConcurrentSet<RemoteCommandSender>()
fun readln(prompt: String = "> "): String = reader?.readLine(prompt) ?: error("Reader not prepared")
fun println(any: Any?) {
if (consoleUsers.isNotEmpty()) {
scope.launch {
consoleUsers.removeIf { !it.isActive }
consoleUsers.forEach {
it.sendMessage(any.toString())
}
}
}
reader?.printAbove(any.toString()) ?: kotlin.io.println(any)
}
@Suppress("NOTHING_TO_INLINE")
inline fun println(string: String?) = println(any = string)
internal fun redirectToJLine() {
if (System.out === JLineRedirector) return
System.setErr(JLineRedirector)
System.setOut(JLineRedirector)
}
internal fun redirectToNull() {
val out = PrintStream(OutputStream.nullOutputStream())
System.setOut(out)
System.setErr(out)
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/console/JLineRedirector.kt
================================================
package org.sorapointa.console
import java.io.PrintStream
import java.util.*
/**
* Work around, not a good implementation
*/
internal object JLineRedirector : PrintStream(nullOutputStream()) {
@Suppress("NOTHING_TO_INLINE")
private inline fun println0() = Console.println("")
@Suppress("NOTHING_TO_INLINE")
private inline fun println0(x: Any?) = Console.println(x)
override fun println(x: Any?) = println0(x)
override fun println(x: Boolean) = println0(x)
override fun println(x: Char) = println0(x)
override fun println(x: CharArray) = println0(x.contentToString())
override fun println(x: Double) = println0(x)
override fun println(x: Float) = println0(x)
override fun println(x: Int) = println0(x)
override fun println(x: Long) = println0(x)
override fun println(x: String?) = println0(x)
override fun printf(format: String, vararg args: Any?): PrintStream {
println0(String.format(format, args = args))
return this
}
override fun format(format: String, vararg args: Any?): PrintStream {
String.format(format, args = args)
return this
}
override fun format(l: Locale?, format: String, vararg args: Any?): PrintStream {
println0(String.format(l, format, args = args))
return this
}
override fun printf(l: Locale?, format: String, vararg args: Any?): PrintStream {
return super.printf(l, format, *args)
}
override fun println() = println0()
override fun append(csq: CharSequence?): PrintStream {
println0(csq)
return this
}
override fun append(csq: CharSequence?, start: Int, end: Int): PrintStream {
println0(csq)
return this
}
override fun writeBytes(buf: ByteArray?) = println0(
if (buf != null) {
println0(String(buf, Charsets.UTF_8))
} else {
"null"
},
)
override fun write(buf: ByteArray) {
println0(String(buf, Charsets.UTF_8))
}
override fun write(buf: ByteArray, off: Int, len: Int) {
println0(String(buf, Charsets.UTF_8))
}
// Below functions are single element print,
// JLine do not support print above of them, so direct print
override fun print(x: Any?) = kotlin.io.print(x)
override fun print(x: Boolean) = kotlin.io.print(x)
override fun print(x: Char) = kotlin.io.print(x)
override fun print(x: CharArray) = kotlin.io.print(x.joinToString())
override fun print(x: Double) = kotlin.io.print(x)
override fun print(x: Float) = kotlin.io.print(x)
override fun print(x: Int) = kotlin.io.print(x)
override fun print(x: Long) = kotlin.io.print(x)
override fun print(x: String?) = kotlin.io.print(x)
override fun append(c: Char): PrintStream {
kotlin.io.print(c)
return this
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/console/SoraHighlighter.kt
================================================
package org.sorapointa.console
import org.jline.reader.Highlighter
import org.jline.reader.LineReader
import org.jline.utils.AttributedString
import org.jline.utils.AttributedStringBuilder
import org.jline.utils.AttributedStyle
import org.jline.utils.AttributedStyle.*
import org.sorapointa.command.CommandManager
import java.util.regex.Pattern
object SoraHighlighter : Highlighter {
override fun highlight(reader: LineReader?, buffer: String?): AttributedString = runCatching {
if (buffer.isNullOrEmpty()) return AttributedString("")
if (buffer.isBlank()) return AttributedString(buffer)
val builder = AttributedStringBuilder()
val slices = buffer.splitsNoStrip().map { buffer.slice(it) }
var hasFirstNonBlank = false
slices.forEach {
val isFirstNonBlank = it.isNotBlank() && !hasFirstNonBlank
if (it.isNotBlank()) hasFirstNonBlank = true
val style: AttributedStyle = when {
isFirstNonBlank -> if (CommandManager.hasCommand(it)) {
DEFAULT.foreground(YELLOW)
} else {
BOLD.foreground(RED)
}
it.startsWith("\"") -> DEFAULT.foreground(YELLOW)
it.startsWith("-") -> DEFAULT.foreground(CYAN)
else -> DEFAULT
}
builder.append(it, style)
}
return builder.toAttributedString()
}.getOrElse { AttributedString(buffer ?: "") }
override fun setErrorPattern(errorPattern: Pattern?) =
throw UnsupportedOperationException("setErrorPattern is not unsupported")
override fun setErrorIndex(errorIndex: Int) =
throw UnsupportedOperationException("setErrorIndex is not unsupported")
}
private const val QUOTE = '"'
private const val SPLITTER = ' '
private const val ESCAPE = '\\'
private fun String.splitsNoStrip(): List<IntRange> {
if (this.isBlank()) return listOf(indices)
val slices = mutableListOf<IntRange>()
var idx = 0
var groupStart = 0
var inQuote = false
var inSplitter = false
var inNormal = false
while (idx < length) {
val escaped = this.getOrNull(idx - 1) == ESCAPE
val char = this[idx]
val isLast = idx == lastIndex
if (inNormal) {
when {
char == QUOTE || char == SPLITTER -> {
slices += groupStart until idx
inNormal = false
}
isLast -> {
slices += groupStart..idx
inNormal = false
}
}
}
if (char == QUOTE && !inQuote && isLast) {
slices += idx..idx
}
if (inQuote && isLast) {
slices += groupStart..idx
inQuote = false
}
if (!escaped) {
when (char) {
QUOTE -> {
if (inQuote) {
inQuote = false
slices += groupStart..idx
} else {
groupStart = idx
inQuote = true
}
}
SPLITTER -> {
if (!inQuote) {
val hasNext = this.getOrNull(idx + 1) == SPLITTER
when {
// single space
!inSplitter && !hasNext -> {
slices += idx..idx
}
!inSplitter /* && hasNext */ -> {
groupStart = idx
inSplitter = true
}
inSplitter && !hasNext -> {
inSplitter = false
slices += groupStart..idx
}
/* inSplitter && hasNext -> just ignore*/
}
}
}
}
}
if (char != QUOTE && char != SPLITTER) {
when {
!inQuote && !inNormal && this.getOrNull(idx - 1) == SPLITTER && isLast -> {
slices += idx..idx
}
!inQuote && !inNormal -> {
inNormal = true
groupStart = idx
}
}
}
idx++
}
return slices
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/console/WebSocketConsole.kt
================================================
package org.sorapointa.console
import com.password4j.Password
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.websocket.*
import io.ktor.serialization.kotlinx.*
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.server.websocket.WebSockets
import io.ktor.websocket.*
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import mu.KotlinLogging
import org.jline.reader.EndOfFileException
import org.jline.reader.UserInterruptException
import org.sorapointa.command.CommandManager
import org.sorapointa.command.RemoteCommandSender
import org.sorapointa.data.provider.DataFilePersist
import org.sorapointa.dispatch.data.argon2Function
import org.sorapointa.utils.ModuleScope
import org.sorapointa.utils.configDirectory
import org.sorapointa.utils.networkJson
import java.io.File
import java.security.cert.X509Certificate
import java.time.Duration
import java.util.concurrent.ConcurrentHashMap
import javax.net.ssl.X509TrustManager
import kotlin.collections.set
import kotlin.system.exitProcess
private val logger = KotlinLogging.logger {}
@Serializable
internal sealed class WebConsolePacket
/**
* @param username user to login
* @param password password
*/
@Serializable
internal class VerifyReq(val username: String, val password: String) : WebConsolePacket()
@Serializable
internal class VerifyResp(val ok: Boolean) : WebConsolePacket()
@Serializable
internal class CommandReq(val command: String) : WebConsolePacket()
@Serializable
internal object CommandEndResp : WebConsolePacket()
@Serializable
internal class MessageNotify(val message: String) : WebConsolePacket()
internal object ConsoleUsers : DataFilePersist<ConsoleUsers.Data>(
File(configDirectory, "consoleUsers.json"),
Data(),
Data.serializer(),
) {
@Serializable
data class Data(
val tokenLength: Int = 128,
val users: MutableMap<String, String> = ConcurrentHashMap<String, String>(),
)
override suspend fun init() = withContext(Dispatchers.IO) {
super.init()
save()
}
fun addOrUpdate(username: String, password: String) {
val user = username.lowercase()
data.users[user] = Password.hash(password).addSalt(user).with(argon2Function).result
}
fun verify(username: String, password: String): Boolean {
val user = username.lowercase()
val encrypted = data.users[user] ?: return false
return Password.check(password, encrypted).addSalt(user).with(argon2Function)
}
}
internal fun Application.setupWebConsoleServer() {
install(WebSockets) {
contentConverter = KotlinxWebsocketSerializationConverter(networkJson)
pingPeriod = Duration.ofSeconds(30)
timeout = Duration.ofSeconds(60)
}
routing {
webSocket("/webconsole") {
logger.info { "WebSocket Console Connected" }
val closedNotifyJob = launch {
val reason = closeReason.await()
logger.info { "Closed: $reason" }
}
val atomicVerified = atomic(false)
var verified by atomicVerified
val remoteSender = RemoteCommandSender(this, null)
launch {
delay(30_000)
if (!verified) {
close(CloseReason(CloseReason.Codes.NORMAL, "Client is not verified after 30 seconds"))
}
}
incoming.consumeEach { frame ->
try {
if (frame !is Frame.Text) {
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Only accept for Text data"))
logger.info { "WebSocketConsole closed because of illegal data type ${frame.frameType}" }
return@consumeEach
}
val pkt = runCatching {
val json = frame.readText()
logger.debug { "Received json: $json" }
networkJson.decodeFromString<WebConsolePacket>(json)
}.onFailure {
if (it is CancellationException) {
throw it
} else {
logger.info(it) { "Unexpected exception:" }
}
}.getOrNull() ?: return@consumeEach
when (pkt) {
is VerifyReq -> {
if (verified) {
sendSerialized<WebConsolePacket>(VerifyResp(true))
return@consumeEach
}
val pwd = pkt.password
val success = ConsoleUsers.verify(pkt.username, pwd)
verified = success
if (success) {
logger.info { "Successfully verified for user '${pkt.username}'" }
}
Console.consoleUsers.add(remoteSender)
sendSerialized<WebConsolePacket>(VerifyResp(success))
}
is CommandReq -> {
if (!verified) {
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Not verified"))
return@consumeEach
}
CommandManager.invokeCommand(remoteSender, pkt.command)
sendSerialized<WebConsolePacket>(CommandEndResp)
}
else ->
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Illegal Data Class"))
}
} catch (e: ClosedReceiveChannelException) {
logger.info(e) { "WebSocketConsole Closed by client" }
}
}
closedNotifyJob.join()
}
}
}
private val client by lazy {
HttpClient(CIO) {
install(io.ktor.client.plugins.websocket.WebSockets) {
contentConverter = KotlinxWebsocketSerializationConverter(networkJson)
}
engine {
https {
trustManager = object : X509TrustManager {
override fun checkClientTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
override fun checkServerTrusted(p0: Array<out X509Certificate>?, p1: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate>? = null
}
}
}
}
}
internal suspend fun setupConsoleClient(username: String, password: String, url: String) {
val scope = ModuleScope("ConsoleClient")
client.webSocket(url) {
logger.info { "Try connecting remote console $url..." }
sendSerialized<WebConsolePacket>(
VerifyReq(
username,
password,
),
)
val closedNotifyJob = scope.launch {
val reason = closeReason.await()
logger.info { "Closed: $reason" }
exitProcess(0)
}
val mutex = Mutex()
val incomingJob = scope.launch {
incoming.consumeEach {
val json = (it as? Frame.Text)?.readText() ?: return@consumeEach
logger.debug { "Received json: $json" }
when (val pkt = networkJson.decodeFromString<WebConsolePacket>(json)) {
is VerifyResp -> {
if (!pkt.ok) {
logger.error { "Server denied connection, maybe username or password error" }
exitProcess(0)
}
logger.info { "Successfully connected to remote!" }
}
is MessageNotify -> println(pkt.message)
is CommandEndResp -> mutex.unlock()
else -> {
// do nothing
}
}
}
}
val inputJob = scope.launch {
while (isActive) {
try {
mutex.lock()
sendSerialized<WebConsolePacket>(CommandReq(Console.readln("> ")))
} catch (e: CancellationException) {
throw e
} catch (e: UserInterruptException) {
println("<Interrupted> use Ctrl + D to exit client")
} catch (e: EndOfFileException) {
exitProcess(0)
}
}
}
listOf(closedNotifyJob, incomingJob, inputJob).joinAll()
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/events/PlayerEvent.kt
================================================
@file:Suppress("unused")
package org.sorapointa.events
import com.squareup.wire.Message
import com.squareup.wire.ProtoAdapter
import org.sorapointa.event.AbstractEvent
import org.sorapointa.event.CancelableEvent
import org.sorapointa.game.Player
import org.sorapointa.proto.PacketHead
import org.sorapointa.server.network.NetworkHandler
import org.sorapointa.server.network.OutgoingPacket
abstract class PlayerEvent : AbstractEvent() {
abstract val player: Player
}
// Login order: Init -> (FirstCreate) -> Login -> Disconnect
abstract class RecvPacketTriggerEvent : PlayerEvent() {
abstract val metadata: PacketHead?
fun sendPacket(packet: OutgoingPacket<*>) {
player.sendPacket(packet, metadata)
}
suspend fun sendPacketSync(packet: OutgoingPacket<*>) {
player.sendPacketSync(packet, metadata)
}
}
class PlayerInitEvent(
override val player: Player,
) : PlayerEvent()
class PlayerFirstCreateEvent(
override val player: Player,
override val metadata: PacketHead?,
val pickAvatarId: Int,
) : RecvPacketTriggerEvent()
class PlayerLoginEvent(
override val player: Player,
override val metadata: PacketHead? = null, // metadata from PlayerLoginReq
) : RecvPacketTriggerEvent()
class PlayerDisconnectEvent(
override val player: Player,
) : PlayerEvent()
internal abstract class NetworkEvent : AbstractEvent() {
abstract val networkHandler: NetworkHandler
}
class HandlePacketEvent<T : Message<*, *>>(
override val player: Player,
val dataPacket: T,
val metadata: PacketHead,
val adapter: ProtoAdapter<T>,
) : PlayerEvent(), CancelableEvent
internal class HandlePreLoginPacketEvent<T : Message<*, *>>(
override val networkHandler: NetworkHandler,
val dataPacket: T,
val metadata: PacketHead,
val adapter: ProtoAdapter<T>,
) : NetworkEvent(), CancelableEvent
internal class SendPacketEvent<T : Message<*, *>>(
override val networkHandler: NetworkHandler,
val dataPacket: OutgoingPacket<T>,
) : NetworkEvent(), CancelableEvent
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/game/AvatarEntity.kt
================================================
package org.sorapointa.game
import org.sorapointa.dataloader.common.ElementType
import org.sorapointa.dataloader.common.EntityIdType
import org.sorapointa.dataloader.common.FightProp.*
import org.sorapointa.dataloader.common.PlayerProp
import org.sorapointa.dataloader.def.*
import org.sorapointa.game.data.Position
import org.sorapointa.proto.*
import org.sorapointa.utils.*
import org.sorapointa.utils.encoding.bkdrHash
import kotlin.contracts.contract
interface AvatarEntity : SceneEntity {
val ownerPlayer: Player
val avatar: AbstractAvatar
val equipWeaponEntityId: Int?
get() = avatar.equipWeapon?.let {
ownerPlayer.getOrNextEntityId(EntityIdType.WEAPON, it.guid)
}
}
abstract class AbstractAvatarEntity : SceneEntityBase(), AvatarEntity {
internal abstract val avatarProto: AvatarProto
}
class AvatarEntityImpl(
override val ownerPlayer: Player,
override val avatar: AbstractAvatar,
) : AbstractAvatarEntity() {
override val id: Int by lazy {
ownerPlayer.getOrNextEntityId(EntityIdType.AVATAR, avatar.guid)
}
override val scene: Scene = ownerPlayer.scene
override val position: Position
get() = ownerPlayer.avatarComp.pbOnlyCurPos
override val entityType = ProtEntityType.PROT_ENTITY_TYPE_AVATAR
override val avatarProto: AvatarProto = AvatarProto(this)
override val entityProto: SceneEntityProto<*> = avatarProto
override fun toString(): String =
"Avatar[guid: ${avatar.guid}, position: $position]"
}
@Suppress("NOTHING_TO_INLINE")
inline fun Map<Int, PropValue>.toFlattenPropMap(): Map<Int, Long> =
map { it.key to it.value.val_ }.toMap()
internal class AvatarProto(
override val entity: AvatarEntity,
) : AbstractSceneEntityProto<AvatarEntity>() {
private val avatar = entity.avatar
private val excelData = avatar.excelData
private val selectedDepot
get() = avatar.depot.getSelectedDepot()
private val protoExcelInfo by lazy {
AvatarExcelInfo(
prefab_path_hash = excelData.prefabPathHash.toLong(),
prefab_path_remote_hash = excelData.prefabPathRemoteHash.toLong(),
controller_path_hash = excelData.controllerPathHash.toLong(),
controller_path_remote_hash = excelData.controllerPathRemoteHash.toLong(),
combat_config_hash = excelData.combatConfigHash.toLong(),
)
}
val protoPropMap
get() = buildList {
add(PlayerProp.PROP_LEVEL map avatar.level)
if (avatar is FormalAvatar) {
add(PlayerProp.PROP_EXP map avatar.exp)
}
add(PlayerProp.PROP_BREAK_LEVEL map avatar.promoteLevel)
add(PlayerProp.PROP_SATIATION_VAL mapFloat avatar.satiationVal)
add(PlayerProp.PROP_SATIATION_PENALTY_TIME mapFloat avatar.satiationPenaltyTime)
}.toMap()
private val protoSceneReliquaryInfoList
get() = avatar.equipList.mapNotNull { if (it is ReliquaryItem) it.toSceneReliquaryInfoProto() else null }
private val protoFightPropMap
get() = buildList {
add(FIGHT_PROP_BASE_HP map avatar.excelData.hpBase)
// add(FIGHT_PROP_HP map avatar.curHp)
// add(FIGHT_PROP_HP_PERCENT map entity.data.hpPercent)
add(FIGHT_PROP_BASE_ATTACK map avatar.excelData.attackBase)
// add(FIGHT_PROP_ATTACK map entity.data.attack)
// add(FIGHT_PROP_ATTACK_PERCENT map entity.data.attackPercent)
add(FIGHT_PROP_BASE_DEFENSE map avatar.excelData.defenseBase)
// add(FIGHT_PROP_DEFENSE map entity.data.defense)
// add(FIGHT_PROP_DEFENSE_PERCENT map entity.data.defensePercent)
// add(FIGHT_PROP_BASE_SPEED map entity.data.baseSpeed)
// add(FIGHT_PROP_SPEED_PERCENT map entity.data.speedPercent)
add(FIGHT_PROP_CRITICAL map avatar.excelData.critical)
// add(FIGHT_PROP_ANTI_CRITICAL map entity.data.antiCritical)
add(FIGHT_PROP_CRITICAL_HURT map avatar.excelData.criticalHurt)
// add(FIGHT_PROP_CHARGE_EFFICIENCY map entity.data.chargeEfficiency)
// add(FIGHT_PROP_ADD_HURT map entity.data.addHurt)
// add(FIGHT_PROP_SUB_HURT map entity.data.subHurt)
// add(FIGHT_PROP_HEAL_ADD map entity.data.healAdd)
// add(FIGHT_PROP_HEALED_ADD map entity.data.healedAdd)
// add(FIGHT_PROP_ELEMENT_MASTERY map entity.data.elementMastery)
// add(FIGHT_PROP_PHYSICAL_SUB_HURT map entity.data.physicalSubHurt)
// add(FIGHT_PROP_PHYSICAL_ADD_HURT map entity.data.physicalAddHurt)
// add(FIGHT_PROP_DEFENCE_IGNORE_RATIO map entity.data.defenceIgnoreRatio)
// add(FIGHT_PROP_DEFENCE_IGNORE_DELTA map entity.data.defenceIgnoreDelta)
// add(FIGHT_PROP_FIRE_ADD_HURT map entity.data.fireAddHurt)
// add(FIGHT_PROP_ELEC_ADD_HURT map entity.data.electricAddHurt)
// add(FIGHT_PROP_WATER_ADD_HURT map entity.data.waterAddHurt)
// add(FIGHT_PROP_GRASS_ADD_HURT map entity.data.grassAddHurt)
// add(FIGHT_PROP_WIND_ADD_HURT map entity.data.windAddHurt)
// add(FIGHT_PROP_ROCK_ADD_HURT map entity.data.rockAddHurt)
// add(FIGHT_PROP_ICE_ADD_HURT map entity.data.iceAddHurt)
// add(FIGHT_PROP_HIT_HEAD_ADD_HURT map entity.data.hitHeadAddHurt)
// add(FIGHT_PROP_FIRE_SUB_HURT map entity.data.fireSubHurt)
// add(FIGHT_PROP_ELEC_SUB_HURT map entity.data.electricSubHurt)
// add(FIGHT_PROP_WATER_SUB_HURT map entity.data.waterSubHurt)
// add(FIGHT_PROP_GRASS_SUB_HURT map entity.data.grassSubHurt)
// add(FIGHT_PROP_WIND_SUB_HURT map entity.data.windSubHurt)
// add(FIGHT_PROP_ROCK_SUB_HURT map entity.data.rockSubHurt)
// add(FIGHT_PROP_ICE_SUB_HURT map entity.data.iceSubHurt)
// add(FIGHT_PROP_SKILL_CD_MINUS_RATIO map entity.data.skillCDMinusRatio)
// add(FIGHT_PROP_SHIELD_COST_MINUS_RATIO map entity.data.shieldCostMinusRatio)
if (selectedDepot.elementType != ElementType.None) {
selectedDepot.maxEnergy?.let {
add(selectedDepot.elementType.maxEnergyProp map it)
}
add(selectedDepot.elementType.curEnergyProp map avatar.curElemEnergy)
}
// Must load maxHp before curHp to make sure curHp <= maxHp
add(FIGHT_PROP_MAX_HP map entity.maxHp)
add(FIGHT_PROP_CUR_HP map entity.curHp)
add(FIGHT_PROP_CUR_ATTACK map entity.curAttack)
add(FIGHT_PROP_CUR_DEFENSE map entity.curDefense)
FIGHT_PROP_CUR_SPEED map entity.curSpeed
}.toMap().filter { it.value != 0f }
override val fightPropPairList: List<FightPropPair>
get() = protoFightPropMap.map { it.key fightProp it.value } // crazy hoyo
fun toAvatarInfoProto(): AvatarInfo = AvatarInfo(
avatar_id = avatar.avatarId,
guid = avatar.guid,
prop_map = protoPropMap,
life_state = avatar.lifeState,
equip_guid_list = avatar.equipGuidList,
talent_id_list = selectedDepot.talentList,
fight_prop_map = protoFightPropMap,
skill_map = selectedDepot.getProtoSkillMap(),
skill_depot_id = avatar.depot.selectedDepotId,
// TODO: Fetter system
// Official server packet doesn't have `coreProudSkillLevel` field
core_proud_skill_level = selectedDepot.coreProudSkillLevel,
inherent_proud_skill_list = selectedDepot.inherentProudSkillList,
skill_level_map = selectedDepot.getSkillLevelMap(),
// TODO: Proud skill extra level map
// TODO: isFocus is whether one of teammate of selected team
avatar_type = avatar.avatarType.value,
// TODO: Team resonance
wearing_flycloak_id = avatar.flyCloakId.value,
// Still don't know what is `equip_affix_list` in this packet
// It seems related to weapon passive skill and cd time
// 比如西风和祭礼系列武器的被动冷却时间 - 持久化数据库存储,就像 protoSkillMap
born_time = avatar.bornTime,
// TODO: `pending_promote_reward_list`
costume_id = avatar.costumeId,
excel_info = protoExcelInfo,
)
override fun SceneEntityInfo.toProto(): SceneEntityInfo {
val avatarProto = getSceneAvatarInfo()
return copy(avatar = avatarProto)
}
fun getSceneAvatarInfo(): SceneAvatarInfo {
val weapon = if (avatar.equipWeapon != null && entity.equipWeaponEntityId != null) {
avatar.equipWeapon!!.toSceneWeaponInfoProto(entity.equipWeaponEntityId!!)
} else {
null
}
return SceneAvatarInfo(
uid = entity.ownerPlayer.uid,
avatar_id = avatar.avatarId,
guid = avatar.guid,
peer_id = entity.ownerPlayer.peerId,
equip_id_list = avatar.equipIdList,
skill_depot_id = avatar.depot.selectedDepotId,
talent_id_list = selectedDepot.talentList,
weapon = weapon,
reliquary_list = protoSceneReliquaryInfoList,
core_proud_skill_level = selectedDepot.coreProudSkillLevel,
inherent_proud_skill_list = selectedDepot.inherentProudSkillList,
skill_level_map = selectedDepot.getSkillLevelMap(),
// TODO: Proud skill extra level map
// TODO: Server Buff List
// TODO: Team resonance
wearing_flycloak_id = avatar.flyCloakId.value,
born_time = avatar.bornTime,
// TODO: `pending_promote_reward_list`
costume_id = avatar.costumeId,
// curVehicleInfo
excel_info = protoExcelInfo,
// animHash
)
}
fun getAbilityControlBlock(): AbilityControlBlock {
// TODO: hardcode
if (avatar.avatarId == 10000005) {
val abilities = listOf(
"",
"Avatar_PlayerBoy_NormalAttack_DamageHandler",
"Avatar_Player_FlyingBomber",
"Avatar_Player_CamCtrl",
"Avatar_PlayerBoy_FallingAnthem",
"GrapplingHookSkill_Ability",
"Avatar_DefaultAbility_VisionReplaceDieInvincible",
"Avatar_DefaultAbility_AvartarInShaderChange",
"Avatar_SprintBS_Invincible",
"Avatar_Freeze_Duration_Reducer",
"Avatar_Attack_ReviveEnergy",
"Avatar_Component_Initializer",
"Avatar_HDMesh_Controller",
"Avatar_Trampoline_Jump_Controller",
"Avatar_PlayerBoy_ExtraAttack_Common",
"Avatar_FallAnthem_Achievement_Listener",
)
return AbilityControlBlock(
ability_embryo_list = abilities.map {
AbilityEmbryo(
ability_id = abilities.indexOf(it),
ability_name_hash = bkdrHash(it),
ability_override_name_hash = 1178079449,
)
},
)
} else if (avatar.avatarId == 10000007) {
val abilities = listOf(
"",
"Avatar_PlayerGirl_NormalAttack_DamageHandler",
"Avatar_Player_FlyingBomber",
"Avatar_Player_CamCtrl",
"Avatar_PlayerGirl_FallingAnthem",
"GrapplingHookSkill_Ability",
"Avatar_DefaultAbility_VisionReplaceDieInvincible",
"Avatar_DefaultAbility_AvartarInShaderChange",
"Avatar_SprintBS_Invincible",
"Avatar_Freeze_Duration_Reducer",
"Avatar_Attack_ReviveEnergy",
"Avatar_Component_Initializer",
"Avatar_HDMesh_Controller",
"Avatar_Trampoline_Jump_Controller",
"Avatar_PlayerGirl_ExtraAttack_Common",
"Avatar_FallAnthem_Achievement_Listener",
)
return AbilityControlBlock(
ability_embryo_list = abilities.map {
AbilityEmbryo(
ability_id = abilities.indexOf(it),
ability_name_hash = bkdrHash(it),
ability_override_name_hash = 1178079449,
)
},
)
}
return AbilityControlBlock()
}
}
@Suppress("NOTHING_TO_INLINE")
internal inline fun AvatarEntity.impl(): AbstractAvatarEntity {
contract { returns() implies (this@impl is AbstractAvatarEntity) }
check(this is AbstractAvatarEntity) {
"A Avatar instance is not instance of AbstractAvatar. Your instance: ${this::class.qualifiedOrSimple}"
}
return this
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/game/Player.kt
================================================
package org.sorapointa.game
import com.squareup.wire.Message
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.Job
import mu.KotlinLogging
import org.sorapointa.Sorapointa
import org.sorapointa.command.CommandSender
import org.sorapointa.dataloader.common.EntityIdType
import org.sorapointa.dataloader.common.PlayerProp
import org.sorapointa.dispatch.data.Account
import org.sorapointa.event.*
import org.sorapointa.events.PlayerDisconnectEvent
import org.sorapointa.events.PlayerEvent
import org.sorapointa.events.PlayerInitEvent
import org.sorapointa.events.PlayerLoginEvent
import org.sorapointa.game.data.PlayerData
import org.sorapointa.proto.MpSettingType
import org.sorapointa.proto.PacketHead
import org.sorapointa.proto.SoraPacket
import org.sorapointa.proto.bin.PlayerDataBin
import org.sorapointa.server.network.*
import org.sorapointa.utils.*
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
import kotlin.contracts.contract
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
private val logger = KotlinLogging.logger {}
interface Player : CommandSender {
override var locale: Locale?
val state: StateController<PlayerStateI.State, PlayerStateI, Player>
val account: Account
val uid: Int
val scene: Scene
val world: World
val playerProto: PlayerProto
val basicComp: PlayerBasicComp
val avatarComp: PlayerAvatarComp
val itemComp: PlayerItemComp
val sceneComp: PlayerSceneComp
val socialComp: PlayerSocialComp
val guidEntityMap: MutableMap<Long, Int> // Guid -> EntityId
val peerId: Int
val enterSceneToken: Int
val isMpModeAvailable: Boolean
suspend fun init()
suspend fun close()
fun hasLoadedScene(id: Int, loadNow: Boolean = true): Boolean
fun getNextEnterSceneToken(set: Boolean = true): Int
fun getNextEntityId(idType: EntityIdType): Int
fun getOrNextEntityId(idType: EntityIdType, guid: Long): Int
fun <T : Message<*, *>> sendPacket(
packet: OutgoingPacket<T>,
metadata: PacketHead? = null,
): Job
suspend fun <T : Message<*, *>> sendPacketSync(
packet: OutgoingPacket<T>,
metadata: PacketHead? = null,
)
fun forwardHandlePacket(packet: SoraPacket): Job
suspend fun saveData()
}
interface PlayerStateI : WithState<PlayerStateI.State> {
enum class State {
LOGIN,
OK,
CLOSED,
}
}
class PlayerImpl internal constructor(
override val account: Account,
private val data: PlayerData,
initDataBin: PlayerDataBin,
internal val networkHandler: NetworkHandler,
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
) : Player {
override val uid: Int = account.id.value
override var locale: Locale? = data.locale
private val scope = ModuleScope(toString(), parentCoroutineContext)
override val state by lazy {
StateController<PlayerStateI.State, PlayerStateI, Player>(
scope = scope,
parentStateClass = this,
Login(this),
)
}
override val basicComp by lazy {
PlayerBasicComp(this, initDataBin.basic_bin ?: error("PlayerBasicComp is null"))
}
override val avatarComp by lazy {
PlayerAvatarComp(this, initDataBin.avatar_bin ?: error("PlayerAvatarComp is null"))
}
override val itemComp by lazy {
PlayerItemComp(this, initDataBin.item_bin ?: error("PlayerItemComp is null"))
}
override val sceneComp by lazy {
PlayerSceneComp(this, initDataBin.scene_bin ?: error("PlayerSceneComp is null"))
}
override val socialComp by lazy {
PlayerSocialComp(this, initDataBin.social_bin ?: error("PlayerSocialComp is null"))
}
override val scene: Scene
get() = _scene.value
private val _scene by lazy {
atomic(SceneImpl(this, sceneComp.myCurSceneId))
}
override val world by lazy {
WorldImpl(this, scene)
}
override val playerProto by lazy {
PlayerProto(this)
}
override val guidEntityMap: MutableMap<Long, Int> = ConcurrentHashMap()
override val peerId: Int
get() = scene.playerMap[this] ?: error("Could not find player peer id from scene $scene")
private val _enterSceneToken = atomic(getNextEnterSceneToken(false))
override val enterSceneToken: Int
get() = _enterSceneToken.value
private val loadedSceneList = ConcurrentLinkedDeque<Int>()
private val _isMpModeAvailable = atomic(true)
override val isMpModeAvailable: Boolean
get() = _isMpModeAvailable.value
override fun hasLoadedScene(id: Int, loadNow: Boolean): Boolean =
loadedSceneList.contains(id).also {
if (!it && loadNow) loadedSceneList.add(id)
}
override fun getNextEnterSceneToken(set: Boolean): Int =
(1000..99999).random().also {
if (set) _enterSceneToken.value = it
}
override fun getNextEntityId(idType: EntityIdType): Int =
scene.owner.world.getNextEntityId(idType)
override fun getOrNextEntityId(idType: EntityIdType, guid: Long): Int =
guidEntityMap[guid] ?: getNextEntityId(idType).also { entityId ->
guidEntityMap[guid] = entityId
}
override suspend fun close() {
networkHandler.close()
state.setState(Closed(this))
scope.dispose()
}
override suspend fun init() {
logger.info { toString() + " has joined to the server" }
state.init()
networkHandler.state.observeStateChange { _, state ->
if (state == NetworkHandlerStateI.State.CLOSED) {
close()
}
}
basicComp.init()
avatarComp.init()
itemComp.init()
sceneComp.init()
socialComp.init()
scene.init()
data.player = this
PlayerInitEvent(this).broadcast()
}
override suspend fun sendMessage(msg: String) {
}
override fun <T : Message<*, *>> sendPacket(
packet: OutgoingPacket<T>,
metadata: PacketHead?,
): Job = networkHandler.sendPacket(packet, metadata)
override suspend fun <T : Message<*, *>> sendPacketSync(
packet: OutgoingPacket<T>,
metadata: PacketHead?,
) = networkHandler.sendPacketSync(packet, metadata)
override fun forwardHandlePacket(
packet: SoraPacket,
): Job = networkHandler.handlePacket(packet)
override suspend fun saveData() {
data.save()
}
inner class Login(private val player: PlayerImpl) : PlayerStateI {
override val state: PlayerStateI.State = PlayerStateI.State.LOGIN
internal suspend fun onLogin(metadata: PacketHead? = null) {
PlayerLoginEvent(player, metadata).broadcast()
basicComp.updateLastLoginTime()
}
}
inner class OK : PlayerStateI {
override val state: PlayerStateI.State = PlayerStateI.State.OK
}
inner class Closed(private val player: PlayerImpl) : PlayerStateI {
override val state: PlayerStateI.State = PlayerStateI.State.CLOSED
override suspend fun startState() {
// onClosed
player.data.save()
logger.info { toString() + " has disconnected to the server" }
PlayerDisconnectEvent(player).broadcast()
Sorapointa.removePlayer(player.uid)
basicComp.updateLastLoginTime()
}
}
override fun toString(): String =
"Player[id: $uid, host: ${networkHandler.host}]"
}
class PlayerProto(
private val player: Player,
) {
val propMap
get() = mapOf(
PlayerProp.PROP_MAX_SPRING_VOLUME map 100, // TODO: hardcode
PlayerProp.PROP_CUR_SPRING_VOLUME map player.avatarComp.curSpringVolume,
PlayerProp.PROP_IS_SPRING_AUTO_USE map player.avatarComp.isSpringAutoUse.toInt(),
PlayerProp.PROP_SPRING_AUTO_USE_PERCENT map player.avatarComp.springAutoUsePercent,
PlayerProp.PROP_IS_FLYABLE map player.avatarComp.isFlyable.toInt(),
PlayerProp.PROP_IS_TRANSFERABLE map player.avatarComp.isTransferable.toInt(),
PlayerProp.PROP_MAX_STAMINA mapFloat player.basicComp.persistStaminaLimit,
PlayerProp.PROP_CUR_PERSIST_STAMINA mapFloat player.basicComp.curPersistStamina,
PlayerProp.PROP_CUR_TEMPORARY_STAMINA mapFloat player.basicComp.curTemporaryStamina,
PlayerProp.PROP_PLAYER_LEVEL map player.basicComp.level,
PlayerProp.PROP_EXP map player.basicComp.exp,
PlayerProp.PROP_PLAYER_HCOIN map player.itemComp.primoGem,
PlayerProp.PROP_PLAYER_SCOIN map player.itemComp.mora,
PlayerProp.PROP_PLAYER_MP_SETTING_TYPE map
MpSettingType.MP_SETTING_TYPE_ENTER_AFTER_APPLY.value, // TODO: hardcode
PlayerProp.PROP_IS_MP_MODE_AVAILABLE map 1, // TODO: hardcode
PlayerProp.PROP_PLAYER_WORLD_LEVEL map player.sceneComp.world.level,
PlayerProp.PROP_PLAYER_RESIN map 160, // TODO: hardcode
PlayerProp.PROP_PLAYER_MCOIN map player.itemComp.genesisCrystal,
PlayerProp.PROP_PLAYER_LEGENDARY_KEY map player.itemComp.legendaryKey,
PlayerProp.PROP_IS_HAS_FIRST_SHARE map player.socialComp.isHaveFirstShare.toInt(),
PlayerProp.PROP_PLAYER_FORGE_POINT map 0, // TODO: hardcode
PlayerProp.PROP_PLAYER_WORLD_LEVEL_ADJUST_CD map 0, // TODO: hardcode
PlayerProp.PROP_PLAYER_LEGENDARY_DAILY_TASK_NUM map 0, // TODO: hardcode
PlayerProp.PROP_PLAYER_HOME_COIN map player.itemComp.homeCoin,
)
}
@Suppress("NOTHING_TO_INLINE")
internal inline fun Player.impl(): PlayerImpl {
contract { returns() implies (this@impl is PlayerImpl) }
check(this is PlayerImpl) {
"A Player instance is not instance of PlayerImpl. Your instance: ${this::class.qualifiedOrSimple}"
}
return this
}
inline fun <reified T : PlayerEvent> Player.registerEventListener(
noinline listener: suspend T.() -> Unit,
) {
registerListener<T> {
if (it.player == this) {
listener(it)
}
}
}
inline fun <reified T : PlayerEvent> Player.registerEventListener(
priority: EventPriority,
noinline listener: suspend T.() -> Unit,
) {
registerListener<T>(priority) {
if (it.player == this) {
listener(it)
}
}
}
inline fun <reified T : PlayerEvent> Player.registerEventBlockListener(
noinline listener: suspend T.() -> Unit,
) {
registerBlockListener<T> {
if (it.player == this) {
listener(it)
}
}
}
inline fun <reified T : PlayerEvent> Player.registerEventBlockListener(
priority: EventPriority,
noinline listener: suspend T.() -> Unit,
) {
registerBlockListener<T>(priority) {
if (it.player == this) {
listener(it)
}
}
}
================================================
FILE: sorapointa-core/src/main/kotlin/org/sorapointa/game/PlayerAvatarComp.kt
================================================
package org.sorapointa.game
import kotlinx.atomicfu.atomic
import org.sorapointa.dataloader.common.*
import org.sorapointa.dataloader.def.*
import org.sorapointa.events.PlayerFirstCreateEvent
import org.sorapointa.events.PlayerLoginEvent
import org.sorapointa.game.data.Position
import org.sorapointa.game.data.START_POSITION
import org.sorapointa.proto.AvatarSkillInfo
import org.sorapointa.proto.AvatarTeam
import org.sorapointa.proto.SceneTeamAvatar
import org.sorapointa.proto.bin.*
import org.sorapointa.proto.bin.AvatarType
import org.sorapointa.server.network.AvatarDataNotifyPacket
import org.sorapointa.server.network.OpenStateUpdateNotifyPacket
import org.sorapointa.utils.buildConcurrencyMap
import org.sorapointa.utils.nowSeconds
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
@Suppress("MemberVisibilityCanBePrivate")
class PlayerAvatarComp(
override val player: Player,
private val initAvatarCompBin: PlayerAvatarCompBin,
) : PlayerModule {
private val avatarMap = buildConcurrencyMap(
initCapacity = initAvatarCompBin.avatar_list.size,
) {
initAvatarCompBin.avatar_list.forEach {
gitextract_y0piscv0/
├── .editorconfig
├── .git-hooks/
│ ├── commit-msg
│ └── pre-commit
├── .gitattributes
├── .github/
│ └── workflows/
│ ├── api_check.yml
│ └── test.yml
├── .gitignore
├── .idea/
│ └── encodings.xml
├── LICENSE
├── build.gradle.kts
├── buildSrc/
│ ├── build.gradle.kts
│ ├── settings.gradle.kts
│ └── src/
│ └── main/
│ └── kotlin/
│ ├── BuildConfigExtension.kt
│ ├── GitHook.kt
│ ├── JniHeader.kt
│ ├── OptInAnnotations.kt
│ ├── Properties.kt
│ ├── ResourcesCopy.kt
│ ├── Test.kt
│ ├── sorapointa-conventions.gradle.kts
│ └── sorapointa-publish.gradle.kts
├── docs/
│ ├── CONTRIBUTING.md
│ ├── CONTRIBUTING.zh-CN.md
│ ├── README.md
│ ├── README.zh-CN.md
│ └── guides/
│ ├── concurrency.md
│ ├── concurrency.zh-CN.md
│ ├── database.md
│ ├── database.zh-CN.md
│ ├── kotlin-atomicfu.md
│ ├── kotlin-atomicfu.zh-CN.md
│ ├── unit-test.md
│ └── unit-test.zh-CN.md
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── renovate.json
├── settings.gradle.kts
├── sorapointa-core/
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── kotlin/
│ │ │ └── org/
│ │ │ └── sorapointa/
│ │ │ ├── CoreBundle.kt
│ │ │ ├── Main.kt
│ │ │ ├── Sorapointa.kt
│ │ │ ├── SorapointaConfig.kt
│ │ │ ├── command/
│ │ │ │ ├── Command.kt
│ │ │ │ ├── CommandLocalization.kt
│ │ │ │ ├── CommandManager.kt
│ │ │ │ ├── CommandSender.kt
│ │ │ │ ├── ConsoleCommandSender.kt
│ │ │ │ ├── defaults/
│ │ │ │ │ ├── Defaults.kt
│ │ │ │ │ ├── console/
│ │ │ │ │ │ ├── ConsoleUser.kt
│ │ │ │ │ │ └── Quit.kt
│ │ │ │ │ └── general/
│ │ │ │ │ ├── Help.kt
│ │ │ │ │ ├── ListPlayer.kt
│ │ │ │ │ ├── LocaleCommand.kt
│ │ │ │ │ └── Version.kt
│ │ │ │ └── utils/
│ │ │ │ └── Options.kt
│ │ │ ├── console/
│ │ │ │ ├── Completer.kt
│ │ │ │ ├── Console.kt
│ │ │ │ ├── JLineRedirector.kt
│ │ │ │ ├── SoraHighlighter.kt
│ │ │ │ └── WebSocketConsole.kt
│ │ │ ├── events/
│ │ │ │ └── PlayerEvent.kt
│ │ │ ├── game/
│ │ │ │ ├── AvatarEntity.kt
│ │ │ │ ├── Player.kt
│ │ │ │ ├── PlayerAvatarComp.kt
│ │ │ │ ├── PlayerComp.kt
│ │ │ │ ├── PlayerItemComp.kt
│ │ │ │ ├── Scene.kt
│ │ │ │ ├── SceneEntity.kt
│ │ │ │ ├── World.kt
│ │ │ │ └── data/
│ │ │ │ ├── GameConstants.kt
│ │ │ │ ├── PlayerData.kt
│ │ │ │ ├── Position.kt
│ │ │ │ └── SorapointaStoreEntry.kt
│ │ │ ├── server/
│ │ │ │ ├── ServerNetwork.kt
│ │ │ │ └── network/
│ │ │ │ ├── NetworkHandler.kt
│ │ │ │ ├── OutgoingPacket.kt
│ │ │ │ ├── PacketHandler.kt
│ │ │ │ ├── PacketHandlerImpl.kt
│ │ │ │ └── SoraPacket.kt
│ │ │ └── utils/
│ │ │ ├── Console.kt
│ │ │ ├── GameUtils.kt
│ │ │ ├── NetworkUtils.kt
│ │ │ ├── OptionalContainer.kt
│ │ │ ├── PropDelegate.kt
│ │ │ └── TypoSuggestor.kt
│ │ └── resources/
│ │ ├── logback.xml
│ │ └── messages/
│ │ ├── CoreBundle.properties
│ │ └── CoreBundle_zh_CN.properties
│ └── test/
│ ├── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ ├── command/
│ │ │ └── defaults/
│ │ │ └── HelpTest.kt
│ │ └── logger/
│ │ └── LogTest.kt
│ └── resources/
│ └── logback-test.xml
├── sorapointa-crypto/
│ ├── build.gradle.kts
│ └── src/
│ └── main/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── crypto/
│ └── Crypto.kt
├── sorapointa-dataloader/
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── dataloader/
│ │ ├── DataLoader.kt
│ │ ├── common/
│ │ │ ├── AddProp.kt
│ │ │ ├── CurveInfo.kt
│ │ │ ├── Enum.kt
│ │ │ ├── ItemParamData.kt
│ │ │ ├── ItemParamStringData.kt
│ │ │ ├── OpenCondData.kt
│ │ │ ├── PointData.kt
│ │ │ ├── PropGrowCurve.kt
│ │ │ ├── RewardItemData.kt
│ │ │ └── ScenePointConfig.kt
│ │ └── def/
│ │ ├── AvatarExcelData.kt
│ │ ├── AvatarSkillData.kt
│ │ ├── AvatarSkillDepotData.kt
│ │ ├── MaterialData.kt
│ │ ├── ReliquaryAffixData.kt
│ │ ├── ReliquaryData.kt
│ │ ├── ReliquaryLevelData.kt
│ │ ├── ReliquaryMainPropData.kt
│ │ ├── ReliquarySetData.kt
│ │ ├── SceneData.kt
│ │ └── WeaponData.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── dataloader/
│ └── DataLoaderTest.kt
├── sorapointa-dataprovider/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── data/
│ │ └── provider/
│ │ ├── AutoLoadFilePersist.kt
│ │ ├── AutoSaveFilePersist.kt
│ │ ├── DataFilePersist.kt
│ │ ├── DatabaseConfig.kt
│ │ ├── DatabaseManager.kt
│ │ ├── FilePersist.kt
│ │ └── sql/
│ │ ├── SQLJson.kt
│ │ ├── SQLMap.kt
│ │ └── SQLSet.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── data/
│ └── provider/
│ ├── DatabaseProviderTest.kt
│ ├── FileProviderTest.kt
│ └── Init.kt
├── sorapointa-dispatch/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── kotlin/
│ │ │ └── org/
│ │ │ └── sorapointa/
│ │ │ └── dispatch/
│ │ │ ├── DispatchBundle.kt
│ │ │ ├── DispatchServer.kt
│ │ │ ├── data/
│ │ │ │ ├── AccountData.kt
│ │ │ │ ├── DispatchData.kt
│ │ │ │ ├── DispatchKeyData.kt
│ │ │ │ └── SwitchData.kt
│ │ │ ├── events/
│ │ │ │ └── DispatchEvent.kt
│ │ │ ├── plugins/
│ │ │ │ ├── HTTP.kt
│ │ │ │ ├── Monitoring.kt
│ │ │ │ ├── RouteHandler.kt
│ │ │ │ ├── Routing.kt
│ │ │ │ ├── Serialization.kt
│ │ │ │ └── StatusPage.kt
│ │ │ └── utils/
│ │ │ ├── CertBuilder.kt
│ │ │ ├── Certificates.kt
│ │ │ ├── KeyProvider.kt
│ │ │ └── Route.kt
│ │ └── resources/
│ │ └── messages/
│ │ ├── DispatchBundle.properties
│ │ └── DispatchBundle_zh_CN.properties
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── dispatch/
│ ├── AccountTest.kt
│ ├── CertTest.kt
│ └── DispatchServerTest.kt
├── sorapointa-event/
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── event/
│ │ ├── Event.kt
│ │ ├── EventManager.kt
│ │ └── StateController.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── event/
│ ├── EventPipelineTest.kt
│ └── StateControllerTest.kt
├── sorapointa-i18n/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── utils/
│ │ ├── I18n.kt
│ │ └── MessageBundle.kt
│ └── test/
│ ├── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── utils/
│ │ ├── I18nTest.kt
│ │ ├── LocalSerializerTest.kt
│ │ └── TestBundle.kt
│ └── resources/
│ └── messages/
│ ├── TestBundle.properties
│ └── TestBundle_nl.properties
├── sorapointa-native/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── build.gradle.kts
│ └── src/
│ ├── jnienv.rs
│ ├── lib.rs
│ └── logger.rs
├── sorapointa-native-wrapper/
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── rust/
│ │ ├── Setup.kt
│ │ └── logging/
│ │ └── RustLogger.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── rust/
│ └── logging/
│ └── LoggerTest.kt
├── sorapointa-proto/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── proto/
│ │ ├── PacketUtils.kt
│ │ └── ProtoInfo.kt
│ ├── proto/
│ │ ├── AbilityAppliedAbility.proto
│ │ ├── AbilityAppliedModifier.proto
│ │ ├── AbilityAttachedModifier.proto
│ │ ├── AbilityControlBlock.proto
│ │ ├── AbilityEmbryo.proto
│ │ ├── AbilityGadgetInfo.proto
│ │ ├── AbilityMixinRecoverInfo.proto
│ │ ├── AbilityScalarType.proto
│ │ ├── AbilityScalarValueEntry.proto
│ │ ├── AbilityString.proto
│ │ ├── AbilitySyncStateInfo.proto
│ │ ├── AdjustTrackingInfo.proto
│ │ ├── AnimatorParameterValueInfo.proto
│ │ ├── AnimatorParameterValueInfoPair.proto
│ │ ├── AvatarDataNotify.proto
│ │ ├── AvatarEnterSceneInfo.proto
│ │ ├── AvatarEquipAffixInfo.proto
│ │ ├── AvatarExcelInfo.proto
│ │ ├── AvatarExpeditionState.proto
│ │ ├── AvatarFetterInfo.proto
│ │ ├── AvatarFightPropNotify.proto
│ │ ├── AvatarFightPropUpdateNotify.proto
│ │ ├── AvatarInfo.proto
│ │ ├── AvatarLifeStateChangeNotify.proto
│ │ ├── AvatarPropChangeReasonNotify.proto
│ │ ├── AvatarPropNotify.proto
│ │ ├── AvatarRenameInfo.proto
│ │ ├── AvatarSkillInfo.proto
│ │ ├── AvatarTeam.proto
│ │ ├── AvatarTeamUpdateNotify.proto
│ │ ├── AvatarUpgradeRsp.proto
│ │ ├── Birthday.proto
│ │ ├── BlockInfo.proto
│ │ ├── BlossomChestInfo.proto
│ │ ├── BossChestInfo.proto
│ │ ├── BreakoutAction.proto
│ │ ├── BreakoutBrickInfo.proto
│ │ ├── BreakoutElementReactionCounter.proto
│ │ ├── BreakoutPhysicalObject.proto
│ │ ├── BreakoutPhysicalObjectModifier.proto
│ │ ├── BreakoutSnapShot.proto
│ │ ├── BreakoutSpawnPoint.proto
│ │ ├── BreakoutSyncAction.proto
│ │ ├── BreakoutSyncConnectUidInfo.proto
│ │ ├── BreakoutSyncCreateConnect.proto
│ │ ├── BreakoutSyncFinishGame.proto
│ │ ├── BreakoutSyncPing.proto
│ │ ├── BreakoutSyncSnapShot.proto
│ │ ├── BreakoutVector2.proto
│ │ ├── ChangeGameTimeReq.proto
│ │ ├── ChangeGameTimeRsp.proto
│ │ ├── ClientGadgetInfo.proto
│ │ ├── CoinCollectOperatorInfo.proto
│ │ ├── CurVehicleInfo.proto
│ │ ├── CustomCommonNodeInfo.proto
│ │ ├── CustomGadgetTreeInfo.proto
│ │ ├── DeshretObeliskGadgetInfo.proto
│ │ ├── DoSetPlayerBornDataNotify.proto
│ │ ├── EchoShellInfo.proto
│ │ ├── EnterSceneDoneReq.proto
│ │ ├── EnterSceneDoneRsp.proto
│ │ ├── EnterScenePeerNotify.proto
│ │ ├── EnterSceneReadyReq.proto
│ │ ├── EnterSceneReadyRsp.proto
│ │ ├── EnterType.proto
│ │ ├── EntityAuthorityInfo.proto
│ │ ├── EntityClientData.proto
│ │ ├── EntityClientExtraInfo.proto
│ │ ├── EntityEnvironmentInfo.proto
│ │ ├── EntityRendererChangedInfo.proto
│ │ ├── Equip.proto
│ │ ├── FeatureBlockInfo.proto
│ │ ├── FetterData.proto
│ │ ├── FightPropPair.proto
│ │ ├── FishPoolInfo.proto
│ │ ├── FishtankFishInfo.proto
│ │ ├── ForceUpdateInfo.proto
│ │ ├── FoundationInfo.proto
│ │ ├── FoundationStatus.proto
│ │ ├── FriendEnterHomeOption.proto
│ │ ├── FriendOnlineState.proto
│ │ ├── Furniture.proto
│ │ ├── GadgetBornType.proto
│ │ ├── GadgetCrucibleInfo.proto
│ │ ├── GadgetGeneralRewardInfo.proto
│ │ ├── GadgetPlayInfo.proto
│ │ ├── GatherGadgetInfo.proto
│ │ ├── GetPlayerSocialDetailReq.proto
│ │ ├── GetPlayerSocialDetailRsp.proto
│ │ ├── GetPlayerTokenReq.proto
│ │ ├── GetPlayerTokenRsp.proto
│ │ ├── HostPlayerNotify.proto
│ │ ├── Item.proto
│ │ ├── ItemParam.proto
│ │ ├── LifeStateChangeNotify.proto
│ │ ├── MPLevelEntityInfo.proto
│ │ ├── MassivePropParam.proto
│ │ ├── MassivePropSyncInfo.proto
│ │ ├── Material.proto
│ │ ├── MaterialDeleteInfo.proto
│ │ ├── MathQuaternion.proto
│ │ ├── ModifierDurability.proto
│ │ ├── MonsterBornType.proto
│ │ ├── MonsterRoute.proto
│ │ ├── MotionInfo.proto
│ │ ├── MotionState.proto
│ │ ├── MovingPlatformType.proto
│ │ ├── MpPlayRewardInfo.proto
│ │ ├── MpSettingType.proto
│ │ ├── NightCrowGadgetInfo.proto
│ │ ├── OfferingInfo.proto
│ │ ├── OnlinePlayerInfo.proto
│ │ ├── OpenStateUpdateNotify.proto
│ │ ├── PacketHead.proto
│ │ ├── PingReq.proto
│ │ ├── PingRsp.proto
│ │ ├── PlatformInfo.proto
│ │ ├── PlayTeamEntityInfo.proto
│ │ ├── PlayerDataNotify.proto
│ │ ├── PlayerDieOption.proto
│ │ ├── PlayerDieType.proto
│ │ ├── PlayerEnterSceneInfoNotify.proto
│ │ ├── PlayerEnterSceneNotify.proto
│ │ ├── PlayerGameTimeNotify.proto
│ │ ├── PlayerLocationInfo.proto
│ │ ├── PlayerLoginReq.proto
│ │ ├── PlayerLoginRsp.proto
│ │ ├── PlayerPropChangeNotify.proto
│ │ ├── PlayerPropChangeReasonNotify.proto
│ │ ├── PlayerPropNotify.proto
│ │ ├── PlayerRTTInfo.proto
│ │ ├── PlayerSetPauseReq.proto
│ │ ├── PlayerSetPauseRsp.proto
│ │ ├── PlayerStoreNotify.proto
│ │ ├── PlayerWidgetInfo.proto
│ │ ├── PlayerWorldLocationInfo.proto
│ │ ├── PlayerWorldSceneInfo.proto
│ │ ├── PlayerWorldSceneInfoListNotify.proto
│ │ ├── PostEnterSceneReq.proto
│ │ ├── PostEnterSceneRsp.proto
│ │ ├── ProfilePicture.proto
│ │ ├── PropChangeReason.proto
│ │ ├── PropPair.proto
│ │ ├── PropValue.proto
│ │ ├── ProtEntityType.proto
│ │ ├── QueryCurrRegionHttpRsp.proto
│ │ ├── QueryRegionListHttpRsp.proto
│ │ ├── RegionInfo.proto
│ │ ├── RegionSimpleInfo.proto
│ │ ├── Reliquary.proto
│ │ ├── ResVersionConfig.proto
│ │ ├── Retcode.proto
│ │ ├── RoguelikeGadgetInfo.proto
│ │ ├── Route.proto
│ │ ├── RoutePoint.proto
│ │ ├── SceneAvatarInfo.proto
│ │ ├── SceneDataNotify.proto
│ │ ├── SceneEntityAiInfo.proto
│ │ ├── SceneEntityAppearNotify.proto
│ │ ├── SceneEntityInfo.proto
│ │ ├── SceneFishInfo.proto
│ │ ├── SceneGadgetInfo.proto
│ │ ├── SceneInitFinishReq.proto
│ │ ├── SceneInitFinishRsp.proto
│ │ ├── SceneMonsterInfo.proto
│ │ ├── SceneNpcInfo.proto
│ │ ├── ScenePlayerInfo.proto
│ │ ├── ScenePlayerInfoNotify.proto
│ │ ├── ScenePlayerLocationNotify.proto
│ │ ├── SceneReliquaryInfo.proto
│ │ ├── SceneTeamAvatar.proto
│ │ ├── SceneTeamUpdateNotify.proto
│ │ ├── SceneTimeNotify.proto
│ │ ├── SceneWeaponInfo.proto
│ │ ├── ScreenInfo.proto
│ │ ├── ServantInfo.proto
│ │ ├── ServerBuff.proto
│ │ ├── ServerDisconnectClientNotify.proto
│ │ ├── ServerTimeNotify.proto
│ │ ├── SetPlayerBornDataReq.proto
│ │ ├── SetPlayerBornDataRsp.proto
│ │ ├── SetPlayerPropReq.proto
│ │ ├── SetPlayerPropRsp.proto
│ │ ├── ShortAbilityHashPair.proto
│ │ ├── SocialDetail.proto
│ │ ├── SocialShowAvatarInfo.proto
│ │ ├── StatueGadgetInfo.proto
│ │ ├── StopServerInfo.proto
│ │ ├── StoreType.proto
│ │ ├── StoreWeightLimitNotify.proto
│ │ ├── SyncScenePlayTeamEntityNotify.proto
│ │ ├── SyncTeamEntityNotify.proto
│ │ ├── TeamEnterSceneInfo.proto
│ │ ├── TeamEntityInfo.proto
│ │ ├── TrackingIOInfo.proto
│ │ ├── TrialAvatarGrantRecord.proto
│ │ ├── TrialAvatarInfo.proto
│ │ ├── UnionCmd.proto
│ │ ├── UnionCmdNotify.proto
│ │ ├── Vector.proto
│ │ ├── VehicleInfo.proto
│ │ ├── VehicleLocationInfo.proto
│ │ ├── VehicleMember.proto
│ │ ├── VisionType.proto
│ │ ├── Weapon.proto
│ │ ├── WeatherInfo.proto
│ │ ├── WeeklyBossResinDiscountInfo.proto
│ │ ├── WidgetSlotData.proto
│ │ ├── WidgetSlotTag.proto
│ │ ├── WorktopInfo.proto
│ │ ├── WorldDataNotify.proto
│ │ ├── WorldPlayerDieNotify.proto
│ │ ├── WorldPlayerInfoNotify.proto
│ │ ├── WorldPlayerLocationNotify.proto
│ │ ├── WorldPlayerRTTNotify.proto
│ │ ├── WorldPlayerReviveReq.proto
│ │ ├── WorldPlayerReviveRsp.proto
│ │ └── server_side/
│ │ ├── bin.block.proto
│ │ ├── bin.home.proto
│ │ ├── bin.server.proto
│ │ ├── bin_common.server.proto
│ │ ├── cmd_activity.server.proto
│ │ ├── cmd_id_config.proto
│ │ ├── cmd_match.server.proto
│ │ ├── cmd_misc.server.proto
│ │ ├── cmd_mp.server.proto
│ │ ├── cmd_muip.server.proto
│ │ ├── cmd_offline_op.server.proto
│ │ ├── cmd_player.server.proto
│ │ ├── config.server.proto
│ │ ├── define.proto
│ │ └── enum.server.proto
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── proto/
│ └── ProtoTest.kt
├── sorapointa-task/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── task/
│ │ ├── Cron.kt
│ │ ├── CronTask.kt
│ │ └── TaskManager.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── task/
│ └── TaskManagerTest.kt
└── sorapointa-utils/
├── build.gradle.kts
├── sorapointa-utils-all/
│ └── build.gradle.kts
├── sorapointa-utils-core/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── utils/
│ │ ├── Annotations.kt
│ │ ├── ByteUtils.kt
│ │ ├── Cast.kt
│ │ ├── Collection.kt
│ │ ├── Environment.kt
│ │ ├── File.kt
│ │ ├── Files.kt
│ │ ├── JVM.kt
│ │ ├── Locks.kt
│ │ ├── ModuleScope.kt
│ │ ├── Optional.kt
│ │ ├── Random.kt
│ │ ├── Reflection.kt
│ │ ├── String.kt
│ │ ├── Test.kt
│ │ ├── XML.kt
│ │ ├── encoding/
│ │ │ ├── Base64Provider.kt
│ │ │ ├── Digest.kt
│ │ │ ├── Hex.kt
│ │ │ └── RSAProvider.kt
│ │ └── logging/
│ │ └── PatternLayoutNoLambda.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── utils/
│ ├── FilesTest.kt
│ ├── LocksTest.kt
│ ├── ScopeTest.kt
│ ├── StringTest.kt
│ └── encoding/
│ ├── Base64.kt
│ └── HexTest.kt
├── sorapointa-utils-crypto/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── utils/
│ │ ├── ByteReadUtils.kt
│ │ └── crypto/
│ │ ├── Ec2b.kt
│ │ ├── Ec2bAes.kt
│ │ ├── MT19937.kt
│ │ ├── Magic.kt
│ │ └── RSA.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── utils/
│ ├── ScopeTest.kt
│ └── crypto/
│ ├── Ec2bTest.kt
│ ├── MT64Test.kt
│ └── RSAKeyTest.kt
├── sorapointa-utils-serialization/
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── org/
│ │ └── sorapointa/
│ │ └── utils/
│ │ ├── Json.kt
│ │ └── Yaml.kt
│ └── test/
│ └── kotlin/
│ └── org/
│ └── sorapointa/
│ └── utils/
│ └── YamlCompatible.kt
└── sorapointa-utils-time/
├── build.gradle.kts
└── src/
└── main/
└── kotlin/
└── org/
└── sorapointa/
└── utils/
└── Time.kt
SYMBOL INDEX (11 symbols across 2 files)
FILE: sorapointa-native/src/jnienv.rs
type JNIEnvExt (line 3) | pub trait JNIEnvExt<'a> {
method throw_new_or_eprint (line 4) | fn throw_new_or_eprint<'c, T>(&self, class: T, msg: &str)
function throw_new_or_eprint (line 10) | fn throw_new_or_eprint<'c, T>(&self, class: T, msg: &str)
FILE: sorapointa-native/src/logger.rs
constant LOG_CLASS (line 11) | const LOG_CLASS: &str = "org/sorapointa/rust/logging/RustLogger";
constant LOG_SIG (line 12) | const LOG_SIG: &str = "(Ljava/lang/String;)V";
constant ILLEGAL_STATE_EXCEPTION (line 13) | const ILLEGAL_STATE_EXCEPTION: &str = "java/lang/IllegalStateException";
type Logger (line 18) | struct Logger {
method enabled (line 61) | fn enabled(&self, metadata: &Metadata) -> bool {
method log (line 65) | fn log(&self, record: &Record) {
method flush (line 85) | fn flush(&self) {}
function Java_org_sorapointa_rust_logging_RustLogger_setup (line 90) | pub extern "system" fn Java_org_sorapointa_rust_logging_RustLogger_setup(
Condensed preview — 479 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,319K chars).
[
{
"path": ".editorconfig",
"chars": 244,
"preview": "[*]\ninsert_final_newline = true\ncharset = utf-8\nindent_style = space\nend_of_line = lf\n\n[{*.kt,*.kts}]\nindent_size = 4\nma"
},
{
"path": ".git-hooks/commit-msg",
"chars": 514,
"preview": "#!/usr/bin/env bash\n\nINPUT_FILE=$1\n\nSTART_LINE=$(head -n1 \"$INPUT_FILE\")\n\nPATTERN='^(feat(ure)?|fix|docs|style|refactor|"
},
{
"path": ".git-hooks/pre-commit",
"chars": 1869,
"preview": "#!/bin/bash\n\necho \"[pre-commit check]\"\n\nif ! [ -x \"$(command -v cargo)\" ]; then\n echo -e 'Rust toolchains are not insta"
},
{
"path": ".gitattributes",
"chars": 147,
"preview": "# Linux start script should use lf\n/gradlew text eol=lf\n\n# These are Windows script files and should use crlf\n*.b"
},
{
"path": ".github/workflows/api_check.yml",
"chars": 716,
"preview": "name: API Check\n\non:\n workflow_dispatch:\n push:\n branches: [ master ]\n paths:\n - '**.kt'\n - '**.kts'\n "
},
{
"path": ".github/workflows/test.yml",
"chars": 2767,
"preview": "name: Test\n\non:\n workflow_dispatch:\n push:\n branches: [ master ]\n paths:\n - '**.kt'\n - '**.kts'\n "
},
{
"path": ".gitignore",
"chars": 979,
"preview": "### Gradle ###\n.gradle\nbuild/\n!gradle/wrapper/gradle-wrapper.jar\n!**/src/main/**/build/\n!**/src/test/**/build/\n\n### Carg"
},
{
"path": ".idea/encodings.xml",
"chars": 201,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"Encoding\" defaultCharsetForPropertiesFil"
},
{
"path": "LICENSE",
"chars": 11370,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "build.gradle.kts",
"chars": 2498,
"preview": "import com.diffplug.gradle.spotless.FormatExtension\n\nplugins {\n kotlin(\"jvm\") apply false\n java\n // NOT AN ERRO"
},
{
"path": "buildSrc/build.gradle.kts",
"chars": 790,
"preview": "plugins {\n `kotlin-dsl`\n}\n\nrepositories {\n gradlePluginPortal()\n mavenCentral()\n maven(\"https://s01.oss.sona"
},
{
"path": "buildSrc/settings.gradle.kts",
"chars": 154,
"preview": "dependencyResolutionManagement {\n versionCatalogs {\n create(\"libs\") {\n from(files(\"../gradle/libs.v"
},
{
"path": "buildSrc/src/main/kotlin/BuildConfigExtension.kt",
"chars": 928,
"preview": "import com.github.gmazzo.gradle.plugins.BuildConfigSourceSet\n\nfun BuildConfigSourceSet.string(name: String, value: Strin"
},
{
"path": "buildSrc/src/main/kotlin/GitHook.kt",
"chars": 538,
"preview": "import org.gradle.api.Project\nimport org.gradle.internal.os.OperatingSystem\nimport java.io.File\nimport java.nio.file.Fil"
},
{
"path": "buildSrc/src/main/kotlin/JniHeader.kt",
"chars": 4107,
"preview": "import org.gradle.api.Project\nimport org.gradle.api.tasks.TaskContainer\nimport org.jetbrains.kotlin.gradle.dsl.kotlinExt"
},
{
"path": "buildSrc/src/main/kotlin/OptInAnnotations.kt",
"chars": 202,
"preview": "object OptInAnnotations {\n val list = listOf(\n \"kotlin.ExperimentalUnsignedTypes\",\n \"kotlin.contracts.E"
},
{
"path": "buildSrc/src/main/kotlin/Properties.kt",
"chars": 676,
"preview": "import org.gradle.api.Project\nimport org.gradle.kotlin.dsl.extra\nimport java.util.*\n\nfun Project.getRootProjectLocalProp"
},
{
"path": "buildSrc/src/main/kotlin/ResourcesCopy.kt",
"chars": 1462,
"preview": "import org.gradle.api.Project\nimport org.gradle.api.tasks.Copy\nimport org.gradle.kotlin.dsl.register\n\ninternal fun Proje"
},
{
"path": "buildSrc/src/main/kotlin/Test.kt",
"chars": 39,
"preview": "val isCI = System.getenv(\"CI\") != null\n"
},
{
"path": "buildSrc/src/main/kotlin/sorapointa-conventions.gradle.kts",
"chars": 2941,
"preview": "import org.jetbrains.kotlin.gradle.tasks.KotlinCompile\n\nplugins {\n kotlin(\"jvm\")\n id(\"com.github.gmazzo.buildconfi"
},
{
"path": "buildSrc/src/main/kotlin/sorapointa-publish.gradle.kts",
"chars": 2990,
"preview": "plugins {\n `java-library`\n `maven-publish`\n signing\n}\n\njava {\n withJavadocJar()\n withSourcesJar()\n}\n\nval "
},
{
"path": "docs/CONTRIBUTING.md",
"chars": 3927,
"preview": "# Contributing Guideline\n\n[简体中文](CONTRIBUTING.zh-CN.md)\n\n## Code Style\n\n- [Kotlin Official Code Style](https://kotlinlan"
},
{
"path": "docs/CONTRIBUTING.zh-CN.md",
"chars": 2275,
"preview": "# 贡献指南\n\n[English](CONTRIBUTING.zh-CN.md)\n\n## 代码风格\n\n- Kotlin 官方代码风格 - [中文站参考](https://book.kotlincn.net/text/coding-conve"
},
{
"path": "docs/README.md",
"chars": 5284,
"preview": "<!--Logo-->\n\n\n\n## Setup\n\n```kotlin\ndependencies {\n implementation(\"org."
},
{
"path": "docs/guides/kotlin-atomicfu.zh-CN.md",
"chars": 1349,
"preview": "# Kotlin AtomicFU 使用指南\n\n[English](kotlin-atomicfu.md)\n\n## Setup\n\n```kotlin\ndependencies {\n implementation(\"org.jetbrain"
},
{
"path": "docs/guides/unit-test.md",
"chars": 2951,
"preview": "# JUnit Guideline\n\n[简体中文](unit-test.zh-CN.md)\n\n[JUnit Official User Guide](https://junit.org/junit5/docs/current/user-gu"
},
{
"path": "docs/guides/unit-test.zh-CN.md",
"chars": 2154,
"preview": "# JUnit 使用指南\n\n[English](unit-test.md)\n\n[JUnit 官方文档](https://junit.org/junit5/docs/current/user-guide/)\n\n## 基本用法\n\n你可以在 `t"
},
{
"path": "gradle/libs.versions.toml",
"chars": 4258,
"preview": "[versions]\nkotlin = \"1.8.10\"\nktor = \"2.1.2\"\nktlint = \"0.48.1\"\nexposed = \"0.41.1\"\n\n[plugins]\nkotlin-serialization = { id "
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 221,
"preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
},
{
"path": "gradle.properties",
"chars": 225,
"preview": "kotlin.code.style=official\norg.gradle.caching=true\norg.gradle.parallel=true\norg.gradle.warning.mode=summary\norg.gradle.c"
},
{
"path": "gradlew",
"chars": 8474,
"preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
},
{
"path": "gradlew.bat",
"chars": 2868,
"preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
},
{
"path": "renovate.json",
"chars": 107,
"preview": "{\n \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n \"extends\": [\n \"config:base\"\n ]\n}\n"
},
{
"path": "settings.gradle.kts",
"chars": 746,
"preview": "rootProject.name = \"Sorapointa\"\n\ninclude(\"sorapointa-core\")\ninclude(\"sorapointa-crypto\")\ninclude(\"sorapointa-dataloader\""
},
{
"path": "sorapointa-core/README.md",
"chars": 2278,
"preview": "# Core Module\n\n[简体中文](README.zh-CN.md)\n\n## Command System\n\nThe command system is based on [**Yac**](https://githubfast.c"
},
{
"path": "sorapointa-core/README.zh-CN.md",
"chars": 1550,
"preview": "# Core 模块\n\n[English](README.zh-CN.md)\n\n## 命令系统\n\n命令系统基于 [**Yac**](https://githubfast.com/Colerar/Yac) ([**clikt**](https:"
},
{
"path": "sorapointa-core/build.gradle.kts",
"chars": 1202,
"preview": "@file:Suppress(\"GradlePackageUpdate\")\n\nplugins {\n `sorapointa-conventions`\n `sorapointa-publish`\n application\n}"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/CoreBundle.kt",
"chars": 483,
"preview": "package org.sorapointa\n\nimport org.jetbrains.annotations.Nls\nimport org.jetbrains.annotations.PropertyKey\nimport org.sor"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/Main.kt",
"chars": 7356,
"preview": "package org.sorapointa\n\nimport io.ktor.server.application.*\nimport kotlinx.coroutines.*\nimport moe.sdl.yac.core.CliktCom"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/Sorapointa.kt",
"chars": 2060,
"preview": "package org.sorapointa\n\nimport io.ktor.server.application.*\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coro"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/SorapointaConfig.kt",
"chars": 3761,
"preview": "package org.sorapointa\n\nimport com.charleskorn.kaml.YamlComment\nimport kotlinx.datetime.TimeZone\nimport kotlinx.serializ"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/Command.kt",
"chars": 1581,
"preview": "package org.sorapointa.command\n\nimport moe.sdl.yac.core.CliktCommand\nimport moe.sdl.yac.core.context\nimport org.jetbrain"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/CommandLocalization.kt",
"chars": 11298,
"preview": "package org.sorapointa.command\n\nimport moe.sdl.yac.core.*\nimport moe.sdl.yac.output.HelpFormatter\nimport moe.sdl.yac.out"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/CommandManager.kt",
"chars": 6105,
"preview": "package org.sorapointa.command\n\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport moe.sdl.yac.core.C"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/CommandSender.kt",
"chars": 884,
"preview": "package org.sorapointa.command\n\nimport org.jetbrains.annotations.Nls\nimport org.jetbrains.annotations.PropertyKey\nimport"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/ConsoleCommandSender.kt",
"chars": 706,
"preview": "package org.sorapointa.command\n\nimport io.ktor.server.websocket.*\nimport kotlinx.coroutines.isActive\nimport org.sorapoin"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/Defaults.kt",
"chars": 946,
"preview": "package org.sorapointa.command.defaults\n\nimport org.sorapointa.command.AbstractCommandNode\nimport org.sorapointa.command"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/console/ConsoleUser.kt",
"chars": 3124,
"preview": "package org.sorapointa.command.defaults.console\n\nimport moe.sdl.yac.core.PrintMessage\nimport moe.sdl.yac.parameters.opti"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/console/Quit.kt",
"chars": 449,
"preview": "package org.sorapointa.command.defaults.console\n\nimport org.sorapointa.command.ConsoleCommand\nimport org.sorapointa.comm"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/general/Help.kt",
"chars": 2349,
"preview": "package org.sorapointa.command.defaults.general\n\nimport moe.sdl.yac.core.UsageError\nimport moe.sdl.yac.parameters.argume"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/general/ListPlayer.kt",
"chars": 759,
"preview": "package org.sorapointa.command.defaults.general\n\nimport org.sorapointa.Sorapointa\nimport org.sorapointa.command.Command\n"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/general/LocaleCommand.kt",
"chars": 3457,
"preview": "package org.sorapointa.command.defaults.general\n\nimport moe.sdl.yac.parameters.arguments.argument\nimport moe.sdl.yac.par"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/defaults/general/Version.kt",
"chars": 556,
"preview": "package org.sorapointa.command.defaults.general\n\nimport org.sorapointa.command.Command\nimport org.sorapointa.command.Com"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/command/utils/Options.kt",
"chars": 1241,
"preview": "package org.sorapointa.command.utils\n\nimport moe.sdl.yac.parameters.groups.ChoiceGroup\nimport moe.sdl.yac.parameters.gro"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/console/Completer.kt",
"chars": 1432,
"preview": "@file:Suppress(\"unused\")\n\npackage org.sorapointa.console\n\nimport moe.sdl.yac.core.CliktCommand\nimport moe.sdl.yac.parame"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/console/Console.kt",
"chars": 3822,
"preview": "package org.sorapointa.console\n\nimport io.ktor.util.collections.*\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.c"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/console/JLineRedirector.kt",
"chars": 2872,
"preview": "package org.sorapointa.console\n\nimport java.io.PrintStream\nimport java.util.*\n\n/**\n * Work around, not a good implementa"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/console/SoraHighlighter.kt",
"chars": 4497,
"preview": "package org.sorapointa.console\n\nimport org.jline.reader.Highlighter\nimport org.jline.reader.LineReader\nimport org.jline."
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/console/WebSocketConsole.kt",
"chars": 9041,
"preview": "package org.sorapointa.console\n\nimport com.password4j.Password\nimport io.ktor.client.*\nimport io.ktor.client.engine.cio."
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/events/PlayerEvent.kt",
"chars": 2047,
"preview": "@file:Suppress(\"unused\")\n\npackage org.sorapointa.events\n\nimport com.squareup.wire.Message\nimport com.squareup.wire.Proto"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/AvatarEntity.kt",
"chars": 12804,
"preview": "package org.sorapointa.game\n\nimport org.sorapointa.dataloader.common.ElementType\nimport org.sorapointa.dataloader.common"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/Player.kt",
"chars": 11000,
"preview": "package org.sorapointa.game\n\nimport com.squareup.wire.Message\nimport kotlinx.atomicfu.atomic\nimport kotlinx.coroutines.J"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/PlayerAvatarComp.kt",
"chars": 22254,
"preview": "package org.sorapointa.game\n\nimport kotlinx.atomicfu.atomic\nimport org.sorapointa.dataloader.common.*\nimport org.sorapoi"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/PlayerComp.kt",
"chars": 8907,
"preview": "package org.sorapointa.game\n\nimport kotlinx.atomicfu.atomic\nimport kotlinx.datetime.Instant\nimport org.sorapointa.events"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/PlayerItemComp.kt",
"chars": 17134,
"preview": "package org.sorapointa.game\n\nimport kotlinx.atomicfu.atomic\nimport kotlinx.atomicfu.getAndUpdate\nimport org.sorapointa.d"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/Scene.kt",
"chars": 2467,
"preview": "package org.sorapointa.game\n\nimport org.sorapointa.dataloader.common.ClimateType\nimport org.sorapointa.dataloader.def.Sc"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/SceneEntity.kt",
"chars": 8885,
"preview": "package org.sorapointa.game\n\nimport org.sorapointa.dataloader.common.EntityIdType\nimport org.sorapointa.dataloader.commo"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/World.kt",
"chars": 1288,
"preview": "package org.sorapointa.game\n\nimport kotlinx.atomicfu.atomic\nimport org.sorapointa.dataloader.common.EntityIdType\nimport "
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/data/GameConstants.kt",
"chars": 331,
"preview": "package org.sorapointa.game.data\n\n// const val MAIN_CHARACTER_MALE = 10000005\n// const val MAIN_CHARACTER_FEMALE = 10000"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/data/PlayerData.kt",
"chars": 5275,
"preview": "package org.sorapointa.game.data\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.sync.Mutex\nimport kotl"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/data/Position.kt",
"chars": 3357,
"preview": "package org.sorapointa.game.data\n\nimport kotlinx.serialization.Serializable\nimport org.sorapointa.proto.Vector\nimport or"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/game/data/SorapointaStoreEntry.kt",
"chars": 1723,
"preview": "package org.sorapointa.game.data\n\nimport org.jetbrains.exposed.dao.id.EntityID\nimport org.jetbrains.exposed.dao.id.IdTab"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/server/ServerNetwork.kt",
"chars": 1775,
"preview": "package org.sorapointa.server\n\nimport kcp.highway.ChannelConfig\nimport kcp.highway.KcpServer\nimport kotlinx.coroutines.J"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/server/network/NetworkHandler.kt",
"chars": 10485,
"preview": "package org.sorapointa.server.network\n\nimport com.squareup.wire.Message\nimport io.netty.buffer.ByteBuf\nimport io.netty.b"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/server/network/OutgoingPacket.kt",
"chars": 22363,
"preview": "package org.sorapointa.server.network\n\nimport com.squareup.wire.ProtoAdapter\nimport io.ktor.util.*\nimport okio.ByteStrin"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/server/network/PacketHandler.kt",
"chars": 4985,
"preview": "package org.sorapointa.server.network\n\nimport com.squareup.wire.Message\nimport com.squareup.wire.ProtoAdapter\nimport org"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/server/network/PacketHandlerImpl.kt",
"chars": 10343,
"preview": "package org.sorapointa.server.network\n\nimport com.squareup.wire.ProtoAdapter\nimport io.ktor.util.*\nimport kotlinx.corout"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/server/network/SoraPacket.kt",
"chars": 1524,
"preview": "package org.sorapointa.server.network\n\nimport com.squareup.wire.Message\nimport com.squareup.wire.ProtoAdapter\nimport io."
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/utils/Console.kt",
"chars": 411,
"preview": "package org.sorapointa.utils\n\nimport org.jline.style.StyleExpression\nimport org.sorapointa.console.Console\n\nprivate val "
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/utils/GameUtils.kt",
"chars": 2814,
"preview": "@file:Suppress(\"NOTHING_TO_INLINE\")\n\npackage org.sorapointa.utils\n\nimport kotlinx.datetime.Instant\nimport kotlinx.dateti"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/utils/NetworkUtils.kt",
"chars": 2055,
"preview": "package org.sorapointa.utils\n\nimport io.ktor.utils.io.core.*\nimport io.netty.buffer.ByteBuf\nimport mu.KotlinLogging\nimpo"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/utils/OptionalContainer.kt",
"chars": 546,
"preview": "package org.sorapointa.utils\n\nclass OptionalContainer<T>(\n private val defaultValue: T,\n) {\n var value: T = defaul"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/utils/PropDelegate.kt",
"chars": 1213,
"preview": "package org.sorapointa.utils\n\nimport org.jetbrains.exposed.dao.Entity\nimport org.jetbrains.exposed.sql.Column\nimport kot"
},
{
"path": "sorapointa-core/src/main/kotlin/org/sorapointa/utils/TypoSuggestor.kt",
"chars": 403,
"preview": "package org.sorapointa.utils\n\nimport moe.sdl.yac.core.jaroWinklerSimilarity\n\nfun suggestTypo(input: String, possibleValu"
},
{
"path": "sorapointa-core/src/main/resources/logback.xml",
"chars": 2529,
"preview": "<!--Please edit this file on Sorapointa Core, other copy would be overwritten-->\n<configuration debug=\"false\">\n <!--N"
},
{
"path": "sorapointa-core/src/main/resources/messages/CoreBundle.properties",
"chars": 5850,
"preview": "sora.cmd.manager.alias=\\n\\nAlias={0}\nsora.cmd.manager.invoke.empty=No command input\nsora.cmd.manager.invoke.error=No suc"
},
{
"path": "sorapointa-core/src/main/resources/messages/CoreBundle_zh_CN.properties",
"chars": 4348,
"preview": "sora.cmd.manager.alias=\\n\\n别名={0}\nsora.cmd.manager.invoke.empty=未输入命令\nsora.cmd.manager.invoke.error=不存在此命令 \"{0}\"\nsora.cm"
},
{
"path": "sorapointa-core/src/test/kotlin/org/sorapointa/command/defaults/HelpTest.kt",
"chars": 2208,
"preview": "package org.sorapointa.command.defaults\n\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.BeforeAll\nim"
},
{
"path": "sorapointa-core/src/test/kotlin/org/sorapointa/logger/LogTest.kt",
"chars": 1288,
"preview": "package org.sorapointa.logger\n\nimport mu.KotlinLogging\nimport org.junit.jupiter.api.Test\n\nprivate val logger = KotlinLog"
},
{
"path": "sorapointa-core/src/test/resources/logback-test.xml",
"chars": 788,
"preview": "<!--Please edit this file on Sorapointa Core, other copy would be overwritten-->\n<configuration debug=\"true\">\n <!--Te"
},
{
"path": "sorapointa-crypto/build.gradle.kts",
"chars": 399,
"preview": "plugins {\n `sorapointa-conventions`\n `sorapointa-publish`\n kotlin(\"plugin.serialization\")\n}\n\ndependencies {\n "
},
{
"path": "sorapointa-crypto/src/main/kotlin/org/sorapointa/crypto/Crypto.kt",
"chars": 10760,
"preview": "package org.sorapointa.crypto\n\nimport com.charleskorn.kaml.YamlComment\nimport kotlinx.serialization.Serializable\nimport "
},
{
"path": "sorapointa-dataloader/README.md",
"chars": 1009,
"preview": "# Data Loader Module\n\n[简体中文](README.zh-CN.md)\n\n## Register Resource\n\nYou can register resource like this:\n\n```kotlin\n@Se"
},
{
"path": "sorapointa-dataloader/README.zh-CN.md",
"chars": 862,
"preview": "# Data Loader 模块\n\n[English](README.md)\n\n## 注册 Resource\n\n像这样注册资源:\n\n```kotlin\n@Serializable // 用 kotlinx.serialization 序列化"
},
{
"path": "sorapointa-dataloader/build.gradle.kts",
"chars": 728,
"preview": "@file:Suppress(\"GradlePackageUpdate\")\n\nimport org.jetbrains.kotlin.gradle.tasks.KotlinCompile\n\nplugins {\n `sorapointa"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/DataLoader.kt",
"chars": 5071,
"preview": "package org.sorapointa.dataloader\n\nimport io.github.classgraph.ClassGraph\nimport io.github.classgraph.ClassRefTypeSignat"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/common/AddProp.kt",
"chars": 451,
"preview": "package org.sorapointa.dataloader.common\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Js"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/common/CurveInfo.kt",
"chars": 319,
"preview": "package org.sorapointa.dataloader.common\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Js"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/common/Enum.kt",
"chars": 26424,
"preview": "@file:Suppress(\"unused\")\n\npackage org.sorapointa.dataloader.common\n\nimport kotlinx.serialization.json.JsonPrimitive\n\nint"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/common/ItemParamData.kt",
"chars": 314,
"preview": "package org.sorapointa.dataloader.common\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Js"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/common/ItemParamStringData.kt",
"chars": 269,
"preview": "package org.sorapointa.dataloader.common\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Js"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/common/OpenCondData.kt",
"chars": 298,
"preview": "package org.sorapointa.dataloader.common\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Js"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/common/PointData.kt",
"chars": 547,
"preview": "package org.sorapointa.dataloader.common\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Js"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/common/PropGrowCurve.kt",
"chars": 548,
"preview": "package org.sorapointa.dataloader.common\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Js"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/common/RewardItemData.kt",
"chars": 285,
"preview": "package org.sorapointa.dataloader.common\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Js"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/common/ScenePointConfig.kt",
"chars": 278,
"preview": "package org.sorapointa.dataloader.common\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Js"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/AvatarExcelData.kt",
"chars": 4262,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/AvatarSkillData.kt",
"chars": 2441,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/AvatarSkillDepotData.kt",
"chars": 2406,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/MaterialData.kt",
"chars": 3906,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/ReliquaryAffixData.kt",
"chars": 1107,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/ReliquaryData.kt",
"chars": 3446,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/ReliquaryLevelData.kt",
"chars": 851,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/ReliquaryMainPropData.kt",
"chars": 929,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/ReliquarySetData.kt",
"chars": 839,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/SceneData.kt",
"chars": 1476,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/main/kotlin/org/sorapointa/dataloader/def/WeaponData.kt",
"chars": 3538,
"preview": "package org.sorapointa.dataloader.def\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonN"
},
{
"path": "sorapointa-dataloader/src/test/kotlin/org/sorapointa/dataloader/DataLoaderTest.kt",
"chars": 665,
"preview": "package org.sorapointa.dataloader\n\nimport org.junit.jupiter.api.Test\nimport org.sorapointa.dataloader.def.avatarDataList"
},
{
"path": "sorapointa-dataprovider/build.gradle.kts",
"chars": 1556,
"preview": "@file:Suppress(\"GradlePackageUpdate\")\n\nimport com.github.gmazzo.gradle.plugins.BuildConfigSourceSet\nimport org.jetbrains"
},
{
"path": "sorapointa-dataprovider/src/main/kotlin/org/sorapointa/data/provider/AutoLoadFilePersist.kt",
"chars": 1231,
"preview": "package org.sorapointa.data.provider\n\nimport kotlinx.coroutines.*\nimport kotlinx.serialization.KSerializer\nimport kotlin"
},
{
"path": "sorapointa-dataprovider/src/main/kotlin/org/sorapointa/data/provider/AutoSaveFilePersist.kt",
"chars": 1333,
"preview": "package org.sorapointa.data.provider\n\nimport kotlinx.coroutines.*\nimport kotlinx.serialization.KSerializer\nimport kotlin"
},
{
"path": "sorapointa-dataprovider/src/main/kotlin/org/sorapointa/data/provider/DataFilePersist.kt",
"chars": 2112,
"preview": "package org.sorapointa.data.provider\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.co"
},
{
"path": "sorapointa-dataprovider/src/main/kotlin/org/sorapointa/data/provider/DatabaseConfig.kt",
"chars": 3921,
"preview": "package org.sorapointa.data.provider\n\nimport com.charleskorn.kaml.YamlComment\nimport kotlinx.serialization.SerialName\nim"
},
{
"path": "sorapointa-dataprovider/src/main/kotlin/org/sorapointa/data/provider/DatabaseManager.kt",
"chars": 2583,
"preview": "package org.sorapointa.data.provider\n\nimport com.zaxxer.hikari.HikariConfig\nimport com.zaxxer.hikari.HikariDataSource\nim"
},
{
"path": "sorapointa-dataprovider/src/main/kotlin/org/sorapointa/data/provider/FilePersist.kt",
"chars": 1088,
"preview": "package org.sorapointa.data.provider\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.serialization.Serializable"
},
{
"path": "sorapointa-dataprovider/src/main/kotlin/org/sorapointa/data/provider/sql/SQLJson.kt",
"chars": 1794,
"preview": "package org.sorapointa.data.provider.sql\n\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.json.Jso"
},
{
"path": "sorapointa-dataprovider/src/main/kotlin/org/sorapointa/data/provider/sql/SQLMap.kt",
"chars": 3643,
"preview": "package org.sorapointa.data.provider.sql\n\nimport org.jetbrains.exposed.dao.id.EntityID\nimport org.jetbrains.exposed.dao."
},
{
"path": "sorapointa-dataprovider/src/main/kotlin/org/sorapointa/data/provider/sql/SQLSet.kt",
"chars": 2981,
"preview": "package org.sorapointa.data.provider.sql\n\nimport org.jetbrains.exposed.dao.id.EntityID\nimport org.jetbrains.exposed.dao."
},
{
"path": "sorapointa-dataprovider/src/test/kotlin/org/sorapointa/data/provider/DatabaseProviderTest.kt",
"chars": 1275,
"preview": "package org.sorapointa.data.provider\n\nimport org.jetbrains.exposed.sql.Column\nimport org.jetbrains.exposed.sql.Table\nimp"
},
{
"path": "sorapointa-dataprovider/src/test/kotlin/org/sorapointa/data/provider/FileProviderTest.kt",
"chars": 2554,
"preview": "package org.sorapointa.data.provider\n\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.runBlocking\nimport kotli"
},
{
"path": "sorapointa-dataprovider/src/test/kotlin/org/sorapointa/data/provider/Init.kt",
"chars": 189,
"preview": "package org.sorapointa.data.provider\n\nimport kotlinx.coroutines.runBlocking\n\nfun initTestDataProvider(): Unit = runBlock"
},
{
"path": "sorapointa-dispatch/build.gradle.kts",
"chars": 1312,
"preview": "@file:Suppress(\"GradlePackageUpdate\")\n\nplugins {\n kotlin(\"plugin.serialization\")\n application\n `sorapointa-conv"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/DispatchBundle.kt",
"chars": 500,
"preview": "package org.sorapointa.dispatch\n\nimport org.jetbrains.annotations.Nls\nimport org.jetbrains.annotations.PropertyKey\nimpor"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/DispatchServer.kt",
"chars": 11715,
"preview": "package org.sorapointa.dispatch\n\nimport com.charleskorn.kaml.YamlComment\nimport com.password4j.types.Argon2\nimport io.kt"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/data/AccountData.kt",
"chars": 6554,
"preview": "package org.sorapointa.dispatch.data\n\nimport com.password4j.Argon2Function\nimport com.password4j.Password\nimport io.ktor"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/data/DispatchData.kt",
"chars": 8792,
"preview": "@file:Suppress(\"unused\")\n\npackage org.sorapointa.dispatch.data\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.s"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/data/DispatchKeyData.kt",
"chars": 1685,
"preview": "package org.sorapointa.dispatch.data\n\nimport org.jetbrains.exposed.dao.Entity\nimport org.jetbrains.exposed.dao.EntityCla"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/data/SwitchData.kt",
"chars": 17840,
"preview": "@file:Suppress(\"unused\")\n\npackage org.sorapointa.dispatch.data\n\nobject CodeSwitchData {\n const val DEFAULT = 0\n co"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/events/DispatchEvent.kt",
"chars": 2540,
"preview": "@file:Suppress(\"unused\")\n\npackage org.sorapointa.dispatch.events\n\nimport io.ktor.server.application.*\nimport org.sorapoi"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/plugins/HTTP.kt",
"chars": 233,
"preview": "package org.sorapointa.dispatch.plugins\n\nimport io.ktor.server.application.*\nimport io.ktor.server.plugins.compression.*"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/plugins/Monitoring.kt",
"chars": 811,
"preview": "package org.sorapointa.dispatch.plugins\n\nimport io.ktor.http.*\nimport io.ktor.server.application.*\nimport io.ktor.server"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/plugins/RouteHandler.kt",
"chars": 18120,
"preview": "package org.sorapointa.dispatch.plugins\n\nimport io.ktor.client.call.*\nimport io.ktor.client.request.*\nimport io.ktor.cli"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/plugins/Routing.kt",
"chars": 5131,
"preview": "package org.sorapointa.dispatch.plugins\n\nimport io.ktor.server.application.*\nimport io.ktor.server.response.*\nimport io."
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/plugins/Serialization.kt",
"chars": 282,
"preview": "package org.sorapointa.dispatch.plugins\n\nimport io.ktor.serialization.kotlinx.json.*\nimport io.ktor.server.application.*"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/plugins/StatusPage.kt",
"chars": 615,
"preview": "package org.sorapointa.dispatch.plugins\n\nimport io.ktor.http.*\nimport io.ktor.server.application.*\nimport io.ktor.server"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/utils/CertBuilder.kt",
"chars": 4372,
"preview": "package org.sorapointa.dispatch.utils\n\nimport io.ktor.network.tls.*\nimport io.ktor.network.tls.extensions.*\nimport io.kt"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/utils/Certificates.kt",
"chars": 12901,
"preview": "@file:Suppress(\"unused\")\n\npackage org.sorapointa.dispatch.utils\n\nimport io.ktor.network.tls.*\nimport io.ktor.utils.io.co"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/utils/KeyProvider.kt",
"chars": 3188,
"preview": "package org.sorapointa.dispatch.utils\n\nimport io.ktor.network.tls.extensions.*\nimport io.ktor.util.*\nimport kotlinx.coro"
},
{
"path": "sorapointa-dispatch/src/main/kotlin/org/sorapointa/dispatch/utils/Route.kt",
"chars": 393,
"preview": "package org.sorapointa.dispatch.utils\n\nimport io.ktor.server.application.*\nimport io.ktor.server.routing.*\nimport io.kto"
},
{
"path": "sorapointa-dispatch/src/main/resources/messages/DispatchBundle.properties",
"chars": 559,
"preview": "dispatch.login.error.split='Please use `:` to split your username and password in the username input field'\ndispatch.log"
},
{
"path": "sorapointa-dispatch/src/main/resources/messages/DispatchBundle_zh_CN.properties",
"chars": 352,
"preview": "dispatch.login.error.split=请使用 `:` 在用户名输入框中分割您的帐号和密码\ndispatch.login.error.length.name=输入的名称长度必须在 3-16 字符之间\ndispatch.logi"
},
{
"path": "sorapointa-dispatch/src/test/kotlin/org/sorapointa/dispatch/AccountTest.kt",
"chars": 3280,
"preview": "package org.sorapointa.dispatch\n\nimport kotlinx.coroutines.joinAll\nimport kotlinx.coroutines.launch\nimport org.jetbrains"
},
{
"path": "sorapointa-dispatch/src/test/kotlin/org/sorapointa/dispatch/CertTest.kt",
"chars": 294,
"preview": "package org.sorapointa.dispatch\n\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Test\nimport org.sora"
},
{
"path": "sorapointa-dispatch/src/test/kotlin/org/sorapointa/dispatch/DispatchServerTest.kt",
"chars": 619,
"preview": "package org.sorapointa.dispatch\n\nimport io.ktor.client.request.*\nimport io.ktor.client.statement.*\nimport io.ktor.http.*"
},
{
"path": "sorapointa-event/README.md",
"chars": 5249,
"preview": "# Event Module\n\n[简体中文](README.zh-CN.md)\n\nYou could refer a lot of examples provided in [EventPipelineTest](src/test/kotl"
},
{
"path": "sorapointa-event/README.zh-CN.md",
"chars": 4745,
"preview": "# 事件模块\n\n[English](README.md)\n\n你可以查看 [EventPipelineTest](src/test/kotlin/org/sorapointa/event/EventPipelineTest.kt) 提供的例子"
},
{
"path": "sorapointa-event/build.gradle.kts",
"chars": 504,
"preview": "@file:Suppress(\"GradlePackageUpdate\")\n\nplugins {\n `sorapointa-conventions`\n `sorapointa-publish`\n}\n\ndependencies {"
},
{
"path": "sorapointa-event/src/main/kotlin/org/sorapointa/event/Event.kt",
"chars": 2890,
"preview": "package org.sorapointa.event\n\nimport kotlinx.atomicfu.atomic\n\n/**\n * Event Interface, if you want to implement your own "
},
{
"path": "sorapointa-event/src/main/kotlin/org/sorapointa/event/EventManager.kt",
"chars": 10553,
"preview": "@file:Suppress(\"unused\")\n\npackage org.sorapointa.event\n\nimport com.charleskorn.kaml.YamlComment\nimport kotlinx.atomicfu."
},
{
"path": "sorapointa-event/src/main/kotlin/org/sorapointa/event/StateController.kt",
"chars": 10936,
"preview": "@file:Suppress(\"unused\")\n\npackage org.sorapointa.event\n\nimport kotlinx.atomicfu.atomic\nimport kotlinx.atomicfu.update\nim"
},
{
"path": "sorapointa-event/src/test/kotlin/org/sorapointa/event/EventPipelineTest.kt",
"chars": 4488,
"preview": "package org.sorapointa.event\n\nimport kotlinx.atomicfu.atomic\nimport kotlinx.coroutines.*\nimport org.junit.jupiter.api.Be"
},
{
"path": "sorapointa-event/src/test/kotlin/org/sorapointa/event/StateControllerTest.kt",
"chars": 6271,
"preview": "package org.sorapointa.event\n\nimport kotlinx.atomicfu.atomic\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.j"
},
{
"path": "sorapointa-i18n/build.gradle.kts",
"chars": 326,
"preview": "plugins {\n `sorapointa-conventions`\n `sorapointa-publish`\n kotlin(\"plugin.serialization\")\n}\n\ndependencies {\n "
},
{
"path": "sorapointa-i18n/src/main/kotlin/org/sorapointa/utils/I18n.kt",
"chars": 2258,
"preview": "package org.sorapointa.utils\n\nimport com.charleskorn.kaml.YamlComment\nimport kotlinx.serialization.KSerializer\nimport ko"
},
{
"path": "sorapointa-i18n/src/main/kotlin/org/sorapointa/utils/MessageBundle.kt",
"chars": 4274,
"preview": "package org.sorapointa.utils\n\nimport org.jetbrains.annotations.Nls\nimport org.jetbrains.annotations.NonNls\nimport org.je"
},
{
"path": "sorapointa-i18n/src/test/kotlin/org/sorapointa/utils/I18nTest.kt",
"chars": 1332,
"preview": "package org.sorapointa.utils\n\nimport org.junit.jupiter.api.Test\nimport java.util.*\nimport kotlin.test.assertEquals\n\nclas"
},
{
"path": "sorapointa-i18n/src/test/kotlin/org/sorapointa/utils/LocalSerializerTest.kt",
"chars": 437,
"preview": "package org.sorapointa.utils\n\nimport org.junit.jupiter.api.Test\nimport java.util.*\nimport kotlin.test.assertEquals\n\nclas"
},
{
"path": "sorapointa-i18n/src/test/kotlin/org/sorapointa/utils/TestBundle.kt",
"chars": 447,
"preview": "package org.sorapointa.utils\n\nimport org.jetbrains.annotations.Nls\nimport org.jetbrains.annotations.PropertyKey\nimport j"
},
{
"path": "sorapointa-i18n/src/test/resources/messages/TestBundle.properties",
"chars": 149,
"preview": "sora.test.simple=This is a simple test\nsora.test.english.only=This string is in default\nsora.test.parameterized=This is "
},
{
"path": "sorapointa-i18n/src/test/resources/messages/TestBundle_nl.properties",
"chars": 41,
"preview": "sora.test.simple=Dit is een simpele test\n"
},
{
"path": "sorapointa-native/.gitignore",
"chars": 20,
"preview": "### IDEA ###\n/.idea\n"
},
{
"path": "sorapointa-native/Cargo.toml",
"chars": 209,
"preview": "[package]\nname = \"spnative\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[profile.release]\nopt-level = 3\n\n[lib]\ncrate_type = [\"cd"
},
{
"path": "sorapointa-native/README.md",
"chars": 1489,
"preview": "# Native Module\n\n[简体中文](README.zh-CN.md)\n\nThis module provide Rust native library.\n\n## Build\n\nTo build this module, Rust"
},
{
"path": "sorapointa-native/README.zh-CN.md",
"chars": 998,
"preview": "# Native 模块\n\n[English](README.md)\n\n本模块提供 Rust 原生库。\n\n## 构建\n\n构建本模块需要 Rust 工具链。推荐通过 [Rustup](https://rustup.rs/) 安装,Unix-li"
},
{
"path": "sorapointa-native/build.gradle.kts",
"chars": 158,
"preview": "plugins {\n alias(libs.plugins.rust.wrapper)\n}\n\nrust {\n release.set(true)\n command.set(\"cargo\")\n targets {\n "
},
{
"path": "sorapointa-native/src/jnienv.rs",
"chars": 498,
"preview": "use jni::{descriptors::Desc, objects::JClass, JNIEnv};\n\npub trait JNIEnvExt<'a> {\n fn throw_new_or_eprint<'c, T>(&sel"
},
{
"path": "sorapointa-native/src/lib.rs",
"chars": 24,
"preview": "mod jnienv;\nmod logger;\n"
},
{
"path": "sorapointa-native/src/logger.rs",
"chars": 3770,
"preview": "use anyhow::{Context, Result};\nuse jni::objects::{JClass, JStaticMethodID};\nuse jni::signature::{Primitive, ReturnType};"
},
{
"path": "sorapointa-native-wrapper/README.md",
"chars": 854,
"preview": "# Native Wrapper Module\n\nThis module is a wrapper for [`sorapointa-native`](../sorapointa-native/README.zh-CN.md) with J"
},
{
"path": "sorapointa-native-wrapper/README.zh-CN.md",
"chars": 442,
"preview": "# Native Wrapper 模块\n\n本模块是对 [`sorapointa-native`](../sorapointa-native/README.zh-CN.md) 的 JNI 绑定和封装。\n\n## JNI 头文件\n\n可以使用 Gr"
},
{
"path": "sorapointa-native-wrapper/build.gradle.kts",
"chars": 529,
"preview": "plugins {\n `sorapointa-conventions`\n id(\"fr.stardustenterprises.rust.importer\")\n}\n\ndependencies {\n implementati"
},
{
"path": "sorapointa-native-wrapper/src/main/kotlin/org/sorapointa/rust/Setup.kt",
"chars": 427,
"preview": "package org.sorapointa.rust\n\nimport fr.stardustenterprises.yanl.NativeLoader\nimport java.util.concurrent.atomic.AtomicBo"
},
{
"path": "sorapointa-native-wrapper/src/main/kotlin/org/sorapointa/rust/logging/RustLogger.kt",
"chars": 1287,
"preview": "package org.sorapointa.rust.logging\n\nimport ch.qos.logback.classic.Logger\nimport mu.KotlinLogging\nimport org.slf4j.Logge"
},
{
"path": "sorapointa-native-wrapper/src/test/kotlin/org/sorapointa/rust/logging/LoggerTest.kt",
"chars": 311,
"preview": "package org.sorapointa.rust.logging\n\nimport org.junit.jupiter.api.Test\nimport kotlin.test.assertFailsWith\n\nobject Logger"
},
{
"path": "sorapointa-proto/build.gradle.kts",
"chars": 655,
"preview": "@file:Suppress(\"GradlePackageUpdate\")\n\nplugins {\n `sorapointa-conventions`\n `sorapointa-publish`\n id(\"com.squar"
},
{
"path": "sorapointa-proto/src/main/kotlin/org/sorapointa/proto/PacketUtils.kt",
"chars": 2106,
"preview": "package org.sorapointa.proto\n\nimport com.squareup.moshi.JsonAdapter\nimport com.squareup.moshi.Moshi\nimport com.squareup."
},
{
"path": "sorapointa-proto/src/main/kotlin/org/sorapointa/proto/ProtoInfo.kt",
"chars": 123652,
"preview": "@file:Suppress(\"unused\")\n\npackage org.sorapointa.proto\n\nconst val START_MAGIC: UShort = 0x4567u\nconst val END_MAGIC: USh"
},
{
"path": "sorapointa-proto/src/proto/AbilityAppliedAbility.proto",
"chars": 332,
"preview": "syntax = \"proto3\";\n\nimport \"AbilityScalarValueEntry.proto\";\nimport \"AbilityString.proto\";\n\noption java_package = \"org.so"
},
{
"path": "sorapointa-proto/src/proto/AbilityAppliedModifier.proto",
"chars": 700,
"preview": "syntax = \"proto3\";\n\nimport \"AbilityAttachedModifier.proto\";\nimport \"AbilityString.proto\";\nimport \"ModifierDurability.pro"
},
{
"path": "sorapointa-proto/src/proto/AbilityAttachedModifier.proto",
"chars": 257,
"preview": "syntax = \"proto3\";\n\noption java_package = \"org.sorapointa.proto\";\n\nmessage AbilityAttachedModifier {\n bool is_invalid ="
},
{
"path": "sorapointa-proto/src/proto/AbilityControlBlock.proto",
"chars": 180,
"preview": "syntax = \"proto3\";\n\nimport \"AbilityEmbryo.proto\";\n\noption java_package = \"org.sorapointa.proto\";\n\nmessage AbilityControl"
},
{
"path": "sorapointa-proto/src/proto/AbilityEmbryo.proto",
"chars": 193,
"preview": "syntax = \"proto3\";\n\noption java_package = \"org.sorapointa.proto\";\n\nmessage AbilityEmbryo {\n uint32 ability_id = 1;\n fi"
},
{
"path": "sorapointa-proto/src/proto/AbilityGadgetInfo.proto",
"chars": 181,
"preview": "syntax = \"proto3\";\n\noption java_package = \"org.sorapointa.proto\";\n\nmessage AbilityGadgetInfo {\n uint32 camp_id = 1;\n u"
},
{
"path": "sorapointa-proto/src/proto/AbilityMixinRecoverInfo.proto",
"chars": 457,
"preview": "syntax = \"proto3\";\n\nimport \"BreakoutSnapShot.proto\";\nimport \"MassivePropSyncInfo.proto\";\n\noption java_package = \"org.sor"
}
]
// ... and 279 more files (download for full content)
About this extraction
This page contains the full source code of the Sorapointa/Sorapointa GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 479 files (1.2 MB), approximately 394.1k tokens, and a symbol index with 11 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.