Skip to content

Replace T7x with Ezz BOIII as the Black Ops III client - #10

Open
Ayymoss wants to merge 7 commits into
mainfrom
boiii
Open

Replace T7x with Ezz BOIII as the Black Ops III client#10
Ayymoss wants to merge 7 commits into
mainfrom
boiii

Conversation

@Ayymoss

@Ayymoss Ayymoss commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Black Ops III is served by Ezz BOIII now, and t7x is removed rather than kept alongside it. PLUTAINER_GAME=boiii, engine field ezz after the client's owner (the Alterware equivalent), client name Ezz BOIII.

Breaking, but nothing about a volume changes. BOIII reads the same gamefiles mount, the same app/configs/ and the same zone/ config directory, so moving a server across is one line of compose. A t7x tag is caught before the "unknown game" message and told exactly that — a tag that used to work is not the same as one that never did, and "unknown" sends someone hunting for a typo:

[ERROR] 't7x' is no longer supported. Black Ops III is served by
        Ezz BOIII now: set PLUTAINER_GAME=boiii.
        Nothing else changes — same gamefiles mount, same
        app/configs/, same zone/ config directory.

The hooks

cod_stage_boiii symlinks the BO3 server files, mirrors zone/ as a real directory, and mirrors usermaps/ when the mount has one — which is how Workshop maps reach the server. usermaps/ is tested for rather than handed to link_dir_contents, since most installs have none and it would warn on every start.
cod_update_boiii wget -N from r2.ezz.lol, so nothing is re-downloaded unless upstream is newer.
cod_launch_boiii -headless -dedicated -quiet-crash -watchdog, plus +map_rotate. -headless is what removes the X dependency, exactly as it did for t7x; -quiet-crash keeps a crash off a dialog nothing in a container can dismiss; -watchdog reports a wedged script VM instead of leaving a server that holds its port and answers nothing.

PLUTAINER_AUTO_UPDATE=false disables two updaters here, not one. BOIII ships an in-game updater as well as the binary Plutainer downloads, so the variable skips the wget and passes -noupdate. Without the second half, a hand-built boiii.exe in the volume is replaced at the next start — not hypothetical, that is how it is developed.

Seeds

Re-vendored as seed-configs/boiii from the same upstream commit, carrying the Dss0 zone/ configs and nothing else:

  • the bundle's t7x/ tree is another client's data directory, and BOIII unpacks its own boiii/ from the binary;
  • server_cp.cfg is a config for a mode BOIII cannot host, and a seeded config is a suggestion that the mode works.

Both are dropped by tools/refresh-seeds.sh rather than filtered at runtime, so what ships is what is in the repo.

Verified

On a clean volume holding nothing but boiii.exe:

[INFO] Initialised fresh v2 volume at /home/plutainer/app
[INFO] Ezz BOIII (Black Ops III) — boiii
Skipping BOIII update because PLUTAINER_AUTO_UPDATE is set to 'false'.
[INFO] rcon_password set from PLUTAINER_RCON_PASSWORD (15 chars) in server_zm.cfg.
### healthy after 30s

configs/ holds server.cfg and server_zm.cfg only, zone/*.cfg symlink back to configs/, rcon_password comes from the environment, five logs are symlinked into app/logs/, rcon-cli status answers, default map zm_tomb, and a Workshop map from usermaps/ hosts and reports healthy. A real client joined, played and cycled maps against this build.

Known limitation, not fixed here

The boiii.exe published at r2.ezz.lol cannot cold-start a dedicated server: it gates a launcher-UI file check on a file a server never downloads, so a fresh container dies with "BOIII needs an active internet connection for the first time you launch it" however good the connection is. Fixed upstream but not yet published, so a first run currently needs a binary supplied by hand. Worth holding the merge for, or worth a caveat in the docs — your call.

BOIII is a second client for Black Ops III, so it is added next to t7x rather
than in place of it: PLUTAINER_GAME=boiii. The two share the gamefiles mount,
the zone/ engine config dir, the Dss0/t7-server-config seed bundle and the
default port, which makes switching a server between them one line of compose.
Staging is written once as cod_stage_bo3 and both hooks call it; only the
binary and its launch flags differ. Verified end to end on a fresh volume:
healthy, map loaded, rcon-cli answering, config symlinks fanned out into
zone/, rcon_password written from the environment.

usermaps/ is mirrored into the volume when the mount has one, which is how
Workshop maps reach either server. It is tested for rather than passed
straight to link_dir_contents, since most installs have none and the helper
would warn about it on every start. Verified by hosting a Workshop map through
the container.

BOIII launches with -quiet-crash, so a crash cannot stop on a dialog nothing
in a container can dismiss, and -watchdog, which reports a hung script VM to
the log instead of leaving a server that holds its port and answers nothing.

PLUTAINER_AUTO_UPDATE=false does more here than elsewhere. BOIII ships an
in-game updater as well as the binary Plutainer downloads, so the variable has
to turn off both halves - it skips the wget and passes -noupdate - or a build
placed in app/runtime/gamefiles/boiii.exe by hand is replaced at the next
start. That is not hypothetical: the build under test is a local one.

The engine field is renamed from `alterware` to `t7x`, and cod_update_alterware
and cod_launch_alterware with it. T7x is no longer an Alterware project and
nothing else in the table used that engine, so the name resolved to exactly one
game and misdescribed it. Nothing about hook resolution changes: t7x's game
tag, base game and engine were already all "t7x".

Known limitation, not fixed here: the boiii.exe published at r2.ezz.lol cannot
cold-start as a dedicated server. It gates a launcher-UI file check on a file
a server never fetches, so a fresh container dies with "BOIII needs an active
internet connection for the first time you launch it" however good the
connection is. Fixed upstream but not yet published, so a first run currently
needs a binary supplied by hand.
BOIII shares the Dss0/t7-server-config bundle with t7x, but the bundle is
zone/*.cfg plus a t7x/ tree - bots.txt, gamesettings, lobby scripts - and
seed_configs places everything that is not a lifted cfg under the asset root.
So a clean BOIII volume came up with a t7x/ directory in its game files that
nothing ever opens: BOIII reads boiii/, which it unpacks from its own binary.

COD_SEED_CFG_ONLY=true takes the configs and leaves the rest, so the bundle
can be shared without the parts that are not shared. Verified on a clean
volume: configs seeded, rcon_password written from the environment, config
symlinks fanned into zone/, healthy in 30 s, and no t7x/.
BOIII hosts multiplayer and zombies only, so server_cp.cfg is a config for a
mode it cannot run - and a seeded config is a suggestion that it can.
COD_SEED_SKIP_CFGS names the cfgs a shared bundle carries that a given game
has no use for; t7x still gets all three.

Verified on a clean volume: configs/ holds server.cfg and server_zm.cfg only.
BOIII replaces t7x rather than joining it: Black Ops III has one supported
client here now. `PLUTAINER_GAME=t7x` is retired, `boiii` takes its place, and
the engine field is `ezz` after the client's owner - the Alterware equivalent -
with `Ezz BOIII` as the client name.

Nothing about a volume changes. BOIII reads the same gamefiles mount, the same
app/configs/, and the same zone/ config directory, so moving a server across is
one line of compose. A t7x tag is caught before the "unknown game" message and
told exactly that, because a tag that used to work is not the same as one that
never did and "unknown" sends someone hunting for a typo.

  cod_stage_boiii   symlinks the BO3 server files, mirrors zone/ as a real
                    directory, and mirrors usermaps/ when the mount has one -
                    which is how Workshop maps reach the server. usermaps/ is
                    tested for rather than passed to link_dir_contents, since
                    most installs have none and it would warn on every start.
  cod_update_boiii  wget -N from r2.ezz.lol, so nothing is re-downloaded
                    unless upstream is newer.
  cod_launch_boiii  -headless -dedicated -quiet-crash -watchdog, +map_rotate.
                    -headless is what removes the X dependency, exactly as it
                    did for t7x; -quiet-crash keeps a crash off a dialog
                    nothing in a container can dismiss; -watchdog reports a
                    wedged script VM instead of leaving a server that holds
                    its port and answers nothing.

PLUTAINER_AUTO_UPDATE=false has to disable two updaters here, not one: BOIII
ships an in-game updater as well as the binary Plutainer downloads, so the
variable skips the wget and passes -noupdate. Without the second half a
hand-built boiii.exe in the volume is replaced at the next start, which is not
hypothetical - that is how it is developed.

The seed bundle is re-vendored as seed-configs/boiii from the same upstream
commit, carrying the Dss0 zone/ configs and nothing else. The bundle's t7x/
tree is another client's data directory, and server_cp.cfg is a config for a
mode BOIII cannot host - a seeded config is a suggestion that the mode works.
Both are dropped by tools/refresh-seeds.sh rather than filtered at runtime, so
what ships is what is in the repo.

Verified on a clean volume holding nothing but boiii.exe: fresh v2 layout,
configs seeded (server.cfg and server_zm.cfg only), rcon_password written from
the environment, config symlinks fanned into zone/, healthy in 30 s, rcon-cli
answering, default map zm_tomb, and a Workshop map hosted from usermaps/.
@Ayymoss Ayymoss changed the title BOIII support, and rename the alterware engine to t7x Replace T7x with Ezz BOIII as the Black Ops III client Aug 19, 2026
After an ERR_DROP a BO3 server can shut the game down, fail to rebuild its
lobby, and never host anything again - while getinfo keeps answering with the
LAST map's name and sv_running reads 0. Measured on boiii before the rotation
recovery landed: the port answered, the reply looked right, and the server was
dead. The health check accepted it, so Docker called the container healthy for
as long as it sat there.

Where the engine reports sv_running, believe it over the name. Engines that do
not report it are unaffected - the key is simply absent and the map-name test
decides as before.

Verified three ways against a stub that replies exactly as a stranded server
does, plus the live container:

  sv_running 0, map name present   exit 1  "answered but sv_running is 0"
  sv_running 1, map name present   exit 0
  real boiii server                exit 0
A clean volume with auto-update at its default fetches
r2.ezz.lol/boiii/boiii.exe and produces a container that is Up, silent and
useless. That build has two independent faults, both fixed in the client's
source and neither in the artifact being served:

  * the launcher-UI check runs before the client/server split, so a server
    fails a check for a file only a client downloads and exits rc=1 blaming
    the network, which is not the cause.
  * the headless console calls AllocConsole() at static-init. Under Wine with
    no display that starts conhost.exe and the process blocks on its pipe:
    one thread, 0% CPU, 36MB RSS, wchan pipe_read, no UDP socket, and nothing
    in `docker logs` for the container's whole lifetime.

The obvious workaround does not work. The first check is a bare file_exists
on %LOCALAPPDATA%/boiii/data/launcher/main.html, so an empty stub clears it -
verified - and the run then lands on the console hang, which no file can
satisfy.

The second fault is why this refuses rather than warns. A warning would be
followed by exactly the silent container described above, which is the least
debuggable thing this image can produce; hold_indefinitely at least says why.

Pinned by the sha256 of the known-BAD artifact rather than of a known-good
one, so the day anything else is published the check stops firing on its own
and no release of this image is needed to unblock it.

Measured both directions on 10.10.1.22: published binary on a clean volume
refuses with the explanation and holds, restarts=0; a build carrying both
fixes is untouched by the check and reaches healthy, hosting zm_tomb. Also
fills the "No cod_validate_* hook for: boiii" warning, which was real.
parse_packet split the response into type and payload at the first newline.
That is right for query replies, which are framed "statusResponse\n\key\value",
and wrong for rcon replies, which are framed "print <text>" with the first line
of the answer on the same line as the token. The token therefore swallowed it.

How much that cost depended on the command, and the worst case was silent:

  status          lost its "map: <name>" banner - cosmetic
  <dvar>          lost the value line, which is the entire answer. Querying a
                  dvar printed only "  Domain is any text"
  <typo>          lost 'Unknown command "..."' completely, so rcon-cli printed
                  nothing at all and a mistyped command was indistinguishable
                  from a server that had stopped answering

Split on the first whitespace of any kind instead, since the separator is
framing rather than payload and which one appears varies by reply. A reply with
no separator at all - t5 zombies answers an unexpected connectionless packet
with a bare "disconnect" - still parses as "type, empty body".

This was never BOIII-specific. Measured on 10.10.1.22 across t6zm, iw5mp, iw4x,
t4mp, cod4x and boiii: all six now print the first line that was being dropped
("map: zm_buried", "hostname: My Server Name"), and all six still pass the
health check afterwards - including iw5, which answers getinfo rather than
getstatus, and so exercises the fallback path the same parser backs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant