A Modern 2D MMO Game Engine Platform
Frostfire Forge is an upcoming 2D MMO engine platform designed to empower developers and hobbyists alike to bring their dream games and worlds to life. Built with cutting-edge technology, it offers a highly secure and optimized foundation for MMO development. With a focus on simplicity and performance, Frostfire Forge makes creating your own multiplayer universe easier than ever.
Note
Project Status: This project is currently a work in progress
Core Development Team: Lillious, Deph0
Community: Join our Discord
Note
Teaser
- Requirements
- Architecture
- Environment Variables
- Realm Whitelist Configuration
- Quick Start
- Commands Reference
- API Documentation
- System API Reference
Important
Required Software:
- Bun - JavaScript runtime & package manager
- MySQL - Database
- Frostfire Forge Gateway - Authentication and reverse proxy gateway (required for all deployments)
- Frostfire Forge Assets - Asset server for map data, sprites, and resources (required for all deployments)
- Docker (Optional) - For containerized deployment
Frostfire Forge requires the Frostfire Forge Gateway for all deployments. The gateway handles centralized user authentication, game server registration and management, automatic failover, and request routing to game servers.
Game servers automatically register with the gateway on startup using the GATEWAY_URL, GATEWAY_AUTH_KEY, and GATEWAY_GAME_SERVER_SECRET environment variables. The server will continuously poll until the gateway is available.
Frostfire Forge requires the Frostfire Forge Assets server for all deployments. The asset server manages and distributes critical game data including:
- Map Data - Tile maps, collision layers, spawn points, and warps
- Sprites & Animations - Character sprites, item graphics, and animation frames
- Game Resources - Particle effects, NPC data, quest data, items, spells, and mounts
- Dynamic Updates - Real-time map updates from the tile editor for collaborative world building
The asset server provides a centralized repository for all game assets, enabling the game engine to fetch required data on-demand and persist editor changes back to permanent storage.
The game server connects to the asset server using the ASSET_SERVER_URL and ASSET_SERVER_AUTH_KEY environment variables.
DATABASE_ENGINE="mysql"
DATABASE_HOST="your_db_host"
DATABASE_NAME="your_db_name"
DATABASE_PASSWORD="your_db_password"
DATABASE_PORT="3306"
DATABASE_USER="your_db_user"
SQL_SSL_MODE="DISABLED" | "ENABLED"
# Translation Services
GOOGLE_TRANSLATE_API_KEY="your_google_api_key"
OPENAI_API_KEY="your_openai_api_key"
TRANSLATION_SERVICE="google_translate" | "openai"
OPENAI_MODEL="gpt-4.1-nano-2025-04-14"
# Application Settings
WEB_SOCKET_PORT="3000" # Internal WebSocket port
WEB_SOCKET_USE_SSL="true" | "false" # Enable SSL/TLS for WebSocket
WEB_SOCKET_CERT_PATH="./src/certs/cert.pem"
WEB_SOCKET_KEY_PATH="./src/certs/key.pem"
WEB_SOCKET_CA_PATH="./src/certs/cert.ca-bundle"
GAME_NAME="Your Game Name"
LOG_LEVEL="info" # Logging level: trace, debug, info, warn, error
# CORS Configuration (Security)
CORS_ALLOWED_ORIGINS="https://game.example.com,https://client.example.com" # Comma-separated list of allowed origins
# Gateway (Required)
GATEWAY_URL="http://gateway:9999" # Gateway registration endpoint
GATEWAY_AUTH_KEY="your_secret_key" # Shared secret for server registration
GATEWAY_GAME_SERVER_SECRET="another_secret_key" # Game server authentication token
SERVER_HOST="game-server-hostname" # Internal server hostname
PUBLIC_HOST="yourdomain.com" # External hostname for clients
SERVER_ID="server-1" # Game server identification
SERVER_DESCRIPTION="The server description" # Game server description
# Asset Server (Required)
ASSET_SERVER_URL="http://assets:8000" # Asset server endpoint
ASSET_SERVER_AUTH_KEY="your_secret_key" # Asset server authentication token
# Realm Configuration
WHITELIST="true" | "false" # Enable/disable username whitelist for this realmThe whitelist feature restricts user access to a specific realm to only approved usernames. When enabled, any user attempting to authenticate with a username not in the whitelist will be disconnected with the message "Username not whitelisted on this realm".
1. Enable the whitelist for the realm:
Set the environment variable in your .env file:
WHITELIST=true2. Create/update the whitelist file:
Create a whitelist.txt file in the project root directory (same directory as the game server binary):
admin
moderator
testuser
approved_player
3. File Format:
- One username per line
- Usernames are case-insensitive (converted to lowercase on matching)
- Lines starting with
#are treated as comments - Empty lines are ignored
- Whitespace at the beginning and end of each line is trimmed
Example whitelist.txt:
# Game Admins
admin
moderator
# Test Players
testuser
lillious
# Developer Accounts
dev_account
4. Restart the server:
The whitelist is loaded at server startup. After updating whitelist.txt, restart the game server for changes to take effect.
5. Realm Status in Gateway:
The realm will display a "whitelist" badge in the realm selection UI when WHITELIST=true, allowing players to see which realms have restricted access.
Option 1: Use prebuilt Docker image:
docker run -d --name frostfire-forge-dev -p 3000:3000 ghcr.io/lillious-networks/frostfire-forge-dev:latestOption 2: Build and run from source:
bun developmentOptional: Update .env.development before running
Default admin login credentials:
Username: demo_user
Password: Changeme123!
Update the .env.production file
Configure your production environment variables.
Start the production server:
bun productionOptional: Run setup separately
If you prefer to set up the database manually before starting the server:
bun setup-productionDisconnect Player
/kick [username | id]- Aliases:
disconnect - Permission:
admin.kick|admin.*
Warp
/warp [map]- Permission:
admin.warp|admin.*
Reload Map
/reloadmap [map]- Permission:
admin.reloadmap|admin.*
Ban Player
/ban [username | id]- Permission:
admin.ban|admin.*
Unban Player
/unban [username | id]- Permission:
admin.unban|admin.*
Send Message to Players
/notify [audience?] [message]- Audience:
all(default) |map|admins - Aliases:
notify - Permission:
server.notify|server.*
Toggle Admin Status
/admin [username | id]- Aliases:
setadmin - Permission:
server.admin|server.*
Server Shutdown
/shutdown- Permission:
server.shutdown|server.*
Server Restart (Scheduled: 15 minutes)
/restart- Permission:
server.restart
Respawn Player
/respawn [username | id]- Permission:
admin.respawn|admin.*
Summon Player
/summon [username | id]- Permission:
admin.summon|admin.*
Update Player Permissions
/permission [mode] [username | id] [permissions?]- Aliases:
permissions - Permission:
admin.permission|admin.*
Modes:
add- Permission:permission.add|permission.*remove- Permission:permission.remove|permission.*set- Permission:permission.add|permission.*clear- Permission:permission.remove|permission.*list- Permission:permission.list|permission.*
Tile Editor
/tileeditor- Aliases:
te - Permission:
tools.tile_editor|tools.*
NPC Editor
/npceditor- Aliases:
ne - Permission:
tools.npc_editor|tools.*
Particle Editor
/particleeditor- Aliases:
pe - Permission:
tools.particle_editor|tools.*
Entity Editor
/entityeditor- Aliases:
ee - Permission:
tools.entity_editor|tools.*
Manage Whitelist
/whitelist [mode] [username]- Permission:
admin.whitelist|admin.*
Modes:
add- Add a player to the whitelistremove- Remove a player from the whitelist
Whisper
/whisper [username] [message]- Aliases:
w
Party Chat
/party [message]- Aliases:
p - Requirement: Must be in a party
- Description: Send a message to all party members
Local Chat
/say [message]- Aliases:
s - Description: Send a message to local players
Plugins are self-contained modules that extend the engine without modifying engine source code. They live under src/plugins/ and are auto-discovered via manifest.json manifests.
1. Directory structure:
src/plugins/
βββ MyPlugin/
βββ manifest.json # Manifest
βββ src/
βββ index.ts # Entry point
2. Manifest file (manifest.json):
{
"name": "my-plugin",
"version": "1.0.0",
"description": "What this plugin does",
"entry": "./src/index.ts",
"requires": {
"engine": ">=1.0.0"
},
"provides": [
"feature.one",
"feature.two"
]
}3. Entry point (src/index.ts):
import { listener, Events } from "@engine/systems/events";
export default {
async register(engine: EngineAPI, manifest: PluginManifest) {
// `manifest` contains name, version, description from manifest.json
// Register packet types, builders, interceptors, and event listeners
listener.on(Events.PARTY_CHANGED, (data) => { ... });
},
async unregister() {
// Cleanup
},
};The loader reads name, version, and description from manifest.json. The plugin module only exports register and optionally unregister.
The engine object passed to register() provides these methods:
| Method | Description |
|---|---|
engine.addPacketTypes(types: string[]) |
Register custom packet type constants |
engine.addPacketBuilders(builders: Record<string, Function>) |
Register packet builder functions |
engine.registerHandlers(handlers: Record<string, Function>) |
Register packet handlers |
engine.onWarpCollision(fn) |
Push a warp collision interceptor. Receives (warp, ws, player, sendPacket). Return true to suppress engine handling, false to let engine proceed. |
engine.onPacket(fn) |
Push a packet interceptor. Receives (type, data, ws, player). Return true to suppress engine handling. |
engine.addHttpRoute(method, path, handler) |
Register an HTTP route. handler receives (req: Request) and returns Response. |
engine.teleportPlayer(playerObj, mapName, x, y) |
Teleport a player to a map position. |
Use the @engine/ prefix to import engine modules:
import log from "@engine/modules/logger.ts";
import playerCache from "@engine/services/playermanager.ts";
import assetCache from "@engine/services/assetCache.ts";
import packet from "@engine/modules/packet.ts";
import { listener, Events } from "@engine/systems/events";Types (EngineAPI, PluginManifest, PluginHandlerFn) are declared globally in types.d.ts - no import required.
Import the listener from @engine/systems/events:
import { listener } from "@engine/systems/events";| Event | Payload | When |
|---|---|---|
onAwake |
- | Server starts |
onStart |
- | After onAwake |
onPluginLoad |
{ name, version, dirPath } |
Plugin manifest discovered and module imported |
onPluginInitialize |
{ name, engine } |
Before plugin.register() is called |
onPluginRegister |
{ name } |
After plugin.register() succeeds |
onPluginUnregister |
{ name } |
Plugin unloaded |
| Event | Interval |
|---|---|
onUpdate |
Every frame (~60 FPS) |
onFixedUpdate |
Every 100ms |
onSave |
Every 60 seconds |
onServerTick |
Every 1 second |
| Event | Payload | When |
|---|---|---|
onConnection |
{ id, ... } |
New WebSocket connection |
onDisconnect |
{ id, ... } |
WebSocket disconnected |
| Event | Payload | When |
|---|---|---|
onWarp |
{ mapName, metadata } |
constructMapMetadata() builds a LOAD_MAP packet. metadata is mutable - modify metadata.name to change the map name sent to the client. |
onMapEnter |
{ player, mapName, position } |
Player enters a new map (after AOI update, before LOAD_MAP sent) |
onPlayerAuthComplete |
{ username, spawnLocation, playerData } |
After login spawn location is resolved, before map validation. spawnLocation is mutable - modify .map, .x, .y to redirect. |
onPartyChanged |
{ type, members, username?, kickedUsername? } |
After party kick/leave/disband. type = "kick" | "leave" | "disband". members = affected usernames. |
onPlayerChat |
{ player, message, mapName, language? } |
After chat message is decrypted and broadcast to map |
onPlayerDeath |
{ player, killer? } |
After a player dies (health β€ 0) and death packets are sent |
onPlayerRespawn |
{ player, mapName, x, y } |
After a player is respawned via admin command |
onGuildChanged |
{ type, guildId, guildName, playerUsername, kickedUsername? } |
After guild create/join/leave/kick/disband. type = "create" | "join" | "leave" | "kick" | "disband" |
onItemEquip |
{ player, item, slot } |
After an item is equipped and stats are recalculated |
onItemUnequip |
{ player, slot } |
After an item is unequipped and stats are recalculated |
onPlayerMount |
{ player, mounted, mountType? } |
After mount/dismount toggle |
onPlayerMoved |
{ player, position } |
After MOVEXY processes and game loop registers player |
onPlayerLogout |
{ player } |
After player state saved and logout cleanup |
onPlayerDisconnect |
{ player } |
After WebSocket disconnect and drag-release cleanup |
onSpellCast |
{ player, spellName, target, isEntityTarget } |
After spell effects applied, last-attack timers set |
onSpellInterrupted |
{ player } |
After spell cancelled via ESC and state cleared |
onPlayerDamaged |
{ attacker, target, damage, isCrit } |
After damage applied to player target health |
onPlayerLevelUp |
{ player, oldLevel, newLevel } |
After XP reward causes level increase |
onFriendAdded |
{ type, playerUsername, friendUsername } |
After friend request accepted and lists updated |
onFriendRemoved |
{ type, playerUsername, friendUsername } |
After friend removed and list synced |
onPartyInvite |
{ inviterUsername, invitedUsername } |
After party invitation sent |
onPartyJoin |
{ playerUsername, partyMembers } |
After party invitation accepted and layers synced |
onWhisper |
{ fromUsername, toUsername, message } |
After private message sent |
onPlayerStealthChange |
{ player, isStealth } |
After stealth/unstealth toggle and spawn/despawn packets |
import { packetTypes } from "./types";Packet type definitions for client-server communication.
import playerCache from "../services/playermanager"; // Player cache
import assetCache from "../services/assetCache"; // Asset cache| Method | Description |
|---|---|
playerCache.add(key, value) |
Add a player to cache |
playerCache.get(key) |
Get a player by key |
playerCache.list() |
Get all cached players |
playerCache.remove(key) |
Remove a player from cache |
playerCache.set(key, value) |
Update a player in cache |
playerCache.setNested(key, nestedKey, value) |
Set a nested property on a player |
assetCache.add(key, value) |
Add an asset to cache |
assetCache.get(key) |
Get an asset by key |
assetCache.addNested(key, nestedKey, value) |
Add nested asset data |
assetCache.getNested(key, nestedKey) |
Get nested asset data |
assetCache.set(key, value) |
Update an asset in cache |
assetCache.setNested(key, nestedKey, value) |
Update nested asset data |
The event bus is available via @engine/systems/events:
import { listener } from "@engine/systems/events";| Method | Description |
|---|---|
listener.on(event, handler) |
Register an event handler |
listener.emit(event, payload) |
Emit an event |
listener.off(event, handler) |
Remove an event handler |
Built with β€οΈ by the Frostfire Forge Team

