Programarea în C++ și utilizarea unei baze de date MySQL, aplicate pe un game server Coordonator Științific: Conf. univ. dr. Nicoleta Iacob Student:… [305469]
UNIVERSITATEA „SPIRU HARET” [anonimizat]:
Conf. univ. dr. Nicoleta Iacob
Student: [anonimizat]-[anonimizat], 2018 –
UNIVERSITATEA „SPIRU HARET” [anonimizat] C++ [anonimizat]:
Conf. univ. dr. Nicoleta Iacob
Student: [anonimizat]-[anonimizat], 2018 –
[anonimizat]
1 [anonimizat], [anonimizat], acestea având cea mai largă răspândire.
[anonimizat]. Și-[anonimizat], [anonimizat], [anonimizat].
Figura 1.1 – „Evoluția biroului”
„Evolution of the Desk”, [anonimizat]. [anonimizat], pentru a [anonimizat].
Cu ajutorul internetului oamenii au dezvoltat și metode pentru a obține venit. [anonimizat], [anonimizat].
[anonimizat]. Jocurile sunt făcute în mare parte pentru a ne distra și pentru a [anonimizat].
[anonimizat] a metodelor de a [anonimizat],
3
Constantin-[anonimizat], [anonimizat] a unui joc bine realizat.
[anonimizat] a jocurilor, dezvoltatorii pot obține venit adițional cu ajutorul unor componente multiplayer care permit mai multor jucători din diverse părți ale lumii să joace același joc unii cu alții în același moment. [anonimizat], [anonimizat], [anonimizat], [anonimizat], în funcție de genul jocului.
Figura 1.2 – „[anonimizat] ’Sims 3’”
store.steampowered.com/app/47890/The_Sims_3/https://store.steampowered.com/app/271590 /Grand_Theft_Auto_V/
O altă metodă bună de a [anonimizat] a [anonimizat] o [anonimizat]puși să ofere sume mari de bani în schimbul avantajelor adiționale în joc, astfel încât lor să le fie mai ușor să fie mai buni decât alți jucători care nu oferă sume de bani sau să le ofere posibilitatea de a își personaliza caracterele, armele sau vehiculele într-un mod cât mai unic, de a obține mai multe monede virtuale sau multe altele posibilități.
4
Constantin-Flavius Nistor Introducere
Figura 1.3 – „Prețul monedelor virtuale adiționale în jocul ’GTA V’”
store.steampowered.com/app/271590/Grand_Theft_Auto_V/
Multe jocuri nu oferă o componentă multiplayer sau oferă una care nu este pe placul tuturor jucătorilor. Din această cauză, anumiți jucători pot începe să creeze propria modificare („mod”) care odată instalată să ofere cu ajutorul programării o nouă componentă multiplayer care este pe placul lor. Astfel de moduri sunt realizate cu ajutorul modificării memoriei procesului jocului, iar pentru că jocurile nu oferă suport pentru asemenea modificări, dezvoltatorii acestora sunt nevoiți să descopere noi limite ale motorului („engine”) și să încerce să le evite într-un mod sau altul, după care pot pune la dispoziția tuturor celorlalți jucători diverse unelte pentru accesarea ușoară a elementelor jocului.
Un astfel de joc este și Grand Theft Auto: San Andreas care a fost creat de studioul Rockstar North 1 si a fost publicat de către Rockstar Games 2 în anul 2005 pentru sistemul de operare Microsoft Windows, fiind un joc de tip „open world”, în care jucătorii se pot abate de la povestea principală și pot face diverse misiuni secundare sau pot accesa orice punct de pe hartă. Jocul nu oferă o componentă multiplayer, așa că anumiți membri din comunitate s-au hotărât să creeze astfel de componente. Pentru că oricine are posibilitatea să creeze o astfel de componentă, în momentul de față există două moduri multiplayer foarte populare: primul se numește Grand Theft Auto: San Andreas – Multiplayer 3 (abreviere: „SA-MP”), care la momentul actual are un singur dezvoltator și este un mod care este dezvoltat în privat, fără ca restul comunității să aibă acces la sursă („closed-source”), iar a doua modificare multiplayer se
Site: rockstarnorth.com
Site: rockstargames.com
Site: sa-mp.com
5
Constantin-Flavius Nistor Introducere
numeste Multi Theft Auto 4 (abreviere: MTA), care este o modificare la care poate contribui oricine 5 (”open-source”). Ambele modificări multiplayer vin în perechea game server – game
client.
Un game server (care se numește și „host”) 6 este autoritatea care administrează evenimentele din joc. Serverul transmite informațiile pentru a permite jucătorilor să vadă cu o precizie mare tot ce fac ceilalți jucători, să mențină sesiunea fiecărui jucător sincronizată cu sesiunile celorlalți, iar un game client 7 este o instanță a jocului care se folosește de pachetele transmise cu ajutorul internetului către game server și înapoi. Cu ajutorul clientului jucătorul poate face diverse acțiuni în joc în timp ce toți ceilalți clienți conectați la game server primesc modificările respective pentru a fi replicate în jocul lor. Pentru ca un client să se poată conecta la un server, serverul trebuie în primul rând să ruleze, acesta fiind în principiu un simplu program care interpretează pachetele transmise de către clienți.
Conținutul acestei lucrări gravitează în jurul unui game server realizat cu ajutorul modificării multiplayer Grand Theft Auto: San Andreas – Multiplayer. Numele acestui game server este „Green Zone Stuntage” („GZS”) 8, acesta fiind un produs care trebuie să poată fi prezentat cu un nume unic, pentru a fi identificat cu ușurință de către jucători. Un beneficiu major oferit de acest server este faptul că toate textele sunt doar în limba engleză, fiind cea mai folosită limbă de circulație internațională, astfel permițând jucătorilor din întreaga lume să se conecteze și să împărtășească experiențe cu ceilalți. Această lucrare poate fi considerată de actualitate datorită faptului că anual apar componente multiplayer asemănătoare, inclusiv pentru cele mai populare jocuri care sunt jucate de milioane de jucători și care obțin venituri foarte mari.
Figura 1.4 – „Banner Green Zone Stuntage”
Site: mtasa.com
Site dezvoltare: github.com/multitheftauto/mtasa-blue
Mai multe informații: wikipedia.org/wiki/Game_server
Mai multe informații: wikipedia.org/wiki/Game_client
Videoclip de prezentare: youtube.com/watch?v=DUdT3HhNhuU
6
Constantin-Flavius Nistor Componente third-party
Această lucrare conține următoarele capitole:
Componente third-party. În acest capitol se vor prezenta toate componentele făcute de terțe părți și utilizate de către server pentru ușurarea dezvoltării.
Prezentare. Aici se va efectua prezentarea elementelor distinctive ale serverului, atât
în joc, cât și în partea de programare și de bază de date, astfel descriind comunicarea dintre acestea.
Dezvoltare și monetizare. Cu ajutorul acestui capitol se vor oferi niște repere pentru
viitorul serverului, acesta având nevoie de planuri de dezvoltare și de monetizare pentru a nu fi depășit de către competitori.
Concluzii. Fiind ultimul capitol, se vor stabili concluziile lucrării după tot ce s-a descris în capitolele anterioare, astfel stabilind și importanța unui server asupra celor implicați.
2 Componente third-party
Pentru a avea parte de o dezvoltare ușoară a proiectelor, dezvoltatorilor le este recomandat să folosească diverse componente stabile care sunt create de alte persoane sau grupuri de persoane pentru a preveni „reinventarea roții”, astfel micșorând volumul necesar de muncă pentru a ajunge la rezultatul final dorit.
2.1 Grand Theft Auto: San Andreas – Multiplayer
Ca o primă componentă avem însuși nucleul pe care este aplicat proiectul acestei lucrări, fără de care aceasta nu poate exista: componenta multiplayer care oferă, bineînțeles, un game client și un game server. Cu ajutorul clientului în procesul jocului se adaugă componenta multiplayer care este vizibilă jucătorilor, care funcționează prin transmisia de date către game server și înapoi. Acest mod face uz de caracteristicile jocului original, aici incluzându-se părțile grafice și fizice ale jocului. Un client se poate conecta la un server cu ajutorul navigatorului („browser”) SA-MP în care se pot stoca servere favorite sau se pot căuta altele, existând liste speciale pentru acestea. Fiecare server are abilitatea de a anunța IP-ul pe care a fost deschis către autoritatea care administrează listele respective, pentru a putea fi găsit de către alți jucători. De asemenea, în navigator jucătorii pot specifica numele cu care să se conecteze la diverse servere, fiecare jucător având nevoie de un astfel de nume pentru a putea fi identificat de către ceilalți jucători, acesta aflându-se deasupra caracterului fiecărui jucător de pe un server.
7
Constantin-Flavius Nistor Componente third-party
Figura 2.1 – „Interfața navigatorului SA-MP”
Serverul oferă un API („Application Programming Interface” = „Interfață de Programare a Aplicației”) 9 pentru ca persoanele care deschid un astfel de server să poată realiza diverse modificări care să poată fi interpretate de către clienții conectați la acesta. Game serverul este valabil în două versiuni: Microsoft Windows și Linux, ambele având disponibilă doar arhitectura pe 32 de biți (x86), jocul fiind de asemenea valabil doar pe acea arhitectură. Pe Microsoft Windows, directorul serverului vine cu următoarea structură, acolo unde „Pawno” este editorul pentru fișierele „.pwn”, fiind similar cu Microsoft Notepad cu mici funcționalități în plus care includ, bineînțeles, evidențierea sintaxei (”syntax highlighting” 10):
Figura 2.2 – „Structura elementară a unui director de server”
Mai multe informații: wikipedia.org/wiki/Application_programming_interface
Mai multe informații: wikipedia.org/wiki/Syntax_highlighting
8
Constantin-Flavius Nistor Componente third-party
API-ul nativ este disponibil pentru versiunea 3.2.3664.0 a limbajului de scripting numit Pawn 11. API-ul oferă diverse evenimente („callbacks”) care se acționează atunci când se modifică orice informație în client și este transmisă către server, sau când serverul însuși a produs o modificare și deținătorul serverului trebuie să aibă o metodă de aflare a acelei schimbări, ca exemplu având evenimentul OnPlayerConnect, care este apelat când un jucător se conectează pe server. De asemenea, în API există foarte multe funcții pentru modificarea elementelor din joc, precum funcția SetPlayerHealth care setează viața caracterului unui jucător.
Serverul se concentrează asupra unui singur fișier pentru limbajul Pawn (extensia „.pwn” indicând faptul că fișierul conține cod care este disponibil pentru limbajul respectiv), care trebuie să conțină majoritatea deciziilor serverului, acesta fiind numit „gamemode”, neavând posibilitatea de avea mai multe astfel de gamemode-uri la un moment dat. Pe lângă gamemode, există și fișiere de tip „filterscript”, care nu sunt limitate la un singur astfel de exemplare, acestea având abilitatea de a fi pornite și oprite oricând de către deținătorul serverului. O problemă în utilizarea unor multiple filterscript-uri este faptul că acestea nu pot comunica ușor între ele și nici cu gamemode-ul, niciunul având acces la variabilele din celelalte. Un mare avantaj al acestui mod multiplayer este posibilitatea de a avea încă o componentă numită „plugin”, fiind o librărie asemănătoare unor filterscript-uri, serverul nefiind limitat la o singură componentă de acest tip, dar care pot fi încărcate doar atunci când se face inițializarea serverului, precum gamemode-ul.
Plugin-urile pot fi realizate in limbajul C, SA-MP oferind un „Software Development Kit” (SDK) numit „sampSDK” cu ajutorul căruia API-ul poate fi accesat chiar și în acestea, dar care este mai greu de folosit, SDK-ul neoferind acces direct la toate evenimentele, funcțiile și definițiile deja oferite limbajului Pawn. Pentru a rula pe Microsoft Windows un plugin trebuie să aibă extensia „.dll” („Dynamic-Link Library”), iar pe Linux extensia „.so” (”Shared Object”). Tot codul serverului (care este alcătuit din gamemode, filterscript-uri și plugin-uri) se acționează într-un singur fir de execuție („thread” 12), sincronizarea informațiilor între server și jucători producându-se în același fir de execuție, ceea ce înseamnă că dacă există o porțiune de cod lentă undeva tot serverul va suferi, nimic altceva executându-se până ce execuția acesteia nu se finalizează. Din această cauză dezvoltatorul trebuie să se asigure că orice linie nouă de cod este optimizată atât de mult pe cât este posibil.
Site oficial: compuphase.com/pawn/pawn.htm
Mai multe informații: wikipedia.org/wiki/Thread_(computing)
9
Constantin-Flavius Nistor Componente third-party
Datorită faptului că API-ul pune la dispoziție diverse evenimente și funcții cu care se pot controla diverse aspecte ale serverului și ale clientului, există o multitudine de entități care pot fi controlate, acestea fiind disponibile încă din jocul original, iar altele fiind introduse de către SA-MP. Următoarele tipuri de entități sunt prezente pe acest server:
Jucătorii. Aceste entități sunt clienții conectați la server, fiind reprezentarea în joc a acestora. Asupra acestor entități se pot acționa diverse funcții și evenimente din API, precum
funcțiile de setare a vieții (SetPlayerHealth), a poziției în lume (SetPlayerPos), oferirea unei arme (GivePlayerWeapon) sau evenimentele care se acționează atunci când un jucător se conectează la server (OnPlayerConnect), când se deconectează (OnPlayerDisconnect), când apare în lume (OnPlayerSpawn), când scrie un mesaj (OnPlayerText) sau o comandă (OnPlayerCommandText – singura diferență dintre un mesaj și o comandă fiind faptul că o comandă este un simplu mesaj, dar care este precedat de o bară oblică) în chat, când își schimbă starea (OnPlayerStateChange) sau multe alte funcții și evenimente. Bineînțeles, jucătorii trebuie să aibă și un model de caracter 13 (”skin”) vizibil pentru alții, care se poate seta cu ajutorul funcțiilor SetPlayerSkin, care face schimbarea caracterului pe loc, sau cu ajutorul funcțiilor AddPlayerClass sau SetSpawnInfo, care setează caracterul abia atunci când jucătorul apare în lumea jocului („spawn”), iar ID-ul (”Identifier” – „Identificator”, acesta fiind un număr unic într-o categorie de lucruri care ajută la identificarea fiecărui lucru; uneori acesta poate fi un șir de caractere, pentru a ști ce reprezintă) modelului skin-ului unui jucător se poate obține cu ajutorul funcției GetPlayerSkin. Limita este de 1000 de jucători, iar API-ul oferă definiția MAX_PLAYERS pentru aceasta. Fiecare jucător are un ID, care este folosit de către funcții și evenimente, care începe de la 0 și se termină la MAX_PLAYERS-1.
Figura 2.3 – „Un caracter de jucător făcând cu mâna”
Listă caractere: wiki.sa-mp.com/wiki/Skins
10
Constantin-Flavius Nistor Componente third-party
Camerele. Sunt punctele din care jucătorii își văd caracterul sau alte puncte din lumea jocului. API-ul oferă diverse funcții utile precum setarea camerei în spatele jucătorului
(SetCameraBehindPlayer), obținerea poziției camerei (GetPlayerCameraPos) setarea camerei la o altă poziție (SetPlayerCameraPos), setarea punctului la care să se uite camera (SetPlayerCameraLookAt) sau multe altele. API-ul nu oferă și evenimente pentru acțiunile camerei.
Vehiculele 14. Acestea sunt entități aproape la fel de importante precum jucătorii, care pot fi controlate, desigur, cu ajutorul funcțiilor și evenimentelor din API. Funcțiile principale
de creare ale unui vehicul sunt AddStaticVehicle(Ex) și CreateVehicle, ele având diferențe foarte mici, toate returnând un ID. De exemplu, poziția vehiculului se poate afla cu funcția GetVehiclePos și se poate seta cu ajutorul funcției SetVehiclePos. Vehiculele pot avea două culori: cea primară și cea secundară, care se pot seta cu ajutorul funcției ChangeVehicleColor, ambele culori din parametri diferiți necesitând câte un ID 15. Când un vehicul este distrus se apelează evenimentul OnVehicleDeath. Limita este de 2000 de vehicule, definiția utilizată fiind MAX_VEHICLES. ID-ul unui vehicul poate avea valori de la 1 până la MAX_VEHICLES.
Figura 2.4 – „Vehicule parcate”
Obiectele 16. Jocul este compus dintr-o multitudine de modele care sunt incluse în jocul original pentru a crea harta pe care se poate juca, iar o mică parte din acestea sunt oferite de
către SA-MP, fiecare model de obiect având un ID. De asemenea, API-ul oferă diverse funcții și evenimente pentru manipularea obiectelor. Cu ajutorul funcției CreateObject un
Listă vehicule: wiki.sa-mp.com/wiki/Vehicles:All
Listă culori pentru vehicule: wiki.sa-mp.com/wiki/Vehicle_Color_IDs
Listă obiecte adăugate de SA-MP: wiki.sa-mp.com/wiki/Samp_objects
11
Constantin-Flavius Nistor Componente third-party
obiect poate fi creat oriunde în lume cu orice ID de model existent, la o anumită poziție, cu o anumită rotație Euler (X, Y și Z) 17, returnând un ID de obiect. După ce un obiect este creat cu această funcție el este global, fiind vizibil pentru orice jucător. Alte funcții permit mișcarea obiectului (MoveObject), setarea directă a unei alte poziții (SetObjectPos), atașarea unui obiect de un jucător (AttachObjectToPlayer), setarea unei texturi diferite (SetObjectMaterial) și multe altele. Jucătorilor li se poate deschide un „Graphical User Interface” (GUI) pentru a edita un obiect cu ajutorul funcției EditObject, după care evenimentul OnPlayerEditObject este apelat cu diverse stări, în caz ca jucătorul a mutat, învârtit sau salvat obiectul. Un alt eveniment este și OnObjectMoved, care este apelat după ce se termină mișcarea începută cu ajutorul funcției MoveObject. Aceste entități au posibilitatea de a fi văzute doar de către un singur jucător (fiind numite „player objects” – „obiecte de jucător”), fiind disponibilă funcția CreatePlayerObject pentru crearea unui astfel de obiect, aproape fiecare funcție și eveniment a obiectelor globale având și versiuni pentru aceste obiecte care sunt vizibile doar pentru un singur jucător, parametrul adițional la majoritatea fiind ID-ul jucătorului pentru care s-au acționat. Limita de obiecte create la un moment dat este de 1000, fiind valabilă definiția MAX_OBJECTS. Obiectele globale în general se creează la inițializarea serverului, fiind obiecte de bază, care trebuie să fie prezente mereu în joc. Ca prim exemplu despre limitele impuse, se pot crea doar 1000 de obiecte globale care sunt create pentru orice jucător în orice moment. Mai des întâlnit este faptul că pe un server pot exista, de exemplu, 100 de obiecte globale, iar pentru oricare jucător se mai pot crea maxim 900 obiecte de jucător, ceea ce înseamnă că limita vizibilă pentru fiecare jucător va putea ajunge la cea de 1000, deci dacă serverul are 1000 de jucători conectați, atunci vor fi 100 de obiecte globale și 900,000 obiecte de jucători diferite în total. Mai multe obiecte adăugate într-un loc alcătuiesc o nouă hartă, precum în imaginea de mai jos:
Figura 2.5 – „Obiecte care formează o hartă”
Mai multe informații: wikipedia.org/wiki/Euler_angles
12
Constantin-Flavius Nistor Componente third-party
Obiectele atașabile pe jucători („Attached objects”). Acestea funcționează separat de obiectele obișnuite, ele neutilizând limita de 1000 obiecte. În schimb, se pot atașa maxim 10
obiecte pe un jucător, care pot fi văzute de către oricine, dar care nu au coliziune. Atașarea se face cu ajutorul funcției SetPlayerAttachedObject care acceptă ca parametri ID-ul locului pe care să-l folosească (”index” sau „slot”; de la 0 la 9), ID-ul modelului de obiect, ID-ul părții din corp pe care să atașeze obiectul (capul, mâna stângă, abdomenul sau altele au fiecare câte un ID), pentru ca atunci când jucătorul mișcă partea respectivă obiectul atașat să se miște și el deodată cu aceasta. Funcția mai acceptă și poziții relative (acestea pornind de la valorile 0.0, 0.0, 0.0) la centrul părții din corp specificată anterior. Dacă un alt obiect este deja atașat în index-ul specificat, atunci cel vechi va fi înlocuit de noul obiect cu proprietățile respective. De asemenea, se acceptă valori de rotație Euler, iar ca o premieră în SA-MP se acceptă și valori pe cele trei axe pentru a redimensiona obiectul în diverse direcții, făcând posibilă, de exemplu, atașarea unei clădiri făcută foarte mică pe un jucător, un papagal pe umăr, un costum de papagal (în cazul ultimelor două exemple fiind posibil să se specifice același model de obiect) sau până la zece pălării puse unele peste altele. Un astfel de obiect atașat poate fi scos oricând de pe jucător cu ajutorul funcției RemovePlayerAttachedObject care permite și un parametru pentru ID-ul index-ului specificat anterior pe care să-l șteargă.
Figura 2.6 – „O perucă de clovn și o mulțime de pălării atașate”
Dialog-urile. Ele sunt niște simple meniuri pe ecran, care au mai multe stiluri 18. Aceste
entități nu sunt valabile în jocul original, ele fiind adăugate strict de către SA-MP, cu ajutorul serverului și în special al clientului. Un meniu poate fi afișat pentru un jucător cu ajutorul funcției ShowPlayerDialog, iar în momentul în care jucătorul ia o decizie în acel dialog (de exemplu, are posibilitatea să selecteze un element din listă și să apese unul din butoanele
Listă stiluri: wiki.sa-mp.com/wiki/Dialog_Styles
13
Constantin-Flavius Nistor Componente third-party
disponibile) se apelează evenimentul OnDialogResponse care are ca parametri ID-ul jucătorului care a luat decizia, ID-ul dialogului la care s-a răspuns, ID-ul butonului apăsat, ID-ul elementului selectat și textul introdus în acesta. Conținutul fiecărui parametru diferă în funcție de stilul dialogului specificat în ShowPlayerDialog. ID-ul maxim este 32767, dar această limită poate fi ocolită ușor folosind diverse variabile și metode de reținere a ultimului ID de dialog afișat unui jucător.
Figura 2.7 – „Un dialog de tip DIALOG_STYLE_TABLIST_HEADERS”
3DTextLabel-urile. Precum dialogurile, acestea sunt entități care nu sunt valabile în
jocul original. Ele sunt, de fapt, niște simple texte 2D valabile în spațiul 3D al jocului, astfel încât dacă un jucător le privește din orice unghi le vede, de fapt, frontal. O astfel de entitate se creează cu ajutorul funcției Create3DTextLabel, având în principal un text, o culoare și o poziție. Asemenea obiectelor, pe lângă 3DTextLabel-urile globale există și o versiune de jucător care poate fi creată cu ajutorul funcției CreatePlayer3DTextLabel. Limita este de 1024 entități globale (MAX_3DTEXT_GLOBAL) de acest tip și tot de 1024 (MAX_3DTEXT_PLAYER) per jucător.
Figura 2.8 – „3DTextLabel cu două linii și două culori”
14
Constantin-Flavius Nistor Componente third-party
Textdraw-urile 19. Acestea sunt texte 2D pe ecran, care sunt disponibile și în jocul
original sub diverse forme, ele fiind asemănătoare desenelor, ecranul fiind planșa. Pot exista diverse texte create oriunde pe ecran, care pot avea anumite culori prestabilite, o anumită proporționalitate, aliniere, umbră, font, contur sau multe alte elemente care pot fi schimbate cu ajutorul API-ului oferit de către SA-MP. De asemenea, cu ajutorul API-ului acestora pot fi adăugate pe ecran diverse modele de vehicule, caractere sau obiecte. Există, de asemenea, o variantă globală de textdraw-uri, dar și o variantă de jucător. Limita globală este de 2048 (MAX_TEXT_DRAWS) și de 256 per jucător (MAX_PLAYER_TEXT_DRAWS). Un textdraw se poate crea cu ajutorul funcției TextDrawCreate. Cu ajutorul acestor elemente se poate crea o interfață foarte plăcută pentru fiecare jucător, câteva fiind pe ecran aproape în permanență pe aproape orice server.
Figura 2.9 – „Banner făcut cu ajutorul textdraw-urilor”
Checkpoint-urile. Sunt niște obiecte cilindrice de o înălțime mică, care pot avea ca proprietăți poziția și mărimea. Doar un checkpoint poate fi vizibil la un moment dat pentru
un jucător, folosind funcția SetPlayerCheckpoint. Când un jucător intră în checkpoint se acționează evenimentul OnPlayerEnterCheckpoint, iar atunci când iese din el se acționează un alt eveniment, OnPlayerLeaveCheckpoint. Pentru a ști în care checkpoint a intrat jucătorul, serverul trebuie să facă diverse verificări, în funcție de cazul pentru care a apărut acel checkpoint, folosind variabile.
Mai multe informații: wiki.sa-mp.com/wiki/TextDraws
15
Constantin-Flavius Nistor Componente third-party
Figura 2.10 – „Un checkpoint pentru schimbarea unui vehicul”
Checkpoint-urile de curse (Race checkpoints). Acestea sunt foarte asemanatoare cu checkpoint-urile, dar au o înălțime mai mare. Sunt folosite în general pentru curse, așa cum o spune și numele lor, având posibilitatea de a afișa o săgeată către o direcție pentru a ști unde va apărea următorul checkpoint, sau un steag în cazul în care este ultimul checkpoint
din cursă. Pot fi create cu ajutorul funcției SetPlayerRaceCheckpoint și acționează evenimentul OnPlayerEnterRaceCheckpoint atunci când un jucător intră în checkpoint-ul vizibil pentru el și OnPlayerLeaveRaceCheckpoint atunci când iese. Un checkpoint are, pe lângă poziție și mărime, încă două proprietăți: tipul și coordonatele punctului spre care să indice, tipul fiind folosit pentru a ști dacă acea cursă se desfășoară pe pământ sau în aer și dacă este un checkpoint intermediar sau este cel final.
Figura 2.11 – „Checkpoint de început de cursă”
16
Constantin-Flavius Nistor Componente third-party
Pickup-urile. Aceste entități folosesc ID-uri de modele de obiecte pentru a se afișa, iar acestea plutesc și se învârt tot timpul, pentru a exista o diferență vizibilă între acestea și
simplele obiecte. Asemenea checkpoint-urilor, atunci când un jucător intră într-un checkpoint se acționează evenimentul OnPlayerPickUpPickup. Un pickup se creează cu ajutorul funcției CreatePickup, aceasta returnând un ID de pickup valid sau -1 în cazul în care nu s-a putut crea, în cazul atingerii limitei de 4096 (MAX_PICKUPS). O altă metodă de adăugare este AddStaticPickup, care doar returnează 1 în cazul în care pickup-ul s-a creat cu succes, această funcție fiind utilă doar în cazul în care se dorește ca jocul să facă singur acțiunea în cazul modelelor de obiecte de inimă (pentru a seta la maxim punctele de viață ale jucătorului), armură (asemănător inimii, dar pentru punctele de armură) sau în cazul armelor (pentru ca jucătorul să primească automat arma), fără ca pe server să se mai execute evenimentul de intrare în pickup.
Figura 2.12 – „Pickup pentru semnalarea unei afaceri”
Actorii. Asemenea jucătorilor, aceștia au un anumit ID de caracter, aceștia stând pur și
simplu pe loc, opțional având posibilitatea de a rula o animație cu ajutorul funcției ApplyActorAnimation. Pentru a detecta când un jucător acționează împotriva unui actor se apelează evenimentul OnPlayerGiveDamageActor, doar în cazul în care actorul nu este setat ca invulnerabil (funcția SetActorInvulnerable). Bineînțeles, un actor poate fi creat cu ajutorul funcției CreateActor, limita fiind de 1000, API-ul oferind definiția MAX_ACTORS.
17
Constantin-Flavius Nistor Componente third-party
Figura 2.13 – „Simplu actor fără animație, jucând rol de vânzător”
Gangzone-urile. Acestea apar doar pe radar sau pe hartă, oferind ajutor pentru
delimitarea zonelor, sub formă de dreptunghiuri. Ele se creează cu ajutorul funcției GangZoneCreate specificând perechile minime și maxime de puncte X și Y, acestea fiind o formă 2D. Pentru a se afișa un astfel de gangzone pentru un jucător sau pentru toți se pot folosi funcțiile GangZoneShowForPlayer sau GangZoneShowForAll, care primesc ca parametru culoarea de afișare a zonei, cu suport pentru transparență (canalul „alpha” al culorii). O altă abilitate a gang zone-urilor este posibilitatea de a clipi intermitent, pentru un jucător sau pentru toți, cu ajutorul funcțiilor GangZoneFlashForPlayer sau GangZoneFlashForAll, acceptând ca parametru adițional culoarea a doua, acesta clipind intermitent între vechea culoare afișată și cea nouă, la un interval constant. Limita este de 1024 (MAX_GANG_ZONES).
Figura 2.14 – „Mai multe gangzone-uri pe hartă capturate, în mare parte, de aceeași bandă”
18
Constantin-Flavius Nistor Componente third-party
Iconițele de pe hartă („Map Icons”) 20. La o anumită poziție pe hartă se pot afișa iconițe,
care au anumite ID-uri prestabilite. Există o limită de 100 de iconițe care pot fi afișate deodată pentru un jucător. Spre deosebire de toate celelalte entități, funcția de creare SetPlayerMapIcon primește un parametru care specifică index-ul acelei iconițe, de la 0 la 99, pentru a fi posibilă eliminarea acesteia cu ajutorul funcției RemovePlayerMapIcon.
Figura 2.15 – „Iconiță pe hartă, care indică o casă”
Timerele. Sunt niște entități disponibile doar pentru server, acestea oferind abilitatea
unei funcții de a executa la un anumit timp specificat ca număr de milisecunde. ID-urile încep de la 1 și nu au o limită specifică, fiind limitate doar de stresul pus pe procesor și pe cantitate de memorie RAM utilizată. Ele pot fi create cu ajutorul funcțiilor SetTimer și SetTimerEx, singura diferență fiind faptul că în cea de-a doua se pot specifica și anumiți parametri cu care să fie apelate funcțiile respective. Crearea unui timer permite setarea unui argument cu valoarea 1 pentru ca acesta să se repete la infinit. O astfel de entitate se poate distruge în orice moment cu ajutorul funcției KillTimer.
Armele. Acestea nu sunt asemănătoare cu celelalte entități, deoarece acestea nu necesită creare, ci pot fi oferite unui jucător în orice moment se dorește, neexistând o limită globală, ci doar o limită de 16 locuri valabile (slot-uri) de arme, unele tipuri de arme ocupând
același slot și deci nefiind posibil ca un jucător să dețină simultan două arme care necesită ocuparea aceluiași slot. Fiecare armă are, bineînțeles, un ID 21 care poate fi raportat și ca
Listă icoane: wiki.sa-mp.com/wiki/MapIcons
Listă arme și motive: wiki.sa-mp.com/wiki/Weapons
19
Constantin-Flavius Nistor Componente third-party
argument al unui eveniment, ca de exemplu în parametrul „weaponid” al evenimentului OnPlayerTakeDamage, atunci când unui jucător îi este scăzută viața.
Figura 2.16 – „Un lansator de rachete în mâinile unui jucător”
Limitele sunt impuse de server, în mare parte din cauza limitărilor jocului, și sunt
specificate în conformitate cu ultima versiune a SA-MP-ului, 0.3.7.
Pentru că limita jucătorilor, vehiculelor și a actorilor este destul de mare, serverul oferă o optimizare pentru îmbunătățirea transferului datelor către jucători. Să presupunem acea optimizare nu există și că pe un server sunt 1000 (maximul) de jucători conectați, 2000 (maximul) de vehicule create și 1000 (maximul) de actori creați. Pentru a actualiza mereu pentru fiecare jucător toți ceilalți jucători, vehicule și actori ar fi nevoie de un transfer foarte mare de date, iar acest lucru nu este ideal, pentru că mașina pe care se află găzduit serverul poate avea o conexiune nu foarte puternică la internet, iar jucătorii care dețin computere slabe ar putea avea probleme cu procesorul, RAM-ul sau hard disk-ul.
Din cauza acestor probleme, serverul de SA-MP introduce noțiunea de „streaming”, care poate fi configurată în fișierul server.cfg 22 din directorul principal al acestuia. Prin „streaming” înțelegem începerea sau oprirea transmisiei datelor către un client doar atunci când este absolută nevoie de ele. Această verificare se produce, configurabil, o dată la un anumit interval de timp, iar alte entități sunt transmise pentru crearea în clientul respectiv doar atunci când acesta se află în jurul lor, această distanță fiind configurabilă și ea. Această metodă ajută, de asemenea, la prevenirea abuzului clienților care se folosesc de modificări care afectează
Mai multe informații și opțiuni: wiki.sa-mp.com/wiki/Server.cfg
20
Constantin-Flavius Nistor Componente third-party
experiența jocului celorlalți. De exemplu, un client malițios ar putea schimba rapid poziția tuturor vehiculelor de pe server sau ar putea omorî orice jucător, oriunde s-ar afla acesta. Serverul deține în orice moment informația despre toate proprietățile entităților, dar decide să le trimită mai departe clienților doar atunci când știe că aceia au nevoie de ele create.
Datorită acestei metode, SA-MP-ul oferă în API și anumite evenimente care se acționează doar în cazul în care este pornită sau oprită transmisia informațiilor unei entități către un client, fiecare având ca parametru ID-ul entității ale căror informații au început să fie transmise sau oprite din transmisie și ID-ul jucătorului respectiv:
OnPlayerStreamIn, OnPlayerStreamOut: Aceste evenimente sunt apelate atunci când
un jucător a început să transmită sau a încetat să transmită informațiile către un alt jucător. De obicei, dacă există o conexiune bună între server și ambii jucători, aceste evenimente sunt apelate de două ori. Presupunând că avem jucătorii cu ID-urile X si Y, atunci aceste evenimente se vor apela cu parametrii (X, Y) și (Y, X).
OnVehicleStreamIn, OnVehicleStreamOut: Sunt similare cu cele două evenimente
precedente, dar parametrii sunt ID-ul vehiculului ale cărui informații au început sau au încetat să fie transmise către ID-ul jucătorului respectiv.
OnActorStreamIn, OnActorStreamOut: Aceste evenimente sunt similare evenimentelor vehiculelor anterior specificate, singura diferență fiind, bineînțeles, tipul
entității, acestea aplicându-se doar pentru actori.
Pentru a se observa cu ușurință caracteristicile implementate pe acest server, se poate avea ca idee faptul că pe un server gol nu există nimic, ci doar harta jocului original și un model de caracter identic pentru toți jucătorii care se conectează și apar în lume. Nu există nici o entitate creată de server. De asemenea, nu există nici o comandă, ci doar cele existente deja în clientul SA-MP (”comenzi client” – „client commands” 23, care sunt executate doar pentru jucătorul curent, fără ca serverul să știe de execuția acestora), precum și comenzile RCON („Remote CONtrol” 24, cu ajutorul căruia un jucător poate executa comenzi care au efect asupra serverului, permițând, de exemplu, închiderea sau deschiderea acestuia, încărcarea sau scoaterea unor filterscript-uri, sau multe altele, iar pentru a le putea folosi acesta trebuie să se autentifice folosind comanda ‘/rcon login’, existând și funcția IsPlayerAdmin în API care returnează 1 în caz că jucătorul este autentificat, aceasta fiind utilă pentru comenzile adiționale
Listă comenzi client: wiki.sa-mp.com/wiki/Client_Commands
Mai multe informații: wiki.sa-mp.com/wiki/RCON
21
Constantin-Flavius Nistor Componente third-party
care necesită o securitate mai mare pentru a fi executate), care sunt disponibile direct în executabilul serverului. Jucătorii nu pot alege nici un caracter după ce se conectează la server, ci pot doar să apese pe butonul de spawn, după care vor primi caracterul cu ID-ul 0 și vor fi puși la punctul 0.0, 0.0, 0.0 al jocului, care se află sub suprafața hărții, astfel jucătorul căzând în gol până va ajunge prea jos, iar jocul va decide că trebuie să-l teleporteze la cel mai apropiat loc prestabilit pentru astfel de evenimente:
Figura 2.17 – „Meniul gol pentru selectarea caracterului”
Figura 2.18 – „Apariția în lumea jocului”
Figura 2.19 – „Teleportarea la suprafața hărții”
22
Constantin-Flavius Nistor Componente third-party
2.2 C++, Premake și plugin-ul sampGDK
Din cauza faptului că limbajul Pawn este foarte limitat (din acesta lipsind componente foarte importante precum pointerii, alocarea dinamică, namespace-urile, orientarea pe obiecte sau multe altele) și datorită faptului că este posibil ca API-ul serverului să fie folosit în plugin-urile care pot fi realizate în limbajul C, anumiți membri ai comunității au lansat anumite extensii sub formă de plugin-uri pentru a permite dezvoltarea mai ușoară chiar și în alte limbaje sau o dezvoltare mult mai simplă în limbajul C.
Unul dintre plugin-urile care oferă o dezvoltare mult mai simplă în limbajul C (care este compatibil cu C++, care este cu mult mai avansat și mai plăcut, având foarte multe componente în plus) se numește „SA-MP Game Development Kit” („sampGDK”) 25, acesta oferind plugin-urilor tot ceea ce este deja existent în API-ul pentru limbajul Pawn (inclusiv definiții) și chiar mai multe (API-ul pentru Pawn are destule lipsuri, omițând chiar și declarații pentru funcții care există nativ în server). Bineînțeles, sursa acestui plugin poate fi inclusă cu ușurință în alte plugin-uri, pentru a le permite acestora să folosească beneficiile aduse de către aceasta.
Acest plugin este realizat în C pentru a le permite celor care doresc să dezvolte un alt plugin în acest limbaj să nu folosească C++. Avantajul utilizării limbajului C fiind compatibilitatea cu C++, codurile realizate în acest limbaj, C, având posibilitatea de a fi integrate cu ușurință în altele care folosesc limbajul C++. Dacă acest plugin ar folosi bucăți de cod valabile doar în limbajul C++, cei care ar dori să realizeze un alt plugin strict în limbajul C n-ar putea face acest lucru.
Însă, datorită faptului că ambele limbaje, C și C++, suportă definițiile, inclusiv cele adăugate direct în preprocesor („Preprocessor definitions”), din afara codului, și faptului că sampGDK oferă o sursă diferită, sub formă de amalgamare a tuturor fișierelor într-un fișier .c pentru implementarea tuturor funcțiilor și evenimentelor și într-un fișier .h pentru prototipurile acestora și definiții, pentru includere ușoară în orice alt plugin care poate fi folosit cu rol de gamemode pentru utilizarea tuturor acestor funcții și evenimente într-un limbaj de programare avansat, există în sampGDK o verificare de definiție „SAMPGDK_CPP_WRAPPERS” doar atunci când fișierele sunt într-adevăr compilate folosind un compilator de C++. Când aceste verificări se execută cu succes, toate funcțiile existente se vor pune în namespace-ul numit
Site dezvoltare: github.com/Zeex/sampgdk
23
Constantin-Flavius Nistor Componente third-party
„sampgdk”, astfel încât, de exemplu, funcția SetPlayerHealth va putea fi folosită doar folosind apelul sampgdk::SetPlayerHealth, pentru a nu polua spațiul de nume global.
Plugin-ul principal al serverului, care este de asemenea obiectivul acestei lucrări, folosește standardul din 2014 al limbajului C++ (C++14 26), care a fost lansat la data de 15 decembrie 2014, ușurând limbajul C++. Cum acesta a fost lansat de câțiva ani, creatorii de compilatoare au avut destul timp să implementeze majoritatea schimbărilor aduse de acest standard. Acest plugin folosește compilatorul Microsoft Visual C++ pentru Microsoft Windows, folosit în mediul de dezvoltare Microsoft Visual Studio 2017 (”VS2017”), iar pentru Linux compilatorul GNU GCC, care citește fișierele de configurație GNU Make (”GMake”), ambele compilatoare având suport aproape complet pentru standardul C++14 și fiind probabil cele mai folosite compilatoare din întreaga lume pentru Microsoft Windows și Linux.
Pentru a se genera fișierele necesare pentru compilarea pe ambele platforme, se folosește software-ul Premake 27, acesta oferind suport pentru fișiere de configurație pentru „VS2017”, „GMake”, dar și pentru altele. Acest software este asemănător cu mult mai celebrul CMake, dar este, probabil, mai ușor de folosit și mai limitat. Pentru configurație s-a creat un singur fișier numit „premake5.lua” (ultima versiune a software-ului Premake și cea folosită de plugin fiind cea cu numărul 5; în acest fișier se specifică și definiția utilizată de către sampGDK, anterior menționată), numele implicit al fișierului de configurație pe care-l caută software-ul fiind acesta, pentru a evita pasarea unui parametru adițional de fiecare dată când se schimbă configurația pentru regenerarea fișierelor necesare pentru fiecare compilator. Pentru a începe generarea fișierelor de configurație se poate folosi linia de comandă, ca în imaginea de mai jos, singurul parametru fiind cel al tipului configurației generate.
Figura 2.20 – „Generarea fișierelor de configurație cu ajutorul Premake”
Mai multe informații: wikipedia.org/wiki/C++14
Site dezvoltare: github.com/premake/premake-core
24
Constantin-Flavius Nistor Componente third-party
2.3 Streamer Plugin
Din cauza faptului că serverul de SA-MP impune limite pentru majoritatea entităților, un membru al comunității a reușit să creeze un alt plugin, numele acestui plugin fiind „Streamer Plugin” 28. Un „streamer” în SA-MP ajută la extinderea limitelor de entități. Un streamer se poate realiza și în Pawn cu ajutorul API-ului, dar acest limbaj este mult mai lent, așa că acesta a fost realizat sub formă de plugin pentru a profita de viteza de procesare și de structurile avansate posibile cu ajutorul limbajului C++. Acesta oferă, de asemenea, extinderea API-ului disponibil pentru entitățile native. Entitățile suportate sunt: obiectele, pickup-urile, checkpoint-urile, checkpoint-urile de curse, iconițele de pe hartă, 3DTextLabel-urile și actorii. În plus, acesta oferă și zone („areas”) de diferite tipuri: cercuri, cilindre, sfere, dreptunghiuri, cuboizi și poligoane (toate aceste tipuri de zone împărțind aceeași numerotare a ID-urilor, nefiind entități diferite) care, cu ajutorul unui interval configurabil, permite apelarea evenimentelor OnPlayerEnterDynamicArea și OnPlayerLeaveDynamicArea pentru a se putea lua o acțiune în momentul în care un jucător a intrat sau a ieșit dintr-o asemenea zonă. De asemenea, se poate verifica în orice moment dacă un jucător este într-o zonă, cu ajutorul funcției IsPlayerInDynamicArea, existând și funcția similară IsPointInDynamicArea, pentru a se verifica dacă un punct anume din lume este în zona respectivă, creată anterior. Pentru a se putea diferenția, aceste entități au nume precedate de cuvântul „dynamic” (dinamic). De exemplu, avem „dynamic objects” („obiecte dinamice”) sau „dynamic actors” („actori dinamici”). Aceste entități se creează cu ajutorul funcțiilor CreateDynamicObject, CreateDynamic3DTextLabel, CreateDynamicPickup, CreateDynamicCircle, CreateDynamicPolygon și altele, aceste funcții returnând un ID care ajută la manipularea entităților create, fiind separate de ID-urile entităților create cu ajutorul API-ului oferit de SA-MP.
Poate exista un număr infinit de entități de orice tip „dinamic” cu ajutorul acestui streamer, ele fiind doar stocate în memoria plugin-ului pentru crearea ulterioară, atunci când va fi nevoie de ele pentru fiecare jucător. Singura limită impusă este una ale entităților vizibile la un moment dat pentru un jucător, în cazurile în care entitățile native din SA-MP de acel tip au o versiune per jucător, precum obiectele sau 3DTextLabel-urile, astfel de entități fiind create doar cu ajutorul acelor tipuri, pentru extinderea ușoară a limitei și pentru a nu fi create inclusiv pentru cine nu are nevoie de ele. În schimb, de exemplu, pickup-urile au doar versiunea globală, așa că streamer-ul va crea un astfel de pickup pentru toți, doar atunci când orice jucător de pe server este în jurul coordonatelor la care s-a creat.
Site dezvoltare: github.com/samp-incognito/samp-streamer-plugin
25
Constantin-Flavius Nistor Componente third-party
Acest plugin oferă, adițional, funcții de tip get („getters”, care află o proprietate) pentru entitățile sale „dinamice” care nu există în versiunile entităților din API-ul SA-MP. De exemplu, cu API-ul SA-MP, dezvoltatorii ar trebui să creeze variabile în care să stocheze noile valori în cazul că acestea sunt schimbate de către aceștia, pentru că API-ul oferă foarte puține funcții de preluare ale proprietăților. Similar, există și funcții de tip set („setters”, care setează o proprietate) adiționale pentru aceste entități, API-ul SA-MP neoferind prea multe posibilități de schimbare a proprietăților entităților după ce acestea au fost create inițial.
Funcțiile acestui plugin sunt oferite nativ pentru limbajul Pawn, dar datorită plugin-ului sampGDK ele pot fi utilizate și de către plugin-ul principal al acestui server (asupra căruia se axează această lucrare) cu ajutorul funcțiilor specializate sampgdk::FindNative și sampgdk::InvokeNative. Definițiile și declarațiile funcțiilor și evenimentelor oferite pentru limbajul Pawn fiind situate în fișiere cu extensia „.inc”, specifice acestui limbaj, nu sunt compatibile cu limbajele C sau C++, așa că pentru implementarea acestora a fost nevoie de crearea unor fișiere „.cpp” și „.hpp”, făcând posibilă utilizarea acestora și în plugin-uri. De exemplu, așa arată în Pawn librăria pentru invocarea API-ului oferit de Streamer Plugin, implementările fiind valabile în sursa plugin-ului:
Tabelul 2.1 – „streamer.inc”
// Definiții
#define STREAMER_TYPE_OBJECT (0)
#define STREAMER_TYPE_PICKUP (1)
…
// Funcții
native Streamer_GetTickRate();
native Streamer_GetPlayerTickRate(playerid);
…
// Evenimente
forward OnDynamicObjectMoved(STREAMER_TAG_OBJECT objectid);
forward OnPlayerEditDynamicObject(playerid, STREAMER_TAG_OBJECT objectid, response, Float:x, Float:y, Float:z, Float:rx, Float:ry, Float:rz);
Sursa: github.com/samp-incognito/samp-streamer-plugin/blob/master/streamer.inc
Pentru a replica aceste linii în limbajul C++ am optat pentru următoarele metode, ele fiind făcute publice la adresa github.com/IstuntmanI/samp-streamer-plugin-sampgdk-invoke:
26
Constantin-Flavius Nistor Componente third-party
Tabelul 2.2 – „streamer.hpp”
// Definiții
#define STREAMER_TYPE_OBJECT (0)
#define STREAMER_TYPE_PICKUP (1)
…
Funcții namespace Plugins {
namespace Streamer { namespace Settings {
int GetTickRate( );
int GetPlayerTickRate( int playerid );
} } }
…
Evenimente
bool OnDynamicObjectMoved( int objectid );
bool OnPlayerEditDynamicObject( int playerid, int objectid, int response, float x, float y, float z, float rx, float ry, float rz );
Tabelul 2.3 – „streamer.cpp”
Funcții
int Plugins::Streamer::Settings::GetTickRate( )
{
static AMX_NATIVE Native = sampgdk::FindNative( "Streamer_GetTickRate" ); if( Native != NULL ) return sampgdk::InvokeNative( Native, "" );
else return sampgdk::logprintf( "'" __FILE__ "' / '%s' – Function not discovered !",
__func__ ), 0;
}
…
Pentru că plugin-ul sampGDK acționează evenimentul OnPublicCall pentru fiecare
eveniment apelat, fie că este un eveniment nativ din SA-MP sau unul din alte plugin-uri și
pentru că toate evenimentele native din SA-MP sunt implementate deja de către sampGDK, un
eveniment care provine dintr-un alt plugin trebuie sa fie prima dată importat în fișierul „.def”
al plugin-ului și preluat în evenimentul OnPublicCall din plugin-ul nostru în modul următor:
Tabelul 2.4 – „Preluarea unui eveniment cu ajutorul evenimentului OnPublicCall”
PLUGIN_EXPORT bool PLUGIN_CALL OnPublicCall( AMX * amx, const char * name,
cell * params, cell * retval )
{
if( std::string( name ) == "OnDynamicObjectMoved" ) { return OnDynamicObjectMoved( static_cast< int >( params[ 1 ] ) ); }
… alte evenimente return 1;
}
27
Constantin-Flavius Nistor Componente third-party
2.4 Formatarea textelor: Librăria fmt
În C++ există doar o metodă nativă de a formata mesajele, aceasta realizându-se cu ajutorul librăriei „sstream” (”string stream”), dar care este dificilă și lentă. O altă metodă nu provine din C++, ci din standardul limbajului C, fiind aflată în librăria „cstdio” (varianta C++ pentru librăria „stdio.h”, care înseamnă „standard input/output”, care spre deosebire de aceasta pune toate funcțiile valabile în namespace-ul „std”, caracterul „c” din față semnalizând faptul ca aceasta este o librărie provenită din standardul limbajului C, ci nu din cel al limbajului C++), aceasta neoferind suport pentru noile clase din C++, precum std::string. Funcția din librăria provenită din C asupra căreia ne vom axa este „std::snprintf” .
Pentru a obține textul „3.14” din numărul „3.141592” cu ajutorul librăriei „sstream” din C++ se poate executa codul „std::stringstream varStream; varStream << std::setprecision( 3 ) << 3.141592;” (funcția std::setprecision este valabilă doar dacă se include și librăria „iomanip”, numele acesteia însemnând „input/output manipulators”). Același text se poate obține cu funcția std::snprintf din standardul limbajului C, într-un mod mai ușor dar care nu permite obiecte specializate din C++, precum std::string: „char var[ 16 ]; std::snprintf( var, 16, „%.2f”, 3.141592 );” . Se poate observa faptul că prin intermediul metodei din C++ nu este nevoie de un „placeholder” (bucată de text care ține locul unei valori specificate ulterior ca parametru), acesta fiind capabil să deducă automat tipul introdus, dar în schimb se folosește o altă funcție pentru setarea preciziei numerice a numărului real. Această metodă este, totuși, destul de lentă în timpul execuției și face ca fișierele compilate să aibă o mărime ceva mai mare.
Pentru varianta de C este nevoie să se specifice numărul maxim de caractere care poate fi stocat în variabila respectivă și, de asemenea, este nevoie de un placeholder care să indice și tipul valorii date ca parametru. Există o funcție care elimină nevoia de a specifica o mărime maximă de caractere suportate, aceasta fiind std::sprintf, dar aceasta poate crea probleme în caz că dezvoltatorul nu este atent și încearcă să scrie prea mult text într-o variabilă prea mică, aceasta accesând locuri de memorie pe care n-ar trebui să le acceseze, astfel având posibilitatea de a o modifica și eventual de a face serverul să se închidă brusc („server crash”). Pentru a folosi aceste funcții este nevoie de o foarte mare atenție, acestea având posibilitatea de a accesa foarte ușor zone invalide de memorie. Spre deosebire de varianta din C++, aceasta este mai rapidă în execuție, de asemenea fiind și cea mai rapidă în comparație cu alte librării existente.
Din cauza acestor minusuri mari, funcțiile anterior menționate nu oferă o stabilitate prea mare, iar acest lucru a fost observat și de alți programatori care au reușit să creeze diverse librării care să adreseze aceste probleme. O astfel de librărie, care este folosită de acest server,
28
Constantin-Flavius Nistor Componente third-party
este „fmt” 29, anterior cunoscută sub numele de „cppformat”. Mai jos avem aceste trei metode de formatare comparate la capitolul de viteză în execuție, fiind executate de 2,000,000 de ori în timpul specificat:
Tabelul 2.5 – „Diferența vitezei de execuție dintre diverse metode de formatare”
Sursa: github.com/fmtlib/fmt/blob/master/README.rst
Din această librărie serverul folosește în principal funcția fmt::format 30, aceasta oferind diverse avantaje, ea îmbinând cele mai bune aspecte ale librăriilor standard din C și C++, dar având și alte avantaje. Astfel, cele mai importante avantaje ale acestei librării sunt:
Posibilitatea deducerii automate a tipului (precum funcționalitatea valabilă în librăria „sstream” din C++). În unele cazuri se poate specifica manual tipul valorii date. Un exemplu
pentru deducerea automată a tipului, placeholderii fiind umpluți automat cu parametrii adiționali în ordinea dată poate fi linia „string = fmt::format( „{}{}{} {} – {}”, „Hello”, „ world”, ‘!’, 5, 3.6 );”, care setează variabila dată („string”, de tipul std::string) ca fiind textul „Hello world! 5 – 3.6”. Se poate forța deducerea unui tip într-un mod asemănător cu funcțiile pentru formatare valabile în standardul limbajului C, astfel încât dacă în interiorul ghilimelelor specificăm „:d” atunci valoarea dată va fi forțată să facă o conversie (”cast”) către o valoare numerică naturală. De exemplu, dacă în loc de ultimul placeholder „{}” (căruia i se dă valoarea „3.6”) scriem „{:d}”, atunci în loc de „3.6” textul adăugat va fi „3”, deoarece prin conversia unui număr real către un număr natural se preia doar partea întreagă.
Precizia și alte setări se pot seta similar sintaxei din librăria „cstdio” provenită din
limbajul C, variantă care realizează acest lucru într-un mod mai scurt. Acestea se pot specifica după introducerea caracterului „:” (două puncte). Pentru exemplul dat anterior, varianta de afișare a aceluiași text cu această librărie se poate face folosind linia „string = fmt::format( „{:.2f}”, 3.141592 );” care pune șirul de caractere „3.14” în variabila dată.
Un alt mare avantaj care nu este existent în celelalte două librării este faptul că în placeholderi se poate specifica numărul parametrului adițional pe care să-l preia pentru a
Site dezvoltare: github.com/fmtlib/fmt
Sintaxă: fmtlib.net/latest/syntax.html
29
Constantin-Flavius Nistor Componente third-party
umple locul respectiv cu valoarea acestuia. De exemplu, o poziție de parametru poate fi folosită de mai multe ori, spre deosebire de metodele valabile nativ în C și C++. De exemplu, pentru a stoca textul „5.5…” într-o variabilă putem utiliza linia „string = fmt::format( „{0}{1}{0}{1}{1}{1}”, 5, „.” );”.
Se poate crea, cu ajutorul acestei librării, și o formatare personalizată, în cazul în care se dorește specificarea ca valoare a unui obiect dintr-o clasă care nu se află deja în librăriile
standard ale limbajului C++, acest lucru fiind posibil, de asemenea, și pentru formatarea cu librăria standard „sstream”. Acest lucru se poate realiza, de exemplu, pentru o clasă pentru culori, unde atributul „color” este codul pentru culoare. SA-MP folosește formatul „{RRGGBB}” (”RR” fiind cantitatea de roșu, „GG” de verde, iar „BB” de albastru, acestea fiind cele trei culori prime din care sunt compuse toate celelalte culori), aceasta fiind o culoare în format hexazecimal 31, pentru a implementa o culoare într-o anumită parte a unui text (numit „color embedding” – „încorporarea culorii” 32), formatul acesta fiind valabil în mesajele din chat, dialog-uri, 3DTextLabel-uri, texturile obiectelor și numerele de înmatriculare ale vehiculelor. Din cauza faptului că SA-MP are nevoie de caracterele „{ „ și „}” pentru a ști exact unde începe și unde se termină specificarea unei culori și din cauza faptului că librăria fmt folosește aceleași două caractere pentru delimitarea unui placeholder, suntem nevoiți să specificăm acele două caractere de trei ori fiecare: primele două sunt pentru a introduce unul singur (această acțiune numindu-se „escaping” 33), în acest mod transmițând librăriei că se dorește introducerea acelui caracter în format și că se dorește ca acesta să facă parte dintr-un placeholder, iar a 3-a oară este pentru a fi folosit de către placeholder. Această formatare personalizată trebuie să se realizeze într-o funcție din namespace-ul „Utility”, noi având deja o clasă Utility::Color care are un atribut „color” de tip „unsigned int”:
Tabelul 2.6 – „Formatarea personaliză cu ajutorul librăriei fmt”
void Utility::format_arg( fmt::BasicFormatter< char > &f, const char *& format_str, Utility::Color c ) {
f.writer( ).write( "{{{:06X}}}", c.color >> 8 );
}
Mai multe informații: wikipedia.org/wiki/Hexadecimal
Mai multe informații: wiki.sa-mp.com/wiki/Colors_List#Color_embedding
Mai multe informații: wikipedia.org/wiki/Escape_character
30
Constantin-Flavius Nistor Componente third-party
Datorită acestui mod de a formata, vom avea posibilitatea să includem într-un text multiple culori cu ușurință, ca de exemplu: „string = fmt::format( „{}Hello {}world!”, Utility::Color( 0xFF0000FF ), Utility::Color( 0xFFFF00FF ) );”, după primul placeholder textul fiind roșu, iar după al doilea acesta fiind galben. În primul caz se poate observa că apelăm constructorul clasei „Utility::Color” cu un parametru, acel parametru fiind culoarea în format „0xRRGGBBAA”. „0x” este utilizat pentru că în sintaxa limbajului C++ este nevoie de acest prefix în cazul pasării unei valori în format hexazecimal, iar „AA” este necesar pentru a specifica transparența culorii, deși formatarea personalizată de mai sus o ignoră complet din cauza mutării la dreapta cu 8 biți (”>> 8”, aceștia reprezentând transparența în format binar), nivelul de transparență nefiind utilizat în asemenea texte. În schimb, nivelul de transparență este utilizat în alte cazuri, de exemplu atunci când SA-MP oferă anumite funcții care acceptă pasarea specială ca parametru a unui număr care reprezintă o culoare. În acei parametri speciali în API-ul SA-MP-ului este obligatoriu să se conțină și 8 biți pentru transparență, pentru că altfel nu mai este reprezentată corect culoarea, nivelul „RR” de culoare fiind interpretat ca „00”.
Pentru a evita construcția unui obiect de fiecare dată când este necesară o culoare, de obicei fiind folosite aceleași câteva culori, pentru păstrarea consistenței vizuale, s-a putut construi o structură cu cele mai folosite culori de pe server, fiecare element din această structură fiind un obiect de tip Utility::Color, cu ajutorul căreia s-a construit un obiect „Utility::col” care permite utilizarea culorilor prestabilite sub forma „Utility::col.NUME” pentru o specificare rapidă. De exemplu, în loc de a specifica Utility::Color( 0xFF0000FF ) ca valoare, se poate specifica Utility::col.RED, având două avantaje principale: nu este nevoie să se țină minte codul culorii și nu se va mai construi un obiect temporar de fiecare dată când este utilizată o culoare folosită des, astfel îmbunătățind timpul de execuție.
2.5 Plugin-ul sscanf
Datorită faptului că jucătorii trebuie să ofere date de intrare (”input”) atunci când doresc să scrie anumite comenzi mai complexe sau să completeze anumite dialog-uri de tip DIALOG_STYLE_INPUT, serverul trebuie să știe să extragă fiecare informație din aceste date.
Pentru a interpreta un șir de caractere cu fiecare informație extrasă într-o variabilă de tip corect cu ajutorul librăriei standard a limbajului C++, s-ar putea folosi librăria „sstream” care permite o rezolvare precum „sirText >> numeJucator >> numarZile >> motiv;”, acolo unde „sirText” este un std::stringstream care conține textul din care să se extragă informația (ca de exemplu „jucător 5 motiv”), „numeJucator” fiind un std::string care va trebui să conțină numele exact sau parțial al jucătorului (după care va trebui interpretat de server printr-o buclă
31
Constantin-Flavius Nistor Componente third-party
prin toți jucătorii), iar „motiv” fiind tot un std::string care va conține partea de motiv din text. Această linie nu oferă suport pentru cazul în care jucătorul n-ar introduce corect toți cei trei parametri despărțiți de un spațiu gol, iar dacă acest lucru s-ar întâmpla ar putea apărea probleme neprevăzute, precum utilizarea unor alte valori. Pentru a oferi suport pentru posibilitatea unui input invalid sunt necesare alte linii care pot include o buclă de tip while și o condiție if, fiind o metodă de rezolvare mai dificilă. De asemenea, această librărie din C++ este destul de lentă.
O altă variantă este funcția „sscanf” 34 (alături de alte variante de funcții similare, dar o vom utiliza pe aceasta ca exemplu) din librăria standard „stdio.h” a limbajului C, valabilă în spațiul de nume „std” al limbajului C++ în librăria „cstdio”. Problema acestei librării este faptul că aceasta nu suportă noile tipuri de date pe care le oferă limbajul C++, precum std::string. Un alt dezavantaj al acestei funcții este faptul că aceasta acceptă doar pointeri (adrese de memorie) către variabile, tipul variabilei respective specificat în al doilea parametru al funcției, care este formatul pe care-l respectă textul dat în primul parametru, pointerii începând a fi dați din al treilea parametru. Linia de cod dată ca exemplu în paragraful anterior poate fi recreată utilizând această funcție în modul următor: „std::sscanf( sirText, „%s %d %s”, &numeJucator, &numarZile, &motiv );”, acolo unde toate cele trei variabile care erau de tip std::string sau std::stringstream fiind acum transformate într-un șir de caractere de tip char *. Avantajul acestei funcții este faptul că returnează numărul de argumente specificate corect, astfel încât dacă jucătorul nu oferă nici o informație, aceasta va returna 0, iar dacă oferă formatul corect aceasta va returna 3. Similar, există alte librării create de către diverși programatori în limbajul C++, dar cele mai populare sunt și ele destul de dificil de utilizat, din cauza necesității unor linii de cod complexe.
Datorită valorii care este returnată de către funcție, având posibilitatea de a introduce apelurile către aceasta în condiții de tip if în care se verifică dacă valoarea returnată este egală cu numărul de parametri dat în format (un alt dezavantaj este faptul că pentru această verificare trebuie specificat manual numărul de argumente dorite), dar și a modului rapid de a specifica formatul dorit (spre deosebire de C++, care pentru un format mai avansat necesită cod mai lent, mai divers și mai lung), această funcție este mult mai ușor de utilizat spre deosebire de varianta din C++, aceasta fiind aproape ideală pentru utilizare.
Însă, în comunitatea SA-MP un alt membru a decis să creeze un plugin 35 pentru recrearea comportamentului funcției sscanf din limbajul C, care este denumit precum funcția
Mai multe informații: en.cppreference.com/w/c/io/fscanf
Site dezvoltare: github.com/maddinat0r/sscanf
32
Constantin-Flavius Nistor Componente third-party
respectivă, deoarece API-ul oferit de către SA-MP nu oferă nimic asemănător, acesta fiind foarte limitat. Parametrii utilizați de această funcție sunt în aceeași ordine cu cei din funcția din limbajul C, singura diferență fiind modul de a specifica formatul, diferențele principale ale acestuia fiind faptul că nu se mai folosește caracterul „%” („procent”) pentru a specifica un nou element al formatului (aceștia fiind numiți „specifiers” – „specificatori”) și, de asemenea, faptul că nu se mai folosește nici delimitatorul direct între parametri. Spre deosebire de funcția din C, această funcție returnează 1 în caz că nu s-a utilizat un format corect, iar 0 în caz contrar, astfel eliminând nevoia de a specifica manual numărul de parametri utilizați de format. Un alt avantaj important este faptul că această funcție suportă un specificator care este specializat pentru SA-MP, fiind folosit foarte mult de acest server, specificator care permite oferirea unui ID de jucător, a unei părți din nume sau a numelui complet, rezultatul acestui specificator fiind stocat într-o variabilă de tip „int” care va fi ID-ul de jucător, eliminând nevoia unor bucle adiționale pentru concluzionarea acestei valori. Dacă informația respectivă din input nu există la nici un jucător conectat, fiind specificat un jucător invalid, valoarea din variabila respectivă va deveni INVALID_PLAYER_ID, ceea ce va permite o verificare simplă de forma „if( idJucator == INVALID_PLAYER_ID ) return SendError( playerid, „Jucătorul introdus nu există!” );”, astfel eliminând nevoia de utilizare a funcției IsPlayerConnected din API-ul SA-MP, care ar fi fost necesară în cazul specificatorului „d” pentru un simplu ID de jucător, care este mai lentă decât o comparație directă a unei valori cu o constantă. Această funcție este apelată din acest server care folosește C++, ci nu Pawn, limbaj pentru care este făcut plugin-ul respectiv, folosind, iarăși, funcțiile sampgdk::FindNative și sampgdk::InvokeNative. Funcția este valabilă pentru apel în server ca Plugins::SSCANF::unformat, acest nume de funcție fiind o alternativă valabilă și în plugin-ul sscanf pentru a evita confuzia dintre această funcție și funcția sscanf din C care are un alt tip de format.
Astfel, pentru recrearea aceleiași linii de cod pentru extragerea a trei parametri dintr-un input, vom avea următoarea linie de cod: „Plugins::SSCANF::unformat( sirText, „uds[128]”, &idJucator, &numarZile, &motiv );”, unde 128 este lungimea maximă a motivului care poate fi specificat, iar delimitatorul implicit este „ „ (”spațiu”), acesta fiind posibil a fi schimbat în alt caracter, precum „|”, în același format cu ajutorul specificatorului „p”, în modul următor: „p<|>uds[128]”. Un șir de caractere (”s”) este recomandat să fie ultimul parametru atunci când delimitatorul este lăsat implicit din cauza faptului că acesta consumă toate caracterele introduse până când atinge limita specificată de caractere.
Pentru ca un specificator să fie opțional, acesta trebuie să fie scris cu literă mare, iar în paranteze rotunde să fie scrisă valoarea implicită, în caz că acesta nu se introduce, ca de
33
Constantin-Flavius Nistor Componente third-party
exemplu „D(0)” care acceptă opțional un număr care dacă nu este introdus va stoca valoarea 0 în adresa care se specifică în parametrul adițional.
Această funcție este utilizată în majoritatea comenzilor și răspunsurilor dialog-uri de tip DIALOG_STYLE_INPUT care necesită un input parametrizat, cu excepția celor care necesită doar un șir de caractere, acesta fiind preluat direct. Exemple de specificatori și formate utilizate des:
2.6 MySQL și plugin-ul MySQL
Pentru ca serverul să poată stoca pe termen lung anumite informații, acesta are nevoie de o bază de date. O bază de date ne ajută să manipulăm informațiile stocate în diverse moduri cu o foarte mare lejeritate. Acest server încearcă să stocheze cât mai multe informații în baza de date, serverul având abilitatea de a o extinde, de a o interoga și de a îi prelua rezultatele prin simple adaptări ale codului în C++, pentru a putea interpreta corect informațiile din aceasta. De asemenea, pentru o stocare eficientă, se pot utilizata anumite tipuri de reprezentări speciale ale unor informații, precum un „unix timestamp” 36 (numărul de secunde din prima zi a anului 1970 și până la momentul dat) de tip numeric („INT”) pentru stocarea timpului, care spre deosebire de alte metode de reprezentare, precum cele text, ocupă doar 32 de biți și conține orice informație a timpului la o precizie de o secundă.
Sistemul de gestiune al bazei de date care este utilizat de către acest server este Oracle MySQL 37, datorită faptului că acesta oferă foarte multe beneficii, dar și a faptului că în comunitatea SA-MP acesta este foarte bine suportat, existând cel puțin un plugin capabil să ofere suport excelent pentru acesta. Un asemenea plugin este folosit și de către acest server, el
Mai multe informații: wikipedia.org/wiki/Unix_time
Site: mysql.com
34
Constantin-Flavius Nistor Componente third-party
având numele de „MySQL Plugin” 38. Spre deosebire de „Streamer Plugin”, acesta a fost inclus direct în sursa serverului, dar cu anumite modificări, din cauza anumitor incompatibilități din plugin-ul MySQL, acesta folosind anumite moduri de afișare a informațiilor care compatibile doar cu limbajul Pawn, care folosite prin intermediul unui alt plugin fac serverul să oprească rularea brusc („server crash”). Spre deosebire de simpla utilizare a plugin-ului MySQL, pentru că sursa acestuia este inclusă direct în server, funcțiile acesteia sunt mai rapide, datorită faptului că se elimină pasul de găsire și execuție a funcției exportată pentru Pawn din limbajul C++. De asemenea, pentru funcțiile de încărcare a informațiilor din baza de date se profită de supraîncărcarea unei funcții („function overloading” 39, procesul prin care se pot crea mai multe funcții în același spațiu de nume, singura diferență dintre ele fiind numărul și/sau tipul parametrilor acceptați, două funcții neavând posibilitatea să difere doar prin valoarea returnată), pentru a evita necesitatea unui nume diferit de funcție pentru fiecare tip de date dorit la încărcare. De exemplu, prin intermediul Pawn se putea încărca o informație dintr-un set de rezultat din baza de date cu ajutorul unor funcții numite cache_get_value_index/name, cache_get_value_index/name_int, cache_get_value/name_float. Cu ajutorul modificărilor realizate sursei plugin-ului MySQL, singurul nume de funcție disponibil pentru aceste acțiuni de încărcare este Plugins::cMySQL::Cache::get_value (namespace-ul „cMySQL” venind de la „custom MySQL”), care poate primi ca parametru pasat prin referință („pass-by-reference”, pentru ca schimbarea în funcție a valorii unui parametru să se reflecte și în afara funcției) un int, un float, un char sau un std::string, tip pentru care plugin-ul nu oferă suport nativ. În aceste funcții, primul parametru este întotdeauna ID-ul rândului de la care să preia valoarea, acesta pornind de la 0, al doilea parametru este numele coloanei sau ID-ul acesteia, pornind de asemenea de la 0, iar al treilea parametru este cel pasat prin referință.
De asemenea, există și conectorul MySQL pentru limbajul C, oferit chiar de către Oracle, dar acesta oferă doar minimul necesar, fără optimizări sau funcții care să ajute prea mult, fiind destul de dificilă utilizarea acestuia. De asemenea, plugin-ul MySQL este făcut doar pentru C++, nefiind compatibil cu limbajul C, iar acesta încearcă să profite cât mai mult de C++, în special de librăriile din standard care pot ușura și eficientiza lucrurile. Acest plugin folosește la bază, desigur, conectorul MySQL oferit de către Oracle, pe care-l extinde.
Cu ajutorul plugin-ului MySQL, ne putem conecta la o bază de date folosind funcția Plugins::cMySQL::connect, care returnează un ID al conexiunii, dacă s-a efectuat cu succes.
Site dezvoltare: github.com/pBlueG/samp-mysql-plugin
Mai multe informații: wikipedia.org/wiki/Function_overloading
35
Constantin-Flavius Nistor Componente third-party
Există trei metode de a trimite interogări către baza de date cu ajutorul plugin-ului MySQL, toate acceptând ca prim parametru ID-ul unei conexiuni create anterior:
Plugins::cMySQL::query („Unthreaded Query”). Această funcție face uz de firul principal de execuție al serverului. Funcția primește interogarea, după care încearcă s-o
trimită către baza de date, netrecându-se mai departe în execuția codului până nu primește răspunsul. Acest lucru înseamnă că dacă o interogare durează mai mult din diverse motive aceasta va bloca toată sincronizarea dintre jucători sau executarea altor părți ale serverului. Acest tip de interogare este recomandată pentru a fi folosită în principal doar atunci când serverul se inițializează, deoarece atunci nu se află nici un jucător pe server, iar cei care încearcă să se conecteze sunt puși să aștepte până se inițializează complet serverul. Această funcție returnează un ID de set de rezultat („result set”), în cazul în care s-a specificat un parametru „use_cache” ca „true” (aceasta fiind și valoarea implicită) pentru a anunța plugin-ul că este nevoie de salvarea rezultatului. Pentru a se folosi funcțiile obișnuite pentru prelevarea rezultatelor asupra acestui rezultat de interogare, valoarea returnată trebuie salvată într-o variabilă, memoria alocată setului de rezultat cu ID-ul respectiv trebuind să fie scoasă din memorie după procesarea sa, pentru a evita ocuparea inutilă a memoriei de către o informație care probabil nu va mai fi accesată (astfel fiind posibilă pierderea memoriei – „Memory Leak” 40). În cazul în care nu se dorește ștergerea setului de rezultat, acesta poate fi setat pentru prelucrare ulterior cu ajutorul funcției Plugins::cMySQL::Cache::set_active, singurul parametru dorit fiind ID-ul setului de rezultat creat anterior. În cazul de față, această funcție este folosită, în mare parte, doar atunci când se inițializează serverul, adică atunci când este apelat evenimentul OnGameModeInit oferit de API-ul SA-MP.
Plugins::cMySQL::tquery („Threaded Query”). Această funcție folosește un singur alt
fir de execuție, diferit de cel principal al serverului, fir de execuție care a fost creat anterior în timp ce s-a executat funcția Plugins::cMySQL::connect. Prima diferență dintre această funcție și cea anterior menționată este faptul că aceasta nu mai blochează firul principal de execuție al serverului în timp ce se așteaptă rezultatul, ceea ce înseamnă că jucătorii se vor actualiza normal, fără a observa anumite momente mai îndelungate de desincronizare între ei și server. A doua diferență este faptul că această funcție necesită ca parametru o funcție pe care s-o execute atunci când rezultatul s-a primit, dar de asemenea se poate specifica și un număr nelimitat de parametri adiționali pentru apelarea cu aceștia a funcției anterior specificate. Atunci când această funcție este folosită, serverul adaugă interogarea, funcția și
Mai multe informații: wikipedia.org/wiki/Memory_leak
36
Constantin-Flavius Nistor Componente third-party
parametrii specificați într-o coadă („queue”, fiind o structură de date de tip cunoscut ca „FIFO”, abreviere a „First-In-First-Out”, ceea ce înseamnă „Primul venit, primul ieșit”, tipul respectiv din C++ fiind std::queue) a acelui fir de execuție, care este luată la rând de fiecare dată când funcția care rulează pe acel fir de execuție este în curs de executare, acolo fiind interogarea trimisă către baza de date, firul de execuție respectiv fiind blocat până vine răspunsul, dar nu este blocat și firul principal de execuție al serverului. Atunci când rezultatul este transmis de către baza de date, funcția respectivă din firul de execuție creat execută în firul principal de execuție al serverului funcția specificată pentru interogarea respectivă, serverul având posibilitatea de a interpreta rezultatul interogării în această funcție.
Plugins::cMySQL::pquery („Parallel Query”). Această funcție poate fi gândită ca un
vector de fire de execuții identice cu cel din Plugins::cMySQL::tquery. Există posibilitatea ca în funcția Plugins::cMySQL::connect să se specifice un parametru numit „pool_size”, care creează numărul specificat de fire de execuție. Avantajul față de funcția precedentă este faptul că se pot trimite mai multe interogări diferite care pot pune în lucru baza de date simultan, fără să existe o singură coadă, ci existând atâtea câte fire de execuție s-au specificat în funcția de conectare. Pe acest server această funcție este folosită aproape în fiecare caz, datorită diferenței mari de performanță resimțită.
2.7 Localizarea geografică
Aceasta se realizează cu ajutorul produsului „GeoLite Country” care este oferit gratuit de către compania „MaxMind”. Acesta este în esență o bază de date cu toate intervalele de adrese IPv4 („Internet Protocol version 4” 41) din lume și cu țările în care sunt folosite. Adresele IP sunt stocate ca tip numeric fără semn („UNSIGNED INT”, deci poate fi doar pozitiv), datorită faptului că acesta are 4 părți, fiecare fiind stocată pe 8 biți (un byte/un octet). Pentru a obține un asemenea număr care reprezintă o adresă IP se poate folosi operația pe biți „OR” (exemplu adresă IP: „93.119.26.22”, pentru a obține numărul pe 32 de biți se pot folosi operațiile „22 | ( 26 << 8 ) | ( 119 << 16 ) | ( 93 << 24 )” care rezultă numărul „1568086550”) sau funcția de conversie INET_ATON din MySQL.
Pentru a stoca toate informațiile, în baza de date oferită există două tabele care au fost transferate în baza de date deja existentă a serverului, pentru a evita necesitatea unei conexiuni la două baze de date.
Mai multe informații: wikipedia.org/wiki/IPv4
37
Constantin-Flavius Nistor Componente third-party
Aceste două tabele sunt următoarele:
„geoip”. Acesta conține intervalele de adrese IP alături de codul ISO 3166 42 (un cod
care este mereu format din două litere, în tabel având tipul „CHAR(2)”) al țării în care este folosit acel interval. Există două adrese IP stocate: începutul intervalului (numit „Start”) și finalul (numit „End”), ambele de tip „UNSIGNED INT”. Pentru a se afla intervalul și țara unei adrese IP se poate executa o interogare MySQL precum „SELECT * FROM `geoip` WHERE INET_ATON( '93.119.26.22' ) BETWEEN `Start` AND `End`”, acolo unde „93.119.26.22” este adresa IP pentru care se dorește aflarea țării de origine, iar rezultatul poate arăta în următorul mod:
Tabelul 2.8 – „Exemplu de rezultat de interogare pentru aflarea locației unui IP”
„geoip_countries”. Acesta conține codul țării, fiind cheia primară, de același tip ca în tabelul anterior („CHAR(2)”), având numele „Code”, iar de asemenea acesta conține și numele complet al țării („VARCHAR(50)”), având numele „Name”. S-a recurs la această metodă de a stoca țările intervalelor deoarece primul tabel are peste 150,000 de înregistrări, iar dacă fiecare dintre acestea ar stoca numele complet al țării ar ocupa mult mai mult spațiu, această stocare având nevoie de normalizare 43. Câteva exemple de înregistrări:
Detalii adiționale pentru codurile țărilor: dev.maxmind.com/geoip/legacy/codes/iso3166
Importanța normalizării unei baze de date: wikipedia.org/wiki/Database_normalization
38
Constantin-Flavius Nistor Prezentare
3 Prezentare
În acest capitol se vor prezenta funcționalitățile principale ale serverului, atât pe partea de cod și de bază de date, dar și de joc („gameplay” – cum acționează respectivele ). Acestea vor folosi ca bază tot ceea ce s-a descris anterior.
3.1 Interacționarea cu jucătorii
3.1.1 Identificarea jucătorilor
Pentru a putea identifica jucătorii, în API-ul SA-MP-ului există 3 funcții utile:
GetPlayerName. Fiecare jucător poate specifica în navigatorul SA-MP un nume cu care se poate conecta la servere, nume care poate conține doar caracterele a-z, A-Z, 0-9, [, ], (, ),
$, @, ., _ și =. Din momentul în care un jucător s-a conectat cu succes la server, acesta preia numele introdus și îl stochează. Funcția respectivă acceptă ca parametru un ID de jucător, o variabilă pasată prin referință, care va conține numele ID-ului de jucător specificat, dar și un parametru care specifică lungimea maximă a numelui care se dorește a fi stocat în variabila pasată. Un jucător se poate conecta cu un nume de lungime maximă 20, dar poate avea un nume de lungime maximă 24 (definiția MAX_PLAYER_NAME) în caz că serverul folosește funcția SetPlayerName asupra sa. Numele unui jucător se poate schimba pe server doar în acel caz, altfel numele pasat de funcția GetPlayerName este mereu același. Șirurile de caractere („string”) din Pawn sunt similare celor din limbajul C, adică ele au mereu la final un caracter care are codul 0 din sistemul ASCII („American Standard Code for Information Interchange” 44, fiind o modalitate de codificare a caracterelor, fiecare caracter având o valoare numerică pornind de la 0, SA-MP folosind doar caracterele din acest sistem de codare, fără a suporta caracterele care nu se află în acesta), care reprezintă caracterul nul („NULL”), acesta neavând nici o reprezentare vizuală și care are ca scop identificarea sfârșitului unui șir de caractere (de aceea se poate numi și „caracterul terminal”), fără a fi nevoie de o variabilă separată pentru reținerea lungimii curente a unui șir de caractere. Acest caracter se stochează la finalul fiecărei variabile care conține un astfel de tip de reprezentare a șirurilor de caractere. Din acest motiv, dacă un nume are lungimea maximă de 24, atunci funcția GetPlayerName va avea nevoie de valoarea 24+1, adică 25, pentru a putea reține corect toate caracterele numelor care ating această limită, alături de caracterul nul. Funcția returnează lungimea numelui în caz că aceasta s-a executat cu succes, sau valoarea 0 în caz
Mai multe informații: wikipedia.org/wiki/ASCII
39
Constantin-Flavius Nistor Prezentare
contrar, atunci când ID-ul de jucător specificat nu este conectat. De reținut este faptul că oricine poate încerca să se conecteze cu orice nume, exact ca în cazul tentativelor de autentificare pe site-uri.
GetPlayerIp. Pentru a putea fi identificat pe internet, oricine are o adresă IP. Un server
de SA-MP funcționează doar prin intermediul adreselor IPv4, acesta nesuportând standardul IPv6, care este mai nou. Această adresă IPv4 poate avea maxim 3 cifre pentru fiecare dintre cele 4 părți ale sale, alături de cele 3 puncte care despart toate părțile, astfel ajungând la un maxim de 15 caractere, deci mărimea maximă care trebuie trimisă în funcție trebuie să fie de 15+1, adică 16, pentru ca terminatorul anterior descris să se poată introduce. Această funcție are comportamentul funcției precedentei, serverul preluând IP-ul jucătorului atunci când acesta se conectează, iar funcția returnează aceeași adresă cât timp jucătorul respectiv este conectat. Un lucru de reținut și aici este faptul că un jucător își poate schimba adresa IP prin trei metode principale: prima fiind posibilă în cazul în care acesta are un ISP („Internet Service Provider” – firma care oferă conexiunea la internet) care permite ca adresele IP să se schimbe o dată cu reconectarea la internet, utilizatorul având posibilitatea, de exemplu, să restarteze router-ul și să aștepte reconectarea la el. O a doua metodă este alăturarea la o altă conexiune de internet, fie ea din aceeași casă, de la un prieten sau de la o altă locație. O a treia metodă este utilizarea unui VPN („Virtual Private Network” 45), acesta permițând jucătorului să folosească o altă conexiune de internet, de obicei oferită și de firme specializate, schimbând adresa IP a jucătorului, făcând inclusiv serverele să vadă doar adresa IP la care sunt conectați de la distanță. Această ultimă metodă poate să facă un jucător să fie detectat ca fiind din Japonia sau din oricare altă țară, dar din cauză că acea rețea nouă de internet poate fi la foarte mare depărtare, jucătorii pot avea o conexiune foarte slabă la server deoarece se adaugă un nou nod în transmisia datelor între server și jucător.
gpci. Acesta este un cod (îl vom numi „serial”) obținut probabil cu o funcție „hash” (transformă un text în alt text, cu o altă formă, ireversibil, pentru a nu putea fi citit). Nu se
cunoaște exact modul în care este obținut, dar se pare că depinde de locația de instalare a clientului SA-MP și de numele contului curent al sistemului de operare (Microsoft Windows). Acesta se pare că poate avea între 36 și 40 de caractere. Această funcție este de asemenea similară cu cele două de dinainte. De reținut despre această funcție este faptul că mai mulți jucători pot avea același serial, ceea ce înseamnă ca nu se pot diferenția doi jucători doar pe baza acestei informații, iar el poate fi falsificat cu ușurință.
Mai multe informații: wikipedia.org/wiki/Virtual_private_network
40
Constantin-Flavius Nistor Prezentare
3.1.2 Înregistrarea și autentificarea
Cel mai important aspect al unui server este probabil stocarea informațiilor utilizatorilor săi, pentru ca aceștia să poată reveni fără să piardă progresul făcut anterior. Conturile jucătorilor reprezintă cea mai importantă parte a bazei de date, majoritatea tabelelor acesteia fiind dedicate diverselor informații ale conturilor. Jucătorii nu pot continua pe server dacă nu sunt înregistrați, din cauză că toate informațiile acestora sunt memorate în baza de date cu ajutorul ID-ului contului.
De reținut este faptul că oricine se poate conecta pe server cu orice nume, de aceea fiind nevoie să se stabilească diverse metode pentru securitatea contului.
Când un jucător se conectează la server, acesta verifică dacă numele jucătorului există deja în baza de date, astfel încât:
Dacă nu este deja înregistrat, iar jucătorul încearcă să apese butonul de spawn, serverul
îi va afișa un dialog care conține regulile serverului și în care poate scrie o parolă cu care să se acceseze numele respectiv pe viitor:
Figura 3.1 – „Dialog-ul de înregistrare”
41
Constantin-Flavius Nistor Prezentare
După ce scrie o parolă care să respecte limita de 4-20 caractere și apasă pe butonul de
înregistrare („Register”), serverul va verifica dacă în ultimele 24 de ore s-au creat mai mult de 3 conturi de pe IP-ul jucătorului, pentru a nu oferi jucătorilor posibilitatea de a crea conturi multiple. Acest lucru este realizat cu ajutorul interogării MySQL „SELECT `Name`, `RegisteredDate` FROM `a_accounts` WHERE `IP` = INET_ATON( '{}' ) AND `RegisteredDate` > ( UNIX_TIMESTAMP( ) – ( 60 * 60 * 24 ) )” în care coloanele „Name” (numele conturilor găsite) și „RegisteredDate” (unix timestamp-ul momentelor înregistrării conturilor) sunt selectate din tabelul „a_accounts”, acesta fiind tabelul principal al conturilor jucătorilor, care conține numele, parola, momentul înregistrării și multe altele, pentru a putea afișa jucătorului într-un alt dialog toate conturile înregistrate în ultimele 24 de ore. În interogarea respectivă, funcția INET_ATON este cea descrisă anterior, care transformă adresa IPv4 specificată ca parametru într-un număr, adresele IP fiind stocate doar sub formă de număr pentru a eficientiza stocarea, iar UNIX_TIMESTAMP fiind funcția care returnează unix timestamp-ul momentului execuției interogării, iar „60 * 60 * 24” fiind calcularea ultimelor 24 de ore în secunde, pentru a selecta doar conturile care sunt înregistrate în ultimele 24 de ore.
Dacă totul este în regulă, serverul va introduce o înregistrare în tabelul „a_accounts” cu o parolă goală, care după ce se execută va permite interpretarea rezultatului care a fost trimis serverului. Acest tabel având o coloană numită „Key”, care este un număr incrementat automat („INT AUTO_INCREMENT”) și care este cheie primară, va putea fi preluată cu ajutorul funcției „Plugins::cMySQL::Cache::insert_id”, stocând-o în variabila „GZS::Player::Info[ playerid
].pKey” pentru a permite serverului să o utilizeze pentru identificarea ușoară a contului respectiv.
Pentru securitate, câmpul parolei a fost inițial lăsat gol. Deoarece acest server
utilizează o metodă mai securizată de a stoca parolele, acesta folosind funcția de tip „hash” numită „SHA-1” 46 (aceasta are și versiuni mai noi și mai securizate, dar versiunea de server MySQL folosită de majoritatea firmelor gazde este de obicei mai veche și nu le suportă, ele fiind introduse destul de recent în MySQL, iar din acest motiv serverul are mai multe metode de securitate a parolelor), care transformă parola într-un alt text, ireversibil, deci nu se poate obține printr-o metodă inversă parola introdusă de jucător.
O altă problemă de securitate a acestor tipuri de funcții este că ele mereu ajung la același text hash, deci dacă doi jucători au setat ca parolă textul „parola”, atunci această funcție
standard „SHA-1” va rezulta de ambele dăți hash-ul
Mai multe informații: wikipedia.org/wiki/SHA-1
42
Constantin-Flavius Nistor Prezentare
„83592796bc17705662dc9a750c8b6d0a4fd93396”. Dacă cineva rău intenționat pune mâna pe o bază de date cu multe parole criptate în acest mod, el va putea sorta ușor parolele după numărul de utilizări ale acestora și va încerca să ajungă la acel hash cu forța brută (va încerca să cripteze toate variantele de text până ce va ajunge ca una să aibă același hash precum cea pe care încearcă s-o afle, începând cu textele „a”, „b”, … „z”, „aa”, „ab” și eventual până la un text lung ca de exemplu „zzzzzzzzzzzzzz”, de asemenea combinând tot felul de caractere speciale, în acest caz din sistemul ASCII) sau va încerca să afle parolele comparându-le cu o altă bază de date care conține foarte multe texte care au avut deja funcția hash „SHA-1” executată pe ele și ale căror hash-uri au fost stocate pentru a evita necesitatea reaplicării funcției pentru calculare (acest mod de aflare a textului inițial fiind numit „rainbow table” 47), o funcție hash pentru a fi bună trebuind să aibă un timp de execuție mai mare, pentru a preveni pe cât posibil forța brută pe foarte multe texte).
Pentru a împiedica repetarea hash-ului a două parole identice, se poate genera un șir aleatoriu de caractere numit „salt” („sare”) 48, care cu cât este mai lung cu atât este mai bun, datorită unei posibilități mai mici de a repeta un șir de caractere generat înainte, care să fie alăturat parolei înainte de aplicarea funcției hash. Acest server generează un astfel de șir de caractere cu ajutorul funcției MySQL „UUID( )”, („Universally Unique Identifier” 49), care are aproximativ 36 de caractere în acest caz, o mare parte dintre acestea fiind generate aleatoriu, acest identificator având un format standard în care se include versiunea standardului funcției, dar și 4 cratime mereu în aceleași poziții, fiind bazat parțial pe timpul la care s-a executat funcția și care garantează o șansă de repetare a aceluiași șir aproape de 0. După ce este generat acest identificator, acestuia i se aplică, de asemenea, funcția SHA-1, acesta devenind salt-ul final, după care este stocat în baza de date și se începe generarea hash-ului final al parolei folosind concatenarea acestui salt de două ori în poziții diferite, o dată fiind cel inițial, pe care s-a aplicat funcția SHA-1 doar o dată, iar a doua oară aplicându-se încă o dată funcția pe salt-ul inițial, în acea concatenare aflându-se bineînțeles și parola introdusă de către jucător, căreia i s-a aplicat funcția pentru obținerea unui hash, dar și ID-ul contului asupra căruia s-a aplicat de asemenea funcția respectivă, din acest motiv prima dată în baza de date s-a setat contului o parolă goală, iar pe lângă toate acestea se mai adaugă și diverse caractere prestabilite. După toate acestea, asupra textului în care se află diversele elemente descrise anterior se aplică ultima dată funcția
Mai multe informații: wikipedia.org/wiki/Rainbow_table
Mai multe informații: wikipedia.org/wiki/Salt_(cryptography)
Mai multe informații: wikipedia.org/wiki/Universally_unique_identifier
43
Constantin-Flavius Nistor Prezentare
SHA-1, rezultând un alt hash care este în final stocat în câmpul „Password” al înregistrării respective din baza de date.
După aceea, serverul îi va afișa jucătorului un alt dialog, care îi va prezenta foarte pe scurt motivele pentru a juca pe server și un rezumat cu numele celor mai importante caracteristici ale serverului:
Figura 3.2 – „Dialog-ul cu informații despre server, după înregistrare”
După ce apasă pe butonul „OK”, serverul îi va cere jucătorului adresa email a sa pentru a putea fi contactat în caz de necesitate, acest lucru fiind opțional, existând un buton „Cancel” („Anulare”) pentru a nu transmite nici un email.
Figura 3.3 – „Dialog-ul de setare a adresei de email”
44
Constantin-Flavius Nistor Prezentare
Dacă jucătorul decide să apese pe butonul de setare („Set”), atunci serverul va face o analiză rapidă a formatului adresei introduse pentru a verifica dacă acesta este valid, astfel diminuând șansele ca adresa email introdusă să fie falsă. Acest lucru se realizează cu ajutorul următoarei funcții:
Tabelul 3.1 – „Funcția pentru verificarea formatului unui email”
bool IsValid( const std::string &string )
{
static std::regex lPattern( "^([a-zA-Z0-9_.-]+)@([a-zA-Z0-9_.-]+)\\.([a-zA-Z0-9]{2,6})$" );
std::smatch lMatches;
if( !std::regex_search( string, lMatches, lPattern ) )
return 0;
std::string lDomain = lMatches[ 3 ].str( );
for( auto const &e: Utility::EMail::Internal::TopLevelDomains )
if( Utility::String::ci_equals( lDomain, e ) )
return 1;
return 0;
}
Această funcție folosește un model („pattern”) pasat ca parametru în constructorul unui obiect de tip std::regex („Regular Expression” – „Expresie Regulată” 50) din librăria „regex” care a apărut în standardul C++11 51. Acest model stabilește faptul că textul verificat în acest caz cu ajutorul funcției std::regex_search trebuie să aibă trei părți (pe care le va și stoca într-o variabilă de tipul std::smatch): numele pentru care este făcută adresa de email (permite doar caractere din intervalele a-a, A-Z, 0-9 și caracterele „_” („underscore”), „.” („punct”) și „-„ („cratimă”) ), numele domeniului care oferă serviciul de email (aceleași reguli de formatare precum prima parte), despărțit de un caracter arond („@”) și domeniul serviciului (între două și șase caractere în intervalele a-z, A-Z și 0-9), acesta fiind despărțit de un caracter de tip punct, iar dacă tot acest format nu este respectat funcția va returna 0. Altfel, funcția va continua și va verifica dacă domeniul există, ignorând dacă domeniul specificat ca parametru în funcție este scris cu litere mari sau mici (deci este o verificare de tip „case insensitive”), luând la rând, într-o buclă, toate elementele dintr-un vector care conține toate numele de domenii existente
Mai multe informații: wikipedia.org/wiki/Regular_expression
Mai multe informații: wikipedia.org/wiki/C++11
45
Constantin-Flavius Nistor Prezentare
(precum „ro”, „com”, „net”, „info”, „co.uk”, „asia” și așa mai departe). Această buclă este una bazată pe interval („range based for loop”), care a fost introdusă, de asemenea, în standardul C++11, fiecare element din vector fiind stocat într-o variabilă de tip „auto const &”, ceea ce înseamnă că acel element este pasat prin referință („&”) din vector, pentru a nu se construi încă un asemenea element, „const” („constant”) stabilește faptul că acesta nu are voie să se modifice în interiorul buclei, iar „auto” stabilește tipul obiectului la compilare, acest cuvânt cheie fiind introdus, de asemenea, în standardul C++11, tipul elementelor din acel vector fiind „std::string”. Dacă unul dintre aceste domenii este găsit funcția va returna 1, astfel bucla oprindu-se.
Revenind la celălalt caz, cel în care numele jucătorului este deja înregistrat, există două posibilități:
Dacă jucătorul are autentificarea automată activată pe cont (setare care se poate
schimba din panoul de control, asupra căruia vom reveni mai târziu) se va verifica dacă adresa IP stocată în contul respectiv este identică cu adresa IP curentă a jucătorului, iar dacă aceste două verificări sunt executate cu succes, jucătorul va fi autentificat automat, fără să mai trebuiască să introducă parola. În schimb, dacă acel cont n-are autentificarea automată activată sau ultima adresă IP folosită este diferită de cea actuală, serverul va executa procesele de la următorul punct.
În celălalt caz, al autentificării automate dezactivare sau a unei adrese IP
diferite, jucătorului i se trimite prima dată un mesaj că există deja un nume pe contul respectiv și că trebuie să se autentifice. În momentul când va apăsa pe butonul de spawn, acesta va fi forțat să se autentifice, fiindu-i afișat bineînțeles un alt dialog pentru acest lucru:
Figura 3.4 – „Dialogul de autentificare”
Dacă jucătorul apasă butonul „Quit” („Părăsește”) va fi dat afară temporar de pe server („kick”, asupra căruia vom reveni), având apoi posibilitatea de a se conecta cu un alt nume sau să reîncerce autentificarea pe acel nume.
46
Constantin-Flavius Nistor Prezentare
În schimb, dacă jucătorul apasă pe butonul „Login” („Autentifică-te”) și lungimea parolei nu este zero (altfel, încercarea curentă de autentificare va eșua direct, trimițând un mesaj de eroare) se va trimite o interogare MySQL care va prelua maxim o înregistrare (nefiind posibil ca mai multe înregistrări să aibă exact același nume) care are numele și parola specificate. Deoarece parola nu este stocată direct, ci este un hash al acesteia, în interogarea MySQL se va reproduce exact modul în care este alcătuit un astfel de hash de parolă de cont, pentru a se putea compara corect parolele. Datorită acestui mod de interogare, există două posibilități, ambele executând o funcție în serverul de SA-MP pentru a putea interpreta rezultatele, în această funcție interpretându-se numărul de înregistrări găsite cu ajutorul funcției Plugins::cMySQL::Cache::row_count, astfel:
Dacă nu se va găsi nici o înregistrare, din cauza faptului că numele contului existent în baza de date are un hash de parolă stocat care este diferit față de hash-ul
generat de parola introdusă anterior de jucător, poate însemna că jucătorul care s-a conectat cu acel nume încearcă să intre pe contul respectiv din simplă curiozitate, pentru a face rău sau se poate ca pur și simplu jucătorul să aibă un nume care a fost înregistrat anterior de către altcineva, fără a putea să-l creeze iarăși. Din cauza acestor posibilități, s-a adăugat o protecție cu ajutorul căreia un jucător nu poate încerca să nimerească parola unui cont de mai mult de 4 ori, astfel acesta luând kick.
Dacă se va găsi o înregistrare, acela fiind contul corect al jucătorului, atunci serverul va începe procesul final de autentificare al jucătorului. În primul rând, cu
ajutorul funcției supraîncărcate Plugins::cMySQL::Cache::get_value, serverul va prelua toate coloanele înregistrării corespondente contului din tabelul „a_accounts”, pe care le va stoca în variabilele care corespund, pentru a evita necesitatea interogării bazei de date de fiecare dată când este nevoie de una dintre acele informații, rezultatul din baza de date primindu-se foarte lent în comparație cu accesarea unei variabile. După aceea, serverul va trimite interogări adiționale în diverse tabele, informațiile jucătorului din fiecare tabel fiind identificate cu ajutorul ID-ului de cont creat la înregistrare, pentru a prelua toate informațiile necesare, precum casele și afacerile deținute, alertele, realizările sau alte aspecte ale serverului. De asemenea, tot la acest pas se trimit diverse mesaje informative despre faptul că jucătorul s-a autentificat, în caz că jucătorul este un V.I.P. („Very Important Player”, aspect asupra căruia vom intra în detaliu mult mai târziu).
47
Constantin-Flavius Nistor Prezentare
3.1.3 Regulile și consecințele încălcării acestora
Așa cum s-a specificat anterior, pentru a menține o atmosferă organizată între jucători, evitând posibilitatea ca ei să trișeze sau să provoace alte probleme, există un set prestabilit de reguli. Pentru a ne asigura că toți jucătorii le respectă este nevoie ca anumiți jucători foarte activi și de încredere să aibă mai multă putere, aceștia numindu-se administratori, având la dispoziție comenzi pe care le pot aplica asupra altor jucători, acestea fiind împărțite între cele 10 nivele existente de administrator, astfel încât dacă un administrator primește un nivel mai mare i se oferă acces la și mai multe comenzi, ca răsplată pentru activitate, implicare în administrarea serverului și dovadă de seriozitate.
Printre aceste comenzi există și diverse caracteristici ale serverului care îi ajută pe administratori să mențină ordinea fără să dea jucătorii afară de pe server, aceste caracteristici numindu-se „pedepse” („punishments”). Fiecare pedeapsă are un timp limită pe care se poate aplica, acesta fiind de 60 de minute. Ele sunt de următoarele tipuri:
Închisoare („Jail”). Acest tip de pedeapsă se poate aplica, de exemplu, de către un administrator atunci când un jucător conduce un vehicul în mod repetat peste un alt jucător, astfel încercând să-i scadă viața, sau în alte momente în care acesta încalcă diverse reguli ale
serverului. De asemenea, există și cazul în care jucătorul reușește să-l omoare pe celălalt cu vehiculul, serverul având posibilitatea de a detecta acest lucru utilizând evenimentul OnPlayerDeath care este apelat de către clientul jucătorului care a decedat, verificând parametrii evenimentului. Parametrul „playerid” conține ID-ul jucătorului decedat, iar parametrul „killerid” conține ID-ul jucătorului de care acesta a fost omorât (acesta luând valoarea 65535 în caz că n-a fost omorât de către un alt jucător, această valoare fiind reprezentată ca 0xFFFF în format hexazecimal și fiind valabilă în definiția INVALID_PLAYER_ID din API-ul SA-MP). De asemenea, parametrul „reason” conține un ID al motivului decesului, acesta fiind identic cu ID-urile armelor, dar având și ID-uri pentru alte motive, precum exploziile. Pentru acest caz se folosesc motivele cu ID-urile 49 și 50, primul motiv fiind pentru cazurile în care jucătorul este omorât direct de un vehicul după ce l-a călcat, iar al doilea fiind pentru cazurile în care este omorât de elicele unui elicopter, dorind să tratăm aceste două motive în mod similar. Pentru a se realiza această detectare, în evenimentul OnPlayerDeath avem următoarea porțiune, de asemenea dorind ca administratorii să nu fie afectați de această pedeapsă:
48
if( ( reason == 49 || reason == 50 ) && !( GZS::Player::Info[ killerid ].pPunishments & P_JAIL ) && GZS::Player::Info[ killerid ].pAdmin == 0 ) {
gString = fmt::format( "{}({}) {}is arrested for 1 minute because he vehicle-killed {}{}({}){}.", GZS::Player::Info[ killerid ].pPlayerName, killerid, Utility::col.WHITE, Utility::Color( sampgdk::GetPlayerColor( playerid ) ), GZS::Player::Info[ playerid ].pPlayerName, playerid, Utility::col.WHITE );
::Hooks::SendClientMessageToAll( Utility::Color( sampgdk::GetPlayerColor( killerid ) ), gString );
GZS::Player::Info[ killerid ].pPunishments |= P_JAIL;
GZS::Player::Info[ killerid ].pJail_Seconds = 60;
GZS::Player::Info[ killerid ].pJail_Timer = Timers::SetTimer( 1000, 1,
PunishmentUpdate, killerid, static_cast< int >( P_JAIL ) ); ::Hooks::SpawnPlayer( killerid );
}
Putem observa faptul că setăm pentru jucător doar variabilele necesare pentru ca serverul să știe că acesta trebuie să se afle în închisoare, după care creăm un timer cu un interval de o secundă în care se actualizează un textdraw de pe ecranul jucătorului pentru a îl informa la secundă despre timpul rămas până la eliberarea din închisoare. Apoi, forțăm serverul să transmită clientului faptul că acesta trebuie să reapară în lume, acesta executând evenimentul OnPlayerSpawn în care se are grijă de faptul că jucătorul trebuie să apară în închisoare. După ce jucătorul va apărea în închisoare, acesta nu va mai putea folosi majoritatea comenzilor. În codul de mai sus se mai poate observa și o mai bună exemplificare pentru librăria de formatare a textelor. Pentru a pune un jucător în închisoare administratorii au la dispoziție comanda ‘/jail’, iar aceștia pot scoate mai rapid de la închisoare un jucător înaintea expirării timpului cu ajutorul comenzii ‘/unjail’.
Încătușare („Cuff”). Această pedeapsă se poate aplica în aceleași cazuri cu pedeapsa
de închisoare, în cazul unor prime abateri. Acest tip este aplicat doar de către administratori, ei având la dispoziție comenzile ‘/cuff’ și ‘/uncuff’. Când un jucător este încătușat, acestuia i se va aplica o acțiune specială cu ajutorul funcției SetPlayerSpecialAction, căreia i se pasează ID-ul jucătorului pe care să se aplice, dar și un ID de acțiune 52, în acest caz fiind ID-ul 24, având la dispoziție și definiția SPECIAL_ACTION_CUFFED din API, care îi setează mâinile la spate. De asemenea, trebuie folosit un obiect atașabil de tip cătușe cu ajutorul funcției SetPlayerAttachedObject pentru a fi vizibil faptul că jucătorul nu poate
Listă acțiuni: wiki.sa-mp.com/wiki/SpecialActions
49
Constantin-Flavius Nistor Prezentare
mișca mâinile din poziție din cauza acestora. Când un jucător este încătușat, acesta nu va mai putea scrie comenzi și nici nu va mai putea sări. Pentru a afla timpul rămas până dispar cătușele, jucătorul poate scrie aproape orice comandă pentru a i se trimite un mesaj de eroare care conține timpul rămas. Atunci când sare, jucătorului i se va aplica o animație pentru împiedicarea acestui lucru, făcându-l să cadă, iar pentru această acțiune se folosește evenimentul OnPlayerKeyStateChange, care permite detectarea schimbării apăsării de butoane a unui jucător, având doi parametri „newkeys” (noile butoane apăsate) și „oldkeys” (vechile butoane apăsate) care reprezintă câte o mască de biți (deoarece nu toate butoanele tastaturii sunt detectabile, fiecare buton detectabil are un bit care-l reprezintă, fiind o limită de 32 de biți, acesta fiind setat ca 1 dacă jucătorul a apăsat într-adevăr și acel buton), din cauza faptului că un jucător poate apăsa mai multe butoane deodată 53, pe care serverul le va recepționa în același apel al evenimentului. Din cauza acestui fapt, trebuiesc folosite operațiile pe biți pentru a putea detecta apăsarea sau eliberarea unui buton. Pentru a detecta acțiunea de săritură, trebuie să ne asigurăm că jucătorul a apăsat butonul pentru a sări și că acesta nu este ținut apăsat, pentru a evita repetarea animației înainte ca aceasta să se oprească:
Tabelul 3.3 – „Oprirea săriturii unui jucător când este încătușat”
if( newkeys & KEY_JUMP && !( oldkeys & KEY_JUMP ) )
if( sampgdk::GetPlayerSpecialAction( playerid ) ==
SPECIAL_ACTION_CUFFED )
::Hooks::ApplyAnimation( playerid, "GYMNASIUM", "gym_jog_falloff", 4.1f, 0, 1, 1, 0, 0, 1 );
Amuțire („Mute”). Această pedeapsă are efect, în special, asupra chat-ului principal, cel care acționează evenimentul OnPlayerText atunci când este trimis un mesaj. La sfârșitul acestui eveniment, în caz că este returnată valoarea 1, se trimite pe tot serverul un mesaj formatat într-un mod implicit. Pentru a opri acest comportament al evenimentului acesta
trebuie să returneze 0. Acest server returnează mereu 0 în evenimentul respectiv pentru că a implementat propriul tip de mesaj (inclusiv alte tipuri de mesaje, precum mesajele pentru echipe sau altele). Înainte de a se trimite mesajele personalizate ale serverului, acesta va verifica dacă jucătorul este amuțit și nu va mai permite evenimentului să continue executarea, fiindu-i trimis un mesaj de eroare doar jucătorului amuțit (cu ajutorul funcției
Listă butoane: wiki.sa-mp.com/wiki/Keys
50
Constantin-Flavius Nistor Prezentare
SendClientMessage, care va conține inclusiv timpul rămas până la momentul în care va putea vorbi iarăși. Acest efect de amuțire se aplică pentru jucător inclusiv în cazul executării unor comenzi care trimit anumite mesaje specializate tuturor jucătorilor sau doar unei părți dintre aceștia. Administratorii pot amuți un jucător cu ajutorul comenzii ‘/mute’ și pot să anuleze acest efect cu ajutorul comenzii ‘/unmute’.
Înghețare („Freeze”). Această pedeapsă oprește abilitatea jucătorului de a se mai putea
mișca de pe loc și de a mai putea scrie comenzi (asemenea pedepselor celelalte, dacă jucătorul va scrie o comandă acesta va primi un mesaj de eroare care va include și timpul de înghețare rămas). Aceasta se poate aplica atunci când jucătorul trebuie să fie atent în chat la ceea ce îi scrie un administrator și este prioritar, ca de exemplu când este anunțat că a încălcat anumite reguli ale serverului, fapt care trebuie să-l ia la cunoștință și să explice de ce le-a încălcat. Funcțiile valabile pentru administratori sunt ‘/freeze’ și ‘/unfreeze’.
Pentru a preveni resetarea pedepselor active ale unui jucător prin simpla deconectare și conectare a acestuia este nevoie de salvarea în baza de date a pedepselor aplicate. Salvarea este realizată de acest server într-un tabel separat față de cel principal al informațiilor conturilor pentru că procentul de jucători care pot avea pedepse active la un moment dat poate fi destul de mic, iar dacă am fi avut coloane noi pentru fiecare pedeapsă în tabelul principal acestea ar fi ocupat spațiu pentru fiecare cont de pe server, chit că ar putea fi posibil ca pe nici un cont de pe server să nu fie nici o pedeapsă aplicată la un moment dat. Acest tabel, numit „a_punishments” are următoarele coloane, fiecare cont deținând maxim o înregistrare, deoarece informațiile tuturor pedepselor sunt stocate în aceeași înregistrare:
Coloana „Key” este ID-ul contului care deține înregistrarea respectivă. „Punishments” este o mască pe biți, fiecare pedeapsă având o poziție alocată pentru a putea fi identificată, acest lucru producându-se în cod cu ajutorul unei enumerații și mutării la stânga a primului bit:
51
Constantin-Flavius Nistor Prezentare
Tabelul 3.5 – „Biții alocați pedepselor în cod”
enum
{
P_JAIL = 1 << 0,
P_CUFF = 1 << 1,
P_MUTE = 1 << 2,
P_FREEZE = 1 << 3
};
Astfel, statusul pedepsei cu închisoarea este reprezentată de către primul bit, numărul 1 în cod binar fiind chiar 1, care este mutat la stânga cu 0 biți și deci rămâne la fel, statusul comenzii de încătușare fiind al doilea bit, deoarece în cod este setat ca bitul 1 mutat la stânga cu un bit, ceea ce înseamnă 10 în cod binar, statusul de amuțire este reprezentat de către al treilea bit, deoarece acesta este setat ca fiind bitul 1 mutat la stânga cu doi biți, ceea ce înseamnă 100 în cod binar, iar înghețarea este reprezentată de către bitul al patrulea. Coloanele „JailSeconds”, „CuffSeconds”, „MuteSeconds” și „FreezeSeconds” stochează timpul rămas în secunde al pedepselor cu închisoarea, încătușarea, amuțirea și înghețarea.
Deci, ca exemplu, dacă un jucător este în închisoare și este înghețat, ambele cu câte 20 de secunde rămase, coloana „Punishments” va fi setată ca 1001, ceea ce înseamnă că atunci când vom vizualiza informația acea coloană va fi afișată ca fiind numărul 9, aceasta fiind reprezentarea în bază 10 (cea care este citită de către oameni, având 10 cifre) a codului binar specificat, iar coloanele „JailSeconds” și „FreezeSeconds” vor avea amândouă valoarea 20.
Deoarece variabila pentru stocarea statusului pedepselor, dar și coloana respectivă din baza de date, funcționează la nivel de biți, pentru a modifica o pedeapsă în variabila respectivă va trebui să folosim următoarele operații binare, exemplificate cu ajutorul pedepsei cu închisoarea:
Tabelul 3.6 – „Operații binare pentru modificarea variabilei pentru statusul pedepselor”
Constantin-Flavius Nistor Prezentare
O altă problemă este faptul că un jucător poate încălca anumite reguli și poate fi văzut de către administratori, dar înainte ca aceștia să poată aplica o pedeapsă jucătorul poate părăsi serverul, iar faptul că administratorii pot folosi comenzile ‘/jail’, ‘/cuff’, ‘/mute’ și ‘/freeze’ doar pe jucătorii care sunt conectați la momentul execuției reprezintă o problemă. Pentru a rezolva această problemă, a fost implementată comanda ‘/opunish’ („Offline Punish” – „Pedeapsă offline”). Această comandă îmbină toate cele patru pedepse, inclusiv comenzile de anulare ale acestora, parametrii acceptați de aceasta fiind „[Jucătorul] [Tipul: 1(Închisoare) / 2(Încătușare) / 3(Amuțire) / 4(Înghețare)] [Minute (0 pentru anulare)] [Opțional: Motiv]”. Primul parametru trebuie să fie numele exact al contului care trebuie pedepsit, al doilea parametru trebuind să fie una dintre cele patru cifre, fiecare reprezentând o pedeapsă, al treilea parametru este numărul de minute pentru care se va aplica pedeapsa, iar dacă jucătorul este deja pedepsit și se va specifica valoarea 0 în acest parametru, acestuia i se va anula pedeapsa. Dacă al treilea parametru a fost 0, atunci al patrulea parametru, motivul, nu mai trebuie specificat. Deoarece prima dată trebuie să se verifice dacă numele de cont specificat există, iar dacă da, să se verifice și nivelul de administrator, pentru ca un administrator de nivel mai mic să nu poată pedepsi un administrator de nivel mai mare și, de asemenea, pentru a se verifica faptul că pedeapsa se poate aplica sau anula, precum a specificat administratorul în parametrii comenzii, serverul trebuie să trimită o interogare inițială care va prelua rezultatele în funcția OnAdminOfflinePunish, care va fi apelată cu anumite argumente preluate din anumite variabile din funcția comenzii:
Tabelul 3.7 – „Executarea selecției informațiilor pentru pedeapsa offline”
gQuery = fmt::format(
"SELECT `a_accounts`.`Key`, `a_accounts`.`Admin`, IFNULL( `p`.`Punishments`, 0 ) AS `Punishments`, IFNULL( `p`.`JailSeconds`, 0 ) AS `JailSeconds`, IFNULL( `p`.`CuffSeconds`, 0 ) AS `CuffSeconds`, IFNULL( `p`.`MuteSeconds`, 0 ) AS `MuteSeconds`, IFNULL( `p`.`FreezeSeconds`, 0 ) AS `FreezeSeconds`
FROM `a_accounts` "
"LEFT JOIN `a_punishments` `p` ON `p`.`Key` = `a_accounts`.`Key` "
"WHERE `a_accounts`.`Name` = '{}' "
"LIMIT 1", lPlayerName );
Plugins::cMySQL::pquery( 1, gQuery, OnAdminOfflinePunish, playerid, GZS::Player::Info[ playerid ].pMySQLRaceID, std::string( lPlayerName ), lType, lMinutes, std::string( lReason ) );
Funcția IFNULL verifică dacă o valoare este nulă, returnând valoarea 0 în cazurile de mai sus, valorile respective fiind nule atunci când jucătorul specificat nu are nici o pedeapsă.
53
Constantin-Flavius Nistor Prezentare
În exemplul respectiv se mai poate observa variabila GZS::Player::Info[ playerid ].pMySQLRaceID, aceasta având un rol în tratarea problemei numite „race condition” 54, aceasta fiind o problemă care se aplică în cazul interogărilor care rulează pe un alt fir de execuție. Această problemă este, așa cum îi spune și numele, o cursă („race”): dacă, în acest caz, administratorul execută comanda, iar baza de date trimite mai lent serverului rezultatul interogării, administratorul s-ar putea deconecta de la server, după care funcția se va executa având rezultatul primit de la baza de date, dar și ID-ul de jucător al administratorului care a executat comanda, astfel fiind posibilă producerea unor efecte nedorite. Pentru a rezolva această problemă, a trebuit creată o variabilă „pMySQLRaceID” de tip numeric („int”) pentru fiecare jucător, care atunci când se conectează sau deconectează, aceasta se incrementează cu valoarea unu, iar în funcția rezultat a unei interogări asemănătoare trebuind să se verifice dacă argumentul este egal cu valoarea variabilei jucătorului datorită căruia s-a trimis interogarea, iar dacă valorile respective sunt diferite executarea funcției trebuind oprită cu un „return”.
O altă pedeapsă asemănătoare, dar care se poate aplica doar jucătorilor care sunt conectați pe server, sunt avertizările, care atunci când ating numărul de 5 (valoare poate fi schimbată cu ușurință datorită definiției MAX_WARNINGS) pentru un jucător, acesta primește kick. Aceste avertizări se pot da atunci când jucătorul nu respectă regulile aferente chat-ului, lucru pentru care nu ar trebui să se amuțească jucătorul din prima pentru că acesta ar putea părăsi serverul din cauza faptului că nu a primit nici o avertizare înainte, iar acesta nu știa despre regulă sau a scris fără să se gândească la consecințe. Avertizările sunt date automat de către server doar atunci când acesta detectează o posibilă reclamă, acesta oprind mesajul și anunțând jucătorul că trebuie să înceteze acea acțiune pentru că va putea fi dat afară de pe server. Altfel, avertizările pot fi date de către administratori cu ajutorul comenzii ‘/warn’ care acceptă ca parametri jucătorul care să fie avertizat (fiind utilizat, iarăși, specificatorul „u” din plugin-ul sscanf), dar și motivul pentru care este avertizat. Avertizarea automată pentru reclamă de către server este disponibilă în următoarele linii de cod, care stochează mesajul suspect în tabelul „log_advertisements”:
Tabelul 3.8 – „Avertizarea automată”
GZS::Player::Info[ playerid ].pWarnings ++;
gString = fmt::format( "Suspect advertisement detected ! (Warnings: {}/{})", GZS::Player::Info[ playerid ].pWarnings, MAX_WARNINGS );
Mai multe informații: wikipedia.org/wiki/Race_condition
54
Constantin-Flavius Nistor Prezentare
Utility::Message::SendWarning( playerid, gString );
gString = fmt::format( "[A ONLY] [{}] ADVERTISE: {}{}({}){}: '{}{}{}' .", source, Utility::Color( sampgdk::GetPlayerColor( playerid ) ), GZS::Player::Info[ playerid ].pPlayerName, playerid, Utility::col.RED, Utility::col.GREY, string, Utility::col.RED ); SendMessageToAdmins( Utility::col.RED, gString );
gQuery = fmt::format( "INSERT INTO `log_advertisements` VALUES( 0,
UNIX_TIMESTAMP( ), '{}', {}, '{}' )", source, GZS::Player::Info[ playerid ].pKey, Plugins::cMySQL::escape_string( string ) ); Plugins::cMySQL::pquery( 1, gQuery );
if( GZS::Player::Info[ playerid ].pWarnings == MAX_WARNINGS )
CustomKick( playerid, INVALID_PLAYER_ID, "Advertisement", 0, true );
Pe lângă pedepsele enumerate anterior, în regulamentul serverului mai există două alte tipuri de pedepse, mai diferite, acestea fiind kick-ul, descris anterior, care dă afară temporar un jucător, care va putea reintra imediat după aceea, și ban-ul, ceea ce poate interzice unui jucător să mai intre pe server pentru o anumită perioadă de timp.
Kick-ul este folosit în mare parte pentru cazul în care unui jucător nu ii funcționează corect sincronizarea cu ceilalți jucători, el neștiind acest lucru, iar pentru a rezolva această problemă fiind nevoie ca acesta să se reconecteze la server. Această pedeapsă este implementată nativ în SA-MP și este valabilă cu ajutorul funcției Kick din API, care deconectează instant jucătorul. Pentru ca administratorii să dea kick unui jucător, aceștia pot folosi comanda cu același nume, ‘/kick’, care acceptă ca parametri un jucător (fiind folosit specificatorul „u” din plugin-ul sscanf) asupra căruia să se acționeze, dar și de un motiv pentru care s-a dat kick-ul. După ce se aplică această comandă, jucătorul asupra căruia s-a acționat va primi un dialog cu numele celui care i-a dat kick, dar și cu motivul dat de acesta. Din cauza faptului că serverul de SA-MP oprește instant conexiunea dintre acesta și jucător când se apelează funcția Kick, s-a implementat pe acest server o funcție adițională CustomKick care acceptă ca parametri ID-ul de jucător căruia să-i dea kick, ID-ul de jucător al administratorului care a dat kick, motivul acestuia, un parametru de tip „bool” dacă să se execute într-adevăr instant (nu se va mai afișa dialogul pentru jucătorul care a luat kick), iar în final un alt parametru de tip „bool” pentru a decide dacă se afișează tuturor jucătorilor sau doar administratorilor kick-ul dat.
Pe de altă parte, ban-ul este folosit atunci când un jucător face probleme mai grave, precum reclama pentru alte servere sau utilizarea programelor de tip „cheat” („to cheat” = „a trișa”), care oferă jucătorului un avantaj nedrept, existând nenumărate asemenea tipuri de cheat-uri, precum cele care fac glonțul armei să se ducă mereu în inamic, cele care stabilizează ținta
55
Constantin-Flavius Nistor Prezentare
armei, cele care dau mai multă viteză vehiculului și multe altele. În API-ul SA-MP un ban se poate da cu ajutorul funcțiilor Ban, pentru a nu specifica nici un motiv, și BanEx, pentru a specifica unul. Fiecare ban este adăugat pe o nouă linie din fișierul numit „samp.ban”, aflat în directorul principal al serverului, fișier care dacă nu există se creează la primul ban acordat. O linie din acel fișier are formatul „IP [DD/MM/YY | HH:MM:SS] Nume – Motiv”, unde „IP” este adresa IP care a primit ban, „DD/MM/YY” este data la care s-a primit ban, iar „HH:MM:SS” timpul, celelalte două motive fiind destul de descriptive. O astfel de linie poate fi de exemplu „93.130.110.14 [09/06/18 | 18:37:05] TestJucător – Nerespectarea regulilor”. Când asupra unei adrese IP s-a aplicat un asemenea ban, nici un jucător cu adresa IP respectivă nu se poate conecta la server, serverul afișând jucătorului doar faptul că are ban, fără alte informații, ceea ce înseamnă că nu poate vedea numele pe care a primit ban, dar nici cine a aplicat banul, când și pe ce motiv. În concluzie, un astfel de ban verifică doar adresa IP a celui care se conectează la server, astfel încât dacă un nume de jucător este notat ca având un ban aplicat, acesta se poate conecta de pe un IP diferit.
Din aceste motive, s-a decis crearea unui alt tip de a acorda ban jucătorilor, utilizând funcția Kick pentru a preveni accesul jucătorilor care au ban, fapt care se verifică după conectare. Un astfel de ban se acordă cu ajutorul funcției CustomBan și comenzii ‚/ban’ din joc, iar informațiile despre ban-urile acordate cu această funcție sunt stocate în baza de date MySQL în trei tabele numite „bans” (tabelul cu toate setările ban-urilor), „bans_changes” (lista tuturor modificărilor aduse ulterior de către administratori ban-urilor) și „bans_visits” (lista tuturor încercărilor de conectare ale jucătorilor care au fost detectați ca fiind prinși într-un ban). Un asemenea ban permite filtrarea jucătorilor după nume, IP, țară sau serial, în diverse combinații (acesta fiind numit „filter” – „filtru”), filtrul final fiind calculat binar, asemenea pedepselor, fiecare astfel de tip de filtru având alocat un bit pentru a ști dacă este setat în filtrul final, rezultatul acestui calcul fiind stocat apoi în baza de date în tabelul „bans” într-o coloană de tip INT. Valorile binare ale fiecărui tip de filtru sunt definite în cod, de la bitul 1 până la bitul 4:
Tabelul 3.9 – „Biții alocați tipurilor de filtru în cod”
enum
{
BAN_FILTER_NAME = 1 << 0,
BAN_FILTER_IP = 1 << 1,
BAN_FILTER_COUNTRY = 1 << 2,
BAN_FILTER_SERIAL = 1 << 3,
};
56
Constantin-Flavius Nistor Prezentare
Încă o abilitate adițională a acestui nou mod de a acorda ban-uri jucătorilor este cea de a seta o dată de expirare a unui ban (se specifică un număr de zile ca parametru în comanda ‘/ban’, valoarea 0 reprezentând un ban care va fi permanent), care este stocată în tabel sub forma unei valori INT, aceasta fiind de tip unix timestamp, la fel cum se stochează și momentul acordării unui asemenea ban. Dacă un ban nu trebuie să expire, în coloana de expirare are stocată valoarea 0. De asemenea, există și posibilitatea ca un administrator să scoată un ban în orice moment cu ajutorul comenzii ‘/unban’ și ID-ul ban-ului ca parametru, fiecare ban având un ID incrementat automat cu 1, înregistrarea ban-ului respectiv având schimbate valorile pentru ID-ul contului administratorului care a scos ban-ul (inițial această valoare fiind 0) și valoarea de tip unix timestamp a momentului în care a fost scos ban-ul.
Când un ban este acordat, în tabelul „bans” se stochează, de asemenea, ID-ul administratorului care l-a acordat, dar și ID-ul de cont, IP-ul și serial-ul celui care a primit ban. Din cauza faptului că există riscul ca un jucător să primească ban înainte ca acesta să se înregistreze (de exemplu, acesta se poate acorda în cazul în care un jucător intră cu un nume sau IP injurios sau care conține IP-ul sau numele altui server), în tabelul respectiv nu se va mai stoca ID-ul contului celui care a primit ban, ci numele, în mod normal numele fiind stocat ca NULL pentru că acesta poate fi preluat din înregistrarea contului pentru a nu fi stocat și în tabelul cu ban-uri, astfel fiind optimizată stocarea și evitarea repetării informației. În acest caz, filtrul implicit dat cu ajutorul comenzii de ban este doar pentru IP, astfel nici un jucător nu se va putea conecta cu IP-ul respectiv, dar se vor putea conecta în continuare cu același nume. În schimb, dacă jucătorul este autentificat în cont în momentul în care primește ban, filtrul va fi pe nume și pe IP, nici un jucător nu se va mai putea conecta astfel cu acel nume sau cu acel IP.
Așadar, după ce se acționează comanda ‘/ban’, se apelează funcția CustomBan care conține inserarea în baza de date în următorul mod, așteptând rezultatul în funcția OnPlayerBanned:
Tabelul 3.10 – „Inserarea unui ban”
bool CustomBan( int playerid, int adminid, const std::string reason, int days, int filter, bool
instant, bool showtoplayers )
{
// …
if( GZS::Player::Info[ playerid ].pKey == 0 )
{
gString = std::string( "'" ) + GZS::Player::Info[ playerid ].pPlayerName +
"'";
filter = BAN_FILTER_IP;
57
Constantin-Flavius Nistor Prezentare
}
else gString = "NULL";
int lTimeStamp = Utility::TimeDate::gettime( );
gQuery = fmt::format( "INSERT INTO `bans` VALUES( 0, 1, {}, {}, {}, INET_ATON( '{}' ), '{}', '{}', {}, '{}', {}, {}, 0, 0 )", filter, gString, GZS::Player::Info[ playerid ].pKey, GZS::Player::Info[ playerid ].pIP_Address, GZS::Player::Info[ playerid ].pSerial, GZS::Player::Info[ playerid ].pCountryCode, ( adminid == INVALID_PLAYER_ID ) ? ( 0 ) : ( GZS::Player::Info[ adminid ].pKey ), Plugins::cMySQL::escape_string( reason ), lTimeStamp, ( days == 0 ) ? ( 0 ) : ( lTimeStamp + ( 60 * 60 * 24 * days ) ) );
Plugins::cMySQL::pquery( 1, gQuery, OnPlayerBanned, playerid, adminid, reason, days, instant, showtoplayers );
…
return 1;
}
Apoi, când funcția OnPlayerBanned este apelată, toți jucătorii sau doar administratorii
(în funcție de valoarea parametrului „showtoplayers”) vor primi mesaje despre faptul că
respectivul jucător a primit ban, inclusiv cu durata acestuia (permanent sau temporar pe un
număr de zile). După aceea, dacă parametrul „instant” n-a fost setat ca fiind „true”, se va afișa
un dialog cu detaliile ban-ului și se va seta un timer care după două secunde va da kick
jucătorului, pentru că dialog-ul nu se afișează imediat, ci prioritate are kick-ul, astfel făcând ca
dialog-ul să nu se mai afișeze jucătorului care a fost dat afară.
În următoarea imagine avem câteva exemple de înregistrări pentru ban-uri:
Figura 3.5 – „Exemple de ban-uri în baza de date”
58
Constantin-Flavius Nistor Prezentare
În imaginea precedentă se poate observa faptul că există 14 coloane:
Desigur, pentru că un ban poate necesita modificări, există comanda ‘/baninfo’ care afișează într-un dialog informațiile ID-ului de ban specificat ca parametru:
Figura 3.6 – „Dialog comanda ‚/baninfo’”
Din acel dialog se poate modifica filtrul și motivul, dar se pot vedea și lista ultimelor încercări de conectare care au fost prinse în acel ban („vizite”):
Figura 3.7 – „Listarea vizitelor unui ban”
60
Constantin-Flavius Nistor Prezentare
Unele informații trebuie să fie combinate într-o singură coloană din cauza faptului că un astfel de tip de dialog din SA-MP are o limită de doar patru coloane. Structura ultimelor vizite din baza de date pentru banul arătat anterior se pot vedea în următoarea imagine:
Figura 3.8 – „Rezultatul interogării ultimelor vizite ale unui ban”
De asemenea, se poate lista ultimele câteva schimbări ale ban-ului respectiv:
Figura 3.9 – „Listarea schimbărilor unui ban”
Structura ultimelor schimbări din baza de date pentru banul respectiv se pot vedea în următoarea imagine:
Figura 3.10 – „Rezultatul interogării ultimelor schimbări ale unui ban”
61
Constantin-Flavius Nistor Prezentare
Pentru ca administratorii să poată urmări ultimele vizite și schimbări de pe toate ban-urile există comenzile ‘/banvisits’ și ‘/banchanges’, care afișează paginat în dialog-uri toate înregistrările din tabelele respective:
Figura 3.11 – „Dialog ‘/banvisits’”
Figura 3.12 – „Dialog ‘/banchanges’”
De exemplu, pentru comanda ‘/banchanges’, pentru a se afișa rezultatele parțiale bazate pe numărul paginii curente, se execută următoarea interogare:
SELECT `bans_changes`.*, `a`.`Name` FROM `bans_changes`
LEFT JOIN `a_accounts` `a` ON `a`.`Key` = `bans_changes`.`AdminKey` ORDER BY `ID` DESC LIMIT {}, {}
62
Constantin-Flavius Nistor Prezentare
În interogare, primul placeholder primește valoarea de „( numar_pagini – 1 ) * 25”,
acolo unde numărul paginii începe a fi stocat de la 1 (de aceea fiind nevoie să se scadă 1), 25
fiind numărul de astfel de înregistrări de pe o pagină, iar al doilea placeholder primește ca
valoare numărul de elemente care să fie afișat pe pagină, adică tot valoarea 25.
Așadar, după ce au fost prezentate toate elementele acestui nou mod de a acorda ban
jucătorilor, în următorul bloc de cod avem întreaga interogare care verifică dacă un jucător
conectat are ban, aceasta oferind întregul suport pentru toate caracteristicile descrise anterior,
interogare care este formatată și returnată într-o funcție specială din cauza complexității sale,
complexitate care este datorată, în mare parte, faptului că se extrag numele tuturor ID-urilor de
conturi implicate în fiecare ban, dar și a selectării numărului de vizite care s-au efectuat, partea
de filtrare fiind și ea destul de complexă în clauza „WHERE”:
Tabelul 3.11 – „Interogare verificare ban pentru jucător”
const std::string CreateBanCheckQuery( const std::string &name, const std::string &countrycode, const std::string &ip, const std::string &serial ) {
return fmt::format(
"SELECT `bans`.*, INET_NTOA( `bans`.`IP` ) AS `IP_Real`, `g`.`Name` AS `Country`, `p`.`Name` AS `PlayerKeyName`, `a`.`Name` AS `Admin`, `u`.`Name` AS `UnBanAdmin`, "
"( SELECT COUNT( * ) FROM `bans_visits` WHERE `BanID` = `bans`.`ID` ) AS `Visits` FROM `bans` "
"LEFT JOIN `a_accounts` `p` ON `p`.`Key` = `bans`.`PlayerKey` "
"LEFT JOIN `a_accounts` `a` ON `a`.`Key` = `bans`.`AdminKey` "
"LEFT JOIN `a_accounts` `u` ON `u`.`Key` = `bans`.`UnBanAdminKey` "
"LEFT JOIN `geoip_countries` `g` ON `g`.`Code` = `bans`.`CountryCode` "
"WHERE `bans`.`Active` = 1 AND ( `bans`.`ExpireTime` = 0 OR `bans`.`ExpireTime` >
UNIX_TIMESTAMP( ) ) AND "
"( "
"( ( `Filter` & {} ) AND IFNULL( `bans`.`Player`, `p`.`Name` ) = '{}' ) OR "
"( ( `Filter` & {} ) AND `bans`.`IP` = INET_ATON( '{}' ) ) OR "
"( ( `Filter` & ( {} | {} ) ) AND `bans`.`CountryCode` = '{}' AND `bans`.`Serial` = '{}' ) OR "
"( ( ( `Filter` & {} ) AND !( `Filter` & {} ) ) AND `bans`.`Serial` = '{}' ) " ") LIMIT 1",
static_cast< int >( BAN_FILTER_NAME ), name, static_cast< int >( BAN_FILTER_IP ), ip,
static_cast< int >( BAN_FILTER_COUNTRY ), static_cast< int >( BAN_FILTER_SERIAL ), countrycode, serial,
static_cast< int >( BAN_FILTER_SERIAL ), static_cast< int >( BAN_FILTER_COUNTRY ), serial ); }
63
Constantin-Flavius Nistor Prezentare
Acestea fiind pedepsele implementate pe server, administratorii au abilitatea de a le aplica asupra jucătorilor conform regulamentului sau în alte cazuri speciale. Deoarece încălcarea anumitor reguli poate fi detectată cu ușurință de către server, s-au implementat anumite mijloace de protecție ale serverului cunoscute sub numele de „anticheat” („anti trișare”), pentru detectarea jucătorilor care folosesc cheat-uri, dar și pentru detectarea acțiunilor jucătorilor care încearcă să facă rău direct serverului, precum flood-ul (realizarea unei acțiuni în mod foarte rapid, precum trimirea a foarte multor mesaje în chat sau conectarea multor clienți falși, pentru a îngreuna procesarea informațiilor de către server) sau conectarea cu un IP în nume care poate fi o reclamă pentru un alt server de SA-MP. Cu ajutorul acestor detectări, care pot fi foarte precise sau nu (din cauza diverselor elemente precum viteza de internet a jucătorului sau performanța calculatorului), se pot aplica pedepse imediat (amuțire, închisoare, ban sau celelalte) sau se pot anunța administratorii în cazurile în care nu sunt detectări sigure.
De exemplu, când un jucător se conectează la server se verifică IP-ul de pe care se conectează, iar dacă există peste cinci jucători conectați cu aceeași adresă IP, ultimul jucător care tocmai s-a conectat va primi kick, după care ceilalți jucători cu aceeași adresă IP vor pierde conexiunea la server din cauza faptului că adresa IP este blocată temporar pentru a evita reconectarea rapidă cu ajutorul unor modificări ale clientului.
Tabelul 3.12 – „Detectarea conectării unui jucător pe o adresă IP deținută de prea mulți alții”
if( Utility::GetPlayerCountOnIP( GZS::Player::Info[ playerid ].pIP_Address ) >
MAX_CONNECTIONS_FROM_IP )
{
CustomKick( playerid, INVALID_PLAYER_ID, "Too many connections from IP", false, false );
return 1;
}
În funcția Utility::GetPlayerCountOnIP se află o simplă buclă prin toți jucătorii, în care se compară adresa IP valoarea de adresă IP introdusă ca parametru, iar în caz că acestea două sunt identice o variabilă se incrementează cu 1, variabilă care apoi este returnată de funcție. În apelul funcției CustomKick argumentul INVALID_PLAYER_ID este ID-ul jucătorului care a dat kick, în acest caz fiind invalid deoarece serverul a efectuat automat această acțiune, următorul argument fiind motivul, apoi primul argument false fiind parametrul „instant”, ceea ce înseamnă că jucătorul va primi dialog-ul că a primit kick, iar următorul parametru fiind cel care specifică faptul că toți ceilalți jucători nu vor trebui să vadă mesajul că
64
prevenirea conectării rapide a multor jucători de pe aceeași adresă IP.
O altă verificare importantă este cea care previne trimiterea multor mesaje rapide în chat de către un singur jucător, aceasta făcând uz de funcția ajutătoare Utility::TimeDate::GetTickCount care returnează numărul de milisecunde de la pornirea
serverului și până în momentul actual, aceasta fiind implementată astfel, cu ajutorul librăriei standard std::chrono din C++:
Tabelul 3.13 – „Implementarea funcției Utility:TimeDate::GetTickCount”
static std::chrono::time_point< std::chrono::system_clock > Internal::startTime = std::chrono::system_clock::now( );
std::int32_t Utility::TimeDate::GetTickCount( )
{
return static_cast< int >( ( ( std::chrono::duration_cast< std::chrono::milliseconds >( std::chrono::system_clock::now( ) – Internal::startTime ) ).count( ) ) % std::numeric_limits< std::int32_t >::max( ) ); }
Variabila „Internal::startTime” este punctul de timp al pornirii serverului, creată într-
un alt spațiu de nume (Utility::TimeDate::Internal) din spațiul curent de nume
(Utility::TimeDate) și care se inițializează automat la pornirea serverului.
Această verificare de flood în chat se realizează în următoarele linii:
Tabelul 3.14 – „Detectare flood chat”
int lTick = Utility::TimeDate::GetTickCount( );
if( ( lTick – GZS::Player::Info[ playerid ].pChat_AntiSpamLastTick ) < 350 )
{
GZS::Player::Info[ playerid ].pChat_AntiSpamWarnings ++;
if( GZS::Player::Info[ playerid ].pChat_AntiSpamWarnings == 3 )
{
CustomKick( playerid, INVALID_PLAYER_ID, "Chat flooding", false,
true );
return 0;
}
}
else if( GZS::Player::Info[ playerid ].pChat_AntiSpamWarnings != 0 )
GZS::Player::Info[ playerid ].pChat_AntiSpamWarnings –;
GZS::Player::Info[ playerid ].pChat_AntiSpamLastTick = lTick;
65
Constantin-Flavius Nistor Prezentare
În liniile respective se poate observa faptul că dacă un mesaj este scris la mai puțin de 350 de milisecunde față de precedentul, atunci el este considerat suspect și se incrementează o variabilă care să țină cont de câte mesaje au fost suspecte, trei astfel de mesaje la rând făcând ca jucătorul respectiv să ia kick.
O altă detectare pentru încălcarea regulilor este cea menționată când s-a descris pedeapsa cu închisoarea, detectarea respectivă fiind verificarea care pune jucătorul în închisoare după ce omoară un jucător cu un vehicul sau cu elicele unui elicopter.
De această dată, o detectare existentă care dă ban este cea care descoperă că se află un IP în numele jucătorului la conectare, după ce se face verificarea dacă jucătorul este deja prins într-un ban. Această verificare poate fi dezactivată și dezactivată, precum alte setări ale serverului, cu ajutorul dialog-ului afișat cu ajutorul comenzii ‘/ssettings’ („Server Settings”) din următoarea imagine:
Figura 3.13 – „Dialog ‘/ssettings’”
Fiecare opțiune din acest dialog poate fi schimbată dacă este selectată și se apasă butonul din partea stângă, cel numit „Toggle” („Comută”).
Revenind, verificarea respectivă la conectare pentru detectarea unei adrese IP din numele jucătorului se face cu ajutorul următoarelor linii de cod, acordând ban jucătorului pentru
zile:
66
Constantin-Flavius Nistor Prezentare
Tabelul 3.15 – „Detectare adresă IP în nume la conectare”
if( CServerSettings::Get< bool >( "antiadvertisement" ) == true && AntiIP( GZS::Player::Info[ extraid ].pPlayerName, true ) != 0 ) {
CustomBan( extraid, INVALID_PLAYER_ID, "Name Advertising", 7, BAN_FILTER_NAME | BAN_FILTER_IP, 0, false );
return;
}
Cea mai importantă parte din funcția AntiIP fiind implementată astfel:
Tabelul 3.16 – „O parte din implementarea funcției AntiIP”
int AntiIP( const std::string &string, bool isname )
{
if( string.size( ) >= 7 )
{
static std::regex lPattern( "([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-
9]{1,3})(.*?)" );
std::smatch lMatch;
for( std::sregex_iterator i = std::sregex_iterator( string.begin( ), string.end( ), lPattern ); i != std::sregex_iterator( ); ++ i )
{
lMatch = *i;
if( lMatch.str( ) != SERVER_IP )
return 1;
}
}
return 0;
}
Se poate observa că se utilizează iarăși librăria std::regex, cu ajutorul căreia se creează un obiect care are un model cu ajutorul căreia să găsească eventualele IP-uri din șirul de caractere dat în parametrul „string”. După ce s-a efectuat verificarea, serverul ia la rând toate IP-urile găsite și verifică dacă nu cumva IP-ul respectiv este chiar cel al serverului pe care se joacă, acesta nefiind interzis.
Din cauza faptului că anumiți clienți malițioși pot trimite apelări false de evenimente către server, cu argumente invalide, s-a apelat la verificarea acestor valori, pentru ca ele să conțină întotdeauna doar valorile valide. Un asemenea eveniment este OnPlayerWeaponShot, cel care este apelat atunci când un jucător trage un glonț cu o armă de foc. Dacă se trimit anumite valori false, există chiar și posibilitatea ca jucătorii din jurul celui care trimite informațiile false
67
Constantin-Flavius Nistor Prezentare
să ia „crash” (blocarea jocului și apoi închiderea sa cu un eventual mesaj de eroare) din cauza lipsei de protecții suficiente a clientului SA-MP, jocul neștiind cum să interpreteze valorile invalide. Acest eveniment acceptă parametrii „playerid” (ID-ul jucătorului care a tras cu arma), „weaponid” (ID-ul armei de foc cu care s-a tras), „hittype” (tipul impactului făcut – 0 dacă a tras în aer, 1 dacă a tras într-un jucător, 2 dacă a tras într-un vehicul, 3 dacă a tras într-un obiect sau 4 dacă a tras într-un obiect de jucător), „hitid” (ID-ul entității în care a tras, acesta fiind mereu 0 dacă parametrul hittype este 0) și parametrii „fX”, „fY”, „fZ” care sunt coordonatele în care glonțul s-a oprit, acestea fiind absolute (poziția exactă în lume) atunci când hittype este 0 sau relative (poziție bazată pe diferența dintre centrul poziției entității și poziția la care a lovit glonțul) atunci când are o altă valoare. Pentru a opri propagarea efectului unui glonț către ceilalți jucători se poate returna valoarea 0 în eveniment, această valoare fiind folosită în acest caz pentru a nu face ceilalți jucători să ia crash din cauza glonțului invalid. Anumite verificări care se pot face pentru a oferi o siguranță mai mare a validității apelării evenimentului pot fi:
Verificarea faptului că jucătorul este autentificat în cont când se apelează evenimentul, deoarece jucătorul trebuie să fie autentificat pentru a putea primi spawn, deci neexistând șansa ca vreun jucător să tragă atunci când nu este autentificat, din cauza faptului că acesta nici nu poate avea arme în acel moment, iar jucătorul nici nu poate trage cu arma atunci când selectează caracterul:
Tabelul 3.17 – „Verificare dacă jucătorul nu este autentificat în cont”
if( GZS::Player::Info[ playerid ].pLoggedIn != 2 )
{
CustomKick( playerid, INVALID_PLAYER_ID, "Possible hacks (didn't login before shooting)", false, true );
return 0;
}
Verificarea ID-ului tipului de impact, pentru a nu permite tipuri care nu există:
Tabelul 3.18 – „Verificare dacă ID-ul tipului impactului nu există”
if( !( -1 < hittype && hittype < 5 ) )
{
CustomKick( playerid, INVALID_PLAYER_ID, "Possible hacks (invalid shooting type).", false, true );
return 0;
}
68
Constantin-Flavius Nistor Prezentare
Verificarea ID-ului armei cu care s-a tras, pentru ca aceasta să fie una de foc:
Tabelul 3.19 – „Verificare dacă arma cu care s-a tras nu este una de foc”
if( !( 22 <= weaponid && weaponid <= 38 ) )
{
CustomKick( playerid, INVALID_PLAYER_ID, "Possible hacks (invalid weapon shooting).", false, true );
return 0;
}
Verificarea poziției de la care jucătorul a tras, nefiind posibil ca una dintre valorile X, Y sau Z să aibă o valoare mai mică de -20000 sau mai mare de 20000:
Tabelul 3.20 – „ Verificare dacă poziția de la care s-a tras nu este validă”
float X = 0.0f, Y = 0.0f, Z = 0.0f; sampgdk::GetPlayerPos( playerid, &X, &Y, &Z );
if( !( -20000.0f <= X && X <= 20000.0f ) ||
!( -20000.0f <= Y && Y <= 20000.0f ) ||
!( -20000.0f <= Z && Z <= 20000.0f ) )
{
CustomKick( playerid, INVALID_PLAYER_ID, "Possible hacks (invalid shooting positions).", false, true );
return 0;
}
Verificarea faptului că jucătorul nu a tras în el însuși în caz că parametrul hittype are tipul 1 (în jucător), acest lucru nefiind posibil în mod normal în acest joc, sau a verificării faptului că acesta nu a tras într-un jucător cu ID invalid:
Tabelul 3.21 – „Verificare dacă jucătorul în care s-a tras este el însuși sau invalid”
switch( hittype )
{
case BULLET_HIT_TYPE_PLAYER:
{
if( hitid == playerid )
{
CustomKick( playerid, INVALID_PLAYER_ID, "Possible hacks (shot himself).", false, true );
return 0;
}
69
Constantin-Flavius Nistor Prezentare
if( !( -1 < hitid && hitid < MAX_PLAYERS ) )
{
CustomKick( playerid, INVALID_PLAYER_ID, "Possible hacks (shot invalid player).", false, true );
return 0;
}
break;
}
}
În caz că jucătorul a tras într-un vehicul (hittype având valoarea 2), verificăm faptul că nu a tras în propriul vehicul, acest lucru nefiind posibil, de asemenea, în acest joc:
Tabelul 3.22 – „Verificare dacă vehiculul în care s-a tras este cel condus de jucător”
switch( hittype )
{
case BULLET_HIT_TYPE_VEHICLE:
{
if( hitid == sampgdk::GetPlayerVehicleID( playerid ) )
{
CustomKick( playerid, INVALID_PLAYER_ID, "Possible hacks (shot own vehicle).", false, true );
return 0;
}
break;
}
}
Pentru aceste verificări care aplică automat pedeapse este sigur faptul că acestea nu s-au produs dintr-o greșeală. Pentru alte tipuri de verificări de informații invalide, care nu pot avea un verdict sigur din cauza calității conexiunii jucătorului la server, se trimit tuturor administratorilor, pentru a verifica manual jucătorul, mesaje de avertizare care conțin tipul de cheat de care jucătorul este suspect, alături de o informație de bază din cauza căreia acel jucător este suspect. De exemplu, există funcțiile GetPlayerVelocity și GetVehicleVelocity (care returnează valoarea 1 în care acestea s-au executat cu succes, adică dacă ID-ul de jucător sau vehicul specificat există într-adevăr) pentru a prelua viteza de pe cele trei axe X, Y și Z ale jucătorului sau ale vehiculului jucătorului, viteză care este setată în cele trei argumente pasate prin referință funcțiilor, iar dacă aceasta se verifică a fi prea mare din diverse motive, administratorii vor fi înștiințați pentru a verifica jucătorul. Aceste verificări se fac în evenimentul OnPlayerUpdate, eveniment care se apelează de mai multe ori pe secundă, fiind apelat de fiecare dată când clientul jucătorului trimite serverului informațiile despre sine,
70
Constantin-Flavius Nistor Prezentare
precum noua armă din mâini, poziția, viteza sau multe altele. În acest eveniment se poate returna valoarea 0 pentru a opri actualizarea jucătorului pentru ceilalți, deci jucătorii îi vor cunoaște doar ultima informație trimisă către server care nu a fost anulată. Această verificare se produce întotdeauna pentru că anumite cheat-uri pot actualiza foarte repede viteza unui vehicul ca fiind mare și apoi, imediat, ca fiind mică, pentru a permite jucătorului care utilizează cheat-urile respective să arunce în aer ceilalți jucători fără ca ei să observe acest lucru, din cauza vitezei cu care s-a produs acțiunea. Verificarea arată în felul următor:
Tabelul 3.23 – „Verificarea vitezei prea mari a unui jucător”
float vx = 0.0f, vy = 0.0f, vz = 0.0f;
int vehicleid = sampgdk::GetPlayerVehicleID( playerid ), tickcount = Utility::TimeDate::GetTickCount( );
if( sampgdk::GetPlayerState( playerid ) == PLAYER_STATE_DRIVER )
{
if( sampgdk::GetVehicleVelocity( vehicleid, &vx, &vy, &vz ) && ( vx || vy || vz ) ) if( std::abs( vx ) >= 10.0f || std::abs( vy ) >= 10.0f || std::abs( vz ) >= 10.0f
)
if( !OnPlayerHack_Velocity( playerid, tickcount, vehicleid, vx, vy,
vz ) )
return 0;
}
else
{
if( sampgdk::GetPlayerVelocity( playerid, &vx, &vy, &vz ) && ( vx || vy || vz ) )
if( std::abs( vx ) >= 6.0f || std::abs( vy ) >= 6.0f || std::abs( vz ) >= 6.0f )
if( !OnPlayerHack_Velocity( playerid, tickcount, vehicleid, vx, vy,
vz ) )
return 0;
}
Se poate observa că aceste verificări apelează funcția OnPlayerHack_Velocity, în care se iau apoi deciziile detectării respective, iar dacă aceasta returnează 0, funcția OnPlayerUpdate va returna și ea 0, astfel oprind actualizarea jucătorului pentru ceilalți. Însă, pentru această detectare, există și o decizie de a da afară jucătorul de pe server în caz că acumulează mai mult de 5 avertizări la un interval mai mic de un minut între toate acestea. Dacă intervalul dintre o avertizare și următoarea depășește un minut, atunci numărul de avertizări va fi resetat, iar dacă intervalul dintre două detectări este de sub 3 secunde, atunci acestea vor fi contorizate ca sub-avertizări, acestea fiind afișate în mesajul pentru administratori al următoarei avertizări, pentru cazul în care jucătorul capătă o viteză mare în mod normal din cauza fizicii jocului, care poate
71
Constantin-Flavius Nistor Prezentare
avea probleme, și se menține mai multe secunde, pentru a nu lua kick rapid dintr-o greșeală a
serverului. În funcția respectivă se transmite, bineînțeles, avertizarea către administratori, în caz
că nu se ia kick automat din cauza depășirii numărului de avertizări maxime. O parte din liniile
acestei funcții se pot vedea în următoarea bucată de cod:
Tabelul 3.24 – „Conținutul funcției OnPlayerHack_Velocity”
int lMsPassed = ( tick – GZS::Player::Info[ playerid ].pVelocity_LastWarnTime );
if( lMsPassed < 3000 )
{
GZS::Player::Info[ playerid ].pVelocity_LastWarns ++; return 0;
}
else if( lMsPassed > 60000 )
{
GZS::Player::Info[ playerid ].pVelocity_Warns = 0; GZS::Player::Info[ playerid ].pVelocity_LastWarns = 0;
}
GZS::Player::Info[ playerid ].pVelocity_LastWarnTime = tick; GZS::Player::Info[ playerid ].pVelocity_Warns ++;
gQuery = fmt::format( "INSERT INTO `log_hacks` VALUES( 0, UNIX_TIMESTAMP( ), {}, 'Slap Hack', 'Velocity: {}, {}, {}. Vehicle: {}. Warn {}-{}.' )", GZS::Player::Info[ playerid ].pKey, x, y, z, ( vehicleid != 0 ) ? ( "YES" ) : ( "NO" ), GZS::Player::Info[ playerid ].pVelocity_Warns, GZS::Player::Info[ playerid ].pVelocity_LastWarns ); Plugins::cMySQL::pquery( 1, gQuery );
if( GZS::Player::Info[ playerid ].pVelocity_Warns < 5 )
{
gString = fmt::format( "SUSPECT: {}({}) may be using slap hack. Velocity: {:.2f}, {:.2f}, {:.2f}. Vehicle ID: {}. Warn {}-{}.", GZS::Player::Info[ playerid ].pPlayerName, playerid, x, y, z, vehicleid, GZS::Player::Info[ playerid ].pVelocity_Warns, GZS::Player::Info[ playerid ].pVelocity_LastWarns );
SendMessageToAdmins( Utility::col.RED, gString );
}
else
{
CustomKick( playerid, INVALID_PLAYER_ID, "Possible hacks (Slap Hack)", 0,
true );
}
GZS::Player::Info[ playerid ].pVelocity_LastWarns = 0;
72
Constantin-Flavius Nistor Prezentare
De asemenea, în funcția din tabelul anterior se poate observa faptul că se introduce în baza de date o înregistrare într-un tabel numit „log_hacks”, pentru a reține într-un istoric toate detectările de cheat-uri ale jucătorului, astfel având posibilitatea de a stabili mai ușor pe viitor dacă un jucător are o probabilitate mare de a utiliza cheat-uri. Acest tabel conține coloana cu ID-ul detectării, unix timestamp-ul momentului detectării, ID-ul contului al celui care a fost detectat, titlul tipului de detectare și detaliile detectării. Exemple de înregistrări din acest tabel:
Figura 3.14 – „Exemplu de înregistrări din tabelul detectărilor de cheat-uri”
3.1.4 Detectarea conturilor multiple
Deoarece jucătorii pot dori să scape de pedepsele aplicate pe un cont, pot încerca parola unui cont care nu le aparține, pot uita numele și să se conecteze cu un altul, întrebând care era numele corect, sau pot pur și simplu să creeze mai multe conturi pentru a li se pierde urma, serverul ține cont de toate conexiunile jucătorilor, ele fiind stocate în tabelul „aka”, această caracteristică fiind numită „A.K.A.” („Also Known As” – „De asemenea cunoscut ca și”). Acest tabel conține ID-ul înregistrării, momentul conectării ca unix timestamp, adresa IP de pe care s-a produs conectarea (stocat ca număr), serial-ul conectării, țara IP-ului, dar și numele contului de pe care s-a produs conectarea.
Figura 3.15 – „Exemple de înregistrări din tabelul ‘aka’”
73
Constantin-Flavius Nistor Prezentare
Pentru întreținerea acestui tabel, în momentul în care un jucător se conectează, serverul încearcă să trimită o interogare către baza de date cu ajutorul căreia să se actualizeze înregistrarea care are același nume și adresă IP precum jucătorul care s-a conectat, aceasta încercând să actualizeze momentul ultimei conectări și serial-ul. Interogarea care face acest lucru este „UPDATE `aka` SET `Date` = UNIX_TIMESTAMP( ), `Serial` = '{}' WHERE INET_NTOA( `IP` ) = '{}' AND `Name` = '{}'”, acolo unde primul placeholder este noul serial care trebuie setat, al doilea fiind adresa IP a jucătorului care s-a conectat pe server, iar ultimul fiind numele jucătorului conectat. Dacă nu se actualizează nici un astfel de rând, acest lucru fiind detectat utilizând funcția Plugins::cMySQL::Cache::affected_rows în funcția care este apelată atunci când se primește răspunsul de la baza de date, atunci serverul va trimite o altă interogare către baza de date cu o nouă înregistrare pentru a insera toate valorile necesare pentru un nou rând care să aparțină sesiunii curent a jucătorului.
Pentru a permite administratorilor să vadă toate posibilele nume la care s-a conectat un anumit jucător în ultimul timp, aceștia pot utiliza comanda ‘/aka’, aceasta acceptând ca singur parametru un jucător conectat (folosind specificatorul „u” din plugin-ul sscanf), dar și comanda ‘/oaka’ pentru jucătorii care nu sunt conectați, dând numele exact al contului de pe care să preia informațiile. Aceste comenzi listează într-un dialog toate informațiile ultimelor 30 de înregistrări care au același IP sau serial, primele în listă fiind înregistrările care au același IP precum cel curent al contului, următoarea sortare fiind pentru cele care au IP diferit, dar sunt din aceeași țară, apoi fiind pentru cele care au data conectării mai recentă, iar apoi fiind sortarea pentru cele care au același serial, fiecare dintre aceste sortări având o prioritate din ce în ce mai mică, fiind prioritară sortarea după IP. Interogarea pentru comanda de listare a înregistrărilor unor jucător conectat este următoarea:
Tabelul 3.25 – „Interogarea bazei de date a comenzii ‘/aka’”
gQuery = fmt::format( "SELECT `aka`.*, INET_NTOA( `aka`.`IP` ) AS `RealIP`, '{}' = `aka`.`Name` AS `SameName`, INET_ATON( '{}' ) = `aka`.`IP` AS `SameIP`, '{}' = `aka`.`Serial` AS `SameSerial` FROM `aka` "
"WHERE ( `IP` = INET_ATON( '{}' ) OR `Serial` = '{}' ) "
"ORDER BY "
"`IP` <> INET_ATON( '{}' ) ASC, "
"`Country` <> '{}' ASC, "
"`Date` DESC, "
"`Serial` <> '{}' ASC "
"LIMIT 30",
GZS::Player::Info[ lPlayerID ].pPlayerName,
GZS::Player::Info[ lPlayerID ].pIP_Address,
74
Constantin-Flavius Nistor Prezentare
GZS::Player::Info[ lPlayerID ].pSerial,
GZS::Player::Info[ lPlayerID ].pIP_Address,
GZS::Player::Info[ lPlayerID ].pSerial,
GZS::Player::Info[ lPlayerID ].pIP_Address,
GZS::Player::Info[ lPlayerID ].pCountryCode,
GZS::Player::Info[ lPlayerID ].pSerial );
Plugins::cMySQL::pquery( 1, gQuery, ShowPlayerAKA, playerid, std::string( GZS::Player::Info[ lPlayerID ].pPlayerName ), lPlayerID );
Pentru afișarea informațiilor rezultate din interogarea precedentă, serverul afișează dialog-ul anterior menționat, în care numele identice cu cel al contului verificat vor fi cu verde, IP-urile identice vor fi cu verde (primele în listă), iar serialele identice vor fi tot cu verde:
Figura 3.16 – „Lista afișată de comanda ‘/aka’ pentru un jucător”
De asemenea, comanda ‘/oaka’ poate fi utilizată și de către utilizatorii care dețin „V.I.P.” de nivel 3 sau 4, iar aceștia, alături de administratorii de nivel mai mic decât 5, nu vor putea vedea exact IP-ul și serialelul înregistrărilor, ci doar dacă acestea sunt identice cu informațiile respective de pe contul verificat, coloanele respective conținând doar valorile „YES” („DA”), în caz că sunt identice, sau „NO” („NU”), în caz contrar, coloanele respective fiind redenumite în „Same IP” („Același IP”) și „Same Serial” („Același Serial”).
Deoarece administratorii au la dispoziție filtre avansate pentru a da ban unor jucători, care includ filtre pe IP sau pe serial, aceștia pot folosi cele două comenzi pentru a interpreta rezultatele și pentru a decide dacă aplicarea unui filtru mai extins ar afecta jucători inocenți, având în vedere, în principal, faptul că un serial nu este unic.
75
Constantin-Flavius Nistor Prezentare
3.2 Panoul de control, alertele și alte elemente
Pentru a controla o parte din aspectele configurabile ale contului, orice jucător are la dispoziție comanda ‘/pcp’ („Player Control Panel” – „Panou de Control Jucător”). Această comandă afișează un dialog care este asemănător cu cel de la comanda ‘/ssettings’ prezentată mai sus, doar că aceasta, bineînțeles, conține setările contului jucătorului care a executat-o. Jucătorul poate activa/dezactiva afișarea comenzilor de teleportare scrise de el pe ecran către ceilalți, poate schimba afișarea numelui celorlalți jucători când îi vede fizic, pe harta jocului, poate schimba dacă să primească un sunet atunci când acesta afecteză viața unui alt jucător, dar și altele, anumite setări speciale pentru vehicule fiind disponibile într-un sub meniu. Aceste setări pot fi schimbate cu un dublu click pe ele sau cu selectarea elementului dorit pentru a fi schimbat, urmat de apăsarea butonului „Select”. De asemenea, există anumiți itemi în dialog care afișează alte dialog-uri, acestea fiind cele pentru schimbarea parolei contului (care este disponibil și cu ajutorul comenzii ‘/changepass’), schimbarea email-ului (fiind disponibil și cu ajutorul comenzii ‘/changepass’) sau verificarea informațiilor contului (similar, comanda ‘/stats’).
Figura 3.17 – „Dialog-ul comenzii ‘/pcp’”
Din cauza faptului că se pot întâmpla acțiuni asupra contului în timp ce jucătorul nu este conectat (precum expirarea nivelului de V.I.P. din cauza inactivității, vânzarea unei case deținute, câștigarea la loterie sau altele), a fost nevoie de implementarea alertelor (cunoscute, de asemenea, sub numele de notificări). Acestea sunt stocate în tabelul „alerts” din baza de date, tabel care conține 6 coloane: „id” (ID-ul alertei, incrementat automat), „playerkey” (ID-
ul contului care a obținut alerta), „date” (unix timestamp-ul momentului emiterii alertei), „type”
76
Constantin-Flavius Nistor Prezentare
(un scurt titlu pentru alertă, precum „VIP”, „BUSINESS” sau „SECURITY”), „message” (informația detaliată a alertei, pentru a ști exact ce s-a întâmplat) și „read” (unix timestamp-ul momentului la care s-a verificat alerta prima dată, iar când aceasta n-a fost verificată având valoarea 0). Dacă un jucător are cel puțin o alertă atunci când se conectează pe server, acesta va primi un mesaj pentru a executa comanda ‘/alerts’ pentru verificarea alertelor, acestea fiind afișate în ordinea inversă a ID-urilor, cea mai recentă alertă fiind prima în listă. În momentul în care alertele sunt afișate, toate alertele care n-au fost citite încă vor avea unix timestamp-ul momentului afișării listei de alerte stocat în coloana „read” din baza de date. Mai jos putem vedea ultimele alerte din baza de date:
Figura 3.18 – „Ultimele alerte din tabelul corespunzător”
O alertă nu poate fi ștearsă manual din dialog-ul comenzii ’/alerts’, ci acestea se curăță automat, după un timp. Alertele care sunt citite se șterg după 30 de zile de la citire, altfel, dacă jucătorul care a primit o alertă a fost activ ultima dată pe server după ce s-a primit alerta (deci n-a citit alerta când s-a conectat), aceasta se șterge tot după 30 de zile, iar dacă jucătorul a fost activ ultima dată înainte să primească alerta, aceasta va fi ștearsă după 366 de zile, în caz că jucătorul va reveni pe server în acest interval de timp. Aceste verificări de curățare se realizează o dată la 30 de minute, iar o alertă nu poate fi ștearsă dacă jucătorul este pe server în momentul respectiv, interogarea pentru toate acestea fiind următoarea, acolo unde placeholder-ul „{}” primește ca valoare adresa IP a serverului:
Tabelul 3.26 – „Interogare curățare alerte”
DELETE `a_alerts` FROM `a_alerts`
LEFT JOIN `playersonline` AS `p` ON `a_alerts`.`playerkey` = `p`.`ID` AND `p`.`ServerIP` = INET_ATON( '{}' )
LEFT JOIN `a_accounts` AS `a` ON `a_alerts`.`playerkey` = `a`.`Key` WHERE `p`.`ID` IS NULL AND
77
Constantin-Flavius Nistor Prezentare
(
( `a_alerts`.`read` != 0 AND `a_alerts`.`read` < ( UNIX_TIMESTAMP( ) – ( 60 * 60 * 24 * 30 ) ) ) OR
(
`a_alerts`.`read` = 0 AND ( `a_alerts`.`date` < ( UNIX_TIMESTAMP( ) – ( 60 * 60 * 24 * IF( `a`.`LastActive` < `a_alerts`.`date`, 366, 30 ) ) ) )
)
)
Deoarece jucătorii pot decide să-și schimbe numele contului, aceștia neavând posibilitatea de a intra pur și simplu cu un cont nou pentru că ar trebui să-l creeze în baza de date, există comanda ‘/newnick’, care prima dată cere parola contului într-un dialog, pentru a preveni schimbarea numelui contului de către o altă persoană, iar dacă parola a fost introdusă corect, va apărea un alt dialog care îl va pune să introducă noul nume dorit. După ce se specifică un nume corect (care este în standardele SA-MP, adică trebuie să conțină doar caracterele a-z, A-Z, 0-9, [, ], (, ), $, @, ., _ și =, cu o limită de 20 de caractere) va apărea un ultim dialog, de tip listă, care-l va întreba dacă este sigur că acela este numele dorit (fiind specificat în titlul dialog-ului), iar dacă jucătorul este sigur acesta va trebui să selecteze itemul „Yes” („Da”), după care va putea apăsa butonul „Select” pentru a trimite răspunsul. După aceea, serverul va trimite o interogare bazei de date pentru a vedea dacă nu cumva un cont cu noul nume specificat există deja, iar dacă nu există nici un alt cont cu acel nume se va apela funcția SetPlayerName care returnează valoarea -1 în caz că numele este incorect (fapt deja verificat după introducerea numelui în dialog), valoarea 0 în caz că jucătorul are deja numele introdus (de asemenea, fapt deja verificat), iar în final valoarea 1 dacă schimbarea numelui s-a produs cu succes pe server. Dacă se returnează valoarea 1, serverul trimite un mesaj jucătorului că i s-a schimbat numele cu succes și un alt mesaj care să anunțe pentru toți ceilalți jucători această schimbare. De asemenea, în acest caz se va actualiza numele jucătorului peste tot unde este nevoie, precum acolo unde este reținut în variabile ca fiind numele deținătorului unei afaceri, al unei case sau altele, iar în final se schimbă numele contului în baza de date și se introduce o nouă înregistrare în tabelul „log_newnick” pentru a stoca tot istoricul schimbărilor de nume ale jucătorilor, într-o asemenea înregistrare fiind stocate numele vechi, numele noi, dar și ID-ul conturilor pe care s-a produs schimbarea. Deodată ce numele a fost schimbat, contul cu numele vechi nu va mai exista, iar pentru a se putea autentifica data viitoare la conectarea pe server, jucătorul va trebui să intre cu noul nume, această schimbare trebuind făcută manual din navigatorul SA-MP.
Pentru ca administratorii și cei care au un cont V.I.P. să poată vedea diverse schimbări de nume, în special în cazul în care cineva și-a uitat numele contului, există comanda
78
Constantin-Flavius Nistor Prezentare
‘/namechanges’ care acceptă ca parametru o parte din noul sau din fostul nume căutat, după
care se afișează un dialog de tip listă, cu maxim 25 de elemente, care conține câteva informații
importante legate de fiecare schimbare:
Figura 3.19 – „Exemplu dialog comanda ’/namechanges’”
Comanda respectivă, care include interogarea care trimite serverului rezultatul pe care să-l interpreteze pentru afișarea dialog-ului de mai sus, este următoarea:
Tabelul 3.27 – „Comanda ‘/namechanges’”
CMDFUNC( namechanges, playerid, params[ ], FLAG_EVERYWHERE, { } ) {
if( GZS::Player::Info[ playerid ].pAdmin < 1 && GZS::Player::Info[ playerid ].pVip < 1 ) return SendErrorMessage( playerid, 0 );
char lPlayerName[ 128 ] = "";
if( Plugins::SSCANF::unformat( params, "s[128]", lPlayerName ) )
return Utility::Message::SendUsage(playerid, "[Part of Old/New Name]");
if( !Utility::IsValidName( lPlayerName ) )
return Utility::Message::SendError( playerid, "This name is not valid !" );
gQuery = fmt::format(
"SELECT `log_newnick`.*, `a_accounts`.`Name` AS `Actual` FROM `log_newnick` "
"LEFT JOIN `a_accounts` ON `a_accounts`.`Key` = `log_newnick`.`PlayerKey` "
"WHERE `Old` LIKE '%{}%' OR `New` LIKE '%{}%' "
"ORDER BY `ID` DESC LIMIT 25", lPlayerName, lPlayerName ); Plugins::cMySQL::pquery( 1, gQuery, ShowPlayerNameChanges, playerid,
std::string( lPlayerName ) );
return 1;
}
79
Constantin-Flavius Nistor Prezentare
Se poate observa faptul că tot blocul de cod are la bază linia care începe cu CMDFUNC, aceasta fiind un macro (definiție care variază în funcție de parametrii specificați) care creează un obiect al cărei adrese este adăugată într-un std::map 55 (container de tip cheie-valoare din librăria standard a limbajului C++, unde cheia poate apărea doar o singură dată), cheia fiind numele comenzii, iar valoarea fiind adresa respectivă. Comenzile sunt procesate în acest fel pentru a evita compararea comenzii introduse de către un jucător cu alte mii de comenzi, acest container având avantajul enorm de a se duce direct la ceea ce s-a căutat, astfel salvând foarte mult timp de procesare. Într-un mod similar funcționează, de asemenea, și procesorul pentru dialog-uri.
Pentru ca rezultatul să poată fi interpretat, în funcția ShowPlayerNameChanges trebuie
preluat numărul de linii din rezultatul primit cu ajutorul funcției Plugins::cMySQL::Cache::get_row_count, după care va trebui făcută o buclă prin toate acestea pentru a prelua toate informațiile necesare, în care se formează, de asemenea, conținutul dialog-ului. gString, gText și gQuery sunt variabile globale de tip std::string, pentru ca astfel de obiecte să nu se creeze tot timpul unde este nevoie de ele, astfel salvând timp datorită evitării necesității de a construi astfel de obiecte:
Tabelul 3.28 – „Comanda ‘/namechanges’”
int lRows = 0, lPlayerKey = 0, lDate = 0; Plugins::cMySQL::Cache::get_row_count( lRows );
for( int i = 0; i < lRows; i ++ )
{
Plugins::cMySQL::Cache::get_value( i, "PlayerKey", lPlayerKey );
Plugins::cMySQL::Cache::get_value( i, "Actual", gString );
Plugins::cMySQL::Cache::get_value( i, "Old", gText );
Plugins::cMySQL::Cache::get_value( i, "New", gQuery );
Plugins::cMySQL::Cache::get_value( i, "Date", lDate );
// … formatarea cu ajutorul funcției fmt::format
}
… afișarea dialog-ului
Un alt chat special este cel de mesaje private, acela în care doar jucătorul care a trimis și cel care a primit mesajul vor putea să-l vadă. Pentru a asigura respectarea regulilor, administratorii de nivel mare (7, 8, 9 și 10) pot vedea și ei mesajul trimis, pentru a evita certurile
Mai multe informații: en.cppreference.com/w/cpp/container/map
80
Constantin-Flavius Nistor Prezentare
în privat sau reclama la alte servere, administratorii neavând voie să folosească în interes personal nimic din ceea ce observă în respectivele mesaje. De asemenea, din cauza faptului că un jucător poate dori să vadă mesajele private primite în trecut, există comanda ‘/lastpm’ care listează 20 de astfel de înregistrări pe pagină, jucătorul având la dispoziție toate mesajele private trimise sau primite de când a creat contul. Aceste mesaje private sunt preluate din tabelul „log_pm” acolo unde se stochează ID-ul mesajului privat, unix timestamp-ul momentului în care s-a trimis, ID-ul contului care a primit mesajul și al celui care l-a trimis și, în același tabel se mai află, bineînțeles, mesajul însuși. Primul mesaj în listă este cel primit/trimis cel mai recent.
Pentru ca un jucător să vadă majoritatea informațiilor contului, acesta are la dispoziție comanda ‘/stats’, așa cum s-a specificat anterior. Această comandă acceptă ca parametru opțional un alt nume sau ID de jucător (de asemenea, cu ajutorul specificatorului „u” din sscanf), pentru ca orice jucător să poată vedea informațiile conturilor altor jucători. Când se execută această comandă, se afișează un dialog care are câțiva itemi care atunci când sunt selectați afișează la rândul lor alte dialog-uri. Acești itemi sunt: „Regular” (care conține informații mai importante ale contului, care sunt stocate și în baza de date a serverului, precum data de înregistrare, banii contului, nivelul de V.I.P. sau altele), „Current” (conținând diverse informații despre starea actuală a jocului jucătorului, precum ID-ul de caracter, ora din joc, nivelul de viață sau altele), „Vehicle” (dacă este cazul, vor apărea informații despre vehiculul în care jucătorul se află, acest item apelând automat comanda ‘/vinfo’ cu ID-ul vehiculului în care se află jucătorul transmis ca unic parametru), „Weapons” (afișând armele curente ale jucătorului, alături de numărul de gloanțe, iar în josul dialog-ului fiind afișate până la ultimele 10 arme cumpărate de către jucător cu ajutorul comenzii ‘/weapons’), „Gang” (informațiile jucătorului din banda în care se află), „Businesses” (lista de afaceri deținute de către jucător, cu diverse informații despre acestea), „Houses” (similar, lista caselor deținute, aici având abilitatea de a se teleporta în ele prin selectarea unui element din listă), „Countable” (diverse statistici automatizate, despre care vom reveni cu detalii), „Network” (informații despre conexiunea jucătorului la server, cu informații interne destul de precise), „Name Changes” (o listă cu ultimele maxim 35 de schimbări de nume ale contului), „Warnings” (o listă cu ultimele maxim 20 de avertizări, cu tot cu motiv), „Cash Transfers” (ultimele maxim 30 de transferuri de bani, primite sau trimise cu ajutorul comenzii ‘/givecash’), „Shop Purchases” (ultimele maxim 35 de achiziții din ‘/shop’, asupra căruia vom reveni cu detalii), „Last Hack Detections” (ultimele maxim 35 de detectări de cheat-uri ale contului de către server, fiecare element din listă având informațiile complete ale suspiciunii).
81
Constantin-Flavius Nistor Prezentare
Figura 3.20 – „Exemplu dialog ‚Vehicle’ din comanda ’/stats’”
Pe lângă toate aceste comenzi de verificare și schimbare a informațiilor contului există multe altele, astfel oferind jucătorilor o senzație de control absolut asupra conturilor.
3.3 Nivelul, experiența și deblocabilele
Datorită nevoii de a vedea într-o manieră rapidă intensitatea activității de-a lungul timpului a unui jucător, s-a decis implementarea unor alte caracteristici: experiența („Experience”) și nivelul („Level”), acestea fiind legate între ele. De asemenea, acestea sporesc competitivitatea între jucători.
Punctele de experiență („Experience Points” = „XP”) se obține când jucătorul completează anumite acțiuni, precum câștigarea a diverse joculețe, completarea misiunilor de la locul de muncă, omorârea inamicilor sau altele.
Un jucător pornește cu nivelul („level”) 1, iar pentru a obține următorul nivel acesta are nevoie de un anumit număr de XP. Când se atinge numărul de XP necesar, jucătorului i se va incrementa cu 1 nivelul, iar numărul de XP i se va reseta. Nu există o limită de nivel. Funcția de calcul a XP-ului necesar pentru un anumit nivel este „f( nivel ) = ceil( ( nivel * 8 ) + pow( nivel, 1.5 ) )”, acolo unde „ceil” este funcția de rotunjire prin adaos a rezultatului calculelor, iar „pow” este funcția de ridicare a primului parametru la putere, aceasta fiind al doilea parametru. XP-ul necesar, conform funcției, pentru primele 60 de nivele se află în următorul tabel:
82
Se poate observa că următorul nivel cere întotdeauna mai mult XP decât precedentul, acest lucru fiind făcut pentru a crește dificultatea de obținere a următorului nivel atunci când acesta are o valoare din ce în ce mai mare. Pentru a avea o idee despre dificultatea obținerii nivelului 60, care cere 945 XP, se poate lua ca exemplu XP-ul obținut atunci când se omoară un inamic, XP-ul acesta fiind aleator între valorile 0 și 1, deci vom aproxima că există o șansă de 50% să se obțină un singur punct de experiență, ceea ce înseamnă că este nevoie în medie de 1890 de inamici omorâți, jucătorul nefăcând nici o altă acțiune care este răsplătită cu XP și neținând cont în calcul de eventualul bonusul de XP care se acordă dacă se realizează omorârea a mai mult de 2 inamici la rând fără ca jucătorul să moară la rândul său.
Un jucător își poate vedea nivelul și punctele de experiență cu ajutorul comenzii ‘/stats’, descrisă anterior, în categoria „Regular”, sau mai rapid în partea din stânga jos a ecranului, acestea fiind afișate cu ajutorul unor textdraw-uri (asupra cărora vom reveni cu imagini). De asemenea, orice jucător poate vedea nivelul altor jucători tot cu ajutorul comenzii ‘/stats’, dar și din lista de jucători, apăsând TAB, acolo unde coloana „Score” (aceasta neavând abilitatea de a fi redenumită din cauza faptului că SA-MP nu oferă această abilitate, fiind concepută doar cu scopul de a afișa scorul jucătorilor, care poate fi schimbat cu ajutorul funcței SetPlayerScore) reprezintă, de fapt, nivelul, această coloană fiind schimbată pentru un jucător cu ajutorul funcției SetPlayerScore specificate de fiecare dată când nivelul jucătorului se schimbă, așa cum se poate observa în următoarea imagine:
83
Constantin-Flavius Nistor Prezentare
Figura 3.21 – „Listă jucători”
Pentru a mări dorința jucătorilor de a trece la următoarele nivele, s-a implementat o caracteristică pe nume „Unlockables” („Deblocabile”), comanda principală pentru aceasta fiind ‘/unlockables’, care afișează ca dialog o listă cu toate beneficiile obținute la fiecare nivel, beneficii precum abilitatea de a cumpăra mai multe case sau afaceri, abilitatea de a cumpăra un nivel mai mare de V.I.P., abilitatea de a se alătura unei bande sau altele:
Figura 3.22 – „Dialog ‘/unlockables’”
În cod, toate acestea sunt adăugate într-un std::multimap 56 (container de tip cheie-valoare din librăria standard a limbajului C++, în care cheia poate apărea de mai multe ori) prin instanțiere, cheia fiind nivelul la care se deblochează, iar valoarea fiind o pereche (std::pair 57)
Mai multe informații: en.cppreference.com/w/cpp/container/multimap
Mai multe informații: en.cppreference.com/w/cpp/utility/pair
84
Constantin-Flavius Nistor Prezentare
formată dintr-un nume unic scurt (ID) și dintr-un mesaj descriptiv (toate aceste mesaje fiind afișate în dialog-ul de mai sus):
Tabelul 3.30 – „Container deblocabile”
std::multimap< unsigned int, std::pair<std::string, std::string> > gMultiMap_Unlockables = {
{ 4, { "reputation_send", "Can send reputation." } },
{ 5, { "gang_join", "Can join gangs ( '{C3C3C3}/ghelp{FFFFFF}' )." } }, { 20, { "news_command", "Can use '{C3C3C3}/news{FFFFFF}' ." } },
{ 3, { "chat_sec_three", "Can send one chat message at three seconds." } }, { 6, { "chat_sec_two", "Can send one chat message at two seconds." } }, { 4, { "business_first", "Can buy the first business." } },
{ 9, { "business_second", "Can buy the second business." } }, { 23, { "business_third", "Can buy the third business." } },
// …
};
Evitând repetarea generării unui dialog de tip listă prin citirea repetată a tuturor
elementelor de mai sus, lista este generată o singură dată la inițializarea serverului, apoi fiind
stocată într-un obiect creat cu o funcție specializată pentru stocarea conținutului dialog-urilor,
funcția fiind numită DLGProcessor::Stored::Create și care acceptă ca parametri ID-ul creat
anterior într-o variabilă pentru identificarea elementului stocat, tipul de dialog, titlul acestuia,
conținutul dialog-ului și cele două butoane, al doilea fiind opțional (pentru a nu se afișa acesta
trebuie să conțină un text gol, de lungime 0):
Tabelul 3.31 – „Generarea listei deblocabilelor”
int lCount = -1;
unsigned int lPreviousLevel = 0;
gBigString.clear( );
for( auto level: gMultiMap_Unlockables )
{
if( lPreviousLevel == level.first )
continue;
lPreviousLevel = level.first;
auto it = gMultiMap_Unlockables.equal_range( level.first );
if( std::distance( it.first, it.second ) > 1 && lCount != -1 && lCount < 2 ) gBigString += fmt::format( " \t \n" );
lCount = 0;
gBigString += fmt::format( "{}Level {}{}{}:\t", Utility::col.GREEN, Utility::col.GREY, level.first, Utility::col.GREEN );
for( auto &unlockable = it.first; unlockable != it.second; ++ unlockable )
{
85
Constantin-Flavius Nistor Prezentare
lCount ++;
if( lCount == 1 ) gBigString += fmt::format( "{}- {}{}\n", Utility::col.RED, Utility::col.WHITE, unlockable->second.second );
else gBigString += fmt::format( " \t{}- {}{}\n", Utility::col.RED, Utility::col.WHITE, unlockable->second.second );
}
if( lCount > 1 && level.first != gMultiMap_Unlockables.rbegin( )->first )
gBigString += fmt::format( " \t \n" );
}
DLGProcessor::Stored::Create( DIALOG_STORED_UNLOCKABLES_LIST, DIALOG_STYLE_TABLIST, fmt::format( "{}{} {}unlockables:", Utility::col.GREY, gMultiMap_Unlockables.size( ), Utility::col.WHITE ), gBigString, "OK", "" );
Pentru ca serverul să preia mai târziu nivelul necesar al unuia dintre numele unice (ID-
ul, primul element al perechilor de mai sus) pentru a ști când să permită jucătorului utilizarea
noii abilități, se folosește funcția GetUnlockableLevel ca în următorul exemplu:
Tabelul 3.32 – „Utilizare funcție GetUnlockableLevel”
int lRequiredLevel = GetUnlockableLevel( "gang_join" ); if( GZS::Player::Info[ playerid ].pLevel < lRequiredLevel ) {
return Utility::Message::SendError( playerid, fmt::format( "You need level {}{} {}to enter in a gang !", Utility::col.GREY, lRequiredLevel, Utility::col.WHITE ) ); }
Funcție care este implementată astfel:
Tabelul 3.33 – „Implementare funcție GetUnlockableLevel”
int GetUnlockableLevel( const std::string &shortname )
{
for( const auto &i: gMultiMap_Unlockables )
if( i.second.first == shortname )
return i.first;
Utility::Console::PrintError( "GetUnlockableLevel – shortname '{}' wasn't found !", shortname );
return -1;
}
86
Constantin-Flavius Nistor Prezentare
3.4 Statisticile automatizate, realizările și topurile
Pentru că serverul are o mulțime de caracteristici accesibile tuturor jucătorilor al căror statistici trebuiesc salvate, nefiind ideală stocarea acestora în tabelul principal în care se stochează conturile („a_accounts”) din cauză că ar fi prea multe coloane (dintre care majoritatea ar rămâne cu valoarea prestabilită 0, din cauză că foarte multe conturi accesează o mică parte din capacitățile serverului, așadar mărind inutil cu mult spațiul utilizat de către baza de date) și din cauză că adăugarea unor noi statistici (acestea fiind reprezentate în coloane) ar avea nevoie de alterarea tabelului, astfel făcând procedeul mai greoi, s-a decis implementarea unor statistici automatizate, care respectă regulile de normalizare a unei baze de date.
Acestea sunt stocate într-un tabel „a_wins” sub forma unui triplet unic format din ID-ul contului, un nume scurt de identificare (ID, similar cu deblocabilele) al statisticii și un număr care reprezintă progresul jucătorului asupra acelei statistici. De exemplu, dacă jucătorul cu ID-ul contului „1528” a plasat 5923 de obiecte în casă, acțiune al cărei statistici are ID-ul „itemsplaced”, dar a omorât și 149 de inamici al bandelor din care a făcut parte, aceasta având ID-ul „gangenemykill”, în tabelul „a_wins” vom avea două înregistrări, prima fiind formată din coloanele „1528”, „itemsplaced” și „5923”, iar a doua din coloanele „1528”, „gangenemykill” și „149”.
O astfel de statistică este stocată în cod ca un obiect care are câteva atribute (variabile ale unui obiect). Primul atribut este de tip numeric care stochează, pe biți, faptul că statistica trebuie să se salveze în baza de date, faptul că are realizări („achievements”) și faptul că are un top, alături de categoria de statistică din care face parte (existând câteva categorii de statistici: „Regular” având diverse statistici, „Test” conținând statisticile testelor de reacție, „Job” afișând statisticile locurilor de muncă, „Minigame” având statisticile participării la joculețe, „Gang” conținând statisticile acțiunilor în diverse bande ale jucătorului, iar „Admin” afișând numărul acțiunilor de administrator ale jucătorului, dacă este cazul), celelalte atribute fiind ordinea numerică a statisticii (pentru ca serverul să știe ordinea de listare, să fie exact așa cum se inițializează), titlul statisticii, descrierea statisticii și un container de tip set (std::set 58) care conține toate realizările posibile ale acesteia (acesta fiind gol dacă nu este setat în atributul de tip bitul care specifică faptul că statistica nu are realizări). Obiectele respective sunt create prin instanțierea unui std::map, unde cheia este mesajul de identificare al statisticii, iar valoarea este obiectul respectiv:
Mai multe informații: en.cppreference.com/w/cpp/container/set
87
Constantin-Flavius Nistor Prezentare
Tabelul 3.34 – „Implementare funcție GetUnlockableLevel”
int gStatOrder = 0;
std::map< std::string, CPlayerStatistics > gPlayerStatistics = {
{ "killstreak", { gStatOrder ++, STAT_FLAG_STORE |
STAT_FLAG_ACHIEVEMENT | STAT_FLAG_TOP | STAT_FLAG_TYPE_REGULAR, "Killstreaks", "How many killstreaks were started.", { 1, 10, 50, 100 } } },
{ "weaponbought", { gStatOrder ++, STAT_FLAG_STORE |
STAT_FLAG_ACHIEVEMENT | STAT_FLAG_TOP | STAT_FLAG_TYPE_REGULAR, "Weapons Bought", "Amount of weapons bought.", { 1, 50, 100, 500 } } },
// … multe altele
};
Fiecare statistică poate avea un număr nelimitat de realizări, pentru a oferi jucătorilor
anumite praguri care pot fi atinse cu ajutorul progresului acestora, primind bani și XP pentru
atingerea unei realizări. Realizările se stochează într-un alt tabel, numit „a_achievements”, care
conține ID-ul realizării obținute, ID-ul contului care are realizarea respectivă, mesajul de
identificare al statisticii și realizarea obținută, alături de unix timestamp-ul momentului
obținerii realizării respective. Când jucătorul obține o realizare, acesta vede temporar în partea
din stânga jos a ecranului un textdraw cu diverse informații despre aceasta, inclusiv cu numele
și descrierea statisticii pentru care a fost obținută. Exemple de realizări stocate în baza de date:
Figura 3.23 – „Înregistrări cu realizări”
Fiecare jucător poate vedea își poate vedea progresele asupra tuturor acestor statistici
automatizate tot cu ajutorul comenzii ‘/stats’, din itemul „Countable”, astfel fiind posibil să
vadă și progresele altor jucători, acolo unde apar într-un dialog (de tip listă, pe pagini) ca itemi
tipurile de statistici specificate anterior. Când un jucător selectează, de exemplu, itemul
„Regular”, acesta va vedea doar statisticile care au acel tip, alături de progresul acestora
(anumite statistici nu arată progresul din cauza faptului că ele sunt stocate separat, în tabelul
88
Constantin-Flavius Nistor Prezentare
principal al utilizatorilor), dar și de toate realizările lor, dacă este cazul (dacă o statistică nu are realizări, linia care numeră realizările nu va apărea), fiecare realizare completată de către jucătorul respectiv fiind notată cu verde, în imaginea de mai jos observându-se că sunt doar două realizări care n-au fost obținute:
Figura 3.24 – „Lista unui tip de statistici”
De asemenea, pentru a spori competitivitatea între jucători cu ajutorul acestor statistici automatizate, s-a creat un top al progresului jucătorilor asupra tuturor statisticilor automatizate. Topul este accesibil cu ajutorul comenzii ‘/top’, care afișează un dialog de tip listă, în care există, de asemenea, anumiți itemi prestabiliți (introduși manual, precum „The Richest”, „Most Achievements”, „Most Deaths” sau altele), dar și itemii care conțin statisticile care dețin ca proprietate faptul că acestea au un top. Fiecare top are un ID, care poate fi folosit ca parametru opțional pentru comanda respectivă, astfel ajungând direct la listarea topului. Generarea este realizată astfel, acolo unde variabila „gBigString” este conținutul final al listei, iar „gPlayerStatisticsOrdered” este tot un std::map, dar care este inițializat la pornirea serverului, cheia fiind numărul de ordine al statisticii, acesta fiind setat la inițializarea variabilei „gPlayerStatistics”, iar valoarea fiind mesajul de identificare al statisticii:
89
Constantin-Flavius Nistor Prezentare
Tabelul 3.35 – „Generare itemi statistici în dialog-ul listă al comenzii ‘/top’”
for( auto const &eMap: gPlayerStatisticsOrdered )
{
auto const &e = *( gPlayerStatistics.find( eMap.second ) ); if( e.second.flags & STAT_FLAG_TOP )
gBigString += fmt::format( "{}{}. {}Most '{}'\n", Utility::col.YELLOW,
lCount, Utility::col.WHITE, e.second.title );
}
După ce se selectează un item care este preluat automat din containerul cu statistici, de exemplu, către baza de date este trimisă o interogare care este generată astfel:
Tabelul 3.36 – „Generare interogare itemi automați din ‘/top’”
gQuery = fmt::format( „SELECT `a`.`Name` AS `Player`, IF( `p`.`ID` IS NULL, -1, `p`.`ID` ) AS `ID`, `t`.`Amount` AS `Amount`, `a`.`RegisteredDate` AS `RegDate` FROM„
( SELECT `Key`, `Total` AS `Amount` FROM `a_wins` WHERE `ShortName` = ‚{}’ ORDER BY `Amount` DESC LIMIT {} ) `t` „
„LEFT JOIN `playersonline` `p` ON `p`.`Key` = `t`.`Key` AND `p`.`ServerIP` = INET_ATON( ‚{}’ ) „
„LEFT JOIN `a_accounts` `a` ON `t`.`Key` = `a`.`Key` „ „WHERE `Amount` != 0 „
„ORDER BY `Amount` DESC”, e.first, TOP_PLAYERS, gServerIP );
Al cărei rezultat odată primit acționează o funcție în care se afișează un dialog de tip
listă cu primii 15 jucători din acesta (TOP_PLAYERS fiind o definiție care are valoarea 15), în
care primul jucător are culoarea aurie (medalia de aur), al doilea are culoarea argintie (medalia
de argint), iar al treilea are culoarea maronie (medalia de bronz), ceilalți fiind cu roșu, culori
care pot fi înlocuite doar de culoarea jucătorului respectiv dacă se află pe server, caz în care va
apărea și ID-ul acestuia de pe server:
Figura 3.25 – „Listarea unui top”
90
Constantin-Flavius Nistor Prezentare
3.5 Casele și afacerile
Alte caracteristici cu impact mare asupra jucătorilor, care sunt parțial asemănătoare între ele, sunt casele („houses”) și afacerile („businesses”), fiecare dintre ele având posibilitatea de a fi deținută de către un singur jucător. Fiecare dintre acestea sunt create de către deținătorul serverului cu ajutorul comenzilor ‘/hcreate’ sau ‘/createbusiness’.
Intrările în case sunt plasate în fața caselor existente deja în joc, iar acestea sunt stocate în baza de date în tabelul „houses”. O casă are în baza de date un ID, pornind de la 0, care este setat automat ca primul număr pozitiv care nu este utilizat de nici o altă casă. Altfel spus, dacă există casele cu ID-urile 0, 1, 2, 3 și 4, iar casa cu ID-ul 2 este ștearsă cu ajutorul comenzii ’/hremove’, următoarea casă care se va crea va avea tot ID-ul 2, deoarece acela va fi primul număr liber, nealocat unei alte case. O casă poate de avea, de asemenea, un nume, care inițial este gol (setat ca NULL în tabel), care poate fi schimbat doar de către jucătorul care o deține, în tabel fiind stocat ID-ul contului (0 în caz că nu are un deținător), iar numărul total al deținătorilor de când a fost creată este stocat tot în acest tabel, fiind incrementat cu 1 de fiecare dată când casa este cumpărată de către cineva. De asemenea, se stochează unix timestamp-ul momentului în care a fost cumpărată ultima dată, acesta fiind setat ca 0 dacă nu există nici un deținător curent. Pe lângă poziții, se stochează și unghiul la care jucătorii trebuie să fie poziționați atunci când ies din casă, pentru a fi cu spatele la ușă, se mai stochează ID-urile de lume virtuală („virtual world”, acestea fiind un fel de universuri paralele sau „dimensiuni”, cu ajutorul acestora având posibilitatea ca într-un singur loc să fie mulți jucători care nu se pot vedea între ei din cauza ID-ului diferit de lume virtuală care se poate seta pentru fiecare, dar se poate ca într-o lume virtuală să fie și alte entități precum obiectele față de oricare altă lume virtuală, lumea virtuală implicită fiind cea cu ID-ul 0) și de interior 59 (acestea sunt folosite de către motorul jocului original pentru a afișa jucătorului anumite obiecte speciale, precum cele din interiorul unui magazin sau altele, nefiind vizibile la pozițiile respective dacă jucătorul are setat alt ID de interior), numele locației din joc (pentru a evita detectarea locației de fiecare dată când se încarcă o casă din baza de date, detectarea fiind realizată cu ajutorul unei bucle printr-o matrice de dimensiuni mari care stochează toate limitele și numele de zone), parola de acces în casă (inițial fiind setată ca NULL în baza de date, aceasta fiind setată doar în timp ce deținătorul acesteia a decis că este nevoie de o parolă de acces, fiind stocată, de asemenea, pentru securitate, în format hash, precum parolele de conturi, dar într-un alt format de generare), prețul casei, valoarea de vânzare a casei (inițial este setată ca 0, poate fi schimbată de către
Listă cu toate interioarele: weedarr.wikidot.com/interior
91
Constantin-Flavius Nistor Prezentare
deținător dacă acesta vrea s-o vândă unui jucător pentru un preț diferit față de cel inițial, jucătorul având de asemenea posibilitatea să vândă casa direct, dar doar pentru 75% din prețul acesteia), statusul ușii casei (0 în caz că este încuiată, astfel jucătorul având posibilitatea de a seta o parolă de acces pe care o poate da doar jucătorilor cărora dorește să le ofere acces, iar 1 în caz contrar, astfel având acces orice alt jucător) și o setare pentru a ști dacă jucătorii trebuiesc dezarmați automat în casă (0 dacă se permit arme, iar 1 în caz contrar, serverul dezarmând jucătorii cu ajutorul funcției SetPlayerArmedWeapon într-un timer constant). Exemple de înregistrări de case:
Figura 3.26 – „Exemple de înregistrări case”
O casă are în exterior câteva entități care îi aparțin, pentru a fi posibilă identificarea acesteia de către jucători, entitățile fiind pickup-ul (când un jucător intră în acesta i se poate afișa un dialog pentru a o cumpăra sau pentru a intra în aceasta, pickup care este afișat cu modelul unei căsuțe verzi atunci când poate fi cumpărată direct, cu o căsuță galbenă, când este pusă la vânzare de către deținător sau cu o căsuță albastră atunci când este deja cumpărată de către cineva și nu este pusă la vânzare), 3DTextLabel-ul (care este creat la aceeași poziție cu pickup-ul, în acesta fiind afișate diverse informații despre casă), iconița de pe hartă (care apare doar deținătorului, pentru a evita supraaglomerarea hărții jocului) și, eventual, vehiculele parcate de către deținătorul acesteia, el având posibilitatea de a cumpăra și edita vehicule cu ajutorul comenzii ‘/hveh’, fiind posibil ca acestea să fie parcate doar în jurul casei:
Figura 3.27 – „Entitățile exteriorului unei case”
92
Constantin-Flavius Nistor Prezentare
Vehiculele unei case sunt stocate în tabelul „housevehicles”, în acesta fiind stocate toate proprietățile acestora, precum casa de care aparțin, slot-ul în care este salvat, numele, prețul de cumpărare, poziția, culorile, plăcuța de înmatriculare, componentele sau altele:
Figura 3.28 – „Exemple de înregistrări vehicule de case”
Când deținătorul casei intră în aceasta, acestuia i se va afișa un textdraw temporar pentru a i se aminti faptul că există comanda ‘/housemenu’ cu ajutorul căreia poate seta un nume casei, o poate pune la vânzare, poate vedea ultimii vizitatori sau multe altele. În interior, există, de asemenea, câteva entități: checkpoint-ul de ieșire și obiectele din casă, implicit interiorul casei fiind gol, conținând doar o ușă, podeaua, tavanul și pereții, jucătorii având posibilitatea de a își crea singuri alți pereți, uși și de a își mobila casa cu ajutorul interfeței de editare a poziției și rotației unui obiect, care este disponibilă cu ajutorul funcției EditObject:
Figura 3.29 – „Interiorul unei case și interfața de editare a unui obiect”
93
Constantin-Flavius Nistor Prezentare
Obiectele care sunt adăugate de către deținătorul unei case (cu ajutorul comenzii ‘/housemenu’, în acest dialog existând o categorie de la care se pot adăuga, modifica sau șterge aceste obiecte) se numesc „items” („articole” sau „elemente”), având o limită de 300 per casă (slot-urile de obiecte pornind de la 0), iar acestea sunt stocate în tabelul „houseitems”, acesta având coloanele care stochează ID-ul casei de care aparține obiectul, slot-ul de obiect al casei în care este adăugat, ID-ul modelului obiectului, poziția, rotația, numele obiectului la achiziție, prețul de achiziție și unix timestamp-ul ultimei modificări, acesta fiind inițial setat cu valoarea momentului în care a fost adăugat:
Figura 3.30 – „Exemple de înregistrări obiecte de case”
Pentru ca toate aceste elemente ale unei case să fie stocate cât mai bine structurat în cod, evitând și nevoia de a interoga mereu baza de date, s-a creat clasa CHouse, care conține sub clase pentru vehicule și obiecte. Fiecare casă este stocată într-un std::map acolo unde cheia este ID-ul casei, iar valoarea este un pointer unic inteligent („smart unique pointer” 60 – atunci când pointer-ul este șters se distruge automat și obiectul, acționând destructorul clase), fiecare astfel de valoare conținând având în ea și câte un std::map similar pentru vehicule și obiecte, cel de vehicule având încă un astfel de std::map pentru toate componentele vehiculului, acestea fiind clasificate în 14 categorii 61, categoria fiind cheia, iar ID-ul componentei fiind valoarea:
Mai multe informații: en.cppreference.com/w/cpp/memory/unique_ptr
Lista categoriilor de componente: wiki.sa-mp.com/wiki/Componentslots
94
Constantin-Flavius Nistor Prezentare
Tabelul 3.37 – „Prototipul clasei CHouse și al containerului caselor”
class CHouse
{
public:
int id = 0;
std::string name;
std::string password;
int value = 0;
int sellValue = 0;
GZS::House::Privacy privacy = GZS::House::Privacy::OPENED;
…
Vehicles
class CHouseVehicle
{
public:
int house = 0;
int slot = 0;
std::map< int, int > components;
int paintjob = 0;
…
~CHouseVehicle( );
};
std::map< int, CHouseVehicle > vehicles;
…
Items
class CHouseItem
{
public:
int house = 0;
int slot = 0;
int model = 0;
std::string name;
int buyPrice = 0;
…
~CHouseItem( );
};
std::map< int, CHouseItem > items;
Destructor ~CHouse( );
};
std::map< int, std::unique_ptr< GZS::House::CHouse > > hMap;
95
Constantin-Flavius Nistor Prezentare
Unde un astfel de obiect de casă este alocat astfel la crearea acesteia, după ce se identifică primul ID liber:
Tabelul 3.38 – „Identificarea primului ID liber de casă și alocarea uneia”
int h = 1;
while( GZS::House::hMap.find( h ) != GZS::House::hMap.end( ) )
h ++;
GZS::House::hMap[ h ] = std::unique_ptr< GZS::House::CHouse >( new GZS::House::CHouse( ) );
Afacerile, spre deosebire de case, sunt mult mai limitate, acestea având doar un exterior la care se află un pickup (când se intră în acesta apare un dialog care arată informațiile afacerii, din acesta existând posibilitatea de a cumpăra sau de a vinde, în funcție de caz), un 3DTextLabel conținând numele afacerii și o iconiță care este afișată tuturor, afacerile fiind mult mai rare decât casele. O afacere acordă deținătorului o dată la 30 de minute o valoare în bani echivalentă cu 5% din prețul de achiziționare al acesteia, ceea ce înseamnă că o afacere cu cât este mai scumpă, cu atât este mai profitabilă. Din momentul în care o afacere este cumpărată, aceasta nu poate fi cumpărată de către nimeni altcineva timp de 7 zile, dar deținătorul poate extinde această perioadă de imunitate cu încă 7 zile cu ajutorul comenzii ‘/buybiztime’, care poate fi utilizată doar atunci când au mai rămas mai puțin de 3 ore din perioada de imunitate, această extindere costând ~47.6% (1/3 + 1/7 = o treime plus o șeptime) din prețul afacerii. Jucătorul poate vinde afacerea, iar în schimb acesta va obține 50% din prețul inițial. Datorită numărului restrâns de afaceri și a timpului limită, între jucători este iarăși creată o competitivitate semnificativă, ceea ce ajută serverul.
Similar caselor, pentru afaceri este utilizată o clasă care este utilizată pentru a adăuga obiecte într-un std::map, iar în baza de date afacerile sunt stocate în tabelul „businesses”, cu următoarele exemple de înregistrări:
Figura 3.31 – „Exemple de înregistrări de afaceri”
96
Constantin-Flavius Nistor Prezentare
3.6 Mod de joc: Bandele
O altă caracteristică foarte importantă a acestui server care, probabil, are cel mai mare impact asupra jucătorilor, este cea de bande („gangs”). Un gang poate avea membri, o bază pe harta jocului, vehicule capturate, zone capturate, protecții, un aliat și multe altele. Există 10 gang-uri prestabilite care le imită ca nume, culori și caractere pe cele deja existente în povestea singleplayer a jocului: „Los Santos Vagos” (ID 1), „The Bikers” (ID 2), „The Ballas” (ID 3), „Varrios Los Aztecas” (ID 4), „Grove Street Families” (ID 5), „Las Venturas Mafia” (ID 6), „The Hitmen” (ID 7), „San Fierro Triads” (ID 8), „San Fierro Rifa” (ID 9) și „Da Nang Boys” (ID 10).
Jucătorii au la dispoziție 5 ranguri („ranks”) într-un gang: „helper” („ajutor”, ID 1), „member” („membru”, ID 2), „loyal” („loial”, ID 3), „coleader” („colider”, ID 4) și „leader” („lider”, ID 5). Fiecare jucător dintr-un gang poate avea un număr de puncte („gang points”) care se poate mări prin omorârea unui inamic sau prin alte modalități, aceste puncte reflectând direct activitatea jucătorilor în gang-uri. Fiecare rang are o varietate de comenzi disponibile, acestea fiind listate într-un dialog accesibil cu ajutorul comenzii ‘/gcmds’:
Figura 3.32 – „Listarea comenzilor pentru gang-uri”
Primul rang, cel de helper, se obține doar în momentul în care jucătorul intră în gang-ul respectiv cu ajutorul comenzii ‘/genter’, care poate fi utilizată doar dacă jucătorul are nivelul 5 și când acesta se află în jurul unui pickup de intrare în gang, care arată astfel:
97
Constantin-Flavius Nistor Prezentare
Figura 3.33 – „Un loc de utilizare a comenzii ‚/genter’”
Al doilea rang, „member”, se poate obține prin acceptarea cu ajutorul comenzii ‚/gaccept’ a unei invitații directe („/ginvite”) de la un membru deja alăturat gang-ului (care deține rangul „loial” sau mai mare). De asemenea, acest rang poate fi obținut de un jucător care are rangul „helper” în momentul în care acesta depășește 99 de puncte de gang, fiind automat promovat. Rangul de lider poate fi setat doar de către deținătorul serverului cu ajutorul comenzii ‘/makemember’.
Un jucător care are rangul „coleader” sau mai mare poate promova orice alt jucător din gang-ul respectiv cu ajutorul comenzii ‘/gpromote’ doar dacă are rangul mai mare cu unu față de rangul pe care-l va avea jucătorul respectiv după promovare, ceea ce înseamnă că un jucător cu rangul „coleader” nu va putea da același rang unui alt jucător. De asemenea, jucătorii care dețin rangul „coleader” pot retrograda alți jucători din gang cu ajutorul comenzii ‘/gdemote’ doar dacă are rangul mai mare decât al acestora. Pentru a promova și retrograda un jucător din gang când nu se află pe server, se pot folosi, similar, comenzile ‘/gopromote’ și ‘/godemote’, unde litera „o” înseamnă „offline” („deconectat”), aceste comenzi acceptând ca parametru numele complet al jucătorului asupra căruia să se aplice acțiunea. Pentru a ieși dintr-un gang, un jucător poate utiliza comanda ‘/gleave’, care necesită și un motiv de minim 7 caractere. Dacă un lider al gang-ului dorește să dea afară un jucător, acesta poate folosi comanda ‘/gkick’, dacă acel jucător este conectat, sau comanda ‘/gokick’, dacă este deconectat, el neavând posibilitatea de a da afară alți lideri.
Într-un gang există o infinitate de locuri pentru membrii cu rangul „helper”, aceștia neavând acces la comenzi sau permisiuni care permit efectuarea unor acțiuni împotriva gang-ului. Jucătorii cu acest rang pot fi listați într-un dialog, pe pagini, cu ajutorul comenzii ‘/gh’ („gang helpers”), în care primii jucători sunt cei conectați pe server, toți fiind sortați descendent
98
Constantin-Flavius Nistor Prezentare
după numărul de puncte și după ultimul moment al activității pe server (în caz că doi jucători au același număr de puncte):
Figura 3.34 – „Dialog-ul afișat cu ajutorul comenzii ‘/gh’”
Pentru toate celelalte ranguri în gang, există un număr limitat de locuri, care poate fi schimbat de către deținătorul serverului cu ajutorul comenzii ‘/ssettings’, implicit având valoarea 30. Similar comenzii ‘/gh’, aceștia pot fi listați cu ajutorul comenzii ‘/gm’, sortarea prioritară fiind cea a rangului, apoi cea a punctelor și, în final, cea a ultimului moment de activitate:
Figura 3.35 – „Dialog-ul afișat cu ajutorul comenzii ‘/gm’”
99
Constantin-Flavius Nistor Prezentare
Implementarea comenzii ‘/gm’ alături de interogarea trimisă de către aceasta este următoarea:
Tabelul 3.39 – „Implementarea comenzii ‘/gm’”
CMDFUNC( gm, playerid, params[ ], FLAG_EVERYWHERE, { } )
{
int lGangID = 0;
if( Plugins::SSCANF::unformat( params, "i", &lGangID ) )
return Utility::Message::SendUsage( playerid, "[ GangID:
'{C3C3C3}/gangs{FFFFFF}' ]" );
if( gSet_Gangs.find( lGangID ) == gSet_Gangs.end( ) )
return Utility::Message::SendError( playerid, "Invalid Gang ID !" );
gQuery = fmt::format( "SELECT `a_accounts`.`Key`, `a_accounts`.`Name`, `LastActive`, `GangRank`, `GangPoints`, IF( `playersonline`.`ID` IS NULL, -1, `playersonline`.`ID` ) AS `ID` FROM `a_accounts` "
"LEFT JOIN `playersonline` ON `playersonline`.`Key` = `a_accounts`.`Key` AND `playersonline`.`ServerIP` = INET_ATON( '{}' ) " "WHERE `Gang` = {} AND `GangRank` > 1 "
"ORDER BY `ID` = -1 DESC, `GangRank` DESC, `GangPoints` DESC, `LastActive` DESC", gServerIP, lGangID );
Plugins::cMySQL::pquery( 1, gQuery, ShowPlayerGangMembers, playerid, lGangID );
return 1;
}
Similar cu punctele de gang pentru jucători, există, de asemenea, și punctele totale ale
unui astfel de gang. Când un jucător obține prin diverse acțiuni un număr de puncte de gang,
același număr de puncte este adăugat și gang-ului din care face parte. Pentru a spori
competitivitatea, există un top care poate fi văzut cu ajutorul comenzii ‘/topg’:
Figura 3.36 – „Dialog-ul afișat cu ajutorul comenzii ‘/gm’”
100
Constantin-Flavius Nistor Prezentare
Datorită faptului că serverul trebuie să stocheze în variabile numărul de puncte al unui gang, pentru a evita să se interogheze des baza de date, lista de mai sus s-a putut implementa cu ajutorul unui std::multimap, cheia fiind numărul de puncte, iar valoarea fiind ID-ul gang-ului. Acest lucru a fost posibil datorită faptului că un astfel de container este ordonat automat, în ordine crescătoare, după valoarea cheii. Pentru a afișa lista sortată invers, containerul trebuie iterat, bineînțeles, invers, cu ajutorul funcțiilor std::multimap::crbegin și std::multimap::crend (unde „c” înseamnă „constant”, pentru a indica faptul că nu se dorește modificarea valorilor din acesta, iar „r” înseamnă „reverse”, astfel încât primul element va fi, de fapt, ultimul din containerul sortat ascendent), comanda fiind implementată astfel:
Tabelul 3.40 – „Implementarea comenzii ‘/topg’”
CMDFUNC( topg, playerid, params[ ], FLAG_EVERYWHERE, { } )
{
std::multimap< int, int > lMap_PointsGangs; // points, gangid
for( auto gangid: gSet_Gangs )
{
lMap_PointsGangs.insert( { gGangInfo[ gangid ].points, gangid } );
}
int lCount = 0;
gBigString = "{FFFFFF}#\t{FFFFFF}Gang\t{FFFFFF}Points\n";
for( auto e = lMap_PointsGangs.crbegin( ); e != lMap_PointsGangs.crend( ); ++ e ) {
gBigString += fmt::format( "{}{}.\t{}{}({})\t{}{}\n", Utility::col.WHITE,
lCount, gGangInfo[ e->second ].hexcolor, gGangInfo[ e->second ].name, e->second, Utility::col.GREY, Utility::String::FormatNumber( e->first ) );
}
DLGProcessor::ShowCustom( playerid, DIALOG_EMPTY,
DIALOG_STYLE_TABLIST_HEADERS, "{FFFFFF}Top Gangs:", gBigString, "OK", ""
);
return 1;
}
Pentru a evita ca un gang sau mai multe să domine topul pentru o perioadă nedeterminată de timp, s-a implementat o caracteristică numită „Gang Winners”, care resetează topul în prima zi a lunii și care stochează toate informațiile ale tuturor gang-urilor din luna care a trecut, membrii echipei câștigătoare primind un premiu în bani și XP, acesta fiind mai mare pentru jucătorii care au depășit 500 de puncte de gang. Datorită faptului că gang-urile sunt cele
101
Constantin-Flavius Nistor Prezentare
mai extinse și jucate caracteristici a serverului, această resetare a topului și premiere a câștigătorului face ca între jucători să existe o competitivitate mult mai mare, stabilind, de asemenea, o comunicare mai bună între jucători datorită necesității de a se organiza în echipe. Gang-urile câștigătoare pot fi listate cu ajutorul comenzii ‘/gwinners’, aceasta oferind și o scurtă idee despre activitatea gang-urilor pe diverse luni:
Figura 3.37 – „Dialog-ul afișat cu ajutorul comenzii ‘/gwinners’”
Atunci când se dă click pe butonul de selectare al unei luni, serverul va afișa informațiile principale ale tuturor gang-urilor din acea lună:
Figura 3.38 – „Topul gang-urilor dintr-o lună, din comanda ‘/gwinners’”
102
Constantin-Flavius Nistor Prezentare
Aceste informații lunare sunt stocate în două tabele din baza de date, primul tabel fiind „gangmonths”, în care există trei coloane, prima fiind cheia primară reprezentată de ID-ul incrementat automat al lunii care a trecut, a doua fiind anul din care aceasta face parte, iar a treia coloană fiind numărul lunii respective, de la 1 la 12. Al doilea tabel se numește „gangmonthsinfo”, iar acesta stochează informațiile propriu-zise, acesta conținând o coloană care are valoarea ID-ului lunii din tabelul precedent, iar a doua fiind ID-ul gang-ului, aceste două coloane formând o cheie unică („Unique Key” 62) aplicată tabelului, restul coloanelor fiind informațiile disponibile în dialog-ul de mai sus.
Fiecare gang are o bază pe harta jocului, așa cum s-a menționat anterior. Pentru a nu avea acces oricine în aceasta, fiecare bază are o hartă care conține ziduri mari, porți și alte elemente pentru înfrumusețare.
Figura 3.39 – „Baza unui gang”
Dacă un jucător se alătură unui gang, acesta va primi mereu spawn în baza gang-ului, doar dacă în momentul alăturării jucătorul nu are deja activat un spawn personalizat, precum cel într-o casă pe care o deține. Dacă jucătorul dorește ulterior să nu mai primească spawn în baza gang-ului, sau dorește să activeze acest lucru, o poate face cu ajutorul comenzii ‘/gtogglespawn’, după ce dezactivează spawn-ul personalizat anterior.
Fiecare bază conține un sediu central („HQ” – „headquarter”) accesibil dintr-un pickup doar de către jucătorii care sunt alăturați gang-ului, iar din acesta se pot face diverse achiziții,
Mai multe informații: wikipedia.org/wiki/Unique_key
103
Constantin-Flavius Nistor Prezentare
precum obținerea de viață și armură, schimbarea interiorului sediului de către lideri, cumpărarea unor protecții, mine sau altele.
Figura 3.40 – „Pickup-ul și 3DTextLabel-ul unui sediu central”
Majoritatea achizițiilor din sediu se fac utilizând banii din banca gang-ului, cu ajutorul dialog-ului magazinului care se accesează folosind comanda ‘/gshop’. Orice jucător alăturat poate utiliza, doar în sediu, comanda ‘/gbank’, cu ajutorul căreia poate depozita orice sumă de bani și care nu poate fi retrasă. Banca gang-ului obține anumite sume mici de bani atunci când jucătorii efectuează diverse acțiuni de capturare. Pentru a încuraja depozitarea a cât mai multor sume de bani, în această comandă există, de asemenea, un top 3 al donatorilor care se află în mod curent în gang:
Figura 3.41 – „Dialog-ul comenzii ’/gbank’”
Pentru ca o bază să nu poată fi accesată ușor de către oricine există porți de acces. Un gang poate avea o poartă sau mai multe, în funcție de mărimea bazei și de locația din joc la care se află aceasta. O poartă este un obiect care poate fi mișcat cu ajutorul funcției MoveObject.
104
Constantin-Flavius Nistor Prezentare
Pentru a fi deschis de către jucătorii care fac parte din gang-ul al căruia este baza, există comanda ‘/gate’ care are efect doar pe cea mai apropiată poartă, jucătorul trebuind să fie la o distanță relativ mică față de aceasta. La locația unei porți este afișat, de asemenea, un 3DTextLabel, pentru ca jucătorii să poată vedea comenzi sau informații utile despre aceasta.
Figura 3.42 – „O poartă și 3DTextLabel-ul acesteia”
Pentru ca baza să fie accesată de jucători inamici, aceștia au la dispoziție comanda ‘/blowgate’, care după un timp de așteptare va pune pe poartă un pickup cu model de exploziv (pentru a fi utilizată comanda, jucătorul are nevoie de cel puțin un C4 cumpărat cu ajutorul comenzii ‘/c4’) care după câteva secunde va crea o explozie pentru jucătorii din jur cu ajutorul funcției CreateExplosionForPlayer, moment în care obiectul porții va fi distrus. Pentru ca jucătorii care fac parte din gang-ul respectiv să poată îngreuna tot acest proces de distrugere, aceștia pot pune armură pe poartă cu ajutorul comenzii ‘/plantarmour’ (care necesită o armură cumpărată din sediu) pentru ca aceasta să necesite o explozie în plus pentru distrugere (și deci inamicii vor trebui să utilizeze iarăși comanda ‘/blowgate’), la prima explozie fiind distrusă doar armura, poarta rămânând închisă.
105
Constantin-Flavius Nistor Prezentare
O altă caracteristică importantă disponibilă pentru gang-uri este cea de capturare a vehiculelor. La începutul lunii, după ce topul de gang-uri este resetat, în fiecare bază există un număr de 10 vehicule (deoarece există 10 gang-uri, există un total de 100 de astfel de vehicule). Un vehicul de gang poate fi capturat de către un jucător al unui gang inamic cu ajutorul comenzii ‘/gbreakveh’, fiecare gang având o limită de 50 de vehicule care pot fi deținute, iar pentru a fi utilizată această comandă este necesară cumpărarea unor unelte de spargere a vehiculului („Gang Breaking Tools”) din magazinul gang-ului. Pentru că atunci când jucătorul sparge cu succes (există șanse ca spargerea să eșueze) un vehicul inamic este pus în acesta fără ca vehiculul să fie mutat direct în baza noului gang deținător, el trebuie să conducă vehiculul până în propria bază și să-l parcheze utilizând comanda ‘/gplaceveh’ pe teritoriul acesteia (în limitele gangzone-ului), pentru care va primi bani, XP și puncte adiționale, pe lângă cele obținute după ce a spart vehiculul cu succes cu ajutorul comenzii ‘/gbreakveh’. În baza de date, vehiculele de gang-uri sunt stocate în tabelul „gangvehicles” care conține, în coloane, ID-ul vehiculului, ID-ul modelului de vehicul, coordonatele implicite de la începutul lunii, ID-ul gang-ului care deține vehiculul la începutul lunii, coordonatele actuale, gang-ul care îl deține în momentul de față și ID-ul contului care a parcat ultima dată vehiculul:
Figura 3.43 – „Exemple înregistrări vehicule de gang”
O bază este reprezentată pe hartă cu un gangzone de culoarea gang-ului proprietar, iar aceasta poate fi capturată de către oricare gang inamic al celui care deține baza în mod curent, sediul central și vehiculele (ele trebuind să fie capturate individual) rămânând ale gang-ului care le dețin în mod implicit, iar jucătorii care fac parte din gang-ul care deține baza în mod implicit nu vor mai primi spawn în aceasta, ci vor trebui să meargă până la aceasta pentru a o captura iarăși. O bază poate fi capturată de la gang-ul care o deține implicit doar dacă acesta nu mai deține nici un vehicul. Fiecare bază are un punct prestabilit din care se poate captura cu ajutorul comenzii ‘/capture’, aceasta finalizându-se după câteva minute, iar jucătorul care a
106
Constantin-Flavius Nistor Prezentare
pornit capturarea nu trebuie să moară și nici să părăsească zona de la care se poate captura, altfel capturarea va eșua. Punctul de capturare este situat, în general, pe un turn aflat pe structuri înalte care pot fi accesate cu greu, din cauza drumului lung și ascendent, fiind cel mai îndepărtat loc din bază. În timp ce este capturată, gangzone-ul acesteia clipește intermitent (cu ajutorul funcției GangZoneFlashForPlayer) între două culori: cea a gang-ului deținător și cea a gang-ului care încearcă să o captureze.
Figura 3.44 – „Punct de capturare al unui gang”
Jucătorii din gang-uri pot obține puncte adiționale capturând zone de gang, acestea fiind reprezentate pe hartă tot cu ajutorul gangzone-urilor. Capturarea acestora se realizează tot cu ajutorul comenzii ‘/capture’, iar jucătorii pot fi oriunde pe teritoriul zonei pentru pornirea și finalizării capturării, altfel capturarea eșuând (folosind zonele din Streamer Plugin, utilizând funcția IsPlayerInDynamicArea pentru comandă și evenimentul OnPlayerLeaveDynamicArea pentru detectarea părăsirii zonei). În timp ce un jucător capturează o zonă, un jucător din același gang sau din cel aliat pot asista la capturare (obținând puncte de asist în gang, bani și XP), iar inamicii pot intra în zonă cât timp durata de capturare n-a ajuns la final, astfel încât aceștia vor trebui omorâți pentru a reuși capturarea, chiar dacă timpul de capturare s-a finalizat. Există un număr prestabilit de 100 de zone, iar capturarea durează aproximativ jumătate din durata de capturare a unei baze.
107
Constantin-Flavius Nistor Prezentare
Figura 3.45 – „Locațiile bazelor și zonelor de gang pe harta completă a jocului”
Deoarece aceste zone au formă dreptunghiulară, în tabelul „gangzones” se stochează
ID-ul zonei, ID-ul gang-ului care o deține, dar și valorile minime și maxime ale coordonatelor
X și Y, valori care se utilizează în funcțiile GangZoneCreate și CreateDynamicRectangle:
Figura 3.46 – „Exemple înregistrări zone de gang”
Pentru ca duratele de distrugere a porților și de capturare a bazelor și zonelor gang-ului să fie mai lungi, alături de creșterea timpului de spargere a vehiculelor și de micșorarea șanselor de spargere cu succes ale acestora, jucătorii care dețin rangul „loial” în acesta pot achiziționa protecții din magazinul gang-ului, protecții care durează 60 de minute și costă diverse sume de bani din banca gang-ului.
O altă caracteristică utilă care este valabilă pentru gang-uri este cea care permite plasarea unor mine pe teritoriul unei baze cu ajutorul comenzii ‘/plantmine’, care explodează doar atunci când un inamic se apropie de acestea (folosind zonele din Streamer Plugin și
108
Constantin-Flavius Nistor Prezentare
evenimentul OnPlayerEnterDynamicArea), omorându-l instant. Minele pot fi cumpărate din magazinul gang-ului, iar un jucător poate deține și plasa un total de două astfel de mine la un moment dat (limita este extinsă cu unu dacă jucătorul este „V.I.P. Premium”). Minele nu pot fi plasate aproape de porțile bazei (pentru a nu opri complet accesul în bază cu ajutorul acestora) și nici foarte aproape unele de altele.
Un gang poate avea un alt gang ca aliat dacă dorește, o astfel de invitație având posibilitatea de a fi trimisă cu ajutorul comenzii ‘/gally invite’ și fiind acceptată de către un lider al gang-ului invitat cu ajutorul aceleiași comenzi cu un alt parametru: ‘/gally accept’. Gang-urile aliate nu își pot captura vehiculele, zonele sau bazele între ele. De asemenea, datorită faptului că orice gang are un chat special („gang chat”) în care jucătorii alăturați acestuia pot trimite mesaje precedate de caracterul ‘!’ („semnul exclamării”; cu ajutorul evenimentului OnPlayerText), din momentul în care gang-ul are un aliat, membrii acestuia vor vedea și ei toate mesajele care se trimit în acel gang chat. De asemenea, majoritatea informațiilor care sunt trimise jucătorilor unui gang vor fi trimise și jucătorilor gang-ului aliat, acestea fiind utile, de exemplu, când bazele, zonele sau vehiculele acestuia sunt în proces de capturare de către un gang inamic, aliatul având astfel posibilitatea de a se duce să ajute la apărarea acestora.
Așa cum s-a specificat anterior, fiecare gang are o culoare unică, aceasta fiind stocată în baza de date într-o coloană de tip INT, pentru a fi reprezentată pe 32 de biți, salvând spațiu de stocare și ușurând stocarea rezultatului preluării din baza de date. De asemenea, fiecare gang are 3 caractere prestabilite de model diferit care sunt setate jucătorilor la spawn-ul în bază, în funcție de rang, fiecare gang având și un număr de 6 arme care se acordă jucătorilor acestora tot la spawn, gloanțele variind în funcție de numărul de puncte ale gang-ului, mai multe puncte însemnând mai multe gloanțe, armele având abilitatea de a fi schimbate cu ajutorul comenzii ‘/gsettings’ de către lideri. Aceste informații sunt stocate în tabelul „gangs”, alături de alte informații precum ID-ul gang-ului, numele, numele scurt al acestuia (pentru a scurta mesajele de gang din chat, dar și pentru a fi folosit ca număr de înmatriculare al vehiculelor capturate, plăcuța de înmatriculare având un număr restrâns de caractere care poate fi vizibil pe aceasta), punctele, coordonatele la care vor primi spawn jucătorii (care pot fi schimbate de către lideri tot cu ajutorul comenzii ‘/gsettings’), ID-ul interiorului de sediu deținut (fiind stocat separat într-un tabel „ganghq” în care se află informații precum prețul sediului sau coordonatele din interior ale pickup-ului de ieșire), coordonatele de intrare în sediu, ID-ul gang-ului aliat, ID-ul culorii vehiculului respectiv, numărul de bani din bancă și celelalte informații precum cele stocate în topurile lunar.
109
Constantin-Flavius Nistor Prezentare
3.7 Mod de joc: Cursele
Pentru a permite întrecerea organizată a jucătorilor cu vehiculele sau fără, există o altă caracteristică importantă: cursele („races”). Acestea au la început un checkpoint de cursă care are o săgeată care indică spre locul primului checkpoint de cursă după ce acestea pornesc. Alături de acest checkpoint de cursă este și un 3DTextLabel, care indică numele cursei și ID-ul acesteia. De asemenea, pe hartă există o iconiță de tip steag care indică faptul că la acea poziție poate începe o cursă. O cursă este pornită automat o dată la 17 minute, în cazul în care alta nu este pornită deja de către un jucător cu ajutorul comenzii ‘/start’, iar acestea se pot desfășura pe pământ, cu vehicul sau fără, pe apă, în apă sau în aer. Fiecare cursă poate avea o mărime diferită a checkpoint-urilor, dar și o limită de jucători diferită. Toate informațiile curselor se stochează în tabelul „races” din baza de date:
Figura 3.47 – „Exemple înregistrări de curse”
Pentru a se vedea informații despre toate cursele de pe server, se poate utiliza comanda ‘/raceslist’, care afișează un dialog de tip listă care conține toate ID-urile și numele de curse, care dacă apăsând butonul de selectare pe una dintre ele va afișa un alt dialog cu informații despre cursa selectată:
Figura 3.48 – „Dialog cu informații despre o cursă”
110
Constantin-Flavius Nistor Prezentare
Așa cum se poate observa, oricărei curse i se calculează distanța („length”) de la început până la sfârșit. Acest calcul se realizează după ce toate cursele și checkpoint-urile acestora s-au încărcat din baza de date. Checkpoint-urile de curse sunt stocate în tabelul „racescps”, el conținând pentru fiecare înregistrare ID-ul cursei din care face parte checkpoint-ul, ID-ul ordinii din cursă a checkpoint-ului, dar și coordonatele X, Y și Z ale acestuia. Pentru a se calcula distanța, se efectuează următoarele bucle, prima buclă luând toate cursele la rând, iar a doua buclă luând toate checkpoint-urile cursei la rând, distanța finală a unei curse fiind suma tuturor distanțelor dintre checkpoint-urile acesteia:
Tabelul 3.41 – „Calcularea distanței unei curse”
for( auto const &e_race: GZS::Race::gMap )
{
e_race.second->length = 0.0;
for( auto const &e_cp: e_race.second->checkpoints )
{
if( e_cp.first != e_race.second->checkpoints.begin( )->first )
{
auto lPrevCP = std::prev( e_race.second->checkpoints.find(
e_cp.first ) );
e_race.second->length +=
Utility::Coords::GetPointDistanceFromPoint( lPrevCP->second.x, lPrevCP->second.y,
lPrevCP->second.z, e_cp.second.x, e_cp.second.y, e_cp.second.z );
}
}
GZS::Race::CreateStartCheckpoint( e_race.first );
}
Pentru stocarea informațiilor unei curse s-a construit o clasă specială, care ajută la alocarea dinamică a unui pointer unic inteligent (anterior descris) către un obiect într-un std::map, cheia fiind ID-ul cursei, iar valoarea fiind pointerul respectiv:
Tabelul 3.42 – „Calcularea distanței unei curse”
class GZS::Race::gMainClass
{
public:
int id = 0;
std::string name;
int type = 0;
int vehicleModel = 0;
int world = 0;
111
Constantin-Flavius Nistor Prezentare
int interior = 0;
bool loadObjects = false;
float length = 0.0;
float checkpointSize = 0.0;
int maxRacers = 0;
… alte valori precum poziția de start sau ID-urile entităților struct lStruct_Checkpoints
{
float x = 0; float y = 0; float z = 0;
};
std::map< int, lStruct_Checkpoints > checkpoints;
Destructor
~gMainClass( );
};
std::map< int, std::unique_ptr< GZS::Race::gMainClass > > gMap;
Pentru a crește, iarăși, competitivitatea între jucători, există un top 10 pentru fiecare cursă, în care sunt afișați jucătorii care au terminat cel mai rapid cursa. Acești timpi de terminare a unei cursei sunt stocați în tabelul „racestimes” sub formă de milisecunde, pentru o precizie cât mai mare în stabilirea timpului final, care conține ID-ul cursei din care face parte timpul de terminare, ID-ul contului care l-a stabilit, numărul de milisecunde al timpului, dar și unix timestamp-ul momentului inserării înregistrării.
Figura 3.49 – „Exemple înregistrări de timpi de curse”
În momentul în care un jucător termină o cursă toți jucătorii primesc mesaj cu timpul finalizării acesteia, dar și dacă jucătorul respectiv a stabilit un nou timp în top 10 și cu cât timp este mai rapid decât vechiul timp de pe poziția respectivă. Topul poate fi accesat cu ajutorul ‘/racetop’, lista fiind afișată automat atunci când jucătorul termină cursa, aceasta afișând și diferența de timp dintre o poziție și primul loc din top:
112
Constantin-Flavius Nistor Prezentare
Figura 3.50 – „Topul timpilor de terminare ai unei curse”
3.8 Mod de joc: Joculețele
Serverul deține o mulțime de joculețe („minigames”), care mai de care mai diferite și mai unice în lumea SA-MP-ului. Anumite minigame-uri pot fi jucate doar de către o persoană sau de mai multe la un moment dat, împărțiți pe echipe sau nu, neexistând o perioadă de înscriere, iar alte minigame-uri pot accepta înscrieri de jucători într-un timp de aproximativ 30 de secunde. O listă cu cele mai importante minigame-uri, alături de stadiul lor (oprit, pornit sau în desfășurare, stadiile fiind reprezentate de culori) și de numărul de jucători din acestea se poate vedea pe o bandă de tip textdraw din josul ecranului, asupra căreia vom reveni cu imagini mai jos. O listă cu toate minigame-urile se poate vedea cu ajutorul comenzii ‘/minigames’, unele având, de asemenea, o comandă care permite jucătorilor care nu participă să fie spectatori dacă minigame-urile sunt pornite (acceptă înscrieri) sau sunt în desfășurare (nu se mai acceptă înscrieri, iar jucătorii se luptă pentru câștigarea minigame-ului):
Figura 3.51 – „Lista afișată de comanda ‘/minigames’”
113
Constantin-Flavius Nistor Prezentare
Pentru că există un număr mare de minigame-uri care pot fi prezentate foarte detaliat, vor fi enumerate doar cele mai semnificative sau mai creative.
Un minigame simplu, dar creativ, este cel de prindere al albinelor de pe ecran, care poate fi pornit cu ajutorul comenzii ‘/ctb’ („Catch The Bee” – „Prinde Albina”). Orice jucător poate să-l pornească doar pentru el în orice moment, pe ecran apărând o dată la câteva secunde câte o albină, la orice punct de pe ecran, care dă rapid din aripi pe loc. Acest lucru este realizat cu ajutorul unui timer, deoarece o albină este redată pe ecran cu ajutorul unor componente numite „sprites” 63 ale textdraw-urilor cu ajutorul cărora se pot afișa pe ecran diverse texturi statice din fișierele „.txd” ale jocului. Din cauza faptului că un astfel de „sprite” este static și a faptului că există imagini cu albinuțe care au aripile în două poziții, acestea se pot schimba rapid, intermitent, cu ajutorul unui timer. Jucătorul poate prinde un maxim de 5 albine, câștigul constând în bani și XP care este direct proporțional cu numărul de albine prinse.
Figura 3.52 – „Imaginea unei albine de pe ecran din minigame-ul ‘/ctb’”
Alt minigame, care de această dată permite înscrierea mai multor jucători care se pot lupta între ei, fără o limită de timp sau perioadă de înscrieri, este „Gun Game” („Joc de Arme”), care poate fi accesat cu ajutorul comenzii ‘/gungame’, la înscriere jucătorii primind un simplu pistol. În momentul în care un jucător omoară trei alți jucători cu o armă, acesta primește următoarea armă, jucătorul revenind la prima armă după ce trece și de ultima. De fiecare dată când jucătorul atinge ultima armă, acestuia i se incrementează o statistică pentru a reține acest lucru.
Un alt minigame care permite înscrierea jucătorilor în orice moment dat este cel de fotbal („Football”), fiind accesibil cu ajutorul comenzii ‘/football’, jucătorii având abilitatea de a alege una dintre cele două echipe. Desigur, pe terenul de fotbal există o minge care poate fi lovită cu ajutorul click-ului dreapta (detectând dacă jucătorul se află în jurul acesteia, preluând poziția actuală a obiectului cu ajutorul funcției GetDynamicObjectPos din Streamer Plugin, care este echivalentul funcției GetObjectPos din SA-MP pentru obiectele globale, verificarea
Mai multe informații: wiki.sa-mp.com/wiki/TextDraw_Sprites
114
Constantin-Flavius Nistor Prezentare
de buton fiind realizată în evenimentul OnPlayerKeyStateChange), după care jucătorului i se va aplica o animație (cu ajutorul funcției ApplyAnimation) de lovire cu piciorul. De asemenea, pe hartă se află o tabelă de scor care se resetează în momentul în care o echipă atinge 10 goluri, iar un gol al unui jucător nu se calculează dacă nu există nici un adversar. Pentru ca pe tabelă să fie afișat scorul, se folosește funcția SetDynamicObjectMaterial (fiind echivalentul funcției SetObjectMaterialText a obiectelor globale). Pentru a detecta dacă mingea a intrat într-o poartă după ce a fost mutată cu ajutorul funcției MoveDynamicObject (echivalentul obiectelor globale fiind MoveObject), se folosesc zonele din Streamer Plugin, având posibilitatea verificării acelui fapt cu ajutorul funcției IsPointInDynamicArea.
Figura 3.53 – „Terenul, tabela și mingea de fotbal”
Figura 3.54 – „Mai mulți jucători la fotbal”
115
Constantin-Flavius Nistor Prezentare
Alt minigame disponibil este cel de capturare a camionului inamic („Steal The Truck” – „Fură Camionul”). Acest minigame are o perioadă de înscrieri de aproximativ 30 de secunde, după care toți jucătorii înscriși sunt repartizați aleatoriu pe echipe. Scopul unei echipe este cel de a fura vehiculul inamic, pe care trebuie să-l ducă la bază, harta acestui minigame fiind destul de extinsă. Comanda pentru alăturarea la acest minigame este ‘/stt’.
Există, de asemenea, minigame-ul „Parachute Landing” („Aterizare cu Parașuta”), care este accesibil cu ajutorul comenzii ‘/paraland’. În acest minigame există un checkpoint de cursă în aer care este circular datorită faptului că este un checkpoint de final de cursă în aer. Jucătorii trebuie să aterizeze in acel checkpoint cât mai aproape de centru, ei fiind puși mult mai sus în aer din momentul în care se alătură acestui minigame, având poziții aleatorii, astfel încât să nu fie puși fix deasupra checkpoint-ului. Jucătorii trebuie să schimbe direcția în aer cu ajutorul parașutei deținute. La final, câștigă jucătorul care a picat cel mai în centru, fiind comparată distanța poziției 2D (doar coordonatele X și Y) la care jucător a intrat în checkpoint (detectat cu ajutorul evenimentului OnPlayerEnterDynamicRaceCP din Streamer Plugin – echivalent, OnPlayerEnterRaceCheckpoint al checkpoint-urilor normale de curse din SA-MP) până la poziția 2D a centrului cercului. Sunt comparate pozițiile 2D din cauza faptului că evenimentul respectiv poate fi apelat de la diverse altitudini, în funcție de viteza de transmitere a informațiilor între jucător și server, altitudinea afectând distanța finală.
În plus, există și minigame-uri cu hărți aleatorii, ele aflându-se la diverse coordonate pe harta jocului, sau pot avea hărți special făcute pentru ele. O hartă este aleasă aleatoriu în momentul în care minigame-ul este pornit, informațiile hărților fiind stocate în cod.
Un prim astfel de exemplu de minigame cu hartă aleatorie este „Water Car Sumo”, accesibil cu ajutorul comenzii ‘/wcs’. Acesta este un minigame în care fiecare jucător este pe cont propriu, ei trebuind să distrugă vehiculele inamice sau să le facă să cadă de pe hartă, jucătorii neavând posibilitatea de a părăsi vehiculul (acest lucru a fost realizat cu ajutorul funcției PutPlayerInVehicle în evenimentul OnPlayerExitVehicle, astfel jucătorul fiind pus instant în vehicul în momentul în care acesta încearcă să iasă din vehicul). Informațiile principale ale unei astfel de hărți sunt stocate într-o variabilă (gMaps), iar informațiile fiecărei poziții de teleportare pe hartă după așteptarea jucătorilor este stocată separat (gMapsSpawns). De asemenea, când jucătorii așteaptă ca minigame-ul să înceapă, există 4 poziții aleatoare la care sunt puși, fiind de asemenea stocate într-o altă variabilă (gMapsWaitInfo). Structura acestor variabile este următoarea, ele fiind, în principal, de tipul std::array 64:
Mai multe informații: en.cppreference.com/w/cpp/container/array
116
Constantin-Flavius Nistor Prezentare
Tabelul 3.43 – „Variabilele pentru stocarea hărților din ‘/wcs’”
std::array< GZS::Minigame::WCS::gStruct_Maps, MAX_WCS_MAPS > gMaps = { {
structura: nume hartă, poziția minimă de la care se cade de pe hartă, cea maximă, pe care nu o poate depăși și pozițiile pentru a fi spectator pe hartă
{ "Water Everywhere", 1.00f, 10.00f, -3205.4451f, 3407.0598f, 7.980500f, – 3205.4451f, 3407.0598f, 27.980500f, WORLD_WCS },
alte hărți
} };
std::array< std::array< GZS::Minigame::WCS::gStruct_MapsWaitInfo,
MAX_WCS_WAIT_POSITIONS >, MAX_WCS_MAPS > gMapsWaitInfo =
{ {
Water Everywhere (1) | structura: playerX,Y,Z,A | cameraX,Y,Z| cameraLookAtX,Y,Z
{ {
{ -3243.3000f, 3388.85000f, 6.31000f, 102.7300f, -3255.9300f, 3392.39000f, 10.0900f, -3251.0900f, 3393.66000f, 8.79000f },
{ -3246.6400f, 3401.20000f, 6.59000f, 102.7300f, -3255.9300f, 3392.39000f, 10.0900f, -3251.0900f, 3393.66000f, 8.79000f },
// alte poziții
} },
alte hărți
} };
std::array< std::array< std::array< float, 4 >, WCS_MAX_RANDOM_SPAWNS >,
MAX_WCS_MAPS > gMapsSpawns =
{ {
Water Everywhere (1)
{ {
{ -3078.4656f, 3431.1353f, 2.6635f, 13.1501f }, { -3082.4216f, 3451.7500f, 2.6824f, 189.0420f },
// alte poziții
} },
// alte hărți
} };
Un ultim minigame prezentat conține o hartă în aer, care se află mereu la poziții
prestabilite aleatorii, deasupra hărții jocului. Acest minigame se numește „Platforms”
(„Platforme”), fiind accesibil cu ajutorul comenzii ‘/platforms’, iar jucătorii participanți trebuie
să ajungă pe ultima platformă sau să rămână ultimii jucători care n-au căzut de pe platformele
mișcătoare. Prima platformă, cea pe care sunt puși când se alătură, se întoarce vertical după 10
secunde pentru a nu putea jucătorii să rămână pe ea, fiind singura platformă care nu se mișcă
constant, celelalte platforme mișcându-se de la dreapta la stânga, iar pereții hărții la fel, cu
diverse viteze. De asemenea, nici ultima platformă nu se mișcă, aceea fiind de culoare neagră,
117
Constantin-Flavius Nistor Prezentare
în timp ce toate celelalte platforme au diverse culori setate cu ajutorul funcției SetDynamicObjectMaterial (echivalentul pentru obiectele globale este funcția SetObjectMaterial).
Figura 3.55 – „Harta minigame-ului ‘/platforms’”
3.9 Altele
Desigur, acest server are multe alte caracteristici. Câteva dintre acestea, de o importanță semnificativă, sunt:
Locurile de muncă („Jobs”). Fiecare jucător se poate alătura unui loc de muncă oricând, cu ajutorul comenzii ‘/jobs’, comandă care afișează un dialog de tip listă care conține toate
locurile de muncă existente. Când un anumit loc de muncă este selectat, jucătorul poate vedea instrucțiuni despre ceea ce are de făcut, dar și despre cum poate porni locul de muncă, unele dintre locurile de muncă având nevoie ca jucătorul să fie într-un vehicul de anumit model sau să nu aibă un vehicul.
Figura 3.56 – „Harta minigame-ului ‘/platforms’”
118
Constantin-Flavius Nistor Prezentare
Așa cum se specifică în imagine, toate locurile de muncă oferă bani când se completează o sarcină, iar o parte dintre acestea oferă și XP. Locurile de muncă pot fi cooperative („cooperative”), ceea ce înseamnă că fie au nevoie de alți jucători pentru a putea duce la îndeplinire anumite sarcini, fie pot avea alți jucători drept coechipieri.
Ca prim exemplu, putem avea locul de muncă „Garbage Job” (gunoier), care permite ca un jucător să înceapă serviciul singur sau alături de cineva, dacă este pasager în vehicul, acel jucător primind o invitație într-un dialog pe care o poate accepta sau respinge. Când se începe serviciul, în funcție de orașul în care se află jucătorul, acestuia îi va apărea un checkpoint la o locație aleatorie pe harta orașului la care trebuie să se ducă, iar după ce intră în el îi vor apărea 20 de pickup-uri ce reprezintă pungi de gunoi în fața caselor, fiecare astfel de pickup având un steguleț roșu pe hartă, pentru a putea fi găsite ușor, dar și un 3DTextLabel care va conține numele jucătorului sau jucătorilor care vor trebui să-l ridice. După ce se ridică un astfel de pickup, el trebuie dus în spatele mașinii de gunoi, iar după ce sunt colectate toate pickup-urile, mașina de gunoi trebuie condusă către groapa de gunoi, care este undeva la mijlocul celor trei orașe mari din joc.
Figura 3.57 – „Serviciul de gunoier”
Ca al doilea și ultimul exemplu, avem locul de muncă „Deer Hunter” („Vânător de Căprioare”) care este cel mai dezvoltat dintre toate. Un jucător poate vâna căprioare singur sau alături de alți jucători. Jucătorul poate porni acest loc de muncă în modurile „singleplayer” sau „multiplayer”. În modul singleplayer va fi singur pe hartă în cele câteva zone verzi marcate pe hartă cu gangzone-uri în care există căprioare, limita totală de căprioare fiind de 100, iar în modul multiplayer va putea genera o cameră privată sau publică, cea privată nefiind direct
119
Constantin-Flavius Nistor Prezentare
vizibilă pentru ceilalți jucători, iar jucătorul care deține camera poate invita alți jucători doar cu ajutorul comenzii ‘/deer invite’, aceasta fiind utilizată și pentru a invita alți jucători într-o cameră publică, limita de căprioare fiind de 200, existând comanda ‘/deer players’ pentru a putea vedea câți bani a obținut fiecare jucător și câte căprioare a vânat. Dacă există camere publice, jucătorii se pot alătura oricăreia dintre acestea, iar în momentul în care deținătorul unei camere iese de la locul de muncă utilizând comanda ‘/exit’ (care poate fi folosită și pentru a ieși dintr-o casă, minigame, sediu de gang sau din multe alte astfel de zone speciale), toți jucătorii alăturați acesteia sunt scoși și ei de la locul de muncă, camera fiind ștearsă automat. În cazul ambelor tipuri de moduri, pe ecran apare numărul total de căprioare care au fost vânate, de către toți jucătorii participanți. Când o căprioară este omorâtă, jucătorul va primi o sumă de bani care este mai mare dacă a omorât căprioara de la o distanță mai mare, dar și 0 sau 1 XP, aleatoriu. Jucătorii pot omorî căprioare cu ajutorul unei puști cu lunetă, astfel având o posibilitate mai bună de a primi mai mulți bani pentru o căprioară omorâtă. Când o căprioară este omorâtă, camera jucătorului va fi pusă aproape de un obiect care se mută rapid către căprioară, reprezentând glonțul tras. De asemenea, pentru căprioare s-au implementat anumite elemente de inteligență artificială 65. Primul element de inteligență artificială este faptul că un număr aleator de căprioare se deplasează în diverse direcții o dată la câteva secunde, cu ajutorul funcției MoveDynamicObject. Al doilea element este faptul că o căprioară poate simula faptul că se ferește de gloanțe și de jucători cu ajutorul evenimentelor OnPlayerWeaponShot, OnPlayerShotDynamicObject și OnPlayerEnterDynamicArea (fiecare căprioară având o zonă din Streamer Plugin lipită de ea), astfel încât dacă un jucător ratează la o distanță mică o căprioară, dacă trage în căprioară și aceasta mai are viață rămasă sau dacă se apropie de o căprioară, aceasta fuge (viteză mai mare în funcția MoveDynamicObject) într-un loc aleatoriu apropiat, dar care este mai departe de locul la care a lovit glonțul sau de locația jucătorului.
Figura 3.58 – „Lista care conține camerele publice de vânat căprioare”
Mai multe informații: wikipedia.org/wiki/Artificial_intelligence
120
Constantin-Flavius Nistor Prezentare
Figura 3.59 – „Camera jucătorului când omoară o căprioară”
Interfața permanentă de textdraw-uri a serverului, aceasta fiind afișată oricărui jucător, dacă acesta nu le-a dezactivat din ‘/pcp’: ‘Textdraw Settings’:
Figura 3.60 – „Elementele interfeței formate cu ajutorul textraw-urilor”
121
Constantin-Flavius Nistor Prezentare
Aceste elemente, notate pe imaginea anterioară, sunt:
Logo-ul și versiunea serverului. O nouă versiune este lansată atunci când se produc suficiente modificări, care sunt anunțate în comanda ‘/news’ care afișează o listă cu toate versiunile lansate, alături de data la care au fost lansate.
Linia de textdraw-uri de jos, fiind o bandă cu diverse informații. Cel mai în stânga jucătorul poate să-și vadă nivelul, iar puțin mai la dreapta poate să vadă XP-ul actual, alături
de XP-ul la care trebuie să ajungă pentru a atinge următorul nivel. Mai la dreapta, pe partea de sus, există o listă cu toate joculețele, alături de numărul de jucători care se află în acestea. Dacă un joculeț este pornit apare cu roșu, dacă este în curs de acceptare de înscrieri este notat cu verde, iar dacă nu este pornit are culoarea galbenă. Pe partea de jos, sub lista de joculețe, se află mesaje aleatoare care oferă diverse informații jucătorilor, dar și reclame pentru accesarea magazinului (asupra căruia vom reveni).
3. Lista de mesaje globale. Aceasta este o listă cu informații de pe tot serverul care sunt legate de jucători și de acțiunile mai importante ale acestora. Această listă este în principal utilizată pentru a semnala faptul că un jucător s-a conectat sau s-a deconectat, dar este folosită și pentru a arăta că un jucător a pus jocul pe pauză sau a revenit, dacă a pornit o serie de omorâre a inamicilor („killstreak”) sau alte informații. Cel mai vechi mesaj este întotdeauna pe ultima linie, astfel încât în momentul în care apare un nou mesaj acesta este trecut pe prima linie, iar celelalte sunt trecute mai jos cu o linie decât erau.
Lista de comenzi de teleportare executate de către jucători care este asemănătoare listei
de mesaje globale. O linie nouă va apărea de fiecare dată când un jucător se teleportează undeva cu ajutorul unei comenzi. Apariția unei astfel de linii poate fi dezactivată de fiecare jucător, în caz că nu vrea să fie urmărit de alți jucători, această abilitate fiind posibilă cu ajutorul comenzii ‘/pcp’ de la itemul ‘Show My Teleports’.
Testul activ. Acesta apare o dată la câteva minute automat pentru a oferi premii în bani
și XP, cel mai rapid jucător câștigând testul. Există mai multe tipuri de astfel de teste, fiecare având o culoare diferită de fundal a căsuței, teste asupra cărora vom reveni puțin mai jos.
De asemenea, pe elementul 2, care este bara de jos, în dreapta de tot apar întotdeauna când jucătorul este într-un vehicul anumite informații importante despre starea acelui vehicul, acesta fiind numit „Speedometer” („vitezometru”, deși nu conține doar viteza actuală a vehiculului): numele vehiculului, viața acelui vehicul, în procente (100% când nu este atins de nimic, iar 0% când începe să ia foc, acesta fiind distrus complet – viața fiind preluată cu ajutorul funcției GetVehicleHealth, unde maximul implicit este 1000.0, iar punctul la care începe să ia foc este de 250.0, atunci când este distrusă complet și nu se mai poate intra în ea, având valoarea
122
Constantin-Flavius Nistor Prezentare
0.0), și, bineînțeles, viteza actuală a vehiculului, în km/h (kilometri pe oră) sau mp/h (mile pe oră), aceste unități de afișare fiind posibil să se schimbe cu ajutorul comenzii ‘/speedtype’.
Revenind, există câteva tipuri de teste care cresc competitivitatea între jucătorii de pe server: „Reaction” (este generat un șir aleatoriu de 5-15 caractere, iar primul jucător care scrie șirul identic câștigă premiul), „Math” (generează un calcul cu două operații, prima fiind de adunare, scădere sau înmulțire, iar a doua este doar de adunare sau de scădere, iar primul jucător care scrie răspunsul corect câștigă premiul), „Teleport” (este selectată o comandă aleatorie de teleportare de pe server, iar primul jucător care o execută câștigă premiul), „Question” (este oferită o întrebare aleatorie cu patru variante de răspuns, toate acestea fiind stocate în baza de date și în memoria serverului, iar primul jucător care scrie litera corespunzătoare variantei corecte câștigă, existând doar o posibilitate de introducere a unui răspuns), „GoldPOT” (în baza de date sunt stocate mai multe astfel de locații, iar atunci când acest test începe este selectată aleatoriu una dintre locații, care sunt stocate și în memoria serverului, la locația câștigătoare fiind disponibil un checkpoint care face ca primul și singurul jucător intrat în el să câștige), „Roll” (câștigă primul jucător care nimerește numerele 11, 22, 33, 44, 55, 66, 77, 88 sau 99 cu ajutorul comenzii ‘/roll’, care extrage automat un număr aleatoriu de la 1 la 100, un jucător având posibilitatea de a folosi în mod implicit această comandă de două ori, iar dacă deține „V.I.P. Premium” are o șansă adițională) și „Car Spawn” (pe acest server se poate folosi comanda ‘/v’ pentru a creea pe loc vehiculul dorit, care este introdus ca prim parametru, fie ca o parte din numele acestuia sau complet, fie ca ID, iar primul jucător care folosește această comandă pentru vehiculul aleator specificat de test câștigă premiul). Jucătorii pot vedea testul curent, de asemenea, cu ajutorul comenzii ‘/test’, în care se afișează toate informațiile ale acestuia, dar și numele următorului test, alături de timpul rămas până va începe. De asemenea, când unul din testele „Question” sau „Roll” se termină sau este câștigat, va apărea în textdraw numărul total de încercări, dar și în dialog-ul afișat de comanda ‘/test’. Un administrator poate porni teste în orice moment cu ajutorul comenzii ‘/starttest’, acestea fiind generate aleatoriu, ca în mod normal.
Bineînțeles, există multe alte caracteristici, precum loteria (accesibilă cu ajutorul comenzii ‘/lotto’, care are câteva categorii care conțin un număr maxim diferit de bilete, iar extragerile sunt făcute la ore fixe, timpul fiind verificat constant într-un timer, pentru fiecare categorie de la care s-au cumpărat toate biletele, extrăgându-se aleator un câștigător, toți câștigătorii fiind stocați în baza de date și fiind listați în dialog-ul comenzii ‘/lottowinners’), duelul (comanda ’/duel’; doi jucători se pot lupta între ei cu o armă aleasă de cel care trimite invitația, pe o arenă aleasă tot de acela, iar câștigătorul obține toți banii, fiind puși la bătaie la
123
Constantin-Flavius Nistor Prezentare
începutul bătăliei), animații (acestea fiind listate cu ajutorul comenzii ‘/a’ care permite ca parametru opțional numele unei animații din listă, jucătorul având posibilitatea de a aplica o animație pe caracter, precum statul pe jos, făcutul cu mâna sau altele), modificarea („tuning”) vehiculului (jucătorul își poate adăuga ușor componente pe vehicul, dar și schimba culorile și desenele de pe acesta, iar pentru că anumite vehicule pot fi modificate mai mult decât altele, există și posibilitatea de a salva câteva modificări favorite, pentru a fi încărcate ușor în viitor), amplificatoarele de viteză și de altitudine (vehiculul jucătorului având posibilitatea de a zbura și de a merge în aer), pachetele aleatoare de pe hartă (acestea oferind diverse premii în bani, XP, arme sau altele jucătorului care găsește unul dintre ele; pachetele apar o dată la 30 de minute, cele vechi fiind șterse, existând 33 de astfel de pachete în fiecare oraș, deci având un total de 99 de locații de pachete; fiecare pachet are o șansă de 50% de a apărea în lume și fiind plasat în locuri rar accesate ale jocului) sau multe altele.
Figura 3.61 – „Afișarea categoriilor loteriei cu ajutorul comenzii ‘/lotto’”
Figura 3.62 – „Afișarea ultimilor câștigători la loterie cu ajutorul comenzii ‘/lottowinners’”
124
Constantin-Flavius Nistor Prezentare
Figura 3.63 – „Dialog-ul comenzii‘/tune’, pentru modificarea vehiculului”
Figura 3.64 – „Afișarea listei cu modificările favorite salvate”
Figura 3.65 – „Un pachet de pe hartă”
125
Constantin-Flavius Nistor Dezvoltare și monetizare
Dezvoltare și monetizare
Pentru a se asigura un viitor serverului, acesta are nevoie de două aspecte foarte importante: planurile și sursa de venit.
Așadar, pentru rezolvarea primului aspect, putem avea ca planuri pentru viitor următoarele caracteristici care vor putea avea un impact mare asupra bazei de jucători:
Topuri săptămânale/lunare/anuale. Din cauza faptului că actualul top accesează informațiile serverului de la început, indiferent de perioada de timp a realizării statisticilor
respective, jucătorii noi pot reuși cu greu să apără în vreun top, ceea ce înseamnă că se scade competitivitatea dintre jucători din ce în ce mai mult. Pentru crearea unui astfel de top va trebui ca baza de date să stocheze separat fiecare progres realizat, alături de data și de timpul realizării, ceea ce înseamnă că mărimea bazei de date va crește considerabil din cauza faptului că în momentul actual un progres al unui anumit tip de statistică al unui jucător este stocat doar o dată, fiecare asemenea înregistrare având o coloană care determină de câte ori s-a realizat. De exemplu, un jucător care duce la finalizare statistica cu codul unic „blowgate” (de câte ori a distrus porțile gang-urilor inamice) de 50 de ori va avea o singură înregistrare în tabelul „a_wins” pentru acest tip de statistică, în care va fi o coloană care să indice că s-a realizat de 50 de ori, iar pentru realizarea topului pe diverse perioade de timp acesta va avea 50 de înregistrări doar pentru acest tip de statistică, fiecare având și o coloană care va specifica cu ajutorul unui unix timestamp momentul realizării progresului. Datorită acestei extinderi se vor putea manipula informațiile în mai multe moduri, de exemplu permițând jucătorului pe server să vadă exact timpul realizării fiecărei statistici de un anumit tip.
Posibilitatea ca o casă să aibă o familie. Familia va trebui să fie setată la fiecare casă de către deținătorul acesteia. Când un jucător este adăugat în familia serverului, tot deținătorul casei va avea posibilitatea de a oferi diverse permisiuni precum editarea
obiectelor din casă, conducerea vehiculelor acesteia, posibilitatea încuierii ușii casei, posibilitatea de a primi spawn direct în acea casă și alte tipuri de permisiuni. Această caracteristică va oferi jucătorilor posibilitatea de a colabora, stabilind o atmosferă familială pe server, creând stabilitate între jucători și legând prieteniile dintre aceștia mult mai bine.
Posibilitatea plasării actorilor într-o casă. Datorită faptului că actorii stau pe loc, deținătorul unei case va putea adăuga astfel de caractere în diverse puncte din casă, pentru a nu mai părea lipsită de viață atunci când nici un alt jucător nu se află în aceasta.
Adăugarea mai multor joculețe. Datorită unicității și varietății acestor tipuri de moduri de joc, jucătorii doresc adăugarea mai multor astfel de caracteristici, reușind să obțină XP și
bani prin intermediul acestora. Numele câtorva dintre acestea sunt: „Kickstart” (imitând
126
Constantin-Flavius Nistor Dezvoltare și monetizare
misiunea cu același nume din jocul original, în care jucătorul trebuie să folosească o motocicletă de teren pentru a culege anumite pickup-uri plasate în diverse moduri pe o hartă, fiecare având un nivel de dificultate diferit care este premiat cu mai multe puncte atunci când este mai greu de obținut; pe server acest joculeț va permite ca acel mod să fie jucat, bineînțeles, de către mai mulți jucători deodată, aceștia trebuind să fie rapizi pentru că un pickup va putea fi obținut doar de către un singur jucător; câștigătorul după limita de timp va fi jucătorul care are cele mai multe puncte din culegerea pickup-urilor, iar dacă doi jucători au același număr de puncte va câștiga cel care a obținut punctele respective de mai mult timp, la precizie de milisecunde), „Land Grab” (imitând modul de joc cu nume similar din jocul GTA V, în care jucătorilor li se va oferi un set de arme, aceștia trebuind să captureze platforme prin simpla deplasare pe acestea, iar pentru a împiedica ceilalți jucători să captureze mai multe asemenea platforme ei vor trebui să încerce să se omoare între ei, după ce mor aceștia primind iarăși spawn în acest joculeț pentru a continua lupta, la final jucătorul cu cele mai multe platforme capturate în cel mai rapid timp ieșind câștigător), „Guess Who” („Ghici Cine”; acest joculeț va avea hărți aleatorii la fiecare pornire, pe care vor fi plasați zeci sau sute de actori cu ID-uri de caracter aleatorii și cu diverse animații aplicate, aceștia încercând să se potrivească pe hartă, având câteva sute de poziții și animații prestabilite; la începutul joculețului un jucător va fi selectat aleatoriu pentru a fi cel care va trebui să găsească alți jucători, aceștia fiind obligați să se ascundă, iar dacă au fost prinși într-o poziție proastă să facă să pară că de fapt sunt actori, astfel păcălind jucătorul care caută; după ce un jucător este prins, acesta va trebui să caute și el alți jucători, care vor avea de asemenea posibilitatea să schimbe în orice moment ID-ul de caracter pe care-l au prin apropierea de un actor și apăsarea butonului Y; dacă vânătorul trage cu arma într-un actor acestuia i se va scădea viața; joculețul va avea un timp limită la finalul căruia echipa câștigătoare va primi un premiu care constă, bineînțeles, în XP și bani) și multe altele, posibilitățile de creare unor asemenea joculețe fiind aproape infinite, acestea fiind limitate doar de creativitatea dezvoltatorului, dar și a jucătorilor, care pot transmite sugestii.
Adăugarea unor noi locuri de muncă. Asemenea joculețelor, se poate adăuga pe server un număr mare de locuri de muncă. Ceea ce ajută creativitatea pentru adăugarea unor noi locuri de muncă este faptul că serverul are posibilitatea de a le imita pe cele existente în
realitate. Locurile de muncă noi ar putea fi „Pizza Boy” („Băiatul cu pizza”), „Bus Driver” („șofer de autobuz”), „Vehicle Mechanic” („mecanic de vehicule”), „Quarry Worker” („lucrător în mină”) și multe altele, încercând ca acestea să fie reproduse cât mai fidel realității.
127
Constantin-Flavius Nistor Dezvoltare și monetizare
Datorită faptului că serverul utilizează limba engleză pentru a permite jucătorilor din diverse părți ale lumii să se conecteze și să înțeleagă textele, aceasta fiind cea mai utilizată limbă de circulație internațională, serverul are un public țintă mult mai larg. Însă, există în continuare miliarde de oameni de oameni care nu cunosc limba engleză, ceea ce restrânge
acest public țintă la un număr mult mai mic. Pentru a lărgi acest număr, serverul va putea oferi localizarea (traducerea) 66 textelor, ceea ce înseamnă că fiecare jucător își va putea alege limba pe care o preferă. Textele vor fi localizate cu ajutorul comunității, jucătorii având posibilitatea de a crea traduceri pentru bucăți de text care n-au fost deja traduse (sau de a corecta anumite texte deja existente) cu ajutorul unui site specializat pentru acest server, alți jucători având, de asemenea, posibilitatea de a vota traducerile care sunt corecte, celor care nu sunt corecte acordându-le un vot negativ. Cu ajutorul acestei caracteristici se vor putea implementa, de asemenea, statistici și realizări noi pentru jucători, pentru a încuraja participarea acestora la traducerea textelor. Pentru realizarea acestui lucru, procesul este unul foarte complex, fiind nevoie ca fiecare text valabil pe server să fie stocat în baza de date, jucătorii transmițând variantele traduse care vor fi iarăși stocate în baza de date. La acest procedeu ajută foarte mult și librăria „fmt” de formatare a textelor, el permițând placeholderi care accesează argumentele după poziție, unele limbi având ordinea textului diferită.
Alte idei pot conține, de asemenea, extinderea tuturor caracteristicilor serverului în diverse moduri, precum extinderea timpului de înscriere la joculețe bazat pe numărul actual de jucători de pe server. Altă idee, care este absolut necesară, este permiterea unui control mai bun al administratorilor asupra acțiunilor jucătorilor în timp ce accesează diverse caracteristici ale serverului, administratorii având astfel abilități în plus precum scoaterea numelui unei case în caz că acesta nu corespunde regulilor serverului.
De asemenea, există numeroase alte idei de extindere a serverului, anterior fiind listate doar cele care probabil vor avea cea mai mare popularitate în rândul jucătorilor și care oferă o scurtă idee despre posibilitatea aproape infinită de extindere a unui asemenea server.
De asemenea, pentru al doilea aspect, aspectul monetizării, din cauza faptului că este nevoie de disponibilitate non-stop a serverului, acesta are nevoie de o gazdă („host”). Gazda poate fi oferită de către firme specializate sau poate fi un VPS (“Virtual Private Server”) 67 sau
Mai multe informații: wikipedia.org/wiki/Language_localisation
Mai multe informații: wikipedia.org/wiki/Virtual_private_server
128
Constantin-Flavius Nistor Dezvoltare și monetizare
un server dedicat 68 oarecare. Însă, pentru ca o mașină să fie gazdă, este nevoie de bani, iar din această cauză serverul trebuie să poată să-și obțină singur veniturile necesare.
Pentru a rezolva această problemă, serverul are la dispoziție comanda ‘/shop’ („magazin”) care o dată executată afișează un dialog care oferă jucătorilor posibilitatea de a cumpăra în joc diverse elemente („market” = „piață”) care sunt accesibile doar după achiziționarea acestora cu monedele virtuale numite „credite” („credits”). De asemenea, cu ajutorul acestui dialog se pot vedea avantajele oferite de către nivelul special de V.I.P. (nivel 4, numit „Premium V.I.P.”; pe lângă acesta fiind accesibile alte trei nivele care se pot achiziționa tot prin intermediul acestei comenzi cu ajutorul unor sume de bani din joc, dar care cer și un anumit nivel al contului, acestea oferind mai puține avantaje). Desigur, cu ajutorul acestor dialog-uri se oferă diverse informații despre donații, pentru a facilita donațiile jucătorilor.
Figura 4.1 – „Dialog-ul principal al magazinului”
Când se accesează piața din dialog-ul principal, se vor enumera toate produsele valabile, în titlul dialog-ului fiind afișate numărul de credite ale contului jucătorului care a executat comanda. Anumite produse sunt valabile în diverse cantități și perioade de expirare, prețul fiind diferit pentru fiecare dintre acestea. Cele trei nivele de V.I.P. care pot fi cumpărate cu sume de bani din joc sunt valabile tot în această piață ca o strategie de marketing, jucătorii fiind nevoiți să acceseze această listă, așadar aceștia având o posibilitate destul de mare să vadă și alte oferte valabile doar în schimbul unor credite, aceștia fiind tentați să doneze pentru achiziționarea unui anumit produs din piață:
Mai multe informații: wikipedia.org/wiki/Dedicated_hosting_service
129
Constantin-Flavius Nistor Dezvoltare și monetizare
Figura 4.2 – „Piața”
În această listă există nivele de administrator începător (primele 4 nivele din cele 10 existente, care nu au prea multe permisiuni și comenzi valabile, pentru a preveni accesul unor persoane care doresc să facă rău) care au un cost destul de ridicat și o perioadă de valabilitate destul de mică, în caz că jucătorii doresc să facă parte din echipa administrativă dacă nu au fost aleși înainte, astfel având șansa de a demonstra că sunt capabili să facă parte din echipă.
Produsul cel mai recomandat pentru jucători este „V.I.P. Premium”, care a fost deseori menționat anterior, acesta oferind o multitudine de avantaje într-un singur pachet. Toate avantajele și comenzile pot fi văzute din dialog-ul ‘/shop’ și din dialog-ul ‘/vipcmds’. Avantajele includ un loc adițional pentru case și afaceri, teleportarea directă prin click dreapta pe harta jocului, schimbarea numelui mai rapidă și alte avantaje.
Figura 4.3 – „Dialog avantaje V.I.P. Premium”
130
Constantin-Flavius Nistor Dezvoltare și monetizare
Figura 4.4 – „Dialog listare comenzi V.I.P.”
Separat, se pot achiziționa în schimbul unui număr de credite slot-uri adiționale de vehicule pentru case, în caz că există jucători care vor să investească în acestea. De asemenea, se pot achiziționa bani în joc, XP și resetarea unor limite de timp pentru anumite caracteristici ale serverului, precum limita de timp de la schimbarea numelui contului.
Creditele sunt obținute în schimbul donațiilor, jucătorii având posibilitatea de a ajuta serverul prin diverse sume de bani din viața reală, în schimbul acestora obținând un cod generat aleatoriu de forma „xxxx-xxxx-xxxx-xxxx” care conține numărul de credite obținut prin calcularea sumei oferite în euro multiplicată cu un număr variabil, în funcție de metoda de donație, după ce transferul banilor este taxat de serviciile care oferă metoda de transfer a banilor. Un cod poate fi folosit doar o dată, iar acesta poate fi folosit imediat sau se poate oferi altui jucător sub formă de cadou, extrăgându-se tot dintr-un dialog valabil în unul dintre dialog-urile accesibile cu ajutorul comenzii ‘/shop’ („Redeem Code”):
Figura 4.5 – „Dialog-ul de extragere cod”
131
Constantin-Flavius Nistor Dezvoltare și monetizare
Pentru stocare, există un tabel numit „redeemcodes” având în structură următoarele cinci coloane: „Code” (de tip VARCHAR(19) și fiind cheie primară; acesta este codul însuși, stocat sub forma specificată mai sus, care conține întotdeauna 19 caractere), „Credits” (de tip INT, acesta fiind numărul de credite care vor fi acordate jucătorului care extrage codul), „Date” (de tip INT; unix timestamp-ul momentului la care a fost generat codul), „UsedKey” (de tip INT; ID-ul contului care a extras codul, dacă este cazul, altfel fiind 0) și „UsedDate” (de tip INT; unix timestamp-ul momentului în care a fost extras codul, dacă este cazul, altfel fiind 0).
Pentru a genera un asemenea cod, avem următoarea funcție care acceptă un parametru de tip „int” pentru specificarea numărul de credite pe care să-l conțină și care returnează codul generat, de tip std::string, funcție care datorită faptului că se execută rar și nu există șanse prea mari ca un cod generat aleatoriu să existe deja în baza de date poate utiliza funcția Plugins::cMySQL::query pentru a verifica dacă deja există codul în baza de date și pentru a primi în aceeași funcție rezultatul:
Tabelul 4.1 – „Funcția de generare aleatorie a unui cod”
std::string GenerateRedeemCode( int credits )
{
int lRows = 0, lCacheID = 0;
do
{
gString = fmt::format( "{:04d}-{:04d}-{:04d}-{:04d}", Utility::Random::Random( 10000 ), Utility::Random::Random( 10000 ), Utility::Random::Random( 10000 ), Utility::Random::Random( 10000 ) );
lCacheID = Plugins::cMySQL::query( 1, fmt::format( "SELECT `Code` FROM `redeemcodes` WHERE `Code` = '{}' LIMIT 1", gString ) );
Plugins::cMySQL::Cache::get_row_count( lRows ); Plugins::cMySQL::Cache::delete__( lCacheID );
}
while( lRows );
Plugins::cMySQL::pquery( 1, fmt::format( "INSERT INTO `redeemcodes` VALUES( '{}', {}, UNIX_TIMESTAMP( ), 0, 0 )", gString, credits ) );
return gString;
}
Se poate observa faptul că în blocul de cod din interiorul buclei „do while” (buclă care se execută cel puțin odată, iar apoi de fiecare dată când nu verifică valoarea de adevăr a expresiei acesteia din while, încetând să se mai execute doar atunci când expresia este calculată ca fiind adevărată) se generează numărul în formatul anterior specificat, după care se trimite o funcție în firul de execuție principal al serverului, acesta preluând numărul de rânduri (valoare
132
Constantin-Flavius Nistor Dezvoltare și monetizare
care poate fi doar 0 sau 1) din selecția executată în variabila „lRows” și apoi trecând în condiția while, care face ca blocul anterior de cod să fie repetat dacă variabila „lRows” este diferită de 0, astfel știind că ultimul cod generat există deja. După ce se trece de bucla respectivă, se crează interogarea de inserarea a codului în baza de date, acceptând ca valori codul respectiv care a fost generat în buclă, numărul de credite care a fost pasat ca parametru al funcției, execuția funcției UNIX_TIMESTAMP pentru a stabili momentul creării codului, dar și două valori 0 pentru ID-ul contului care a extras codul și pentru data extragerii codului, din cauza faptului că acest cod tocmai a fost generat și deci nu a fost extras de nimeni.
La momentul actual se descriu, succint, în dialog-uri multiple, câteva metode pentru a dona, clarificând pașii pentru jucătorii care nu știu cum s-o facă:
“Paypal”, cu 15 credite per euro (fiind cea mai avantajoasă metodă pentru server,
acesta fiind un serviciu larg adoptat, acești bani fiind stocați ușor într-un cont din care pot fi transferați ușor pentru diverse plăți):
Figura 4.6 – „Dialog-ul pentru descrierea metodei ‘Paypal”
“Mobile”, cu 13 credite per euro, oferind și o metodă directă prin SMS care poate fi oferită direct de către gazda serverului:
Figura 4.7 – „Dialog-ul pentru descrierea metodei ‘Mobile”
133
Constantin-Flavius Nistor Dezvoltare și monetizare
– “Paysafecard”, cu 10 credite per euro:
Figura 4.8 – „Dialog-ul pentru descrierea metodei ‘Paysafecard”
Adițional, pentru mai multe întrebări frecvente despre donații ale jucătorilor, există dialog-ul „F.A.Q.” („Frequently Asked Questions” – „Întrebări frecvente”), care adresează întrebări precum modul de a primi și extrage un cod, cum se finalizează o donație și altele.
Figura 4.9 – „Dialog-ul pentru întrebări frecvente”
Pentru finalizarea unei donații, jucătorul trebuie să contacteze deținătorul serverului pentru ca acesta să verifice tot ceea ce este necesar, după care va calcula creditele de oferit și va genera un asemenea cod de extras. Desigur, pentru a putea fi contactat există și un dialog cu diverse rețele de socializare populare:
Figura 4.10 – „Dialog contact”
134
Constantin-Flavius Nistor Dezvoltare și monetizare
Deoarece pot apărea diverse neînțelegeri și probleme de ordin legal, au fost stabiliți diverși termeni și condiții care sunt afișați tot într-un dialog din comanda ‘/shop’, care reamintesc numărul de credite oferite pentru fiecare metodă de donație, taxele existente, regulile cumpărării din „piață” și multe altele:
Figura 4.11 – „Dialog termeni și condiții”
Magazinul este o caracteristică foarte importantă a serverului care trebuie dezvoltată în continuu pentru a eficientiza monetizarea acestuia, dezvoltarea fiind realizată prin extinderea caracteristicilor actuale și viitoare ale serverului, astfel încât acestea să permită elemente adiționale (precum extinderea slot-urilor) contra cost sau care să fie accesibile doar celor care dețin deja un cont „V.I.P. Premium”. Pentru a avea o monetizare bună, produsele din „piață” nu trebuie să aibă un preț mare în comparație cu ceea ce oferă.
135
Constantin-Flavius Nistor Concluzii
5 Concluzii
Deținerea unui server, oricare ar fi jocul pentru care este construit, se merită pe deplin datorită unei varietăți mari de motive, toate fiind la fel de importante.
În primul rând, avem ca motiv bucuria oferită celorlalți, ei având posibilitatea de a împărtăși experiențe redate grafic cu persoane din toate colțurile lumii, astfel dând iluzia de apropiere, în timp ce se bucură de anumite caracteristici nemaivăzute. Prin alăturarea la un astfel de server se pot crea, de asemenea, prietenii foarte strânse și de lungă durată.
În al doilea rând, avem creativitatea îmbunătățită în timp a dezvoltatorului serverului, datorită faptului că este necesar ca acesta să se gândească în permanență la noi caracteristici de implementat într-un mod cât mai eficient, extins și unic, dar și datorită necesității de a rezolva diverse probleme, fie ele ale funcționalității serverului sau simple probleme dintre jucători.
În al treilea rând, tot datorită nevoii de a rezolva diverse probleme dintre jucători, putem considera contribuția serverului la dezvoltarea obiectivității deținătorului acestuia, dar și a echipei administrative. Aceștia nu au voie să fie subiectivi, ci trebuie să analizeze corect toate evenimentele care au dus la problema respectivă și s-o rezolve, astfel fiind echidistanți.
În al patrulea rând, dezvoltatorul este nevoit să învețe strategii de marketing pentru a putea promova serverul prin prezentarea caracteristicilor acestuia într-un mod cât mai plăcut, astfel atrăgând noi jucători și motivându-i pe cei existenți să rămână în comunitate, astfel dezvoltatorul îmbunătățindu-și abilitățile de promovare a propriei munci.
În al cincilea rând, avem ca motiv venitul care poate fi obținut dintr-un asemenea server, dezvoltatorul primind din partea jucătorilor, astfel, confirmarea faptului că deciziile acestuia sunt bune și îi permite să asigure stabilitatea și dezvoltarea în continuare a serverului, iar în cazul unui surplus de venit dezvoltatorul poate fi premiat cu acesta.
În ultimul rând, cel mai important pentru dezvoltator și viitorul acestuia, este pregătirea profesională în subdomenii variate din întreg domeniul IT, datorită posibilității utilizării a celor mai noi sau mai folosite tehnologii și standarde datorită necesității de implementare a noi caracteristici variate. Implementarea ultimelor tehnologii oferă unui server posibilitatea ca acesta să fie de actualitate, neținând cont de jocul pentru care este realizat, indiferent de anul apariției acestuia.
În concluzie, toate aceste motive pot contribui în armonie pentru a crea un server de succes și pentru a dezvolta persoanele implicate, atât ca dezvoltatori, dar și ca simpli jucători.
136
Constantin-Flavius Nistor Bibliografie
Bibliografie
wiki.sa-mp.com
isocpp.org
cppreference.com
isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
Andrei Alexandrescu; „Modern C++ Design”; 2001.
Bjarne Stroustrup; "A Tour of C++”; 2013.
Bjarne Stroustrup; "The C++ Programming Language, 4th Edition”; 2013.
Bjarne Stroustrup; „Programming: Principles and Practice Using C++”; 2008.
Scott Meyers; „Overview of the New C++”; 2015.
Scott Meyers: „Effective Modern C++”; 2014.
dev.mysql.com/doc/
w3schools.com/sql/sql_ref_mysql.asp
Arjen Lentz, Baron Schwartz, Derek J. Balling, Jeremy Zawodny și Peter Zaitsev; „High Performance MySQL”; 2008.
Paul DuBois; „MySQL Cookbook, 3rd Edition”; 2014.
Sasha Pachev; „Understanding MySQL Internals”; 2009.
137
Copyright Notice
© Licențiada.org respectă drepturile de proprietate intelectuală și așteaptă ca toți utilizatorii să facă același lucru. Dacă consideri că un conținut de pe site încalcă drepturile tale de autor, te rugăm să trimiți o notificare DMCA.
Acest articol: Programarea în C++ și utilizarea unei baze de date MySQL, aplicate pe un game server Coordonator Științific: Conf. univ. dr. Nicoleta Iacob Student:… [305469] (ID: 305469)
Dacă considerați că acest conținut vă încalcă drepturile de autor, vă rugăm să depuneți o cerere pe pagina noastră Copyright Takedown.
