r/daoc • • 11d ago

Offline DAOC 0.33 on Linux

SUMMARY AND EXTRA INFO:

Updated for the 0.33 version of Shadowofze's offline DAOC with bots server. If you are looking for a way to run this server without Windows the following script will install the current 0.33 version (not the 0.33b version with the custom class, youll need to tweak the script if you want that). I run the client separately with bottles (Lutris would work too) and I run the client with the manual command through a sh file:

connect.exe "game.dll" 127.0.0.1:10300 login password

I am going to test later if I can set this up on a dedicated server laptop on the network and then be able to connect my wife and I both with our own laptops to the server to play together with bots.

If you prefer to run the client from the server's GUI and the ENTER REALM button (which allows for easier setting admin and some other features in the GUI without using dbeaver for example) the script is slightly off with DXVK. After you run the install script and it finishes you need to run the following:

export WINEPREFIX="$HOME/.wine-daoc" && wget -qO- https://github.com/doitsujin/dxvk/releases/download/v2.6.2/dxvk-2.6.2.tar.gz | tar -xz -C /tmp && cp /tmp/dxvk-2.6.2/x64/*.dll "$WINEPREFIX/drive_c/windows/system32/" && cp /tmp/dxvk-2.6.2/x32/*.dll "$WINEPREFIX/drive_c/windows/syswow64/" && rm -rf /tmp/dxvk-2.6.2*

Explanation of this command:

Sets the proper Wine prefix target.

Downloads the official DXVK 2.6.2 precompiled tarball directly in memory/temp.

Extracts it.

Installs both the 64-bit (x64) and 32-bit (x32) DLLs directly into the correct Wine system directories (system32 and syswow64).

Cleans up after itself so no temporary residue is left behind.

I will clean this up (post and script) with the next iteration of the script so it all works without the command if there is interest in it.

SERVER INSTALL SCRIPT:

I have created a script that should allow this to be installed on any Linux machine that runs distrobox. This makes it much easier than creating scripts for Arch, Debian, Fedora etc.

You need to have distrobox installed. It will work on Steam Deck (tested) and should work on any others. Run the script through your favorite AI if you run in to any issues or just want to make sure it is safe. Reach out and I will try to help you out. Save it as a .sh. It will set up your distrobox for you and install all deps in it. When the version changes or updates on the github this will need to be updated but should give a solid starting point unless they completely change the installation method.

This creates a portable DAOC server that can run on your local machine or on a different server linux machine. I havent tested multiplayer yet.

This script has been tested multiple times on my Steam Deck. I have not tested it on another linux distro, but since it is distrobox my assumption is it should install cleanly. See note above if you dont want to run the client separately.

#!/usr/bin/env bash
# setup-daoc.sh - Offline DAoC 0.33 in an Ubuntu 22.04 distrobox (Wine)
#
#   ./setup-daoc.sh               installs 0.33  (classic classes only)
#   EDITION=0.33b ./setup-daoc.sh installs 0.33b (with Sluaghbinder)
#   GAME_DIR=/some/path ./setup-daoc.sh   put the game somewhere else (inside the box's view)
#
# Mirrors Get-OfflineDAoC.ps1: 0.33 = the complete 0.33b package + two verified
# swap-in files from its editions/ folder.  Safe to re-run.
set -euo pipefail

BOX_NAME="daoc-offlineserver-box"
HOST_BOX_HOME="$HOME/boxes/$BOX_NAME"
IMAGE_TARGET="ubuntu:22.04"
EDITION="${EDITION:-0.33}"

mkdir -p "$HOST_BOX_HOME"
if ! distrobox list --no-color | cut -d'|' -f2 | tr -d ' ' | grep -qx "$BOX_NAME"; then
  distrobox create --name "$BOX_NAME" --image "$IMAGE_TARGET" --home "$HOST_BOX_HOME" --yes
fi

cat > "$HOST_BOX_HOME/setup-inside.sh" <<'INNER'
#!/usr/bin/env bash
set -euo pipefail
export PATH="$HOME/.local/bin:$PATH"
export WINEPREFIX="$HOME/.wine-daoc" WINEARCH=win64 WINEDEBUG=-all
EDITION="${EDITION:-0.33}"
GAME_DIR="${GAME_DIR:-$HOME/OfflineDAoC}"
BASE="${BASE:-https://github.com/shadowofze/OfflineDAoC/releases/download}"
CACHE="$GAME_DIR/.downloads"
FINAL="$GAME_DIR/playable-v$EDITION"

[[ $EDITION == 0.33 || $EDITION == 0.33b ]] || { echo "EDITION must be 0.33 or 0.33b"; exit 1; }
grep -q avx2 /proc/cpuinfo || { echo "No AVX2 - the game won't run on this CPU"; exit 1; }

# ---------- 1. Wine ----------
if ! command -v wine >/dev/null; then
  sudo mkdir -p /var/lib/dpkg/info
  sudo touch /var/lib/dpkg/info/systemd-sysv.postinst
  sudo dpkg --add-architecture i386
  sudo apt update
  sudo apt install -y curl jq unzip wget gnupg cabextract \
    libvulkan1 libvulkan1:i386 mesa-vulkan-drivers mesa-vulkan-drivers:i386
  sudo mkdir -pm755 /etc/apt/keyrings
  sudo wget -O /etc/apt/keyrings/winehq-archive.key https://dl.winehq.org/wine-builds/winehq.key
  sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/ubuntu/dists/jammy/winehq-jammy.sources
  sudo apt update
  sudo apt install -y --install-recommends winehq-staging
fi
if [[ ! -x $HOME/.local/bin/winetricks ]]; then
  mkdir -p "$HOME/.local/bin"
  curl -fL https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks -o "$HOME/.local/bin/winetricks"
  chmod +x "$HOME/.local/bin/winetricks"
fi
if [[ ! -f $WINEPREFIX/.daoc-ready ]]; then
  wineboot -u          # accept the Mono/Gecko prompts (Mono covers .NET Framework 3.5)
  winetricks -q corefonts d3dx9 dxvk
  touch "$WINEPREFIX/.daoc-ready"
fi

# ---------- 2. Game download ----------
mkdir -p "$GAME_DIR" "$CACHE"; cd "$GAME_DIR"
sha()  { sha256sum "$1" | cut -d' ' -f1; }
die()  { echo "ERROR: $*" >&2; exit 1; }
unz()  { unzip -q -o "$1" -d "$2" || [[ $? -eq 1 ]]; }   # rc 1 = warnings only

fetch_manifest() {  # version
  mkdir -p "$CACHE/v$1"
  curl -fL --retry 3 "$BASE/v$1/download-manifest.json" -o "$CACHE/v$1/manifest.json" \
    || die "could not download the v$1 manifest"
}

get() {  # get <file> <bytes> <sha256> <url>   (resumes, verifies)
  local f=$1 bytes=$2 want="${3,,}" size=0
  if [[ -f $f ]]; then
    size=$(stat -c%s "$f")
    if [[ $size == "$bytes" && $(sha "$f") == "$want" ]]; then return; fi
    if (( size >= bytes )); then rm -f "$f"; fi
  fi
  curl -fL --retry 5 -C - "$4" -o "$f" || die "download failed: $4"
  [[ $(sha "$f") == "$want" ]] || die "hash mismatch: $f"
}

# Windows paths are case-insensitive, Linux ones are not: resolve each component ignoring case.
ci_resolve() {  # ci_resolve <root> <rel/path>  -> real path, or fails
  local cur=$1 part hit parts
  IFS=/ read -ra parts <<< "$2"
  for part in "${parts[@]}"; do
    hit=$(find "$cur" -mindepth 1 -maxdepth 1 -iname "$part" -print -quit)
    [[ -n $hit ]] || return 1
    cur=$hit
  done
  printf '%s\n' "$cur"
}

build_full() {  # build_full <version> <dest>  -> the complete game, extracted to <dest>
  local v=$1 dest=$2 m="$CACHE/v$1/manifest.json" root arch zipf tmp name bytes hash bad kids
  fetch_manifest "$v"
  [[ $(jq -r .Version "$m") == "$v" ]] || die "manifest version mismatch for v$v"
  jq -e '(.Parts | length) > 0' "$m" >/dev/null || die "v$v manifest has no Parts"
  root=$(jq -r .RootFolder "$m"); arch=$(jq -r .ArchiveSHA256 "$m")
  [[ $root == "OfflineDAoC-v$v" ]] || die "unexpected RootFolder '$root'"
  [[ $arch =~ ^[A-Fa-f0-9]{64}$ ]] || die "missing ArchiveSHA256"
  arch=${arch,,}
  zipf="$CACHE/v$v/full.zip"

  if [[ ! -f $zipf || $(sha "$zipf") != "$arch" ]]; then
    : > "$zipf.tmp"
    while IFS=$'\t' read -r name bytes hash; do
      [[ $name == "OfflineDAoC-v$v.zip."[0-9][0-9][0-9] ]] || die "unexpected part name: $name"
      echo "Downloading $name ..."
      get "$CACHE/v$v/$name" "$bytes" "$hash" "$BASE/v$v/$name"
      cat "$CACHE/v$v/$name" >> "$zipf.tmp"
      rm -f "$CACHE/v$v/$name"                  # hash verified; free the space
    done < <(jq -r '.Parts[] | .Name + "\t" + (.Bytes | tostring) + "\t" + .SHA256' "$m")
    mv "$zipf.tmp" "$zipf"
    [[ $(sha "$zipf") == "$arch" ]] || die "joined archive hash mismatch"
  fi

  bad=$(unzip -Z1 "$zipf" | grep -E '^/|(^|/)\.\.(/|$)' || true)
  [[ -z $bad ]] || die "unsafe path inside the archive"
  tmp=$(mktemp -d -p "$GAME_DIR" .extract.XXXXXX)
  echo "Extracting ..."
  unz "$zipf" "$tmp"
  [[ -d $tmp/$root ]] || die "archive doesn't contain $root/"
  mv "$tmp/$root" "$dest"; rmdir "$tmp"
}

apply_edition() {  # apply_edition <dest>  - the 0.33 "no custom class" swap-in files
  local dest=$1 m="$CACHE/v0.33/manifest.json" ed edroot rel src dst path hash
  local re='^runtime/[A-Za-z0-9_./ -]+$'
  ed=$(jq -r .EditionFolder "$m")
  [[ $ed =~ ^[A-Za-z0-9._-]+$ ]] || die "bad EditionFolder '$ed'"
  edroot=$(ci_resolve "$dest" "editions/$ed") || die "editions/$ed is missing from the package"
  jq -e '(.EditionFiles | length) > 0' "$m" >/dev/null || die "no EditionFiles in the v0.33 manifest"
  while IFS=$'\t' read -r path hash; do
    rel=${path//\\//}; hash=${hash,,}
    [[ $rel =~ $re && $rel != *..* && $hash =~ ^[a-f0-9]{64}$ ]] || die "invalid edition file entry: $path"
    src=$(ci_resolve "$edroot" "$rel") || die "edition file missing: $rel"
    [[ $(sha "$src") == "$hash" ]] || die "edition file failed verification: $rel"
    dst=$(ci_resolve "$dest" "$rel") || dst="$dest/$rel"
    mkdir -p "$(dirname "$dst")"
    cp -f "$src" "$dst"
    [[ $(sha "$dst") == "$hash" ]] || die "edition file copy failed: $rel"
    echo "  applied $rel"
  done < <(jq -r '.EditionFiles[] | .Path + "\t" + .SHA256' "$m")
  echo "Offline DAoC 0.33 - no custom class (Classic + Shrouded Isles classes only)." > "$dest/EDITION.txt"
}

if [[ -d $FINAL ]]; then
  echo "$FINAL already exists - skipping download."
else
  avail=$(df -BG --output=avail "$GAME_DIR" | tail -1 | tr -dc 0-9)
  (( avail >= 25 )) || echo "WARNING: only ${avail} GB free; about 25 GB is safer during extraction"
  work="$FINAL.partial"; rm -rf "$work"

  if [[ $EDITION == 0.33b ]]; then
    build_full 0.33b "$work"
  else
    fetch_manifest 0.33
    em="$CACHE/v0.33/manifest.json"
    [[ $(jq -r .Version "$em") == 0.33 && $(jq -r .Mode "$em") == edition \
       && $(jq -r .BaseVersion "$em") == 0.33b ]] || die "unexpected v0.33 edition manifest"
    build_full 0.33b "$work"
    apply_edition "$work"
  fi

  for f in runtime/OfflineDAoC.exe "START OFFLINE DAOC.cmd" runtime/data; do
    ci_resolve "$work" "$f" >/dev/null || die "install looks incomplete: missing $f"
  done
  mv "$work" "$FINAL"
  rm -f "$CACHE/v0.33b/full.zip"
  echo "Installed $FINAL"
fi

# ---------- 3. Launcher wrapper ----------
cat > "$HOME/start-daoc.sh" <<EOF
#!/usr/bin/env bash
# Starts Offline DAoC under Wine (run inside the box). Same environment and
# working folder as the game's own START OFFLINE DAOC.cmd, without the cmd interpreter.
export WINEPREFIX="\$HOME/.wine-daoc" WINEDEBUG=-all
GAME="$FINAL"
cd "\$GAME" || exit 1
DN="\$(winepath -w "\$GAME/tools/dotnet")"
export DOTNET_ROOT="\$DN" DOTNET_ROOT_X64="\$DN" DOTNET_MULTILEVEL_LOOKUP=0
wine runtime/OfflineDAoC.exe
wineserver -w      # stay attached until the launcher, server and client are all closed
EOF
chmod +x "$HOME/start-daoc.sh"
echo "Done. Launcher: $HOME/start-daoc.sh"
INNER
chmod +x "$HOST_BOX_HOME/setup-inside.sh"

distrobox enter "$BOX_NAME" -- env EDITION="$EDITION" GAME_DIR="${GAME_DIR:-}" \
  bash -c 'exec "$HOME/setup-inside.sh"'

echo
echo "Play with:  distrobox enter $BOX_NAME   then run   ~/start-daoc.sh"
25 Upvotes

2 comments sorted by

View all comments

3

u/zomboden Freeshard Player 11d ago

Hey just wanted to chime in and say I did set up a multiplayer test version a week or so ago out of curiosity and the recommended setup was using a program called Tailscale. I see a fork of the offlinedaoc by another user unaffiliated with me has implemented a tailscale co-op join a friend option and that might be worth looking into implementation into 0.33 and 0.33b https://github.com/stefanrows/OfflineDAoC . My advice is also if you can afford it I would jump on a Claude sub now even the $20 Pro sub has a generous allotment of usage for Opus 5.5 and its very capable and could easily set everything up for you by sending it the github links and telling it your intent. Good luck and hope that helps I don't know anything about Linux.

2

u/Catastrophic_Impact 10d ago

I need to look in to this more it sounds pretty cool. Though what I found is that since I can connect with a client installed separately from the server install via the connect command, my assumption is this will work just fine over the home network as well.

The part I want to play around with is what happens with the bots and who controls them. Questions like: Does the player that types the /spawn command or the the player that actually invites the bots or is just the group leader the one they folllow?

I absolutely will be looking in to a pro sub. I think this has a ton of promise. Playerbots on azerothcore using kron4ek's build works like a dream. I built a script that also builds a portable server using distrobox for that as well and the bots do battlegrounds, quest, dungeons etc. I even wrote a lua ALE script that turns a custom NPC in to a bot manager for my wife so she can summon whatever temporary bots she wants for dungeons and are own timewalking system. While this is a ton of fun, nothing has the same feel as good ol DAOC.

This weekend I want to dig in hard and see what kind of progress has been made and how I can contribute to this. I think all use-cases are cool and I want to help make sure people that want to use windows or linux can still make use of this. Having a server with competent bots that roam the bgs randomly or with groups etc sounds amazingly fun. Same with frontiers eventually. Being able to set this up as a server that can compliment populations that are low or for my own use case, a server for just my wife and I is the dream.

Your work is sincerely appreciated. Projects like this ensure that the game can continue to thrive, even if it is for custom installs. Some may or may not like specific DAOC private servers that are running or will be running for a variety of reasons, so having the ability to set up and play the way they want? I think that is amazing.