-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #24 from way-zer/contents-patch
✨ (core) 增加有关`ContentsPatch`功能
- Loading branch information
Showing
5 changed files
with
256 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,36 +1,112 @@ | ||
[![Build](https://github.com/way-zer/MindustryContents/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/way-zer/MindustryContents/actions/workflows/build.yml) | ||
[![](https://jitpack.io/v/way-zer/MindustryContents.svg)](https://jitpack.io/#way-zer/MindustryContents) | ||
|
||
# MindustryContentsLoader | ||
|
||
一个`内容包`加载器的像素工厂MOD | ||
A Mindustry MOD to dynamicly load `Contents Pack` | ||
A Mindustry MOD to dynamicly load `Contents Pack` and `Contents Patch` | ||
|
||
## 功能 Features | ||
* 接受服务器指令,为加载下张地图时,更换指定的`内容包` | ||
* Receive Info from Server, load special `Contents Pack` when join server or change map. | ||
* 为其他MOD提供接口,提供动态加载`内容包`的能力 | ||
* Provide API for other mods, provide feature to dynamicly load `Contents Pack` | ||
|
||
* 接受服务器指令,为加载下张地图时,更换指定的`内容包`和`内容补丁` | ||
* Receive Info from Server, load special `Contents Pack` or `Contents Patch` when join server or change map. | ||
* 为其他MOD提供接口,提供动态加载`内容包`和`内容补丁`的能力 | ||
* Provide API for other mods, provide feature to dynamicly load `Contents Pack` or `Contents Patch` | ||
|
||
## 内容包定义 Defination for `Contents Pack` | ||
|
||
一组ContentList代码,没有属性,仅包含load函数,为原版Contents赋值 | ||
A group of ContentList code, NO member, ONLY functionn `load` to assign new instances to original contents. | ||
|
||
例子参见[洪水模式内容包](./contents/flood) | ||
For example, see [flood ContentPack](./contents/flood) | ||
|
||
### maven引用 import from maven | ||
|
||
供插件或其他项目引用 | ||
For plugin or other projects to import. | ||
|
||
```groovy | ||
repositories { | ||
maven { url 'https://jitpack.io' } | ||
repositories{ | ||
maven{ url 'https://jitpack.io' } | ||
} | ||
dependencies { | ||
dependencies{ | ||
implementation 'cf.wayzer.MindustryContents:CONTENT_TYPE:VERSION' | ||
} | ||
``` | ||
|
||
* VERSION: 编译版本号 build version | ||
* CONTENT_TYPE: 内容包类型(如flood) name of `Contents Pack`(ex `flood`) | ||
|
||
## 内容补丁 Defination for `Contents Patch` | ||
|
||
一个(h)json文件,可以修改游戏内所有物品的属性 | ||
A (h)json file. According to it modify all contents property. | ||
|
||
客户端将会自动加载`config/contents-patch/default.(h)json`文件(如果存在), | ||
并且根据地图信息或服务器指令,加载补丁(如果不存在,会自动从服务器下载) | ||
Client will auto load patch in `config/contents-patch/default.(h)json` (if exists) | ||
And will load patch according to map info or server command.(May auto download patch for server) | ||
|
||
### 与内容包对比 vs `Contents Pack` | ||
|
||
内容补丁为新功能,相比有诸多优势。支持服务器发送,支持离线游玩,抗sync,支持游戏中加载(无需重新加载地图)。 | ||
以及更好的Mod兼容性,使用Patch甚至可以修改其他Mod的属性。 | ||
`Contents Patch` is a newer feature, many advantages: Support offline, Anti `sync`, Load when play(no need to reload world). | ||
And better mod compatibility, you can even patch other mod content. | ||
|
||
局限性:不能修改物品的类型; 不能使用java代码 | ||
Limit: Can't change content type; Can't use java code. | ||
|
||
### 示例 Exmaple | ||
|
||
```json5 | ||
{ | ||
//ContentType | ||
block: { | ||
//Content name | ||
"copper-wall-large": { | ||
//Property to modify | ||
//Value is format of origin json | ||
"health": 1200 | ||
}, | ||
"phase-wall": { | ||
"chanceDeflect": 0, | ||
"absorbLasers": true | ||
}, | ||
"force-projector": { | ||
"shieldHealth": 2500 | ||
}, | ||
"lancer": { | ||
"shootType.damage": 30, | ||
"requirements": [ | ||
"copper/10", | ||
"lead/100" | ||
] | ||
}, | ||
}, | ||
} | ||
``` | ||
|
||
### 网络协议 Protocal | ||
|
||
* map tag: `ContentsPatch` | ||
地图所需patch列表 List of patch names | ||
例(For example): `flood;patchA;xxxx` | ||
* S->C ContentsLoader|loadPatch | ||
命令客户端加载一个补丁(传递参数: 仅name) | ||
command client to load a patch (param only name) | ||
* C-> ContentsLoader|requestPatch | ||
客户端找不到时,向服务器请求patch(传递参数: 仅name) | ||
send when client not found this patch locally (param only name) | ||
* S->C ContentsLoader|newPatch | ||
命令客户端加载一个新补丁,通常作为`requestPatch`的回复(传递参数: name & content) | ||
command client to load a patch, normally as respond of `requestPatch` (params: name & content) | ||
|
||
通常来说,补丁名应该为其内容的hash,方便客户端进行缓存. | ||
Normally patch's name should be a hash of content, which can be cached currently. | ||
|
||
## 安装 Setup | ||
|
||
安装Release中的MOD即可(多人游戏兼容) | ||
Install mod in Release(multiplayer compatible) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package cf.wayzer | ||
|
||
import arc.util.Log | ||
import arc.util.Strings | ||
import arc.util.serialization.Json | ||
import arc.util.serialization.JsonValue | ||
import arc.util.serialization.JsonWriter | ||
import cf.wayzer.util.reflectDelegate | ||
import mindustry.Vars | ||
import mindustry.content.Bullets | ||
import mindustry.ctype.Content | ||
import mindustry.ctype.ContentType | ||
import mindustry.entities.bullet.BulletType | ||
import mindustry.io.JsonIO | ||
import mindustry.mod.ContentParser | ||
import mindustry.mod.Mods | ||
import mindustry.type.Item | ||
import mindustry.world.consumers.* | ||
import java.lang.reflect.Field | ||
|
||
object ContentsPatcher { | ||
private val Mods.parser: ContentParser by reflectDelegate() | ||
private val ContentParser.parser: Json by reflectDelegate() | ||
private val json by lazy { Vars.mods.parser.parser } | ||
private val bulletMap by lazy { | ||
Bullets::class.java.fields | ||
.filter { it.type == BulletType::class.java } | ||
.associate { it.name to it.get(null) as BulletType } | ||
} | ||
|
||
|
||
private fun findContent(type: ContentType, name: String): Content { | ||
return when (type) { | ||
ContentType.bullet -> bulletMap[Strings.kebabToCamel(name)] | ||
else -> Vars.content.getByName(type, Strings.camelToKebab(name)) | ||
} ?: error("Not found $type : $name") | ||
} | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
fun <T> readType(cls: Class<T>, jsonValue: JsonValue): T { | ||
return when (cls) { | ||
Consumers::class.java -> Consumers().apply { | ||
jsonValue.forEach { child -> | ||
when (child.name) { | ||
"item" -> item(findContent(ContentType.item, child.asString()) as Item) | ||
"items" -> add(readType(ConsumeItems::class.java, child)) | ||
"liquid" -> add(readType(ConsumeLiquid::class.java, child)) | ||
"coolant" -> add(readType(ConsumeCoolant::class.java, child)) | ||
"power" -> { | ||
if (child.isNumber) power(child.asFloat()) | ||
else add(readType(ConsumePower::class.java, child)) | ||
} | ||
"powerBuffered" -> powerBuffered(child.asFloat()) | ||
else -> error("Unknown consumption type: ${child.name}") | ||
} | ||
} | ||
init() | ||
} as T | ||
else -> json.readValue(cls, jsonValue) | ||
} | ||
} | ||
|
||
private val bakField = mutableMapOf<String, () -> Unit>() | ||
private fun handleContent(type: String, value: JsonValue) { | ||
val content: Any = findContent(ContentType.valueOf(type), value.name) | ||
value.forEach { prop -> | ||
val id = "$type.${value.name}.${prop.name}" | ||
try { | ||
val (obj, field) = resolveObj(content, prop.name) | ||
val bakV = field.get(obj) | ||
bakField.putIfAbsent(id) { field.set(obj, bakV) } | ||
field.set(obj, readType(field.type, prop)) | ||
Log.info("Load Content $id = ${prop.toJson(JsonWriter.OutputType.javascript)}") | ||
} catch (e: Throwable) { | ||
Log.err("Fail to handle Content \"$id\"", e) | ||
} | ||
} | ||
} | ||
|
||
private val fieldCache = mutableMapOf<Pair<Class<*>, String>, Field>() | ||
private fun getField(obj: Any, name: String): Field { | ||
var cls = obj.javaClass | ||
if (cls.isAnonymousClass) | ||
cls = cls.superclass | ||
return fieldCache.getOrPut(cls to name) { | ||
cls.getField(name).apply { | ||
if (name == "consumes") | ||
isAccessible = true | ||
} | ||
} | ||
} | ||
|
||
private fun resolveObj(obj: Any, key: String): Pair<Any, Field> { | ||
var o = obj | ||
var field: Field? = null | ||
for (it in key.split(".")) { | ||
if (field != null) o = field.get(o) | ||
field = getField(o, it) | ||
} | ||
return o to field!! | ||
} | ||
|
||
object Api { | ||
const val tagName = "ContentsPatch" | ||
fun load(text: String) { | ||
val json = JsonIO.read<JsonValue>(null, text) | ||
json.forEach { type -> | ||
type.forEach { content -> | ||
handleContent(type.name, content) | ||
} | ||
} | ||
} | ||
|
||
fun reset() { | ||
bakField.values.forEach { it.invoke() } | ||
bakField.clear() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package cf.wayzer.util | ||
|
||
import java.lang.reflect.Field | ||
import kotlin.properties.PropertyDelegateProvider | ||
import kotlin.properties.ReadWriteProperty | ||
import kotlin.reflect.KProperty | ||
|
||
class ReflectDelegate<T, R>( | ||
private val field: Field, private val cls: Class<R> | ||
) : ReadWriteProperty<T?, R> { | ||
override fun getValue(thisRef: T?, property: KProperty<*>): R = cls.cast(field.get(thisRef)) | ||
override fun setValue(thisRef: T?, property: KProperty<*>, value: R) = field.set(thisRef, value) | ||
} | ||
|
||
inline fun <reified T, reified R> reflectDelegate() = PropertyDelegateProvider<Any?, ReflectDelegate<T, R>> { _, property -> | ||
val field = T::class.java.getDeclaredField(property.name) | ||
field.isAccessible = true | ||
ReflectDelegate(field, R::class.java) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters