În [lecția trecută](/blog/npc-eveniment-metin2/) am făcut un NPC care pornește „Vânătoarea de lupi”. Problema: evenimentul merge până când cineva își amintește să-l oprească. În lecția asta îi dăm un **ceas**: pornește pentru 30 de minute, anunță cu 5 minute înainte de final și se oprește singur. Și, la fel de important, vedem ce se întâmplă cu acest ceas dacă serverul repornește în mijlocul evenimentului.
server_timer("nume", secunde) programează un bloc when nume.server_timer care rulează o singură dată, după numărul de secunde dat. Timerele trăiesc doar în memoria canalului pe care au fost pornite, deci se pierd la restart. De aceea salvăm și ora de final într-un event flag și o verificăm direct în efectul evenimentului.Cuvinte pe care le vei întâlni
| Cuvânt | Ce înseamnă |
|---|---|
| Timer | Un „cronometru de bucătărie”: îl pornești acum, sună peste X secunde și atunci rulează un bloc din quest. |
server_timer | Timer al serverului, nelegat de un jucător. Merge și dacă nu e nimeni conectat. |
timer | Timer legat de un jucător. Se oprește dacă jucătorul iese din joc. |
| Core | Un proces game care rulează un canal (sau o parte din el). Fiecare are memoria lui. |
Ce timere există în pachetul de referință
Toate funcțiile de mai jos apar în quest_functions:
| Funcție | Pentru ce | Ce se întâmplă la ieșire sau restart |
|---|---|---|
timer("nume", secunde) | Ceva legat de un jucător, de exemplu „peste 10 secunde, teleportează-l” | Se oprește când jucătorul iese |
loop_timer("nume", secunde) | La fel, dar se repetă | La fel |
server_timer("nume", secunde) | Ceva pentru tot serverul, o singură dată | Se pierde la restartul acelui core |
server_loop_timer("nume", secunde) | La fel, dar se repetă | La fel |
clear_server_timer("nume", 0) | Anulează un server_timer | — |
cleartimer("nume") | Anulează un timer al jucătorului | — |
Pentru un eveniment global folosim server_timer. În sursa verificată, timpul e în secunde.
Codul complet
Este versiunea a doua a fișierului eveniment_lupi.quest din lecția trecută. Înlocuiește-l pe cel vechi (după backup).
quest eveniment_lupi begin
state start begin
when 20011.chat."GM: Starea vanatorii" with pc.is_gm() begin
say_title("Vanatoarea de lupi")
local ramas = game.get_event_flag("ev_lupi_final") - get_time()
say(string.format("ev_lupi = %d", game.get_event_flag("ev_lupi")))
if ramas > 0 then
say(string.format("Se termina in %d minute.", math.ceil(ramas / 60)))
elseif game.get_event_flag("ev_lupi") == 1 then
say("Timpul a expirat, dar flag-ul e inca 1. Apasa Opreste.")
end
end
when 20011.chat."GM: Porneste vanatoarea (30 min)" with pc.is_gm() and game.get_event_flag("ev_lupi") == 0 begin
say_title("Vanatoarea de lupi")
say("Pornesti evenimentul pe 30 de minute, pe TOATE canalele?")
local alegere = select("Da, porneste", "Nu")
if alegere ~= 1 then
return
end
if game.get_event_flag("ev_lupi") ~= 0 then
say("Evenimentul e deja pornit.")
return
end
game.set_event_flag("ev_lupi", 1)
game.set_event_flag("ev_lupi_final", get_time() + 1800)
notice_all("Vanatoarea de lupi a inceput! Dureaza 30 de minute.")
server_timer("ev_lupi_5min", 1500)
end
when ev_lupi_5min.server_timer begin
notice_all("Vanatoarea de lupi se termina in 5 minute!")
server_timer("ev_lupi_stop", 300)
end
when ev_lupi_stop.server_timer begin
game.set_event_flag("ev_lupi", 0)
notice_all("Vanatoarea de lupi s-a terminat. Multumim!")
end
when 20011.chat."GM: Opreste vanatoarea" with pc.is_gm() and game.get_event_flag("ev_lupi") == 1 begin
say_title("Vanatoarea de lupi")
local alegere = select("Da, opreste", "Nu")
if alegere == 1 then
clear_server_timer("ev_lupi_5min", 0)
clear_server_timer("ev_lupi_stop", 0)
game.set_event_flag("ev_lupi", 0)
notice_all("Vanatoarea de lupi a fost oprita.")
end
end
when 102.kill with game.get_event_flag("ev_lupi") == 1 and get_time() < game.get_event_flag("ev_lupi_final") begin
if number(1, 100) <= 5 then
pc.give_item2(30028, 1)
end
end
end
endExplicat rând cu rând
Ce e nou față de lecția trecută:
| Rând | Ce înseamnă |
|---|---|
game.set_event_flag("ev_lupi_final", get_time() + 1800) | Salvăm ora de final: acum plus 1.800 de secunde (30 de minute). Fiind event flag, rămâne salvată și după restart. |
server_timer("ev_lupi_5min", 1500) | Pornim cronometrul: peste 1.500 de secunde (25 de minute) va rula blocul ev_lupi_5min. |
when ev_lupi_5min.server_timer begin | Blocul care rulează când „sună” cronometrul ev_lupi_5min. |
notice_all("... in 5 minute!") | Anunțul de avertizare, pentru toți. |
server_timer("ev_lupi_stop", 300) | Pornim al doilea cronometru: încă 5 minute până la final. |
when ev_lupi_stop.server_timer begin | La final: punem flag-ul la 0 și anunțăm. |
clear_server_timer("ev_lupi_5min", 0) | La oprirea manuală, anulăm cronometrele rămase, ca să nu apară anunțuri după oprire. Al doilea argument (0) e un număr suplimentar pe care nu-l folosim aici. |
and get_time() < game.get_event_flag("ev_lupi_final") | Siguranța noastră: efectul merge doar până la ora de final, chiar dacă cronometrul s-a pierdut. |
elseif game.get_event_flag("ev_lupi") == 1 then | În „Starea vânătorii”, îi spunem GM-ului dacă flag-ul a rămas 1 după ora de final. |
Ce nu ai voie să faci într-un bloc server_timer
Un server_timer nu are un jucător „în față”. În sursa pachetului verificat, când rulează un astfel de bloc, nu există un personaj curent. Asta înseamnă:
- Nu folosi funcții
pc.*(pc.get_name,pc.give_item2,pc.setqf...) într-un blocserver_timer. Nu au pe cine să se aplice și pot provoca erori grave, inclusiv căderea canalului. - Nu folosi
say()sauselect(). Nu are cine să vadă fereastra. - Folosește doar lucruri globale:
notice_all,game.set_event_flag,game.get_event_flag, alteserver_timer.
Și încă o regulă: blocurile server_timer le pui în state start, ca în questurile de dungeon din pachetul de referință.
Ce se întâmplă la restart (cea mai importantă parte)
Gândește-te la evenimentul nostru ca la o mașină cu stări: oprit, pornit, „5 minute rămase”, terminat. Ce se întâmplă dacă serverul repornește în fiecare stare?
| Când repornește serverul | Ce se pierde | Ce rămâne | Rezultat cu codul nostru |
|---|---|---|---|
| Evenimentul e oprit | Nimic | ev_lupi = 0 | Totul normal |
| În primele 25 de minute | Cronometrul ev_lupi_5min | ev_lupi = 1, ora de final | Lupii dau gheare până la ora de final, apoi se opresc singuri. Nu mai apar anunțurile de final. Flag-ul rămâne 1 până apeși „Oprește”. |
| În ultimele 5 minute | Cronometrul ev_lupi_stop | La fel | La fel |
| După final | Nimic | ev_lupi = 0 | Totul normal |
Fără ora de final (versiunea din lecția trecută), un restart în mijlocul evenimentului l-ar fi lăsat pornit pentru totdeauna, cu gheare gratuite pentru toți, până observă cineva.
Mai e un detaliu: cronometrele trăiesc pe core-ul game unde a fost apăsat butonul. Dacă pornești evenimentul de pe CH1 și îl oprești de pe CH2, clear_server_timer de pe CH2 nu găsește nimic, iar CH1 tot va anunța „s-a terminat” mai târziu. Nu strică nimic, dar arată ciudat. Regula simplă: pornești și oprești de pe același canal.
Nu porni același timer de două ori
În sursa verificată, dacă pornești un server_timer cu un nume care deja rulează, în syserr apare already registered server timer, dar al doilea cronometru tot pornește. Rezultatul: anunțuri duble. Condiția game.get_event_flag("ev_lupi") == 0 de la „Pornește” te protejează de asta în situațiile obișnuite.
Numele cronometrelor sunt comune pentru toate questurile. Dacă două questuri folosesc stop, se încurcă. Pune mereu un prefix: ev_lupi_stop, nu stop.
Testează pe serverul de test
Ca să nu aștepți 30 de minute, schimbă temporar 1800 în 180, 1500 în 120 și 300 în 60.
- Pornește evenimentul. Ar trebui să vezi anunțul de start.
- După 2 minute: anunțul „5 minute”. După încă un minut: „s-a terminat”.
- Pornește din nou și oprește manual după 30 de secunde: nu trebuie să mai apară niciun anunț după oprire.
- Pornește din nou și repornește serverul de test. După restart, la „Starea vânătorii” ar trebui să vezi ora de final; după ce trece, lupii nu mai dau gheare, iar mesajul îți spune să apeși „Oprește”.
- Pune la loc valorile reale (1800, 1500, 300) înainte de publicare.
Ce ar trebui să vezi: evenimentul se termină la timp, chiar și după un restart.
Greșeli des întâlnite
pc.*într-un blocserver_timer. Erori sau chiar căderea canalului.- Doar cronometru, fără ora de final salvată. După un restart evenimentul nu se mai oprește.
- Nume de cronometru generic (
stop,final). Se încurcă cu alte questuri. - Valorile de test uitate în fișier. Evenimentul de pe serverul live durează 3 minute.
/reload qcât timp evenimentul rulează. N-am verificat ce se întâmplă cu cronometrele în așteptare la reîncărcare. Fă modificările între evenimente, nu în timpul lor.
Întrebări frecvente
Pot face un eveniment care pornește singur, în fiecare sâmbătă?
Se poate, de exemplu cu un server_loop_timer care verifică ora, dar cronometrul trebuie pornit de cineva după fiecare restart. Pentru început, pornirea manuală de către GM, cu oprire automată, e mult mai ușor de controlat.
Pot afișa anunțuri la fiecare 10 minute?
Da: în blocul unui server_timer poți porni următorul server_timer, ca un lanț, exact cum am făcut cu ev_lupi_5min și ev_lupi_stop. Nu exagera cu anunțurile; jucătorii le ignoră dacă vin prea des.
Ce diferență e între notice și notice_all?
În pachetul de referință, notice afișează mesajul doar jucătorului care rulează questul, iar notice_all îl trimite tuturor, pe toate canalele.
Ce urmează
Ai acum misiuni, recompense și evenimente. Ultimul pas al capitolului e să le pui la încercare așa cum o vor face jucătorii: testezi questurile la relogare și restart.
Toate lecțiile sunt în lista tutorialelor Metin2.
Server Metin2 gata de joc
Găzduire Metin2 în România, cu instalare de serverfiles, bază de date MySQL și protecție Anti-DDoS inclusă.