From bf94a0a2118acca5f330d79d924fda19190fbdb4 Mon Sep 17 00:00:00 2001 From: Su5eD Date: Sun, 26 Nov 2023 15:20:22 +0100 Subject: [PATCH] Add Forge implementation of Forge Config API Port --- README.md | 7 + build.gradle.kts | 23 +- forgeconfigapiport/LICENSE.md | 373 ++++++++++++++++++ forgeconfigapiport/README.md | 299 ++++++++++++++ forgeconfigapiport/build.gradle.kts | 13 + .../ForgeConfigApiPortImpl.java | 37 ++ .../ForgeConfigPathsImpl.java | 34 ++ .../ForgeConfigRegistryImpl.java | 29 ++ .../api/config/v2/ForgeConfigPaths.java | 63 +++ .../api/config/v2/ForgeConfigRegistry.java | 46 +++ .../api/config/v2/ModConfigEvents.java | 149 +++++++ .../src/main/resources/META-INF/mods.toml | 30 ++ .../src/main/resources/mod_logo.png | Bin 0 -> 7137 bytes .../src/main/resources/pack.mcmeta | 6 + gradle.properties | 2 +- settings.gradle.kts | 3 +- 16 files changed, 1103 insertions(+), 11 deletions(-) create mode 100644 forgeconfigapiport/LICENSE.md create mode 100644 forgeconfigapiport/README.md create mode 100644 forgeconfigapiport/build.gradle.kts create mode 100644 forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigApiPortImpl.java create mode 100644 forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigPathsImpl.java create mode 100644 forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigRegistryImpl.java create mode 100644 forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ForgeConfigPaths.java create mode 100644 forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ForgeConfigRegistry.java create mode 100644 forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ModConfigEvents.java create mode 100644 forgeconfigapiport/src/main/resources/META-INF/mods.toml create mode 100644 forgeconfigapiport/src/main/resources/mod_logo.png create mode 100644 forgeconfigapiport/src/main/resources/pack.mcmeta diff --git a/README.md b/README.md index 5621920..31de759 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,13 @@ A Forge port of the Fabric [Amecs API](https://github.com/Siphalor/amecs-api) library, implemented alongside Forge's key modifier system. Allows for compatiblity with Fabric mods that require it, but wouldn't otherwise be compatible due to the heavy forge mixin conflicts of the original version. +### Forge Config API Port - Forge bridge + +A clean implementation of [Forge Config API Port](https://www.curseforge.com/minecraft/mc-mods/forge-config-api-port-fabric) +on Forge, allowing Fabric mods to easily access the Forge config system and register their own configs. +Includes the [Forge Config API Port API](https://github.com/Fuzss/forgeconfigapiport/tree/8dbbc0d49afb46928a1dfc0c7f828af06da1638d/Fabric/src/main/java/fuzs/forgeconfigapiport/api/config/v2), +licensed under the MPL-2.0 license. + ## Get help If you're having trouble using Connector Extras or believe it is not functioning correctly, ask us diff --git a/build.gradle.kts b/build.gradle.kts index 4d405d5..7145a01 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,8 @@ import org.gradle.jvm.tasks.Jar import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.StandardCopyOption +import kotlin.io.path.deleteExisting +import kotlin.io.path.exists plugins { java @@ -93,6 +95,7 @@ dependencies { includeProject("modmenu-bridge") includeProject("continuity-compat") includeProject("amecs-api") + includeProject("forgeconfigapiport") // Misc modImplementation("curse.maven:mcpitanlibarch-682213:4723157") @@ -129,16 +132,18 @@ val relocateNestedJars by tasks.registering { doLast { FileSystemUtil.getJarFileSystem(archiveFile.get().asFile.toPath(), false).use { fs -> val sourceDirectory = fs.getPath("META-INF", "jars") - val destinationDirectory = fs.getPath("META-INF", "jarjar") - Files.newDirectoryStream(sourceDirectory).forEach { path -> - Files.move(path, destinationDirectory.resolve(path.fileName), StandardCopyOption.COPY_ATTRIBUTES) + if (sourceDirectory.exists()) { + val destinationDirectory = fs.getPath("META-INF", "jarjar") + Files.newDirectoryStream(sourceDirectory).forEach { path -> + Files.move(path, destinationDirectory.resolve(path.fileName), StandardCopyOption.COPY_ATTRIBUTES) + } + sourceDirectory.deleteExisting() + + val metadata = fs.getPath("META-INF", "jarjar", "metadata.json") + val text = Files.readString(metadata, StandardCharsets.UTF_8) + val replaced = text.replace("META-INF/jars/", "META-INF/jarjar/") + Files.writeString(metadata, replaced, StandardCharsets.UTF_8) } - Files.delete(sourceDirectory) - - val metadata = fs.getPath("META-INF", "jarjar", "metadata.json") - val text = Files.readString(metadata, StandardCharsets.UTF_8) - val replaced = text.replace("META-INF/jars/", "META-INF/jarjar/") - Files.writeString(metadata, replaced, StandardCharsets.UTF_8) } } } diff --git a/forgeconfigapiport/LICENSE.md b/forgeconfigapiport/LICENSE.md new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/forgeconfigapiport/LICENSE.md @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/forgeconfigapiport/README.md b/forgeconfigapiport/README.md new file mode 100644 index 0000000..b018fbf --- /dev/null +++ b/forgeconfigapiport/README.md @@ -0,0 +1,299 @@ +# Forge Config API Port + +A Minecraft mod. Downloads can be found on [CurseForge](https://www.curseforge.com/members/fuzs_/projects) and [Modrinth](https://modrinth.com/user/Fuzs). + +![](https://raw.githubusercontent.com/Fuzss/modresources/main/pages/data/forgeconfigapiport/banner.png) + +## ABOUT THE PROJECT +**!!! Forge Config API Port is in no way affiliated with the Minecraft Forge project !!!** + +**The sole purpose of this library is to enable usage of the Minecraft Forge config api on both Fabric and Quilt mod loaders. This is done in the hopes of removing one more obstacle for developers wishing to maintain their mods on various mod loaders.** + +This is a direct port from Minecraft Forge, all package names are the same, so you don't even have to readjust imports when porting from Forge. +As Fabric and Quilt are very different mod loaders from Forge, there obviously have to be some differences, even though they're quite small. + +The main advantage of Forge Config Api Port over other config libraries lies in the fact that no additional library is required on Forge (since this very exact config api is built-in), only Fabric/Quilt needs this one library. + +For more information regarding the licensing of this project and different parts of it check the [LICENSING.md](LICENSING.md) file. + +## DEVELOPER INFORMATION + +### Adding Forge Config Api Port to your Gradle workspace + +
+ +#### Via Fuzs Mod Resources +Fuzs Mod Resources is the recommended way of adding Forge Config API Port to your project in your `build.gradle` file. +```groovy +repositories { + maven { + name = "Fuzs Mod Resources" + url = "https://raw.githubusercontent.com/Fuzss/modresources/main/maven/" + } +} + +dependencies { + modApi "fuzs.forgeconfigapiport:forgeconfigapiport-fabric:" // `modVersion` e.g. 7.0.0 for Minecraft 1.20 +} +``` + +When developing for both multiple mod loaders simultaneously using a multi-loader setup, Forge Config Api Port can also be included in the common project to provide all classes common to both loaders. Instead of the mod loader specific version, simply include the common publication in your `build.gradle` file. The common publication is not obfuscated, it therefore is sufficient to use `api` over `modApi`. +```groovy +dependencies { + api "fuzs.forgeconfigapiport:forgeconfigapiport-common:" // `modVersion` e.g. 7.0.0 for Minecraft 1.20 +} +``` + +#### Via Curse Maven / Modrinth Maven +Alternatively you can use the Curse Maven / Modrinth Maven to include Forge Config Api Port in your workspace. Javadoc and sources will be absent in that case. + +```groovy +repositories { + exclusiveContent { + forRepository { + maven { + name = "CurseForge" + url = "https://cursemaven.com" + } + } + filter { + includeGroup "curse.maven" + } + } + exclusiveContent { + forRepository { + maven { + name = "Modrinth" + url = "https://api.modrinth.com/maven" + } + } + filter { + includeGroup "maven.modrinth" + } + } +} + +dependencies { + modApi "curse.maven:forgeconfigapiport-547434:" // `fileId` e.g. `3671141` for mod version 3.2.0 for Minecraft 1.18.2 + modApi "maven.modrinth:forge-config-api-port:" // `fileId` e.g. `v6.0.2-1.19.4-Fabric` for mod version 6.0.2 for Minecraft 1.19.4 +} +``` + +#### Before Minecraft 1.20 + +
+ +**Versions of Forge Config Api Port for Minecraft before 1.19.3 are distributed using the `net.minecraftforge` Maven group instead of `fuzs.forgeconfigapiport`.** + +It is important to note that there is a minor difference from the production jars released on CurseForge and Modrinth: Jars from this Maven do not have a dependency set on Night Config in `fabric.mod.json`. This is necessary as there is no proper way of getting Night Config to be recognized as a Fabric / Quilt mod. + +With this in mind, when you plan to `include` Forge Config API Port as a Jar-in-Jar, absolutely make sure to set a proper dependency on Night Config within your own mod's `fabric.mod.json`, since Forge Config API Port won't have any set. +```json +{ + "depends": { + "com_electronwill_night-config_core": "*", + "com_electronwill_night-config_toml": "*" + } +} +``` + +There's also one more thing that will have to be done: When including Forge Config API Port from the Curse Maven, the mod will not be able to recognize the required Night Config libraries. You'll know that is the case when upon running the game, you are greeted with the following message: +``` + net.fabricmc.loader.impl.FormattedException: net.fabricmc.loader.impl.discovery.ModResolutionException: Mod resolution encountered an incompatible mod set! +A potential solution has been determined: + - Install com_electronwill_night-config_core, any version. + - Install com_electronwill_night-config_toml, any version. +``` +To resolve this issue, manually add dependency overrides (check the [Fabric Wiki](https://fabricmc.net/wiki/tutorial:dependency_overrides) for more information on this topic) to your run configuration. Do that by creating a new file at `run/config/fabric_loader_dependencies.json`, in which you put the following contents: +```json +{ + "version": 1, + "overrides": { + "forgeconfigapiport": { + "-depends": { + "com_electronwill_night-config_core": "", + "com_electronwill_night-config_toml": "" + } + } + } +} +``` + +**Also don't forget to manually add this file to your VCS, since the whole `run` directory is usually ignored by default. A dedicated common publication does not exist for these versions.** + +
+ +
+ +### Working with Forge Config API Port + +
+ +**These instructions exist only to highlight differences between the original Forge implementation and Forge Config Api Port. It is assumed you are generally familiar with Forge's configs.** + +#### Registering configs +The recommended point for registering your configs is directly in your `ModInitializer::onInitialize` method. + +Registering your configs works via `fuzs.forgeconfigapiport.api.config.v2.ForgeConfigRegistry`, obtain an instance of the implementation from `ForgeConfigRegistry#INSTANCE`. + +You'll have to provide the mod id of your mod, as there is no context which would be aware of the current mod as there is on Forge. +```java +void register(String modId, ModConfig.Type type, IConfigSpec spec) +``` +And as on Forge there is also a version which supports a custom file name. +```java +void register(String modId, ModConfig.Type type, IConfigSpec spec, String fileName) +``` + +#### Config loading +As Forge's mod loading process is split into multiple stages, configs aren't loaded immediately upon being registered. On Fabric/Quilt though, no such mod loading stages exist. Therefore, Forge Config API Port loads all registered configs **immediately**. + +#### Listening for config loading, reloading and unloading +Forge's `net.minecraftforge.fml.event.config.ModConfigEvent.Loading` and `net.minecraftforge.fml.event.config.ModConfigEvent.Reloading` events are both adapted for Fabric's/Quilt's callback event style. They can be accessed from the `fuzs.forgeconfigapiport.api.config.v2.ModConfigEvents` class. Additionally, there is an event that fires when server configs are unloading. As on Forge, all these events provide is the config being handled. + +All mod config related events run on the ModLifecycle event bus instead of the primary event bus on Forge (this essentially means events do not run globally, but are instead only handed to the mod that initially registered a listener). As there is no mod specific event bus on Fabric/Quilt, your mod id must be provided when registering a listener for a config callback to achieve the same behavior as on Forge, where the listener will only run for your mod. + +As an example, a complete implementation of the reloading callback looks something like this: +```java +ModConfigEvents.reloading().register((ModConfig config) -> { + <...> +}); +``` + +
+ +### Other differences from Forge in Forge Config Api Port + +
+ +Apart from the obviously necessary differences in implementation details from Forge mentioned above, Forge Config Api Port additionally includes minor tweaks to certain aspects of the config system. These tweaks are optional via a separate config file (found at `.minecraft/config/forgeconfigapiport.toml`) and only concern the implementation of certain features, their implementations do **NOT** result in changes to public facing code. + +These options are also available for Forge as a separate mod project: [Night Config Fixes](https://www.curseforge.com/minecraft/mc-mods/night-config-fixes) + +#### A fix for `ParsingException: Not enough data available` + +> recreateConfigsWhenParsingFails = true + +If your game has ever crashed with the following exception, this workaround is just for you and the main reason why Night Config Fixes was made in the first place: +> Caused by: com.electronwill.nightconfig.core.io.ParsingException: Not enough data available + +Sometimes and very randomly (also only reported on Windows systems), existing config files just loose all of their data and go completely blank. This is when the exception mentioned above is thrown, as Night Config is unable to read the file. + +With this workaround enabled, instead of the game crashing, the invalid blank file is simply deleted and a new file with default values is created in its place. No settings from the previous file can be restored, unfortunately. + +**Note:** +When enabling this workaround in a mod pack which ships some already configured configs, make sure to place those configs in the `defaultconfigs` directory, not just in `config`, so that when restoring a faulty config the desired default values from `defaultconfigs` are used instead of the built-in values. + +#### Apply default config values from `defaultconfigs` + +> correctConfigValuesFromDefaultConfig = true + +When only individual options in a config are invalid, like an option is missing or contains a set value that cannot be parsed, Forge corrects those individual options by restoring them to their default values in the config file. You can observe Forge doing this in the console when the following message is printed: + +> [net.minecraftforge.fml.config.ConfigFileTypeHandler/CONFIG]: Configuration file CONFIG_PATH is not correct. Correcting + +The problem with that is, Forge uses the built-in default value defined by the mod providing the config, but ignores any value from a possibly present default config in `defaultconfigs` which a mod pack might ship. + +This workaround changes this behavior and checks if an entry in a config in `defaultconfigs` exists first before falling back to correcting to the built-in default value. + +**Example:** +A config contains an option which requires an integer value. +The default value for this option defined by the mod the config is from is 3. +The default value for this option defined by the current mod pack via the config placed in `defaultconfigs` is 5 though. +When the user now accidentially enters a value such as 10.5, Forge corrects the input back to the default 3 (since 10.5 is a double, not an integer and therefore invalid). +With this workaround enabled the value will instead be corrected to 5. + +#### Global server configs + +> forceGlobalServerConfigs = true + +Changes Forge's server config type to generate in the global `config` directory, instead of on a local basis per world in `saves/WORLD_NAME/serverconfig`. + +This design decision by Forge simply causes too much confusion and frustration among users, so this mod felt like a good enough opportunity to include a fix for that. + +
+ +### Forge Config API Port in a multi-loader workspace + +
+ +As the sole purpose of Forge Config Api Port is to allow for config parity on Forge and Fabric/Quilt, it works especially great when developing your mod using a multi-loader workspace Gradle setup such as [this one](https://github.com/jaredlll08/MultiLoader-Template), arranged by [Jaredlll08](https://github.com/jaredlll08). + +Configs can be created and used within the common project without having to use any abstractions at all: Simply add Forge Config API Port to the common project (use the dedicated common publication so no mod loader specific code makes its way into your common project!). +```groovy +api "fuzs.forgeconfigapiport:forgeconfigapiport-common:" +``` + +As all class and package names are the same as Forge your code will compile on both Forge and Fabric/Quilt without any issues. The only thing where you'll actually have to use mod loader specific code is when registering configs, that's all! + +An example implementation of this can be found [here](https://github.com/thexaero/open-parties-and-claims). + +
+ +### In-game configuration using Forge Config API Port + +
+ +Just as with Forge itself, in-game configuration is not available in Forge Config Api Port by default. Instead, users will have to rely on third-party mods to offer that capability. + +Forge Config Api Port includes default support for and recommends the [Configured (Fabric)](https://www.curseforge.com/minecraft/mc-mods/configured-fabric) mod, which already is the most popular way of handling in-game configs on Forge. To use the configs provided by Configured in-game [Mod Menu](https://github.com/TerraformersMC/ModMenu) needs to be installed, too. + +Adding Configured and Mod Menu to your workspace is not a requirement, but highly recommended. +```groovy +repositories { + maven { + name = 'Curse Maven' + url = 'https://cursemaven.com' + } + maven { + name = 'Terraformers' + url = "https://maven.terraformersmc.com/" + } +} + +dependencies { + // Configured + modLocalRuntime "curse.maven:configured-fabric-667378:4166864" // Configured version 2.0.2 for Minecraft 1.19.3 + + // Mod Menu + modLocalRuntime "com.terraformersmc:modmenu:5.0.2" +} +``` + +
+ +### Config library alternatives to Forge's configs + +
+ +Forge Config Api Port is really useful when porting a Forge mod to Fabric/Quilt (or maintaining a mod on multiple loaders) as the existing config implementation does not need any major adjustments, and also no new library dependency needs to be added to the Forge project. + +As with every library though, the Forge config system does have a number of shortcomings, such as: +- Existence of three different config types with different functionalities that can be annoying to work with as a developer and easily become confusing for users +- Handling of server config files per world, without easy access to the file, and a major effort when changing server config values globally (this is addressed by Forge Config Api Port by moving server configs to the common config directory in `.minecraft/config/`) +- Lack of in-game configuration (possible via the third-party [Configured] mod though) +- Lack of annotation support when defining new config files + +So when starting on a brand-new mod project, it might be advisable to consider a completely different config library with more features than the Forge system has. Here is an overview with some recommendations: + +| Project | Forge | Fabric | Quilt | Minecraft Versions | Comments | +|-----------------------|-------|--------|-------|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Forge Config Api | ✅ | ✅ | ✅ | 1.16, 1.17, 1.18, 1.19 | Fabric and Quilt support provided by Forge Config Api Port. In-game config screens provided by the [Configured] mod. | +| [Spectre Lib] | ✅ | ✅ | ✅ | 1.19 | Not primarily a config library, implementation is very much based on Forge's configs. No in-game config screens. | +| [Pollen] | ✅ | ✅ | ✅ | 1.16, 1.18 | Not primarily a config library, implementation is very much based on Forge's configs. No in-game config screens. | +| [Cloth Config Api] | ✅ | ✅ | ✅ | 1.14, 1.15, 1.16, 1.17, 1.18, 1.19 | Very extensive config library with annotation support, in-game config screens, and great api documentation. | +| [Omega Config] | ❌ | ✅ | ✅ | 1.18, 1.19 | Config library with annotation support and in-game config screens. | +| [Owo Lib] | ❌ | ✅ | ✅ | 1.17, 1.18, 1.19 | Not primarily a config library, with annotation support and the most beautiful in-game config screens (customizable via XML!). Also the best api documentation. | +| [Midnight Lib] | ✅ | ✅ | ✅ | 1.17, 1.18, 1.19 | Great config library with in-game screens, also contains non-config related features though. | +| [YetAnotherConfigLib] | ❌ | ✅ | ✅ | 1.19 | Great library with lots of features, in-game screens are heavily inspired by Sodium. Great api documentation. | + +
+ +[Configured]: https://www.curseforge.com/minecraft/mc-mods/configured +[Spectre Lib]: https://github.com/illusivesoulworks/spectrelib +[Pollen]: https://www.curseforge.com/minecraft/mc-mods/pollen +[Cloth Config Api]: https://www.curseforge.com/minecraft/mc-mods/cloth-config +[Omega Config]: https://www.curseforge.com/minecraft/mc-mods/omega-config +[Owo Lib]: https://www.curseforge.com/minecraft/mc-mods/owo-lib +[Midnight Lib]: https://modrinth.com/mod/midnightlib +[YetAnotherConfigLib]: https://github.com/isXander/YetAnotherConfigLib diff --git a/forgeconfigapiport/build.gradle.kts b/forgeconfigapiport/build.gradle.kts new file mode 100644 index 0000000..7e923d1 --- /dev/null +++ b/forgeconfigapiport/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("dev.architectury.loom") +} + +val versionMc: String by rootProject +val versionForge: String by rootProject + +dependencies { + mappings(loom.officialMojangMappings()) + forge(group = "net.minecraftforge", name = "forge", version = "$versionMc-$versionForge") + + modImplementation(group = "dev.su5ed.sinytra.fabric-api", name = "fabric-api-base", version = "0.4.30+ef105b4977") +} diff --git a/forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigApiPortImpl.java b/forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigApiPortImpl.java new file mode 100644 index 0000000..386fb53 --- /dev/null +++ b/forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigApiPortImpl.java @@ -0,0 +1,37 @@ +package dev.su5ed.sinytra.connectorextras.forgeconfigapiport; + +import fuzs.forgeconfigapiport.api.config.v2.ModConfigEvents; +import net.fabricmc.fabric.api.event.Event; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.config.ModConfigEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; + +@Mod(ForgeConfigApiPortImpl.MODID) +public class ForgeConfigApiPortImpl { + public static final String MODID = "forgeconfigapiport"; + + // TODO Normalize modids + public ForgeConfigApiPortImpl() { + IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus(); + + bus.addListener(ForgeConfigApiPortImpl::onConfigLoad); + bus.addListener(ForgeConfigApiPortImpl::onConfigReload); + bus.addListener(ForgeConfigApiPortImpl::onConfigUnload); + } + + private static void onConfigLoad(ModConfigEvent.Loading event) { + Event fabricEvent = ModConfigEvents.loading(event.getConfig().getModId()); + fabricEvent.invoker().onModConfigLoading(event.getConfig()); + } + + private static void onConfigReload(ModConfigEvent.Reloading event) { + Event fabricEvent = ModConfigEvents.reloading(event.getConfig().getModId()); + fabricEvent.invoker().onModConfigReloading(event.getConfig()); + } + + private static void onConfigUnload(ModConfigEvent.Unloading event) { + Event fabricEvent = ModConfigEvents.unloading(event.getConfig().getModId()); + fabricEvent.invoker().onModConfigUnloading(event.getConfig()); + } +} diff --git a/forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigPathsImpl.java b/forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigPathsImpl.java new file mode 100644 index 0000000..dff6293 --- /dev/null +++ b/forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigPathsImpl.java @@ -0,0 +1,34 @@ +package dev.su5ed.sinytra.connectorextras.forgeconfigapiport; + +import fuzs.forgeconfigapiport.api.config.v2.ForgeConfigPaths; +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.fml.loading.FMLPaths; + +import java.nio.file.Path; + +public class ForgeConfigPathsImpl implements ForgeConfigPaths { + @Override + public Path getClientConfigDirectory() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public Path getCommonConfigDirectory() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public Path getServerConfigDirectory(MinecraftServer server) { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public boolean forceGlobalServerConfigs() { + return false; + } + + @Override + public Path getDefaultConfigsDirectory() { + return FMLPaths.CONFIGDIR.get(); + } +} diff --git a/forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigRegistryImpl.java b/forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigRegistryImpl.java new file mode 100644 index 0000000..6afc7aa --- /dev/null +++ b/forgeconfigapiport/src/main/java/dev/su5ed/sinytra/connectorextras/forgeconfigapiport/ForgeConfigRegistryImpl.java @@ -0,0 +1,29 @@ +package dev.su5ed.sinytra.connectorextras.forgeconfigapiport; + +import fuzs.forgeconfigapiport.api.config.v2.ForgeConfigRegistry; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.config.IConfigSpec; +import net.minecraftforge.fml.config.ModConfig; + +public class ForgeConfigRegistryImpl implements ForgeConfigRegistry { + @Override + public ModConfig register(String modId, ModConfig.Type type, IConfigSpec spec) { + ModContainer container = getModContainer(modId); + ModConfig modConfig = new ModConfig(type, spec, container); + container.addConfig(modConfig); + return modConfig; + } + + @Override + public ModConfig register(String modId, ModConfig.Type type, IConfigSpec spec, String fileName) { + ModContainer container = getModContainer(modId); + ModConfig modConfig = new ModConfig(type, spec, container, fileName); + container.addConfig(modConfig); + return modConfig; + } + + private static ModContainer getModContainer(String modid) { + return ModList.get().getModContainerById(modid).orElseThrow(() -> new RuntimeException("Mod container not found for mod " + modid)); + } +} diff --git a/forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ForgeConfigPaths.java b/forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ForgeConfigPaths.java new file mode 100644 index 0000000..5384ead --- /dev/null +++ b/forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ForgeConfigPaths.java @@ -0,0 +1,63 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package fuzs.forgeconfigapiport.api.config.v2; + +import dev.su5ed.sinytra.connectorextras.forgeconfigapiport.ForgeConfigPathsImpl; +import net.minecraft.server.MinecraftServer; + +import java.nio.file.Path; + +/** + * Access to paths where different kinds of config files are stored by Forge. + */ +public interface ForgeConfigPaths { + /** + * implementation instance for retrieving config paths + */ + ForgeConfigPaths INSTANCE = new ForgeConfigPathsImpl(); + + /** + * The directory where client configs are stored at. + * + * @return client config path + */ + Path getClientConfigDirectory(); + + /** + * The directory where common configs are stored at. + * + * @return common config path + */ + Path getCommonConfigDirectory(); + + /** + * The directory where server configs are stored at. By default, this is inside the world directory instead of the common .minecraft/config/ directory. + *

Forge Config Api Port has a config setting (which is not present in Forge itself) to load server configs from the common directory instead. + * This method will always provide the directory that is currently set via the Forge Config Api Port config. + * + * @param server the current {@link MinecraftServer} + * @return server config path + */ + Path getServerConfigDirectory(final MinecraftServer server); + + /** + * Prevent server config files from being saved inside the current world directory. Instead, save them to the global config directory in .minecraft/config/. + * This option effectively disables per world server configs, but helps a lot with avoiding user confusion. + * + * @return are server configs stored in the global .minecraft/config/ directory + */ + boolean forceGlobalServerConfigs(); + + /** + * Path where default configs are stored, by default inside of .minecraft/defaultconfigs. + *

This path is configurable via the Forge Config Api Port config. + *

Default configs are mainly intended for server configs to allow pre-configured configs to be applied to every + * newly created world (since server configs are handled per world). This mechanic also applies to other config types (although they are only created once). + * + * @return default configs path + */ + Path getDefaultConfigsDirectory(); +} diff --git a/forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ForgeConfigRegistry.java b/forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ForgeConfigRegistry.java new file mode 100644 index 0000000..73a5955 --- /dev/null +++ b/forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ForgeConfigRegistry.java @@ -0,0 +1,46 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package fuzs.forgeconfigapiport.api.config.v2; + +import dev.su5ed.sinytra.connectorextras.forgeconfigapiport.ForgeConfigRegistryImpl; +import net.minecraftforge.fml.config.IConfigSpec; +import net.minecraftforge.fml.config.ModConfig; + +/** + * Registry for adding your configs. On Forge this is done using net.minecraftforge.fml.ModLoadingContext, which does not exist in Forge Config Api Port. + *

Note that opposed to Forge, configs are loaded and usable immediately after registration. + */ +public interface ForgeConfigRegistry { + /** + * implementation instance for registering configs + */ + ForgeConfigRegistry INSTANCE = new ForgeConfigRegistryImpl(); + + /** + * Register a new mod config, only difference from registering on Forge is modId has to be provided as there is no loading context to get that information from + * + * @param modId mod id of your mod + * @param type type of this mod config (client, common, or server) + * @param spec the built config spec + * @return the {@link ModConfig} instance + * + * @throws IllegalArgumentException when no mod container is found for modId + */ + ModConfig register(String modId, ModConfig.Type type, IConfigSpec spec); + + /** + * Register a new mod config, only difference from registering on Forge is modId has to be provided as there is no loading context to get that information from + * + * @param modId mod id of your mod + * @param type type of this mod config (client, common, or server) + * @param spec the built config spec + * @param fileName file name to use instead of default + * @return the {@link ModConfig} instance + * + * @throws IllegalArgumentException when no mod container is found for modId + */ + ModConfig register(String modId, ModConfig.Type type, IConfigSpec spec, String fileName); +} diff --git a/forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ModConfigEvents.java b/forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ModConfigEvents.java new file mode 100644 index 0000000..bed2374 --- /dev/null +++ b/forgeconfigapiport/src/main/java/fuzs/forgeconfigapiport/api/config/v2/ModConfigEvents.java @@ -0,0 +1,149 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +package fuzs.forgeconfigapiport.api.config.v2; + +import com.google.common.collect.Maps; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraftforge.fml.config.ModConfig; + +import java.util.Map; +import java.util.Objects; + +/** + * Mod config events adapted for Fabric's callback event style. + * + *

Unlike on Forge there are is no mod event bus for firing mod specific events on Fabric meaning general callbacks would be fired for every mod for every config which is undesirable. + * To solve this issue without introducing a manual mod id check in the events implementation itself, mod config event callbacks are instead created separately for every mod that has configs registered with Forge Config Api Port. + * Accessing events for a specific mod is done by calling {@link #loading(String)}, {@link #reloading(String)} or {@link #unloading(String)}. + */ +public final class ModConfigEvents { + + private ModConfigEvents() { + + } + + /** + * access to mod specific loading event + * + * @param modId the mod id to access config event for + * @return the {@link Loading} event + */ + public static Event loading(String modId) { + Objects.requireNonNull(modId, "mod id is null"); + return ModConfigEventsHolder.modSpecific(modId).loading(); + } + + /** + * access to mod specific reloading event + * + * @param modId the mod id to access config event for + * @return the {@link Reloading} event + */ + public static Event reloading(String modId) { + Objects.requireNonNull(modId, "mod id is null"); + return ModConfigEventsHolder.modSpecific(modId).reloading(); + } + + /** + * access to mod specific unloading event + * + * @param modId the mod id to access config event for + * @return the {@link Unloading} event + */ + public static Event unloading(String modId) { + Objects.requireNonNull(modId, "mod id is null"); + return ModConfigEventsHolder.modSpecific(modId).unloading(); + } + + /** + * Called when a config is loaded or unloaded (unloading only applies for server configs) + */ + @FunctionalInterface + public interface Loading { + + /** + * @param config the mod config that is loading + */ + void onModConfigLoading(ModConfig config); + } + + /** + * Called when a config is reloaded which happens when the file is updated by ConfigWatcher and when it is synced from the server + */ + @FunctionalInterface + public interface Reloading { + + /** + * @param config the mod config that is reloading + */ + void onModConfigReloading(ModConfig config); + } + + /** + * Called when a config is unloaded which happens only for server configs when the corresponding world is unloaded + */ + @FunctionalInterface + public interface Unloading { + + /** + * @param config the mod config that is unloading + */ + void onModConfigUnloading(ModConfig config); + } + + /** + * internal mod specific event storage + * + * @param modId the mod + * @param loading loading event + * @param reloading reloading event + */ + private record ModConfigEventsHolder(String modId, Event loading, Event reloading, + Event unloading) { + /** + * internal storage for mod specific config events + */ + private static final Map MOD_SPECIFIC_EVENT_HOLDERS = Maps.newConcurrentMap(); + + /** + * internal access to mod specific config events + * + *

the method is synchronized as access from different threads is possible (e.g. the config watcher thread) + * + * @param modId the mod id to access config events for + * @return access to a holder with both mod specific {@link Loading} and {@link Reloading} events + */ + public static ModConfigEventsHolder modSpecific(String modId) { + return MOD_SPECIFIC_EVENT_HOLDERS.computeIfAbsent(modId, ModConfigEventsHolder::create); + } + + /** + * creates a new holder duh + * + * @param modId the mod + * @return holder with newly created events + */ + private static ModConfigEventsHolder create(String modId) { + Event loading = EventFactory.createArrayBacked(Loading.class, listeners -> config -> { + for (Loading event : listeners) { + event.onModConfigLoading(config); + } + }); + Event reloading = EventFactory.createArrayBacked(Reloading.class, listeners -> config -> { + for (Reloading event : listeners) { + event.onModConfigReloading(config); + } + }); + Event unloading = EventFactory.createArrayBacked(Unloading.class, listeners -> config -> { + for (Unloading event : listeners) { + event.onModConfigUnloading(config); + } + }); + return new ModConfigEventsHolder(modId, loading, reloading, unloading); + } + } +} diff --git a/forgeconfigapiport/src/main/resources/META-INF/mods.toml b/forgeconfigapiport/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..2f28a52 --- /dev/null +++ b/forgeconfigapiport/src/main/resources/META-INF/mods.toml @@ -0,0 +1,30 @@ +modLoader="javafml" +loaderVersion="[47,)" +license="MPL-2.0" +issueTrackerURL="https://github.com/Sinytra/ConnectorExtras/issues" + +[[mods]] +modId="forgeconfigapiport" +version="8.0.0" +displayName="Forge Config API Port (ConnectorExtras)" +authors="Su5eD" +credits="Fuzs - upstream author" +displayURL="https://github.com/Sinytra/ConnectorExtras" +description=''' +Forge's whole config system provided to the Fabric ecosystem. Designed for a multiloader architecture. +''' +logoFile="mod_logo.png" +displayTest = 'IGNORE_ALL_VERSION' + +[[dependencies.forgeconfigapiport]] + modId="forge" + mandatory=true + versionRange="[47,)" + ordering="NONE" + side="BOTH" +[[dependencies.forgeconfigapiport]] + modId="minecraft" + mandatory=true + versionRange="[1.20.1,1.21)" + ordering="NONE" + side="BOTH" diff --git a/forgeconfigapiport/src/main/resources/mod_logo.png b/forgeconfigapiport/src/main/resources/mod_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0e477a1dadf0fd1ce249bf32653a2572fd71e785 GIT binary patch literal 7137 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