├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
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 | [](https://github.com/way-zer/ContentsTweaker/actions/workflows/build.yml)
2 | [](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 |
--------------------------------------------------------------------------------
| |