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";
let myService: MyService | null = null;
export default registerModule({
name: "my-feature",
dependencies: ["characters", "banking"],
services: [MyService],
onStart(module) {
myService = module.getService(MyService) ?? null;
if (__RUNTIME__ === "server" && myService) {
myService.initServerSide();
}
if (__RUNTIME__ === "client" && myService) {
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/init/module";
import charactersModule from "@modules/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
MODULESarray
Module Lifecycle
Boot Sequence
- Core initialization - MongoDB connection, RPC system
- Module import - All modules imported from
src/runtime/modules.ts - Dependency resolution - Topological sort based on dependencies
- Service instantiation - Services created, DI container populated
- RPC registration - Event handlers registered via
__registerEvents() - Lifecycle hooks -
onStart()called in dependency order
Teardown Sequence
onStop()called in reverse dependency order- RPC handlers unregistered via
__unregisterEvents() - 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__ checks instead.
// ✅ Correct: No directive, use __RUNTIME__ checks
import { registerModule } from "@core/module";
import { fivemEvents } from "@core/events/server";
export default registerModule({
name: "my-module",
onStart() {
if (__RUNTIME__ === "server") {
// Server-only code
fivemEvents.playerJoining.on((id) => {
console.log(`Player ${id} joining`);
});
}
if (__RUNTIME__ === "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
- Keep modules focused - One feature per module
- Declare all dependencies - Explicit is better than implicit
- Use services for logic - Keep
module.tsthin - Co-locate UI - Keep HUDs/pages with their module
- Clean up resources - Always implement
onStop() - Use path aliases - Never use relative imports