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/state
src/modules/weather/
├── module.ts
├── weather.service.ts
├── types.ts
└── huds/
└── weather-display/
├── feature.tsx
├── Panel.tsx
└── state/
└── slice.ts
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
// src/modules/weather/weather.service.ts
import { serverOwned } from "@core/observable";
import { sendHudEvent } from "@core/hud";
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 {
private weatherState = serverOwned<WeatherState>({
id: "weather:state",
initialValue: {
type: "clear",
temperature: 72,
windSpeed: 5,
},
broadcast: true,
});
@ServerOnly
initServerSide(): void {
// Default weather cycle could go here
}
@ClientOnly
initClientSide(): void {
// Subscribe to weather changes
this.weatherState.subscribe((state) => {
// Apply weather to game
SetWeatherTypeNowPersist(WEATHER_NATIVES[state.type]);
// Update UI
sendHudEvent("weather:update", 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";
let weatherService: WeatherService | null = null;
export default registerModule({
name: "weather",
dependencies: [],
services: [WeatherService],
huds: {
"weather-display": () => import("@modules/weather/huds/weather-display/feature"),
},
onStart(module) {
weatherService = module.getService(WeatherService) ?? null;
if (__RUNTIME__ === "server" && weatherService) {
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 (__RUNTIME__ === "client" && weatherService) {
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 Redux Slice
// src/modules/weather/huds/weather-display/state/slice.ts
"use web";
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import type { WeatherState } from "@modules/weather/types";
const initialState: WeatherState = {
type: "clear",
temperature: 72,
windSpeed: 5,
};
export const weatherSlice = createSlice({
name: "weather",
initialState,
reducers: {
updateWeather: (state, action: PayloadAction<WeatherState>) => {
return action.payload;
},
},
});
export const { updateWeather } = weatherSlice.actions;
export const selectWeather = (state: { weather: WeatherState }) => state.weather;
export const selectWeatherType = (state: { weather: WeatherState }) => state.weather.type;
export const selectTemperature = (state: { weather: WeatherState }) => state.weather.temperature;
export default weatherSlice.reducer;
Step 7: Create UI Component
// src/modules/weather/huds/weather-display/Panel.tsx
"use web";
import { useNuiEvent, useAppDispatch, useAppSelector } from "@ui/hooks";
import { updateWeather, selectWeather } from "@modules/weather/huds/weather-display/state/slice";
import type { WeatherState } from "@modules/weather/types";
import { Cloud, Sun, CloudRain, CloudLightning, Snowflake, CloudFog } from "lucide-react";
const WEATHER_ICONS = {
clear: Sun,
clouds: Cloud,
rain: CloudRain,
thunder: CloudLightning,
snow: Snowflake,
fog: CloudFog,
};
export default function WeatherPanel() {
const dispatch = useAppDispatch();
const weather = useAppSelector(selectWeather);
useNuiEvent<WeatherState>("weather:update", (data) => {
dispatch(updateWeather(data));
});
const Icon = WEATHER_ICONS[weather.type];
return (
<div className="flex items-center gap-3 rounded-lg bg-black/70 px-4 py-2 backdrop-blur">
<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>
);
}
Step 8: Create Feature Definition
// src/modules/weather/huds/weather-display/feature.tsx
"use web";
import { defineFeature } from "@ui/feature";
import Panel from "@modules/weather/huds/weather-display/Panel";
import reducer, { weatherSlice } from "@modules/weather/huds/weather-display/state/slice";
export default defineFeature({
panel: Panel,
slices: [{ name: weatherSlice.name, reducer }],
hudPosition: "top-right",
hudPriority: 10,
});
Step 9: Build and Test
# Build all targets
pnpm build
# Or run in development mode
pnpm dev
Testing 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
- ✅ Service with RPC methods
- ✅ Client-side game integration
- ✅ UI HUD with Redux state
- ✅ Module registration
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