# Developer > Developer API reference for ML Diary Category: DIARY · Source: https://miciomods.it/docs/ml-diary-developer · Last updated: 2026-07-09 ## Overview ML Diary exposes server exports for discoveries and personal notes, client exports for instant local reads, and a replicated state bag (`diaryDiscoveries`) for real-time sync. All server-side operations target the diary the player is currently holding, there is no per-export "ownership" gate; if the player carries the diary, they can act on it. License mode is enforced implicitly because the serial format binds the diary to the player's identifier. ## Server Exports ### Discoveries. Query ```lua local has = exports.ml_diary:hasDiary(source) local serial = exports.ml_diary:getDiarySerial(source) local found = exports.ml_diary:hasDiscovered(source, 'my_key') local all = exports.ml_diary:getDiscoveries(source) local unlocked, total, pct = exports.ml_diary:getDiscoveryProgress(source, 'fauna') local complete = exports.ml_diary:isCategoryComplete(source, 'fauna') local count, limit = exports.ml_diary:getPersonalNoteCount(source) ``` - `hasDiary(source)`: Returns `true` if the player has a diary item - `getDiarySerial(source)`: Returns the active diary's serial string, or `nil` - `hasDiscovered(source, key)`: Returns `true` if the entry is unlocked on the active diary - `getDiscoveries(source)`: Returns a table of all unlocked keys mapped to their unlock timestamp - `getDiscoveryProgress(source, category?)`: Returns `unlocked, total, percentage`. With no category argument, returns overall progress - `isCategoryComplete(source, category)`: Returns `true` if every entry in the category is unlocked - `getPersonalNoteCount(source)`: Returns current note count and `Config.PersonalPageLimit` ### Discoveries. Mutation ```lua exports.ml_diary:unlockDiscovery(source, 'my_key') exports.ml_diary:removeDiscovery(source, 'my_key') exports.ml_diary:unlockAllDiscoveries(source) exports.ml_diary:unlockCategory(source, 'fauna') exports.ml_diary:resetDiscoveries(source) ``` - `unlockDiscovery(source, key)`: Unlocks a single discovery. Returns `true` on success, `false` if the key is invalid or the player has no diary - `removeDiscovery(source, key)`: Removes a discovery - `unlockAllDiscoveries(source)`: Unlocks every entry in `Config.Discoverables` - `unlockCategory(source, category)`: Unlocks every entry whose `collection` matches. Returns the count of newly unlocked entries - `resetDiscoveries(source)`: Clears all discoveries on the active diary Every mutation marks the diary cache as dirty, updates the state bag, and triggers `ml_diary:client:discoveryUnlocked` (or `discoveryRemoved`) on the source client for live UI sync. ### Personal Notes ```lua local ok, noteId = exports.ml_diary:addPersonalNote(source, { title = 'Quest: The Hidden Door', type = 'text', content = 'You found a sealed door behind the Elix Gen safe...', }) exports.ml_diary:removePersonalNote(source, noteId) exports.ml_diary:noteContainsKeywords(source, { 'safe', 'Elix Gen' }) exports.ml_diary:forceSave(source) ``` - `addPersonalNote(source, noteData)`: Adds a note to the active diary. Returns `ok, noteId`. Marked `external = true` so the user's save flow preserves it. **Bypasses `Config.PersonalPageLimit`**: quests and missions can always add page #16 even when the player has filled their personal pages. The `MaxDiaryTotalSize` cap is still enforced - `removePersonalNote(source, noteId)`: Removes any note (user-written or external) by ID - `noteContainsKeywords(source, needles, opts?)`: Returns `true` if the diary contains the given keywords (see below) - `forceSave(source)`: Schedules an async UPDATE to disk. Non-blocking. Returns `true` if a save was queued or the diary was already clean ### Note Data Schema `noteData` for `addPersonalNote` accepts: Field. Type. Default. Notes `title`: string, `'Note'`: Truncated to 200 chars `type`: string, `'text'`: `'text'` or `'drawing'` `content`: string, `nil`: Body of a text note. Capped at `Config.MaxTextSize` `dataUrl`: string, `nil`: Base64 data URL for a drawing. Capped at `Config.MaxDataUrlSize` `backgroundImage`: string, `nil`: Optional background image URL or data URL `imageSize`: string, `'medium'`: `'small'`, `'medium'`, or `'large'` External notes are read-only from the in-game UI. The user can view them but cannot edit or delete them, `savePersonalNotes` strips and re-merges externals on every save. Use `removePersonalNote` from the originating script to remove them. ### Keyword Search `noteContainsKeywords(source, needles, opts?)` runs a single concatenated scan over every note's title and content. ```lua -- Single substring (case-insensitive by default) exports.ml_diary:noteContainsKeywords(src, 'safe') -- ALL must match (default mode) exports.ml_diary:noteContainsKeywords(src, { 'safe', 'Elix Gen', 'sealed door' }) -- ANY match wins exports.ml_diary:noteContainsKeywords(src, { 'door', 'gate' }, { mode = 'any' }) -- Lua pattern (regex-lite) exports.ml_diary:noteContainsKeywords(src, 'lab%-?%d+', { pattern = true }) -- Case sensitive exports.ml_diary:noteContainsKeywords(src, 'Boss', { caseSensitive = true }) ``` `opts` fields: Field. Type. Default. Effect `mode`: string, `'all'`: `'all'` requires every needle to match, `'any'` returns on first hit `pattern`: boolean, `false`: When `true`, needles are treated as Lua patterns; when `false`, plain substrings `caseSensitive`: boolean, `false`: When `false`, both haystack and needles are lowered before matching ### Maintenance ```lua exports.ml_diary:forceSave(source) ``` Schedules a non-blocking `UPDATE` for the active diary. Useful before triggering a critical event (e.g. a player about to be kicked or a server shutdown). The auto-save thread runs every `Config.SaveInterval` minutes anyway and `onResourceStop` flushes dirty diaries on shutdown. ## Client Exports ```lua local found = exports.ml_diary:hasDiscovered('my_key') local all = exports.ml_diary:getDiscoveries() ``` - `hasDiscovered(key)`: Reads from `LocalPlayer.state.diaryDiscoveries`. No server roundtrip - `getDiscoveries()`: Returns the full unlocked-keys table from the state bag ## State Bag ```lua local discoveries = LocalPlayer.state.diaryDiscoveries ``` Replicated `true`. Mirrors the discovery map of whatever diary the player is currently holding. Re-syncs automatically on inventory changes. When the player gives, takes, drops, or picks up a diary, the bag is rewritten to match the new active diary (or cleared when no diary is held). ## Client Events These events are emitted by the server for live UI sync. They are not part of the public API but documented for completeness. Event. Payload. Fires when `ml_diary:client:discoveryUnlocked`: `key, timestamp`: After `unlockDiscovery` server-side `ml_diary:client:discoveryRemoved`: `key`: After `removeDiscovery` server-side `ml_diary:client:notesUpdated`: `notes[]`: After external `addPersonalNote` or `removePersonalNote` `ml_diary:client:openInterface`: `mode, serial`: When the player uses the diary item `ml_diary:client:openPagePreview`: `pageData`: When the player uses a torn page item ## Examples **Unlock a discovery on fish caught** **your_script/server.lua** ```lua AddEventHandler('fishing:onCatch', function(source, fishModel) exports.ml_diary:unlockDiscovery(source, fishModel) end) ``` **Gate crafting on a known herb** **your_script/client.lua** ```lua local knows = exports.ml_diary:hasDiscovered('wild_salvia') if not knows then Bridge.Notify('You need to discover this herb first', 'error') return end ``` **Track category completion** **your_script/server.lua** ```lua local _, _, pct = exports.ml_diary:getDiscoveryProgress(source, 'fauna') if pct == 100 then Bridge.NotifyPlayer(source, 'success', 'Fauna encyclopedia complete!') end ``` **Add a quest page from a mission script** **your_script/server.lua** ```lua RegisterNetEvent('quest:hiddenDoor:start', function() local src = source local ok, noteId = exports.ml_diary:addPersonalNote(src, { title = 'Quest: The Hidden Door', type = 'text', content = 'A locked door waits behind the Elix Gen safe. Find the key.', }) if ok then ActiveQuests[src] = noteId -- save to remove the page later end end) ``` **Reactive quest unlock based on player's diary** **your_script/server.lua** ```lua -- When the player examines the Elix Gen safe, check whether they've already -- documented the lead. If yes, unlock the next stage and append a quest page. RegisterNetEvent('elixgen:examineSafe', function() local src = source local matched = exports.ml_diary:noteContainsKeywords(src, { 'safe', 'Elix Gen', 'door' }, { mode = 'all' }) if matched then TriggerEvent('quest:hiddenDoor:unlockStage2', src) exports.ml_diary:addPersonalNote(src, { title = 'Door Unlocked', content = 'The door behind the safe opened. Inside: a hidden lab.', }) end end) ``` **Pattern matching for serial-style notes** **your_script/server.lua** ```lua -- Match any note that mentions a lab code like "lab12", "lab-7", "LAB-99" local hasLabReference = exports.ml_diary:noteContainsKeywords(src, 'lab%-?%d+', { pattern = true }) ```