Developer
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
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): Returnstrueif the player has a diary itemgetDiarySerial(source): Returns the active diary's serial string, ornilhasDiscovered(source, key): Returnstrueif the entry is unlocked on the active diarygetDiscoveries(source): Returns a table of all unlocked keys mapped to their unlock timestampgetDiscoveryProgress(source, category?): Returnsunlocked, total, percentage. With no category argument, returns overall progressisCategoryComplete(source, category): Returnstrueif every entry in the category is unlockedgetPersonalNoteCount(source): Returns current note count andConfig.PersonalPageLimit
Discoveries. Mutation
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. Returnstrueon success,falseif the key is invalid or the player has no diaryremoveDiscovery(source, key): Removes a discoveryunlockAllDiscoveries(source): Unlocks every entry inConfig.DiscoverablesunlockCategory(source, category): Unlocks every entry whosecollectionmatches. Returns the count of newly unlocked entriesresetDiscoveries(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
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. Returnsok, noteId. Markedexternal = trueso 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. TheMaxDiaryTotalSizecap is still enforcedremovePersonalNote(source, noteId): Removes any note (user-written or external) by IDnoteContainsKeywords(source, needles, opts?): Returnstrueif the diary contains the given keywords (see below)forceSave(source): Schedules an async UPDATE to disk. Non-blocking. Returnstrueif 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.
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
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
1local found = exports.ml_diary:hasDiscovered('my_key')
2local all = exports.ml_diary:getDiscoveries()hasDiscovered(key): Reads fromLocalPlayer.state.diaryDiscoveries. No server roundtripgetDiscoveries(): Returns the full unlocked-keys table from the state bag
State Bag
1local discoveries = LocalPlayer.state.diaryDiscoveriesReplicated 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
1AddEventHandler('fishing:onCatch', function(source, fishModel)
2 exports.ml_diary:unlockDiscovery(source, fishModel)
3end)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
5end1local _, _, pct = exports.ml_diary:getDiscoveryProgress(source, 'fauna')
2if pct == 100 then
3 Bridge.NotifyPlayer(source, 'success', 'Fauna encyclopedia complete!')
4end1RegisterNetEvent('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)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)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 })