Introduce μScript and replace the condition and resource expansion systems with it (backwards compatibility is provided)
This commit is contained in:
parent
306279e92e
commit
23e81eccf3
12
build.gradle
12
build.gradle
|
@ -18,9 +18,9 @@ repositories {
|
|||
|
||||
dependencies {
|
||||
modImplementation("net.fabricmc.fabric-api:fabric-api:${project.fabric_version}")
|
||||
modImplementation("com.terraformersmc:modmenu:3.0.1")
|
||||
include modImplementation("com.github.MeteorDevelopment:starscript:0.1.9")
|
||||
modApi("me.shedaniel.cloth:cloth-config-fabric:6.2.57") {
|
||||
modImplementation("com.terraformersmc:modmenu:4.0.0-beta.4")
|
||||
include modImplementation("io.gitlab.jfronny:muscript:${project.muscript_version}")
|
||||
modApi("me.shedaniel.cloth:cloth-config-fabric:7.0.65") {
|
||||
exclude(group: "net.fabricmc.fabric-api")
|
||||
}
|
||||
modCompileOnly "io.vram:frex-fabric-mc118:+"
|
||||
|
@ -29,13 +29,13 @@ dependencies {
|
|||
|
||||
testImplementation('org.junit.jupiter:junit-jupiter:5.8.2')
|
||||
|
||||
//Canvas for FREX testing
|
||||
// Canvas for FREX testing
|
||||
// modRuntimeOnly("io.vram:canvas-fabric-mc118:+") {
|
||||
// exclude(group: "me.shedaniel.cloth")
|
||||
// }
|
||||
|
||||
//DashLoader "compatibility"
|
||||
modRuntimeOnly 'dev.quantumfusion.dashloader:dashloader-definitions:3.0-rc14-1.18'
|
||||
// DashLoader "compatibility"
|
||||
// modRuntimeOnly 'dev.quantumfusion.dashloader:dashloader-definitions:3.0-rc14-1.18'
|
||||
}
|
||||
|
||||
test {
|
||||
|
|
|
@ -1,87 +1,28 @@
|
|||
# Advanced Conditions
|
||||
Respackopts uses "conditions" for deciding what files to load.
|
||||
Respackopts conditions are usually a simple string with a pack and entry name
|
||||
(IE `<pack id>:<entry name>` or `<entry name>`),
|
||||
Respackopts conditions are usually a simple string with the entry name
|
||||
(for example, `someTexture`),
|
||||
however, respackopts provides various additional features to enhance their functionality.
|
||||
This page will go over everything you can do with conditions
|
||||
This functionality is powered by the [μScript language](https://gitlab.com/JFronny/java-commons/-/tree/master/muscript)
|
||||
This page will provide a quick overview over the available operations.
|
||||
For more complete documentation, use the above link.
|
||||
|
||||
## (Boolean) logic
|
||||
Any JSON object will be treated as a logical condition.
|
||||
This behavior allows defining resources that are only used when a set of conditions are met,
|
||||
not just one. Be aware that nesting logic operations is supported and recommended for advanced setups
|
||||
The following expressions are supported:
|
||||
You can use logic operations on several values to only show resources when a combination of them applies
|
||||
Some common logical operations are supported, others indirectly.
|
||||
That includes NOT (`!`), AND (`&`), OR (`|`), XOR (`==`) and XNOR (`!=`)
|
||||
|
||||
| Example | Explaination |
|
||||
|-----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| `{"not": "examplePack:condition1"}` | True if the contained condition is false |
|
||||
| `{"and": ["examplePack:condition1", "examplePack:condition2"]}` | True if all contained conditions are true |
|
||||
| `{"or": ["examplePack:condition1", "examplePack:condition2"]}` | True if any contained condition is true |
|
||||
| `{"nor": ["examplePack:condition1", "examplePack:condition2"]}` | True if none of the contained conditions are true |
|
||||
| `{"xor": ["examplePack:condition1", "examplePack:condition2"]}` | True if the number of contained conditions which are true is an odd number |
|
||||
| `{"eq": ["examplePack:condition1", "examplePack:condition2"]}` | True if all contained conditions have the same value <br>(equivalent to `{"or": [{"and": [...]}, {"nor": [...]}]}`) |
|
||||
## Numbers
|
||||
Numeric values can be accessed by their name, just like boolean values. All typical mathematical operations are supported,
|
||||
that includes addition (`+`), subtraction (`-`), multiplication (`*`), division (`/`) and modulo (`%`)
|
||||
|
||||
|
||||
## Built-in conditions
|
||||
Respackopts provides some conditions by default, you can use them in any pack.
|
||||
|
||||
| Condition | Explaination |
|
||||
|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `modversion:<mod>:<predicate>` | True if the specified mod is loaded (use the mod ID) and the version fits the predicate.<br>Example: `modversion:minecraft:>=1.7.10`, `modversion:continuity:*` |
|
||||
## Accessing values
|
||||
By default, all values for the current pack will be available by their name or as entries of a category (`someCategory.someOption`).
|
||||
In addition, all values of other packs are accessible under their pack id (`somePack.someOption`).
|
||||
Respackopts also allows checking mod (or minecraft) versions against predicates as follows:
|
||||
`version('<mod name>', '<predicate>')`, for example `version('minecraft', '<=1.16.5')`
|
||||
|
||||
## Fabric Resource Conditions Interop
|
||||
Fabric API provides a system called resource conditions, which is similar to respackopts own system.
|
||||
To improve compatibility, respackopts allows accessing fabric resource conditions and respackopts conditions from one another.
|
||||
|
||||
### Fabric Resource Conditions from Respackopts
|
||||
A json object containing a singular entry whose key is `fabric:load_conditions` will be treated as a Fabric Resource Condition.
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"condition": {
|
||||
"fabric:load_conditions": {
|
||||
"condition": "fabric:not",
|
||||
"value": {
|
||||
"condition": "fabric:all_mods_loaded",
|
||||
"values": [
|
||||
"a"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
### Respackopts conditions from Fabric
|
||||
You may access respackopts conditions from Fabric API Resource Condition blocks by specifying a condition with the ID
|
||||
`config`. Its value will be treated as a respackopts condition.
|
||||
|
||||
Please be aware that fabric resource conditions are not tied to resource packs and entry IDs must therefore be specified fully.
|
||||
(Using `someTexture` instead of `examplePack:someTexture` is impossible)
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"type": "minecraft:crafting_shapeless",
|
||||
"ingredients": [
|
||||
{
|
||||
"item": "minecraft:stick"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"item": "minecraft:diamond"
|
||||
},
|
||||
"fabric:conditions": [
|
||||
{
|
||||
"condition": "fabric:not",
|
||||
"value": {
|
||||
"condition": "respackopts:config",
|
||||
"value": {
|
||||
"and": [
|
||||
"examplePack:someTexture"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Respackopts previously allowed accessing fabric resource conditions and respackopts conditions from one another,
|
||||
however, this feature was removed in favor of enforcing the new muscript syntax.
|
||||
|
|
|
@ -23,12 +23,14 @@ One common issue is that you removed an option but still use it somewhere.
|
|||
The log will usually reference that option and the source.
|
||||
|
||||
## Ensure you are using the correct dots
|
||||
Ensure conditions follow one of the following conventions.
|
||||
Using dots, colons or commas where they don't belong will break your pack:
|
||||
|
||||
- `<pack id>:<entry id>`, eg `breeze16:bushyLeaves.full`
|
||||
- `<entry id>`, eg `bushyLeaves.full`
|
||||
Respackopts only supports normal dots. If you write commas or colons by accident,
|
||||
your pack WILL fail to load.
|
||||
|
||||
## Avoid infinite loops
|
||||
Ensure that you do not reference an original file or a previous fallback from a fallback.
|
||||
Respackopts WILL crash if it runs into an infinite loop!
|
||||
Respackopts WILL crash if it runs into an infinite loop!
|
||||
|
||||
## Contact me for support
|
||||
If you are unable to identify the issue, you can try [contacting](https://jfronny.gitlab.io/contact.html) me.
|
||||
As respackopts is a complex mod, there is a non-zero chance that your issue may be caused by a bug in the mod.
|
||||
Even if the issue is in your code, I can still try helping you fix it.
|
||||
|
|
|
@ -15,7 +15,7 @@ You can add [translations](./Translations.md) to work around this in the UI show
|
|||
```json
|
||||
{
|
||||
"id": "<PackID>",
|
||||
"version": 6,
|
||||
"version": 8,
|
||||
"capabilities": ["FileFilter", "DirFilter"],
|
||||
"conf": {
|
||||
// Your config options here
|
||||
|
@ -37,7 +37,7 @@ To add a boolean entry, add code like this: `"entryName": <Default Option (true/
|
|||
```json
|
||||
{
|
||||
"id": "examplePack",
|
||||
"version": 6,
|
||||
"version": 8,
|
||||
"capabilities": ["FileFilter", "DirFilter"],
|
||||
"conf": {
|
||||
"someTexture": true,
|
||||
|
@ -58,7 +58,7 @@ A number box follows the same principle as a boolean: `"entryName": Default Numb
|
|||
```json
|
||||
{
|
||||
"id": "examplePack",
|
||||
"version": 6,
|
||||
"version": 8,
|
||||
"capabilities": ["FileFilter", "DirFilter"],
|
||||
"conf": {
|
||||
"someOption": 10
|
||||
|
@ -79,7 +79,7 @@ To allow users to select one entry from a list, you can use a json array with st
|
|||
```json
|
||||
{
|
||||
"id": "examplePack",
|
||||
"version": 6,
|
||||
"version": 8,
|
||||
"capabilities": ["FileFilter", "DirFilter"],
|
||||
"conf": {
|
||||
"someOption": [
|
||||
|
@ -99,7 +99,7 @@ To allow users to select one entry from a list, you can use a json array with st
|
|||
```json
|
||||
{
|
||||
"id": "examplePack",
|
||||
"version": 6,
|
||||
"version": 8,
|
||||
"capabilities": ["FileFilter", "DirFilter"],
|
||||
"conf": {
|
||||
"someCategory": {
|
||||
|
|
|
@ -57,4 +57,11 @@ Corresponds to version 2.7.2 - 2.9.1
|
|||
## v7
|
||||
Corresponds to version 2.10.0
|
||||
|
||||
- Replace StarScript representation of enums
|
||||
- Replace StarScript representation of enums
|
||||
|
||||
## v8
|
||||
Corresponds to version 3.0.0
|
||||
|
||||
- Entirely new condition representation via MuScript
|
||||
- Entirely new resource expansion using MuScript
|
||||
- Removal of fabric conditions API interop in favor of enabling cleaner syntax
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
# Select one of multiple files
|
||||
|
||||
This Allows you to pick between multiple files (3 or more files) using similar logic to [Toggle Files](./ToggleFiles.md)
|
||||
But this requires multiple `.rpo` files to achieve the desired goal.
|
||||
You will need the `PackID` and `EntryName` from your `/assets/respackopts/conf.json` that you created earlier
|
||||
if you have not, see [Main Config](./MainConfig.md) on how to do so.
|
||||
<br>
|
||||
|
||||
You will need to navigate to the file you would like to toggle inside your resource, and create a `.rpo` file for it in the same folder(directory).
|
||||
You will need to navigate to the file you would like to toggle inside your resource, and create a `.rpo` file for it in the same folder (directory).
|
||||
<br>
|
||||
|
||||
### Examples:
|
||||
|
@ -14,13 +13,14 @@ You will need to navigate to the file you would like to toggle inside your resou
|
|||
`some_recipe.json` would be `some_recipe.json.rpo`<br>
|
||||
|
||||
### `.rpo` explanation :
|
||||
You need a `.rpo` file per texture (except for the last one in your list) if you have 3 files to select beteen then you will need 2 `.rpo` files, If you have 4 files to select between then you will need 3 `.rpo` files and so on etc...
|
||||
You need a `.rpo` file per texture (except for the last one in your list) if you have 3 files to select beteen then you will need 2 `.rpo` files,
|
||||
if you have 4 files to select between then you will need 3 `.rpo` files and so on etc...
|
||||
|
||||
### Layout:
|
||||
This is your first `.rpo` with your first file:
|
||||
```json
|
||||
{
|
||||
"condition": "<your pack id>:<your entry>",
|
||||
"condition": "<your entry>",
|
||||
"fallback": "path/to/second.json/.png"
|
||||
}
|
||||
```
|
||||
|
@ -28,7 +28,7 @@ This is your first `.rpo` with your first file:
|
|||
This is your second `.rpo` with your second file:
|
||||
```json
|
||||
{
|
||||
"condition": "<your pack id>:<your entry>",
|
||||
"condition": "<your entry>",
|
||||
"fallback": "path/to/thirdfile.json/.png"
|
||||
}
|
||||
```
|
||||
|
@ -36,14 +36,14 @@ This is your second `.rpo` with your second file:
|
|||
### Example:
|
||||
```json
|
||||
{
|
||||
"condition": "examplePack:option_one",
|
||||
"condition": "option_one",
|
||||
"fallback": "path/to/secondfile.json"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"condition": "examplePack:option_two",
|
||||
"condition": "option_two",
|
||||
"fallback": "path/to/thirdfile.json"
|
||||
}
|
||||
```
|
||||
|
|
|
@ -2,10 +2,13 @@
|
|||
Sometimes, you may wish to use respackopts values inside your text files (for example as model transforms),
|
||||
without switching between a bunch of similar files. For this purpose, respackopts allows replacing text in files via resource expansion.
|
||||
|
||||
You can use the `expansions` block in your .rpo to replace content with a value computed through executing a custom [StarScript](https://github.com/MeteorDevelopment/starscript/wiki).
|
||||
Values can be accessed in star script in a similar way to normal condition objects: `<pack id>.<entry>` or `<pack id>.<subcategory>.<entry>`.
|
||||
You can use the `expansions` block in your .rpo to replace content with a value computed through executing a custom [μScript](https://gitlab.com/JFronny/java-commons/-/tree/master/muscript) block.
|
||||
Values can be accessed in the script in a the same exact way as in condition objects,
|
||||
as the same language and object representation is used, though the result of a script is a string and not a boolean here.
|
||||
|
||||
The string value of an enum can be accessed via `<pack id>.<entry>.value`
|
||||
I should probably point out here that, since the result here is always a string,
|
||||
string concatenation via the `||` operator is possible and recommended.
|
||||
For example, the following is a valid script: `'Text ' || (someNumber * 15) || someBoolean`
|
||||
|
||||
### Example:
|
||||
conf.json:
|
||||
|
@ -27,12 +30,12 @@ oak_fence.json.rpo:
|
|||
```json
|
||||
{
|
||||
"expansions": {
|
||||
"uvlock": "{examplePack.oakFence.uvLock}",
|
||||
"orientation": "{!examplePack.oakFence.invertOrientation}",
|
||||
"y000": "{examplePack.oakFence.yFactor + 0}",
|
||||
"y090": "{examplePack.oakFence.yFactor + 90}",
|
||||
"y180": "{examplePack.oakFence.yFactor + 180}",
|
||||
"y270": "{examplePack.oakFence.yFactor + 270}"
|
||||
"uvlock": "examplePack.oakFence.uvLock",
|
||||
"orientation": "!examplePack.oakFence.invertOrientation",
|
||||
"y000": "examplePack.oakFence.yFactor + 0",
|
||||
"y090": "examplePack.oakFence.yFactor + 90",
|
||||
"y180": "examplePack.oakFence.yFactor + 180",
|
||||
"y270": "examplePack.oakFence.yFactor + 270"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Toggle Files
|
||||
This is a simple `IF` statement simply returning true/false to if the texture/file should be loaded into the pack.
|
||||
A condition can be looked at like an `IF` statement simply returning true/false to configure whether the texture/file should be loaded into the pack.
|
||||
(Turns the texture "on" or "off").
|
||||
You will need the `Pack ID` and `Entry Name` from your `/assets/respackopts/conf.json` that you created earlier
|
||||
if you have not, see [Main Config](./MainConfig.md) on how to do so.
|
||||
|
@ -16,13 +16,13 @@ You will need to navigate to the file you would like to toggle inside your resou
|
|||
### Layout:
|
||||
```json
|
||||
{
|
||||
"condition": "<pack id>:<entry name>"
|
||||
"condition": "<entry name>"
|
||||
}
|
||||
```
|
||||
### Example:
|
||||
```json
|
||||
{
|
||||
"condition": "examplePack:someTexture"
|
||||
"condition": "someTexture"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -16,14 +16,14 @@ You will need to navigate to the file you would like to toggle inside your resou
|
|||
### Layout:
|
||||
```json
|
||||
{
|
||||
"condition": "<pack id>:<entry name>",
|
||||
"condition": "<entry name>",
|
||||
"fallback": "location/of/the/file"
|
||||
}
|
||||
```
|
||||
### Example:
|
||||
```json
|
||||
{
|
||||
"condition": "examplePack:someTexture",
|
||||
"condition": "someTexture",
|
||||
"fallback": "assets/minecraft/textures/example/alternate.png"
|
||||
}
|
||||
```
|
||||
|
@ -33,7 +33,7 @@ Respackopts supports specifying multiple possible fallbacks when configuring sin
|
|||
You can use this functionality as follows:
|
||||
```json
|
||||
{
|
||||
"condition": "<pack id>:<entry name>",
|
||||
"condition": "<entry name>",
|
||||
"fallbacks": [
|
||||
"location/of/the/file",
|
||||
"assets/minecraft/textures/example/alternate.png"
|
||||
|
@ -43,7 +43,7 @@ You can use this functionality as follows:
|
|||
|
||||
```json
|
||||
{
|
||||
"condition": "examplePack:someTexture"
|
||||
"condition": "someTexture"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# https://fabricmc.net/develop/
|
||||
minecraft_version=1.18.2
|
||||
yarn_mappings=build.3
|
||||
loader_version=0.14.5
|
||||
minecraft_version=1.19-rc2
|
||||
yarn_mappings=build.1
|
||||
loader_version=0.14.6
|
||||
maven_group=io.gitlab.jfronny
|
||||
archives_base_name=respackopts
|
||||
fabric_version=0.51.1+1.18.2
|
||||
jfapi_version=2.8.1
|
||||
fabric_version=0.55.0+1.19
|
||||
jfapi_version=2.8.1-1654289121
|
||||
muscript_version=2022.6.4+17-30-50
|
||||
|
||||
modrinth_id=TiF5QWZY
|
||||
modrinth_required_dependencies=P7dR8mSH, 9s6osm5g, WKwQAwke
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"expansions": {
|
||||
"uvlock": "{lumi.oakFence.uvLock}",
|
||||
"orientation": "{!lumi.oakFence.invertOrientation}",
|
||||
"y000": "{lumi.oakFence.yFactor + 0}",
|
||||
"y090": "{lumi.oakFence.yFactor + 90}",
|
||||
"y180": "{lumi.oakFence.yFactor + 180}",
|
||||
"y270": "{lumi.oakFence.yFactor + 270}"
|
||||
"uvlock": "lumi.oakFence.uvLock",
|
||||
"orientation": "!lumi.oakFence.invertOrientation",
|
||||
"y000": "lumi.oakFence.yFactor + 0",
|
||||
"y090": "lumi.oakFence.yFactor + 90",
|
||||
"y180": "lumi.oakFence.yFactor + 180",
|
||||
"y270": "lumi.oakFence.yFactor + 270"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"multipart": [
|
||||
{
|
||||
"apply": {
|
||||
"model": "minecraft:block/oak_fence_post"
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"north": "true"
|
||||
},
|
||||
"apply": {
|
||||
"model": "minecraft:block/oak_fence_side"
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"east": "true"
|
||||
},
|
||||
"apply": {
|
||||
"model": "minecraft:block/oak_fence_side"
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"south": "true"
|
||||
},
|
||||
"apply": {
|
||||
"model": "minecraft:block/oak_fence_side"
|
||||
}
|
||||
},
|
||||
{
|
||||
"when": {
|
||||
"west": "true"
|
||||
},
|
||||
"apply": {
|
||||
"model": "minecraft:block/oak_fence_side"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"condition": "version('minecraft', '<=1.16.5')"
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"conditions": ["false"],
|
||||
"conditions": "false",
|
||||
"fallback": "assets/minecraft/langer"
|
||||
}
|
|
@ -1,27 +1,11 @@
|
|||
{
|
||||
"conditions": [
|
||||
{
|
||||
"fabric:load_conditions": {
|
||||
"condition": "respackopts:config",
|
||||
"value": "lumi:subcategoryTest.enableLang"
|
||||
}
|
||||
},
|
||||
{
|
||||
"not": "subcategoryTest.enableLangForceDisable"
|
||||
},
|
||||
{
|
||||
"fabric:load_conditions": {
|
||||
"condition": "fabric:all_mods_loaded",
|
||||
"values": ["respackopts"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"conditions": "lumi.subcategoryTest.enableLang !subcategoryTest.enableLangForceDisable & version('respackopts', '*')",
|
||||
"fallback": "assets/minecraft/lang/en_us_joke.json",
|
||||
"expansions": {
|
||||
"Lights": "{lumi.subcategoryTest.enableLang}",
|
||||
"Mode": "{lumi.debugMode}",
|
||||
"Normal": "{lumi.numTest * lumi.subcategoryTest.numberInSub}",
|
||||
"Lumi": "model",
|
||||
"Regeneration": "Industrie"
|
||||
"Lights": "lumi.subcategoryTest.enableLang",
|
||||
"Mode": "lumi.debugMode",
|
||||
"Normal": "lumi.numTest * lumi.subcategoryTest.numberInSub",
|
||||
"Lumi": "'model'",
|
||||
"Regeneration": "'Industrie'"
|
||||
}
|
||||
}
|
|
@ -1,5 +1,3 @@
|
|||
{
|
||||
"conditions": [
|
||||
"subcategoryTest.enableLangJokeFallback"
|
||||
]
|
||||
"conditions": "subcategoryTest.enableLangJokeFallback"
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"id": "lumi",
|
||||
"version": 6,
|
||||
"version": 8,
|
||||
"capabilities": ["FileFilter", "DirFilter", "DirFilterAdditive"],
|
||||
"conf": {
|
||||
"tonemap": [
|
||||
|
|
|
@ -1,25 +1,16 @@
|
|||
package io.gitlab.jfronny.respackopts.util;
|
||||
package io.gitlab.jfronny.respackopts;
|
||||
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigBranch;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigEntry;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.GuiEntryBuilderParam;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.PackReloadType;
|
||||
import io.gitlab.jfronny.respackopts.integration.DashLoaderCompat;
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
|
||||
import me.shedaniel.clothconfig2.api.ConfigBuilder;
|
||||
import me.shedaniel.clothconfig2.api.ConfigCategory;
|
||||
import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
|
||||
import net.minecraft.client.gui.screen.FatalErrorScreen;
|
||||
import net.minecraft.client.gui.screen.Screen;
|
||||
import net.minecraft.text.LiteralText;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.text.TranslatableText;
|
||||
import net.minecraft.util.Language;
|
||||
import io.gitlab.jfronny.respackopts.integration.*;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.*;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.*;
|
||||
import io.gitlab.jfronny.respackopts.util.*;
|
||||
import me.shedaniel.clothconfig2.api.*;
|
||||
import net.minecraft.client.gui.screen.*;
|
||||
import net.minecraft.text.*;
|
||||
import net.minecraft.util.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
|
||||
public class GuiFactory {
|
||||
public void buildCategory(ConfigBranch source, String screenId, Consumer<AbstractConfigListEntry<?>> config, Consumer<PackReloadType> reloadTypeAggregator, ConfigEntryBuilder entryBuilder, String namePrefix) {
|
||||
|
@ -32,7 +23,7 @@ public class GuiFactory {
|
|||
String k = (source.getVersion() < 3 ? "respackopts.tooltip." : "rpo.tooltip.") + screenId + "." + entryName;
|
||||
if (Language.getInstance().hasTranslation(k)) {
|
||||
Text[] res = new Text[1];
|
||||
res[0] = new TranslatableText(k);
|
||||
res[0] = Text.translatable(k);
|
||||
return Optional.of(res);
|
||||
} else
|
||||
return Optional.empty();
|
||||
|
@ -43,8 +34,8 @@ public class GuiFactory {
|
|||
public static Text getText(String name, String translationPrefix) {
|
||||
String translatableNameKey = translationPrefix + "." + name;
|
||||
return Language.getInstance().hasTranslation(translatableNameKey)
|
||||
? new TranslatableText(translatableNameKey)
|
||||
: new LiteralText(name);
|
||||
? Text.translatable(translatableNameKey)
|
||||
: Text.literal(name);
|
||||
}
|
||||
|
||||
public Screen buildGui(ConfigBranch source, String packId, Screen parent) {
|
||||
|
@ -70,7 +61,7 @@ public class GuiFactory {
|
|||
}
|
||||
catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
return new FatalErrorScreen(new TranslatableText("respackopts.loadFailed"), new TranslatableText("respackopts.loadError"));
|
||||
return new FatalErrorScreen(Text.translatable("respackopts.loadFailed"), Text.translatable("respackopts.loadError"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,61 +1,37 @@
|
|||
package io.gitlab.jfronny.respackopts;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import io.gitlab.jfronny.respackopts.filters.DirFilterEventImpl;
|
||||
import io.gitlab.jfronny.respackopts.filters.FileFilterEventImpl;
|
||||
import io.gitlab.jfronny.gson.*;
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.respackopts.filters.*;
|
||||
import io.gitlab.jfronny.respackopts.gson.*;
|
||||
import io.gitlab.jfronny.respackopts.gson.entry.BooleanEntrySerializer;
|
||||
import io.gitlab.jfronny.respackopts.gson.entry.ConfigBranchSerializer;
|
||||
import io.gitlab.jfronny.respackopts.gson.entry.EnumEntrySerializer;
|
||||
import io.gitlab.jfronny.respackopts.gson.entry.NumericEntrySerializer;
|
||||
import io.gitlab.jfronny.respackopts.integration.FrexCompat;
|
||||
import io.gitlab.jfronny.respackopts.model.ConfigFile;
|
||||
import io.gitlab.jfronny.respackopts.model.condition.Condition;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigBooleanEntry;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigBranch;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigEnumEntry;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigNumericEntry;
|
||||
import io.gitlab.jfronny.respackopts.util.GuiFactory;
|
||||
import io.gitlab.jfronny.respackopts.util.MetaCache;
|
||||
import io.gitlab.jfronny.respackopts.util.RpoCommand;
|
||||
import io.gitlab.jfronny.respackopts.util.RpoFormatException;
|
||||
import meteordevelopment.starscript.Script;
|
||||
import meteordevelopment.starscript.StandardLib;
|
||||
import meteordevelopment.starscript.Starscript;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.api.EnvType;
|
||||
import net.fabricmc.api.Environment;
|
||||
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.impl.gui.FabricGuiEntry;
|
||||
import net.minecraft.client.MinecraftClient;
|
||||
import net.minecraft.server.integrated.IntegratedServer;
|
||||
import net.minecraft.util.Identifier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.gitlab.jfronny.respackopts.gson.entry.*;
|
||||
import io.gitlab.jfronny.respackopts.integration.*;
|
||||
import io.gitlab.jfronny.respackopts.model.*;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.*;
|
||||
import io.gitlab.jfronny.respackopts.util.*;
|
||||
import net.fabricmc.api.*;
|
||||
import net.fabricmc.loader.api.*;
|
||||
import net.minecraft.client.*;
|
||||
import net.minecraft.server.integrated.*;
|
||||
import net.minecraft.util.*;
|
||||
import org.slf4j.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
@Environment(EnvType.CLIENT)
|
||||
public class Respackopts implements ClientModInitializer {
|
||||
public static final Integer META_VERSION = 7;
|
||||
public static final Integer META_VERSION = 8;
|
||||
public static final String FILE_EXTENSION = ".rpo";
|
||||
public static final Gson GSON;
|
||||
|
||||
public static Starscript STAR_SCRIPT;
|
||||
public static final String ID = "respackopts";
|
||||
public static final Logger LOGGER = LoggerFactory.getLogger(ID);
|
||||
public static final Identifier CONF_ID = new Identifier(ID, "conf.json");
|
||||
public static final Set<Runnable> SAVE_ACTIONS = new HashSet<>();
|
||||
public static final GuiFactory GUI_FACTORY = new GuiFactory();
|
||||
public static final Identifier FAPI_CONDITION = new Identifier(ID, "config");
|
||||
|
||||
public static boolean forcePackReload = false;
|
||||
public static Path FALLBACK_CONF_DIR;
|
||||
|
@ -69,10 +45,10 @@ public class Respackopts implements ClientModInitializer {
|
|||
.registerTypeAdapter(ConfigNumericEntry.class, new NumericEntrySerializer())
|
||||
.registerTypeAdapter(ConfigBooleanEntry.class, new BooleanEntrySerializer())
|
||||
.registerTypeAdapter(ConfigBranch.class, new ConfigBranchSerializer())
|
||||
.registerTypeAdapter(Script.class, new ScriptDeserializer())
|
||||
.registerTypeAdapter(Condition.class, new ConditionDeserializer())
|
||||
.registerTypeAdapterFactory(new SingleEntrySetTypeAdapterFactory())
|
||||
.registerTypeAdapterFactory(new SingleEntryListTypeAdapterFactory())
|
||||
.registerTypeAdapter(Expr.class, new ExprDeserializer())
|
||||
.registerTypeAdapter(Expr.StringExpr.class, new ExprDeserializer.StringX())
|
||||
.registerTypeAdapter(Expr.BoolExpr.class, new BoolExprDeserializer())
|
||||
.setLenient()
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
try {
|
||||
|
@ -81,12 +57,6 @@ public class Respackopts implements ClientModInitializer {
|
|||
} catch (Throwable e) {
|
||||
LOGGER.error("Could not resolve config directory", e);
|
||||
}
|
||||
try {
|
||||
STAR_SCRIPT = new Starscript();
|
||||
} catch (Exception e) {
|
||||
FabricGuiEntry.displayCriticalError(new Exception("Respackopts could not initialize Starscript. Expect issues with packs that use it", e), false);
|
||||
}
|
||||
StandardLib.init(STAR_SCRIPT);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -98,7 +68,6 @@ public class Respackopts implements ClientModInitializer {
|
|||
}
|
||||
if (CONFIG.debugLogs)
|
||||
SAVE_ACTIONS.add(() -> LOGGER.info("Save"));
|
||||
SAVE_ACTIONS.add(() -> MetaCache.forEach((key, state) -> STAR_SCRIPT.set(sanitizeString(state.packId()), state.configBranch()::buildStarScript)));
|
||||
SAVE_ACTIONS.add(() -> {
|
||||
if (CONFIG.debugLogs)
|
||||
LOGGER.info("Generating shader code");
|
||||
|
@ -116,15 +85,6 @@ public class Respackopts implements ClientModInitializer {
|
|||
if (FabricLoader.getInstance().isModLoaded("frex")) {
|
||||
FrexCompat.onInitializeFrex();
|
||||
}
|
||||
ResourceConditions.register(FAPI_CONDITION, jsonObject -> {
|
||||
try {
|
||||
if (!jsonObject.has("value")) throw new RpoFormatException(FAPI_CONDITION + " does not have a value");
|
||||
return GSON.fromJson(jsonObject.get("value"), Condition.class).evaluate("fabric");
|
||||
} catch (RpoFormatException | JsonSyntaxException e) {
|
||||
LOGGER.error("Could not parse condition", e);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static String sanitizeString(String s) {
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package io.gitlab.jfronny.respackopts;
|
||||
|
||||
import io.gitlab.jfronny.respackopts.util.*;
|
||||
import net.fabricmc.fabric.api.client.command.v2.*;
|
||||
import net.fabricmc.loader.api.*;
|
||||
import net.minecraft.text.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*;
|
||||
|
||||
public class RpoCommand {
|
||||
private static final ModContainer CONTAINER = FabricLoader.getInstance().getModContainer(Respackopts.ID).get();
|
||||
public static void register() {
|
||||
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
|
||||
dispatcher.register(literal("rpo").then(literal("dump").then(literal("glsl").executes(ctx -> {
|
||||
ctx.getSource().sendFeedback(dump(Respackopts.getShaderImportSource(), "frex.glsl"));
|
||||
return 1;
|
||||
}))));
|
||||
dispatcher.register(literal("rpo").then(literal("dump").then(literal("config").executes(ctx -> {
|
||||
MetaCache.forEach((id, branch) -> ctx.getSource().sendFeedback(dump(branch.toString(), id + ".txt")));
|
||||
return 1;
|
||||
}))));
|
||||
dispatcher.register(literal("rpo").then(literal("dump").executes(ctx -> {
|
||||
MetaCache.forEach((id, branch) -> ctx.getSource().sendFeedback(dump(branch.toString(), id + ".txt")));
|
||||
return 1;
|
||||
})));
|
||||
dispatcher.register(literal("rpo").then(literal("version").executes(ctx -> {
|
||||
ctx.getSource().sendFeedback(Text.translatable("respackopts.versionText", CONTAINER.getMetadata().getVersion(), Respackopts.META_VERSION));
|
||||
return 1;
|
||||
})));
|
||||
dispatcher.register(literal("rpo").executes(ctx -> {
|
||||
ctx.getSource().sendFeedback(Text.translatable("respackopts.versionText", CONTAINER.getMetadata().getVersion(), Respackopts.META_VERSION));
|
||||
return 1;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private static final Path dumpPath = FabricLoader.getInstance().getGameDir().resolve("respackopts");
|
||||
private static Text dump(String text, String fileName) {
|
||||
try {
|
||||
if (!Files.exists(dumpPath)) Files.createDirectories(dumpPath);
|
||||
Path filePath = dumpPath.resolve(fileName);
|
||||
try (BufferedWriter bw = Files.newBufferedWriter(filePath, StandardOpenOption.CREATE)) {
|
||||
bw.write(text);
|
||||
}
|
||||
return Text.translatable("respackopts.dumpSucceeded", filePath.toAbsolutePath());
|
||||
}
|
||||
catch (Throwable e) {
|
||||
return Text.translatable("respackopts.dumpFailed");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,8 +2,11 @@ package io.gitlab.jfronny.respackopts.filters;
|
|||
|
||||
import io.gitlab.jfronny.libjf.ResourcePath;
|
||||
import io.gitlab.jfronny.libjf.data.manipulation.api.UserResourceEvents;
|
||||
import io.gitlab.jfronny.muscript.debug.*;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.gson.*;
|
||||
import io.gitlab.jfronny.respackopts.model.DirRpo;
|
||||
import io.gitlab.jfronny.respackopts.model.cache.*;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.PackCapability;
|
||||
import io.gitlab.jfronny.respackopts.util.MetaCache;
|
||||
import io.gitlab.jfronny.respackopts.util.RpoFormatException;
|
||||
|
@ -22,7 +25,7 @@ public class DirFilterEventImpl {
|
|||
return previous.get();
|
||||
String path = new ResourcePath(type, id).getName();
|
||||
DirRpo rpo = findDirRpo(pack, path);
|
||||
if (rpo != null && dirHidden(rpo, MetaCache.getId(MetaCache.getKeyByPack(pack)), path)) {
|
||||
if (rpo != null && dirHidden(rpo, MetaCache.getKeyByPack(pack), path)) {
|
||||
path = findReplacementDir(path, rpo);
|
||||
if (path == null) throw new FileNotFoundException();
|
||||
ResourcePath rp = new ResourcePath(path);
|
||||
|
@ -30,7 +33,7 @@ public class DirFilterEventImpl {
|
|||
}
|
||||
return previous.get();
|
||||
});
|
||||
UserResourceEvents.FIND_RESOURCE.register((type, namespace, prefix, maxDepth, pathFilter, previous, pack) -> {
|
||||
UserResourceEvents.FIND_RESOURCE.register((type, namespace, prefix, allowedPathPredicate, previous, pack) -> {
|
||||
// Warning: the Identifiers here DON'T CONTAIN THE TYPE!
|
||||
// Therefore, it needs to be added when calling a method that generates a ResourcePath!
|
||||
Collection<Identifier> prevVals = previous.get();
|
||||
|
@ -42,7 +45,7 @@ public class DirFilterEventImpl {
|
|||
String path = type.getDirectory() + "/" + identifier.getNamespace() + "/" + identifier.getPath();
|
||||
DirRpo rpo = findDirRpo(pack, path);
|
||||
if (rpo != null) {
|
||||
if (dirHidden(rpo, MetaCache.getId(MetaCache.getKeyByPack(pack)), path)) {
|
||||
if (dirHidden(rpo, MetaCache.getKeyByPack(pack), path)) {
|
||||
path = findReplacementDir(path, rpo);
|
||||
if (path == null)
|
||||
nextRes.remove(identifier);
|
||||
|
@ -51,11 +54,11 @@ public class DirFilterEventImpl {
|
|||
if (s.length == 3) {
|
||||
ResourcePath rp = new ResourcePath(path);
|
||||
//TODO improve this impl (used for files that aren't at the original location
|
||||
for (Identifier resource : pack.findResources(rp.getType(), rp.getId().getNamespace(), rp.getId().getPath(), maxDepth, (a) -> true)) {
|
||||
for (Identifier resource : pack.findResources(rp.getType(), rp.getId().getNamespace(), rp.getId().getPath(), (a) -> true)) {
|
||||
String p = type.getDirectory() + "/" + resource.getNamespace() + "/" + resource.getPath();
|
||||
p = p.replace(rpo.fallback, rpo.path + "/");
|
||||
rp = new ResourcePath(p);
|
||||
if (pathFilter.test(p))
|
||||
if (allowedPathPredicate.test(rp.getId()))
|
||||
nextRes.add(rp.getId());
|
||||
}
|
||||
}
|
||||
|
@ -70,7 +73,7 @@ public class DirFilterEventImpl {
|
|||
return previous.get();
|
||||
String path = new ResourcePath(type, id).getName();
|
||||
DirRpo rpo = findDirRpo(pack, path);
|
||||
if (rpo != null && dirHidden(rpo, MetaCache.getId(MetaCache.getKeyByPack(pack)), path)) {
|
||||
if (rpo != null && dirHidden(rpo, MetaCache.getKeyByPack(pack), path)) {
|
||||
path = findReplacementDir(path, rpo);
|
||||
if (path == null)
|
||||
return false;
|
||||
|
@ -86,19 +89,24 @@ public class DirFilterEventImpl {
|
|||
return dir.replace(rpo.path + "/", rpo.fallback);
|
||||
}
|
||||
|
||||
private static boolean dirHidden(DirRpo rpo, String packId, String file) {
|
||||
private static boolean dirHidden(DirRpo rpo, CacheKey key, String file) {
|
||||
if (rpo.condition == null)
|
||||
return false;
|
||||
try {
|
||||
return !rpo.condition.evaluate(packId);
|
||||
} catch (RpoFormatException e) {
|
||||
Respackopts.LOGGER.error("Couldn't parse dir conditions for " + file + " (" + packId + ")", e);
|
||||
return !rpo.condition.get(MetaCache.getParameter(key));
|
||||
} catch (RuntimeException e) {
|
||||
try {
|
||||
Respackopts.LOGGER.error("Could not evaluate condition " + file + " (pack: " + key.packName() + ") with condition:\n" + ObjectGraphPrinter.printGraph(rpo.condition) + ")", e);
|
||||
} catch (IllegalAccessException ex) {
|
||||
Respackopts.LOGGER.error("Could not evaluate condition " + file + " (pack: " + key.packName() + ")", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static DirRpo findDirRpo(ResourcePack pack, String name) {
|
||||
Map<String, DirRpo> drpReg = MetaCache.getState(MetaCache.getKeyByPack(pack)).cachedDirRPOs();
|
||||
CachedPackState state = MetaCache.getState(MetaCache.getKeyByPack(pack));
|
||||
Map<String, DirRpo> drpReg = state.cachedDirRPOs();
|
||||
int li = name.lastIndexOf('/');
|
||||
if (li <= 0)
|
||||
return null;
|
||||
|
@ -118,7 +126,7 @@ public class DirFilterEventImpl {
|
|||
}
|
||||
if (UserResourceEvents.disable(() -> pack.contains(rp.getType(), rp.getId()))) {
|
||||
try (InputStream stream = UserResourceEvents.disable(() -> pack.open(rp.getType(), rp.getId())); Reader w = new InputStreamReader(stream)) {
|
||||
drp = Respackopts.GSON.fromJson(w, DirRpo.class);
|
||||
drp = AttachmentHolder.deserialize(state.metadata().version, w, DirRpo.class);
|
||||
drp.path = name;
|
||||
if (drp.fallback != null && !drp.fallback.endsWith("/"))
|
||||
drp.fallback += "/";
|
||||
|
|
|
@ -41,7 +41,7 @@ public class FileFilterEventImpl {
|
|||
}
|
||||
else return null;
|
||||
});
|
||||
UserResourceEvents.FIND_RESOURCE.register((type, namespace, prefix, maxDepth, pathFilter, previous, pack) -> {
|
||||
UserResourceEvents.FIND_RESOURCE.register((type, namespace, prefix, allowedPathPredicate, previous, pack) -> {
|
||||
// Warning: the Identifiers here DON'T CONTAIN THE TYPE!
|
||||
// Therefore, it needs to be added when calling a method that generates a ResourcePath!
|
||||
Collection<Identifier> prevVals = previous.get();
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package io.gitlab.jfronny.respackopts.filters.util;
|
||||
|
||||
import io.gitlab.jfronny.muscript.debug.*;
|
||||
import io.gitlab.jfronny.respackopts.model.cache.*;
|
||||
import io.gitlab.jfronny.respackopts.util.MetaCache;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.util.RpoFormatException;
|
||||
|
@ -10,10 +12,15 @@ public class FileExclusionProvider {
|
|||
return FileRpoSearchProvider.modifyWithRpo(file, pack, rpo -> {
|
||||
if (rpo.condition == null)
|
||||
return false;
|
||||
CacheKey key = MetaCache.getKeyByPack(pack);
|
||||
try {
|
||||
return !rpo.condition.evaluate(MetaCache.getId(MetaCache.getKeyByPack(pack)));
|
||||
} catch (RpoFormatException e) {
|
||||
Respackopts.LOGGER.error("Could not evaluate condition " + file, e);
|
||||
return !rpo.condition.get(MetaCache.getParameter(key));
|
||||
} catch (RuntimeException e) {
|
||||
try {
|
||||
Respackopts.LOGGER.error("Could not evaluate condition " + file + " (pack: " + key.packName() + ") with condition:\n" + ObjectGraphPrinter.printGraph(rpo.condition) + ")", e);
|
||||
} catch (IllegalAccessException ex) {
|
||||
Respackopts.LOGGER.error("Could not evaluate condition " + file + " (pack: " + key.packName() + ")", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, false);
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
package io.gitlab.jfronny.respackopts.filters.util;
|
||||
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import meteordevelopment.starscript.Script;
|
||||
import net.minecraft.resource.ResourcePack;
|
||||
import io.gitlab.jfronny.muscript.*;
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
import io.gitlab.jfronny.respackopts.*;
|
||||
import io.gitlab.jfronny.respackopts.util.*;
|
||||
import net.minecraft.resource.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
public class FileExpansionProvider {
|
||||
public static synchronized InputStream replace(InputStream is, Map<String, Script> expansions) throws IOException {
|
||||
public static synchronized InputStream replace(OAny<?> parameter, InputStream is, Map<String, Expr.StringExpr> expansions) throws IOException {
|
||||
String s = new String(is.readAllBytes());
|
||||
for (Map.Entry<String, Script> entry : expansions.entrySet()) {
|
||||
s = s.replace("${" + entry.getKey() + "}", Respackopts.STAR_SCRIPT.run(entry.getValue()).toString());
|
||||
for (Map.Entry<String, Expr.StringExpr> entry : expansions.entrySet()) {
|
||||
s = s.replace("${" + entry.getKey() + "}", entry.getValue().get(parameter));
|
||||
}
|
||||
return new ByteArrayInputStream(s.getBytes());
|
||||
}
|
||||
|
@ -21,7 +24,7 @@ public class FileExpansionProvider {
|
|||
if (rpo.expansions == null || rpo.expansions.isEmpty())
|
||||
return inputStream;
|
||||
try {
|
||||
return replace(inputStream, rpo.expansions);
|
||||
return replace(MetaCache.getParameter(MetaCache.getKeyByPack(pack)), inputStream, rpo.expansions);
|
||||
} catch (IOException e) {
|
||||
Respackopts.LOGGER.error("Could not perform file expansion on " + file, e);
|
||||
return inputStream;
|
||||
|
|
|
@ -2,8 +2,10 @@ package io.gitlab.jfronny.respackopts.filters.util;
|
|||
|
||||
import io.gitlab.jfronny.libjf.ResourcePath;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.gson.*;
|
||||
import io.gitlab.jfronny.respackopts.model.DirRpo;
|
||||
import io.gitlab.jfronny.respackopts.model.FileRpo;
|
||||
import io.gitlab.jfronny.respackopts.model.cache.*;
|
||||
import io.gitlab.jfronny.respackopts.util.MetaCache;
|
||||
import net.minecraft.resource.ResourceNotFoundException;
|
||||
import net.minecraft.resource.ResourcePack;
|
||||
|
@ -22,7 +24,8 @@ public class FileRpoSearchProvider {
|
|||
public static <T> T modifyWithRpo(String fileName, ResourcePack pack, ModifiedGenerator<T> getModified, T defaultValue) {
|
||||
if (FileRpoSearchProvider.isRpo(fileName))
|
||||
return defaultValue;
|
||||
Map<String, FileRpo> frpReg = MetaCache.getState(MetaCache.getKeyByPack(pack)).cachedFileRPOs();
|
||||
CachedPackState state = MetaCache.getState(MetaCache.getKeyByPack(pack));
|
||||
Map<String, FileRpo> frpReg = state.cachedFileRPOs();
|
||||
String rpPathName = fileName + Respackopts.FILE_EXTENSION;
|
||||
if (frpReg.containsKey(rpPathName))
|
||||
return getModified.getModified(frpReg.get(rpPathName));
|
||||
|
@ -40,7 +43,7 @@ public class FileRpoSearchProvider {
|
|||
try (InputStream stream = rpoPath == null ? pack.openRoot(rpPathName) : pack.open(rpoPath.getType(), rpoPath.getId());
|
||||
Reader w = stream == null ? null : new InputStreamReader(stream)) {
|
||||
if (w == null) throw new FileNotFoundException("Could not find file: " + fileName);
|
||||
FileRpo frp = Respackopts.GSON.fromJson(w, FileRpo.class);
|
||||
FileRpo frp = AttachmentHolder.deserialize(state.metadata().version, w, FileRpo.class);
|
||||
frp.path = rpPathName;
|
||||
frpReg.put(rpPathName, frp);
|
||||
return getModified.getModified(frp);
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package io.gitlab.jfronny.respackopts.gson;
|
||||
|
||||
import io.gitlab.jfronny.commons.throwable.*;
|
||||
import io.gitlab.jfronny.respackopts.*;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class AttachmentHolder {
|
||||
private static final ThreadLocal<Attachment> attachments = new ThreadLocal<>();
|
||||
|
||||
public static <TEx extends Throwable> void attach(int version, ThrowingRunnable<TEx> runnable) throws TEx {
|
||||
try {
|
||||
attachments.set(new Attachment(version));
|
||||
runnable.run();
|
||||
} finally {
|
||||
attachments.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public static <TOut, TEx extends Throwable> TOut attach(int version, ThrowingSupplier<TOut, TEx> runnable) throws TEx {
|
||||
try {
|
||||
attachments.set(new Attachment(version));
|
||||
return runnable.get();
|
||||
} finally {
|
||||
attachments.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T deserialize(int version, Reader r, Class<T> klazz) {
|
||||
return attach(version, () -> Respackopts.GSON.fromJson(r, klazz));
|
||||
}
|
||||
|
||||
public static int getAttachedVersion() {
|
||||
return attachments.get().version;
|
||||
}
|
||||
|
||||
private record Attachment(int version) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package io.gitlab.jfronny.respackopts.gson;
|
||||
|
||||
import io.gitlab.jfronny.gson.*;
|
||||
import io.gitlab.jfronny.gson.reflect.*;
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.compiler.expr.*;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
|
||||
public class BoolExprDeserializer implements JsonDeserializer<Expr.BoolExpr> {
|
||||
private static final Type conditionListType = new TypeToken<List<Expr.BoolExpr>>(){}.getType();
|
||||
|
||||
@Override
|
||||
public Expr.BoolExpr deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (AttachmentHolder.getAttachedVersion() > 7) {
|
||||
return ((Expr<?>)context.deserialize(json, Expr.class)).asBoolExpr();
|
||||
}
|
||||
|
||||
// Legacy JSON syntax
|
||||
if (json.isJsonObject()) {
|
||||
JsonObject jo = json.getAsJsonObject();
|
||||
if (jo.size() != 1)
|
||||
throw new JsonParseException("More than one key in a condition object");
|
||||
for (Map.Entry<String, JsonElement> entry : jo.entrySet()) {
|
||||
return switch (entry.getKey().toLowerCase(Locale.ROOT)) {
|
||||
case "and", "add", "&" -> merge(context.deserialize(entry.getValue(), conditionListType), Token.And);
|
||||
case "==", "=", "equal", "eq" -> merge(context.deserialize(entry.getValue(), conditionListType), Token.EqualEqual);
|
||||
case "not", "nor", "!" -> new Not(merge(context.deserialize(entry.getValue(), conditionListType), Token.Or));
|
||||
case "or", "|" -> merge(context.deserialize(entry.getValue(), conditionListType), Token.Or);
|
||||
case "^", "xor" -> merge(context.deserialize(entry.getValue(), conditionListType), Token.BangEqual);
|
||||
default -> throw new JsonParseException("Unknown condition type: " + entry.getKey());
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (json.isJsonArray()) {
|
||||
return merge(context.deserialize(json, conditionListType), Token.And);
|
||||
}
|
||||
else if (json.isJsonPrimitive()) {
|
||||
JsonPrimitive pr = json.getAsJsonPrimitive();
|
||||
if (pr.isString()) {
|
||||
String name = pr.getAsString();
|
||||
if (name.toLowerCase(Locale.ROOT).equals("true"))
|
||||
return Expr.literal(true);
|
||||
if (name.toLowerCase(Locale.ROOT).equals("false"))
|
||||
return Expr.literal(false);
|
||||
return rpoBooleanCondition(name);
|
||||
}
|
||||
else if (pr.isBoolean()) {
|
||||
return Expr.literal(pr.getAsBoolean());
|
||||
}
|
||||
}
|
||||
throw new JsonParseException("Invalid data type for condition");
|
||||
}
|
||||
|
||||
private Expr.BoolExpr merge(List<Expr.BoolExpr> expressions, Token token) {
|
||||
Expr.BoolExpr current = expressions.get(0);
|
||||
for (Expr.BoolExpr expr : expressions.subList(1, expressions.size())) {
|
||||
current = switch (token) {
|
||||
case EqualEqual -> new Equal(current, expr);
|
||||
case BangEqual -> new Not(new Equal(current, expr));
|
||||
default -> new LogicBiExpr(current, expr, token);
|
||||
};
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private Expr.BoolExpr rpoBooleanCondition(String name) {
|
||||
if (name.startsWith("modversion:")) {
|
||||
String code = name.substring("modversion:".length());
|
||||
String mod = code.substring(0, code.indexOf(':'));
|
||||
String predicate = code.substring(code.indexOf(':') + 1);
|
||||
return new Call(new Variable("version"), List.of(
|
||||
Expr.literal(mod).asObjectExpr(),
|
||||
Expr.literal(predicate).asObjectExpr()
|
||||
)).asBoolExpr();
|
||||
}
|
||||
Expr.ObjectExpr e = null;
|
||||
String[] arr = name.split("[:.]");
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
if (i == 0) e = new Variable(arr[i]);
|
||||
else e = new Get(e, Expr.literal(arr[i]));
|
||||
}
|
||||
if (e == null) throw new JsonParseException("Invalid RPO condition: \"" + name + "\"");
|
||||
return e.asBoolExpr();
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.gson;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import io.gitlab.jfronny.respackopts.model.condition.*;
|
||||
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class ConditionDeserializer implements JsonDeserializer<Condition> {
|
||||
private static final AndConditionFactory and = new AndConditionFactory();
|
||||
private static final Set<CollectingConditionFactory> factories = Set.of(
|
||||
and,
|
||||
new EqualityConditionFactory(),
|
||||
new NorConditionFactory(),
|
||||
new OrConditionFactory(),
|
||||
new XorConditionFactory()
|
||||
);
|
||||
private static final Type conditionListType = new TypeToken<List<Condition>>(){}.getType();
|
||||
|
||||
@Override
|
||||
public Condition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json.isJsonObject()) {
|
||||
JsonObject jo = json.getAsJsonObject();
|
||||
if (jo.size() != 1)
|
||||
throw new JsonParseException("More than one key in a condition object");
|
||||
for (Map.Entry<String, JsonElement> entry : jo.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
if (name.equals(ResourceConditions.CONDITIONS_KEY)) {
|
||||
if (entry.getValue() instanceof JsonObject fabricCondition) {
|
||||
return new FabricCondition(fabricCondition);
|
||||
}
|
||||
else throw new JsonParseException("Expected " + ResourceConditions.CONDITIONS_KEY + " to be an object condition");
|
||||
}
|
||||
for (CollectingConditionFactory factory : factories) {
|
||||
if (factory.getNames().contains(name))
|
||||
return factory.build(context.deserialize(entry.getValue(), conditionListType));
|
||||
}
|
||||
throw new JsonParseException("Unknown condition type: " + name);
|
||||
}
|
||||
}
|
||||
else if (json.isJsonArray()) {
|
||||
return and.build(context.deserialize(json, conditionListType));
|
||||
}
|
||||
else if (json.isJsonPrimitive()) {
|
||||
JsonPrimitive pr = json.getAsJsonPrimitive();
|
||||
if (pr.isString()) {
|
||||
String name = pr.getAsString();
|
||||
if (name.toLowerCase(Locale.ROOT).equals("true"))
|
||||
return new BooleanCondition(true);
|
||||
if (name.toLowerCase(Locale.ROOT).equals("false"))
|
||||
return new BooleanCondition(false);
|
||||
return new RpoBooleanCondition(name);
|
||||
}
|
||||
else if (pr.isBoolean()) {
|
||||
return new BooleanCondition(pr.getAsBoolean());
|
||||
}
|
||||
}
|
||||
throw new JsonParseException("Invalid data type for condition");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package io.gitlab.jfronny.respackopts.gson;
|
||||
|
||||
import io.gitlab.jfronny.gson.*;
|
||||
import io.gitlab.jfronny.muscript.*;
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
|
||||
public class ExprDeserializer implements JsonDeserializer<Expr<?>> {
|
||||
private static final Map<String, Expr<?>> compiledScripts = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Expr<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) {
|
||||
String s = json.getAsJsonPrimitive().getAsString();
|
||||
if (compiledScripts.containsKey(s))
|
||||
return compiledScripts.get(s);
|
||||
try {
|
||||
Expr<?> expr = Parser.parse(AttachmentHolder.getAttachedVersion() <= 7
|
||||
? StarScriptIngester.starScriptToMu(s)
|
||||
: s);
|
||||
compiledScripts.put(s, expr);
|
||||
return expr;
|
||||
} catch (Parser.ParseException e) {
|
||||
throw new JsonParseException("Could not create script", e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new JsonParseException("Could not parse script: Expected string");
|
||||
}
|
||||
}
|
||||
|
||||
public static class StringX implements JsonDeserializer<Expr.StringExpr> {
|
||||
@Override
|
||||
public Expr.StringExpr deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
|
||||
Expr<?> expr = jsonDeserializationContext.deserialize(jsonElement, Expr.class);
|
||||
return expr.asStringExpr();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.gson;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import meteordevelopment.starscript.Script;
|
||||
import meteordevelopment.starscript.compiler.Compiler;
|
||||
import meteordevelopment.starscript.compiler.Parser;
|
||||
import meteordevelopment.starscript.utils.Error;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ScriptDeserializer implements JsonDeserializer<Script> {
|
||||
private static final Map<String, Script> compiledScripts = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Script deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json.isJsonPrimitive() && json.getAsJsonPrimitive().isString()) {
|
||||
String s = json.getAsJsonPrimitive().getAsString();
|
||||
if (compiledScripts.containsKey(s))
|
||||
return compiledScripts.get(s);
|
||||
Parser.Result result = Parser.parse(s);
|
||||
if (result.hasErrors()) {
|
||||
for (Error error : result.errors) {
|
||||
Respackopts.LOGGER.error(error.toString());
|
||||
}
|
||||
throw new JsonParseException("Could not parse script: See errors above");
|
||||
}
|
||||
Script ss = Compiler.compile(result);
|
||||
compiledScripts.put(s, ss);
|
||||
return ss;
|
||||
}
|
||||
else {
|
||||
throw new JsonParseException("Could not parse script: Expected string");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.gson;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SingleEntryListTypeAdapterFactory implements TypeAdapterFactory {
|
||||
@Override
|
||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
|
||||
Type type = typeToken.getType();
|
||||
if (typeToken.getRawType() != List.class
|
||||
|| !(type instanceof ParameterizedType pt))
|
||||
return null;
|
||||
if (Respackopts.CONFIG.debugLogs)
|
||||
Respackopts.LOGGER.info("Using SingleEntryListTypeAdapter for " + typeToken);
|
||||
Type elementType = pt.getActualTypeArguments()[0];
|
||||
TypeAdapter<?> elementAdapter = gson.getAdapter(TypeToken.get(elementType));
|
||||
return (TypeAdapter<T>) createAdapter(elementAdapter);
|
||||
}
|
||||
|
||||
private <T> TypeAdapter<List<T>> createAdapter(final TypeAdapter<T> elementAdapter) {
|
||||
return new TypeAdapter<>() {
|
||||
@Override
|
||||
public void write(JsonWriter out, List<T> value) throws IOException {
|
||||
if (value == null) {
|
||||
out.nullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
out.beginArray();
|
||||
for (T entry : value) {
|
||||
elementAdapter.write(out, entry);
|
||||
}
|
||||
out.endArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<T> read(JsonReader in) throws IOException {
|
||||
if (in.peek() == JsonToken.NULL) {
|
||||
in.nextNull();
|
||||
return null;
|
||||
}
|
||||
|
||||
List<T> list = new ArrayList<>();
|
||||
if (in.peek() == JsonToken.BEGIN_ARRAY) {
|
||||
in.beginArray();
|
||||
while (in.hasNext()) {
|
||||
list.add(elementAdapter.read(in));
|
||||
}
|
||||
in.endArray();
|
||||
}
|
||||
else list.add(elementAdapter.read(in));
|
||||
return list;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.gson;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
import com.google.gson.stream.JsonToken;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class SingleEntrySetTypeAdapterFactory implements TypeAdapterFactory {
|
||||
@Override
|
||||
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
|
||||
Type type = typeToken.getType();
|
||||
if (typeToken.getRawType() != Set.class
|
||||
|| !(type instanceof ParameterizedType pt))
|
||||
return null;
|
||||
if (Respackopts.CONFIG.debugLogs)
|
||||
Respackopts.LOGGER.info("Using SingleEntrySetTypeAdapter for " + typeToken);
|
||||
Type elementType = pt.getActualTypeArguments()[0];
|
||||
TypeAdapter<?> elementAdapter = gson.getAdapter(TypeToken.get(elementType));
|
||||
return (TypeAdapter<T>) createAdapter(elementAdapter);
|
||||
}
|
||||
|
||||
private <T> TypeAdapter<Set<T>> createAdapter(final TypeAdapter<T> elementAdapter) {
|
||||
return new TypeAdapter<>() {
|
||||
@Override
|
||||
public void write(JsonWriter out, Set<T> value) throws IOException {
|
||||
if (value == null) {
|
||||
out.nullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
out.beginArray();
|
||||
for (T entry : value) {
|
||||
elementAdapter.write(out, entry);
|
||||
}
|
||||
out.endArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<T> read(JsonReader in) throws IOException {
|
||||
if (in.peek() == JsonToken.NULL) {
|
||||
in.nextNull();
|
||||
return null;
|
||||
}
|
||||
|
||||
Set<T> list = new LinkedHashSet<>();
|
||||
if (in.peek() == JsonToken.BEGIN_ARRAY) {
|
||||
in.beginArray();
|
||||
while (in.hasNext()) {
|
||||
list.add(elementAdapter.read(in));
|
||||
}
|
||||
in.endArray();
|
||||
}
|
||||
else list.add(elementAdapter.read(in));
|
||||
return list;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package io.gitlab.jfronny.respackopts.gson.entry;
|
||||
|
||||
import com.google.gson.*;
|
||||
import io.gitlab.jfronny.gson.*;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigBooleanEntry;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package io.gitlab.jfronny.respackopts.gson.entry;
|
||||
|
||||
import com.google.gson.*;
|
||||
import io.gitlab.jfronny.gson.*;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.*;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.PackReloadType;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package io.gitlab.jfronny.respackopts.gson.entry;
|
||||
|
||||
import com.google.gson.*;
|
||||
import io.gitlab.jfronny.gson.*;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigEnumEntry;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package io.gitlab.jfronny.respackopts.gson.entry;
|
||||
|
||||
import com.google.gson.*;
|
||||
import io.gitlab.jfronny.gson.*;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigNumericEntry;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.NumericEntryType;
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import me.shedaniel.clothconfig2.api.ConfigBuilder;
|
|||
import me.shedaniel.clothconfig2.api.ConfigCategory;
|
||||
import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
|
||||
import net.minecraft.client.gui.screen.FatalErrorScreen;
|
||||
import net.minecraft.text.TranslatableText;
|
||||
import net.minecraft.text.*;
|
||||
|
||||
public class ModMenuCompat implements ModMenuApi {
|
||||
@Override
|
||||
|
@ -20,7 +20,7 @@ public class ModMenuCompat implements ModMenuApi {
|
|||
ConfigBuilder builder;
|
||||
builder = ConfigBuilder.create()
|
||||
.setParentScreen(parent)
|
||||
.setTitle(new TranslatableText("respackopts.mainconfig"));
|
||||
.setTitle(Text.translatable("respackopts.mainconfig"));
|
||||
ConfigEntryBuilder entryBuilder = builder.entryBuilder();
|
||||
PackReloadType.Aggregator agg = new PackReloadType.Aggregator();
|
||||
builder.setSavingRunnable(() -> {
|
||||
|
@ -34,21 +34,21 @@ public class ModMenuCompat implements ModMenuApi {
|
|||
});
|
||||
//Respackopts config screen
|
||||
ConfigFile defaultConfig = new ConfigFile();
|
||||
ConfigCategory mainConfig = builder.getOrCreateCategory(new TranslatableText("respackopts.mainconfig"));
|
||||
ConfigCategory mainConfig = builder.getOrCreateCategory(Text.translatable("respackopts.mainconfig"));
|
||||
mainConfig.addEntry(
|
||||
entryBuilder.startBooleanToggle(new TranslatableText("respackopts.mainconfig.debugCommands"), Respackopts.CONFIG.debugCommands)
|
||||
entryBuilder.startBooleanToggle(Text.translatable("respackopts.mainconfig.debugCommands"), Respackopts.CONFIG.debugCommands)
|
||||
.setDefaultValue(defaultConfig.debugCommands)
|
||||
.setSaveConsumer(b -> Respackopts.CONFIG.debugCommands = b)
|
||||
.build()
|
||||
);
|
||||
mainConfig.addEntry(
|
||||
entryBuilder.startBooleanToggle(new TranslatableText("respackopts.mainconfig.debugLogs"), Respackopts.CONFIG.debugLogs)
|
||||
entryBuilder.startBooleanToggle(Text.translatable("respackopts.mainconfig.debugLogs"), Respackopts.CONFIG.debugLogs)
|
||||
.setDefaultValue(defaultConfig.debugLogs)
|
||||
.setSaveConsumer(b -> Respackopts.CONFIG.debugLogs = b)
|
||||
.build()
|
||||
);
|
||||
mainConfig.addEntry(
|
||||
entryBuilder.startBooleanToggle(new TranslatableText("respackopts.mainconfig.dashloaderCompat"), Respackopts.CONFIG.dashloaderCompat)
|
||||
entryBuilder.startBooleanToggle(Text.translatable("respackopts.mainconfig.dashloaderCompat"), Respackopts.CONFIG.dashloaderCompat)
|
||||
.requireRestart()
|
||||
.setDefaultValue(defaultConfig.dashloaderCompat)
|
||||
.setSaveConsumer(b -> Respackopts.CONFIG.dashloaderCompat = b)
|
||||
|
@ -56,14 +56,14 @@ public class ModMenuCompat implements ModMenuApi {
|
|||
);
|
||||
//Pack config screens
|
||||
MetaCache.forEach((key, state) -> {
|
||||
ConfigCategory config = builder.getOrCreateCategory(new TranslatableText((state.metadata().version >= 5 ? "rpo." : "respackopts.title.") + state.packId()));
|
||||
ConfigCategory config = builder.getOrCreateCategory(Text.translatable((state.metadata().version >= 5 ? "rpo." : "respackopts.title.") + state.packId()));
|
||||
Respackopts.GUI_FACTORY.buildCategory(state.configBranch(), state.packId(), config::addEntry, agg, entryBuilder, "");
|
||||
});
|
||||
return builder.build();
|
||||
}
|
||||
catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
return new FatalErrorScreen(new TranslatableText("respackopts.loadFailed"), new TranslatableText("respackopts.loadError"));
|
||||
return new FatalErrorScreen(Text.translatable("respackopts.loadFailed"), Text.translatable("respackopts.loadError"));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ public abstract class ResourcePackEntryMixin {
|
|||
|
||||
@Inject(at = @At("TAIL"), method = "render(Lnet/minecraft/client/util/math/MatrixStack;IIIIIIIZF)V")
|
||||
private void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta, CallbackInfo info) {
|
||||
if (this.isSelectable() && MetaCache.getKeyByDisplayName(pack.getDisplayName().asString()) != null) {
|
||||
if (this.isSelectable() && MetaCache.getKeyByDisplayName(pack.getDisplayName().getString()) != null) {
|
||||
int d = mouseX - x;
|
||||
d = widget.getRowWidth() - d;
|
||||
int e = mouseY - y;
|
||||
|
@ -44,8 +44,7 @@ public abstract class ResourcePackEntryMixin {
|
|||
public void mouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> info) {
|
||||
if (!info.getReturnValue()) {
|
||||
if (this.isSelectable()) {
|
||||
String displayName = pack.getDisplayName().asString();
|
||||
CacheKey dataLocation = MetaCache.getKeyByDisplayName(displayName);
|
||||
CacheKey dataLocation = MetaCache.getKeyByDisplayName(pack.getDisplayName().getString());
|
||||
if (dataLocation != null && rpo$selected) {
|
||||
info.setReturnValue(true);
|
||||
MinecraftClient c = MinecraftClient.getInstance();
|
||||
|
|
|
@ -38,7 +38,7 @@ public class ResourcePackManagerMixin {
|
|||
}
|
||||
if (packConfType != null) {
|
||||
try (InputStream is = rpi.open(packConfType, Respackopts.CONF_ID); InputStreamReader isr = new InputStreamReader(is)) {
|
||||
String displayName = v.getDisplayName().asString();
|
||||
String displayName = v.getDisplayName().getString();
|
||||
String packName = rpi.getName();
|
||||
PackMeta conf = Respackopts.GSON.fromJson(isr, PackMeta.class);
|
||||
if (Respackopts.CONFIG.debugLogs)
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
package io.gitlab.jfronny.respackopts.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import io.gitlab.jfronny.libjf.gson.GsonHidden;
|
||||
import io.gitlab.jfronny.respackopts.model.condition.Condition;
|
||||
import com.google.gson.annotations.*;
|
||||
import io.gitlab.jfronny.commons.serialize.gson.api.*;
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
|
||||
public class DirRpo {
|
||||
@SerializedName(value = "condition", alternate = {"conditions"})
|
||||
public Condition condition;
|
||||
public Expr.BoolExpr condition;
|
||||
@SerializedName(value = "fallback", alternate = {"fallbacks"})
|
||||
public String fallback;
|
||||
|
||||
@GsonHidden
|
||||
@Ignore
|
||||
public String path;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
package io.gitlab.jfronny.respackopts.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import io.gitlab.jfronny.libjf.gson.GsonHidden;
|
||||
import io.gitlab.jfronny.respackopts.model.condition.Condition;
|
||||
import meteordevelopment.starscript.Script;
|
||||
import com.google.gson.annotations.*;
|
||||
import io.gitlab.jfronny.commons.serialize.gson.api.*;
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
public class FileRpo {
|
||||
@SerializedName(value = "condition", alternate = {"conditions"})
|
||||
public Condition condition;
|
||||
public Expr.BoolExpr condition;
|
||||
@SerializedName(value = "fallback", alternate = {"fallbacks"})
|
||||
public Set<String> fallbacks;
|
||||
@SerializedName(value = "expansion", alternate = {"expansions"})
|
||||
public Map<String, Script> expansions;
|
||||
public Map<String, Expr.StringExpr> expansions;
|
||||
|
||||
@GsonHidden
|
||||
@Ignore
|
||||
public String path;
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model;
|
||||
|
||||
public interface ThrowingBiConsumer<T, U, TEx extends Throwable> {
|
||||
void accept(T var1, U var2) throws TEx;
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package io.gitlab.jfronny.respackopts.model.cache;
|
||||
|
||||
import io.gitlab.jfronny.muscript.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
import io.gitlab.jfronny.respackopts.model.DirRpo;
|
||||
import io.gitlab.jfronny.respackopts.model.FileRpo;
|
||||
import io.gitlab.jfronny.respackopts.model.PackMeta;
|
||||
|
@ -15,9 +17,17 @@ public record CachedPackState(
|
|||
ConfigBranch configBranch,
|
||||
PackMeta metadata,
|
||||
Map<String, FileRpo> cachedFileRPOs,
|
||||
Map<String, DirRpo> cachedDirRPOs
|
||||
Map<String, DirRpo> cachedDirRPOs,
|
||||
ExpressionParameter expressionParameter
|
||||
) {
|
||||
public CachedPackState(CacheKey key, PackMeta meta, ConfigBranch branch) {
|
||||
this(meta.id, key.displayName(), key.packName(), branch, meta, new HashMap<>(), new HashMap<>());
|
||||
this(meta.id,
|
||||
key.displayName(),
|
||||
key.packName(),
|
||||
branch,
|
||||
meta,
|
||||
new HashMap<>(),
|
||||
new HashMap<>(),
|
||||
new ExpressionParameter(new HashMap<>(branch.getOptic().getValue())));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model.condition;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class AndConditionFactory implements CollectingConditionFactory {
|
||||
@Override
|
||||
public Condition build(List<Condition> conditions) {
|
||||
return packId -> {
|
||||
for (Condition condition : conditions) {
|
||||
if (!condition.evaluate(packId))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNames() {
|
||||
return Set.of("and", "add", "&");
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model.condition;
|
||||
|
||||
public record BooleanCondition(boolean value) implements Condition {
|
||||
@Override
|
||||
public boolean evaluate(String packId) {
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model.condition;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public interface CollectingConditionFactory {
|
||||
Condition build(List<Condition> conditions);
|
||||
Set<String> getNames();
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model.condition;
|
||||
|
||||
import io.gitlab.jfronny.respackopts.util.RpoFormatException;
|
||||
|
||||
public interface Condition {
|
||||
boolean evaluate(String packId) throws RpoFormatException;
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model.condition;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class EqualityConditionFactory implements CollectingConditionFactory {
|
||||
@Override
|
||||
public Condition build(List<Condition> conditions) {
|
||||
return packId -> {
|
||||
Boolean b = null;
|
||||
for (Condition condition : conditions) {
|
||||
if (b == null)
|
||||
b = condition.evaluate(packId);
|
||||
else if (b != condition.evaluate(packId))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNames() {
|
||||
return Set.of("==", "=", "equal", "eq");
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model.condition;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import io.gitlab.jfronny.respackopts.util.RpoFormatException;
|
||||
import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions;
|
||||
|
||||
public record FabricCondition(JsonObject condition) implements Condition {
|
||||
@Override
|
||||
public boolean evaluate(String packId) throws RpoFormatException {
|
||||
return ResourceConditions.conditionMatches(condition);
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model.condition;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class NorConditionFactory implements CollectingConditionFactory {
|
||||
@Override
|
||||
public Condition build(List<Condition> conditions) {
|
||||
return packId -> {
|
||||
for (Condition condition : conditions) {
|
||||
if (condition.evaluate(packId))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNames() {
|
||||
return Set.of("not", "nor", "!");
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model.condition;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class OrConditionFactory implements CollectingConditionFactory {
|
||||
@Override
|
||||
public Condition build(List<Condition> conditions) {
|
||||
return packId -> {
|
||||
for (Condition condition : conditions) {
|
||||
if (condition.evaluate(packId))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNames() {
|
||||
return Set.of("or", "|");
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model.condition;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.model.cache.CacheKey;
|
||||
import io.gitlab.jfronny.respackopts.model.cache.CachedPackState;
|
||||
import io.gitlab.jfronny.respackopts.util.MetaCache;
|
||||
import io.gitlab.jfronny.respackopts.model.ThrowingBiConsumer;
|
||||
import io.gitlab.jfronny.respackopts.util.RpoFormatException;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.api.ModContainer;
|
||||
import net.fabricmc.loader.api.VersionParsingException;
|
||||
import net.fabricmc.loader.api.metadata.version.VersionPredicate;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public record RpoBooleanCondition(String name) implements Condition {
|
||||
public RpoBooleanCondition {
|
||||
if (name == null)
|
||||
throw new JsonParseException("Condition must not be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(String packId) throws RpoFormatException {
|
||||
String condition = name;
|
||||
if (!condition.contains(":")) {
|
||||
condition = packId + ":" + condition;
|
||||
}
|
||||
String sourcePack = condition.substring(0, condition.indexOf(':'));
|
||||
condition = condition.substring(condition.indexOf(':') + 1);
|
||||
final String finalCondition = condition;
|
||||
if (sourcePack.equals("modversion")) {
|
||||
// builtin conditions
|
||||
String modName = condition.substring(0, condition.indexOf(':'));
|
||||
Optional<ModContainer> container = FabricLoader.getInstance().getModContainer(modName);
|
||||
if (container.isEmpty()) {
|
||||
if (Respackopts.CONFIG.debugLogs) Respackopts.LOGGER.info("Mod not found: " + modName);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return VersionPredicate.parse(condition.substring(condition.indexOf(':') + 1)).test(container.get().getMetadata().getVersion());
|
||||
} catch (VersionParsingException e) {
|
||||
throw new RpoFormatException("Could not parse version predicate", e);
|
||||
}
|
||||
}
|
||||
AtomicReference<Boolean> result = new AtomicReference<>(null);
|
||||
MetaCache.forEach((ThrowingBiConsumer<CacheKey, CachedPackState, ? extends RpoFormatException>) (key, data) -> {
|
||||
if (Objects.equals(MetaCache.getId(key), sourcePack)) {
|
||||
try {
|
||||
result.set(data.configBranch().getBoolean(finalCondition));
|
||||
} catch (RpoFormatException ex) {
|
||||
throw new RpoFormatException("Could not get value", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (result.get() == null)
|
||||
throw new RpoFormatException("Could not find pack with specified ID: " + sourcePack);
|
||||
return result.get();
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.model.condition;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class XorConditionFactory implements CollectingConditionFactory {
|
||||
@Override
|
||||
public Condition build(List<Condition> conditions) {
|
||||
return packId -> {
|
||||
boolean bl = false;
|
||||
for (Condition condition : conditions) {
|
||||
if (condition.evaluate(packId))
|
||||
bl = !bl;
|
||||
}
|
||||
return bl;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNames() {
|
||||
return Set.of("^", "xor");
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package io.gitlab.jfronny.respackopts.model.tree;
|
||||
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
|
||||
import meteordevelopment.starscript.value.Value;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -29,8 +29,8 @@ public class ConfigBooleanEntry extends ConfigEntry<Boolean> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Value buildStarScript() {
|
||||
return Value.bool(getValue());
|
||||
public OAny<?> getOptic() {
|
||||
return (OBool) this::getValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
package io.gitlab.jfronny.respackopts.model.tree;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import io.gitlab.jfronny.gson.reflect.TypeToken;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.ConfigSyncMode;
|
||||
import io.gitlab.jfronny.respackopts.util.IndentingStringBuilder;
|
||||
import io.gitlab.jfronny.respackopts.util.RpoFormatException;
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
|
||||
import me.shedaniel.clothconfig2.impl.builders.SubCategoryBuilder;
|
||||
import meteordevelopment.starscript.value.Value;
|
||||
import meteordevelopment.starscript.value.ValueMap;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
|
@ -113,12 +112,12 @@ public class ConfigBranch extends ConfigEntry<Map<String, ConfigEntry<?>>> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Value buildStarScript() {
|
||||
ValueMap vm = new ValueMap();
|
||||
public OObject getOptic() {
|
||||
Map<String, OAny<?>> map = new HashMap<>();
|
||||
for (Map.Entry<String, ConfigEntry<?>> e : super.getValue().entrySet()) {
|
||||
vm.set(Respackopts.sanitizeString(e.getKey()), () -> e.getValue().buildStarScript());
|
||||
map.put(Respackopts.sanitizeString(e.getKey()), e.getValue().getOptic());
|
||||
}
|
||||
return Value.map(vm);
|
||||
return OFinal.of(map);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package io.gitlab.jfronny.respackopts.model.tree;
|
||||
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.ConfigSyncMode;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.PackReloadType;
|
||||
import io.gitlab.jfronny.respackopts.util.IndentingStringBuilder;
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
|
||||
import meteordevelopment.starscript.value.Value;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -102,7 +102,7 @@ public abstract class ConfigEntry<T> {
|
|||
|
||||
public abstract void buildShader(StringBuilder sb, String valueName);
|
||||
|
||||
public abstract Value buildStarScript();
|
||||
public abstract OAny<?> getOptic();
|
||||
|
||||
public abstract AbstractConfigListEntry<?> buildEntry(GuiEntryBuilderParam guiEntryBuilderParam);
|
||||
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
package io.gitlab.jfronny.respackopts.model.tree;
|
||||
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.ConfigSyncMode;
|
||||
import io.gitlab.jfronny.respackopts.util.GuiFactory;
|
||||
import io.gitlab.jfronny.respackopts.util.IndentingStringBuilder;
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
|
||||
import meteordevelopment.starscript.value.Value;
|
||||
import meteordevelopment.starscript.value.ValueMap;
|
||||
import net.minecraft.text.Text;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
import io.gitlab.jfronny.respackopts.*;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.*;
|
||||
import io.gitlab.jfronny.respackopts.util.*;
|
||||
import me.shedaniel.clothconfig2.api.*;
|
||||
import net.minecraft.text.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.*;
|
||||
|
||||
public class ConfigEnumEntry extends ConfigEntry<String> {
|
||||
public List<String> values = new ArrayList<>();
|
||||
|
@ -115,16 +113,8 @@ public class ConfigEnumEntry extends ConfigEntry<String> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Value buildStarScript() {
|
||||
String selected = getValue();
|
||||
if (getVersion() <= 6)
|
||||
return Value.string(selected);
|
||||
ValueMap map = new ValueMap();
|
||||
map.set("value", Value.string(selected));
|
||||
for (String value : values) {
|
||||
map.set(value, Value.bool(selected.equals(value)));
|
||||
}
|
||||
return Value.map(map);
|
||||
public OAny<?> getOptic() {
|
||||
return new OEnum(values, getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package io.gitlab.jfronny.respackopts.model.tree;
|
||||
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.ConfigSyncMode;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.NumericEntryType;
|
||||
|
@ -7,7 +8,6 @@ import io.gitlab.jfronny.respackopts.gson.entry.NumericEntrySerializer;
|
|||
import io.gitlab.jfronny.respackopts.util.IndentingStringBuilder;
|
||||
import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
|
||||
import me.shedaniel.clothconfig2.impl.builders.DoubleFieldBuilder;
|
||||
import meteordevelopment.starscript.value.Value;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -67,8 +67,8 @@ public class ConfigNumericEntry extends ConfigEntry<Double> {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Value buildStarScript() {
|
||||
return Value.number(getValue());
|
||||
public OAny<?> getOptic() {
|
||||
return (ONumber) this::getValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
package io.gitlab.jfronny.respackopts.util;
|
||||
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import io.gitlab.jfronny.respackopts.model.PackMeta;
|
||||
import io.gitlab.jfronny.respackopts.model.ThrowingBiConsumer;
|
||||
import io.gitlab.jfronny.respackopts.model.cache.CacheKey;
|
||||
import io.gitlab.jfronny.respackopts.model.cache.CachedPackState;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.ConfigSyncMode;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.PackCapability;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigBranch;
|
||||
import net.minecraft.resource.ResourcePack;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import io.gitlab.jfronny.commons.throwable.*;
|
||||
import io.gitlab.jfronny.muscript.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
import io.gitlab.jfronny.respackopts.*;
|
||||
import io.gitlab.jfronny.respackopts.model.*;
|
||||
import io.gitlab.jfronny.respackopts.model.cache.*;
|
||||
import io.gitlab.jfronny.respackopts.model.enums.*;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.*;
|
||||
import net.fabricmc.loader.api.*;
|
||||
import net.fabricmc.loader.api.metadata.version.*;
|
||||
import net.minecraft.resource.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
public class MetaCache {
|
||||
private static final Map<CacheKey, CachedPackState> PACK_STATES = new HashMap<>();
|
||||
|
@ -99,7 +97,6 @@ public class MetaCache {
|
|||
ConfigBranch b = Respackopts.GSON.fromJson(reader, ConfigBranch.class);
|
||||
if (PACK_STATES.containsKey(key))
|
||||
getBranch(key).sync(b, ConfigSyncMode.CONF_LOAD);
|
||||
Respackopts.STAR_SCRIPT.set(Respackopts.sanitizeString(getId(key)), b::buildStarScript);
|
||||
} catch (IOException e) {
|
||||
Respackopts.LOGGER.error("Failed to load " + key, e);
|
||||
}
|
||||
|
@ -134,6 +131,32 @@ public class MetaCache {
|
|||
return PACK_STATES.get(key);
|
||||
}
|
||||
|
||||
public static ExpressionParameter getParameter(@Nullable CacheKey key) {
|
||||
return hydrateParameter(key == null ? new ExpressionParameter() : MetaCache.getState(key).expressionParameter());
|
||||
}
|
||||
|
||||
public static ExpressionParameter hydrateParameter(ExpressionParameter parameter) {
|
||||
MetaCache.forEach((id, state) -> {
|
||||
String key = Respackopts.sanitizeString(state.packId());
|
||||
if (!parameter.has(key))
|
||||
parameter.set(key, state.configBranch().getOptic());
|
||||
});
|
||||
StandardLib.addTo(parameter);
|
||||
parameter.set("version", OFinal.of(args -> {
|
||||
if (args.size() != 2) throw new IllegalArgumentException("Expected 2 arguments on version but got " + args.size());
|
||||
VersionPredicate predicate;
|
||||
try {
|
||||
predicate = VersionPredicate.parse(args.get(1).asString().getValue());
|
||||
} catch (VersionParsingException e) {
|
||||
throw new RuntimeException("Could not parse version predicate", e);
|
||||
}
|
||||
return OFinal.of(FabricLoader.getInstance().getModContainer(args.get(0).asString().getValue())
|
||||
.map(c -> predicate.test(c.getMetadata().getVersion()))
|
||||
.orElse(false));
|
||||
}));
|
||||
return parameter;
|
||||
}
|
||||
|
||||
public static boolean hasCapability(ResourcePack pack, PackCapability capability) {
|
||||
CacheKey key = getKeyByPack(pack);
|
||||
if (key == null) return false;
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts.util;
|
||||
|
||||
import io.gitlab.jfronny.respackopts.Respackopts;
|
||||
import net.fabricmc.loader.api.FabricLoader;
|
||||
import net.fabricmc.loader.api.ModContainer;
|
||||
import net.minecraft.text.Text;
|
||||
import net.minecraft.text.TranslatableText;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import static net.fabricmc.fabric.api.client.command.v1.ClientCommandManager.*;
|
||||
|
||||
public class RpoCommand {
|
||||
private static final ModContainer respackopts = FabricLoader.getInstance().getModContainer(Respackopts.ID).get();
|
||||
public static void register() {
|
||||
DISPATCHER.register(literal("rpo").then(literal("dump").then(literal("glsl").executes(ctx -> {
|
||||
ctx.getSource().sendFeedback(dump(Respackopts.getShaderImportSource(), "frex.glsl"));
|
||||
return 1;
|
||||
}))));
|
||||
DISPATCHER.register(literal("rpo").then(literal("dump").then(literal("config").executes(ctx -> {
|
||||
MetaCache.forEach((id, branch) -> ctx.getSource().sendFeedback(dump(branch.toString(), id + ".txt")));
|
||||
return 1;
|
||||
}))));
|
||||
DISPATCHER.register(literal("rpo").then(literal("dump").executes(ctx -> {
|
||||
MetaCache.forEach((id, branch) -> ctx.getSource().sendFeedback(dump(branch.toString(), id + ".txt")));
|
||||
return 1;
|
||||
})));
|
||||
DISPATCHER.register(literal("rpo").then(literal("version").executes(ctx -> {
|
||||
ctx.getSource().sendFeedback(new TranslatableText("respackopts.versionText", respackopts.getMetadata().getVersion(), Respackopts.META_VERSION));
|
||||
return 1;
|
||||
})));
|
||||
DISPATCHER.register(literal("rpo").executes(ctx -> {
|
||||
ctx.getSource().sendFeedback(new TranslatableText("respackopts.versionText", respackopts.getMetadata().getVersion(), Respackopts.META_VERSION));
|
||||
return 1;
|
||||
}));
|
||||
}
|
||||
|
||||
private static final Path dumpPath = FabricLoader.getInstance().getGameDir().resolve("respackopts");
|
||||
private static Text dump(String text, String fileName) {
|
||||
try {
|
||||
if (!Files.exists(dumpPath)) Files.createDirectories(dumpPath);
|
||||
Path filePath = dumpPath.resolve(fileName);
|
||||
try (BufferedWriter bw = Files.newBufferedWriter(filePath, StandardOpenOption.CREATE)) {
|
||||
bw.write(text);
|
||||
}
|
||||
return new TranslatableText("respackopts.dumpSucceeded", filePath.toAbsolutePath());
|
||||
}
|
||||
catch (Throwable e) {
|
||||
return new TranslatableText("respackopts.dumpFailed");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package io.gitlab.jfronny.respackopts;
|
||||
|
||||
import io.gitlab.jfronny.gson.reflect.*;
|
||||
import io.gitlab.jfronny.muscript.*;
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
import io.gitlab.jfronny.respackopts.gson.*;
|
||||
import io.gitlab.jfronny.respackopts.model.*;
|
||||
import io.gitlab.jfronny.respackopts.util.*;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
import static io.gitlab.jfronny.respackopts.Respackopts.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ConditionJsonSerializationTest {
|
||||
@BeforeAll
|
||||
static void initialize() {
|
||||
Respackopts.LOGGER.info("Expected error end");
|
||||
Respackopts.FALLBACK_CONF_DIR = Paths.get("./test");
|
||||
Respackopts.CONFIG = new ConfigFile();
|
||||
SAVE_ACTIONS.add(() -> Respackopts.LOGGER.info("Save"));
|
||||
Respackopts.LOGGER.info(Respackopts.FALLBACK_CONF_DIR.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonListOrder() {
|
||||
String[] expected = new String[] {"one", "two", "three", "zero", "76"};
|
||||
String gson = "[\"one\", \"two\", \"three\", \"zero\", \"76\"]";
|
||||
List<String> parsed = Respackopts.GSON.fromJson(gson, new TypeToken<List<String>>(){}.getType());
|
||||
int i = 0;
|
||||
for (String s : parsed) {
|
||||
assertEquals(s, expected[i++]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonSetOrder() {
|
||||
String[] expected = new String[] {"one", "two", "three", "zero", "76"};
|
||||
String gson = "[\"one\", \"two\", \"three\", \"zero\", \"76\"]";
|
||||
Set<String> parsed = Respackopts.GSON.fromJson(gson, new TypeToken<Set<String>>(){}.getType());
|
||||
int i = 0;
|
||||
for (String s : parsed) {
|
||||
assertEquals(s, expected[i++]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonSingleEntryList() {
|
||||
String gson = "\"someText\"";
|
||||
List<String> parsed = Respackopts.GSON.fromJson(gson, new TypeToken<List<String>>(){}.getType());
|
||||
assertEquals(parsed.size(), 1);
|
||||
for (String s : parsed) {
|
||||
assertEquals(s, "someText");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonSingleEntrySet() {
|
||||
String gson = "\"someText\"";
|
||||
Set<String> parsed = Respackopts.GSON.fromJson(gson, new TypeToken<Set<String>>(){}.getType());
|
||||
assertEquals(parsed.size(), 1);
|
||||
for (String s : parsed) {
|
||||
assertEquals(s, "someText");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonConditionSimple() {
|
||||
assertTrue(assertDoesNotThrow(() -> evaluateCondition("true")));
|
||||
assertTrue(assertDoesNotThrow(() -> evaluateCondition("{\"not\": false}")));
|
||||
assertTrue(assertDoesNotThrow(() -> evaluateCondition("[true, {\"not\": false}]")));
|
||||
assertFalse(assertDoesNotThrow(() -> evaluateCondition("[true, {\"not\": true}]")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonConditionXor() {
|
||||
assertTrue(assertDoesNotThrow(() -> evaluateCondition("{\"xor\": [true, false]}")));
|
||||
assertFalse(assertDoesNotThrow(() -> evaluateCondition("{\"xor\": [true, true]}")));
|
||||
assertFalse(assertDoesNotThrow(() -> evaluateCondition("{\"xor\": [false, false]}")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonConditionOr() {
|
||||
assertTrue(assertDoesNotThrow(() -> evaluateCondition("{\"or\": [true, false]}")));
|
||||
assertTrue(assertDoesNotThrow(() -> evaluateCondition("{\"or\": [true, true]}")));
|
||||
assertFalse(assertDoesNotThrow(() -> evaluateCondition("{\"or\": [false, false]}")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonConditionNor() {
|
||||
assertFalse(assertDoesNotThrow(() -> evaluateCondition("{\"nor\": [true, false]}")));
|
||||
assertFalse(assertDoesNotThrow(() -> evaluateCondition("{\"nor\": [true, true]}")));
|
||||
assertTrue(assertDoesNotThrow(() -> evaluateCondition("{\"nor\": [false, false]}")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonConditionAnd() {
|
||||
assertFalse(assertDoesNotThrow(() -> evaluateCondition("{\"and\": [true, false]}")));
|
||||
assertTrue(assertDoesNotThrow(() -> evaluateCondition("{\"and\": [true, true]}")));
|
||||
assertFalse(assertDoesNotThrow(() -> evaluateCondition("{\"and\": [false, false]}")));
|
||||
}
|
||||
|
||||
private boolean evaluateCondition(String json) {
|
||||
Expr.BoolExpr condition = AttachmentHolder.attach(7, () -> GSON.fromJson(json, Expr.BoolExpr.class));
|
||||
return condition.get(MetaCache.hydrateParameter(new ExpressionParameter()));
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package io.gitlab.jfronny.respackopts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import io.gitlab.jfronny.respackopts.model.ConfigFile;
|
||||
import io.gitlab.jfronny.respackopts.model.condition.Condition;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static io.gitlab.jfronny.respackopts.Respackopts.SAVE_ACTIONS;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ConditionSerializationTest {
|
||||
@BeforeAll
|
||||
static void initialize() {
|
||||
Respackopts.LOGGER.info("Expected error end");
|
||||
Respackopts.FALLBACK_CONF_DIR = Paths.get("./test");
|
||||
Respackopts.CONFIG = new ConfigFile();
|
||||
SAVE_ACTIONS.add(() -> Respackopts.LOGGER.info("Save"));
|
||||
Respackopts.LOGGER.info(Respackopts.FALLBACK_CONF_DIR.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonListOrder() {
|
||||
String[] expected = new String[] {"one", "two", "three", "zero", "76"};
|
||||
String gson = "[\"one\", \"two\", \"three\", \"zero\", \"76\"]";
|
||||
List<String> parsed = Respackopts.GSON.fromJson(gson, new TypeToken<List<String>>(){}.getType());
|
||||
int i = 0;
|
||||
for (String s : parsed) {
|
||||
assertEquals(s, expected[i++]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonSetOrder() {
|
||||
String[] expected = new String[] {"one", "two", "three", "zero", "76"};
|
||||
String gson = "[\"one\", \"two\", \"three\", \"zero\", \"76\"]";
|
||||
Set<String> parsed = Respackopts.GSON.fromJson(gson, new TypeToken<Set<String>>(){}.getType());
|
||||
int i = 0;
|
||||
for (String s : parsed) {
|
||||
assertEquals(s, expected[i++]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonSingleEntryList() {
|
||||
String gson = "\"someText\"";
|
||||
List<String> parsed = Respackopts.GSON.fromJson(gson, new TypeToken<List<String>>(){}.getType());
|
||||
assertEquals(parsed.size(), 1);
|
||||
for (String s : parsed) {
|
||||
assertEquals(s, "someText");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonSingleEntrySet() {
|
||||
String gson = "\"someText\"";
|
||||
Set<String> parsed = Respackopts.GSON.fromJson(gson, new TypeToken<Set<String>>(){}.getType());
|
||||
assertEquals(parsed.size(), 1);
|
||||
for (String s : parsed) {
|
||||
assertEquals(s, "someText");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonConditionSimple() {
|
||||
assertTrue(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("true", Condition.class).evaluate("test")));
|
||||
assertTrue(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"not\": false}", Condition.class).evaluate("test")));
|
||||
assertTrue(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("[true, {\"not\": false}]", Condition.class).evaluate("test")));
|
||||
assertFalse(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("[true, {\"not\": true}]", Condition.class).evaluate("test")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonConditionXor() {
|
||||
assertTrue(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"xor\": [true, false]}", Condition.class).evaluate("test")));
|
||||
assertFalse(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"xor\": [true, true]}", Condition.class).evaluate("test")));
|
||||
assertFalse(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"xor\": [false, false]}", Condition.class).evaluate("test")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonConditionOr() {
|
||||
assertTrue(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"or\": [true, false]}", Condition.class).evaluate("test")));
|
||||
assertTrue(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"or\": [true, true]}", Condition.class).evaluate("test")));
|
||||
assertFalse(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"or\": [false, false]}", Condition.class).evaluate("test")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonConditionNor() {
|
||||
assertFalse(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"nor\": [true, false]}", Condition.class).evaluate("test")));
|
||||
assertFalse(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"nor\": [true, true]}", Condition.class).evaluate("test")));
|
||||
assertTrue(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"nor\": [false, false]}", Condition.class).evaluate("test")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gsonConditionAnd() {
|
||||
assertFalse(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"and\": [true, false]}", Condition.class).evaluate("test")));
|
||||
assertTrue(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"and\": [true, true]}", Condition.class).evaluate("test")));
|
||||
assertFalse(assertDoesNotThrow(() -> Respackopts.GSON.fromJson("{\"and\": [false, false]}", Condition.class).evaluate("test")));
|
||||
}
|
||||
}
|
|
@ -1,18 +1,11 @@
|
|||
package io.gitlab.jfronny.respackopts;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import io.gitlab.jfronny.gson.*;
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.respackopts.gson.*;
|
||||
import io.gitlab.jfronny.respackopts.gson.entry.BooleanEntrySerializer;
|
||||
import io.gitlab.jfronny.respackopts.gson.entry.ConfigBranchSerializer;
|
||||
import io.gitlab.jfronny.respackopts.gson.entry.EnumEntrySerializer;
|
||||
import io.gitlab.jfronny.respackopts.gson.entry.NumericEntrySerializer;
|
||||
import io.gitlab.jfronny.respackopts.model.condition.Condition;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigBooleanEntry;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigBranch;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigEnumEntry;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.ConfigNumericEntry;
|
||||
import meteordevelopment.starscript.Script;
|
||||
import io.gitlab.jfronny.respackopts.gson.entry.*;
|
||||
import io.gitlab.jfronny.respackopts.model.*;
|
||||
import io.gitlab.jfronny.respackopts.model.tree.*;
|
||||
|
||||
public class TemplateTree {
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
|
@ -20,10 +13,10 @@ public class TemplateTree {
|
|||
.registerTypeAdapter(ConfigNumericEntry.class, new NumericEntrySerializer())
|
||||
.registerTypeAdapter(ConfigBooleanEntry.class, new BooleanEntrySerializer())
|
||||
.registerTypeAdapter(ConfigBranch.class, new ConfigBranchSerializer())
|
||||
.registerTypeAdapter(Script.class, new ScriptDeserializer())
|
||||
.registerTypeAdapter(Condition.class, new ConditionDeserializer())
|
||||
.registerTypeAdapterFactory(new SingleEntrySetTypeAdapterFactory())
|
||||
.registerTypeAdapterFactory(new SingleEntryListTypeAdapterFactory())
|
||||
.registerTypeAdapter(Expr.class, new ExprDeserializer())
|
||||
.registerTypeAdapter(Expr.StringExpr.class, new ExprDeserializer.StringX())
|
||||
.registerTypeAdapter(Expr.BoolExpr.class, new BoolExprDeserializer())
|
||||
.setLenient()
|
||||
.setPrettyPrinting()
|
||||
.create();
|
||||
|
||||
|
|
Loading…
Reference in New Issue