Gamemodes
A gamemode defines the identity of a server. It controls the rules, progression, and feel of the game. Each server runs exactly one gamemode.
Gamemodes live in server/gamemodes/{id}/ and export a createGamemode() function that returns a GamemodeDefinition.
What a gamemode controls
- XP multipliers and drop rates
- Spawn location and tutorial flow
- Player initialization and state serialization
- Login handshake (varps, varbits, feature flags)
- Per-tick hooks and interaction restrictions
- Handler registration (banking, shops, equipment, UI widgets, content interactions)
- Display name formatting and chat player types
- Custom content data packets
- Service providers exposed to script handlers
Inheritance Chain
BaseGamemode (abstract — sensible OSRS defaults, no content)
└─ VanillaGamemode (full OSRS — banking, shops, combat, skills, widgets)
└─ LeaguesVGamemode (league tasks, relics, area unlocks, tutorial)
└─ YourGamemode (your customizations on top of vanilla)There are two paths for creating a gamemode:
| Base class | When to use |
|---|---|
BaseGamemode | Building from scratch. You get valid defaults but no content — no banking, no shops, no skills. Suitable for minigame servers or highly custom experiences. |
VanillaGamemode | Most common. You inherit the full OSRS experience and override what you need. This is what Leagues V does. |
Bundled Gamemodes
| Gamemode | Base | Description |
|---|---|---|
vanilla | BaseGamemode | Baseline OSRS — banking, shops, equipment, combat, all skills, all UI widgets, core content interactions |
leagues-v | VanillaGamemode | Raging Echoes — area unlocks, relics, masteries, tasks, league tutorial, custom XP/drop rates |
Creating a Gamemode
1. Create the directory
server/gamemodes/my-gamemode/
index.tsThe GamemodeRegistry discovers gamemodes by scanning server/gamemodes/ for directories containing an index.ts or index.js that exports createGamemode().
2. Write the gamemode class
Extending VanillaGamemode (recommended)
// server/gamemodes/my-gamemode/index.ts
import type { GamemodeDefinition, GamemodeInitContext } from "../../src/game/gamemodes/GamemodeDefinition";
import type { PlayerState } from "../../src/game/player";
import type { IScriptRegistry, ScriptServices } from "../../src/game/scripts/types";
import { VanillaGamemode } from "../vanilla/index";
class MyGamemode extends VanillaGamemode {
override readonly id = "my-gamemode";
override readonly name = "My Gamemode";
override getSkillXpMultiplier(): number {
return 10;
}
override getDropRateMultiplier(): number {
return 3;
}
override initializePlayer(player: PlayerState): void {
player.energy.drainEnabled = false;
}
override registerHandlers(registry: IScriptRegistry, services: ScriptServices): void {
super.registerHandlers(registry, services); // inherit all vanilla handlers
// register your own handlers here
}
override initialize(context: GamemodeInitContext): void {
super.initialize(context); // initialize vanilla systems (combat, banking, shops, etc.)
// initialize your own systems here
}
}
export function createGamemode(): GamemodeDefinition {
return new MyGamemode();
}This gives you the full vanilla experience (banking, shops, equipment, skills, combat, all UI) with 10x XP, 3x drop rates, and infinite run energy.
Extending BaseGamemode (from scratch)
// server/gamemodes/my-gamemode/index.ts
import type { GamemodeDefinition } from "../../src/game/gamemodes/GamemodeDefinition";
import { BaseGamemode } from "../../src/game/gamemodes/BaseGamemode";
class MyGamemode extends BaseGamemode {
readonly id = "my-gamemode";
readonly name = "My Gamemode";
override getSkillXpMultiplier(): number {
return 5; // 5x XP
}
}
export function createGamemode(): GamemodeDefinition {
return new MyGamemode();
}This gives you a working gamemode with 5x XP and all other OSRS defaults (Lumbridge spawn, no tutorial, standard drop rates). It won't have banking, shops, or skills — you'd register those yourself.
3. Run your gamemode
Set the gamemode ID in your server configuration:
- config.json:
{ "gamemode": "my-gamemode" } - Environment variable:
GAMEMODE=my-gamemode
The default gamemode is vanilla.
Where Logic Should Live
| Logic type | Location | Example |
|---|---|---|
| Engine systems (ticks, networking, player sync) | server/src/ | Collision, pathfinding, packet routing |
| Pluggable data providers (combat formulas, spells) | server/src/game/providers/ interfaces, vanilla/combat/ implementations | CombatFormulaProvider, SpellDataProvider |
| Reusable services (shop orchestration, banking) | Service classes in the gamemode (vanilla/shops/ShopService.ts) | ShopService wraps ShopManager + server integration |
| Gamemode-specific rules (XP rates, tutorials, relics) | server/gamemodes/{id}/index.ts overrides | LeaguesV XP multiplier, tutorial flow |
| Universal tools (debug commands, admin) | server/extrascripts/{id}/ | item-spawner |
Keep gamemode index.ts files thin — they should wire systems together, not implement them. Extract complex logic into dedicated service classes (like ShopService) so the gamemode just instantiates and connects them.
Structure
server/gamemodes/vanilla/
├── index.ts # VanillaGamemode class (extends BaseGamemode)
├── banking/ # BankingManager + handler registration
├── combat/ # Combat formulas, special attacks, equipment bonuses
├── data/ # Weapons, spells, runes, projectiles, login defaults
├── equipment/ # Equipment actions + widget handlers
├── shops/ # ShopManager, ShopService + widget handlers
├── skills/ # All skill implementations (mining, fishing, etc.)
├── scripts/
│ ├── content/ # Climbing, doors, al-kharid border, etc.
│ ├── items/ # Followers, packs
│ └── levelup.ts # Level-up display
├── modals/ # Widget open/close handlers, smithing modal
└── widgets/ # Combat, prayer, spellbook, minimap, settings, etc.
server/gamemodes/leagues-v/
├── index.ts # LeaguesVGamemode (extends VanillaGamemode)
├── LeagueContentProvider.ts # Custom content data packet
├── LeagueTaskManager.ts # Task completion tracking
├── LeagueTaskService.ts # Task progress helpers
├── LeaguesVUiController.ts # League-specific UI controller
├── scripts/ # League tutor, league widgets, tutorial widgets
├── data/ # Task/mastery/relic definitions, custom items
└── ...Handler Registration
Gamemodes register interaction handlers via registerHandlers(). The IScriptRegistry supports 86+ handler types including NPC interactions, loc interactions, item actions, widget buttons, commands, and more.
override registerHandlers(registry: IScriptRegistry, services: ScriptServices): void {
super.registerHandlers(registry, services); // inherit parent handlers
registerMyNpcHandlers(registry, services);
registerMyWidgetHandlers(registry, services);
}Handlers registered by the gamemode run first. Extrascript handlers are loaded after.
Service Providers
Gamemodes can create stateful managers and expose them to script handlers via contributeScriptServices():
override initialize(context: GamemodeInitContext): void {
super.initialize(context);
this.bankingManager = new BankingManager(context.serverServices);
}
contributeScriptServices(services: ScriptServices): void {
services.banking = {
openBank: (player, opts) => this.bankingManager.openBank(player, opts),
// ...
};
}Handlers then access these via services.banking.openBank(player) without knowing about the underlying manager.
Global Providers
VanillaGamemode registers 13 global data providers during initialize() that power the core combat and spell systems. Each provider is a singleton registered to the ProviderRegistry — the last call wins, so you can replace any provider after super.initialize().
| Provider | Create function | Source file |
|---|---|---|
CombatFormulaProvider | createCombatFormulaProvider() | vanilla/combat/CombatFormulas.ts |
WeaponDataProvider | createWeaponDataProvider() | vanilla/data/weapons.ts |
SpecialAttackProvider | createSpecialAttackProvider() | vanilla/combat/SpecialAttackRegistry.ts |
EquipmentBonusProvider | createEquipmentBonusProvider() | vanilla/combat/EquipmentBonuses.ts |
SpellDataProvider | createSpellDataProvider() | vanilla/data/spells.ts |
SpellXpProvider | createSpellXpProvider() | vanilla/combat/SpellXpData.ts |
RuneDataProvider | createRuneDataProvider() | vanilla/data/runes.ts |
ProjectileParamsProvider | createProjectileParamsProvider() | vanilla/data/projectileParams.ts |
SkillConfigurationProvider | createSkillConfiguration() | vanilla/combat/SkillConfiguration.ts |
CombatStyleSequenceProvider | createCombatStyleSequenceProvider() | vanilla/combat/CombatStyleSequences.ts |
SpecialAttackVisualProvider | createSpecialAttackVisualProvider() | vanilla/combat/SpecialAttackVisuals.ts |
InstantUtilitySpecialProvider | createInstantUtilitySpecialProvider() | vanilla/combat/RockKnockerSpecial.ts |
AmmoDataProvider | createDefaultAmmoDataProvider() | server/src/game/combat/AmmoSystem.ts |
Gamemodes extending VanillaGamemode inherit all of these via super.initialize(). Gamemodes extending BaseGamemode directly must register their own providers if they need combat.
Customizing a provider
To override a specific provider while keeping the rest, call super.initialize() then re-register just the one you want to change. The last register call wins.
Replace entirely — write your own implementation of the interface:
import { getProviderRegistry } from "../../src/game/providers/ProviderRegistry";
// inside your gamemode class:
override initialize(context: GamemodeInitContext): void {
super.initialize(context); // registers all 13 vanilla providers
// Replace combat formulas with custom ones
const registry = getProviderRegistry();
registry.combatFormula = {
maxHit: (player, target) => 99, // everyone hits 99s
hitChance: () => 1.0, // never miss
// ... implement remaining CombatFormulaProvider methods
};
}Wrap vanilla's provider — import the create function, spread it, and override specific methods:
import { getProviderRegistry } from "../../src/game/providers/ProviderRegistry";
import { createCombatFormulaProvider } from "../vanilla/combat/CombatFormulas";
// inside your gamemode class:
override initialize(context: GamemodeInitContext): void {
super.initialize(context);
const registry = getProviderRegistry();
const base = createCombatFormulaProvider();
registry.combatFormula = {
...base,
maxHit: (player, target) => base.maxHit(player, target) * 2, // double max hit
};
}Reuse vanilla providers from BaseGamemode — if you extend BaseGamemode but still want standard OSRS combat:
import { getProviderRegistry } from "../../src/game/providers/ProviderRegistry";
import { createCombatFormulaProvider } from "../vanilla/combat/CombatFormulas";
import { createWeaponDataProvider } from "../vanilla/data/weapons";
// inside your gamemode class:
override initialize(context: GamemodeInitContext): void {
// No super.initialize() — BaseGamemode's is a no-op
// Cherry-pick the providers you need
const registry = getProviderRegistry();
registry.combatFormula = createCombatFormulaProvider();
registry.weaponData = createWeaponDataProvider();
// ... register only the providers you need
}Provider lifecycle
Providers are cleaned up when dispose() is called on the gamemode. VanillaGamemode's dispose() calls resetProviderRegistry() which clears all providers at once. If your gamemode extends VanillaGamemode, call super.dispose() at the end of your own dispose():
override dispose(): void {
// Clean up your own state first
this.myManager = undefined;
super.dispose(); // resets all providers
}GamemodeDefinition Interface
The full interface is defined in server/src/game/gamemodes/GamemodeDefinition.ts. Required methods that BaseGamemode provides defaults for:
| Method | Default |
|---|---|
getSkillXpMultiplier() | 1 |
getDropRateMultiplier() | 1 |
transformDropItemId() | passthrough |
canInteract() | true |
initializePlayer() | no-op |
serializePlayerState() | undefined |
deserializePlayerState() | no-op |
onNpcKill() | no-op |
isTutorialActive() | false |
getSpawnLocation() | Lumbridge (3222, 3218, 0) |
onPlayerHandshake() | no-op |
onPlayerLogin() | no-op |
getPlayerTypes() | [PlayerType.Normal] |
registerHandlers() | no-op |
initialize() | no-op |
Optional hooks (not required, return undefined if absent):
getDefaultSkillXp, getSkillXpAward, getDropTable, getSupplementalDrops, getLootDistributionConfig, canInteractWithNpc, onItemCraft, getLoginVarbits, getLoginVarps, isTutorialPreStart, onPlayerRestore, onPostDesignComplete, resolveAccountStage, onVarpTransmit, onWidgetOpen, onResumePauseButton, onPlayerTick, onPlayerDisconnect, getGamemodeServices, contributeScriptServices, createUiController, getContentDataPacket, dispose
