@Override
    public void run() {
      Asset asset;
      while ((asset = filesLeft.poll()) != null) {
        for (int i = 1; i < MAX_TRIES + 1; i++) {
          try {
            File file = new File(getAssetsDir(), "objects/" + asset.path);

            // does exist? create
            if (!file.exists()) {
              file.getParentFile().mkdirs();
              file.createNewFile();
            }

            File localMc = new File(minecraftDir, asset.path);
            BufferedInputStream stream;

            // check for local copy
            if (localMc.exists() && Constants.hash(localMc, "SHA1").equals(asset.hash))
              // if so, copy
              stream = new BufferedInputStream(Files.newInputStreamSupplier(localMc).getInput());
            else
              // otherwise download
              stream =
                  new BufferedInputStream(
                      new URL(Constants.ASSETS_URL + "/" + asset.path).openStream());

            Files.write(ByteStreams.toByteArray(stream), file);
            stream.close();

            // check hash...
            String hash = Constants.hash(file, "SHA1");
            if (asset.hash.equals(hash)) break; // hashes are fine;
            else {
              file.delete();
              getLogger()
                  .error("download attempt " + i + " failed! : " + asset.hash + " != " + hash);
            }
          } catch (Exception e) {
            getLogger().error("Error downloading asset: " + asset.path);
            e.printStackTrace();
            if (!errored) errored = true;
          }
        }
      }
    }
 public String getMainClass() {
   return Constants.resolveString(mainClass);
 }
 public String getRunDir() {
   return Constants.resolveString(runDir);
 }
 public String getJvmArguments() {
   return Constants.resolveString(jvmArgs);
 }
 public String getArguments() {
   return Constants.resolveString(runArgs);
 }
 public String getProjectName() {
   return Constants.resolveString(projectName);
 }
public class DownloadAssetsTask extends DefaultTask {
  DelayedFile assetsDir;

  @Input Closure<AssetIndex> index;

  private boolean errored = false;
  private final ConcurrentLinkedQueue<Asset> filesLeft = new ConcurrentLinkedQueue<Asset>();
  private final ArrayList<AssetsThread> threads = new ArrayList<AssetsThread>();
  private final File minecraftDir = new File(Constants.getMinecraftDirectory(), "assets/objects");

  private static final int MAX_THREADS = Runtime.getRuntime().availableProcessors();
  private static final int MAX_TRIES = 5;

  @TaskAction
  public void doTask()
      throws ParserConfigurationException, SAXException, IOException, InterruptedException {
    File out = new File(getAssetsDir(), "objects");
    out.mkdirs();

    AssetIndex index = getIndex();

    for (Entry<String, AssetEntry> e : index.objects.entrySet()) {
      Asset asset = new Asset(e.getValue().hash, e.getValue().size);
      File file = new File(out, asset.path);

      // exists but not the right size?? delete
      if (file.exists() && file.length() != asset.size) file.delete();

      // does the file exist (still) ??
      if (!file.exists()) filesLeft.offer(asset);
    }

    getLogger().info("Finished parsing JSON");
    int max = filesLeft.size();
    getLogger().info("Files Missing: " + max + "/" + index.objects.size());

    // get number of threads
    int threadNum = max / 100;
    if (threadNum == 0 && max > 0) threadNum++; // atleats 1 thread

    // spawn threads
    for (int i = 0; i < threadNum; i++) spawnThread();

    getLogger().info("Threads initially spawned: " + threadNum);

    while (stillRunning()) {
      int done = max - filesLeft.size();
      getLogger()
          .lifecycle(
              "Current status: "
                  + done
                  + "/"
                  + max
                  + "   "
                  + (int) ((double) done / max * 100)
                  + "%");
      spawnThread();
      Thread.sleep(1000);
    }

    if (errored) {
      // CRASH!
      getLogger().error("Something went wrong with the assets downloading!");
      this.setDidWork(false);
      return;
    }
  }

  private void spawnThread() {
    if (threads.size() < MAX_THREADS) {
      getLogger().debug("Spawning thread #" + (threads.size() + 1));
      AssetsThread thread = new AssetsThread();
      thread.start();
      threads.add(thread);
    }
  }

  private boolean stillRunning() {
    for (Thread t : threads) {
      if (t.isAlive()) {
        return true;
      }
    }
    getLogger().info("All " + threads.size() + " threads Complete");
    return false;
  }

  public File getAssetsDir() {
    return assetsDir.call();
  }

  public void setAssetsDir(DelayedFile assetsDir) {
    this.assetsDir = assetsDir;
  }

  public AssetIndex getIndex() {
    return index.call();
  }

  public void setIndex(Closure<AssetIndex> index) {
    this.index = index;
  }

  private static class Asset {
    public final String path;
    public final String hash;
    public final long size;

    Asset(String hash, long size) {
      this.path = hash.substring(0, 2) + "/" + hash;
      this.hash = hash.toLowerCase();
      this.size = size;
    }
  }

  private class AssetsThread extends Thread {
    public AssetsThread() {
      this.setDaemon(true);
    }

    @Override
    public void run() {
      Asset asset;
      while ((asset = filesLeft.poll()) != null) {
        for (int i = 1; i < MAX_TRIES + 1; i++) {
          try {
            File file = new File(getAssetsDir(), "objects/" + asset.path);

            // does exist? create
            if (!file.exists()) {
              file.getParentFile().mkdirs();
              file.createNewFile();
            }

            File localMc = new File(minecraftDir, asset.path);
            BufferedInputStream stream;

            // check for local copy
            if (localMc.exists() && Constants.hash(localMc, "SHA1").equals(asset.hash))
              // if so, copy
              stream = new BufferedInputStream(Files.newInputStreamSupplier(localMc).getInput());
            else
              // otherwise download
              stream =
                  new BufferedInputStream(
                      new URL(Constants.ASSETS_URL + "/" + asset.path).openStream());

            Files.write(ByteStreams.toByteArray(stream), file);
            stream.close();

            // check hash...
            String hash = Constants.hash(file, "SHA1");
            if (asset.hash.equals(hash)) break; // hashes are fine;
            else {
              file.delete();
              getLogger()
                  .error("download attempt " + i + " failed! : " + asset.hash + " != " + hash);
            }
          } catch (Exception e) {
            getLogger().error("Error downloading asset: " + asset.path);
            e.printStackTrace();
            if (!errored) errored = true;
          }
        }
      }
    }
  }
}