Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

AoE2DE CaptureAge on Linux for 2026

I ended up having to use a fuller script from TeknoHamster.

I also had to do some crazy tricks with protontricks or winetricks to run protontrikcs 813780 d3dcompiler_47. I think what ended up working was adjusting the script to run a winetricks command in the WINEPREFIX.

files/2026/listings/captureage.sh (Source)

#!/usr/bin/env bash
# Taken directly from https://gist.githubusercontent.com/TeknoHamster/d8b2f90a1cd618074aa1e35c81e7bc4b/raw/972832a3e37ef32d255804d19aae99550ec9fb26/captureage from https://gist.github.com/TeknoHamster/d8b2f90a1cd618074aa1e35c81e7bc4b
APPID=813780
STEAM_PATH="${STEAM_PATH:-$HOME/.local/share/Steam}"
PROTON_NAME="${PROTON_NAME:-Proton - Experimental}"
export PROTON_LOG=1
export STEAMAPPS="$STEAM_PATH/steamapps"
export STEAM_COMPAT_DATA_PATH="$STEAMAPPS/compatdata/$APPID"
export STEAM_COMPAT_CLIENT_INSTALL_PATH="$STEAM_PATH"
export WINEPREFIX="$STEAM_COMPAT_DATA_PATH/pfx"
export GAME_DIR="$STEAMAPPS/common/AoE2DE"
export GAME_DIR_WIN='S:\common\AoE2DE'
export CAPTUREAGE_STATE="$WINEPREFIX/drive_c/users/steamuser/AppData/Roaming/CaptureAge/persistedState_prod.json"
export CAPTUREAGE_EXE_WIN='C:\users\steamuser\AppData\Local\Programs\CaptureAge\CaptureAge.exe'
export CAPTUREAGE_DIR_WIN='C:\users\steamuser\AppData\Local\Programs\CaptureAge'
export STEAM_COMPAT_INSTALL_PATH="$GAME_DIR"
export STEAM_COMPAT_LIBRARY_PATHS="$STEAMAPPS"
export PROTON_SET_GAME_DRIVE=1
# Adjust the path to the proton executable used by Age of Empires II
export PROTON_EXEC="${PROTON_EXEC:-$STEAMAPPS/common/$PROTON_NAME/proton}"
# export WINEDEBUG=warn+all # This is for debug logs
# export OBS_VKCAPTURE=1 # Enable this for OBS Videogame capture
prepare_prefix() {
  local game_dir_win
  local game_dir_escaped
  game_dir_win="$GAME_DIR_WIN"
  game_dir_escaped="${game_dir_win//\\/\\\\}"
  mkdir -p "$WINEPREFIX/dosdevices"
  ln -sfn "$STEAMAPPS" "$WINEPREFIX/dosdevices/s:"
  if [ -f "$WINEPREFIX/user.reg" ]; then
    GAME_DIR_ESCAPED="$game_dir_escaped" perl -0pi -e 'BEGIN{$r=$ENV{GAME_DIR_ESCAPED}} s#"Install Location"="[^"]*"#"Install Location"="$r"#g' "$WINEPREFIX/user.reg"
  fi
  if [ -f "$CAPTUREAGE_STATE" ]; then
    tmp_state="$(mktemp)"
    jq --arg dir "$game_dir_win" '.lastUsedGameDirectory = $dir' "$CAPTUREAGE_STATE" > "$tmp_state" && mv "$tmp_state" "$CAPTUREAGE_STATE"
  fi
}
reg_add() {
  "$PROTON_EXEC" run reg add "$@"
}
#This creates Windows registries to make the Spectate with CA button work
register_captureage() {
  local protocol_command
  protocol_command="\"$CAPTUREAGE_EXE_WIN\" \"%1\""
  for root in HKCU\\Software\\Classes HKLM\\Software\\Classes; do
    reg_add "$root\\captureage" /ve /d "URL:captureage" /f
    reg_add "$root\\captureage" /v "URL Protocol" /d "" /f
    reg_add "$root\\captureage\\shell\\open\\command" /ve /d "$protocol_command" /f
  done
  for root in HKCU HKLM; do
    reg_add "$root\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\CaptureAge.exe" /ve /d "$CAPTUREAGE_EXE_WIN" /f
    reg_add "$root\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\CaptureAge.exe" /v "Path" /d "$CAPTUREAGE_DIR_WIN" /f
  done
}
#install capture age (only needed once). Adjust the location to point to the installer you downloaded.
#"$PROTON_EXEC" run "$PWD/CaptureAgeSetup.exe"
# run capture age
# Older version of CA:DE were installed in AppData/Local/Programs/CaptureAge . If it doesn't launch for you, check this path
prepare_prefix
register_captureage
"$PROTON_EXEC" run "C:/users/steamuser/AppData/Local/Programs/CaptureAge/CaptureAge.exe"

onfoss.org from browser history

I was on a travel laptop and I was looking through my browser history. I found this really awesome site I didn't recall visiting: onfoss.org. This is some group of people who get together to virtual LAN party!

This is really fascinating, that I didn't remember this at all! This place links to so many free and libre software titles. I've only heard of some of them.

I already play some of them.

I've heard of a few that I haven't played.

And loads I haven't heard of before!

There's so many here that I hadn't heard of before! Most of them are first-person shooters, and I play only a couple of those. But they're all FLOSS, which is very important!

Idea: use protonup for Lego Universe

I have not formalized this process, but I tested it at least once: you can use protonup to get a Wine (Proton) environment for running Lego Universe.

I use Devuan GNU+Linux, release Ceres (unstable) which is based on Debian Sid (unstable), and the current package for wine is version 10.0~repack-12, but 10.0~repack-6 is the last wine package that runs Lego Universe client. I suspect it is an oversight, related to the combining of 32-bit and 64-bit/wow64 stuff I don't understand, and people in Debian unstable don't seem to test or care about.

If you want to fetch 10.0~repack-6, set this in file /etc/apt/sources.list.d/snapshot.list:

# 2025-11-02-1 16:05 Fix DLU Lego Universe client
deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/20251001T203719Z/ unstable main

So for over half a year now I've had to pin an old version of wine, albeit the same upstream version string but a different repack in my Linux distro. I dislike having to pin to old versions of things (that are too big for me to compile myself).

If I ever need to upgrade wine to current (and they haven't fixed the 32-bit/64-bit whatever), I could use protonup to easily fetch GE-Proton.

To install protonup, I had to use a virtual environment.

python3 -m venv ~/venv1
. ~/venv1/bin/activate
pip install protonup
protonup --releases | tail # find highest version
protonup -t GE-Proton11-1

It will prompt to confirm the installation, and also print the deployed directory.

[INFO] Installed in: /home/bgstack15/.steam/root/compatibilitytools.d/GE-Proton11-1

Use that for PROTONPREFIX or WINEPREFIX when running an application.

luanti inventory pouches update from upstream

I have some small fixes for Luanti mod inventory_pouches (ContentDB). I have sent them to upstream when I wrote them, but they have not been absorbed yet.

Upstream had a new commit that fixes color dyeing! It's pretty great. So I didn't need my color fixes anymore, and my remaining patches applied cleanly.

  • persist inventory across server restarts
  • load pouches when player logs in (affects inventory_icon mod)
  • fix pouch inventory alignment (graphical only, MCL)

I wrote a new patch also, to convert the old color system to the new color system, when the player opens the pouch. So as long as the player has used the pouch once, then he can re-dye the pouch in the expected fashion if desired.

I also needed to patch inventory_icon to read the new meta attribute that stores the color.

Screenshot showing dyeing a pouch in Mineclonia

OS Updates notes, July 2026

The postinst script for GNU Screen, that is, the dpkg component that runs after a package gets installed/configured, now depends on functionality provided by systemd-tmpfiles.

Snippet from /var/lib/dpkg/info/screen.postinst

# Automatically added by dh_installtmpfiles/14.1
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
    systemd-tmpfiles ${DPKG_ROOT:+--root="$DPKG_ROOT"} --create screen-cleanup.conf
fi

It doesn't give you the option to ignore errors. I assume it's generated from https://salsa.debian.org/debian/screen/-/blob/master/debian/screen.screen-cleanup.tmpfiles?ref_type=heads

LeePen suggested that I try seedfiles, which I did on a few systems. Both the standalone-tmpfiles and seedfiles worked fine, to let screen install correctly.

Luanti contributions, part 6

Here's an overview of what I've been up to in the Luanti world since the last time.

Jellyfin 10.11 and me

I tried upgrading Jellyfin past 10.10 series to the 10.11 series a few months ago, and the database corruption broke the application for me. I had to restore from backup. I have a rpm-switched-to-docker installation on CentOS 7, so my configs and logs are in RHEL-like places rather than in ${HOME}. Thankfully, restoring from backup is just exploding the tarball which restores paths /etc/jellyfin and /var/lib/jellyfin.

Downgrading went fine, thankfully.

Well, I recently tried to upgrade again, now that they've had many point releases underneath 10.11. The db migration went OK, and I did notice that the home screen and overall navigation feels visibly slower.

The app would crash randomly though, when doing boring things like navigation or checking a video's "watched" checkbox. I couldn't find anything in the /var/log/jellyfin/jellyfin_YYYYMMDD.log file, or in docker-compose logs -f --tail=30. I ended up finding something interesting in /var/log/messages. The application was throwing weird errors, not quite a kernel panic: See Appendix A below.

I googled it, and found that lib_sqlite3.so is trying to use sse4.1. I happen to know the reason this system is using CentOS 7 is because CentOS 8's Linux kernel 4.0 series had dropped the drivers for the RAID controller. So of course this system is also so ancient it definitely doesn't have any sse4 instructions. The way to check is to run </proc/cpuinfo grep sse4.

So I reverted to 10.10.7 again and thankfully my recent backup mostly worked. I swear there's still some sort of performance degredation, particularly of video playback which is the "one job" of Jellyfin. But technically the video did play, eventually. It felt like one of those big mkv files where the system is trying to parse all 20GBs rather than streaming it a little at a time.

And upstream Jellyfin has a way forward for this sse4.1 problem. They have submitted a bug to their upstream which will take time, to not require sse4.1. Apparently lots of other Jellyfin users are using old/underpowered hardware too. So I'll wait even longer for upgrading. Man, I want to stay up to date so I don't have huge jumps in behavior, design, functionality, and config changes. But Jellyfin's 10.11 series has had a bunch of bumps. Don't worry; I'm still never leaving. Jellyfin is FLOSS, and FLOSS is life!

Appendices

Appendix A: Crash entry in /var/log/messages

Jul  8 18:54:27 vm4 kernel: traps: .NET TP Worker[6110] trap invalid opcode ip:7f35222a0dfa sp:7f3515efac00 error:0 in libe_sqlite3.so[7f3522279000+d9000]
Jul  8 18:54:27 vm4 containerd: time="2026-07-08T18:54:27.900368087-04:00" level=info msg="shim disconnected" id=4602ed007f02a68cda8a1ba8236d6d57a327f5622eee8949d2d707a5e2478bcb 
Jul  8 18:54:27 vm4 containerd: time="2026-07-08T18:54:27.902140773-04:00" level=warning msg="cleaning up after shim disconnected" id=4602ed007f02a68cda8a1ba8236d6d57a327f5622eee8949d2d707a5e2478bcb namespace=moby
Jul  8 18:54:27 vm4 containerd: time="2026-07-08T18:54:27.902185128-04:00" level=info msg="cleaning up dead shim"
Jul  8 18:54:27 vm4 dockerd: time="2026-07-08T18:54:27.900515177-04:00" level=info msg="ignoring event" container=4602ed007f02a68cda8a1ba8236d6d57a327f5622eee8949d2d707a5e2478bcb module=libcontainerd namespace=moby topic=/tasks/delete type="*events.TaskDelete"
Jul  8 18:54:27 vm4 containerd: time="2026-07-08T18:54:27.922833969-04:00" level=warning msg="cleanup warnings time=\"2026-07-08T18:54:27-04:00\" level=info msg=\"starting signal loop\" namespace=moby pid=6511 runtime=io.containerd.runc.v2\n"
Jul  8 18:54:28 vm4 containerd: time="2026-07-08T18:54:28.939694602-04:00" level=info msg="loading plugin \"io.containerd.event.v1.publisher\"..." runtime=io.containerd.runc.v2 type=io.containerd.event.v1
Jul  8 18:54:28 vm4 containerd: time="2026-07-08T18:54:28.940012414-04:00" level=info msg="loading plugin \"io.containerd.internal.v1.shutdown\"..." runtime=io.containerd.runc.v2 type=io.containerd.internal.v1
Jul  8 18:54:28 vm4 containerd: time="2026-07-08T18:54:28.940049838-04:00" level=info msg="loading plugin \"io.containerd.ttrpc.v1.task\"..." runtime=io.containerd.runc.v2 type=io.containerd.ttrpc.v1

How to play AoE2DE multiplayer on linux without steam

Overview

I recently purchased the remaining DLCs for Age of Empires II: Definitive Edition (steam) and I wanted to make sure that my ability to play this game survives Steam, my relationship with them, and even the world wide web.

After a lot of research and the benefit of many Free Software tools, I can play multiplayer AoE2DE with my mods without Steam, on Linux! Here is my method for assembling a package for distributing to my systems without Internet. I have tested only for a few minutes between two machines in the same room. I don't know how this would scale to a vpn or wireless clients.

This does not use system Wine, which crashes immediately with some opengl32 problem.

Building the package

This first asset is the proper Steam directory with all my DLCs installed.

tar -zcf aoe2de-alldlcs-raw.2026-06-28.tgz "${HOME}/.steam/steam/steamapps/common/AoE2DE"
  1. Explode my asset aoe2de-alldlcs-raw.2026-06-28.tgz to ~/aoe2de.

    tar -zx -C ~ -f aoe2de-alldlcs-raw.2026-06-28.tgz
    
  2. Download dependency files to ~/aoe2de/prepare, which is a temporary spot.

    wget https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton11-1/GE-Proton11-1.tar.gz https://github.com/Open-Wine-Components/umu-launcher/releases/download/1.4.0/python3-umu-launcher_1.4.0-1_amd64_debian-13.deb https://github.com/Open-Wine-Components/umu-launcher/releases/download/1.4.0/umu-launcher_1.4.0-1_all_debian-13.deb https://github.com/luskaner/ageLANServerLauncherCompanion/releases/download/v1.3.0.0/ageLANServerLauncherCompanion_Age2FakeOnline_1.0.0.0.zip https://github.com/Detanup01/gbe_fork/releases/download/release-2026_05_30/emu-win-release.7z https://github.com/luskaner/ageLANServer/releases/download/v1.14.1/ageLANServer_full_1.14.1_linux_x86-64.tar.gz
    
  3. Move debs to ~/aoe2de/debs. They will need to be installed on any system that uses this package.

  4. Explode GE-Proton11-1 to ~/aoe2de.

    tar -zx -C ~/aoe2de -f ~/aoe2de/prepare/GE-Proton11-1.tar.gz
    
  5. Prepare files from Goldberg Emulator fork.

    cd ~/aoe2de/prepare
    7za x emu-win-release.7z
    cd ~/aoe2de/
    cp -pi prepare/release/steamclient_experimental/{ColdClientLoader.ini,steamclient*.dll,steamclient_loader_x64.exe} .
    

    Prepare the ColdClientLoader.ini. Here is the non-comment values I used.

    [SteamClient]
    Exe=AoE2DE\AoE2DE_s.exe
    ExeRunDir=
    ExeCommandLine=NOSTARTUP SKIPINTRO
    AppId=813780
    SteamClientDll=steamclient.dll
    SteamClient64Dll=steamclient64.dll
    [Injection]
    ForceInjectSteamClient=0
    ForceInjectGameOverlayRenderer=0
    DllsToInjectFolder=dlls
    IgnoreInjectionError=1
    IgnoreLoaderArchDifference=0
    [Persistence]
    Mode=0
    [Debug]
    ResumeByDebugger=0
    
  6. Prepare configs for steam emulator

    cd ~/aoe2de
    mkdir -p dlls steam_settings saves
    
  7. Load dlls.

    cd ~/aoe2de/dlls
    7za x ~/aoe2de/prepare/ageLANServerLauncherCompanion_Age2FakeOnline_1.0.0.0.zip
    cp -pi ../prepare/release/steamclient_experimental/GameOverlayRenderer64.dll ../prepare/release/steamclient_experimental/extra_dlls/steamclient_extra_x64.dll .
    
  8. Prepare steam_settings.

    Prepare these based on Reference 5.

    File achievements.json is lengthy and not shown here.

    The DLCs that I own are these, for file configs.app.ini.

    [app::dlcs]
    unlock_all=0
    # Expansions
    2141580=Age of Empires II: Definitive Edition - Return of Rome
    2555420=Age of Empires II: Definitive Edition - The Mountain Royals
    #2805510=Age of Empires II: Definitive Edition - Victors and Vanquished
    2805520=Age of Empires II: DE - Chronicles: Battle for Greece
    3080080=Age of Empires II: DE - The Three Kingdoms
    #3793540=Age of Empires II: DE - Chronicles: Alexander the Great
    3219700=Age of Empires II: DE – The Last Chieftains
    1389240=Age of Empires II: Definitive Edition - Lords of the West
    1869820=Age of Empires II: Definitive Edition - Dynasties of India
    1557210=Age of Empires II: Definitive Edition - Dawn of the Dukes
    # Animated Icons
    #2672800=Age of Empires II: Definitive Edition – La Hire’s Dry Humor Animated Icons
    #2672810=Age of Empires II: Definitive Edition – Joan’s Marvelous Moving Masterpieces Animated Icons
    #2672820=Age of Empires II: Definitive Edition – Barbarossa’s Barrel of Pickled Goods Animated Icons
    # Other
    #1039811=Enhanced Graphics Pack
    

    Prepare file configs.user.ini.

    [user::general]
    # Fill your account name as you would normally see.
    account_name=SpacesAreEvil65
    # Fill with your real steamid or leave empty for it to be auto-generated.
    account_steamid=
    # Choose one from steam_settings\supported_languages.txt (described in https://partner.steamgames.com/doc/store/localization/languages) or leave empty to be set as 'english'
    language=
    # Choose from 'Alpha-2' country code: https://www.iban.com/country-codes or leave empty to be set as 'US'
    ip_country=
    
    [user::saves]
    local_save_path=saves
    

    File supported_languages.txt is exactly as is from Reference 5.

    english
    french
    italian
    german
    japanese
    koreana
    brazilian
    russian
    schinese
    latam
    tchinese
    turkish
    vietnamese
    spanish
    polish
    hindi
    malay
    
  9. Prepare launcher script aoe2de.sh.

    #!/bin/sh
    PROTON_DISABLE_LSTEAMCLIENT=1 WINEPREFIX=~/.wine-aoe2de PROTONPATH=~/aoe2de/GE-Proton11-1 umu-run ~/aoe2de/steamclient_loader_x64.exe NOSTARTUP SKIPINTRO

    The first time you run, umu wants to download components from the Internet for your user. Client scripts set-initial-umu.sh might be able to prevent that requirement.

  10. Prepare scripts/ files.

    files/2026/listings/aoe2de-get-account-steamid.sh (Source)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #!/bin/sh
    if test -z "${INFILE}" ;
    then
        scriptdir="$( dirname "$( readlink -f "${0}" )" )"
        # find first one. IGNORE the saves/settings/configs.user.ini which is the auto-generated one.
        INFILE="$( find "${scriptdir}" "${scriptdir}/.." "${scriptdir}/../.." -ipath '*/steam_settings/configs.user.ini' -print -quit )"
    fi
    final_steamid="$( awk -F'=' '$1~/account_steamid/{print $NF}' "${INFILE}" )"
    echo "account_steamid = ${final_steamid}"
    
    files/2026/listings/aoe2de-get-username.sh (Source)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #!/bin/sh
    if test -z "${INFILE}" ;
    then
        scriptdir="$( dirname "$( readlink -f "${0}" )" )"
        # find first one
        INFILE="$( find "${scriptdir}" "${scriptdir}/.." "${scriptdir}/../.." -ipath '*/steam_settings/configs.user.ini' -print -quit )"
    fi
    
    awk -F'=' '$1~/account_name/{print $NF}' "${INFILE}"
    
    files/2026/listings/aoe2de-set-account-steamid.sh (Source)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    #!/bin/sh
    # Useful because the emulator seems to pick the same number on every system when it needs to generate its own account_steamid.
    # Dependencies:
    #    dep-devuan: moreutils
    # Env vars: APPLY VERBOSE
    if test -z "${INFILE}" ;
    then
        scriptdir="$( dirname "$( readlink -f "${0}" )" )"
        # find first one. IGNORE the saves/settings/configs.user.ini which is the auto-generated one.
        INFILE="$( find "${scriptdir}" "${scriptdir}/.." "${scriptdir}/../.." -ipath '*/steam_settings/configs.user.ini' -print -quit )"
    fi
    
    # Steam id is 17 digits long, decimal, and starts with 765611982
    new_steamid="765611982$( </dev/random tr -dc '0-9' | head -c8 )"
    
    # modconf.py breaks on set account_steamid because of the underscore. I will have to do this with awk or sed or similar 2026-06-30-3 23:55
    #if test -n "${VERBOSE}" ; then echo "/usr/libexec/bgscripts/py/modconf.py ${APPLY:+-a} ${VERBOSE:+-v} -s '[user::general]' \"${INFILE}\" set account_steamid \"${new_steamid}\"" ; fi
    #/usr/libexec/bgscripts/py/modconf.py ${APPLY:+-a} ${VERBOSE:+-v} -s '[user::general]' "${INFILE}" set account_steamid "${new_steamid}"
    
    # So use basic awk instead
    if test -n "${VERBOSE}" ;
    then
        echo "awk -v \"b=${new_steamid}\" -F'=' 'BEGIN{OFS=\"=\"} \$1~/account_steamid/{\$2=b} {print}' \"${INFILE}\" | sponge \"${INFILE}\""
    fi
    if test -n "${APPLY}" ;
    then
        awk -v "b=${new_steamid}" -F'=' 'BEGIN{OFS="="} $1~/account_steamid/{$2=b} {print}' "${INFILE}" | sponge "${INFILE}"
        # Regardless if it worked, read the value and show it.
        final_steamid="$( awk -F'=' '$1~/account_steamid/{print $NF}' "${INFILE}" )"
        echo "account_steamid = ${final_steamid}"
    fi
    
    # modconf.py has a bug where it adds an additional newline after that updated line, so
    # collapse multiple empty lines into a single empty line: ref https://unix.stackexchange.com/a/72749
    awk '!NF {if (++n <= 1) print; next}; {n=0;print}' "${INFILE}" | sponge "${INFILE}"
    if test -z "${APPLY}" ; then echo "Dry run only! Use APPLY=1" 1>&2 ; fi
    
    files/2026/listings/aoe2de-set-cert-from-server.sh (Source)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    #!/bin/sh
    IPADDRESS="${IPADDRESS:-${1}}"
    if test -z "${INFILE}" ;
    then
        scriptdir="$( dirname "$( readlink -f "${0}" )" )"
        INFILE="$( find "${scriptdir}" "${scriptdir}/.." "${scriptdir}/../.." -ipath '*/certificates/cacert.pem' -print -quit )"
    fi
    INFILE="$( readlink -f "${INFILE}" )"
    if test -z "${IPADDRESS}" ;
    then
        echo "Fatal! Provide \$1 or IPADDRESS (IP address or hostname) to fetch certificate. Aborted." 1>&2
        exit 1
    fi
    cert="$( </dev/null openssl s_client -connect "${IPADDRESS}:443" 2>/dev/null | openssl x509 -in /dev/stdin )"
    # Exit if the connection failed. Openssl x509 already printed an error, so do not print another.
    if test -z "${cert}" ; then exit 1 ; fi
    next_next_to_last="$( echo "${cert}" | awk '{a[NR]=$0} END {print a[NR-2]}' )"
    if ! grep -q -e "${next_next_to_last}" "${INFILE}" ;
    then
        echo "You need the cert! File ${INFILE} does not have the cert."
        if test -n "${APPLY}" ;
        then
            echo "Applying the the cert." 1>&2
            echo "${cert}" >> "${INFILE}"
        else
            echo "Dry run only! Please run with APPLY=1 to add the cert."
        fi
    else
        echo "File ${INFILE} has the cert already."
    fi
    
    files/2026/listings/aoe2de-set-etc-hosts.sh (Source)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    #!/bin/sh
    # Dependencies:
    #    dep-devuan: bgscripts
    # TODO:
    #    clean up #ageLANServer entries
    scriptdir="$( dirname "$( readlink -f "${0}" )" )"
    INFILE="${INFILE:-/etc/hosts}"
    mkdir -p "${scriptdir}/backups"
    # needs bgscripts
    sudo bup "${INFILE}" "${scriptdir}/backups"
    IPADDRESS="${IPADDRESS:-${1}}"
    if test -z "${IPADDRESS}" ;
    then
        echo "Fatal! Need \$1 or IPADDRESS. Aborted." 1>&1
        exit 1
    fi
    # I need to remove the loopback address for this host, only if this is the server
    echo "WARNING: for server only, please modify /etc/hosts and remove 127.0.0.1-type entries. Not implemented automatically yet." 1>&2
    
    # nice idea, but I don't want to depend on this
    # This only adds the relevant api.ageofempires.com entries.
    #sudo "${scriptdir}/../ageLANServer/linux/launcher/bin/config-admin" setup --game age2 --ip "${IPADDRESS}"
    
    _resolved="$( getent hosts "${IPADDRESS}" | awk '{print $1}' )"
    echo "Using resolved ip address ${_resolved}" 1>&2
    
    for word in \
    aoe-api.reliclink.com \
    aoe-api.worldsedgelink.com \
    api.ageofempires.com \
    cdn.ageofempires.com \
    pb-live-release1-api.worldsedgelink.com \
    pb-live-release2-api.worldsedgelink.com \
    pb-live-release3-api.worldsedgelink.com \
    pb-live-release4-api.worldsedgelink.com \
    pb-live-release5-api.worldsedgelink.com \
    pb-live-release6-api.worldsedgelink.com \
    pb-live-release7-api.worldsedgelink.com \
    pb-live-release8-api.worldsedgelink.com \
    pb-live-release9-api.worldsedgelink.com \
    ; do
        long="${word}#ageLANServer"
        # depends on bgscripts
        if test -n "${VERBOSE}" ; then
            echo "sudo updateval -v ${INFILE}" \"${_resolved} ${long}\" \"${_resolved} ${long}\"
        fi
        if test -n "${APPLY}" ; then
            sudo updateval -a "${INFILE}" "${_resolved} ${long}" "${_resolved} ${long}"
        fi
    done
    
    files/2026/listings/aoe2de-set-initial-mods.sh (Source)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    #!/bin/sh
    scriptdir="$( dirname "$( readlink -f "${0}" )" )"
    INFILE="${INFILE:-/mnt/public/Support/Games/aoe2de-on-linux/aoe2de-mods-subscribed.2026-06-26.tgz}"
    steamid="$( "${scriptdir}/get-account-steamid.sh" | awk '{print $NF}' )"
    WINEPREFIX="${WINEPREFIX:-${HOME}/.wine-aoe2de}"
    destdir="${WINEPREFIX}/drive_c/users/steamuser/Games/Age of Empires 2 DE/${steamid}/mods"
    if test -n "${VERBOSE}" ;
    then
        echo "tar -zx -C \"${destdir}\" -f \"${INFILE}\""
    fi
    if test -n "${APPLY}" ;
    then
        mkdir -p "${destdir}"
        tar -zx -C "${destdir}" -f "${INFILE}"
    else
        echo "Dry run only! Please run with APPLY=1 to apply the initial mods."
    fi
    
    files/2026/listings/aoe2de-set-initial-profile.sh (Source)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    #!/bin/sh
    scriptdir="$( dirname "$( readlink -f "${0}" )" )"
    indir="$( readlink -f "${scriptdir}/../initial-files" )"
    steamid="$( "${scriptdir}/get-account-steamid.sh" | awk '{print $NF}' )"
    WINEPREFIX="${WINEPREFIX:-${HOME}/.wine-aoe2de}"
    destdir="${WINEPREFIX}/drive_c/users/steamuser/Games/Age of Empires 2 DE/${steamid}"
    if test -n "${VERBOSE}" ;
    then
        echo "cp -pr \"${indir}/profile\" \"${destdir}\""
    fi
    if test -n "${APPLY}" ;
    then
        mkdir -p "${destdir}"
        cp -pr "${indir}/profile" "${destdir}"
    else
        echo "Dry run only! Please run with APPLY=1 to apply the initial profile."
    fi
    
    files/2026/listings/aoe2de-set-initial-registry.sh (Source)
    1
    2
    #!/bin/sh
    PROTON_DISABLE_LSTEAMCLIENT=1 WINEPREFIX=~/.wine-aoe2de PROTONPATH=~/aoe2de/GE-Proton11-1 umu-run regedit "x:\aoe2de\initial-files\aoe2de-partial.reg"
    
    files/2026/listings/aoe2de-set-initial-umu.sh (Source)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    #!/bin/sh
    scriptdir="$( dirname "$( readlink -f "${0}" )" )"
    indir="$( readlink -f "${scriptdir}/../initial-files" )"
    destdir="${HOME}/.local/share"
    if test -n "${VERBOSE}" ;
    then
        echo "cp -pr \"${indir}/umu\" \"${destdir}\""
    fi
    if test -n "${APPLY}" ;
    then
        mkdir -p "${destdir}"
        cp -pr "${indir}/umu" "${destdir}"
    else
        echo "Dry run only! Please run with APPLY=1 to apply the initial umu files."
    fi
    
    files/2026/listings/aoe2de-set-username.sh (Source)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #!/bin/sh
    # Dependencies:
    #    dep-devuan: bgscripts, moreutils
    # Env vars: APPLY VERBOSE
    USERNAME="${USERNAME:-${1}}"
    if test -z "${USERNAME}" ; then echo "Fatal! Need \$1 or USERNAME. Aborted." 1>&2 ; exit 1 ; fi
    if test -z "${INFILE}" ;
    then
        scriptdir="$( dirname "$( readlink -f "${0}" )" )"
        # find first one
        INFILE="$( find "${scriptdir}" "${scriptdir}/.." "${scriptdir}/../.." -ipath '*/steam_settings/configs.user.ini' -print -quit )"
    fi
    
    # This depends on bgscripts.
    /usr/libexec/bgscripts/py/modconf.py ${APPLY:+-a} ${VERBOSE:+-v} -s '[user::general]' "${INFILE}" set account_name "${USERNAME}"
    
    # modconf.py has a bug where it adds an additional newline after that updated line, so
    # collapse multiple empty lines into a single empty line: ref https://unix.stackexchange.com/a/72749
    awk '!NF {if (++n <= 1) print; next}; {n=0;print}' "${INFILE}" | sponge "${INFILE}"
    if test -z "${APPLY}" ; then echo "Dry run only! Use APPLY=1" 1>&2 ; fi
    
  11. Prepare ageLANServer assets for server.

    These assets are only needed for the machine that will run the server.

    mkdir -p ~/aoe2de/ageLANServer/linux
    tar -zx -C ~/aoe2de/ageLANServer/linux -f ~/aoe2de/prepare/ageLANServer_full_1.14.1_linux_x86-64.tar.gz
    

    Adjust battle-server-manager/resources/config.age2.toml

    Path = '../../../AoE2DE/BattleServer/BattleServer.exe'
    

    Adjust server/resources/config/config.toml

    Enabled = ["age2"]
    

    I could have written sed statements, but I didn't bother.

  12. Prepare initial files.

    I copied what I think is a reasonably configured "low graphics" user profile from ~/.wine-aoe2de.attempt2/drive_c/users/steamuser/Games/Age\ of\ Empires\ 2\ DE/7656119EXAMPLE892. Place it as ~/aoe2de/initial-files/profile.

    Prepare initial-files/README.md.

    # Profile
    This initial profile already sets graphics to low. Most, but probably not all, my preferred settings are loaded already.
    
    The destination for `profile/` is the following.
    
        ${WINEPREFIX}/drive_c/users/steamuser/Games/Age of Empires 2 DE/7656119EXAMPLE892
    
    # Umu
    This is a snapshot of `${HOME}/.local/share/umu` from 2026-07-01.
    
    # Registry key
    
    It appears some values are in registry, and some are in `${WINEPREFIX}/drive_c/users/steamuser/Games/Age\ of\ Empires\ 2\ DE/${account_steamid}/profile/Player.nfp`. So this is the ones from the registry. However, it also appears to use key `HKEY_USERS\{sid}-{UID}-1000\Software\Microsoft\Microsoft Games\Age of Empires II DE` which is not predictable.
    

    Prepare the initial umu from a working environment. I found one that works for clients that is less than 1GB, with only steamrt4/.

    cp -pr ~/.local/share/umu ~/aoe2de/initial-files/
    

    NOTE: It appears some values are in registry, and some are in ${WINEPREFIX}/drive_c/users/steamuser/Games/Age\ of\ Empires\ 2\ DE/${account_steamid}/profile/Player.nfp.

    This command exports the registry keys for the game to ${HOME}\aoe2de-partial.reg. This depends on symlink ${WINEPREFIX}/dosdevices/x: -> ${HOME}

    PROTON_DISABLE_LSTEAMCLIENT=1 WINEPREFIX=~/.wine-aoe2de PROTONPATH=~/aoe2de/GE-Proton11-1 umu-run regedit /E "x:\aoe2de\initial-files\aoe2de-partial.reg" "HKEY_CURRENT_USER\Software\Microsoft\Microsoft Games\Age of Empires II DE"
    
  13. Build release asset #2.

    tar -zc -C ~ -f ~/aoe2de-other-components-2026-07-01.tgz --exclude 'AoE2DE' --exclude 'prepare' aoe2de
    

    This tar operation only took about r0:03.30.

Description of this working environment

Operations

See also /mnt/public/Support/Games/aoe2de-on-linux/aoe2de-without-steam.md

Installing this offline bundle

A server and client get the same assets. The server needs some additional steps.

  1. Explode tarball of AoE2DE itself, to ~/aoe2de.

    tar -zxf /mnt/public/Support/Games/aoe2de-on-linux/aoe2de-alldlcs-raw.2026-06-28.tgz -C ~
    
  2. Explode the other components.

    tar -zxf /mnt/public/Support/Games/aoe2de-on-linux/aoe2de-other-components-2026-07-01.tgz -C ~
    

Installing a client

In ~/aoe2de, run some of the `scripts/set-*.sh`.

    APPLY=1 ./set-account-steamid.sh # randomize the steamid in the emulator
    APPLY=1 ./set-initial-profile.sh
    APPLY=1 ./set-initial-umu.sh
    APPLY=1 ./set-initial-mods.sh # optional
    APPLY=1 ./set-username.sh "Steve Harvee 3"
    ./set-initial-registry.sh

Some can only be run after the server is up and running; see section "Running a client".

Installing a server

Run each step from the "Installing a client" first, except the ones for after a server is running. When you adjust /etc/hosts, remove or comment any 127.0.0.1 <hostname>-style records.

It is possible that on a new system, you have to run this.

sudo setcap CAP_NET_BIND_SERVICE=+eip ~/aoe2de/ageLANServer/linux/server/server

Initialize a certificate.

~/aoe2de/ageLANServer/linux/server/bin/genCert.sh

Running a client

If you will play multiplayer, you need to know the IP address or hostname of the server. This sets the entries in /etc/hosts which can be done manually if necessary.

APPLY=1 ./set-etc-hosts.sh 192.168.1.45 # IP address or hostname of server
APPLY=1 ./set-cert-from-server.sh 192.168.1.45

Run the short launcher.

~/aoe2de/aoe2de.sh

Running a server

Run the battle server manager in a terminal. It will spawn a new one to run, and return this prompt to you for the next command.

~/aoe2de/ageLANServer/linux/battle-server-manager/start_age2.sh

Run the server.

~/aoe2de/ageLANServer/linux/server/start_age2.sh

Then run the client as described above in "Running a client".

Adding mods

With script

From the second asset, use ~/aoe2de/scripts/set-initial-mods.sh. It depends on this network path for this file.

/mnt/public/Support/Games/aoe2de-on-linux/aoe2de-mods-subscribed.2026-06-26.tgz

Manually

Find the generated userid from ~/aoe2de/saves/settings.configs.user.ini.

eval $( awk '/account_steamid/' ~/aoe2de/saves/settings/configs.user.ini | tr -dc 'a-zA-Z=0-9_' )

Visit the user's mods directory and copy in the mods I have provided.

cd ~/".wine-aoe2de/drive_c/users/steamuser/Games/Age of Empires 2 DE/${account_steamid}/mods"
tar -zxf /mnt/public/Support/Games/aoe2de-on-linux/aoe2de-mods-subscribed.2026-06-26.tgz

Updating the game

A full copy of the official game from steam needs to be updated, and then built into a tarball. The path would be similar to /mnt/vm2/pcb-009-steam/steamapps/common/AoE2DE.

cd /mnt/vm2/pcb-009-steam/steamapps/common
tar -zcf "AoE2DE-$( date "+%F" ).tgz" AoE2DE

Note that this would not have the exact same directory structure as the original first asset which was aoe2de/AoE2DE/.

Updating "other components"

These dependencies might have newer versions. Check the references, and follow the "Building the package" steps.

Updating mods

Build a new tarball from the upstream machine that has the desired mods.

tar -zc -C "/mnt/vm2/pcb-009-steam/steamapps/compatdata/813780/pfx/drive_c/users/steamuser/Games/Age of Empires 2 DE/${account_steamid}/mods" -f /mnt/public/Support/Games/aoe2de-on-linux/aoe2de-mods-subscribed.$( date "+%F" ).tgz

Related files

  • aoe2de-alldlcs-raw.2026-06-28.tgz
  • aoe2de-other-components-2026-07-01.tgz
  • aoe2de-mods-subscribed.2026-06-26.tgz

References

  1. Installing Proton on Linux: A Comprehensive Guide — linuxvox.com
  2. proton ge: https://github.com/GloriousEggroll/proton-ge-custom/releases/download/GE-Proton11-1/GE-Proton11-1.tar.gz from Releases · GloriousEggroll/proton-ge-custom
  3. Open-Wine-Components/umu-launcher: Unified launcher for Windows games on Linux
  4. UMU Database Search shows nothing for "Empires" about AoE2, so GAMEID can be left undefined.
  5. luskaner/ageLANServerLauncherCompanion: Libraries to enhance or fix the game behaviour when using a custom launcher in Age LAN Server
  6. Releases · Detanup01/gbe_fork
  7. luskaner/ageLANServer: Web Server (and its launcher) for AoE 1, 2, 3 (all DE), AoE 4 and AoM: RT supporting offline multiplayer
  8. Age of Empires II: Definitive Edition DLCs · SteamDB to find ids for the two DLCs I own that were not demonstrated in the configs.app.ini

CoMaps will eventually trust user provided root certificates!

Squee! Some of my favorite words colliding: free software, android apps, and certificates! If only it had FreeIPA (ldap/kerberos) and Devuan and Age of Empires 2 somehow, it'd be the best post on the World Wide Web!

Just like with a fix for f-droid a few years ago, CoMaps will eventually be able to trust user-provided root certificates in Android! While I wrote the PRs for the linked issues, I was just standing on the shoulders of giants. And the docs are public.

This would only be useful if you are using a custom map server. At present OrganicMaps does not have a custom map server option and wouldn't need this feature and indeed does not have it.

Screenshot showing my merged pull request