Creating Modules
This guide walks through creating a complete module from scratch.
Overview
We'll create a "weather" module that:
- Syncs weather state from server to clients
- Provides a HUD showing current weather
- Allows admins to change weather via command
Step 1: Create Directory Structure
mkdir -p src/modules/weather/huds/weather-displaysrc/modules/weather/
├── module.ts
├── weather.service.ts
├── types.ts
└── huds/
└── weather-display/
└── index.tsxStep 2: Define Types
// src/modules/weather/types.ts
export type WeatherType = "clear" | "clouds" | "rain" | "thunder" | "snow" | "fog";
export interface WeatherState {
type: WeatherType;
temperature: number;
windSpeed: number;
}Step 3: Create the Service
The service uses observables from @core/observable for reactive state management. The observable API provides:
| Observable Type | Description | Key Methods |
|---|---|---|
serverOwned | Server-authoritative state, syncs to clients | .value, .set(), .subscribe(), .update() |
clientOwned | Client-authoritative state, syncs to server | .value, .set(), .subscribe(), .update() |
webOwned | UI state, updated by client/server | .value, .set(), .subscribe(), .update() |
shared | Bidirectional sync with conflict resolution | .value, .set(), .subscribe(), .update() |
For complete API documentation, see the Observable System guide.
// src/modules/weather/weather.service.ts
import { serverOwned, webOwned } from "@core/observable";
import type { WeatherState, WeatherType } from "@modules/weather/types";
const WEATHER_NATIVES: Record<WeatherType, string> = {
clear: "CLEAR",
clouds: "CLOUDS",
rain: "RAIN",
thunder: "THUNDER",
snow: "SNOW",
fog: "FOGGY",
};
export class WeatherService {
// Server-owned for authoritative state (syncs to clients)
private weatherState = serverOwned<WeatherState>({
id: "weather:state",
initialValue: {
type: "clear",
temperature: 72,
windSpeed: 5,
},
broadcast: true,
});
// Web-owned for UI state (client/server can update, web subscribes)
public hudState = webOwned<WeatherState>({
id: "weather:hud",
initialValue: {
type: "clear",
temperature: 72,
windSpeed: 5,
},
});
@ServerOnly
initServerSide(): void {
// Default weather cycle could go here
}
@ClientOnly
initClientSide(): void {
// Subscribe to weather changes and sync to UI
this.weatherState.subscribe((state) => {
// Apply weather to game
SetWeatherTypeNowPersist(WEATHER_NATIVES[state.type]);
// Update web UI via webOwned observable
this.hudState.set(state);
});
}
@ServerOnly
setWeather(type: WeatherType, temperature?: number): void {
const current = this.weatherState.value;
this.weatherState.set({
...current,
type,
temperature: temperature ?? current.temperature,
});
}
@Server
async requestWeatherChange(ctx: EventContext<[type: WeatherType]>): Promise<{ success: boolean }> {
const [type] = ctx.args;
// Add permission check here
this.setWeather(type);
return { success: true };
}
}Step 4: Create the Module
// src/modules/weather/module.ts
import { registerModule } from "@core/module";
import { WeatherService } from "@modules/weather/weather.service";
import WeatherDisplay from "@modules/weather/huds/weather-display/index";
import { IS_CLIENT, IS_SERVER } from "@core/runtime";
import type { WeatherType } from "@modules/weather/types";
let weatherService: WeatherService | null = null;
export default registerModule({
name: "weather",
dependencies: [],
services: [WeatherService],
// HUDs are rendered in a 3x3 grid
huds: [
{
name: "weather-display",
panel: WeatherDisplay,
hudPosition: "top-right",
hudPriority: 10,
hudGroup: "default",
},
],
onStart(module) {
weatherService = module.getService(WeatherService) ?? null;
if (!weatherService) return;
if (IS_SERVER) {
weatherService.initServerSide();
// Register admin command
RegisterCommand(
"setweather",
(source: number, args: string[]) => {
if (source !== 0) return; // Console only
const type = args[0] as WeatherType;
weatherService?.setWeather(type);
},
true,
);
}
if (IS_CLIENT) {
weatherService.initClientSide();
}
},
onStop() {
weatherService = null;
},
});Step 5: Register the Module
// src/runtime/modules.ts
import type { Module } from "@core/module";
// ... existing imports
import weatherModule from "@modules/weather/module";
export const MODULES: Module[] = [
// ... existing modules
weatherModule,
];Step 6: Create UI Component
// src/modules/weather/huds/weather-display/index.tsx
"use web";
import React from "react";
import { useService, useObservable } from "@core/react";
import { WeatherService } from "@modules/weather/weather.service";
import { Cloud, Sun, CloudRain, CloudLightning, Snowflake, CloudFog } from "lucide-react";
import type { WeatherType } from "@modules/weather/types";
const WEATHER_ICONS: Record<WeatherType, React.ComponentType<{ className?: string }>> = {
clear: Sun,
clouds: Cloud,
rain: CloudRain,
thunder: CloudLightning,
snow: Snowflake,
fog: CloudFog,
};
export default function WeatherDisplay() {
const weatherService = useService(WeatherService);
const weather = useObservable(weatherService.hudState);
const Icon = WEATHER_ICONS[weather.type];
return (
<div className="flex items-center gap-3 rounded-lg bg-black/80 px-4 py-2">
<Icon className="h-6 w-6 text-white" />
<div className="flex flex-col">
<span className="text-lg font-semibold text-white">{weather.temperature}°F</span>
<span className="text-xs capitalize text-gray-400">{weather.type}</span>
</div>
</div>
);
}React Patterns
- Use
useService()to get a service instance via dependency injection - Use
useObservable()to subscribe to observable state with automatic cleanup - Access observable values directly:
weather.typenotweather().type - Use
classNamefor CSS classes (React standard) - Components are functions that return JSX
Step 7: Build and Test
# Build all targets
pnpm build
# Or run in development mode
pnpm devTesting the Module
- Start the FiveM server with the resource
- Connect a client
- The weather HUD should appear in top-right
- From server console:
setweather rain - Weather should change on all clients
Summary
You've created a complete module with:
- ✅ Type definitions
- ✅ Server-owned observable state (authoritative game state)
- ✅ Web-owned observable state (UI state synced from client)
- ✅ Service with RPC methods
- ✅ Client-side game integration
- ✅ React HUD component with reactive observables
- ✅ Module registration with HUD definition
Next Steps
- Add weather transition animations
- Implement time-based weather cycles
- Add weather effects (rain drops, snow particles)
- Create an admin page for weather control