BeamMP nemá žádné herní režimy. Server BeamMP přenáší stav vozidel a spouští Lua a všechno, co z něj dělá víc než parkoviště - závody, whitelisty, pravidla pro vozidla, chatové příkazy, propojení s Discordem - je plugin v Lua ve složce Resources/Server. Serverové API je dost malé na to, aby ses ho naučil za jeden večer: zaregistruješ obslužné funkce pro tucet událostí, zavoláš pár desítek funkcí MP.* a pro cokoli, co probíhá v čase, použiješ časovače. To stačí i na skutečné závody, protože server umí přečíst poslední hlášenou pozici každého vozidla přes MP.GetPositionRaw, a víc systém checkpointů nepotřebuje. Tento návod pokrývá model pluginů, události a funkce, které opravdu použiješ, a kompletní plugin pro závody s odpočtem, branami, koly a cílovými časy.
Návod na server BeamMP pokrývá instalaci, ServerConfig.toml, mapy a klientské mody; pokud ti server ještě neběží, přečti si ho jako první. Vše tady míří na server verze 3.x, který používá Lua 5.3. Starší pluginy psané pro API 2.x nebo pro dávno opuštěné komunitní frameworky se často nenačtou, a to je první věc, kterou zkontroluj, když stažený skript nic nedělá.
Jak se serverové pluginy načítají#
Plugin je složka pod Resources/Server. Server načte každý soubor .lua v kořeni té složky, v abecedním pořadí, a jednou spustí kód na nejvyšší úrovni. Soubory v podsložkách se ignorují, pokud je nezavoláš přes require, což je správné místo pro pomocné funkce a data.
Resources/ Client/ mod zips sent to players Server/ RaceControl/ main.lua loaded at start lib/gates.lua only if main.lua requires it Whitelist/ main.luaKaždý plugin běží ve vlastním stavu Lua. Dva pluginy si nemohou přečíst navzájem proměnné; komunikují přes události, o kterých je řeč níže. Uvnitř pluginu je vzorec vždy stejný: definuješ globální funkce a potom je zaregistruješ k názvům událostí přes MP.RegisterEvent(eventName, functionName). Druhý argument je název globální funkce jako řetězec, ne funkce samotná, a proto obslužná funkce deklarovaná jako local tiše nikdy nezabere.
Server také sleduje složku s pluginy. Uložení souboru v kořeni pluginu spustí onFileChanged a znovu načte stav toho pluginu, takže můžeš iterovat bez restartu serveru. Opětovné načtení smaže proměnné pluginu, takže rozjetý závod zmizí, když uložíš main.lua; upravuj mezi závody, ne během nich.
Výstup jde do konzole serveru. print zapisuje s předponou [LUA] a Util.LogInfo, Util.LogWarn, Util.LogError a Util.LogDebug zapisují na odpovídající úrovni. Syntaktická chyba se ohlásí při načítání se souborem a řádkem a plugin se přeskočí. Pokud plugin „nic nedělá“, odpověď je v konzoli od okamžiku načítání - jak ji číst efektivně, popisuje čtení konzole.
Události, které budeš používat#
| Událost | Argumenty | Lze zrušit |
|---|---|---|
onInit | žádné | Ne |
onPlayerAuth | name, role, is_guest, identifiers | Ano - vrať 1 nebo řetězec s důvodem |
onPlayerConnecting | player_id | Ne |
onPlayerJoining | player_id | Ne |
onPlayerJoin | player_id | Ne |
onPlayerDisconnect | player_id | Ne |
onChatMessage | player_id, player_name, message | Ano - vrať 1, aby se zpráva skryla |
onVehicleSpawn | player_id, vehicle_id, data | Ano - vrať 1, aby se spawn odmítl |
onVehicleEdited | player_id, vehicle_id, data | Ano - vrať 1, aby se úprava odmítla |
onVehicleReset | player_id, vehicle_id, data | Ne |
onVehicleDeleted | player_id, vehicle_id | Ne |
onConsoleInput | input | Ne |
onShutdown | žádné | Ne |
Tři z nich odvedou většinu práce. onPlayerAuth běží dřív, než hráč dostane ID, takže tam patří whitelisty a bany; dostane tabulku identifiers s ID beammp hráče, jeho IP a, pokud je propojený, i s ID na Discordu. onChatMessage je způsob, jakým se implementuje každý chatový příkaz - žádný registr příkazů neexistuje, text parsuješ sám. onVehicleSpawn a onVehicleEdited jsou místa pro pravidla o vozidlech: argument data obsahuje konfiguraci vozidla jako JSON, takže můžeš odmítnout model, díl nebo spawn mimo boxy.
Rušení se dělá návratovou hodnotou, a jen jí. Vrácení 1 z onChatMessage skryje zprávu všem, což u příkazů chceš - nikdo nepotřebuje vidět /start v chatu. Nevrátíš-li nic, zpráva projde.
Funkce, které stojí za to znát#
| Funkce | Co dělá |
|---|---|
MP.SendChatMessage(id, text) | Chat jednomu hráči, nebo všem pomocí -1 |
MP.GetPlayers() | Tabulka ID hráče na jméno |
MP.GetPlayerName(id) | Jméno podle ID |
MP.GetPlayerIdentifiers(id) | beammp, ip a discord, pokud jsou známé |
MP.IsPlayerGuest(id) | Zda hráč nemá účet BeamMP |
MP.GetPlayerVehicles(id) | Tabulka ID vozidla na data vozidla |
MP.GetPositionRaw(id, vid) | Poslední hlášené pos, rot, vel, rvel, ping a tim |
MP.RemoveVehicle(id, vid) | Smaže vozidlo |
MP.DropPlayer(id, reason) | Vyhodí se zprávou |
MP.CreateEventTimer(name, ms) | Spouští událost name každých ms milisekund |
MP.CancelEventTimer(name) | Zastaví ho |
MP.TriggerLocalEvent(name, ...) | Zavolá obslužné funkce v tomto pluginu, synchronně |
MP.TriggerGlobalEvent(name, ...) | Zavolá obslužné funkce ve všech pluginech |
MP.TriggerClientEvent(id, name, data) | Pošle řetězec klientskému modu |
MP.Set(MP.Settings.MaxCars, n) | Změní hodnotu konfigurace do restartu |
MP.CreateTimer() | Stopky s :Start() a :GetCurrent() v sekundách |
Pozice jsou vektory v polích Lua, takže raw.pos[1], raw.pos[2] a raw.pos[3] jsou x, y a z. BeamNG používá z jako osu nahoru, takže rovina země je x a y. Util.JsonEncode a Util.JsonDecode převádějí tabulky na řetězce a zpět, což potřebuješ jak pro ukládání dat, tak pro komunikaci s klientskými mody. FS.Exists, FS.CreateDirectory a FS.ConcatPaths řeší soubory a cesty přenositelně.
Signatury se mezi verzemi serveru mění a oficiální reference ke skriptování pro verzi, kterou provozuješ, je lepší než jakákoli tabulka na blogu, včetně té tady.
Chatové příkazy a admini#
Zabudovaný seznam adminů neexistuje, takže o tom, kdo je admin, rozhoduje plugin. Postav to na identifikátoru beammp, nikdy na zobrazovaném jménu, které hráči mohou měnit:
local ADMINS = { ["1234567"] = true }local function isAdmin(pid) local ids = MP.GetPlayerIdentifiers(pid) return ids ~= nil and ADMINS[tostring(ids.beammp)] == trueendfunction AdminChat(pid, name, msg) local cmd, arg = msg:match("^/(%S+)%s*(.*)$") if not cmd then return end if not isAdmin(pid) then return end if cmd == "kick" then for id, pname in pairs(MP.GetPlayers()) do if pname == arg then MP.DropPlayer(id, "Kicked by " .. name) end end elseif cmd == "cars" then MP.Set(MP.Settings.MaxCars, tonumber(arg) or 1) MP.SendChatMessage(-1, "Car limit is now " .. arg) else return end return 1endMP.RegisterEvent("onChatMessage", "AdminChat")Vzor ^/(%S+)%s*(.*)$ rozdělí /kick SomeName na příkaz a zbytek řádku. Neznámé příkazy projdou dál a objeví se v chatu, což je přívětivější způsob selhání. MP.Set mění nastavení jen pro běžící server; hodnota v ServerConfig.toml se vrátí při dalším restartu, což je ideální pro večerní akce.
Pravidla pro vozidla při spawnu#
Argument data události onVehicleSpawn je řetězec s předponou před JSONem, takže před dekódováním odřízni od první složené závorky. Než začneš psát pravidla, zaloguj jeden spawn - názvy polí pocházejí ze hry a nejbezpečnější způsob, jak je poznat, je podívat se:
local ALLOWED = { covet = true, sunburst = true }function CarSpawn(pid, vid, data) local start = data:find("{") if not start then return end local cfg = Util.JsonDecode(data:sub(start)) print("spawn by " .. MP.GetPlayerName(pid) .. ": " .. tostring(cfg.jbm)) if cfg.jbm and not ALLOWED[cfg.jbm] then MP.SendChatMessage(pid, "Only the race cars are allowed tonight.") return 1 endendMP.RegisterEvent("onVehicleSpawn", "CarSpawn")MP.RegisterEvent("onVehicleEdited", "CarSpawn")Zaregistruj stejnou obslužnou funkci i na onVehicleEdited, jinak hráči zaspawnují povolené auto a pak ho v konfigurátoru vozidla vymění. Odmítnutý spawn nezanechá nic na obrazovkách ostatních hráčů; auto odstraněné dodatečně přes MP.RemoveVehicle se nejdřív na okamžik objeví, takže kdykoli to jde, odmítej už při spawnu.
Plugin pro závody s checkpointy#
Závody potřebují čtyři věci: seznam účastníků, odpočet, způsob, jak poznat, že auto projelo bodem, a hodiny. Server má všechny čtyři. Brány jsou body na mapě s poloměrem; každý jezdec je musí projet v pořadí a projetí poslední brány dokončí kolo. Pozice se čtou na časovači.
local ADMINS = { ["1234567"] = true }local GATES = { -- fill in with /pos, finish line last { x = 0.0, y = 0.0, r = 15 }, { x = 0.0, y = 0.0, r = 15 },}local LAPS = 3local race = { state = "idle", count = 0, racers = {}, finished = 0 }local clock = MP.CreateTimer()local function say(pid, text) MP.SendChatMessage(pid, "[Race] " .. text) endlocal function isAdmin(pid) local ids = MP.GetPlayerIdentifiers(pid) return ids ~= nil and ADMINS[tostring(ids.beammp)] == trueendlocal function firstVehicle(pid) local vehicles = MP.GetPlayerVehicles(pid) if vehicles == nil then return nil end for vid, _ in pairs(vehicles) do return vid endendlocal function reset() MP.CancelEventTimer("RaceCountdown") MP.CancelEventTimer("RaceTick") race = { state = "idle", count = 0, racers = {}, finished = 0 }endfunction RaceChat(pid, name, msg) local cmd = msg:match("^/(%S+)") if cmd == "join" and race.state == "idle" then race.racers[pid] = { gate = 1, lap = 1, done = false } say(-1, name .. " is on the grid") elseif cmd == "pos" then local vid = firstVehicle(pid) local raw = vid and MP.GetPositionRaw(pid, vid) if raw and raw.pos then say(pid, string.format("x=%.1f y=%.1f", raw.pos[1], raw.pos[2])) end elseif cmd == "start" and isAdmin(pid) and race.state == "idle" then race.state = "countdown" race.count = 5 MP.CreateEventTimer("RaceCountdown", 1000) elseif cmd == "stop" and isAdmin(pid) then reset() say(-1, "Race cancelled") else return end return 1endfunction RaceCountdown() if race.count > 0 then say(-1, tostring(race.count)) race.count = race.count - 1 return end MP.CancelEventTimer("RaceCountdown") race.state = "running" clock:Start() say(-1, "GO") MP.CreateEventTimer("RaceTick", 200)endfunction RaceTick() for pid, r in pairs(race.racers) do local vid = firstVehicle(pid) local raw = vid and MP.GetPositionRaw(pid, vid) if raw and raw.pos and not r.done then local g = GATES[r.gate] local dx, dy = raw.pos[1] - g.x, raw.pos[2] - g.y if dx * dx + dy * dy <= g.r * g.r then r.gate = r.gate + 1 if r.gate > #GATES then r.gate = 1 if r.lap >= LAPS then r.done = true race.finished = race.finished + 1 say(-1, string.format("P%d %s %.2fs", race.finished, MP.GetPlayerName(pid), clock:GetCurrent())) else r.lap = r.lap + 1 end end end end endendfunction RaceLeave(pid) race.racers[pid] = nil endMP.RegisterEvent("onChatMessage", "RaceChat")MP.RegisterEvent("RaceCountdown", "RaceCountdown")MP.RegisterEvent("RaceTick", "RaceTick")MP.RegisterEvent("onPlayerDisconnect", "RaceLeave")Nastavení: projeď se ke každému bodu trati - o pár set metrů od sebe, v zatáčkách, které se nedají zkrátit - a napiš /pos. Zkopíruj x a y do GATES v pořadí jízdy, přičemž čára start-cíl je poslední položka, aby auta opouštějící startovní rošt mířila nejdřív k první bráně. Ulož soubor, plugin se znovu načte a jezdci napíšou /join dřív, než admin napíše /start.
Jak je to přesné, upřímně
Server ví jen to, kde bylo auto při své poslední aktualizaci, takže přesnost závisí na dvou věcech. Čtení pozic každých 200 ms znamená, že auto jedoucí 200 km/h se mezi kontrolami posune asi o 11 metrů, proto je poloměr brány 15 metrů a ne tři. A každá pozice je stará tak, jak velké je síťové zpoždění daného hráče, takže jezdec s pingem 150 ms je měřen o něco později. V praxi jde o pár desetin sekundy, což je v pořádku pro nedělní závod pro zábavu, ale ne pro šampionát rozhodnutý setinami.
Dva způsoby, jak to zpřesnit. Snížení ticku na 100 ms zmenší problém se vzdáleností na polovinu a zdvojnásobí práci; s hrstkou jezdců je ta cena zanedbatelná, se čtyřiceti ne. Pro skutečnou přesnost musí detekce probíhat na počítači každého hráče: klientský mod v Resources/Client lokálně sleduje průjezdy branami a hlásí je přes TriggerServerEvent a serverový plugin je přijímá přes MP.RegisterEvent s obslužnou funkcí, která bere ID hráče a datový řetězec. Je to víc práce a potřebuje klientský mod, který udržuješ, a proto většina komunitních závodních serverů používá pro nenáročné akce jednodušší serverový přístup a pro ligy specializovaný měřicí mod.
Aby závody působily jako závody
Pár doplňků, které stojí málo:
- Zamkni startovní pole před závodem nastavením
MaxCarsna1přesMP.Set, aby nikdo uprostřed závodu nespawnul druhé auto, a po závodě hodnotu vrať. - Zakaž resety během závodu.
onVehicleResetnejde zrušit, ale jde diskvalifikovat: pokud jezdec resetuje, zatímco jerace.staterovnorunning, nastavdonea oznam to. - Oznamuj výsledky na Discord zápisem do souboru, který čte samostatný bot, nebo pluginem určeným pro webhooky - přijímací stranu popisuje webhooky Discordu pro stav serveru.
- Drž rozložení jako data. Dej brány každé trati do vlastního souboru pod
lib/a zavolej správný přesrequire, aby změna místa konání neznamenala úpravu pluginu.
Ukládání dat mezi restarty#
Proměnné pluginu zanikají se serverem. Cokoli, co má přežít - rekordy kol, seznam banů, historie závodů - se musí zapsat na disk. Stačí knihovna io z Lua a Util.JsonEncode:
local DIR = "Resources/Server/RaceControl/data"local FILE = FS.ConcatPaths(DIR, "records.json")local function loadRecords() if not FS.Exists(FILE) then return {} end local f = io.open(FILE, "r") local text = f:read("*a") f:close() return Util.JsonDecode(text) or {}endlocal function saveRecords(records) if not FS.Exists(DIR) then FS.CreateDirectory(DIR) end local f = io.open(FILE, "w") f:write(Util.JsonEncode(records)) f:close()endCesty jsou relativní k pracovnímu adresáři serveru. Zápis souboru do kořenové složky pluginu může spustit hot reload toho pluginu, který smaže závod, jenž právě zapsal rekord - proto data ukládej do podsložky, například data/, jako výše, kterou server nenačítá jako kód pluginu. To je nejzáhadnější chyba při vývoji pluginů pro BeamMP a vypadá, jako by server plugin náhodně restartoval.
Tyto datové soubory jsou část tvého serveru, kterou si znovu nestáhneš. Zálohuj Resources/Server před každým editačním sezením a před každou aktualizací serveru; zálohy, které opravdu obnovíš je argument pro to, abys nějakou skutečně vyzkoušel.
Komunikace mezi pluginy a s klientskými mody#
Pluginy jsou izolované, takže whitelistový plugin nemůže zavolat funkci v pluginu pro závody. Spojují je události: MP.TriggerGlobalEvent("RaceFinished", pid, time) v jednom pluginu zavolá všechny obslužné funkce registrované pro RaceFinished ve všech pluginech. MP.TriggerLocalEvent dělá totéž v rámci jednoho pluginu a vrací se synchronně, což se hodí při dělení velkého pluginu na soubory.
Klientské mody jsou opačný směr. Zip v Resources/Client může obsahovat Lua, které běží v hře každého hráče, registruje obslužné funkce přes AddEventHandler a posílá data přes TriggerServerEvent. Server posílá přes MP.TriggerClientEvent(pid, name, data) nebo MP.TriggerClientEventJson pro tabulku. Všechno putuje jako řetězce, takže tabulky na obou koncích kóduj jako JSON. Využij to pro uživatelské rozhraní - odpočet na obrazovce místo v chatu, měřič kol, žebříček - a autoritu nech na serveru: klientský mod, který hlásí vlastní časy kol, si hráč, který ho spouští, může upravit.
Bezpečnost, výkon a udržení funkčních pluginů#
Pluginy běží s plnými oprávněními serveru a vidí každý řádek chatu i každé připojení. Cokoli stáhneš, si přečti dřív, než to vložíš do složky, která se načítá automaticky, a upřednostňuj pluginy s veřejným repozitářem se zdrojovým kódem. Obecnou hygienu popisuje udržování modovaného serveru čistého a co všechno umí nepřátelský plugin, ukazuje škodlivé pluginy a mody.
Problémy s výkonem v BeamMP jsou téměř vždy obslužná funkce, která dělá příliš mnoho příliš často. Časovač po 50 ms, který prochází každého hráče a dekóduje JSON, se projeví jako zátěž CPU, kterou nečekáš. Util.DebugExecutionTime() vrací statistiky časování jednotlivých obslužných funkcí, což z odhadu udělá číslo. Nikdy nevolej MP.Sleep na dlouho uvnitř obslužné funkce; blokuje ten plugin.
Na RE:NODE ti tarif BeamMP dává konzoli s historií pro čtení výstupu pluginů, editor správce souborů se zvýrazněním syntaxe pro main.lua a SFTP, pokud raději pracuješ lokálně. Uložení souboru pluginu ho hot-reloaduje, takže cyklus úprav a testování nepotřebuje restart, a slot zálohy vytvořený před editačním sezením je tlačítko zpět pro celou složku Resources.
Po aktualizaci serveru BeamMP si přečti changelog kvůli změnám ve skriptování dřív, než restartuješ server plný pluginů. Pokud to zjistíš po špatné zkušenosti, postup obnovy popisuje co dělat, když aktualizace modu něco rozbije.
Řešení problémů#
Obslužná funkce se nikdy nespustí. Funkce je local, má překlep v MP.RegisterEvent, nebo má název události špatná velká a malá písmena. Názvy událostí rozlišují velikost písmen.
Plugin se náhodně resetuje. Zapisuje soubor do vlastní kořenové složky a tím spouští hot reload. Přesuň data do podsložky.
`attempt to index a nil value` u pozic. Hráč nemá vozidlo, nebo MP.GetPositionRaw vrátilo prázdný výsledek. Před použitím zkontroluj raw a raw.pos.
Příkazy se objevují v chatu. Obslužná funkce nevrací 1 pro zpracované příkazy.
Brány se nikdy nespustí. Souřadnice byly zkopírovány jako x a z místo x a y, nebo je poloměr menší než vzdálenost, kterou auto urazí mezi čteními.
Starý plugin nic nedělá. Míří na API 2.x. Hledej MP.RegisterEvent s názvy obslužných funkcí jako řetězci; cokoli jiného je potřeba přepsat.
FAQ#
Má BeamMP vestavěné závodní režimy?
Ne. Server nemá žádné herní režimy. Závody, měření času a žebříčky jsou pluginy v Lua, buď napsané tebou, nebo stažené, někdy s klientským modem pro zobrazení času na obrazovce.
Jakou verzi Lua používá server BeamMP?
Lua 5.3 na serveru 3.x. Pluginy psané pro staré API 2.x je potřeba přepsat, hlavně na obslužné funkce MP.RegisterEvent předávané názvem a na současné názvy událostí.
Musí hráči pro serverové pluginy něco instalovat?
Ne pro serverové pluginy v Resources/Server. K hráčům se dostanou jen klientské mody v Resources/Client a ty se při připojení stáhnou automaticky.
Jak přesné je serverové měření kol?
V rozmezí několika desetin sekundy, omezené intervalem čtení a pingem každého hráče. Na nenáročné závody to stačí. Soutěžní měření potřebuje detekci na klientovi, hlášenou serveru.
Dá se pluginem přidat heslo?
Pole pro heslo neexistuje, ale v onPlayerAuth můžeš odmítnout každého, jehož identifikátor beammp není na tvém seznamu, což je přísnější než heslo, protože se nedá sdílet.
Musím po úpravě pluginu restartovat server?
Ne. Uložení souboru v kořenové složce pluginu ten plugin znovu načte. Jeho proměnné se resetují, takže to dělej mezi akcemi, ne během nich.




Komentáře
Zcela anonymně: žádný účet, žádný e-mail, žádné cookie. Ukládáme jen jméno, které napíšeš, text a čas - nic dalšího. Počet odkazů je omezený a značkování se nevykresluje.