Wróć do bloga
Tutorial9 min czytania

System zamków do drzwi FiveM: Skryptuj bezpieczne drzwi i klucze

Dodaj zamki do drzwi na dowolnym serwerze FiveM. Konfiguracja, klucze, drzwi tylko dla frakcji i najlepsze skrypty blokad drzwi dla komisariatów policji, domów oraz wnętrz biznesowych.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Dlaczego zamki w drzwiach mają znaczenie w Roleplay

Systemy zamków drzwi to jedne z najbardziej niedocenianych, a zarazem kluczowych elementów każdego poważnego serwera FiveM roleplay. Bez odpowiedniego zarządzania drzwiami gracze mogą bez przeszkód wchodzić do zbrojowni policji, pokojów personelu szpitala i kryjówek gangów, całkowicie psując immersję. Dobrze wdrożony system zamków pozwala właścicielom serwerów kontrolować dostęp do każdego wnętrza na mapie, od budynków rządowych po firmy należące do graczy. Tworzy naturalne granice, które napędzają interakcje roleplay, takie jak wymiana kluczy, scenariusze z użyciem wytrychów i skoordynowane wejścia siłowe. W tym poradniku przejdziemy przez tworzenie kompletnego systemu zamków drzwi w oparciu o ox_doorlock, a następnie rozbudujemy go o własną mechanikę wytrychów, uwierzytelnianie kartami magnetycznymi, grupy drzwi i kontrolę dostępu dla konkretnych biznesów.

Konfiguracja ox_doorlock

Zasób ox_doorlock to najczęściej wybierane rozwiązanie do blokowania drzwi w ekosystemie FiveM, oferujące przejrzyste API, wbudowany interfejs administracyjny do ustawiania drzwi oraz obsługę zarówno podwójnych drzwi, jak i animowanych bram garażowych. Zanim napiszesz własny kod, potrzebujesz solidnych fundamentów. Zainstaluj ox_doorlock wraz z ox_lib, od którego zależy w kwestii komponentów UI i funkcji pomocniczych. Twój server.cfg musi zapewnić uruchamianie zasobów w odpowiedniej kolejności: najpierw baza danych, potem ox_lib, a na końcu ox_doorlock. Po uruchomieniu wbudowana komenda administratorska pozwala podejść do dowolnych drzwi w świecie gry i zarejestrować je jednym kliknięciem.

-- server.cfg load order
ensure oxmysql
ensure ox_lib
ensure ox_doorlock

-- Grant admin access for door placement
add_ace group.admin command.doorlock allow

Po zarejestrowaniu drzwi przez interfejs administratora, ox_doorlock zapisuje każdy wpis w bazie danych ze współrzędnymi, headingiem, hashem modelu i stanem blokady. Możesz odpytywać je i manipulować nimi programowo. Zasób udostępnia zarówno exporty, jak i eventy do przełączania zamków, sprawdzania stanu i zarządzania dostępem. Zrozumienie struktury danych jest kluczowe przed budowaniem na jej podstawie własnych funkcji.

-- Checking if a specific door is locked
local doorId = 1
local isLocked = exports.ox_doorlock:getDoorState(doorId)

-- Toggle a door lock from server side
exports.ox_doorlock:setDoorState(doorId, not isLocked)

-- Listen for door state changes
AddEventHandler('ox_doorlock:stateChanged', function(id, state, source)
    print(('Door %d changed to %s by player %s'):format(id, state and 'locked' or 'unlocked', source))
end)

Tworzenie mechaniki wytrychów

Włamywanie wytrychem dodaje warstwę rozgrywki, która nadaje zamkniętym drzwiom znaczenie dla postaci przestępczych. Zamiast być barierą nie do przejścia, drzwi stają się wyzwaniem opartym na umiejętnościach. Najlepszym podejściem jest minigra wymagająca wyczucia czasu i precyzji, dzięki czemu wynik wydaje się zasłużony, a nie losowy. Użyjemy wbudowanego w ox_lib systemu skillcheck, aby stworzyć mechanikę otwierania wytrychem, która wymaga posiadania przedmiotu lockpick w ekwipunku gracza, wyzwala animację i uruchamia wieloetapowy skillcheck. Nieudany skillcheck powinien nieść za sobą konsekwencje, takie jak złamanie wytrycha lub powiadomienie pobliskiej policji poprzez zgłoszenie dispatch.

-- client/lockpicking.lua
local function AttemptLockpick(doorId)
    local hasLockpick = exports.ox_inventory:Search('count', 'lockpick')
    if hasLockpick < 1 then
        lib.notify({ title = 'No Lockpick', type = 'error' })
        return
    end

    -- Play lockpicking animation
    local ped = PlayerPedId()
    TaskPlayAnim(ped, 'anim@amb@clubhouse@tutorial@bkr_tut_ig3@', 'machinic_loop_mechandlenry', 8.0, -8.0, -1, 1, 0, false, false, false)

    -- Multi-stage skillcheck: easy, medium, hard
    local success = lib.skillCheck({'easy', 'medium', 'hard'}, {'w', 'a', 's', 'd'})

    ClearPedTasks(ped)

    if success then
        TriggerServerEvent('doorlock:lockpick:success', doorId)
        lib.notify({ title = 'Lock Picked', description = 'The door clicks open.', type = 'success' })
    else
        TriggerServerEvent('doorlock:lockpick:fail', doorId)
        lib.notify({ title = 'Failed', description = 'The lockpick snapped.', type = 'error' })
    end
end

Po stronie serwera musisz zweryfikować próbę użycia wytrycha, usunąć wytrych w przypadku niepowodzenia, odblokować drzwi po udanej próbie i opcjonalnie wysłać powiadomienie do policji. Rate limiting jest tutaj ważny, aby zapobiec próbom spamu. Zapisuj cooldown na gracza i na drzwi, aby nie mógł natychmiast ponowić próby po porażce. Handler serwera powinien również sprawdzić, czy gracz faktycznie znajduje się w pobliżu drzwi, które próbuje otworzyć, co zapobiega zdalnym exploitom.

-- server/lockpicking.lua
local cooldowns = {}

RegisterNetEvent('doorlock:lockpick:success', function(doorId)
    local src = source
    local playerCoords = GetEntityCoords(GetPlayerPed(src))

    -- Verify proximity to door (anti-cheat)
    local doorData = exports.ox_doorlock:getDoor(doorId)
    if not doorData then return end
    if #(playerCoords - doorData.coords) > 3.0 then return end

    -- Check cooldown
    local key = ('%s:%s'):format(src, doorId)
    if cooldowns[key] and os.time() - cooldowns[key] < 30 then return end

    exports.ox_doorlock:setDoorState(doorId, 0) -- Unlock
    cooldowns[key] = os.time()

    -- Auto-relock after 60 seconds
    SetTimeout(60000, function()
        exports.ox_doorlock:setDoorState(doorId, 1)
    end)
end)

RegisterNetEvent('doorlock:lockpick:fail', function(doorId)
    local src = source
    exports.ox_inventory:RemoveItem(src, 'lockpick', 1)

    -- Alert police dispatch
    TriggerEvent('dispatch:alert', {
        coords = GetEntityCoords(GetPlayerPed(src)),
        message = 'Attempted break-in reported',
        code = '10-31',
        job = 'police'
    })
end)

Wdrażanie systemu kart dostępu

Karty dostępu zapewniają bardziej uporządkowany mechanizm kontroli dostępu niż zwykłe klucze. Sprawdzają się świetnie w biurowcach, obiektach rządowych i strefach zamkniętych, gdzie dostęp powinien zależeć od określonego poziomu uprawnień. System kart dostępu przypisuje każdorazowo drzwiom poziom bezpieczeństwa od 1 do 5, a gracze muszą posiadać kartę o równym lub wyższym poziomie uprawnień, aby je otworzyć. Tworzy to naturalną hierarchię, w której karta poziomu 3 otwiera wszystkie drzwi poziomów 1, 2 i 3, lecz nie pozwala wejść do stref zastrzeżonych poziomu 4 lub 5. Karty dostępu mogą być wydawane przez systemy prac, znajdowane jako łup lub wytwarzane w ramach systemu progresji.

-- shared/config.lua
Config = {}
Config.KeycardLevels = {
    { name = 'keycard_1', label = 'Green Keycard',  level = 1 },
    { name = 'keycard_2', label = 'Blue Keycard',   level = 2 },
    { name = 'keycard_3', label = 'Yellow Keycard', level = 3 },
    { name = 'keycard_4', label = 'Red Keycard',    level = 4 },
    { name = 'keycard_5', label = 'Black Keycard',  level = 5 },
}

Config.DoorSecurity = {
    [10] = { level = 1, name = 'Office Lobby' },
    [11] = { level = 2, name = 'Server Room' },
    [12] = { level = 3, name = 'Executive Floor' },
    [13] = { level = 4, name = 'Vault Anteroom' },
    [14] = { level = 5, name = 'Main Vault' },
}

-- client/keycard.lua
local function TryKeycardAccess(doorId)
    local security = Config.DoorSecurity[doorId]
    if not security then return false end

    for _, card in ipairs(Config.KeycardLevels) do
        if card.level >= security.level then
            local count = exports.ox_inventory:Search('count', card.name)
            if count > 0 then
                -- Play card swipe animation
                lib.requestAnimDict('anim@heists@keycard@')
                TaskPlayAnim(PlayerPedId(), 'anim@heists@keycard@', 'exit', 5.0, 1.0, -1, 16, 0, 0, 0, 0)
                Wait(1200)
                ClearPedTasks(PlayerPedId())
                return true, card
            end
        end
    end
    return false
end

Grupy drzwi i dostęp biznesowy

Zarządzanie pojedynczymi drzwiami staje się nieskalowalne, gdy twój serwer ma setki zablokowanych drzwi. Grupy drzwi rozwiązują ten problem, pozwalając przypisać wiele drzwi do nazwanej grupy i kontrolować dostęp do całej grupy naraz. Posterunek policji może mieć 15 zablokowanych drzwi, do których dostęp powinna mieć praca policjanta. Zamiast konfigurować każde drzwi z osobna, przypisujesz je wszystkie do grupy "police_station" i przyznajesz dostęp na podstawie nazwy frakcji. Gdy administrator zatrudni nowego funkcjonariusza, zyskuje on automatycznie dostęp do wszystkich drzwi w grupie bez żadnej ręcznej konfiguracji.

-- server/door_groups.lua
local DoorGroups = {
    police_station = {
        doors = {1, 2, 3, 4, 5, 6, 7, 8},
        access = {
            { type = 'job', name = 'police', minGrade = 0 },
            { type = 'job', name = 'sheriff', minGrade = 0 },
        }
    },
    pillbox_hospital = {
        doors = {20, 21, 22, 23, 24},
        access = {
            { type = 'job', name = 'ambulance', minGrade = 0 },
            { type = 'job', name = 'doctor', minGrade = 2 },
        }
    },
    vangelico = {
        doors = {30, 31},
        access = {
            { type = 'job', name = 'jeweler', minGrade = 0 },
            { type = 'item', name = 'vangelico_key' },
        }
    },
}

function HasGroupAccess(source, groupName)
    local group = DoorGroups[groupName]
    if not group then return false end

    for _, rule in ipairs(group.access) do
        if rule.type == 'job' then
            local job = GetPlayerJob(source)
            if job and job.name == rule.name and job.grade >= (rule.minGrade or 0) then
                return true
            end
        elseif rule.type == 'item' then
            local count = exports.ox_inventory:GetItem(source, rule.name, nil, true)
            if count and count > 0 then return true end
        end
    end
    return false
end

Drzwi firmowe dodają kolejny wymiar, łącząc dostęp do drzwi z danymi własności. Gdy gracz kupi biznes, powinien automatycznie uzyskać kontrolę nad wszystkimi drzwiami przypisanymi do tej nieruchomości. System musi sprawdzać własność biznesu w czasie rzeczywistym, ponieważ nieruchomości mogą być sprzedawane lub przekazywane. Zaimplementuj callback odpytujący tabelę biznesów w celu weryfikacji obecnego właściciela, a następnie buforuj wynik z krótkim czasem TTL, aby nie przeciążać bazy danych przy każdej interakcji z drzwiami.

Synchronizacja stanów drzwi między klientami

Synchronizacja drzwi to jeden z trudniejszych aspektów systemu zamków drzwi. Gdy jeden gracz odblokuje drzwi, każdy gracz w pobliżu musi widzieć je otwarte. Natywne mechanizmy kontroli drzwi FiveM działają dla każdego klienta osobno, co oznacza, że każdy klient niezależnie zarządza stanami drzwi. Bez właściwej synchronizacji jeden gracz widzi drzwi otwarte, podczas gdy inny widzi je zamknięte. ox_doorlock obsługuje podstawową synchronizację stanu, ale niestandardowe rozszerzenia muszą uważać na propagację stanu. Używaj state bags lub autorytatywnych zdarzeń serwerowych do rozsyłania zmian stanu drzwi do wszystkich graczy w rozsądnym zasięgu. Unikaj synchronizacji z całym serwerem przy każdym przełączeniu drzwi, ponieważ generuje to niepotrzebny narzut sieciowy na dużych serwerach z setkami drzwi.

-- server: broadcast door state to nearby players only
local function SyncDoorToNearby(doorId, state, coords, range)
    range = range or 100.0
    local players = GetPlayers()
    for _, playerId in ipairs(players) do
        local ped = GetPlayerPed(playerId)
        if ped and DoesEntityExist(ped) then
            local playerCoords = GetEntityCoords(ped)
            if #(playerCoords - coords) <= range then
                TriggerClientEvent('doorlock:sync', tonumber(playerId), doorId, state)
            end
        end
    end
end

-- client: apply synced door state
RegisterNetEvent('doorlock:sync', function(doorId, state)
    local door = exports.ox_doorlock:getDoor(doorId)
    if door then
        door.state = state
    end
end)

Kwestie bezpieczeństwa i anti-exploit

Systemy zamków drzwi są częstym celem cheaterów, ponieważ ominięcie drzwi często daje dostęp do zastrzeżonych przedmiotów, broni lub pieniędzy. Każde żądanie przełączenia drzwi musi być weryfikowane po stronie serwera. Nigdy nie ufaj identyfikatorom drzwi przesyłanym przez klienta bez sprawdzenia odległości gracza i jego uprawnień dostępu. Wdróż logowanie każdej zmiany stanu drzwi, aby administratorzy mogli przeglądać podejrzane schematy, na przykład gracza odblokowującego drzwi do skarbca, do których nie powinien mieć dostępu. Zastosuj ograniczenie częstotliwości (rate limit) dla wszystkich interakcji z drzwiami, aby zapobiec próbom siłowym (brute-force) w systemie wytrychów. Rozważ dodanie rejestratora zdarzeń po stronie serwera, który zapisuje Steam ID gracza, identyfikator drzwi, znacznik czasu oraz metodę dostępu do celów audytu. Te logi stają się nieocenione podczas badania exploitów lub rozwiązywania sporów między graczami o to, kto uzyskał do czegoś dostęp i kiedy.

Łącząc wszystko w całość

Gotowy do wdrożenia na produkcji system blokady drzwi łączy wszystkie te komponenty w ujednolicony framework. Punktem wejścia jest interakcja z systemem targetu, ox_target lub qb-target, który wykrywa spojrzenie gracza na zarejestrowane drzwi i wyświetla opcje kontekstowe w oparciu o poziom uprawnień. Jeśli gracz posiada dostęp z tytułu pracy, pokaż przycisk odblokowania. Jeśli ma kartę dostępu, pokaż opcję przeciągnięcia karty. Jeśli ma wytrych, a drzwi są oznaczone jako podatne na otwarcie wytrychem, pokaż opcję wytrychu. Każda ścieżka kieruje do odpowiedniego handlera, który przeprowadza walidację po stronie serwera i synchronizuje wynik z pobliskimi klientami. Konfiguracja powinna opierać się na danych za pośrednictwem wpisów w bazie danych lub wspólnych plików konfiguracyjnych, aby właściciele serwerów mogli dodawać, usuwać i modyfikować dostęp do drzwi bez ingerencji w kod. Ta modułowa architektura oznacza, że możesz wymienić minigrę wytrycha, dodać nowe metody dostępu, np. skanery biometryczne, lub zintegrować całość z zupełnie innymi systemami ekwipunku bez konieczności przepisywania logiki bazowej.

Pasujący skrypt

Gotowe na twój serwer, aktualizacje w zestawie.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.