Files
2026-08-05 01:29:39 +02:00

254 lines
9.2 KiB
Lua

local function cleanName(value)
if type(value) ~= 'string' then return nil end
value = value:gsub('^%s+', ''):gsub('%s+$', '')
if #value < 2 or #value > 32 then return nil end
if not value:match("^[%aÀ-ÿ'%- ]+$") then return nil end
return value
end
local function validDate(value)
if type(value) ~= 'string' then return false end
local year, month, day = value:match('^(%d%d%d%d)%-(%d%d)%-(%d%d)$')
year, month, day = tonumber(year), tonumber(month), tonumber(day)
if not year or year < 1900 or year > 2010 then return false end
if not month or month < 1 or month > 12 then return false end
if not day or day < 1 or day > 31 then return false end
local daysInMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
local leapYear = year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0)
if leapYear then daysInMonth[2] = 29 end
return day <= daysInMonth[month]
end
local function normalizeDate(value)
if type(value) == 'number' then
local seconds = value > 100000000000 and math.floor(value / 1000) or math.floor(value)
return os.date('!%Y-%m-%d', seconds)
end
local text = tostring(value or '')
local numeric = tonumber(text)
if numeric then
local seconds = numeric > 100000000000 and math.floor(numeric / 1000) or math.floor(numeric)
return os.date('!%Y-%m-%d', seconds)
end
return text:match('^(%d%d%d%d%-%d%d%-%d%d)') or text
end
local function accountNumberFor(characterId)
return ('LS%010d'):format(tonumber(characterId))
end
local function phoneNumberFor(characterId)
return ('%s-%04d'):format(DevXConfig.PhonePrefix or '555', tonumber(characterId))
end
local function decodeMetadata(value)
if type(value) ~= 'string' or value == '' then return {} end
local ok, result = pcall(json.decode, value)
return ok and type(result) == 'table' and result or {}
end
local function listCharacters(license)
local rows = MySQL.query.await([[
SELECT id, slot, first_name AS firstName, last_name AS lastName,
date_of_birth AS dateOfBirth, sex, model, phone_number AS phoneNumber, metadata
FROM devx_characters
WHERE owner_license = ? AND deleted_at IS NULL
ORDER BY slot ASC
]], { license }) or {}
for _, character in ipairs(rows) do
local metadata = decodeMetadata(character.metadata)
character.id = tonumber(character.id)
character.slot = tonumber(character.slot)
character.introCompleted = metadata.introCompleted == true
character.metadata = nil
character.dateOfBirth = normalizeDate(character.dateOfBirth)
if not character.phoneNumber or character.phoneNumber == '' then
character.phoneNumber = phoneNumberFor(character.id)
MySQL.update.await(
'UPDATE devx_characters SET phone_number = ? WHERE id = ?',
{ character.phoneNumber, character.id }
)
end
end
return rows
end
DevXRegisterCallback('characters:list', function(source)
local license = exports.devx_core:GetLicense(source)
if not license then return { ok = false, error = 'Keine Lizenzkennung.' } end
return {
ok = true,
characters = listCharacters(license),
maxCharacters = DevXConfig.MaxCharacters
}
end)
DevXRegisterCallback('characters:create', function(source, payload)
local license = exports.devx_core:GetLicense(source)
if not license then return { ok = false, error = 'Keine Lizenzkennung.' } end
local firstName = cleanName(payload.firstName)
local lastName = cleanName(payload.lastName)
local sex = payload.sex
local dateOfBirth = payload.dateOfBirth
if not firstName or not lastName then
return { ok = false, error = 'Vor- und Nachname sind ungültig.' }
end
if sex ~= 'm' and sex ~= 'f' and sex ~= 'x' then
return { ok = false, error = 'Ungültige Geschlechtsangabe.' }
end
if not validDate(dateOfBirth) then
return { ok = false, error = 'Geburtsdatum ist ungültig.' }
end
local existing = listCharacters(license)
if #existing >= DevXConfig.MaxCharacters then
return { ok = false, error = 'Alle Charakterplätze sind belegt.' }
end
local used = {}
for _, character in ipairs(existing) do used[tonumber(character.slot)] = true end
local slot = 1
while used[slot] do slot = slot + 1 end
local model = sex == 'f' and 'mp_f_freemode_01' or 'mp_m_freemode_01'
local metadata = json.encode({ introCompleted = false, rpXp = 0 })
local characterId = tonumber(MySQL.insert.await([[
INSERT INTO devx_characters
(owner_license, slot, first_name, last_name, date_of_birth, sex, model, cash, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
]], {
license, slot, firstName, lastName, dateOfBirth, sex, model,
DevXConfig.StartCash, metadata
}))
if not characterId or characterId < 1 then
return { ok = false, error = 'Charakter konnte nicht in der Datenbank erstellt werden.' }
end
local phoneNumber = phoneNumberFor(characterId)
MySQL.update.await('UPDATE devx_characters SET phone_number = ? WHERE id = ?', {
phoneNumber, characterId
})
local accountNumber = accountNumberFor(characterId)
local accountId = tonumber(MySQL.insert.await([[
INSERT INTO devx_bank_accounts
(character_id, account_number, account_name, account_type, balance)
VALUES (?, ?, ?, 'personal', ?)
]], {
characterId, accountNumber, ('%s %s'):format(firstName, lastName), DevXConfig.StartBank
}))
if not accountId or accountId < 1 then
MySQL.update.await('DELETE FROM devx_characters WHERE id = ?', { characterId })
return { ok = false, error = 'Bankkonto konnte nicht erstellt werden.' }
end
MySQL.insert.await([[
INSERT INTO devx_bank_transactions
(account_id, direction, amount, balance_after, counterparty, reference)
VALUES (?, 'credit', ?, ?, 'Stadtverwaltung', 'Startguthaben')
]], { accountId, DevXConfig.StartBank, DevXConfig.StartBank })
return {
ok = true,
characterId = characterId,
phoneNumber = phoneNumber,
characters = listCharacters(license)
}
end)
DevXRegisterCallback('characters:select', function(source, payload)
local characterId = tonumber(payload.characterId)
if not characterId then return { ok = false, error = 'Ungültiger Charakter.' } end
local player, errorMessage = exports.devx_core:ActivateCharacter(source, characterId)
if not player then return { ok = false, error = errorMessage } end
return { ok = true, player = player }
end)
DevXRegisterCallback('characters:completeIntro', function(source)
local player = exports.devx_core:GetPlayer(source)
if not player then return { ok = false, error = 'Kein Charakter aktiv.' } end
local saved = exports.devx_core:SetMetadataValue(source, 'introCompleted', true)
if not saved then
return { ok = false, error = 'Introstatus konnte nicht gespeichert werden.' }
end
local finalSpawn = DevXCharacterConfig.Intro.finalSpawn
player.position = {
x = finalSpawn.x,
y = finalSpawn.y,
z = finalSpawn.z,
heading = finalSpawn.heading
}
MySQL.update.await(
'UPDATE devx_characters SET position = ? WHERE id = ?',
{ json.encode(player.position), player.characterId }
)
return { ok = true }
end)
DevXRegisterCallback('characters:delete', function(source, payload)
local license = exports.devx_core:GetLicense(source)
local characterId = tonumber(payload.characterId)
if not license or not characterId then
return { ok = false, error = 'Ungültige Anfrage.' }
end
local character = MySQL.single.await([[
SELECT id, first_name, last_name
FROM devx_characters
WHERE id = ? AND owner_license = ? AND deleted_at IS NULL
LIMIT 1
]], { characterId, license })
if not character then return { ok = false, error = 'Charakter wurde nicht gefunden.' } end
local ownedProperties = tonumber(MySQL.scalar.await([[
SELECT COUNT(*) FROM devx_property_owners WHERE character_id = ?
]], { characterId })) or 0
if ownedProperties > 0 then
return { ok = false, error = 'Verkaufe zuerst alle Wohnungen dieses Charakters.' }
end
local changed = MySQL.update.await([[
DELETE FROM devx_characters
WHERE id = ? AND owner_license = ?
]], { characterId, license })
if not changed or changed < 1 then
return { ok = false, error = 'Charakter konnte nicht gelöscht werden.' }
end
print(('[devx_characters] %s %s (ID %s) wurde von Lizenz %s gelöscht.'):format(
character.first_name, character.last_name, characterId, license
))
return { ok = true, characters = listCharacters(license) }
end)
RegisterCommand('resetintro', function(source, args)
if source ~= 0 and not IsPlayerAceAllowed(source, 'devx.admin') then return end
local target = tonumber(args[1]) or source
if not target or target == 0 then return end
local player = exports.devx_core:GetPlayer(target)
if not player then return end
exports.devx_core:SetMetadataValue(target, 'introCompleted', false)
end, false)