├── .idea
├── .name
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── vcs.xml
├── kotlinc.xml
├── modules
│ ├── MindustryContentsTweaker.test.iml
│ ├── core
│ │ └── MindustryContents.core.test.iml
│ ├── contents
│ │ ├── MindustryContents.contents.test.iml
│ │ ├── flood
│ │ │ └── MindustryContents.contents.flood.test.iml
│ │ └── origin
│ │ │ └── MindustryContents.contents.origin.test.iml
│ └── loaderMod
│ │ └── MindustryContents.loaderMod.test.iml
├── .gitignore
├── markdown.xml
├── compiler.xml
├── jarRepositories.xml
└── misc.xml
├── settings.gradle.kts
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── mod.json
├── src
└── main
│ ├── resources
│ └── mod.json
│ └── kotlin
│ └── cf
│ └── wayzer
│ └── contentsTweaker
│ ├── util
│ ├── ReflectHelper.kt
│ └── ExtendableClass.kt
│ ├── CTNodeHelper.kt
│ ├── resolvers
│ ├── BetterJsonResolver.kt
│ ├── ReflectResolver.kt
│ ├── ArrayResolver.kt
│ ├── SeqResolver.kt
│ ├── ObjectMapResolver.kt
│ ├── MindustryContentsResolver.kt
│ ├── BlockConsumesResolver.kt
│ └── UIExtResolver.kt
│ ├── ModMain.kt
│ ├── TypeRegistry.kt
│ ├── ContentsTweaker.kt
│ └── CTNode.kt
├── .github
├── workflows
│ ├── changeLog.yml
│ ├── build.yml
│ └── release.yml
└── release-drafter.yml
├── .gitignore
├── README.md
├── gradlew.bat
├── gradlew
└── contents
└── flood.json
/.idea/.name:
--------------------------------------------------------------------------------
1 | ContentsTweaker
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "ContentsTweaker"
--------------------------------------------------------------------------------
/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/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules/MindustryContentsTweaker.test.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules/core/MindustryContents.core.test.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.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/modules/contents/MindustryContents.contents.test.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules/loaderMod/MindustryContents.loaderMod.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 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl = https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl = true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/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": "3.1.0",
8 | "hidden": true,
9 | "minGameVersion": 147
10 | }
11 |
--------------------------------------------------------------------------------
/.idea/markdown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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": 149
10 | }
11 |
--------------------------------------------------------------------------------
/.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@v4
17 |
18 | - uses: release-drafter/release-drafter@v5
19 | env:
20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.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@v4
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@v4
24 | with:
25 | path: build/dist/*
--------------------------------------------------------------------------------
/.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@v4
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@v2
28 | with:
29 | files: build/dist/*
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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)
32 | val elementType = objInfo.elementType
33 | if (value == null && elementType == null) return null
34 | return node.getOrCreate("#" + TypeRegistry.getKeyString(keyV)).apply {
35 | if (value != null) {
36 | extendOnce>(CTNode.ObjInfo(value))
37 | } else {
38 | extendOnce>(CTNode.ObjInfo(null, elementType!!))
39 | }
40 | }
41 | }
42 |
43 | override fun resolve(name: String): CTNode? {
44 | //后向兼容
45 | return super.resolve(name) ?: runCatching { resolveIndex(name) }.getOrNull()
46 | }
47 | }
48 | modifier("-") {
49 | val key = TypeRegistry.resolveType(it, keyType)
50 | map.copy().apply { remove(key) }
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/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/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/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 | }
--------------------------------------------------------------------------------
/.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/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 |
--------------------------------------------------------------------------------
/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 | import java.util.*
10 |
11 | /**
12 | * 所有节点都是[CTNode]
13 | * 如果一个节点与某个具体对象有关,应当实现[ObjInfo]
14 | * [ObjInfo.obj]应当始终返回原始值(不管后来有没有修改过)
15 | * 如果一个节点绑定属性可被重新赋值,实现[Modifiable]
16 | * 如果一个节点是运算符节点(末端节点),可接收JSON对象,实现[Modifier]
17 | * [AfterHandler]将在节点或子节点[Modifiable.setValue]后注册,批处理结束后统一调用
18 | *
19 | * 所有节点,在获得实例,使用前必须调用[collectAll] ([ContentsTweaker.NodeCollector]内不需要)
20 | * */
21 | class CTNode private constructor() : ExtendableClass() {
22 | val children = mutableMapOf()
23 | private var collected = false
24 | fun collectAll(): CTNode {
25 | if (collected) return this
26 | collected = true
27 | for (resolver in resolvers) {
28 | kotlin.runCatching { resolver.collectChild(this) }.onFailure { e ->
29 | throw Exception("Fail to collectChild in $resolver", e)
30 | }
31 | }
32 | get>()?.let { modifiable ->
33 | getOrCreate("=").apply {
34 | +Modifier { json ->
35 | modifiable.setJson(json)
36 | }
37 | +ToJson {
38 | it.value(modifiable.currentValue?.let(TypeRegistry::getKeyString))
39 | }
40 | }
41 | }
42 | return this
43 | }
44 |
45 | fun resolve(name: String): CTNode {
46 | val node = children[name]
47 | ?: getAll().firstNotNullOfOrNull { it.resolve(name) }
48 | ?: error("Not found child $name")
49 | //If it's a duplicate Node for object, only Modifiers are accessible.
50 | if (get>()?.isDuplicated == true && node.get() == null)
51 | error("Duplicated Object Node, cannot visit children")
52 | return node.collectAll()
53 | }
54 |
55 | /** 供[ContentsTweaker.NodeCollector]使用,解析清使用[resolve]*/
56 | @ExtendableClassDSL
57 | fun getOrCreate(child: String): CTNode {
58 | return children.getOrPut(child) { CTNode() }
59 | }
60 |
61 | interface CTExtInfo
62 | data class ObjInfo(
63 | val obj: T, val type: Class,
64 | val elementType: Class<*>? = null, // List.T or Map.V
65 | val keyType: Class<*>? = null // Map.K
66 | ) : CTExtInfo {
67 | val isDuplicated = duplicatedObjects.put(obj, Unit) != null
68 |
69 | constructor(obj: T & Any) : this(obj, obj::class.java)
70 |
71 | companion object {
72 | val duplicatedObjects = IdentityHashMap()
73 | }
74 | }
75 |
76 | fun interface Resettable : CTExtInfo {
77 | fun reset()
78 | }
79 |
80 | /** [T] must equal [ObjInfo.type]*/
81 | @Suppress("MemberVisibilityCanBePrivate")
82 | abstract class Modifiable(node: CTNode) : CTExtInfo, Resettable {
83 | val info = node.get>()!!
84 | internal var nodeStack: List? = null
85 | abstract val currentValue: T
86 | protected abstract fun setValue0(value: T)
87 | fun setValue(value: T) {
88 | PatchHandler.modified(this)
89 | setValue0(value)
90 | }
91 |
92 | fun setValueAny(value: Any?) {
93 | val type = info.type
94 |
95 | @Suppress("UNCHECKED_CAST")
96 | val v = (if (type.isPrimitive) value else type.cast(value)) as T
97 | setValue(v)
98 | }
99 |
100 | fun setJson(v: JsonValue) {
101 | val value = info.run { TypeRegistry.resolveType(v, type, elementType, keyType) }
102 | setValue(value)
103 | }
104 |
105 | override fun reset() = setValue(info.obj)
106 | }
107 |
108 | fun interface IndexableRaw : CTExtInfo {
109 | fun resolve(name: String): CTNode?
110 | }
111 |
112 | fun interface Indexable : IndexableRaw {
113 | /** 解析索引,[key]已去除# */
114 | fun resolveIndex(key: String): CTNode?
115 |
116 | /** 通用的[name]索引(不一定以#开头) */
117 | override fun resolve(name: String): CTNode? {
118 | if (name.isEmpty() || name[0] != '#') return null
119 | return resolveIndex(name.substring(1))
120 | }
121 | }
122 |
123 | fun interface Modifier : CTExtInfo {
124 | /**
125 | * 1. 判断状态(parent), 根据[ObjInfo.type],[ObjInfo.elementType],[ObjInfo.keyType]解析[json]
126 | * 2. 赋值直接调用[Modifiable.setValue]。如果是增量修改,使用[Modifiable.currentValue]获取读取值
127 | */
128 | @Throws(Throwable::class)
129 | fun setValue(json: JsonValue)
130 | }
131 |
132 | fun interface AfterHandler : CTExtInfo {
133 | fun handle()
134 | }
135 |
136 | /** 为[ContentsTweaker.exportAll]自定义输出 */
137 | fun interface ToJson : CTExtInfo {
138 | fun write(jsonWriter: JsonWriter)
139 | }
140 |
141 | companion object {
142 | private val resolvers = ContentsTweaker.resolvers
143 | val Root = CTNode()
144 | val Nope = CTNode()
145 |
146 | init {
147 | Nope.apply {
148 | +Modifier { }
149 | +IndexableRaw { Nope }
150 | }
151 | }
152 | }
153 |
154 | object PatchHandler {
155 | private object NodeStack {
156 | private val stack = mutableListOf>()
157 | val last get() = stack.last()
158 | fun getParents() = stack.map { it.first }
159 | fun getId(node: CTNode? = null) =
160 | stack.takeWhile { it.first !== node }.joinToString(".") { it.second }
161 |
162 | fun resolve(node: CTNode, child: String): CTNode {
163 | stack.add(node to child)
164 | return node.resolve(child)
165 | }
166 |
167 | fun pop(node: CTNode) {
168 | @Suppress("ControlFlowWithEmptyBody")
169 | while (stack.removeLast().first !== node);
170 | }
171 | }
172 |
173 | val resetHandlers = mutableSetOf()
174 | internal val afterHandlers = mutableSetOf()
175 |
176 | fun modified(modifiable: Modifiable<*>) {
177 | resetHandlers.add(modifiable)
178 | val parents = modifiable.nodeStack ?: NodeStack.getParents().also {
179 | modifiable.nodeStack = it
180 | }
181 | parents.flatMapTo(afterHandlers) { it.getAll() }
182 | }
183 |
184 | fun doAfterHandle() {
185 | afterHandlers.forEach { it.handle() }
186 | afterHandlers.clear()
187 | }
188 |
189 | fun recoverAll() {
190 | resetHandlers.forEach { it.reset() }
191 | resetHandlers.clear()
192 | doAfterHandle()
193 |
194 | Root.children.clear()
195 | Root.collected = false
196 | ObjInfo.duplicatedObjects.clear()
197 | }
198 |
199 | fun handle(json: JsonValue, node: CTNode = Root.collectAll()) {
200 | //部分简化,如果value不是object可省略=运算符
201 | if (!json.isObject && node.get() == null)
202 | return handle(json, NodeStack.resolve(node, "="))
203 | node.get()?.let {
204 | try {
205 | it.setValue(json)
206 | } catch (e: Throwable) {
207 | Log.err("Fail to apply Modifier ${NodeStack.getId()}: ${json.prettyPrint(JsonWriter.OutputType.minimal, 0)}:\n $e")
208 | }
209 | return
210 | }
211 |
212 | for (child in json) {
213 | val names = child.name.split(".")
214 | val childNode = try {
215 | names.fold(node, NodeStack::resolve)
216 | } catch (e: Throwable) {
217 | val (errNode, errChild) = NodeStack.last
218 | Log.err("Fail to resolve child ${NodeStack.getId(errNode)}->${errChild}:\n $e")
219 | NodeStack.pop(node)
220 | continue
221 | }
222 | handle(child, childNode)
223 | NodeStack.pop(node)
224 | }
225 | }
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH="\\\"\\\""
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | if ! command -v java >/dev/null 2>&1
137 | then
138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
139 |
140 | Please set the JAVA_HOME variable in your environment to match the
141 | location of your Java installation."
142 | fi
143 | fi
144 |
145 | # Increase the maximum file descriptors if we can.
146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
147 | case $MAX_FD in #(
148 | max*)
149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
150 | # shellcheck disable=SC2039,SC3045
151 | MAX_FD=$( ulimit -H -n ) ||
152 | warn "Could not query maximum file descriptor limit"
153 | esac
154 | case $MAX_FD in #(
155 | '' | soft) :;; #(
156 | *)
157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
158 | # shellcheck disable=SC2039,SC3045
159 | ulimit -n "$MAX_FD" ||
160 | warn "Could not set maximum file descriptor limit to $MAX_FD"
161 | esac
162 | fi
163 |
164 | # Collect all arguments for the java command, stacking in reverse order:
165 | # * args from the command line
166 | # * the main class name
167 | # * -classpath
168 | # * -D...appname settings
169 | # * --module-path (only if needed)
170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
171 |
172 | # For Cygwin or MSYS, switch paths to Windows format before running java
173 | if "$cygwin" || "$msys" ; then
174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
176 |
177 | JAVACMD=$( cygpath --unix "$JAVACMD" )
178 |
179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
180 | for arg do
181 | if
182 | case $arg in #(
183 | -*) false ;; # don't mess with options #(
184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
185 | [ -e "$t" ] ;; #(
186 | *) false ;;
187 | esac
188 | then
189 | arg=$( cygpath --path --ignore --mixed "$arg" )
190 | fi
191 | # Roll the args list around exactly as many times as the number of
192 | # args, so each arg winds up back in the position where it started, but
193 | # possibly modified.
194 | #
195 | # NB: a `for` loop captures its iteration list before it begins, so
196 | # changing the positional parameters here affects neither the number of
197 | # iterations, nor the values presented in `arg`.
198 | shift # remove old arg
199 | set -- "$@" "$arg" # push replacement arg
200 | done
201 | fi
202 |
203 |
204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
206 |
207 | # Collect all arguments for the java command:
208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
209 | # and any embedded shellness will be escaped.
210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
211 | # treated as '${Hostname}' itself on the command line.
212 |
213 | set -- \
214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
215 | -classpath "$CLASSPATH" \
216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
217 | "$@"
218 |
219 | # Stop when "xargs" is not available.
220 | if ! command -v xargs >/dev/null 2>&1
221 | then
222 | die "xargs is not available"
223 | fi
224 |
225 | # Use "xargs" to parse quoted args.
226 | #
227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
228 | #
229 | # In Bash we could simply go:
230 | #
231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
232 | # set -- "${ARGS[@]}" "$@"
233 | #
234 | # but POSIX shell has neither arrays nor command substitution, so instead we
235 | # post-process each arg (as a line of input to sed) to backslash-escape any
236 | # character that might be a shell metacharacter, then use eval to reverse
237 | # that process (while maintaining the separation between arguments), and wrap
238 | # the whole thing up as a single "set" statement.
239 | #
240 | # This will of course break if any of these variables contains a newline or
241 | # an unmatched quote.
242 | #
243 |
244 | eval "set -- $(
245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
246 | xargs -n1 |
247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
248 | tr '\n' ' '
249 | )" '"$@"'
250 |
251 | exec "$JAVACMD" "$@"
252 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
| |