about summary refs log tree commit diff
path: root/pkgs/games
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/games')
-rw-r--r--pkgs/games/gog/crosscode.nix4
-rw-r--r--pkgs/games/gog/default.nix4
-rw-r--r--pkgs/games/gog/fetch-gog/default.nix40
-rw-r--r--pkgs/games/gog/homm3/default.nix97
-rw-r--r--pkgs/games/gog/homm3/launcher-execl.patch36
-rw-r--r--pkgs/games/gog/kingdoms-and-castles.nix14
-rw-r--r--pkgs/games/gog/the-longest-journey/default.nix106
-rw-r--r--pkgs/games/gog/the-longest-journey/predefined-config.patch29
-rw-r--r--pkgs/games/gog/warcraft2/default.nix148
-rw-r--r--pkgs/games/gog/warcraft2/xdg.patch37
-rw-r--r--pkgs/games/humblebundle/default.nix1
-rw-r--r--pkgs/games/humblebundle/fetch-humble-bundle/default.nix91
-rw-r--r--pkgs/games/humblebundle/fetch-humble-bundle/guard-code.patch121
-rw-r--r--pkgs/games/humblebundle/minimetro.nix15
-rw-r--r--pkgs/games/itch/fetch-itch/default.nix1
-rw-r--r--pkgs/games/steam/fetchsteam/default.nix1
16 files changed, 718 insertions, 27 deletions
diff --git a/pkgs/games/gog/crosscode.nix b/pkgs/games/gog/crosscode.nix
index 9e60ab08..f33cdb45 100644
--- a/pkgs/games/gog/crosscode.nix
+++ b/pkgs/games/gog/crosscode.nix
@@ -2,12 +2,12 @@
 
 buildGame rec {
   name = "crosscode-${version}";
-  version = "1.0.2";
+  version = "1.1.0";
 
   src = fetchGog {
     productId = 1252295864;
     downloadName = "en3installer0";
-    sha256 = "0gd3i99g79w7nr6dnkjkpfq5s2y20dwrf706ipzkggknygmg9xad";
+    sha256 = "1rqf1vlg151hxy5f9nwldmb4l3853dmvcf7fiakab8vzsmjmldlm";
   };
 
   nativeBuildInputs = [ makeWrapper ];
diff --git a/pkgs/games/gog/default.nix b/pkgs/games/gog/default.nix
index 1ef58c30..f1010a00 100644
--- a/pkgs/games/gog/default.nix
+++ b/pkgs/games/gog/default.nix
@@ -15,13 +15,17 @@ let
     crosscode = callPackage ./crosscode.nix {};
     dungeons3 = callPackage ./dungeons3.nix {};
     epistory = callPackage ./epistory.nix { };
+    homm3 = callPackage ./homm3 {};
+    kingdoms-and-castles = callPackage ./kingdoms-and-castles.nix {};
     overload = callPackage ./overload.nix {};
     party-hard = callPackage ./party-hard.nix {};
     satellite-reign = callPackage ./satellite-reign.nix {};
     settlers2 = callPackage ./settlers2.nix {};
     stardew-valley = callPackage ./stardew-valley.nix {};
+    the-longest-journey = callPackage ./the-longest-journey {};
     thimbleweed-park = callPackage ./thimbleweed-park.nix {};
     war-for-the-overworld = callPackage ./war-for-the-overworld.nix {};
+    warcraft2 = callPackage ./warcraft2 {};
     wizard-of-legend = callPackage ./wizard-of-legend.nix {};
     xeen = callPackage ./xeen.nix {};
   };
diff --git a/pkgs/games/gog/fetch-gog/default.nix b/pkgs/games/gog/fetch-gog/default.nix
index b49f0dab..c1d350ce 100644
--- a/pkgs/games/gog/fetch-gog/default.nix
+++ b/pkgs/games/gog/fetch-gog/default.nix
@@ -147,13 +147,24 @@ let
       }
     '';
 
-  in runCommandCC "get-captcha" {
-    nativeBuildInputs = [ pkgconfig ];
+  in stdenv.mkDerivation {
+    name = "get-captcha";
+
+    dontUnpack = true;
+
+    nativeBuildInputs = [ pkgconfig (qt5.wrapQtAppsHook or null) ];
     buildInputs = [ qt5.qtbase qt5.qtwebengine ];
-  } ''
-    g++ $(pkg-config --libs --cflags Qt5WebEngineWidgets Qt5WebEngine) \
-      -Wall -std=c++11 -o "$out" ${application}
-  '';
+    preferLocalBuild = true;
+
+    buildPhase = ''
+      g++ $(pkg-config --libs --cflags Qt5WebEngineWidgets Qt5WebEngine) \
+        -Wall -std=c++11 -o get-captcha ${application}
+    '';
+
+    installPhase = ''
+      install -vD get-captcha "$out/bin/get-captcha"
+    '';
+  };
 
   mkPyStr = str: "'${stdenv.lib.escape ["'" "\\"] (toString str)}'";
 
@@ -177,7 +188,7 @@ let
       def login(self):
         browser = mechanicalsoup.StatefulBrowser()
         response = browser.open(${mkPyStr authURL})
-        if "google.com/recaptcha" in response.text:
+        if "https://www.recaptcha.net/recaptcha" in response.text:
           token_url = self.login_with_captcha()
         else:
           browser.select_form('form[name="login"]')
@@ -185,13 +196,19 @@ let
           browser['login[password]'] = ${mkPyStr password}
           browser.submit_selected()
 
-          auth_code = parse_qs(urlsplit(browser.get_url()).query)['code']
+          query = parse_qs(urlsplit(browser.get_url()).query)
+
+          if 'code' not in query:
+            sys.stderr.write(
+              "Unable to login with the provided GOG credentials.\n"
+            )
+            raise SystemExit(1)
 
           token_url = "https://auth.gog.com/token?" + urlencode({
             'client_id': ${mkPyStr clientId},
             'client_secret': ${mkPyStr clientSecret},
             'grant_type': 'authorization_code',
-            'code': auth_code,
+            'code': query['code'],
             'redirect_uri': ${mkPyStr redirectUri}
           })
 
@@ -203,7 +220,8 @@ let
 
       def login_with_captcha(self):
         sys.stderr.write("Solving a captcha is required to log in.\n")
-        sys.stderr.write("Please run " ${mkPyStr getCaptcha} " now.\n")
+        sys.stderr.write("Please run " ${mkPyStr getCaptcha}
+                         "/bin/get-captcha now.\n")
         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         sys.stderr.write("Waiting for connection")
         i = 0
@@ -278,6 +296,8 @@ in stdenv.mkDerivation {
   outputHashAlgo = "sha256";
   outputHash = sha256;
 
+  preferLocalBuild = true;
+
   nativeBuildInputs = [
     curl python3Packages.tabulate python3Packages.MechanicalSoup
   ];
diff --git a/pkgs/games/gog/homm3/default.nix b/pkgs/games/gog/homm3/default.nix
new file mode 100644
index 00000000..5aecb8f2
--- /dev/null
+++ b/pkgs/games/gog/homm3/default.nix
@@ -0,0 +1,97 @@
+{ stdenv, lib, buildSandbox, fetchGog, runCommand, makeWrapper, fetchFromGitHub
+, cmake, pkgconfig, python3, boost, zlib, minizip, qt5
+, SDL2, SDL2_image, SDL2_mixer, SDL2_ttf
+, innoextract, parallel, ffmpeg
+}:
+
+let
+  data = runCommand "homm3-complete-data" rec {
+    version = "4.0";
+
+    # We need a newer version that 1.7, because GOG uses a newer archive
+    # format.
+    nativeBuildInputs = lib.singleton (innoextract.overrideAttrs (drv: {
+      src = fetchFromGitHub {
+        owner = "dscharrer";
+        repo = "innoextract";
+        rev = "4c61bc4da822fc89f2e05bdb2c45e6c4dd7a3673";
+        sha256 = "197pr7dzlza4isssvhqhvnrr7wzc9c4b3wnnp03sxpmhviyidln1";
+      };
+    })) ++ [ parallel ffmpeg ];
+
+    data = fetchGog {
+      name = "setup_homm_3_complete_${version}.bin";
+      productId = 1207658787;
+      downloadName = "en1installer1";
+      sha256 = "1wfly3024yi64kaczfdca4wx5g09053dpc1gwp08w637833n4kq4";
+    };
+
+    setup = fetchGog {
+      name = "setup_homm_3_complete_${version}.exe";
+      productId = 1207658787;
+      downloadName = "en1installer0";
+      sha256 = "1cwr28ml9z3iq6q9z1vs1jkbnjjrkv2m39bhqw78a5hvj43mgxza";
+    };
+  } ''
+    ln -s "$data" archive-1.bin
+    ln -s "$setup" archive.exe
+    innoextract -L -I Data -I Maps -I Mp3 archive.exe
+    mkdir -p "$out/music"
+    parallel -v ffmpeg -hide_banner -loglevel warning -i {} -acodec libvorbis \
+      "$out/music/{/.}.ogg" ::: mp3/*.mp3
+    mv -t "$out" data maps
+  '';
+
+  engine = stdenv.mkDerivation rec {
+    name = "vcmi-${version}";
+    version = "20190609";
+
+    src = fetchFromGitHub {
+      owner = "vcmi";
+      repo = "vcmi";
+      rev = "e7bced112cf36007da8f418ba3313d2dd4b3e045";
+      sha256 = "0qk0mpz3amg2kw5m99bk3qi19rwcwjj6s1lclby1ws0v8nxh2cmb";
+      fetchSubmodules = true;
+    };
+
+    inherit data;
+
+    patches = [ ./launcher-execl.patch ];
+
+    postPatch = ''
+      find -type f -name '*.cpp' -exec sed -i -e '/^ *# *include/ {
+        s!["<]SDL_\(ttf\|image\|mixer\)\.h[">]!<SDL2/SDL_\1.h>!
+      }' {} +
+
+      sed -i -e 's/"Mp3"/"music"/' config/filesystem.json
+    '';
+
+    cmakeFlags = [ "-DCMAKE_INSTALL_LIBDIR=lib" "-DENABLE_TEST=0" ];
+    enableParallelBuilding = true;
+    nativeBuildInputs = [ cmake pkgconfig python3 makeWrapper ];
+    buildInputs = [
+      boost zlib minizip SDL2 SDL2_image SDL2_mixer SDL2_ttf ffmpeg
+      qt5.qtbase
+    ];
+    postInstall = let
+      inherit (qt5.qtbase) qtPluginPrefix;
+      qtPlugins = "${qt5.qtbase}/${qtPluginPrefix}";
+    in ''
+      rm "$out/bin/vcmibuilder"
+      for i in "$out/bin/"*; do
+        rpath="$(patchelf --print-rpath "$i")"
+        patchelf --set-rpath "$out/lib/vcmi:$rpath" "$i"
+      done
+
+      wrapProgram "$out/bin/vcmilauncher" \
+        --suffix QT_PLUGIN_PATH : ${lib.escapeShellArg qtPlugins}
+      cp -rst "$out/share/vcmi" "$data"/*
+    '';
+    dontStrip = true;
+  };
+
+in buildSandbox engine {
+  allowBinSh = true;
+  paths.required = [ "$XDG_DATA_HOME/vcmi" "$XDG_CONFIG_HOME/vcmi" ];
+  paths.runtimeVars = [ "LD_LIBRARY_PATH" "LOCALE_ARCHIVE" ];
+}
diff --git a/pkgs/games/gog/homm3/launcher-execl.patch b/pkgs/games/gog/homm3/launcher-execl.patch
new file mode 100644
index 00000000..fae0fa80
--- /dev/null
+++ b/pkgs/games/gog/homm3/launcher-execl.patch
@@ -0,0 +1,36 @@
+diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp
+index a07774ed2..3275af71a 100644
+--- a/launcher/mainwindow_moc.cpp
++++ b/launcher/mainwindow_moc.cpp
+@@ -11,7 +11,7 @@
+ #include "mainwindow_moc.h"
+ #include "ui_mainwindow_moc.h"
+ 
+-#include <QProcess>
++#include <unistd.h>
+ #include <QDir>
+ 
+ #include "../lib/CConfigHandler.h"
+@@ -77,19 +77,11 @@ void MainWindow::on_startGameButton_clicked()
+ 
+ void MainWindow::startExecutable(QString name)
+ {
+-	QProcess process;
+-
+-	// Start the executable
+-	if(process.startDetached(name))
+-	{
+-		close(); // exit launcher
+-	}
+-	else
+-	{
++	if (execl(name.toLatin1().data(), "vcmiclient", nullptr) == -1) {
++		QString msg("Failed to start %1\nReason: %2");
+ 		QMessageBox::critical(this,
+ 		                      "Error starting executable",
+-		                      "Failed to start " + name + "\n"
+-		                      "Reason: " + process.errorString(),
++							  msg.arg(name).arg(strerror(errno)),
+ 		                      QMessageBox::Ok,
+ 		                      QMessageBox::Ok);
+ 		return;
diff --git a/pkgs/games/gog/kingdoms-and-castles.nix b/pkgs/games/gog/kingdoms-and-castles.nix
new file mode 100644
index 00000000..e31551cc
--- /dev/null
+++ b/pkgs/games/gog/kingdoms-and-castles.nix
@@ -0,0 +1,14 @@
+{ buildUnity, fetchGog }:
+
+buildUnity {
+  name = "kingdoms-and-castles";
+  fullName = "KingdomsAndCastles";
+  saveDir = "LionShield/Kingdoms and Castles";
+  version = "115r12";
+
+  src = fetchGog {
+    productId = 2067763543;
+    downloadName = "en3installer0";
+    sha256 = "1ag03piq09z7hljcbs145hyj8z0gjcvffj99znf3mnbw2qipb7pq";
+  };
+}
diff --git a/pkgs/games/gog/the-longest-journey/default.nix b/pkgs/games/gog/the-longest-journey/default.nix
new file mode 100644
index 00000000..9dca199a
--- /dev/null
+++ b/pkgs/games/gog/the-longest-journey/default.nix
@@ -0,0 +1,106 @@
+{ stdenv, lib, fetchGog, fetchFromGitHub, innoextract, runCommand, buildSandbox
+, SDL2, SDL2_net, freetype, libGLU_combined, glew, alsaLib
+, libogg, libvorbis, xvfb_run
+}:
+
+let
+  gameData = runCommand "the-longest-journey-data" rec {
+    version = "142";
+
+    # We need a newer version that 1.7, because GOG uses a newer archive
+    # format.
+    nativeBuildInputs = lib.singleton (innoextract.overrideAttrs (drv: {
+      src = fetchFromGitHub {
+        owner = "dscharrer";
+        repo = "innoextract";
+        rev = "4c61bc4da822fc89f2e05bdb2c45e6c4dd7a3673";
+        sha256 = "197pr7dzlza4isssvhqhvnrr7wzc9c4b3wnnp03sxpmhviyidln1";
+      };
+    }));
+
+    data = fetchGog {
+      name = "the-longest-journey-${version}.bin";
+      productId = 1207658794;
+      downloadName = "en1installer1";
+      sha256 = "08jg5snlxkzxppq37lsmbhgv9zhwnk1zr4cid5gynzq9b1048rzc";
+    };
+
+    setup = fetchGog {
+      name = "the-longest-journey-${version}.exe";
+      productId = 1207658794;
+      downloadName = "en1installer0";
+      sha256 = "1h4c2bhf5mhz004r37dwdydl3rhpg1wyr4kyvxxwma7x9grxqyzc";
+    };
+  } ''
+    ln -s "$data" archive-1.bin
+    ln -s "$setup" archive.exe
+    innoextract -L -m archive.exe
+    mkdir "$out"
+    mv -t "$out" \
+      game.exe gui.ini chapters.ini language.ini x.xarc \
+      static global fonts [a-f0-9][a-f0-9]
+  '';
+
+  residualvm = stdenv.mkDerivation rec {
+    name = "residualvm-${version}";
+    version = "20190611";
+
+    src = fetchFromGitHub {
+      owner = "residualvm";
+      repo = "residualvm";
+      rev = "ae1a7fbf6fa6bf88a7adebaedb2cd713d5ccc718";
+      sha256 = "1521578jis9s3ilz0ws0msanviyqf70dp54db3d6ssfikc0w3myx";
+    };
+
+    patches = [ ./predefined-config.patch ];
+
+    # Current Git version has an --enable-static option so the stdenv setup
+    # thinks that there is --disable-static as well, which doesn't exist.
+    dontDisableStatic = true;
+
+    enableParallelBuilding = true;
+    buildInputs = [
+      SDL2 SDL2_net freetype libGLU_combined glew alsaLib
+      libogg libvorbis
+    ];
+
+    configureFlags = [ "--disable-all-engines" "--enable-engine=stark" ];
+  };
+
+  configFile = runCommand "residualvm-stark.ini" {
+    nativeBuildInputs = [ xvfb_run residualvm ];
+    inherit gameData;
+  } ''
+    xvfb-run residualvm -p "$gameData" -a
+    sed -e '/^\[residualvm\]/a enable_unsupported_game_warning=false' \
+      residualvm.ini > "$out"
+  '';
+
+  unsandboxed = runCommand "the-longest-journey-${gameData.version}" {
+    residualCmd = "${residualvm}/bin/residualvm";
+    configArgs = let
+      mkXdg = what: fallback: extra: let
+        basePath = "\${XDG_${what}_HOME:-$HOME/${fallback}}";
+      in "\"${basePath}/the-longest-journey${extra}\"";
+    in [
+      "--savepath=${mkXdg "DATA" ".local/share" ""}"
+      "--config=${mkXdg "CONFIG" ".config" "/settings.ini"}"
+    ];
+    inherit (stdenv) shell;
+    residualArgs = lib.escapeShellArgs [ "--predefined-config=${configFile}" ];
+  } ''
+    mkdir -p "$out/bin"
+    cat > "$out/bin/the-longest-journey" <<EOF
+    #!$shell
+    exec $residualCmd $residualArgs $configArgs "\$@" tlj-win
+    EOF
+    chmod +x "$out/bin/the-longest-journey"
+  '';
+
+in buildSandbox unsandboxed {
+  paths.required = [
+    "$XDG_CONFIG_HOME/the-longest-journey"
+    "$XDG_DATA_HOME/the-longest-journey"
+  ];
+  paths.runtimeVars = [ "LD_LIBRARY_PATH" ];
+}
diff --git a/pkgs/games/gog/the-longest-journey/predefined-config.patch b/pkgs/games/gog/the-longest-journey/predefined-config.patch
new file mode 100644
index 00000000..504e3867
--- /dev/null
+++ b/pkgs/games/gog/the-longest-journey/predefined-config.patch
@@ -0,0 +1,29 @@
+diff --git a/base/commandLine.cpp b/base/commandLine.cpp
+index ab741917..8723fc0d 100644
+--- a/base/commandLine.cpp
++++ b/base/commandLine.cpp
+@@ -425,6 +425,9 @@ Common::String parseCommandLine(Common::StringMap &settings, int argc, const cha
+ 			DO_LONG_COMMAND("list-saves")
+ 			END_COMMAND
+ 
++			DO_LONG_OPTION("predefined-config")
++			END_OPTION
++
+ 			DO_OPTION('c', "config")
+ 			END_OPTION
+ 
+diff --git a/base/main.cpp b/base/main.cpp
+index 2fbfc679..d74b15e5 100644
+--- a/base/main.cpp
++++ b/base/main.cpp
+@@ -394,6 +394,10 @@ extern "C" int scummvm_main(int argc, const char * const argv[]) {
+ 	Common::StringMap settings;
+ 	command = Base::parseCommandLine(settings, argc, argv);
+ 
++	// Load config file with predefined options
++	if (settings.contains("predefined-config"))
++		ConfMan.loadConfigFile(settings["predefined-config"]);
++
+ 	// Load the config file (possibly overridden via command line):
+ 	if (settings.contains("config")) {
+ 		ConfMan.loadConfigFile(settings["config"]);
diff --git a/pkgs/games/gog/warcraft2/default.nix b/pkgs/games/gog/warcraft2/default.nix
new file mode 100644
index 00000000..c576439f
--- /dev/null
+++ b/pkgs/games/gog/warcraft2/default.nix
@@ -0,0 +1,148 @@
+{ stdenv, lib, buildSandbox, writeTextFile, runCommand, fetchGog, fetchurl
+, fetchFromGitHub, winePackages, xvfb_run, ffmpeg, rename
+
+# Dependencies for the Stratagus engine
+, cmake, pkgconfig, toluapp, lua5_1, libpng, libmng, zlib, SDL, fluidsynth
+, bzip2, libmikmod, libogg, libvorbis, libtheora, libGLU_combined, sqlite
+}:
+
+let
+  timgm6mb = fetchurl {
+    name = "TimGM6mb.sf2";
+    url = "https://sourceforge.net/p/mscore/code/3412/"
+        + "tree/trunk/mscore/share/sound/TimGM6mb.sf2?format=raw";
+    sha256 = "0m68a5z43nirirq9rj2xzz6z5qpyhdwk40s83sqhr4lc09i8ndy5";
+  };
+
+  stratagus = stdenv.mkDerivation {
+    name = "stratagus-${version}";
+    version = "2.4.2git20190615";
+
+    src = fetchFromGitHub {
+      owner = "Wargus";
+      repo = "stratagus";
+      rev = "c7fc80ff7e89ab969d867121b6f679f81ea60ecb";
+      sha256 = "1n39lxd8qg03kw884llcal3h95y34lys44hib2mdb3qhd5dg9a18";
+    };
+
+    patches = [ ./xdg.patch ];
+
+    # Both check_version and detectPresence in addition to a bunch of functions
+    # in stratagus-tinyfiledialogs.h are trying to run various tools via
+    # /bin/sh, so let's NOP them out.
+    postPatch = ''
+      sed -i -e '/^int/!s/\(check_version\|detectPresence\)([^)]*)/1/g' \
+        gameheaders/stratagus-game-launcher.h
+      sed -i -e '/^\(static \+\)\?int/!s/[a-zA-Z0-9]\+Present *([^)]*)/0/g' \
+        gameheaders/stratagus-tinyfiledialogs.h
+    '';
+
+    NIX_CFLAGS_COMPILE = [ "-Wno-error=format-overflow" ];
+    cmakeFlags = [
+      "-DOpenGL_GL_PREFERENCE=GLVND" "-DENABLE_DEV=ON"
+      "-DGAMEDIR=${placeholder "out"}/bin"
+    ];
+    nativeBuildInputs = [ cmake pkgconfig toluapp ];
+    buildInputs = [
+      toluapp lua5_1 libpng libmng zlib SDL fluidsynth bzip2 libmikmod libogg
+      libvorbis libtheora libGLU_combined sqlite
+    ];
+  };
+
+  stormlib = stdenv.mkDerivation {
+    name = "stormlib-${version}";
+    version = "9.22git20190615";
+
+    src = fetchFromGitHub {
+      owner = "ladislav-zezula";
+      repo = "StormLib";
+      rev = "2f0e0e69e6b3739d7c450ac3d38816aee45ac3c2";
+      sha256 = "04f43c6bwfxiiw1kplxb3ds8g9r633y587z8ir97hrrzw5nmni3w";
+    };
+
+    nativeBuildInputs = [ cmake ];
+    buildInputs = [ zlib bzip2 ];
+  };
+
+  wargus = stdenv.mkDerivation {
+    name = "wargus-${version}";
+    version = "2.4.2git20190615";
+
+    src = fetchFromGitHub {
+      owner = "Wargus";
+      repo = "wargus";
+      rev = "932c4974dfea9805f6710f254de191e65dadb50d";
+      sha256 = "13vpfd4yx43n5sqzj79km7gcv9al3mqskkij335f0c9p28rqf47v";
+    };
+
+    # This fixes up the data path by letting it be set using an environment
+    # variable later in the wrapper and also hardcodes the MuseScore sound
+    # font.
+    #
+    # In addition, wartool.cpp contains a few lines like this:
+    #
+    #   sprintf(extract, "%s.something", extract);
+    #
+    # The problem here is that sprintf() modifies the data pointed by extract
+    # *IN PLACE*, so this essentially truncates the contents to ".something".
+    #
+    # While it may be even better to use strncat() here, the application isn't
+    # critical for security, so for the sake of laziness, let's just use
+    # strcat() instead.
+    postPatch = ''
+      sed -i -e '
+        s/^\( *# *define \+DATA_PATH\).*/\1 getenv("WARGUS_DATAPATH")/
+      ' wargus.cpp
+
+      sed -i -e 's!"[^"]*\/TimGM6mb.sf2"!"${timgm6mb}"!g' \
+        wargus.cpp scripts/stratagus.lua
+
+      sed -i -e '
+        s/sprintf( *\([^,]\+\), "%s\([^"]\+\)", *\1 *)/strcat(\1, "\2")/
+      ' wartool.cpp
+
+      # XXX: Crashes the game, see https://github.com/Wargus/wargus/issues/260
+      rm -r scripts/lists/campaigns/For\ the\ Motherland\ *
+    '';
+
+    cmakeFlags = [ "-DGAMEDIR=${placeholder "out"}/bin" ];
+    nativeBuildInputs = [ cmake pkgconfig ];
+    buildInputs = [ stratagus zlib libpng bzip2 stormlib ];
+  };
+
+  version = "2.02.4";
+
+  # XXX: Unfortunately, innoextract (even Git master) doesn't support this
+  #      archive, so let's resort to a headless wine for extraction.
+  gameData = runCommand "warcraft2-data-${version}" {
+    src = fetchGog {
+      name = "setup-warcraft-2-${version}.exe";
+      productId = 1418669891;
+      downloadName = "en1installer0";
+      sha256 = "1cf3c1ylgdrvkk7y25v47f66m6lp9m4wvl2aldpxzrrqdrlk34k3";
+    };
+    nativeBuildInputs = [ winePackages.minimal xvfb_run ffmpeg wargus rename ];
+  } ''
+    export WINEPREFIX="$PWD"
+    wine wineboot.exe
+    xvfb-run -s '-screen 0 1024x768x24' wine "$src" /sp- /lang=english /silent
+    gameDir='drive_c/GOG Games/Warcraft II BNE'
+    find "$gameDir" -mindepth 1 -depth \
+      -exec rename 's/(.*)\/([^\/]*)/$1\/\L$2/' {} \;
+    wartool -v -r "$gameDir" "$out"
+    cp -rnst "$out" ${lib.escapeShellArg wargus}/share/games/stratagus/wargus/*
+  '';
+
+in buildSandbox (writeTextFile {
+  name = "warcraft2-${version}";
+  destination = "/bin/warcraft2";
+  executable = true;
+  text = ''
+    #!${stdenv.shell}
+    export WARGUS_DATAPATH=${lib.escapeShellArg gameData}
+    exec ${lib.escapeShellArg "${wargus}/bin/wargus"} "$@"
+  '';
+}) {
+  paths.required = [ "$XDG_DATA_HOME/wc2" ];
+  paths.runtimeVars = [ "LD_LIBRARY_PATH" ];
+}
diff --git a/pkgs/games/gog/warcraft2/xdg.patch b/pkgs/games/gog/warcraft2/xdg.patch
new file mode 100644
index 00000000..979a86e3
--- /dev/null
+++ b/pkgs/games/gog/warcraft2/xdg.patch
@@ -0,0 +1,37 @@
+diff --git a/src/stratagus/parameters.cpp b/src/stratagus/parameters.cpp
+index a705ba21d..8585856b0 100644
+--- a/src/stratagus/parameters.cpp
++++ b/src/stratagus/parameters.cpp
+@@ -48,26 +48,13 @@ void Parameters::SetDefaultValues()
+ 
+ void Parameters::SetDefaultUserDirectory()
+ {
+-#ifdef USE_GAME_DIR
+-	userDirectory = StratagusLibPath;
+-#elif USE_WIN32
+-	userDirectory = getenv("APPDATA");
+-#else
+-	userDirectory = getenv("HOME");
+-#endif
+-
+-	if (!userDirectory.empty()) {
+-		userDirectory += "/";
++	const char *xdg_data_home = getenv("XDG_DATA_HOME");
++	if (xdg_data_home == NULL) {
++		userDirectory = getenv("HOME");
++		userDirectory += "/.local/share";
++	} else {
++		userDirectory = xdg_data_home;
+ 	}
+-
+-#ifdef USE_GAME_DIR
+-#elif USE_WIN32
+-	userDirectory += "Stratagus";
+-#elif defined(USE_MAC)
+-	userDirectory += "Library/Stratagus";
+-#else
+-	userDirectory += ".stratagus";
+-#endif
+ }
+ 
+ static std::string GetLocalPlayerNameFromEnv()
diff --git a/pkgs/games/humblebundle/default.nix b/pkgs/games/humblebundle/default.nix
index 2193ca50..5de3f40e 100644
--- a/pkgs/games/humblebundle/default.nix
+++ b/pkgs/games/humblebundle/default.nix
@@ -25,6 +25,7 @@ let
     jamestown = callPackage ./jamestown.nix {};
     liads = callPackage ./liads.nix {};
     megabytepunch = callPackage_i686 ./megabytepunch.nix {};
+    minimetro = callPackage ./minimetro.nix {};
     opus-magnum = callPackage ./opus-magnum.nix {};
     owlboy = callPackage ./owlboy.nix {};
     pico-8 = callPackage ./pico-8.nix {};
diff --git a/pkgs/games/humblebundle/fetch-humble-bundle/default.nix b/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
index 063d4a02..1340bce8 100644
--- a/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
+++ b/pkgs/games/humblebundle/fetch-humble-bundle/default.nix
@@ -1,13 +1,20 @@
-{ stdenv, curl, cacert, writeText, fetchFromGitHub, fetchpatch
-, python, pythonPackages
+{ stdenv, curl, cacert, writeText, writeScript, fetchFromGitHub, fetchpatch
+, python, python3, pythonPackages
 
 # Dependencies for the captcha solver
-, pkgconfig, qt5, runCommandCC
+, pkgconfig, qt5
 
 , email, password
 }:
 
-{ name ? null, machineName, downloadName ? "Download", suffix ? "humblebundle", md5 }: let
+{ name ? null
+, machineName
+, downloadName ? "Download"
+, suffix ? "humblebundle"
+, md5
+}:
+
+let
   cafile = "${cacert}/etc/ssl/certs/ca-bundle.crt";
 
   getCaptcha = let
@@ -77,12 +84,34 @@
       }
     '';
 
-  in runCommandCC "get-captcha" {
-    nativeBuildInputs = [ pkgconfig ];
+  in stdenv.mkDerivation {
+    name = "get-captcha";
+
+    dontUnpack = true;
+
+    nativeBuildInputs = [ pkgconfig (qt5.wrapQtAppsHook or null) ];
     buildInputs = [ qt5.qtbase qt5.qtwebengine ];
-  } ''
-    g++ $(pkg-config --libs --cflags Qt5WebEngineWidgets Qt5WebEngine) \
-      -Wall -std=c++11 -o "$out" ${application}
+    preferLocalBuild = true;
+
+    buildPhase = ''
+      g++ $(pkg-config --libs --cflags Qt5WebEngineWidgets Qt5WebEngine) \
+        -Wall -std=c++11 -o get-captcha ${application}
+    '';
+
+    installPhase = ''
+      install -vD get-captcha "$out/bin/get-captcha"
+    '';
+  };
+
+  getGuard = writeScript "get-guard" ''
+    #!${python3.interpreter}
+    import socket
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+      sock.bind(('localhost', 18129))
+      sock.listen(1)
+      with sock.accept()[0] as conn:
+        guard = input("Guard code: ")
+        conn.sendall(guard.encode())
   '';
 
   humbleAPI = pythonPackages.buildPythonPackage rec {
@@ -96,6 +125,8 @@
       sha256 = "1kcg42nh7sbjabim1pbqx14468pypznjy7fx2bv7dicy0sqd9b8j";
     };
 
+    patches = [ ./guard-code.patch ];
+
     postPatch = ''
       sed -i -e '/^LOGIN_URL *=/s,/login,/processlogin,' humblebundle/client.py
       sed -i -e '/self\.supports_canonical.*data.*supports_canonical/d' \
@@ -137,7 +168,8 @@
 
     def login_with_captcha(hb):
       print >>sys.stderr, "Solving a captcha is required to log in."
-      print >>sys.stderr, "Please run " ${pyStr (toString getCaptcha)} " now."
+      print >>sys.stderr, "Please run " ${pyStr (toString getCaptcha)} \
+                          "/bin/get-captcha now."
       sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
       print >>sys.stderr, "Waiting for connection",
       i = 0
@@ -152,13 +184,41 @@
       response = sock.recv(4096)
       sock.close()
       print >>sys.stderr, "Captcha solved correctly, logging in."
-      hb.login(${pyStr email}, ${pyStr password}, recaptcha_response=response)
+      api_login(hb, recaptcha_response=response)
+
+    def login_with_guard(hb, skip_code):
+      print >>sys.stderr, "A guard code has been sent to your email address."
+      print >>sys.stderr, "Please run " ${pyStr (toString getGuard)} " now."
+      sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+      print >>sys.stderr, "Waiting for connection",
+      # XXX: DRY!
+      i = 0
+      while sock.connect_ex(("127.0.0.1", 18129)) != 0:
+        time.sleep(0.1)
+        if i % 10 == 0:
+          sys.stderr.write('.')
+          sys.stderr.flush()
+        i += 1
+      print >>sys.stderr, " connected."
+      print >>sys.stderr, "Waiting for guard code..."
+      response = sock.recv(4096)
+      sock.close()
+      print >>sys.stderr, "Guard code supplied, logging in."
+      api_login(hb, captcha_skip_code=skip_code, guard_code=response)
+
+    def api_login(hb, recaptcha_response=None,
+                  captcha_skip_code=None, guard_code=None):
+      try:
+        hb.login(${pyStr email}, ${pyStr password},
+                 recaptcha_response=recaptcha_response,
+                 captcha_skip_code=captcha_skip_code, guard_code=guard_code)
+      except humblebundle.exceptions.HumbleCaptchaException:
+        login_with_captcha(hb)
+      except humblebundle.exceptions.HumbleGuardRequiredException as e:
+        login_with_guard(hb, e.captcha_skip_code)
 
     hb = humblebundle.HumbleApi()
-    try:
-      hb.login(${pyStr email}, ${pyStr password})
-    except humblebundle.exceptions.HumbleCaptchaException:
-      login_with_captcha(hb)
+    api_login(hb)
 
     products = dict(get_products(hb))
     dstruct = find_download(products)
@@ -183,6 +243,7 @@ in stdenv.mkDerivation {
   outputHashAlgo = "md5";
   outputHash = md5;
 
+  preferLocalBuild = true;
   buildInputs = [ python humbleAPI ];
 
   buildCommand = ''
diff --git a/pkgs/games/humblebundle/fetch-humble-bundle/guard-code.patch b/pkgs/games/humblebundle/fetch-humble-bundle/guard-code.patch
new file mode 100644
index 00000000..f17928ae
--- /dev/null
+++ b/pkgs/games/humblebundle/fetch-humble-bundle/guard-code.patch
@@ -0,0 +1,121 @@
+diff --git a/humblebundle/client.py b/humblebundle/client.py
+index fbc31c9..44184a1 100644
+--- a/humblebundle/client.py
++++ b/humblebundle/client.py
+@@ -75,7 +75,9 @@ class HumbleApi(object):
+     """
+ 
+     @callback
+-    def login(self, username, password, authy_token=None, recaptcha_challenge=None, recaptcha_response=None,
++    def login(self, username, password, authy_token=None,
++              recaptcha_challenge=None, recaptcha_response=None,
++              guard_code=None, captcha_skip_code=None,
+               *args, **kwargs):
+         """
+         Login to the Humble Bundle API. The response sets the _simpleauth_sess cookie which is stored in the session
+@@ -87,6 +89,8 @@ class HumbleApi(object):
+         :type authy_token: integer or str
+         :param str recaptcha_challenge: (optional) The challenge signed by Humble Bundle's public key from reCAPTCHA
+         :param str recaptcha_response: (optional) The plaintext solved CAPTCHA
++        :param str guard_code: (optional) The guard code sent via email
++        :param str captcha_skip_code: (optional) A token to skip the CAPTCHA
+         :param list args: (optional) Extra positional args to pass to the request
+         :param dict kwargs: (optional) Extra keyword args to pass to the request. If a data dict is supplied a key
+                             collision with any of the above params will resolved in favor of the supplied param
+@@ -108,7 +112,9 @@ class HumbleApi(object):
+             'password': password,
+             'authy-token': authy_token,
+             'recaptcha_challenge_field': recaptcha_challenge,
+-            'recaptcha_response_field': recaptcha_response}
++            'recaptcha_response_field': recaptcha_response,
++            'guard': guard_code,
++            'captcha-skip-code': captcha_skip_code}
+         kwargs.setdefault('data', {}).update({k: v for k, v in default_data.items() if v is not None})
+ 
+         response = self._request('POST', LOGIN_URL, *args, **kwargs)
+diff --git a/humblebundle/exceptions.py b/humblebundle/exceptions.py
+index 9041219..fe4eeaf 100644
+--- a/humblebundle/exceptions.py
++++ b/humblebundle/exceptions.py
+@@ -9,7 +9,7 @@ __copyright__ = "Copyright 2014, Joel Pedraza"
+ __license__ = "MIT"
+ 
+ __all__ = ['HumbleException', 'HumbleResponseException', 'HumbleAuthenticationException', 'HumbleCredentialException',
+-           'HumbleCaptchaException', 'HumbleTwoFactorException', 'HumbleParseException']
++           'HumbleCaptchaException', 'HumbleTwoFactorException', 'HumbleGuardRequiredException', 'HumbleParseException']
+ 
+ from requests import RequestException
+ 
+@@ -38,6 +38,7 @@ class HumbleAuthenticationException(HumbleResponseException):
+     def __init__(self, *args, **kwargs):
+         self.captcha_required = kwargs.pop('captcha_required', None)
+         self.authy_required = kwargs.pop('authy_required', None)
++        self.captcha_skip_code = kwargs.pop('captcha_skip_code', None)
+         super(HumbleAuthenticationException, self).__init__(*args, **kwargs)
+ 
+ 
+@@ -62,6 +63,13 @@ class HumbleTwoFactorException(HumbleAuthenticationException):
+     pass
+ 
+ 
++class HumbleGuardRequiredException(HumbleAuthenticationException):
++    """
++    A guard code is required
++    """
++    pass
++
++
+ class HumbleParseException(HumbleResponseException):
+     """
+     An error occurred while parsing
+diff --git a/humblebundle/handlers.py b/humblebundle/handlers.py
+index 36fc6e1..a8acebf 100644
+--- a/humblebundle/handlers.py
++++ b/humblebundle/handlers.py
+@@ -64,29 +64,42 @@ def login_handler(client, response):
+     success = data.get('success', None)
+     if success is True:
+         return True
++    if data.get('goto', None) is not None:
++        return True
+ 
+     captcha_required = data.get('captcha_required')
+     authy_required = data.get('authy_required')
++    captcha_skip_code = data.get('skip_code', [None])[0]
++
++    guard = data.get('humble_guard_required', False)
++    if guard:
++        raise HumbleGuardRequiredException('Guard code required', request=response.request, response=response,
++                                           captcha_required=captcha_required, authy_required=authy_required,
++                                           captcha_skip_code=captcha_skip_code)
+ 
+     errors, error_msg = get_errors(data)
+     if errors:
+         captcha = errors.get('captcha')
+         if captcha:
+             raise HumbleCaptchaException(error_msg, request=response.request, response=response,
+-                                         captcha_required=captcha_required, authy_required=authy_required)
++                                         captcha_required=captcha_required, authy_required=authy_required,
++                                         captcha_skip_code=captcha_skip_code)
+ 
+         username = errors.get('username')
+         if username:
+             raise HumbleCredentialException(error_msg, request=response.request, response=response,
+-                                            captcha_required=captcha_required, authy_required=authy_required)
++                                            captcha_required=captcha_required, authy_required=authy_required,
++                                            captcha_skip_code=captcha_skip_code)
+ 
+         authy_token = errors.get("authy-token")
+         if authy_token:
+             raise HumbleTwoFactorException(error_msg, request=response.request, response=response,
+-                                           captcha_required=captcha_required, authy_required=authy_required)
++                                           captcha_required=captcha_required, authy_required=authy_required,
++                                           captcha_skip_code=captcha_skip_code)
+ 
+     raise HumbleAuthenticationException(error_msg, request=response.request, response=response,
+-                                        captcha_required=captcha_required, authy_required=authy_required)
++                                        captcha_required=captcha_required, authy_required=authy_required,
++                                        captcha_skip_code=captcha_skip_code)
+ 
+ 
+ def gamekeys_handler(client, response):
diff --git a/pkgs/games/humblebundle/minimetro.nix b/pkgs/games/humblebundle/minimetro.nix
new file mode 100644
index 00000000..a5c48775
--- /dev/null
+++ b/pkgs/games/humblebundle/minimetro.nix
@@ -0,0 +1,15 @@
+{ buildUnity, fetchHumbleBundle }:
+
+buildUnity rec {
+  name = "minimetro";
+  version = "39";
+  fullName = "Mini Metro";
+  saveDir = "Dinosaur Polo Club/Mini Metro";
+
+  src = fetchHumbleBundle {
+    name = "MiniMetro-release-39-linux.tar.gz";
+    machineName = "minimetro_linux";
+    downloadName = "Download";
+    md5 = "3e7afefbcc68b6295821394e31f5e48b";
+  };
+}
diff --git a/pkgs/games/itch/fetch-itch/default.nix b/pkgs/games/itch/fetch-itch/default.nix
index 3700f5c6..121868be 100644
--- a/pkgs/games/itch/fetch-itch/default.nix
+++ b/pkgs/games/itch/fetch-itch/default.nix
@@ -69,6 +69,7 @@ in stdenv.mkDerivation {
 
   SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";
 
+  preferLocalBuild = true;
   nativeBuildInputs = [ python3Packages.python ];
 
   buildCommand = ''
diff --git a/pkgs/games/steam/fetchsteam/default.nix b/pkgs/games/steam/fetchsteam/default.nix
index 5c1faf55..646e0a14 100644
--- a/pkgs/games/steam/fetchsteam/default.nix
+++ b/pkgs/games/steam/fetchsteam/default.nix
@@ -79,6 +79,7 @@ let
 in with stdenv.lib; runCommand "${name}-src" {
   buildInputs = [ DepotDownloader ];
   inherit username password appId depotId manifestId;
+  preferLocalBuild = true;
   outputHashAlgo = "sha256";
   outputHash = sha256;
   outputHashMode = "recursive";