Ansible meets CurseForge on Linux

If you want to play modern Minecraft ModPacks on CurseForge in Java on Linux, the Twitch Launcher for Windows is not very helpful.

But since the project artifacts are available for public download and the data format is pretty straightforward, it was not a big deal to script a download of all files. I have chosen an Ansible Playbook for this task.

Some experience with this method on different modpacks including upgrade shows, it’s best to assemble the artifacts in a separate folder, which can be copied to the .minecraft game folder simply using rsync.

Install Ansible

For this method you will first need an installation of the Python-based Automation Tool Ansible on your Linux box. But since we are doing this to play modpacks on Linux, the preparation of Ansible as valuable Add-On should be no big deal, see Installation Guide for details. Please prefer an up-to-date release 2.4+ and don’t rely on standard repositories of your distribution too strictly.

Lookup Project Details

Please begin with a visit on the Minecraft ModPack’s Files page at CurseForge resp. Feed the Beast. You will later need a meaningful PROJECT_NAME, the VERSION, the MD5 CHECKSUM and the DOWNLOAD_URL. The PROJECT_NAME is used for a dedicated folder in the local filesystem, so you can set this to something comfortable, even though I recommend a lowercase name without blanks.

Create your Ansible Playbook

Here is a template for CurseForge-PROJECT_NAME.yml in which you normally just need to set 4 variables pack_* on the top. You can also override the values on command line or in host variables, but we will assume, that the information from the previous step goes into the pack_* values.

---
- hosts: localhost

  vars:
# ModPack URL: https://minecraft.curseforge.com/projects/PROJECT_NAME
    pack_name: 'PROJECT_NAME'
    pack_url: 'DOWNLOAD_URL'
    pack_version: 'VERSION'
    pack_sum: 'md5:CHECKSUM'

    base: "{{playbook_dir}}/{{pack_name}}/dist"
    json: "{{playbook_dir}}/{{pack_name}}/manifest.json"
    copy: "{{playbook_dir}}/{{pack_name}}/overrides"
    manifest: "{{lookup('file', json)|from_json}}"

    skip_files: no
    skip_forge: no

  tasks:
  - block:
    # Download ModPack.
    - name: download pack
      get_url:
        url: "{{pack_url}}"
        dest: "{{playbook_dir}}/{{pack_name|default('pack')}}-{{pack_version|default('00')}}.zip"
        checksum: "{{pack_sum|default(omit)}}"
    # Directory for pack
    - name: directory pack
      file:
        path: "{{playbook_dir}}/{{pack_name|default('pack')}}-{{pack_version|default('00')}}"
        state: directory
    # Extract pack.
    - name: extract pack
      unarchive:
        src: "{{playbook_dir}}/{{pack_name|default('pack')}}-{{pack_version|default('00')}}.zip"
        dest: "{{playbook_dir}}/{{pack_name|default('pack')}}-{{pack_version|default('00')}}"
    - name: link pack
      file:
        src: "{{pack_name|default('pack')}}-{{pack_version|default('00')}}"
        path: "{{playbook_dir}}/{{pack_name|default('pack')}}"
        state: link
    when: pack_url is defined

  # Download mods' files from CurseForge.
  - name: create mods target
    file:
      path: "{{base}}/mods"
      state: directory
  - name: download file
    get_url:
      dest: "{{base}}/mods"
      url: "http://minecraft.curseforge.com/projects/{{item.projectID}}/files/{{item.fileID}}/download"
      force: no
    loop: "{{manifest.files}}"
    when: not skip_files

  # Override with files from ModPack.
  - name: check for files to copy
    local_action:
      module: stat
      path: "{{copy}}"
    register: stat_copy
  - name: sync files
    synchronize:
      dest: "{{base}}"
      src: "{{copy}}/"
    when: stat_copy.stat.exists

# Download Forge Installer in the recommended release.
- name: version for forge
  set_fact:
    forge_version: "{{manifest.minecraft.modLoaders[0].id|regex_replace('^forge-', '')}}"
  when: >-
    manifest.minecraft is defined and
    manifest.minecraft.modLoaders is defined and
    (manifest.minecraft.modLoaders[0].id|regex_search('^forge-') != None)    
- name: download forge
  get_url:
    dest: "{{base}}/"
    url: "https://files.minecraftforge.net/maven/net/minecraftforge/forge/{{manifest.minecraft.version}}-{{forge_version}}/forge-{{manifest.minecraft.version}}-{{forge_version}}-installer.jar"
    force: no
  when: not skip_forge and forge_version is defined

Apply ansible-playbook CurseForge-PROJECT_NAME.yml on this Ansible Playbook to download the project file and extract it into a folder PROJECT_NAME-VERSION aside the playbook file. It reads files list in the manifest.json and downloads every entry, synchronizing the additional files in overrides to the destination. It finishes with a download of the recommended Forge Installer.

I have prepared the example to assemble the downloads in a sub-folder dist of PROJECT_NAME. The download of files is disabled by the setting skip_files to false, as well as the download of the Forge Installer by skip_forge.

Minecraft Installation with Forge and ModPack

  • The first time you prepare a new CurseForge ModPack I recommend to start with an empty directory (e.g. $HOME/Minecraft/PROJECT_NAME, that is symlinked to $HOME/.minecraft.
  • Place the official Minecraft Launcher on your disk and start Vanilla Minecraft with a proper login, passing over to Play. After some downloading the Game Menu will show up, in which you can directly Quit Game. Beware, you need the Minecraft Release matching the the project with all the assets, which are downloaded in this step.
  • Apply the Forge Installer downloaded in the previous step to PROJECT_NAME/dist in Client Mode: java -jar forge*-installer.jar
  • Start Vanilla Minecraft with Forge and configure the new Forge Profile (check Minecraft Version, tune memory settings for Java, e.g. -Xmx6G). You can pass over to play for the Game Menu again and this time you should already see the typical Forge Animation. You may directly terminate on success like before.
  • Now it’s time to synchronize the PROJECT_NAME/dist folder: cd PROJECT_NAME && rsync -av dist/ $HOME/.minecraft/

That’s it, you should now be ready to enjoy the new ModPack by starting your launcher and passing over to the Forge Profile.

Update ModPack

In case you want to update the ModPack I recommend the following workflow:

  • Get new VERSION, MD5 CHECKSUM and DOWNLOAD_URL from the well-known project page (I recommende a comment with the ModPack URL in the playbook).
  • Change pack_* variables in the Ansible Playbook accordingly and apply the playbook to download all artifacts of the ModPack: ansible-playbook CurseForge-PROJECT_NAME.yml
  • The playbook creates a new project folder this VERSION and links it to PROJECT_NAME.
  • Backup your Minecraft folder from the previous step and delete $HOME/.minecraft/mods, so you get no conflicts by leftovers.
  • Synchronize the new project PROJECT_NAME/dist: cd PROJECT_NAME && rsync -av dist/ $HOME/.minecraft/

Normally this procedure will update the ModPack, keep your settings and you can easily rollback to the previous version as long as you do not cleanup your backup.

Organizing multiple ModPacks

In my environment I use a shell script to set the link to $HOME/.minecraft before starting the Minecraft Launcher. Usage of this script is often delegated to Desktop Shortcuts in my family.

Keeping all the ModPacks in a folder $HOME/Minecraft, starting with the procedure above, keeping the previous version as backup in the same folder and putting a shell script with the name of the folder extended by .sh is after all this command line stuff quite comfortable.

Here is the example of a shell script $HOME/Minecraft/start_minecraft.sh, that extrapolates the location of the ModPack from it’s name:

#!/bin/bash

set -x

# Link project to $HOME/.minecraft.
ln -snf \
	"$(readlink -f "$(dirname "$0")/$(basename "$0" .sh)")" \
	"$HOME/.minecraft"
cd "$HOME/.minecraft" || exit

# Pass over to Minecraft Launcher.
if [ -s "minecraft-launcher.sh" ]; then
	exec /bin/sh minecraft-launcher.sh "$@"
fi
if [ -s "launcher.jar" ]; then
	exec java -jar launcher.jar "$@"
fi
echo "ERROR: Can't locate launcher in $HOME/.minecraft!" >&2
exit 1

Link or copy this script to $HOME/Minecraft/PROJECT_NAME.sh and it will link your ModPack before starting the game. This will also work on backups.

Troubleshooting

  • The example script for linking before executing the launcher expects references in the folder. So please place the installation files of the launcher in the project folder. The old java launcher is happy with launcher.jar, the latest launcher provides an archive with several files including a script minecraft-launcher.sh.
  • If the Start of the ModPack issues errors or warnings, you should first check, if your Minecraft and Forge Release is (still) matching the recommended version in PROJECT_NAME/dist.
  • Please also check the memory settings in the Minecraft Forge Profile, the standard -Xmx1G is often not enough, try -Xmx4G or better -Xmx6G instead and watch out for recommended memory limits on the Project Page.
  • If the problems still exists after an update, please start over with an empty folder linked to .minecraft like you never installed the PROJECT_NAME before.
  • Sometimes the Download of a mod fails, because the fileID is no longer available. Instead of waiting for an update of the ModPack you can try the following solution:
    • In that case the playbook will continue the Download Section which is looping over all mods and stops before going over to the next step.
    • Investigate the download problems with the URL that is visible in the error message.
    • You can go the Project Page looking for newer versions of the *.jar: http://minecraft.curseforge.com/projects/{{item.projectID}}
    • If you are able to download the file manually, put it into the PROJECT_NAME/dist folder and retry the playbook while skipping the mod downloading: ansible-playbook -e skip_files=yes CurseForge-PROJECT_NAME.yml