Developer

5 min readUpdated 2 weeks ago

Integration points

Use Config.Maps and Config.Items to define map images and their UI or DUI items. Use calibration to align those images with world coordinates. Default markers belong in Config.DefaultMarkers; player annotations and fog progress belong in ml_custommap_data.

Client exports

lua
1exports.ml_custommap:SetMinimapOffset(offsetX, offsetY)
2local x, y = exports.ml_custommap:GetMinimapOffset()

SetMinimapOffset changes the radar position by a pixel offset. GetMinimapOffset returns the current X and Y offsets. Position changes apply when Config.MinimapControl is enabled.

lua
1local panels = exports.ml_custommap:MLAdminPanel()
2exports.ml_custommap:MLAdminOpen('calibrate')

MLAdminPanel returns the enabled admin tools for an admin hub. MLAdminOpen accepts calibrate or blips to invoke the corresponding command. The server still checks permission. These exports are registered when at least one admin tool is enabled.

Internal events

These events support the resource's own item, keybind and editor flows. Server handlers validate payloads, inventory access and permissions. Client events are not a substitute for a server access check.

Server events

  • ml_custommap:server:saveMapData(serial, payload) validates and caches annotation changes for an active serial owned by the player.
  • ml_custommap:server:openByKeybind(mapId, itemName) checks access, resolves storage and opens the bound display mode.
  • ml_custommap:server:startFogSession(mapId, itemName) checks access and supplies the active map's saved discovery.
  • ml_custommap:server:fogSync(serial, delta) merges discovered cell indices into the active, owned serial's bitmask.
  • ml_custommap:server:setWaypoint(data) validates and replaces the player's Bridge waypoint. removeWaypoint(wpId) removes an owned waypoint or all of that player's active waypoints when no ID is supplied.
  • ml_custommap:server:adminOpen(mapId) and calibrateCheck(mapId) require the configured staff permission.
  • ml_custommap:server:debugOpen(mapId) requires debug mode and an admin permission accepted by ml_bridge.

Client events

  • ml_custommap:client:open(mapId, serial, savedData, opts) receives annotations and discovery. opts.dui = true selects the held paper mode.
  • ml_custommap:client:startFogSession(mapId, serial, discoveredBytes) starts the active discovery session without opening the viewer.
  • ml_custommap:client:setNativeMap(state) controls the calling player's native map access.
  • ml_custommap:client:calibrateAllowed(mapId) and adminOpen(mapId) follow a successful server permission check.

Storage

sql
1CREATE TABLE IF NOT EXISTS `ml_custommap_data` (
2    `serial` VARCHAR(64) NOT NULL,
3    `owner_identifier` VARCHAR(100) DEFAULT NULL,
4    `map_id` VARCHAR(64) NOT NULL,
5    `markers` MEDIUMTEXT DEFAULT '[]',
6    `labels` MEDIUMTEXT DEFAULT '[]',
7    `radii` MEDIUMTEXT DEFAULT '[]',
8    `images` MEDIUMTEXT DEFAULT '[]',
9    `discovered` MEDIUMTEXT DEFAULT NULL,
10    `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
11    PRIMARY KEY (`serial`),
12    INDEX `idx_owner` (`owner_identifier`)
13) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

The schema is created automatically. In metadata mode, the item stores its serial; the row stores annotations and fog. Moving the item preserves that serial when the inventory preserves its metadata. In database mode, the key is license:<playerIdentifier>:<mapId>, where the identifier comes from ml_bridge.

owner_identifier records the identifier used when the row was created. It is not a live inventory ownership lookup after an item is traded. Use the inventory and serial when you need its current holder.

markers, labels, radii and images are JSON arrays. discovered is a Base64 bitmask with one bit per grid cell. The grid depends on the map's coordinate plane, calibration scale and discovery radius.

json
1{
2  "markers": [{ "id": "m_example", "worldX": 200.0, "worldY": -800.0, "presetId": "camp", "customName": "Base Camp", "customIcon": "fa-campground", "customColor": "#22c55e", "bgCircle": true, "customSize": 28, "customAlpha": 100 }],
3  "radii": [{ "id": "r_example", "worldX": 200.0, "worldY": -800.0, "presetId": "small", "radiusPixels": 120, "customColor": "#ef4444", "customAlpha": 20 }],
4  "labels": [{ "id": "l_example", "text": "Base Camp", "pixelX": 200.0, "pixelY": 400.0, "fontSize": 14, "color": "#ffffff", "rotation": 0 }],
5  "images": [{ "id": "img_example", "url": "https://example.com/map-note.png", "pixelX": 200.0, "pixelY": 400.0, "width": 256, "height": 256, "alpha": 100, "rotation": 0 }]
6}

Give a map item

lua
1Bridge.GiveItem(src, 'atlas_map_dui', 1)

Register the inventory item before giving it to a player. In metadata mode, its first use resolves or creates a serial and its database row. Keep map items non-stackable and preserve their metadata when moving them.

Create a regional map

The bundled del_perro_treasure image covers part of Del Perro. Use it as a reference for maps limited to a city block, island, camp or other defined area. Its decorative route does not run a quest.

[figure]

  1. Place the image in maps/. Keep north up and preserve road and landmark positions.
  2. Create a unique Config.Maps entry. Set its label, filename, coordinate dimensions, zoom bounds and fogRevealRadius for that area.
  3. Register separate inventory items for the UI and DUI modes if you want both.
shared/config.lua
1Config.Items['my_map_ui'] = 'my_map'
2Config.Items['my_map_dui'] = { map = 'my_map', mode = 'dui' }
  1. Stand at a recognizable point in the area and run /mapcalibrate my_map. Click its matching position on the image. Repeat at points spread across both axes.
  2. Run /mapcalibrate done and paste the returned calibration into the map entry. Use /mapcalibrate reset to discard a session.

The solver accepts two points, but four or more spread across the image provide a better fit. It calculates independent scales and offsets for X and Y. It does not correct image rotation, skew or displaced roads.

lua
1calibration = {
2    scaleX = 1.14654,
3    offsetX = 2448.05,
4    scaleY = -1.14664,
5    offsetY = -598.61,
6    imageSize = 1254,
7}

Image coordinates are pixelX = worldX * scaleX + offsetX and pixelY = worldY * scaleY + offsetY. The inverse converts map clicks back to world positions for markers and waypoints.

Calibration can be shared only when projection, framing and coordinate dimensions match. A crop needs its own scale and offsets. The full-island calibration cannot be pasted unchanged onto a regional crop.

Default markers

Run /addblip <mapId> and place a marker. The tool prints and copies an entry for Config.DefaultMarkers; it does not save it as a player annotation.

lua
1{
2    mapId = 'atlas',
3    worldX = 200.0,
4    worldY = -800.0,
5    preset = 'camp',
6    label = 'Base Camp',
7},

Paste the entry into the config to display it to players viewing that map ID. Optional icon and color fields override the preset appearance.

Read saved map data

lua
1local rows = MySQL.query.await([[
2    SELECT serial, map_id, discovered FROM ml_custommap_data
3    WHERE owner_identifier = ?
4]], { playerIdentifier })

This query returns rows recorded under that identifier. Decode discovered as Base64 and interpret its bits with the correct map grid. Cached changes can remain pending until the next save. Read-only integrations should account for that delay; direct database edits require the resource cache to be reloaded.

Cartography Developer, FiveM Docs | Micio Mods