2025-08-14 01:13:48 +02:00
|
|
|
lib.locale()
|
2023-07-09 21:19:35 +02:00
|
|
|
NDCore = {}
|
2023-08-23 00:30:38 +02:00
|
|
|
NDCore.players = {}
|
2023-07-11 01:05:13 +02:00
|
|
|
PlayersInfo = {}
|
2023-07-12 04:53:00 +02:00
|
|
|
local resourceName = GetCurrentResourceName()
|
2023-07-11 01:05:13 +02:00
|
|
|
local tempPlayersInfo = {}
|
2026-03-16 08:04:08 +01:00
|
|
|
-- Groups are now loaded from the database in MySQL.ready via NDCore.loadGroups()
|
2022-07-16 04:46:27 +02:00
|
|
|
|
2023-07-12 04:51:50 +02:00
|
|
|
Config = {
|
|
|
|
|
serverName = GetConvar("core:serverName", "Unconfigured ND-Core Server"),
|
|
|
|
|
discordInvite = GetConvar("core:discordInvite", "https://discord.gg/Z9Mxu72zZ6"),
|
2024-01-09 15:50:09 -10:00
|
|
|
discordMemeberRequired = GetConvarInt("core:discordMemeberRequired", 1) == 1,
|
2023-07-12 04:51:50 +02:00
|
|
|
discordAppId = GetConvar("core:discordAppId", "858146067018416128"),
|
|
|
|
|
discordAsset = GetConvar("core:discordAsset", "andyyy"),
|
|
|
|
|
discordAssetSmall = GetConvar("core:discordAssetSmall", "andyyy"),
|
|
|
|
|
discordActionText = GetConvar("core:discordActionText", "DISCORD"),
|
|
|
|
|
discordActionLink = GetConvar("discordActionLink", "https://discord.gg/Z9Mxu72zZ6"),
|
|
|
|
|
discordActionText2 = GetConvar("core:discordActionText2", "STORE"),
|
|
|
|
|
discordActionLink2 = GetConvar("core:discordActionLink2", "https://andyyy.tebex.io/category/fivem-scripts"),
|
|
|
|
|
characterIdentifier = GetConvar("core:characterIdentifier", "license"),
|
2026-03-16 05:42:38 +01:00
|
|
|
selectIdentifiers = json.decode(GetConvar("core:selectIdentifiers", '["discord", "license", "license2", "fivem"]')),
|
2023-12-03 08:38:35 -07:00
|
|
|
discordGuildId = GetConvar("core:discordGuildId", "false"),
|
|
|
|
|
discordBotToken = GetConvar("core:discordBotToken", "false"),
|
2023-07-12 04:51:50 +02:00
|
|
|
randomUnlockedVehicleChance = GetConvarInt("core:randomUnlockedVehicleChance", 30),
|
|
|
|
|
disableVehicleAirControl = GetConvarInt("core:disableVehicleAirControl", 1) == 1,
|
|
|
|
|
useInventoryForKeys = GetConvarInt("core:useInventoryForKeys", 1) == 1,
|
2026-03-16 08:04:08 +01:00
|
|
|
groups = {},
|
2023-10-16 00:28:36 +02:00
|
|
|
admins = json.decode(GetConvar("core:admins", "[]")),
|
2023-10-21 21:44:17 +02:00
|
|
|
adminDiscordRoles = json.decode(GetConvar("core:adminDiscordRoles", "[]")),
|
2024-02-29 22:28:25 +01:00
|
|
|
groupRoles = json.decode(GetConvar("core:groupRoles", "[]")),
|
2023-11-11 03:02:08 +01:00
|
|
|
multiCharacter = false,
|
2025-01-17 20:52:28 +01:00
|
|
|
compatibility = json.decode(GetConvar("core:compatibility", "[]")),
|
|
|
|
|
sv_lan = GetConvar("sv_lan", "false") == "true",
|
2025-08-13 16:57:35 +02:00
|
|
|
platePattern = GetConvar("core:platePattern", "11AAA111")
|
2023-07-12 04:51:50 +02:00
|
|
|
}
|
2022-11-30 03:32:52 +01:00
|
|
|
|
2023-08-30 01:21:53 +02:00
|
|
|
SetConvarServerInfo("Discord", Config.discordInvite)
|
2023-08-28 03:04:25 +02:00
|
|
|
SetConvarServerInfo("NDCore", GetResourceMetadata(resourceName, "version", 0) or "invalid")
|
2023-07-11 01:05:13 +02:00
|
|
|
SetConvarReplicated("inventory:framework", "nd")
|
2023-07-10 02:31:42 +02:00
|
|
|
|
2025-02-22 23:37:30 -10:00
|
|
|
lib.versionCheck('ND-Framework/ND_Core')
|
|
|
|
|
|
2023-07-11 01:05:13 +02:00
|
|
|
local function getIdentifierList(src)
|
|
|
|
|
local list = {}
|
|
|
|
|
for i=0, GetNumPlayerIdentifiers(src) do
|
|
|
|
|
local identifier = GetPlayerIdentifier(src, i)
|
|
|
|
|
if identifier then
|
|
|
|
|
local colon = identifier:find(":")
|
2023-07-12 04:53:00 +02:00
|
|
|
local identifierType = identifier:sub(1, colon-1)
|
|
|
|
|
list[identifierType] = identifier
|
2023-07-11 01:05:13 +02:00
|
|
|
end
|
|
|
|
|
end
|
2025-01-17 20:52:28 +01:00
|
|
|
|
|
|
|
|
if Config.sv_lan then
|
|
|
|
|
list[Config.characterIdentifier] = NDCore.getPlayerIdentifierByType(src, Config.characterIdentifier)
|
|
|
|
|
end
|
|
|
|
|
|
2023-07-11 01:05:13 +02:00
|
|
|
return list
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
AddEventHandler("playerJoining", function(oldId)
|
|
|
|
|
local src = source
|
2023-12-16 19:46:35 +01:00
|
|
|
local oldTempId = tonumber(oldId)
|
|
|
|
|
PlayersInfo[src] = tempPlayersInfo[oldTempId]
|
|
|
|
|
tempPlayersInfo[oldTempId] = nil
|
2023-10-16 00:28:36 +02:00
|
|
|
|
2025-01-17 20:52:28 +01:00
|
|
|
if Config.sv_lan then
|
|
|
|
|
lib.addPrincipal(("player.%s"):format(src), "group.admin")
|
|
|
|
|
end
|
|
|
|
|
|
2026-03-16 05:42:38 +01:00
|
|
|
local identifiers = PlayersInfo[src] and PlayersInfo[src].identifiers or getIdentifierList(src)
|
|
|
|
|
|
|
|
|
|
local whereParts = {}
|
|
|
|
|
local params = {}
|
|
|
|
|
|
|
|
|
|
for identifierType, identifier in pairs(identifiers) do
|
|
|
|
|
local isSelected = false
|
|
|
|
|
for i=1, #Config.selectIdentifiers do
|
|
|
|
|
local selectedType = Config.selectIdentifiers[i]
|
|
|
|
|
if identifierType == selectedType then
|
|
|
|
|
isSelected = true
|
|
|
|
|
break
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if isSelected then
|
|
|
|
|
local columnName = "id_" .. identifierType
|
|
|
|
|
local cleanId = identifier:gsub("^[^:]*:", "")
|
|
|
|
|
table.insert(whereParts, columnName .. " = ?")
|
|
|
|
|
table.insert(params, cleanId)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local user = nil
|
|
|
|
|
if #whereParts > 0 then
|
|
|
|
|
local query = "SELECT user_id FROM nd_users WHERE " .. table.concat(whereParts, " OR ") .. " LIMIT 1"
|
|
|
|
|
user = MySQL.query.await(query, params)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if user and user[1] then
|
|
|
|
|
PlayersInfo[src].userId = user[1].user_id
|
|
|
|
|
|
|
|
|
|
local updateParts = {}
|
|
|
|
|
local updateParams = {}
|
|
|
|
|
|
|
|
|
|
for identifierType, identifier in pairs(identifiers) do
|
|
|
|
|
local columnName = "id_" .. identifierType
|
|
|
|
|
local cleanId = identifier:gsub("^[^:]*:", "")
|
|
|
|
|
table.insert(updateParts, columnName .. " = ?")
|
|
|
|
|
table.insert(updateParams, cleanId)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if #updateParts > 0 then
|
|
|
|
|
table.insert(updateParams, user[1].user_id)
|
|
|
|
|
local updateQuery = "UPDATE nd_users SET " .. table.concat(updateParts, ", ") .. " WHERE user_id = ?"
|
|
|
|
|
MySQL.update.await(updateQuery, updateParams)
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
local columns = {}
|
|
|
|
|
local values = {}
|
|
|
|
|
local insertParams = {}
|
|
|
|
|
|
|
|
|
|
for identifierType, identifier in pairs(identifiers) do
|
|
|
|
|
local columnName = "id_" .. identifierType
|
|
|
|
|
local cleanId = identifier:gsub("^[^:]*:", "")
|
|
|
|
|
table.insert(columns, columnName)
|
|
|
|
|
table.insert(values, "?")
|
|
|
|
|
table.insert(insertParams, cleanId)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local insertQuery = "INSERT INTO nd_users (" .. table.concat(columns, ", ") .. ") VALUES (" .. table.concat(values, ", ") .. ")"
|
|
|
|
|
local user_id = MySQL.insert.await(insertQuery, insertParams)
|
|
|
|
|
PlayersInfo[src].userId = user_id
|
|
|
|
|
end
|
|
|
|
|
|
2023-10-16 00:28:36 +02:00
|
|
|
if Config.multiCharacter then return end
|
|
|
|
|
Wait(3000)
|
|
|
|
|
|
|
|
|
|
local characters = NDCore.fetchAllCharacters(src)
|
|
|
|
|
local id = next(characters)
|
|
|
|
|
if id then
|
|
|
|
|
return NDCore.setActiveCharacter(src, id)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local player = NDCore.newCharacter(src, {
|
|
|
|
|
firstname = GetPlayerName(src),
|
|
|
|
|
lastname = "",
|
|
|
|
|
dob = "",
|
|
|
|
|
gender = ""
|
|
|
|
|
})
|
|
|
|
|
NDCore.setActiveCharacter(src, player.id)
|
2023-07-11 01:05:13 +02:00
|
|
|
end)
|
|
|
|
|
|
2023-08-19 22:07:41 +02:00
|
|
|
local function checkDiscordIdentifier(identifiers)
|
2023-12-03 08:41:46 -07:00
|
|
|
if Config.discordBotToken == "false" or Config.discordGuildId == "false" then return end
|
2023-08-19 22:07:41 +02:00
|
|
|
|
|
|
|
|
local discordIdentifier = identifiers["discord"]
|
|
|
|
|
if not discordIdentifier then return end
|
|
|
|
|
|
2023-08-23 00:30:38 +02:00
|
|
|
return NDCore.getDiscordInfo(discordIdentifier:gsub("discord:", ""))
|
2023-08-19 22:07:41 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
AddEventHandler("onResourceStart", function(name)
|
2023-12-03 16:43:51 +01:00
|
|
|
if name ~= resourceName then return end
|
2023-08-19 22:07:41 +02:00
|
|
|
for _, playerId in ipairs(GetPlayers()) do
|
|
|
|
|
local src = tonumber(playerId)
|
|
|
|
|
local identifiers = getIdentifierList(src)
|
|
|
|
|
PlayersInfo[src] = {
|
|
|
|
|
identifiers = identifiers,
|
2023-12-03 16:43:51 +01:00
|
|
|
discord = checkDiscordIdentifier(identifiers) or {}
|
2023-08-19 22:07:41 +02:00
|
|
|
}
|
2023-08-23 00:30:38 +02:00
|
|
|
Wait(65)
|
2023-08-19 22:07:41 +02:00
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
2023-07-11 01:05:13 +02:00
|
|
|
AddEventHandler("playerConnecting", function(name, setKickReason, deferrals)
|
|
|
|
|
local tempSrc = source
|
|
|
|
|
local identifiers = getIdentifierList(tempSrc)
|
|
|
|
|
local mainIdentifier = identifiers[Config.characterIdentifier]
|
2023-12-20 13:02:13 +01:00
|
|
|
local discordInfo = nil
|
2023-07-11 01:05:13 +02:00
|
|
|
|
|
|
|
|
deferrals.defer()
|
|
|
|
|
Wait(0)
|
|
|
|
|
|
2023-12-20 13:02:13 +01:00
|
|
|
if mainIdentifier and Config.discordBotToken ~= "false" and Config.discordGuildId ~= "false" then
|
2023-08-19 22:07:41 +02:00
|
|
|
discordInfo = checkDiscordIdentifier(identifiers)
|
2025-01-17 20:52:28 +01:00
|
|
|
if not discordInfo and Config.discordMemeberRequired and not Config.sv_lan then
|
2025-08-14 01:13:48 +02:00
|
|
|
deferrals.done(locale("not_in_discord", Config.discordInvite))
|
2023-07-11 01:05:13 +02:00
|
|
|
Wait(0)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2025-08-14 01:13:48 +02:00
|
|
|
deferrals.update(locale("connecting"))
|
2023-07-11 01:05:13 +02:00
|
|
|
Wait(0)
|
|
|
|
|
|
2025-01-17 20:52:28 +01:00
|
|
|
if Config.sv_lan then
|
|
|
|
|
tempPlayersInfo[tempSrc] = {
|
|
|
|
|
identifiers = {
|
|
|
|
|
[Config.characterIdentifier] = "sv_lan"
|
|
|
|
|
},
|
|
|
|
|
discord = discordInfo
|
|
|
|
|
}
|
|
|
|
|
deferrals.done()
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
2023-07-11 01:05:13 +02:00
|
|
|
if mainIdentifier then
|
|
|
|
|
tempPlayersInfo[tempSrc] = {
|
|
|
|
|
identifiers = identifiers,
|
|
|
|
|
discord = discordInfo
|
|
|
|
|
}
|
|
|
|
|
deferrals.done()
|
|
|
|
|
else
|
2025-08-14 01:13:48 +02:00
|
|
|
deferrals.done(locale("identifier_not_found", Config.characterIdentifier))
|
2023-07-11 01:05:13 +02:00
|
|
|
Wait(0)
|
|
|
|
|
end
|
|
|
|
|
end)
|
2023-07-12 04:53:00 +02:00
|
|
|
|
|
|
|
|
AddEventHandler("playerDropped", function()
|
|
|
|
|
local src = source
|
2023-08-23 00:30:38 +02:00
|
|
|
local char = NDCore.players[src]
|
2023-08-27 17:59:22 +02:00
|
|
|
if char then char.unload() end
|
2023-07-12 04:53:00 +02:00
|
|
|
PlayersInfo[src] = nil
|
|
|
|
|
end)
|
|
|
|
|
|
2023-08-23 03:29:36 +02:00
|
|
|
AddEventHandler("onResourceStop", function(name)
|
|
|
|
|
if name ~= resourceName then return end
|
2023-08-27 17:59:22 +02:00
|
|
|
for _, player in pairs(NDCore.players) do
|
|
|
|
|
player.unload()
|
2023-08-23 03:29:36 +02:00
|
|
|
Wait(10)
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
|
2023-09-24 11:19:36 +02:00
|
|
|
RegisterNetEvent("ND:playerEliminated", function(info)
|
|
|
|
|
local src = source
|
|
|
|
|
local player = NDCore.getPlayer(src)
|
|
|
|
|
if not player then return end
|
|
|
|
|
player.setMetadata({
|
|
|
|
|
dead = true,
|
|
|
|
|
deathInfo = info
|
|
|
|
|
})
|
|
|
|
|
end)
|
2024-02-25 23:54:10 +01:00
|
|
|
|
2024-03-16 19:12:34 +01:00
|
|
|
RegisterNetEvent("ND:updateClothing", function(clothing)
|
2024-02-25 23:54:10 +01:00
|
|
|
local src = source
|
|
|
|
|
local player = NDCore.getPlayer(src)
|
|
|
|
|
if not player or not clothing or type(clothing) ~= "table" then return end
|
|
|
|
|
player.setMetadata("clothing", clothing)
|
|
|
|
|
end)
|
2026-03-16 05:42:38 +01:00
|
|
|
|
|
|
|
|
MySQL.ready(function()
|
|
|
|
|
Wait(100)
|
|
|
|
|
NDCore.loadSQL({
|
|
|
|
|
"database/users.sql",
|
|
|
|
|
"database/characters.sql",
|
|
|
|
|
"database/vehicles.sql",
|
2026-03-16 08:04:08 +01:00
|
|
|
"database/moneylogs.sql",
|
|
|
|
|
"database/groups.sql",
|
|
|
|
|
"database/group_ranks.sql"
|
2026-03-16 05:42:38 +01:00
|
|
|
}, resourceName)
|
2026-03-16 08:04:08 +01:00
|
|
|
Wait(200)
|
|
|
|
|
NDCore.seedGroupsFromJson()
|
|
|
|
|
NDCore.loadGroups()
|
2026-03-16 05:42:38 +01:00
|
|
|
end)
|
|
|
|
|
|
|
|
|
|
-- Hourly cron, purge soft-deleted characters older than 30 days.
|
|
|
|
|
lib.cron.new("0 * * * *", function()
|
|
|
|
|
MySQL.update.await("DELETE FROM nd_characters WHERE deleted_at IS NOT NULL AND deleted_at < DATE_SUB(NOW(), INTERVAL 30 DAY)")
|
|
|
|
|
end)
|