├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── nl │ └── theepicblock │ └── polyconfig │ ├── PolyConfig.java │ ├── Utils.java │ ├── block │ ├── BlockGroup.java │ ├── BlockGroupUtil.java │ ├── BlockNodeParser.java │ ├── BlockReplaceReference.java │ ├── BlockStateSubgroup.java │ ├── ConfigFormatException.java │ ├── CustomBlockPoly.java │ └── PropertyFilter.java │ ├── entity │ ├── CustomEntityWizard.java │ └── EntityNodeParser.java │ └── mixin │ ├── FunctionalBlockStatePolyAccessor.java │ └── OnBuildPolyMap.java └── resources ├── assets └── polyconfig │ └── icon.png ├── defaultconfig.kdl ├── fabric.mod.json └── polyconfig.mixins.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'README.md' 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Cache 14 | uses: actions/cache@v2 15 | with: 16 | path: | 17 | ~/.gradle/loom-cache 18 | ~/.gradle/caches 19 | ~/.gradle/wrapper 20 | key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 21 | restore-keys: | 22 | gradle- 23 | 24 | - name: Checkout repository 25 | uses: actions/checkout@v2 26 | 27 | - name: Validate gradle wrapper 28 | uses: gradle/wrapper-validation-action@v1 29 | 30 | - name: Setup JDK 17 31 | uses: actions/setup-java@v1 32 | with: 33 | java-version: 17 34 | 35 | - name: Ensure gradlew is executable 36 | run: chmod +x ./gradlew 37 | 38 | - name: Validate the gradle wrapper 39 | uses: gradle/wrapper-validation-action@v1 40 | 41 | - name: Build 42 | run: ./gradlew build -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | github-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Cache 13 | uses: actions/cache@v2 14 | with: 15 | path: | 16 | ~/.gradle/loom-cache 17 | ~/.gradle/caches 18 | ~/.gradle/wrapper 19 | key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 20 | restore-keys: | 21 | gradle- 22 | 23 | - name: Checkout repository 24 | uses: actions/checkout@v2 25 | 26 | - name: Setup JDK 17 27 | uses: actions/setup-java@v1 28 | with: 29 | java-version: 17 30 | 31 | - name: Ensure gradlew is executable 32 | run: chmod +x gradlew 33 | 34 | - name: Build mod 35 | run: ./gradlew build 36 | 37 | - name: Upload GitHub release 38 | uses: AButler/upload-release-assets@v2.0 39 | with: 40 | files: 'build/libs/*.jar;!build/libs/*-sources.jar;!build/libs/*-dev.jar' 41 | repo-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # macos 28 | 29 | *.DS_Store 30 | 31 | # fabric 32 | 33 | run/ 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PolyConfig 2 | PolyConfig is a mod allowing you to configure [PolyMc](https://github.com/TheEpicBlock/PolyMc). 3 | It uses [PolyMc's api](https://theepicblock.github.io/PolyMc/api/) internally. 4 | 5 | ```groovy 6 | version 1 7 | 8 | // Just mentioning a block will fill in the defaults for it, effectively prioritizing it 9 | block "test:prioritized_block" 10 | 11 | // Simple replacement 12 | block "test:test_block" { 13 | replace "minecraft:glass" 14 | } 15 | 16 | // Simple replacement with a specific state 17 | block "test:test_block2" { 18 | replace "minecraft:redstone_lamp" lit=true 19 | } 20 | 21 | // Unless specified otherwise, properties from the modded block will be copied over into the vanilla block 22 | block "test:rope_ladder" { 23 | replace "minecraft:ladder" 24 | } 25 | 26 | // Pick from a specific group. The first one entered will be tried first 27 | block "test:other_block" { 28 | replace (group)"noteblock" 29 | replace (group)"tripwire" 30 | } 31 | 32 | // You can match blocks with a regex 33 | // Blocks that are explicitely declared will always override those declared with a regex 34 | block "test:(other_)?block" { 35 | replace "stone" 36 | } 37 | 38 | // You can filter that specific group (TODO) 39 | block "test:some_other_block" { 40 | replace (group)"leaves" waterlogged=false distance="5.." 41 | } 42 | 43 | block "test:some_kind_of_plant" { 44 | // You can merge specific values to avoid generating a block for all of them. You can use the argument to specify which one of these is the canonical one, which will be used to retrieve the resources. 45 | merge age="0..2" 46 | merge age="3.." 6 47 | // You can use any regex for string type properties. * will also be recognized 48 | merge direction="*" 49 | // Eliding any "replace" statement will leave PolyMc to generate it 50 | } 51 | 52 | // You can specify different replace entries per source state. 53 | block "test:my_slab" { 54 | state type="top|double" { 55 | replace "minecraft:white_stained_glass" 56 | } 57 | state type="bottom" { 58 | replace (group)"tripwire" 59 | } 60 | } 61 | 62 | // You can also configure entities 63 | entity "test:my_test_entity" { 64 | base "minecraft:zombie" 65 | name "yeet" 66 | } 67 | 68 | entity "test:my_test_entity_2" { 69 | base "minecraft:zombie" 70 | name null 71 | } 72 | 73 | // And you can also configure items, if you need to 74 | // NOTE: This is not implemented yet! 75 | item "test:magic_sword" { 76 | replacement "minecraft:diamond_sword" 77 | enchanted true 78 | rarity "uncommon" 79 | lore (literal)"A very cool sword that makes explosions or smth like that" 80 | } 81 | 82 | item "test:combustable_powder" { 83 | replacement (group)"fuels" // Items can also have groups, for those a random item out of the group will be picked 84 | } 85 | ``` 86 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'fabric-loom' version '0.12-SNAPSHOT' 3 | id 'maven-publish' 4 | } 5 | 6 | sourceCompatibility = JavaVersion.VERSION_17 7 | targetCompatibility = JavaVersion.VERSION_17 8 | 9 | archivesBaseName = project.archives_base_name 10 | version = project.mod_version 11 | group = project.maven_group 12 | 13 | repositories { 14 | maven { 15 | url "https://maven.theepicblock.nl" 16 | content { 17 | includeGroup("nl.theepicblock") 18 | includeGroup("dev.hbeck.kdl") // Mirrored on my maven because Jitpack wouldn't build it and GH packages requires auth 19 | } 20 | } 21 | maven { 22 | url "https://maven.nucleoid.xyz" 23 | } 24 | } 25 | 26 | dependencies { 27 | // To change the versions see the gradle.properties file 28 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 29 | mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 30 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 31 | 32 | modImplementation "nl.theepicblock:PolyMc:${project.polymc_version}" 33 | implementation include("dev.hbeck.kdl:kdl4j:0.2.0") 34 | } 35 | 36 | processResources { 37 | inputs.property "version", project.version 38 | 39 | filesMatching("fabric.mod.json") { 40 | expand "version": project.version 41 | } 42 | } 43 | 44 | tasks.withType(JavaCompile).configureEach { 45 | // Minecraft 1.18 (1.18-pre2) upwards uses Java 17. 46 | it.options.release = 17 47 | } 48 | 49 | java { 50 | // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task 51 | // if it is present. 52 | // If you remove this line, sources will not be generated. 53 | withSourcesJar() 54 | } 55 | 56 | jar { 57 | from("LICENSE") { 58 | rename { "${it}_${project.archivesBaseName}"} 59 | } 60 | } 61 | 62 | // configure the maven publication 63 | publishing { 64 | publications { 65 | mavenJava(MavenPublication) { 66 | from components.java 67 | } 68 | } 69 | 70 | // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. 71 | repositories { 72 | // Add repositories to publish to here. 73 | // Notice: This block does NOT have the same function as the block in the top level. 74 | // The repositories here will be used for publishing your artifact, not for 75 | // retrieving dependencies. 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | 4 | # Fabric Properties 5 | # check these on https://fabricmc.net/develop 6 | minecraft_version=1.19.3 7 | yarn_mappings=1.19.3+build.5 8 | loader_version=0.14.12 9 | 10 | # Mod Properties 11 | mod_version = 0.3.7 12 | maven_group = nl.theepicblock 13 | archives_base_name = polyconfig 14 | 15 | # Dependencies 16 | fabric_version=0.71.0+1.19.3 17 | polymc_version=5.3.2+1.19.3 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheEpicBlock/PolyConfig/8ed44c9299776bcfb04755a0659a11a46cc0cda7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheEpicBlock/PolyConfig/8ed44c9299776bcfb04755a0659a11a46cc0cda7/gradlew -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/PolyConfig.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig; 2 | 3 | import dev.hbeck.kdl.objects.KDLNode; 4 | import dev.hbeck.kdl.parse.KDLParseException; 5 | import dev.hbeck.kdl.parse.KDLParser; 6 | import io.github.theepicblock.polymc.api.PolyMcEntrypoint; 7 | import io.github.theepicblock.polymc.api.PolyRegistry; 8 | import io.github.theepicblock.polymc.api.block.BlockStateManager; 9 | import net.fabricmc.loader.api.FabricLoader; 10 | import net.minecraft.util.Identifier; 11 | import nl.theepicblock.polyconfig.block.BlockNodeParser; 12 | import nl.theepicblock.polyconfig.block.ConfigFormatException; 13 | import nl.theepicblock.polyconfig.block.CustomBlockPoly; 14 | import nl.theepicblock.polyconfig.entity.CustomEntityWizard; 15 | import nl.theepicblock.polyconfig.entity.EntityNodeParser; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import java.io.File; 20 | import java.io.FileInputStream; 21 | import java.io.IOException; 22 | import java.nio.file.FileVisitOption; 23 | import java.nio.file.Files; 24 | import java.util.HashMap; 25 | import java.util.LinkedHashMap; 26 | import java.util.Map; 27 | 28 | public class PolyConfig implements PolyMcEntrypoint { 29 | private static final int CURRENT_VERSION = 1; 30 | public static final Logger LOGGER = LoggerFactory.getLogger("polyconfig"); 31 | 32 | /** 33 | * A temporary record that holds the parsed nodes before they're applied 34 | */ 35 | public record Declarations(Map blockDeclarations, Map entityDeclarations) {} 36 | 37 | @Override 38 | public void registerPolys(PolyRegistry registry) { 39 | var parser = new KDLParser(); 40 | var declarations = new Declarations(new LinkedHashMap<>(), new LinkedHashMap<>()); 41 | var configdir = FabricLoader.getInstance().getConfigDir(); 42 | 43 | var oldLocation = configdir.resolve("polyconfig.kdl").toFile(); 44 | var polyconfigdir = configdir.resolve("polyconfig"); 45 | if (oldLocation.exists()) { 46 | // Still uses the old location, respect that 47 | handleFile(oldLocation, parser, declarations); 48 | } else { 49 | // Try see if there's a file at the new location, if not we should create a directory 50 | if (!polyconfigdir.toFile().exists()) { 51 | polyconfigdir.toFile().mkdirs(); 52 | try { 53 | Files.copy(FabricLoader.getInstance().getModContainer("polyconfig").orElseThrow().findPath("defaultconfig.kdl").orElseThrow(), polyconfigdir.resolve("main.kdl")); 54 | } catch (IOException e) { 55 | e.printStackTrace(); 56 | } 57 | } 58 | } 59 | 60 | try { 61 | if (Files.exists(polyconfigdir)) { 62 | Files.walk(polyconfigdir, Integer.MAX_VALUE, FileVisitOption.FOLLOW_LINKS).forEachOrdered(path -> { 63 | if (Files.isDirectory(path)) return; 64 | handleFile(path.toFile(), parser, declarations); 65 | }); 66 | } 67 | } catch (IOException e) { 68 | e.printStackTrace(); 69 | } 70 | 71 | // Apply block nodes to PolyMc 72 | declarations.blockDeclarations().forEach((identifier, blockEntry) -> { 73 | registry.registerBlockPoly( 74 | blockEntry.moddedBlock(), 75 | new CustomBlockPoly( 76 | blockEntry.moddedBlock(), 77 | (state, isUniqueCallback) -> blockEntry.rootNode().grabBlockState(state, isUniqueCallback, registry.getSharedValues(BlockStateManager.KEY)), 78 | blockEntry.merger())); 79 | }); 80 | 81 | // Apply entity nodes 82 | declarations.entityDeclarations().forEach((identifier, entityEntry) -> { 83 | registry.registerEntityPoly(entityEntry.moddedEntity(), (info, entity) -> new CustomEntityWizard<>(info, entity, entityEntry.vanillaReplacement(), entityEntry.name())); 84 | }); 85 | } 86 | 87 | private static void handleFile(File configFile, KDLParser parser, Declarations declarations) { 88 | var path = FabricLoader.getInstance().getConfigDir().relativize(configFile.toPath()); 89 | try (var stream = new FileInputStream(configFile)) { 90 | var config = parser.parse(stream); 91 | stream.close(); 92 | 93 | // Check version 94 | var versionNodes = config.getNodes().stream().filter(node -> node.getIdentifier().equals("version")).toList(); 95 | if (versionNodes.size() != 1) throw multipleVersionDeclarations(versionNodes.size()); 96 | var versionNode = versionNodes.get(0); 97 | if (versionNode.getArgs().size() != 1) throw invalidVersionArgs(versionNode.getArgs().size()); 98 | var version = versionNode.getArgs().get(0).getAsNumber().orElseThrow(); 99 | if (version.getValue().intValue() != CURRENT_VERSION) throw unsupportedVersion(version.getValue()); 100 | 101 | // Loop through config nodes 102 | for (var node : config.getNodes()) { 103 | // These errors are only warnings, so we wrap this in another try block 104 | try { 105 | switch (node.getIdentifier()) { 106 | case "version" -> {} 107 | case "block" -> BlockNodeParser.parseBlockNode(node, declarations.blockDeclarations); 108 | case "item" -> handleItemNode(node); 109 | case "entity" -> EntityNodeParser.parseEntityNode(node, declarations.entityDeclarations); 110 | default -> throw unknownNode(node); 111 | } 112 | } catch (ConfigFormatException e) { 113 | LOGGER.warn("(polyconfig) " + e 114 | .withHelp("the offending node was located in " + path) 115 | .withHelp("the offending node looked something like this (formatting may differ)\n | " + node.toKDL().replace("\n", "\n | ")) 116 | .toString()); 117 | } 118 | } 119 | } catch (IOException e) { 120 | LOGGER.error("(polyconfig) Couldn't read config ("+path+")", e); 121 | } catch (KDLParseException e) { 122 | LOGGER.error("(polyconfig) Invalid config file ("+path+")", e); 123 | } catch (ConfigFormatException e) { 124 | LOGGER.error("(polyconfig) error in "+path+": "+e); 125 | } 126 | } 127 | 128 | private static void handleItemNode(KDLNode node) { 129 | 130 | } 131 | 132 | private static ConfigFormatException multipleVersionDeclarations(int found) { 133 | return new ConfigFormatException("Expected 1 version declaration. Found "+found) 134 | .withHelp("Your config file includes `version ...` multiple times. Try deleting all but the topmost one."); 135 | } 136 | 137 | private static ConfigFormatException invalidVersionArgs(int found) { 138 | return new ConfigFormatException("Invalid number of arguments in version declaration. Expected 1, found "+found) 139 | .withHelp("Your config should include `version "+CURRENT_VERSION+"` at the top. There's supposed to be a single number there"); 140 | } 141 | 142 | private static ConfigFormatException unsupportedVersion(Number v) { 143 | return new ConfigFormatException("Version "+v+" is not supported"); 144 | } 145 | 146 | private static ConfigFormatException unknownNode(KDLNode node) { 147 | return new ConfigFormatException(node.getIdentifier()+" is not a recognized node type") 148 | .withHelp("try removing it"); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/Utils.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig; 2 | 3 | import dev.hbeck.kdl.objects.KDLNode; 4 | import dev.hbeck.kdl.objects.KDLString; 5 | import dev.hbeck.kdl.objects.KDLValue; 6 | import net.minecraft.registry.Registry; 7 | import net.minecraft.state.property.Property; 8 | import net.minecraft.util.Identifier; 9 | import nl.theepicblock.polyconfig.block.ConfigFormatException; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.List; 14 | import java.util.regex.Pattern; 15 | 16 | public class Utils { 17 | public static List getChildren(KDLNode node) { 18 | var childDoc = node.getChild(); 19 | if (childDoc.isPresent()) { 20 | return childDoc.get().getNodes(); 21 | } else { 22 | return Collections.emptyList(); 23 | } 24 | } 25 | 26 | public static void getFromRegistry(KDLString string, String v, Registry registry, GetFromRegistryConsumer consumer) throws ConfigFormatException { 27 | boolean isRegex; 28 | if (string.getType().isEmpty()) { 29 | // Automatically determine if it's a regex 30 | isRegex = Identifier.tryParse(string.getValue()) == null; 31 | } else { 32 | isRegex = switch (string.getType().get()) { 33 | case "identifier" -> false; 34 | case "id" -> false; 35 | case "regex" -> true; 36 | default -> throw new ConfigFormatException("Invalid type "+string.getType().get()); 37 | }; 38 | } 39 | if (isRegex) { 40 | var predicate = Pattern.compile(string.getValue()).asMatchPredicate(); 41 | var count = 0; 42 | var exceptions = new ArrayList(); 43 | for (var id : registry.getIds()) { 44 | if (predicate.test(id.toString())) { 45 | count++; 46 | try { 47 | consumer.accept(id, registry.get(id), true); 48 | } catch (ConfigFormatException e) { 49 | exceptions.add(e); 50 | } 51 | } 52 | } 53 | 54 | if (!exceptions.isEmpty()) { 55 | if (exceptions.size() == 1) { 56 | throw exceptions.get(0); 57 | } else { 58 | throw new ConfigFormatException("Warning: regex caused multiple errors with multiple "+v+"s").withSubExceptions(exceptions); 59 | } 60 | } 61 | 62 | if (count == 0) { 63 | PolyConfig.LOGGER.warn("The regex "+string.getValue()+" didn't match any "+v+"s!"); 64 | } 65 | } else { 66 | var id = new Identifier(string.getValue()); 67 | var value = registry 68 | .getOrEmpty(id) 69 | .orElseThrow(() -> notFoundInRegistry(id, v)); 70 | consumer.accept(id, value, false); 71 | } 72 | } 73 | 74 | @FunctionalInterface 75 | public interface GetFromRegistryConsumer { 76 | void accept(Identifier id, T v, boolean isRegex) throws ConfigFormatException; 77 | } 78 | 79 | public static > String name(Property property, T v) { 80 | return property.name(v); 81 | } 82 | 83 | public static KDLValue getSingleArgNoProps(KDLNode node) throws ConfigFormatException { 84 | if (node.getArgs().size() != 1) { 85 | throw wrongAmountOfArgsForNode(node.getArgs().size(), node.getIdentifier()); 86 | } 87 | if (!node.getProps().isEmpty()) throw new ConfigFormatException(node.getIdentifier()+" nodes should not have any properties").withHelp("try removing any x=.. attached to the node"); 88 | return node.getArgs().get(0); 89 | } 90 | 91 | public static ConfigFormatException wrongAmountOfArgsForNode(int v, String nodeName) { 92 | return new ConfigFormatException("Expected 1 argument, found "+v) 93 | .withHelp("`"+nodeName+"` nodes are supposed to only have a single argument, namely the identifier of the modded "+nodeName+".") 94 | .withHelp("There shouldn't be anything else between `"+nodeName+"` and the { or the end of the line"); 95 | } 96 | 97 | public static ConfigFormatException duplicateEntry(Identifier id) { 98 | return new ConfigFormatException(id+" was already registered"); 99 | } 100 | 101 | public static ConfigFormatException notFoundInRegistry(Identifier id, String type) { 102 | return new ConfigFormatException("Couldn't find any "+type+" matching "+id) 103 | .withHelp("Try checking the spelling"); 104 | } 105 | 106 | public static ConfigFormatException invalidId(String id) { 107 | return new ConfigFormatException("Invalid identifier "+id); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/block/BlockGroup.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.block; 2 | 3 | import io.github.theepicblock.polymc.api.block.BlockStateProfile; 4 | 5 | public enum BlockGroup { 6 | SAPLINGS(BlockStateProfile.SAPLING_SUB_PROFILE, "saplings"), 7 | SUGARCANE(BlockStateProfile.SUGARCANE_SUB_PROFILE, "sugarcane"), 8 | TRIPWIRE(BlockStateProfile.TRIPWIRE_SUB_PROFILE, "tripwire"), 9 | SMALL_DRIPLEAF(BlockStateProfile.SMALL_DRIPLEAF_SUB_PROFILE, "drip_leaf"), 10 | CAVE_VINES(BlockStateProfile.CAVE_VINES_SUB_PROFILE, "cave_vines"), 11 | NETHER_VINES(BlockStateProfile.NETHER_VINES_SUB_PROFILE, "nether_vines"), 12 | KELP(BlockStateProfile.KELP_SUB_PROFILE, "kelp"), 13 | NOTEBLOCK(BlockStateProfile.NOTE_BLOCK_SUB_PROFILE, "note_block"), 14 | TARGET_BLOCK(BlockStateProfile.TARGET_BLOCK_SUB_PROFILE, "target_block"), 15 | DISPENSERS_AND_DROPPERS(BlockStateProfile.DISPENSER_SUB_PROFILE, "dispensers_and_droppers"), 16 | TNT(BlockStateProfile.TNT_SUB_PROFILE, "tnt"), 17 | JUKEBOX(BlockStateProfile.JUKEBOX_SUB_PROFILE, "jukebox"), 18 | BEEHIVES(BlockStateProfile.BEEHIVE_SUB_PROFILE, "beehives"), 19 | SNOWY_GRASSES(BlockStateProfile.SNOWY_GRASS_SUB_PROFILE, "snowy_grasses"), 20 | DOUBLE_SLABS(BlockStateProfile.DOUBLE_SLAB_SUB_PROFILE, "slabs"), 21 | WATERLOGGED_SLABS(BlockStateProfile.WATERLOGGED_SLAB_SUB_PROFILE, "waterlogged_slabs"), 22 | WAXED_COPPER_FULLBLOCKS(BlockStateProfile.WAXED_COPPER_FULLBLOCK_SUB_PROFILE, "waxed_copper_fullblocks"), 23 | INFESTED_STONES(BlockStateProfile.INFESTED_STONE_SUB_PROFILE, "infested_stones"), 24 | PETRIFIED_OAK_SLABS(BlockStateProfile.PETRIFIED_OAK_SLAB_SUB_PROFILE, "petrified_oak_slabs"), 25 | WAXED_COPPER_SLABS(BlockStateProfile.WAXED_COPPER_SLAB_SUB_PROFILE, "waxed_copper_slabs"), 26 | OPEN_FENCE_GATES(BlockStateProfile.OPEN_FENCE_GATE_PROFILE, "open_fence_gates"), 27 | FENCE_GATES(BlockStateProfile.FENCE_GATE_PROFILE, "fence_gates"), 28 | PRESSURE_PLATES(BlockStateProfile.PRESSURE_PLATE_PROFILE, "pressure_plates"), 29 | CACTUS(BlockStateProfile.CACTUS_PROFILE, "cactus"), 30 | LEAVES(BlockStateProfile.LEAVES_PROFILE, "leaves"), 31 | WALLS(BlockStateProfile.NO_COLLISION_WALL_PROFILE, "empty_walls"), 32 | 33 | FULL_BLOCKS(BlockStateProfile.FULL_BLOCK_PROFILE, "fullblocks"), 34 | CLIMBABLES(BlockStateProfile.CLIMBABLE_PROFILE, "climbables"); 35 | 36 | public final String name; 37 | public final BlockStateProfile profile; 38 | BlockGroup(BlockStateProfile profile, String name) { 39 | this.name = name; 40 | this.profile = profile; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/block/BlockGroupUtil.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.block; 2 | 3 | import io.github.theepicblock.polymc.api.block.BlockStateProfile; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.Optional; 8 | 9 | public class BlockGroupUtil { 10 | /** 11 | * These names are for compatibility reasons 12 | */ 13 | private static final Map COMPAT_NAMES = new HashMap<>(); 14 | static { 15 | COMPAT_NAMES.put("saplings", BlockStateProfile.SAPLING_SUB_PROFILE); 16 | COMPAT_NAMES.put("sugarcane", BlockStateProfile.SUGARCANE_SUB_PROFILE); 17 | COMPAT_NAMES.put("tripwire", BlockStateProfile.TRIPWIRE_SUB_PROFILE); 18 | COMPAT_NAMES.put("drip_leaf", BlockStateProfile.SMALL_DRIPLEAF_SUB_PROFILE); 19 | COMPAT_NAMES.put("cave_vines", BlockStateProfile.CAVE_VINES_SUB_PROFILE); 20 | COMPAT_NAMES.put("nether_vines", BlockStateProfile.NETHER_VINES_SUB_PROFILE); 21 | COMPAT_NAMES.put("kelp", BlockStateProfile.KELP_SUB_PROFILE); 22 | COMPAT_NAMES.put("note_block", BlockStateProfile.NOTE_BLOCK_SUB_PROFILE); 23 | COMPAT_NAMES.put("target_block", BlockStateProfile.TARGET_BLOCK_SUB_PROFILE); 24 | COMPAT_NAMES.put("dispensers_and_droppers", BlockStateProfile.DISPENSER_SUB_PROFILE); 25 | COMPAT_NAMES.put("tnt", BlockStateProfile.TNT_SUB_PROFILE); 26 | COMPAT_NAMES.put("jukebox", BlockStateProfile.JUKEBOX_SUB_PROFILE); 27 | COMPAT_NAMES.put("beehives", BlockStateProfile.BEEHIVE_SUB_PROFILE); 28 | COMPAT_NAMES.put("snowy_grasses", BlockStateProfile.SNOWY_GRASS_SUB_PROFILE); 29 | COMPAT_NAMES.put("slabs", BlockStateProfile.DOUBLE_SLAB_SUB_PROFILE); 30 | COMPAT_NAMES.put("waterlogged_slabs", BlockStateProfile.WATERLOGGED_SLAB_SUB_PROFILE); 31 | COMPAT_NAMES.put("waxed_copper_fullblocks", BlockStateProfile.WAXED_COPPER_FULLBLOCK_SUB_PROFILE); 32 | COMPAT_NAMES.put("infested_stones", BlockStateProfile.INFESTED_STONE_SUB_PROFILE); 33 | COMPAT_NAMES.put("petrified_oak_slabs", BlockStateProfile.PETRIFIED_OAK_SLAB_SUB_PROFILE); 34 | COMPAT_NAMES.put("waxed_copper_slabs", BlockStateProfile.WAXED_COPPER_SLAB_SUB_PROFILE); 35 | COMPAT_NAMES.put("open_fence_gates", BlockStateProfile.OPEN_FENCE_GATE_PROFILE); 36 | COMPAT_NAMES.put("fence_gates", BlockStateProfile.FENCE_GATE_PROFILE); 37 | COMPAT_NAMES.put("pressure_plates", BlockStateProfile.PRESSURE_PLATE_PROFILE); 38 | COMPAT_NAMES.put("empty_walls", BlockStateProfile.NO_COLLISION_WALL_PROFILE); 39 | COMPAT_NAMES.put("fullblocks", BlockStateProfile.FULL_BLOCK_PROFILE); 40 | COMPAT_NAMES.put("climbables", BlockStateProfile.CLIMBABLE_PROFILE); 41 | } 42 | 43 | public static Optional tryGet(String name) { 44 | return BlockStateProfile.ALL_PROFILES.stream() 45 | .filter(profile -> profile.name.equals(name)) 46 | .findFirst() 47 | .or(() -> Optional.ofNullable(COMPAT_NAMES.get(name))); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/block/BlockNodeParser.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.block; 2 | 3 | import dev.hbeck.kdl.objects.KDLNode; 4 | import io.github.theepicblock.polymc.api.block.BlockStateMerger; 5 | import net.minecraft.block.Block; 6 | import net.minecraft.block.BlockState; 7 | import net.minecraft.registry.Registries; 8 | import net.minecraft.state.property.Property; 9 | import net.minecraft.util.Identifier; 10 | import nl.theepicblock.polyconfig.Utils; 11 | 12 | import java.util.Map; 13 | 14 | public class BlockNodeParser { 15 | /** 16 | * Interprets a block node 17 | * @param resultMap the map in which the result will be added. 18 | */ 19 | public static void parseBlockNode(KDLNode node, Map resultMap) throws ConfigFormatException { 20 | Utils.getFromRegistry(Utils.getSingleArgNoProps(node).getAsString(), "block", Registries.BLOCK, (id, block, isRegex) -> { 21 | if (isRegex) { 22 | if (!resultMap.containsKey(id)) { 23 | processBlock(id, block, node, resultMap, true); 24 | } 25 | } else { 26 | // Things declared as regexes can be safely overriden 27 | if (resultMap.containsKey(id) && !resultMap.get(id).regex()) { 28 | throw Utils.duplicateEntry(id); 29 | } 30 | processBlock(id, block, node, resultMap, false); 31 | } 32 | }); 33 | } 34 | 35 | private static void processBlock(Identifier moddedId, Block moddedBlock, KDLNode node, Map resultMap, boolean regex) throws ConfigFormatException { 36 | var mergeNodes = Utils.getChildren(node) 37 | .stream() 38 | .filter(n -> n.getIdentifier().equals("merge")) 39 | .toList(); 40 | BlockStateMerger merger; 41 | if (mergeNodes.isEmpty()) { 42 | merger = BlockStateMerger.DEFAULT; 43 | } else { 44 | merger = a -> a; // Do nothing by default 45 | for (var mergeNode : mergeNodes) { 46 | merger = merger.combine(getMergerFromNode(mergeNode, moddedBlock)); 47 | } 48 | } 49 | 50 | // The `block` node can have nested `state` children. So we're going to parse it into a tree of BlockStateSubgroup's, with the `block` node being the root 51 | var rootNode = BlockStateSubgroup.parseNode(node, moddedBlock, true); 52 | resultMap.put(moddedId, new BlockEntry(moddedBlock, merger, rootNode, regex)); 53 | } 54 | 55 | public record BlockEntry(Block moddedBlock, BlockStateMerger merger, BlockStateSubgroup rootNode, boolean regex) {} 56 | 57 | private static BlockStateMerger getMergerFromNode(KDLNode mergeNode, Block block) throws ConfigFormatException { 58 | // A blockstate merger will merge the input blockstate to a canonical version. 59 | // The merge node will specify a property and a range of values for that property. 60 | // It can also optionally specify an argument which contains the desired canonical value for that property. 61 | 62 | if (mergeNode.getProps().size() != 1) throw wrongAmountOfPropertiesForMergeNode(mergeNode.getProps().size()); 63 | // The property declaration will be something like `age="1..4"` 64 | var propDeclaration = mergeNode.getProps().entrySet().stream().findFirst().get(); 65 | var propertyName = propDeclaration.getKey(); 66 | var valueRange = propDeclaration.getValue(); 67 | 68 | var propertyWithFilter = PropertyFilter.get(propertyName, valueRange, block); 69 | 70 | // Parse the canonical value 71 | if (mergeNode.getArgs().size() > 1) throw wrongAmountOfArgsForMergeNode(mergeNode.getArgs().size()); 72 | Comparable canonicalValue; 73 | if (mergeNode.getArgs().size() == 0) { 74 | try { 75 | canonicalValue = findCanonicalValue(propertyWithFilter); 76 | } catch (IllegalStateException e) { 77 | throw new ConfigFormatException("Couldn't find any properties of "+propertyName+" matching "+valueRange); 78 | } 79 | } else { 80 | canonicalValue = propertyWithFilter.property().parse(mergeNode.getArgs().get(0).getAsString().getValue()).orElseThrow(); 81 | } 82 | 83 | return state -> { 84 | if (propertyWithFilter.testState(state)) { 85 | return uncheckedWith(state, propertyWithFilter.property(), canonicalValue); 86 | } else { 87 | return state; 88 | } 89 | }; 90 | } 91 | 92 | private static > BlockState uncheckedWith(BlockState state, Property property, Object value) { 93 | return state.with(property, (T)value); 94 | } 95 | 96 | private static > T findCanonicalValue(PropertyFilter.PropertyWithFilter propertyWithFilter) { 97 | return propertyWithFilter.property() 98 | .getValues() 99 | .stream() 100 | .filter(propertyWithFilter.filter()) 101 | .findFirst() 102 | .orElseThrow(IllegalStateException::new); 103 | } 104 | 105 | private static ConfigFormatException wrongAmountOfArgsForMergeNode(int v) { 106 | return new ConfigFormatException("Expected 1 or zero arguments, found "+v) 107 | .withHelp("`merge` nodes can specify a single freestanding value which is the canonical value for the merge.") 108 | .withHelp("Try looking at the examples in the README"); 109 | } 110 | 111 | private static ConfigFormatException wrongAmountOfPropertiesForMergeNode(int v) { 112 | return new ConfigFormatException("Expected 1 property pair, found "+v) 113 | .withHelp("`merge` nodes should have a single key-value pair to specify which property and which range of values for that property should be merged.") 114 | .withHelp("Try looking at the examples in the README"); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/block/BlockReplaceReference.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.block; 2 | 3 | import io.github.theepicblock.polymc.api.block.BlockStateManager; 4 | import io.github.theepicblock.polymc.api.block.BlockStateProfile; 5 | import io.github.theepicblock.polymc.impl.misc.BooleanContainer; 6 | import net.minecraft.block.Block; 7 | import net.minecraft.block.BlockState; 8 | import net.minecraft.state.property.Property; 9 | import org.jetbrains.annotations.Nullable; 10 | 11 | import java.util.List; 12 | 13 | // Me: can we have Rust-like enums? 14 | // Mom: we've got Rust-like enums at home 15 | // Rust-like enums at home: 16 | 17 | public interface BlockReplaceReference { 18 | @Nullable BlockState tryAllocate(BlockState input, BooleanContainer isUniqueCallback, BlockStateManager stateManager); 19 | 20 | record BlockGroupReference(BlockStateProfile group) implements BlockReplaceReference { 21 | @Override 22 | public @Nullable BlockState tryAllocate(BlockState input, BooleanContainer isUniqueCallback, BlockStateManager stateManager) { 23 | try { 24 | isUniqueCallback.set(true); 25 | return stateManager.requestBlockState(this.group); 26 | } catch (BlockStateManager.StateLimitReachedException e) { 27 | isUniqueCallback.set(false); 28 | return null; 29 | } 30 | } 31 | } 32 | record BlockReference(Block block, List> forcedValues) implements BlockReplaceReference { 33 | @Override 34 | public @Nullable BlockState tryAllocate(BlockState input, BooleanContainer isUniqueCallback, BlockStateManager stateManager) { 35 | var state = block.getDefaultState(); 36 | 37 | for (var inputProperty : input.getBlock().getStateManager().getProperties()) { 38 | try { 39 | state = copy(state, inputProperty, input); 40 | } catch (IllegalArgumentException ignored) {} 41 | } 42 | 43 | for (var value : forcedValues) { 44 | state = with(state, value); 45 | } 46 | 47 | isUniqueCallback.set(false); 48 | return state; 49 | } 50 | 51 | private static > BlockState with(BlockState in, Property.Value value) { 52 | return in.with(value.property(), value.value()); 53 | } 54 | 55 | private static > BlockState copy(BlockState in, Property property, BlockState toCopy) { 56 | return in.with(property, toCopy.get(property)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/block/BlockStateSubgroup.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.block; 2 | 3 | import dev.hbeck.kdl.objects.KDLNode; 4 | import io.github.theepicblock.polymc.api.block.BlockStateManager; 5 | import io.github.theepicblock.polymc.api.block.BlockStateProfile; 6 | import io.github.theepicblock.polymc.impl.generator.BlockPolyGenerator; 7 | import io.github.theepicblock.polymc.impl.misc.BooleanContainer; 8 | import net.minecraft.block.Block; 9 | import net.minecraft.block.BlockState; 10 | import net.minecraft.block.Blocks; 11 | import net.minecraft.registry.Registries; 12 | import net.minecraft.state.property.Property; 13 | import net.minecraft.util.Identifier; 14 | import nl.theepicblock.polyconfig.PolyConfig; 15 | import nl.theepicblock.polyconfig.Utils; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.function.Predicate; 20 | import java.util.stream.Collectors; 21 | 22 | public record BlockStateSubgroup(Predicate filter, List children, List replaces) { 23 | /** 24 | * Allocates the right clientside blockstate for the modded input blockstate according to the rules defined in here. 25 | */ 26 | public BlockState grabBlockState(BlockState input, BooleanContainer isUniqueCallback, BlockStateManager stateManager) { 27 | for (var child : this.children) { 28 | if (child.filter.test(input)) { 29 | return child.grabBlockState(input, isUniqueCallback, stateManager); 30 | } 31 | } 32 | 33 | if (this.replaces.isEmpty()) { 34 | // Let PolyMc figure it out automagically 35 | return BlockPolyGenerator.registerClientState(input, isUniqueCallback, stateManager); 36 | } 37 | 38 | for (var replaceStatement : this.replaces) { 39 | var result = replaceStatement.tryAllocate(input, isUniqueCallback, stateManager); 40 | if (result != null) { 41 | return result; 42 | } 43 | } 44 | 45 | PolyConfig.LOGGER.info("(polyconfig) couldn't allocate any blocks for "+input+", we probably ran out of states"); 46 | isUniqueCallback.set(false); 47 | return Blocks.STONE.getDefaultState(); 48 | } 49 | 50 | public static BlockStateSubgroup parseNode(KDLNode node, Block moddedBlock, boolean isRoot) throws ConfigFormatException { 51 | Predicate filter; 52 | if (isRoot) { 53 | // The root node is the `block` node, which currently can't do any filtering 54 | filter = o -> true; 55 | } else { 56 | filter = o -> true; 57 | for (var entry : node.getProps().entrySet()) { 58 | var propFilter = PropertyFilter.get(entry.getKey(), entry.getValue(), moddedBlock); 59 | filter = filter.and(propFilter::testState); 60 | } 61 | } 62 | 63 | var children = new ArrayList(); 64 | var replaceEntries = new ArrayList(); 65 | 66 | for (var child : Utils.getChildren(node)) { 67 | switch (child.getIdentifier()) { 68 | case "merge" -> { if (!isRoot) { throw invalidMerge(child); } } 69 | case "state" -> children.add(parseNode(child, moddedBlock, false)); 70 | case "replace" -> replaceEntries.add(parseReplaceNode(child)); 71 | default -> throw unknownNode(child); 72 | } 73 | } 74 | 75 | return new BlockStateSubgroup(filter, children, replaceEntries); 76 | } 77 | 78 | private static BlockReplaceReference parseReplaceNode(KDLNode node) throws ConfigFormatException { 79 | var args = node.getArgs(); 80 | if (args.size() != 1) throw wrongAmountOfArgsForReplaceNode(args.size()); 81 | var replacementArg = args.get(0); 82 | var replacementArgAsString = replacementArg.getAsString().getValue(); 83 | var replacementArgType = replacementArg.getType().orElse("state"); 84 | 85 | if (replacementArgType.equals("state")) { 86 | var id = Identifier.tryParse(replacementArgAsString); 87 | if (id == null) throw Utils.invalidId(replacementArgAsString); 88 | var block = Registries.BLOCK.getOrEmpty(id).orElseThrow(() -> Utils.notFoundInRegistry(id, "block")); 89 | var forcedValues = new ArrayList>(); 90 | 91 | for (var entry : node.getProps().entrySet()) { 92 | var propertyString = entry.getKey(); 93 | var valueString = entry.getValue().getAsString().getValue(); 94 | var property = block.getStateManager().getProperty(propertyString); 95 | if (property == null) throw PropertyFilter.propertyNotFound(propertyString, block); 96 | var value = parseAndGetValue(property, valueString); 97 | forcedValues.add(value); 98 | } 99 | 100 | return new BlockReplaceReference.BlockReference(block, forcedValues); 101 | } else if (replacementArgType.equals("group")) { 102 | var blockgroup = BlockGroupUtil.tryGet(replacementArgAsString) 103 | .orElseThrow(() -> new ConfigFormatException(replacementArgAsString+" is not a valid group.") 104 | .withHelp("valid groups are: "+ BlockStateProfile.ALL_PROFILES.stream().map(group -> group.name).collect(Collectors.joining(", ", "[","]")))); 105 | 106 | for (var entry : node.getProps().entrySet()) { 107 | var propertyString = entry.getKey(); 108 | var valueString = entry.getValue().getAsString().getValue(); 109 | blockgroup = blockgroup.and(state -> { 110 | try { 111 | var block = state.getBlock(); 112 | var property = block.getStateManager().getProperty(propertyString); 113 | if (property == null) throw PropertyFilter.propertyNotFound(propertyString, block); 114 | var value = parseAndGetValue(property, valueString); 115 | return state.get(property).equals(value.value()); 116 | } catch (ConfigFormatException e) { 117 | // TODO properly report error 118 | throw new RuntimeException(e); 119 | } 120 | }); 121 | } 122 | return new BlockReplaceReference.BlockGroupReference(blockgroup); 123 | } else { 124 | throw new ConfigFormatException(replacementArgType+" is an invalid type for a replace node's argument. Should be either `state` or `group`"); 125 | } 126 | } 127 | 128 | private static > Property.Value parseAndGetValue(Property property, String valueString) throws ConfigFormatException { 129 | return property.createValue(property.parse(valueString).orElseThrow(() -> invalidPropertyValue(property.getName(), valueString))); 130 | } 131 | 132 | private static ConfigFormatException invalidPropertyValue(String propertyName, String valueName) { 133 | return new ConfigFormatException("Property `"+propertyName+"` does not have a value of `"+valueName+"`"); 134 | } 135 | 136 | private static ConfigFormatException wrongAmountOfArgsForReplaceNode(int v) { 137 | return new ConfigFormatException("Expected 1 argument, found "+v) 138 | .withHelp("`replace` nodes are supposed to only have a single argument. (they may have more than one property though)") 139 | .withHelp("There shouldn't be anything else between `replace` and the end of the line"); 140 | } 141 | 142 | private static ConfigFormatException invalidMerge(KDLNode node) { 143 | return new ConfigFormatException(node.getIdentifier()+" is not valid in this context") 144 | .withHelp("try moving it to the `block` level"); 145 | } 146 | 147 | private static ConfigFormatException unknownNode(KDLNode node) { 148 | return new ConfigFormatException(node.getIdentifier()+" is not a recognized node type") 149 | .withHelp("try removing it"); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/block/ConfigFormatException.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.block; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class ConfigFormatException extends Exception { 7 | public final List helpMessages = new ArrayList<>(); 8 | public List subExceptions = new ArrayList<>(); 9 | 10 | public ConfigFormatException(String message) { 11 | super(message); 12 | } 13 | 14 | public ConfigFormatException withHelp(String help) { 15 | if (help != null) { 16 | this.helpMessages.add(help); 17 | } 18 | return this; 19 | } 20 | 21 | public ConfigFormatException withSubExceptions(List subExceptions) { 22 | this.subExceptions = subExceptions; 23 | return this; 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | var b = new StringBuilder(); 29 | b.append(this.getMessage()); 30 | appendTrailingStuff(b, ""); 31 | return b.toString(); 32 | } 33 | 34 | public void appendTrailingStuff(StringBuilder b, String indent) { 35 | for (var help : helpMessages) { 36 | b.append("\n").append(indent).append("help: ").append(help); 37 | } 38 | for (var subException : subExceptions) { 39 | b.append("\n").append(indent); 40 | b.append("SUB ERROR: "+subException.getMessage()); 41 | subException.appendTrailingStuff(b, indent+" "); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/block/CustomBlockPoly.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.block; 2 | 3 | import io.github.theepicblock.polymc.api.block.BlockStateMerger; 4 | import io.github.theepicblock.polymc.impl.misc.BooleanContainer; 5 | import io.github.theepicblock.polymc.impl.poly.block.FunctionBlockStatePoly; 6 | import net.minecraft.block.Block; 7 | import net.minecraft.block.BlockState; 8 | import nl.theepicblock.polyconfig.mixin.FunctionalBlockStatePolyAccessor; 9 | 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | import java.util.function.BiFunction; 13 | 14 | public class CustomBlockPoly extends FunctionBlockStatePoly { 15 | public final Set noRecheck; 16 | 17 | public CustomBlockPoly(Block moddedBlock, BiFunction registrationProvider, BlockStateMerger merger) { 18 | super(moddedBlock, registrationProvider, merger); 19 | this.noRecheck = new HashSet<>(); 20 | noRecheck.addAll(((FunctionalBlockStatePolyAccessor)this).getUniqueClientBlocks()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/block/PropertyFilter.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.block; 2 | 3 | import dev.hbeck.kdl.objects.*; 4 | import net.minecraft.block.Block; 5 | import net.minecraft.block.BlockState; 6 | import net.minecraft.state.property.BooleanProperty; 7 | import net.minecraft.state.property.IntProperty; 8 | import net.minecraft.state.property.Property; 9 | import nl.theepicblock.polyconfig.Utils; 10 | 11 | import java.util.function.Predicate; 12 | import java.util.regex.Pattern; 13 | 14 | public interface PropertyFilter> extends Predicate { 15 | static PropertyWithFilter get(String propertyName, KDLValue valueRange, Block moddedBlock) throws ConfigFormatException { 16 | // The input will be something like age="1..5". With "age being passed to `propertyName` and "1..5" to `valueRange` 17 | var property = moddedBlock.getStateManager().getProperty(propertyName); 18 | if (property == null) throw propertyNotFound(propertyName, moddedBlock); 19 | 20 | return get(property, valueRange, moddedBlock); 21 | } 22 | 23 | static > PropertyWithFilter get(Property property, KDLValue valueRange, Block moddedBlock) throws ConfigFormatException { 24 | if (valueRange instanceof KDLNull) { 25 | throw new ConfigFormatException("null is not a valid range of properties"); 26 | } else if (valueRange instanceof KDLBoolean bValueRange) { 27 | if (property instanceof BooleanProperty booleanProperty) { 28 | return (PropertyWithFilter)new PropertyWithFilter<>(booleanProperty, v -> v == bValueRange.getValue()); 29 | } else { 30 | // Try it again but using "true" or "false" as if it were a string 31 | valueRange = KDLString.from(String.valueOf(valueRange.getValue())); 32 | } 33 | } else if (valueRange instanceof KDLNumber nValueRange) { 34 | if (property instanceof IntProperty intProperty) { 35 | var valueInt = nValueRange.getValue().intValue(); 36 | if (intProperty.getValues().contains(valueInt)) { 37 | return (PropertyWithFilter)new PropertyWithFilter<>(intProperty, v -> v == valueInt); 38 | } else { 39 | // Try it again but using the number as a string 40 | valueRange = KDLString.from(String.valueOf(valueRange.getValue())); 41 | } 42 | } 43 | } else if (valueRange instanceof KDLString sValueRange) { 44 | var string = sValueRange.getValue(); 45 | if (string.contains("..")) { 46 | var split = string.split("\\.\\.", 2); 47 | var left = split[0].equals("") ? KDLNumber.from(Integer.MIN_VALUE) : KDLNumber.from(split[0]).orElse(null); 48 | var right = split[1].equals("") ? KDLNumber.from(Integer.MAX_VALUE) : KDLNumber.from(split[1]).orElse(null); 49 | 50 | if (left == null) throw new ConfigFormatException("Invalid number "+split[0]+" in range "+string); 51 | if (right == null) throw new ConfigFormatException("Invalid number "+split[1]+" in range "+string); 52 | 53 | var leftInt = left.getValue().intValue(); 54 | var rightInt = right.getValue().intValue(); 55 | if (rightInt < leftInt) throw new ConfigFormatException("Right value is bigger than left value in range "+string); 56 | 57 | if (property instanceof IntProperty intProperty) { 58 | return (PropertyWithFilter)new PropertyWithFilter<>(intProperty, v -> v >= leftInt && v <= rightInt); 59 | } 60 | } else if (string.equals("*")) { 61 | return new PropertyWithFilter<>(property, block -> true); 62 | } else { 63 | var regex = Pattern.compile(string).asMatchPredicate(); 64 | return new PropertyWithFilter<>(property, v -> regex.test(Utils.name(property, v))); 65 | } 66 | } 67 | throw new IllegalStateException("This should not happen"); 68 | } 69 | 70 | record PropertyWithFilter>(Property property, PropertyFilter filter) { 71 | public boolean testState(BlockState state) { 72 | return this.filter().test(state.get(this.property())); 73 | } 74 | } 75 | 76 | static ConfigFormatException propertyNotFound(String name, Block block) { 77 | return new ConfigFormatException(block.getTranslationKey()+" does not have the property '"+name+"'"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/entity/CustomEntityWizard.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.entity; 2 | 3 | import io.github.theepicblock.polymc.api.wizard.PacketConsumer; 4 | import io.github.theepicblock.polymc.api.wizard.VirtualEntity; 5 | import io.github.theepicblock.polymc.api.wizard.WizardInfo; 6 | import io.github.theepicblock.polymc.impl.poly.entity.EntityWizard; 7 | import io.github.theepicblock.polymc.impl.poly.entity.DefaultedEntityPoly.DefaultedEntityWizard; 8 | import io.github.theepicblock.polymc.impl.poly.wizard.AbstractVirtualEntity; 9 | import io.github.theepicblock.polymc.impl.poly.wizard.EntityUtil; 10 | import io.github.theepicblock.polymc.mixins.wizards.EntityAccessor; 11 | import net.minecraft.entity.Entity; 12 | import net.minecraft.entity.EntityType; 13 | import net.minecraft.entity.data.DataTracker; 14 | import net.minecraft.text.Text; 15 | 16 | import java.util.List; 17 | import java.util.Optional; 18 | 19 | public class CustomEntityWizard extends EntityWizard { 20 | private final VirtualEntity virtualEntity; 21 | private final Text name; 22 | 23 | public CustomEntityWizard(WizardInfo info, T entity, EntityType vanillaEntityType, Text name) { 24 | super(info, entity); 25 | this.name = name; 26 | this.virtualEntity = new AbstractVirtualEntity(entity.getUuid(), entity.getId()) { 27 | @Override 28 | public EntityType getEntityType() { 29 | return vanillaEntityType; 30 | } 31 | }; 32 | } 33 | 34 | @Override 35 | public void addPlayer(PacketConsumer player) { 36 | virtualEntity.spawn(player, this.getPosition()); 37 | 38 | if (this.name != EntityNodeParser.EMPTY_TEXT) { 39 | player.sendPacket(EntityUtil.createDataTrackerUpdate( 40 | this.virtualEntity.getId(), 41 | List.of( 42 | new DataTracker.Entry<>(EntityAccessor.getCustomName(), Optional.of(name == null ? this.getEntity().getName() : name)), 43 | new DataTracker.Entry<>(EntityAccessor.getNameVisible(), true)) 44 | ) 45 | ); 46 | } 47 | 48 | DefaultedEntityWizard.sendStandardPackets(player, this.getEntity()); 49 | } 50 | 51 | @Override 52 | public void removePlayer(PacketConsumer player) { 53 | virtualEntity.remove(player); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/entity/EntityNodeParser.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.entity; 2 | 3 | import dev.hbeck.kdl.objects.KDLNode; 4 | import net.minecraft.entity.EntityType; 5 | import net.minecraft.registry.Registries; 6 | import net.minecraft.text.Text; 7 | import net.minecraft.util.Identifier; 8 | import nl.theepicblock.polyconfig.Utils; 9 | import nl.theepicblock.polyconfig.block.ConfigFormatException; 10 | 11 | import java.util.Map; 12 | 13 | public class EntityNodeParser { 14 | public static final Text EMPTY_TEXT = Text.empty(); 15 | 16 | /** 17 | * Interprets a block node 18 | * @param resultMap the map in which the result will be added. 19 | */ 20 | public static void parseEntityNode(KDLNode node, Map resultMap) throws ConfigFormatException { 21 | Utils.getFromRegistry(Utils.getSingleArgNoProps(node).getAsString(), "entity", Registries.ENTITY_TYPE, (id, entity, isRegex) -> { 22 | if (isRegex) { 23 | if (!resultMap.containsKey(id)) { 24 | processEntity(id, entity, node, resultMap, true); 25 | } 26 | } else { 27 | // Things declared as regexes can be safely overriden 28 | if (resultMap.containsKey(id) && !resultMap.get(id).regex()) { 29 | throw Utils.duplicateEntry(id); 30 | } 31 | processEntity(id, entity, node, resultMap, false); 32 | } 33 | }); 34 | } 35 | 36 | private static void processEntity(Identifier moddedId, EntityType moddedEntity, KDLNode node, Map resultMap, boolean regex) throws ConfigFormatException { 37 | var baseNodes = Utils.getChildren(node).stream().filter(n -> n.getIdentifier().equals("base")).toList(); 38 | if (baseNodes.size() != 1) { 39 | throw new ConfigFormatException("Expected 1 base node, found "+baseNodes.size()); 40 | } 41 | var baseEntityStr = Utils.getSingleArgNoProps(baseNodes.get(0)).getAsString().getValue(); 42 | var baseEntityId = Identifier.tryParse(baseEntityStr); 43 | if (baseEntityId == null) throw Utils.invalidId(baseEntityStr); 44 | var baseEntity = Registries.ENTITY_TYPE.getOrEmpty(baseEntityId) 45 | .orElseThrow(() -> new ConfigFormatException("Couldn't find any entity matching "+baseEntityStr) 46 | .withHelp("Try checking the spelling")); 47 | 48 | var nameNodes = Utils.getChildren(node).stream().filter(n -> n.getIdentifier().equals("name")).toList(); 49 | if (nameNodes.size() > 1) { 50 | throw new ConfigFormatException("Expected 0 or 1 name node, found "+nameNodes.size()); 51 | } 52 | 53 | Text name; 54 | if (!nameNodes.isEmpty()) { 55 | var nameArg = Utils.getSingleArgNoProps(nameNodes.get(0)); 56 | if (nameArg.isNull()) { 57 | name = EMPTY_TEXT; 58 | } else { 59 | boolean isJson; 60 | if (nameArg.getType().isEmpty()) { 61 | isJson = false; 62 | } else { 63 | isJson = switch (nameArg.getType().get()) { 64 | case "json" -> true; 65 | case "literal" -> false; 66 | default -> throw new ConfigFormatException("Invalid type "+nameArg.getType().get()); 67 | }; 68 | } 69 | 70 | if (isJson) { 71 | name = Text.Serializer.fromLenientJson(nameArg.getAsString().getValue()); 72 | } else { 73 | name = Text.literal(nameArg.getAsString().getValue()); 74 | } 75 | } 76 | } else { 77 | name = null; 78 | } 79 | resultMap.put(moddedId, new EntityEntry(moddedEntity, baseEntity, name, regex)); 80 | } 81 | 82 | public record EntityEntry(EntityType moddedEntity, EntityType vanillaReplacement, Text name, boolean regex) {} 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/mixin/FunctionalBlockStatePolyAccessor.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.mixin; 2 | 3 | import java.util.ArrayList; 4 | 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Accessor; 7 | 8 | import io.github.theepicblock.polymc.impl.poly.block.FunctionBlockStatePoly; 9 | import net.minecraft.block.BlockState; 10 | 11 | @Mixin(FunctionBlockStatePoly.class) 12 | public interface FunctionalBlockStatePolyAccessor { 13 | @Accessor 14 | ArrayList getUniqueClientBlocks(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/nl/theepicblock/polyconfig/mixin/OnBuildPolyMap.java: -------------------------------------------------------------------------------- 1 | package nl.theepicblock.polyconfig.mixin; 2 | 3 | import io.github.theepicblock.polymc.api.PolyMap; 4 | import io.github.theepicblock.polymc.api.PolyRegistry; 5 | import io.github.theepicblock.polymc.api.block.BlockPoly; 6 | import io.github.theepicblock.polymc.impl.poly.block.FunctionBlockStatePoly; 7 | import net.minecraft.block.Block; 8 | import nl.theepicblock.polyconfig.block.CustomBlockPoly; 9 | import org.spongepowered.asm.mixin.Final; 10 | import org.spongepowered.asm.mixin.Mixin; 11 | import org.spongepowered.asm.mixin.Shadow; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 15 | 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | @Mixin(PolyRegistry.class) 20 | public class OnBuildPolyMap { 21 | @Shadow @Final protected Map blockPolys; 22 | 23 | @Inject(method = "build", at = @At("HEAD"), remap = false) 24 | public void recheckPolys(CallbackInfoReturnable cir) { 25 | var newBlockPolys = new HashMap(); 26 | 27 | this.blockPolys.forEach((moddedBlock, poly) -> { 28 | if (poly instanceof CustomBlockPoly customPoly) { 29 | newBlockPolys.put(moddedBlock, new FunctionBlockStatePoly( 30 | moddedBlock, 31 | (moddedState, isUniqueCallback) -> { 32 | var oldClientState = customPoly.getClientBlock(moddedState); 33 | isUniqueCallback.set(customPoly.noRecheck.contains(oldClientState)); 34 | if (!customPoly.noRecheck.contains(oldClientState) && this.blockPolys.containsKey(oldClientState.getBlock())) { 35 | // Recheck the client state 36 | return this.blockPolys.get(oldClientState.getBlock()).getClientBlock(oldClientState); 37 | } else { 38 | return oldClientState; 39 | } 40 | }, 41 | (a) -> a // Don't merge any states 42 | )); 43 | } 44 | }); 45 | 46 | this.blockPolys.putAll(newBlockPolys); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/assets/polyconfig/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheEpicBlock/PolyConfig/8ed44c9299776bcfb04755a0659a11a46cc0cda7/src/main/resources/assets/polyconfig/icon.png -------------------------------------------------------------------------------- /src/main/resources/defaultconfig.kdl: -------------------------------------------------------------------------------- 1 | version 1 2 | 3 | // Have a look at https://github.com/TheEpicBlock/PolyConfig/ for an example config -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "polyconfig", 4 | "version": "${version}", 5 | 6 | "name": "polyconfig", 7 | "description": "Mod to conveniently access the PolyMc api via a config file", 8 | "authors": [ 9 | { 10 | "name": "TheEpicBlock", 11 | "contact": { "homepage": "https://theepicblock.nl" } 12 | } 13 | ], 14 | "contact": { 15 | "homepage": "https://github.com/TheEpicBlock/PolyConfig/blob/master/README.md", 16 | "sources": "https://github.com/TheEpicBlock/PolyConfig" 17 | }, 18 | 19 | "license": "CC0-1.0", 20 | "icon": "assets/polyconfig/icon.png", 21 | 22 | "environment": "*", 23 | "entrypoints": { 24 | "polymc": [ 25 | "nl.theepicblock.polyconfig.PolyConfig" 26 | ] 27 | }, 28 | "mixins": [ 29 | "polyconfig.mixins.json" 30 | ], 31 | 32 | "depends": { 33 | "fabricloader": ">=0.14.6", 34 | "java": ">=17", 35 | "polymc": ">=5.3.2", 36 | "minecraft": ">=1.19.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/polyconfig.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "nl.theepicblock.polyconfig.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | "OnBuildPolyMap", 8 | "FunctionalBlockStatePolyAccessor" 9 | ], 10 | "injectors": { 11 | "defaultRequire": 1 12 | } 13 | } 14 | --------------------------------------------------------------------------------