diff --git a/build.gradle b/build.gradle index eddb762ed..4bcca0da4 100644 --- a/build.gradle +++ b/build.gradle @@ -99,6 +99,7 @@ sourceSets { resources { srcDir "src/main/resources/" include "assets/appliedenergistics2/recipes/*.recipe", + "assets/appliedenergistics2/recipes/README.html", "assets/appliedenergistics2/lang/*.lang", "assets/appliedenergistics2/textures/blocks/*", "assets/appliedenergistics2/textures/guis/*", diff --git a/src/api/java/appeng/api/recipes/IRecipeLoader.java b/src/api/java/appeng/api/recipes/IRecipeLoader.java index 0e05c745e..9684cb06b 100644 --- a/src/api/java/appeng/api/recipes/IRecipeLoader.java +++ b/src/api/java/appeng/api/recipes/IRecipeLoader.java @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Copyright (c) 2013 AlgorithmX2 + * Copyright (c) 2013 - 2015 AlgorithmX2 * * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in @@ -25,10 +25,17 @@ package appeng.api.recipes; import java.io.BufferedReader; +import javax.annotation.Nonnull; public interface IRecipeLoader { - - BufferedReader getFile( String s ) throws Exception; + /** + * @param filePath the path to the to be loaded file + * + * @return reader handler of the file + * + * @throws Exception if reading goes wrong + */ + BufferedReader getFile( @Nonnull String filePath ) throws Exception; } diff --git a/src/main/java/appeng/core/AEConfig.java b/src/main/java/appeng/core/AEConfig.java index 68edfa9de..9942594e1 100644 --- a/src/main/java/appeng/core/AEConfig.java +++ b/src/main/java/appeng/core/AEConfig.java @@ -1,6 +1,6 @@ /* * This file is part of Applied Energistics 2. - * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved. + * Copyright (c) 2013 - 2015, AlgorithmX2, All rights reserved. * * Applied Energistics 2 is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by diff --git a/src/main/java/appeng/core/RecipeLoader.java b/src/main/java/appeng/core/RecipeLoader.java new file mode 100644 index 000000000..cfed19b72 --- /dev/null +++ b/src/main/java/appeng/core/RecipeLoader.java @@ -0,0 +1,99 @@ +/* + * This file is part of Applied Energistics 2. + * Copyright (c) 2013 - 2015, AlgorithmX2, All rights reserved. + * + * Applied Energistics 2 is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Applied Energistics 2 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Applied Energistics 2. If not, see . + */ + +package appeng.core; + + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import javax.annotation.Nonnull; + +import com.google.common.base.Preconditions; + +import org.apache.commons.io.FileUtils; + +import appeng.api.recipes.IRecipeHandler; +import appeng.recipes.loader.ConfigLoader; +import appeng.recipes.loader.JarLoader; +import appeng.recipes.loader.RecipeResourceCopier; + + +/** + * handles the decision if recipes should be loaded from jar, loaded from file system or force copied from jar + * + * @author thatsIch + * @version rv3 - 12.05.2015 + * @since rv3 12.05.2015 + */ +public class RecipeLoader implements Runnable +{ + private final IRecipeHandler handler; + + /** + * @param handler handler to load the recipes + * + * @throws NullPointerException if handler is null + */ + public RecipeLoader( @Nonnull IRecipeHandler handler ) + { + Preconditions.checkNotNull( handler ); + + this.handler = handler; + } + + @Override + public void run() + { + // setup copying + final RecipeResourceCopier copier = new RecipeResourceCopier( "assets/appliedenergistics2/recipes/" ); + final File configDirectory = AppEng.instance().getConfigDirectory(); + final File generatedRecipesDir = new File( configDirectory, "generated-recipes" ); + final File userRecipesDir = new File( configDirectory, "user-recipes" ); + final File readmeGenDest = new File( generatedRecipesDir, "README.html" ); + final File readmeUserDest = new File( userRecipesDir, "README.html" ); + + // generates generated and user recipes dir + // will clean the generated every time to keep it up to date + // copies over the recipes in the jar over to the generated folder + // copies over the readmes + try + { + FileUtils.forceMkdir( generatedRecipesDir ); + FileUtils.forceMkdir( userRecipesDir ); + FileUtils.cleanDirectory( generatedRecipesDir ); + + copier.copyTo( generatedRecipesDir ); + FileUtils.copyFile( readmeGenDest, readmeUserDest ); + + // parse recipes prioritising the user scripts by using the generated as template + this.handler.parseRecipes( new ConfigLoader( generatedRecipesDir, userRecipesDir ), "index.recipe" ); + } + // on failure use jar parsing + catch( IOException e ) + { + AELog.error( e ); + this.handler.parseRecipes( new JarLoader( "/assets/appliedenergistics2/recipes/" ), "index.recipe" ); + } + catch( URISyntaxException e ) + { + AELog.error( e ); + this.handler.parseRecipes( new JarLoader( "/assets/appliedenergistics2/recipes/" ), "index.recipe" ); + } + } +} diff --git a/src/main/java/appeng/core/Registration.java b/src/main/java/appeng/core/Registration.java index 2a5ae5ce7..94468ffac 100644 --- a/src/main/java/appeng/core/Registration.java +++ b/src/main/java/appeng/core/Registration.java @@ -108,8 +108,6 @@ import appeng.recipes.handlers.Pulverizer; import appeng.recipes.handlers.Shaped; import appeng.recipes.handlers.Shapeless; import appeng.recipes.handlers.Smelt; -import appeng.recipes.loader.ConfigLoader; -import appeng.recipes.loader.JarLoader; import appeng.recipes.ores.OreDictionaryHandler; import appeng.spatial.BiomeGenStorage; import appeng.spatial.StorageWorldProvider; @@ -516,14 +514,8 @@ public final class Registration // Perform ore camouflage! ItemMultiMaterial.instance.makeUnique(); - if( AEConfig.instance.isFeatureEnabled( AEFeature.CustomRecipes ) ) - { - this.recipeHandler.parseRecipes( new ConfigLoader( AppEng.instance().getConfigDirectory() ), "index.recipe" ); - } - else - { - this.recipeHandler.parseRecipes( new JarLoader( "/assets/appliedenergistics2/recipes/" ), "index.recipe" ); - } + final Runnable recipeLoader = new RecipeLoader( this.recipeHandler ); + recipeLoader.run(); partHelper.registerNewLayer( "appeng.parts.layers.LayerISidedInventory", "net.minecraft.inventory.ISidedInventory" ); partHelper.registerNewLayer( "appeng.parts.layers.LayerIFluidHandler", "net.minecraftforge.fluids.IFluidHandler" ); diff --git a/src/main/java/appeng/core/features/AEFeature.java b/src/main/java/appeng/core/features/AEFeature.java index 06ec9ae38..95c81b57e 100644 --- a/src/main/java/appeng/core/features/AEFeature.java +++ b/src/main/java/appeng/core/features/AEFeature.java @@ -1,6 +1,6 @@ /* * This file is part of Applied Energistics 2. - * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved. + * Copyright (c) 2013 - 2015, AlgorithmX2, All rights reserved. * * Applied Energistics 2 is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -63,7 +63,7 @@ public enum AEFeature UnsupportedDeveloperTools( "Misc", false ), Creative( "Misc" ), - GrinderLogging( "Misc", false ), Logging( "Misc" ), IntegrationLogging( "Misc", false ), CustomRecipes( "Crafting", false ), WebsiteRecipes( "Misc", false ), + GrinderLogging( "Misc", false ), Logging( "Misc" ), IntegrationLogging( "Misc", false ), WebsiteRecipes( "Misc", false ), enableFacadeCrafting( "Crafting" ), inWorldSingularity( "Crafting" ), inWorldFluix( "Crafting" ), inWorldPurification( "Crafting" ), UpdateLogging( "Misc", false ), diff --git a/src/main/java/appeng/recipes/loader/ConfigLoader.java b/src/main/java/appeng/recipes/loader/ConfigLoader.java index 0fb2aa369..ecb83fcec 100644 --- a/src/main/java/appeng/recipes/loader/ConfigLoader.java +++ b/src/main/java/appeng/recipes/loader/ConfigLoader.java @@ -1,6 +1,6 @@ /* * This file is part of Applied Energistics 2. - * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved. + * Copyright (c) 2013 - 2015, AlgorithmX2, All rights reserved. * * Applied Energistics 2 is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -23,24 +23,35 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; +import javax.annotation.Nonnull; + +import com.google.common.base.Preconditions; import appeng.api.recipes.IRecipeLoader; public final class ConfigLoader implements IRecipeLoader { - private final File rootDirectory; + private final File generatedRecipesDir; + private final File userRecipesDir; - public ConfigLoader( File rootDirectory ) + public ConfigLoader( File generatedRecipesDir, File userRecipesDir ) { - this.rootDirectory = rootDirectory; + this.generatedRecipesDir = generatedRecipesDir; + this.userRecipesDir = userRecipesDir; } @Override - public BufferedReader getFile( String s ) throws Exception + public BufferedReader getFile( @Nonnull String relativeFilePath ) throws Exception { - final File f = new File( this.rootDirectory, s ); + Preconditions.checkNotNull( relativeFilePath ); + Preconditions.checkArgument( !relativeFilePath.isEmpty(), "Supplying an empty String will result creating a reader of a folder." ); - return new BufferedReader( new InputStreamReader( new FileInputStream( f ), "UTF-8" ) ); + final File generatedFile = new File( this.generatedRecipesDir, relativeFilePath ); + final File userFile = new File( this.userRecipesDir, relativeFilePath ); + + final File toBeLoaded = ( userFile.exists() && userFile.isFile() ) ? userFile : generatedFile; + + return new BufferedReader( new InputStreamReader( new FileInputStream( toBeLoaded ), "UTF-8" ) ); } } diff --git a/src/main/java/appeng/recipes/loader/JarLoader.java b/src/main/java/appeng/recipes/loader/JarLoader.java index 9a9bcafe1..009577992 100644 --- a/src/main/java/appeng/recipes/loader/JarLoader.java +++ b/src/main/java/appeng/recipes/loader/JarLoader.java @@ -1,6 +1,6 @@ /* * This file is part of Applied Energistics 2. - * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved. + * Copyright (c) 2013 - 2015, AlgorithmX2, All rights reserved. * * Applied Energistics 2 is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -21,6 +21,9 @@ package appeng.recipes.loader; import java.io.BufferedReader; import java.io.InputStreamReader; +import javax.annotation.Nonnull; + +import com.google.common.base.Preconditions; import appeng.api.recipes.IRecipeLoader; @@ -36,8 +39,11 @@ public class JarLoader implements IRecipeLoader } @Override - public BufferedReader getFile( String s ) throws Exception + public BufferedReader getFile( @Nonnull String s ) throws Exception { + Preconditions.checkNotNull( s ); + Preconditions.checkArgument( !s.isEmpty() ); + return new BufferedReader( new InputStreamReader( this.getClass().getResourceAsStream( this.rootPath + s ), "UTF-8" ) ); } } diff --git a/src/main/java/appeng/recipes/loader/RecipeResourceCopier.java b/src/main/java/appeng/recipes/loader/RecipeResourceCopier.java new file mode 100644 index 000000000..00686f85e --- /dev/null +++ b/src/main/java/appeng/recipes/loader/RecipeResourceCopier.java @@ -0,0 +1,181 @@ +/* + * This file is part of Applied Energistics 2. + * Copyright (c) 2013 - 2015, AlgorithmX2, All rights reserved. + * + * Applied Energistics 2 is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Applied Energistics 2 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Applied Energistics 2. If not, see . + */ + +package appeng.recipes.loader; + + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; + +import com.google.common.base.Preconditions; + +import org.apache.commons.io.FileUtils; + + +/** + * copies recipes in jars onto file system + * includes the readme, + * needs to be modified if other files needs to be handled + * + * @author thatsIch + * @version rv3 - 11.05.2015 + * @since rv3 11.05.2015 + */ +public class RecipeResourceCopier +{ + /** + * Most expected size of recipes found + */ + private static final int INITIAL_RESOURCE_CAPACITY = 20; + private static final Pattern DOT_COMPILE_PATTERN = Pattern.compile( ".", Pattern.LITERAL ); + + /** + * copy source in the jar + */ + private final String root; + + /** + * @param root source root folder of the recipes inside the jar. + * + * @throws NullPointerException if root is null + */ + public RecipeResourceCopier( @Nonnull String root ) + { + Preconditions.checkNotNull( root ); + + this.root = root; + } + + /** + * copies recipes found in the root to destination. + * + * @param destination destination folder to which the recipes are copied to + * + * @throws URISyntaxException {@see #getResourceListing} + * @throws IOException {@see #getResourceListing} and if copying the detected resource to file is not possible + * @throws NullPointerException if either parameter is null + * @throws IllegalArgumentException if destination is not a directory + */ + public void copyTo( @Nonnull File destination ) throws URISyntaxException, IOException + { + Preconditions.checkNotNull( destination ); + Preconditions.checkArgument( destination.isDirectory() ); + + final String[] listing = this.getResourceListing( this.getClass(), this.root ); + for( String list : listing ) + { + if( list.endsWith( ".recipe" ) || list.endsWith( ".html" ) ) + { + final InputStream inStream = this.getClass().getResourceAsStream( '/' + this.root + list ); + final File outFile = new File( destination, list ); + if( !outFile.exists() ) + { + if( inStream != null ) + { + FileUtils.copyInputStreamToFile( inStream, outFile ); + inStream.close(); + } + } + } + } + } + + /** + * List directory contents for a resource folder. Not recursive. + * This is basically a brute-force implementation. + * Works for regular files and also JARs. + * + * @param clazz Any java class that lives in the same place as the resources you want. + * @param path Should end with "/", but not start with one. + * + * @return Just the name of each member item, not the full paths. + * + * @throws URISyntaxException if it is a file path and the URL can not be converted to URI + * @throws IOException if jar path can not be decoded + * @throws UnsupportedOperationException if it is neither in jar nor in file path + */ + @Nonnull + private String[] getResourceListing( Class clazz, String path ) throws URISyntaxException, IOException + { + assert clazz != null; + assert path != null; + + URL dirURL = clazz.getClassLoader().getResource( path ); + if( dirURL != null && dirURL.getProtocol().equals( "file" ) ) + { + // A file path: easy enough + return new File( dirURL.toURI() ).list(); + } + + if( dirURL == null ) + { + /* + * In case of a jar file, we can't actually find a directory. + * Have to assume the same jar as clazz. + */ + final String me = DOT_COMPILE_PATTERN.matcher( clazz.getName() ).replaceAll( "/" ) + ".class"; + dirURL = clazz.getClassLoader().getResource( me ); + } + + if( dirURL != null && dirURL.getProtocol().equals( "jar" ) ) + { + /* A JAR path */ + final String jarPath = dirURL.getPath().substring( 5, dirURL.getPath().indexOf( '!' ) ); //strip out only the JAR file + final JarFile jar = new JarFile( URLDecoder.decode( jarPath, "UTF-8" ) ); + try + { + final Enumeration entries = jar.entries(); //gives ALL entries in jar + final Collection result = new HashSet( INITIAL_RESOURCE_CAPACITY ); //avoid duplicates in case it is a subdirectory + while( entries.hasMoreElements() ) + { + final String name = entries.nextElement().getName(); + if( name.startsWith( path ) ) + { //filter according to the path + String entry = name.substring( path.length() ); + final int checkSubDir = entry.indexOf( '/' ); + if( checkSubDir >= 0 ) + { + // if it is a subdirectory, we just return the directory name + entry = entry.substring( 0, checkSubDir ); + } + result.add( entry ); + } + } + + return result.toArray( new String[result.size()] ); + } + finally + { + jar.close(); + } + } + + throw new UnsupportedOperationException( "Cannot list files for URL " + dirURL ); + } +} diff --git a/src/main/resources/assets/appliedenergistics2/recipes/README.html b/src/main/resources/assets/appliedenergistics2/recipes/README.html new file mode 100644 index 000000000..6266574c3 --- /dev/null +++ b/src/main/resources/assets/appliedenergistics2/recipes/README.html @@ -0,0 +1,347 @@ + + + + + Custom Recipes + + + + +
+ + +
+ + +

Introduction

+

This file is the README of Applied Energistics 2 for Custom Recipes. Applied Energistics 2 is extremely configurable and supports 100% customize-able + recipes if desired. This page will direct you on how to get started. The entry file for the recipe system is index.recipe.

+ +

Auto-Generation

+

AE2 will generate 2 folders in your AE2 configuration folder config/AppliedEneristics2/. One is generated-recipes/ and the other is user-recipes/. Recipes will be automatically exported from AE2 into generated-recipes/ on every start-up containing always the latest valid recipes.

+
+ You require file system permissions to generate the folders. Recipes will be loaded directly from AE2 if user has no permissions. +
+ + +

Recipe Modification

+

You can modify the recipes by copying over the recipes from generated-recipes/ into user-recipes/. You do not need to copy over all files, but the ones you will be changing. AE2 will prefer loading a user recipe if one would replace a generated recipe.

+

That way changed recipes by us are not overwriting your modified files.

+ +

Item References

+

In Minecraft each item is referenced by a namespace and a name, for example all of minecrafts items use the namespace minecraft. A glass block would be minecraft:glass.

+

The recipe system also exposes access to the oredictionary via a namespace, so you can use oredictionary:glass to use any type of glass.

+ +

Recipe Types

+

Different recipe types are there to interface with specific machines or mods. Use a , to add a new row to the recipe.

+ +

Shapeless

+
    +
  • crafted in a minecraft crafting table
  • +
  • can take any shape as long as the inputs are correct
  • +
  • takes up to 9 items as input
  • +
  • outputs a single item as output, optionally with quantity
  • +
  • Example: Log -> Planks recipe
  • +
  • + AE2:
    + shapeless = minecraft:log -> 4 minecraft:planks +
  • +
+ +

Shaped

+
    +
  • crafted in a minecraft crafting table
  • +
  • requires the specific shape
  • +
  • takes up to 9 items as input
  • +
  • outputs a single item as output, optionally with quantity
  • +
  • Example: 8 Cobblestone -> Furnace recipe
  • +
  • + AE2: +
    shaped =
    +    minecraft:cobblestone minecraft:cobblestone minecraft:cobblestone,
    +    minecraft:cobblestone _                     minecraft:cobblestone,
    +    minecraft:cobblestone minecraft:cobblestone minecraft:cobblestone
    +    -> minecraft:furnace
    +
  • +
+ +

Smelt

+
    +
  • items to be burned in furnaces
  • +
  • takes 1 item as input
  • +
  • takes 1 item as output, optionally with quantity
  • +
  • Example: Log -> Charcoal recipe
  • +
  • + AE2:
    + smelt = minecraft:log -> minecraft:coal:1 +
  • +
+ +

Grind

+
    +
  • items to be grinded in the grindstone
  • +
  • takes 1 item as input
  • +
  • takes 1 item as output, optionally with quantity
  • +
  • Example: Gravel -> Flint recipe
  • +
  • + AE2:
    + grind = minecraft:gravel -> minecraft:flint +
  • +
+ +

Grindfz

+

Same as grind for Factorization Grinder

+ +

Mekcrusher

+

Same as grind for Mekanism Crusher

+ +

Mekechamber

+

Same as grind for Mekanism Enrichment Chamber

+ +

Hccrusher

+

Same as grind for HydrauliCraft Crusher

+ +

Crusher

+

Same as grind for RailCraft Crusher

+ +

Macerator

+

Same as grind for IndustrialCraft Macerator

+ +

Pulverizer

+

Same as grind for Thermal Expansion Pulverizer

+ +

Inscribe

+
    +
  • used in the AE2 Inscriber
  • +
  • takes 2 or 3 items as input, first item is the center item
  • +
  • takes 1 item as output, optionally with quantity
  • +
  • center item is consumed
  • +
  • + Example: duplicate logic processor plate
    + Iron Block + Logic Processor Plate -> Logic Processor Plate +
  • +
  • + AE2: +
    inscribe =
    +    minecraft:iron_block
    +    appliedenergistics2:ItemMaterial.LogicProcessorPress
    +    -> appliedenergistics2:ItemMaterial.LogicProcessorPress
    +
  • +
+ +

Press

+
    +
  • used in the AE2 Inscriber
  • +
  • takes 2 or 3 items as input, first item is the center item
  • +
  • takes 1 item as output, optionally with quantity
  • +
  • all items are consumed
  • +
  • + Example: create logic processor
    + Redstone + logic processor print + silicon print -> logic processor +
  • +
  • + AE2: +
    press =
    +    minecraft:redstone
    +    appliedenergistics2:ItemMaterial.LogicProcessorPrint
    +    appliedenergistics2:ItemMaterial.SiliconPrint
    +    -> appliedenergistics2:ItemMaterial.LogicProcessor
    +
  • +
+ +

Recipe Function

+

Adds convenience for easier recipe management.

+ +

Alias

+
    +
  • creates a shorthand for a longer value
  • +
  • + AE2: alias appliedenergistics2 to ae2
    + alias = ae2 -> appliedenergistics2 +
  • +
  • appliedenergistics2:ItemMaterial.LogicProcessorPrint could be aliased to ae2:ItemMaterial.LogicProcessorPrint
  • +
+ +

Ore

+
    +
  • lets you add items into an ore dictionary value.
  • +
  • + AE2: add minecraft wool to OreDictionary as blockWool
    + ore = minecraft:wool:* -> blockWool +
  • +
  • oredictioned items can be accessed as oredictionary:blockWool for example
  • +
+ +

Group

+
    +
  • lets you create a item group for 1 or more inputs
  • +
  • + Example: define a common group for both different AE2 interfaces
    + Block Interface + Part Interface = Interface +
  • +
  • + AE2:
    + group = ae2:BlockInterface ae2:ItemPart.Interface -> interface +
  • +
  • + This also enables shortening annoying names for example
    + group = ae2:ToolNetherQuartzWrench -> wrench +
  • +
+ +

Import

+
    +
  • lets you load an additional recipe file
  • +
  • + AE2: import all recipes for stairs
    + import = stairs.recipe +
  • +
  • Any knowledge through alias, ore, group are carried over to the imported file
  • +
+ + + + + + + +
+
+ + \ No newline at end of file