Developer

6 min readUpdated 2 weeks ago

Overview

ML Diary exposes server exports for discoveries and personal notes, client exports for 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

Reading Discoveries

lua
1local has = exports.ml_diary:hasDiary(source)
2local serial = exports.ml_diary:getDiarySerial(source)
3local found = exports.ml_diary:hasDiscovered(source, 'my_key')
4local all = exports.ml_diary:getDiscoveries(source)
5local unlocked, total, pct = exports.ml_diary:getDiscoveryProgress(source, 'fauna')
6local complete = exports.ml_diary:isCategoryComplete(source, 'fauna')
7local 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

Changing Discoveries

lua
1exports.ml_diary:unlockDiscovery(source, 'my_key')
2exports.ml_diary:removeDiscovery(source, 'my_key')
3exports.ml_diary:unlockAllDiscoveries(source)
4exports.ml_diary:unlockCategory(source, 'fauna')
5exports.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 change marks the diary cache as dirty and updates the state bag. unlockDiscovery and removeDiscovery also trigger ml_diary:client:discoveryUnlocked or discoveryRemoved on the source client for live UI sync.

Personal Notes

lua
1local ok, noteId = exports.ml_diary:addPersonalNote(source, {
2    title   = 'Quest: The Hidden Door',
3    type    = 'text',
4    content = 'You found a sealed door behind the Elix Gen safe...',
5})
6
7exports.ml_diary:removePersonalNote(source, noteId)
8exports.ml_diary:noteContainsKeywords(source, { 'safe', 'Elix Gen' })
9exports.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 4 MB total 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:

  • title (string, default 'Note'): Truncated to 200 chars
  • type (string, default 'text'): 'text' or 'drawing'
  • content (string, default nil): Body of a text note
  • dataUrl (string, default nil): Base64 data URL for a drawing
  • backgroundImage (string, default nil): Optional background image URL or data URL
  • imageSize (string, default '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.

noteContainsKeywords(source, needles, opts?) runs a single concatenated scan over every note's title and content.

lua
1-- Single substring (case-insensitive by default)
2exports.ml_diary:noteContainsKeywords(src, 'safe')
3
4-- ALL must match (default mode)
5exports.ml_diary:noteContainsKeywords(src, { 'safe', 'Elix Gen', 'sealed door' })
6
7-- ANY match wins
8exports.ml_diary:noteContainsKeywords(src, { 'door', 'gate' }, { mode = 'any' })
9
10-- Lua pattern (regex-lite)
11exports.ml_diary:noteContainsKeywords(src, 'lab%-?%d+', { pattern = true })
12
13-- Case sensitive
14exports.ml_diary:noteContainsKeywords(src, 'Boss', { caseSensitive = true })

opts fields:

  • mode (string, default 'all'): 'all' requires every needle to match, 'any' returns on first hit
  • pattern (boolean, default false): When true, needles are treated as Lua patterns; when false, plain substrings
  • caseSensitive (boolean, default false): When false, both haystack and needles are lowered before matching

Maintenance

lua
1exports.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
1local found = exports.ml_diary:hasDiscovered('my_key')
2local 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
1local 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

The server sends these events to keep the interface in sync.

  • ml_diary:client:discoveryUnlocked (key, timestamp): after unlockDiscovery
  • ml_diary:client:discoveryRemoved (key): after removeDiscovery
  • ml_diary:client:notesUpdated (notes[]): after an external addPersonalNote or removePersonalNote
  • ml_diary:client:openInterface (mode, serial): the player uses the diary item
  • ml_diary:client:openPagePreview (pageData): the player uses a torn page item

Examples

your_script/server.lua
1AddEventHandler('fishing:onCatch', function(source, fishName)
2    exports.ml_diary:unlockDiscovery(source, 'fish_' .. fishName)
3end)
your_script/client.lua
1local knows = exports.ml_diary:hasDiscovered('wild_salvia')
2if not knows then
3    Bridge.Notify('You need to discover this herb first', 'error')
4    return
5end
your_script/server.lua
1local _, _, pct = exports.ml_diary:getDiscoveryProgress(source, 'fauna')
2if pct == 100 then
3    Bridge.NotifyPlayer(source, 'success', 'Fauna encyclopedia complete!')
4end
your_script/server.lua
1RegisterNetEvent('quest:hiddenDoor:start', function()
2    local src = source
3    local ok, noteId = exports.ml_diary:addPersonalNote(src, {
4        title   = 'Quest: The Hidden Door',
5        type    = 'text',
6        content = 'A locked door waits behind the Elix Gen safe. Find the key.',
7    })
8    if ok then
9        ActiveQuests[src] = noteId -- save to remove the page later
10    end
11end)
your_script/server.lua
1-- When the player examines the Elix Gen safe, check whether they've already
2-- documented the lead. If yes, unlock the next stage and append a quest page.
3RegisterNetEvent('elixgen:examineSafe', function()
4    local src = source
5    local matched = exports.ml_diary:noteContainsKeywords(src,
6        { 'safe', 'Elix Gen', 'door' }, { mode = 'all' })
7
8    if matched then
9        TriggerEvent('quest:hiddenDoor:unlockStage2', src)
10        exports.ml_diary:addPersonalNote(src, {
11            title   = 'Door Unlocked',
12            content = 'The door behind the safe opened. Inside: a hidden lab.',
13        })
14    end
15end)
your_script/server.lua
1-- Match any note that mentions a lab code like "lab12", "lab-7", "LAB-99"
2local hasLabReference = exports.ml_diary:noteContainsKeywords(src,
3    'lab%-?%d+', { pattern = true })