diff --git a/README.md b/README.md index 155e51c..be24fb6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0#gltf-20-sampl In order to adapt into BSL Shaders' SEUS/Old PBR format, some change were made: - All normal textures had been converted from OpenGL format (Y+) to DirectX format (Y-) by flipping green channel. -- `Occlusion(R)Roughness(G)Metallic(B)` textures and `Emissive color(RGB)` textures had been edited and combined into `Glossiness(R) Metallic(G)Emissive strength(B)` textures for specular map. +- `Occlusion(R)Roughness(G)Metallic(B)` textures and `Emissive color(RGB)` textures had been edited and combined into `Glossiness(R)Metallic(G)Emissive strength(B)` textures for specular map. ## Additonal Note About Setup This Project 1. Create a folder named `libs` in the same dir level as `src`. 2. Download MCglTF jar from curseforge and then put into the `libs` folder. diff --git a/build.gradle b/build.gradle index c4cb4a6..742499b 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ apply plugin: 'net.minecraftforge.gradle' apply plugin: 'eclipse' apply plugin: 'maven-publish' -version = '1.12.2-Forge-1.1.0.1' +version = '1.12.2-Forge-1.2.0.0' group = 'com.timlee9024.mcgltf.example' // http://maven.apache.org/guides/mini/guide-naming-conventions.html archivesBaseName = 'MCglTF-Example' @@ -82,31 +82,6 @@ minecraft { } } } - - data { - workingDirectory project.file('run') - - // Recommended logging data for a userdev environment - // The markers can be changed as needed. - // "SCAN": For mods scan. - // "REGISTRIES": For firing of registry events. - // "REGISTRYDUMP": For getting the contents of all registries. - property 'forge.logging.markers', 'REGISTRIES' - - // Recommended logging level for the console - // You can set various levels here. - // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels - property 'forge.logging.console.level', 'debug' - - // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. - args '--mod', 'example_mcgltf_usage', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') - - mods { - example_mcgltf_usage { - source sourceSets.main - } - } - } } } diff --git a/src/main/java/com/timlee9024/mcgltf/example/AbstractItemGltfModelReceiver.java b/src/main/java/com/timlee9024/mcgltf/example/AbstractItemGltfModelReceiver.java deleted file mode 100644 index 637c428..0000000 --- a/src/main/java/com/timlee9024/mcgltf/example/AbstractItemGltfModelReceiver.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.timlee9024.mcgltf.example; - -import java.util.List; - -import com.timlee9024.mcgltf.IGltfModelReceiver; -import com.timlee9024.mcgltf.RenderedGltfModel; - -import de.javagl.jgltf.model.GltfAnimations; -import de.javagl.jgltf.model.animation.Animation; - -public abstract class AbstractItemGltfModelReceiver implements IGltfModelReceiver { - - public List commands; - - public List animations; - - @Override - public void onReceiveSharedModel(RenderedGltfModel renderedModel) { - commands = renderedModel.sceneCommands.get(0); - animations = GltfAnimations.createModelAnimations(renderedModel.gltfModel.getAnimationModels()); - } - -} diff --git a/src/main/java/com/timlee9024/mcgltf/example/ClientProxy.java b/src/main/java/com/timlee9024/mcgltf/example/ClientProxy.java index 262538e..0006d08 100644 --- a/src/main/java/com/timlee9024/mcgltf/example/ClientProxy.java +++ b/src/main/java/com/timlee9024/mcgltf/example/ClientProxy.java @@ -1,12 +1,8 @@ package com.timlee9024.mcgltf.example; -import org.lwjgl.opengl.GL11; - import com.timlee9024.mcgltf.ItemCameraTransformsHelper; import com.timlee9024.mcgltf.MCglTF; -import de.javagl.jgltf.model.animation.Animation; -import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.block.model.ModelResourceLocation; import net.minecraft.client.renderer.entity.Render; import net.minecraft.client.renderer.entity.RenderManager; @@ -39,57 +35,36 @@ public Render createRenderFor(RenderManager manager) { @Override public void preInit(FMLPreInitializationEvent event) { - AbstractItemGltfModelReceiver itemModelReceiver = new AbstractItemGltfModelReceiver() { + ItemRendererExample itemRenderer = new ItemRendererExample() { @Override public ResourceLocation getModelLocation() { return new ResourceLocation("mcgltf", "models/item/water_bottle.gltf"); } }; - MCglTF.getInstance().addGltfModelReceiver(itemModelReceiver); + MCglTF.getInstance().addGltfModelReceiver(itemRenderer); - AbstractItemGltfModelReceiver blockItemModelReceiver = new AbstractItemGltfModelReceiver() { + ItemRendererExample blockItemRenderer = new ItemRendererExample() { @Override public ResourceLocation getModelLocation() { return new ResourceLocation("mcgltf", "models/block/boom_box.gltf"); } }; - MCglTF.getInstance().addGltfModelReceiver(blockItemModelReceiver); + MCglTF.getInstance().addGltfModelReceiver(blockItemRenderer); //According to Forge Doc "Each mod should only have one instance of a custom TEISR/ISTER/BEWLR.", due to creating an instance will also initiate unused fields inside the class which waste a lots of memory. TileEntityItemStackRenderer teisr = new TileEntityItemStackRenderer() { @Override public void renderByItem(ItemStack itemStackIn) { - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); - GL11.glShadeModel(GL11.GL_SMOOTH); - Item currentItem = itemStackIn.getItem(); if(currentItem == item) { - //Require ItemCameraTransformsHelper$registerDummyModelToAccessCurrentTransformTypeForTEISR(yourItem) during ModelRegistryEvent to make ItemCameraTransformsHelper$getCurrentTransformType() work - switch(ItemCameraTransformsHelper.getCurrentTransformType()) { - case GUI: - GL11.glEnable(GL11.GL_LIGHTING); - break; - default: - break; - } - - //Play every animation clips simultaneously - for(Animation animation : itemModelReceiver.animations) { - animation.update(net.minecraftforge.client.model.animation.Animation.getWorldTime(Minecraft.getMinecraft().world, net.minecraftforge.client.model.animation.Animation.getPartialTickTime()) % animation.getEndTimeS()); - } - itemModelReceiver.commands.forEach(Runnable::run); + itemRenderer.renderWithItemCameraTransformsHelper(); } else if(currentItem == itemBlock) { - for(Animation animation : blockItemModelReceiver.animations) { - animation.update(net.minecraftforge.client.model.animation.Animation.getWorldTime(Minecraft.getMinecraft().world, net.minecraftforge.client.model.animation.Animation.getPartialTickTime()) % animation.getEndTimeS()); - } - blockItemModelReceiver.commands.forEach(Runnable::run); + blockItemRenderer.render(); } - - GL11.glPopAttrib(); } }; diff --git a/src/main/java/com/timlee9024/mcgltf/example/Example.java b/src/main/java/com/timlee9024/mcgltf/example/Example.java index b1b2d98..f224f9a 100644 --- a/src/main/java/com/timlee9024/mcgltf/example/Example.java +++ b/src/main/java/com/timlee9024/mcgltf/example/Example.java @@ -8,7 +8,7 @@ import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.registry.GameRegistry; -@Mod(modid = "example_mcgltf_usage", useMetadata = true) +@Mod(modid = "example_mcgltf_usage", dependencies = "required-after-client:mcgltf;", useMetadata = true) public class Example { @SidedProxy(clientSide = "com.timlee9024.mcgltf.example.ClientProxy", serverSide = "com.timlee9024.mcgltf.example.ServerProxy") diff --git a/src/main/java/com/timlee9024/mcgltf/example/ItemRendererExample.java b/src/main/java/com/timlee9024/mcgltf/example/ItemRendererExample.java new file mode 100644 index 0000000..efc2844 --- /dev/null +++ b/src/main/java/com/timlee9024/mcgltf/example/ItemRendererExample.java @@ -0,0 +1,105 @@ +package com.timlee9024.mcgltf.example; + +import java.util.ArrayList; +import java.util.List; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; + +import com.timlee9024.mcgltf.IGltfModelReceiver; +import com.timlee9024.mcgltf.ItemCameraTransformsHelper; +import com.timlee9024.mcgltf.MCglTF; +import com.timlee9024.mcgltf.RenderedGltfModel; +import com.timlee9024.mcgltf.RenderedGltfScene; +import com.timlee9024.mcgltf.animation.GltfAnimationCreator; +import com.timlee9024.mcgltf.animation.InterpolatedChannel; + +import de.javagl.jgltf.model.AnimationModel; +import net.minecraft.client.Minecraft; +import net.minecraftforge.client.model.animation.Animation; + +public abstract class ItemRendererExample implements IGltfModelReceiver { + + protected RenderedGltfScene renderedScene; + + protected List> animations; + + @Override + public void onReceiveSharedModel(RenderedGltfModel renderedModel) { + renderedScene = renderedModel.renderedGltfScenes.get(0); + List animationModels = renderedModel.gltfModel.getAnimationModels(); + animations = new ArrayList>(animationModels.size()); + for(AnimationModel animationModel : animationModels) { + animations.add(GltfAnimationCreator.createGltfAnimation(animationModel)); + } + } + + public void render() { + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glShadeModel(GL11.GL_SMOOTH); + + float time = Animation.getWorldTime(Minecraft.getMinecraft().world, Animation.getPartialTickTime()); + for(List animation : animations) { + animation.parallelStream().forEach((channel) -> { + float[] keys = channel.getKeys(); + channel.update(time % keys[keys.length - 1]); + }); + } + + if(MCglTF.getInstance().isShaderModActive()) { + renderedScene.renderForShaderMod(); + } + else { + renderedScene.renderForVanilla(); + } + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0); + GL30.glBindVertexArray(0); + RenderedGltfModel.nodeGlobalTransformLookup.clear(); + + GL11.glPopAttrib(); + } + + /** + * Require {@link ItemCameraTransformsHelper#registerDummyModelToAccessCurrentTransformTypeForTEISR(net.minecraft.item.Item) ItemCameraTransformsHelper#registerDummyModelToAccessCurrentTransformTypeForTEISR(yourItem)} during + * {@link net.minecraftforge.client.event.ModelRegistryEvent ModelRegistryEvent} to make {@link ItemCameraTransformsHelper#getCurrentTransformType()} work + */ + public void renderWithItemCameraTransformsHelper() { + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glShadeModel(GL11.GL_SMOOTH); + + float time = Animation.getWorldTime(Minecraft.getMinecraft().world, Animation.getPartialTickTime()); + //Play every animation clips simultaneously + for(List animation : animations) { + animation.parallelStream().forEach((channel) -> { + float[] keys = channel.getKeys(); + channel.update(time % keys[keys.length - 1]); + }); + } + + switch(ItemCameraTransformsHelper.getCurrentTransformType()) { + case GUI: + GL11.glEnable(GL11.GL_LIGHTING); + renderedScene.renderForVanilla(); + break; + default: + if(MCglTF.getInstance().isShaderModActive()) { + renderedScene.renderForShaderMod(); + } + else { + renderedScene.renderForVanilla(); + } + break; + } + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0); + GL30.glBindVertexArray(0); + RenderedGltfModel.nodeGlobalTransformLookup.clear(); + + GL11.glPopAttrib(); + } + +} diff --git a/src/main/java/com/timlee9024/mcgltf/example/RenderEntityExample.java b/src/main/java/com/timlee9024/mcgltf/example/RenderEntityExample.java index dd0a304..1463f73 100644 --- a/src/main/java/com/timlee9024/mcgltf/example/RenderEntityExample.java +++ b/src/main/java/com/timlee9024/mcgltf/example/RenderEntityExample.java @@ -1,26 +1,33 @@ package com.timlee9024.mcgltf.example; +import java.util.ArrayList; import java.util.List; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; import com.timlee9024.mcgltf.IGltfModelReceiver; +import com.timlee9024.mcgltf.MCglTF; import com.timlee9024.mcgltf.RenderedGltfModel; +import com.timlee9024.mcgltf.RenderedGltfScene; +import com.timlee9024.mcgltf.animation.GltfAnimationCreator; +import com.timlee9024.mcgltf.animation.InterpolatedChannel; -import de.javagl.jgltf.model.GltfAnimations; -import de.javagl.jgltf.model.animation.Animation; +import de.javagl.jgltf.model.AnimationModel; import net.minecraft.client.renderer.culling.ICamera; import net.minecraft.client.renderer.entity.Render; import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.entity.Entity; import net.minecraft.util.ResourceLocation; +import net.minecraftforge.client.model.animation.Animation; public class RenderEntityExample extends Render implements IGltfModelReceiver { - protected List commands; + protected RenderedGltfScene renderedScene; - protected List animations; + protected List> animations; public RenderEntityExample(RenderManager renderManager) { super(renderManager); @@ -33,8 +40,12 @@ public ResourceLocation getModelLocation() { @Override public void onReceiveSharedModel(RenderedGltfModel renderedModel) { - commands = renderedModel.sceneCommands.get(0); - animations = GltfAnimations.createModelAnimations(renderedModel.gltfModel.getAnimationModels()); + renderedScene = renderedModel.renderedGltfScenes.get(0); + List animationModels = renderedModel.gltfModel.getAnimationModels(); + animations = new ArrayList>(animationModels.size()); + for(AnimationModel animationModel : animationModels) { + animations.add(GltfAnimationCreator.createGltfAnimation(animationModel)); + } } @Override @@ -61,21 +72,37 @@ else if (livingEntity.getLeashed() && livingEntity.getLeashHolder() != null) @Override public void doRender(EntityExample entity, double x, double y, double z, float entityYaw, float partialTicks) { - if(commands != null) { - GL11.glPushMatrix(); - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); - GL11.glShadeModel(GL11.GL_SMOOTH); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - GL11.glEnable(GL11.GL_BLEND); - GL11.glTranslated(x, y, z); - GL11.glRotatef(interpolateRotation(entity.prevRenderYawOffset, entity.renderYawOffset, partialTicks), 0.0F, 1.0F, 0.0F); - for(Animation animation : animations) { - animation.update(net.minecraftforge.client.model.animation.Animation.getWorldTime(entity.world, partialTicks) % animation.getEndTimeS()); - } - commands.forEach(Runnable::run); - GL11.glPopAttrib(); - GL11.glPopMatrix(); + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glShadeModel(GL11.GL_SMOOTH); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_BLEND); + GL11.glTranslated(x, y, z); + GL11.glRotatef(interpolateRotation(entity.prevRenderYawOffset, entity.renderYawOffset, partialTicks), 0.0F, 1.0F, 0.0F); + + float time = Animation.getWorldTime(entity.world, partialTicks); + //Play every animation clips simultaneously + for(List animation : animations) { + animation.parallelStream().forEach((channel) -> { + float[] keys = channel.getKeys(); + channel.update(time % keys[keys.length - 1]); + }); + } + + if(MCglTF.getInstance().isShaderModActive()) { + renderedScene.renderForShaderMod(); } + else { + renderedScene.renderForVanilla(); + } + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0); + GL30.glBindVertexArray(0); + RenderedGltfModel.nodeGlobalTransformLookup.clear(); + + GL11.glPopAttrib(); + GL11.glPopMatrix(); super.doRender(entity, x, y, z, entityYaw, partialTicks); } diff --git a/src/main/java/com/timlee9024/mcgltf/example/TileEntitySpecialRendererExample.java b/src/main/java/com/timlee9024/mcgltf/example/TileEntitySpecialRendererExample.java index 569209b..3e1e4ee 100644 --- a/src/main/java/com/timlee9024/mcgltf/example/TileEntitySpecialRendererExample.java +++ b/src/main/java/com/timlee9024/mcgltf/example/TileEntitySpecialRendererExample.java @@ -1,25 +1,32 @@ package com.timlee9024.mcgltf.example; +import java.util.ArrayList; import java.util.List; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; import com.timlee9024.mcgltf.IGltfModelReceiver; +import com.timlee9024.mcgltf.MCglTF; import com.timlee9024.mcgltf.RenderedGltfModel; +import com.timlee9024.mcgltf.RenderedGltfScene; +import com.timlee9024.mcgltf.animation.GltfAnimationCreator; +import com.timlee9024.mcgltf.animation.InterpolatedChannel; -import de.javagl.jgltf.model.GltfAnimations; -import de.javagl.jgltf.model.animation.Animation; +import de.javagl.jgltf.model.AnimationModel; import net.minecraft.block.BlockHorizontal; import net.minecraft.client.renderer.tileentity.TileEntitySpecialRenderer; import net.minecraft.util.ResourceLocation; import net.minecraft.world.World; +import net.minecraftforge.client.model.animation.Animation; public class TileEntitySpecialRendererExample extends TileEntitySpecialRenderer implements IGltfModelReceiver { - protected List commands; + protected RenderedGltfScene renderedScene; - protected List animations; + protected List> animations; @Override public ResourceLocation getModelLocation() { @@ -28,47 +35,66 @@ public ResourceLocation getModelLocation() { @Override public void onReceiveSharedModel(RenderedGltfModel renderedModel) { - commands = renderedModel.sceneCommands.get(0); - animations = GltfAnimations.createModelAnimations(renderedModel.gltfModel.getAnimationModels()); + renderedScene = renderedModel.renderedGltfScenes.get(0); + List animationModels = renderedModel.gltfModel.getAnimationModels(); + animations = new ArrayList>(animationModels.size()); + for(AnimationModel animationModel : animationModels) { + animations.add(GltfAnimationCreator.createGltfAnimation(animationModel)); + } } @Override public void render(TileEntityExample te, double x, double y, double z, float partialTicks, int destroyStage, float alpha) { - if(commands != null) { - GL11.glPushMatrix(); - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); - GL11.glShadeModel(GL11.GL_SMOOTH); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - GL11.glEnable(GL11.GL_BLEND); - GL11.glTranslated(x, y, z); - World world = te.getWorld(); - if(world != null) { - GL11.glTranslatef(0.5F, 0.0F, 0.5F); //Make sure it is in the center of the block - switch(world.getBlockState(te.getPos()).getValue(BlockHorizontal.FACING)) { - case DOWN: - break; - case UP: - break; - case NORTH: - break; - case SOUTH: - GL11.glRotatef(180.0F, 0.0F, 1.0F, 0.0F); - break; - case WEST: - GL11.glRotatef(90.0F, 0.0F, 1.0F, 0.0F); - break; - case EAST: - GL11.glRotatef(-90.0F, 0.0F, 1.0F, 0.0F); - break; - } - } - for(Animation animation : animations) { - animation.update(net.minecraftforge.client.model.animation.Animation.getWorldTime(world, partialTicks) % animation.getEndTimeS()); + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glShadeModel(GL11.GL_SMOOTH); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_BLEND); + GL11.glTranslated(x, y, z); + World world = te.getWorld(); + if(world != null) { + GL11.glTranslatef(0.5F, 0.0F, 0.5F); //Make sure it is in the center of the block + switch(world.getBlockState(te.getPos()).getValue(BlockHorizontal.FACING)) { + case DOWN: + break; + case UP: + break; + case NORTH: + break; + case SOUTH: + GL11.glRotatef(180.0F, 0.0F, 1.0F, 0.0F); + break; + case WEST: + GL11.glRotatef(90.0F, 0.0F, 1.0F, 0.0F); + break; + case EAST: + GL11.glRotatef(-90.0F, 0.0F, 1.0F, 0.0F); + break; } - commands.forEach(Runnable::run); - GL11.glPopAttrib(); - GL11.glPopMatrix(); } + float time = Animation.getWorldTime(world, partialTicks); + //Play every animation clips simultaneously + for(List animation : animations) { + animation.parallelStream().forEach((channel) -> { + float[] keys = channel.getKeys(); + channel.update(time % keys[keys.length - 1]); + }); + } + + if(MCglTF.getInstance().isShaderModActive()) { + renderedScene.renderForShaderMod(); + } + else { + renderedScene.renderForVanilla(); + } + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0); + GL30.glBindVertexArray(0); + RenderedGltfModel.nodeGlobalTransformLookup.clear(); + + GL11.glPopAttrib(); + GL11.glPopMatrix(); } } diff --git a/src/main/resources/mcgltf_icon.png b/src/main/resources/mcgltf_icon.png new file mode 100644 index 0000000..fd6bcb5 Binary files /dev/null and b/src/main/resources/mcgltf_icon.png differ diff --git a/src/main/resources/mcmod.info b/src/main/resources/mcmod.info index 5b390e1..93787ca 100644 --- a/src/main/resources/mcmod.info +++ b/src/main/resources/mcmod.info @@ -3,13 +3,13 @@ "modid": "example_mcgltf_usage", "name": "Example MCglTF Usage", "description": "Example mod to demonstrate the usage of MCglTF.", - "version": "1.12.2-Forge-1.1.0.1", + "version": "1.12.2-Forge-1.2.0.0", "mcversion": "1.12.2", "url": "github.com/TimLee9024/MCglTF-Example/tree/1.12.2-Forge/", "updateUrl": "raw.githubusercontent.com/TimLee9024/MCglTF-Example/1.12.2-Forge/updates.json", "authorList": ["TimLee9024"], "credits": "Microsoft and Cesium, for providing glTF sample models", - "logoFile": "", + "logoFile": "mcgltf_icon.png", "screenshots": [], "dependencies": [] } diff --git a/updates.json b/updates.json index 255a2a6..4c5fec6 100644 --- a/updates.json +++ b/updates.json @@ -1,12 +1,13 @@ { "homepage": "https://github.com/TimLee9024/MCglTF-Example/releases", "1.12.2": { - "1.12.2-Forge-1.1.0.0": "Fix updateURL", + "1.12.2-Forge-1.2.0.0": "Update to compatible with MCglTF-1.2.0.0", + "1.12.2-Forge-1.1.0.1": "Fix updateURL", "1.12.2-Forge-1.1.0.0": "Change TEISR usage", "1.12.2-Forge-1.0.0.0": "Initial release" }, "promos": { - "1.12.2-latest": "1.12.2-Forge-1.1.0.1", - "1.12.2-recommended": "1.12.2-Forge-1.1.0.1" + "1.12.2-latest": "1.12.2-Forge-1.2.0.0", + "1.12.2-recommended": "1.12.2-Forge-1.2.0.0" } } \ No newline at end of file