Tribes 2 // Mod Development Handbook GitHub

48 · TacoServer

The codebase most public Tribes 2 servers run today. Maintained by ChocoTaco1 at github.com/ChocoTaco1/TacoServer, openly developed, still receiving commits in 2026, and the presumptive basis — directly or as a fork — for the 25th anniversary tournament.

It is also the best-documented case in this handbook of a mod inheriting from one ancestor while absorbing features from another, and then deliberately severing the second.

   
Repository ChocoTaco1/TacoServer, branch Stable [mod-script]
Latest commit surveyed 14 Jun 2026, “Merge branch ‘Dev’ into Stable”
Contents 58 .cs — 55 scripts, 3 prefs
Base Classic 1.5.2, required separately
Strategy Overlay — replace some files, add per-feature packages
Licence Open source; see the repository

It is an overlay, not a mod

The README is unambiguous [mod-script]:

  • Meant to be installed on top of Classic 1.5.2
  • If a file isnt on this github it is unmodified in Classic 1.5.2

This is the single most important fact about TacoServer and it explains its file count. Fingerprinting against Classic 1.5.2:

  Count
Files 1.5.2 has that TacoServer does not ship 47
Files present in both, modified 23
Files present in both, byte-identical 2
Files new in TacoServer 30

Those 47 absences are not deletions. turret.cs, station.cs, deployables.cs, all seven turret barrels, most weapons and most vehicles are simply left alone — they stay on disk from the 1.5.2 install underneath. Install TacoServer without 1.5.2 present and you get a broken tree, because the engine’s mod path will fall through to base/ for everything TacoServer does not ship, silently discarding every Classic rule in those files.

This is the mod path stack used as a versioning tool. See Mod paths and overrides.

The lineage, evidenced

The received account — and the one this handbook was asked to document — is that ChocoTaco’s Classic codebase started from Evolution Admin Mod 1.2.3c plus its improvements, and later deviated into his own administration source. The repository bears that out precisely, and the evidence divides cleanly.

Classic 1.5.2 is the base, not Evolution

Signal Finding
z0dd’s authorship signature 589 occurrences still present [mod-script]
$Host::ClassicLoad* toggles All ten survive intact (section 40)
.ovl files None
evoSupport.cs, evoPackage.cs, createEvolutionPackage() None
README prerequisite “Update to Classic 1.5.2”

Evolution’s entire architecture — the generated package, the .ovl split, the 89 $Host::Evo* prefs — is gone. Classic 1.5.2’s is entirely intact.

Evolution’s features were ported, with attribution

What remains of Evolution is source-level inheritance in individual features, marked by the authors:

Location Evidence
autoexec/NoBaseRape.cs:4 // From Evolution MOD
autoexec/TKwarn.cs // From Evo, on a DefaultGame::testTeamKill override
DMGame.cs:944, LCTFGame.cs:2699 // For voting to work properly - evo admin.ovl
autoexec/zCheckVer.cs:149,158 Comments citing defaultgame.ovl and admin.ovl by name
autoexec/AntiLouExploitFixes.cs The AntiLou fix set — Ilys’ work, packaged by Evolution 1.2.3 [mod-script]
autoexec/ObserveFlag.cs:289 Commented-out $Host::EvoFullServerPWEnabled, %pizzaOptMask

That last line is a small archaeological gem: %pizzaOptMask is a bitmask named for Pizza Admin Mod, Evolution’s own ancestor (section 45). A variable name has survived three codebases and twenty-two years.

AntiLouExploitFixes.cs is the thread from section 39 completing its journey: exploit fixes credited to ilys in Classic 1.5’s changelog, bundled by Evolution 1.2.3 as AntiLou, and carried into TacoServer under that name.

The severance was deliberate and dated

Git history names it. The earliest commit reachable in the surveyed history is, verbatim:

2021-07-17  Merge branch 'NoEvo'

NoEvo was not a one-off cleanup. It was a maintained branch, merged repeatedly:

Date  
2021-07-17 Merge branch ‘NoEvo’
2021-08-16 Merge branch ‘NoEvo’
2021-08-29 Merge branch ‘NoEvo’
2021-09-14 Merge branch ‘NoEvo’
2021-11-23 Merge branch ‘NoEvo’
2022-01-11 Merge branch ‘NoEvo’
2022-02-17 Merge branch ‘NoEvo’
2022-04-05 Merge branch ‘NoEvo’
2025-01-11 Remove EvoStats

Nine months of merges to remove a dependency, and a final Evolution component — the stats system — not removed until January 2025. Some Evolution compatibility is still present as display options: $dtStats::evoStyleDebrief selects an Evolution-style end-of-match screen, and degrades if an EvoStats.cs is found [mod-script]:

$dtStats::evoStyleDebrief = isFile("scripts/autoexec/EvoStats.cs") == 0 ? $dtStats::evoStyleDebrief : 0;

So the accurate summary is: TacoServer’s code descends from Classic 1.5.2; its feature set descends from Evolution; and disentangling the two took from 2021 to 2025. The user-facing behaviour Evolution established outlived Evolution’s code by four years.

The architecture that replaced .ovl

Evolution’s problem was that packages cannot span files, so it generated one giant package (section 45). TacoServer solves the same problem the other way: one package per feature, one feature per file, all in scripts/autoexec/.

autoexec/TKwarn.cs opens [mod-script]:

//exec("scripts/autoexec/TKwarn.cs");

package TKwarn
{

// From Evo
function DefaultGame::testTeamKill(%game, %victimID, %killerID, %damageType)
{
   
}

Twenty-one of the twenty-five autoexec files declare their own package. The base engine’s autoexec sweep executes everything in scripts/autoexec/ at startup (Boot sequence), so each file registers and activates itself with no central manifest.

Compared to Evolution this is strictly better on every axis that matters:

  Evolution .ovl TacoServer autoexec
Packages One, generated ~21, hand-written
Build step Yes — writes a file at boot None
Stale-cache trap Yes, notorious None
Individually disableable No Yes — delete or rename one file
Error line numbers Point into generated code Point at real source
Load order control findFirstFile order Filename prefix — z, zz

The z, zz and z_dt prefixes are the load-order mechanism: zzDiscordBot.cs loads after zCheckVer.cs loads after AntiPack.cs, because the autoexec sweep is alphabetical. The same trick as zz_Classic_client_v1.vl2 (section 38), applied to script load order rather than archive precedence.

This is the pattern to copy. It needs no generation step, no cache, and no manifest; a feature is a file, and removing the file removes the feature.

Four files declare no package — EnableLogs.cs, MemPatches.cs, multipleMapRotation.cs, spawnDir.cs. Those define new functions and globals rather than overriding existing ones, which is exactly the split Evolution drew between its .cs helpers and its .ovl overrides. The distinction is real and both codebases found it independently: only overrides need packages.

The teamkill log, twenty-two years on

Sections 45–47 traced a single logging line. TacoServer’s treatment of the same function closes the arc.

Evolution logged inline inside the override, referencing an undeclared %client (section 47). TacoServer overrides the same function but extracts the logging into a separate function taking exactly what it needs [mod-script]:

// in TKwarn.cs, inside package TKwarn
   teamkillLog(%victimID, %killerID, %damageType);
// in EnableLogs.cs
function teamkillLog(%victimID, %killerID, %damageType)
{
   
   $teamkillLog = formatTimeString("M-d") SPC formatTimeString("[hh:nn:a]") SPC %s
      @ %killerID.nameBase @ "(" @ %killerID.guid @ ")[" @ %type @ "][" @ %ktk @ " tk] teamkilled"
      SPC %victimID.nameBase @ "[" @ %vtk @ " tk]. #P[" @ $HostGamePlayerCount @ "] CM[" @ $CurrentMission @ "]";
   $teamkillLog = stripChars($teamkillLog, "\c0\c1\c2\c3\c4\c5\c6\c7\c8\c9\x10\x11\co\cp");
   export("$teamkillLog", %logpath, true);
   logEcho($teamkillLog);
}

The 2004 bug is now structurally impossible. teamkillLog’s parameters are precisely the two players and the damage type; there is no ambient %client to reach for. It also gained running teamkill counts for both players, the damage type, player count and current mission — and stripChars to remove colour control codes before writing, so the log is greppable.

The fix was not “use the right variable”; it was “give the function only what it needs”. That is the general lesson, and it is why the same defect cannot recur here.

One live defect in the same file

While reading it: EnableLogs.cs:161 guards the function with

   if(!$CurrentMissionType $= "CTF" && !$CurrentMissionType $= "SCTF")
      return;

In TorqueScript, unary ! binds tighter than the string comparison $=, so this parses as (!$CurrentMissionType) $= "CTF". $CurrentMissionType holds a non-numeric string such as "CTF", whose numeric value is 0, so !$CurrentMissionType is 1 — and "1" $= "CTF" is false. Both operands are false, the && is false, and the guard never fires [inferred].

The intended form is almost certainly:

   if($CurrentMissionType !$= "CTF" && $CurrentMissionType !$= "SCTF")
      return;

The effect is benign — teamkills are logged on every mission type rather than only CTF and Spawn CTF, so the log is broader than intended, not missing entries. It is marked [inferred] because the reasoning is from operator precedence and not from an executed test; if you are running TacoServer, a non-CTF map and a glance at logs/ settles it in a minute. The pattern is the same family as section 47’s: a condition that reads correctly in English and does nothing in TorqueScript, failing silently.

Scale

File Lines  
autoexec/z_dtStats.cs 20,558 The statistics system — 1.9 MB, by far the largest script in this handbook
autoexec/VoteMenu.cs 1,924 Vote system rework
defaultGame.cs 152 KB
server.cs 114 KB
LCTFGame.cs 104 KB LakRabbit CTF
LakRabbitGame.cs 93 KB  

z_dtStats.cs deserves its own note: a 20,000-line single-file stats engine, in a language with no modules, that tracks per-player statistics across a match and renders them in-game. It is the largest single artefact the Tribes 2 modding community produced and it lives in an autoexec directory.