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";

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 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__ 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

  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