├── gradle.properties ├── loader ├── mindustry │ ├── res │ │ ├── META-INF │ │ │ └── kotlin │ │ │ │ └── script │ │ │ │ └── templates │ │ │ │ └── .gitkeep │ │ └── plugin.json │ └── src │ │ ├── Loader.kt │ │ └── Main.kt ├── bukkit │ ├── res │ │ └── plugin.yml │ └── src │ │ └── Main.kt └── common │ ├── standalone │ ├── loader.kt │ └── Main.kt │ ├── util │ ├── CAStore.kt │ ├── BuiltinScriptRegistry.kt │ ├── CASPackScriptRegistry.kt │ ├── CASScriptPacker.kt │ ├── CommonMain.kt │ └── CASScriptSource.kt │ └── ConfigExt.kt ├── scripts ├── coreMindustry │ ├── util │ │ ├── spawnAround.kts │ │ ├── trackBuilding.kts │ │ ├── spawnAround.api.kt │ │ ├── trackBuilding.api.kt │ │ └── packetHelper.kts │ ├── lib │ │ ├── PermissionExt.kt │ │ ├── CommandExt.kt │ │ ├── ContentHelper.kt │ │ ├── ContentExt.kt │ │ └── ListenExt.kt │ ├── utilNextChat.kts │ ├── module.kts │ ├── utilTextInput.kts │ ├── menu.kts │ ├── scoreboard.kts │ └── utilMapRule.kts ├── metadata │ ├── kcp.metadata │ ├── javalin.metadata │ ├── mapScript.metadata │ ├── coreMindustry.metadata │ └── coreLibrary.metadata ├── wayzer │ ├── map │ │ ├── mapSnap.block_colors.png │ │ ├── autoHost.kts │ │ ├── backCompatibility.kts │ │ ├── autoSave.kts │ │ ├── pvpProtect.kts │ │ ├── mapSnap.kts │ │ └── mapInfo.kts │ ├── cmds │ │ ├── clearUnit.kts │ │ ├── jsCmd.kts │ │ ├── serverStatus.kts │ │ ├── helpfulCmd.kts │ │ ├── spawnMob.kts │ │ ├── mapsCmd.kts │ │ ├── restart.kts │ │ ├── voteMap.kts │ │ ├── gatherTp.kts │ │ ├── voteKick.kts │ │ ├── vote.kts │ │ └── pixelPicture.kts │ ├── ext │ │ ├── tpsLimit.kts │ │ ├── alert.kts │ │ ├── welcomeMsg.kts │ │ ├── goServer.kts │ │ ├── profiler.kts │ │ └── observer.kts │ ├── reGrief │ │ ├── limitFire.kts │ │ ├── autoChangeMap.kts │ │ ├── bugFixer.kts │ │ ├── limitLogicPacket.kts │ │ └── unitLimit.kts │ ├── user │ │ ├── ext │ │ │ ├── skills.kts │ │ │ └── chatPing.kts │ │ ├── lang.kts │ │ ├── nameExt.kts │ │ ├── suffix.kts │ │ ├── shortID.kts │ │ └── banStore.kts │ ├── lib │ │ ├── ConnectAsyncEvent.kt │ │ └── PlayerData.kt │ ├── pvp │ │ ├── pvpChat.kts │ │ ├── autoGameover.kts │ │ └── pvpAlert.kts │ ├── vote.kts │ └── module.kts ├── javalin │ ├── simple.kts │ ├── lib │ │ └── ScriptExt.kt │ └── module.kts ├── coreLibrary │ ├── lib │ │ ├── event │ │ │ ├── ServiceProvidedEvent.kt │ │ │ └── RequestPermissionEvent.kt │ │ ├── util │ │ │ ├── ReflectHelper.kt │ │ │ ├── nextEvent.kt │ │ │ ├── menu.kt │ │ │ ├── coroutine.kt │ │ │ └── ServiceRegistry.kt │ │ └── ColorApi.kt │ ├── module.kts │ ├── extApi │ │ ├── KVStore.kts │ │ ├── redisApi.kts │ │ ├── remoteEventApi.lib.kt │ │ ├── mongoApi.kts │ │ ├── remoteEventApi.kts │ │ └── rpcService.kts │ ├── commands │ │ ├── varsCmd.kts │ │ ├── helpful.kts │ │ ├── hotReload.kts │ │ └── permissionCmd.kts │ ├── DBConnector.kts │ └── variables.kts ├── bootStrap │ ├── default.kts │ └── generate.kts ├── mapScript │ ├── lib │ │ ├── ContentExt.kt │ │ ├── TagSupport.kt │ │ └── util.kt │ ├── tags │ │ ├── limitAir.kts │ │ ├── mapRule.kts │ │ ├── towerDefend.ai.kt │ │ ├── autoExchange.kts │ │ └── towerDefend.kts │ ├── shared │ │ ├── hexed.kts │ │ ├── posMark.kts │ │ └── hexed.HexedGenerator.kt │ ├── module.kts │ ├── 13545.kts │ └── 1002.kts ├── kcp │ └── serialization.kts └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitprefix ├── settings.gradle.kts ├── .github ├── actions │ ├── package.json │ ├── .gitignore │ ├── tsconfig.json │ └── changelog.ts └── workflows │ ├── buildPlugin.yml │ ├── release.yml │ └── checkScripts.yml ├── .gitignore ├── README.md └── gradlew.bat /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.jvm.target.validation.mode = IGNORE -------------------------------------------------------------------------------- /loader/mindustry/res/META-INF/kotlin/script/templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/coreMindustry/util/spawnAround.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry.util 2 | -------------------------------------------------------------------------------- /scripts/coreMindustry/util/trackBuilding.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry.util -------------------------------------------------------------------------------- /scripts/metadata/kcp.metadata: -------------------------------------------------------------------------------- 1 | ID kcp/serialization 2 | FQ_NAME coreLibrary.kcp.Serialization 3 | 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/way-zer/ScriptAgent4MindustryExt/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /scripts/wayzer/map/mapSnap.block_colors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/way-zer/ScriptAgent4MindustryExt/HEAD/scripts/wayzer/map/mapSnap.block_colors.png -------------------------------------------------------------------------------- /scripts/javalin/simple.kts: -------------------------------------------------------------------------------- 1 | package javalin 2 | 3 | webRoutes { 4 | get("/about") { ctx -> 5 | ctx.result("Powered by Javalin and ScriptAgent") 6 | } 7 | } -------------------------------------------------------------------------------- /.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: 其他非代码更新 -------------------------------------------------------------------------------- /scripts/wayzer/cmds/clearUnit.kts: -------------------------------------------------------------------------------- 1 | package wayzer.cmds 2 | 3 | command("clearUnit", "清除所有单位") { 4 | permission = dotId 5 | body { 6 | broadcast("[green]管理员使用了灭霸(清除所有单位)".with()) 7 | Groups.unit.toList().forEach { it.kill() } 8 | } 9 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include("scripts") 2 | 3 | file("scripts").listFiles()?.forEach { 4 | if (it.isDirectory && it.name.startsWith("@")) { 5 | include("scripts:${it.name.substring(1)}") 6 | project(":scripts:${it.name.substring(1)}").projectDir = it 7 | } 8 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /scripts/metadata/javalin.metadata: -------------------------------------------------------------------------------- 1 | ID javalin 2 | FQ_NAME javalin.Module 3 | +DEPENDS coreLibrary 4 | +IMPORT DefaultImport javalin.lib.* 5 | +IMPORT MavenDepends io.javalin:javalin:6.7.0 6 | +IMPORT MavenDepends org.slf4j:slf4j-simple:2.0.16 7 | 8 | ID javalin/simple 9 | FQ_NAME javalin.Simple 10 | +DEPENDS javalin module 11 | 12 | -------------------------------------------------------------------------------- /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/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 | val groups = buildList { 8 | add(uuid()) 9 | if (admin) add("@admin") 10 | } 11 | return PermissionApi.handleThoughEvent(this, permission, groups).has 12 | } -------------------------------------------------------------------------------- /scripts/javalin/lib/ScriptExt.kt: -------------------------------------------------------------------------------- 1 | package javalin.lib 2 | 3 | import cf.wayzer.scriptAgent.define.Script 4 | import cf.wayzer.scriptAgent.define.ScriptDsl 5 | import cf.wayzer.scriptAgent.util.DSLBuilder.Companion.callbackKey 6 | import io.javalin.Javalin 7 | import io.javalin.config.JavalinConfig 8 | 9 | @ScriptDsl 10 | val Script.configJavalin by callbackKey<(JavalinConfig) -> Unit>() 11 | @ScriptDsl 12 | val Script.webRoutes by callbackKey Unit>() -------------------------------------------------------------------------------- /scripts/wayzer/cmds/jsCmd.kts: -------------------------------------------------------------------------------- 1 | package wayzer.cmds 2 | 3 | import mindustry.Vars 4 | 5 | command("js", "UNSAFE: 在服务器运行js") { 6 | permission = dotId 7 | body { 8 | val js = arg.joinToString(" ") 9 | reply("[red]JS> []{cmd}".with("cmd" to js)) 10 | Vars.player = player 11 | try { 12 | reply(mods.scripts.runConsole(js).asPlaceHoldString()) 13 | } finally { 14 | Vars.player = null 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /scripts/wayzer/ext/tpsLimit.kts: -------------------------------------------------------------------------------- 1 | package wayzer.ext 2 | 3 | import arc.func.Floatp 4 | import arc.util.Reflect 5 | import arc.util.Time 6 | import mindustry.core.Version 7 | 8 | val provider: Floatp = Reflect.get(Time::class.java, "deltaimpl") 9 | 10 | onEnable { 11 | if (Version.build > 146) { 12 | logger.warning("Mindustry新版本已增加TPS限制,可以删除该脚本") 13 | } 14 | Time.setDeltaProvider { 15 | provider.get().coerceAtMost(6f)//10TPS at least 16 | } 17 | } 18 | 19 | onDisable { 20 | Time.setDeltaProvider(provider) 21 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /scripts/wayzer/reGrief/limitFire.kts: -------------------------------------------------------------------------------- 1 | package wayzer.reGrief 2 | 3 | val limit by config.key(500, "火焰格数限制") 4 | 5 | val fireCount get() = Groups.fire.size() 6 | var done = false 7 | listen { done = false } 8 | listen(EventType.Trigger.update) { 9 | if (state.rules.fire && fireCount > limit) { 10 | done = true 11 | state.rules.fire = false 12 | broadcast("[yellow]火焰过多造成服务器卡顿,自动关闭火焰".with()) 13 | } 14 | } 15 | 16 | listen { 17 | if (done) { 18 | it.player.sendMessage("[yellow]火焰过多造成服务器卡顿,自动关闭火焰".with()) 19 | } 20 | } -------------------------------------------------------------------------------- /scripts/wayzer/ext/alert.kts: -------------------------------------------------------------------------------- 1 | package wayzer.ext 2 | 3 | import java.time.Duration 4 | 5 | val type by config.key(MsgType.InfoMessage, "发送方式") 6 | val time by config.key(Duration.ofMinutes(10)!!,"公告间隔") 7 | val list by config.key(emptyList(),"公告列表,支持颜色和变量") 8 | 9 | var i = 0 10 | fun broadcast(){ 11 | if(list.isEmpty())return 12 | i %= list.size 13 | broadcast(list[i].with(),type,15f) 14 | i++ 15 | } 16 | 17 | onEnable{ 18 | launch(Dispatchers.game) { 19 | while (true) { 20 | delay(time.toMillis()) 21 | broadcast() 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /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"] -------------------------------------------------------------------------------- /scripts/wayzer/cmds/serverStatus.kts: -------------------------------------------------------------------------------- 1 | package wayzer.cmds 2 | 3 | command("status", "获取服务器信息") { 4 | aliases = listOf("服务器状态") 5 | body { 6 | reply( 7 | """ 8 | |[green]服务器状态[] 9 | | [green]地图: [ [yellow]{map.id} [green]][yellow]{map.name}[green] 模式: [yellow]{map.mode} [green]波数[yellow]{state.wave} 10 | | [green]{tps} TPS, {heapUse} MB used, UPTIME {state.uptime}[] 11 | | [green]总单位数: {state.allUnit} 玩家数: {state.playerSize} 12 | | [yellow]被禁封总数: {state.allBan} 13 | """.trimMargin().with() 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /.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/@*/ 26 | /loader/mindustry/res/META-INF/kotlin/script/templates/* -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/wayzer/map/autoHost.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/maps", "地图管理") 2 | 3 | package wayzer.map 4 | 5 | import wayzer.MapManager 6 | import java.time.Duration 7 | 8 | name = "自动Host" 9 | 10 | val autoHostTime by config.key(Duration.ofSeconds(5)!!, "自动Host的延迟时间,太短可能服务器未准备就绪") 11 | 12 | onEnable { 13 | if (net.server()) return@onEnable 14 | logger.info("Auto Host after ${autoHostTime.seconds} seconds") 15 | launch { 16 | delay(autoHostTime.toMillis()) 17 | if (net.server()) {//Already host 18 | logger.info("[AutoHost]Already host, pass!") 19 | return@launch 20 | } 21 | MapManager.loadMap() 22 | } 23 | } -------------------------------------------------------------------------------- /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 | import cf.wayzer.scriptAgent.util.DSLBuilder 8 | 9 | @ScriptDsl 10 | fun Script.modeIntroduce(mode: String, introduce: String) { 11 | onEnable { 12 | depends("wayzer/map/mapInfo")?.import<(String, String) -> Unit>("addModeIntroduce") 13 | ?.invoke(mode, introduce) 14 | } 15 | } 16 | 17 | @ScriptDsl 18 | var Script.mapScriptController by DSLBuilder.dataKey() 19 | 20 | @ScriptDsl 21 | var Script.mapPatches by DSLBuilder.dataKey>() -------------------------------------------------------------------------------- /scripts/wayzer/user/ext/skills.kts: -------------------------------------------------------------------------------- 1 | package wayzer.user.ext 2 | 3 | 4 | listen { 5 | SkillCommands.allCooldown.forEach { it.reset() } 6 | } 7 | command("skill", "技能菜单") { 8 | aliases = listOf("技能") 9 | attr(SkillPrecheck) 10 | body(SkillCommands) 11 | } 12 | 13 | command("mono", "技能: 召唤采矿机,一局限一次,PVP禁用".with(), commands = SkillCommands) { 14 | aliases = listOf("矿机") 15 | attr(SkillPrecheck) 16 | attr(SkillNoPvp) 17 | attr(SkillCooldown()) 18 | requirePermission("wayzer.user.skills.mono") 19 | skillBody { 20 | UnitTypes.mono.create(player.team()).also { 21 | it.set(player) 22 | }.add() 23 | broadcastSkill("采矿机?") 24 | } 25 | } -------------------------------------------------------------------------------- /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 | // 本模块实现一些平台无关的库 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/wayzer/lib/ConnectAsyncEvent.kt: -------------------------------------------------------------------------------- 1 | package wayzer.lib 2 | 3 | import cf.wayzer.scriptAgent.Event 4 | import mindustry.net.NetConnection 5 | import mindustry.net.Packets.ConnectPacket 6 | 7 | /** 8 | * Call when before [ConnectPacket] 9 | */ 10 | class ConnectAsyncEvent( 11 | val con: NetConnection, 12 | val packet: ConnectPacket, 13 | ) : Event, Event.Cancellable { 14 | var reason: String? = null 15 | private set 16 | override var cancelled: Boolean 17 | get() = reason != null || con.kicked 18 | set(@Suppress("UNUSED_PARAMETER") value) { 19 | error("Can't cancel,please use kick") 20 | } 21 | 22 | fun reject(reason: String) { 23 | this.reason = reason 24 | } 25 | 26 | companion object : Event.Handler() 27 | } -------------------------------------------------------------------------------- /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/mapScript/tags/limitAir.kts: -------------------------------------------------------------------------------- 1 | package mapScript.tags 2 | 3 | import coreLibrary.lib.util.loop 4 | 5 | registerMapTag("@limitAir") 6 | 7 | onEnable { 8 | loop(Dispatchers.game) { 9 | delay(3_000) 10 | Groups.unit.forEach { 11 | if (it.type().flying && it.closestEnemyCore()?.within(it, state.rules.enemyCoreBuildRadius) == true) { 12 | it.player?.sendMessage("[red]该地图限制空军,禁止进入敌方领空".with()) 13 | it.kill() 14 | } 15 | } 16 | } 17 | } 18 | 19 | listen { 20 | if (it.tile.block() == Blocks.airFactory && !it.breaking) { 21 | Call.label("[yellow]本地图限制空军,禁止进入敌方领空", 60f, it.tile.getX(), it.tile.getY()) 22 | } 23 | } 24 | 25 | listen { 26 | it.player.sendMessage("[yellow]本地图限制空军,禁止进入敌方领空") 27 | } -------------------------------------------------------------------------------- /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/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/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/mapScript/lib/TagSupport.kt: -------------------------------------------------------------------------------- 1 | package mapScript.lib 2 | 3 | import cf.wayzer.scriptAgent.define.Script 4 | import mindustry.Vars 5 | import mindustry.game.Rules 6 | import kotlin.properties.ReadOnlyProperty 7 | 8 | /**用于注册Tag类的mapScript,通常存放位置为`mapScript/tag/xxx` */ 9 | object TagSupport { 10 | // tag -> scriptId 11 | val knownTags = mutableMapOf() 12 | 13 | fun findTags(rules: Rules): Map { 14 | val mapTags = rules.tags.keys().toSet() 15 | return knownTags.filterKeys { it in mapTags } 16 | } 17 | } 18 | 19 | fun Script.registerMapTag(name: String) { 20 | TagSupport.knownTags[name] = id 21 | onUnload { TagSupport.knownTags.remove(name) } 22 | } 23 | 24 | fun Script.mapTag(name: String): ReadOnlyProperty { 25 | registerMapTag(name) 26 | return ReadOnlyProperty { _, _ -> 27 | Vars.state.rules.tags.get(name).orEmpty() 28 | } 29 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /scripts/wayzer/reGrief/autoChangeMap.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/maps", "换图") 2 | 3 | package wayzer.reGrief 4 | 5 | import wayzer.MapManager 6 | import kotlin.time.Duration.Companion.seconds 7 | 8 | //预防一些卡服图,玩家无法复活发起投票 9 | 10 | var newMap = false 11 | var counter = 0 12 | onEnable { 13 | loop(Dispatchers.game) { 14 | delay(1.seconds) 15 | if (newMap || Groups.player.count { !it.dead() && it.unit().health > 0 } > 0) { 16 | counter = 0 17 | return@loop 18 | } 19 | counter += 1 20 | if (counter > 100) { 21 | broadcast("[red]无人游玩,5秒后自动换图".with()) 22 | delay(5.seconds) 23 | MapManager.loadMap() 24 | newMap = true 25 | } 26 | } 27 | } 28 | 29 | listen { 30 | newMap = true 31 | } 32 | 33 | listen { 34 | //Someone request connect, maybe want to play 35 | newMap = false 36 | } -------------------------------------------------------------------------------- /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/kcp/serialization.kts: -------------------------------------------------------------------------------- 1 | package coreLibrary.kcp 2 | 3 | import cf.wayzer.scriptAgent.define.annotations.ImportData 4 | import cf.wayzer.scriptAgent.events.ScriptCompileEvent 5 | import cf.wayzer.scriptAgent.util.DependencyManager 6 | import cf.wayzer.scriptAgent.util.maven.Dependency 7 | 8 | val library = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0" 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)) return@listenTo 21 | registerImportData(ImportData(ImportData.Type.MavenDepends, library)) 22 | addCompileOptions("-Xplugin=${pluginFile.absolutePath}") 23 | } -------------------------------------------------------------------------------- /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/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/tags/mapRule.kts: -------------------------------------------------------------------------------- 1 | package mapScript.tags 2 | 3 | import coreLibrary.lib.event.RequestPermissionEvent 4 | 5 | val group by mapTag("@mapRule") 6 | fun whiteList(it: String) = 7 | it.startsWith("-wayzer.user.skills.") 8 | || it == "-wayzer.vote.skipwave" 9 | || it == "-wayzer.ext.gather" 10 | 11 | @Savable(false) 12 | var permissions: List = emptyList() 13 | onEnable { 14 | PermissionApi.default.groups.remove(group) 15 | permissions = state.rules.tags.get("@permission")?.split(";").orEmpty() 16 | .filter { whiteList(it) } 17 | PermissionApi.default.registerPermission(group, permissions) 18 | } 19 | 20 | listenTo(Event.Priority.After) { 21 | if (permissions.isEmpty()) return@listenTo 22 | //put @mapRule before all group permission 23 | val index = group.indexOfFirst { it.startsWith("@") } 24 | group = if (index == -1) group + "@mapRule" 25 | else group.toMutableList() 26 | .apply { add(index, "@mapRule") } 27 | } -------------------------------------------------------------------------------- /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/wayzer/map/backCompatibility.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/maps") 2 | 3 | package wayzer.map 4 | 5 | import mindustry.ctype.ContentType 6 | import mindustry.type.UnitType 7 | import wayzer.MapChangeEvent 8 | import java.time.Instant 9 | import java.util.* 10 | 11 | listenTo(Event.Priority.Before) { 12 | val oldBanUnit = rules.tags["@banUnit"].orEmpty() 13 | .split(';') 14 | .filterNot { it.isEmpty() } 15 | .mapNotNull { content.getByName(ContentType.unit, it) } 16 | if (oldBanUnit.isNotEmpty()) rules.bannedUnits.addAll(*oldBanUnit.toTypedArray()) 17 | val time = map.tag("saved")?.toLongOrNull()?.let { Instant.ofEpochMilli(it) } ?: return@listenTo 18 | 19 | if (!rules.tags.containsKey("@banTeam") && 20 | Calendar.getInstance().apply { set(2020, 3 - 1, 3) }.toInstant() < time &&//too old, may no pvp protect 21 | time < Calendar.getInstance().apply { set(2022, 3 - 1, 3) }.toInstant() 22 | ) { 23 | modifyRule { tags.put("@banTeam", "2") } 24 | } 25 | } -------------------------------------------------------------------------------- /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/wayzer/ext/welcomeMsg.kts: -------------------------------------------------------------------------------- 1 | package wayzer.ext 2 | 3 | import cf.wayzer.placehold.PlaceHoldApi.with 4 | import mindustry.net.Administration 5 | 6 | val customWelcome by config.key("customWelcome", false, "是否开启自定义进服信息(中文)") { 7 | if (dataDirectory != null) 8 | Administration.Config.showConnectMessages.set(!it) 9 | } 10 | val type by config.key(MsgType.InfoMessage, "发送方式") 11 | val template by config.key( 12 | """ 13 | Welcome to this Server 14 | [green]欢迎{player.name}[green]来到本服务器[] 15 | """.trimIndent(), "欢迎信息模板" 16 | ) 17 | val welcomeTemplate by config.key( 18 | "[cyan][+] {player.name} [goldenrod]加入了服务器", "玩家加入提示消息模板,仅在customWelcome开启时生效" 19 | ) 20 | 21 | listen { 22 | it.player.sendMessage(template.with(), type) 23 | if (customWelcome) 24 | broadcast(welcomeTemplate.with("player" to it.player)) 25 | } 26 | 27 | listen { 28 | if (customWelcome && it.player.lastText != "[Silent_Leave]") 29 | broadcast("[coral][-]{player.name} [brick]离开了服务器".with("player" to it.player)) 30 | } -------------------------------------------------------------------------------- /scripts/mapScript/shared/hexed.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/map/betterTeam", "队伍分配") 2 | 3 | package mapScript.shared 4 | 5 | import mindustry.world.blocks.storage.CoreBlock.CoreBuild 6 | import mindustry.world.modules.ItemModule 7 | import kotlin.time.Duration.Companion.minutes 8 | 9 | listen { 10 | val core = it.tile.build as? CoreBuild ?: return@listen 11 | core.items = ItemModule() //防止爆炸 12 | launch(Dispatchers.gamePost) { 13 | HexData.pos2hex[core.pos()]?.occupy(core.lastDamage) 14 | } 15 | } 16 | listen { 17 | val core = it.tile.build as? CoreBuild ?: return@listen 18 | launch(Dispatchers.gamePost) { 19 | HexData.pos2hex[core.pos()]?.occupy(core.team) 20 | } 21 | } 22 | 23 | listenTo { 24 | team = HexData.assignTeam(player, group) 25 | } 26 | 27 | onEnable { 28 | launch(Dispatchers.game) { 29 | delay(1.minutes) 30 | state.rules.canGameOver = true 31 | } 32 | } 33 | 34 | onDisable { 35 | HexData.reset() 36 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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() -------------------------------------------------------------------------------- /scripts/wayzer/user/ext/chatPing.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/user/shortID") 2 | 3 | package wayzer.user.ext 4 | 5 | import arc.util.Strings 6 | import mindustry.net.Administration 7 | 8 | val regex = Regex("@([a-zA-Z0-9+/]{3})") 9 | 10 | val filter = Administration.ChatFilter { p, msg -> 11 | val players = mutableListOf() 12 | msg.replace(regex) { match -> 13 | val id = match.groupValues[1] 14 | //TODO no runBlocking 15 | val name = if (id == "all" && runBlocking { p.hasPermission("$dotId.pingAll") }) { 16 | players.addAll(Groups.player) 17 | "全体成员" 18 | } else { 19 | val player = PlayerData.findByShortId(id)?.player ?: return@replace match.value 20 | players.add(player) 21 | Strings.stripColors(player.name) 22 | } 23 | " [gold]@$name[] " 24 | }.also { newMsg -> 25 | players.forEach { 26 | Call.announce(it.con, "[red]有人在聊天区@你,请注意查看:[]\n$newMsg") 27 | } 28 | } 29 | } 30 | onEnable { netServer.admins.chatFilters.add(filter) } 31 | onDisable { netServer.admins.chatFilters.remove(filter) } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/wayzer/reGrief/bugFixer.kts: -------------------------------------------------------------------------------- 1 | package wayzer.reGrief 2 | 3 | import arc.struct.ObjectMap 4 | import arc.util.pooling.Pools 5 | import mindustry.core.Version 6 | import mindustry.game.EventType.ResetEvent 7 | import mindustry.net.Administration 8 | import mindustry.world.blocks.storage.StorageBlock 9 | import mindustry.world.blocks.storage.StorageBlock.StorageBuild 10 | 11 | //update on 7/5 12 | // 修复 Pool内存泄漏bug mod图卡服(无法进入) 13 | // 历史修复 塑钢带bug(断开连接) 14 | 15 | //内存泄漏 16 | listen { 17 | Pools::class.java.getDeclaredField("typePools").apply { 18 | isAccessible = true 19 | (get(null) as ObjectMap<*, *>).clear() 20 | } 21 | } 22 | 23 | //未确认加入bug 24 | registerActionFilter { it.player.con.hasConnected } 25 | 26 | if (Version.build == 146) { 27 | //e仓库刷物品bug 28 | registerActionFilter { 29 | if (it.type != Administration.ActionType.placeBlock) return@registerActionFilter true 30 | val block = it.block as? StorageBlock ?: return@registerActionFilter true 31 | val old = it.tile.block() as? StorageBlock ?: return@registerActionFilter true 32 | (!block.coreMerge && old.coreMerge && (it.tile.build as StorageBuild).linkedCore != null).not() 33 | } 34 | } -------------------------------------------------------------------------------- /scripts/wayzer/user/lang.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("coreLibrary/lang", "多语言支持-核心") 2 | @file:Depends("coreLibrary/extApi/KVStore", "储存语言设置") 3 | 4 | package wayzer.user 5 | 6 | import cf.wayzer.placehold.DynamicVar 7 | import org.h2.mvstore.type.StringDataType 8 | 9 | name = "玩家语言设置" 10 | 11 | val settings = contextScript().open("langSettings", StringDataType.INSTANCE) 12 | 13 | var PlayerData.lang: String 14 | get() = settings[id] ?: player?.locale ?: "zh" 15 | set(v) { 16 | if (lang == v) return 17 | if (v == player?.locale) { 18 | settings.remove(id) 19 | } else { 20 | settings[id] = v 21 | } 22 | } 23 | 24 | registerVarForType() 25 | .registerChild("lang", "多语言支持", { 26 | kotlin.runCatching { PlayerData[it].lang }.getOrNull() 27 | }) 28 | 29 | command("lang", "设置语言") { 30 | permission = "wayzer.lang.set" 31 | type = CommandType.Client 32 | body { 33 | if (arg.isEmpty()) returnReply("[yellow]你的当前语言是: {receiver.lang}".with()) 34 | val data = PlayerData[player!!] 35 | data.lang = arg[0] 36 | reply("[green]你的语言已设为 {v}".with("v" to data.lang)) 37 | } 38 | } 39 | 40 | PermissionApi.registerDefault("wayzer.lang.set") -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /scripts/wayzer/pvp/pvpChat.kts: -------------------------------------------------------------------------------- 1 | package wayzer.pvp 2 | 3 | import arc.util.Log 4 | import mindustry.game.Team 5 | import mindustry.net.Administration 6 | 7 | 8 | fun message(from: Player, msg: String, teamMsg: Boolean) { 9 | for (p in Groups.player) { 10 | val prefix = when { 11 | !teamMsg && from.team().id == 255 -> "[cyan][公屏][观战]" 12 | !teamMsg -> "[cyan][公屏][[${from.team().coloredName()}]" 13 | from.team() == p.team() && from.team().id == 255 -> "[violet][观战]" 14 | from.team() == p.team() -> "[violet][队内]" 15 | p.team() == Team.all[255] -> "[violet][[${from.team().coloredName()}队内]" 16 | else -> continue 17 | } 18 | p.sendMessage(prefix + netServer.chatFormatter.format(from, msg), from, msg) 19 | } 20 | } 21 | 22 | val filter = Administration.ChatFilter { p, t -> 23 | if (!state.rules.pvp) return@ChatFilter t 24 | message(p, t, true) 25 | Log.info("&fi@: @", "&lc" + p.name, "&lw$t") 26 | null 27 | } 28 | 29 | onEnable { 30 | netServer.admins.chatFilters.add(filter) 31 | onDisable { 32 | netServer.admins.chatFilters.remove(filter) 33 | } 34 | } 35 | 36 | command("t", "PVP模式全体聊天") { 37 | type = CommandType.Client 38 | body { 39 | val msg = arg.joinToString(" ") 40 | message(player!!, msg, false) 41 | } 42 | } -------------------------------------------------------------------------------- /scripts/wayzer/ext/goServer.kts: -------------------------------------------------------------------------------- 1 | package wayzer.ext 2 | //WayZer 版权所有(请勿删除版权注解) 3 | 4 | name = "跨服传送" 5 | 6 | val servers by config.key(mapOf(), "服务器传送列表", "格式: {名字: \"介绍;地址\"} (;作为分割符)") 7 | 8 | data class Info(val name: String, val desc: String, val address: String, val port: Int) 9 | 10 | val infos: Map 11 | get() = servers.mapValues { (k, v) -> 12 | val sp1 = v.split(";") 13 | assert(sp1.size == 2) { "格式错误: $v" } 14 | val sp2 = sp1[1].split(":") 15 | val port = sp2.getOrNull(1)?.toIntOrNull() ?: port 16 | Info(k, sp1[0], sp2[0], port) 17 | } 18 | 19 | 20 | command("go", "传送到其他服务器") { 21 | usage = "[名字,为空列出]" 22 | type = CommandType.Client 23 | aliases = listOf("前往") 24 | body { 25 | val info = arg.firstOrNull() 26 | ?.let { infos[it] ?: returnReply("[red]错误的服务器名字".with()) } 27 | ?: let { 28 | val list = infos.values.map { "[gold]{name}:[tan]{desc}".with("name" to it.name, "desc" to it.desc) } 29 | returnReply("[violet]可用服务器: \n{list|joinLines}".with("list" to list)) 30 | } 31 | Call.connect(player!!.con, info.address, info.port) 32 | broadcast( 33 | "[cyan][-][salmon]{player.name}[salmon]传送到了{name}服务器(/go {name})".with( 34 | "player" to player!!, 35 | "name" to info.name 36 | ) 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /scripts/wayzer/pvp/autoGameover.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/map/betterTeam", "获取可用队伍") 2 | 3 | package wayzer.pvp 4 | 5 | import mindustry.game.Team 6 | 7 | val teams = contextScript() 8 | 9 | val score = IntArray(Team.all.size) 10 | fun doCheck() { 11 | val existed = Groups.player.map { it.team() }.toSet() 12 | teams.allTeam.forEach { team -> 13 | val old = score[team.id] 14 | score[team.id] = when { 15 | team in existed -> 0 16 | old >= 60 -> 1.also { 17 | broadcast("[red][系统][yellow]队伍{team0}没有玩家,自动投降".with("team0" to team)) 18 | team.cores().toArray().forEach { 19 | it.lastDamage = Team.derelict 20 | it.kill() 21 | } 22 | } 23 | 24 | else -> (old + 1).also { 25 | if (old == 0) 26 | broadcast("[red][系统][yellow]队伍{team0}没有玩家,60s后自动投降".with("team0" to team)) 27 | } 28 | } 29 | } 30 | } 31 | 32 | fun check() { 33 | if (!state.rules.pvp) return 34 | launch(Dispatchers.game) { 35 | delay(30000) 36 | while (true) { 37 | doCheck() 38 | delay(1000) 39 | } 40 | } 41 | } 42 | 43 | listen { check() } 44 | onEnable { check() } 45 | 46 | listen { 47 | coroutineContext[Job]?.cancelChildren() 48 | score.fill(0) 49 | } -------------------------------------------------------------------------------- /scripts/wayzer/cmds/helpfulCmd.kts: -------------------------------------------------------------------------------- 1 | package wayzer.cmds 2 | 3 | import arc.graphics.Color 4 | import arc.graphics.Colors 5 | import mindustry.entities.Effect 6 | 7 | command("showColor", "显示所有颜色".with()) { 8 | body { 9 | reply(Colors.getColors().joinToString("[],") { "[#${it.value}]${it.key}" }.with()) 10 | } 11 | } 12 | 13 | command("dosBanClear", "清理dosBan".with()) { 14 | permission = dotId 15 | body { 16 | reply("[yellow]被ban列表: {list}".with("list" to netServer.admins.dosBlacklist)) 17 | netServer.admins.dosBlacklist.clear() 18 | reply("[green]已清空DosBan".with()) 19 | } 20 | } 21 | 22 | val map by lazy { 23 | Fx::class.java.fields.filter { it.type == Effect::class.java } 24 | .associate { it.name to (it.get(null) as Effect) } 25 | } 26 | 27 | command("showEffect", "显示粒子效果".with()) { 28 | usage = "[-a 全体可见] [类型=列出] [半径=10] [颜色=red]" 29 | body { 30 | val all = checkArg("-a") 31 | val type = arg.getOrNull(0)?.let { map[it] } 32 | ?: returnReply("[red]请输入类型: {list}".with("list" to map.keys)) 33 | val arg1 = arg.getOrNull(1)?.toFloatOrNull() ?: 10f 34 | val arg2 = arg.getOrNull(2)?.let { Color.valueOf(it) } ?: Color.red 35 | 36 | if (all && !hasPermission("wayzer.ext.showEffect")) replyNoPermission() 37 | if (all) Call.effect(type, player!!.x, player!!.y, arg1, arg2) 38 | else Call.effect(player!!.con, type, player!!.x, player!!.y, arg1, arg2) 39 | } 40 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /scripts/wayzer/ext/profiler.kts: -------------------------------------------------------------------------------- 1 | @file:Import("tools.profiler:async-profiler:4.1", mavenDepends = true) 2 | @file:Import("https://repo1.maven.org/maven2/", mavenRepository = true) 3 | 4 | package wayzer.ext 5 | 6 | import one.profiler.AsyncProfiler 7 | import java.time.Duration 8 | import java.time.Instant 9 | import kotlin.time.toKotlinDuration 10 | 11 | val command by config.key("start,jfr,event=cpu,interval=500us,file=FILE", "采样器启动命令, FILE会被替换为实际文件路径") 12 | var running: DisposableHandle? = null 13 | 14 | fun start(cmd: String) { 15 | val profiler = AsyncProfiler.getInstance() 16 | val file = Config.cacheDir.resolve("${Instant.now()}.jfr") 17 | val start = Instant.now() 18 | profiler.execute(cmd.replace("FILE", file.absolutePath)) 19 | logger.info("Profiler started, output file: ${file.absolutePath}") 20 | running = DisposableHandle { 21 | val elapsed = Duration.between(start, Instant.now()).toKotlinDuration() 22 | profiler.execute("stop") 23 | logger.info("Profiler stopped, elapsed time: $elapsed") 24 | logger.info("Output file: ${file.absolutePath}") 25 | 26 | } 27 | } 28 | 29 | onDisable { 30 | running?.dispose() 31 | running = null 32 | } 33 | 34 | command("profiler", "性能采样") { 35 | requirePermission(dotId) 36 | usage = "[command]" 37 | body { 38 | running?.let { 39 | running = null 40 | it.dispose() 41 | return@body 42 | } 43 | start(arg.firstOrNull() ?: command) 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /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/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/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/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/metadata/mapScript.metadata: -------------------------------------------------------------------------------- 1 | ID mapScript 2 | FQ_NAME mapScript.Module 3 | +DEPENDS coreMindustry 4 | +DEPENDS wayzer/maps 5 | +DEPENDS wayzer/maps 获取地图信息 6 | +IMPORT DefaultImport mapScript.lib.* 7 | +SOFT_DEPENDS wayzer/map/mapInfo 显示地图信息 8 | 9 | ID mapScript/1001 10 | FQ_NAME mapScript._1001 11 | +DEPENDS coreMindustry/utilMapRule 12 | +DEPENDS coreMindustry/utilMapRule 参数平衡 13 | +DEPENDS mapScript module 14 | +DEPENDS mapScript/shared/hexed 15 | 16 | ID mapScript/1002 17 | FQ_NAME mapScript._1002 18 | +DEPENDS mapScript module 19 | +DEPENDS mapScript/shared/hexed 20 | +SOFT_DEPENDS mapScript/tags/autoExchange 等价交换 21 | 22 | ID mapScript/1003 23 | FQ_NAME mapScript._1003 24 | +DEPENDS coreMindustry/utilMapRule 25 | +DEPENDS coreMindustry/utilMapRule 参数平衡 26 | +DEPENDS mapScript module 27 | +DEPENDS mapScript/shared/hexed 28 | 29 | ID mapScript/1009 30 | FQ_NAME mapScript._1009 31 | +DEPENDS coreMindustry/utilMapRule 32 | +DEPENDS coreMindustry/utilMapRule 参数平衡 33 | +DEPENDS mapScript module 34 | +DEPENDS mapScript/shared/hexed 35 | 36 | ID mapScript/shared/hexed 37 | FQ_NAME mapScript.shared.Hexed 38 | +DEPENDS mapScript module 39 | +DEPENDS wayzer/map/betterTeam 40 | +DEPENDS wayzer/map/betterTeam 队伍分配 41 | 42 | ID mapScript/shared/posMark 43 | FQ_NAME mapScript.shared.PosMark 44 | +DEPENDS mapScript module 45 | 46 | ID mapScript/tags/autoExchange 47 | FQ_NAME mapScript.tags.AutoExchange 48 | +DEPENDS coreMindustry/utilMapRule 49 | +DEPENDS coreMindustry/utilMapRule 修改核心容量 50 | +DEPENDS mapScript module 51 | 52 | ID mapScript/tags/voidProduce 53 | FQ_NAME mapScript.tags.VoidProduce 54 | +DEPENDS coreMindustry/contentsTweaker 55 | +DEPENDS mapScript module 56 | 57 | -------------------------------------------------------------------------------- /.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 "name=date::$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT 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 | -------------------------------------------------------------------------------- /scripts/mapScript/tags/towerDefend.ai.kt: -------------------------------------------------------------------------------- 1 | package mapScript.tags 2 | 3 | import mindustry.ai.Pathfinder 4 | import mindustry.entities.Units 5 | import mindustry.entities.units.AIController 6 | import mindustry.gen.Teamc 7 | import mindustry.gen.Unitc 8 | import mindustry.world.Tile 9 | import mindustry.world.blocks.environment.Floor 10 | import mindustry.world.blocks.storage.CoreBlock 11 | 12 | class TowerDefendAI(private val floors: Set) : AIController() { 13 | override fun shouldShoot(): Boolean = target != null && !invalid(target) 14 | override fun invalid(target: Teamc?) = when (target) { 15 | is CoreBlock.CoreBuild -> true 16 | is Tile -> target.block() is CoreBlock 17 | is Unitc -> with(target) { 18 | !isFlying && !isPlayer && floorOn() in floors 19 | } 20 | 21 | else -> false 22 | }.not() 23 | 24 | override fun findTarget(x: Float, y: Float, range: Float, air: Boolean, ground: Boolean): Teamc? { 25 | return if (unit.type.flying) unit.closestEnemyCore() 26 | else Units.closestEnemy(unit.team, x, y, range) { !invalid(it) } 27 | } 28 | 29 | override fun updateMovement() { 30 | val core = unit.closestEnemyCore() ?: return 31 | val range = (unit.type.range * 0.8f).coerceAtMost(80f) 32 | if (unit.within(core, range)) { 33 | target = core 34 | if (unit.type.circleTarget) { 35 | this.circleAttack(range) 36 | } 37 | } else { 38 | if (unit.type.flying) 39 | moveTo(core, unit.type.range * 0.8f) 40 | else 41 | pathfind(Pathfinder.fieldCore) 42 | } 43 | faceTarget() 44 | } 45 | } -------------------------------------------------------------------------------- /scripts/wayzer/cmds/spawnMob.kts: -------------------------------------------------------------------------------- 1 | package wayzer.cmds 2 | //WayZer 版权所有(请勿删除版权注解) 3 | import mindustry.ctype.ContentType 4 | import mindustry.game.Team 5 | import mindustry.type.UnitType 6 | 7 | name = "扩展功能: 召唤单位" 8 | 9 | command("spawn", "召唤单位") { 10 | usage = "[类型ID=列出] [队伍ID,默认为sharded] [数量=1] [armor=?]" 11 | permission = id.replace("/", ".") 12 | aliases = listOf("召唤") 13 | body { 14 | val list = content.getBy(ContentType.unit).filterNot { it.internal } 15 | val type = arg.getOrNull(0)?.toIntOrNull()?.let { list.getOrNull(it) } ?: returnReply( 16 | "[red]请输入类型ID: {list}" 17 | .with("list" to list.mapIndexed { i, type -> "[yellow]$i[green]($type)" }.joinToString()) 18 | ) 19 | val team = arg.getOrNull(1)?.let { s -> 20 | s.toIntOrNull()?.let { Team.all.getOrNull(it) } ?: returnReply( 21 | "[red]请输入队伍ID: {list}" 22 | .with("list" to Team.baseTeams.mapIndexed { i, type -> "[yellow]$i[green]($type)" }.joinToString()) 23 | ) 24 | } ?: Team.sharded 25 | val unit = player?.unit() 26 | val num = arg.getOrNull(2)?.toIntOrNull() ?: 1 27 | val armor = arg.getOrNull(3)?.toFloatOrNull() 28 | repeat(num) { 29 | type.create(team).apply { 30 | this.armor = armor ?: this.armor 31 | if (unit != null) set(unit.x, unit.y) 32 | else team.data().core()?.let { 33 | set(it.x, it.y) 34 | } 35 | add() 36 | } 37 | } 38 | reply("[green]成功为 {team} 生成 {num} 只 {type}".with("team" to team, "num" to num, "type" to type.name)) 39 | } 40 | } -------------------------------------------------------------------------------- /.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 | `) -------------------------------------------------------------------------------- /scripts/wayzer/cmds/mapsCmd.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/maps") 2 | @file:Depends("coreMindustry/menu", "maps菜单") 3 | @file:Depends("wayzer/cmds/voteMap", "发起投票换图") 4 | 5 | package wayzer.cmds 6 | 7 | import coreMindustry.MenuV2 8 | import coreMindustry.renderPaged 9 | import wayzer.MapRegistry 10 | 11 | val mapsPrePage by config.key(9, "/maps每页显示数") 12 | val voteMap = contextScript() 13 | 14 | command("maps", "列出服务器地图") { 15 | usage = "[page/filter] [page]" 16 | aliases = listOf("地图") 17 | body { 18 | val page = arg.lastOrNull()?.toIntOrNull() ?: 1 19 | val filter = arg.getOrNull(0).takeUnless { it == page.toString() } 20 | val maps = MapRegistry.searchMaps(filter)/*.sortedBy { it.id }*/ 21 | val template = "[red]{info.id} [green]{info.name}[blue] | {info.mode}" 22 | val player = player ?: returnReply(menu("服务器地图 By WayZer", maps, page, mapsPrePage) { info -> 23 | template.with("info" to info) 24 | }) 25 | MenuV2(player) { 26 | title = "服务器地图($filter)" 27 | msg = "SA4Mindustry By WayZer\n" + 28 | "点击选项可发起投票换图" 29 | val url = "https://www.mindustry.top" 30 | option("点击打开Mindustry资源站,查看更多地图\n$url") { 31 | Call.openURI(player.con, url) 32 | } 33 | renderPaged(maps, page, mapsPrePage) { 34 | option(template.with("info" to it).toPlayer(player)) { 35 | if (!player.hasPermission("wayzer.vote.map")) { 36 | player.sendMessage("[red]你没有投票换图的权限".with()) 37 | return@option 38 | } 39 | voteMap.voteMap(player, it) 40 | } 41 | } 42 | }.send().awaitWithTimeout() 43 | } 44 | } -------------------------------------------------------------------------------- /scripts/wayzer/user/nameExt.kts: -------------------------------------------------------------------------------- 1 | package wayzer.user 2 | 3 | import cf.wayzer.placehold.PlaceHoldApi 4 | import cf.wayzer.placehold.TypeBinder 5 | 6 | @Savable(serializable = false) 7 | val realName = mutableMapOf() 8 | customLoad(::realName) { realName.putAll(it) } 9 | 10 | val TypeBinder<*>.tree: Map by reflectDelegate() 11 | 12 | registerVarForType().apply { 13 | registerChild("prefix", "名字前缀,可通过prefix.xxx变量注册") { p -> 14 | PlaceHoldApi.typeBinder().run { 15 | val keys = tree.keys.filter { it.startsWith("prefix.") }.sorted() 16 | keys.joinToString("") { k -> 17 | resolve(this@registerChild, p, k)?.let { resolveVarForString(it) }.orEmpty() 18 | } 19 | } 20 | } 21 | registerChild("suffix", "名字后缀,可通过suffix.xxx变量注册") { p -> 22 | PlaceHoldApi.typeBinder().run { 23 | val keys = tree.keys.filter { it.startsWith("suffix.") }.sorted() 24 | keys.joinToString("") { k -> 25 | resolve(this@registerChild, p, k)?.let { resolveVarForString(it) }.orEmpty() 26 | } 27 | } 28 | } 29 | } 30 | 31 | 32 | fun Player.updateName() { 33 | name = "[white]{player.prefix}[]{name}[white]{player.suffix}".with( 34 | "player" to this, 35 | "name" to (realName[uuid()] ?: "NotInit") 36 | ).toString() 37 | } 38 | 39 | listen { 40 | val p = it.player 41 | realName[p.uuid()] = p.name 42 | p.updateName() 43 | } 44 | onEnable { 45 | loop(Dispatchers.game) { 46 | Groups.player.forEach { 47 | if (it.uuid() !in realName) 48 | realName[it.uuid()] = it.name 49 | } 50 | delay(5000) 51 | Groups.player.forEach { it.updateName() } 52 | } 53 | } -------------------------------------------------------------------------------- /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/wayzer/cmds/restart.kts: -------------------------------------------------------------------------------- 1 | package wayzer.cmds 2 | 3 | import arc.Events 4 | import mindustry.core.GameState 5 | import mindustry.game.Team 6 | import mindustry.net.Packets 7 | import kotlin.system.exitProcess 8 | 9 | var msg: String? = null 10 | var doRestart: () -> Unit = {} 11 | listen { 12 | val msg = msg ?: return@listen 13 | broadcast("[yellow]服务器即将重启: {msg}".with("msg" to msg), quite = true) 14 | } 15 | listen { 16 | val msg = msg ?: return@listen 17 | launch(Dispatchers.gamePost) { 18 | it.player.sendMessage("[yellow]服务器将在本局游戏后自动重启: {msg}".with("msg" to msg)) 19 | } 20 | } 21 | //Don't using ResetEvent, as Groups.player is cleared 22 | //listen 23 | listen { 24 | if (it.to == GameState.State.menu) 25 | doRestart() 26 | } 27 | 28 | fun scheduleRestart(msg: String, beforeExit: () -> Unit = {}) { 29 | this.msg = msg 30 | broadcast("[yellow]服务器将在本局游戏后自动重启: {msg}".with("msg" to msg)) 31 | doRestart = { 32 | broadcast("[yellow]服务器重启:\n{msg}".with("msg" to msg), MsgType.InfoMessage) 33 | Thread.sleep(1000L) 34 | Groups.player.forEach { 35 | it.kick(Packets.KickReason.serverRestarting) 36 | } 37 | Thread.sleep(100L) 38 | beforeExit() 39 | exitProcess(2) 40 | } 41 | if (state.isMenu) 42 | Core.app.post(doRestart) 43 | } 44 | 45 | command("restart", "计划重启") { 46 | usage = "[--now] " 47 | permission = dotId 48 | body { 49 | val now = checkArg("--now") 50 | val msg = arg.joinToString(" ") 51 | scheduleRestart(msg) 52 | if (now) { 53 | Events.fire(EventType.GameOverEvent(Team.derelict)) 54 | doRestart() 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /scripts/wayzer/vote.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("coreMindustry/menu") 2 | 3 | package wayzer 4 | 5 | import cf.wayzer.placehold.DynamicVar 6 | import cf.wayzer.scriptAgent.events.ScriptDisableEvent 7 | import java.time.Duration 8 | import java.time.Instant 9 | 10 | name = "投票服务" 11 | 12 | listen { 13 | (VoteEvent.active.get() ?: return@listen) 14 | .vote(it.player, VoteEvent.Action.Join) 15 | } 16 | 17 | listen { 18 | VoteEvent.lastAction = System.currentTimeMillis() 19 | (VoteEvent.active.get() ?: return@listen) 20 | .vote(it.player, VoteEvent.Action.Quit) 21 | } 22 | 23 | listen { 24 | val action = when (it.message.lowercase()) { 25 | "赞成", "y", "1" -> VoteEvent.Action.Agree 26 | "反对", "n", "0" -> VoteEvent.Action.Disagree 27 | "中立", "." -> VoteEvent.Action.Ignore 28 | else -> return@listen 29 | } 30 | (VoteEvent.active.get() ?: return@listen it.player.sendMessage("[red]投票已结束")) 31 | .vote(it.player, action) 32 | } 33 | 34 | listen { VoteEvent.coolDowns.clear() } 35 | 36 | registerVar("scoreboard.ext.vote", "投票状态显示", DynamicVar { 37 | VoteEvent.active.get()?.run { 38 | "{cK}投票{cV}{desc}:\n {status} {cV}\uE867{left 秒}".with( 39 | "desc" to voteDesc, 40 | "status" to status(), 41 | "left" to Duration.between(Instant.now(), endTime) 42 | ) 43 | } 44 | }) 45 | 46 | command("vote", "发起投票") { 47 | type = CommandType.Client 48 | aliases = listOf("投票") 49 | attr { 50 | if (VoteEvent.active.get() != null) 51 | returnReply("[red]投票进行中".with()) 52 | } 53 | body(VoteEvent.VoteCommands) 54 | } 55 | listenTo { 56 | VoteEvent.VoteCommands.removeAll(script) 57 | } 58 | PermissionApi.registerDefault("wayzer.vote.*") -------------------------------------------------------------------------------- /scripts/mapScript/shared/posMark.kts: -------------------------------------------------------------------------------- 1 | package mapScript.shared 2 | 3 | import mindustry.game.Team 4 | import mindustry.world.Tile 5 | 6 | //定义一种标记格式 7 | //放置世界信息版,每行可设置一个标记,形如 8 | // @zone w=5 h=5 9 | //地图加载后,将存储在@zone类型信息中 10 | 11 | data class Pos(val type: String, val tile: Tile, val team: Team, val arg: Map) { 12 | fun error(msg: String, duration: Float = 10f) { 13 | Call.labelReliable(msg, duration, tile.worldx(), tile.worldy()) 14 | } 15 | } 16 | 17 | fun parse(tile: Tile): List? { 18 | if (tile.block() != Blocks.worldMessage) return null 19 | val lines = tile.build.config().toString().lines() 20 | if (lines.any { it.firstOrNull() != '@' }) return null 21 | val team = tile.team() 22 | return lines.map { line -> 23 | val sp = line.split(" ").filter { it.isNotBlank() } 24 | Pos(sp[0], tile, team, 25 | sp.drop(1).associate { it.substringBefore('=') to it.substringAfter('=', "") }) 26 | } 27 | } 28 | 29 | //静态地图标记 30 | 31 | val posMap by autoInit { 32 | val all = buildList { 33 | world.tiles.forEach { 34 | val parsed = parse(it) ?: return@forEach 35 | it.remove() 36 | addAll(parsed) 37 | } 38 | } 39 | all.groupBy { it.type } 40 | } 41 | 42 | fun getPoses(type: String) = posMap[type].orEmpty() 43 | 44 | //动态地图标记 45 | listen { event -> 46 | val tile = event.tile 47 | if (tile.block() != Blocks.worldMessage) return@listen 48 | launch(Dispatchers.gamePost) { 49 | val parsed = parse(tile) ?: return@launch 50 | tile.removeNet() 51 | parsed.forEach { 52 | commands[it.type]?.invoke(it) 53 | } 54 | } 55 | } 56 | val commands = mutableMapOf Unit>() 57 | onDisable { commands.clear() } 58 | fun registerCommand(type: String, handler: (pos: Pos) -> Unit) { 59 | commands[type] = handler 60 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | renderPaged(commands, page) { 27 | option(buildString { 28 | append("[lightgray]${prefix}[gold]${it.name}") 29 | if (it.aliases.isNotEmpty()) 30 | append("[gray](${it.aliases.joinToString()})") 31 | appendLine(" [lightgray]${it.usage}") 32 | append("[sky]${it.description.toPlayer(player)}") 33 | if (showAll) { 34 | it.script?.let { append(" | ${it.id}") } 35 | if (it.permission.isNotBlank()) append(" | ${it.permission}") 36 | } 37 | }) { 38 | arg = listOf(it.name) 39 | reply("[yellow][快捷输入指令][] {command}".with("command" to (prefix + it.name))) 40 | cmds.handle() 41 | } 42 | } 43 | }.send().awaitWithTimeout() 44 | CommandInfo.Return() 45 | } 46 | } -------------------------------------------------------------------------------- /scripts/wayzer/cmds/voteMap.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/maps", "地图管理") 2 | @file:Depends("wayzer/vote", "投票实现") 3 | 4 | package wayzer.cmds 5 | 6 | import arc.util.Strings.stripColors 7 | import arc.util.Strings.truncate 8 | import wayzer.* 9 | 10 | fun voteMap(player: Player, map: MapInfo) { 11 | launch(Dispatchers.game) { 12 | val event = VoteEvent( 13 | this, player, 14 | "换图([green]{nextMap.id}[]: [green]{nextMap.name}[yellow]|[green]{nextMap.mode}[])".with("nextMap" to map), 15 | extDesc = "[white]地图作者: [lightgrey]${stripColors(map.author)}[][]\n" + 16 | "[white]地图简介: [lightgrey]${truncate(stripColors(map.description), 100, "...")}[][]", 17 | supportSingle = true 18 | ) 19 | if (!event.awaitResult()) return@launch 20 | broadcast("[yellow]异步加载地图中,请耐心等待".with()) 21 | if (MapManager.loadMapSync(map)) 22 | broadcast("[green]换图成功,当前地图[yellow]{map.name}[green](id: {map.id})".with()) 23 | } 24 | } 25 | 26 | fun VoteService.register() { 27 | addSubVote("换图投票", "<地图ID>", "map", "换图") { 28 | if (arg.isEmpty()) 29 | returnReply("[red]请输入地图序号".with()) 30 | val map = arg[0].toIntOrNull()?.let { MapRegistry.findById(it, reply) } 31 | ?: returnReply("[red]地图序号错误,可以通过/maps查询".with()) 32 | voteMap(player!!, map) 33 | } 34 | addSubVote("回滚到某个存档(使用/slots查看)", "<存档ID>", "rollback", "load", "回档") { 35 | val save = arg.firstOrNull()?.toIntOrNull() 36 | ?: returnReply("[red]请输入正确的存档编号".with()) 37 | val map = MapManager.getSlot(save) 38 | ?: returnReply("[red]存档不存在或存档损坏".with()) 39 | start(player!!, "回档({save})".with("save" to save), supportSingle = true) { 40 | MapManager.loadSave(map) 41 | broadcast("[green]回档成功".with(), quite = true) 42 | } 43 | } 44 | } 45 | 46 | onEnable { 47 | VoteService.register() 48 | } -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/coreMindustry/scoreboard.kts: -------------------------------------------------------------------------------- 1 | package coreMindustry 2 | //WayZer 版权所有(请勿删除版权注解) 3 | import arc.util.Align 4 | import java.time.Duration 5 | 6 | val defaultTemplate = """ 7 | {magic}[sky]欢迎 {cV}{player.name} [sky] 8 | {cK}当前地图: {cV}[{map.id}]{map.name} 9 | {cK}游戏时间: {cV}{state.gameTime 分钟} 10 | {listPrefix scoreboard.ext|joinLines} 11 | {listPrefix scoreBroad.ext|joinLines} 12 | {cA}输入 /broad 可以开关该显示 13 | """.trimIndent() 14 | 15 | val template by config.key( 16 | defaultTemplate, "积分榜模板", 17 | "其中{cK}{cV}{cA}为颜色变量,{listPrefix xx}行供其他插件动态扩展。", 18 | "开头{magic}会被替换特殊颜色,供MDTX客户端识别", 19 | ) 20 | //Color变量 cK - KEY, cV - VALUE, cA - ACTION 21 | val msg 22 | get() = template.with( 23 | "magic" to "[#FEBBEF][]",//供MDTX识别 24 | "cK" to "[gray]", "cV" to "[lightgray]", "cA" to "[slate]", 25 | ) 26 | 27 | val disabled = mutableSetOf() 28 | 29 | command("board", "开关积分板显示") { 30 | aliases = listOf("broad", "scoreboard") 31 | attr(ClientOnly) 32 | body { 33 | if (!disabled.remove(player!!.uuid())) 34 | disabled.add(player!!.uuid()) 35 | reply("[green]切换成功".with()) 36 | } 37 | } 38 | 39 | //避免找不到 scoreboard.ext.* 变量 40 | registerVar("scoreboard.ext.null", "空占位", null) 41 | registerVar("scoreBroad.ext.null", "空占位(兼容旧插件)", null) 42 | 43 | registerVar("scoreboard.ext.patches-count", "Patcher状态显示", DynamicVar { 44 | if (state.patcher.patches.isEmpty) return@DynamicVar null 45 | "{cK}属性修改已加载: {cV}{count}".with("count" to state.patcher.patches.size) 46 | }) 47 | 48 | onEnable { 49 | loop(Dispatchers.game) { 50 | delay(Duration.ofSeconds(2).toMillis()) 51 | Groups.player.forEach { 52 | if (disabled.contains(it.uuid())) return@forEach 53 | val mobile = it.con?.mobile == true 54 | Call.infoPopup( 55 | it.con, msg.with().toPlayer(it), 2.013f, 56 | Align.topLeft, if (mobile) 210 else 155, 0, 0, 0 57 | ) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /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/mapScript/lib/util.kt: -------------------------------------------------------------------------------- 1 | package mapScript.lib 2 | 3 | import cf.wayzer.placehold.VarString 4 | import cf.wayzer.scriptAgent.define.Script 5 | import cf.wayzer.scriptAgent.define.ScriptInfo 6 | import coreLibrary.lib.with 7 | import coreMindustry.lib.broadcast 8 | import coreMindustry.lib.game 9 | import coreMindustry.lib.gamePost 10 | import kotlinx.coroutines.CoroutineScope 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.delay 13 | import kotlinx.coroutines.launch 14 | import mindustry.Vars 15 | import mindustry.core.GameState 16 | import kotlin.coroutines.CoroutineContext 17 | import kotlin.coroutines.EmptyCoroutineContext 18 | import kotlin.time.Duration 19 | import kotlin.time.Duration.Companion.seconds 20 | 21 | /** 为onEnable中使用,确保玩家能够收到信息 */ 22 | fun Script.delayBroadcast(msg: VarString) = launch(Dispatchers.gamePost) { 23 | broadcast(msg) 24 | } 25 | 26 | @Suppress("UnusedReceiverParameter") 27 | val GameState.gameTime get() = (Vars.state.tick / 60).seconds 28 | 29 | /** 延时,直到特定游戏时间(支持暂停) @see [schedule] */ 30 | suspend fun delayUntil(gameTime: Duration) { 31 | while (true) { 32 | val left = gameTime - Vars.state.gameTime 33 | if (left.isNegative()) break 34 | delay(left) 35 | } 36 | } 37 | 38 | /** 计划在特定游戏时间执行 @see [delayUntil] */ 39 | fun CoroutineScope.schedule( 40 | time: Duration, 41 | context: CoroutineContext = EmptyCoroutineContext, 42 | body: suspend CoroutineScope.() -> Unit 43 | ) { 44 | if (Vars.state.gameTime > time) return 45 | launch(Dispatchers.game + context) { 46 | delayUntil(time) 47 | body() 48 | } 49 | } 50 | 51 | fun Script.checkEnabled(script: ScriptInfo): Boolean { 52 | if (script.enabled) { 53 | delayBroadcast("[yellow]加载地图脚本完成: {id}".with("id" to script.id)) 54 | } else { 55 | delayBroadcast( 56 | "[red]地图脚本{id}加载失败,请联系管理员: {reason}" 57 | .with("id" to script.id, "reason" to script.failReason.orEmpty()) 58 | ) 59 | } 60 | return script.enabled 61 | } -------------------------------------------------------------------------------- /scripts/wayzer/user/suffix.kts: -------------------------------------------------------------------------------- 1 | package wayzer.user 2 | 3 | import cf.wayzer.placehold.DynamicVar 4 | import mindustry.gen.Iconc 5 | 6 | val logVersion by config.key(false, "记录玩家的版本信息") 7 | 8 | val cache = mutableMapOf() 9 | listen { cache.remove(it.player.uuid()) } 10 | fun Player.getSuffix(): String? { 11 | cache[uuid()]?.let { return it } 12 | launch { 13 | cache[uuid()] = when { 14 | hasPermission("suffix.admin") -> "${Iconc.admin}" 15 | hasPermission("suffix.vip") -> "[gold]V[]" 16 | else -> return@launch 17 | } 18 | } 19 | return null 20 | } 21 | 22 | @Savable 23 | val clientType = mutableMapOf() 24 | customLoad(::clientType) { clientType.putAll(it) } 25 | listen { clientType.remove(it.player.uuid()) } 26 | onEnable { 27 | netServer.addPacketHandler("ARC") { p, v -> 28 | if (logVersion) 29 | logger.info("ARC ${p.name} $v") 30 | clientType[p.uuid()] = Iconc.blockArc 31 | } 32 | netServer.addPacketHandler("MDTX") { p, v -> 33 | if (logVersion) 34 | logger.info("MDTX ${p.name} $v") 35 | clientType[p.uuid()] = 'X' 36 | } 37 | netServer.addPacketHandler("fooCheck") { p, v -> 38 | if (logVersion) 39 | logger.info("FOO ${p.name} $v") 40 | clientType[p.uuid()] = '⒡' 41 | } 42 | } 43 | onDisable { 44 | netServer.getPacketHandlers("ARC").clear() 45 | netServer.getPacketHandlers("MDTX").clear() 46 | netServer.getPacketHandlers("fooCheck").clear() 47 | } 48 | 49 | 50 | registerVarForType().apply { 51 | 52 | registerChild("suffix.s2-clientType", "客户端类型后缀", { p -> clientType[p.uuid()] }) 53 | registerChild("suffix.s3-computer", "电脑玩家后缀", { p -> ''.takeIf { !p.con.mobile } }) 54 | registerChild("suffix.s5-group", "权限组后缀", { it.getSuffix() }) 55 | } 56 | 57 | PermissionApi.registerDefault("suffix.admin", group = "@admin") 58 | PermissionApi.registerDefault("suffix.vip", group = "@vip") -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | if (service !is UnicastRemoteObject) 51 | UnicastRemoteObject.exportObject(service, port) 52 | registry.bind(name, service) 53 | logger.info("RPC service registered: $name") 54 | service.thisContextScript().onDisable { 55 | registry.unbind(name) 56 | UnicastRemoteObject.unexportObject(service, true); 57 | } 58 | } -------------------------------------------------------------------------------- /scripts/wayzer/user/shortID.kts: -------------------------------------------------------------------------------- 1 | package wayzer.user 2 | 3 | import arc.util.serialization.Base64Coder 4 | import com.google.common.cache.Cache 5 | import com.google.common.cache.CacheBuilder 6 | import mindustry.net.Administration 7 | import java.security.MessageDigest 8 | import java.time.Duration 9 | 10 | val md5Digest = MessageDigest.getInstance("md5")!! 11 | fun shortStr(str: String): String { 12 | fun md5Md5(bs: ByteArray) = synchronized(md5Digest) { 13 | // md5(md5(bs)+bs) 14 | md5Digest.update(md5Digest.digest(bs)) 15 | md5Digest.digest(bs) 16 | } 17 | 18 | val bs = md5Md5(str.toByteArray()) 19 | return Base64Coder.encode(bs).sliceArray(0..2).map { 20 | when (it) { 21 | 'k' -> 'K' 22 | 'S' -> 's' 23 | 'l' -> 'L' 24 | '+' -> 'A' 25 | '/' -> 'B' 26 | else -> it 27 | } 28 | }.joinToString("") 29 | } 30 | 31 | fun Player.shortID() = shortStr(uuid()) 32 | 33 | val shortIDs: Cache = CacheBuilder.newBuilder() 34 | .expireAfterWrite(Duration.ofMinutes(60)).build() 35 | listen { 36 | val uuid = it.player.uuid() 37 | val new = it.player.shortID() 38 | val old = shortIDs.getIfPresent(new) 39 | shortIDs.put(new, uuid) 40 | if (old != null && old != uuid) 41 | logger.warning("3位ID碰撞: $uuid $old") 42 | } 43 | 44 | onEnable { 45 | PlayerData.IGetUidByShortId.provide(this, object : PlayerData.IGetUidByShortId { 46 | override fun getShortId(data: PlayerData): String = shortStr(data.uuid) 47 | 48 | override fun getUidByShortId(id: String): String? = 49 | Groups.player.find { it.uuid() == id || it.shortID() == id }?.uuid() 50 | ?: shortIDs.getIfPresent(id) 51 | }) 52 | } 53 | 54 | registerVarForType().apply { 55 | registerChild("shortID", "uuid 3位前缀,可以展现给其他玩家") { it.shortID() } 56 | registerChild("suffix.9shortID", "名字后缀:3位ID") { " [gray]${it.shortID()}[]" } 57 | } 58 | registerVarForType().apply { 59 | registerChild("shortID", "uuid 3位前缀,可以展现给其他玩家") { shortStr(it.id) } 60 | } -------------------------------------------------------------------------------- /scripts/metadata/coreMindustry.metadata: -------------------------------------------------------------------------------- 1 | ID coreMindustry 2 | FQ_NAME coreMindustry.Module 3 | +DEPENDS coreLibrary 4 | +IMPORT DefaultImport arc.Core 5 | +IMPORT DefaultImport coreMindustry.lib.* 6 | +IMPORT DefaultImport mindustry.Vars.* 7 | +IMPORT DefaultImport mindustry.content.* 8 | +IMPORT DefaultImport mindustry.game.EventType 9 | +IMPORT DefaultImport mindustry.gen.Call 10 | +IMPORT DefaultImport mindustry.gen.Groups 11 | +IMPORT DefaultImport mindustry.gen.Player 12 | +IMPORT LibraryByClass arc.Core 13 | +IMPORT LibraryByClass mindustry.Vars 14 | 15 | ID coreMindustry/console 16 | FQ_NAME coreMindustry.Console 17 | +DEPENDS coreMindustry module 18 | +IMPORT MavenDependsSingle org.fusesource.jansi:jansi:2.4.0 19 | +IMPORT MavenDependsSingle org.jline:jline-reader:3.21.0 20 | +IMPORT MavenDependsSingle org.jline:jline-terminal-jansi:3.21.0 21 | +IMPORT MavenDependsSingle org.jline:jline-terminal:3.21.0 22 | 23 | ID coreMindustry/contentsTweaker 24 | FQ_NAME coreMindustry.ContentsTweaker 25 | +DEPENDS coreMindustry module 26 | +IMPORT MavenDependsSingle cf.wayzer:ContentsTweaker:v3.0.1 27 | +IMPORT MavenRepository https://www.jitpack.io/ 28 | 29 | ID coreMindustry/menu 30 | FQ_NAME coreMindustry.Menu 31 | +DEPENDS coreMindustry module 32 | 33 | ID coreMindustry/scorebroad 34 | FQ_NAME coreMindustry.Scorebroad 35 | +DEPENDS coreMindustry module 36 | 37 | ID coreMindustry/util/packetHelper 38 | FQ_NAME coreMindustry.util.PacketHelper 39 | +DEPENDS coreMindustry module 40 | 41 | ID coreMindustry/util/spawnAround 42 | FQ_NAME coreMindustry.util.SpawnAround 43 | +DEPENDS coreMindustry module 44 | 45 | ID coreMindustry/util/trackBuilding 46 | FQ_NAME coreMindustry.util.TrackBuilding 47 | +DEPENDS coreMindustry module 48 | 49 | ID coreMindustry/utilMapRule 50 | FQ_NAME coreMindustry.UtilMapRule 51 | +DEPENDS coreMindustry module 52 | 53 | ID coreMindustry/utilNextChat 54 | FQ_NAME coreMindustry.UtilNextChat 55 | +DEPENDS coreMindustry module 56 | 57 | ID coreMindustry/utilTextInput 58 | FQ_NAME coreMindustry.UtilTextInput 59 | +DEPENDS coreMindustry module 60 | 61 | ID coreMindustry/variables 62 | FQ_NAME coreMindustry.Variables 63 | +DEPENDS coreMindustry module 64 | 65 | -------------------------------------------------------------------------------- /scripts/metadata/coreLibrary.metadata: -------------------------------------------------------------------------------- 1 | ID coreLibrary 2 | FQ_NAME coreLibrary.Module 3 | +IMPORT CompileArg -Xcontext-receivers 4 | +IMPORT DefaultImport cf.wayzer.placehold.* 5 | +IMPORT DefaultImport coreLibrary.lib.* 6 | +IMPORT DefaultImport coreLibrary.lib.event.* 7 | +IMPORT DefaultImport coreLibrary.lib.util.* 8 | +IMPORT MavenDepends io.github.config4k:config4k:0.7.0 9 | +IMPORT MavenDependsSingle com.github.way-zer:PlaceHoldLib:v7.3 10 | +IMPORT MavenDependsSingle org.slf4j:slf4j-api:2.0.16 11 | +IMPORT MavenDependsSingle org.slf4j:slf4j-simple:2.0.16 12 | +IMPORT MavenRepository https://www.jitpack.io/ 13 | 14 | ID coreLibrary/DBApi 15 | FQ_NAME coreLibrary.DBApi 16 | +DEPENDS coreLibrary module 17 | +IMPORT MavenDepends org.jetbrains.exposed:exposed-core:0.59.0 18 | +IMPORT MavenDepends org.jetbrains.exposed:exposed-dao:0.59.0 19 | +IMPORT MavenDepends org.jetbrains.exposed:exposed-java-time:0.59.0 20 | +IMPORT MavenDepends org.jetbrains.exposed:exposed-jdbc:0.59.0 21 | 22 | ID coreLibrary/DBConnector 23 | FQ_NAME coreLibrary.DBConnector 24 | +DEPENDS coreLibrary module 25 | +DEPENDS coreLibrary/DBApi 26 | 27 | ID coreLibrary/commands/configCmd 28 | FQ_NAME coreLibrary.commands.ConfigCmd 29 | +DEPENDS coreLibrary module 30 | 31 | ID coreLibrary/commands/control 32 | FQ_NAME coreLibrary.commands.Control 33 | +DEPENDS coreLibrary module 34 | 35 | ID coreLibrary/commands/helpful 36 | FQ_NAME coreLibrary.commands.Helpful 37 | +DEPENDS coreLibrary module 38 | 39 | ID coreLibrary/commands/hotReload 40 | FQ_NAME coreLibrary.commands.HotReload 41 | +DEPENDS coreLibrary module 42 | 43 | ID coreLibrary/commands/permissionCmd 44 | FQ_NAME coreLibrary.commands.PermissionCmd 45 | +DEPENDS coreLibrary module 46 | 47 | ID coreLibrary/commands/varsCmd 48 | FQ_NAME coreLibrary.commands.VarsCmd 49 | +DEPENDS coreLibrary module 50 | 51 | ID coreLibrary/extApi/KVStore 52 | FQ_NAME coreLibrary.extApi.KVStore 53 | +DEPENDS coreLibrary module 54 | +IMPORT MavenDependsSingle com.h2database:h2-mvstore:2.3.232 55 | 56 | ID coreLibrary/extApi/rpcService 57 | FQ_NAME coreLibrary.extApi.RpcService 58 | +DEPENDS coreLibrary module 59 | 60 | ID coreLibrary/lang 61 | FQ_NAME coreLibrary.Lang 62 | +DEPENDS coreLibrary module 63 | 64 | ID coreLibrary/variables 65 | FQ_NAME coreLibrary.Variables 66 | +DEPENDS coreLibrary module 67 | 68 | -------------------------------------------------------------------------------- /scripts/wayzer/map/autoSave.kts: -------------------------------------------------------------------------------- 1 | package wayzer.map 2 | 3 | import arc.files.Fi 4 | import arc.struct.StringMap 5 | import coreLibrary.lib.util.loop 6 | import mindustry.core.GameState 7 | import mindustry.io.SaveIO 8 | import java.util.* 9 | import java.util.concurrent.TimeUnit 10 | import java.util.logging.Level 11 | 12 | name = "自动存档" 13 | val autoSaveRange = 100 until 106 14 | command("slots", "列出自动保存的存档") { 15 | body { 16 | val list = autoSaveRange.map { it to SaveIO.fileFor(it) }.filter { it.second.exists() }.map { (id, file) -> 17 | "[red]{id}[]: [yellow]Save on {date hh:mm}".with("id" to id, "date" to file.lastModified().let(::Date)) 18 | } 19 | reply( 20 | """ 21 | |[green]===[white] 自动存档 [green]=== 22 | |{list|joinLines} 23 | |[green]===[white] {range} [green]=== 24 | """.trimMargin().with("range" to autoSaveRange, "list" to list) 25 | ) 26 | } 27 | } 28 | 29 | val nextSaveTime: Date 30 | get() {//Every 10 minutes 31 | val t = Calendar.getInstance() 32 | t.set(Calendar.SECOND, 0) 33 | val mNow = t.get(Calendar.MINUTE) 34 | t.add(Calendar.MINUTE, (mNow + 10) / 10 * 10 - mNow) 35 | return t.time 36 | } 37 | 38 | onEnable { 39 | loop { 40 | val nextTime = nextSaveTime.time 41 | delay(nextTime - System.currentTimeMillis()) 42 | if (state.`is`(GameState.State.playing)) { 43 | val minute = ((nextTime / TimeUnit.MINUTES.toMillis(1)) % 60).toInt() //Get the minute 44 | Core.app.post { 45 | val id = autoSaveRange.first + minute / 10 46 | val tmp = Fi.tempFile("save") 47 | try { 48 | val extTag = StringMap.of( 49 | "name", "[回档$id]" + state.map.name(), 50 | "description", state.map.description(), 51 | "author", state.map.author(), 52 | ) 53 | SaveIO.write(tmp, extTag) 54 | tmp.moveTo(SaveIO.fileFor(id)) 55 | } catch (e: Exception) { 56 | logger.log(Level.SEVERE, "存档存档出错", e) 57 | tmp.delete() 58 | } 59 | broadcast("[green]自动存档完成(整10分钟一次),存档号 [red]{id}".with("id" to id)) 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /scripts/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") 5 | } 6 | dependencies { 7 | defineModule("kcp") {} 8 | defineModule("bootStrap") {} 9 | defineModule("coreLibrary") { 10 | api("com.github.way-zer:PlaceHoldLib:v7.3") 11 | api("io.github.config4k:config4k:0.7.0") 12 | api("org.slf4j:slf4j-api:2.0.16") 13 | //coreLib/kcp/serialization 14 | api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0") 15 | //coreLib/DBApi 16 | val exposedVersion = "0.59.0" 17 | api("org.jetbrains.exposed:exposed-core:$exposedVersion") 18 | api("org.jetbrains.exposed:exposed-dao:$exposedVersion") 19 | api("org.jetbrains.exposed:exposed-java-time:$exposedVersion") 20 | //coreLib/extApi/redisApi 21 | api("redis.clients:jedis:4.3.1") 22 | //coreLib/extApi/mongoApi 23 | api("org.litote.kmongo:kmongo-coroutine:4.8.0") 24 | implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") 25 | //coreLib/extApi/KVStore 26 | api("com.h2database:h2-mvstore:2.3.232") 27 | } 28 | 29 | defineModule("javalin") { 30 | dependsModule("coreLibrary") 31 | api("io.javalin:javalin:6.7.0") 32 | } 33 | 34 | defineModule("coreMindustry") { 35 | dependsModule("coreLibrary") 36 | api("com.github.TinyLake.MindustryX:core") 37 | //coreMindustry/console 38 | implementation("org.jline:jline-terminal:3.21.0") 39 | implementation("org.jline:jline-reader:3.21.0") 40 | } 41 | defineModule("scratch") { 42 | dependsModule("coreLibrary") 43 | } 44 | 45 | defineModule("wayzer") { 46 | dependsModule("coreMindustry") 47 | api("com.google.guava:guava:30.1-jre") 48 | //wayzer/ext/profiler 49 | implementation("tools.profiler:async-profiler:4.1") 50 | } 51 | defineModule("mapScript") { 52 | dependsModule("wayzer") 53 | } 54 | } 55 | 56 | allprojects { 57 | tasks.withType().configureEach { 58 | compilerOptions { 59 | freeCompilerArgs = listOf( 60 | "-Xinline-classes", 61 | "-opt-in=kotlin.RequiresOptIn", 62 | "-Xnullability-annotations=@arc.util:strict", 63 | "-Xcontext-receivers", 64 | ) 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /scripts/wayzer/lib/PlayerData.kt: -------------------------------------------------------------------------------- 1 | package wayzer.lib 2 | 3 | import com.google.common.cache.CacheBuilder 4 | import coreLibrary.lib.util.ServiceRegistry 5 | import mindustry.gen.Groups 6 | import mindustry.gen.Player 7 | import mindustry.net.Packets.ConnectPacket 8 | import java.time.Duration 9 | 10 | class PlayerData(val name: String, val uuid: String, val ids: Set = mutableSetOf(uuid)) { 11 | var player: Player? = null 12 | var id: String = uuid 13 | private set 14 | val authed get() = id !== uuid 15 | 16 | fun addId(id: String, asPrimary: Boolean) { 17 | (ids as MutableSet).add(id) 18 | if (asPrimary) this.id = id 19 | } 20 | 21 | val shortId: String get() = IGetUidByShortId.getOrNull()?.getShortId(this) ?: id 22 | 23 | override fun toString(): String { 24 | return "PlayerData(id='$id', name='$name', authed=$authed)" 25 | } 26 | 27 | 28 | interface IGetUidByShortId { 29 | fun getShortId(data: PlayerData): String 30 | fun getUidByShortId(id: String): String? 31 | 32 | companion object : ServiceRegistry() 33 | } 34 | 35 | companion object { 36 | val history = CacheBuilder.newBuilder() 37 | .expireAfterWrite(Duration.ofDays(1)) 38 | .build()!! 39 | private val preOnline = mutableMapOf() 40 | private val online = mutableMapOf() 41 | 42 | fun forAuth(packet: ConnectPacket) = preOnline.getOrPut(packet.usid) { 43 | PlayerData(packet.name, packet.uuid) 44 | } 45 | 46 | operator fun get(player: Player): PlayerData = online.getOrPut(player) { 47 | if (player.con == null) error("player is not online") 48 | (preOnline.remove(player.usid()) ?: PlayerData(player.plainName(), player.uuid())) 49 | .also { it.player = player } 50 | } 51 | 52 | fun onLeave(player: Player) { 53 | val data = get(player) 54 | online.remove(player) 55 | history.put(player.uuid(), data) 56 | data.player = null 57 | } 58 | 59 | fun findByShortId(id: String): PlayerData? { 60 | val uuid = IGetUidByShortId.getOrNull()?.getUidByShortId(id) ?: id 61 | return Groups.player.find { it.uuid() == uuid }?.let { PlayerData[it] } 62 | ?: history.getIfPresent(uuid) 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /scripts/mapScript/tags/autoExchange.kts: -------------------------------------------------------------------------------- 1 | package mapScript.tags 2 | 3 | import mindustry.game.EventType.Trigger 4 | import mindustry.game.Team 5 | import mindustry.type.Item 6 | import mindustry.world.blocks.storage.CoreBlock 7 | import org.intellij.lang.annotations.Language 8 | 9 | registerMapTag("@autoExchange") 10 | modeIntroduce( 11 | "等价交换", """ 12 | 创意/实现:[gold]WayZer[] 13 | 所有资源自动进行等价交换,按比例保持同步,核心资源为当前最大可兑换数量 14 | [green]TIP[]: 可能有暴富的错觉,建议只看单种资源判断价值 15 | 16 | 所有核心容量=原版*10 17 | 0值资源(BAN):沙,休眠胞,裂变物质 18 | 1值资源:绝大部分 19 | 2值资源:钛,石墨,硅,硫 20 | 3值资源:钍,塑料,氧化物,爆炸化合物 21 | 4值资源:钨,碳化物,布,合金 22 | """.trimIndent() 23 | ) 24 | 25 | val cores = content?.run { 26 | blocks().filterIsInstance().map { 27 | """block.${it.name}.itemCapacity: ${it.itemCapacity * 10},""" 28 | } 29 | }.orEmpty() 30 | @Language("JSON5") 31 | val patch = """ 32 | { 33 | "name": "CoreWar", 34 | ${cores.joinToString("\n")} 35 | } 36 | """.trimIndent() 37 | mapPatches = listOf(patch) 38 | 39 | val score = IntArray(Team.all.size) 40 | onEnableForGame { 41 | score.fill(0) 42 | state.teams.getActive().forEach { 43 | score[it.team.id] = it.team.items().get(Items.copper) 44 | } 45 | } 46 | onDisable { 47 | score.fill(0) 48 | } 49 | val Item.score: Int 50 | get() = when (this) { 51 | Items.sand, Items.fissileMatter, Items.dormantCyst -> 0 52 | Items.titanium, Items.graphite, Items.silicon, Items.pyratite -> 2 53 | Items.thorium, Items.plastanium, Items.oxide, Items.blastCompound -> 3 54 | Items.tungsten, Items.carbide, Items.phaseFabric, Items.surgeAlloy -> 4 55 | else -> 1 56 | } 57 | 58 | fun Int.toCount(item: Item): Int = if (item.score == 0) 0 else (this / item.score).coerceAtLeast(0) 59 | fun Int.toScore(item: Item): Int = this * item.score 60 | fun syncItems(team: Team) { 61 | val old = score[team.id] 62 | val items = team.data().core()?.items ?: return 63 | val delta = content.items().sumOf { item -> 64 | (items.get(item) - old.toCount(item)).toScore(item) 65 | } 66 | val new = old + delta 67 | score[team.id] = new 68 | content.items().forEach { 69 | if (it.score != 0) 70 | items.set(it, new.toCount(it)) 71 | } 72 | } 73 | 74 | listen(Trigger.update) { 75 | Team.all.forEach { 76 | if (it.data().noCores()) score[it.id] = 0 77 | else syncItems(it) 78 | } 79 | } 80 | 81 | command("debugScore", "".asPlaceHoldString()) { 82 | body { 83 | reply(score.toList().toString().asPlaceHoldString()) 84 | } 85 | } -------------------------------------------------------------------------------- /scripts/wayzer/cmds/gatherTp.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/user/ext/skills", "Gather也算技能") 2 | 3 | package wayzer.cmds 4 | 5 | import cf.wayzer.placehold.PlaceHoldApi.with 6 | import mindustry.entities.Units 7 | import mindustry.gen.Unit 8 | import mindustry.world.Tile 9 | import wayzer.user.ext.SkillCooldown 10 | import wayzer.user.ext.SkillNoPvp 11 | import wayzer.user.ext.SkillPrecheck 12 | import wayzer.user.ext.skillBody 13 | import java.time.Duration 14 | import java.time.Instant 15 | 16 | PermissionApi.registerDefault("wayzer.ext.gather") 17 | 18 | var lastPos: Tile? = null 19 | var lastTime: Instant = Instant.MIN 20 | 21 | command("gather", "发出集合请求") { 22 | usage = "[可选说明]" 23 | aliases = listOf("集合") 24 | attr(SkillPrecheck) 25 | attr(SkillNoPvp) 26 | attr(SkillCooldown(30_000)) 27 | requirePermission("wayzer.ext.gather") 28 | skillBody { 29 | if (player.dead() || !player.unit().type.targetable) 30 | returnReply("[red]当前单位无法使用 集合".with()) 31 | if (Duration.between(lastTime, Instant.now()) < Duration.ofSeconds(10)) { 32 | returnReply("[red]刚刚有人发起请求,请稍等10s再试".with()) 33 | } 34 | val message = "[white]\"${arg.firstOrNull() ?: ""}[white]\"" 35 | val tile = player.tileOn() ?: returnReply("[red]请在地图内使用".with()) 36 | lastPos = tile 37 | lastTime = Instant.now() 38 | broadcastSkill("集合(${tile.x},${tile.y})") 39 | broadcast("可输入\"[gold]go[white]\"前往:{message}".with("message" to message), quite = true) 40 | } 41 | } 42 | 43 | command("tp", "传送到鼠标坐标") { 44 | attr(ClientOnly) 45 | requirePermission("wayzer.ext.tp") 46 | body { 47 | val player = player!! 48 | player.unit()?.apply { 49 | set(player.mouseX, player.mouseY) 50 | snapInterpolation() 51 | } 52 | } 53 | } 54 | 55 | fun check(unit: Unit, tile: Tile): Boolean { 56 | if (unit.type.flying) return true 57 | return unit.canPass(tile.x.toInt(), tile.y.toInt()) && 58 | Units.count(tile.worldx(), tile.worldy(), unit.physicSize()) { it.isGrounded && it.hitSize > 14.0F } > 0 59 | } 60 | listen { 61 | val tile = lastPos ?: return@listen 62 | if (it.message.equals("go", true)) { 63 | it.player.unit()?.apply { 64 | if (!check(this, tile)) { 65 | it.player.sendMessage("[yellow]目标位置无法安全传送") 66 | return@listen 67 | } 68 | set(tile) 69 | snapInterpolation() 70 | } 71 | } 72 | } 73 | 74 | listen { 75 | lastPos = null 76 | } -------------------------------------------------------------------------------- /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 java.lang.reflect.Proxy 14 | import kotlin.properties.ReadOnlyProperty 15 | 16 | /** 17 | * 模块化服务提供工具库 18 | */ 19 | 20 | @Suppress("unused") 21 | open class ServiceRegistry { 22 | private val impl = MutableSharedFlow(1, 0, BufferOverflow.DROP_OLDEST) 23 | private val mutex = Mutex() 24 | 25 | suspend fun provide(script: Script, inst: T) { 26 | script.providedService.add(this to inst) 27 | this.impl.emit(inst) 28 | ServiceProvidedEvent(inst, script).emitAsync() 29 | 30 | @OptIn(ExperimentalCoroutinesApi::class) 31 | script.onDisable { 32 | if (getOrNull() == inst) 33 | impl.resetReplayCache() 34 | } 35 | } 36 | 37 | fun getOrNull() = impl.replayCache.firstOrNull() 38 | fun get() = getOrNull() ?: error("No Provider for ${this.javaClass.canonicalName}") 39 | 40 | val provided get() = getOrNull() != null 41 | fun toFlow() = impl.asSharedFlow() 42 | 43 | suspend fun awaitInit() = impl.first() 44 | 45 | @JvmOverloads 46 | fun subscribe(scope: CoroutineScope, async: Boolean = false, body: suspend (T) -> Unit) { 47 | impl.onEach { 48 | if (async) body(it) 49 | else mutex.withLock { body(it) } 50 | }.launchIn(scope) 51 | } 52 | 53 | val nullable get() = ReadOnlyProperty { _, _ -> getOrNull() } 54 | val notNull get() = ReadOnlyProperty { _, _ -> get() } 55 | 56 | fun createProxy(cls: Class): T { 57 | check(cls.isInterface) { "$cls is not an interface" } 58 | @Suppress("UNCHECKED_CAST") 59 | return Proxy.newProxyInstance(cls.classLoader, arrayOf(cls)) { _, method, args -> 60 | val inst = getOrNull() ?: error("No Provider for ${cls.canonicalName}") 61 | method.invoke(inst, *args) 62 | } as T 63 | } 64 | 65 | companion object { 66 | val Script.providedService by DSLBuilder.dataKeyWithDefault { mutableSetOf, *>>() } 67 | } 68 | } 69 | 70 | inline fun ServiceRegistry.createProxy(): T = createProxy(T::class.java) -------------------------------------------------------------------------------- /scripts/wayzer/map/pvpProtect.kts: -------------------------------------------------------------------------------- 1 | package wayzer.map 2 | 3 | import arc.math.geom.Geometry 4 | import arc.math.geom.Point2 5 | import mindustry.game.Gamemode 6 | import mindustry.gen.Unit 7 | import java.time.Duration 8 | import kotlin.math.ceil 9 | 10 | val time by config.key(600, "pvp保护时间(单位秒,小于等于0关闭)") 11 | 12 | val Unit.inEnemyArea: Boolean 13 | get() { 14 | val closestCore = state.teams.active 15 | .mapNotNull { it.cores.minByOrNull(this::dst2) } 16 | .minByOrNull(this::dst2) ?: return false 17 | return closestCore.team != team() && (state.rules.polygonCoreProtection || 18 | dst(closestCore) < state.rules.enemyCoreBuildRadius) 19 | } 20 | 21 | listen { 22 | var leftTime = state.rules.tags.getInt("@pvpProtect", time) 23 | if (state.rules.mode() != Gamemode.pvp || time <= 0) return@listen 24 | loop(Dispatchers.game) { 25 | delay(1000) 26 | Groups.unit.forEach { 27 | if (it.inEnemyArea) { 28 | it.player?.sendMessage("[red]PVP保护时间,禁止进入敌方区域".with()) 29 | it.closestCore()?.run { 30 | val valid = mutableListOf() 31 | Geometry.circle(tileX(), tileY(), world.width(), world.height(), 10) { x, y -> 32 | if (it.canPass(x, y) && (!it.canDrown() || floorOn()?.isDeep == false)) 33 | valid.add(Point2(x, y)) 34 | } 35 | val r = valid.randomOrNull() ?: return@run 36 | it.x = r.x * tilesize.toFloat() 37 | it.y = r.y * tilesize.toFloat() 38 | it.snapInterpolation() 39 | } 40 | it.resetController() 41 | if (leftTime > 60) 42 | it.apply(StatusEffects.unmoving, (leftTime - 60) * 60f) 43 | // it.kill() 44 | } 45 | } 46 | } 47 | launch(Dispatchers.game) { 48 | broadcast( 49 | "[yellow]PVP保护时间,禁止在其他基地攻击(持续{time 分钟})".with( 50 | "time" to Duration.ofSeconds(leftTime.toLong()) 51 | ), 52 | quite = true 53 | ) 54 | repeat(leftTime / 60) { 55 | delay(60_000) 56 | leftTime -= 60 57 | broadcast("[yellow]PVP保护时间还剩 {time}分钟".with("time" to ceil(leftTime / 60f)), quite = true) 58 | } 59 | delay(leftTime * 1000L) 60 | broadcast("[yellow]PVP保护时间已结束, 全力进攻吧".with()) 61 | thisScript.coroutineContext.cancelChildren() 62 | } 63 | } 64 | 65 | listen { 66 | coroutineContext.cancelChildren() 67 | } -------------------------------------------------------------------------------- /scripts/javalin/module.kts: -------------------------------------------------------------------------------- 1 | @file:Import("io.javalin:javalin:6.7.0", mavenDepends = true) 2 | @file:Import("org.slf4j:slf4j-simple:2.0.16", mavenDepends = true) 3 | @file:Import("javalin.lib.*", defaultImport = true) 4 | @file:Depends("coreLibrary") 5 | 6 | package javalin 7 | 8 | import cf.wayzer.scriptAgent.events.ScriptDisableEvent 9 | import cf.wayzer.scriptAgent.events.ScriptEnableEvent 10 | import io.javalin.Javalin 11 | import kotlinx.coroutines.channels.BufferOverflow 12 | import kotlinx.coroutines.flow.MutableSharedFlow 13 | import kotlinx.coroutines.flow.debounce 14 | import kotlinx.coroutines.flow.launchIn 15 | import kotlinx.coroutines.flow.onEach 16 | import org.eclipse.jetty.server.Server 17 | import org.eclipse.jetty.server.handler.StatisticsHandler 18 | import java.util.logging.Level 19 | 20 | val port by config.key(7001, "Javalin 端口") 21 | 22 | fun startJavalin(server: Server): Javalin { 23 | val app = Javalin.create { config -> 24 | config.pvt.jetty.server = server 25 | config.startupWatcherEnabled = false 26 | ScriptRegistry.allScripts { it.enabled }.forEach { 27 | it.inst?.configJavalin?.forEach { 28 | it(config) 29 | } 30 | } 31 | } 32 | ScriptRegistry.allScripts { it.enabled }.forEach { 33 | it.inst?.webRoutes?.forEach { 34 | it(app) 35 | } 36 | } 37 | withContextClassloader { app.start() } 38 | return app 39 | } 40 | 41 | 42 | onEnable { 43 | val server = Server(port).apply { 44 | insertHandler(StatisticsHandler()) 45 | } 46 | val initHandler = server.handler ?: null 47 | onDisable { server.stop() } 48 | 49 | restartFlow.emit(Unit) 50 | 51 | @OptIn(FlowPreview::class) 52 | restartFlow.debounce(1000) 53 | .onEach { 54 | logger.info("(Re)Starting Javalin server...") 55 | try { 56 | //reset handler 57 | server.stop() 58 | server.handler = initHandler 59 | startJavalin(server) 60 | } catch (e: Throwable) { 61 | logger.log(Level.WARNING, "Exception when start", e) 62 | } 63 | }.launchIn(this) 64 | } 65 | 66 | val restartFlow = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) 67 | listenTo(Event.Priority.Watch) { 68 | if (script.dslExists(configJavalin) || script.dslExists(webRoutes)) { 69 | restartFlow.emit(Unit) 70 | } 71 | } 72 | 73 | listenTo(Event.Priority.Watch) { 74 | if (script.dslExists(configJavalin) || script.dslExists(webRoutes)) { 75 | restartFlow.emit(Unit) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /scripts/wayzer/pvp/pvpAlert.kts: -------------------------------------------------------------------------------- 1 | package wayzer.pvp 2 | 3 | import mindustry.game.Team 4 | import mindustry.type.Item 5 | import mindustry.type.UnitType 6 | import mindustry.world.Block 7 | import mindustry.world.blocks.production.Drill 8 | import mindustry.world.blocks.production.GenericCrafter 9 | import mindustry.world.blocks.storage.CoreBlock 10 | 11 | val specialBuild = arrayOf( 12 | Blocks.graphitePress, Blocks.multiPress, 13 | Blocks.siliconSmelter, Blocks.siliconCrucible, 14 | Blocks.plastaniumCompressor, Blocks.phaseWeaver, Blocks.surgeSmelter, 15 | Blocks.thermalGenerator, Blocks.foreshadow 16 | ) 17 | 18 | class TeamData { 19 | val ores = mutableSetOf()//drill+ore 20 | val unit = mutableSetOf() 21 | val build = mutableSetOf() 22 | } 23 | 24 | val data = arrayOfNulls(Team.all.size) 25 | listen { 26 | data.fill(null) 27 | } 28 | val Team.myData 29 | get() = data[id] ?: TeamData().also { 30 | data[id] = it 31 | } 32 | 33 | fun log(team: Team, msg: PlaceHoldString) { 34 | if (state.rules.fog) return 35 | broadcast( 36 | "{team.colorizeName}[]{msg}".with("team" to team, "msg" to msg), 37 | MsgType.InfoToast, 38 | quite = true 39 | ) 40 | } 41 | 42 | listen { 43 | if (!state.rules.pvp || it.breaking) return@listen 44 | fun produce(block: Block, item: Item) { 45 | log(it.team, "开始使用 {block} 生产 {item}".with("block" to block, "item" to item)) 46 | } 47 | 48 | val data = it.team.myData 49 | val block = it.tile.block() 50 | if (block is Drill) { 51 | val item = (it.tile.build as Drill.DrillBuild).dominantItem 52 | if (data.ores.add("${block.name}-${item.name}")) { 53 | produce(block, item) 54 | } 55 | } 56 | if (block in specialBuild && data.build.add(block)) { 57 | if (block is GenericCrafter) { 58 | val item = block.outputItem.item 59 | produce(block, item) 60 | } else { 61 | log(it.team, "开始建筑 {block}".with("block" to block)) 62 | } 63 | } 64 | } 65 | 66 | listen { 67 | if (!state.rules.pvp || it.unit.spawnedByCore) return@listen 68 | val team = it.unit.team 69 | val type = it.unit.type 70 | if (team.myData.unit.add(type)) { 71 | log(team, "完成生产单位 {unit}".with("unit" to type)) 72 | } 73 | } 74 | 75 | listen { e -> 76 | if (!state.rules.pvp || e.tile.block() !is CoreBlock) return@listen 77 | val team = e.tile.team() 78 | if (team.data().cores.none { it != e.tile.build }) { 79 | log(team, "淘汰".with()) 80 | } 81 | } -------------------------------------------------------------------------------- /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/wayzer/user/banStore.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/user/ban") 2 | @file:Depends("coreLibrary/DBApi", "数据库储存") 3 | 4 | package wayzer.user 5 | 6 | import coreLibrary.DBApi.DB.registerTable 7 | import org.jetbrains.exposed.dao.id.IntIdTable 8 | import org.jetbrains.exposed.sql.* 9 | import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq 10 | import org.jetbrains.exposed.sql.javatime.CurrentTimestamp 11 | import org.jetbrains.exposed.sql.javatime.timestamp 12 | import org.jetbrains.exposed.sql.transactions.transaction 13 | import java.rmi.server.UnicastRemoteObject 14 | import java.time.Duration 15 | import java.time.Instant 16 | 17 | object Table : IntIdTable("PlayerBanV2") { 18 | val ids = text("ids", eagerLoading = true) 19 | val reason = text("reason", eagerLoading = true) 20 | val operator = text("operator").nullable() 21 | val createTime = timestamp("createTime").defaultExpression(CurrentTimestamp) 22 | val endTime = timestamp("endTime").defaultExpression(CurrentTimestamp) 23 | } 24 | registerTable(Table) 25 | 26 | object ServiceImpl : UnicastRemoteObject(), Ban.PlayerBanStore { 27 | private fun readResolve(): Any = ServiceImpl 28 | private fun ResultRow.toBan() = Ban.PlayerBan( 29 | get(Table.id).value, 30 | ids = get(Table.ids).split("$").toSet(), 31 | reason = get(Table.reason), 32 | operator = get(Table.operator), 33 | createTime = get(Table.createTime), 34 | endTime = get(Table.endTime) 35 | ) 36 | 37 | private fun getById(id: Int) = transaction { 38 | Table.selectAll().where { Table.id eq id }.firstOrNull()?.toBan() 39 | } 40 | 41 | override fun create(ids: Set, duration: Duration, reason: String, operator: String?): Ban.PlayerBan = 42 | transaction { 43 | Table.insertAndGetId { 44 | it[Table.ids] = ids.joinToString("$", "$", "$") 45 | it[Table.endTime] = Instant.now() + duration 46 | it[Table.operator] = operator 47 | it[Table.reason] = reason 48 | }.let { 49 | getById(it.value)!! 50 | } 51 | } 52 | 53 | override fun findNotEnd(id: String): Ban.PlayerBan? = transaction { 54 | Table.selectAll().where { (Table.ids like "%$${id}$%") and Table.endTime.greater(CurrentTimestamp) } 55 | .firstOrNull()?.toBan() 56 | } 57 | 58 | override fun delete(record: Int): Ban.PlayerBan? = transaction { 59 | getById(record)?.also { 60 | Table.deleteWhere { id eq record } 61 | } 62 | } 63 | } 64 | 65 | val rpcService = contextScript() 66 | 67 | onEnable { 68 | rpcService.register { ServiceImpl } 69 | } -------------------------------------------------------------------------------- /scripts/bootStrap/generate.kts: -------------------------------------------------------------------------------- 1 | package bootStrap 2 | 3 | import cf.wayzer.scriptAgent.events.ScriptStateChangeEvent 4 | import cf.wayzer.scriptAgent.util.CASScriptPacker 5 | import cf.wayzer.scriptAgent.util.DependencyManager 6 | import cf.wayzer.scriptAgent.util.maven.Dependency 7 | import java.io.File 8 | import kotlin.system.exitProcess 9 | import kotlin.system.measureTimeMillis 10 | 11 | fun prepareBuiltin(outputFile: File = File("build/tmp/builtin.packed.zip")) { 12 | val scripts = ScriptRegistry.allScripts { it.scriptState.loaded } 13 | .mapNotNull { it.compiledScript } 14 | println("prepare Builtin for ${scripts.size} scripts.") 15 | @OptIn(SAExperimentalApi::class) 16 | CASScriptPacker(outputFile.outputStream()) 17 | .use { scripts.forEach(it::add) } 18 | } 19 | 20 | suspend fun compileOnlyLoad(script: ScriptInfo) { 21 | Config.logger.info("编译脚本 ${script.id}") 22 | val compiled = try { 23 | @OptIn(SAExperimentalApi::class) 24 | ScriptManager.compileScript(script.source) 25 | } catch (e: Exception) { 26 | val msg = if (e is IllegalStateException) e.message else e.toString() 27 | script.failReason = "编译失败: $msg" 28 | return 29 | } 30 | try { 31 | compiled.loadLibraries() 32 | } catch (e: Exception) { 33 | script.failReason = "依赖下载失败:$e" 34 | return 35 | } 36 | script.compiledScript = compiled 37 | script.stateUpdateForce(ScriptState.Loaded).join() 38 | } 39 | 40 | //Replaced with compileOnlyLoad 41 | listenTo { 42 | if (next == ScriptState.Loading) { 43 | cancelled = true 44 | compileOnlyLoad(script) 45 | } 46 | } 47 | 48 | onEnable { 49 | if (id != Config.mainScript) 50 | return@onEnable ScriptManager.disableScript(this, "仅可通过SAMAIN启用") 51 | //so we can use `listenTo` in main 52 | launch { main() } 53 | } 54 | suspend fun main() { 55 | DependencyManager { 56 | addRepository("https://www.jitpack.io/") 57 | require(Dependency.parse("com.github.TinyLake.MindustryX:core:v2025.10.X21")) 58 | loadToClassLoader(Config.mainClassloader) 59 | } 60 | ScriptManager.transaction { 61 | if (Config.args.isEmpty()) 62 | addAll() 63 | else 64 | Config.args.forEach { add(it) } 65 | 66 | load() 67 | } 68 | val fail = ScriptRegistry.allScripts { it.failReason != null } 69 | println("共加载${ScriptRegistry.allScripts { it.scriptState != ScriptState.Found }.size}个脚本,失败${fail.size}个") 70 | fail.forEach { 71 | println("\t${it.id}: ${it.failReason}") 72 | } 73 | if (System.getProperty("ScriptAgent.PreparePack") != null) { 74 | println("Finish pack in ${measureTimeMillis { prepareBuiltin() }}ms") 75 | } 76 | exitProcess(fail.size) 77 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![For Mindustry](https://img.shields.io/badge/For-Mindustry-orange) 2 | ![Lang CN](https://img.shields.io/badge/Lang-ZH--CN-blue) 3 | ![Support 8.0](https://img.shields.io/badge/Support_Version-8.0(147+)-success) 4 | ![GitHub Releases](https://img.shields.io/github/downloads/way-zer/ScriptAgent4MindustryExt/latest/total) 5 | [![BuildPlugin](https://github.com/way-zer/ScriptAgent4MindustryExt/actions/workflows/buildPlugin.yml/badge.svg)](https://github.com/way-zer/ScriptAgent4MindustryExt/actions/workflows/buildPlugin.yml) 6 | [![CheckScript](https://github.com/way-zer/ScriptAgent4MindustryExt/actions/workflows/checkScripts.yml/badge.svg)](https://github.com/way-zer/ScriptAgent4MindustryExt/actions/workflows/checkScripts.yml) 7 | 8 | > For English README see [README_en](./README_en.md) 9 | 10 | ## ScriptAgent 11 | 一套基于Kotlin脚本(kts)的模块化框架 12 | - 强大:基于Kotlin,可以访问所有Java接口(所有插件能实现的功能,脚本都能实现) 13 | - 高效:脚本加载后转换为JVM字节码,与Java插件性能无异 14 | - 灵活:模块和脚本具有完整生命周期,支持热加载和热重载 15 | - 快速开发:提供大量实用辅助函数,无需编译即可快速部署到服务器 16 | - 智能:开发时支持IDEA或Android Studio的智能补全 17 | - 可定制:除核心部分外,插件功能均通过脚本实现,可根据需求自由修改,模块定义脚本还可扩展DSL 18 | 19 | 加载器(jar)本身无具体功能,仅负责脚本的加载与管理,所有功能均由脚本实现。 20 | 21 | ### ScriptAgent for Mindustry (SA4MDT) 22 | 该框架针对Mindustry的实现,包含加载器(Loader)和一系列功能脚本,具体分为以下6个模块: 23 | - coreLib(coreLibrary):框架的标准库 24 | - core(coreMindustry):针对Mindustry的具体实现 25 | - main模块:用于存放简单脚本 26 | - wayzer模块:一套完整的Mindustry服务器基础插件(By: WayZer) 27 | - 交流QQ群:1033116078 或直接在Discussions讨论 28 | - 插件测试服务器:cn.mindustry.top 29 | - mapScript:专为MDT设计的特殊脚本,生命周期与单局游戏绑定,仅在需要时加载 30 | - ~~mirai模块:QQ机器人库mirai的脚本封装(因上游不可控因素,计划移除)~~ 31 | 32 | ### 客户端预览 33 | ![image](https://user-images.githubusercontent.com/15688938/132090295-59a57f81-cc72-4ab5-8c10-deadf7ae452a.png) 34 | ![image](https://user-images.githubusercontent.com/15688938/132090317-cc62339d-8ce5-4906-90d0-e8fda1bacf36.png) 35 | 36 | ### 服务器后台预览 37 | ![image](https://user-images.githubusercontent.com/15688938/132090197-e041d11c-e09a-49ee-94e8-d2cdae30038f.png) 38 | ![image](https://user-images.githubusercontent.com/15688938/132090212-1f924326-4ba7-43be-bbb8-e055599fa75c.png) 39 | ![image](https://user-images.githubusercontent.com/15688938/132090238-bbfcaf2e-154a-446c-9d1f-92f391835f0a.png) 40 | 41 | ## 快速入门 42 | ### 插件安装(推荐普通用户使用) 43 | allInOne版本在加载器内集成了编译好的脚本 44 | 1. 从Release页面下载`xxx.allinone.jar`文件,并将其放置在`config/mods`目录下 45 | 2. 启动服务器(首次启动会从网络下载依赖,耗时较长) 46 | 47 | ### 加载器+脚本安装(高级用户) 48 | 1. 从Release页面下载预编译的jar和脚本包zip 49 | 2. 将jar文件放置在`config/mods`文件夹下,将脚本包解压到`config/scripts`文件夹(需自行创建) 50 | 3. 启动服务器(首次启动会从网络下载依赖,耗时较长) 51 | 4. 等待插件加载完成(脚本首次运行会进行编译,耗时较长,编译完成后会保存缓存) 52 | 53 | ### 独立运行/脚本开发 54 | 请查阅[Wiki](https://github.com/way-zer/ScriptAgent4MindustryExt/wiki) 55 | 56 | ## 版权说明 57 | - 加载器:免费使用,未经许可禁止转载和用作其他用途 58 | - 本仓库脚本: 59 | - 默认允许私人修改并使用,但禁止修改原作者版权信息,公开使用需注明出处(fork或引用该仓库) 60 | - mirai模块及依赖该模块的所有代码,遵循AGPLv3协议 61 | - 其他脚本:归脚本作者所有,作者可自行声明开源协议,不受加载器版权影响 -------------------------------------------------------------------------------- /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/mapScript/shared/hexed.HexedGenerator.kt: -------------------------------------------------------------------------------- 1 | package mapScript.shared 2 | 3 | import arc.math.Mathf 4 | import arc.math.geom.Bresenham2 5 | import arc.math.geom.Geometry 6 | import arc.math.geom.Intersector 7 | import arc.math.geom.Point2 8 | import mindustry.content.Blocks 9 | import mindustry.game.Rules 10 | import mindustry.world.Tiles 11 | import kotlin.math.ceil 12 | import kotlin.math.sqrt 13 | 14 | @Suppress("MemberVisibilityCanBePrivate") 15 | open class HexedGenerator( 16 | val hNum: Int = 5, 17 | val wNum: Int = 7, 18 | val spacing: Int = 76,//最近中心距 19 | val wallWidth: Int = 3,//内六边形长 20 | val pathWidth: Int = 5,//相邻过道宽度 21 | ) { 22 | val width = ceil(((wNum - 1) / 2 * sqrt(3.0) + 1) * spacing).toInt() 23 | val height = hNum * spacing 24 | fun applyRules(rules: Rules) = rules.run { 25 | tags.put("hexed", "true") 26 | canGameOver = false 27 | polygonCoreProtection = true 28 | cleanupDeadTeams = true 29 | } 30 | 31 | val chunkCenters by lazy { 32 | buildList { 33 | val dx: Double = sqrt(3.0) * spacing / 2 34 | val dy = spacing / 2 35 | for (x in 0 until wNum) { 36 | for (y in 0 until hNum) { 37 | //忽略最上的交错排 38 | if (y == hNum - 1 && x % 2 == 1) continue 39 | val cx = (spacing / 2 + x * dx).toInt() 40 | val cy = dy * (1 + y * 2 + x % 2) 41 | add(Point2(cx, cy)) 42 | } 43 | } 44 | } 45 | } 46 | 47 | /**挖出六边形网格*/ 48 | fun genHex(tiles: Tiles) { 49 | val d = (spacing - wallWidth) * 2 / Mathf.sqrt3 50 | chunkCenters.forEach { 51 | hexShape(it.x, it.y, d) { hx, hy -> 52 | tiles.getn(hx, hy).setBlock(Blocks.air) 53 | } 54 | } 55 | } 56 | 57 | /**挖出六边形网格*/ 58 | fun genPath(tiles: Tiles) { 59 | chunkCenters.forEach { chunk -> 60 | chunkCenters.filter { it != chunk && it.dst(chunk) < spacing * 1.1 }.forEach { 61 | lineShape(chunk, it, pathWidth) { lx, ly -> 62 | tiles.getn(lx, ly).setBlock(Blocks.air) 63 | } 64 | } 65 | } 66 | } 67 | 68 | 69 | //util 70 | fun hexShape(cx: Int, cy: Int, d: Float, body: (Int, Int) -> Unit) { 71 | Geometry.circle(cx, cy, width, height, ceil(d).toInt()) { x, y -> 72 | if (Intersector.isInsideHexagon(cx.toFloat(), cy.toFloat(), d, x.toFloat(), y.toFloat())) { 73 | body(x, y) 74 | } 75 | } 76 | } 77 | 78 | fun lineShape(a: Point2, b: Point2, lineWidth: Int, body: (Int, Int) -> Unit) { 79 | Bresenham2.line(a.x, a.y, b.x, b.y) { lx, ly -> 80 | Geometry.circle(lx, ly, width, height, lineWidth / 2 + 1) { x, y -> 81 | body(x, y) 82 | } 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /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/mapScript/module.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("coreMindustry") 2 | @file:Depends("wayzer/maps", "获取地图信息") 3 | @file:Depends("wayzer/map/mapInfo", "显示地图信息", soft = true) 4 | @file:Import("mapScript.lib.*", defaultImport = true) 5 | 6 | /** 7 | * 该模块定义了一种特殊的kts:kts的生命周期与地图关联。 8 | * 当地图满足特定条件时(id/tag),关联的kts会被enable,而一局游戏结束后,所有的kts会被disable。 9 | * */ 10 | package mapScript 11 | 12 | import wayzer.MapManager 13 | import wayzer.MapRegistry 14 | 15 | val children get() = ScriptRegistry.allScripts { it != scriptInfo && it.dependsOn(scriptInfo) } 16 | 17 | onEnable { 18 | //Disable all non-controller scripts 19 | children.forEach { 20 | if (it.scriptState == ScriptState.ToEnable && it.inst?.mapScriptController != true) { 21 | it.stateUpdateForce(ScriptState.Loaded) 22 | } 23 | } 24 | } 25 | 26 | listen { 27 | MindustryDispatcher.safeBlocking { 28 | ScriptManager.transaction { 29 | addAll(children) 30 | disable() 31 | getForState(ScriptState.ToEnable).forEach { 32 | it.stateUpdateForce(ScriptState.Loaded) 33 | } 34 | } 35 | } 36 | } 37 | 38 | listen { 39 | //try update child scripts 40 | ScriptRegistry.scanRoot() 41 | MindustryDispatcher.safeBlocking { 42 | ScriptManager.transaction { 43 | addAll(children) 44 | removeIf { it.compiledScript?.source.run { this == null || this == it.source } } 45 | if (isEmpty()) return@transaction 46 | 47 | logger.info("Unload outdated script: ${toList()}") 48 | unload()//unload all updatable 49 | } 50 | } 51 | } 52 | 53 | fun getToLoadMapScripts(): List { 54 | return buildList { 55 | ScriptManager.getScriptNullable("mapScript/${MapManager.current.id}")?.id?.let { add(it) } 56 | state.rules.tags.get("@mapScript")?.let { add("mapScript/${it.toIntOrNull() ?: MapManager.current.id}") } 57 | addAll(TagSupport.findTags(state.rules).values) 58 | }.mapNotNull { scriptId -> 59 | ScriptRegistry.getScriptInfo(scriptId) ?: null.also { 60 | delayBroadcast("[red]该服务器不存在对应地图脚本,请联系管理员: {id}".with("id" to scriptId)) 61 | } 62 | } 63 | } 64 | 65 | listen { 66 | val patches = getToLoadMapScripts().flatMap { it.inst?.mapPatches.orEmpty() } 67 | if (patches.isEmpty()) return@listen 68 | logger.info("Patches loaded: ${patches.size}") 69 | it.patches.addAll(patches) 70 | } 71 | 72 | listen { 73 | //load scripts 74 | val toLoad = getToLoadMapScripts() 75 | if (toLoad.isEmpty()) return@listen 76 | MindustryDispatcher.safeBlocking { 77 | ScriptManager.transaction { 78 | addAll(toLoad) 79 | load(); enable() 80 | } 81 | } 82 | toLoad.forEach { checkEnabled(it) } 83 | } 84 | 85 | onEnable { 86 | MapRegistry.register(this, ScriptMapGenerator.Provider) 87 | } -------------------------------------------------------------------------------- /scripts/wayzer/cmds/voteKick.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/vote", "投票实现") 2 | @file:Depends("wayzer/user/ban", "禁封实现") 3 | @file:Depends("coreMindustry/utilTextInput", "输入理由") 4 | @file:Depends("coreMindustry/menu", "菜单选人") 5 | 6 | package wayzer.cmds 7 | 8 | import coreMindustry.PagedMenuBuilder 9 | import wayzer.VoteEvent 10 | 11 | fun CommandContext.readArg(): String? = arg.firstOrNull().also { arg = arg.drop(1) } 12 | 13 | suspend fun CommandContext.getTarget(): Player { 14 | val id = readArg() ?: player?.let { player -> 15 | var result: Player? = null 16 | PagedMenuBuilder(Groups.player.toList()) { 17 | option(it.name) { result = it } 18 | }.apply { 19 | title = "选择目标玩家" 20 | sendTo(player, 60_000) 21 | } 22 | if (result != null) return result!! 23 | null 24 | } ?: returnReply("[red]请输入玩家名/三位id".with()) 25 | if (id.startsWith("#")) 26 | Groups.player.getByID(id.substring(1).toIntOrNull() ?: 0)?.let { return it } 27 | //Try find by name 28 | val allPlayers = Groups.player.associateBy { it.name.replace(" ", "") } 29 | for (addLen in 0..arg.size) { 30 | val argAsName = id + arg.take(addLen).joinToString("") 31 | val found = allPlayers[argAsName] ?: continue 32 | arg = arg.drop(addLen) 33 | return found 34 | } 35 | //find by uuid 36 | return PlayerData.findByShortId(id)?.player 37 | ?: returnReply("[red]请输入正确的玩家名".with()) 38 | } 39 | 40 | val textInput = contextScript() 41 | suspend fun CommandContext.getInput(name: String, whenEmpty: VarString): String { 42 | return arg.takeIf { it.isNotEmpty() }?.joinToString(" ") 43 | ?: player?.let { p -> 44 | (textInput.textInput(p, "请在60s内输入$name") ?: returnReply("[yellow]已取消输入".with())) 45 | .takeIf { it.isNotBlank() } 46 | } ?: returnReply(whenEmpty) 47 | } 48 | 49 | val banImpl = contextScript() 50 | command("kick", "踢出某人".with(), commands = VoteEvent.VoteCommands) { 51 | aliases = listOf("踢出") 52 | usage = "<玩家名/id> <理由>" 53 | requirePermission("wayzer.vote.kick") 54 | body { 55 | val target = getTarget() 56 | val reason = getInput("踢人理由", "[red]投票踢人需要理由".with()) 57 | val player = player!! 58 | val event = VoteEvent( 59 | thisScript, player, 60 | voteDesc = "踢人(踢出[red]{target}[yellow])".with("target" to target), 61 | extDesc = "[red]理由: [yellow]${reason}" 62 | ) 63 | val snapshot = PlayerData[target] 64 | if (event.awaitResult()) { 65 | if (target.hasPermission("wayzer.admin.skipKick")) 66 | return@body broadcast( 67 | "[red]错误: {target.name}[red]为管理员, 如有问题请与服主联系".with("target" to target) 68 | ) 69 | banImpl.ban(snapshot, 60, "投票踢出: $reason", player) 70 | } 71 | } 72 | } 73 | 74 | PermissionApi.registerDefault("wayzer.admin.skipKick", group = "@admin") -------------------------------------------------------------------------------- /scripts/wayzer/module.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("coreMindustry") 2 | @file:Import("com.google.guava:guava:30.1-jre", mavenDepends = true) 3 | @file:Import("wayzer.lib.*", defaultImport = true) 4 | 5 | package wayzer 6 | 7 | import mindustry.net.Packets 8 | import mindustry.net.Packets.ConnectPacket 9 | 10 | name = "WayZer Mindustry Plugin" 11 | /** 12 | * 移植自 https://github.com/way-zer/MyMindustryPlugin 13 | * 功能: 14 | * (maps) better Maps,GameOver,ChangeMap | 更好的地图管理系统 15 | * (admin) independent Admin System | 独立的管理员系统 16 | * (playerInfo) extend variables for PlayerInfo | 扩展info相关变量 17 | * (permission) permission system | 权限系统 18 | * (voteProvider) provider for vote service | 投票服务实现 19 | * (user/profileBind) user token generate,check and user bind | 账号令牌生成,检测及用户绑定 20 | * (user/level) user exp/level system | 用户经验等级系统 21 | * (user/expReward) exp/time reward | 经验等级给与(若没有,用户经验和在线时间不会改变) 22 | * (user/achievement) user achievement system | 用户成就系统 23 | * (user/infoCommand) get profile info /info /mInfo(server) | /info指令(查看个人信息) 24 | * (user/statistics) statistics of one game and give exp after game | 游戏结束贡献榜及经验分发 25 | * (user/skills) skills based on command | 技能 26 | * (map/autoHost) autoHost after startup | 启动后自动开服 27 | * (map/autoSave) autoSave every 10 minutes | 自动保存(10分钟) 28 | * (map/mapInfo) show map info in game| 在游戏内显示地图信息 29 | * (map/limitAir) ext [@limitAir] for map| 提供地图[@limitAir]标签 30 | * (map/mapSnap) get current map snapshot| 游戏缩略图生成,保存到data/mapSnap下 31 | * (ext/vote) Vote System includes: changeMap gameOver skipWave kick rollBack | 投票系统(换图,投降,跳波,踢人,回滚) 32 | * (ext/welcomeMsg) join Welcome | 进服欢迎信息 33 | * (ext/alert) alert per interval | 定时轮播公告 34 | * (ext/betterTeam) better management for pvp team and support for observer| 更好的PVP队伍管理,管理员强制换队以及观察者支持 35 | * (ext/pvpProtect) pvp protect time | 开局pvp保护功能 36 | * (ext/serverStatus) /status | 获取服务器状态 37 | * (ext/autoUpdate) auto update server jar and restart after finish game | 自动升级服务端 38 | * (ext/lang) i18n | 国际化多语言支持,语言文件保存在scripts/data/lang 39 | * (ext/reGrief/history) get tile action history | 获取某个的操作记录 40 | * (ext/reGrief/unitLimit) limit units pre team | 限制每个队伍的单位总数 41 | * TODO: (ext/reGrief/reactor) 42 | * TODO: (ext/special/builderRobot) 43 | */ 44 | 45 | listenPacket2ServerAsync { con, packet -> 46 | if (con.address.startsWith("steam:")) { 47 | packet.uuid = con.address.substring("steam:".length) 48 | } 49 | 50 | if (Groups.player.any { pp -> pp.uuid() == packet.uuid }) { 51 | con.kick(Packets.KickReason.idInUse) 52 | return@listenPacket2ServerAsync false 53 | } 54 | val event = ConnectAsyncEvent(con, packet).emitAsync() 55 | if (event.cancelled && !con.kicked) 56 | con.kick("[red]拒绝入服: ${event.reason}") 57 | !event.cancelled 58 | } 59 | 60 | listenTo { 61 | val player = this.subject as? Player ?: return@listenTo 62 | PlayerData[player].ids.let { 63 | group = it.toList() + group 64 | } 65 | } 66 | 67 | listen { 68 | PlayerData.onLeave(it.player) 69 | } -------------------------------------------------------------------------------- /scripts/wayzer/cmds/vote.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/vote", "投票实现") 2 | 3 | package wayzer.cmds 4 | 5 | import arc.Events 6 | import arc.util.Time 7 | import mindustry.game.EventType.GameOverEvent 8 | import wayzer.VoteService 9 | import java.time.Instant 10 | import kotlin.math.ceil 11 | import kotlin.math.max 12 | import kotlin.random.Random 13 | 14 | fun VoteService.register() { 15 | addSubVote("投降或结束该局游戏,进行结算", "", "gameOver", "投降", "结算") { 16 | if (!state.rules.canGameOver) 17 | returnReply("[red]当前地图不允许投降".with()) 18 | if (state.rules.pvp) { 19 | val team = player!!.team() 20 | if (!state.teams.isActive(team) || state.teams.get(team)!!.cores.isEmpty) 21 | returnReply("[red]队伍已输,无需投降".with()) 22 | 23 | start( 24 | player!!, "投降({team.colorizeName}[yellow]队|要求80%同意)".with("player" to player!!, "team" to team), 25 | canVote = { it.team() == team }, requireNum = { ceil(it * 0.8).toInt() } 26 | ) { 27 | team.data().cores.toArray().forEach { 28 | if (it.team == team) it.kill() 29 | } 30 | } 31 | return@addSubVote 32 | } 33 | start(player!!, "投降".with(), supportSingle = true) { 34 | player!!.team().cores().toArray().forEach { Time.run(Random.nextFloat() * 60 * 3, it::kill) } 35 | Events.fire(GameOverEvent(state.rules.waveTeam)) 36 | } 37 | } 38 | addSubVote("快速出波(默认10波,最高50)", "[波数]", "skipWave", "跳波") { 39 | if (Groups.player.any { it.team() == state.rules.waveTeam }) 40 | returnReply("[red]当前模式禁止跳波".with()) 41 | val lastResetTime by PlaceHold.reference("state.startTime") 42 | val t = (arg.firstOrNull()?.toIntOrNull() ?: 10).coerceIn(1, 50) 43 | start(player!!, "跳波({t}波)".with("t" to t), supportSingle = true) { 44 | val startTime = Instant.now() 45 | repeat(t) { 46 | if (lastResetTime > startTime) return@start //Have change map 47 | val before = state.enemies 48 | logic.runWave() 49 | while (spawner.isSpawning) delay(1000L) 50 | val after = state.enemies 51 | while (state.enemies > max(before, (after - before) * 3 / 10)) { 52 | delay(1000L) 53 | } 54 | delay(3000L) 55 | } 56 | } 57 | } 58 | addSubVote("清理本队建筑记录", "", "clear", "清理", "清理记录") { 59 | val team = player!!.team() 60 | start( 61 | player!!, "清理建筑记录({team.colorizeName}[yellow]队|需要2/5同意)".with("team" to team), 62 | canVote = { it.team() == team }, requireNum = { ceil(it * 0.4).toInt() } 63 | ) { 64 | team.data().plans.clear() 65 | } 66 | } 67 | addSubVote("自定义投票", "<内容>", "text", "文本", "t") { 68 | if (arg.isEmpty()) returnReply("[red]请输入投票内容".with()) 69 | start(player!!, "自定义([green]{text}[yellow])".with("text" to arg.joinToString(" "))) {} 70 | } 71 | } 72 | 73 | onEnable { 74 | VoteService.register() 75 | } -------------------------------------------------------------------------------- /scripts/wayzer/reGrief/limitLogicPacket.kts: -------------------------------------------------------------------------------- 1 | package wayzer.reGrief 2 | 3 | import arc.struct.IntSet 4 | import arc.util.Interval 5 | import arc.util.Time 6 | import mindustry.core.NetServer 7 | import mindustry.world.blocks.logic.LogicBlock 8 | import mindustryX.events.SendPacketEvent 9 | 10 | try { 11 | LogicBlock::class.java.getDeclaredField("running") 12 | } catch (e: Exception) { 13 | error("本脚本依赖MindustryX v143.102 或更新版本") 14 | } 15 | 16 | //带滑动平均的速率统计 17 | class RateKeeper(val timeWindow: Double = 5.0) { 18 | private var lastTime = Time.time 19 | var avgCount = 0.0 20 | fun count(count: Int = 1) { 21 | avgCount += count / timeWindow 22 | val delta = Time.time - lastTime 23 | if (delta > Time.toSeconds) { 24 | avgCount *= (1 - delta / timeWindow * Time.toSeconds).coerceAtLeast(0.0) 25 | lastTime = Time.time 26 | } 27 | } 28 | } 29 | 30 | val list = mutableListOf>() 31 | val interval = Interval() 32 | val packetRate = RateKeeper() 33 | listen { event -> 34 | if (event.con == null && LogicBlock.running && state.isGame) { 35 | packetRate.count() 36 | list.add(event.packet::class.java) 37 | if (packetRate.avgCount > 2000) { 38 | if (state.rules.disableWorldProcessors) return@listen 39 | state.rules.disableWorldProcessors = true 40 | broadcast("[red]世界处理器发包严重,禁用世界处理器。请联系地图地图作者优化.".with()) 41 | } else if (interval.get(30 * 60f)) { 42 | if (packetRate.avgCount > 1000) { 43 | val list = list.groupBy { it } 44 | .map { it.key.simpleName to it.value.size } 45 | .sortedByDescending { it.second } 46 | broadcast( 47 | "[red]世界处理器发包超限, 未来将被禁用,请联系地图地图作者优化。当前值: {avg}/s\n{list|joinLines}".with( 48 | "avg" to "%.2f".format(packetRate.avgCount), "list" to list 49 | ) 50 | ) 51 | } 52 | list.clear() 53 | } 54 | } 55 | } 56 | 57 | val healthChanged = RateKeeper(3.0) 58 | val NetServer.buildHealthChanged: IntSet by reflectDelegate() 59 | listen(EventType.Trigger.update) { 60 | if (!state.isPlaying) return@listen 61 | healthChanged.count(netServer.buildHealthChanged.size) 62 | if (healthChanged.avgCount > 100_000) { 63 | if (state.rules.disableWorldProcessors) return@listen 64 | state.rules.disableWorldProcessors = true 65 | broadcast("[red]世界处理器发包严重,禁用世界处理器。请联系地图地图作者优化.".with()) 66 | broadcast("BuildHealthChanged ${healthChanged.avgCount.toInt()}/s".asPlaceHoldString()) 67 | } 68 | } 69 | command("debugLogicPacket", "查看逻辑发包速率") { 70 | permission = dotId 71 | body { 72 | val list = list.groupBy { it } 73 | .map { it.key to it.value.size } 74 | .sortedByDescending { it.second } 75 | reply( 76 | """ 77 | 血量变化: ${healthChanged.avgCount.toInt()}/s 78 | 当前发包速率: ${packetRate.avgCount.toInt()}/s 79 | ${list} 80 | """.trimIndent().asPlaceHoldString() 81 | ) 82 | } 83 | } -------------------------------------------------------------------------------- /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/mapScript/13545.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("coreMindustry/menu", "调用菜单") 2 | @file:Depends("coreMindustry/util/spawnAround") 3 | 4 | /**@author WayZer*/ 5 | 6 | package mapScript 7 | 8 | import arc.util.Align 9 | import coreLibrary.lib.util.loop 10 | import mindustry.game.Team 11 | import mindustry.gen.Iconc 12 | import mindustry.net.Administration 13 | import mindustry.world.blocks.storage.CoreBlock 14 | import org.intellij.lang.annotations.Language 15 | 16 | modeIntroduce( 17 | "招兵买马 CoreWar", """ 18 | 点击核心可以打开菜单 19 | 使用铜为队伍购买单位和属性升级 20 | 价格会随购买逐渐变贵 21 | Tip1: 不要乱花钱被队友嫌弃哦 22 | Tip2: 单位会随机刷在核心附近(5格左右),周围没水船会白给 23 | """.trimIndent() 24 | ) 25 | 26 | @Language("JSON5") 27 | val patch = """ 28 | { 29 | "name": "CoreWar", 30 | "block.core-foundation.unitType": "alpha", 31 | "block.core-nucleus.unitType": "alpha", 32 | "block.core-nucleus.itemCapacity": 1000000, 33 | } 34 | """.trimIndent() 35 | mapPatches = listOf(patch) 36 | 37 | data class TeamData(val team: Team) { 38 | var blockDamageMultiplier by team.rules()::blockDamageMultiplier 39 | var blockHealthMultiplier by team.rules()::blockHealthMultiplier 40 | var unitDamageMultiplier by team.rules()::unitDamageMultiplier 41 | var unitHealthMultiplier by team.rules()::unitHealthMultiplier 42 | } 43 | 44 | val teamData = mutableMapOf() 45 | val Team.myData get() = teamData.getOrPut(this) { TeamData(this) } 46 | onDisable { teamData.clear() } 47 | 48 | registerActionFilter { 49 | if(it.type == Administration.ActionType.control || it.type==Administration.ActionType.command){ 50 | if(it.unit.type == UnitTypes.mono) 51 | return@registerActionFilter false 52 | } 53 | true 54 | } 55 | 56 | listen { 57 | (it.tile.build as? CoreBlock.CoreBuild)?.let { core -> 58 | val player = it.player 59 | if (!player.dead() && core.team == player.team()) 60 | launch(Dispatchers.game) { 61 | CoreWarMenu(player, core).sendTo(player, 60_000) 62 | } 63 | } 64 | } 65 | 66 | onEnable { 67 | state.rules.bannedBlocks.add(Blocks.deconstructor) 68 | Call.setRules(state.rules) 69 | loop(Dispatchers.game) { 70 | delay(2000) 71 | val teams = Groups.player.mapTo(mutableSetOf()) { it.team() } 72 | teams.removeAll { !it.active() } 73 | val text = "[green]点击核心可以打开升级菜单\n" + 74 | "[yellow]更新:所有单位价格不再增长\n" + 75 | teams.sortedByDescending { it.myData.unitDamageMultiplier }.take(5) 76 | .joinToString("\n") { team -> 77 | "[#${team.color}]${team.name}[white]属性:" + 78 | "${Iconc.modePvp}${team.myData.unitDamageMultiplier} " + 79 | "${team.myData.unitHealthMultiplier} " + 80 | "${Iconc.turret}${team.myData.blockDamageMultiplier} " + 81 | "${Iconc.defense}${team.myData.blockHealthMultiplier} " 82 | } 83 | Call.infoPopup( 84 | text, 2.013f, 85 | Align.topLeft, 350, 0, 0, 0 86 | ) 87 | } 88 | } -------------------------------------------------------------------------------- /scripts/wayzer/reGrief/unitLimit.kts: -------------------------------------------------------------------------------- 1 | package wayzer.reGrief 2 | 3 | import arc.Events 4 | import arc.util.Interval 5 | import mindustry.gen.BuildingTetherc 6 | import mindustry.gen.TimedKillc 7 | import mindustry.gen.Unit 8 | import kotlin.math.min 9 | 10 | val unitToWarn by config.key(190, "开始警告的单位数") 11 | val unitToKill by config.key(220, "单位数上限,禁止产生新的") 12 | 13 | val interval = Interval(1) 14 | listen { e -> 15 | if (e.unit.team == state.rules.waveTeam && state.rules.waves && state.rules.defaultTeam != state.rules.waveTeam) 16 | return@listen 17 | fun alert(text: PlaceHoldString) { 18 | if (interval[0, 2 * 60f]) {//2s cooldown 19 | broadcast(text, MsgType.InfoToast, 4f, true, e.unit.team.data().players) 20 | } 21 | } 22 | 23 | val count = e.unit.team.data().unitCount 24 | when { 25 | count >= unitToKill -> { 26 | launch(Dispatchers.gamePost) { 27 | val toKill = count - unitToKill 28 | val m = Groups.unit.filter { it.team == e.unit.team && it.maxHealth < 1000f && !it.isPlayer } 29 | .filterNot { it is TimedKillc || it is BuildingTetherc } 30 | .sortedBy { it.health } 31 | if (m.isNotEmpty()) 32 | alert("[red]警告: 单位过多,可能造成服务器卡顿,随机杀死低级单位".with("count" to count)) 33 | repeat(min(toKill, m.size)) { 34 | m[it].kill() 35 | } 36 | if (toKill > m.size) { 37 | alert("[red]警告: 单位过多,可能造成服务器卡顿,已禁止生成".with("count" to count)) 38 | e.unit.kill() 39 | } 40 | } 41 | } 42 | 43 | count >= unitToWarn -> { 44 | alert("[yellow]警告: 建筑过多单位,可能造成服务器卡顿,当前: {count}".with("count" to count)) 45 | } 46 | } 47 | } 48 | 49 | listen { e -> 50 | if (e.unit.team.data().unitCount > 5000 && !state.gameOver) { 51 | broadcast("[red]敌方单位超过5000,自动投降".with()) 52 | state.gameOver = true 53 | Events.fire(EventType.GameOverEvent(state.rules.waveTeam)) 54 | Core.app.post { 55 | Groups.unit.filter { it.team == state.rules.waveTeam }.forEach(Unit::kill) 56 | } 57 | } 58 | } 59 | 60 | var gameOverWave = -1 61 | listen { gameOverWave = -1 } 62 | fun checkNextWave() { 63 | val flySpawn = spawner.countFlyerSpawns() 64 | val groundSpawn = spawner.countGroundSpawns() 65 | val sum = state.rules.spawns.sum { 66 | it.getSpawned(state.wave) * when { 67 | it.spawn != -1 -> 1 68 | it.type.flying -> flySpawn 69 | else -> groundSpawn 70 | } 71 | } 72 | if (sum >= 3000) { 73 | state.rules.spawns.clear() 74 | gameOverWave = state.wave 75 | } 76 | } 77 | listen { checkNextWave() } 78 | listen { 79 | if (gameOverWave > 0 && state.wave > gameOverWave && !state.gameOver) { 80 | broadcast("[red]到达终结波,自动投降".with()) 81 | state.gameOver = true 82 | Events.fire(EventType.GameOverEvent(state.rules.waveTeam)) 83 | return@listen 84 | } 85 | checkNextWave() 86 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/wayzer/ext/observer.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("wayzer/map/betterTeam") 2 | @file:Depends("coreMindustry/menu") 3 | 4 | package wayzer.ext 5 | 6 | import cf.wayzer.placehold.DynamicVar 7 | import coreMindustry.MenuBuilder 8 | import mindustry.game.Team 9 | import mindustry.gen.PlayerSpawnCallPacket 10 | import mindustry.world.blocks.storage.CoreBlock 11 | 12 | val teams = contextScript() 13 | 14 | @Savable(serializable = false) 15 | val obTeam = mutableMapOf() 16 | customLoad(::obTeam) { 17 | obTeam.putAll(it.filterKeys { it.con != null }) 18 | } 19 | fun getObTeam(player: Player): Team? = obTeam[player]?.takeIf { it != player.team() } 20 | export(::getObTeam) 21 | listen { obTeam.clear() } 22 | listen { obTeam.remove(it.player) } 23 | listenPacket2Server { con, _ -> con.player !in obTeam } 24 | listen { 25 | if (it.tile.build is CoreBlock.CoreBuild && it.player in obTeam) { 26 | val team = it.tile.team() 27 | if (obTeam[it.player] == team) return@listen 28 | obTeam[it.player] = team 29 | broadcast( 30 | "[yellow]玩家[green]{player.name}[yellow]正在观战{team}" 31 | .with("player" to it.player, "team" to team), type = MsgType.InfoToast, quite = true 32 | ) 33 | } 34 | } 35 | registerVarForType().apply { 36 | registerChild("prefix.3obTeam", "观战队伍显示") { 37 | getObTeam(it)?.let { team -> "[观战${team.coloredName()}]" } 38 | } 39 | } 40 | fun setObTeam(player: Player, team: Team?) { 41 | if (team == null) { 42 | teams.changeTeam(player, teams.spectateTeam) 43 | obTeam.remove(player) 44 | teams.changeTeam(player) 45 | broadcast( 46 | "[yellow]玩家[green]{player.name}[yellow]重新投胎到{player.team.colorizeName}" 47 | .with("player" to player), type = MsgType.InfoToast, quite = true 48 | ) 49 | return 50 | } 51 | 52 | teams.changeTeam(player, teams.spectateTeam) 53 | obTeam[player] = team 54 | broadcast( 55 | "[yellow]玩家[green]{player.name}[yellow]正在观战{team}" 56 | .with("player" to player, "team" to team), type = MsgType.InfoToast, quite = true 57 | ) 58 | player.sendMessage("[green]再次输入指令可以重新投胎。点击核心可以快速切换观战队伍") 59 | } 60 | 61 | command("ob", "切换为观察者") { 62 | type = CommandType.Client 63 | permission = "wayzer.ext.observer" 64 | body { 65 | val player = player!! 66 | val team = arg.firstOrNull()?.toIntOrNull()?.let { Team.all.getOrNull(it) } 67 | if (team != null) { 68 | setObTeam(player, team.takeUnless { it == Team.derelict }) 69 | return@body 70 | } 71 | MenuBuilder { 72 | title = "观战系统" 73 | msg = "By [gold]WayZer\n选择队伍观战" 74 | teams.allTeam.forEach { 75 | option(it.coloredName()) { setObTeam(player, it) } 76 | newRow() 77 | } 78 | option("退出观战/重新投胎") { setObTeam(player, null) } 79 | newRow() 80 | option("关闭菜单") { } 81 | }.sendTo(player) 82 | } 83 | } 84 | 85 | PermissionApi.registerDefault("wayzer.ext.observer") 86 | 87 | listen { 88 | world.tiles.iterator().forEach { 89 | if (it.team().id == 255) it.setAir() 90 | } 91 | } -------------------------------------------------------------------------------- /scripts/wayzer/map/mapSnap.kts: -------------------------------------------------------------------------------- 1 | package wayzer.map 2 | 3 | import mindustry.core.ContentLoader 4 | import mindustry.core.World 5 | import mindustry.world.Tile 6 | import java.awt.image.BufferedImage 7 | import java.text.SimpleDateFormat 8 | import java.util.* 9 | import javax.imageio.ImageIO 10 | 11 | //参考 mindustry.graphics.MinimapRenderer 12 | object MapRenderer { 13 | var img: BufferedImage? = null 14 | fun drawAll(world: World) { 15 | img = BufferedImage(world.width(), world.height(), BufferedImage.TYPE_INT_ARGB).apply { 16 | repeat(world.width()) { x -> 17 | repeat(world.height()) { y -> 18 | setRGB(x, height - 1 - y, getARGB(world.tile(x, y))) 19 | } 20 | } 21 | } 22 | } 23 | 24 | fun update(tile: Tile) { 25 | img?.apply { 26 | setRGB(tile.x.toInt(), height - 1 - tile.y, getARGB(tile)) 27 | } 28 | } 29 | 30 | //参考 mindustry.core.ContentLoader.loadColors 31 | fun loadColors(content: ContentLoader) { 32 | if (content.blocks().isEmpty) return 33 | val logger = thisContextScript().logger 34 | val img = javaClass.getResourceAsStream("/block_colors.png")?.use { ImageIO.read(it) } 35 | ?: return logger.warning("找不到图集 block_colors.png") 36 | repeat(img.width) { i -> 37 | val color = img.getRGB(i, 0) 38 | if (color != 0 && color != 255) { 39 | content.block(i).apply { 40 | mapColor.argb8888(color) 41 | squareSprite = mapColor.a > 0.5f 42 | mapColor.a = 1.0f 43 | hasColor = true 44 | } 45 | } 46 | } 47 | logger.info("加载方块颜色集成功") 48 | } 49 | 50 | private fun getARGB(tile: Tile?): Int { 51 | val rgba = getRGBA(tile) 52 | val a = rgba and 255 53 | val rgb = rgba ushr 8 54 | return (a shl 24) + rgb 55 | } 56 | 57 | private fun getRGBA(tile: Tile?): Int { 58 | return when { 59 | tile == null -> 0 60 | tile.block().minimapColor(tile) != 0 -> tile.block().minimapColor(tile) 61 | tile.block().synthetic() -> tile.team().color.rgba() 62 | tile.block().solid -> tile.block().mapColor.rgba() 63 | tile.overlay() != Blocks.air -> tile.overlay().mapColor.rgba() 64 | else -> tile.floor().mapColor.rgba() 65 | } 66 | } 67 | } 68 | listen { 69 | MapRenderer.drawAll(world) 70 | } 71 | listen { 72 | it.tile.getLinkedTiles(MapRenderer::update) 73 | } 74 | listen { 75 | MapRenderer.img = null 76 | } 77 | onEnable { 78 | MapRenderer.loadColors(content) 79 | if (net.server()) 80 | MapRenderer.drawAll(world) 81 | } 82 | registerVar("wayzer.ext.mapSnap._get", "地图快照截图接口", { MapRenderer.img }) 83 | 84 | command("saveSnap", "保存当前服务器地图截图") { 85 | type = CommandType.Server 86 | body { 87 | val img = MapRenderer.img ?: returnReply("[red]地图未加载".with()) 88 | val dir = dataDirectory.child("mapSnap").apply { mkdirs() } 89 | val file = dir.child("mapSnap-${SimpleDateFormat("YYYYMMdd-hhmm").format(Date())}.png") 90 | file.write().use { ImageIO.write(img, "png", it) } 91 | reply("[green]快照已保存到{file}".with("file" to file)) 92 | } 93 | } -------------------------------------------------------------------------------- /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/wayzer/map/mapInfo.kts: -------------------------------------------------------------------------------- 1 | package wayzer.map 2 | 3 | @Savable(false) 4 | val customModeIntroduce = mutableListOf() 5 | customLoad(::customModeIntroduce, customModeIntroduce::addAll) 6 | 7 | fun addModeIntroduce(mode: String, introduce: String) { 8 | if (customModeIntroduce.any { it.contains("===[gold]$mode[]===") }) return 9 | customModeIntroduce += "[magenta]===[gold]$mode[]===[white]\n$introduce" 10 | } 11 | export(this::addModeIntroduce) 12 | listen { customModeIntroduce.clear() } 13 | 14 | registerVar("scoreboard.ext.customMode", "自定义模式Tip", DynamicVar { 15 | if (customModeIntroduce.isEmpty()) return@DynamicVar null 16 | "{cK}本地图有自定义模式,详情使用{cV}/mapInfo[]查看".with() 17 | }) 18 | 19 | 20 | fun Player.showDetail() { 21 | val pages = (listOf(buildString { 22 | appendLine("[white]${state.map.name()}") 23 | appendLine() 24 | appendLine("[purple]By: [scarlet]${state.map.author()}") 25 | appendLine("[white]${state.map.description()}") 26 | }) + customModeIntroduce).autoPage() 27 | for (page in pages.size downTo 1) { 28 | sendMessage( 29 | "[green]==== [white]地图信息[] ====\n{body}\n[green]==== [white]{page}/{total}[] ====" 30 | .with("body" to pages[page - 1], "page" to page, "total" to pages.size), type = MsgType.InfoMessage 31 | ) 32 | } 33 | } 34 | 35 | fun Player.showInfo() { 36 | if (con == null) return 37 | val desc = state.map.description().autoWrapLine() 38 | val msg = buildString { 39 | appendLine("[white]${state.map.name()}") 40 | appendLine() 41 | appendLine("[purple]By: [scarlet]${state.map.author()}") 42 | appendLine("[white]$desc") 43 | if (customModeIntroduce.isNotEmpty()) { 44 | appendLine() 45 | appendLine("[yellow]本地图共有${customModeIntroduce.size}个特殊模式") 46 | appendLine("具体请使用[gold]/mapinfo[]查看") 47 | } 48 | } 49 | Call.label(con, msg, 2 * 60f, core()?.x ?: 0f, core()?.y ?: 0f) 50 | } 51 | 52 | listen { 53 | launch(Dispatchers.gamePost) { 54 | Groups.player.forEach { 55 | it.showInfo() 56 | } 57 | } 58 | } 59 | 60 | listen { e -> 61 | Core.app.post { e.player.showInfo() } 62 | } 63 | 64 | command("mapInfo", "地图详情") { 65 | type = CommandType.Client 66 | body { 67 | player!!.showDetail() 68 | } 69 | } 70 | 71 | //region util 72 | fun Iterable.autoPage(lines: Int = 10): List { 73 | val out = mutableListOf() 74 | var acc = "" 75 | fun add() { 76 | if (acc.isBlank()) return 77 | out.add(acc) 78 | acc = "" 79 | } 80 | for (text in this) { 81 | acc += text 82 | if (acc.lineSequence().count() > lines) add() 83 | } 84 | add() 85 | return out 86 | } 87 | 88 | fun String.autoWrapLine(limit: Int = 25): String { 89 | var lastChar = ' ' 90 | var i = 0 91 | return map { 92 | if (i > limit && it in charArrayOf(' ', ',', ',', '.', '。', '!', '!')) 93 | if (it != '.' || lastChar.code !in '0'.code..'9'.code) { 94 | i = 0 95 | lastChar = it 96 | return@map '\n' 97 | } 98 | lastChar = it 99 | i++ 100 | return@map it 101 | }.joinToString("") 102 | } 103 | //endregion -------------------------------------------------------------------------------- /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/mapScript/tags/towerDefend.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("mapScript/tags/TDDrop", "掉落", soft = true) 2 | 3 | package mapScript.tags 4 | 5 | import arc.math.geom.Geometry 6 | import mindustry.net.Administration 7 | import mindustry.world.blocks.ConstructBlock 8 | import mindustry.world.blocks.environment.Floor 9 | 10 | /** 塔防模式 11 | * @author WayZer 12 | * 如果有修改建议,建议提交PR,维护社区统一。 13 | * */ 14 | 15 | registerMapTag("@towerDefend") 16 | modeIntroduce( 17 | "塔防模式", """ 18 | 1.怪物只会攻击核心,可以放心建筑 19 | 2.过于靠近怪物的兵可能会被杀掉 20 | 3.道路上不准建筑(原因由上) 21 | 4.核心附加的道路允许放置武装传送带 22 | 5.怪物会掉落大量战利品(不限距离) 23 | 6.部分物品被禁用(保证平衡) 24 | """.trimIndent() 25 | ) 26 | 27 | val allowBlocks = arrayOf(Blocks.armoredConveyor, Blocks.plastaniumConveyor) 28 | val floors = mutableSetOf() 29 | onEnable { 30 | state.rules.bannedBlocks.takeIf { it.isEmpty }?.apply { 31 | add(Blocks.arc) 32 | add(Blocks.lancer) 33 | add(Blocks.airFactory) 34 | add(Blocks.mendProjector) 35 | } 36 | for (tile in spawner.spawns) { 37 | Geometry.circle(0, 0, 4) { dx, dy -> 38 | floors += tile.nearby(dx, dy)?.floor() ?: return@circle 39 | } 40 | } 41 | } 42 | onDisable { floors.clear() } 43 | 44 | //build 45 | 46 | registerActionFilter { 47 | when (it.type) { 48 | Administration.ActionType.placeBlock -> { 49 | if (it.block in allowBlocks && it.player.team().cores().any { core -> core.dst(it.tile) < 80 }) { 50 | return@registerActionFilter true //允许在核心附近建武装传送带 51 | } 52 | (it.tile.floor() !in floors).also { b -> 53 | if (!b) it.player.sendMessage("[red]你不能在此处建造".with(), MsgType.InfoToast, 3f) 54 | } 55 | } 56 | 57 | Administration.ActionType.configure -> { 58 | when { 59 | it.tile.block() == Blocks.itemSource || it.tile.block() == Blocks.liquidSource -> { 60 | it.player.sendMessage("[red]不允许修改源".with(), MsgType.InfoToast, 3f) 61 | return@registerActionFilter false 62 | } 63 | } 64 | true 65 | } 66 | 67 | else -> true 68 | } 69 | } 70 | listen { 71 | if (it.breaking) return@listen 72 | if (it.tile.floor() in floors) { 73 | if (it.unit.isPlayer) { 74 | return@listen//limited by filter 75 | } 76 | it.tile.remove() 77 | } 78 | } 79 | listen { 80 | val tile = it.tile 81 | if (tile.block() == Blocks.air || tile.block() in allowBlocks || tile.floor() !in floors) return@listen 82 | val building = tile.build 83 | if (building is ConstructBlock.ConstructBuild && building.current in allowBlocks) 84 | return@listen 85 | launch(Dispatchers.gamePost) { 86 | Call.deconstructFinish(tile, Blocks.air, null) 87 | } 88 | } 89 | 90 | //unit 91 | 92 | val specialFlag = 1024.0//use for identify unit spawned from factory 93 | listen { 94 | if (it.unit.team == state.rules.waveTeam) 95 | it.unit.flag = specialFlag 96 | } 97 | listen(EventType.Trigger.update) { 98 | val units = state.rules.waveTeam.data().units 99 | units.forEach { 100 | if (it.flag == specialFlag) return@forEach 101 | if (it.controller() !is TowerDefendAI) 102 | it.controller(TowerDefendAI(floors)) 103 | } 104 | } -------------------------------------------------------------------------------- /scripts/wayzer/cmds/pixelPicture.kts: -------------------------------------------------------------------------------- 1 | package wayzer.cmds 2 | 3 | import arc.graphics.Color 4 | import mindustry.game.Team 5 | import mindustry.type.Item 6 | import java.awt.Image 7 | import java.awt.image.BufferedImage 8 | import java.net.URL 9 | import javax.imageio.ImageIO 10 | 11 | //WayZer 版权所有(禁止删除版权声明) 12 | 13 | fun draw(x: Int, y: Int, rgb: Int) { 14 | fun getClosestColor(color: Color): Item { 15 | fun pow2(x: Float) = x * x 16 | return content.items().min { item -> 17 | with(item.color) { 18 | pow2(r - color.r) + pow2(g - color.g) + pow2(b - color.b) 19 | } 20 | } 21 | } 22 | 23 | fun argb8888ToColor(value: Int): Color { 24 | val color = Color() 25 | color.a = (value and -16777216 ushr 24).toFloat() / 255.0f 26 | color.r = (value and 16711680 ushr 16).toFloat() / 255.0f 27 | color.g = (value and '\uff00'.code ushr 8).toFloat() / 255.0f 28 | color.b = (value and 255).toFloat() / 255.0f 29 | return color 30 | } 31 | 32 | val color = argb8888ToColor(rgb) 33 | if (color.a < 0.001) return //Pass alpha pixel 34 | val closest = getClosestColor(color) 35 | //debug 36 | // p.sendMessage("Closest color for [#$color]$color is: [#${closest.color}]${closest.color}") 37 | world.tiles.getc(x, y).apply { 38 | if (block() != Blocks.air && block() != Blocks.sorter) return@apply 39 | setNet(Blocks.sorter, Team.crux, 0) 40 | Call.tileConfig(null, build, closest) 41 | } 42 | } 43 | 44 | fun BufferedImage.resize(maxSize: Int): BufferedImage { 45 | var scale = 1 46 | while (height / scale > maxSize || width / scale > maxSize) scale++ 47 | val height = height / scale 48 | val width = width / scale 49 | val scaled = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) 50 | scaled.createGraphics().apply { 51 | drawImage(getScaledInstance(width, height, Image.SCALE_REPLICATE), 0, 0, null) 52 | } 53 | return scaled 54 | } 55 | 56 | command("pixel", "绘制像素画".with()) { 57 | usage = "[size=32] " 58 | type = CommandType.Client 59 | permission = id.replace("/", ".") 60 | aliases = listOf("像素画") 61 | body { 62 | if (arg.isEmpty()) replyUsage() 63 | val size = arg.firstOrNull()?.toIntOrNull() ?: 32 64 | val url = kotlin.runCatching { URL(arg.last()) }.getOrElse { 65 | returnReply("[red]错误的URL: {error}".with("error" to it.message.orEmpty())) 66 | } 67 | launch { 68 | reply("[yellow]准备开始绘制".with()) 69 | var img = withContext(Dispatchers.IO) { ImageIO.read(url) } 70 | reply("[yellow]原始图片尺寸{w}x{h}".with("w" to img.width, "h" to img.height)) 71 | img = withContext(Dispatchers.Default) { img.resize(size) } 72 | reply("[yellow]缩放后比例{w}x{h}".with("w" to img.width, "h" to img.height)) 73 | withContext(Dispatchers.game) { 74 | var i = 0 75 | val p = player!! 76 | for (x in 1..img.width) 77 | for (y in 1..img.height) { 78 | i++ 79 | draw(p.tileX() - img.width / 2 + x, p.tileY() + img.height / 2 - y, img.getRGB(x - 1, y - 1)) 80 | if (i > 10) { 81 | i = 0 82 | nextTick() 83 | } 84 | } 85 | } 86 | reply("[green]绘制完成".with()) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /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/mapScript/1002.kts: -------------------------------------------------------------------------------- 1 | @file:Depends("mapScript/shared/hexed") 2 | @file:Depends("mapScript/tags/autoExchange", "等价交换", soft = true) 3 | 4 | package mapScript 5 | 6 | import arc.math.geom.Geometry 7 | import mapScript.shared.GeneratorHelper 8 | import mapScript.shared.HexData 9 | import mapScript.shared.HexedGenerator 10 | import mindustry.game.Gamemode 11 | import mindustry.type.ItemStack 12 | import mindustry.world.blocks.environment.Floor 13 | import kotlin.time.Duration.Companion.minutes 14 | 15 | /** @author WayZer */ 16 | 17 | val generator = HexedGenerator(4, 5, 144, 34) 18 | registerGenerator( 19 | "HEXed PVP 超大区块", "WayZer", """插件控制的随机pvp图[@pvpProtect=480]""", 20 | mode = Gamemode.pvp, 21 | filter = setOf("all", "display", "pvp", "hexed"), 22 | width = generator.width, height = generator.height 23 | ) { 24 | rules.apply { 25 | generator.applyRules(this) 26 | loadout = ItemStack.list( 27 | Items.copper, 500, 28 | Items.lead, 500, 29 | Items.silicon, 200, 30 | Items.plastanium, 50 31 | ) 32 | enemyCoreBuildRadius = 65f * tilesize 33 | } 34 | genRound("topography") { GeneratorHelper.genTopography(it) } 35 | genRound("genHex", generator::genHex) 36 | genRound("genPath", generator::genPath) 37 | genRound("ores", GeneratorHelper::genOres) 38 | genRound("baseResource") { 39 | generator.chunkCenters.forEach { chunk -> 40 | arrayOf(-20, 20).forEach { dx -> 41 | Geometry.circle(chunk.x + dx, chunk.y, it.width, it.height, 3) { x, y -> 42 | it[x, y].setFloor(Blocks.sandWater as Floor) 43 | } 44 | } 45 | Geometry.circle(chunk.x, chunk.y, it.width, it.height, 15) { x, y -> 46 | it[x, y].setFloor(Blocks.sand as Floor) 47 | } 48 | } 49 | } 50 | genRound("genRandomStone", GeneratorHelper::genRandomStone) 51 | genRound("initHexData") { HexData.init(generator.chunkCenters) } 52 | } 53 | 54 | 55 | onEnable { 56 | HexData.extraLoadout.add { 57 | val tileSize = tilesize.toFloat() 58 | repeat(2) { 59 | UnitTypes.mono.spawn(controller, x * tileSize, y * tileSize).apply { 60 | armor = 200f 61 | } 62 | } 63 | } 64 | 65 | schedule(20.minutes) { 66 | HexData.extraLoadout.add { 67 | val tileSize = tilesize.toFloat() 68 | repeat(4) {//all 6 mono 69 | UnitTypes.mono.spawn(controller, x * tileSize, y * tileSize).apply { 70 | armor = 200f 71 | } 72 | } 73 | repeat(2) { 74 | UnitTypes.poly.spawn(controller, x * tileSize, y * tileSize).apply { 75 | armor = 200f 76 | } 77 | } 78 | } 79 | } 80 | schedule(40.minutes) { 81 | HexData.extraLoadout.add { 82 | val tileSize = tilesize.toFloat() 83 | repeat(2) { 84 | UnitTypes.vela.spawn(controller, x * tileSize, y * tileSize).apply { 85 | armor = 100f 86 | } 87 | } 88 | } 89 | } 90 | schedule(60.minutes) { 91 | HexData.extraLoadout.add { 92 | val items = coreTile.build?.items ?: return@add 93 | content.items().toMutableSet().apply { 94 | removeAll(setOf(Items.blastCompound, Items.surgeAlloy, Items.surgeAlloy, Items.phaseFabric)) 95 | }.forEach { items.set(it, coreTile.block().itemCapacity) } 96 | } 97 | } 98 | } --------------------------------------------------------------------------------