Phantasy Star Online server

Dreamcast
SEGA
GameCube
PSO
server
Docker
Bring the online glory of Pioneer 2 crew to your own homelab!
Author

ProtossGP32

Published

June 30, 2024

Phantasy Star Online logo

Introduction

Phantasy Star Online is the first online RPG game made for consoles and was published by SEGA in 2000 for the Dreamcast. After its initial succses, the game was ported to several platforms like PC, XBOX and GameCube. Online gaming was hosted on private SEGA servers at that time and are long time unavailable, but the fan community brough them back as private servers and since some years ago there are public source code for deploying our own servers at home!

Getting started

We’re going to follow this excellent blog entry from the guys at Super CD-ROM² as an entrypoint to build our own Docker image so it is cleaner to build and maintain new versions. Later on, we’ll use docker-compose to deploy it in our server of choice (be it a VM or a SBC like a Raspberry Pi)

Tools of the trade

  • Code repositories:
    • phosg from Martin Michelsen aka fuzziqersoftware: a tool required to build newserv, the actual PSO server. Active development as of 2024/06/30
    • newserv, again from Martin Michelsen aka fuzziqersoftware: a game server, proxy, and reverse-engineering tool for Phantasy Star Online (PSO). The README.md file contains very detailed information on how to deploy and troubleshoot it and the developers are still active as of today (2024/06/30), releasing several versions a year
    • resource_dasm, again from Martin Michelsen aka fuzziqersoftware: an optional tool that enables newserv to send memory patches and load DOL files on PSO GC clients. PSO GC clients can play PSO normally on newserv without this
  • Container runtime:
    • Docker or Podman. Both are compatible with the container images we’ll build, but for convenience we’ll stick with docker and docker compose commands here. Feel free to swap them to podman and podman compose if you decide to use the latter
  • Architecture:
    • We’ll be deploying the server in a [Proxmox] Alpine LXC to make sure we use the least amount of resources for this and so we can respawn it easily if something goes wrong

Setting our own repository

We’ll be hosting our own Docker images in GitHub by using GitHub Actions to ensure a proper CI workflow. For this, we’ll add the code repositories as submodules so we can update them easily if any new versions are released. Our folder structure is as follows:

PSO server file structure
pso-server
├── Dockerfile
├── newserv
├── phosg
└── resource_dasm

Our Dockerfile should look like this:

Dockerfile
# 1st stage: prepare the base image for builds
FROM debian:bookworm-slim AS base
# Install dependencies
RUN apt update && apt install --no-install-recommends -y \
    # Common build dependencies
    cmake make g++ gcc \
    # phosg build dependencies
    zlib1g-dev python3 \
    # newserv build dependencies
    libevent-dev \
    && rm -rf /var/lib/apt/lists/*

# Build target
FROM base as build
# Define some argument variables
ARG BUILD_TYPE=Release
# First build and install phosg
COPY phosg /tmp/build/phosg
WORKDIR /tmp/build/phosg
RUN cmake . && make && make test && make install
# After that, build and install resource_dasm
COPY resource_dasm /tmp/build/resource_dasm
WORKDIR /tmp/build/resource_dasm
RUN cmake . && make && make install
# Then build newserv
COPY newserv /tmp/build/newserv
WORKDIR /tmp/build/newserv
# - Configure CMake and build
RUN cmake . && make

# Release target
FROM debian:bookworm-slim as release
# Install runtime dependencies
RUN apt update && apt install --no-install-recommends -y \
    # phosg runtime dependencies
    zlib1g \
    # newserv runtime dependencies
    libevent-dev \
    && rm -rf /var/lib/apt/lists/*
# Copy required libraries and objects
#COPY --from=build /usr/local/lib/libphosg.a /usr/local/lib/
#COPY --from=build /usr/local/lib/libresource_file.a /usr/local/lib/
#COPY --from=build /usr/local/include/resource_file /usr/local/include/resource_file
#COPY --from=build /usr/local/include/resource_file /usr/local/include/resource_file
# Set the workdir for newserv
WORKDIR /usr/newserv
# Copy all binaries
COPY --from=build /usr/local/bin /usr/local/bin/
COPY --from=build /tmp/build/newserv/newserv /usr/local/bin/newserv
# Copy newserv files
COPY --from=build /tmp/build/newserv/system /usr/newserv/system
# Set a default config file within the image
#RUN cp system/config.example.json system/config.json
ENTRYPOINT [ "newserv" ]

Where:

  • base: is the base image with all the required dependencies for compiling the several parts of the project
  • build: is the image that compiles and installs each component
  • release: is the image that should only contain the bare minimum dependencies and binaries required to run the PSO server

⚠️ TODO: Improve the Docker images layers and research whether it’s best to include the system folder inside the container image or externally mount it

Building the server

Creating the Docker image

Run the following command from the same folder where the Dockerfile is:

Build pso-server image
docker build --target release -t pso-server:release .

Running the server

Preparing a server configuration file

You must bind a config.json file to the Docker container so newserv finds it and configures the PSO server according to your needs. You can take newserv/system/config.example.json as an example and modify whatever parameter so it matches your network and server features.

Once done, save it as config.json.

Executing the Docker container

Run the following command depending on your situation:

  • network=host mode is the most straightforward but the less secure:

    Run pso-server with host network mode
    docker run -d --rm --name pso-server -v /path/to/your/config.json:/usr/newserv/system/config.json --network=host pso-server:release
  • (WIP) Isolating the container and exposing the ports should be the way to go:

    Run pso-server in isolated mode
    docker run -d --rm --name pso-server -p 53:53 -p 9000:9000 -p 9001:9001 -p 9002:9002 -p 9003:9003 -p 9064:9064 -p 9100:9100 -p 9103:9103 -p 9200:9200 -p 9201:9201 -p 9202:9202 -p 9203:9203 -p 9204:9204 -p 9300:9300 -p 5100:5100 -p 5110:5110 -p 5122:5122 -v /path/to/your/config.json:/usr/newserv/system/config.json pso-server:release

where:

  • --name is the container name of our server
  • -p is each exposed port from inside the container. Setting --network=host discards these as all ports are exposed to the host
  • -d means that the container execution is detached from the terminal and runs on background
  • --rm means that the container will be deleted once its main process ends. Any data generated inside the container and not stored in a bind volume will be lost
  • -v is for binding volumes inside the container, allowing persistence of data. In this case, we’re binding an external server config so newserv finds it when called

⚠️ TODO: Don’t use host as the docker network type when running the container! Try to expose only the required ports and redirect the DNS requests towards it

⚠️ TODO: Add screenshots of the server running and some clients connecting to it!

Maintaning the server

Currently all the server data is stored inside the Docker container (quests, players’ info, etc.), so if we want to add new quests or patches we have to recreate the image. newserv should be able to reload any new data stored in the system folder on runtime (pending to verify), so next steps would involve externally mounting the system folder or just bind new folders inside there.

As of now, any information stored during runtime will be lost on server shutdown.

⚠️ TODO: Learn where the quests and users are stored so we can persist their information

Updating the server

GitHub Actions

⚠️ TODO: build a CI workflow with GitHub Actions to automate the Docker image generation and publishing to the repository’s container registry!