Zurück zum Blog
Guide8 Min. Lesezeit

FiveM-Autohändlersystem: Autohäuser richtig skripten

Erstelle einen realistischen FiveM-Autohändler mit Probefahrten, Showroom, NPCs und Spielerbetrieb. Entdecke die besten Skripte für QBCore und ESX.

Agency Scripts

Gründer und leitender Entwickler bei Agency Scripts

Architektur eines Fahrzeughändlers

Ein Fahrzeughändlersystem ist einer der wichtigsten Wirtschaftsfaktoren auf jedem FiveM-Rollenspielserver und dient als Anlaufstelle für Spieler, um Fahrzeuge zu erwerben. Ein gut gestalteter Händler bietet mehr als nur ein einfaches Kaufmenü. Zu den Funktionen gehören Fahrzeugvorschauen im Showroom, Probefahrten, Finanzierungsoptionen, Inzahlungnahmen und die Erfassung von Verkäufen für Mitarbeiter. Die Architektur gliedert sich in ein Katalogsystem, das verfügbare Fahrzeuge mit Preisen und Kategorien definiert, eine Showroom-Anzeige, über die Spieler Fahrzeuge vor dem Kauf begutachten können, eine Transaktionslogik für Käufe und Finanzierungen sowie eine Mitarbeiterebene für von Spielern betriebene Händler. Jede Ebene kommuniziert über serverseitig validierte Events, um Preismanipulation und das unerlaubte Spawnen von Fahrzeugen zu verhindern.

Fahrzeugkatalog und Kategoriesystem

Der Fahrzeugkatalog definiert alle zum Kauf verfügbaren Fahrzeuge und ordnet sie für eine einfache Suche in Kategorien ein. Jeder Eintrag enthält den Spawn-Namen, die Anzeigenbezeichnung, den Preis, die Kategorie und optionale Metadaten wie Höchstgeschwindigkeit und Sitzplatzanzahl für die Showroom-Anzeige. Speichere den Katalog in einer gemeinsam genutzten Konfigurationsdatei, auf die Client und Server zugreifen. So wird der Preis serverseitig validiert, während der Client dieselben Daten zur Darstellung verwendet. Hier ist eine strukturierte Katalogdefinition:

Config.VehicleCatalog = {
    categories = {
        {id = 'sedan', label = 'Sedans', icon = 'fa-car'},
        {id = 'sport', label = 'Sports Cars', icon = 'fa-flag-checkered'},
        {id = 'suv', label = 'SUVs & Trucks', icon = 'fa-truck'},
        {id = 'muscle', label = 'Muscle Cars', icon = 'fa-bolt'},
        {id = 'motorcycle', label = 'Motorcycles', icon = 'fa-motorcycle'},
    },
    vehicles = {
        -- Sedans
        {model = 'sultan', label = 'Karin Sultan', price = 24500,
         category = 'sedan', seats = 4, testDrive = true},
        {model = 'schafter2', label = 'Benefactor Schafter', price = 38000,
         category = 'sedan', seats = 4, testDrive = true},
        -- Sports
        {model = 'elegy2', label = 'Annis Elegy RH8', price = 95000,
         category = 'sport', seats = 2, testDrive = true},
        {model = 'comet2', label = 'Pfister Comet', price = 110000,
         category = 'sport', seats = 2, testDrive = true},
        -- SUVs
        {model = 'baller', label = 'Gallivanter Baller', price = 55000,
         category = 'suv', seats = 4, testDrive = true},
        -- Muscle
        {model = 'dominator', label = 'Vapid Dominator', price = 42000,
         category = 'muscle', seats = 2, testDrive = true},
        -- Motorcycles
        {model = 'bati', label = 'Pegassi Bati 801', price = 18000,
         category = 'motorcycle', seats = 2, testDrive = true},
    },
}

-- Build lookup table for fast server-side price validation
Config.VehiclePrices = {}
for _, v in ipairs(Config.VehicleCatalog.vehicles) do
    Config.VehiclePrices[v.model] = v.price
end

Die Preissuchtabelle Config.VehiclePrices ermöglicht eine Preisvalidierung auf dem Server mit O(1)-Komplexität und verhindert, dass Clients manipulierte Preise übermitteln. Validiere immer den Modellnamen anhand dieser Tabelle, bevor du einen Kauf verarbeitest. Wenn Serveradministratoren Preise über ein Admin-Panel anpassen können sollen, ohne den Server neu zu starten, solltest du die Fahrzeugpreise statt aus einer Konfigurationsdatei aus einer Datenbanktabelle laden.

Showroom-Vorschau

Im Showroom können Spieler Fahrzeuge vor dem Kauf in einer kontrollierten Umgebung begutachten. Spawne ein Vorschaufahrzeug an einer festgelegten Showroom-Position, richte eine Kamera ein, die der Spieler um das Fahrzeug drehen kann, und zeige neben dem Modell die Fahrzeugdaten an. Das Vorschaufahrzeug sollte nicht interaktiv sein und entfernt werden, wenn der Spieler das Menü schließt oder ein anderes Fahrzeug auswählt. Hier ist die clientseitige Vorschau-Logik:

-- client/showroom.lua
local previewVehicle = nil
local previewCam = nil
local camAngle = 0.0

function ShowVehiclePreview(modelName)
    -- Clean up previous preview
    DestroyPreview()

    local model = GetHashKey(modelName)
    RequestModel(model)
    while not HasModelLoaded(model) do Wait(10) end

    local showroomPos = Config.ShowroomPosition -- vector4
    previewVehicle = CreateVehicle(model, showroomPos.x, showroomPos.y,
        showroomPos.z, showroomPos.w, false, false)

    SetEntityInvincible(previewVehicle, true)
    SetVehicleDoorsLocked(previewVehicle, 2)
    FreezeEntityPosition(previewVehicle, true)
    SetVehicleOnGroundProperly(previewVehicle)
    SetModelAsNoLongerNeeded(model)

    -- Create orbiting camera
    previewCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
    UpdateCameraPosition()
    SetCamActive(previewCam, true)
    RenderScriptCams(true, true, 500, true, true)
end

function UpdateCameraPosition()
    if not previewCam or not previewVehicle then return end
    local vehPos = GetEntityCoords(previewVehicle)
    local radius = 6.0
    local height = 2.0
    local rad = math.rad(camAngle)
    local camX = vehPos.x + radius * math.cos(rad)
    local camY = vehPos.y + radius * math.sin(rad)
    SetCamCoord(previewCam, camX, camY, vehPos.z + height)
    PointCamAtEntity(previewCam, previewVehicle, 0.0, 0.0, 0.0, true)
end

function DestroyPreview()
    if previewVehicle then
        DeleteEntity(previewVehicle)
        previewVehicle = nil
    end
    if previewCam then
        SetCamActive(previewCam, false)
        RenderScriptCams(false, true, 500, true, true)
        DestroyCam(previewCam, false)
        previewCam = nil
    end
end

Ermögliche Kameradrehungen über Mausbewegungen oder Tastatursteuerung, solange der Showroom geöffnet ist. Mit der Orbitkamera können Spieler das Fahrzeug aus allen Winkeln betrachten, ohne darum herumlaufen zu müssen. Ergänze die Vorschau um die Möglichkeit, die Fahrzeugfarbe anzupassen. So sehen Spieler vor dem Kauf ihre gewünschte Lackierung, was spätere Reue und Supportanfragen reduziert.

Kauf- und Transaktionsverarbeitung

Fahrzeugkäufe müssen vollständig serverseitig verarbeitet werden, um Exploits zu verhindern. Der Server prüft, ob sich der Spieler das Fahrzeug leisten kann, zieht den Kaufpreis ab, generiert ein eindeutiges Kennzeichen, erstellt den Fahrzeugeintrag in der Datenbank und weist den Client an, das gekaufte Fahrzeug zu spawnen. Unterstütze sowohl den vollständigen Kauf mit Bargeld als auch Banküberweisungen und binde bei Bedarf dein Banksystem für Überweisungen ein:

RegisterNetEvent('dealer:server:purchaseVehicle', function(modelName, paymentType)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    -- Validate model exists and get price
    local price = Config.VehiclePrices[modelName]
    if not price then
        TriggerClientEvent('QBCore:Notify', src, 'Vehicle not available', 'error')
        return
    end

    -- Check payment
    local moneyType = paymentType == 'bank' and 'bank' or 'cash'
    if Player.PlayerData.money[moneyType] < price then
        TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
        return
    end

    -- Generate unique plate
    local plate = GenerateUniquePlate()

    -- Process payment
    Player.Functions.RemoveMoney(moneyType, price, 'vehicle-purchase-' .. modelName)

    -- Create vehicle record
    local vehicleHash = GetHashKey(modelName)
    MySQL.insert([[
        INSERT INTO player_vehicles
        (citizenid, vehicle, hash, plate, garage, state, fuel, engine, body)
        VALUES (?, ?, ?, ?, ?, 1, 100, 1000.0, 1000.0)
    ]], {
        Player.PlayerData.citizenid,
        modelName,
        tostring(vehicleHash),
        plate,
        'pillboxgarage',
    })

    -- Log transaction
    MySQL.insert([[
        INSERT INTO vehicle_sales (citizenid, vehicle, plate, price, sold_at)
        VALUES (?, ?, ?, ?, NOW())
    ]], {Player.PlayerData.citizenid, modelName, plate, price})

    TriggerClientEvent('QBCore:Notify', src,
        'Vehicle purchased! Plate: ' .. plate, 'success')
    TriggerClientEvent('dealer:client:vehiclePurchased', src, modelName, plate)
end)

function GenerateUniquePlate()
    local plate
    repeat
        plate = ''
        local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
        for i = 1, 8 do
            local idx = math.random(1, #chars)
            plate = plate .. chars:sub(idx, idx)
        end
        local exists = MySQL.scalar.await(
            'SELECT 1 FROM player_vehicles WHERE plate = ?', {plate}
        )
    until not exists
    return plate
end

Die GenerateUniquePlate Die Funktion verwendet eine Wiederholungsschleife, um ein im gesamten Datenbestand eindeutiges Kennzeichen zu garantieren. Bei 8 alphanumerischen Zeichen sind Überschneidungen statistisch selten, die Prüfung gewährleistet jedoch absolute Sicherheit. Protokolliere jeden Verkauf in einer separaten Tabelle, damit Administratoren ihn prüfen und die Serverwirtschaft analysieren können. Erfasse dabei, welche Fahrzeuge am beliebtesten sind und wie viel Geld insgesamt durch den Händler fließt.

Probefahrtsystem

Mit Probefahrten können Spieler ein Fahrzeug vor dem Kauf ausprobieren. Das ist besonders bei teuren Fahrzeugen wichtig. Spawne ein temporäres Fahrzeug mit Zeitlimit und geografischer Begrenzung. Sobald die Zeit abläuft oder der Spieler den erlaubten Bereich verlässt, bringst du ihn automatisch zum Händler zurück. Kennzeichne das Probefahrzeug so, dass es weder in einer Garage abgestellt noch modifiziert werden kann. So verhinderst du, dass Spieler das System ausnutzen, um kostenlose Fahrzeuge zu erhalten:

-- Client: test drive logic
local testDriveVehicle = nil
local testDriveTimer = 0
local testDriveActive = false

function StartTestDrive(modelName, duration)
    local model = GetHashKey(modelName)
    RequestModel(model)
    while not HasModelLoaded(model) do Wait(10) end

    local spawnPos = Config.TestDriveSpawn
    testDriveVehicle = CreateVehicle(model, spawnPos.x, spawnPos.y,
        spawnPos.z, spawnPos.w, true, false)

    SetVehicleNumberPlateText(testDriveVehicle, 'TESTDRVE')
    TaskWarpPedIntoVehicle(PlayerPedId(), testDriveVehicle, -1)
    SetModelAsNoLongerNeeded(model)

    testDriveTimer = duration
    testDriveActive = true

    -- Timer and boundary check thread
    CreateThread(function()
        while testDriveActive and testDriveTimer > 0 do
            Wait(1000)
            testDriveTimer = testDriveTimer - 1

            -- Show remaining time
            SendNUIMessage({
                action = 'updateTestDrive',
                timeLeft = testDriveTimer
            })

            -- Check boundary
            local playerPos = GetEntityCoords(PlayerPedId())
            local dealerPos = Config.DealerLocation
            if #(playerPos - dealerPos) > Config.TestDriveRadius then
                QBCore.Functions.Notify('Too far from dealer, returning...', 'error')
                EndTestDrive()
                return
            end
        end

        if testDriveActive then
            EndTestDrive()
        end
    end)
end

function EndTestDrive()
    testDriveActive = false
    if testDriveVehicle and DoesEntityExist(testDriveVehicle) then
        DeleteEntity(testDriveVehicle)
        testDriveVehicle = nil
    end

    local returnPos = Config.DealerLocation
    SetEntityCoords(PlayerPedId(), returnPos.x, returnPos.y, returnPos.z)
    QBCore.Functions.Notify('Test drive ended', 'info')
end

Die geografische Begrenzung verhindert, dass Spieler mit dem Probefahrzeug über die Karte fahren und es irgendwo zurücklassen. Lege einen angemessenen Radius fest, der eine sinnvolle Fahrt durch die Straßen in der Umgebung erlaubt und gleichzeitig sicherstellt, dass das Fahrzeug wiedergefunden werden kann. Das Kennzeichen "TESTDRVE" zeigt anderen Spielern sichtbar an, dass das Fahrzeug nur vorübergehend zur Verfügung steht.

Finanzierung und Zahlungspläne

Bei teuren Fahrzeugen kannst du eine Finanzierung anbieten, bei der Spieler eine Anzahlung leisten und den Restbetrag über einen bestimmten Zeitraum in Raten zahlen. Erfasse den Kredit in einer Datenbanktabelle mit Restbetrag, Zahlungsplan und Zinssatz. Wenn ein Spieler Zahlungen versäumt, kann das Fahrzeug zur Rücknahme markiert werden. Diese Funktion erweitert die Wirtschaft des Servers und macht hochwertige Fahrzeuge auch für Spieler zugänglich, die noch nicht genug Geld für den vollständigen Kauf angespart haben. Implementiere die Ratenprüfung als serverseitigen wiederkehrenden Job, der täglich anhand der Spielzeit läuft, Zahlungen vom Bankkonto des Spielers abbucht und ihn über jede Belastung benachrichtigt. Reicht das Guthaben nicht aus, erhöhe einen Zähler für versäumte Zahlungen und gib eine Warnung aus. Nach einer konfigurierbaren Anzahl versäumter Zahlungen markierst du das Fahrzeug zur Rücknahme. Es wird dann aus der Garage des Spielers entfernt und dem Händlerbestand wieder hinzugefügt.

Verkäufe durch Mitarbeiter und Provisionsabrechnung

Von Spielern betriebene Händler benötigen Funktionen zur Verwaltung des Verkaufspersonals, zur Leistungserfassung und zur Auszahlung von Provisionen. Wenn ein Mitarbeiter einen Verkauf vermittelt, erhält er einen konfigurierbaren Prozentsatz des Fahrzeugpreises als Provision. Erfasse die Anzahl der Verkäufe, den erzielten Gesamtumsatz und die verdiente Provision jedes Mitarbeiters in einer Datenbanktabelle. Erstelle ein über die Händler-NUI erreichbares Mitarbeiter-Dashboard mit persönlichen Statistiken, einer Übersicht der letzten Verkäufe und einer Rangliste zum Leistungsvergleich innerhalb des Verkaufsteams. Der Händlerbesitzer oder Mitarbeiter mit Manager-Rang sollte über ein Admin-Panel Provisionssätze anpassen, Mitarbeiter hinzufügen oder entfernen und zusammengefasste Verkaufsberichte einsehen können. So wird aus einer statischen Interaktion mit einem NPC ein dynamischer, von Spielern geführter Betrieb mit echten Managementaufgaben und Wettbewerbsanreizen.

Optimierung und Maßnahmen gegen Exploits

Händlerskripte sind gängigen Exploit-Angriffswegen ausgesetzt, gegen die du vorbeugende Maßnahmen ergreifen solltest. Am wichtigsten ist der Schutz vor Preismanipulation, bei der ein modifizierter Client eine Kaufanfrage mit einem niedrigeren Preis sendet. Validiere Preise immer serverseitig anhand des Katalogs und vertraue niemals auf vom Client übermittelte Werte. Begrenze die Häufigkeit von Kauf-Events, um wiederholte Käufe zu verhindern, die Fahrzeuge duplizieren oder Datenbank-Race-Conditions verursachen könnten. Stelle bei der Showroom-Vorschau sicher, dass das Vorschaufahrzeug mit false für den Netzwerkparameter erstellt wird, damit es nur lokal existiert und von anderen Spielern weder betreten noch gestohlen werden kann. Entferne Vorschaufahrzeuge im onResourceStop Handler, damit bei einem Neustart der Resource keine verwaisten Entities zurückbleiben. Wenn dein Server mehrere Händler hat, solltest du den Fahrzeugkatalog im Arbeitsspeicher zwischenspeichern und nur dann neu laden, wenn ein Administrator einen Aktualisierungsbefehl ausführt. So werden wiederholte Lesezugriffe auf die Konfigurationsdatei bei jedem Öffnen der NUI vermieden. Überwache die Kaufprotokolle auf Auffälligkeiten, etwa wenn derselbe Spieler in kurzer Zeit Dutzende Fahrzeuge kauft. Das könnte auf einen Exploit oder eine Geldduplizierung hindeuten und sollte untersucht werden.

Das passende Script

Fertig für deinen Server, Updates inklusive.

Bereit loszulegen?

Hol dir die Scripts in unserem Shop, oder komm auf den Discord für Support, Updates und einen Blick auf das, was kommt.