├── .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 | 
2 | 
3 | -success)
4 | 
5 | [](https://github.com/way-zer/ScriptAgent4MindustryExt/actions/workflows/buildPlugin.yml)
6 | [](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 | 
34 | 
35 |
36 | ### 服务器后台预览
37 | 
38 | 
39 | 
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