# Developer > Item bindings, client exports, storage, calibration and regional map integration. Category: CARTOGRAPHY · Source: https://miciomods.it/docs/ml-custommap-developer · Last updated: 2026-10-05 ## 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 exports.ml_custommap:SetMinimapOffset(offsetX, offsetY) local 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 local panels = exports.ml_custommap:MLAdminPanel() exports.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 CREATE TABLE IF NOT EXISTS `ml_custommap_data` ( `serial` VARCHAR(64) NOT NULL, `owner_identifier` VARCHAR(100) DEFAULT NULL, `map_id` VARCHAR(64) NOT NULL, `markers` MEDIUMTEXT DEFAULT '[]', `labels` MEDIUMTEXT DEFAULT '[]', `radii` MEDIUMTEXT DEFAULT '[]', `images` MEDIUMTEXT DEFAULT '[]', `discovered` MEDIUMTEXT DEFAULT NULL, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`serial`), INDEX `idx_owner` (`owner_identifier`) ) 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::`, 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 { "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 }], "radii": [{ "id": "r_example", "worldX": 200.0, "worldY": -800.0, "presetId": "small", "radiusPixels": 120, "customColor": "#ef4444", "customAlpha": 20 }], "labels": [{ "id": "l_example", "text": "Base Camp", "pixelX": 200.0, "pixelY": 400.0, "fontSize": 14, "color": "#ffffff", "rotation": 0 }], "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 }] } ``` ## Give a map item ```lua Bridge.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. ![Del Perro treasure example in the map viewer with the surrounding area cropped to a regional map.](https://cdn.sanity.io/images/spkq08dw/production/469ae10f75b2a083f51babc2586e67180cc971ef-2560x1440.webp) _A map limited to Del Perro. The drawn trail and red X are decorative._ 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** ```lua Config.Items['my_map_ui'] = 'my_map' Config.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 calibration = { scaleX = 1.14654, offsetX = 2448.05, scaleY = -1.14664, offsetY = -598.61, imageSize = 1254, } ``` 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 ` and place a marker. The tool prints and copies an entry for `Config.DefaultMarkers`; it does not save it as a player annotation. ```lua { mapId = 'atlas', worldX = 200.0, worldY = -800.0, preset = 'camp', label = 'Base Camp', }, ``` 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 local rows = MySQL.query.await([[ SELECT serial, map_id, discovered FROM ml_custommap_data WHERE owner_identifier = ? ]], { 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.