Gametypes
A gametype is a file named scripts/<Name>Game.cs. It is discovered automatically, instantiated as a
ScriptObject named Game, and its same-named package is activated and deactivated with the mission.
There is no registration step anywhere.
Discovery
CreateServer() globs for them [script]:
//automatically load any mission type that follows naming convention typeGame.name.cs
%search = "scripts/*Game.cs";
for(%file = findFirstFile(%search); %file !$= ""; %file = findNextFile(%search))
{
%type = fileBase(%file); // get the name of the script
exec("scripts/" @ %type @ ".cs");
}
The glob runs through the mod path stack, so MyMod/scripts/RaceGame.cs is found and executed with no
further work. This is the cleanest extension point in the engine.
The shipped set: DefaultGame, CTFGame, DMGame, CnHGame, DnDGame, HuntersGame,
TeamHuntersGame, RabbitGame, SiegeGame, BountyGame, SinglePlayerGame, and the five Training*
gametypes.
Instantiation
loadMissionStage2 creates the object [script]:
if($CurrentMissionType $= "")
{
new ScriptObject(Game) {
class = DefaultGame;
};
}
else
{
new ScriptObject(Game) {
class = $CurrentMissionType @ "Game"; // e.g. "CTFGame"
superClass = DefaultGame;
};
}
// allow the game to activate any packages.
Game.activatePackages();
So Game.someMethod() resolves against CTFGame::someMethod first, DefaultGame::someMethod second. See
SimObjects and namespaces.
The package convention
function DefaultGame::activatePackages(%game)
{
// activate the default package for the game type
activatePackage(DefaultGame);
if(isPackage(%game.class) && %game.class !$= DefaultGame)
activatePackage(%game.class);
}
function DefaultGame::deactivatePackages(%game)
{
deactivatePackage(DefaultGame);
if(isPackage(%game.class) && %game.class !$= DefaultGame)
deactivatePackage(%game.class);
}
A package whose name matches the gametype class is activated at mission start and deactivated at mission end. Every shipped gametype uses it [script]:
package CTFGame {
function ShapeBaseData::onDestroyed(%data, %obj)
{
… CTF-specific scoring for destroyed objects …
}
…
};
That override only applies during a CTF mission. In a Deathmatch mission it is not in the chain at all. This is a genuinely elegant piece of design and worth exploiting: anything your mod should do only in one gametype belongs in that gametype’s package.
The file structure
A gametype file has four parts, in order:
// DisplayName = Capture the Flag ← 1. metadata comment
//--- GAME RULES BEGIN ---
//Prevent enemy from capturing your flag
//Score one point for grabbing the enemy's flag
//To capture, your flag must be at its stand
//Score 100 points each time enemy flag is captured
//--- GAME RULES END ---
//exec the AI scripts
exec("scripts/aiCTF.cs"); ← 2. AI companion file
function CTFGame::initGameVars(%game) ← 3. namespaced methods
{
%game.SCORE_PER_KILL = 1;
%game.SCORE_PER_PLYR_FLAG_CAP = 3;
%game.SCORE_PER_TEAM_FLAG_CAP = 100;
…
%game.FLAG_RETURN_DELAY = 45 * 1000; //45 seconds
%game.TIME_CONSIDERED_FLAGCARRIER_THREAT = 3 * 1000;
%game.RADIUS_GEN_DEFENSE = 20; //meters
}
…
package CTFGame { ← 4. the package
…
};
// DisplayName =
The first-line comment is parsed at runtime [script]:
function getMissionTypeDisplayNames()
{
%file = new FileObject();
for ( %type = 0; %type < $HostTypeCount; %type++ )
{
$HostTypeDisplayName[%type] = $HostTypeName[%type];
if ( %file.openForRead( "scripts/" @ $HostTypeName[%type] @ "Game.cs" ) )
{
while ( !%file.isEOF() )
{
%line = %file.readLine();
if ( getSubStr( %line, 0, 17 ) $= "// DisplayName = " )
{
$HostTypeDisplayName[%type] = getSubStr( %line, 17, 1000 );
break;
}
}
}
}
%file.delete();
}
The match is exact, on the first 17 characters — "// DisplayName = " with one space after //, one
space either side of =. Get it wrong and your gametype appears in menus under its raw class name.
Without it, RaceGame shows as “Race”. With it you can call it whatever you like.
initGameVars — the tuning block
DefaultGame::initGameVars sets defaults; each gametype overrides it to set its own [script]:
function CTFGame::initGameVars(%game)
{
%game.SCORE_PER_KILL = 1;
%game.SCORE_PER_SUICIDE = -1;
%game.SCORE_PER_TEAMKILL = -1;
%game.SCORE_PER_DEATH = -1;
%game.SCORE_PER_PLYR_FLAG_CAP = 3;
%game.SCORE_PER_TEAM_FLAG_CAP = 100;
%game.SCORE_PER_TEAM_FLAG_TOUCH = 1;
%game.SCORE_PER_GEN_DESTROY = 2;
%game.SCORE_PER_ESCORT_ASSIST = 1;
%game.SCORE_PER_TURRET_KILL = 1;
%game.SCORE_PER_FLAG_DEFEND = 1;
%game.SCORE_PER_CARRIER_KILL = 1;
%game.SCORE_PER_FLAG_RETURN = 1;
%game.SCORE_PER_GEN_DEFEND = 1;
%game.SCORE_PER_GEN_REPAIR = 1;
%game.FLAG_RETURN_DELAY = 45 * 1000; //45 seconds
%game.TIME_CONSIDERED_FLAGCARRIER_THREAT = 3 * 1000; //after damaging enemy flag carrier
%game.RADIUS_GEN_DEFENSE = 20; //meters
…
}
Every tunable number lives on %game as an ALL_CAPS field. It is called from
DefaultGame::missionLoadDone [script], so overriding initGameVars in a package is the tidiest way
to rebalance a gametype:
package MyMod
{
function CTFGame::initGameVars(%game)
{
Parent::initGameVars(%game);
%game.SCORE_PER_TEAM_FLAG_CAP = 50;
%game.FLAG_RETURN_DELAY = 20 * 1000;
}
};
The shipped roster
Nine gametypes ship in base/scripts.vl2, all extending defaultGame.cs, plus a tenth — Team Rabbit 2 —
that ships as its own set of archives entirely outside scripts.vl2. Sections 22–30 cover each in depth;
this table is the map between them [script]:
| File | Lines | Covered in |
|---|---|---|
CTFGame.cs |
2014 | 22 · Capture the Flag |
DnDGame.cs |
1331 | 23 · Defend and Destroy |
SiegeGame.cs |
1213 | 24 · Siege |
BountyGame.cs |
899 | 25 · Bounty |
CnHGame.cs |
466 | 26 · Capture and Hold |
DMGame.cs |
374 | 27 · Deathmatch |
HuntersGame.cs / TeamHuntersGame.cs |
1745 / 614 | 28 · Hunters & Team Hunters |
RabbitGame.cs |
739 | 29 · Rabbit |
TR2Game.cs (own archives) |
3204 | 30 · Team Rabbit 2 |
SinglePlayerGame.cs |
1310 | Not covered — the campaign, not a multiplayer gametype |
Four relationships to Classic
Reading all nine base gametypes against the Classic mod tree (37 · Classic) sorts them into four categories:
Shadowed — Classic ships its own replacement file. Only CTFGame.cs and SiegeGame.cs, the two
gametypes competitive play actually runs. Classic also adds a gametype in this style, SCtFGame.cs
(Spawn CTF), that exists only in the Classic tree.
Untouched — five gametypes (Bounty, Capture and Hold, Deathmatch, Hunters/Team Hunters, Rabbit) have
no Classic-side file at all and run exactly as base ships them under -mod Classic, because the mod path
stack falls through to base/ for anything the mod does not shadow. Classic changed the game’s speed and
admin surface; it left the party-game modes alone.
Added to base, not to Classic — Defend and Destroy is the anomaly documented in
31 · The base ruleset: a gametype written by Classic’s own
author, z0dd, but shipped by Sierra directly in base/scripts.vl2 rather than in the Classic mod tree.
Integrated by reference — Team Rabbit 2 is none of the above. Classic does not shadow it, but its
server.cs conditionally exec()s the base copy, and six other Classic files patch themselves to
recognise TR2’s custom weapons and HUD data. See 30 · Team Rabbit 2.
The DefaultGame:: callback surface
Roughly sixty methods [script]. The ones you will actually override:
Lifecycle
| Callback | When |
|---|---|
initGameVars(%game) |
Set scoring and tuning constants |
missionLoadDone(%game) |
Mission loaded — set up sensor groups, power, game state |
setUpTeams(%game) |
Build the teams |
startMatch(%game) |
The match begins |
checkTimeLimit(%game, %forced) |
Time-limit test |
checkObjectives(%game) |
Win-condition test |
gameOver(%game) |
The match ends |
sendDebriefing(%game, %client) |
Post-match summary |
activatePackages(%game) / deactivatePackages(%game) |
Package lifecycle |
Clients
| Callback | When |
|---|---|
clientMissionDropReady(%game, %client) |
Client finished loading |
assignClientTeam(%game, %client, %respawn) |
Team assignment |
clientJoinTeam(%game, %client, %team, %respawn) |
Joined a team |
clientChangeTeam(%game, %client, %team, %fromObs) |
Changed team |
onClientLeaveGame(%game, %client) |
Disconnected |
sendClientTeamList(%game, %client) |
Send the roster |
setupClientHuds(%game, %client) |
Initialise their HUD |
onClientEnterObserverMode(%game, %client) |
Went to observer |
forceObserver(%game, %client, %reason) |
Forced to observer |
Spawning
| Callback | When |
|---|---|
spawnPlayer(%game, %client, %respawn) |
Spawn entry point |
createPlayer(%game, %client, %spawnLoc, %respawn) |
Create the player object |
playerSpawned(%game, %player) |
After spawning |
equip(%game, %player) |
Give the starting loadout |
pickPlayerSpawn(%game, %client, %respawn) |
Choose a spawn point |
pickTeamSpawn(%game, %team) |
Team spawn selection |
selectSpawnSphere(%game, %team) |
Spawn sphere selection |
selectSpawnZone(%game, %sphere) |
Zone within a sphere |
selectSpawnFacing(%game, %src, %target, %zone) |
Facing direction |
pickObserverSpawn(%game, %client, %next) |
Observer camera |
Combat and scoring
| Callback | When |
|---|---|
onClientDamaged(%game, %clVictim, %clAttacker, %damageType, %sourceObject) |
Damage dealt |
onClientKilled(%game, %clVictim, %clKiller, %damageType, %implement, %damageLocation) |
Kill |
displayDeathMessages(%game, %clVictim, %clKiller, %damageType, %implement) |
Kill message |
friendlyFireMessage(%game, %damaged, %damager) |
Team-damage warning |
testKill / testSuicide / testTeamKill / testTurretKill / testOOBDeath |
Kill classification |
recalcScore(%game, %cl) |
Recompute a score |
recalcTeamRanks(%game, %client) |
Recompute rankings |
Objects and triggers
| Callback | When |
|---|---|
playerTouchFlag(%game, %player, %flag) |
Flag touched |
playerDroppedFlag(%game, %player, %flag) |
Flag dropped |
flagStandCollision(%game, %dataBlock, %obj, %colObj) |
Flag stand contact |
onEnterTrigger(%game, %triggerName, %data, %obj, %colobj) |
Entered a trigger |
onLeaveTrigger(...) / onTickTrigger(...) |
Left / inside a trigger |
notifyMineDeployed(%game, %mine) |
Mine placed |
claimSpawn(%game, %obj, %newTeam, %oldTeam) |
Spawn point captured |
claimFlipflopResources(%game, %flipflop, %team) |
Contested resource captured |
clearDeployableMaxes(%game) |
Reset deployable counts |
Item and weapon vetoes
Three methods act as vetoes and are how gametypes restrict equipment:
| Method | Vetoes |
|---|---|
Game.weaponOnUse(%data, %obj) |
Mounting a weapon — called from Weapon::onUse [script] |
Game.weaponOnInventory(%this, %obj, %amount) |
Weapon inventory changes |
Game.handInvOnUse(%data, %obj) |
Throwing a hand item — called from HandInventory::onUse [script] |
function Weapon::onUse(%data, %obj)
{
if(Game.weaponOnUse(%data, %obj)) // ← return false to block
if (%obj.getDataBlock().className $= Armor)
%obj.mountImage(%data.image, $WeaponSlot);
}
Overriding these in your gametype package is the correct way to build a weapon-restricted mode.
Recipe: a new gametype
MyMod/scripts/RaceGame.cs:
// DisplayName = Race
//--- GAME RULES BEGIN ---
//Reach every checkpoint in order, then return to the start
//Fastest lap wins
//--- GAME RULES END ---
//------------------------------------------------------------------------------
function RaceGame::initGameVars(%game)
{
Parent::initGameVars(%game);
%game.SCORE_PER_CHECKPOINT = 1;
%game.SCORE_PER_LAP = 25;
%game.LAP_COUNT = 3;
%game.numTeams = 1; // free-for-all
}
//------------------------------------------------------------------------------
function RaceGame::missionLoadDone(%game)
{
Parent::missionLoadDone(%game);
%game.checkpointCount = 0;
// Count the checkpoint triggers the .mis placed.
for (%i = 0; %i < MissionGroup.getCount(); %i++)
{
%obj = MissionGroup.getObject(%i);
if (%obj.getName() $= "checkpoint" @ %game.checkpointCount)
%game.checkpointCount++;
}
echo("RaceGame: " @ %game.checkpointCount @ " checkpoints");
}
//------------------------------------------------------------------------------
function RaceGame::equip(%game, %player)
{
// No weapons in a race.
%player.clearInventory();
%player.setInventory(RepairKit, 1);
}
//------------------------------------------------------------------------------
function RaceGame::onEnterTrigger(%game, %triggerName, %data, %obj, %colobj)
{
if (getSubStr(%triggerName, 0, 10) !$= "checkpoint")
{
Parent::onEnterTrigger(%game, %triggerName, %data, %obj, %colobj);
return;
}
%client = %colobj.client;
if (!isObject(%client))
return;
%index = getSubStr(%triggerName, 10, 10);
if (%index != %client.nextCheckpoint)
return; // out of order — ignore
%client.nextCheckpoint++;
%game.recalcScore(%client);
if (%client.nextCheckpoint >= %game.checkpointCount)
{
%client.nextCheckpoint = 0;
%client.laps++;
bottomPrint(%client, "Lap " @ %client.laps @ " of " @ %game.LAP_COUNT, 3, 1);
if (%client.laps >= %game.LAP_COUNT)
%game.gameOver();
}
else
bottomPrint(%client, "Checkpoint " @ %client.nextCheckpoint, 2, 1);
}
//------------------------------------------------------------------------------
function RaceGame::clientJoinTeam(%game, %client, %team, %respawn)
{
Parent::clientJoinTeam(%game, %client, %team, %respawn);
%client.nextCheckpoint = 0;
%client.laps = 0;
}
//------------------------------------------------------------------------------
// Gametype-scoped overrides go in the package — automatically activated when a
// Race mission loads, deactivated when it ends.
package RaceGame
{
function Weapon::onUse(%data, %obj)
{
// Weapons are disabled entirely in Race.
}
};
Then a mission needs // MissionTypes = Race in its header — see Missions.
A real example at scale. The Construction mod ships two gametypes this way (
ConstructionGame,TR2Game) and preserves thescripts/*Game.csdiscovery glob verbatim inside its own shadowedserver.cs[mod-script] — a total conversion that still loads its gametypes through Sierra’s mechanism untouched.
Overriding an existing gametype
package MyMod
{
function CTFGame::initGameVars(%game)
{
Parent::initGameVars(%game);
%game.SCORE_PER_TEAM_FLAG_CAP = 50;
}
function DefaultGame::onClientKilled(%game, %clVictim, %clKiller, %damageType,
%implement, %damageLocation)
{
Parent::onClientKilled(%game, %clVictim, %clKiller, %damageType,
%implement, %damageLocation);
if (%damageType == $DamageType::ShockLance)
messageAll('MyModStyleKill', '\c2%1 shocked %2!',
getTaggedString(%clKiller.name), getTaggedString(%clVictim.name));
}
};
activatePackage(MyMod);
Remember getTaggedString() around player names — see
Text and messaging.
Under the community patches
Gametype discovery, instantiation, the package convention, and the entire DefaultGame:: callback
surface are unchanged. A gametype written against vanilla runs on a patched server as-is.
Two things to know.
The master-server type functions are stubbed
t2csri_server replaces four vanilla functions with no-ops [patch-script]:
// deactivating old master list server protocol handlers in script
// sending a game type list to a dedicated server would result in a massive number
// of nuiscance calls to the following functions, and spam the console with pages of errors
// the errors were the main source of CPU utilization, so just setting stubs is adequate protection
function addGameType() { return; }
function clearGameTypes() { return; }
function clearMissionTypes() { return; }
function sortGameAndMissionTypeLists() { return; }
If your gametype calls addGameType(), it silently does nothing on a patched server. This is
deliberate and harmless — the old WON master-server type registration has nowhere to go, and the calls
were a genuine CPU cost.
What still works, unaffected:
scripts/*Game.csauto-discovery byCreateServer().// MissionTypes =parsing bybuildMissionList().// DisplayName =parsing bygetMissionTypeDisplayNames().- The
GameScriptObject,class/superClassdispatch, and package auto-activation.
Your gametype appears in the host menu through the mission scan, not through addGameType.
CreateServer has another link in the chain
The QoL patch packages it to load the server auth stack after vanilla’s work [patch-script]. If your
gametype file or mod also packages CreateServer, all three chain correctly provided every link calls
Parent::. See
Modding against a patched install.
Client-connection callbacks fire differently
DefaultGame::clientMissionDropReady and the rest of the client lifecycle are unchanged, but
GameConnection::onConnect now runs twice per remote client because of the pre-authentication phase. If
your gametype hooks connection directly rather than through DefaultGame::, read
Client/server split.
Related
- 22–30 · Gametypes — the full roster, each covered in depth
- Missions — the
.misside of the pairing - 31 · The base ruleset — the eleven shipped gametypes and how thin they are
- Boot sequence — the mission load chain
- 38 · Classic 1.1 — Defend and Destroy, a shipped gametype addition you can read end to end
- 39 · Classic 1.5.2 — Spawn CTF, and the
ai<Type>.cscompanion in practice - Packages — the auto-activation convention
- AI and bots — the
ai<Type>.cscompanion file - Modding against a patched install — the collision surface