Developer

6 min readUpdated 2 weeks ago

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
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

Discoveries. Mutation

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 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
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 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.

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:

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
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

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

your_script/server.lua
1AddEventHandler('fishing:onCatch', function(source, fishModel)
2    exports.ml_diary:unlockDiscovery(source, fishModel)
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 })