Skip to main content

Module System

The module system is the foundation of True Life's architecture. Modules encapsulate features and are automatically loaded based on their declared dependencies.

Module Structure​

src/modules/{module-name}/
├── module.ts           # Module registration (NO DIRECTIVE)
├── {name}.service.ts   # Service class with RPC decorators
├── repository.ts       # Database operations ("use server")
├── types.ts            # Shared types (no directive)
├── config.ts           # Configuration (no directive)
├── huds/               # HUD UI features
│   └── {hud-name}/
│       ├── feature.tsx     # "use web"
│       ├── Panel.tsx       # "use web"
│       └── state/slice.ts  # "use web"
└── pages/              # Page UI features
    └── {page-name}/
        ├── feature.tsx     # "use web"
        └── Page.tsx        # "use web"

Creating a Module​

Basic Module​

// src/modules/my-feature/module.ts
import { registerModule } from "@core/module";
 
export default registerModule({
	name: "my-feature",
 
	onStart() {
		console.log("My feature started");
	},
 
	onStop() {
		console.log("My feature stopped");
	},
});

Module with Dependencies​

import { registerModule } from "@core/module";
import { MyService } from "@modules/my-feature/my.service";
import { IS_CLIENT, IS_SERVER } from "@core/runtime";
 
let myService: MyService | null = null;
 
export default registerModule({
	name: "my-feature",
	dependencies: ["characters", "banking"],
	services: [MyService],
 
	onStart(module) {
		myService = module.getService(MyService) ?? null;
		if (!myService) return;
 
		if (IS_SERVER) {
			myService.initServerSide();
		}
		if (IS_CLIENT) {
			myService.initClientSide();
		}
	},
 
	onStop() {
		myService = null;
	},
});

Module with UI Features​

import { registerModule } from "@core/module";
import { MyService } from "@modules/my-feature/my.service";
 
export default registerModule({
	name: "my-feature",
	dependencies: ["characters"],
	services: [MyService],
 
	// HUD features appear in the game overlay
	huds: {
		"my-hud": () => import("@modules/my-feature/huds/my-hud/feature"),
	},
 
	// Page features are full-screen interfaces
	pages: {
		"my-page": () => import("@modules/my-feature/pages/my-page/feature"),
	},
});

Module Registration​

All modules must be registered in src/runtime/modules.ts:

// src/runtime/modules.ts
import type { Module } from "@core/module";
 
import initModule from "@modules/infrastructure/init/module";
import charactersModule from "@modules/core/characters/module";
import myFeatureModule from "@modules/my-feature/module";
 
export const MODULES: Module[] = [initModule, charactersModule, myFeatureModule];
Key Points
  • Order in the array doesn't matter - modules are sorted by dependencies
  • Dependencies declared in registerModule() config determine load order
  • The UI automatically discovers features from the MODULES array

Module Lifecycle​

Boot Sequence​

  1. Core initialization - MongoDB connection, RPC system
  2. Module import - All modules imported from src/runtime/modules.ts
  3. Dependency resolution - Topological sort based on dependencies
  4. Service instantiation - Services created, DI container populated
  5. RPC registration - Event handlers registered via __registerEvents()
  6. Lifecycle hooks - onStart() called in dependency order

Teardown Sequence​

  1. onStop() called in reverse dependency order
  2. RPC handlers unregistered via __unregisterEvents()
  3. State cleanup, MongoDB connection closed

ModuleHandle API​

The onStart callback receives a ModuleHandle with these methods:

interface ModuleHandle {
	// Get a service instance by class
	getService<T>(ServiceClass: new () => T): T | undefined;
 
	// Check if module is loaded
	isLoaded(): boolean;
 
	// Get module metadata
	getName(): string;
	getDependencies(): string[];
}

Runtime-Specific Code​

Critical Rule

Module files (module.ts) must NEVER have environment directives. Use runtime constants instead.

Use the constants from @core/runtime for runtime-specific code:

  • IS_CLIENT - true only in client builds
  • IS_SERVER - true only in server builds
  • IS_WEB - true only in web builds

These are compile-time constants that enable dead-code elimination. The raw __RUNTIME__ global is also available but prefer the IS_* constants for cleaner code.

// ✅ Correct: No directive, use runtime constants
import { registerModule } from "@core/module";
import { fivemEvents } from "@core/events/server";
import { IS_CLIENT, IS_SERVER } from "@core/runtime";
 
export default registerModule({
	name: "my-module",
 
	onStart() {
		if (IS_SERVER) {
			// Server-only code
			fivemEvents.playerJoining.on((id) => {
				console.log(`Player ${id} joining`);
			});
		}
 
		if (IS_CLIENT) {
			// Client-only code
		}
	},
});
// ❌ Wrong: Directive causes module to be stubbed in other builds
"use server";
 
export default registerModule({
	// This breaks the UI feature discovery!
});

Dev Mode Filtering​

During development, you can filter which modules load:

# server.cfg
set dev_mode "true"
set dev_features "characters,banking"  # Only load these modules

Set dev_features to empty string to load all modules:

set dev_features ""

Best Practices​

  1. Keep modules focused - One feature per module
  2. Declare all dependencies - Explicit is better than implicit
  3. Use services for logic - Keep module.ts thin
  4. Co-locate UI - Keep HUDs/pages with their module
  5. Clean up resources - Always implement onStop()
  6. Use path aliases - Never use relative imports