【Elurair】 Automating patcher updates for rAthena & Hercules based MMORPG servers

2025-05-08   #stuff 

Mage plz tell me artist I need 2 know

Patch-server CI/CD automation for client patchers compliant with action MMORPG servers based on rAthena or Hercules. The patcher in use in this setup is Elurair by Ai4rei.

Disclaimers

WARNING: This is not a guide on how to use Elurair. It assumes you already have Elurair configured and a deployment server for distributing patches. This is a guide on how to automate updates to remote files relevant to Elurair, like a WebList file. It references git repos that you probably do not have access to. When I say “the client repository”, I am referring to my own git client repo, but it will work with any git repository for generic clients that can be updated via Elurair.

I wrote this as a reference for myself in the context of Freyja ~ Rune Breakers (running RGX), and am posting it publicly because it’s not much extra work for me to do so. If you want to use it you will have to adapt it to your environment, which I will not support. All that said, if you are an RGX developer or you reading this in the distant future after I open-source all of the RGX stuff, it should mostly just work.

I am not Ai4rei. I have no affiliation with Ai4rei (Ai4rei cool tho). I have no affiliation with Elurair. Ai4rei will not support you if you have trouble with this process. Do not bother Ai4rei about anything relating to this process. I will not support you in Ai4rei’s Discord or IRC channel if you whine about it there publicly. If you really want to ask me a question, PM me.

Concepts

Although this article is not about setting up Elurair, it is useful to review some of its basic concepts. Elurair’s main configuration is at elurair.<whatever>.ini.

The patcher has a few “patcher contexts” (I made that term up, Elurair does not use it). Each context has a corresponding <context>.inf file, patch_<context>.txt remote patch server file, and ROCred.Patchers.<context> block in the Elurair config. These are what you see as initial. main, client, etc in the deploy_patcher.py script.

inf / patch_xxx.txt

So a user opens the patcher… What does it do? First it checks each <context>.inf file in its working directory. Inf are small binary files with nothing but an integer written to it in binary. Check the inffile-spec.txt file in Elurair’s docs for details.

The patcher then compares the value from an inf file against the version numbers listed in a corresponding patch_<context>.txt (which hold the value as a regular decimal) on the remote patch server. If the inf file’s value is >= the remote servers, it does nothing. Otherwise it downloads each listed file in the txt sequentially.

Example of an empty inf file:

00 00 00 00

Example of a patch_xxx.txt file:

1   20XX-07-04SetupFix.rgz
2   20XX-07-05PatchFix.rgz  stop

To write a clean inf file you can use:

python3 -c 'open("patch_initial.inf", "wb").write((0).to_bytes(4, "little"))'

Elurair patch context config

Elurair ties together all the different files/paths for a context in its ini config.
Example of a straight-forward context configuration block:

[ROCred.Patchers.Main]
InfFile=patch_main.inf
PakFile=rgx.grf
WebList=patch_main.txt
WebListVer=1
WebPath=/main/
WebSite=patch.roguenarok.online
WebProt=http
WebFlag=0
TryNext=0

The important configuration values are InfFile, PakFile, WebList, WebPath, and WebSite. The final remote URL for a given context is constructed as WebSite+WebPath+WebList. The PakFile is only specified for patcher contexts that focus on an incrementally updatable client archive (GRF); it specifies where to merge gpf/rgz patches.

Context types

Freyja has a bunch of patch contexts which I sort into two informal categories; Incremental update and One-time update.

  • Incremental updates are contexts where each update appends a new version number to the end of the patch_list.txt file, in other words, normal patches. These use WriteMode.APPEND in deploy_patcher.py.
  • One-time updates are contexts where each update overwrites the content of patch_list.txt with the new version number for files that should not do incremental updates, and the user should “only grab newest”. These use WriteMode.OVERWRITE in deploy_patcher.py. Examples:
    • For our Initial context, for example, it grabs all of the client files. Once it’s done it gets set to 1. It never gets touched again.
    • For our Client context, it grabs any .exe files or elurair.new files, which are files that the user should only have to grab a single time, and they only need newest. So to prevent Elurair from downloading unnecessary files (if you’re 4 ragexe verisons old, you dont need to step through every new exe), I overwrite the contents of the patch_client.txt file with the highest version number only.

Freyja’s contexts

Incremental (APPEND):

Main: Updates rgx.grf. (pakfile/grf)
System: Updates lua, txt, and other plain text files EXCEPT itemInfo files. (zip)
Data: Updates bgm, fonts, and other asset files. (zip)

One-time (OVERWITE):

Initial: First run. When the user downloads just the patcher, this downloads all client files. (zip)
Client: Updates .exe files and elurair itself. (zip)
Iteminfo: Updates iteminfo LUA files. (zip)
  • note: If you want to support other PakFile patches, you should edit deploy_patcher.py and mimic whatever is used for PATCH_MAIN
  • note2: Iteminfo is given its own context because its way larger than other system files and the user will only ever need newest.

Automated patcher setup

My requirements for Patcher automation were:

  1. I push to the client repository
  2. My remote patch-server finds all the files updated in the push, prepares them for the patcher, and updates the corresponding patch_context.txt file.

That’s it. I didn’t wanna do nuffin'. It ended up being a relatively straight forward process but the overview of how to do it is:

  1. Setup the remote patch server, pull a client repo to it
  2. Move deploy_patcher.py to the patch_server, configure it.
  3. Create a GitHub push action to trigger your deploy_patcher.py script.
  4. The deploy_patcher.py automates all the stuff.

deploy_patcher.py

This script is the primary component of this automation chain and I think it is simple + commented decently enough to figure out how to use it yourself for the most part. It lives in the patcher/tools directory of the client repo, and will be setup automatically for you.

If you are not in an RGX environment, i.e., if you are adapting this script for general usage, I’ve put an early version of deploy_patcher.py on gist, but no guarantees surrounding updates or support. Note that there is some configuration you will have to do.

The top of the script has various config values. You can delete any patch contexts you don’t use, but be sure to remove them from main() as well. It would be better to use a dictionary to hold configuration information if you care to improve the script.

Probably, Main and Data are enough for most use cases. Note that you’ll want to stick with WriteMode.APPEND unless you understand what WriteMode.OVERWRITE is doing (refer to context types above).

Also, I barely know Python, so sorry for whatever conventions I broke. Feel free to suggest improvements.

Patch Server Setup

Done on Amazon AL2

  1. setup your web-server of choice and make sure it can serve files properly. (figure it out)
  2. install git, python3, and docker (and do docker’s post-install)
sudo yum install -y python3 git

sudo amazon-linux-extras enable docker
sudo yum clean metadata
sudo yum install -y docker

sudo service docker start
sudo systemctl enable docker

sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker
docker run hello-world
  1. Make a central working directory, clone your repo, setup the deploy python script
mkdir -p rgx && cd rgx
git clone <url>
cp -a roguenarok-client/patcher/tools/rgx/* .
chmod +x deploy_patcher.py
mkdir archive_store
cp -a rsuts.exe archive_store/
vim deploy.py
  • note: you can run git config --global credential.helper store before a git pull to have your credentials stored, but they’ll be in plain text.
  • note2: I copy rsuts.exe to archive_store because I am lazy and run it from both locations and use cwd with no pathing nonsense in each.
  1. Build the Docker image, setup an alias if you want
docker build -t wine32 .
docker run --rm -v "$HOME/rgx:/mnt" wine32 rsuts.exe

vim ~/.bashrc
  alias rsuts='docker run --rm -v "$HOME/rgx:/mnt" wine32 rsuts.exe'
source ~/.bashrc
  1. Setup the GitHub action. The action is in roguenarok-client/patcher/tools/deploy_patcher.yml and just runs the python script, but here it is for anyone curious:
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy patcher
        env:
          SERVER: <server address>
          USERNAME: <user>
          KNOWN_HOSTS: ${{ secrets.KNOWN_HOSTS }}
          SSH_KEY: ${{ secrets.SSH_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$KNOWN_HOSTS" >> ~/.ssh/known_hosts
          echo "$SSH_KEY" > private_key.pem
          chmod 600 private_key.pem
          ssh -i private_key.pem $USERNAME@$SERVER 'bash -s' << 'ENDSSH'
            python3 ~/rgx/deploy_patcher.py
          ENDSSH
          rm -f private_key.pem

note: although the above gh action follows some best practices, I do not consider it good enough for production security if your patch server or ssh key have anything meaningful on them. If your patch server contains anything sensitive (and it will if you are storing your git creds on it), I would suggest a higher standard of security (maybe GitHub OIDC or good ole ip-based access control), or an alternative method of checking for pushes on the patch-server so it knows to do a pull.

Why Docker?

One challenge with the process is that I could not find anything comparable to Ai4rei’s rsuts grf tool for unix operating systems and I wanted to create gpf patches over rgz patches (for easier merging, subtractive deletes, and some other reasons). Also, rsuts is useful for doing occasional tests and other utility functions against the grf.

Amazon AL2 does not support wine in any capacity as far as I could tell, even building from source fails due to endless dependency hell. Docker is my convoluted workaround to run wine (yes, a container to run an emulator…). You can skip the docker stuff if you have another way to run rsuts.exe, but if you do you’ll have to edit deploy_patcher.py accordingly.

Caveats & stuff

  • I do not handle elurair.new updates in an automated way because they are edgecase-y and infrequent enough that I can edit the patch_txt by hand for them. To have Elurair patch Elurair, I rename my newly built Elurair file to elurair.new, move it to my client context www directory, and update patch_client.txt manually.
  • If the remote-patch server skips a git pull you might end up in a bad state, because the patcher script only checks between HEAD and HEAD@{1}. The hope is that this does not happen. If it does happen, you can fix it by restoring your backed up patch_txt file (you have one, right?), then step through each missed commit 1 by 1 on the patch-server, running deploy_patch.py manually for each commit. I will make this process better if it becomes a problem for me (it hasnt happened yet…)