├── .github ├── release-drafter.yml └── workflows │ ├── build.yml │ ├── changeLog.yml │ └── release.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── jarRepositories.xml ├── kotlinc.xml ├── markdown.xml ├── misc.xml ├── modules │ ├── MindustryContentsTweaker.test.iml │ ├── contents │ │ ├── MindustryContents.contents.test.iml │ │ ├── flood │ │ │ └── MindustryContents.contents.flood.test.iml │ │ └── origin │ │ │ └── MindustryContents.contents.origin.test.iml │ ├── core │ │ └── MindustryContents.core.test.iml │ └── loaderMod │ │ └── MindustryContents.loaderMod.test.iml └── vcs.xml ├── README.md ├── build.gradle.kts ├── contents └── flood.json ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mod.json ├── settings.gradle.kts └── src └── main ├── kotlin └── cf │ └── wayzer │ └── contentsTweaker │ ├── CTNode.kt │ ├── CTNodeHelper.kt │ ├── ContentsTweaker.kt │ ├── ModMain.kt │ ├── TypeRegistry.kt │ ├── resolvers │ ├── ArrayResolver.kt │ ├── BetterJsonResolver.kt │ ├── BlockConsumesResolver.kt │ ├── MindustryContentsResolver.kt │ ├── ObjectMapResolver.kt │ ├── ReflectResolver.kt │ ├── SeqResolver.kt │ └── UIExtResolver.kt │ └── util │ ├── ExtendableClass.kt │ └── ReflectHelper.kt └── resources └── mod.json /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🌈' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: ':sparkles: MOD更新' 5 | labels: [ 'mod','core' ] 6 | - title: ':sparkles: 内容包' 7 | labels: [ 'contents' ] 8 | - title: ':wrench: 洪水模式 Flood' 9 | labels: [ 'contents:flood' ] 10 | autolabeler: 11 | - label: 'core' 12 | files: 13 | - 'core/**' 14 | - label: 'contents:flood' 15 | files: 16 | - 'contents/flood.json' 17 | - label: 'contents' 18 | files: 19 | - 'contents/**' 20 | - label: 'mod' 21 | files: 22 | - 'loaderMod/**' 23 | change-template: | 24 | - $TITLE @$AUTHOR (#$NUMBER) 25 | $BODY 26 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 27 | version-resolver: 28 | major: 29 | labels: 30 | - 'major' 31 | minor: 32 | labels: 33 | - 'minor' 34 | patch: 35 | labels: 36 | - 'patch' 37 | default: patch 38 | template: | 39 | # 更新内容 40 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | types: [ opened, reopened, synchronize ] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | Build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: build MOD 19 | run: | 20 | export VERSION="beta-$GITHUB_RUN_NUMBER" 21 | ./gradlew dist 22 | - name: Upload MOD JAR 23 | uses: actions/upload-artifact@v2 24 | with: 25 | path: build/dist/* -------------------------------------------------------------------------------- /.github/workflows/changeLog.yml: -------------------------------------------------------------------------------- 1 | name: UpdateChangeLog 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | types: [ opened, reopened, synchronize,labeled,unlabeled ] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | Build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - uses: release-drafter/release-drafter@v5 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | release: 8 | types: 9 | - 'published' 10 | 11 | jobs: 12 | Release: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Set env 19 | env: 20 | TAG: ${{ github.ref_name }} 21 | run: echo "VERSION=${TAG:1}" >> $GITHUB_ENV 22 | 23 | - name: Run unit tests and build JAR 24 | run: ./gradlew dist 25 | 26 | - name: upload artifacts 27 | uses: softprops/action-gh-release@v1 28 | with: 29 | files: build/dist/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains template 2 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 3 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 4 | *.class 5 | # User-specific stuff 6 | .idea/**/workspace.xml 7 | .idea/**/tasks.xml 8 | .idea/**/usage.statistics.xml 9 | .idea/**/dictionaries 10 | .idea/**/shelf 11 | 12 | # Generated files 13 | .idea/**/contentModel.xml 14 | 15 | # Sensitive or high-churn files 16 | .idea/**/dataSources/ 17 | .idea/**/dataSources.ids 18 | .idea/**/dataSources.local.xml 19 | .idea/**/sqlDataSources.xml 20 | .idea/**/dynamic.xml 21 | .idea/**/uiDesigner.xml 22 | .idea/**/dbnavigator.xml 23 | 24 | # Gradle 25 | .idea/**/gradle.xml 26 | .idea/**/libraries 27 | 28 | # Gradle and Maven with auto-import 29 | # When using Gradle or Maven with auto-import, you should exclude module files, 30 | # since they will be recreated, and may cause churn. Uncomment if using 31 | # auto-import. 32 | .idea/artifacts 33 | .idea/compiler.xml 34 | .idea/jarRepositories.xml 35 | .idea/modules.xml 36 | .idea/*.iml 37 | .idea/modules 38 | *.iml 39 | *.ipr 40 | 41 | # CMake 42 | cmake-build-*/ 43 | 44 | # Mongo Explorer plugin 45 | .idea/**/mongoSettings.xml 46 | 47 | # File-based project format 48 | *.iws 49 | 50 | # IntelliJ 51 | out/ 52 | 53 | # mpeltonen/sbt-idea plugin 54 | .idea_modules/ 55 | 56 | # JIRA plugin 57 | atlassian-ide-plugin.xml 58 | 59 | # Cursive Clojure plugin 60 | .idea/replstate.xml 61 | 62 | # Crashlytics plugin (for Android Studio and IntelliJ) 63 | com_crashlytics_export_strings.xml 64 | crashlytics.properties 65 | crashlytics-build.properties 66 | fabric.properties 67 | 68 | # Editor-based Rest Client 69 | .idea/httpRequests 70 | 71 | # Android studio 3.1+ serialized cache file 72 | .idea/caches/build_file_checksums.ser 73 | 74 | ### Gradle template 75 | .gradle 76 | **/build/ 77 | !src/**/build/ 78 | 79 | # Ignore Gradle GUI config 80 | gradle-app.setting 81 | 82 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 83 | !gradle-wrapper.jar 84 | 85 | # Cache of project 86 | .gradletasknamecache 87 | 88 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 89 | # gradle/wrapper/gradle-wrapper.properties 90 | 91 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | ContentsTweaker -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 93 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/markdown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 44 | 45 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /.idea/modules/MindustryContentsTweaker.test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules/contents/MindustryContents.contents.test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules/contents/flood/MindustryContents.contents.flood.test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules/contents/origin/MindustryContents.contents.origin.test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules/core/MindustryContents.core.test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules/loaderMod/MindustryContents.loaderMod.test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/way-zer/ContentsTweaker/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/way-zer/ContentsTweaker/actions/workflows/build.yml) 2 | [![](https://jitpack.io/v/way-zer/ContentsTweaker.svg)](https://jitpack.io/#way-zer/ContentsTweaker) 3 | 4 | # ContentsTweaker (for Mindustry) 5 | 6 | 一个`内容包`加载器的像素工厂MOD 7 | A Mindustry MOD to dynamically load `Contents Patch` 8 | 9 | ## 功能 Features 10 | 11 | * 接受服务器指令,为加载下张地图时,更换指定的`内容补丁` 12 | * Receive Info from Server, load special `Contents Patch` when join server or change map. 13 | * 为其他MOD提供接口,提供动态加载`内容补丁`的能力 14 | * Provide API for other mods, provide feature to dynamically load `Contents Patch` 15 | 16 | ## 内容补丁 Definition for `Contents Patch` 17 | 18 | 一个(h)json文件,可以修改游戏内所有物品的属性 19 | A (h)json file. According to it modify all contents property. 20 | 21 | 客户端将会自动加载`config/contents-patch/default.(h)json`文件(如果存在), 22 | 并且根据地图信息或服务器指令,加载补丁(如果不存在,会自动从服务器下载) 23 | Client will auto load patch in `config/contents-patch/default.(h)json` (if exists) 24 | And will load patch according to map info or server command.(May auto download patch for server) 25 | ### 示例 Exmaple 26 | 27 | ```json5 28 | { 29 | //ContentType 30 | block: { 31 | //Content name 32 | "copper-wall-large": { 33 | //Property to modify 34 | //Value is format of origin json 35 | "health": 1200 36 | }, 37 | "phase-wall": { 38 | "chanceDeflect": 0, 39 | "absorbLasers": true 40 | }, 41 | "force-projector": { 42 | "shieldHealth": 2500 43 | }, 44 | "lancer": { 45 | "shootType.damage": 30, 46 | "requirements": [ 47 | "copper/10", 48 | "lead/100" 49 | ] 50 | }, 51 | }, 52 | } 53 | ``` 54 | 55 | 你也可以单行声明`key:value`形式(Since v2) 56 | Or you can define it in single line like 57 | 58 | ```json5 59 | "block.copper-wall-large.health" : 1200 60 | ``` 61 | 62 | ### 网络协议 Protocal 63 | 64 | * map tag: `ContentsPatch` 65 | 地图所需patch列表 List of patch names 66 | 例(For example): `flood;patchA;xxxx` 67 | * map tag: `CT@{name}` 68 | Patch内容. The Content of patch 69 | * C->S ContentsLoader|version 70 | 发送版本信息,可用来判断是否安装Mod (示例版本号: `2.0.1` or `beta-99`) 71 | Send version, also for checking installation. (version example: `2.0.1` or `beta-99`) 72 | * ~~S->C ContentsLoader|loadPatch 73 | 命令客户端加载一个补丁(传递参数: 仅name) 74 | command client to load a patch (param only name)~~ 75 | * C-> ContentsLoader|requestPatch 76 | 客户端找不到时,向服务器请求patch(传递参数: 仅name) 77 | send when client not found this patch locally (param only name) 78 | * S->C ContentsLoader|newPatch 79 | 命令客户端加载一个新补丁,通常作为`requestPatch`的回复,或动态Patch如`UIExt`相关(传递参数: name & content) 80 | Command client to load a patch, normally as respond of `requestPatch` (params: name & content) 81 | * 约定,若名字`$`开头视为可变patch, 否则应该为不可变patch 82 | Conventionally, if name start with `$`, see it as mutable patch, don't cache it in client 83 | 84 | 通常来说,补丁名应该为其内容的hash,方便客户端进行缓存. 85 | Normally patch's name should be a hash of content, which can be cached currently. 86 | 87 | ## 安装 Setup 88 | 89 | 安装Release中的MOD即可(多人游戏兼容) 90 | Install mod in Release(multiplayer compatible) 91 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar 2 | 3 | plugins { 4 | kotlin("jvm") version "1.9.0" 5 | id("com.github.johnrengelman.shadow") version "8.1.1" 6 | `maven-publish` 7 | } 8 | 9 | group = "cf.wayzer" 10 | version = System.getenv().getOrDefault("VERSION", "3.0-SNAPSHOT") 11 | 12 | repositories { 13 | mavenCentral() 14 | maven(url = "https://www.jitpack.io") 15 | } 16 | 17 | dependencies { 18 | implementation(kotlin("stdlib")) 19 | compileOnly("com.github.Anuken.Arc:arc-core:v146") 20 | compileOnly("com.github.anuken.mindustryjitpack:core:v145") { 21 | exclude(group = "com.github.Anuken.Arc") 22 | } 23 | } 24 | 25 | kotlin { 26 | jvmToolchain(8) 27 | } 28 | 29 | publishing { 30 | publications { 31 | create("maven") { 32 | groupId = rootProject.group.toString() 33 | artifactId = project.name 34 | version = rootProject.version.toString() 35 | 36 | from(components["kotlin"]) 37 | } 38 | } 39 | } 40 | 41 | tasks.withType { 42 | inputs.property("version", rootProject.version) 43 | filter( 44 | filterType = org.apache.tools.ant.filters.ReplaceTokens::class, 45 | properties = mapOf("tokens" to mapOf("version" to rootProject.version)) 46 | ) 47 | } 48 | 49 | val shadowTask: ShadowJar = tasks.withType(ShadowJar::class.java) { 50 | configurations = listOf(project.configurations.runtimeClasspath.get()) 51 | minimize() 52 | }.first() 53 | 54 | val jarAndroid = tasks.create("jarAndroid") { 55 | dependsOn(shadowTask) 56 | val inFile = shadowTask.archiveFile.get().asFile 57 | val outFile = inFile.resolveSibling("${shadowTask.archiveBaseName.get()}-Android.jar") 58 | outputs.file(outFile) 59 | doLast { 60 | val sdkRoot = System.getenv("ANDROID_HOME") ?: System.getenv("ANDROID_SDK_ROOT") 61 | if (sdkRoot == null || !File(sdkRoot).exists()) throw GradleException("No valid Android SDK found. Ensure that ANDROID_HOME is set to your Android SDK directory.") 62 | 63 | val d8Tool = File("$sdkRoot/build-tools/").listFiles()?.sortedDescending() 64 | ?.flatMap { dir -> (dir.listFiles().orEmpty()).filter { it.name.startsWith("d8") } }?.firstOrNull() 65 | ?: throw GradleException("No d8 found. Ensure that you have an Android platform installed.") 66 | val platformRoot = File("$sdkRoot/platforms/").listFiles()?.sortedDescending()?.firstOrNull { it.resolve("android.jar").exists() } 67 | ?: throw GradleException("No android.jar found. Ensure that you have an Android platform installed.") 68 | 69 | //collect dependencies needed for desugaring 70 | val dependencies = (configurations.compileClasspath.get() + configurations.runtimeClasspath.get() + platformRoot.resolve("android.jar")) 71 | .joinToString(" ") { "--classpath ${it.path}" } 72 | exec { 73 | commandLine("$d8Tool $dependencies --min-api 14 --output $outFile $inFile".split(" ")) 74 | workingDir(inFile.parentFile) 75 | standardOutput = System.out 76 | errorOutput = System.err 77 | }.assertNormalExitValue() 78 | } 79 | } 80 | 81 | tasks.create("devInstall", Copy::class.java) { 82 | dependsOn(shadowTask) 83 | from(shadowTask.archiveFile.get()) 84 | into(System.getenv("AppData") + "/mindustry/mods") 85 | } 86 | 87 | tasks.create("dist", Jar::class.java) { 88 | dependsOn(shadowTask) 89 | dependsOn(jarAndroid) 90 | from(zipTree(shadowTask.archiveFile.get())) 91 | from(zipTree(jarAndroid.outputs.files.first())) 92 | destinationDirectory.set(buildDir.resolve("dist")) 93 | archiveFileName.set("ContentsTweaker-${rootProject.version}.jar") 94 | } -------------------------------------------------------------------------------- /contents/flood.json: -------------------------------------------------------------------------------- 1 | { 2 | //TODO merge to v136 3 | "block": { 4 | "conveyor": { 5 | "health": 40 6 | }, 7 | "armored-conveyor": { 8 | "health": 100 9 | }, 10 | "liquid-junction": { 11 | "health": 180 12 | }, 13 | "scrap-wall": { 14 | "health": 240 15 | }, 16 | "copper-wall": { 17 | "health": 340 18 | }, 19 | "titanium-wall": { 20 | "health": 460 21 | }, 22 | "thorium-wall": { 23 | "health": 600 24 | }, 25 | "plastanium-wall": { 26 | "health": 800 27 | }, 28 | "phase-wall": { 29 | "health": 1000, 30 | "chanceDeflect": 0, 31 | "absorbLasers": true, 32 | "insulated": true, 33 | "flashHit": false 34 | }, 35 | "surge-wall": { 36 | "health": 1200, 37 | "absorbLasers": true, 38 | "insulated": true, 39 | "lightningChance": 0 40 | }, 41 | "vault": { 42 | "group": "transportation" 43 | }, 44 | "container": { 45 | "group": "transportation" 46 | }, 47 | "mender": { 48 | "healPercent": 1 49 | }, 50 | "mend-projector": { 51 | "healPercent": 2.5 52 | }, 53 | "force-projector": { 54 | "shieldHealth": 2500 55 | }, 56 | "thorium-reactor": { 57 | "health": 2000 58 | }, 59 | "impact-reactor": { 60 | "liquidCapacity": 150, 61 | "itemCapacity": 30 62 | }, 63 | "lancer": { 64 | "range": 180, 65 | "reloadTime": 160, 66 | "shootType.damage": 30, 67 | "shootType.length": 185 68 | }, 69 | "arc": { 70 | "range": 30, 71 | "reloadTime": 60, 72 | "shootType.damage": 25, 73 | "shootType.lightningLength": 9 74 | }, 75 | "parallax": { 76 | "force": 8, 77 | "scaledForce": 7, 78 | "damage": 6 79 | }, 80 | "swarmer": { 81 | "reloadTime": 25, 82 | "shots": 5, 83 | "burstSpacing": 4 84 | }, 85 | "salvo": { 86 | "range": 200, 87 | "reloadTime": 30 88 | }, 89 | "tsunami": { 90 | "recoilAmount": 0, 91 | "reloadTime": 2, 92 | "range": 250, 93 | "inaccuracy": 2 94 | }, 95 | "fuse": { 96 | "range": 135, 97 | "reloadTime": 270, 98 | "ammoTypes.titanium.damage": 200, 99 | "ammoTypes.titanium.ammoMultiplier": 1, 100 | "ammoTypes.titanium.reloadMultiplier": 1.5, 101 | "ammoTypes.thorium.damage": 500, 102 | "ammoTypes.thorium.ammoMultiplier": 1 103 | }, 104 | "foreshadow": { 105 | "maxAmmo": 60, 106 | "ammoPerShot": 20, 107 | "ammoTypes.surgeAlloy": { 108 | "type": "RailBulletType", 109 | "shootEffect": "railShoot", 110 | "length": 500, 111 | "updateEffectSeg": 60, 112 | "pierceEffect": "railHit", 113 | "updateEffect": "railTrail", 114 | "hitEffect": "massiveExplosion", 115 | "smokeEffect": "shootBig2", 116 | "damage": 2000, 117 | "pierceDamageFactor": 0.3 118 | } 119 | } 120 | }, 121 | "bullet": { 122 | "fragGlass": { 123 | "damage": 20, 124 | "ammoMultiplier": 6 125 | }, 126 | "fragPlastic": { 127 | "damage": 30, 128 | "splashDamageRadius": 60, 129 | "ammoMultiplier": 4 130 | }, 131 | "fragExplosive": { 132 | "damage": 20, 133 | "splashDamage": 60, 134 | "splashDamageRadius": 60, 135 | "ammoMultiplier": 7 136 | }, 137 | "fragSurge": { 138 | "damage": 35, 139 | "splashDamage": 110, 140 | "lightningDamage": 40 141 | }, 142 | "missileExplosive": { 143 | "damage": 50, 144 | "splashDamage": 110, 145 | "ammoMultiplier": 4 146 | }, 147 | "missileIncendiary": { 148 | "damage": 60, 149 | "splashDamage": 60, 150 | "ammoMultiplier": 4 151 | }, 152 | "missileSurge": { 153 | "damage": 60, 154 | "splashDamage": 60, 155 | "ammoMultiplier": 3, 156 | "lightningDamage": 60, 157 | "lightningLength": 13 158 | }, 159 | "standardCopper": { 160 | "damage": 35, 161 | "ammoMultiplier": 5 162 | }, 163 | "standardDense": { 164 | "damage": 50, 165 | "ammoMultiplier": 6 166 | }, 167 | "standardThorium": { 168 | "damage": 60, 169 | "ammoMultiplier": 5, 170 | "pierceCap": 2, 171 | "pierceBuilding": true 172 | }, 173 | "standardHoming": { 174 | "damage": 50, 175 | "ammoMultiplier": 6 176 | }, 177 | "standardIncendiary": { 178 | "damage": 55, 179 | "ammoMultiplier": 6, 180 | "splashDamage": 25 181 | }, 182 | "standardDenseBig": { 183 | "pierceCap": 5, 184 | "pierceBuilding": true 185 | }, 186 | "standardThoriumBig": { 187 | "damage": 60, 188 | "pierceCap": 8, 189 | "ammoMultiplier": 3 190 | }, 191 | "standardIncendiaryBig": { 192 | "pierceCap": 5, 193 | "splashDamage": 25 194 | }, 195 | "basicFlame": { 196 | "damage": 40, 197 | "ammoMultiplier": 6 198 | }, 199 | "pyraFlame": { 200 | "damage": 100, 201 | "ammoMultiplier": 10 202 | }, 203 | "slagShot": { 204 | "damage": 8 205 | }, 206 | "oilShot": { 207 | "damage": 3 208 | }, 209 | "heavyWaterShot": { 210 | "lifetime": 65, 211 | "damage": 1 212 | }, 213 | "heavyCryoShot": { 214 | "lifetime": 65, 215 | "damage": 3 216 | }, 217 | "heavySlagShot": { 218 | "lifetime": 65, 219 | "damage": 12 220 | }, 221 | "heavyOilShot": { 222 | "lifetime": 65, 223 | "damage": 5 224 | } 225 | }, 226 | "unit": { 227 | "scepter": { 228 | "speed": 0.55, 229 | "rotateSpeed": 2.3, 230 | "weapons.0.bullet.lightningLength": 15, 231 | "weapons.0.bullet.lightningDamage": 80, 232 | "weapons.1.bullet.lightningLength": 15, 233 | "weapons.1.bullet.lightningDamage": 80 234 | }, 235 | "reign": { 236 | "speed": 0.5, 237 | "rotateSpeed": 2, 238 | "weapons.0.bullet.damage": 175, 239 | "weapons.0.bullet.pierceCap": 20, 240 | "weapons.0.bullet.lifetime": 20, 241 | "weapons.0.bullet.fragBullet.damage": 25, 242 | "weapons.0.bullet.fragBullet.splashDamage": 25, 243 | "weapons.1.bullet.damage": 175, 244 | "weapons.1.bullet.pierceCap": 20, 245 | "weapons.1.bullet.lifetime": 20, 246 | "weapons.1.bullet.fragBullet.damage": 25, 247 | "weapons.1.bullet.fragBullet.splashDamage": 25 248 | }, 249 | "nova": { 250 | "weapons.0.bullet.healPercent": 2, 251 | "weapons.1.bullet.healPercent": 2 252 | }, 253 | "pulsar": { 254 | "weapons.0.bullet.healPercent": 1, 255 | "weapons.0.bullet.lightningType.healPercent": 1, 256 | "weapons.1.bullet.healPercent": 1, 257 | "weapons.1.bullet.lightningType.healPercent": 1 258 | }, 259 | "quasar": { 260 | "weapons.0.bullet.healPercent": 5, 261 | "weapons.1.bullet.healPercent": 5 262 | }, 263 | "vela": { 264 | "weapons.0.bullet.damage": 25, 265 | "weapons.0.bullet.healPercent": 0.4, 266 | "weapons.1.repairSpeed": 1, 267 | "weapons.2.repairSpeed": 1 268 | }, 269 | "corvus": { 270 | "weapons.0.bullet.length": 600, 271 | "weapons.0.bullet.damage": 800, 272 | "weapons.0.bullet.lightningLength": 10, 273 | "weapons.0.bullet.lightningDamage": 85, 274 | }, 275 | "spiroct": { 276 | "weapons.0.bullet.damage": 26, 277 | "weapons.1.bullet.damage": 26, 278 | "weapons.2.bullet.sapStrength": 0.5, 279 | "weapons.2.bullet.damage": 25, 280 | "weapons.3.bullet.sapStrength": 0.5, 281 | "weapons.3.bullet.damage": 25 282 | }, 283 | "arkyid": { 284 | "weapons.0.bullet.sapStrength": 0.2, 285 | "weapons.0.bullet.damage": 90, 286 | "weapons.7.bullet.damage": 25, 287 | "weapons.7.bullet.lifetime": 30, 288 | }, 289 | "toxopid": { 290 | "legSplashDamage": 200, 291 | "weapons.0.bullet.damage": 150, 292 | "weapons.1.bullet.damage": 150, 293 | "weapons.2.bullet.damage": 75, 294 | "weapons.2.bullet.splashDamage": 100, 295 | "weapons.2.bullet.fragBullet.damage": 50, 296 | "weapons.2.bullet.fragBullet.splashDamage": 60 297 | }, 298 | "flare": { 299 | "health": 200 300 | }, 301 | "horizon": { 302 | "defaultController": "BuilderAI", 303 | "health": 400, 304 | "speed": 2.5, 305 | "buildSpeed": 0.25, 306 | "weapons.0.bullet.damage": 0, 307 | "weapons.1.bullet.damage": 0 308 | }, 309 | "zenith": { 310 | "health": 1400, 311 | }, 312 | "poly": { 313 | "weapons.0.bullet.healPercent": 3, 314 | "weapons.1.bullet.healPercent": 3 315 | }, 316 | "mega": { 317 | "weapons.0.bullet.healPercent": 3, 318 | "weapons.1.bullet.healPercent": 3, 319 | "weapons.2.bullet.healPercent": 3, 320 | "weapons.3.bullet.healPercent": 3 321 | }, 322 | "quad": { 323 | "weapons.0.reload": 600, 324 | "weapons.0.bullet.healPercent": 10, 325 | "weapons.0.bullet.splashDamage": 6000, 326 | "weapons.0.bullet.splashDamageRadius": 40 327 | }, 328 | "oct": { 329 | "abilities.+": [ 330 | { 331 | "type": "StatusFieldAbility", 332 | "effect": "overclock", 333 | "duration": 600, 334 | "reload": 600, 335 | "range": 120 336 | } 337 | ], 338 | }, 339 | "minke": { 340 | "weapons.0.bullet": "fragGlass", 341 | "weapons.1.bullet": "fragGlass" 342 | }, 343 | "sei": { 344 | "weapons.0.bullet.damage": 56, 345 | "weapons.1.bullet.damage": 75, 346 | "weapons.2.bullet.damage": 75 347 | }, 348 | "omura": { 349 | "weapons.0.bullet.length": 500, 350 | "weapons.0.bullet.damage": 2000, 351 | "weapons.0.bullet.pierceDamageFactor": 0.2 352 | }, 353 | "oxynoe": { 354 | "weapons.2.bullet.maxRange": 150 355 | }, 356 | "aegires": { 357 | "abilities.0.damage": 100, 358 | "weapons.0.bullet.maxRange": 240, 359 | "weapons.1.bullet.maxRange": 240, 360 | "weapons.2.bullet.maxRange": 240, 361 | "weapons.3.bullet.maxRange": 240 362 | }, 363 | "navanax": { 364 | "weapons.0.bullet.maxRange": 130, 365 | "weapons.0.bullet.healPercent": 0.3, 366 | "weapons.1.bullet.maxRange": 130, 367 | "weapons.1.bullet.healPercent": 0.3, 368 | "weapons.2.bullet.maxRange": 130, 369 | "weapons.2.bullet.healPercent": 0.3, 370 | "weapons.3.bullet.maxRange": 130, 371 | "weapons.3.bullet.healPercent": 0.3, 372 | "weapons.4.bullet.healPercent": 10, 373 | "weapons.4.bullet.damage": 200, 374 | "weapons.5.bullet.healPercent": 10, 375 | "weapons.5.bullet.damage": 200 376 | }, 377 | "alpha": { 378 | "weapons.0.bullet.buildingDamageMultiplier": 1 379 | }, 380 | "beta": { 381 | "weapons.0.bullet.buildingDamageMultiplier": 1 382 | }, 383 | "gamma": { 384 | "weapons.0.bullet.buildingDamageMultiplier": 1 385 | } 386 | } 387 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.stdlib.default.dependency=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/way-zer/ContentsTweaker/c99fc0a06bd59b8bc9bcc0e08f8a47caa56facde/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 134 | 135 | Please set the JAVA_HOME variable in your environment to match the 136 | location of your Java installation." 137 | fi 138 | 139 | # Increase the maximum file descriptors if we can. 140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 141 | case $MAX_FD in #( 142 | max*) 143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 144 | # shellcheck disable=SC3045 145 | MAX_FD=$( ulimit -H -n ) || 146 | warn "Could not query maximum file descriptor limit" 147 | esac 148 | case $MAX_FD in #( 149 | '' | soft) :;; #( 150 | *) 151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 152 | # shellcheck disable=SC3045 153 | ulimit -n "$MAX_FD" || 154 | warn "Could not set maximum file descriptor limit to $MAX_FD" 155 | esac 156 | fi 157 | 158 | # Collect all arguments for the java command, stacking in reverse order: 159 | # * args from the command line 160 | # * the main class name 161 | # * -classpath 162 | # * -D...appname settings 163 | # * --module-path (only if needed) 164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 165 | 166 | # For Cygwin or MSYS, switch paths to Windows format before running java 167 | if "$cygwin" || "$msys" ; then 168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 170 | 171 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 172 | 173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 174 | for arg do 175 | if 176 | case $arg in #( 177 | -*) false ;; # don't mess with options #( 178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 179 | [ -e "$t" ] ;; #( 180 | *) false ;; 181 | esac 182 | then 183 | arg=$( cygpath --path --ignore --mixed "$arg" ) 184 | fi 185 | # Roll the args list around exactly as many times as the number of 186 | # args, so each arg winds up back in the position where it started, but 187 | # possibly modified. 188 | # 189 | # NB: a `for` loop captures its iteration list before it begins, so 190 | # changing the positional parameters here affects neither the number of 191 | # iterations, nor the values presented in `arg`. 192 | shift # remove old arg 193 | set -- "$@" "$arg" # push replacement arg 194 | done 195 | fi 196 | 197 | 198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 200 | 201 | # Collect all arguments for the java command; 202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 203 | # shell script including quotes and variable substitutions, so put them in 204 | # double quotes to make sure that they get re-expanded; and 205 | # * put everything else in single quotes, so that it's not re-expanded. 206 | 207 | set -- \ 208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 209 | -classpath "$CLASSPATH" \ 210 | org.gradle.wrapper.GradleWrapperMain \ 211 | "$@" 212 | 213 | # Stop when "xargs" is not available. 214 | if ! command -v xargs >/dev/null 2>&1 215 | then 216 | die "xargs is not available" 217 | fi 218 | 219 | # Use "xargs" to parse quoted args. 220 | # 221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 222 | # 223 | # In Bash we could simply go: 224 | # 225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 226 | # set -- "${ARGS[@]}" "$@" 227 | # 228 | # but POSIX shell has neither arrays nor command substitution, so instead we 229 | # post-process each arg (as a line of input to sed) to backslash-escape any 230 | # character that might be a shell metacharacter, then use eval to reverse 231 | # that process (while maintaining the separation between arguments), and wrap 232 | # the whole thing up as a single "set" statement. 233 | # 234 | # This will of course break if any of these variables contains a newline or 235 | # an unmatched quote. 236 | # 237 | 238 | eval "set -- $( 239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 240 | xargs -n1 | 241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 242 | tr '\n' ' ' 243 | )" '"$@"' 244 | 245 | exec "$JAVACMD" "$@" 246 | -------------------------------------------------------------------------------- /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 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ContentsTweaker", 3 | "displayName": "ContentsTweaker-内容包修改器", 4 | "author": "WayZer", 5 | "main": "cf.wayzer.contentsTweaker.ModMain", 6 | "description": "Dynamic load custom contents patch account to server", 7 | "version": "@version@", 8 | "hidden": true, 9 | "minGameVersion": 136 10 | } 11 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ContentsTweaker" -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/CTNode.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker 2 | 3 | import arc.util.Log 4 | import arc.util.serialization.JsonValue 5 | import arc.util.serialization.JsonWriter 6 | import cf.wayzer.contentsTweaker.CTNode.* 7 | import cf.wayzer.contentsTweaker.util.ExtendableClass 8 | import cf.wayzer.contentsTweaker.util.ExtendableClassDSL 9 | 10 | /** 11 | * 所有节点都是[CTNode] 12 | * 如果一个节点与某个具体对象有关,应当实现[ObjInfo] 13 | * [ObjInfo.obj]应当始终返回原始值(不管后来有没有修改过) 14 | * 如果一个节点绑定属性可被重新赋值,实现[Modifiable] 15 | * 如果一个节点是运算符节点(末端节点),可接收JSON对象,实现[Modifier] 16 | * [AfterHandler]将在节点或子节点[Modifiable.setValue]后注册,批处理结束后统一调用 17 | * 18 | * 所有节点,在获得实例,使用前必须调用[collectAll] ([ContentsTweaker.NodeCollector]内不需要) 19 | * */ 20 | class CTNode private constructor() : ExtendableClass() { 21 | val children = mutableMapOf() 22 | private var collected = false 23 | fun collectAll(): CTNode { 24 | if (collected) return this 25 | collected = true 26 | for (resolver in resolvers) { 27 | kotlin.runCatching { resolver.collectChild(this) }.onFailure { e -> 28 | throw Exception("Fail to collectChild in $resolver", e) 29 | } 30 | } 31 | get>()?.let { modifiable -> 32 | getOrCreate("=").apply { 33 | +Modifier { json -> 34 | modifiable.setJson(json) 35 | } 36 | +ToJson { 37 | it.value(modifiable.currentValue?.let(TypeRegistry::getKeyString)) 38 | } 39 | } 40 | } 41 | return this 42 | } 43 | 44 | fun resolve(name: String): CTNode { 45 | val node = children[name] 46 | ?: getAll().firstNotNullOfOrNull { it.resolve(name) } 47 | ?: error("Not found child $name") 48 | node.collectAll() 49 | return node 50 | } 51 | 52 | /** 供[ContentsTweaker.NodeCollector]使用,解析清使用[resolve]*/ 53 | @ExtendableClassDSL 54 | fun getOrCreate(child: String): CTNode { 55 | return children.getOrPut(child) { CTNode() } 56 | } 57 | 58 | interface CTExtInfo 59 | data class ObjInfo( 60 | val obj: T, val type: Class, 61 | val elementType: Class<*>? = null, // List.T or Map.V 62 | val keyType: Class<*>? = null // Map.K 63 | ) : CTExtInfo { 64 | constructor(obj: T & Any) : this(obj, obj::class.java) 65 | } 66 | 67 | fun interface Resettable : CTExtInfo { 68 | fun reset() 69 | } 70 | 71 | /** [T] must equal [ObjInfo.type]*/ 72 | @Suppress("MemberVisibilityCanBePrivate") 73 | abstract class Modifiable(node: CTNode) : CTExtInfo, Resettable { 74 | val info = node.get>()!! 75 | internal var nodeStack: List? = null 76 | abstract val currentValue: T 77 | protected abstract fun setValue0(value: T) 78 | fun setValue(value: T) { 79 | PatchHandler.modified(this) 80 | setValue0(value) 81 | } 82 | 83 | fun setValueAny(value: Any?) { 84 | val type = info.type 85 | 86 | @Suppress("UNCHECKED_CAST") 87 | val v = (if (type.isPrimitive) value else type.cast(value)) as T 88 | setValue(v) 89 | } 90 | 91 | fun setJson(v: JsonValue) { 92 | val value = info.run { TypeRegistry.resolveType(v, type, elementType, keyType) } 93 | setValue(value) 94 | } 95 | 96 | override fun reset() = setValue(info.obj) 97 | } 98 | 99 | fun interface IndexableRaw : CTExtInfo { 100 | fun resolve(name: String): CTNode? 101 | } 102 | 103 | fun interface Indexable : IndexableRaw { 104 | /** 解析索引,[key]已去除# */ 105 | fun resolveIndex(key: String): CTNode? 106 | 107 | /** 通用的[name]索引(不一定以#开头) */ 108 | override fun resolve(name: String): CTNode? { 109 | if (name.isEmpty() || name[0] != '#') return null 110 | return resolveIndex(name.substring(1)) 111 | } 112 | } 113 | 114 | fun interface Modifier : CTExtInfo { 115 | /** 116 | * 1. 判断状态(parent), 根据[ObjInfo.type],[ObjInfo.elementType],[ObjInfo.keyType]解析[json] 117 | * 2. 赋值直接调用[Modifiable.setValue]。如果是增量修改,使用[Modifiable.currentValue]获取读取值 118 | */ 119 | @Throws(Throwable::class) 120 | fun setValue(json: JsonValue) 121 | } 122 | 123 | fun interface AfterHandler : CTExtInfo { 124 | fun handle() 125 | } 126 | 127 | /** 为[ContentsTweaker.exportAll]自定义输出 */ 128 | fun interface ToJson : CTExtInfo { 129 | fun write(jsonWriter: JsonWriter) 130 | } 131 | 132 | companion object { 133 | private val resolvers = ContentsTweaker.resolvers 134 | val Root = CTNode() 135 | val Nope = CTNode() 136 | 137 | init { 138 | Nope.apply { 139 | +Modifier { } 140 | +IndexableRaw { Nope } 141 | } 142 | } 143 | } 144 | 145 | object PatchHandler { 146 | private object NodeStack { 147 | private val stack = mutableListOf>() 148 | val last get() = stack.last() 149 | fun getParents() = stack.map { it.first } 150 | fun getId(node: CTNode? = null) = 151 | stack.takeWhile { it.first !== node }.joinToString(".") { it.second } 152 | 153 | fun resolve(node: CTNode, child: String): CTNode { 154 | stack.add(node to child) 155 | return node.resolve(child) 156 | } 157 | 158 | fun pop(node: CTNode) { 159 | @Suppress("ControlFlowWithEmptyBody") 160 | while (stack.removeLast().first !== node); 161 | } 162 | } 163 | 164 | val resetHandlers = mutableSetOf() 165 | internal val afterHandlers = mutableSetOf() 166 | 167 | fun modified(modifiable: Modifiable<*>) { 168 | resetHandlers.add(modifiable) 169 | val parents = modifiable.nodeStack ?: NodeStack.getParents().also { 170 | modifiable.nodeStack = it 171 | } 172 | parents.flatMapTo(afterHandlers) { it.getAll() } 173 | } 174 | 175 | fun doAfterHandle() { 176 | afterHandlers.forEach { it.handle() } 177 | afterHandlers.clear() 178 | } 179 | 180 | fun recoverAll() { 181 | resetHandlers.forEach { it.reset() } 182 | resetHandlers.clear() 183 | doAfterHandle() 184 | 185 | Root.children.clear() 186 | Root.collected = false 187 | } 188 | 189 | fun handle(json: JsonValue, node: CTNode = Root.collectAll()) { 190 | //部分简化,如果value不是object可省略=运算符 191 | if (!json.isObject && node.get() == null) 192 | return handle(json, NodeStack.resolve(node, "=")) 193 | node.get()?.let { 194 | try { 195 | it.setValue(json) 196 | } catch (e: Throwable) { 197 | Log.err("Fail to apply Modifier ${NodeStack.getId()}: ${json.prettyPrint(JsonWriter.OutputType.minimal, 0)}:\n $e") 198 | } 199 | return 200 | } 201 | 202 | for (child in json) { 203 | val names = child.name.split(".") 204 | val childNode = try { 205 | names.fold(node, NodeStack::resolve) 206 | } catch (e: Throwable) { 207 | val (errNode, errChild) = NodeStack.last 208 | Log.err("Fail to resolve child ${NodeStack.getId(errNode)}->${errChild}:\n $e") 209 | NodeStack.pop(node) 210 | continue 211 | } 212 | handle(child, childNode) 213 | NodeStack.pop(node) 214 | } 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/CTNodeHelper.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker 2 | 3 | import arc.util.serialization.JsonValue 4 | import cf.wayzer.contentsTweaker.util.ExtendableClassDSL 5 | 6 | 7 | @ExtendableClassDSL 8 | inline fun CTNode.getObjInfo() = get>() 9 | ?.takeIf { T::class.java.isAssignableFrom(it.type) || (T::class.java == Any::class.java && it.type.isPrimitive) } 10 | 11 | @JvmInline 12 | value class CTNodeTypeChecked(val node: CTNode) { 13 | val objInfo get() = node.get>()!! 14 | val modifiable get() = node.get>() ?: error("Not Modifiable") 15 | val modifiableNullable get() = node.get>() 16 | } 17 | 18 | inline fun CTNode.checkObjInfoOrNull(): CTNodeTypeChecked? { 19 | getObjInfo()?.obj ?: return null 20 | return CTNodeTypeChecked(this) 21 | } 22 | 23 | inline fun CTNode.checkObjInfo(): CTNodeTypeChecked { 24 | return checkObjInfoOrNull() ?: error("require type ${T::class.java}") 25 | } 26 | 27 | /** @param block *拷贝*当前对象,并输入进行修改*/ 28 | @ExtendableClassDSL 29 | inline fun CTNodeTypeChecked.modifier(name: String, crossinline block: T.(JsonValue) -> T) { 30 | val modifiable = modifiableNullable ?: return 31 | node.getOrCreate(name) += CTNode.Modifier { 32 | modifiable.setValue(modifiable.currentValue.block(it)) 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/ContentsTweaker.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker 2 | 3 | import arc.util.Log 4 | import arc.util.serialization.BaseJsonWriter 5 | import arc.util.serialization.JsonWriter 6 | import cf.wayzer.contentsTweaker.resolvers.* 7 | import mindustry.Vars 8 | import mindustry.io.JsonIO 9 | import mindustry.io.SaveIO 10 | import java.io.ByteArrayInputStream 11 | import java.io.ByteArrayOutputStream 12 | import java.io.DataInputStream 13 | import java.io.DataOutputStream 14 | import kotlin.system.measureTimeMillis 15 | 16 | object ContentsTweaker { 17 | fun interface NodeCollector { 18 | fun collectChild(node: CTNode) 19 | } 20 | 21 | val resolvers: MutableList = mutableListOf( 22 | ReflectResolver, 23 | ArrayResolver, 24 | SeqResolver, 25 | ObjectMapResolver, 26 | 27 | BetterJsonResolver, 28 | MindustryContentsResolver, 29 | BlockConsumesResolver, 30 | UIExtResolver, 31 | ) 32 | val typeResolvers = mutableListOf( 33 | BlockConsumesResolver 34 | ) 35 | 36 | fun afterHandle() { 37 | if (CTNode.PatchHandler.afterHandlers.isEmpty()) return 38 | val time = measureTimeMillis { 39 | CTNode.PatchHandler.doAfterHandle() 40 | } 41 | Log.infoTag("ContentsTweaker", "Do afterHandle costs $time ms") 42 | } 43 | 44 | fun loadPatch(name: String, content: String, doAfter: Boolean = true) { 45 | val time = measureTimeMillis { 46 | CTNode.PatchHandler.handle(JsonIO.read(null, content)) 47 | if (doAfter) afterHandle() 48 | } 49 | Log.infoTag("ContentsTweaker", "Load Content Patch '$name' costs $time ms") 50 | } 51 | 52 | fun recoverAll() { 53 | if (worldInReset) return 54 | worldInReset = true 55 | CTNode.PatchHandler.recoverAll() 56 | worldInReset = false 57 | } 58 | 59 | var worldInReset = false 60 | 61 | fun reloadWorld() { 62 | if (worldInReset) return 63 | val time = measureTimeMillis { 64 | worldInReset = true 65 | val stream = ByteArrayOutputStream() 66 | 67 | val output = DataOutputStream(stream) 68 | val writer = SaveIO.getSaveWriter() 69 | writer.writeMap(output) 70 | 71 | val input = DataInputStream(ByteArrayInputStream(stream.toByteArray())) 72 | @Suppress("INACCESSIBLE_TYPE") 73 | writer.readMap(input, Vars.world.context) 74 | worldInReset = false 75 | } 76 | Log.infoTag("ContentsTweaker", "Reload world costs $time ms") 77 | } 78 | 79 | //Dev test, call from js 80 | @Suppress("unused") 81 | fun eval(content: String) { 82 | loadPatch("console", "{$content}") 83 | } 84 | 85 | //Dev test, call from js 86 | @Suppress("unused") 87 | fun exportAll() { 88 | val visited = mutableSetOf() 89 | fun JsonWriter.writeNode(node: CTNode): BaseJsonWriter { 90 | this.`object`() 91 | node.getObjInfo()?.obj?.let(visited::add) 92 | for ((k, v) in node.children) { 93 | name(k) 94 | v.collectAll() 95 | when { 96 | v.get()?.write(this) != null -> {} 97 | v.getObjInfo()?.obj in visited -> value("RECURSIVE") 98 | v.get() != null -> value("CT_MODIFIER") 99 | //只有=的简单节点,省略= 100 | v.children.keys.singleOrNull() == "=" -> v.resolve("=").get()!!.write(this) 101 | else -> writeNode(v) 102 | } 103 | } 104 | node.getObjInfo()?.obj?.let(visited::remove) 105 | return this.pop() 106 | } 107 | 108 | val writer = JsonWriter(Vars.dataDirectory.child("CT.json").writer(false)) 109 | writer.setOutputType(JsonWriter.OutputType.json) 110 | writer.writeNode(CTNode.Root.collectAll()).close() 111 | } 112 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/ModMain.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker 2 | 3 | import arc.Events 4 | import arc.util.Log 5 | import mindustry.Vars 6 | import mindustry.game.EventType.ResetEvent 7 | import mindustry.game.EventType.WorldLoadBeginEvent 8 | import mindustry.gen.Call 9 | import mindustry.mod.Mod 10 | 11 | @Suppress("unused", "MemberVisibilityCanBePrivate") 12 | class ModMain : Mod() { 13 | override fun init() { 14 | registerContentsParser() 15 | Log.infoTag("ContentsTweaker", "Finish Load Mod") 16 | } 17 | 18 | fun registerContentsParser() { 19 | ContentsTweaker//ensure all resolver init 20 | Events.on(ResetEvent::class.java) { ContentsTweaker.recoverAll() } 21 | 22 | Events.on(WorldLoadBeginEvent::class.java) { 23 | if (ContentsTweaker.worldInReset) return@on 24 | Call.serverPacketReliable("ContentsLoader|version", Vars.mods.getMod(javaClass).meta.version) 25 | val list = Vars.state.map.tags.get("ContentsPatch")?.split(";") 26 | ?: Vars.state.rules.tags.get("ContentsPatch")?.split(";") 27 | ?: return@on 28 | list.forEach { name -> 29 | if (name.isBlank()) return@forEach 30 | val patch = Vars.state.map.tags.get("CT@$name") 31 | ?: return@forEach Call.serverPacketReliable("ContentsLoader|requestPatch", name) 32 | ContentsTweaker.loadPatch(name, patch, doAfter = false) 33 | } 34 | ContentsTweaker.afterHandle() 35 | } 36 | if (Vars.netClient != null) { 37 | Vars.netClient.addPacketHandler("ContentsLoader|newPatch") { 38 | val (name, content) = it.split('\n', limit = 2) 39 | ContentsTweaker.loadPatch(name, content) 40 | } 41 | //TODO: Deprecated 42 | Vars.netClient.addPacketHandler("ContentsLoader|loadPatch") { name -> 43 | val patch = Vars.state.map.tags.get("CT@$name") 44 | ?: return@addPacketHandler Call.serverPacketReliable("ContentsLoader|requestPatch", name) 45 | ContentsTweaker.loadPatch(name, patch) 46 | } 47 | } 48 | Vars.mods.scripts.scope.apply { 49 | put("CT", this, ContentsTweaker) 50 | put("CTRoot", this, CTNode.Root) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/TypeRegistry.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker 2 | 3 | import arc.func.Prov 4 | import arc.struct.EnumSet 5 | import arc.struct.ObjectMap 6 | import arc.util.serialization.Json 7 | import arc.util.serialization.JsonValue 8 | import cf.wayzer.contentsTweaker.util.reflectDelegate 9 | import mindustry.Vars 10 | import mindustry.ctype.MappableContent 11 | import mindustry.mod.ContentParser 12 | import mindustry.mod.Mods 13 | 14 | object TypeRegistry { 15 | private val Mods.parser: ContentParser by reflectDelegate() 16 | private val ContentParser.parser: Json by reflectDelegate() 17 | private val jsonParser by lazy { Vars.mods.parser.parser } 18 | 19 | interface Resolver { 20 | fun resolveType(json: JsonValue, type: Class?, elementType: Class<*>? = null, keyType: Class<*>? = null): T? 21 | } 22 | 23 | private val resolvers = ContentsTweaker.typeResolvers 24 | 25 | fun resolveType(json: JsonValue, type: Class?, elementType: Class<*>? = null, keyType: Class<*>? = null): T { 26 | @Suppress("UNCHECKED_CAST") 27 | when (type) { 28 | EnumSet::class.java -> { 29 | fun > newEnumSet(arr: Array>) = EnumSet.of(*arr as Array) 30 | return newEnumSet(resolve(json, elementType)) as T 31 | } 32 | 33 | Prov::class.java -> { 34 | val cls = getTypeByName(json.asString(), null) 35 | val method = ContentParser::class.java.getDeclaredMethod("supply", Class::class.java) 36 | method.isAccessible = true 37 | return method.invoke(Vars.mods.parser, cls) as T 38 | } 39 | } 40 | return resolvers.firstNotNullOfOrNull { it.resolveType(json, type, elementType, keyType) } 41 | ?: jsonParser.readValue(type, elementType, json, keyType) 42 | } 43 | 44 | inline fun resolve(json: JsonValue, elementType: Class<*>? = null, keyType: Class<*>? = null): T { 45 | return resolveType(json, T::class.java, elementType, keyType) 46 | } 47 | 48 | fun getTypeByName(name: String, def: Class<*>?): Class<*> { 49 | val method = ContentParser::class.java.getDeclaredMethod("resolve", String::class.java, Class::class.java) 50 | method.isAccessible = true 51 | return method.invoke(Vars.mods.parser, name, def) as Class<*> 52 | } 53 | 54 | fun getKeyString(obj: Any?): String { 55 | return when (obj) { 56 | is MappableContent -> obj.name 57 | is Enum<*> -> obj.name 58 | is Class<*> -> obj.name 59 | else -> obj.toString() 60 | } 61 | } 62 | 63 | private val ContentParser.contentTypes: ObjectMap<*, *> by reflectDelegate() 64 | private fun initContentParser() { 65 | if (Vars.mods.parser.contentTypes.isEmpty) { 66 | val method = ContentParser::class.java.getDeclaredMethod("init") 67 | method.isAccessible = true 68 | method.invoke(Vars.mods.parser) 69 | } 70 | } 71 | 72 | init { 73 | initContentParser() 74 | } 75 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/resolvers/ArrayResolver.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker.resolvers 2 | 3 | import cf.wayzer.contentsTweaker.* 4 | 5 | object ArrayResolver : ContentsTweaker.NodeCollector { 6 | override fun collectChild(node: CTNode) { 7 | node.checkObjInfoOrNull>()?.extend() 8 | } 9 | 10 | // private val types = mutableSetOf() 11 | private fun CTNodeTypeChecked>.extend() { 12 | objInfo.type.componentType.let { 13 | if (it.isArray || it.`package`.name.startsWith("arc")) { 14 | // if (types.add(it)) println(it) 15 | return 16 | } 17 | } 18 | val list = objInfo.obj 19 | list.take(10).forEachIndexed { index, item -> 20 | if (item != null) 21 | node.getOrCreate("#$index") += CTNode.ObjInfo(item) 22 | } 23 | node += CTNode.IndexableRaw { key -> 24 | val i = key.toInt() 25 | if (i >= list.size) return@IndexableRaw null 26 | node.getOrCreate("#${i}").apply { 27 | extendOnce>(CTNode.ObjInfo(list[i] ?: return@IndexableRaw null)) 28 | } 29 | } 30 | modifier("-") { json -> 31 | val item = if (json.isNumber && json.asInt() < list.size) { 32 | list[json.asInt()]//删除原始数组中对应索引的元素 33 | } else { 34 | TypeRegistry.resolveType(json, objInfo.elementType) 35 | } 36 | filter { it != item }.toTypedArray() 37 | } 38 | modifier("+") { json -> 39 | val value = TypeRegistry.resolveType(json, objInfo.elementType) 40 | this.plus(element = value) 41 | } 42 | modifier("+=") { json -> 43 | val value = TypeRegistry.resolveType(json, objInfo.type, objInfo.elementType) 44 | this.plus(elements = value) 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/resolvers/BetterJsonResolver.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker.resolvers 2 | 3 | import arc.graphics.Color 4 | import arc.graphics.g2d.TextureRegion 5 | import cf.wayzer.contentsTweaker.CTNode 6 | import cf.wayzer.contentsTweaker.ContentsTweaker 7 | import cf.wayzer.contentsTweaker.checkObjInfoOrNull 8 | import cf.wayzer.contentsTweaker.getObjInfo 9 | import mindustry.content.TechTree.TechNode 10 | import mindustry.graphics.g3d.PlanetGrid 11 | 12 | object BetterJsonResolver : ContentsTweaker.NodeCollector { 13 | override fun collectChild(node: CTNode) { 14 | node.checkObjInfoOrNull()?.apply { 15 | node += CTNode.ToJson { it.value(objInfo.obj) } 16 | } 17 | node.checkObjInfoOrNull()?.apply { 18 | node += CTNode.ToJson { it.value(objInfo.obj.toString()) } 19 | } 20 | node.checkObjInfoOrNull()?.apply { 21 | node.getOrCreate("parent").apply parent@{ 22 | extendOnce(CTNode.ToJson { 23 | it.value(this@parent.getObjInfo()?.obj?.content?.name) 24 | }) 25 | } 26 | node.getOrCreate("children").extendOnce(CTNode.ToJson { 27 | it.value("...") 28 | }) 29 | } 30 | node.checkObjInfoOrNull()?.apply { 31 | node += CTNode.ToJson { it.value("...") } 32 | } 33 | node.checkObjInfoOrNull()?.apply { 34 | node += CTNode.ToJson { it.value("...") } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/resolvers/BlockConsumesResolver.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker.resolvers 2 | 3 | import arc.struct.Seq 4 | import arc.util.serialization.JsonValue 5 | import cf.wayzer.contentsTweaker.* 6 | import cf.wayzer.contentsTweaker.util.reflectDelegate 7 | import mindustry.type.ItemStack 8 | import mindustry.world.Block 9 | import mindustry.world.consumers.* 10 | 11 | object BlockConsumesResolver : ContentsTweaker.NodeCollector, TypeRegistry.Resolver { 12 | private val Block.consumeBuilder: Seq by reflectDelegate() 13 | override fun collectChild(node: CTNode) { 14 | val block = node.getObjInfo()?.obj ?: return 15 | node.getOrCreate("consumers").checkObjInfo>() 16 | .extendConsumers(block) 17 | } 18 | 19 | private fun CTNodeTypeChecked>.extendConsumers(block: Block) { 20 | node += CTNode.AfterHandler { 21 | block.apply { 22 | consPower = consumers.filterIsInstance().firstOrNull() 23 | optionalConsumers = consumers.filter { it.optional && !it.ignore() }.toTypedArray() 24 | nonOptionalConsumers = consumers.filter { !it.optional && !it.ignore() }.toTypedArray() 25 | updateConsumers = consumers.filter { it.update && !it.ignore() }.toTypedArray() 26 | hasConsumers = consumers.isNotEmpty() 27 | itemFilter.fill(false) 28 | liquidFilter.fill(false) 29 | consumers.forEach { it.apply(this) } 30 | } 31 | } 32 | modifier("clearItems") { filterNot { it is ConsumeItems || it is ConsumeItemFilter }.toTypedArray() } 33 | modifier("item") { this + ConsumeItems(arrayOf(ItemStack(TypeRegistry.resolve(it), 1))) } 34 | modifier("items") { this + TypeRegistry.resolve(it) } 35 | modifier("itemCharged") { this + TypeRegistry.resolve(it) } 36 | modifier("itemFlammable") { this + TypeRegistry.resolve(it) } 37 | modifier("itemRadioactive") { this + TypeRegistry.resolve(it) } 38 | modifier("itemExplosive") { this + TypeRegistry.resolve(it) } 39 | modifier("itemExplode") { this + TypeRegistry.resolve(it) } 40 | 41 | modifier("clearLiquids") { filterNot { it is ConsumeLiquidBase || it is ConsumeLiquids }.toTypedArray() } 42 | modifier("liquid") { this + TypeRegistry.resolve(it) } 43 | modifier("liquids") { this + TypeRegistry.resolve(it) } 44 | modifier("liquidFlammable") { this + TypeRegistry.resolve(it) } 45 | modifier("coolant") { this + TypeRegistry.resolve(it) } 46 | 47 | modifier("clearPower") { filterNot { it is ConsumePower }.toTypedArray() } 48 | modifier("power") { 49 | this.filterNot { c -> c is ConsumePower }.toTypedArray() + TypeRegistry.resolve(it) 50 | } 51 | modifier("powerBuffered") { 52 | this.filterNot { c -> c is ConsumePower }.toTypedArray() + ConsumePower(0f, it.asFloat(), true) 53 | } 54 | } 55 | 56 | override fun resolveType(json: JsonValue, type: Class?, elementType: Class<*>?, keyType: Class<*>?): T? { 57 | @Suppress("UNCHECKED_CAST") 58 | when { 59 | type == ConsumeItems::class.java && json.isArray -> 60 | return ConsumeItems(TypeRegistry.resolve(json)) as T 61 | 62 | type == ConsumeLiquids::class.java && json.isArray -> 63 | return ConsumeLiquids(TypeRegistry.resolve(json)) as T 64 | 65 | type == ConsumePower::class.java && json.isNumber -> 66 | return ConsumePower(json.asFloat(), 0.0f, false) as T 67 | 68 | } 69 | return null 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/resolvers/MindustryContentsResolver.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker.resolvers 2 | 3 | import arc.struct.OrderedMap 4 | import arc.util.Strings 5 | import cf.wayzer.contentsTweaker.CTNode 6 | import cf.wayzer.contentsTweaker.CTNodeTypeChecked 7 | import cf.wayzer.contentsTweaker.ContentsTweaker 8 | import cf.wayzer.contentsTweaker.checkObjInfoOrNull 9 | import cf.wayzer.contentsTweaker.util.reflectDelegate 10 | import mindustry.Vars 11 | import mindustry.ctype.Content 12 | import mindustry.ctype.ContentType 13 | import mindustry.ctype.MappableContent 14 | import mindustry.ctype.UnlockableContent 15 | import mindustry.world.Block 16 | import mindustry.world.meta.Stats 17 | 18 | object MindustryContentsResolver : ContentsTweaker.NodeCollector { 19 | private val Block.barMap: OrderedMap<*, *> by reflectDelegate() 20 | private val healthReload = CTNode.AfterHandler { 21 | if (Vars.world.isGenerating) return@AfterHandler 22 | Vars.world.tiles.forEach { 23 | val build = it.build ?: return@forEach 24 | val oldMax = build.maxHealth 25 | val max = build.block.health.toFloat() 26 | if (oldMax == max) return@forEach 27 | build.maxHealth = max 28 | if (build.health == oldMax) 29 | build.health = max 30 | } 31 | } 32 | 33 | private val contentNodes = mutableMapOf() 34 | override fun collectChild(node: CTNode) { 35 | if (node == CTNode.Root) 36 | return node.rootAddContentTypes() 37 | node.checkObjInfoOrNull()?.extendContents() 38 | node.checkObjInfoOrNull()?.extend() 39 | } 40 | 41 | private fun CTNode.rootAddContentTypes() { 42 | contentNodes.clear() 43 | ContentType.all.forEach { 44 | getOrCreate(it.name) += CTNode.ObjInfo(it, ContentType::class.java) 45 | } 46 | } 47 | 48 | private fun CTNodeTypeChecked.extendContents() { 49 | val type = objInfo.obj 50 | Vars.content.getBy(type).forEach { 51 | val name = if (it is MappableContent) it.name else "#${it.id}" 52 | node.getOrCreate(name).apply { 53 | contentNodes[it] = this 54 | +CTNode.ObjInfo(it) 55 | +CTNode.AfterHandler { 56 | if (it is UnlockableContent) { 57 | it.stats = Stats().apply { 58 | useCategories = it.stats.useCategories 59 | } 60 | } 61 | if (it is Block) { 62 | it.barMap.clear() 63 | it.setBars() 64 | } 65 | } 66 | +healthReload 67 | } 68 | } 69 | node += CTNode.IndexableRaw { 70 | val normalize = Strings.camelToKebab(it) 71 | node.children[normalize] 72 | } 73 | // if (type == ContentType.block) 74 | // node += CTNode.AfterHandler { 75 | // ContentsTweaker.reloadWorld() 76 | // } 77 | } 78 | 79 | private fun CTNodeTypeChecked.extend() { 80 | if (contentNodes[objInfo.obj] === node) return 81 | node += CTNode.ToJson { 82 | val content = objInfo.obj 83 | val nameOrId = if (content is MappableContent) content.name else content.id.toString() 84 | it.value("${content.contentType}#${nameOrId}") 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/resolvers/ObjectMapResolver.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker.resolvers 2 | 3 | import arc.struct.ObjectMap 4 | import arc.util.serialization.JsonValue 5 | import cf.wayzer.contentsTweaker.* 6 | 7 | object ObjectMapResolver : ContentsTweaker.NodeCollector { 8 | override fun collectChild(node: CTNode) { 9 | node.checkObjInfoOrNull>()?.extend() 10 | } 11 | 12 | private fun CTNodeTypeChecked>.extend() { 13 | val map = objInfo.obj 14 | map.forEach { 15 | node.getOrCreate("#" + TypeRegistry.getKeyString(it.key)).apply { 16 | +CTNode.ObjInfo(it.value) 17 | +object : CTNode.Modifiable(node) { 18 | override val currentValue: Any get() = map[it.key] 19 | override fun setValue0(value: Any?) { 20 | if (value == null) map.remove(it.key) 21 | else map.put(it.key, value) 22 | } 23 | } 24 | } 25 | } 26 | 27 | val keyType = objInfo.keyType ?: (map.keys().firstOrNull()?.javaClass) 28 | node += object : CTNode.Indexable { 29 | override fun resolveIndex(key: String): CTNode? { 30 | val keyV = TypeRegistry.resolveType(JsonValue(key), keyType) 31 | val value = map.get(keyV) ?: return null 32 | return node.getOrCreate("#" + TypeRegistry.getKeyString(keyV)).apply { 33 | extendOnce>(CTNode.ObjInfo(value)) 34 | } 35 | } 36 | 37 | override fun resolve(name: String): CTNode? { 38 | //后向兼容 39 | return super.resolve(name) ?: runCatching { resolveIndex(name) }.getOrNull() 40 | } 41 | } 42 | modifier("-") { 43 | val key = TypeRegistry.resolveType(it, keyType) 44 | map.copy().apply { remove(key) } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/resolvers/ReflectResolver.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker.resolvers 2 | 3 | import cf.wayzer.contentsTweaker.CTNode 4 | import cf.wayzer.contentsTweaker.ContentsTweaker 5 | import cf.wayzer.contentsTweaker.getObjInfo 6 | import mindustry.io.JsonIO 7 | import java.lang.reflect.Field 8 | import java.lang.reflect.Modifier 9 | 10 | object ReflectResolver : ContentsTweaker.NodeCollector { 11 | override fun collectChild(node: CTNode) { 12 | val objInfo = node.getObjInfo() ?: return 13 | if (objInfo.obj == null) return 14 | extend(node, objInfo) 15 | } 16 | 17 | fun extend(node: CTNode, objInfo: CTNode.ObjInfo<*>, filter: (Field) -> Boolean = { Modifier.isPublic(it.modifiers) }) { 18 | val obj = objInfo.obj ?: return 19 | val fields = runCatching { JsonIO.json.getFields(objInfo.type) }.getOrNull() ?: return 20 | for (entry in fields) { 21 | if (!filter(entry.value.field)) continue 22 | val meta = entry.value 23 | node.getOrCreate(entry.key).apply { 24 | var cls = meta.field.type 25 | if (cls.isAnonymousClass) cls = cls.superclass 26 | +CTNode.ObjInfo(meta.field.get(obj), cls, meta.elementType, meta.keyType) 27 | +object : CTNode.Modifiable(this) { 28 | override val currentValue: Any? get() = meta.field.get(obj) 29 | override fun setValue0(value: Any?) { 30 | meta.field.set(obj, value) 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/resolvers/SeqResolver.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker.resolvers 2 | 3 | import arc.struct.Seq 4 | import cf.wayzer.contentsTweaker.* 5 | 6 | object SeqResolver : ContentsTweaker.NodeCollector { 7 | override fun collectChild(node: CTNode) { 8 | node.checkObjInfoOrNull>()?.extend() 9 | } 10 | 11 | private fun CTNodeTypeChecked>.extend() { 12 | val list = objInfo.obj 13 | list.forEachIndexed { index, item -> 14 | node.getOrCreate("#$index") += CTNode.ObjInfo(item) 15 | //不支持 Modifiable,因为在对象变化后难以获取当前索引。修改某项应该使用`-`和`+`运算配合 16 | } 17 | node += object : CTNode.Indexable { 18 | override fun resolveIndex(key: String): CTNode? { 19 | val i = key.toInt() 20 | if (i >= list.size) return null 21 | return node.getOrCreate("#${i}").apply { 22 | extendOnce>(CTNode.ObjInfo(list.get(i))) 23 | } 24 | } 25 | 26 | override fun resolve(name: String): CTNode? { 27 | super.resolve(name)?.let { return null } 28 | //后向兼容 29 | if (name.toIntOrNull() != null) 30 | return resolveIndex(name) 31 | return null 32 | } 33 | } 34 | modifier("-") { json -> 35 | val item = if (json.isNumber && json.asInt() < list.size) { 36 | list[json.asInt()]//删除原始数组中对应索引的元素 37 | } else { 38 | TypeRegistry.resolveType(json, objInfo.elementType) 39 | } 40 | copy().apply { remove(item) } 41 | } 42 | modifier("+") { json -> 43 | val value = TypeRegistry.resolveType(json, objInfo.elementType) 44 | copy().apply { add(value) } 45 | } 46 | modifier("+=") { json -> 47 | val value = TypeRegistry.resolveType(json, objInfo.type, objInfo.elementType) 48 | copy().apply { addAll(value) } 49 | } 50 | node.getOrCreate("items") += CTNode.ToJson { 51 | it.value("...") 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/resolvers/UIExtResolver.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker.resolvers 2 | 3 | import arc.Core 4 | import arc.func.Cons 5 | import arc.input.KeyCode 6 | import arc.math.geom.Vec2 7 | import arc.scene.Element 8 | import arc.scene.Group 9 | import arc.scene.event.ClickListener 10 | import arc.scene.event.InputEvent 11 | import arc.scene.event.InputListener 12 | import arc.scene.style.Drawable 13 | import arc.scene.ui.Button 14 | import arc.scene.ui.Label 15 | import arc.scene.ui.Label.LabelStyle 16 | import arc.scene.ui.ScrollPane 17 | import arc.scene.ui.TextButton 18 | import arc.scene.ui.layout.Cell 19 | import arc.scene.ui.layout.Table 20 | import arc.util.Align 21 | import arc.util.Tmp 22 | import cf.wayzer.contentsTweaker.* 23 | import mindustry.Vars 24 | import mindustry.gen.Call 25 | import mindustry.ui.Styles 26 | 27 | /** 28 | * UIExt功能 29 | * 主体类似一个Map 30 | * 往节点下增加元素,即在scene新增UI组件 31 | * 32 | * 子节点: 33 | * * #id -> 获取UINode 34 | * * +type#id 获取或创建UINode 35 | * * "-" #id 删除ui节点 36 | * 37 | * 以实现infoPopup为例 38 | * ```json5 39 | * { 40 | * "uiExt.+Table#scoreboard": { 41 | * fillParent: true 42 | * touchable: disabled 43 | * align: 1 44 | * //"actions.+": [{delay:5},{remove:true}] //暂不支持 45 | * "+Table#bg": { 46 | * touchable: enabled 47 | * style: black3, 48 | * pad: [top,left,bottom,right], 49 | * onClick: "/help" //action为往服务器发送信息 50 | * margin: 4 51 | * "+Label#label": { 52 | * text: "Hello world" 53 | * style: outlineLabel 54 | * } 55 | * } 56 | * } 57 | * } 58 | * ``` 59 | */ 60 | object UIExtResolver : ContentsTweaker.NodeCollector { 61 | override fun collectChild(node: CTNode) { 62 | if (Vars.headless) { 63 | if (node == CTNode.Root) 64 | node.children["uiExt"] = CTNode.Nope 65 | return 66 | } 67 | if (node == CTNode.Root) { 68 | node.getOrCreate("uiExt").apply { 69 | +CTNode.ObjInfo(Core.scene.root) 70 | CTNode.PatchHandler.resetHandlers += CTNode.Resettable { 71 | children.values.forEach { it.getObjInfo()?.obj?.remove() } 72 | } 73 | } 74 | return 75 | } 76 | node.checkObjInfoOrNull()?.extend() 77 | node.checkObjInfoOrNull()?.extendTable() 78 | node.checkObjInfoOrNull>()?.extendCell() 79 | } 80 | 81 | private fun CTNodeTypeChecked.extend() { 82 | val obj = objInfo.obj 83 | node += CTNode.IndexableRaw { name -> 84 | if (name.length < 2 || name[0] != '+') return@IndexableRaw null 85 | val idStart = name.indexOf('#') 86 | check(idStart > 0) { "Must provide element id" } 87 | val type = name.substring(1, idStart) 88 | val id = name.substring(idStart) 89 | node.children[id] ?: createUIElement(type).let { element -> 90 | node.getOrCreate(id).apply { 91 | +CTNode.ObjInfo(element) 92 | when (obj) { 93 | is Table -> { 94 | val cell = obj.add(element) 95 | getOrCreate("cell") += CTNode.ObjInfo(cell) 96 | } 97 | 98 | is Group -> obj.addChild(element) 99 | else -> error("Only Group can add child element") 100 | } 101 | } 102 | } 103 | } 104 | node.getOrCreate("+") += CTNode.Modifier { 105 | val type = it.remove("type")?.asString() ?: error("Must provide Element type") 106 | val child = node.resolve("+$type#${it.getString("name")}") 107 | CTNode.PatchHandler.handle(it, child) 108 | } 109 | node.getOrCreate("-") += CTNode.Modifier { 110 | val id = it.asString() 111 | check(id.startsWith('#')) { "Must provide element #id" } 112 | node.children[id]?.getObjInfo()?.obj?.remove() 113 | node.children.remove(id) 114 | } 115 | extendModifiers() 116 | } 117 | 118 | private fun CTNodeTypeChecked
.extendTable() { 119 | val obj = objInfo.obj 120 | node.getOrCreate("cellDefaults") += CTNode.ObjInfo(obj.defaults()) 121 | node.getOrCreate("row") += CTNode.Modifier { 122 | if (obj.cells.peek()?.isEndRow == false) 123 | obj.row() 124 | } 125 | node.getOrCreate("align") += CTNode.Modifier { 126 | val v = if (it.isNumber) it.asInt() else alignMap[it.asString()] ?: error("invalid align: $it") 127 | obj.align(v) 128 | } 129 | node.getOrCreate("margin") += CTNode.Modifier { json -> 130 | val v = if (json.isNumber) json.asFloat().let { v -> FloatArray(4) { v } } 131 | else json.asFloatArray()?.takeIf { it.size == 4 } ?: error("invalid margin: $json") 132 | obj.margin(v[0], v[1], v[2], v[3]) 133 | } 134 | } 135 | 136 | class DragHandler(private val element: Element) : InputListener() { 137 | private val last = Vec2() 138 | override fun touchDown(event: InputEvent, x: Float, y: Float, pointer: Int, button: KeyCode?): Boolean { 139 | event.stop() 140 | last.set(x, y) 141 | return true 142 | } 143 | 144 | override fun touchDragged(event: InputEvent, x: Float, y: Float, pointer: Int) { 145 | event.stop() 146 | val v = element.localToStageCoordinates(Tmp.v1.set(x, y)) 147 | element.setPosition(v.x - last.x, v.y - last.y) 148 | element.keepInStage() 149 | } 150 | } 151 | 152 | private fun CTNodeTypeChecked.extendModifiers() { 153 | val obj = objInfo.obj 154 | node.getOrCreate("draggable") += CTNode.Modifier { json -> 155 | obj.listeners.removeAll { it is DragHandler } 156 | if (json.asBoolean()) 157 | obj.addListener(DragHandler(obj)) 158 | } 159 | node.getOrCreate("onClick") += CTNode.Modifier { json -> 160 | val message = json.asString() 161 | obj.listeners.removeAll { it is ClickListener } 162 | obj.clicked(Cons {}, Cons { Call.sendChatMessage(message) }) 163 | } 164 | 165 | node.getOrCreate("style") += CTNode.Modifier { 166 | val style = stylesMap[it.asString()] ?: error("style not found: $it") 167 | when (obj) { 168 | is Label -> obj.style = style as? LabelStyle ?: error("invalid style: $it") 169 | is Button -> obj.style = style as? Button.ButtonStyle ?: error("invalid style: $it") 170 | is ScrollPane -> obj.style = style as? ScrollPane.ScrollPaneStyle ?: error("invalid style: $it") 171 | is Table -> obj.background = style as? Drawable ?: error("invalid style: $it") 172 | else -> error("TODO: style only support Table,Label,Button,ScrollPane") 173 | } 174 | } 175 | 176 | if (obj is Label || obj is TextButton) 177 | node.getOrCreate("text") += CTNode.Modifier { 178 | val v = it.asString() 179 | when (obj) { 180 | is Label -> obj.setText(v) 181 | is TextButton -> obj.setText(v) 182 | } 183 | } 184 | 185 | if (obj is Label) { 186 | node.getOrCreate("fontScale") += CTNode.Modifier { json -> 187 | val v = if (json.isNumber) json.asFloat().let { v -> FloatArray(2) { v } } 188 | else json.asFloatArray()?.takeIf { it.size == 2 } ?: error("invalid fontScale: $json") 189 | obj.setFontScale(v[0], v[1]) 190 | } 191 | node.getOrCreate("fontScaleX") += CTNode.Modifier { 192 | obj.fontScaleX = it.asFloat() 193 | } 194 | node.getOrCreate("fontScaleY") += CTNode.Modifier { 195 | obj.fontScaleY = it.asFloat() 196 | } 197 | } 198 | } 199 | 200 | //Reference arc.scene.ui.layout.Cell.clear 201 | private val cellFields = setOf( 202 | "minWidth", "maxWidth", "minHeight", "maxHeight", 203 | "padTop", "padLeft", "padBottom", "padRight", 204 | "fillX", "fillY", "expandX", "expandY", "uniformX", "uniformY", "align", "colspan", 205 | ) 206 | 207 | private fun CTNodeTypeChecked>.extendCell() { 208 | ReflectResolver.extend(node, objInfo) { it.name in cellFields } 209 | node.getOrCreate("pad") += CTNode.Modifier { json -> 210 | val v = if (json.isNumber) json.asFloat().let { v -> FloatArray(4) { v } } 211 | else json.asFloatArray()?.takeIf { it.size == 4 } ?: error("invalid pad: $json") 212 | objInfo.obj.pad(v[0], v[1], v[2], v[3]) 213 | } 214 | node.getOrCreate("align") += CTNode.Modifier { 215 | val v = if (it.isNumber) it.asInt() else alignMap[it.asString()] ?: error("invalid align: $it") 216 | objInfo.obj.align(v) 217 | } 218 | } 219 | 220 | private val alignMap by lazy { Align::class.java.declaredFields.associate { it.name to it.getInt(null) } } 221 | private val stylesMap by lazy { Styles::class.java.declaredFields.associate { it.name to it.get(null)!! } } 222 | fun createUIElement(type: String): Element = when (type) { 223 | "Table" -> Table() 224 | "Label" -> Label("") 225 | "TextButton" -> TextButton("") 226 | else -> error("TODO: not support Element: $type") 227 | } 228 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/util/ExtendableClass.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker.util 2 | 3 | @DslMarker 4 | annotation class ExtendableClassDSL 5 | 6 | @ExtendableClassDSL 7 | abstract class ExtendableClass { 8 | val mixins = mutableListOf() 9 | 10 | @ExtendableClassDSL 11 | inline fun get(): T? { 12 | if (this is T) return this 13 | return mixins.filterIsInstance().let { 14 | require(it.size <= 1) { "More than one ${T::class.java} mixin: $it" } 15 | it.firstOrNull() 16 | } 17 | } 18 | 19 | @ExtendableClassDSL 20 | inline fun getAll(): List { 21 | return mixins.filterIsInstance().let { 22 | if (this is T) return it + this else it 23 | } 24 | } 25 | 26 | @ExtendableClassDSL 27 | operator fun Ext.unaryPlus() { 28 | mixins.add(this) 29 | } 30 | 31 | @ExtendableClassDSL 32 | operator fun plusAssign(ext: Ext) { 33 | mixins.add(ext) 34 | } 35 | 36 | @ExtendableClassDSL 37 | inline fun extendOnce(ext: Ext) { 38 | if (get() != null) return 39 | mixins.add(ext) 40 | } 41 | } -------------------------------------------------------------------------------- /src/main/kotlin/cf/wayzer/contentsTweaker/util/ReflectHelper.kt: -------------------------------------------------------------------------------- 1 | package cf.wayzer.contentsTweaker.util 2 | 3 | import java.lang.reflect.Field 4 | import kotlin.properties.PropertyDelegateProvider 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() = PropertyDelegateProvider> { _, property -> 16 | val field = T::class.java.getDeclaredField(property.name) 17 | field.isAccessible = true 18 | ReflectDelegate(field, R::class.java) 19 | } -------------------------------------------------------------------------------- /src/main/resources/mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ContentsTweaker", 3 | "displayName": "ContentsTweaker-内容包修改器", 4 | "author": "WayZer", 5 | "main": "cf.wayzer.contentsTweaker.ModMain", 6 | "description": "Dynamic load custom contents patch account to server", 7 | "version": "@version@", 8 | "hidden": true, 9 | "minGameVersion": 136 10 | } 11 | --------------------------------------------------------------------------------