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)