# Developer > Developer API reference for ML Skills Category: SKILLS · Source: https://miciomods.it/docs/ml-skills-developer · Last updated: 2026-07-28 ## Overview ML Skills exposes 30+ server exports plus three client open-hook overrides. Other resources never need to read the database directly. Every state mutation and every query has a matching export. All exports are PascalCase and live under `exports.ml_skills:(...)`. Names are case-sensitive. > **WARNING:** Argument order > > Category-scoped server exports take the category UID first and the player id last: `AddXp(categoryUid, amount, source)`. Passing the player id first sends the id into the category parameter, the category into the amount, and the call is rejected without an error. Exports that are not category-scoped (`ResetSkills`, `FullWipePlayer`, `GetUnlockedSkills`, `GetPlayerGlobalStats`, `GetPlayerAchievements`, `ClaimAchievement`) take the player id first. ## Server Exports ### XP - `AddXp(categoryUid, amount, source)`: Adds XP to a player. Returns `true` on success, `false` when the amount is invalid, the player is not cached, the category does not exist, or the daily cap absorbed the grant - `RemoveXp(categoryUid, amount, source)`: Subtracts XP. Returns `true, removedAmount` - `GetPlayerXp(categoryUid, source)`: Returns the current-level XP (progress toward the next level, not cumulative) - `GetPlayerTotalXp(categoryUid, source)`: Returns the lifetime XP earned in the category Amounts must be positive integers up to 999999. A cached player always has at least level 1 in every configured category, so a return of `0` from a getter means the player is not loaded yet. ### Points - `AddPoints(categoryUid, amount, source)`: Grants unspent skill points - `RemovePoints(categoryUid, amount, source)`: Subtracts skill points, floored at 0 - `GetPlayerPoints(categoryUid, source)`: Returns the unspent skill points ### Levels - `GetPlayerLevel(categoryUid?, source)`: Returns the current level. Pass `nil` as the category to read the first populated tree - `GetSkillLevel(categoryUid?, source)`: Alias of `GetPlayerLevel`, kept for readability in integration code - `SetLevel(categoryUid, level, source)`: Sets the level to an exact value, clamped between 1 and the category `maxLevel`, and recalculates the point pool. Bypasses the daily XP cap - `GetPlayerGlobalStats(source)`: Returns `{ totalXp, totalLevel, usedPoints, unlockedSkills }` aggregated across every category ### Skill State - `UnlockSkill(categoryUid, skillUid, source)`: Unlocks the skill if the requirements are met. Returns `true` on success or `false, reason` on failure. Gated by `Config.EnableUnlockSkillExport` - `ClaimSkillReward(categoryUid, skillUid, source)`: Claims a pending reward attached to a previously unlocked skill - `LockSkill(categoryUid, skillUid, source)`: Locks a previously unlocked skill and refunds the point - `HasUnlockedSkill(categoryUid?, skillUid, source)`: Returns `true` when the skill is in the player's unlocked list. The category is optional here, a missing one falls back to a scan by UID across every tree - `GetUnlockedSkills(source)`: Returns an array of every unlocked skill across every category - `GetSkillEffect(categoryUid?, skillUid)`: Returns the `effect` value of the skill definition (e.g. `10` for a `+10%` health perk). Reads the tree, so it takes no player id - `FindSkillCategory(skillUid)`: Resolves a skill UID to the category it belongs to, or `nil` ### Tree & Categories - `GetConfig()`: Returns `{ Categories, Trees }`, the runtime tables with the database values already merged over `shared/config.lua` - `GetSkillTrees()`: Returns the full tree definitions, keyed by category UID - `SaveSkillTree(categoryUid, treeArray, source?, suppressBroadcast?)`: Persists a tree, bumps the version counter and broadcasts the update. Returns the new version. `source` is the admin player id recorded in the audit trail - `GetTotalCategoryBonus(categoryUid, source)`: Sums the `effect` of every unlocked skill in the category. This is how a consumer turns a tree into a single gameplay multiplier > **INFO:** Categories live in the database > > Categories created from the admin panel exist only in the database, not in `shared/config.lua`. `GetConfig().Categories` returns the merged runtime map, so it is the correct source when validating a category UID from another resource. ### Lock Helpers - `GetSkillDef(categoryUid?, skillUid)`: Returns the full skill definition from the tree - `IsSkillLocked(categoryUid, skillUid, source)`: Returns `locked, reason, data` - `GetSkillLockReason(categoryUid, skillUid, source)`: Returns `{ locked, reason, data, message }`, where `message` is the localized text and `nil` when the reason has no message - `GetBlockedSkills(categoryUid, skillUid)`: Returns the skills that are mutually exclusive with this one, as an array of `{ category, uid, name }` ### Reset & Wipe - `ResetSkills(source, isAdmin?)`: Resets every category for a player and refunds all spent points. Returns `true, refundedPoints`. Blocked unless `Config.AllowReset` is on or `isAdmin` is `true` - `ResetCategory(categoryUid, source)`: Resets a single category. Requires `Config.AllowReset` - `FullWipePlayer(source)`: Wipes every category, every unlock, every achievement. Irreversible ### Boost, Prestige, Leaderboard - `AddXpBoost(categoryUid, multiplier, durationMs, source)`: Applies a temporary XP multiplier (`1.5` = `+50%`). The multiplier must be between 0 and 10 and the duration must not exceed 86400000 ms (24 hours), longer values are rejected - `PrestigeCategory(categoryUid, source)`: Resets the category at max level and increments the prestige counter, granting the configured XP bonus. Returns `true, newPrestigeLevel`. Requires `Config.PrestigeEnabled` - `GetLeaderboard(categoryUid, limit?)`: Returns the top players in the category, ordered by prestige, then level, then XP. `limit` defaults to 10 ### Achievements - `GetPlayerAchievements(source)`: Returns the player's achievement state, keyed by achievement id, with `completed` and `claimed` flags - `ClaimAchievement(source, achievementId)`: Claims an unlocked achievement and pays out its rewards - `GetAchievementDefinitions()`: Returns the seeded and editor-saved achievement definitions ## Client Exports Called on the client (for example from `open/effects.lua` or another client script). They read the local player's state. - `GetActiveEffect(skillType)`: Returns the summed effect value of an unlocked skill type for the local player, or `0`. This is the integration point other scripts read, ml_recoil folds the `betterAccuracy` effect into its recoil multiplier this way - `HasUnlockedSkill(categoryUid, skillUid)`: Client-side unlock check. The category is required here, unlike the server export - `GetSkillEffect(categoryUid, skillUid)`: Client-side effect value for the local player - `GetTotalCategoryBonus(categoryUid)`: Client-side sum of the effects unlocked in the category - `GetPlayerData()`: Returns a copy of the local player's data, or `nil` while it has not arrived yet. `GetPlayerData().categories[categoryUid].level` is the client-side level read - `RequestXp(categoryUid, amount, reason)`: Asks the server to award a whitelisted XP listener. See below - `OpenSkillTree(categoryUid?)`: Opens the skill UI, optionally focused on a category - `CloseSkillTree()`: Closes the skill UI - `SetXpEarningStatus(enabled)` / `GetXpEarningStatus()`: Pauses and resumes passive XP listeners for the local player - `ReloadDefaultSkills()`: Re-applies every unlocked skill effect > **WARNING:** Client data arrives after the join > > `HasUnlockedSkill` returns `false`, not an error, while the player data is still missing (join window, or a hot `ensure ml_skills` that already connected clients never receive). Probe `GetPlayerData()` first and treat `nil` as "unknown" instead of "not unlocked", otherwise a client-side gate locks players out. ## XP From a Client-Side Event Server exports cannot be called from a client script. `exports.ml_skills:AddXp` only exists on the server, so calling it from a client file throws. When the action is only detectable on the client (a kill from another resource, a minigame result, a client-side event), use the client export `RequestXp`. The client never sends an amount that the server trusts: it sends the name of a listener, and the server resolves the category and the XP value from `Config.XpListeners`, then applies its own rate limits. Declare the listener without `interval` and `condition` so it never fires on its own: **shared/config.lua** ```lua Config.XpListeners = { zombie_kill = { category = 'personal', xp = 2, requiresMovement = false, cooldown = 2000, }, } ``` `requiresMovement = false` matters here: a kill can happen while the player stands still, and without it the server drops the claim as an AFK farm. `cooldown` is the minimum gap between two grants of this listener, 5000 ms when not set. Then request it from your client script: ```lua AddEventHandler('onSomethingKilled', function(entity) if GetPedSourceOfDeath(entity) == PlayerPedId() then exports.ml_skills:RequestXp('personal', 2, 'zombie_kill') end end) ``` The arguments are the category UID first, the XP amount second and the listener name last: `RequestXp(categoryUid, amount, reason)`. The category and the amount must match the listener entry exactly, or the call is dropped on both sides. Server-side the request is validated before any XP is granted: - the listener name must exist in `Config.XpListeners` - the category and the amount must match the configured entry - one grant per listener name per cooldown, taken from the listener `cooldown` or from its own tick rate, never below 1000 ms - the ped must have moved since the previous grant, unless the listener sets `requiresMovement = false` or `Config.XpListenerMovementCheck` is off - `Config.XpListenerCapPerMinute` and `Config.XpListenerDailyCapPerCategory` cap the total per category > **TIP:** Prefer the server when the event exists there > > If the other resource also fires the kill or completion event server-side, award the XP there instead: `exports.ml_skills:AddXp('personal', 2, src)` has no cooldown and no movement check. ## Events - `ml_skills:server:categoriesChanged`: Server-side `TriggerEvent` fired when an admin saves the category list. The argument is an array of removed category UIDs **server/main.lua** ```lua AddEventHandler('ml_skills:server:categoriesChanged', function(removed) MyCache.skills = nil end) ``` Resources that cache a category or skill picker should listen to it, otherwise a deleted category keeps showing in their admin UI until their own cache expires. ## Client Hooks The client side ships three overridable hooks in `open/client.lua`. Return `true` from `OnXpGained` or `OnLevelUp` to skip the built-in notification. ### Notify **open/client.lua** ```lua function OpenClient.Notify(message, type) Bridge.Notify(message, type) end ``` Default routes through `Bridge.Notify`. Override to send notifications to a custom HUD framework. ### OnXpGained **open/client.lua** ```lua function OpenClient.OnXpGained(categoryUid, categoryLabel, amount, catData, xpNeeded) return false end ``` Fires every time the player gains XP. `catData` carries the full category snapshot (level, xp, points, prestige). Return `true` to suppress the default XP toast. ### OnLevelUp **open/client.lua** ```lua function OpenClient.OnLevelUp(categoryUid, categoryLabel, newLevel) return false end ``` Fires when the player crosses a level threshold. Return `true` to suppress the default level-up notification. ## Skill Effect Handlers `open/effects.lua` exposes the `SkillHandlers` table. Each entry maps a `skillType` (the UID with the trailing `_N` stripped) to a function that applies the effect. The core re-applies every handler on resource start, on player spawn, and whenever a skill is unlocked. The total `effect` value passed to the handler is the sum of every unlocked skill of the same `skillType`. **open/effects.lua** ```lua local function applyRunFaster(effect) SetRunSprintMultiplierForPlayer(cache.playerId, 1.0 + (0.03 * effect)) end SkillHandlers.runFaster = applyRunFaster SkillHandlers.run_faster = applyRunFaster ``` Ship snake_case aliases when the tree uses snake_case UIDs so older trees keep working. ### Tick-Loop Effects Some effects need to fire on every frame (regen, low-HP damage bonus, taser resistance). Write a value into the shared `ActiveEffects` table and let the core tick loop consume it. **open/effects.lua** ```lua local function applyHpRegen(effect) if not ActiveEffects then ActiveEffects = {} end ActiveEffects.hpRegen = effect end SkillHandlers.hpRegeneration = applyHpRegen ``` ### Built-in Handlers The default tree ships with handlers for: - Movement: `runFaster`, `swimFaster`, `moreStamina`, `moreStaminaRegen`, `timeUnderwater`, `reduceStaminaOnBike` - Health: `moreMaxHp`, `hpRegeneration` - Damage: `feastDamageMultiplier` (fists), `meleeDamageMultiplier` (melee weapons), `damageReducer`, `ignoreTazer` - Combat: `betterAccuracy` (recoil shake), `fasterReload` (no-op until wired to a weapon framework), `lowHpWeaponDamageMultiplier` Replace any handler by reassigning `SkillHandlers.` in `open/effects.lua`. ## Examples **Award XP from a job resource** ```lua -- After a delivery in your job script, server-side: exports.ml_skills:AddXp('personal', 50, src) ``` **Award XP from a client-side kill event** ```lua -- Client-side. 'zombie_kill' must exist in Config.XpListeners with category 'personal' and xp = 2. exports.ml_skills:RequestXp('personal', 2, 'zombie_kill') ``` **Gate a feature behind a skill** ```lua -- Server-side, before exposing an advanced feature: if exports.ml_skills:HasUnlockedSkill('medical', 'reviveSpeed_3', src) then TriggerClientEvent('myJob:showAdvancedRevive', src) end ``` **Read a category bonus to compute a custom value** ```lua -- Stack a per-skill discount in your shop, server-side: local bonus = exports.ml_skills:GetTotalCategoryBonus('social', src) local finalPrice = basePrice * (1.0 - bonus / 100) ``` **Apply a temporary 2x XP boost** ```lua -- Server-side, on every player join during the event. -- 24 hours is the maximum duration the export accepts: exports.ml_skills:AddXpBoost('personal', 2.0, 24 * 60 * 60 * 1000, src) ``` **Push a leaderboard widget every 5 minutes** ```lua CreateThread(function() while true do local top = exports.ml_skills:GetLeaderboard('personal', 10) TriggerClientEvent('myHud:showLeaderboard', -1, top) Wait(5 * 60 * 1000) end end) ``` **Call an export safely from another resource** ```lua -- ml_skills may be stopped or still booting. Gate and pcall every call: local function AwardSkillXp(src, category, amount) if GetResourceState('ml_skills') ~= 'started' then return false end local ok, granted = pcall(function() return exports.ml_skills:AddXp(category, amount, src) end) return ok and granted or false end ```