Developer
Overview
ml_custommap is mostly configuration-driven. It exposes two client exports for the postal radar position (below); beyond that, map access is gated through usable items registered with Bridge.BindUsableItem, and the full state (markers, radii, labels, images, fog bitmask) lives in the ml_custommap_data table keyed by serial.
Developers extending the script work at three touch points: placing default markers through Config.DefaultMarkers or the /addblip admin command, adding new map images with the /mapcalibrate flow, and reading the database table directly for cross-resource integrations.
Client Exports
Two client exports adjust the postal radar position at runtime. They apply only when Config.MinimapControl is on.
1-- Shift the postal radar by a pixel offset
2exports.ml_custommap:SetMinimapOffset(offsetX, offsetY)
3
4-- Read the current offset
5local x, y = exports.ml_custommap:GetMinimapOffset()SetMinimapOffset(offsetX, offsetY): moves the radar by the given pixel offset.GetMinimapOffset(): returns the current x, y pixel offset.
Net Events
The events below are internal but may be useful for debugging or writing external tooling. They enforce rate limiting and zero-trust payload validation server-side: triggering them manually from a client will not bypass the checks.
Server-Bound
ml_custommap:server:saveMapData(serial, payload): Persists marker / radius / label / image changes. Requires an active registered serial for the source.ml_custommap:server:openByKeybind(mapId, itemName?): Keybind path. Resolves serial, loads cache, triggersclient:open.ml_custommap:server:startFogSession(mapId, itemName?): Auto-bootstrap for background fog tracking on player load.ml_custommap:server:fogSync(serial, delta): Client pushes newly-discovered cell indices. Server OR-merges into the stored bitmask.ml_custommap:server:setWaypoint(data): Creates or replaces the player's 3D Bridge waypoint.ml_custommap:server:removeWaypoint(wpId?): Removes one or all Bridge waypoints.ml_custommap:server:adminOpen(mapId): Permission-gated admin-mode open.ml_custommap:server:calibrateCheck(mapId): Permission-gated calibration trigger.ml_custommap:server:debugOpen(mapId): Debug-only open. RequiresConfig.Debug = true.
Client-Bound
ml_custommap:client:open(mapId, serial, savedData): Opens the map UI with markers, labels, radii, images, and fog bitmask.ml_custommap:client:startFogSession(mapId, serial, discoveredBytes?): Starts background fog tracking without opening the UI.ml_custommap:client:setNativeMap(state): Toggles the client-side native map suppression.ml_custommap:client:calibrateAllowed(mapId): Server grants calibration access after the permission check.ml_custommap:client:adminOpen(mapId): Server grants admin-mode access after the permission check.
Database Schema
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;serial: Primary key. Inmetadatamode it's a generated string stored in item metadata. Indatabasemode it'slicense:<identifier>:<mapId>.owner_identifier: Player license or framework identifier. Useful for cross-referencing.map_id: WhichConfig.Mapsentry this row belongs to.markers,labels,radii,images: JSON arrays of sanitized objects. Schema below.discovered: Base64-encoded fog-of-war bitmask. One bit per grid cell.
JSON Shapes
1markers: [
2 { "id": "m_1700000000000", "worldX": 0.0, "worldY": 0.0,
3 "presetId": "camp", "customName": "string?", "customIcon": "fa-class?",
4 "customColor": "#hex?", "bgCircle": true, "customSize": 28, "customAlpha": 100 }
5]
6
7radii: [
8 { "id": "r_...", "worldX": 0.0, "worldY": 0.0, "presetId": "small",
9 "radiusPixels": 120, "customColor": "#hex?", "customAlpha": 20 }
10]
11
12labels: [
13 { "id": "l_...", "text": "string", "pixelX": 0.0, "pixelY": 0.0,
14 "fontSize": 14, "color": "#hex", "rotation": 0 }
15]
16
17images: [
18 { "id": "img_...", "url": "https://...", "pixelX": 0.0, "pixelY": 0.0,
19 "width": 256, "height": 256, "alpha": 100, "rotation": 0 }
20]Map Calibration
The /mapcalibrate workflow computes the affine transform between GTA world coordinates and image pixel coordinates using least-squares regression on 4+ collected points:
1-- Output written to clipboard
2calibration = {
3 scaleX = 0.65830,
4 offsetX = 3755.58,
5 scaleY = -0.65836,
6 offsetY = 5526.30,
7 imageSize = 8192,
8}Given world (x, y) the image pixel is (x * scaleX + offsetX, y * scaleY + offsetY). The inverse is used when converting clicks back to world coordinates (for waypoints, radii, labels).
Multiple map images that share the same projection and dimensions (e.g. satellite, road, atlas) can reuse the same calibration block. Calibrate one, paste into all.
Admin Default Marker Command
/addblip [mapId] opens the map in admin mode. Placing a marker does not persist: instead the map interface sends adminMarkerCreated to the client, which prints the config block to the F8 console and copies it to clipboard:
1{
2 mapId = 'tourist',
3 worldX = 2194.1,
4 worldY = 3179.7,
5 preset = 'camp',
6 label = 'Zenit',
7},Paste the block into Config.DefaultMarkers and restart to make it visible to all players.
Examples
1-- Add a usable map item (registered via inventory system first)
2-- then give it to the player:
3Bridge.GiveItem(src, 'tourist_map', 1)The first time the player uses the item with Config.StorageMode = 'metadata', the script generates a unique serial, writes it to the item metadata, and creates a row in ml_custommap_data. Subsequent opens reuse the same serial.
1local rows = MySQL.query.await([[
2 SELECT map_id, discovered FROM ml_custommap_data
3 WHERE owner_identifier = ?
4]], { playerLicense })
5
6for _, row in ipairs(rows) do
7 -- row.discovered is base64-encoded. Decode to bytes, then
8 -- iterate bits using the cellPx computed from the map's
9 -- scaleX and Config.FogOfWar.revealRadius.
10end1Config.Items = {
2 ['tourist_map'] = 'tourist',
3 ['treasure_map'] = 'treasure',
4}
5
6Config.Maps['treasure'] = {
7 label = 'Treasure Map',
8 image = 'map_treasure.webp',
9 imageWidth = 2048,
10 imageHeight = 2048,
11 calibration = { scaleX = 0, offsetX = 0, scaleY = 0, offsetY = 0, imageSize = 2048 },
12 defaultZoom = 1.5,
13 maxZoom = 6.0,
14 minZoom = 0.4,
15 fogRevealRadius = 400,
16}Register treasure_map in the inventory, drop map_treasure.webp into maps/, run /mapcalibrate treasure in-game, paste the values. Done.