├── .github ├── actions │ ├── .gitignore │ ├── bun.lock │ ├── changelog.ts │ ├── package.json │ └── tsconfig.json └── workflows │ ├── buildPlugin.yml │ ├── checkScripts.yml │ └── release.yml ├── .gitignore ├── .gitprefix ├── README.md ├── README_en.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── Module.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── loader ├── bukkit │ ├── res │ │ └── plugin.yml │ └── src │ │ └── Main.kt ├── common │ ├── ConfigExt.kt │ ├── standalone │ │ ├── Main.kt │ │ └── loader.kt │ └── util │ │ ├── BuiltinScriptRegistry.kt │ │ ├── CASPackScriptRegistry.kt │ │ ├── CASScriptPacker.kt │ │ ├── CASScriptSource.kt │ │ ├── CAStore.kt │ │ └── CommonMain.kt └── mindustry │ ├── res │ ├── META-INF │ │ └── kotlin │ │ │ └── script │ │ │ └── templates │ │ │ └── .gitkeep │ └── plugin.json │ └── src │ ├── Loader.kt │ └── Main.kt ├── scripts ├── bootStrap │ ├── default.kts │ └── generate.kts ├── build.gradle.kts ├── coreLibrary │ ├── DBApi.kts │ ├── DBConnector.kts │ ├── commands │ │ ├── configCmd.kts │ │ ├── control.kts │ │ ├── helpful.kts │ │ ├── hotReload.kts │ │ ├── permissionCmd.kts │ │ └── varsCmd.kts │ ├── extApi │ │ ├── KVStore.kts │ │ ├── mongoApi.kts │ │ ├── redisApi.kts │ │ ├── remoteEventApi.kts │ │ ├── remoteEventApi.lib.kt │ │ └── rpcService.kts │ ├── kcp │ │ └── serialization.kts │ ├── lang.kts │ ├── lib │ │ ├── ColorApi.kt │ │ ├── CommandApi.kt │ │ ├── ConfigApi.kt │ │ ├── PermissionApi.kt │ │ ├── PlaceHoldApi.kt │ │ ├── event │ │ │ ├── RequestPermissionEvent.kt │ │ │ └── ServiceProvidedEvent.kt │ │ └── util │ │ │ ├── ReflectHelper.kt │ │ │ ├── ServiceRegistry.kt │ │ │ ├── coroutine.kt │ │ │ ├── menu.kt │ │ │ └── nextEvent.kt │ ├── module.kts │ └── variables.kts ├── coreMindustry │ ├── console.kts │ ├── contentsTweaker.kts │ ├── lib │ │ ├── CommandExt.kt │ │ ├── CommandImpl.kt │ │ ├── ContentExt.kt │ │ ├── ContentHelper.kt │ │ ├── DispatcherExt.kt │ │ ├── ListenExt.kt │ │ └── PermissionExt.kt │ ├── menu.kts │ ├── menu.lib.kt │ ├── menu.new.kt │ ├── module.kts │ ├── scorebroad.kts │ ├── util │ │ ├── packetHelper.kts │ │ ├── spawnAround.api.kt │ │ ├── spawnAround.kts │ │ ├── trackBuilding.api.kt │ │ └── trackBuilding.kts │ ├── utilMapRule.kts │ ├── utilNextChat.kts │ ├── utilTextInput.kts │ └── variables.kts ├── mapScript │ ├── lib │ │ ├── ContentExt.kt │ │ ├── GeneratorSupport.kt │ │ ├── TagSupport.kt │ │ └── util.kt │ ├── module.kts │ ├── shared │ │ └── posMark.kts │ └── tags │ │ ├── limitAir.kts │ │ └── mapRule.kts ├── metadata │ ├── coreLibrary.metadata │ ├── coreMindustry.metadata │ ├── mapScript.metadata │ └── wayzer.metadata └── wayzer │ ├── cmds │ ├── clearUnit.kts │ ├── gatherTp.kts │ ├── helpfulCmd.kts │ ├── jsCmd.kts │ ├── mapsCmd.kts │ ├── pixelPicture.kts │ ├── restart.kts │ ├── serverStatus.kts │ ├── spawnMob.kts │ ├── vote.kts │ ├── voteKick.kts │ ├── voteMap.kts │ └── voteOb.kts │ ├── ext │ ├── alert.kts │ ├── autoUpdate.kts │ ├── goServer.kts │ ├── observer.kts │ ├── tpsLimit.kts │ └── welcomeMsg.kts │ ├── lib │ ├── ConnectAsyncEvent.kt │ └── PlayerData.kt │ ├── map │ ├── autoHost.kts │ ├── autoSave.kts │ ├── backCompatibility.kts │ ├── betterTeam.kts │ ├── mapInfo.kts │ ├── mapSnap.block_colors.png │ ├── mapSnap.kts │ ├── pvpProtect.kts │ └── resourceHelper.kts │ ├── maps.kts │ ├── maps.manager.kt │ ├── maps.registry.kt │ ├── module.kts │ ├── pvp │ ├── autoGameover.kts │ ├── pvpAlert.kts │ └── pvpChat.kts │ ├── reGrief │ ├── bugFixer.kts │ ├── history.kts │ ├── limitFire.kts │ └── unitLimit.kts │ ├── user │ ├── ban.kts │ ├── banStore.kts │ ├── ext │ │ └── skills.kts │ ├── lang.kts │ ├── nameExt.kts │ ├── shortID.kts │ └── suffix.kts │ ├── vote.kts │ └── vote.lib.kt └── settings.gradle.kts /.github/actions/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /.github/actions/changelog.ts: -------------------------------------------------------------------------------- 1 | import * as core from "@actions/core" 2 | import {context, getOctokit} from "@actions/github"; 3 | 4 | 5 | const token = process.env["GITHUB_TOKEN"] || core.getInput("token") 6 | const octokit = getOctokit(token) 7 | 8 | const lastRelease = (await octokit.rest.repos.listReleases(context.repo)).data[0]?.tag_name 9 | core.info("Find last release: " + lastRelease) 10 | 11 | const compare = (await octokit.rest.repos.compareCommits({ 12 | ...context.repo, base: lastRelease, head: context.sha 13 | })).data 14 | 15 | const changes = compare.commits.map(({sha, author, commit: {message}}) => { 16 | const [title, ...body] = message.split("\n") 17 | 18 | let out = `* ${title} @${author!.login} (${sha.substring(0, 8)})` 19 | if (body.length) 20 | out += body.map(it => `\n > ${it}`).join("") 21 | return out 22 | }).join("\n") 23 | core.setOutput("changes", changes) 24 | 25 | const changeFiles = (compare.files || []).map(file => { 26 | switch (file.status) { 27 | case 'modified': 28 | return `* :memo: ${file.filename} +${file.additions} -${file.deletions}` 29 | case 'added': 30 | return `* :heavy_plus_sign: ${file.filename}` 31 | case "removed": 32 | return `* :fire: ${file.filename}` 33 | case "renamed": 34 | return `* :truck: ${file.filename} <= ${file.previous_filename}` 35 | default: 36 | return `* ${file.status} ${file.filename}` 37 | } 38 | }).join("\n") 39 | 40 | core.setOutput("releaseBody", ` 41 | ## 更新日记 42 | 43 | ${changes} 44 | 45 | ## 文件变更 46 | 47 |
48 | ${compare.files?.length || 0} 文件 49 | 50 | ${changeFiles} 51 | 52 |
53 | 54 | [完整对比](${compare.html_url}) [获取patch](${compare.patch_url}) 55 | `) -------------------------------------------------------------------------------- /.github/actions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "actions", 3 | "private": true, 4 | "devDependencies": { 5 | "@types/bun": "latest" 6 | }, 7 | "peerDependencies": { 8 | "typescript": "^5" 9 | }, 10 | "dependencies": { 11 | "@actions/core": "^1.11.1", 12 | "@actions/github": "^6.0.0" 13 | } 14 | } -------------------------------------------------------------------------------- /.github/actions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/buildPlugin.yml: -------------------------------------------------------------------------------- 1 | name: BuildPlugin 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'private*' 7 | paths: 8 | - 'loader/' 9 | - 'build.gradle.kts' 10 | pull_request: 11 | paths: 12 | - 'loader/' 13 | - 'build.gradle.kts' 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v4 27 | 28 | - uses: actions/cache@v4 29 | with: 30 | path: | 31 | ~/.gradle/caches 32 | ~/.gradle/wrapper 33 | key: deps-${{ hashFiles('build.gradle.kts', '**/gradle-wrapper.properties') }} 34 | restore-keys: | 35 | deps- 36 | 37 | # Runs a single command using the runners shell 38 | - name: Run gradle buildPlugin 39 | run: ./gradlew buildPlugin 40 | 41 | - name: Upload a Build Artifact 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: ScriptAgent-beta-${{github.run_num}}.jar 45 | path: build/libs 46 | -------------------------------------------------------------------------------- /.github/workflows/checkScripts.yml: -------------------------------------------------------------------------------- 1 | name: CheckScript 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - 'private*' 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 11 | jobs: 12 | # This workflow contains a single job called "build" 13 | build: 14 | # The type of runner that the job will run on 15 | runs-on: ubuntu-latest 16 | 17 | # Steps represent a sequence of tasks that will be executed as part of the job 18 | steps: 19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | - uses: actions/cache@v4 24 | with: 25 | path: | 26 | ~/.gradle/caches 27 | ~/.gradle/wrapper 28 | key: deps-${{ hashFiles('build.gradle.kts', '**/gradle-wrapper.properties') }} 29 | restore-keys: | 30 | deps- 31 | - uses: actions/cache@v4 32 | with: 33 | path: libs 34 | key: sa-deps-${{ hashFiles('scripts/build.gradle.kts') }} 35 | restore-keys: | 36 | sa-deps- 37 | - name: Get current date 38 | id: date 39 | run: echo "::set-output name=date::$(date +'%Y-%m-%d')" 40 | - uses: actions/cache@v4 41 | with: 42 | path: | 43 | scripts/cache 44 | key: kts-cache-${{ steps.date.outputs.date }} 45 | restore-keys: | 46 | kts-cache 47 | 48 | # Runs a single command using the runners shell 49 | - name: Run gradle build 50 | run: ./gradlew precompile 51 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | Release: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | submodules: recursive 17 | - uses: oven-sh/setup-bun@v2 18 | with: 19 | bun-version: latest 20 | - name: Generate Changelog 21 | id: changelog 22 | run: bun run .github/actions/changelog.ts 23 | env: 24 | INPUT_TOKEN: ${{ github.token }} 25 | 26 | - uses: actions/cache@v4 27 | with: 28 | path: | 29 | ~/.gradle/caches 30 | ~/.gradle/wrapper 31 | key: deps-${{ hashFiles('build.gradle.kts', '**/gradle-wrapper.properties') }} 32 | restore-keys: | 33 | deps- 34 | - uses: actions/cache@v4 35 | with: 36 | path: libs 37 | key: sa-deps-${{ hashFiles('scripts/build.gradle.kts') }} 38 | restore-keys: | 39 | sa-deps- 40 | 41 | - name: Run unit tests and build JAR 42 | run: ./gradlew buildPlugin precompileZip allInOneJar 43 | 44 | - name: upload artifacts 45 | uses: softprops/action-gh-release@v1 46 | with: 47 | prerelease: true 48 | name: "${{github.ref_name}}" 49 | body: ${{steps.changelog.outputs.releaseBody}} 50 | files: | 51 | build/distributions/* 52 | build/libs/* 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Gradle template 3 | .gradle 4 | build/ 5 | 6 | # Ignore Gradle GUI config 7 | gradle-app.setting 8 | 9 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 10 | !gradle-wrapper.jar 11 | 12 | # Cache of project 13 | .gradletasknamecache 14 | 15 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 16 | # gradle/wrapper/gradle-wrapper.properties 17 | .idea 18 | .iml 19 | 20 | # ScriptAgent 21 | /scripts/cache 22 | /scripts/data 23 | /libs 24 | /branches/ 25 | /scripts/@*/ -------------------------------------------------------------------------------- /.gitprefix: -------------------------------------------------------------------------------- 1 | //|---文件增减---| 2 | :heavy_plus_sign: 新模块或脚本 3 | :truck: 移动 4 | :fire: 删除 5 | //|代码变化| 6 | :construction: WIP 7 | :arrow_up: 升级依赖或跟随更新 8 | :sparkles: 功能更新或优化 9 | :bug: 修复Bug 10 | :wrench: 非功能性改动 11 | //|其他| 12 | :memo: 其他非代码更新 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![For Mindustry](https://img.shields.io/badge/For-Mindustry-orange) 2 | ![Lang CN](https://img.shields.io/badge/Lang-ZH--CN-blue) 3 | ![Support 8.0](https://img.shields.io/badge/Support_Version-8.0(147+)-success) 4 | ![GitHub Releases](https://img.shields.io/github/downloads/way-zer/ScriptAgent4MindustryExt/latest/total) 5 | [![BuildPlugin](https://github.com/way-zer/ScriptAgent4MindustryExt/actions/workflows/buildPlugin.yml/badge.svg)](https://github.com/way-zer/ScriptAgent4MindustryExt/actions/workflows/buildPlugin.yml) 6 | [![CheckScript](https://github.com/way-zer/ScriptAgent4MindustryExt/actions/workflows/checkScripts.yml/badge.svg)](https://github.com/way-zer/ScriptAgent4MindustryExt/actions/workflows/checkScripts.yml) 7 | 8 | > For English README see [README_en](./README_en.md) 9 | 10 | ## ScriptAgent 11 | 一套基于Kotlin脚本(kts)的模块化框架 12 | - 强大:基于Kotlin,可以访问所有Java接口(所有插件能实现的功能,脚本都能实现) 13 | - 高效:脚本加载后转换为JVM字节码,与Java插件性能无异 14 | - 灵活:模块和脚本具有完整生命周期,支持热加载和热重载 15 | - 快速开发:提供大量实用辅助函数,无需编译即可快速部署到服务器 16 | - 智能:开发时支持IDEA或Android Studio的智能补全 17 | - 可定制:除核心部分外,插件功能均通过脚本实现,可根据需求自由修改,模块定义脚本还可扩展DSL 18 | 19 | 加载器(jar)本身无具体功能,仅负责脚本的加载与管理,所有功能均由脚本实现。 20 | 21 | ### ScriptAgent for Mindustry (SA4MDT) 22 | 该框架针对Mindustry的实现,包含加载器(Loader)和一系列功能脚本,具体分为以下6个模块: 23 | - coreLib(coreLibrary):框架的标准库 24 | - core(coreMindustry):针对Mindustry的具体实现 25 | - main模块:用于存放简单脚本 26 | - wayzer模块:一套完整的Mindustry服务器基础插件(By: WayZer) 27 | - 交流QQ群:1033116078 或直接在Discussions讨论 28 | - 插件测试服务器:cn.mindustry.top 29 | - mapScript:专为MDT设计的特殊脚本,生命周期与单局游戏绑定,仅在需要时加载 30 | - ~~mirai模块:QQ机器人库mirai的脚本封装(因上游不可控因素,计划移除)~~ 31 | 32 | ### 客户端预览 33 | ![image](https://user-images.githubusercontent.com/15688938/132090295-59a57f81-cc72-4ab5-8c10-deadf7ae452a.png) 34 | ![image](https://user-images.githubusercontent.com/15688938/132090317-cc62339d-8ce5-4906-90d0-e8fda1bacf36.png) 35 | 36 | ### 服务器后台预览 37 | ![image](https://user-images.githubusercontent.com/15688938/132090197-e041d11c-e09a-49ee-94e8-d2cdae30038f.png) 38 | ![image](https://user-images.githubusercontent.com/15688938/132090212-1f924326-4ba7-43be-bbb8-e055599fa75c.png) 39 | ![image](https://user-images.githubusercontent.com/15688938/132090238-bbfcaf2e-154a-446c-9d1f-92f391835f0a.png) 40 | 41 | ## 快速入门 42 | ### 插件安装(推荐普通用户使用) 43 | allInOne版本在加载器内集成了编译好的脚本 44 | 1. 从Release页面下载`xxx.allinone.jar`文件,并将其放置在`config/mods`目录下 45 | 2. 启动服务器(首次启动会从网络下载依赖,耗时较长) 46 | 47 | ### 加载器+脚本安装(高级用户) 48 | 1. 从Release页面下载预编译的jar和脚本包zip 49 | 2. 将jar文件放置在`config/mods`文件夹下,将脚本包解压到`config/scripts`文件夹(需自行创建) 50 | 3. 启动服务器(首次启动会从网络下载依赖,耗时较长) 51 | 4. 等待插件加载完成(脚本首次运行会进行编译,耗时较长,编译完成后会保存缓存) 52 | 53 | ### 独立运行/脚本开发 54 | 请查阅[Wiki](https://github.com/way-zer/ScriptAgent4MindustryExt/wiki) 55 | 56 | ## 版权说明 57 | - 加载器:免费使用,未经许可禁止转载和用作其他用途 58 | - 本仓库脚本: 59 | - 默认允许私人修改并使用,但禁止修改原作者版权信息,公开使用需注明出处(fork或引用该仓库) 60 | - mirai模块及依赖该模块的所有代码,遵循AGPLv3协议 61 | - 其他脚本:归脚本作者所有,作者可自行声明开源协议,不受加载器版权影响 -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | sourceSets.main { 6 | kotlin.srcDir("src") 7 | } 8 | 9 | repositories { 10 | mavenCentral() 11 | } -------------------------------------------------------------------------------- /buildSrc/src/Module.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("unused") 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.artifacts.dsl.DependencyHandler 5 | import org.gradle.api.tasks.SourceSet 6 | import org.gradle.api.tasks.SourceSetContainer 7 | import org.gradle.kotlin.dsl.kotlin 8 | import org.gradle.kotlin.dsl.project 9 | 10 | class ModuleScope(val moduleId: String, private val project: Project, private val sourceSet: SourceSet) { 11 | fun DependencyHandler.dependsModule(module: String, project: Project = this@ModuleScope.project) = 12 | add(sourceSet.apiConfigurationName, project(project.path, module + "Exposed")) 13 | 14 | fun DependencyHandler.api(dep: Any) = add(sourceSet.apiConfigurationName, dep) 15 | fun DependencyHandler.implementation(dep: Any) = add(sourceSet.implementationConfigurationName, dep) 16 | } 17 | 18 | fun Project.defineModule( 19 | name: String, 20 | srcDir: String = name, 21 | body: ModuleScope.() -> Unit, 22 | ) { 23 | val sourceSet = sourceSets.create(name) { 24 | java.srcDir(srcDir) 25 | } 26 | val exposed = configurations.create(name + "Exposed") { 27 | extendsFrom(configurations.getByName(name + "Api")) 28 | isCanBeConsumed = true 29 | } 30 | dependencies.apply { 31 | add(exposed.name, sourceSet.output) 32 | } 33 | ModuleScope(name, project, sourceSet).apply { 34 | dependencies.apply { 35 | implementation(kotlin("script-runtime")) 36 | implementation(rootProject) 37 | } 38 | body() 39 | } 40 | } 41 | 42 | private val Project.sourceSets 43 | get() = project.extensions.getByType( 44 | SourceSetContainer::class.java 45 | ) -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.jvm.target.validation.mode = IGNORE -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/way-zer/ScriptAgent4MindustryExt/03cec71cfe69cf4936140d76030feb121bd2a670/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /loader/bukkit/res/plugin.yml: -------------------------------------------------------------------------------- 1 | name: ScriptAgent 2 | main: cf.wayzer.scriptAgent.bukkit.Main 3 | version: "${version}" 4 | author: Way__Zer 5 | api-version: 1.21 6 | folia-supported: true 7 | libraries: 8 | - org.jetbrains.kotlin:kotlin-stdlib:2.1.10 9 | - org.jetbrains.kotlin:kotlin-reflect:2.1.10 10 | - org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1 11 | - com.google.guava:guava:31.1-jre 12 | - org.jetbrains.kotlin:kotlin-scripting-jvm:2.1.10 13 | commands: 14 | ScriptAgent: 15 | description: ScriptAgent Main Command 16 | usage: Please load coreBukkit module to use command 17 | aliases: ["sa"] -------------------------------------------------------------------------------- /loader/bukkit/src/Main.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.bukkit 2 | 3 | import cf.wayzer.scriptAgent.Config 4 | import cf.wayzer.scriptAgent.ScriptManager 5 | import cf.wayzer.scriptAgent.define.LoaderApi 6 | import cf.wayzer.scriptAgent.util.CommonMain 7 | import cf.wayzer.scriptAgent.util.DSLBuilder 8 | import cf.wayzer.scriptAgent.util.DependencyManager 9 | import cf.wayzer.scriptAgent.util.maven.Dependency 10 | import kotlinx.coroutines.runBlocking 11 | import org.bukkit.command.PluginCommand 12 | import org.bukkit.plugin.java.JavaPlugin 13 | 14 | @OptIn(LoaderApi::class) 15 | class Main : JavaPlugin(), CommonMain { 16 | private var Config.pluginMain by DSLBuilder.lateInit() 17 | private var Config.pluginCommand by DSLBuilder.lateInit() 18 | private var Config.delayEnable by DSLBuilder.lateInit>() 19 | 20 | override fun onLoad() { 21 | if (!dataFolder.exists()) dataFolder.mkdirs() 22 | initConfigInfo(dataFolder, pluginMeta.version) 23 | Config.libraryDir = Config.cacheDir.resolve("libs").toPath() 24 | Config.logger = logger 25 | 26 | Config.pluginMain = this 27 | Config.delayEnable = mutableListOf() 28 | 29 | DependencyManager { 30 | require(Dependency.parse("org.jetbrains.kotlin:kotlin-stdlib:${Config.kotlinVersion}")) 31 | require(Dependency.parse("org.jetbrains.kotlin:kotlin-reflect:${Config.kotlinVersion}")) 32 | require(Dependency.parse("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Config.kotlinCoroutineVersion}")) 33 | addAsGlobal() 34 | load() 35 | } 36 | 37 | bootstrap() 38 | } 39 | 40 | override fun onEnable() { 41 | Config.pluginCommand = getCommand("ScriptAgent")!! 42 | Config.delayEnable.toList().let { list -> 43 | Config.delayEnable.clear() 44 | list.forEach { it.run() } 45 | } 46 | } 47 | 48 | override fun onDisable() { 49 | runBlocking { 50 | ScriptManager.disableAll() 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /loader/common/ConfigExt.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnusedReceiverParameter") 2 | 3 | package cf.wayzer.scriptAgent 4 | 5 | import cf.wayzer.scriptAgent.util.DSLBuilder 6 | 7 | //Experimental 8 | object MainScriptsHelper { 9 | const val defaultMain = "bootStrap/default" 10 | var list: List = emptyList() 11 | private set 12 | private var cur = 0 13 | val current get() = list.getOrElse(cur) { defaultMain } 14 | 15 | internal fun load() { 16 | val params = System.getenv("SAMain") 17 | Config.logger.info("SAMain=${params ?: defaultMain}") 18 | if (params != null) 19 | list = params.split(";") 20 | } 21 | 22 | fun next(): String { 23 | if (cur < list.size) cur++ 24 | else error("Already default main.") 25 | return current 26 | } 27 | } 28 | 29 | var Config.args by DSLBuilder.dataKeyWithDefault> { emptyArray() } 30 | internal set 31 | var Config.version by DSLBuilder.lateInit() 32 | internal set 33 | val Config.mainScript get() = MainScriptsHelper.current 34 | fun Config.nextMainScript() = MainScriptsHelper.next() -------------------------------------------------------------------------------- /loader/common/standalone/Main.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.standalone 2 | 3 | import cf.wayzer.libraryManager.MutableURLClassLoader 4 | import cf.wayzer.scriptAgent.ScriptRegistry 5 | import cf.wayzer.scriptAgent.util.CommonMain 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.runBlocking 8 | import java.io.File 9 | import kotlin.system.exitProcess 10 | 11 | object Main : CommonMain { 12 | @JvmStatic 13 | fun main(args: Array) = runBlocking { 14 | initConfigInfo( 15 | rootDir = File(System.getenv("SARoot") ?: "scripts"), 16 | version = javaClass.getResource("/META-INF/ScriptAgent/Version")?.readText() ?: "Unknown Version", 17 | args = args 18 | ) 19 | (javaClass.classLoader as MutableURLClassLoader).addURL(File("nativeLibs").toURI().toURL()) 20 | 21 | bootstrap() 22 | 23 | if (ScriptRegistry.allScripts { it.enabled }.isEmpty()) { 24 | println("No Script Enabled") 25 | exitProcess(-1) 26 | } 27 | while (ScriptRegistry.allScripts { it.enabled }.isNotEmpty()) 28 | delay(1_000) 29 | println("Bye!!") 30 | } 31 | } -------------------------------------------------------------------------------- /loader/common/standalone/loader.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.standalone 2 | 3 | import cf.wayzer.scriptAgent.* 4 | import cf.wayzer.scriptAgent.define.LoaderApi 5 | 6 | @OptIn(LoaderApi::class) 7 | fun main(args: Array?) { 8 | if (System.getProperty("java.util.logging.SimpleFormatter.format") == null) 9 | System.setProperty("java.util.logging.SimpleFormatter.format", "[%1\$tF | %1\$tT | %4\$s] [%3\$s] %5\$s%6\$s%n") 10 | ScriptAgent.loadUseClassLoader()?.apply { 11 | loadClass("cf.wayzer.scriptAgent.standalone.Main") 12 | .getMethod("main", Array::class.java) 13 | .invoke(null, args) 14 | } 15 | } -------------------------------------------------------------------------------- /loader/common/util/BuiltinScriptRegistry.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.util 2 | 3 | import cf.wayzer.scriptAgent.Config 4 | import cf.wayzer.scriptAgent.ScriptRegistry 5 | import cf.wayzer.scriptAgent.define.SAExperimentalApi 6 | import cf.wayzer.scriptAgent.define.ScriptSource 7 | import java.net.URL 8 | 9 | object BuiltinScriptRegistry : ScriptRegistry.IRegistry { 10 | @OptIn(SAExperimentalApi::class) 11 | class SourceImpl(meta: MetadataFile) : CASScriptSource(meta) { 12 | override fun getURL(hash: String): URL = javaClass.getResource("/builtin/CAS/$hash") 13 | ?: error("No builtin resource: $hash") 14 | } 15 | 16 | private val loaded by lazy { 17 | javaClass.getResourceAsStream("/builtin/META")?.reader() 18 | ?.useLines { MetadataFile.readAll(it.iterator()) }.orEmpty() 19 | .map { SourceImpl(it) } 20 | .also { Config.logger.info("BuiltinScriptRegistry found ${it.size} scripts") } 21 | .associateBy { it.id } 22 | } 23 | 24 | override fun scan(): Collection = loaded.values 25 | } -------------------------------------------------------------------------------- /loader/common/util/CASPackScriptRegistry.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.util 2 | 3 | import cf.wayzer.scriptAgent.Config 4 | import cf.wayzer.scriptAgent.ScriptRegistry 5 | import cf.wayzer.scriptAgent.define.SAExperimentalApi 6 | import cf.wayzer.scriptAgent.define.ScriptSource 7 | import java.io.File 8 | import java.net.URL 9 | 10 | @SAExperimentalApi 11 | object CASPackScriptRegistry : ScriptRegistry.IRegistry { 12 | val files = mutableSetOf() 13 | private var cache = emptyMap() 14 | 15 | class SourceImpl(var file: File, meta: MetadataFile) : CASScriptSource(meta) { 16 | override fun getURL(hash: String): URL? = 17 | if (!file.exists()) null else URL("jar:${file.toURI()}!/CAS/$hash") 18 | } 19 | 20 | override fun scan(): Collection { 21 | files.removeIf { !it.exists() } 22 | files += Config.rootDir.listFiles().orEmpty().filter { it.name.endsWith(".packed.zip") } 23 | if (files.size > 0) println("found packed files: ${files.joinToString { it.name }}") 24 | cache = buildMap { 25 | for (file in files) { 26 | val metas = URL("jar:${file.toURI()}!/META") 27 | .openStream().bufferedReader().use { it.readLines() } 28 | .let { MetadataFile.readAll(it.iterator()) } 29 | for (meta in metas) { 30 | if (meta.id in this) continue 31 | val source = SourceImpl(file, meta) 32 | //reuse if not changed 33 | this[source.id] = cache[source.id] 34 | ?.takeIf { it.hash == source.hash && it.resourceHashes == source.resourceHashes } 35 | ?.also { it.file = file } ?: source 36 | } 37 | } 38 | } 39 | return cache.values 40 | } 41 | } -------------------------------------------------------------------------------- /loader/common/util/CASScriptPacker.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.util 2 | 3 | import cf.wayzer.scriptAgent.define.SAExperimentalApi 4 | import cf.wayzer.scriptAgent.impl.SACompiledScript 5 | import cf.wayzer.scriptAgent.impl.ScriptCache 6 | import java.io.OutputStream 7 | import java.security.MessageDigest 8 | import java.util.zip.ZipEntry 9 | import java.util.zip.ZipOutputStream 10 | 11 | @SAExperimentalApi 12 | class CASScriptPacker(stream: OutputStream) : AutoCloseable { 13 | val metas = mutableListOf() 14 | private val added = mutableSetOf() 15 | private val digest = MessageDigest.getInstance("MD5")!! 16 | private val zip = ZipOutputStream(stream) 17 | 18 | fun addCAS(bs: ByteArray): String { 19 | @OptIn(ExperimentalStdlibApi::class) 20 | val hash = digest.digest(bs).toHexString() 21 | if (added.add(hash)) { 22 | zip.putNextEntry(ZipEntry("CAS/$hash")) 23 | zip.write(bs) 24 | zip.closeEntry() 25 | } 26 | return hash 27 | } 28 | 29 | override fun close() { 30 | zip.putNextEntry(ZipEntry("META")) 31 | zip.bufferedWriter().let { f -> 32 | metas.sortedBy { it.id } 33 | metas.forEach { it.writeTo(f) } 34 | f.flush() 35 | } 36 | zip.closeEntry() 37 | zip.close() 38 | } 39 | 40 | fun add(script: SACompiledScript) { 41 | val scriptMD5 = addCAS(script.compiledFile.readBytes()) 42 | val resources = script.source.listResources() 43 | .map { it.name to addCAS(it.loadFile().readBytes()) } 44 | .sortedBy { it.first } 45 | .map { "${it.first} ${it.second}" } 46 | 47 | val meta = ScriptCache.asMetadata(script) 48 | metas += meta.copy( 49 | attr = meta.attr + ("HASH" to scriptMD5), 50 | data = meta.data + ("RESOURCE" to resources) 51 | ) 52 | } 53 | } -------------------------------------------------------------------------------- /loader/common/util/CASScriptSource.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.util 2 | 3 | import cf.wayzer.scriptAgent.define.SAExperimentalApi 4 | import cf.wayzer.scriptAgent.define.ScriptInfo 5 | import cf.wayzer.scriptAgent.define.ScriptResourceFile 6 | import cf.wayzer.scriptAgent.define.ScriptSource 7 | import java.io.File 8 | import java.net.URL 9 | 10 | @SAExperimentalApi 11 | abstract class CASScriptSource( 12 | override val scriptInfo: ScriptInfo, 13 | val hash: String, 14 | val resourceHashes: Map, 15 | ) : ScriptSource.Compiled { 16 | abstract fun getURL(hash: String): URL? 17 | class ResourceImpl( 18 | override val name: String, 19 | private val hash: String, 20 | private val originUrl: URL 21 | ) : ScriptResourceFile { 22 | override val url: URL get() = CAStore.get(hash)?.toURI()?.toURL() ?: originUrl 23 | override fun loadFile(): File = CAStore.getOrLoad(hash, originUrl) 24 | } 25 | 26 | override fun listResources(): Collection = resourceHashes 27 | .mapNotNull { getURL(it.value)?.let { url -> ResourceImpl(it.key, it.value, url) } } 28 | 29 | override fun findResource(name: String): ScriptResourceFile? { 30 | val hash = resourceHashes[name] ?: return null 31 | val url = getURL(hash) ?: return null 32 | return ResourceImpl(name, hash, url) 33 | } 34 | 35 | 36 | override fun compiledValid(): Boolean = getURL(hash) != null 37 | override fun loadCompiled(): File = getURL(hash)?.let { CAStore.getOrLoad(hash, it) } 38 | ?: error("Can't load compiled script") 39 | 40 | constructor(meta: MetadataFile) : this( 41 | ScriptInfo.getOrCreate(meta.id), 42 | meta.attr["HASH"] ?: error("Break META: ${meta.id}, require hash"), 43 | meta.data["RESOURCE"].orEmpty().associate { 44 | val (name, hash) = it.split(' ', limit = 2) 45 | name to hash 46 | } 47 | ) 48 | } -------------------------------------------------------------------------------- /loader/common/util/CAStore.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.util 2 | 3 | import cf.wayzer.scriptAgent.Config 4 | import java.io.File 5 | import java.net.URL 6 | 7 | /** Contents Addressed Store*/ 8 | object CAStore { 9 | private val base = Config.cacheDir.resolve("by_md5").also { it.mkdirs() } 10 | fun getUncheck(md5: String) = base.resolve(md5) 11 | fun get(md5: String) = base.resolve(md5).takeIf { it.exists() } 12 | inline fun getOrLoad(md5: String, loader: (File) -> Unit): File { 13 | val file = getUncheck(md5) 14 | if (!file.exists()) { 15 | loader(file) 16 | } 17 | return file 18 | } 19 | 20 | fun getOrLoad(md5: String, url: URL): File { 21 | return getOrLoad(md5) { f -> 22 | url.openStream().use { inS -> 23 | f.outputStream().use { out -> 24 | inS.copyTo(out) 25 | } 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /loader/common/util/CommonMain.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.util 2 | 3 | import cf.wayzer.scriptAgent.* 4 | import cf.wayzer.scriptAgent.define.SAExperimentalApi 5 | import kotlinx.coroutines.runBlocking 6 | import java.io.File 7 | 8 | interface CommonMain { 9 | private suspend fun doStart(): Boolean { 10 | val mainScript = ScriptRegistry.getScriptInfo(Config.mainScript) ?: return false 11 | ScriptManager.transaction { 12 | add(mainScript) 13 | load();enable() 14 | } 15 | return true 16 | } 17 | 18 | fun initConfigInfo(rootDir: File, version: String, args: Array = emptyArray()) { 19 | Config.rootDir = rootDir 20 | Config.version = version 21 | Config.args = args 22 | } 23 | 24 | fun bootstrap() { 25 | MainScriptsHelper.load() 26 | @OptIn(SAExperimentalApi::class) 27 | ScriptRegistry.registries.add(CASPackScriptRegistry) 28 | ScriptRegistry.registries.add(BuiltinScriptRegistry) 29 | ScriptRegistry.scanRoot() 30 | val foundMain = runBlocking { doStart() } 31 | displayInfo(foundMain) 32 | } 33 | 34 | fun displayInfo(foundMain: Boolean) { 35 | Config.logger.info("===========================") 36 | Config.logger.info(" ScriptAgent ${Config.version} ") 37 | Config.logger.info(" By WayZer ") 38 | Config.logger.info("QQ交流群: 1033116078") 39 | if (foundMain) { 40 | val all = ScriptRegistry.allScripts { true } 41 | Config.logger.info( 42 | "共找到${all.size}脚本,加载成功${all.count { it.scriptState.loaded }},启用成功${all.count { it.scriptState.enabled }},出错${all.count { it.failReason != null }}" 43 | ) 44 | } else 45 | Config.logger.warning("未找到启动脚本(SAMain=${Config.mainScript}),请下载安装脚本包,以发挥本插件功能") 46 | Config.logger.info("===========================") 47 | } 48 | } -------------------------------------------------------------------------------- /loader/mindustry/res/META-INF/kotlin/script/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/way-zer/ScriptAgent4MindustryExt/03cec71cfe69cf4936140d76030feb121bd2a670/loader/mindustry/res/META-INF/kotlin/script/templates/.gitkeep -------------------------------------------------------------------------------- /loader/mindustry/res/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ScriptAgent4Mindustry", 3 | "author": "WayZer", 4 | "main": "cf.wayzer.scriptAgent.mindustry.Loader", 5 | "description": "More commands and features.", 6 | "version": "${version}", 7 | "minGameVersion": 147 8 | } 9 | -------------------------------------------------------------------------------- /loader/mindustry/src/Loader.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.mindustry 2 | 3 | import arc.util.CommandHandler 4 | import cf.wayzer.scriptAgent.* 5 | import cf.wayzer.scriptAgent.define.LoaderApi 6 | import mindustry.mod.Plugin 7 | 8 | @Suppress("unused")//ref by plugin.json 9 | class Loader : Plugin() { 10 | private val impl: Plugin 11 | 12 | init { 13 | if (System.getProperty("java.util.logging.SimpleFormatter.format") == null) 14 | System.setProperty( 15 | "java.util.logging.SimpleFormatter.format", 16 | "[%1\$tF | %1\$tT | %4\$s] [%3\$s] %5\$s%6\$s%n" 17 | ) 18 | @OptIn(LoaderApi::class) 19 | impl = ScriptAgent.loadUseClassLoader() 20 | ?.loadClass(Main::class.java.name) 21 | ?.getConstructor(Plugin::class.java) 22 | ?.newInstance(this) as Plugin? 23 | ?: error("Fail newInstance") 24 | } 25 | 26 | override fun registerClientCommands(handler: CommandHandler?) { 27 | impl.registerClientCommands(handler) 28 | } 29 | 30 | override fun registerServerCommands(handler: CommandHandler?) { 31 | impl.registerServerCommands(handler) 32 | } 33 | 34 | override fun init() { 35 | impl.init() 36 | } 37 | } -------------------------------------------------------------------------------- /loader/mindustry/src/Main.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.scriptAgent.mindustry 2 | 3 | import arc.ApplicationListener 4 | import arc.Core 5 | import arc.files.Fi 6 | import arc.util.CommandHandler 7 | import arc.util.Log 8 | import cf.wayzer.scriptAgent.* 9 | import cf.wayzer.scriptAgent.define.LoaderApi 10 | import cf.wayzer.scriptAgent.util.CommonMain 11 | import cf.wayzer.scriptAgent.util.DSLBuilder 12 | import kotlinx.coroutines.runBlocking 13 | import mindustry.Vars 14 | import mindustry.mod.Plugin 15 | import java.io.File 16 | 17 | @OptIn(LoaderApi::class) 18 | class Main(private val loader: Plugin) : Plugin(), CommonMain { 19 | //Mindustry 20 | private var Config.clientCommands by DSLBuilder.lateInit() 21 | private var Config.serverCommands by DSLBuilder.lateInit() 22 | override fun getConfig(): Fi = loader.config 23 | 24 | override fun registerClientCommands(handler: CommandHandler) { 25 | //call after init(), too late 26 | // Config.clientCommands = handler 27 | } 28 | 29 | override fun registerServerCommands(handler: CommandHandler) { 30 | Config.serverCommands = handler 31 | } 32 | 33 | override fun init() { 34 | initConfigInfo( 35 | rootDir = System.getenv("SARoot")?.let { File(it) } ?: Vars.dataDirectory.child("scripts").file(), 36 | version = Vars.mods.getMod(loader.javaClass).meta.version, 37 | ) 38 | Config.clientCommands = Vars.netServer?.clientCommands ?: CommandHandler("/") 39 | if (!Vars.headless) Config.serverCommands = CommandHandler("") 40 | 41 | bootstrap() 42 | Core.app.addListener(object : ApplicationListener { 43 | override fun pause() { 44 | if (Vars.headless) 45 | exit() 46 | } 47 | 48 | override fun exit() = runBlocking { 49 | ScriptManager.disableAll() 50 | } 51 | }) 52 | } 53 | 54 | override fun displayInfo(foundMain: Boolean) { 55 | Log.info("&y===========================") 56 | Log.info("&lm&fb ScriptAgent &b${Config.version}") 57 | Log.info("&b By &cWayZer ") 58 | Log.info("&b插件官网: https://git.io/SA4Mindustry") 59 | Log.info("&bQQ交流群: 1033116078") 60 | val all = ScriptRegistry.allScripts { true } 61 | Log.info( 62 | "&b共找到${all.size}脚本,加载成功${all.count { it.scriptState.loaded }},启用成功${all.count { it.scriptState.enabled }},出错${all.count { it.failReason != null }}" 63 | ) 64 | if (!foundMain) 65 | Log.warn("&c未找到启动脚本(${Config.mainScript}),请下载安装脚本包,以发挥本插件功能") 66 | Log.info("&y===========================") 67 | } 68 | } -------------------------------------------------------------------------------- /scripts/bootStrap/default.kts: -------------------------------------------------------------------------------- 1 | package bootStrap 2 | 3 | suspend fun boot() = ScriptManager.transaction { 4 | //compiler plugin 5 | add("coreLibrary/kcp") 6 | load();enable() 7 | 8 | //add 添加需要加载的脚本(前缀判断) exclude 排除脚本(可以作为依赖被加载) 9 | addAll() 10 | exclude("bootStrap/") 11 | exclude("coreLibrary/extApi/")//lazy load 12 | exclude("scratch") 13 | exclude("mirai")//Deprecated 14 | load() 15 | 16 | exclude("mapScript/") 17 | enable() 18 | } 19 | 20 | onEnable { 21 | if (Config.mainScript != id) 22 | return@onEnable ScriptManager.disableScript(this, "仅可通过SAMain启用") 23 | boot() 24 | } -------------------------------------------------------------------------------- /scripts/bootStrap/generate.kts: -------------------------------------------------------------------------------- 1 | package bootStrap 2 | 3 | import cf.wayzer.scriptAgent.util.CASScriptPacker 4 | import cf.wayzer.scriptAgent.util.DependencyManager 5 | import cf.wayzer.scriptAgent.util.maven.Dependency 6 | import java.io.File 7 | import kotlin.system.exitProcess 8 | import kotlin.system.measureTimeMillis 9 | 10 | fun prepareBuiltin(outputFile: File = File("build/tmp/builtin.packed.zip")) { 11 | val scripts = ScriptRegistry.allScripts { it.scriptState.loaded } 12 | .mapNotNull { it.compiledScript } 13 | println("prepare Builtin for ${scripts.size} scripts.") 14 | @OptIn(SAExperimentalApi::class) 15 | CASScriptPacker(outputFile.outputStream()) 16 | .use { scripts.forEach(it::add) } 17 | } 18 | 19 | onEnable { 20 | if (id != Config.mainScript) 21 | return@onEnable ScriptManager.disableScript(this, "仅可通过SAMAIN启用") 22 | DependencyManager { 23 | addRepository("https://www.jitpack.io/") 24 | require(Dependency.parse("com.github.TinyLake.MindustryX:core:v2025.05.X9")) 25 | loadToClassLoader(Config.mainClassloader) 26 | } 27 | ScriptManager.transaction { 28 | //compiler plugin 29 | add("coreLibrary/kcp") 30 | load();enable() 31 | 32 | if (Config.args.isEmpty()) 33 | addAll() 34 | else 35 | Config.args.forEach { add(it) } 36 | load() 37 | } 38 | val fail = ScriptRegistry.allScripts { it.failReason != null } 39 | println("共加载${ScriptRegistry.allScripts { it.scriptState != ScriptState.Found }.size}个脚本,失败${fail.size}个") 40 | fail.forEach { 41 | println("\t${it.id}: ${it.failReason}") 42 | } 43 | if (System.getProperty("ScriptAgent.PreparePack") != null) { 44 | println("Finish pack in ${measureTimeMillis { prepareBuiltin() }}ms") 45 | } 46 | exitProcess(fail.size) 47 | } -------------------------------------------------------------------------------- /scripts/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") 5 | } 6 | dependencies { 7 | defineModule("bootStrap") {} 8 | defineModule("coreLibrary") { 9 | api("com.github.way-zer:PlaceHoldLib:v7.3") 10 | api("io.github.config4k:config4k:0.7.0") 11 | api("org.slf4j:slf4j-api:2.0.16") 12 | //coreLib/kcp/serialization 13 | api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") 14 | //coreLib/DBApi 15 | val exposedVersion = "0.59.0" 16 | api("org.jetbrains.exposed:exposed-core:$exposedVersion") 17 | api("org.jetbrains.exposed:exposed-dao:$exposedVersion") 18 | api("org.jetbrains.exposed:exposed-java-time:$exposedVersion") 19 | //coreLib/extApi/redisApi 20 | api("redis.clients:jedis:4.3.1") 21 | //coreLib/extApi/mongoApi 22 | api("org.litote.kmongo:kmongo-coroutine:4.8.0") 23 | implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") 24 | //coreLib/extApi/KVStore 25 | api("com.h2database:h2-mvstore:2.3.232") 26 | } 27 | 28 | defineModule("coreMindustry") { 29 | val mindustryVersion = "v2025.05.X9" //v149 30 | dependsModule("coreLibrary") 31 | // implementation("com.github.Anuken.Mindustry:core:$mindustryVersion") 32 | api("com.github.TinyLake.MindustryX:core:$mindustryVersion") 33 | //coreMindustry/console 34 | implementation("org.jline:jline-terminal:3.21.0") 35 | implementation("org.jline:jline-reader:3.21.0") 36 | //coreMindustry/contentsTweaker 37 | api("cf.wayzer:ContentsTweaker:v3.0.1") 38 | } 39 | defineModule("scratch") { 40 | dependsModule("coreLibrary") 41 | } 42 | 43 | defineModule("wayzer") { 44 | dependsModule("coreMindustry") 45 | api("com.google.guava:guava:30.1-jre") 46 | } 47 | defineModule("mapScript") { 48 | dependsModule("wayzer") 49 | } 50 | } 51 | 52 | allprojects { 53 | tasks.withType().configureEach { 54 | compilerOptions { 55 | freeCompilerArgs = listOf( 56 | "-Xinline-classes", 57 | "-opt-in=kotlin.RequiresOptIn", 58 | "-Xnullability-annotations=@arc.util:strict", 59 | "-Xcontext-receivers", 60 | ) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/DBConnector.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("coreLibrary/DBApi") 2 | 3 | package coreLibrary 4 | 5 | import cf.wayzer.scriptAgent.util.DependencyManager 6 | import cf.wayzer.scriptAgent.util.maven.Dependency 7 | import org.jetbrains.exposed.sql.Database 8 | import org.jetbrains.exposed.sql.DatabaseConfig 9 | import org.jetbrains.exposed.sql.ExperimentalKeywordApi 10 | import java.sql.DriverManager 11 | 12 | val driverMaven by config.key("com.h2database:h2:2.0.206", "驱动程序maven包") 13 | val driver by config.key("org.h2.Driver", "驱动程序类名") 14 | val url by config.key("jdbc:h2:H2DB_PATH", "数据库连接uri", "特殊变量H2DB_PATH 指向data/h2DB.db") 15 | val user by config.key("", "用户名") 16 | val password by config.key("", "密码") 17 | val preserveKeywordCasing by config.key(true, "是否保留关键字大小写, 老用户请设置为false") 18 | 19 | //Postgres example 20 | // driverMaven: org.postgresql:postgresql:42.7.5 21 | // driver: org.postgresql.Driver 22 | // url: jdbc:postgresql://db:5432/postgres 23 | // user: postgres 24 | // password: your_password 25 | 26 | onEnable { 27 | DependencyManager { 28 | require(Dependency.parse(driverMaven)) 29 | loadToClassLoader(thisScript.javaClass.classLoader) 30 | } 31 | Class.forName(driver) 32 | 33 | val url = url.replace("H2DB_PATH", Config.dataDir.resolve("h2DB.db").absolutePath) 34 | val db = Database.connect({ 35 | DriverManager.getConnection(url, user, password) 36 | }, DatabaseConfig { 37 | @OptIn(ExperimentalKeywordApi::class) 38 | preserveKeywordCasing = thisScript.preserveKeywordCasing 39 | }) 40 | DBApi.DB.provide(this, db) 41 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/commands/configCmd.kts: -------------------------------------------------------------------------------- 1 | package coreLibrary.commands 2 | 3 | import cf.wayzer.placehold.PlaceHoldApi.with 4 | 5 | val configCommands = Commands() 6 | command("config", "查看或修改配置".with(), commands = Commands.controlCommand) { 7 | usage = "[help/arg...]" 8 | requirePermission("scriptAgent.$name") 9 | body(configCommands) 10 | } 11 | 12 | command("list", "列出所有配置项".with(), commands = configCommands) { 13 | usage = "[page]" 14 | body { 15 | val page = arg.getOrNull(0)?.toIntOrNull() ?: 1 16 | reply(menu("配置项", ConfigBuilder.all.values.sortedBy { it.path }, page, 15) { 17 | "[green]{key} [blue]{desc}".with( 18 | "key" to it.path, 19 | "desc" to (it.desc.firstOrNull() ?: "") 20 | ) 21 | }) 22 | } 23 | } 24 | command("reload", "重载配置文件".with(), commands = configCommands) { 25 | requirePermission("scriptAgent.config.$name") 26 | body { 27 | ConfigBuilder.reloadFile() 28 | reply("[green]重载成功".with()) 29 | } 30 | } 31 | 32 | @CommandInfo.CommandBuilder 33 | inline fun CommandInfo.subCommand( 34 | usage: String, 35 | crossinline block: suspend context(CommandContext) (ConfigBuilder.ConfigKey<*>) -> Unit 36 | ) { 37 | this.usage = " $usage" 38 | onComplete { 39 | onComplete(0) { ConfigBuilder.all.keys.toList() } 40 | } 41 | body { 42 | val config = arg.firstOrNull()?.let { ConfigBuilder.all[it] } ?: returnReply("[red]找不到配置项".with()) 43 | if (!hasPermission("scriptAgent.config." + config.path)) 44 | returnReply("[red]你没有权限修改配置项: {config}".with("config" to config.path)) 45 | block(context, config) 46 | } 47 | } 48 | command("get", "获取配置项".with(), commands = configCommands) { 49 | subCommand("") { config -> 50 | reply( 51 | """ 52 | |[yellow]==== [light_yellow]配置项: {name}[yellow] ==== 53 | |[purple]{desc|joinLines} 54 | |[cyan]当前值: [yellow]{value} 55 | |[cyan]默认值: [yellow]{default} 56 | |[yellow]使用/sa config help查看可用操作 57 | """.trimMargin().with( 58 | "name" to config.path, "desc" to config.desc, 59 | "value" to config.getString(), "default" to config.default, 60 | ) 61 | ) 62 | } 63 | } 64 | command("reset", "恢复默认值".with(), commands = configCommands) { 65 | subCommand("") { config -> 66 | config.reset() 67 | reply("[green]恢复成功,当前:[yellow]{value}".with("value" to config.getString())) 68 | } 69 | } 70 | command("write", "设置配置项".with(), commands = configCommands) { 71 | subCommand("") { config -> 72 | if (config.get() != config.default) 73 | config.writeDefault() 74 | reply("[green]写入文件成功".with()) 75 | } 76 | } 77 | command("set", "设置配置项".with(), commands = configCommands) { 78 | subCommand("") { config -> 79 | if (arg.size <= 1) returnReply("[red]请输入值".with()) 80 | val value = arg.subList(1, arg.size).joinToString(" ") 81 | reply("[green]设置成功,当前:[yellow]{value}".with("value" to config.setString(value))) 82 | } 83 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/commands/helpful.kts: -------------------------------------------------------------------------------- 1 | package coreLibrary.commands 2 | 3 | import cf.wayzer.placehold.PlaceHoldApi.with 4 | import cf.wayzer.scriptAgent.impl.ScriptCache 5 | import cf.wayzer.scriptAgent.util.CASScriptPacker 6 | import cf.wayzer.scriptAgent.util.MetadataFile 7 | 8 | command("genMetadata", "生成供开发使用的元数据".with(), commands = Commands.controlCommand) { 9 | permission = "scriptAgent.control.genMetadata" 10 | body { 11 | withContext(Dispatchers.Default) { 12 | val grouped = ScriptRegistry.allScripts().mapNotNull { it.compiledScript } 13 | .groupBy { it.id.substringBefore(Config.idSeparator) } 14 | Config.metadataDir.mkdirs() 15 | grouped.forEach { (id, group) -> 16 | reply("[yellow]模块{id}: {size}".with("id" to id, "size" to group.size)) 17 | Config.metadataFile(id).writer().use { 18 | group.sortedBy { it.id }.forEach { info -> 19 | val meta = ScriptCache.asMetadata(info) 20 | MetadataFile(meta.id, meta.attr - "SOURCE_MD5", meta.data).writeTo(it) 21 | } 22 | } 23 | } 24 | reply("[green]生成完成".with()) 25 | } 26 | } 27 | } 28 | command("packModule", "打包模块".with(), commands = Commands.controlCommand) { 29 | usage = "" 30 | permission = "scriptAgent.control.packModule" 31 | body { 32 | val module = arg.getOrNull(0) ?: replyUsage() 33 | val scripts = ScriptRegistry.allScripts { it.id.startsWith("$module/") } 34 | .mapNotNull { it.compiledScript } 35 | @OptIn(SAExperimentalApi::class) 36 | CASScriptPacker(Config.cacheDir.resolve("$module.packed.zip").outputStream()) 37 | .use { scripts.forEach(it::add) } 38 | } 39 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/commands/hotReload.kts: -------------------------------------------------------------------------------- 1 | package coreLibrary.commands 2 | 3 | import cf.wayzer.placehold.PlaceHoldApi.with 4 | import cf.wayzer.scriptAgent.registry.DirScriptRegistry 5 | import java.nio.file.* 6 | 7 | var watcher: WatchService? = null 8 | 9 | fun enableWatch() { 10 | if (watcher != null) return//Enabled 11 | watcher = FileSystems.getDefault().newWatchService() 12 | Config.rootDir.walkTopDown().onEnter { it.name != "cache" && it.name != "lib" && it.name != "res" } 13 | .filter { it.isDirectory }.forEach { 14 | it.toPath().register(watcher!!, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY) 15 | } 16 | launch(Dispatchers.IO) { 17 | while (true) { 18 | val key = try { 19 | watcher?.take() ?: return@launch 20 | } catch (e: ClosedWatchServiceException) { 21 | return@launch 22 | } 23 | key.pollEvents().forEach { event -> 24 | if (event.count() != 1) return@forEach 25 | val file = (key.watchable() as Path).resolve(event.context() as? Path ?: return@forEach) 26 | when { 27 | file.toString().endsWith(Config.scriptSuffix) -> { //处理子脚本 28 | val id = DirScriptRegistry.getIdByFile(file.toFile(), Config.rootDir) 29 | val script = ScriptRegistry.getScriptInfo(id) ?: return@forEach 30 | logger.info("脚本文件更新: ${event.kind().name()} ${script.id}") 31 | delay(1000) 32 | val state = script.scriptState 33 | when { 34 | state == ScriptState.Found -> logger.info(" 新脚本: 请使用sa load加载") 35 | else -> { 36 | val oldEnable = state == ScriptState.ToEnable || state.enabled 37 | ScriptManager.transaction { 38 | add(script) 39 | unload(addAllAffect = true) 40 | load() 41 | if (oldEnable) enable() 42 | } 43 | logger.info( 44 | if (oldEnable) " 新脚本启用成功" else " 新脚本加载成功: 请使用sa enable启用" 45 | ) 46 | } 47 | } 48 | } 49 | 50 | file.toFile().isDirectory -> {//添加子目录到Watch 51 | file.register( 52 | watcher!!, 53 | StandardWatchEventKinds.ENTRY_CREATE, 54 | StandardWatchEventKinds.ENTRY_MODIFY 55 | ) 56 | } 57 | } 58 | } 59 | if (!key.reset()) return@launch 60 | } 61 | } 62 | } 63 | 64 | command("hotReload", "开关脚本自动热重载".with(), commands = Commands.controlCommand) { 65 | permission = "scriptAgent.control.hotReload" 66 | body { 67 | if (watcher == null) { 68 | enableWatch() 69 | reply("[green]脚本自动热重载监测启动".with()) 70 | } else { 71 | watcher?.close() 72 | watcher = null 73 | reply("[yellow]脚本自动热重载监测关闭".with()) 74 | } 75 | } 76 | } 77 | 78 | onDisable { 79 | withContext(Dispatchers.IO) { 80 | watcher?.close() 81 | } 82 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/commands/permissionCmd.kts: -------------------------------------------------------------------------------- 1 | package coreLibrary.commands 2 | 3 | val handler = PermissionApi.StringPermissionHandler() 4 | onEnable { PermissionApi.Global.ByGroup.add(0, handler) } 5 | onDisable { PermissionApi.Global.ByGroup.remove(handler) } 6 | 7 | var groups by config.key( 8 | "groups", mapOf("@default" to emptyList()), 9 | "权限设置", "值为权限,@开头为组,支持末尾通配符.*" 10 | ) { 11 | handler.clear() 12 | it.forEach { (g, list) -> 13 | handler.registerPermission(g, list) 14 | } 15 | } 16 | 17 | command("permission", "权限系统配置".with(), commands = Commands.controlCommand) { 18 | aliases = listOf("pm") 19 | usage = " [permission]" 20 | onComplete { 21 | onComplete(0) { PermissionApi.allKnownGroup.toList() } 22 | onComplete(1) { listOf("add", "list", "remove", "delGroup") } 23 | } 24 | body { 25 | if (arg.isEmpty()) returnReply("当前已有组: {list}".with("list" to PermissionApi.allKnownGroup)) 26 | val group = arg[0] 27 | when (arg.getOrNull(1)?.lowercase() ?: "") { 28 | "add" -> { 29 | if (arg.size < 3) returnReply("[red]请输入需要增减的权限".with()) 30 | val now = groups[group].orEmpty() 31 | if (arg[2] !in now) 32 | groups = groups + (group to (now + arg[2])) 33 | returnReply( 34 | "[green]{op}权限{permission}到组{group}".with( 35 | "op" to "添加", "permission" to arg[2], "group" to group 36 | ) 37 | ) 38 | } 39 | 40 | "remove" -> { 41 | if (arg.size < 3) returnReply("[red]请输入需要增减的权限".with()) 42 | if (group in groups) { 43 | val newList = groups[group].orEmpty() - arg[2] 44 | groups = if (newList.isEmpty()) groups - group else groups + (group to newList) 45 | } 46 | returnReply( 47 | "[green]{op}权限{permission}到组{group}".with( 48 | "op" to "移除", "permission" to arg[2], "group" to group 49 | ) 50 | ) 51 | } 52 | 53 | "", "list" -> { 54 | val now = groups[group].orEmpty() 55 | val defaults = PermissionApi.default.groups[group]?.allNodes().orEmpty() 56 | reply( 57 | """ 58 | [green]组{group}当前拥有权限:[] 59 | {list} 60 | [green]默认定义权限:[] 61 | {defaults} 62 | [yellow]默认组权限仅可通过添加负权限修改 63 | """.trimIndent().with( 64 | "group" to group, "list" to now.toString(), "defaults" to defaults.toString() 65 | ) 66 | ) 67 | } 68 | 69 | "delGroup".lowercase() -> { 70 | val now = groups[group].orEmpty() 71 | if (group in groups) 72 | groups = groups - group 73 | returnReply( 74 | "[yellow]移除权限组{group},原来含有:{list}".with( 75 | "group" to group, "list" to now.toString() 76 | ) 77 | ) 78 | } 79 | 80 | else -> replyUsage() 81 | } 82 | } 83 | } 84 | 85 | val debug by config.key(false, "调试输出,如果开启,则会在后台打印权限请求") 86 | listenTo(Event.Priority.Watch) { 87 | if (debug) 88 | logger.info("$permission $directReturn -- $group") 89 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/commands/varsCmd.kts: -------------------------------------------------------------------------------- 1 | package coreLibrary.commands 2 | 3 | import coreLibrary.lib.PlaceHold.registeredVars 4 | 5 | data class VarInfo(val script: ScriptInfo, val key: String, val desc: String) 6 | 7 | command("vars", "列出注册的所有模板变量".with(), commands = Commands.controlCommand) { 8 | usage = "[-v] [page]" 9 | permission = "scriptAgent.vars" 10 | body { 11 | val detail = checkArg("-v") 12 | val page = arg.firstOrNull()?.toIntOrNull() ?: 1 13 | val all = mutableListOf() 14 | ScriptRegistry.allScripts().sortedBy { it.id }.forEach { script -> 15 | script.inst?.registeredVars?.mapTo(all) { (key, desc) -> 16 | VarInfo(script, key, desc) 17 | } 18 | } 19 | returnReply(menu("模板变量", all, page, 15) { 20 | "[green]{key} [blue]{desc} [purple]{from}".with( 21 | "key" to it.key, "desc" to it.desc, 22 | "from" to (if (detail) it.script.id else "") 23 | ) 24 | }) 25 | } 26 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/extApi/KVStore.kts: -------------------------------------------------------------------------------- 1 | @file:Import("com.h2database:h2-mvstore:2.3.232", mavenDependsSingle = true) 2 | 3 | package coreLibrary.extApi 4 | 5 | import org.h2.mvstore.MVMap 6 | import org.h2.mvstore.MVStore 7 | import org.h2.mvstore.type.DataType 8 | import org.h2.mvstore.type.StringDataType 9 | import java.util.logging.Level 10 | 11 | val store by lazy { 12 | Config.dataDir.mkdirs() 13 | MVStore.Builder() 14 | .fileName(Config.dataDir.resolve("kvStore.mv").path) 15 | .backgroundExceptionHandler { _, e -> logger.log(Level.SEVERE, "MVStore background error", e) } 16 | .open() 17 | .also { onDisable { it.close() } } 18 | } 19 | 20 | fun open(name: String, type: DataType) = open(name, type, StringDataType.INSTANCE) 21 | fun open(name: String, key: DataType, type: DataType) = 22 | store.openMap(name, MVMap.Builder().apply { 23 | keyType(key) 24 | valueType(type) 25 | })!! -------------------------------------------------------------------------------- /scripts/coreLibrary/extApi/mongoApi.kts: -------------------------------------------------------------------------------- 1 | @file:Import("org.litote.kmongo:kmongo-coroutine:4.8.0", mavenDepends = true) 2 | @file:Import("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.1", mavenDepends = true) 3 | 4 | 5 | package coreLibrary.extApi 6 | 7 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 8 | import org.litote.kmongo.coroutine.CoroutineClient 9 | import org.litote.kmongo.coroutine.CoroutineCollection 10 | import org.litote.kmongo.coroutine.coroutine 11 | import org.litote.kmongo.reactivestreams.KMongo 12 | import org.litote.kmongo.util.KMongoConfiguration 13 | import java.util.logging.Level 14 | 15 | @Suppress("unused")//Api 16 | object Mongo : ServiceRegistry() { 17 | const val defaultDBName = "DEFAULT" 18 | fun getDB(db: String = defaultDBName) = get().getDatabase(db) 19 | inline fun collection(db: String = defaultDBName): CoroutineCollection { 20 | return getDB(db).getCollection() 21 | } 22 | } 23 | 24 | val addr by config.key("mongodb://localhost", "mongo地址", "重载生效") 25 | onEnable { 26 | try { 27 | withContextClassloader { 28 | Mongo.provide(this, KMongo.createClient(addr).coroutine) 29 | KMongoConfiguration.registerBsonModule(JavaTimeModule()) 30 | } 31 | } catch (e: Throwable) { 32 | logger.log(Level.WARNING, "连接Mongo数据库失败: $addr", e) 33 | return@onEnable ScriptManager.disableScript(this, "连接Mongo数据库失败: $e") 34 | } 35 | } 36 | 37 | onDisable { 38 | Mongo.getOrNull()?.let { 39 | withContext(Dispatchers.IO) { it.close() } 40 | } 41 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/extApi/redisApi.kts: -------------------------------------------------------------------------------- 1 | @file:Import("redis.clients:jedis:4.4.3", mavenDepends = true) 2 | 3 | package coreLibrary.extApi 4 | 5 | import redis.clients.jedis.Jedis 6 | import redis.clients.jedis.JedisPool 7 | import java.util.logging.Level 8 | 9 | @Suppress("unused")//Api 10 | object Redis : ServiceRegistry() { 11 | inline fun use(body: Jedis.() -> T): T { 12 | return get().resource.use(body) 13 | } 14 | } 15 | 16 | val addr by config.key("redis://redis:6379", "redis地址", "重载生效") 17 | onEnable { 18 | try { 19 | Redis.provide(this, JedisPool(addr).apply { 20 | testOnCreate = true 21 | testOnBorrow = true 22 | resource.use { it.ping() } 23 | }) 24 | } catch (e: Throwable) { 25 | logger.log(Level.WARNING, "连接Redis服务器失败: $addr", e) 26 | return@onEnable ScriptManager.disableScript(this, "连接Redis服务器失败: $e") 27 | } 28 | } 29 | 30 | onDisable { 31 | Redis.getOrNull()?.close() 32 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/extApi/remoteEventApi.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("coreLibrary/extApi/redisApi", "基于redis") 2 | 3 | package coreLibrary.extApi 4 | 5 | import redis.clients.jedis.BinaryJedisPubSub 6 | import java.io.* 7 | import java.util.logging.Level 8 | 9 | val group by config.key("_SA_RemoteEvent") 10 | 11 | fun remoteEmit(event: RemoteEvent) = launch(Dispatchers.IO) { 12 | RedisApi.Redis.use { 13 | publish(group.toByteArray(), ByteArrayOutputStream().use { 14 | ObjectOutputStream(it).writeObject(event) 15 | it.toByteArray() 16 | }) 17 | } 18 | } 19 | 20 | fun handleReceive(msg: ByteArray) { 21 | try { 22 | val event = object : ObjectInputStream(ByteArrayInputStream(msg)) { 23 | var eventClass: Class<*>? = null 24 | override fun resolveClass(desc: ObjectStreamClass): Class<*> { 25 | RemoteEvent.Impl.classMap[desc.name]?.get()?.let { 26 | eventClass = it 27 | return it 28 | } 29 | return eventClass?.classLoader?.loadClass(desc.name) 30 | ?: throw ClassNotFoundException(desc.name) 31 | } 32 | }.use { it.readObject() as RemoteEvent } 33 | launch { event.onReceive() } 34 | } catch (e: Throwable) { 35 | logger.log(Level.WARNING, "Fail to receive remote event", e) 36 | } 37 | } 38 | 39 | onEnable { 40 | loop(Dispatchers.IO) { 41 | RedisApi.Redis.awaitInit() 42 | RedisApi.Redis.use { 43 | subscribe( 44 | object : BinaryJedisPubSub() { 45 | init { 46 | onDisable { unsubscribe() } 47 | } 48 | 49 | override fun onMessage(channel: ByteArray, message: ByteArray) { 50 | handleReceive(message) 51 | } 52 | }, group.toByteArray() 53 | ) 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/extApi/remoteEventApi.lib.kt: -------------------------------------------------------------------------------- 1 | package coreLibrary.extApi 2 | 3 | import cf.wayzer.scriptAgent.Event 4 | import cf.wayzer.scriptAgent.contextScript 5 | import java.io.Serializable 6 | import java.lang.ref.WeakReference 7 | 8 | @Suppress("unused")//Api 9 | abstract class RemoteEvent : Event, Serializable { 10 | private val handler0 get() = super.handler 11 | final override val handler: Event.Handler get() = error("You should use RemoteEvent.emit()") 12 | 13 | fun launchEmit() { 14 | Impl.script.remoteEmit(this) 15 | } 16 | 17 | internal suspend fun onReceive() { 18 | handler0.handleAsync(this) 19 | } 20 | 21 | abstract class Handler : Event.Handler() { 22 | init { 23 | val eventCls = javaClass.enclosingClass 24 | Impl.classMap[eventCls.name] = WeakReference(eventCls) 25 | } 26 | } 27 | 28 | internal object Impl { 29 | val script = contextScript() 30 | val classMap = mutableMapOf>>() 31 | } 32 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/extApi/rpcService.kts: -------------------------------------------------------------------------------- 1 | package coreLibrary.extApi 2 | 3 | import java.rmi.Remote 4 | import java.rmi.registry.LocateRegistry 5 | import java.rmi.registry.Registry 6 | import java.rmi.server.UnicastRemoteObject 7 | 8 | 9 | val port by config.key(10099, "RPC,监听端口") 10 | 11 | val host: String? = System.getenv("RPC_MASTER_HOST") 12 | val isMaster = host == null 13 | 14 | lateinit var registry: Registry 15 | onEnable { 16 | if (isMaster) { 17 | registry = LocateRegistry.createRegistry(port) 18 | logger.info("RPC server started on port $port") 19 | onDisable { 20 | UnicastRemoteObject.unexportObject(registry, true) 21 | logger.info("RPC server stopped") 22 | } 23 | } else { 24 | logger.info("RPC started as client, host $host") 25 | } 26 | } 27 | 28 | inline fun get(): T = get(T::class.java) as T 29 | inline fun register(noinline factory: () -> T) = register(T::class.java, factory) 30 | 31 | fun get(inf: Class): Remote { 32 | if (isMaster) return registry.lookup(inf.name) 33 | val sp = host!!.split(":") 34 | val registry = LocateRegistry.getRegistry(sp[0], sp.getOrNull(1)?.toInt() ?: port) 35 | withContextClassloader(inf.classLoader) { 36 | return registry.lookup(inf.name) 37 | } 38 | } 39 | 40 | fun register(inf: Class, factory: () -> T) { 41 | check(inf.isInterface && Remote::class.java.isAssignableFrom(inf)) { 42 | "T must be a Remote interface" 43 | } 44 | val name = inf.name 45 | if (!isMaster) { 46 | logger.info("Ignore RPC service, not master: $name") 47 | return 48 | } 49 | val service = factory() 50 | registry.bind(name, service) 51 | logger.info("RPC service registered: $name") 52 | service.thisContextScript().onDisable { 53 | registry.unbind(name) 54 | UnicastRemoteObject.unexportObject(service, true); 55 | } 56 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/kcp/serialization.kts: -------------------------------------------------------------------------------- 1 | @file:Import("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0", mavenDepends = true) 2 | 3 | package coreLibrary.kcp 4 | 5 | import cf.wayzer.scriptAgent.events.ScriptCompileEvent 6 | import cf.wayzer.scriptAgent.util.DependencyManager 7 | import cf.wayzer.scriptAgent.util.maven.Dependency 8 | 9 | val pluginFile by lazy { 10 | DependencyManager { 11 | val dep = "org.jetbrains.kotlin:kotlin-serialization-compiler-plugin-embeddable:${Config.kotlinVersion}" 12 | require(Dependency.parse(dep), resolveChild = false) 13 | load() 14 | getFiles().single() 15 | } 16 | } 17 | 18 | @OptIn(SAExperimentalApi::class) 19 | listenTo { 20 | if (script.scriptInfo.dependsOn(thisScript.scriptInfo)) { 21 | addCompileOptions("-Xplugin=${pluginFile.absolutePath}") 22 | } 23 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/lib/ColorApi.kt: -------------------------------------------------------------------------------- 1 | package coreLibrary.lib 2 | 3 | enum class ConsoleColor(val code: String) : ColorApi.Color { 4 | RESET("\u001b[0m"), 5 | BOLD("\u001b[1m"), 6 | ITALIC("\u001b[3m"), 7 | UNDERLINED("\u001b[4m"), 8 | 9 | BLACK("\u001b[30m"), 10 | RED("\u001b[31m"), 11 | GREEN("\u001b[32m"), 12 | YELLOW("\u001b[33m"), 13 | BLUE("\u001b[34m"), 14 | PURPLE("\u001b[35m"), 15 | CYAN("\u001b[36m"), 16 | LIGHT_RED("\u001b[91m"), 17 | LIGHT_GREEN("\u001b[92m"), 18 | LIGHT_YELLOW("\u001b[93m"), 19 | LIGHT_BLUE("\u001b[94m"), 20 | LIGHT_PURPLE("\u001b[95m"), 21 | LIGHT_CYAN("\u001b[96m"), 22 | WHITE("\u001b[37m"), 23 | BACK_DEFAULT("\u001b[49m"), 24 | BACK_RED("\u001b[41m"), 25 | BACK_GREEN("\u001b[42m"), 26 | BACK_YELLOW("\u001b[43m"), 27 | BACK_BLUE("\u001b[44m"); 28 | 29 | override fun toString(): String { 30 | return "[$name]" 31 | } 32 | } 33 | 34 | object ColorApi { 35 | interface Color 36 | 37 | private val map = mutableMapOf()//name(Upper)->source 38 | val all get() = map as Map 39 | fun register(name: String, color: Color) { 40 | map[name.uppercase()] = color 41 | } 42 | 43 | init { 44 | ConsoleColor.entries.forEach { 45 | register(it.name, it) 46 | } 47 | } 48 | 49 | fun consoleColorHandler(color: Color): String { 50 | return if (color is ConsoleColor) color.code else "" 51 | } 52 | 53 | fun handle(raw: String, colorHandler: (Color) -> String): String { 54 | return raw.replace(Regex("\\[([!a-zA-Z_]+)]")) { 55 | val matched = it.groupValues[1] 56 | if (matched.startsWith("!")) return@replace "[${matched.substring(1)}]" 57 | val color = all[matched.uppercase()] ?: return@replace it.value 58 | return@replace colorHandler(color) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /scripts/coreLibrary/lib/event/RequestPermissionEvent.kt: -------------------------------------------------------------------------------- 1 | package coreLibrary.lib.event 2 | 3 | import cf.wayzer.scriptAgent.Event 4 | import coreLibrary.lib.PermissionApi 5 | 6 | @Suppress("MemberVisibilityCanBePrivate") 7 | class RequestPermissionEvent(val subject: Any, val permission: String, var group: List = emptyList()) : 8 | Event, Event.Cancellable { 9 | var directReturn: PermissionApi.Result? = null 10 | fun directReturn(result: PermissionApi.Result) { 11 | directReturn = result 12 | } 13 | 14 | override val handler = Companion 15 | 16 | companion object : Event.Handler() 17 | 18 | override var cancelled: Boolean 19 | get() = directReturn != null 20 | set(value) { 21 | if (value) directReturn(PermissionApi.Result.Reject) 22 | } 23 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/lib/event/ServiceProvidedEvent.kt: -------------------------------------------------------------------------------- 1 | package coreLibrary.lib.event 2 | 3 | import cf.wayzer.scriptAgent.Event 4 | import cf.wayzer.scriptAgent.define.Script 5 | 6 | @Suppress("unused") 7 | class ServiceProvidedEvent(val service: T, val provider: Script) : Event { 8 | override val handler = Companion 9 | 10 | companion object : Event.Handler() 11 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/lib/util/ReflectHelper.kt: -------------------------------------------------------------------------------- 1 | package coreLibrary.lib.util 2 | 3 | import cf.wayzer.scriptAgent.util.DSLBuilder 4 | import java.lang.reflect.Field 5 | import kotlin.properties.ReadWriteProperty 6 | import kotlin.reflect.KProperty 7 | 8 | class ReflectDelegate( 9 | private val field: Field, private val cls: Class 10 | ) : ReadWriteProperty { 11 | override fun getValue(thisRef: T?, property: KProperty<*>): R = cls.cast(field.get(thisRef)) 12 | override fun setValue(thisRef: T?, property: KProperty<*>, value: R) = field.set(thisRef, value) 13 | } 14 | 15 | inline fun reflectDelegate() = DSLBuilder.NameGet { name -> 16 | val field = T::class.java.getDeclaredField(name) 17 | if (!field.isAccessible) field.isAccessible = true 18 | ReflectDelegate(field, R::class.java) 19 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/lib/util/ServiceRegistry.kt: -------------------------------------------------------------------------------- 1 | package coreLibrary.lib.util 2 | 3 | import cf.wayzer.scriptAgent.define.Script 4 | import cf.wayzer.scriptAgent.emitAsync 5 | import cf.wayzer.scriptAgent.util.DSLBuilder 6 | import coreLibrary.lib.event.ServiceProvidedEvent 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.ExperimentalCoroutinesApi 9 | import kotlinx.coroutines.channels.BufferOverflow 10 | import kotlinx.coroutines.flow.* 11 | import kotlinx.coroutines.sync.Mutex 12 | import kotlinx.coroutines.sync.withLock 13 | import kotlin.properties.ReadOnlyProperty 14 | 15 | /** 16 | * 模块化服务提供工具库 17 | */ 18 | 19 | @Suppress("unused") 20 | open class ServiceRegistry { 21 | private val impl = MutableSharedFlow(1, 0, BufferOverflow.DROP_OLDEST) 22 | private val mutex = Mutex() 23 | 24 | suspend fun provide(script: Script, inst: T) { 25 | script.providedService.add(this to inst) 26 | this.impl.emit(inst) 27 | ServiceProvidedEvent(inst, script).emitAsync() 28 | 29 | @OptIn(ExperimentalCoroutinesApi::class) 30 | script.onDisable { 31 | if (getOrNull() == inst) 32 | impl.resetReplayCache() 33 | } 34 | } 35 | 36 | fun getOrNull() = impl.replayCache.firstOrNull() 37 | fun get() = getOrNull() ?: error("No Provider for ${this.javaClass.canonicalName}") 38 | 39 | val provided get() = getOrNull() != null 40 | fun toFlow() = impl.asSharedFlow() 41 | 42 | suspend fun awaitInit() = impl.first() 43 | 44 | @JvmOverloads 45 | fun subscribe(scope: CoroutineScope, async: Boolean = false, body: suspend (T) -> Unit) { 46 | impl.onEach { 47 | if (async) body(it) 48 | else mutex.withLock { body(it) } 49 | }.launchIn(scope) 50 | } 51 | 52 | val nullable get() = ReadOnlyProperty { _, _ -> getOrNull() } 53 | val notNull get() = ReadOnlyProperty { _, _ -> get() } 54 | 55 | companion object { 56 | val Script.providedService by DSLBuilder.dataKeyWithDefault { mutableSetOf, *>>() } 57 | } 58 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/lib/util/coroutine.kt: -------------------------------------------------------------------------------- 1 | package coreLibrary.lib.util 2 | 3 | import cf.wayzer.scriptAgent.define.Script 4 | import cf.wayzer.scriptAgent.define.ScriptUtil 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.launch 8 | import java.util.logging.Level 9 | import kotlin.coroutines.CoroutineContext 10 | import kotlin.coroutines.EmptyCoroutineContext 11 | import kotlin.coroutines.cancellation.CancellationException 12 | 13 | fun Script.loop(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> Unit) { 14 | launch(context) { 15 | while (true) { 16 | try { 17 | block() 18 | } catch (e: Exception) { 19 | if (e is CancellationException) throw e 20 | logger.log(Level.WARNING, "Exception inside loop, auto sleep 10s.", e) 21 | delay(10000) 22 | } 23 | } 24 | } 25 | } 26 | 27 | @Deprecated(level = DeprecationLevel.HIDDEN, message = "Only for script") 28 | fun CoroutineScope.loop(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> Unit) = 29 | (this as Script).loop(context, block) 30 | 31 | @ScriptUtil 32 | inline fun Script.withContextClassloader(loader: ClassLoader = javaClass.classLoader, block: () -> T): T { 33 | val bak = Thread.currentThread().contextClassLoader 34 | return try { 35 | Thread.currentThread().contextClassLoader = loader 36 | block() 37 | } finally { 38 | Thread.currentThread().contextClassLoader = bak 39 | } 40 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/lib/util/menu.kt: -------------------------------------------------------------------------------- 1 | package coreLibrary.lib.util 2 | 3 | import coreLibrary.lib.PlaceHoldString 4 | import coreLibrary.lib.with 5 | import kotlin.math.ceil 6 | 7 | 8 | fun calPage(page: Int, prePage: Int, size: Int): Pair { 9 | val totalPage = ceil(size / prePage.toDouble()).toInt() 10 | //note: totalPage may be 0 (less than 1), so can't use coerceIn 11 | val newPage = page.coerceAtMost(totalPage).coerceAtLeast(1) 12 | return newPage to totalPage 13 | } 14 | 15 | fun menu(title: String, list: List, page: Int, prePage: Int, handle: (E) -> PlaceHoldString): PlaceHoldString { 16 | val (newPage, totalPage) = calPage(page, prePage, list.size) 17 | val list2 = list.subList((newPage - 1) * prePage, (newPage * prePage).coerceAtMost(list.size)) 18 | .map(handle) 19 | return """ 20 | |[green]==== [white]{title}[green] ==== 21 | |{list|joinLines} 22 | |[green]==== [white]{page}/{total}[green] ==== 23 | """.trimMargin().with("title" to title, "list" to list2, "page" to newPage, "total" to totalPage) 24 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/lib/util/nextEvent.kt: -------------------------------------------------------------------------------- 1 | package coreLibrary.lib.util 2 | 3 | import cf.wayzer.scriptAgent.Event 4 | import cf.wayzer.scriptAgent.define.Script 5 | import cf.wayzer.scriptAgent.listenTo 6 | import kotlinx.coroutines.suspendCancellableCoroutine 7 | import kotlin.coroutines.resume 8 | 9 | interface ReceivedEvent { 10 | var received: Boolean 11 | } 12 | 13 | suspend inline fun Script.nextEvent(crossinline filter: (T) -> Boolean): T = 14 | suspendCancellableCoroutine { 15 | lateinit var listen: Event.Listen 16 | listen = listenTo { 17 | if (filter(this)) { 18 | if (this is ReceivedEvent) received = true 19 | listen.unregister() 20 | it.resume(this) 21 | } 22 | } 23 | it.invokeOnCancellation { listen.unregister() } 24 | } -------------------------------------------------------------------------------- /scripts/coreLibrary/module.kts: -------------------------------------------------------------------------------- 1 | @file:Import("https://www.jitpack.io/", mavenRepository = true) 2 | @file:Import("com.github.way-zer:PlaceHoldLib:v7.3", mavenDependsSingle = true) 3 | @file:Import("io.github.config4k:config4k:0.7.0", mavenDepends = true) 4 | @file:Import("org.slf4j:slf4j-simple:2.0.16", mavenDependsSingle = true) 5 | @file:Import("org.slf4j:slf4j-api:2.0.16", mavenDependsSingle = true) 6 | @file:Import("coreLibrary.lib.*", defaultImport = true) 7 | @file:Import("coreLibrary.lib.event.*", defaultImport = true) 8 | @file:Import("coreLibrary.lib.util.*", defaultImport = true) 9 | @file:Import("-Xcontext-receivers", compileArg = true) 10 | @file:Import("cf.wayzer.placehold.*", defaultImport = true) 11 | 12 | package coreLibrary 13 | 14 | // 本模块实现一些平台无关的库 -------------------------------------------------------------------------------- /scripts/coreLibrary/variables.kts: -------------------------------------------------------------------------------- 1 | package coreLibrary 2 | 3 | import cf.wayzer.placehold.DynamicVar 4 | import cf.wayzer.placehold.VarString 5 | import com.typesafe.config.Config 6 | import io.github.config4k.ClassContainer 7 | import io.github.config4k.CustomType 8 | import io.github.config4k.registerCustomType 9 | import io.github.config4k.toConfig 10 | import java.lang.management.ManagementFactory 11 | import java.time.Duration 12 | import java.time.Instant 13 | import java.time.temporal.ChronoUnit 14 | import kotlin.time.toKotlinDuration 15 | 16 | name = "基础变量注册" 17 | 18 | registerVar("\\n", "换行符", "\n") 19 | registerVar("joinLines", "'join \\n'的别名", DynamicVar { 20 | VarToken("join", VarString.Parameters(it.params + "\n")) 21 | }) 22 | registerVarForType().apply { 23 | registerToString("参数设定单位(天,时,分,秒,d,h,m,s,默认m)") { obj -> 24 | DynamicVar { params -> 25 | val arg = params.getOrNull(0)?.name 26 | ?: return@DynamicVar obj.toKotlinDuration().toString() 27 | val unit = when (arg[0].lowercaseChar()) { 28 | 'd', '天' -> ChronoUnit.DAYS 29 | 'h', '小', '时' -> ChronoUnit.HOURS 30 | 'm', '分' -> ChronoUnit.MINUTES 31 | 's', '秒' -> ChronoUnit.SECONDS 32 | else -> ChronoUnit.MINUTES 33 | } 34 | "%.2f%s".format((obj.seconds.toDouble() / unit.duration.seconds), arg) 35 | } 36 | } 37 | } 38 | 39 | val startTime = Instant.ofEpochMilli( 40 | runCatching { ManagementFactory.getRuntimeMXBean().startTime }.getOrElse { System.currentTimeMillis() } 41 | )!! 42 | registerVar("state.uptime", "进程运行时间", DynamicVar { Duration.between(startTime, Instant.now()) }) 43 | 44 | @Suppress("PropertyName") 45 | val NANO_PRE_SECOND = 1000_000_000L 46 | fun Duration.toConfigString(): String { 47 | //Select the smallest unit output 48 | return when { 49 | (nano % 1000) != 0 -> (seconds * NANO_PRE_SECOND + nano).toString() + "ns" 50 | (nano % 1000_000) != 0 -> ((seconds * NANO_PRE_SECOND + nano) / 1000).toString() + "us" 51 | nano != 0 -> ((seconds * NANO_PRE_SECOND + nano) / 1000_000).toString() + "ms" 52 | (seconds % 60) != 0L -> seconds.toString() + "s" 53 | (seconds % (60 * 60)) != 0L -> (seconds / 60).toString() + "m" 54 | (seconds % (60 * 60 * 24)) != 0L -> (seconds / (60 * 60)).toString() + "h" 55 | else -> (seconds / (60 * 60 * 24)).toString() + "d" 56 | } 57 | } 58 | registerCustomType(object : CustomType { 59 | override fun testParse(clazz: ClassContainer) = false 60 | override fun parse(clazz: ClassContainer, config: Config, name: String) = UnsupportedOperationException() 61 | override fun testToConfig(obj: Any) = obj is Duration 62 | override fun toConfig(obj: Any, name: String): Config { 63 | return (obj as Duration).toConfigString().toConfig(name) 64 | } 65 | }) -------------------------------------------------------------------------------- /scripts/coreMindustry/contentsTweaker.kts: -------------------------------------------------------------------------------- 1 | @file:Import("https://www.jitpack.io/", mavenRepository = true) 2 | @file:Import("cf.wayzer:ContentsTweaker:v3.0.1", mavenDependsSingle = true) 3 | 4 | package coreMindustry 5 | 6 | import cf.wayzer.contentsTweaker.ContentsTweaker 7 | import cf.wayzer.placehold.DynamicVar 8 | import mindustry.gen.Iconc 9 | 10 | var patches: String? 11 | get() = state.map.tags.get("ContentsPatch") 12 | set(v) { 13 | state.map.tags.put("ContentsPatch", v) 14 | //back compatibility 15 | state.rules.tags.put("ContentsPatch", v!!) 16 | } 17 | var patchList: List 18 | get() = patches?.split(";").orEmpty() 19 | set(v) { 20 | patches = v.joinToString(";") 21 | } 22 | 23 | val ctPlayers = mutableMapOf() 24 | 25 | class CTHello(val player: Player, val version: String) : Event { 26 | companion object : Event.Handler() 27 | } 28 | 29 | registerVar("scoreBroad.ext.contentsVersion", "ContentsTweaker状态显示", DynamicVar { 30 | val patches = patches ?: return@DynamicVar null 31 | val player = VarToken("receiver").get() as? Player 32 | buildString { 33 | append("[violet]特殊修改已加载: [orange]") 34 | if (player == null || player.uuid() !in ctPlayers) 35 | append("(使用[sky]ContentsTweaker[]MOD获得最佳体验)") 36 | else append("${patches.count { it == ';' } + 1} 修改") 37 | } 38 | }) 39 | registerVarForType().apply { 40 | registerChild("suffix.s3-CT", "CT mod 后缀", { p -> Iconc.wrench.takeIf { p.uuid() in ctPlayers } }) 41 | } 42 | 43 | fun sendPatch(name: String, patch: String) { 44 | Call.clientPacketReliable("ContentsLoader|newPatch", "$name\n$patch") 45 | } 46 | 47 | @JvmName("addPatchV3") 48 | fun addPatch(name: String, patch: String) { 49 | if (!name.startsWith("$")) { 50 | state.map.tags.put("CT@$name", patch) 51 | patchList = patchList.toMutableList().apply { 52 | remove(name);add(name)//put last 53 | } 54 | } 55 | ContentsTweaker.loadPatch(name, patch) 56 | sendPatch(name, patch) 57 | } 58 | @JvmName("addPatch") 59 | fun addPatchOld(name: String, patch: String): String { 60 | addPatch(name, patch) 61 | return name 62 | } 63 | export(::addPatch) 64 | listen { 65 | ContentsTweaker.recoverAll() 66 | ctPlayers.clear() 67 | } 68 | 69 | listen { 70 | ctPlayers.remove(it.player.uuid()) 71 | } 72 | 73 | listen { 74 | if (ContentsTweaker.worldInReset) return@listen 75 | var needAfterHandle = false 76 | state.map.tags.get("ContentsPatch")?.split(";")?.forEach { name -> 77 | if (name.isBlank()) return@forEach 78 | val patch = state.map.tags.get("CT@$name") ?: return@forEach 79 | ContentsTweaker.loadPatch(name, patch, doAfter = false) 80 | needAfterHandle = true 81 | } 82 | if (needAfterHandle) ContentsTweaker.afterHandle() 83 | } 84 | 85 | //处理客户端请求 86 | onEnable { 87 | netServer.addPacketHandler("ContentsLoader|version") { p, msg -> 88 | logger.info("${p.name} $msg") 89 | if (msg.contains("2.")) 90 | Call.sendMessage(p.con, "你当前安装的CT版本过老,请更新到3.0.1", null, null) 91 | ctPlayers[p.uuid()] = msg 92 | launch(Dispatchers.game) { 93 | CTHello(p, msg).emitAsync() 94 | } 95 | } 96 | netServer.addPacketHandler("ContentsLoader|requestPatch") { p, msg -> 97 | state.map.tags["CT@$msg"]?.let { sendPatch(msg, it) } 98 | } 99 | } 100 | 101 | onDisable { 102 | netServer.getPacketHandlers("ContentsLoader|version").clear() 103 | netServer.getPacketHandlers("ContentsLoader|requestPatch").clear() 104 | } -------------------------------------------------------------------------------- /scripts/coreMindustry/lib/CommandExt.kt: -------------------------------------------------------------------------------- 1 | package coreMindustry.lib 2 | 3 | import cf.wayzer.scriptAgent.define.Script 4 | import cf.wayzer.scriptAgent.define.ScriptDsl 5 | import coreLibrary.lib.CommandHandler 6 | import coreLibrary.lib.CommandInfo 7 | import coreLibrary.lib.command 8 | import coreLibrary.lib.with 9 | 10 | /** 11 | * 注册指令 12 | * 所有body将在 Dispatchers.game下调用, 费时操作请注意launch并切换Dispatcher 13 | */ 14 | @ScriptDsl 15 | @Deprecated( 16 | "move to coreLibrary", ReplaceWith("command(name,description.with()){init()}", "coreLibrary.lib.command"), 17 | DeprecationLevel.HIDDEN 18 | ) 19 | fun Script.command(name: String, description: String, init: CommandInfo.() -> Unit) { 20 | command(name, description.with()) { init() } 21 | } 22 | 23 | @Deprecated( 24 | "use new command api", ReplaceWith("command(name,description.with()){init\nbody(handler)}"), DeprecationLevel.ERROR 25 | ) 26 | fun Script.command(name: String, description: String, init: CommandInfo.() -> Unit, handler: CommandHandler) { 27 | command(name, description.with()) { 28 | init() 29 | body(handler) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /scripts/coreMindustry/lib/ContentExt.kt: -------------------------------------------------------------------------------- 1 | package coreMindustry.lib 2 | 3 | import arc.func.Cons2 4 | import arc.struct.ObjectMap 5 | import cf.wayzer.scriptAgent.define.Script 6 | import cf.wayzer.scriptAgent.define.ScriptDsl 7 | import coreLibrary.lib.util.reflectDelegate 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.withContext 11 | import mindustry.Vars 12 | import mindustry.game.EventType 13 | import mindustry.net.Administration 14 | import mindustry.net.Net 15 | import mindustry.net.NetConnection 16 | import mindustry.net.Packet 17 | 18 | val Net.serverListeners: ObjectMap, Cons2> by reflectDelegate() 19 | 20 | @ScriptDsl 21 | inline fun Script.onEnableForGame(crossinline block: suspend () -> Unit) { 22 | onEnable { 23 | withContext(Dispatchers.game) { 24 | block() 25 | } 26 | } 27 | } 28 | 29 | @ScriptDsl 30 | inline fun Script.onDisableForGame(crossinline block: suspend () -> Unit) { 31 | onDisable { 32 | withContext(Dispatchers.game) { 33 | block() 34 | } 35 | } 36 | } 37 | 38 | 39 | @Suppress("UNCHECKED_CAST") 40 | inline fun getPacketHandle() = 41 | (Vars.net.serverListeners[T::class.java] as Cons2?) ?: Cons2 { con: NetConnection, p: T -> 42 | p.handleServer(con) 43 | } 44 | 45 | /** 46 | * @param handle return true to call old handler/origin 47 | */ 48 | @ScriptDsl 49 | inline fun Script.listenPacket2Server(crossinline handle: (NetConnection, T) -> Boolean) { 50 | onEnableForGame { 51 | val old = getPacketHandle() 52 | Vars.net.handleServer(T::class.java) { con, p -> 53 | if (handle(con, p)) 54 | old.get(con, p) 55 | } 56 | onDisableForGame { 57 | Vars.net.handleServer(T::class.java, old) 58 | } 59 | } 60 | } 61 | 62 | @ScriptDsl 63 | inline fun Script.listenPacket2ServerAsync( 64 | crossinline handle: suspend (NetConnection, T) -> Boolean 65 | ) { 66 | onEnableForGame { 67 | val old = getPacketHandle() 68 | Vars.net.handleServer(T::class.java) { con, p -> 69 | this@listenPacket2ServerAsync.launch(Dispatchers.game) { 70 | if (handle(con, p)) 71 | old.get(con, p) 72 | } 73 | } 74 | onDisableForGame { 75 | Vars.net.handleServer(T::class.java, old) 76 | } 77 | } 78 | } 79 | 80 | @ScriptDsl 81 | fun Script.registerActionFilter(handle: Administration.ActionFilter) { 82 | onEnableForGame { 83 | Vars.netServer.admins.actionFilters.add(handle) 84 | onDisableForGame { 85 | Vars.netServer.admins.actionFilters.remove(handle) 86 | } 87 | } 88 | } 89 | 90 | /** 91 | * Support for utilContentOverwrite 92 | * auto re[init] when [EventType.ContentInitEvent] 93 | */ 94 | @ScriptDsl 95 | @Deprecated("no use ContentsLoader", ReplaceWith("lazy{ init() }"), DeprecationLevel.HIDDEN) 96 | inline fun Script.useContents(crossinline init: () -> T) = lazy { init() } -------------------------------------------------------------------------------- /scripts/coreMindustry/lib/ContentHelper.kt: -------------------------------------------------------------------------------- 1 | package coreMindustry.lib 2 | 3 | import arc.util.Log 4 | import arc.util.Strings 5 | import coreLibrary.lib.* 6 | import mindustry.gen.Call 7 | import mindustry.gen.Groups 8 | import mindustry.gen.Iconc 9 | import mindustry.gen.Player 10 | 11 | object ContentHelper { 12 | fun logToConsole(text: String) { 13 | Log.info(Strings.stripColors(ColorApi.handle(text, ColorApi::consoleColorHandler))) 14 | } 15 | 16 | fun logToConsole(text: PlaceHoldString) { 17 | val parsed = text.with("receiver" to CommandContext.ConsoleReceiver).toString() 18 | logToConsole(parsed) 19 | } 20 | 21 | fun mindustryColorHandler(color: ColorApi.Color): String { 22 | if (color is ConsoleColor) { 23 | return when (color) { 24 | ConsoleColor.LIGHT_YELLOW -> "[gold]" 25 | ConsoleColor.LIGHT_PURPLE -> "[magenta]" 26 | ConsoleColor.LIGHT_RED -> "[scarlet]" 27 | ConsoleColor.LIGHT_CYAN -> "[cyan]" 28 | ConsoleColor.LIGHT_GREEN -> "[acid]" 29 | else -> "[${color.name}]" 30 | } 31 | } 32 | return "" 33 | } 34 | } 35 | 36 | enum class MsgType { Message, InfoMessage, InfoToast, WarningToast, Announce } 37 | 38 | fun broadcast( 39 | text: PlaceHoldString, 40 | type: MsgType = MsgType.Message, 41 | time: Float = 10f, 42 | quite: Boolean = false, 43 | players: Iterable = Groups.player 44 | ) { 45 | if (!quite) ContentHelper.logToConsole(text) 46 | MindustryDispatcher.runInMain { 47 | players.forEach { 48 | if (it.con != null) 49 | it.sendMessage(text, type, time) 50 | } 51 | } 52 | } 53 | 54 | fun Player?.sendMessage(text: PlaceHoldString, type: MsgType = MsgType.Message, time: Float = 10f) { 55 | if (this == null) ContentHelper.logToConsole(text) 56 | else { 57 | if (con == null) return 58 | MindustryDispatcher.runInMain { 59 | val msg = text.toPlayer(this) 60 | when (type) { 61 | MsgType.Message -> Call.sendMessage(this.con, msg, null, null) 62 | MsgType.InfoMessage -> Call.infoMessage(this.con, msg) 63 | MsgType.InfoToast -> Call.infoToast(this.con, msg, time) 64 | MsgType.WarningToast -> Call.warningToast(this.con, Iconc.warning.code, msg) 65 | MsgType.Announce -> Call.announce(this.con, msg) 66 | } 67 | } 68 | } 69 | } 70 | 71 | fun PlaceHoldString.toPlayer(player: Player): String = ColorApi.handle( 72 | with("player" to player, "receiver" to player).toString(), 73 | ContentHelper::mindustryColorHandler 74 | ) 75 | 76 | @Deprecated("use PlaceHoldString", ReplaceWith("sendMessage(text.with(), type, time)", "coreLibrary.lib.with")) 77 | fun Player?.sendMessage(text: String, type: MsgType = MsgType.Message, time: Float = 10f) = 78 | sendMessage(text.with(), type, time) -------------------------------------------------------------------------------- /scripts/coreMindustry/lib/DispatcherExt.kt: -------------------------------------------------------------------------------- 1 | package coreMindustry.lib 2 | 3 | import arc.Core 4 | import arc.util.Interval 5 | import cf.wayzer.scriptAgent.thisContextScript 6 | import kotlinx.coroutines.* 7 | import java.util.concurrent.ConcurrentLinkedQueue 8 | import java.util.logging.Level 9 | import kotlin.coroutines.* 10 | import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED 11 | import kotlin.coroutines.intrinsics.intercepted 12 | import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn 13 | 14 | object MindustryDispatcher : CoroutineDispatcher() { 15 | private var mainThread: Thread? = null 16 | private var blockingQueue = ConcurrentLinkedQueue() 17 | 18 | @Volatile 19 | private var inBlocking = false 20 | private val warnTimer = Interval() 21 | 22 | init { 23 | Core.app.post { 24 | mainThread = Thread.currentThread() 25 | } 26 | } 27 | 28 | override fun isDispatchNeeded(context: CoroutineContext): Boolean { 29 | return Thread.currentThread() != mainThread && mainThread?.isAlive == true 30 | } 31 | 32 | override fun dispatch(context: CoroutineContext, block: Runnable) { 33 | if (inBlocking) { 34 | blockingQueue.add(block) 35 | return 36 | } 37 | Core.app.post(block)//Already has catcher in coroutine 38 | } 39 | 40 | @OptIn(InternalCoroutinesApi::class) 41 | override fun dispatchYield(context: CoroutineContext, block: Runnable) { 42 | if (warnTimer[10 * 60f]) 43 | thisContextScript().logger.log( 44 | Level.WARNING, "avoid use yield() in Dispatchers.game, use nextTick instead", Exception() 45 | ) 46 | Core.app.post(block) 47 | } 48 | 49 | /** 50 | * run in mindustry main thread 51 | * call [Core.app.post()] when need 52 | * @see [runInMain] with catch to prevent close main thread 53 | */ 54 | @Suppress("MemberVisibilityCanBePrivate") 55 | fun runInMainUnsafe(block: Runnable) { 56 | if (Thread.currentThread() == mainThread || mainThread?.isAlive == false) block.run() 57 | else Core.app.post(block) 58 | } 59 | 60 | /** 61 | * run in mindustry main thread 62 | * call [Core.app.post()] when need 63 | */ 64 | fun runInMain(block: Runnable) { 65 | runInMainUnsafe { 66 | try { 67 | block.run() 68 | } catch (e: Throwable) { 69 | e.printStackTrace() 70 | } 71 | } 72 | } 73 | 74 | fun safeBlocking(block: suspend CoroutineScope.() -> T): T { 75 | check(Thread.currentThread() == mainThread) { "safeBlocking only for mainThread" } 76 | if (inBlocking) return runBlocking(Dispatchers.game, block) 77 | inBlocking = true 78 | return runBlocking { 79 | launch { 80 | while (inBlocking || blockingQueue.isNotEmpty()) { 81 | blockingQueue.poll()?.run() ?: yield() 82 | } 83 | } 84 | try { 85 | withContext(Dispatchers.game, block) 86 | } finally { 87 | inBlocking = false 88 | } 89 | } 90 | } 91 | 92 | object Post : CoroutineDispatcher() { 93 | override fun dispatch(context: CoroutineContext, block: Runnable) { 94 | if (mainThread?.isAlive == false) 95 | block.run() 96 | else 97 | Core.app.post(block) 98 | } 99 | } 100 | } 101 | 102 | @Suppress("unused") 103 | val Dispatchers.game 104 | get() = MindustryDispatcher 105 | 106 | @Suppress("unused") 107 | val Dispatchers.gamePost 108 | get() = MindustryDispatcher.Post 109 | 110 | //suspend fun nextTick() = yield() 111 | suspend fun nextTick() { 112 | val context = coroutineContext 113 | context.ensureActive() 114 | if (context[ContinuationInterceptor] !is MindustryDispatcher) { 115 | suspendCoroutine { Core.app.post { it.resume(Unit) } } 116 | return 117 | } 118 | suspendCoroutineUninterceptedOrReturn sc@{ cont -> 119 | val co = cont.intercepted() as? Runnable ?: return@sc Unit 120 | Core.app.post(co) 121 | COROUTINE_SUSPENDED 122 | } 123 | } -------------------------------------------------------------------------------- /scripts/coreMindustry/lib/ListenExt.kt: -------------------------------------------------------------------------------- 1 | package coreMindustry.lib 2 | 3 | import arc.Events 4 | import arc.func.Cons 5 | import arc.struct.ObjectMap 6 | import arc.struct.Seq 7 | import arc.util.Log 8 | import cf.wayzer.scriptAgent.Event 9 | import cf.wayzer.scriptAgent.define.Script 10 | import cf.wayzer.scriptAgent.define.ScriptDsl 11 | import cf.wayzer.scriptAgent.events.ScriptDisableEvent 12 | import cf.wayzer.scriptAgent.events.ScriptEnableEvent 13 | import cf.wayzer.scriptAgent.getContextScript 14 | import cf.wayzer.scriptAgent.listenTo 15 | import cf.wayzer.scriptAgent.util.DSLBuilder 16 | import coreMindustry.lib.Listener.Companion.listener 17 | import kotlinx.coroutines.Dispatchers 18 | import kotlinx.coroutines.withContext 19 | 20 | open class Listener( 21 | val script: Script?, 22 | private val key: Any, 23 | val insert: Boolean = false, 24 | val handler: (T) -> Unit 25 | ) : Cons { 26 | fun register() { 27 | map.get(key) { Seq(Cons::class.java) }.let { 28 | if (insert) it.insert(0, this) else it.add(this) 29 | } 30 | } 31 | 32 | fun unregister() { 33 | map[key]?.remove(this) 34 | } 35 | 36 | override fun get(p0: T) { 37 | try { 38 | if (script?.enabled != false) handler(p0) 39 | } catch (e: Exception) { 40 | Log.err("Error when handle event $this in ${script?.id ?: "Unknown"}", e) 41 | } 42 | } 43 | 44 | @Deprecated("removed", level = DeprecationLevel.HIDDEN) 45 | class OnClass( 46 | script: Script?, 47 | cls: Class, 48 | handler: (T) -> Unit 49 | ) : Listener(script, cls, handler = handler) 50 | 51 | companion object { 52 | private val key = DSLBuilder.DataKeyWithDefault("listener") { mutableListOf>() } 53 | val Script.listener by key 54 | 55 | @Suppress("UNCHECKED_CAST") 56 | private val map = Events::class.java.getDeclaredField("events").apply { 57 | isAccessible = true 58 | }.get(this) as ObjectMap>> 59 | 60 | init { 61 | Listener::class.java.getContextScript().apply { 62 | listenTo(Event.Priority.After) { 63 | if (!script.dslExists(key)) return@listenTo 64 | withContext(Dispatchers.game) { 65 | script.listener.forEach { it.register() } 66 | } 67 | } 68 | listenTo(Event.Priority.Before) { 69 | if (!script.dslExists(key)) return@listenTo 70 | withContext(Dispatchers.game) { 71 | script.listener.forEach { it.unregister() } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | @Deprecated("hidden", level = DeprecationLevel.HIDDEN) 80 | @ScriptDsl 81 | fun Script.listen(v: T, handler: (T) -> Unit) { 82 | listener.add(Listener(this, v, handler = handler)) 83 | } 84 | 85 | @ScriptDsl 86 | inline fun Script.listen(insert: Boolean = false, noinline handler: (T) -> Unit) { 87 | listener.add(Listener(this, T::class.java, insert, handler)) 88 | } 89 | 90 | 91 | @ScriptDsl 92 | fun Script.listen(v: T, insert: Boolean = false, handler: (T) -> Unit) { 93 | listener.add(Listener(this, v, insert, handler)) 94 | } 95 | -------------------------------------------------------------------------------- /scripts/coreMindustry/lib/PermissionExt.kt: -------------------------------------------------------------------------------- 1 | package coreMindustry.lib 2 | 3 | import coreLibrary.lib.PermissionApi 4 | import mindustry.gen.Player 5 | 6 | suspend fun Player.hasPermission(permission: String): Boolean { 7 | return PermissionApi.handleThoughEvent(this, permission, listOf(uuid())).has 8 | } -------------------------------------------------------------------------------- /scripts/coreMindustry/menu.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry 2 | 3 | import coreLibrary.lib.Commands.Hidden 4 | 5 | 6 | listen { 7 | MenuChooseEvent(it.player, it.menuId, it.option).launchEmit(coroutineContext + Dispatchers.game) { e -> 8 | if (!e.received && it.menuId < 0) 9 | Call.hideFollowUpMenu(e.player.con, e.menuId) 10 | } 11 | } 12 | 13 | onEnable { 14 | val bak = Commands.helpOverwrite 15 | onDisable { Commands.helpOverwrite = bak } 16 | Commands.helpOverwrite = impl@{ cmds, showAll, page -> 17 | val player = player ?: return@impl 18 | 19 | var commands = cmds.subCommands().values.toSet().sortedBy { it.name } 20 | if (!showAll) commands = commands.filter { info -> 21 | info.attrs.all { it !is Hidden || it.visible() } 22 | } 23 | MenuV2(player) { 24 | title = if (prefix.isEmpty()) "Help" else "Help: $prefix" 25 | msg = "点击选项将直接执行指令" 26 | columnPreRow = 1 27 | renderPaged(commands, page) { 28 | option(buildString { 29 | append("[gold]${prefix}${it.name}") 30 | if (it.aliases.isNotEmpty()) 31 | append("[scarlet](${it.aliases.joinToString()})") 32 | appendLine(" [white]${it.usage}") 33 | append("[cyan]${it.description.toPlayer(player)}") 34 | if (showAll) { 35 | it.script?.let { append(" | ${it.id}") } 36 | if (it.permission.isNotBlank()) append(" | ${it.permission}") 37 | } 38 | }) { 39 | arg = listOf(it.name) 40 | reply("[yellow][快捷输入指令][] {command}".with("command" to (prefix + it.name))) 41 | cmds.handle() 42 | } 43 | } 44 | }.send().awaitWithTimeout() 45 | CommandInfo.Return() 46 | } 47 | } -------------------------------------------------------------------------------- /scripts/coreMindustry/module.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("coreLibrary") 2 | @file:Import("arc.Core", libraryByClass = true) 3 | @file:Import("mindustry.Vars", libraryByClass = true) 4 | @file:Import("arc.Core", defaultImport = true) 5 | @file:Import("mindustry.Vars.*", defaultImport = true) 6 | @file:Import("mindustry.content.*", defaultImport = true) 7 | @file:Import("mindustry.gen.Player", defaultImport = true) 8 | @file:Import("mindustry.gen.Call", defaultImport = true) 9 | @file:Import("mindustry.gen.Groups", defaultImport = true) 10 | @file:Import("mindustry.game.EventType", defaultImport = true) 11 | @file:Import("coreMindustry.lib.*", defaultImport = true) 12 | 13 | package coreMindustry 14 | 15 | Listener//ensure init 16 | onEnable { 17 | RootCommands.hookGameHandler() 18 | } 19 | -------------------------------------------------------------------------------- /scripts/coreMindustry/scorebroad.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry 2 | //WayZer 版权所有(请勿删除版权注解) 3 | import arc.util.Align 4 | import java.time.Duration 5 | 6 | name = "扩展功能: 积分榜" 7 | //建议只修改下面一段,其他地方代码请勿乱动 8 | val msg = """ 9 | [magenta]欢迎[goldenrod]{player.name}[magenta]来到WZ服务器[red](请在语言文件中修改) 10 | [violet]当前地图为: [yellow][{map.id}][orange]{map.name} 11 | [violet]本局游戏时间: [orange]{state.gameTime:分钟} 12 | {scoreBroad.ext.*:${"\n"}} 13 | [royal]输入/broad可以开关该显示 14 | """.trimIndent() 15 | 16 | val disabled = mutableSetOf() 17 | 18 | command("broad", "开关积分板显示") { 19 | this.type = CommandType.Client 20 | body { 21 | if (!disabled.remove(player!!.uuid())) 22 | disabled.add(player!!.uuid()) 23 | reply("[green]切换成功".with()) 24 | } 25 | } 26 | 27 | //避免找不到 scoreBroad.ext.* 变量 28 | registerVar("scoreBroad.ext.null", "空占位", null) 29 | 30 | onEnable { 31 | loop(Dispatchers.game) { 32 | delay(Duration.ofSeconds(2).toMillis()) 33 | Groups.player.forEach { 34 | if (disabled.contains(it.uuid())) return@forEach 35 | val mobile = it.con?.mobile == true 36 | Call.infoPopup( 37 | it.con, msg.with().toPlayer(it), 2.013f, 38 | Align.topLeft, if (mobile) 210 else 155, 0, 0, 0 39 | ) 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /scripts/coreMindustry/util/packetHelper.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry.util 2 | 3 | import arc.util.io.Reads 4 | import arc.util.io.ReusableByteInStream 5 | import arc.util.io.ReusableByteOutStream 6 | import arc.util.io.Writes 7 | import mindustry.gen.Building 8 | import mindustry.gen.Call 9 | import java.io.DataInputStream 10 | import java.io.DataOutputStream 11 | 12 | class Buffer { 13 | private val outStream = ReusableByteOutStream() 14 | private val inStream = ReusableByteInStream() 15 | val writes = Writes(DataOutputStream(outStream)) 16 | val reads = Reads(DataInputStream(inStream)) 17 | 18 | val size get() = outStream.size() 19 | 20 | fun flushBytes(): ByteArray { 21 | writes.close() 22 | val res = outStream.toByteArray() 23 | outStream.reset() 24 | return res 25 | } 26 | 27 | fun flushReads(): Reads { 28 | writes.close() 29 | inStream.setBytes(outStream.bytes, 0, outStream.size()) 30 | outStream.reset() 31 | return reads 32 | } 33 | } 34 | 35 | fun syncTile(builds: List) { 36 | val dataBuffer = Buffer() 37 | var sent = 0 38 | builds.forEach { 39 | sent++ 40 | dataBuffer.writes.i(it.pos()) 41 | dataBuffer.writes.s(it.block.id.toInt()) 42 | it.writeAll(dataBuffer.writes) 43 | if (dataBuffer.size > 800) { 44 | Call.blockSnapshot(sent.toShort(), dataBuffer.flushBytes()) 45 | sent = 0 46 | } 47 | } 48 | if (sent > 0) { 49 | Call.blockSnapshot(sent.toShort(), dataBuffer.flushBytes()) 50 | } 51 | } -------------------------------------------------------------------------------- /scripts/coreMindustry/util/spawnAround.api.kt: -------------------------------------------------------------------------------- 1 | package coreMindustry.util 2 | 3 | import arc.math.geom.Geometry 4 | import arc.math.geom.Point2 5 | import mindustry.Vars 6 | import mindustry.game.Team 7 | import mindustry.gen.Posc 8 | import mindustry.type.UnitType 9 | 10 | /** 11 | * @return 无法找到合适位置,返回null 12 | */ 13 | fun UnitType.spawnAround(pos: Posc, team: Team, radius: Int = 10): mindustry.gen.Unit? { 14 | return create(team).apply { 15 | set(pos) 16 | val valid = mutableListOf() 17 | Geometry.circle(tileX(), tileY(), Vars.world.width(), Vars.world.height(), radius) { x, y -> 18 | if (canPass(x, y) && (!canDrown() || floorOn()?.isDeep == false)) 19 | valid.add(Point2(x, y)) 20 | } 21 | val r = valid.randomOrNull() ?: return null 22 | x = r.x * Vars.tilesize.toFloat() 23 | y = r.y * Vars.tilesize.toFloat() 24 | add() 25 | } 26 | } -------------------------------------------------------------------------------- /scripts/coreMindustry/util/spawnAround.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry.util 2 | -------------------------------------------------------------------------------- /scripts/coreMindustry/util/trackBuilding.api.kt: -------------------------------------------------------------------------------- 1 | package coreMindustry.util 2 | 3 | import arc.math.geom.QuadTree 4 | import cf.wayzer.scriptAgent.define.Script 5 | import coreMindustry.lib.listen 6 | import mindustry.game.EventType 7 | import mindustry.gen.Building 8 | 9 | interface BuildingTracker { 10 | fun onAdd(building: B) 11 | fun onRemove(building: B) 12 | } 13 | 14 | inline fun Script.trackBuilding( 15 | tracker: BuildingTracker, 16 | crossinline filter: (B) -> Boolean = { true } 17 | ) { 18 | listen { 19 | val build = it.tile.build 20 | if (build?.tile == it.tile && build is B && filter(build)) 21 | tracker.onRemove(build) 22 | } 23 | listen { 24 | val build = it.tile.build 25 | if (build?.tile == it.tile && build is B && filter(build)) 26 | tracker.onAdd(build) 27 | } 28 | } 29 | 30 | fun QuadTree.asTracker() = object : BuildingTracker { 31 | override fun onAdd(building: B) = insert(building) 32 | override fun onRemove(building: B) { 33 | remove(building) 34 | } 35 | } -------------------------------------------------------------------------------- /scripts/coreMindustry/util/trackBuilding.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry.util -------------------------------------------------------------------------------- /scripts/coreMindustry/utilMapRule.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry 2 | 3 | import mindustry.core.ContentLoader 4 | import mindustry.ctype.Content 5 | import mindustry.ctype.MappableContent 6 | import java.lang.reflect.Modifier 7 | import kotlin.reflect.KMutableProperty0 8 | 9 | val bakMap = mutableMapOf, Any?>() 10 | 11 | /**Should invoke in [Dispatchers.game] */ 12 | fun newContent(origin: T, block: (origin: T) -> R): R { 13 | val bak = content 14 | content = object : ContentLoader() { 15 | override fun transformName(name: String?) = bak?.transformName(name) ?: name 16 | override fun handleContent(content: Content?) = Unit 17 | override fun handleMappableContent(content: MappableContent?) = Unit 18 | } 19 | return try { 20 | block(origin).also { new -> 21 | origin::class.java.fields.forEach { 22 | if (!it.declaringClass.isInstance(new)) return@forEach 23 | if (Modifier.isPublic(it.modifiers) && !Modifier.isFinal(it.modifiers)) { 24 | it.set(new, it.get(origin)) 25 | } 26 | } 27 | } 28 | } finally { 29 | content = bak 30 | } 31 | } 32 | 33 | fun registerMapRule(field: KMutableProperty0, checkRef: Boolean = true, valueFactory: (T) -> T) { 34 | synchronized(bakMap) { 35 | @Suppress("UNCHECKED_CAST") 36 | val old = (bakMap[field] as T?) ?: field.get() 37 | val new = valueFactory(old) 38 | if (field !in bakMap && checkRef && new is Any && new === old) 39 | error("valueFactory can't return the same instance for $field") 40 | field.set(new) 41 | bakMap[field] = old 42 | } 43 | } 44 | 45 | listen { 46 | synchronized(bakMap) { 47 | bakMap.forEach { (field, bakValue) -> 48 | @Suppress("UNCHECKED_CAST") 49 | (field as KMutableProperty0).set(bakValue) 50 | } 51 | bakMap.clear() 52 | } 53 | } -------------------------------------------------------------------------------- /scripts/coreMindustry/utilNextChat.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry 2 | 3 | import mindustry.gen.SendChatMessageCallPacket 4 | 5 | data class OnChat(val player: Player, val text: String) : Event, ReceivedEvent { 6 | override var received: Boolean = false 7 | 8 | companion object : Event.Handler() 9 | } 10 | 11 | listenPacket2ServerAsync { con, p -> 12 | con.player?.let { OnChat(it, p.message).emitAsync().received.not() } ?: true 13 | } 14 | 15 | suspend fun nextChat(player: Player, timeoutMillis: Int): String? = withTimeoutOrNull(timeoutMillis.toLong()) { 16 | nextEvent { it.player == player }.text 17 | } 18 | export(::nextChat) -------------------------------------------------------------------------------- /scripts/coreMindustry/utilTextInput.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry 2 | 3 | import mindustry.game.EventType.TextInputEvent 4 | import kotlin.random.Random 5 | 6 | data class OnTextInputResult(val player: Player, val id: Int, val text: String?) : Event { 7 | companion object : Event.Handler() 8 | } 9 | 10 | listen { 11 | OnTextInputResult(it.player, it.textInputId, it.text).launchEmit(coroutineContext + Dispatchers.game) 12 | } 13 | 14 | suspend fun textInput( 15 | player: Player, 16 | title: String, 17 | message: String = "", 18 | default: String = "", 19 | lengthLimit: Int = Int.MAX_VALUE, 20 | isNumeric: Boolean = false, 21 | timeoutMillis: Int = 60_000 22 | ): String? = withTimeoutOrNull(timeoutMillis.toLong()) { 23 | val id = Random.nextInt(Int.MIN_VALUE, 0) 24 | Call.textInput(player.con, id, title, message, lengthLimit, default, isNumeric) 25 | nextEvent { it.player == player && it.id == id }.text 26 | } 27 | export(::textInput) -------------------------------------------------------------------------------- /scripts/mapScript/lib/ContentExt.kt: -------------------------------------------------------------------------------- 1 | package mapScript.lib 2 | 3 | import cf.wayzer.scriptAgent.define.Script 4 | import cf.wayzer.scriptAgent.define.ScriptDsl 5 | import cf.wayzer.scriptAgent.depends 6 | import cf.wayzer.scriptAgent.import 7 | 8 | @ScriptDsl 9 | fun Script.modeIntroduce(mode: String, introduce: String) { 10 | onEnable { 11 | depends("wayzer/map/mapInfo")?.import<(String, String) -> Unit>("addModeIntroduce") 12 | ?.invoke(mode, introduce) 13 | } 14 | } -------------------------------------------------------------------------------- /scripts/mapScript/lib/GeneratorSupport.kt: -------------------------------------------------------------------------------- 1 | package mapScript.lib 2 | 3 | import arc.struct.StringMap 4 | import cf.wayzer.scriptAgent.Event 5 | import cf.wayzer.scriptAgent.ScriptRegistry 6 | import cf.wayzer.scriptAgent.define.Script 7 | import cf.wayzer.scriptAgent.define.ScriptDsl 8 | import cf.wayzer.scriptAgent.define.ScriptState 9 | import cf.wayzer.scriptAgent.events.ScriptStateChangeEvent 10 | import cf.wayzer.scriptAgent.listenTo 11 | import cf.wayzer.scriptAgent.thisContextScript 12 | import cf.wayzer.scriptAgent.util.DSLBuilder 13 | import coreLibrary.lib.PlaceHoldString 14 | import mindustry.Vars 15 | import mindustry.game.Gamemode 16 | import mindustry.game.Rules 17 | import mindustry.io.JsonIO 18 | import mindustry.maps.Map 19 | import mindustry.world.Tiles 20 | import wayzer.MapInfo 21 | import wayzer.MapManager 22 | import wayzer.MapProvider 23 | import wayzer.MapRegistry 24 | import java.util.logging.Level 25 | import kotlin.contracts.ExperimentalContracts 26 | import kotlin.contracts.InvocationKind 27 | import kotlin.contracts.contract 28 | import kotlin.system.measureTimeMillis 29 | 30 | object GeneratorSupport { 31 | val knownMaps = mutableMapOf>>() 32 | 33 | object Provider : MapProvider() { 34 | override suspend fun searchMaps(search: String?) = knownMaps.values 35 | .filter { search == null || search in it.second } 36 | .map { it.first } 37 | 38 | override suspend fun findById(id: Int, reply: ((PlaceHoldString) -> Unit)?): MapInfo? { 39 | return knownMaps[id]?.first 40 | } 41 | 42 | override suspend fun loadMap(info: MapInfo) { 43 | val scriptId = "mapScript/${info.id}" 44 | val script = findAndLoadScript(scriptId)?.inst ?: return MapManager.loadMap() 45 | val map = script.mapInfo ?: return MapManager.loadMap() 46 | try { 47 | Vars.world.loadGenerator(map.width, map.height) { tiles -> 48 | script.genRound.forEach { (name, round) -> 49 | val time = measureTimeMillis { round(tiles) } 50 | script.logger.info("Do $name costs $time ms.") 51 | } 52 | } 53 | } catch (e: Throwable) { 54 | script.logger.log(Level.SEVERE, "loadGenerator出错", e) 55 | MapManager.loadMap() 56 | } 57 | } 58 | } 59 | 60 | fun checkScript(script: Script) { 61 | val id = script.id.removePrefix("mapScript/").toIntOrNull() ?: return 62 | knownMaps.remove(id) 63 | 64 | val map = script.mapInfo ?: return 65 | val info = MapInfo(Provider, id, script.mapMode, map) 66 | knownMaps[id] = info to script.mapFilters 67 | } 68 | 69 | private fun Script.init() { 70 | val moduleId = id 71 | listenTo(Event.Priority.Watch) { 72 | if (next == ScriptState.Loaded && script.id.startsWith(moduleId)) { 73 | script.inst?.let(GeneratorSupport::checkScript) 74 | } 75 | } 76 | onEnable { 77 | ScriptRegistry.allScripts { it.scriptState.loaded && it.id.startsWith(moduleId) } 78 | .forEach { it.inst?.let(GeneratorSupport::checkScript) } 79 | } 80 | MapRegistry.register(this, Provider) 81 | } 82 | 83 | init { 84 | thisContextScript().init() 85 | } 86 | } 87 | 88 | var Script.mapInfo by DSLBuilder.dataKey() 89 | var Script.mapFilters by DSLBuilder.dataKeyWithDefault { setOf("all", "display", "special") } 90 | var Script.mapMode by DSLBuilder.dataKeyWithDefault { Gamemode.survival } 91 | 92 | @ScriptDsl 93 | val Script.genRound by DSLBuilder.dataKeyWithDefault { 94 | mutableListOf Unit>>("init" to { it.fill() }) 95 | } 96 | 97 | @OptIn(ExperimentalContracts::class) 98 | @ScriptDsl 99 | inline fun Script.setMapInfo(width: Int, height: Int, tagsApply: StringMap.() -> Unit, rulesApply: Rules.() -> Unit) { 100 | contract { 101 | callsInPlace(tagsApply, InvocationKind.EXACTLY_ONCE) 102 | callsInPlace(rulesApply, InvocationKind.EXACTLY_ONCE) 103 | } 104 | val rules = Rules().apply(rulesApply) 105 | val tags = StringMap().apply(tagsApply) 106 | tags.put("rules", JsonIO.write(rules)) 107 | mapInfo = Map(Vars.customMapDirectory.child("unknown"), width, height, tags, true) 108 | } -------------------------------------------------------------------------------- /scripts/mapScript/lib/TagSupport.kt: -------------------------------------------------------------------------------- 1 | package mapScript.lib 2 | 3 | import cf.wayzer.scriptAgent.define.Script 4 | import mindustry.Vars 5 | import mindustry.game.Rules 6 | import kotlin.properties.ReadOnlyProperty 7 | 8 | /**用于注册Tag类的mapScript,通常存放位置为`mapScript/tag/xxx` */ 9 | object TagSupport { 10 | // tag -> scriptId 11 | val knownTags = mutableMapOf() 12 | 13 | fun findTags(rules: Rules): Map { 14 | val mapTags = rules.tags.keys().toSet() 15 | return knownTags.filterKeys { it in mapTags } 16 | } 17 | } 18 | 19 | fun Script.registerMapTag(name: String) { 20 | TagSupport.knownTags[name] = id 21 | onUnload { TagSupport.knownTags.remove(name) } 22 | } 23 | 24 | fun Script.mapTag(name: String): ReadOnlyProperty { 25 | registerMapTag(name) 26 | return ReadOnlyProperty { _, _ -> 27 | Vars.state.rules.tags.get(name).orEmpty() 28 | } 29 | } -------------------------------------------------------------------------------- /scripts/mapScript/lib/util.kt: -------------------------------------------------------------------------------- 1 | package mapScript.lib 2 | 3 | import cf.wayzer.scriptAgent.ScriptManager 4 | import cf.wayzer.scriptAgent.ScriptRegistry 5 | import cf.wayzer.scriptAgent.contextScript 6 | import cf.wayzer.scriptAgent.define.Script 7 | import cf.wayzer.scriptAgent.define.ScriptInfo 8 | import coreLibrary.lib.PlaceHoldString 9 | import coreLibrary.lib.with 10 | import coreMindustry.lib.MindustryDispatcher 11 | import coreMindustry.lib.broadcast 12 | import coreMindustry.lib.game 13 | import coreMindustry.lib.gamePost 14 | import kotlinx.coroutines.CoroutineScope 15 | import kotlinx.coroutines.Dispatchers 16 | import kotlinx.coroutines.delay 17 | import kotlinx.coroutines.launch 18 | import mindustry.Vars 19 | import mindustry.core.GameState 20 | import kotlin.coroutines.CoroutineContext 21 | import kotlin.coroutines.EmptyCoroutineContext 22 | import kotlin.time.Duration 23 | import kotlin.time.Duration.Companion.seconds 24 | 25 | //find and ensure loaded 26 | internal suspend fun findAndLoadScript(id: String): ScriptInfo? { 27 | val script = ScriptRegistry.getScriptInfo(id) ?: return null 28 | ScriptManager.transaction { 29 | add(script) 30 | load() 31 | } 32 | return script.takeIf { it.inst != null } 33 | } 34 | 35 | /** 为onEnable中使用,确保玩家能够收到信息 */ 36 | fun Script.delayBroadcast(msg: PlaceHoldString) = launch(Dispatchers.gamePost) { 37 | broadcast(msg) 38 | } 39 | 40 | /** 为onEnable中使用,加载其他MapScript脚本 */ 41 | suspend fun Script.loadMapScript(id: String, reply: (PlaceHoldString) -> Unit = { delayBroadcast(it) }): Boolean { 42 | val script = findAndLoadScript(id)?.scriptInfo 43 | if (script == null) { 44 | reply("[red]该服务器不存在对应地图脚本,请联系管理员: {id}".with("id" to id)) 45 | return false 46 | } 47 | contextScript().toEnable.add(script.scriptInfo) 48 | if (script.enabled) { 49 | return true 50 | } 51 | MindustryDispatcher.safeBlocking { 52 | ScriptManager.enableScript(script, true) 53 | } 54 | if (script.enabled) 55 | reply("[yellow]加载地图脚本完成: {id}".with("id" to script.id)) 56 | else 57 | reply( 58 | "[red]地图脚本{id}加载失败,请联系管理员: {reason}" 59 | .with("id" to script.id, "reason" to script.failReason.orEmpty()) 60 | ) 61 | return script.enabled 62 | } 63 | 64 | @Suppress("UnusedReceiverParameter") 65 | val GameState.gameTime get() = (Vars.state.tick / 60).seconds 66 | 67 | /** 延时,直到特定游戏时间(支持暂停) @see [schedule] */ 68 | suspend fun delayUntil(gameTime: Duration) { 69 | while (true) { 70 | val left = gameTime - Vars.state.gameTime 71 | if (left.isNegative()) break 72 | delay(left) 73 | } 74 | } 75 | 76 | /** 计划在特定游戏时间执行 @see [delayUntil] */ 77 | fun CoroutineScope.schedule( 78 | time: Duration, 79 | context: CoroutineContext = EmptyCoroutineContext, 80 | body: suspend CoroutineScope.() -> Unit 81 | ) { 82 | if (Vars.state.gameTime > time) return 83 | launch(Dispatchers.game + context) { 84 | delayUntil(time) 85 | body() 86 | } 87 | } -------------------------------------------------------------------------------- /scripts/mapScript/module.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("coreMindustry") 2 | @file:Depends("wayzer/maps", "获取地图信息") 3 | @file:Depends("wayzer/map/mapInfo", "显示地图信息", soft = true) 4 | @file:Import("mapScript.lib.*", defaultImport = true) 5 | 6 | /** 7 | * 该模块定义了一种特殊的kts:kts的生命周期与地图关联。 8 | * 当地图满足特定条件时(id/tag),关联的kts会被enable,而一局游戏结束后,所有的kts会被disable。 9 | * */ 10 | package mapScript 11 | 12 | import cf.wayzer.scriptAgent.events.ScriptStateChangeEvent 13 | import wayzer.MapManager 14 | 15 | val moduleId = id 16 | 17 | val toEnable = mutableSetOf() 18 | 19 | listen { 20 | toEnable.clear() 21 | MindustryDispatcher.safeBlocking { 22 | ScriptManager.transaction { 23 | add("$moduleId/") 24 | disable() 25 | getForState(ScriptState.ToEnable).forEach { 26 | it.stateUpdateForce(ScriptState.Loaded) 27 | } 28 | } 29 | } 30 | } 31 | 32 | listen { 33 | //try update child scripts 34 | ScriptRegistry.scanRoot() 35 | MindustryDispatcher.safeBlocking { 36 | ScriptManager.transaction { 37 | add("$moduleId/") 38 | removeIf { it.compiledScript?.source.run { this == null || this == it.source } } 39 | if (isEmpty()) return@transaction 40 | 41 | logger.info("Unload outdated script: ${toList()}") 42 | unload()//unload all updatable 43 | } 44 | } 45 | } 46 | 47 | listen { 48 | //load scripts 49 | val scriptId = ScriptManager.getScriptNullable("$moduleId/${MapManager.current.id}")?.id 50 | ?: state.rules.tags.get("@mapScript") 51 | ?.run { "$moduleId/${toIntOrNull() ?: MapManager.current.id}" } 52 | MindustryDispatcher.safeBlocking { 53 | if (scriptId != null) 54 | loadMapScript(scriptId) 55 | TagSupport.findTags(state.rules).values.toSet().forEach { loadMapScript(it) } 56 | } 57 | } 58 | 59 | //阻止其他脚本启用 60 | listenTo(Event.Priority.Intercept) { 61 | if (!script.id.startsWith("$moduleId/")) return@listenTo 62 | fun allowEnable() = toEnable.any { it.dependsOn(script.scriptInfo, includeSoft = true) } 63 | when (next) { 64 | ScriptState.ToEnable -> if (!allowEnable()) cancelled = true 65 | ScriptState.Enabling -> if (!allowEnable()) { 66 | cancelled = true 67 | script.stateUpdateForce(ScriptState.Loaded).join() 68 | } 69 | 70 | else -> {} 71 | } 72 | } 73 | 74 | GeneratorSupport//init 75 | command("mapScriptLoad", "测试: 加载指定地图脚本") { 76 | permission = "$dotId.load" 77 | usage = "