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

  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
  • ✅ 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