Skip to main content

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-display
src/modules/weather/
├── module.ts
├── weather.service.ts
├── types.ts
└── huds/
    └── weather-display/
        └── index.tsx

Step 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 TypeDescriptionKey Methods
serverOwnedServer-authoritative state, syncs to clients.value, .set(), .subscribe(), .update()
clientOwnedClient-authoritative state, syncs to server.value, .set(), .subscribe(), .update()
webOwnedUI state, updated by client/server.value, .set(), .subscribe(), .update()
sharedBidirectional 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.type not weather().type
  • Use className for 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 dev

Testing the Module

  1. Start the FiveM server with the resource
  2. Connect a client
  3. The weather HUD should appear in top-right
  4. From server console: setweather rain
  5. 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