Skip to content

Commit

Permalink
Merge pull request #24 from way-zer/contents-patch
Browse files Browse the repository at this point in the history
✨ (core) 增加有关`ContentsPatch`功能
  • Loading branch information
way-zer authored May 3, 2022
2 parents 1009d31 + 2baee8d commit 2e084d6
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 10 deletions.
92 changes: 84 additions & 8 deletions README.md
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)
2 changes: 1 addition & 1 deletion core/src/cf/wayzer/ContentsLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ object ContentsLoader : ContentLoader() {
"origin" to { contents.forEach { it.content = it.default } }
)
val toLoadPacks = mutableListOf<String>()
var lastLoadedPacks = listOf<String>()
var lastLoadedPacks = listOf("origin")
private set

//platform impl
Expand Down
119 changes: 119 additions & 0 deletions core/src/cf/wayzer/ContentsPatcher.kt
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()
}
}
}
19 changes: 19 additions & 0 deletions core/src/cf/wayzer/util/reflect.kt
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)
}
34 changes: 33 additions & 1 deletion loaderMod/src/main/kotlin/cf/wayzer/contentsMod/Main.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package cf.wayzer.contentsMod

import Contents
import arc.Events
import arc.func.Cons
import arc.func.Prov
import arc.struct.ObjectMap
import arc.struct.Seq
import arc.util.Log
import cf.wayzer.ContentsLoader
import cf.wayzer.ContentsPatcher
import mindustry.Vars
import mindustry.game.EventType.PlayEvent
import mindustry.game.EventType.ResetEvent
import mindustry.gen.Call
import mindustry.gen.ClientPacketReliableCallPacket
import mindustry.mod.Mod
Expand Down Expand Up @@ -55,13 +59,41 @@ class Main : Mod() {

Vars.netClient.addPacketHandler("ContentsLoader|load", ContentsLoader.Api.toLoadPacks::add)
Log.infoTag("ContentsLoader", "Finish Load Mod")
registerContentsParser()
}

fun beforeWorldLoad() {
Log.infoTag("ContentsLoader", "ToLoad ${ContentsLoader.Api.toLoadPacks}")
val notFound = mutableListOf<String>()
ContentsLoader.Api.loadContent(notFound)
Call.serverPacketReliable("ContentsLoader|load", "LOADED: ${ContentsLoader.Api.lastLoadedPacks}")
Call.serverPacketReliable("ContentsLoader|load", "NOTFOUND: $notFound")
if (notFound.isNotEmpty())
Call.serverPacketReliable("ContentsLoader|load", "NOTFOUND: $notFound")
}

val patchCache = mutableMapOf<String, String>()
fun registerContentsParser() {
Events.on(ResetEvent::class.java) { ContentsPatcher.Api.reset() }
fun loadPatch(name: String) {
if (name !in patchCache) {
val localFile = Vars.dataDirectory.child("contents-patch").run {
child("$name.hjson").takeIf { it.exists() }
?: child("$name.json").takeIf { it.exists() }
} ?: return Call.serverPacketReliable("ContentsLoader|requestPatch", name)
patchCache[name] = localFile.readString()
}
ContentsPatcher.Api.load(patchCache[name]!!)
}
Events.on(PlayEvent::class.java) {
loadPatch("default")
val list = Vars.state.rules.tags.get(ContentsPatcher.Api.tagName) ?: return@on
list.split(";").forEach { loadPatch(it) }
}
Vars.netClient.addPacketHandler("ContentsLoader|loadPatch", ::loadPatch)
Vars.netClient.addPacketHandler("ContentsLoader|newPatch") {
val (name, content) = it.split('\n', limit = 2)
patchCache[name] = content
ContentsPatcher.Api.load(content)
}
}
}

0 comments on commit 2e084d6

Please sign in to comment.