A replacement for EA's long-dead online service for Tiger Woods PGA Tour 2004 on the PlayStation 2, for playing online through the PCSX2 emulator.
My server is online at jeddyh.fyi/TW04Online.
Create an account on the website and use it to log into the game's Online feature; the in-game account creation does not work.
It brings back the game's whole online menu:
- Lobby: accounts and personas, game rooms, chat, and challenges.
- Head-to-head play: match play and stroke play, with results recorded.
- Online tournaments: a daily event with its own leaderboards.
- Leaderboards and stats: your rank and statistics on the in-game screens.
- EA Messenger: buddy lists, blocking, who's online, and messages between players.
- In-game news: your own text, plus a digest the server writes from recent results.
- Report abuse: reports are stored with the chat that led up to them.
Alongside the lobby runs a web site. Players create their account and download the game patch there. It also shows live server status, leaderboards, the tournament calendar with a page for every event, monthly seasons and a Hall of Fame, player profiles with handicaps and achievements, head-to-head records, side-by-side comparisons, tour stats with a 30-day activity chart, records, and course pages with what each tournament setting costs in strokes. A private admin page covers password resets, bans, renames and the in-game news.
Both are plain Python with no dependencies: no framework, no database server, no build step.
How the game's online protocol was worked out -- every message, field and screen, read from the game's executable and a live console -- is written up in docs/PROTOCOL.md, and the scripts used to read the executable are in analysis/.
This is a fan project. It is not affiliated with, endorsed by or connected to Electronic Arts or Sony. No game files, BIOS or disc images are included, and none are needed to run the server.
- What you need
- Quick start
- How it fits together
- Running it for real
- How players connect
- Looking after it
- Options
- Files it creates
- Tests
- Troubleshooting
- Known limits
- License
- Python 3.8 or newer. There's nothing to
pip install. The SQLite built into Python must be 3.24 or newer, which any Python from the last several years has (python3 -c "import sqlite3; print(sqlite3.sqlite_version)"). - A machine that players can reach on:
| Port | Protocol | What uses it |
|---|---|---|
| 10200 | TCP | the lobby (lobbyd.py); consoles connect straight to it |
| 10202 | TCP | EA Messenger (buddy lists, presence, messages), inside lobbyd.py |
| 8080 | TCP | the web site (webui.py), or whatever you put in front of it |
The game itself (a match between two players) is peer-to-peer over UDP 3658 directly between the players. It never goes through this server; see Known limits.
git clone https://github.com/jeddyhhh/TW04OnlineServer.git
cd TW04OnlineServer
python3 webui.py --port 8080 # the web site
python3 lobbyd.py # the lobby, on port 10200Run each in its own terminal. Then open http://localhost:8080/, create an
account, and follow the Connect from PCSX2 steps on that page to point a
game at the server.
The site marks its login cookie Secure, which browsers accept over plain HTTP
only at localhost. To try it from another machine before you've set up
HTTPS, add --no-secure-cookie, or you'll be signed straight back out.
Both programs open the same database, data/tw04.db, next to the scripts. It's
created the first time either one starts. Each prints the full path it opened.
If those two paths ever differ, accounts made on the web site won't exist at
the game's login screen.
PCSX2 + the game ──TCP 10200──▶ lobbyd.py ──┐
│ ──TCP 10202──▶ (Messenger) │
│ ├──▶ data/tw04.db (SQLite)
│ │
└── UDP 3658 to the other player │
│
web browser ─────────HTTP──────▶ webui.py ──┘
lobbyd.pytalks to the consoles. It speaks EA's lobby protocol, introduces two players to each other when a challenge is accepted, and records results. It also writes the live picture (who's online, matches in play) into the database for the web site.webui.pyis the web site. It only ever reads what the lobby has written, apart from account sign-up and account management.- The game patch. The lobby's address is an IP written into the game disc,
so every player's game has to be patched to reach your server. The web site
builds that patch (a PCSX2
.pnachfile) for your server on demand, so there's nothing to prepare by hand.
The supporting modules, for anyone reading the code:
| File | What it is |
|---|---|
twdb.py |
the database: accounts, personas, results, tournaments, buddy lists, reports |
twrecords.py |
player pages, stat leaders, records, course pages, and the news digest |
twtourney.py |
the online tournament calendar and its wire format |
twstats.py |
the packed statistics record, and the course list |
tagfield.py, eacrypt.py |
EA's message encoding and the login password cipher |
make_pnach.py |
builds the game patch |
twlog.py |
the size-capped log both programs write |
tw04.sh |
runs and supervises both on Linux (see below) |
docs/PROTOCOL.md |
the protocol research notes: how everything above was worked out |
analysis/ |
the ELF analysis helpers those notes were worked out with (they need capstone and pyelftools; see analysis/README.md) |
tw04.sh starts both programs and restarts either one a few seconds after it
exits, so a crash doesn't need anyone to notice:
chmod +x tw04.sh
./tw04.sh start # start both; safe to run again, won't double-start
./tw04.sh start webui # just one of them
./tw04.sh status
./tw04.sh restart [lobbyd|webui]
./tw04.sh stop [lobbyd|webui]
./tw04.sh log lobbyd # follow a logSettings are at the top of the script, or can be given in the environment:
| Setting | Default | Meaning |
|---|---|---|
LOBBY_PORT |
10200 |
the lobby's TCP port |
BUDDY_PORT |
10202 |
EA Messenger's TCP port; empty to run without it |
BUDDY_ADDR |
(empty) | host:port to give consoles for Messenger, if this machine's own address isn't the public one (behind NAT) |
WEB_HOST |
127.0.0.1 |
where the web site listens; keep it local behind a reverse proxy |
WEB_PORT |
8080 |
the web site's port |
WEB_BASE |
/TW04Online |
the path the site is served under |
LOG_MAX_MB / LOG_KEEP |
10 / 3 |
each log rolls over at this size, keeping this many old copies |
PYTHON |
python3 |
the interpreter to use |
To start everything at boot, add this to crontab -e, as the user that owns
the files rather than root:
@reboot /path/to/repository/tw04.sh startOptionally, add a watchdog that restarts anything that isn't running:
* * * * * /path/to/repository/tw04.sh start >/dev/null 2>&1The web site is plain HTTP, so put it behind a reverse proxy that handles TLS.
With Apache, serving it at https://example.com/TW04Online:
# a2enmod proxy proxy_http
ProxyPreserveHost On
ProxyPass /TW04Online http://127.0.0.1:8080/TW04Online
ProxyPassReverse /TW04Online http://127.0.0.1:8080/TW04Onlineand run the site on localhost with the matching path, which tw04.sh does by
default:
python3 webui.py --host 127.0.0.1 --port 8080 --base-path /TW04Online --trust-proxy--trust-proxymatters here. Without it, every visitor appears to come from127.0.0.1, so the site's lockout after repeated wrong passwords becomes one shared counter for everybody. Only use it with a proxy actually in front, because anyone who can reach the port directly can fake the header it reads.- The session cookie is marked
Secure.--no-secure-cookieexists only for testing over plain HTTP. - If the downloaded patch points players at the wrong address, start the
site with
--advertise <your public hostname or IPv4>. It works out the address automatically otherwise. It's also needed if the lobby runs on a different machine from the site.
Only the web site goes behind the proxy. Consoles connect straight to TCP 10200 and 10202, so open both on your firewall.
- Put HTTPS in front of the web site (above).
- Don't run
lobbyd.pywith--open. It creates an account for anyone who types a new name at the console, which is handy on a LAN and wrong on the internet. - Open TCP 10200 and 10202 on the firewall.
The web site's front page walks players through all of this and gives them the patch file. In short, in PCSX2:
- Install the patch. Download the
.pnachfile from the site and put it in PCSX2'scheatsfolder, keeping its name exactly: PCSX2 matches it to the disc by name. Then right-click the game → Properties → Patches and tick Enable Cheats. - Turn on networking. Settings → Network & HDD:
- tick Enable Network (DEV9);
- set the Ethernet device type to Sockets;
- set Ethernet Device to the network adapter you use on Windows, and to Auto on Linux and the Steam Deck; naming the adapter there gets you into the lobby, but matches never start;
- set DNS1 to a real resolver, such as your router or
1.1.1.1. Leaving it on Auto makes the game's DNAS screen slow, especially on Linux.
- Save a network configuration (first time only). The game needs a PlayStation 2 network configuration on the memory card. If there isn't one, the game says so when you choose ONLINE and offers to create one with the network configurator on the game disc. Don't change anything: just save the default settings to the memory card.
- Sign in. Create an account on the web site, then sign in on the console's ONLINE menu with the same account name and password. Choose a persona on the SELECT ACCOUNT screen.
The patch never modifies the disc image. Deleting the .pnach file undoes it.
The front page also offers the same patch as cheat codes for a real console's
cheat engine: SLUS_207.57.cht for Open PS2 Loader, and a Cheat Device list.
make_pnach.py <ip> --real-ps2 writes both too. They need a PS2 that can run
homebrew, and a game-specific master code hooks the game (it's built in, taken
from the game's CodeBreaker master code). Nobody has tried them on a real
console yet. If you do, whether they work or not, please
open an issue with what
happened.
It works with Tiger Woods PGA Tour 2004, USA (NTSC-U), serial SLUS-20757,
CRC 64F9781E, and no other release.
Most day-to-day jobs are on a page that nothing links to:
<your site>/admin/<key>
Find an account by its name or any of its personas, then:
- Reset its password. Type one, or leave the box empty and the page makes up an 8-character one that's easy to type on the console keyboard. The new password is shown once, on the page. Players can't recover a password by themselves, so this is how a forgotten one gets fixed.
- Ban the account, or lift a ban. A ban blocks signing in, on the console and on the web site, for all of the account's personas. Anyone already in the lobby stays until they leave.
- Rename a persona. Its results, tournament rounds, buddies and reports move with it. The page refuses while that persona is online.
- Delete the account. It removes the account, its personas, and every round and match they played, for good. Type the account name to confirm. Abuse reports about them are kept as the moderation record.
The same page edits the in-game news (see below).
The key is created the first time the web site starts, kept in
data/admin.key, and printed at every start. Anyone with the address can do
all of the above, so keep it private. To change it, delete the file and restart
the site. webui.py --admin-key off turns the page off.
python3 twdb.py --list
python3 twdb.py --create-account NAME --password PASS [--persona PERSONA]
python3 twdb.py --add-persona ACCOUNT PERSONA
python3 twdb.py --set-password ACCOUNT PASS
python3 twdb.py --disable ACCOUNT # a ban; --enable undoes itStarting afresh. To wipe the server's play but keep everyone's account and personas, stop the server and run:
python3 twdb.py --reset-statsIt keeps accounts, personas, their uploaded golfers, buddy lists and the
abuse reports, and wipes matches, tournament rounds, the live board's
history. The tournament calendar goes too, and a new one is drawn when the
lobby next starts. It refuses to run while the lobby is up, saves a full copy
of the database first (before-reset-DATE.db beside it), and asks you to type
RESET (--yes skips that).
Just a new calendar. python3 twdb.py --reset-calendar (server stopped)
clears the tournament events from tomorrow on and picks a random calendar
generation, so the lobby deals a new schedule when it starts. Today, past
events and any day somebody has already played stay, so prize money and
leaderboards are unchanged, and nothing else is touched. Same safeguards:
a copy first (before-calendar-DATE.db) and RESET to confirm.
Players normally manage their own accounts and personas on the web site. An account can hold up to four personas.
The game's news screen shows the text of data/news.txt, if it exists. Edit
it on the admin page, or by hand. It's re-read on every request, so a change
shows up for the next player who opens the screen. The PS2 can only show plain
ASCII, and the admin page won't save anything else. Below it,
the server adds a digest it writes itself: today's event and leader,
yesterday's winner, the week's new records, and the busiest player.
--no-auto-news turns the digest off.
The lobby copies the database once a day into data/backups/, as
tw04-YYYY-MM-DD.db, and keeps the newest 7. It uses SQLite's own backup, so
the copy is consistent even while players are on. --backup-keep N changes
how many are kept (0 turns backups off), and --backup-dir puts them
somewhere else, such as another disk.
To restore one, stop both programs, copy the backup over data/tw04.db,
delete data/tw04.db-wal and data/tw04.db-shm if they exist, and start them
again.
When a player uses REPORT ABUSE in the game, the report is stored with the chat the server relayed in the hour before. Reports are listed on a page that nothing links to:
<your site>/reports/<key>
The key is created the first time the web site starts, kept in
data/reports.key, and printed at every start. Anyone with the address can
read the reports, so keep it private. To change it, delete the file and
restart the site. webui.py --reports-key off turns the page off. To ban
someone, use the admin page or twdb.py --disable.
The most useful ones; --help on either program lists them all.
lobbyd.py
| Option | Default | Meaning |
|---|---|---|
--port N |
10200 |
the lobby's TCP port; it must match the port in the players' patch |
--host ADDR |
0.0.0.0 |
where to listen; :: takes IPv6 and IPv4 together |
--buddy-port N |
10202 |
EA Messenger's port; 0 turns it off |
--buddy-addr HOST:PORT |
the Messenger address to give consoles, when this machine's own address isn't the public one | |
--db PATH |
data/tw04.db |
the database; must be the same file the web site uses |
--news FILE |
data/news.txt |
the news screen text |
--no-auto-news |
the news file only, without the generated digest | |
--backup-keep N |
7 |
daily database copies to keep; 0 turns backups off |
--backup-dir DIR |
data/backups |
where the daily copies go |
--open |
create an account on first login instead of refusing it (LAN only) | |
--logfile FILE |
logs/lobbyd.log |
the log; '' for none |
--quiet |
log to the file only, not the terminal too | |
--log-max-mb / --log-keep |
10 / 3 |
log rollover size and copies kept |
-v, --verbose |
hex dump of every message, for debugging |
webui.py
| Option | Default | Meaning |
|---|---|---|
--port N |
8080 |
the site's port |
--host ADDR |
0.0.0.0 |
where to listen; use 127.0.0.1 behind a proxy |
--base-path PATH |
serve under a path, e.g. /TW04Online |
|
--trust-proxy |
read the visitor's address from X-Forwarded-For (proxy only) |
|
--advertise HOST |
the address the downloadable patch points players at | |
--lobby-port N |
10200 |
the lobby port put in the patch, if the lobby hasn't published one |
--no-secure-cookie |
for testing over plain HTTP only | |
--reports-key KEY |
(from data/reports.key) |
the abuse-reports page's secret; off turns the page off |
--admin-key KEY |
(from data/admin.key) |
the admin page's secret; off turns the page off |
--news FILE |
data/news.txt |
the news file the admin page edits; must be the lobby's --news |
--db PATH |
data/tw04.db |
the database; must be the same file the lobby uses |
--logfile FILE |
logs/webui.log |
the request log; '' for none |
Everything the servers write goes in two folders next to the scripts. Both are
in .gitignore.
| Path | What it is |
|---|---|
data/tw04.db |
the database: every account and password hash, results, tournaments, buddy lists and reports |
data/backups/ |
a copy of the database for each of the last 7 days (see Backups) |
data/reports.key |
the secret in the abuse-reports page's address |
data/admin.key |
the secret in the admin page's address |
data/news.txt |
your news text, if you create it |
logs/lobbyd.log, logs/webui.log |
the logs, each capped at LOG_MAX_MB with LOG_KEEP old copies |
Passwords are stored as salted PBKDF2 hashes, and they're kept out of the logs
unless you start the lobby with --log-passwords, a debugging switch that
says so loudly when it's on.
Each test starts a real server on a spare port with a throwaway database, and drives it the way the game does:
python3 tests/lobbyd_selftest.py # message format and the password cipher
python3 tests/lobbyd_authtest.py # only real credentials get in
python3 tests/lobbyd_roomtest.py # rooms, challenges, rematches
python3 tests/lobbyd_addresstest.py # which address each console is given for peer to peer
python3 tests/twdb_resettest.py # --reset-stats keeps the accounts and wipes the rest
python3 tests/lobbyd_livetest.py # the live picture the web site reads
python3 tests/lobbyd_buddytest.py # EA Messenger
python3 tests/lobbyd_reporttest.py # abuse reports and their private page
python3 tests/twdb_statstest.py # scoring real captured rounds
python3 tests/twdb_tourneytest.py # ties, open days, and every match counted
python3 tests/webui_pagestest.py # player, stats, records and course pages; the news
python3 tests/webui_featurestest.py # event, h2h, compare, Hall of Fame and admin pages; handicapsMost modules also test themselves: python3 twrecords.py, twlog.py,
twtourney.py, twstats.py, tagfield.py and eacrypt.py.
- Nobody can sign in, but the web site works. The two programs are using different databases. Compare the path each one prints at startup.
- The console says the server can't be reached. Check the lobby is running
and TCP 10200 is open. Also check the port in the players' patch matches
--port: a port mismatch looks exactly like a server that's down. - Players get into the lobby, but matches never start. The two players have to reach each other on UDP 3658, as the lobby only introduces them. Check each player can receive on that port (a port forward, behind a home router). On Linux or a Steam Deck, check the Ethernet Device is set to Auto. Wi-Fi networks that isolate devices from each other also break this.
- The DNAS screen takes 30 seconds or more. Set DNS1 in PCSX2 to a real resolver (see How players connect).
- What happened to that connection?
logs/lobbyd.logrecords every message both ways, with passwords redacted (unless the lobby was started with--log-passwords). Add-vfor a hex dump of each message too.
- Matches are peer-to-peer. The two consoles play each other directly over UDP 3658, and there's no relay. Each player needs that port reachable (a port forward behind a home router). Two players behind the same router can play each other through an internet-hosted lobby only if their router supports "hairpin" NAT (NAT loopback); many do.
- IPv4 only, for players. The game can only express IPv4 addresses. The
lobby can listen on IPv6 (
--host ::), but a player connecting over IPv6 can't be introduced to an opponent. - Two emulators on one PC can't play each other, because both need UDP 3658.
- DNAS is passed, not removed. The patch makes the game's dead Sony DNAS check succeed, but the game still tries to reach Sony first, hence the DNS1 advice above.
- Only the one disc. The patch is a list of addresses in one build of the
game (
SLUS-20757). Other regions and Tiger Woods PGA Tour 2005 aren't supported.
The server code is released under the MIT License.
Tiger Woods PGA Tour 2004 is a trademark of Electronic Arts; PlayStation and DNAS are trademarks of Sony Interactive Entertainment. This project contains no code, data or assets from the game, and you need your own legally obtained copy to play.