Developer
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
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
Changing Discoveries
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 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
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. BypassesConfig.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 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:
title(string, default'Note'): Truncated to 200 charstype(string, default'text'):'text'or'drawing'content(string, defaultnil): Body of a text notedataUrl(string, defaultnil): Base64 data URL for a drawingbackgroundImage(string, defaultnil): Optional background image URL or data URLimageSize(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.
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:
mode(string, default'all'):'all'requires every needle to match,'any'returns on first hitpattern(boolean, defaultfalse): Whentrue, needles are treated as Lua patterns; whenfalse, plain substringscaseSensitive(boolean, defaultfalse): Whenfalse, 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
The server sends these events to keep the interface in sync.
ml_diary:client:discoveryUnlocked(key, timestamp): afterunlockDiscoveryml_diary:client:discoveryRemoved(key): afterremoveDiscoveryml_diary:client:notesUpdated(notes[]): after an externaladdPersonalNoteorremovePersonalNoteml_diary:client:openInterface(mode, serial): the player uses the diary itemml_diary:client:openPagePreview(pageData): the player uses a torn page item
Examples
1AddEventHandler('fishing:onCatch', function(source, fishName)
2 exports.ml_diary:unlockDiscovery(source, 'fish_' .. fishName)
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 })