diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b26060e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+#IDE
+.idea/
+*.iml
+/.settings
+/bin
+/.classpath
+/.project
+
+#Gradle
+build/
+.gradle
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..7d65bf1
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,22 @@
+apply plugin: 'java'
+
+sourceCompatibility = '1.8'
+targetCompatibility = '1.8'
+compileJava.options.encoding = 'UTF-8'
+version = '1.0'
+
+repositories {
+	mavenCentral()
+}
+
+dependencies {
+	compile 'commons-io:commons-io:2.4'
+	compile 'net.sf.jopt-simple:jopt-simple:4.5'
+}
+
+jar {
+    from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
+	manifest {
+        attributes("Main-Class": "org.ultramine.bootstrap.Main")
+    }
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..3c7abdf
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..eb67457
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Apr 24 12:13:23 VLAT 2014
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=http\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..91a7e26
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+    echo "$*"
+}
+
+die ( ) {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=$((i+1))
+    done
+    case $i in
+        (0) set -- ;;
+        (1) set -- "$args0" ;;
+        (2) set -- "$args0" "$args1" ;;
+        (3) set -- "$args0" "$args1" "$args2" ;;
+        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+    JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..5390861
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'ultramine_bootstrap'
+
diff --git a/src/main/java/org/ultramine/bootstrap/Constants.java b/src/main/java/org/ultramine/bootstrap/Constants.java
new file mode 100644
index 0000000..fb0da10
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/Constants.java
@@ -0,0 +1,8 @@
+package org.ultramine.bootstrap;
+
+public class Constants
+{
+	public static final String UM_REPO = "http://maven.ultramine.ru";
+	public static final String UM_CORE_GROUP = "org.ultramine.core";
+	public static final String UM_CORE_NAME = "ultramine_core-1.7.10-server";
+}
diff --git a/src/main/java/org/ultramine/bootstrap/Main.java b/src/main/java/org/ultramine/bootstrap/Main.java
new file mode 100644
index 0000000..a0d69b3
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/Main.java
@@ -0,0 +1,135 @@
+package org.ultramine.bootstrap;
+
+import joptsimple.OptionParser;
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
+import org.ultramine.bootstrap.maven.MavenDependency;
+import org.ultramine.bootstrap.exceptions.ApplicationErrorException;
+import org.ultramine.bootstrap.task.DependencyResolver;
+import org.ultramine.bootstrap.task.ScriptCreator;
+import org.ultramine.bootstrap.util.I18n;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Locale;
+import java.util.Set;
+
+public class Main
+{
+	public static void main(String[] args)
+	{
+		System.out.println(I18n.tlt("stage.start"));
+		OptionParser opt = new OptionParser();
+		opt.accepts("help", "Show the help").forHelp();
+//		opt.acceptsAll(Arrays.asList("v", "minecraft"), "Version of minecraft (1.7.10)")
+//				.withRequiredArg()
+//				.ofType(String.class)
+//				.defaultsTo("1.7.10")
+//				.describedAs("version");
+		OptionSpec optLang = opt.accepts("lang", "Language")
+				.withRequiredArg()
+				.ofType(String.class)
+				.defaultsTo(Locale.getDefault().getLanguage() + "_" + Locale.getDefault().getCountry())
+				.describedAs("lang");
+		opt.accepts("stacktrace", "Print detailed stacktrace on application error");
+		OptionSpec optDir = opt.accepts("dir", "Server directory")
+				.withRequiredArg()
+				.ofType(File.class)
+				.defaultsTo(new File("."))
+				.describedAs("file");
+//		opt.accepts("addliburl", "Add extra libs by URL")
+//				.withRequiredArg()
+//				.ofType(URL.class)
+//				.describedAs("lib url");
+		OptionSpec optAddLib = opt.accepts("addlib", "Add extra libs from maven repo (gradle format)")
+				.withRequiredArg()
+				.ofType(String.class)
+				.describedAs("maven lib");
+		OptionSpec optAddRepo = opt.accepts("addrepo", "Add extra maven repository URL")
+				.withRequiredArg()
+				.ofType(URL.class)
+				.describedAs("repo URL");
+		opt.accepts("install", "Install ultramine server");
+		opt.accepts("update", "Update ultramine server");
+		opt.accepts("forge", "Make forge-compatible directory mappings");
+		opt.accepts("beta", "Use beta channel");
+		OptionSpec optVersion = opt.accepts("version", "ultramine core version")
+				.withRequiredArg()
+				.ofType(String.class)
+				.describedAs("version");
+
+		OptionSpec optXMX = opt.accepts("xmx", "Max heap size for server (-Xmx)")
+				.withRequiredArg()
+				.ofType(String.class)
+				.defaultsTo("2048m")
+				.describedAs("max memory");
+		OptionSpec optXMS = opt.accepts("xms", "Start heap size for server (-Xms)")
+				.withRequiredArg()
+				.ofType(String.class)
+				.defaultsTo("2048m")
+				.describedAs("start memory");
+
+		OptionSpec optTerminal = opt.accepts("terminal", "Terminal (console) mode")
+				.withRequiredArg()
+				.ofType(String.class)
+				.defaultsTo("jline")
+				.describedAs("jline/ansi/default/raw");
+
+		OptionSet options;
+
+		try {
+			options = opt.parse(args);
+		} catch (joptsimple.OptionException e) {
+			throw new RuntimeException("Fail to parse command line", e);
+		}
+
+		I18n.select(optLang.value(options));
+
+		if (options.has("help")) {
+			try {
+				opt.printHelpOn(System.out);
+				System.exit(0);
+			} catch (IOException e) {
+				throw new RuntimeException("Fail print help", e);
+			}
+		}
+
+		try
+		{
+			File dir = optDir.value(options);
+			Set deps = DependencyResolver.load(
+					dir,
+					optVersion.value(options),
+					options.has("install") || options.has("update"),
+					options.has("beta"),
+					optAddRepo.values(options),
+					optAddLib.values(options)
+			);
+			ScriptCreator.create(
+					dir,
+					deps,
+					optXMX.value(options),
+					optXMS.value(options),
+					optTerminal.value(options),
+					options.has("forge")
+			);
+			System.out.println(I18n.tlt("stage.finished"));
+		}
+		catch(ApplicationErrorException e)
+		{
+			System.err.println(e.getTranslatedMessage());
+			if(options.has("stacktrace"))
+				e.printStackTrace();
+			else
+				System.err.println(I18n.tlt("hint.stacktrace"));
+			System.exit(1);
+		}
+		catch(RuntimeException e)
+		{
+			e.printStackTrace();
+		}
+
+		System.exit(0);
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/UMCoreVersionsRetriever.java b/src/main/java/org/ultramine/bootstrap/UMCoreVersionsRetriever.java
new file mode 100644
index 0000000..4f44a30
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/UMCoreVersionsRetriever.java
@@ -0,0 +1,33 @@
+package org.ultramine.bootstrap;
+
+import org.ultramine.bootstrap.maven.MavenMetadata;
+import org.ultramine.bootstrap.versioning.DefaultArtifactVersion;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class UMCoreVersionsRetriever
+{
+	private final List betaVersions;
+	private final List stableVersions;
+
+	public UMCoreVersionsRetriever()
+	{
+		List versions = MavenMetadata.loadFromProject(Constants.UM_REPO, Constants.UM_CORE_GROUP, Constants.UM_CORE_NAME).getVersions();
+
+		betaVersions = new ArrayList<>(versions.size());
+		stableVersions = new ArrayList<>(versions.size());
+		for(DefaultArtifactVersion version : versions)
+			(version.getLabel().contains("beta") ? betaVersions : stableVersions).add(version);
+	}
+
+	public List getBetaVersions()
+	{
+		return betaVersions;
+	}
+
+	public List getStableVersions()
+	{
+		return stableVersions;
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/deps/AbstractDownloadable.java b/src/main/java/org/ultramine/bootstrap/deps/AbstractDownloadable.java
new file mode 100644
index 0000000..a08aaf5
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/deps/AbstractDownloadable.java
@@ -0,0 +1,65 @@
+package org.ultramine.bootstrap.deps;
+
+import org.apache.commons.io.IOUtils;
+import org.ultramine.bootstrap.deps.IDownloadable;
+import org.ultramine.bootstrap.util.HashUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+
+public abstract class AbstractDownloadable implements IDownloadable
+{
+	protected File outputDir;
+	protected File checkSumsDir;
+
+	public void setOutputDir(File outputDir)
+	{
+		this.outputDir = outputDir;
+	}
+
+	public File getOutputDir()
+	{
+		return this.outputDir;
+	}
+
+	public void setCheckSumsDir(File checkSumsDir)
+	{
+		this.checkSumsDir = checkSumsDir;
+	}
+
+	protected static String copyAndDigest(InputStream in, OutputStream out) throws IOException
+	{
+		MessageDigest digest = HashUtil.getSHA1();
+		byte[] buffer = new byte[0x10000];
+		try
+		{
+			for(int read = in.read(buffer); read > 0; read = in.read(buffer))
+			{
+				digest.update(buffer, 0, read);
+				out.write(buffer, 0, read);
+			}
+		}
+		finally
+		{
+			IOUtils.closeQuietly(in);
+			IOUtils.closeQuietly(out);
+		}
+
+		return HashUtil.byteArray2Hex(digest.digest());
+	}
+
+	protected static void ensureFileWritable(File target)
+	{
+		if(target.getParentFile() != null && !target.getParentFile().isDirectory())
+		{
+			if(!target.getParentFile().mkdirs() && !target.getParentFile().isDirectory())
+				throw new RuntimeException("Could not create directory " + target.getParentFile());
+		}
+
+		if(target.isFile() && !target.canWrite())
+			throw new RuntimeException("Do not have write permissions for " + target + " - aborting!");
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/deps/IDependency.java b/src/main/java/org/ultramine/bootstrap/deps/IDependency.java
new file mode 100644
index 0000000..fa6c035
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/deps/IDependency.java
@@ -0,0 +1,5 @@
+package org.ultramine.bootstrap.deps;
+
+public interface IDependency
+{
+}
diff --git a/src/main/java/org/ultramine/bootstrap/deps/IDownloadable.java b/src/main/java/org/ultramine/bootstrap/deps/IDownloadable.java
new file mode 100644
index 0000000..6ab6631
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/deps/IDownloadable.java
@@ -0,0 +1,15 @@
+package org.ultramine.bootstrap.deps;
+
+import java.io.File;
+import java.io.IOException;
+
+public interface IDownloadable
+{
+	void setOutputDir(File outputDir);
+
+	File getOutputDir();
+
+	void setCheckSumsDir(File checkSumsDir);
+
+	void download() throws IOException;
+}
diff --git a/src/main/java/org/ultramine/bootstrap/deps/IRepository.java b/src/main/java/org/ultramine/bootstrap/deps/IRepository.java
new file mode 100644
index 0000000..ee3abb0
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/deps/IRepository.java
@@ -0,0 +1,24 @@
+package org.ultramine.bootstrap.deps;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface IRepository
+{
+	InputStream resolve(String path) throws IOException;
+
+	default String resolveCheckSum(String path) throws IOException
+	{
+		InputStream inp = resolve(path + ".sha1");
+		if(inp == null)
+			inp = resolve(path + ".sha");
+		if(inp == null)
+			return null;
+		String sum = IOUtils.toString(inp).trim();
+		if(sum.length() > 40)
+			sum = sum.substring(0, 40);
+		return sum;
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/exceptions/ApplicationErrorException.java b/src/main/java/org/ultramine/bootstrap/exceptions/ApplicationErrorException.java
new file mode 100644
index 0000000..51c14a8
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/exceptions/ApplicationErrorException.java
@@ -0,0 +1,24 @@
+package org.ultramine.bootstrap.exceptions;
+
+public class ApplicationErrorException extends TranslatableMessageException
+{
+	public ApplicationErrorException(String format)
+	{
+		super(format);
+	}
+
+	public ApplicationErrorException(String format, Object... args)
+	{
+		super(format, args);
+	}
+
+	public ApplicationErrorException(Throwable cause, String format)
+	{
+		super(cause, format);
+	}
+
+	public ApplicationErrorException(Throwable cause, String format, Object... args)
+	{
+		super(cause, format, args);
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/exceptions/TranslatableMessageException.java b/src/main/java/org/ultramine/bootstrap/exceptions/TranslatableMessageException.java
new file mode 100644
index 0000000..13b4133
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/exceptions/TranslatableMessageException.java
@@ -0,0 +1,40 @@
+package org.ultramine.bootstrap.exceptions;
+
+import org.ultramine.bootstrap.util.I18n;
+
+public class TranslatableMessageException extends RuntimeException
+{
+	private final String format;
+	private Object[] args;
+
+	public TranslatableMessageException(String format)
+	{
+		super(I18n.tlt(format));
+		this.format = format;
+	}
+
+	public TranslatableMessageException(String format, Object... args)
+	{
+		super(I18n.tlt(format, args));
+		this.format = format;
+		this.args = args;
+	}
+
+	public TranslatableMessageException(Throwable cause, String format)
+	{
+		super(I18n.tlt(format), cause);
+		this.format = format;
+	}
+
+	public TranslatableMessageException(Throwable cause, String format, Object... args)
+	{
+		super(I18n.tlt(format, args), cause);
+		this.format = format;
+		this.args = args;
+	}
+
+	public String getTranslatedMessage()
+	{
+		return args == null ? I18n.tlt(format) : I18n.tlt(format, args);
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/maven/MavenDependency.java b/src/main/java/org/ultramine/bootstrap/maven/MavenDependency.java
new file mode 100644
index 0000000..480fec1
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/maven/MavenDependency.java
@@ -0,0 +1,108 @@
+package org.ultramine.bootstrap.maven;
+
+import org.ultramine.bootstrap.deps.IDependency;
+import org.ultramine.bootstrap.deps.IRepository;
+import org.ultramine.bootstrap.deps.IDownloadable;
+
+import java.util.List;
+import java.util.Objects;
+
+public class MavenDependency implements IDependency
+{
+	public final String group;
+	public final String artifactName;
+	public final String version;
+
+	public MavenDependency(String group, String artifactName, String version)
+	{
+		this.group = Objects.requireNonNull(group);
+		this.artifactName = Objects.requireNonNull(artifactName);
+		this.version = Objects.requireNonNull(version);
+	}
+
+	public MavenDependency(String name)
+	{
+		String[] parts = name.split(":", 3);
+		this.group = parts[0];
+		this.artifactName = parts[1];
+		this.version = parts[2];
+	}
+
+	public String getGroup()
+	{
+		return group;
+	}
+
+	public String getArtifactName()
+	{
+		return artifactName;
+	}
+
+	public String getVersion()
+	{
+		return version;
+	}
+
+	public String getArtifactBaseDir()
+	{
+		return group.replace('.', '/') + "/" + artifactName + "/" + version;
+	}
+
+	public String getArtifactFilename(String classifier, String extension)
+	{
+		return artifactName + "-" + version + (classifier == null || classifier.isEmpty() ? "" : "-" + classifier) + "." + extension;
+	}
+
+	public String getArtifactPath()
+	{
+		return getArtifactPath(null);
+	}
+
+	public String getArtifactPath(String classifier)
+	{
+		return getArtifactBaseDir() + "/" + getArtifactFilename(classifier);
+	}
+
+	public String getArtifactPath(String classifier, String extension)
+	{
+		return getArtifactBaseDir() + "/" + getArtifactFilename(classifier, extension);
+	}
+
+	public String getArtifactFilename()
+	{
+		return getArtifactFilename(null);
+	}
+
+	public String getArtifactFilename(String classifier)
+	{
+		return getArtifactFilename(classifier, "jar");
+	}
+
+	public IDownloadable resolve(List repositories)
+	{
+		return new MavenDownloadable(repositories, this);
+	}
+
+	@Override
+	public boolean equals(Object o)
+	{
+		if(this == o) return true;
+		if(o == null || getClass() != o.getClass()) return false;
+		MavenDependency that = (MavenDependency) o;
+		return Objects.equals(group, that.group) &&
+				Objects.equals(artifactName, that.artifactName) &&
+				Objects.equals(version, that.version);
+	}
+
+	@Override
+	public int hashCode()
+	{
+		return Objects.hash(group, artifactName, version);
+	}
+
+	@Override
+	public String toString()
+	{
+		return group+':'+ artifactName +':'+version;
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/maven/MavenDownloadable.java b/src/main/java/org/ultramine/bootstrap/maven/MavenDownloadable.java
new file mode 100644
index 0000000..215a8f0
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/maven/MavenDownloadable.java
@@ -0,0 +1,78 @@
+package org.ultramine.bootstrap.maven;
+
+import org.apache.commons.io.FileUtils;
+import org.ultramine.bootstrap.deps.IRepository;
+import org.ultramine.bootstrap.deps.AbstractDownloadable;
+import org.ultramine.bootstrap.exceptions.ApplicationErrorException;
+import org.ultramine.bootstrap.util.HashUtil;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ConnectException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.util.List;
+
+public class MavenDownloadable extends AbstractDownloadable
+{
+	private final List repositories;
+	private final MavenDependency dependency;
+
+	public MavenDownloadable(List repositories, MavenDependency dependency)
+	{
+		this.repositories = repositories;
+		this.dependency = dependency;
+	}
+
+	@Override
+	public void download() throws IOException
+	{
+		String artifactName = dependency.getArtifactFilename();
+		File output = new File(outputDir, artifactName);
+		File checkSumFile = new File(checkSumsDir, artifactName+".sha1");
+		if(output.isFile() && checkSumFile.isFile())
+		{
+			if(HashUtil.sha1Str(output).equals(FileUtils.readFileToString(checkSumFile)))
+				return;
+		}
+		ensureFileWritable(output);
+		ensureFileWritable(checkSumFile);
+		String path = dependency.getArtifactPath();
+		for(IRepository repo : repositories)
+			if(tryDownload(output, checkSumFile, path, repo))
+				return;
+		throw new FileNotFoundException("Maven dependency not found in any repositories: "+dependency);
+	}
+
+	private boolean tryDownload(File output, File checkSumFile, String path, IRepository repo) throws IOException
+	{
+		try
+		{
+			InputStream resolved = repo.resolve(path);
+			if(resolved == null)
+				return false;
+			System.out.println("Downloading " + dependency.getArtifactName() + " from " + repo + "/" + path);
+			String computedCheckSum = copyAndDigest(resolved, new FileOutputStream(output));
+			String loadedCheckSum = repo.resolveCheckSum(path);
+			if(loadedCheckSum != null && !loadedCheckSum.equals(computedCheckSum))
+				throw new RuntimeException("CheckSums failed for " + dependency + "("+computedCheckSum + " != " + loadedCheckSum + ")");
+			FileUtils.writeStringToFile(checkSumFile, computedCheckSum);
+			return true;
+		} catch (UnknownHostException e)
+		{
+			throw new ApplicationErrorException(e, "error.unavailable.host", e.getMessage());
+		} catch (SocketTimeoutException | ConnectException e)
+		{
+			throw new ApplicationErrorException(e, "error.unavailable.maven", repo.toString());
+		}
+	}
+
+	@Override
+	public String toString()
+	{
+		return dependency.toString();
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/maven/MavenLocalRepository.java b/src/main/java/org/ultramine/bootstrap/maven/MavenLocalRepository.java
new file mode 100644
index 0000000..20f0ba2
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/maven/MavenLocalRepository.java
@@ -0,0 +1,33 @@
+package org.ultramine.bootstrap.maven;
+
+import org.ultramine.bootstrap.deps.IRepository;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class MavenLocalRepository implements IRepository
+{
+	private final File directory;
+
+	public MavenLocalRepository(File directory)
+	{
+		this.directory = directory;
+	}
+
+	@Override
+	public InputStream resolve(String path) throws IOException
+	{
+		File file = new File(directory, path.replace("/", File.separator));
+		if(!file.isFile())
+			return null;
+		return new FileInputStream(file);
+	}
+
+	@Override
+	public String toString()
+	{
+		return directory.getAbsolutePath();
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/maven/MavenMetadata.java b/src/main/java/org/ultramine/bootstrap/maven/MavenMetadata.java
new file mode 100644
index 0000000..ffb590e
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/maven/MavenMetadata.java
@@ -0,0 +1,58 @@
+package org.ultramine.bootstrap.maven;
+
+import org.ultramine.bootstrap.exceptions.ApplicationErrorException;
+import org.ultramine.bootstrap.versioning.DefaultArtifactVersion;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class MavenMetadata
+{
+	private final List versions;
+
+	private MavenMetadata(String xmlUrl)
+	{
+		Document doc;
+		try
+		{
+			doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(xmlUrl);
+		}
+		catch (Exception e)
+		{
+			throw new ApplicationErrorException(e, "error.mavenmetadata", xmlUrl);
+		}
+		NodeList listNodes = doc.getElementsByTagName("version");
+
+		versions = new ArrayList<>(listNodes.getLength());
+		for(int i = 0, s = listNodes.getLength(); i < s; i++)
+		{
+			String version = listNodes.item(i).getTextContent();
+			versions.add(new DefaultArtifactVersion(version, version));
+		}
+		Collections.sort(versions);
+	}
+
+	public List getVersions()
+	{
+		return versions;
+	}
+
+	public static MavenMetadata loadFromXML(String xmlUrl)
+	{
+		return new MavenMetadata(xmlUrl);
+	}
+
+	public static MavenMetadata loadFromProject(String projectUrl)
+	{
+		return loadFromXML(projectUrl + (projectUrl.endsWith("/") ? "" : "/") + "maven-metadata.xml");
+	}
+
+	public static MavenMetadata loadFromProject(String repoUrl, String group, String project)
+	{
+		return loadFromProject(repoUrl + "/" + group.replace('.', '/') + "/" + project);
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/maven/MavenRemoteRepository.java b/src/main/java/org/ultramine/bootstrap/maven/MavenRemoteRepository.java
new file mode 100644
index 0000000..f04f602
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/maven/MavenRemoteRepository.java
@@ -0,0 +1,52 @@
+package org.ultramine.bootstrap.maven;
+
+import org.ultramine.bootstrap.deps.IRepository;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class MavenRemoteRepository implements IRepository
+{
+	private final String url;
+
+	public MavenRemoteRepository(String url)
+	{
+		this.url = url;
+	}
+
+	public String getURL()
+	{
+		return url;
+	}
+
+	@Override
+	public InputStream resolve(String path) throws IOException
+	{
+		HttpURLConnection conn = makeConnection(new URL(getURL() + "/" + path));
+		int status = conn.getResponseCode();
+		if(status / 100 != 2)
+			return null;
+		return conn.getInputStream();
+	}
+
+	protected HttpURLConnection makeConnection(URL url) throws IOException
+	{
+		HttpURLConnection connection = (HttpURLConnection)url.openConnection();
+		connection.setUseCaches(false);
+		connection.setDefaultUseCaches(false);
+		connection.setRequestProperty("Cache-Control", "no-store,max-age=0,no-cache");
+		connection.setRequestProperty("Expires", "0");
+		connection.setRequestProperty("Pragma", "no-cache");
+		connection.setConnectTimeout(5000);
+		connection.setReadTimeout(30000);
+		return connection;
+	}
+
+	@Override
+	public String toString()
+	{
+		return url;
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/maven/ProjectObjectModel.java b/src/main/java/org/ultramine/bootstrap/maven/ProjectObjectModel.java
new file mode 100644
index 0000000..b9cc9ea
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/maven/ProjectObjectModel.java
@@ -0,0 +1,69 @@
+package org.ultramine.bootstrap.maven;
+
+import org.ultramine.bootstrap.exceptions.ApplicationErrorException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ProjectObjectModel
+{
+	private final List runtimeDependencies;
+
+	private ProjectObjectModel(String pomUrl)
+	{
+		Document doc;
+		try
+		{
+			doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(pomUrl);
+		}
+		catch (Exception e)
+		{
+			throw new ApplicationErrorException(e, "error.pom", pomUrl);
+		}
+		NodeList listNodes = doc.getElementsByTagName("dependency");
+
+		runtimeDependencies = new ArrayList<>(listNodes.getLength());
+		for(int i = 0, s = listNodes.getLength(); i < s; i++)
+		{
+			NodeList dependency = listNodes.item(i).getChildNodes();
+			String group = null;
+			String artifact = null;
+			String version = null;
+			String scope = null;
+			for(int i1 = 0, s1 = dependency.getLength(); i1 < s1; i1++)
+			{
+				Node nd = dependency.item(i1);
+				String name = nd.getNodeName();
+				String val = nd.getTextContent();
+				switch(name)
+				{
+				case "groupId": group = val; break;
+				case "artifactId": artifact = val; break;
+				case "version": version = val; break;
+				case "scope": scope = val; break;
+				}
+			}
+			if("runtime".equals(scope) || "compile".equals(scope))
+				runtimeDependencies.add(new MavenDependency(group, artifact, version));
+		}
+	}
+
+	public List getRuntimeDependencies()
+	{
+		return runtimeDependencies;
+	}
+
+	public static ProjectObjectModel loadFromXML(String pomUrl)
+	{
+		return new ProjectObjectModel(pomUrl);
+	}
+
+	public static ProjectObjectModel loadFromArtifact(String repoUrl, String group, String project, String version)
+	{
+		return loadFromXML(repoUrl + "/" + group.replace('.', '/') + "/" + project + "/" + version + "/" + project + "-" + version + ".pom");
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/task/DependencyResolver.java b/src/main/java/org/ultramine/bootstrap/task/DependencyResolver.java
new file mode 100644
index 0000000..bbf444a
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/task/DependencyResolver.java
@@ -0,0 +1,164 @@
+package org.ultramine.bootstrap.task;
+
+import org.apache.commons.io.FileUtils;
+import org.ultramine.bootstrap.Constants;
+import org.ultramine.bootstrap.util.I18n;
+import org.ultramine.bootstrap.UMCoreVersionsRetriever;
+import org.ultramine.bootstrap.util.UmSslUtil;
+import org.ultramine.bootstrap.deps.IRepository;
+import org.ultramine.bootstrap.maven.MavenDependency;
+import org.ultramine.bootstrap.maven.MavenLocalRepository;
+import org.ultramine.bootstrap.maven.MavenRemoteRepository;
+import org.ultramine.bootstrap.deps.IDownloadable;
+import org.ultramine.bootstrap.exceptions.ApplicationErrorException;
+import org.ultramine.bootstrap.maven.ProjectObjectModel;
+import org.ultramine.bootstrap.versioning.DefaultArtifactVersion;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class DependencyResolver
+{
+	public static Set load(File dir, String selVersion, boolean update, boolean beta, List extraRepos, List extraLibs)
+	{
+		UmSslUtil.checkOrInstall();
+		File libraryDir = new File(dir, "libraries");
+		File checkSumsDir = new File(libraryDir, "checksums");
+
+		List repositories = new ArrayList<>();
+		addMavenLocalIfExists(repositories, findMavenLocal());
+		addMavenLocalIfExists(repositories, findMinecraftLibs());
+		repositories.addAll(Arrays.asList(
+				new MavenRemoteRepository("https://repo1.maven.org/maven2"),
+				new MavenRemoteRepository("https://oss.sonatype.org/content/repositories/snapshots"),
+				new MavenRemoteRepository("http://files.minecraftforge.net/maven"),
+				new MavenRemoteRepository("https://libraries.minecraft.net"),
+				new MavenRemoteRepository("http"+(UmSslUtil.isUseHttps() ? "s" : "")+"://maven.ultramine.ru")
+		));
+
+		for(URL url : extraRepos)
+			try {
+				repositories.add(url.getProtocol().equals("file") ? new MavenLocalRepository(new File(url.toURI())) : new MavenRemoteRepository(url.toString()));
+			} catch(URISyntaxException e) {
+				throw new ApplicationErrorException(e, "error.addrepo.file", url.toString());
+			}
+
+		System.out.println(I18n.tlt("stage.versions"));
+		String targetVersion = selVersion;
+		if(targetVersion == null)
+		{
+			List versions = null;
+			if(!update)
+				versions = retrieveLocalVersions(dir);
+			if(versions == null || versions.size() == 0)
+			{
+				UMCoreVersionsRetriever retr = new UMCoreVersionsRetriever();
+				versions = beta ? retr.getBetaVersions() : retr.getStableVersions();
+			}
+
+			targetVersion = versions.get(versions.size()-1).getLabel();
+		}
+
+		MavenDependency umCoreDep = new MavenDependency(Constants.UM_CORE_GROUP, Constants.UM_CORE_NAME, targetVersion);
+		File umCoreFile = new File(dir, umCoreDep.getArtifactFilename());
+
+		Set dependencies = new HashSet<>();
+		dependencies.addAll(ProjectObjectModel.loadFromArtifact(Constants.UM_REPO, Constants.UM_CORE_GROUP, Constants.UM_CORE_NAME, targetVersion)
+				.getRuntimeDependencies());
+		dependencies.addAll(extraLibs.stream().map(MavenDependency::new).collect(Collectors.toList()));
+
+
+		List toDownload = dependencies.stream().map(d -> d.resolve(repositories)).collect(Collectors.toList());
+		IDownloadable umCoreDownloadable = umCoreDep.resolve(repositories);
+		umCoreDownloadable.setOutputDir(dir);
+		toDownload.add(umCoreDownloadable);
+		System.out.println(I18n.tlt("stage.downloading"));
+		toDownload.parallelStream().forEach(d -> {
+			try
+			{
+				d.setCheckSumsDir(checkSumsDir);
+				if(d.getOutputDir() == null)
+					d.setOutputDir(libraryDir);
+				d.download();
+			} catch (IOException e) {
+				throw new ApplicationErrorException(e, "error.download.dependency", d.toString(), e.toString());
+			}
+		});
+
+		try
+		{
+			File symlink = new File(dir, Constants.UM_CORE_NAME+"-latest.jar");
+			if(symlink.exists())
+				FileUtils.forceDelete(symlink);
+			try {
+				Files.createSymbolicLink(new File(dir, Constants.UM_CORE_NAME + "-latest.jar").toPath(), umCoreFile.toPath());
+			} catch(IOException e) {
+				FileUtils.copyFile(umCoreFile, symlink);
+			}
+		}
+		catch(IOException e)
+		{
+			throw new ApplicationErrorException(e, "error.write.file", Constants.UM_CORE_NAME+"-latest.jar", e.getMessage());
+		}
+
+		return dependencies;
+	}
+
+	private static List retrieveLocalVersions(File dir)
+	{
+		return Stream.of(dir.list((d, name) -> name.startsWith(Constants.UM_CORE_NAME)))
+				.map(s -> s.substring(Constants.UM_CORE_NAME.length()+1, s.length()-4))
+				.map(s -> new DefaultArtifactVersion(s, s)).sorted().collect(Collectors.toList());
+	}
+
+	private static void addMavenLocalIfExists(List list, File repo)
+	{
+		if(repo != null && repo.exists())
+			list.add(new MavenLocalRepository(repo));
+	}
+
+	private static File findMavenLocal()
+	{
+		String home = System.getProperty("user.home");
+		if(home != null)
+			return new File(home, ".m2"+File.separator+"repository");
+		return null;
+	}
+
+	private static File findMinecraftLibs()
+	{
+		return getFromAppdata(".minecraft"+File.separator+"libraries");
+	}
+
+	private static File getFromAppdata(String name)
+	{
+		String osName = System.getProperty("os.name").toLowerCase();
+		String home = System.getProperty("user.home", ".");
+		File dir;
+		if(osName.contains("linux") || osName.contains("unix"))
+			dir = new File(home, name);
+		else if(osName.contains("win"))
+		{
+			String appdata = System.getenv("APPDATA");
+			if(appdata != null)
+				dir = new File(appdata, name);
+			else
+				dir = new File(home, name);
+		}
+		else if(osName.contains("mac"))
+			dir = new File(home, "Library/Application Support/" + name);
+		else
+			dir = new File(home, name);
+		return dir;
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/task/ScriptCreator.java b/src/main/java/org/ultramine/bootstrap/task/ScriptCreator.java
new file mode 100644
index 0000000..1d0a12c
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/task/ScriptCreator.java
@@ -0,0 +1,139 @@
+package org.ultramine.bootstrap.task;
+
+import org.apache.commons.io.FileUtils;
+import org.ultramine.bootstrap.Constants;
+import org.ultramine.bootstrap.util.I18n;
+import org.ultramine.bootstrap.maven.MavenDependency;
+import org.ultramine.bootstrap.exceptions.ApplicationErrorException;
+import org.ultramine.bootstrap.util.Resources;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.Set;
+
+public class ScriptCreator
+{
+	public static void create(File dir, Set dependencies, String parXmx, String parXms, String parTerminal, boolean forge)
+	{
+		System.out.println(I18n.tlt("stage.scripts"));
+		long xmx = resolveMemory(parXmx);
+		long xms = resolveMemory(parXms);
+		long xmn = Math.min(xmx / 4, 1024L*1024*1024);
+		if(xms > xmx)
+			xms = xmx;
+		if(xmx < 32*1024*1024)
+			throw new ApplicationErrorException("Maximum heap size (-Xmx) is too low: %s", parXmx);
+
+		boolean win = System.getProperty("os.name").toLowerCase().contains("win");
+		try(BufferedWriter writer = new BufferedWriter(new FileWriter(
+				new File(dir, "ultramine_server_run_line."+(win ? "bat" : "sh")))))
+		{
+			if(!win)
+			{
+				writer.write("#!/bin/sh");
+				writer.newLine();
+			}
+			writer.write(Resources.getAsString("/assets/script/java_command_line.txt")
+					.replace("{xms}", formatMemory(xmx))
+					.replace("{xmx}", formatMemory(xms))
+					.replace("{xmn}", formatMemory(xmn))
+
+					.replace("{terminal}", parTerminal)
+
+					.replace("{vanilla_dir}", forge ? "." : "storage")
+					.replace("{worlds_dir}", forge ? "." : "worlds")
+
+					.replace("{classpath}", buildClassPath(dependencies))
+					.replace('\\', win ? '^' : '\\')
+					.replace("#", win ? "::" : "#")
+//					.replace("<", win ? "%=" : "`#")
+//					.replace(">", win ? "=%" : "`")
+			);
+			writer.newLine();
+		}
+		catch(IOException e)
+		{
+			throw new ApplicationErrorException(e, "error.write.file", "ultramine_server_run_line."+(win ? "bat" : "sh"), e.getMessage());
+		}
+
+		try
+		{
+			FileUtils.writeStringToFile(new File(dir, "start."+(win ? "bat" : "sh")),
+					Resources.getAsString("/assets/script/"+(win ? "windows/start.bat" : "shell/start.sh")));
+		}
+		catch(IOException e)
+		{
+			throw new ApplicationErrorException(e, "error.write.file", "start."+(win ? "bat" : "sh"), e.getMessage());
+		}
+
+		if(forge)
+		{
+			try
+			{
+				File serverYml = new File(dir, "settings"+File.separator+"server.yml");
+				if(!serverYml.exists())
+				{
+					String confStr = Resources.getAsString("/assets/server.yml");
+					confStr = confStr.replace("{split-world-dirs}", "false");
+					Properties defs = new Properties();
+					defs.load(Resources.getAsStream("/assets/server.properties"));
+					Properties props = new Properties(defs);
+					File propsFile = new File(dir, "server.properties");
+					if(propsFile.exists())
+						try(FileInputStream inp = new FileInputStream(propsFile)) {
+							props.load(inp);
+						}
+					for(Object key : defs.keySet())
+						confStr = confStr.replace("{"+key+"}", props.getProperty(key.toString()));
+					FileUtils.writeStringToFile(serverYml, confStr);
+				}
+			}
+			catch(IOException e)
+			{
+				throw new ApplicationErrorException(e, "error.write.file", "settings/server.yml", e.getMessage());
+			}
+		}
+	}
+
+	/** @return number in bytes */
+	private static long resolveMemory(String s)
+	{
+		try
+		{
+			return Long.parseLong(s);
+		}
+		catch (NumberFormatException e1)
+		{
+			long val = Long.parseLong(s.substring(0, s.length() - 1));
+			char mod = Character.toLowerCase(s.charAt(s.length()-1));
+			if(mod != 'k' && mod != 'm' && mod != 'g')
+				throw new IllegalArgumentException(s);
+			return val * (mod == 'k' ? 1024 : mod == 'm' ? 1024*1024 : 1024*1024*1024);
+		}
+	}
+
+	private static String formatMemory(long bytes)
+	{
+		return (bytes / (1024*1024)) + "m";
+	}
+
+	private static String buildClassPath(Set dependencies)
+	{
+		StringBuilder sb = new StringBuilder();
+		String separator = System.getProperty("path.separator");
+
+		sb.append(Constants.UM_CORE_NAME+"-latest.jar");
+		for(MavenDependency dep : dependencies)
+		{
+			sb.append(separator);
+			sb.append("libraries/");
+			sb.append(dep.getArtifactFilename());
+		}
+
+		return sb.toString();
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/util/HashUtil.java b/src/main/java/org/ultramine/bootstrap/util/HashUtil.java
new file mode 100644
index 0000000..8a1b966
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/util/HashUtil.java
@@ -0,0 +1,68 @@
+package org.ultramine.bootstrap.util;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class HashUtil
+{
+	private static final char[] CHARS = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
+
+	public static MessageDigest getSHA1()
+	{
+		try
+		{
+			return MessageDigest.getInstance("SHA-1");
+		}
+		catch(NoSuchAlgorithmException e)
+		{
+			throw new RuntimeException("SHA-1 not found", e);
+		}
+	}
+
+	public static String sha1Str(File file)
+	{
+		return byteArray2Hex(calculateHash(getSHA1(), file));
+	}
+
+	public static byte[] calculateHash(MessageDigest alg, File file)
+	{
+		DigestInputStream dis = null;
+		try
+		{
+			dis = new DigestInputStream(new FileInputStream(file), alg);
+
+			byte[] buff = new byte[4096];
+			do {} while(dis.read(buff) != -1);
+
+			return alg.digest();
+		}
+		catch(IOException e)
+		{
+			alg.reset();
+			return new byte[0];
+		}
+		finally
+		{
+			IOUtils.closeQuietly(dis);
+		}
+	}
+
+	public static String byteArray2Hex(byte[] hash)
+	{
+		StringBuilder sb = new StringBuilder(hash.length*2);
+		for(int i = 0; i < hash.length; i++)
+		{
+			byte b = hash[i];
+			sb.append(CHARS[(b & 0xff) >> 4]);
+			sb.append(CHARS[b & 15]);
+		}
+
+		return sb.toString();
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/util/I18n.java b/src/main/java/org/ultramine/bootstrap/util/I18n.java
new file mode 100644
index 0000000..5e73754
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/util/I18n.java
@@ -0,0 +1,46 @@
+package org.ultramine.bootstrap.util;
+
+import org.ultramine.bootstrap.util.Resources;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public class I18n
+{
+	private static final Properties enUs = loadLang("en_US");
+	private static Properties selected = enUs;
+
+	public static void select(String locale)
+	{
+		Properties props = locale.equals("en_US") ? enUs : loadLang(locale);
+		selected = props != null ? props : enUs;
+	}
+
+	private static Properties loadLang(String locale)
+	{
+		InputStream inp = Resources.getAsStream("/assets/lang/"+locale+".lang");
+		if(inp == null)
+			return null;
+		try
+		{
+			Properties props = enUs != null ? new Properties(enUs) : new Properties();
+			props.load(inp);
+			return props;
+		}
+		catch(IOException e)
+		{
+			throw new RuntimeException(e);
+		}
+	}
+
+	public static String tlt(String key)
+	{
+		return selected.getProperty(key, key);
+	}
+
+	public static String tlt(String key, Object... args)
+	{
+		return String.format(tlt(key), args);
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/util/Resources.java b/src/main/java/org/ultramine/bootstrap/util/Resources.java
new file mode 100644
index 0000000..f859002
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/util/Resources.java
@@ -0,0 +1,34 @@
+package org.ultramine.bootstrap.util;
+
+import org.apache.commons.io.Charsets;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class Resources
+{
+	public static InputStream getAsStream(String path)
+	{
+		return Resources.class.getResourceAsStream(path);
+	}
+	
+	public static String getAsString(String path)
+	{
+		InputStream is = getAsStream(path);
+		if(is == null)
+			throw new RuntimeException("Requested resource not found: " + path);
+		try
+		{
+			return IOUtils.toString(is, Charsets.UTF_8);
+		}
+		catch(IOException e)
+		{
+			throw new RuntimeException("Failed to load resource: " + path, e);
+		}
+		finally
+		{
+			IOUtils.closeQuietly(is);
+		}
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/util/UmSslUtil.java b/src/main/java/org/ultramine/bootstrap/util/UmSslUtil.java
new file mode 100644
index 0000000..49ed417
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/util/UmSslUtil.java
@@ -0,0 +1,72 @@
+package org.ultramine.bootstrap.util;
+
+import org.ultramine.bootstrap.util.Resources;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+
+public class UmSslUtil
+{
+	private static boolean useHttps = true;
+
+	public static boolean isUseHttps()
+	{
+		return useHttps;
+	}
+
+	public static void checkOrInstall()
+	{
+		//TODO connection caching issues
+//		try
+//		{
+//			new URL("https://maven.ultramine.ru").openConnection().getInputStream().close();
+//		}
+//		catch (SSLHandshakeException e)
+//		{
+			installDSTRootCA();
+//		}
+//		catch(IOException e)
+//		{
+//			throw new ApplicationErrorException(e, "error.unavailable.maven", "maven.ultramine.ru");
+//		}
+	}
+
+	private static void installDSTRootCA()
+	{
+		try
+		{
+			KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+			Path ksPath = Paths.get(System.getProperty("java.home"),
+					"lib", "security", "cacerts");
+			keyStore.load(Files.newInputStream(ksPath),
+					"changeit".toCharArray());
+
+			CertificateFactory cf = CertificateFactory.getInstance("X.509");
+			try(InputStream caInput = new BufferedInputStream( Resources.getAsStream("/assets/crt/DSTRootCAX3.pem")))
+			{
+				Certificate crt = cf.generateCertificate(caInput);
+				keyStore.setCertificateEntry("DSTRootCAX3", crt);
+			}
+
+			TrustManagerFactory tmf = TrustManagerFactory
+					.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+			tmf.init(keyStore);
+			SSLContext sslContext = SSLContext.getInstance("TLS");
+			sslContext.init(null, tmf.getTrustManagers(), null);
+			SSLContext.setDefault(sslContext);
+		}
+		catch (Throwable e)
+		{
+			useHttps = false;
+		}
+	}
+}
diff --git a/src/main/java/org/ultramine/bootstrap/versioning/ArtifactVersion.java b/src/main/java/org/ultramine/bootstrap/versioning/ArtifactVersion.java
new file mode 100644
index 0000000..7ef90f8
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/versioning/ArtifactVersion.java
@@ -0,0 +1,50 @@
+/*
+ * Forge Mod Loader
+ * Copyright (c) 2012-2013 cpw.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser Public License v2.1
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * 
+ * Contributors:
+ *     cpw - implementation
+ */
+
+package org.ultramine.bootstrap.versioning;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Describes an artifactName version in terms of its components, converts it to/from a string and
+ * compares two versions.
+ *
+ * @author Brett Porter
+ */
+public interface ArtifactVersion
+	extends Comparable
+{
+	String getLabel();
+
+	String getVersionString();
+
+	boolean containsVersion(ArtifactVersion source);
+
+	String getRangeString();
+}
\ No newline at end of file
diff --git a/src/main/java/org/ultramine/bootstrap/versioning/ComparableVersion.java b/src/main/java/org/ultramine/bootstrap/versioning/ComparableVersion.java
new file mode 100644
index 0000000..587edf2
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/versioning/ComparableVersion.java
@@ -0,0 +1,500 @@
+/*
+ * Forge Mod Loader
+ * Copyright (c) 2012-2013 cpw.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser Public License v2.1
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ *
+ * Contributors:
+ *     cpw - implementation
+ */
+
+package org.ultramine.bootstrap.versioning;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.Stack;
+
+/**
+ * Generic implementation of version comparison.
+ *
+ * Features:
+ * 
+ * - mixing of '
-' (dash) and '.' (dot) separators, 
+ * - transition between characters and digits also constitutes a separator:
+ *     
1.0alpha1 => [1, 0, alpha, 1] 
+ * - unlimited number of version components,
 
+ * - version components in the text can be digits or strings,
 
+ * - strings are checked for well-known qualifiers and the qualifier ordering is used for version ordering.
+ *     Well-known qualifiers (case insensitive) are:
+ *     snapshot 
+ *     alpha or a 
+ *     beta or b 
+ *     milestone or m 
+ *     rc or cr 
+ *     (the empty string) or ga or final 
+ *     sp 
+ *     
+ *     Unknown qualifiers are considered after known qualifiers, with lexical order (always case insensitive),
+ *    
+ * - a dash usually precedes a qualifier, and is always less important than something preceded with a dot.
 
+ * 
+ *
+ * @see "Versioning" on Maven Wiki
+ * @author Kenney Westerhof
+ * @author Hervé Boutemy
+ */
+public class ComparableVersion
+	implements Comparable
+{
+	private String value;
+
+	private String canonical;
+
+	private ListItem items;
+
+	private interface Item
+	{
+		final int INTEGER_ITEM = 0;
+		final int STRING_ITEM = 1;
+		final int LIST_ITEM = 2;
+
+		int compareTo(Item item);
+
+		int getType();
+
+		boolean isNull();
+	}
+
+	/**
+	 * Represents a numeric item in the version item list.
+	 */
+	private static class IntegerItem
+		implements Item
+	{
+		private static final BigInteger BigInteger_ZERO = new BigInteger( "0" );
+
+		private final BigInteger value;
+
+		public static final IntegerItem ZERO = new IntegerItem();
+
+		private IntegerItem()
+		{
+			this.value = BigInteger_ZERO;
+		}
+
+		public IntegerItem( String str )
+		{
+			this.value = new BigInteger( str );
+		}
+
+		@Override
+		public int getType()
+		{
+			return INTEGER_ITEM;
+		}
+
+		@Override
+		public boolean isNull()
+		{
+			return BigInteger_ZERO.equals( value );
+		}
+
+		@Override
+		public int compareTo( Item item )
+		{
+			if ( item == null )
+			{
+				return BigInteger_ZERO.equals( value ) ? 0 : 1; // 1.0 == 1, 1.1 > 1
+			}
+
+			switch ( item.getType() )
+			{
+				case INTEGER_ITEM:
+					return value.compareTo( ( (IntegerItem) item ).value );
+
+				case STRING_ITEM:
+					return 1; // 1.1 > 1-sp
+
+				case LIST_ITEM:
+					return 1; // 1.1 > 1-1
+
+				default:
+					throw new RuntimeException( "invalid item: " + item.getClass() );
+			}
+		}
+
+		@Override
+		public String toString()
+		{
+			return value.toString();
+		}
+	}
+
+	/**
+	 * Represents a string in the version item list, usually a qualifier.
+	 */
+	private static class StringItem
+		implements Item
+	{
+		private static final String[] QUALIFIERS = { "alpha", "beta", "milestone", "rc", "snapshot", "", "sp" };
+
+		private static final List _QUALIFIERS = Arrays.asList( QUALIFIERS );
+
+		private static final Properties ALIASES = new Properties();
+		static
+		{
+			ALIASES.put( "ga", "" );
+			ALIASES.put( "final", "" );
+			ALIASES.put( "cr", "rc" );
+		}
+
+		/**
+		 * A comparable value for the empty-string qualifier. This one is used to determine if a given qualifier makes
+		 * the version older than one without a qualifier, or more recent.
+		 */
+		private static final String RELEASE_VERSION_INDEX = String.valueOf( _QUALIFIERS.indexOf( "" ) );
+
+		private String value;
+
+		public StringItem( String value, boolean followedByDigit )
+		{
+			if ( followedByDigit && value.length() == 1 )
+			{
+				// a1 = alpha-1, b1 = beta-1, m1 = milestone-1
+				switch ( value.charAt( 0 ) )
+				{
+					case 'a':
+						value = "alpha";
+						break;
+					case 'b':
+						value = "beta";
+						break;
+					case 'm':
+						value = "milestone";
+						break;
+				}
+			}
+			this.value = ALIASES.getProperty( value , value );
+		}
+
+		@Override
+		public int getType()
+		{
+			return STRING_ITEM;
+		}
+
+		@Override
+		public boolean isNull()
+		{
+			return ( comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX ) == 0 );
+		}
+
+		/**
+		 * Returns a comparable value for a qualifier.
+		 *
+		 * This method takes into account the ordering of known qualifiers then unknown qualifiers with lexical ordering.
+		 *
+		 * just returning an Integer with the index here is faster, but requires a lot of if/then/else to check for -1
+		 * or QUALIFIERS.size and then resort to lexical ordering. Most comparisons are decided by the first character,
+		 * so this is still fast. If more characters are needed then it requires a lexical sort anyway.
+		 *
+		 * @param qualifier
+		 * @return an equivalent value that can be used with lexical comparison
+		 */
+		public static String comparableQualifier( String qualifier )
+		{
+			int i = _QUALIFIERS.indexOf( qualifier );
+
+			return i == -1 ? ( _QUALIFIERS.size() + "-" + qualifier ) : String.valueOf( i );
+		}
+
+		@Override
+		public int compareTo( Item item )
+		{
+			if ( item == null )
+			{
+				// 1-rc < 1, 1-ga > 1
+				return comparableQualifier( value ).compareTo( RELEASE_VERSION_INDEX );
+			}
+			switch ( item.getType() )
+			{
+				case INTEGER_ITEM:
+					return -1; // 1.any < 1.1 ?
+
+				case STRING_ITEM:
+					return comparableQualifier( value ).compareTo( comparableQualifier( ( (StringItem) item ).value ) );
+
+				case LIST_ITEM:
+					return -1; // 1.any < 1-1
+
+				default:
+					throw new RuntimeException( "invalid item: " + item.getClass() );
+			}
+		}
+
+		@Override
+		public String toString()
+		{
+			return value;
+		}
+	}
+
+	/**
+	 * Represents a version list item. This class is used both for the global item list and for sub-lists (which start
+	 * with '-(number)' in the version specification).
+	 */
+	private static class ListItem
+		extends ArrayList- 
+		implements Item
+	{
+		/**
+		 *
+		 */
+		private static final long serialVersionUID = 1L;
+
+		@Override
+		public int getType()
+		{
+			return LIST_ITEM;
+		}
+
+		@Override
+		public boolean isNull()
+		{
+			return ( size() == 0 );
+		}
+
+		void normalize()
+		{
+			for( ListIterator
-  iterator = listIterator( size() ); iterator.hasPrevious(); )
+			{
+				Item item = iterator.previous();
+				if ( item.isNull() )
+				{
+					iterator.remove(); // remove null trailing items: 0, "", empty list
+				}
+				else
+				{
+					break;
+				}
+			}
+		}
+
+		@Override
+		public int compareTo( Item item )
+		{
+			if ( item == null )
+			{
+				if ( size() == 0 )
+				{
+					return 0; // 1-0 = 1- (normalize) = 1
+				}
+				Item first = get( 0 );
+				return first.compareTo( null );
+			}
+			switch ( item.getType() )
+			{
+				case INTEGER_ITEM:
+					return -1; // 1-1 < 1.0.x
+
+				case STRING_ITEM:
+					return 1; // 1-1 > 1-sp
+
+				case LIST_ITEM:
+					Iterator
-  left = iterator();
+					Iterator
-  right = ( (ListItem) item ).iterator();
+
+					while ( left.hasNext() || right.hasNext() )
+					{
+						Item l = left.hasNext() ? left.next() : null;
+						Item r = right.hasNext() ? right.next() : null;
+
+						// if this is shorter, then invert the compare and mul with -1
+						int result = l == null ? -1 * r.compareTo( l ) : l.compareTo( r );
+
+						if ( result != 0 )
+						{
+							return result;
+						}
+					}
+
+					return 0;
+
+				default:
+					throw new RuntimeException( "invalid item: " + item.getClass() );
+			}
+		}
+
+		@Override
+		public String toString()
+		{
+			StringBuilder buffer = new StringBuilder( "(" );
+			for( Iterator
-  iter = iterator(); iter.hasNext(); )
+			{
+				buffer.append( iter.next() );
+				if ( iter.hasNext() )
+				{
+					buffer.append( ',' );
+				}
+			}
+			buffer.append( ')' );
+			return buffer.toString();
+		}
+	}
+
+	public ComparableVersion( String version )
+	{
+		parseVersion( version );
+	}
+
+	public final void parseVersion( String version )
+	{
+		this.value = version;
+
+		items = new ListItem();
+
+		version = version.toLowerCase( Locale.ENGLISH );
+
+		ListItem list = items;
+
+		Stack
-  stack = new Stack
- ();
+		stack.push( list );
+
+		boolean isDigit = false;
+
+		int startIndex = 0;
+
+		for ( int i = 0; i < version.length(); i++ )
+		{
+			char c = version.charAt( i );
+
+			if ( c == '.' )
+			{
+				if ( i == startIndex )
+				{
+					list.add( IntegerItem.ZERO );
+				}
+				else
+				{
+					list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
+				}
+				startIndex = i + 1;
+			}
+			else if ( c == '-' )
+			{
+				if ( i == startIndex )
+				{
+					list.add( IntegerItem.ZERO );
+				}
+				else
+				{
+					list.add( parseItem( isDigit, version.substring( startIndex, i ) ) );
+				}
+				startIndex = i + 1;
+
+				if ( isDigit )
+				{
+					list.normalize(); // 1.0-* = 1-*
+
+					if ( ( i + 1 < version.length() ) && Character.isDigit( version.charAt( i + 1 ) ) )
+					{
+						// new ListItem only if previous were digits and new char is a digit,
+						// ie need to differentiate only 1.1 from 1-1
+						list.add( list = new ListItem() );
+
+						stack.push( list );
+					}
+				}
+			}
+			else if ( Character.isDigit( c ) )
+			{
+				if ( !isDigit && i > startIndex )
+				{
+					list.add( new StringItem( version.substring( startIndex, i ), true ) );
+					startIndex = i;
+				}
+
+				isDigit = true;
+			}
+			else
+			{
+				if ( isDigit && i > startIndex )
+				{
+					list.add( parseItem( true, version.substring( startIndex, i ) ) );
+					startIndex = i;
+				}
+
+				isDigit = false;
+			}
+		}
+
+		if ( version.length() > startIndex )
+		{
+			list.add( parseItem( isDigit, version.substring( startIndex ) ) );
+		}
+
+		while ( !stack.isEmpty() )
+		{
+			list = (ListItem) stack.pop();
+			list.normalize();
+		}
+
+		canonical = items.toString();
+	}
+
+	private static Item parseItem( boolean isDigit, String buf )
+	{
+		return isDigit ? new IntegerItem( buf ) : new StringItem( buf, false );
+	}
+
+	@Override
+	public int compareTo( ComparableVersion o )
+	{
+		return items.compareTo( o.items );
+	}
+
+	@Override
+	public String toString()
+	{
+		return value;
+	}
+
+	@Override
+	public boolean equals( Object o )
+	{
+		return ( o instanceof ComparableVersion ) && canonical.equals( ( (ComparableVersion) o ).canonical );
+	}
+
+	@Override
+	public int hashCode()
+	{
+		return canonical.hashCode();
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/ultramine/bootstrap/versioning/DefaultArtifactVersion.java b/src/main/java/org/ultramine/bootstrap/versioning/DefaultArtifactVersion.java
new file mode 100644
index 0000000..ded634a
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/versioning/DefaultArtifactVersion.java
@@ -0,0 +1,105 @@
+/*
+ * Forge Mod Loader
+ * Copyright (c) 2012-2013 cpw.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser Public License v2.1
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ *
+ * Contributors:
+ *     cpw - implementation
+ */
+
+package org.ultramine.bootstrap.versioning;
+
+public class DefaultArtifactVersion implements ArtifactVersion
+{
+	private ComparableVersion comparableVersion;
+	private String label;
+	private boolean unbounded;
+	private VersionRange range;
+
+	public DefaultArtifactVersion(String versionNumber)
+	{
+		comparableVersion = new ComparableVersion(versionNumber);
+		range = VersionRange.createFromVersion(versionNumber, this);
+	}
+
+	public DefaultArtifactVersion(String label, VersionRange range)
+	{
+		this.label = label;
+		this.range = range;
+	}
+	public DefaultArtifactVersion(String label, String version)
+	{
+		this(version);
+		this.label = label;
+	}
+
+	public DefaultArtifactVersion(String string, boolean unbounded)
+	{
+		this.label = string;
+		this.unbounded = true;
+	}
+
+	@Override
+	public boolean equals(Object obj)
+	{
+		return ((DefaultArtifactVersion)obj).containsVersion(this);
+	}
+
+	@Override
+	public int compareTo(ArtifactVersion o)
+	{
+		return unbounded ? 0 : this.comparableVersion.compareTo(((DefaultArtifactVersion)o).comparableVersion);
+	}
+
+	@Override
+	public String getLabel()
+	{
+		return label;
+	}
+
+	@Override
+	public boolean containsVersion(ArtifactVersion source)
+	{
+		if (!source.getLabel().equals(getLabel()))
+		{
+			return false;
+		}
+		if (unbounded)
+		{
+			return true;
+		}
+		if (range != null)
+		{
+			return range.containsVersion(source);
+		}
+		else
+		{
+			return false;
+		}
+	}
+
+	@Override
+	public String getVersionString()
+	{
+		return comparableVersion == null ? "unknown" : comparableVersion.toString();
+	}
+
+	@Override
+	public String getRangeString()
+	{
+		return range == null ? "any" : range.toString();
+	}
+	@Override
+	public String toString()
+	{
+		return label == null ? comparableVersion.toString() : label + ( unbounded ? "" : "@" + range);
+	}
+
+	public VersionRange getRange()
+	{
+		return range;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/ultramine/bootstrap/versioning/InvalidVersionSpecificationException.java b/src/main/java/org/ultramine/bootstrap/versioning/InvalidVersionSpecificationException.java
new file mode 100644
index 0000000..333fd70
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/versioning/InvalidVersionSpecificationException.java
@@ -0,0 +1,47 @@
+/*
+ * Forge Mod Loader
+ * Copyright (c) 2012-2013 cpw.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser Public License v2.1
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ *
+ * Contributors:
+ *     cpw - implementation
+ */
+
+package org.ultramine.bootstrap.versioning;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Occurs when a version is invalid.
+ *
+ * @author Brett Porter
+ */
+public class InvalidVersionSpecificationException extends Exception
+{
+	private static final long serialVersionUID = 1L;
+
+	public InvalidVersionSpecificationException( String message )
+	{
+		super( message );
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/ultramine/bootstrap/versioning/Restriction.java b/src/main/java/org/ultramine/bootstrap/versioning/Restriction.java
new file mode 100644
index 0000000..0baf78e
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/versioning/Restriction.java
@@ -0,0 +1,212 @@
+/*
+ * Forge Mod Loader
+ * Copyright (c) 2012-2013 cpw.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser Public License v2.1
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * 
+ * Contributors:
+ *     cpw - implementation
+ */
+
+package org.ultramine.bootstrap.versioning;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Describes a restriction in versioning.
+ *
+ * @author Brett Porter
+ */
+public class Restriction
+{
+	private final ArtifactVersion lowerBound;
+
+	private final boolean lowerBoundInclusive;
+
+	private final ArtifactVersion upperBound;
+
+	private final boolean upperBoundInclusive;
+
+	public static final Restriction EVERYTHING = new Restriction( null, false, null, false );
+
+	public Restriction( ArtifactVersion lowerBound, boolean lowerBoundInclusive, ArtifactVersion upperBound,
+						boolean upperBoundInclusive )
+	{
+		this.lowerBound = lowerBound;
+		this.lowerBoundInclusive = lowerBoundInclusive;
+		this.upperBound = upperBound;
+		this.upperBoundInclusive = upperBoundInclusive;
+	}
+
+	public ArtifactVersion getLowerBound()
+	{
+		return lowerBound;
+	}
+
+	public boolean isLowerBoundInclusive()
+	{
+		return lowerBoundInclusive;
+	}
+
+	public ArtifactVersion getUpperBound()
+	{
+		return upperBound;
+	}
+
+	public boolean isUpperBoundInclusive()
+	{
+		return upperBoundInclusive;
+	}
+
+	public boolean containsVersion( ArtifactVersion version )
+	{
+		if ( lowerBound != null )
+		{
+			int comparison = lowerBound.compareTo( version );
+
+			if ( ( comparison == 0 ) && !lowerBoundInclusive )
+			{
+				return false;
+			}
+			if ( comparison > 0 )
+			{
+				return false;
+			}
+		}
+		if ( upperBound != null )
+		{
+			int comparison = upperBound.compareTo( version );
+
+			if ( ( comparison == 0 ) && !upperBoundInclusive )
+			{
+				return false;
+			}
+			if ( comparison < 0 )
+			{
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	@Override
+	public int hashCode()
+	{
+		int result = 13;
+
+		if ( lowerBound == null )
+		{
+			result += 1;
+		}
+		else
+		{
+			result += lowerBound.hashCode();
+		}
+
+		result *= lowerBoundInclusive ? 1 : 2;
+
+		if ( upperBound == null )
+		{
+			result -= 3;
+		}
+		else
+		{
+			result -= upperBound.hashCode();
+		}
+
+		result *= upperBoundInclusive ? 2 : 3;
+
+		return result;
+	}
+
+	@Override
+	public boolean equals( Object other )
+	{
+		if ( this == other )
+		{
+			return true;
+		}
+
+		if ( !( other instanceof Restriction ) )
+		{
+			return false;
+		}
+
+		Restriction restriction = (Restriction) other;
+		if ( lowerBound != null )
+		{
+			if ( !lowerBound.equals( restriction.lowerBound ) )
+			{
+				return false;
+			}
+		}
+		else if ( restriction.lowerBound != null )
+		{
+			return false;
+		}
+
+		if ( lowerBoundInclusive != restriction.lowerBoundInclusive )
+		{
+			return false;
+		}
+
+		if ( upperBound != null )
+		{
+			if ( !upperBound.equals( restriction.upperBound ) )
+			{
+				return false;
+			}
+		}
+		else if ( restriction.upperBound != null )
+		{
+			return false;
+		}
+
+		if ( upperBoundInclusive != restriction.upperBoundInclusive )
+		{
+			return false;
+		}
+
+		return true;
+	}
+
+	@Override
+	public String toString()
+	{
+		StringBuilder buf = new StringBuilder();
+
+		buf.append( isLowerBoundInclusive() ? "[" : "(" );
+		if ( getLowerBound() != null )
+		{
+			buf.append( getLowerBound().toString() );
+		}
+		buf.append( "," );
+		if ( getUpperBound() != null )
+		{
+			buf.append( getUpperBound().toString() );
+		}
+		buf.append( isUpperBoundInclusive() ? "]" : ")" );
+
+		return buf.toString();
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/ultramine/bootstrap/versioning/VersionParser.java b/src/main/java/org/ultramine/bootstrap/versioning/VersionParser.java
new file mode 100644
index 0000000..81d24e2
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/versioning/VersionParser.java
@@ -0,0 +1,66 @@
+/*
+ * Forge Mod Loader
+ * Copyright (c) 2012-2013 cpw.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser Public License v2.1
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * 
+ * Contributors:
+ *     cpw - implementation
+ */
+
+package org.ultramine.bootstrap.versioning;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Parses version strings according to the specification here:
+ * http://docs.codehaus.org/display/MAVEN/Versioning
+ * and allows for comparison of versions based on that document.
+ * Bounded version specifications are defined as
+ * http://maven.apache.org/plugins/maven-enforcer-plugin/rules/versionRanges.html
+ *
+ * Borrows heavily from maven version range management code
+ *
+ * @author cpw
+ *
+ */
+public class VersionParser
+{
+	public static ArtifactVersion parseVersionReference(String labelledRef)
+	{
+		if (labelledRef == null || labelledRef.isEmpty())
+		{
+			throw new RuntimeException(String.format("Empty reference %s", labelledRef));
+		}
+		List parts = Arrays.asList(labelledRef.split("@"));
+		if (parts.size()>2)
+		{
+			throw new RuntimeException(String.format("Invalid versioned reference %s", labelledRef));
+		}
+		if (parts.size()==1)
+		{
+			return new DefaultArtifactVersion(parts.get(0), true);
+		}
+		return new DefaultArtifactVersion(parts.get(0),parseRange(parts.get(1)));
+	}
+
+	public static boolean satisfies(ArtifactVersion target, ArtifactVersion source)
+	{
+		return target.containsVersion(source);
+	}
+
+	public static VersionRange parseRange(String range)
+	{
+		try
+		{
+			return VersionRange.createFromVersionSpec(range);
+		}
+		catch (InvalidVersionSpecificationException e)
+		{
+			throw new RuntimeException(e);
+		}
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/ultramine/bootstrap/versioning/VersionRange.java b/src/main/java/org/ultramine/bootstrap/versioning/VersionRange.java
new file mode 100644
index 0000000..8d0a34c
--- /dev/null
+++ b/src/main/java/org/ultramine/bootstrap/versioning/VersionRange.java
@@ -0,0 +1,575 @@
+/*
+ * Forge Mod Loader
+ * Copyright (c) 2012-2013 cpw.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the GNU Lesser Public License v2.1
+ * which accompanies this distribution, and is available at
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ *
+ * Contributors:
+ *     cpw - implementation
+ */
+
+package org.ultramine.bootstrap.versioning;
+/*
+ * Modifications by cpw under LGPL 2.1 or later
+ */
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Construct a version range from a specification.
+ *
+ * @author Brett Porter
+ */
+public class VersionRange
+{
+	private final ArtifactVersion recommendedVersion;
+
+	private final List restrictions;
+
+	private VersionRange( ArtifactVersion recommendedVersion,
+						  List restrictions )
+	{
+		this.recommendedVersion = recommendedVersion;
+		this.restrictions = restrictions;
+	}
+
+	public ArtifactVersion getRecommendedVersion()
+	{
+		return recommendedVersion;
+	}
+
+	public List getRestrictions()
+	{
+		return restrictions;
+	}
+
+	public VersionRange cloneOf()
+	{
+		List copiedRestrictions = null;
+
+		if ( restrictions != null )
+		{
+			copiedRestrictions = new ArrayList();
+
+			if ( !restrictions.isEmpty() )
+			{
+				copiedRestrictions.addAll( restrictions );
+			}
+		}
+
+		return new VersionRange( recommendedVersion, copiedRestrictions );
+	}
+	
+	/**
+	 * Factory method, for custom versioning schemes
+	 * @param version version
+	 * @param restrictions restriction list
+	 * @return a new version range
+	 */
+	public static VersionRange newRange(ArtifactVersion version, List restrictions)
+	{
+		return new VersionRange(version, restrictions);
+	}
+	/**
+	 * Create a version range from a string representation
+	 * 
+	 * Some spec examples are
+	 * 
+	 * 1.0 Version 1.0 
+	 * [1.0,2.0) Versions 1.0 (included) to 2.0 (not included) 
+	 * [1.0,2.0] Versions 1.0 to 2.0 (both included) 
+	 * [1.5,) Versions 1.5 and higher 
+	 * (,1.0],[1.2,) Versions up to 1.0 (included) and 1.2 or higher 
+	 * 
+	 *
+	 * @param spec string representation of a version or version range
+	 * @return a new {@link VersionRange} object that represents the spec
+	 * @throws InvalidVersionSpecificationException
+	 *
+	 */
+	public static VersionRange createFromVersionSpec( String spec )
+		throws InvalidVersionSpecificationException
+	{
+		if ( spec == null )
+		{
+			return null;
+		}
+
+		List restrictions = new ArrayList();
+		String process = spec;
+		ArtifactVersion version = null;
+		ArtifactVersion upperBound = null;
+		ArtifactVersion lowerBound = null;
+
+		while ( process.startsWith( "[" ) || process.startsWith( "(" ) )
+		{
+			int index1 = process.indexOf( ")" );
+			int index2 = process.indexOf( "]" );
+
+			int index = index2;
+			if ( index2 < 0 || index1 < index2 )
+			{
+				if ( index1 >= 0 )
+				{
+					index = index1;
+				}
+			}
+
+			if ( index < 0 )
+			{
+				throw new InvalidVersionSpecificationException( "Unbounded range: " + spec );
+			}
+
+			Restriction restriction = parseRestriction( process.substring( 0, index + 1 ) );
+			if ( lowerBound == null )
+			{
+				lowerBound = restriction.getLowerBound();
+			}
+			if ( upperBound != null )
+			{
+				if ( restriction.getLowerBound() == null || restriction.getLowerBound().compareTo( upperBound ) < 0 )
+				{
+					throw new InvalidVersionSpecificationException( "Ranges overlap: " + spec );
+				}
+			}
+			restrictions.add( restriction );
+			upperBound = restriction.getUpperBound();
+
+			process = process.substring( index + 1 ).trim();
+
+			if ( process.length() > 0 && process.startsWith( "," ) )
+			{
+				process = process.substring( 1 ).trim();
+			}
+		}
+
+		if ( process.length() > 0 )
+		{
+			if ( restrictions.size() > 0 )
+			{
+				throw new InvalidVersionSpecificationException(
+					"Only fully-qualified sets allowed in multiple set scenario: " + spec );
+			}
+			else
+			{
+				version = new DefaultArtifactVersion( process );
+				restrictions.add( Restriction.EVERYTHING );
+			}
+		}
+
+		return new VersionRange( version, restrictions );
+	}
+
+	private static Restriction parseRestriction( String spec )
+		throws InvalidVersionSpecificationException
+	{
+		boolean lowerBoundInclusive = spec.startsWith( "[" );
+		boolean upperBoundInclusive = spec.endsWith( "]" );
+
+		String process = spec.substring( 1, spec.length() - 1 ).trim();
+
+		Restriction restriction;
+
+		int index = process.indexOf( "," );
+
+		if ( index < 0 )
+		{
+			if ( !lowerBoundInclusive || !upperBoundInclusive )
+			{
+				throw new InvalidVersionSpecificationException( "Single version must be surrounded by []: " + spec );
+			}
+
+			ArtifactVersion version = new DefaultArtifactVersion( process );
+
+			restriction = new Restriction( version, lowerBoundInclusive, version, upperBoundInclusive );
+		}
+		else
+		{
+			String lowerBound = process.substring( 0, index ).trim();
+			String upperBound = process.substring( index + 1 ).trim();
+			if ( lowerBound.equals( upperBound ) )
+			{
+				throw new InvalidVersionSpecificationException( "Range cannot have identical boundaries: " + spec );
+			}
+
+			ArtifactVersion lowerVersion = null;
+			if ( lowerBound.length() > 0 )
+			{
+				lowerVersion = new DefaultArtifactVersion( lowerBound );
+			}
+			ArtifactVersion upperVersion = null;
+			if ( upperBound.length() > 0 )
+			{
+				upperVersion = new DefaultArtifactVersion( upperBound );
+			}
+
+			if ( upperVersion != null && lowerVersion != null && upperVersion.compareTo( lowerVersion ) < 0 )
+			{
+				throw new InvalidVersionSpecificationException( "Range defies version ordering: " + spec );
+			}
+
+			restriction = new Restriction( lowerVersion, lowerBoundInclusive, upperVersion, upperBoundInclusive );
+		}
+
+		return restriction;
+	}
+
+	public static VersionRange createFromVersion( String version , ArtifactVersion existing)
+	{
+		List restrictions = Collections.emptyList();
+		if (existing == null)
+		{
+			existing = new DefaultArtifactVersion( version );
+		}
+		return new VersionRange(existing , restrictions );
+	}
+
+	/**
+	 * Creates and returns a new VersionRange that is a restriction of this
+	 * version range and the specified version range.
+	 * 
+	 * Note: Precedence is given to the recommended version from this version range over the
+	 * recommended version from the specified version range.
+	 * 
+	 *
+	 * @param restriction the VersionRange that will be used to restrict this version
+	 *                    range.
+	 * @return the VersionRange that is a restriction of this version range and the
+	 *         specified version range.
+	 *         
+	 *         The restrictions of the returned version range will be an intersection of the restrictions
+	 *         of this version range and the specified version range if both version ranges have
+	 *         restrictions. Otherwise, the restrictions on the returned range will be empty.
+	 *         
+	 *         
+	 *         The recommended version of the returned version range will be the recommended version of
+	 *         this version range, provided that ranges falls within the intersected restrictions. If
+	 *         the restrictions are empty, this version range's recommended version is used if it is not
+	 *         null. If it is null, the specified version range's recommended
+	 *         version is used (provided it is non-null). If no recommended version can be
+	 *         obtained, the returned version range's recommended version is set to null.
+	 *         
+	 * @throws NullPointerException if the specified VersionRange is
+	 *                              null.
+	 */
+	public VersionRange restrict( VersionRange restriction )
+	{
+		List r1 = this.restrictions;
+		List r2 = restriction.restrictions;
+		List restrictions;
+
+		if ( r1.isEmpty() || r2.isEmpty() )
+		{
+			restrictions = Collections.emptyList();
+		}
+		else
+		{
+			restrictions = intersection( r1, r2 );
+		}
+
+		ArtifactVersion version = null;
+		if ( restrictions.size() > 0 )
+		{
+			for ( Restriction r : restrictions )
+			{
+				if ( recommendedVersion != null && r.containsVersion( recommendedVersion ) )
+				{
+					// if we find the original, use that
+					version = recommendedVersion;
+					break;
+				}
+				else if ( version == null && restriction.getRecommendedVersion() != null
+					&& r.containsVersion( restriction.getRecommendedVersion() ) )
+				{
+					// use this if we can, but prefer the original if possible
+					version = restriction.getRecommendedVersion();
+				}
+			}
+		}
+		// Either the original or the specified version ranges have no restrictions
+		else if ( recommendedVersion != null )
+		{
+			// Use the original recommended version since it exists
+			version = recommendedVersion;
+		}
+		else if ( restriction.recommendedVersion != null )
+		{
+			// Use the recommended version from the specified VersionRange since there is no
+			// original recommended version
+			version = restriction.recommendedVersion;
+		}
+/* TODO: should throw this immediately, but need artifactName
+		else
+		{
+			throw new OverConstrainedVersionException( "Restricting incompatible version ranges" );
+		}
+*/
+
+		return new VersionRange( version, restrictions );
+	}
+
+	private List intersection( List r1, List r2 )
+	{
+		List restrictions = new ArrayList( r1.size() + r2.size() );
+		Iterator i1 = r1.iterator();
+		Iterator i2 = r2.iterator();
+		Restriction res1 = i1.next();
+		Restriction res2 = i2.next();
+
+		boolean done = false;
+		while ( !done )
+		{
+			if ( res1.getLowerBound() == null || res2.getUpperBound() == null
+				|| res1.getLowerBound().compareTo( res2.getUpperBound() ) <= 0 )
+			{
+				if ( res1.getUpperBound() == null || res2.getLowerBound() == null
+					|| res1.getUpperBound().compareTo( res2.getLowerBound() ) >= 0 )
+				{
+					ArtifactVersion lower;
+					ArtifactVersion upper;
+					boolean lowerInclusive;
+					boolean upperInclusive;
+
+					// overlaps
+					if ( res1.getLowerBound() == null )
+					{
+						lower = res2.getLowerBound();
+						lowerInclusive = res2.isLowerBoundInclusive();
+					}
+					else if ( res2.getLowerBound() == null )
+					{
+						lower = res1.getLowerBound();
+						lowerInclusive = res1.isLowerBoundInclusive();
+					}
+					else
+					{
+						int comparison = res1.getLowerBound().compareTo( res2.getLowerBound() );
+						if ( comparison < 0 )
+						{
+							lower = res2.getLowerBound();
+							lowerInclusive = res2.isLowerBoundInclusive();
+						}
+						else if ( comparison == 0 )
+						{
+							lower = res1.getLowerBound();
+							lowerInclusive = res1.isLowerBoundInclusive() && res2.isLowerBoundInclusive();
+						}
+						else
+						{
+							lower = res1.getLowerBound();
+							lowerInclusive = res1.isLowerBoundInclusive();
+						}
+					}
+
+					if ( res1.getUpperBound() == null )
+					{
+						upper = res2.getUpperBound();
+						upperInclusive = res2.isUpperBoundInclusive();
+					}
+					else if ( res2.getUpperBound() == null )
+					{
+						upper = res1.getUpperBound();
+						upperInclusive = res1.isUpperBoundInclusive();
+					}
+					else
+					{
+						int comparison = res1.getUpperBound().compareTo( res2.getUpperBound() );
+						if ( comparison < 0 )
+						{
+							upper = res1.getUpperBound();
+							upperInclusive = res1.isUpperBoundInclusive();
+						}
+						else if ( comparison == 0 )
+						{
+							upper = res1.getUpperBound();
+							upperInclusive = res1.isUpperBoundInclusive() && res2.isUpperBoundInclusive();
+						}
+						else
+						{
+							upper = res2.getUpperBound();
+							upperInclusive = res2.isUpperBoundInclusive();
+						}
+					}
+
+					// don't add if they are equal and one is not inclusive
+					if ( lower == null || upper == null || lower.compareTo( upper ) != 0 )
+					{
+						restrictions.add( new Restriction( lower, lowerInclusive, upper, upperInclusive ) );
+					}
+					else if ( lowerInclusive && upperInclusive )
+					{
+						restrictions.add( new Restriction( lower, lowerInclusive, upper, upperInclusive ) );
+					}
+
+					//noinspection ObjectEquality
+					if ( upper == res2.getUpperBound() )
+					{
+						// advance res2
+						if ( i2.hasNext() )
+						{
+							res2 = i2.next();
+						}
+						else
+						{
+							done = true;
+						}
+					}
+					else
+					{
+						// advance res1
+						if ( i1.hasNext() )
+						{
+							res1 = i1.next();
+						}
+						else
+						{
+							done = true;
+						}
+					}
+				}
+				else
+				{
+					// move on to next in r1
+					if ( i1.hasNext() )
+					{
+						res1 = i1.next();
+					}
+					else
+					{
+						done = true;
+					}
+				}
+			}
+			else
+			{
+				// move on to next in r2
+				if ( i2.hasNext() )
+				{
+					res2 = i2.next();
+				}
+				else
+				{
+					done = true;
+				}
+			}
+		}
+
+		return restrictions;
+	}
+
+	@Override
+	public String toString()
+	{
+		if ( recommendedVersion != null )
+		{
+			return recommendedVersion.getVersionString();
+		}
+		else
+		{
+			return restrictions.stream().map(Restriction::toString).collect(Collectors.joining(","));
+		}
+	}
+
+	public ArtifactVersion matchVersion( List versions )
+	{
+		// TODO: could be more efficient by sorting the list and then moving along the restrictions in order?
+
+		ArtifactVersion matched = null;
+		for ( ArtifactVersion version : versions )
+		{
+			if ( containsVersion( version ) )
+			{
+				// valid - check if it is greater than the currently matched version
+				if ( matched == null || version.compareTo( matched ) > 0 )
+				{
+					matched = version;
+				}
+			}
+		}
+		return matched;
+	}
+
+	public boolean containsVersion( ArtifactVersion version )
+	{
+		for ( Restriction restriction : restrictions )
+		{
+			if ( restriction.containsVersion( version ) )
+			{
+				return true;
+			}
+		}
+		return false;
+	}
+
+	public boolean hasRestrictions()
+	{
+		return !restrictions.isEmpty() && recommendedVersion == null;
+	}
+
+	@Override
+	public boolean equals( Object obj )
+	{
+		if ( this == obj )
+		{
+			return true;
+		}
+		if ( !( obj instanceof VersionRange ) )
+		{
+			return false;
+		}
+		VersionRange other = (VersionRange) obj;
+
+		boolean equals =
+			recommendedVersion == other.recommendedVersion
+				|| ( ( recommendedVersion != null ) && recommendedVersion.equals( other.recommendedVersion ) );
+		equals &=
+			restrictions == other.restrictions
+				|| ( ( restrictions != null ) && restrictions.equals( other.restrictions ) );
+		return equals;
+	}
+
+	@Override
+	public int hashCode()
+	{
+		int hash = 7;
+		hash = 31 * hash + ( recommendedVersion == null ? 0 : recommendedVersion.hashCode() );
+		hash = 31 * hash + ( restrictions == null ? 0 : restrictions.hashCode() );
+		return hash;
+	}
+
+	public boolean isUnboundedAbove()
+	{
+		return restrictions.size() == 1 && restrictions.get(0).getUpperBound() == null && !restrictions.get(0).isUpperBoundInclusive();
+	}
+
+	public String getLowerBoundString()
+	{
+		return restrictions.size() == 1 ? restrictions.get(0).getLowerBound().getVersionString() : "";
+	}
+
+}
\ No newline at end of file
diff --git a/src/main/resources/assets/crt/DSTRootCAX3.pem b/src/main/resources/assets/crt/DSTRootCAX3.pem
new file mode 100644
index 0000000..b2e43c9
--- /dev/null
+++ b/src/main/resources/assets/crt/DSTRootCAX3.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
+PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
+Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
+rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
+OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
+xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
+7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
+aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
+SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
+ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
+AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
+R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
+JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
+Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
+-----END CERTIFICATE-----
diff --git a/src/main/resources/assets/lang/en_US.lang b/src/main/resources/assets/lang/en_US.lang
new file mode 100644
index 0000000..7a3630d
--- /dev/null
+++ b/src/main/resources/assets/lang/en_US.lang
@@ -0,0 +1,15 @@
+stage.start=Bootstrap for UltraMine Core 1.7.10
+stage.versions=Retrieving versions
+stage.downloading=Checking & downloading dependencies
+stage.scripts=Creating scripts
+stage.finished=Bootstrap finished. Run ./start.sh (or start.bat) to run ultramine server
+
+error.mavenmetadata=Failed to retriever maven metadata from %s
+error.pom=Failed to retriever pom xml from %s
+error.addrepo.file=Failed to add repository: path is corrupted: %s
+error.unavailable.host=Failed to resolve hostname (%s); check your network connection
+error.unavailable.maven=Maven server (%s) is currently unavailable; check your network connection
+error.download.dependency=Failed to download dependency: %s (%s)
+error.write.file=Error write file %s: %s
+
+hint.stacktrace=Run with --stacktrace for more information
diff --git a/src/main/resources/assets/lang/ru_RU.lang b/src/main/resources/assets/lang/ru_RU.lang
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/main/resources/assets/lang/ru_RU.lang
diff --git a/src/main/resources/assets/script/java_command_line.txt b/src/main/resources/assets/script/java_command_line.txt
new file mode 100644
index 0000000..ced75b2
--- /dev/null
+++ b/src/main/resources/assets/script/java_command_line.txt
@@ -0,0 +1,16 @@
+# Use start.sh to start minecraft server, don't use this file
+
+java \
+-Xms{xms} \
+-Xmx{xmx} \
+-Xmn{xmn} \
+-XX:+UseParallelGC \
+-XX:+UseTLAB \
+-XX:+AggressiveOpts \
+-XX:+UseFastEmptyMethods \
+-XX:+UseFastAccessorMethods \
+-Dfile.encoding=utf8 \
+-Dorg.ultramine.terminal={terminal} \
+-Dorg.ultramine.dirs.vanilla={vanilla_dir} \
+-Dorg.ultramine.dirs.worlds={worlds_dir} \
+-cp "{classpath}" cpw.mods.fml.relauncher.ServerLaunchWrapper
\ No newline at end of file
diff --git a/src/main/resources/assets/script/shell/start.sh b/src/main/resources/assets/script/shell/start.sh
new file mode 100644
index 0000000..e4f222a
--- /dev/null
+++ b/src/main/resources/assets/script/shell/start.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+while :
+do
+	./ultramine_server_run_line.sh
+	echo "waiting before restarting"
+	sleep 10
+done
\ No newline at end of file
diff --git a/src/main/resources/assets/script/windows/start.bat b/src/main/resources/assets/script/windows/start.bat
new file mode 100644
index 0000000..3bd412c
--- /dev/null
+++ b/src/main/resources/assets/script/windows/start.bat
@@ -0,0 +1,5 @@
+:loop
+ultramine_server_run_line.bat
+echo waiting before restarting
+timeout 10
+goto loop
\ No newline at end of file
diff --git a/src/main/resources/assets/server.properties b/src/main/resources/assets/server.properties
new file mode 100644
index 0000000..181cad3
--- /dev/null
+++ b/src/main/resources/assets/server.properties
@@ -0,0 +1,34 @@
+#Minecraft server properties
+#http://server.properties/owner.html
+generator-settings=
+op-permission-level=4
+allow-nether=true
+level-name=world
+enable-query=false
+query-port=25565
+allow-flight=false
+announce-player-achievements=true
+server-port=25565
+level-type=DEFAULT
+enable-rcon=false
+rcon-port=25565
+level-seed=
+force-gamemode=false
+server-ip=
+max-build-height=256
+spawn-npcs=true
+white-list=false
+spawn-animals=true
+snooper-enabled=true
+online-mode=true
+resource-pack=
+pvp=true
+difficulty=1
+enable-command-block=false
+gamemode=0
+player-idle-timeout=0
+max-players=20
+spawn-monsters=true
+generate-structures=true
+view-distance=10
+motd=A Minecraft Server
\ No newline at end of file
diff --git a/src/main/resources/assets/server.yml b/src/main/resources/assets/server.yml
new file mode 100644
index 0000000..11d34d1
--- /dev/null
+++ b/src/main/resources/assets/server.yml
@@ -0,0 +1,72 @@
+listen:
+    minecraft:
+        serverIP: '{server-ip}'
+        port: {server-port}
+    query:
+        enabled: {enable-query}
+        port: {query-port}
+    rcon:
+        enabled: {enable-rcon}
+        port: {rcon-port}
+        password: ''
+        whitelist: null
+settings:
+    authorization:
+        onlineMode: {online-mode}
+    player:
+        playerIdleTimeout: {player-idle-timeout}
+        gamemode: 0
+        maxPlayers: {max-players}
+        forceGamemode: false
+        whiteList: {white-list}
+    other:
+        snooperEnabled: {snooper-enabled}
+        hardcore: false
+        resourcePack: ''
+        enableCommandBlock: {enable-command-block}
+        splitWorldDirs: {split-world-dirs}
+        recipeCacheEnabled: true
+    spawnLocations:
+        firstSpawn: spawn
+        deathSpawn: spawn
+        respawnOnBed: true
+    teleportation:
+        cooldown: 60
+        delay: 5
+        interWorldHome: true
+        interWorldWarp: true
+    messages:
+        announcePlayerAchievements: {announce-player-achievements}
+        motd: {motd}
+    watchdogThread:
+        timeout: 120
+        restart: true
+    inSQLServerStorage:
+        enabled: false
+        database: global
+        tablePrefix: mc_
+    security:
+        allowFlight: false
+        checkBreakSpeed: true
+tools:
+    autobroadcast:
+        enabled: false
+        intervalSeconds: 600
+        messages: []
+        showAllMessages: false
+    autoDebugInfo:
+        enabled: false
+        intervalSeconds: 600
+    autobackup:
+        enabled: false
+        interval: 60
+        maxBackups: 10
+        maxDirSize: 50000
+        worlds: null
+        notifyPlayers: true
+    warpProtection: []
+    economy:
+        startBalance: 30.0
+databases: {}
+vanilla:
+    unresolved: {}