Jump to content
assignment_ind Existing user? Sign In

Sign In



person_add Sign Up
Go-Code.pl - Support SourceMod i Pluginy CS:GO

Search the Community

Showing results for tags '[poradnik]'.



More search options

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


Categories

  • Errors in compiler
  • Warnings in compiler
  • Fatal errors in compiler

Forums

  • Nasze Sprawy
    • Nowości
    • Dyskusje
    • Propozycje
    • Przywitaj się!
  • Sourcemod Scripting
    • Artykuły, poradniki, tutoriale
    • Pytania na temat kodowania
    • Problem z kodem pluginu
    • Prośby o przerobienie pluginu
    • Gotowe funkcje
    • Koduj z Magnetem
  • Konfiguracja pluginów
    • Artykuły, poradniki i tutoriale
    • Szukam pluginu
    • Duże modyfikacje
    • Zbiór pluginów
    • Extensions
    • Gotowe paczki serwerowe
  • Konfiguracja serwera
    • Artykuły, poradniki, tutoriale
    • Pytania
    • Problemy
    • Ochrona
    • Metamod
  • Counter-Strike: Global Offensive
    • Nowości
    • Artykuły, poradniki, tutoriale
    • Pytania
    • Problemy
    • Pliki
    • Wasza twórczość
    • Publikacje serwerów
  • Hostingi serwerów
    • Oferty firm
    • Opinie o hostingach
    • Pytania
  • Plac zabaw
    • Luźne rozmowy
    • Szukam ekipy
    • Rynek
    • Opinie o ludziach
    • RoundSoundy
  • Archiwum
    • Przestarzałe tematy
    • Kosz

Find results in...

Find results that contain...


Date Created

  • Start

    End


Last Updated

  • Start

    End


Filter by number of...

Joined

  • Start

    End


Group


O mnie

Found 13 results

  1. Większość z osób, która czyta ten tekst wie co to bazy danych i żeby zainstalować u siebie na serwerze RankME trzeba stworzyć bazę MySQL...jednak o co w tym tak naprawdę chodzi? Postaram się o tym opowiedzieć w możliwie jak najkrótszej i najprostszej do zrozumienia formie, a także pokażę, jak używać SQL'a w naszych pluginach Zadaniem baz danych jest przechowywanie ważnych dla nas informacji, aby móc korzystać z nich w dowolnym momencie. Istnieje ich wiele rodzajów - obiektowe, strumieniowe, temporalne...a także relacyjne. SQL jest właśnie przedstawicielem relacyjnych baz danych. 1. Teoria Oczywiście nie jest ona nieuporządkowanym zbiorem walających się po pamięci danych - bazy SQL są ułożone w ustrukturyzowany sposób, który teraz omówię. Gdybym miał posłużyć się tutaj analogią, powiedziałbym, że przypominają one...matrioszkę Mamy bazę, w środku są tabele, w tabelach kolumny i wiersze...ale po kolei: Baza danych Wyskoczę teraz trochę w przyszłość. Kiedy konfigurujemy połączenie SQL, wpis w databases.cfg wygląda mniej więcej tak: "rankME" { "host" "sql.twoja-strona.pl" "database" "7213_rankme" "user" "root" "pass" "asQ%c^d" //"timeout" "0" //"port" "0" } (jeśli nie wiesz jeszcze co tu się wydarzyło - spokojnie, wyjaśnienie pojawi się na końcu poradnika) Jako jeden z argumentów podane zostało "database" - i jest to właśnie nasza baza! W niej znajdują się tzw. tabele. Narysujmy sobie taką bazę: Na pewno nie mówi nam to jednak zbyt wiele - musimy zagłębić się jeszcze bardziej... Tabela W każdej bazie danych, która przechowuje jakieś informacje, muszą znaleźć się tabele. Rysunek poglądowy: Nie jesteśmy ograniczeni do jednej tabeli - możemy stworzyć sobie dowolną ich ilość. Właśnie w nich znajdują się dane, które używane są np. do przechowywania informacji o CodModzie, rankME, sourceBansie i wszystkiego, gdzie wykorzystuje się SQL. Dane w tabelach ułożone są w specyficzny sposób - jak tabelki w excelu... Kolumny Definiowane są one podczas tworzenia tabeli i mówią jakie dane, i o jakim typie, będą tam przechowywane. Układ kolumn w tabeli: Na tym etapie możemy zobrazować sobie jak będzie wyglądała struktura jakiejś prostej bazy danych. Załóżmy, że chcemy przechowywać informacje o ilości zabójstw każdego gracza. W najprostszym wydaniu tabela będzie posiadała dwie kolumny: identyfikator gracza oraz liczba zabójstw: steamID | zabójstwa ------------------------- | | | Same kolumny jednak nie wystarczą...trzeba je jakoś zapełnić danymi. Tym zajmą się oczywiście... Wiersze Każdy wiersz to pewna porcja danych i zarazem najmniejsza cząstka naszej bazy danych. Wiedząc o ich istnieniu możemy wypełnić tabelę z zabójstwami przykładowymi danymi: steamID | zabójstwa ------------------------- 3515151 | 34 3774639 | 728 7352395 | 31 W pierwszym wierszu widnieje identyfikator steamID(3515151) oraz zabójstwa(34). W drugim wierszu - steamID 3774639 i zabójstwa 728. Kiedy pobieramy dane z bazy danych, mają one właśnie formę wierszy, lecz o tym będziemy mówić dopiero później. W tym miejscu zakończyliśmy omawianie teorii. Czas opowiedzieć jak odbywa się komunikacja pomiędzy bazą a hostem (np. serwerem CS:GO) 2. Grunt, żeby się dogadać Komunikacja z bazą danych odbywa się na zasadzie tzw. zapytań - mówimy jakie polecenie mamy wykonać, a SQL wykonuje nasze polecenie. Do najpopularniejszych zapytań należą: CREATE TABLE - tworzy nową tabelę w bazie SELECT - używamy go, gdy chcemy pobrać dane z określonej tabeli, INSERT - służy do wprowadzenia nowych wierszy, UPDATE - aktualizuje wiersze już znajdujące się w tabeli, DELETE - usuwa wiersze Tworząc pluginy w CS:GO będziemy opierali się praktycznie tylko na wymienionych wyżej poleceniach - więcej nam nie będzie zwyczajnie potrzebne. Niewiele osób o tym wie, jednak zapytania SQL dzielą się na dwa rodzaje - synchroniczne oraz asynchroniczne. Co to znaczy? zapytania synchroniczne - kiedy w którymś miejscu w kodzie wykonywane jest takie zapytanie, program staje w miejscu i oczekuje na rezultat zwrócony przez bazę. Dopiero po trzymaniu wyniku plugin rusza dalej zapytania asynchroniczne - po odpaleniu zapytania program kontynuuje wykonywanie poleceń, natomiast rezultat zapytania jest przekazywany do specjalnej funkcji. Plugin nie staje się bezczynnie i kontynuuje swoje działanie Którą metodę warto wybrać? Z całą pewnością zapytania synchroniczne są dużo prostsze do zaimplementowania, gdyż kod po prostu wykonuje się linijka po linijce. Z drugiej strony, nie mamy pojęcia kiedy SQL zwróci rezultat naszego działania. Co jeśli łączymy się z sql w Chinach? W zapytaniu asynchronicznym ten problem nie występuje. Plugin kontynuuje swoje działanie i reaguje dopiero kiedy dotrze do niego rezultat query(zapytania). Z drugiej strony, może to powodować problemy w opracowaniu pluginu, który będzie reagował na brak odpowiedzi w mądry sposób, np. poradzenie sobie w sytuacji, gdy wczytujemy dane gracza, który już znalazł się na serwerze (wówczas trzeba blokować niektóre funkcjonalności do czasu, aż jego dane nie zostaną wczytane). Wiemy już jak działa baza danych, jaką ma strukturę i jak komunikuje się z serwerem. Teraz możemy taką bazę zaprogramować 3. Implementacja Na samym początku należy połączyć się z bazą danych. Możemy wykonywać to w OnPluginStart. Wpierw jednak stwórzmy sobie w zmiennych globalnych uchwyt do bazy: Database DB; a następnie w OnPluginStart: char sError[512]; DB = SQL_Connect("NazwaNaszejKonfiguracji",true, sError, sizeof(sError)); if (DB == null) { PrintToServer("Błąd połączenia z bazą danych! Error: %s", sError); } Na samym początku musimy oczywiście wiedzieć z jakim serwerem chcemy sie połączyć. Określa to pierwszy argument funkcji SQL_Connect - pobiera informacje z wpisu z pliku databases.cfg, znajdującego się w folderze sourcemod/configs. Musimy uzupełnić go o dodatkowy wpis: "NazwaNaszejKonfiguracji" { "host" "link do naszej bazy" "database" "nazwa bazy" "user" "nazwa użytkownika" "pass" "hasło użytkownika" //"timeout" "0" //"port" "0" } Baza danych nie stworzy się sama - musimy ją założyć wraz z mającym do niej dostęp użytkownikiem. Najczęściej wykorzystuje się do tego celu tzw phpMyAdmin, jednak poradnik nie obejmuje jego konfiguracji. Hosting, na którym baza się znajduje zawsze pokazuje w jaki sposób taką bazę skonfigurować. Oglądowy film pokazujący jak to środowisko wygląda można zobaczyć tutaj Gdyby okazało się, że coś poszło nie tak, uchwyt do bazy będzie nullem, a do podanego przez nas stringa zostanie przekazany błąd. Należy dostosować się do jego zaleceń i skorygować pomyłkę. a) Tworzenie tabeli Po nazwiązaniu połączenia z bazą nie jesteśmy jeszcze gotowi na umieszczanie w niej danych, ponieważ nie ma w niej żadnych tabeli! Musimy je utworzyć za pomocą polecenia CREATE TABLE. Zapytanie te możemy wysłać w momencie, kiedy połączymy się poprawnie z SQL. Poprawmy więc nasz kod powyżej: char sError[512]; DB = SQL_Connect("NazwaNaszejKonfiguracji",true, sError, sizeof(sError)); if (DB == null) { PrintToServer("Błąd połączenia z bazą danych! Error: %s", sError); } else { if (!SQL_FastQuery(DB, "CREATE TABLE IF NOT EXISTS Zabojstwa(steamID INT NOT NULL UNIQUE, kille INT NOT NULL)")) { char error[255]; SQL_GetError(db, error, sizeof(error)); PrintToServer("Failed to query (error: %s)", error); } } W tym przypadku nie oczekujemy zwrócenia żadnego rezultatu przez bazę danych - można wówczas wykorzystać SQL_FastQuery, co można przetłumaczyć jako "szybkie zapytanie". Przeanalizujmy sobie krótko jego treść: CREATE TABLE IF NOT EXISTS - tworzy tabelę tylko w sytuacji, gdy nie istnieje jeszcze ona w bazie. Jeśli już tam jest, zapytanie jest ignorowane Zabojstwa - nazwa naszej tabeli. Podczas późniejszych zapytań będziemy odwoływali się właśnie do niej steamID INT NOT NULL UNIQUE - dodaje kolumnę do tabeli. Ponadto, określamy jej typ - jest to INT, czyli integer, z którym mieliśmy już do czynienia chociażby tworząc zwykłe zmienne. NOT NULL oznacza, że podczas dodawania kolejnych wierszy pole to nie może być puste - nie chcemy sytuacji, w której pobierzemy puste pole, gdyż mogły by wyniknąć z tego jakieś komplikacje. NOT NULL nie jest wymagany, jednak w tym przypadku jego zastosowanie jest zasadne. UNIQUE, jak nazwa wskazuje, nie pozwala na stworzenie wiersza z drugim takim samym steamID - chcemy, aby nasze identyfikatory były unikatowe, a duble powodowałby błędy w działaniu programu. kille INT NOT NULL - dodanie wiersza, w którym przechowywane będą zabójstwa graczy b) Dodawanie rekordów W celu dodania nowego elementu do naszej bazy danych używamy polecenia INSERT. Przykładowy wygląd takiego zapytania mógłby wyglądać w następujący sposób: INSERT INTO Zabojstwa VALUES(1234123, 0) Powyższe polecenie spowoduje dodanie do bazy danych nowego rekordu, w którym SteamID będzie wynosiło 1234123, natomiast ilość killi 0 - to logiczne, gdyż najprawdopodobniej jest to nowy gracz, który nie zdążył jeszcze nikogo zabić. Kolejność wpisywania wartości odbywa się zgodnie z kolejnością, z jaką została stworzona nasza tabela i należące do niej rekordy w CREATE TABLE (kolumny 'steamid' i 'kille'). Gdybyśmy chcieli dodać wartości tylko do konkretnych kolumn, wówczas można zrobić to w następujący sposób: INSERT INTO Zabojstwa(steamid,kille) VALUES(1234123, 0) Jak takie zapytanie wysłać? Robi się to w taki sam sposób, jak podczas tworzenia tabeli - używając Query (np. SQL_FastQuery). Co ważne, gdybyśmy chcieli przykładowo do bazy dodać nowego gracza, to musimy podać jego dane, takie jak steamid. Te informacje muszą zostać przekazane przez zapytanie, więc aby tego dokonać należy stworzyć buffer, na którym użyjemy reguł formatujących. Może to wyglądać w następujący sposób: void DodajGraczaDoBazy(int steamID, int kille) { char buffer[1024]; Format(buffer, sizeof(buffer), "INSERT INTO Zabojstwa VALUES(%d,%d);", steamID, kille); if (!SQL_FastQuery(DB, buffer)) { char error[255]; SQL_GetError(db, error, sizeof(error)); PrintToServer("Failed to query (error: %s)", error); } } Średnik w miejscu VALUES(%d,%d); został dodany nieprzypadkowo, ponieważ dotyczy się on stricte naszego zapytania, a nie składni SourceModa. Gdybyśmy dodatkowo założyli, że w naszej bazie chcemy przechowywać nick gracza, operowanie na stringu w zapytaniach wiąże się z bardzo ważnym obowiązkiem opatrywania go w apostrofy (' '). Dodatkowo, nazwy naszych kolumn i tabel możemy opatrywać apostrofem, który znajdziemy pod klawiszem ESC (` `). Prawidłowy zapis dodawania nowego gracza do bazy: Format(buffer, sizeof(buffer), "INSERT INTO Zabojstwa(`steamid`,`nick`,`kille`) VALUES(%d,'%s'%d);", steamID, nick, kille); (Wartości kolumn są nieobowiązkowe) c) Pobieranie informacji z bazy Równie często będziemy chcieli otrzymać jakieś informacje z naszej bazy. Tutaj posługujemy się poleceniem SELECT. Polecenie, które pobierze DOSŁOWNIE CAŁĄ TABELĘ wygląda w następujący sposób: SELECT * FROM `Zabojstwa`; Jednak praktycznie nigdy nie chcemy tego robić w ten sposób - przeważnie zależy nam na konkretnych informacjach. Dzisiaj pokażę dwie najprostsze metody selekcji pobieranych informacji: 1. Selekcja kolumn Nic prostszego! W poprzednim zapytaniu * odpowiadała za pobranie wszystkich wierszy. Jeśli chcemy tylko konkretne - po prostu je wypiszmy: SELECT kille FROM `Zabojstwa`; Teraz zamiast obydwu kolumn wyciągniemy jedynie wszystkie kille. 2. Selekcja wierszy Tutaj pomoże nam klauzula WHERE. Wybiera ona wierszy na podstawie wyrażenia logicznego. Używaliście już kiedyś IFów w SourceModzie? Tutaj reguła jest bardzo podobna \ Nasze podstawowe operatory logiczne to: = (równość) > (większe niż) < (mniejsze niż) >= (większe bądź równe) <= (mniejsze bądź równe) <> (nie równa się, coś jak negacja '!' w SM) W 80% przypadków używa się jednak po prostu operatora przyrównania '='. Powiedzmy, że chcemy pobrać z bazy dane dotyczące gracza o konkretnym steamid. Zapytanie będzie wyglądało następująco: SELECT * FROM `Zabojstwa` WHERE `steamid`=1234123; Prawda, że proste? Gdybyśmy natomiast chcieli znaleźć wszystkie osoby, których ilość killi jest większa od 100: SELECT * FROM `Zabojstwa` WHERE kille > 100; Ponadto selekcję po kolumnach i wierszach można łączyć: SELECT steamid FROM `Zabojstwa` WHERE kille > 100; To zapytanie pobierze jedynie identyfikatory osób, którzy nabili ponad 100 killi. Teraz, kiedy wiemy jak przygotować zapytanie pobierające informacje z bazy danych, pojawia się pytanie - jak sprawić, aby teraz te informacje pojawiły się w naszym pluginie? Tak samo, jak dane trzymane są w bazie danych w postaci wierszy, tak samo wiersze są pobierane przez SourceModa. Proces ściągania danych sprowadza się do "odwiedzania" wiersza po wierszu i zczytywania informacji tak długo, aż wiersze nam się skończą. Wróćmy do przykładu, w którym pobieramy graczy, którzy mają więcej niż 100 killi. Powiedzmy, że chcemy wypisać na czacie wszystkich szczęśliwców - załóżmy też, że nasza baza posiada pole (czyli kolumnę) z nickiem gracza. Może to wyglądać tak: DBResultSet query = SQL_Query(db, "SELECT `nick`,`kille` FROM `Zabojstwa` WHERE `kille` > 100;"); if (query == null) { char error[255]; SQL_GetError(db, error, sizeof(error)); PrintToServer("Failed to query (error: %s)", error); } else { char nickBuffer[MAX_NAME_LENGTH]; int kille; while (SQL_FetchRow(query)) { SQL_FetchString(query, 0, nickBuffer, sizeof(nickBuffer)); kille = SQL_FetchInt(query, 1); PrintToChatAll("Gracz: %s | Kille: %d", nickBuffer, kille); } delete query; } Zasadniczą różnicą jest to, że nie używamy już SQL_FastQuery, a SQL_Query - różnica jest taka, że w przypadku SQL_FastQuery nie interesuje nas zwracany rezultat. My chcemy coś z bazy uzyskać, dlatego stosujemy SQL_Query. Wszystkie wiersze pobrane z MySQL trafiają do "query", które jest dość niecodziennego typu "DBResultSet", który jest właśnie zdolny do trzymania wierszy z SQL. Kolejnym etapem jest przygotowanie buffera i zmiennej na nadchodzące dane. Później wchodzimy do pętli, która opatrzona jest warunkiem while(SQL_FetchRow(query)). O co tu chodzi? Najprościej wytłumaczyć to w taki sposób, że w naszym "query" istnieje nieznana nam ilość wierszy i swoisty "wskaźnik", mówiący który wiersz aktualnie przeglądamy. Funkcja SQL_FetchRow sprawia, że wskaźnik przesuwa się do kolejnego wiersza. Ponadto, zwraca nam true, jeśli takowy wiersz w ogóle istnieje, a jeśli dane się skończyły - zwraca false. Dzięki temu możemy przerobić dosłownie wszystkie kolumny z naszego zapytania. W kolejnym etapie wchodzimy do ciała pętli i tutaj następuje pobranie wartości z wiersza, który aktualnie "mielimy". Tutaj najczęściej będziemy używać SQL_FetchInt(pobierz integera), SQL_FetchString(pobierz stringa) oraz SQL_FetchFloat(pobierz floata). Wszystkie trzy funkcje mają zaczynają się tak samo - najpierw ustalamy skąd pobieramy informacje - czyli z naszego query. Następnie mówimy które pole chcemy pobrać, numerując od zera. I tak, w naszym przypadku 0 to będzie nick, a 1 to zabójstwa (zgodnie z kolejnością kolumn podawanej w zapytaniu SELECT). Różnica pomiędzy SQL_FetchInt a SQL_FetchString jest taka, że SQL_FetchInt zwraca bezpośrednio wartość przez returna (stąd mamy kille = SQL_FetchInt(... ) ), natomiast SQL_FetchString robi to przez modyfikowanie stringa, który podamy jako trzeci argumeny (nie zapominając oczywiście o czwartym argumencie - jego długości). I to wszystko A co, gdybyśmy chcieli, aby wyniki przyszły posortowane? Fajnie by było, aby w pierwszym wierszu znajdowała się osoba z największą ilością killi, w drugim druga najlepsza osoba etc. Okazuje się, że nie musimy to robić ręcznie - SQL posiada wbudowaną opcję sortowania wyników! Jedyne co musimy zrobić, to delikatnie zmodyfikować koniec naszego zapytania: SELECT `nick`,`kille` FROM `Zabojstwa` WHERE `kille` > 100 ORDER BY `kille` DESC; "ORDER BY", czyli po angielsku "sortuj po". Dosłownie: sortuj po wartości kolumny `kille`. A co to DESC? to skrót od słowa "descending", czyli malejący. Dodana fraza powoduje sortowanie wierszy według malejącej wartości kolumny `kille`. Gdybyśmy natomiast chcieli posortować rosnąco (od najmniejszej do największej wartości), zamiast DESC wpisalibyśmy po prostu ASC (ascending, czyli rosnący). d) Aktualizowanie wierszy Prosta sprawa - polecenie UPDATE. Mając na uwadze Wasze wcześniejsze doświadczenie z UPDATE i INSERT ta część nie powinna stanowić większego problemu: UPDATE `Zabojstwa` SET `kille`=200,`nick`='MAGNET' WHERE `steamid`=1234123 Krótka piłka - mówicie którą tabelę chcecie zaktualizować, a później wypisujecie po kolei wartości interesujących Was kolumn. Dodatkowo, potrzebujemy tutaj naszego WHERE, gdyż bez niego WSZYSTKIE wiersze w tabeli otrzymałyby wartości z zapytania - WHERE działa tutaj dokładnie tak samo, jak przy SELECT. e) Usuwanie wierszy Rozdział jeszcze prostszy, niż UPDATE. Tutaj używamy DELETE w banalny sposób: DELETE FROM `Zabojstwa` WHERE `kille` < 20; Czyli dosłownie: usuń z tabeli `Zabojstwa` te wiersze, gdzie ilość killi jest mniejsza od 20. Podobnie jak przy UPDATE, nie spodziewamy się tutaj raczej żadnego rezultatu ,więc możemy użyć SQL_FastQuery, jak przy zakładaniu nowej tabeli. No i to na tyle Mam nadzieję, że pomogłem niektórym osobom zagłębić się w świat baz danych i pokazać, że nie są one takie straszne, na jakie wyglądają. Jeśli macie jakieś uwagi, pytania bądź sugestie co można dodać następnym razem, to oczywiście komentarze nie gryzą Dzięki za dotrwanie do końca ^^
  2. Co to są KeyValues? KeyValues to prosta struktura bazująca na organizacji "drzewkowej" która zawiera zagnieżdżone w sobie sekcje które to zawierają klucze i wartości. Dla wzrokowców i objaśnienia sprawy, najprostsza struktura KeyValues wygląda w ten sposób: "sekcja" { "klucz" "wartosc" } Jak napisałem na samym początku, sekcje mogą być w sobie zagnieżdżone, czyli do sekcji możemy dopisać kolejną sekcję, do niej kolejną, itd. Przykład: "sekcja1" { "sekcja2" { "klucz" "wartosc" } } Przy budowaniu struktury KeyValues warto zaznaczyć, że nazwy sekcji i kluczy nie mogą się powtarzać w obrębie danej sekcji. Trochę zagmatwane, ale mówiąc prościej tworząc sekcję "admin" nie możemy później wewnątrz tej sekcji stworzyć np. 2 razy sekcję "Kowalski", czy 2 razy klucza "Kowalski", nie powinniśmy też tworzyć klucza "Kowalski" a potem sekcji "Kowalski" czy też odwrotnie - myślę, że zrozumiałe. Dodatkowo do KeyValues możemy swobodnie dodawać komentarze. Komentarze pozwalają Nam dodawać tekst, który zostanie pominięty przy wczytywaniu KeyValues poprzez plugin. Dodajemy je poprzez użycie dwóch slashy (//) oraz wypisaniu za nimi jakiegoś tekstu. Komentarz jest jednolinijkowy, czyli od kolejnego wiersza będziemy mogli dalej budować naszą strukturę. "sekcja" { "klucz1" "wartosc1" //To jest komentarz. Tekst ten zostanie pominiety przy wczytywaniu KeyValues przez plugin SourceMod "klucz2" "wartosc2" } Z KeyValues mogłeś(aś) się już spotkać wcześniej, np. przy konfigurowaniu połączenia z bazą danych w pliku addons/sourcemod/configs/databases.cfg. Dla przypomnienia taki plik wygląda mniej więcej tak: "Databases" { "driver_default" "mysql" // When specifying "host", you may use an IP address, a hostname, or a socket file path "default" { "driver" "mysql" "host" "localhost" "database" "db" "user" "root" "pass" "" //"timeout" "0" //"port" "0" } "storage-local" { "driver" "sqlite" "database" "sourcemod-local" } } Jak pracować z KeyValues w pluginie SourceMod? Na początku przyjmijmy pewne założenia. Mianowicie napiszemy sobie plugin, który będzie tworzył główne menu serwera. Plugin ten będzie wywoływał komendę w konsoli gracza po naciśnięciu konkretnej opcji w menu. Nasz plik KeyValues będzie w addons/sourcemod/configs, nazwiemy go main_server_menu.ini. Jego struktura będzie wyglądała w ten sposób (wiem, można by to zrobić lepiej): "MainServerMenu" { "sm_vip" { "name" "Sprawdź opis VIP'a" "command" "sm_vip" } "res" { "name" "Menu RoundSoundów" "command" "res" } "rs" { "name" "Resetuj statystyki" "command" "rs" } } Na początku zadeklarujmy zmienną globalną która będzie uchwytem dla naszego menu głównego oraz od razu dodajmy RegConsoleCmd do OnPluginStart: Menu g_mainServerMenu; public void OnPluginStart() { RegConsoleCmd("sm_menu", cmd_MainServerMenu); } public Action cmd_MainServerMenu(int client, int args) { g_mainServerMenu.Display(client, MENU_TIME_FOREVER); return Plugin_Handled; } Następnie zbudujmy funkcję, która będzie odpowiedzialna za stworzenie menu na podstawie naszych KeyValues. Niech to będzie funkcja BuildMenu void BuildMenu() { // kodzik... } Na samym jej początku stworzymy nasze menu: //tworzymy menu g_mainServerMenu = new Menu(MainServerMenu_Handler); g_mainServerMenu.SetTitle("Główne menu serwera", MENU_ACTIONS_ALL); Następnie stworzymy uchwyt do KeyValues oraz wczytamy jego strukturę z naszego pliku konfiguracyjnego: //budujemy sciezke do pliku addons/sourcemod/configs/main_server_menu.ini char path[256]; BuildPath(Path_SM, path, sizeof(path), "configs/main_server_menu.ini"); //tworzymy KeyValues oraz importujemy strukture z pliku KeyValues keyValues = new KeyValues("MainServerMenu"); keyValues.ImportFromFile(path); Warto powiedzieć. że KeyValues wcale nie muszą wykorzystywać żadnego pliku. Można operować chociażby na strukturze podanej jako ciąg znaków wczytanej z zewnętrznego źródła (np. zawartość zewnętrznej strony WWW). Jednak najczęściej oczywiście struktura jest wczytywana z pliku, tak jak w przykładzie wyżej. Co jeszcze jest istotne, to przy deklaracji new KeyValues jako paramtetr podajemy nazwę pierwszej sekcji w naszym pliku, znaną również jako sekcja główna czy węzeł główny (więcej niżej). Mamy już wczytany pliczek do uchwytu, teraz trzeba by się odpowiednio po tej całej strukturze poruszać, a potem wczytać odpowiednie wartości do menu. Przy poruszaniu się po pliku należy wiedzieć, że: Sekcja to też w pewnym sensie klucz, o czym warto pamiętać przy poruszaniu się po strukturze. Sekcja to taka oddzielna nazwa dla kluczy, które posiadają w sobie jeszcze inne klucze. Poruszając się po pliku mamy do czynienia z pozycją. Pozycja określa nam na którym poziomie podsekcji jesteśmy (inaczej poziom zagnieżdżenia) oraz na którym kluczu w danej sekcji jesteśmy. Mówiąc o pozycji mamy zatem do czynienia z dwoma wartościami. Jak to wygląda praktyce? A no tak: "MainServerMenu" //sekcja główna (węzeł główny) { "sm_vip" //pierwszy klucz w pierwszym poziomie zagnieżdżenia { "name" "Sprawdź opis VIP'a" //pierwszy klucz w drugim poziomie zagnieżdżenia "command" "sm_vip" //drugi klucz w drugim poziomie zagnieżdżenia } "res" //drugi klucz w pierwszym poziomie zagnieżdżenia { "name" "Menu RoundSoundów" //pierwszy klucz w drugim poziomie zagnieżdżenia "command" "res" //drugi klucz w drugim poziomie zagnieżdżenia } "rs" //trzeci klucz w pierwszym poziomie zagnieżdżenia { "name" "Resetuj statystyki" //pierwszy klucz w drugim poziomie zagnieżdżenia "command" "rs" //drugi klucz w drugim poziomie zagnieżdżenia } } Podstawowe funkcje do poruszania się po KeyValues: GotoFirstSubKey(); // Przesuwa pozycje do pierwszej podsekcji (poziom zagniezdzenia nizej) // Zwraca false w przypadku nieznalezienia podsekcji GotoNextKey(); // Przesuwa pozycje do kolejnego klucza/sekcji w danej sekcji // Zwraca false w przypadku nieznalezienia kolejnego klucza GoBack(); // Przesuwa pozycje o poziom zagniezdzenia wyzej. // Zwraca false, kiedy nie da sie juz wyzej przesunac (np. jestesmy w wezle glownym) Rewind(); // Ustawia pozycje wezla glownego. Podobny cel mozna osiagnac poprzez // uzywanie GoBack dopoki nie zwroci false, jednak ta funkcja jest optymalniejsza. JumpToKey(const char[] key, bool create=false); // Przenosi nas do klucza w danej sekcji o podanej nazwie w parametrze "key" // W przypadku create=true oraz jezeli dany klucz nie zostanie znaleziony, funkcja ten klucz stworzy. // Zwraca true w przypadku znalezienia klucza, false jezeli klucza nie znaleziono Pominąłem parametry które mogły by być trudne do zrozumienia a mają one swoje domyślne wartości. Dla ciekawskich wszystkie i w pełni opisane funkcje można znaleźć w dokumentacji. Okejj, więc co dalej po otwarciu pliku? Musimy przejść o jedno zagnieżdżenie niżej, a więc kolejny skrawek kodu będzie wyglądać tak: //nie znaleziono zadnej podsekcji? Plik jest nieprawidłowy, koniec :/ if (!keyValues.GotoFirstSubKey()) { PrintToServer("*** I had a problem while building a menu :/ Check main_server_menu.ini"); delete keyValues; return; } Po prawidłowym przejściu niżej, Nasza pozycja wygląda teraz tak: "MainServerMenu" //przed GotoFirstSubKey bylismy tutaj { "sm_vip" //po GotoFirstSubKey jestesmy tutaj { "name" "Sprawdź opis VIP'a" "command" "sm_vip" } "sm_res" { "name" "Menu RoundSoundów" "command" "sm_res" } "sm_rs" { "name" "Resetuj statystyki" "command" "sm_rs" } } Co teraz? Musimy wczytać dane z danej sekcji, dodać opcję do menu i przejść do kolejnego klucza (w Naszym wypadku kolejnej sekcji). Skorzystamy z pętli do while: char command[64], itemName[64]; do { //wczytujemy dane z danej sekcji keyValues.GetString("command", command, sizeof(command)); keyValues.GetString("name", itemName, sizeof(itemName)); //dodajemy opcje do menu g_mainServerMenu.AddItem(command, itemName); } while (keyValues.GotoNextKey()); //przechodzimy do kolejnej sekcji, dopoki te sekcje nie skoncza sie I to by było na tyle z wczytywanie, nasze menu jest już zbudowane ^^ Nie zapomnijmy o zamknięciu uchwytu no iii dajmy odpowiednie info serwerowi: //info dla serwera oraz oproznianie zmiennej kv delete keyValues; PrintToServer("*** Main server menu has been built"); Funkcję BuildMenu możemy zamknąć, a pod nią dodajmy funkcję Handler do naszego menu. Będziemy tam w momencie wybrania opcji wpisywać do konsoli gracza daną komendę zapisaną pod "info" tej właśnie opcji: //obsluga menu public int MainServerMenu_Handler(Menu menu, MenuAction action, int param1, int param2) { switch (action) { case MenuAction_Select: { int client = param1; //wczytujemy komende z pola "info" z danej opcji w menu oraz wywolujemy komende w konsoli char info[64]; menu.GetItem(param2, info, sizeof(info)); ClientCommand(client, info); } } } W OnPluginStart należy jeszcze wywołać funkcję od budowania menu: public void OnPluginStart() { RegConsoleCmd("sm_menu", cmd_MainServerMenu); BuildMenu(); } Nasz plugin jest gotowy Kod *.sp oraz plik *.ini podaję w załącznikach -> main_server_menu.ini server_menu.sp Co jeszcze warto wiedzieć? Do wczytywania wartości z klucza mamy kilka funkcji, które odpowiadają danemu typowi wartości. Możemy np. wartość wczytać od razu w type int, słuzy do tego funkcja GetNum która zwraca wartość klucza. Jest wiele innych ciekawych funkcji jak GetFloat, GetColor, itd. - więcej w dokumentacji Strukturę można zmieniać, a zmiany potem zapisać poprzez funkcję ExportToFile. Są odpowiednie funkcje do usuwania kluczy, ustawiania ich wartości, itd. Jak wyżej - więcej info na ten temat można znaleźć w dokumentacji KeyValues nie powinny być zbyt wielkie. Niedopuszczalne jest skorzystanie z KeyValues w celu zapisywania ilości zabójstw graczy. Przy tak dużej ilości danych zdecydowanie lepszym rozwiązaniem jest baza danych. Wyjątkiem może być plik w którym tych graczy jest mniej, np. plik z graczami którzy dostają specjalny bonus. Plików KeyValues staraj się nie otwierać zbyt często, najlepiej zrobić to maks. raz na mapę bądź też poprzez komendę do której dostęp ma tylko opiekun serwera. Zadanie domowe! Usuń klucz "command" i zamiast wczytywania go wykorzystaj funkcję GetSectionName dzięki której wczytasz nazwę sekcji, a co za tym idzie od teraz komendę będziesz podawać właśnie w nazwie sekcji. Rozwiązaniem możesz się pochwalić w tym temacie
  3. W tym poradniku omówię tzw. ciasteczka, które zapisują nam np. Wybrany TAG w tabeli. Dla przykładu napiszemy plugin w którym będziemy ustawiać swój TAG w tabeli. Na początku musimy dodać odpowiednie include'y oraz zdefiniować nasz Handle(uchwyt) do którego będziemy się odwoływać. *wymagane* #include <cstrike> #include <clientprefs> Handle g_hClientTag; Teraz musimy "zarejestrować" nasze ciasteczko w OnPluginStart używając funkcji RegClientCookie, który jako argumenty przyjmuje: - Nazwa naszego ciasteczka. - Opis naszego ciasteczka. - Dostęp naszego ciasteczka. CookieAccess_Public Jest ciasteczkiem publicznym i każdy gracz może je zmienić za pomocą komeny sm_cookies <nazwa ciasteczka> < wartość> CookieAccess_Protected Jest ciasteczkiem możliwym tylko do odczytania dla gracza. Gracz nie może go zmienić przy użyciu komendy. CookieAccess_Private Jest ciasteczkiem ukrytym. Gracz nie może go ani odczytać ani zmienić (nasz plugin oczywiście może :D). g_hClientTAG = RegClientCookie("sm_selected_tag", "Zapisuje wybrany przez nas tag", CookieAccess_Protected); W momencie kiedy zreloadujemy plugin, a nie mamy dodanego ponownego wczytania ciasteczek gracz, który wybrał dany TAG musi wybrać go ponownie. Dlatego zrobimy opcję gdzie w OnPluginStart załadujemy ciasteczka ponownie graczom, którzy są w grze oraz nie są botami for (int i = 1; i <= MaxClients; i++) { if (IsClientInGame(i) && !IsFakeClient(i)) OnClientCookiesCached(i); } *wymagane* *nie wymagane* Ostatnią rzeczą, którą zrobimy w OnPluginStart to SetCookieMenuItem, ustawienie tego dodaje nam pod komendę !settings . Przyjmuje on 3 argumenty: - Callback do naszego handlera(uchwytu) - Info - Tekst pokazany w menu. SetCookieMenuItem(TagChangeHandler, 0, "[TAG] Wybierz swój TAG w tabeli"); Po ustawieniu nazwy naszego callbacka oraz nazwy menu możemy robić callbacka Tego typu callback przyjmuje 5 argumentów: - Client ( slot gracza ) - CookieMenuAction czyli po prostu akcja jaka zostanie podjęta. CookieMenuAction_SelectOption po wybraniu opcji CookieMenuAction_DisplayOption po wyświetleniu opcji - Info - Buffer - maxlength public void TagChangeHandler(int client, CookieMenuAction action, any info, const char[] buffer, int maxlength) { if (action == CookieMenuAction_SelectOption)ShowTagMenu(client); } Od teraz po wpisaniu !settings pojawi nam się również opcja zmienienia ciastekcza i w moim przypadku TAG'u. *nie wymagane* *wymagane* Teraz musimy zrobić tak aby przy każdym połączeniu gracza ustawiać mu ciasteczko, które miał wybrane wcześniej czyli musimy użyć forwarda OnClientCookiesCached, przyjmuje on tylko 1 argument czyli slot gracza ( int client ). To teraz musimy pobrać ciasteczko, które miał ustawione gracz wcześniej. Użyjemy do tego funkcji GetClientCookie przyjmuje ona 4 argumenty: - Client ( slot gracza ) - Nasz handle(uchwyt) do którego "zapisywana" jest o tym informacja. - buffer do którego zapiszemy to co nam zwróci funkcja - maxlength ( maksymalna wielkość naszego buffer'a ) Po pobraniu ciasteczka ustawiamy graczowi na tą wartość. public void OnClientCookiesCached(int client) { char value[16]; GetClientCookie(client, g_hClientTag, value, sizeof(value)); g_iClientTag[client] = StringToInt(value); } Mamy już nasze ciasteczko wiemy jak je "zarejestrować", pobrać i ustawić teraz czas na zapisywanie. Aby zapisać nasze ciasteczko użyjemy funkcji SetClientCookie, przyjmuje ona 3 argumenty: - Client ( slot gracza ) - Nasz handle(uchwyt) - Wartość Wartość ta nie jest INT'em tylko stringiem, dlatego zanim przekażemy INT'a musimy zamienić go na string używając funkcji IntToString, która przyjmuje 3 argument: - Liczbę, którą chcemy dać do stringa - Buffer w którym będzie się znajdować nasza liczba - Wielkość naszego buffer'u char value[16]; IntToString(2,value,sizeof(value)); W moim przypadku nie będzie to nie potrzebne, ponieważ pobieram wartość z menu gdzie jest już jako string. SetClientCookie(client, g_hClientTag, info); *wymagane* Plugin, który powstał przy poradniku:
  4. W tym poradniku chciałbym możliwie jak najkrócej przedstawić sposób, w jaki można tworzyć menu, a także pokazać mały trik jak przekazywać przez nie informacje. Wykonuję go w oparciu o moje doświadczenie, a więc nie ma w nim zawartych Paneli, czy obsługi blokowania poszczególnych pozycji. Całość przedstawiam w oparciu o nową składnię (1.7+). Menu w SourcePawnie ma swój własny typ danych (methodmapę) - i nazywa się on Menu. Na samym początku musimy utworzyć sobie do niego uchwyt: Menu menu = new Menu(Nazwa_Naszego_Handlera); Od teraz, kiedy będziemy chcieli budować nasze menu, posłużymy się utworzoną zmienną menu. Jak widzimy, w kodzie powyżej widnieje Nazwa_Naszego_Handlera. Otóż kiedy player widzi menu w grze (np. głosowanie na następną mapę), po zapoznaniu się z nim wybiera interesującą go opcję (może wybrać dusta, assaulta...może też zwyczajnie menu zignorować i nie nacisnąć nic). Program musi taką informację odpowiednio przetworzyć i dowiedzieć się, co tak właściwie nacisnął gracz. Cały ten proces odbywa się w tak zwanym Handlerze (pojęcie to będzie jeszcze wykorzystywane w miejscach, gdzie odpowiedź nie pojawia się natychmiastowo, a jest przetwarzana asynchronicznie, jak to ma miejsce w T_SQL). Kolejną rzeczą jest oczywiście nadanie tytułu - gracz musi wiedzieć, dlaczego dane menu widzi :). Służy do tego polecenie: menu.SetTitle("Tutaj wpisujemy nasz tytuł"); Gdybyśmy chcieli sformatować nagłówek (np. pokazać w nim imię danego gracza), musimy zastosować sformatowanego stringa, gdyż SetTitle nie obsługuje tej funkcji: char menuTitleBuffer[128]; Format(menuTitleBuffer, sizeof(menuTitleBuffer), "Witaj %N! Wybierz opcję:", client); menu.SetTitle(menuTitleBuffer); Przy okazji, jeżeli chcemy pobrać imię gracza, nie ma potrzeby korzystania z GetClientName - robi to za nas reguła formatująca %N - w jej miejscu pojawi się nick gracza ? Aby dodać do menu pozycję, korzystamy z polecenia AddItem: menu.AddIitem("id pozycji", "To, co widzi gracz"); Pierwszy parametr to ciąg znaków, służący do identyfikacji itemu Drugi parametr to widoczna dla gracza pozycja w menu. Identyfikator nie musi być unikalny i można wykorzystać go na masę sposobów. Przykład jego zastosowania pokażę na końcu poradnika. Możemy również określić, czy menu będzie posiadało możliwość wyjścia z niego: menu.ExitButton = false; Domyślnie wartość ta jest ustawiona na true. Po przygotowaniu menu musimy je jeszcze wyświetlić (ponieważ nigdzie tego nie zrobiliśmy - to trochę jak przygotowywanie posiłku i podanie go na końcu gościom). Polecenie: menu.Display(client, 120); Pierwszy parametr to id gracza. Drugi parametr to czas, po jakim menu zniknie w razie bezczynności. Istnieje także makrodefinicja, która sprawia, że menu nigdy nie zniknie, jednak nie pamiętam go teraz. Jeśli ktoś je ma, proszę o podesłanie w komentarzu. EDIT: MENU_TIME_FOREVER (dzięki @Qesik i @Brum Brum) W tym momencie menu pokazało się graczowi. Aby przechwycić jego akcję, tworzymy handler, będący funkcją, która w 90% przypadków wygląda następująco: public int Menu_Handler(Menu menu, MenuAction action, int client, int item) { if (action == MenuAction_Select) { char InfoBuffer[32]; menu.GetItem(item, InfoBuffer, sizeof(InfoBuffer)); if (StrEqual(InfoBuffer, "id pozycji 1")) FunkcjaZPozycji1(client); else if (StrEqual(InfoBuffer, "id pozycji 2")) FunkcjaZPozycji2(client); } return 0; } Parametry handlera: menu - identyfikator naszego menu action - określa jaki rodzaj operacji został wykonany (można wybrać pozycję, wyjść z menu etc. Pełna lista: https://sm.alliedmods.net/new-api/menus/MenuAction) client - id gracza, któremu menu wywołaliśmy item - pozycja w menu, którą nacisnęliśmy (numerowane od 0). Można jej użyć zamiast identyfikatora z menu,AddItem (ja dzisiaj jednak będę się posługiwał wspomnianym stringiem). Pierwszą rzeczą, którą robimy, jest sprawdzenie, czy gracz wybrał którąś opcję z menu. W przeciwnym razie nie interesuje nas co dalej się stanie (choć może oczywiście - wyjście z menu może kierować do innego menu itp.) Później identyfikujemy którą opcja została naciśnięta (przez porównanie ciagu znaków). Natrafienie na właściwą opcję przekierowuje nas do odpowiedniej funkcji. Stworzymy teraz prostą funkcję, której zadaniem będzie wyświetlenie wszystkich żywych graczy, a po wybraniu ich z listy - wysłanie do nich powiadomienia na czacie. Aby tego dokonać, musimy wiedzieć, który gracz został wybrany. Może nam do tego celu posłużyć identyfikator z AddItem. Wystarczy, że przekonwertujemy ID gracza na stringa i przekażemy go wraz z pozycją w menu, aby odwrócić proces i odczytać integera i wykonać na wybranym graczu daną operację. Zwróćmy uwagę na bardzo ważną rzecz - nie wiemy, w jakim czasie w przyszłości gracz wybierze pozycję w menu - może to trwać sekundę, może dziesięć minut... . Gdybyśmy przekazali jaki ID identyfikator gracza (client), wybraniec może w czasie, od wywołania menu do wybrania opcji, zwyczajnie wyjść z serwera. Na jego miejsce może wskoczyć inny gracz, co doprowadza do wysłania informacji do niewłaściwej osoby. W naszym przykładzie nie sprawiłoby to większego kłopotu, jednak w pluginach, gdzie precyzja się liczy (np. zapis informacji o graczu w bazie danych) takie działanie może doprowadzić do bardzo niepożądanych skutków. Aby temu zapobiec, będziemy stosować tzw. UserID - unikalny identyfikator gracza na okres trwania serwera. Po starcie pierwszy gracz otrzyma UserID = 1 (lub 0, nieistotne). Kolejny gracz - oczywiście 2. Gdyby jednak gracz nr 1 wyszedł z serwera i połączył się jeszcze raz, otrzyma UserID = 3 (pod warunkiem, że nikt inny nie wszedł na serwer w międzyczasie). Ten właśnie identyfikator będziemy używali, by określić właściwego gracza - i zareagować, jeśli player opuścił już serwer. Do pobierania UserID na podstawie client'a, a także operacja odwrotna - pobranie client'a w oparciu o UserID, służą operacje GetClientUserId oraz GetClientOfUserId. Po wstępie teoretycznym, zajrzyjmy na rezultat końcowy: void MenuPowitalne(int client) { // tworzymy buffery na nick gracza i jego UserID char menuBuffer[MAX_NAME_LENGTH]; char menuOptionBuffer[8]; // tworzymy menu Menu menu = new Menu(MenuPowitalne_Handler); // nadajemy tytuł menu.SetTitle("Kogo chcesz pozdrowić:"); // lecimy pętlą po wszystkich graczach... for (int i = 1; i < MAXPLAYERS; i++) { // odfiltrowujemy niepołączonych graczy i boty... if (!IsValidClient(i) || !IsClientConnected(i) || IsFakeClient(i)) continue; // formatujemy nick... Format(menuBuffer, sizeof(menuBuffer), "%N", i); // userID jako string... Format(menuOptionBuffer, sizeof(menuOptionBuffer), "%d", GetClientUserId(i)); // i dodajemy do menu :) menu.AddItem(menuOptionBuffer, menuBuffer); } // na końcu wyświetlamy menu.Display(client, 120); } // uchwyt do menu public int MenuPowitalne_Handler(Menu menu, MenuAction action, int client, int item) { // jeżeli gracz wybrał pozycję z menu... if (action == MenuAction_Select) { // pobieramy identyfikator z AddItem... char InfoBuffer[8]; menu.GetItem(item, InfoBuffer, sizeof(InfoBuffer)); // zamieniamy go na integera i sprawdzamy poleceniem GetClientOfUserId, czy gracz jest ciagle w grze... int target = GetClientOfUserId(StringToInt(InfoBuffer)); // jeśli gracza nie ma, GetClientOfUserId zwraca zero... if (!target) { PrintToChat(client, "Gracz wyszedł z gry..."); MenuPowitalne(client); return 0; } // od tego miejsca wiemy, że gracz jest dostępny w grze. Możemy go pozdrowić PrintToChat(target, "Pozdrowienia od gracza %N! :)", client); } // na końcu zwracamy 0. return 0; } // Koniec :) Cały proces został wyjaśniony w komentarzach. To by było na tyle. Gdyby pojawiły się pytania i potrzeba rozszerzenia poradnika, z chęcią to zrobię. Zapraszam do dyskusji poniżej. Pozdrowionka! ? @EDIT Po pełny poradnik zapraszam tutaj (angielski): https://wiki.alliedmods.net/Menus_Step_By_Step_(SourceMod_Scripting)
  5. DataPacki Przeważnie podczas tworzenia funkcji definiujemy pożądaną liczbę parametrów, którą chcemy użyć. Przykład: int Mnozenie(int a, int b) { return a * b; } /////////////// void JakasFunkcja() { PrintToChatAll("5x6 = %d", Mnozenie(5,6)); } Aby zrobić tutaj użytek z funkcji Mnozenie() konieczne było przekazanie dwóch argumentów. Niestety, w niektórych sytuacjach nie mamy komfortu przekazywania dowolnej ilości argumentów - jesteśmy ograniczeni tylko do...jednego! Dotyczy to chociażby asynchronicznych zapytać SQL lub timerów. Przykład: void JakasFunkcja(int client) { int a = 5; int b = 6; CreateTimer(10.0, PomnozCosPozniej, /*TUTAJ TYLKO JEDEN ARGUMENT!*/); } public Action PomnozCosPozniej(Handle timer, any data) { /// co tu zrobic ?! } Gdybyśmy chcieli wykonać operację mnożenia w tym przypadku, niezbędne jest przekazanie dwóch zmiennych. Co robić w takiej sytuacji? Z pomocą przychodzą nam DataPacki! Umożliwiają nam one "upakowanie" dowolnej liczby zmiennych o dowolnym typie do jednego ciągu danych. Cały proces zaczyna się od stworzenia uchwytu do DataPacka: DataPack data = new DataPack(); jesteśmy gotowi do załadowania go danymi. Do tego celu wykorzystuje się kolejno WriteCell, WriteFloat, WriteFunction, WriteString W naszym przykładzie poza dwiema zmiennymi przekażemy dodatkowo komunikat do wyświetlenia, aby pokazać jak posługiwać się ciągami znaków. Nasza funkcja będzie teraz wyglądać tak: void JakasFunkcja(int client) { char helloString[128]; Format(helloString, sizeof(helloString), "Siemanko %N!", client); DataPack data = new DataPack(); data.WriteCell(5); data.WriteString(helloString); data.WriteCell(6); CreateTimer(10.0, PomnozCosPozniej, data); } Celowo umieściłem stringa pomiędzy dwiema liczbami, aby pokazać, że odczyt jest odrobinę bardziej skomplikowany niż zapis. Po pierwsze, każdy DataPack posiada swój specjalny wskaźnik, który pokazuje w którym miejscu ma być przeprowadzany odczyt/zapis. Przeanalizujmy proces zapisu informacji w naszej funkcji JakasFunkcja(int client). Po stworzeniu DataPacka mamy pustą strukturę i wskaźnik znajdujący się na początku: [|] Znak '|' to właśnie nasz wskaźnik Po wywołaniu żądania umieszczenia piątki, proces zaczął się w miejscu gdzie stał nasz wskaźnik - czyli na samym początku: [5|] Po dodaniu inta, wskaźnik przesunął się (troche jak wskaźnik podczas pisania na klawiaturze) Kolejny w kolejce jest string: [5string|] I ostatnia już wartość - 6: [5string6|] W tym miejscu mamy już przygotowanego do wysyłki DataPacka. Zauważmy jednak, że nasz wskaźnik nie zmienił swojej pozycji - jest cały czas ustawiony na końcu struktury. Czy domyślacie się, jak wyglądałby odczyt tych danych? Bez przesunięcia wskaźnika na początek pliku ten proces jest niemożliwy... Na szczęście, istnieje operacja, która może tego dokonać. Nazywa się ona Reset: // zauważmy, że drugi argument funkcji zmienił się na nasz DataPack :) public Action PomnozCosPozniej(Handle timer, DataPack data) { data.Reset(); } Od tego momentu struktura DataPacka wygląda tak: [|5string6] Oznacza to, że jesteśmy gotowi do odczytu ? Istnieje jednak jeszcze jeden haczyk. Wspomniałem wcześniej, że dane w DataPacku przyjmują forme ciągu danych. Oznacza to, że SourceMod widzi je jako swoisty strumień. Właśnie do nas należy zadanie zapanowania nad tym haosem. Do odczytywania danych stosujemy kolejno ReadCell, ReadFloat, ReadFunction i ReadString. W naszym DataPacku nie możemy zacząć wyciągania od np. stringa - wszystko musimy wykonywać w takiej samej kolejności, w jakiej wkładaliśmy dane! Na pierwszy ogień pójdzie więc 5, następnie komunikat, a na samym końcu - 6. A więc pierwszą wartością jest int. Korzystamy z ReadCell: // zauważmy, że drugi argument funkcji zmienił się na nasz DataPack :) public Action PomnozCosPozniej(Handle timer, DataPack data) { data.Reset(); int a = data.ReadCell(); } // Aktualny stan DataPacka: [5|string6] Przyszła pora na stringa. W naszym przypadku nasz łańcuch składa się z 128 znaków i dokładnie tyle zapisaliśmy w DataPacku! Pamiętajmy więc o przygotowaniu odpowiednio dużej tablicy: char helloString[256]; // wielkość tablicy do której zapisujemy może się różnić - ważne, abyśmy pamiętali ile znaków zapisaliśmy WCZEŚNIEJ! data.ReadString(helloString, sizeof(helloString)); I na sam koniec ostatnia liczba: int b = data.ReadCell(); Gotowe! Wszystkie dane zostały pobrane prawidłowo. Nasz efekt końcowy: void JakasFunkcja(int client) { char helloString[128]; Format(helloString, sizeof(helloString), "Siemanko %N!", client); DataPack data = new DataPack(); data.WriteCell(5); data.WriteString(helloString); data.WriteCell(6); CreateTimer(10.0, PomnozCosPozniej, data); } public Action PomnozCosPozniej(Handle timer, DataPack data) { char helloString[256]; data.Reset(); int a = data.ReadCell(); data.ReadString(helloString, sizeof(helloString)); int b = data.ReadCell(); PrintToChatAll("Komunikat: %s", helloString); PrintToChatAll("%d x %d = %d", a, b, a*b); } // stan DataPacka: [5string6|] Oczywiście w razie pojawienia się pytań bądź wątpliwości zapraszam do dyskusji pod spodem ? PS: suplementem do tego materiału jest napisany przez @Vasto_Lorde poradnik o timerach:
  6. Łatwy sposób na testowanie pluginów 1. Przechodzimy do folderu gdzie mamy zainstalowany steam 2. Przechodzimy steamapps/sourcemods/ 3. Wrzucamy zawartość załącznika CSGO + SM.7z do /sourcemods/ 4. Wgrywamy tam najpierw Metamoda potem Sourcemoda (tutaj tutorial na ten temat) 5. Uruchamiamy ponownie Steam 6. Wchodzimy do biblioteki gier 7. Ustawiamy dla gry "CSGO + SM" w parametrach startowych -insecure 8. Końcowy układ folderów powinien wyglądać tak: Dzięki temu mamy "osobną" grę CS:GO gdzie możemy testować pluginy LOKALNIE UWAGA Byłoby miło całej ekipie GO-Code.pl jeśli skorzystasz z CSGO + SM by GO-Code.pl.7z zamiast podanego wyżej CSGO + SM. Ta paczka zawiera reklamę naszego forum i za każdym razem gdy będziesz testował pluginy dasz znać znajomym o naszym forum (ponieważ gra nazywa się "CSGO + SM by GO-Code.pl" zamiast samego "CSGO + SM") UWAGA2 Ten poradnik jest skopiowany słowo w słowo z tego postu napisanego przez @mastah7991 za jego pozwoleniem. Uznałem że jest to na tyle wygodny sposób że zasługuje on na osobny temat
  7. Na początku należy dodać do kodu zmienną globalną typu boolean, w tym celu pod zmiennymi dodajemy bool g_bHud[MAXPLAYERS + 1]; Następnie w OnPluginStart należy zarejestrować komendę do wyłączania/włączania hudu. RegConsoleCmd("sm_hud", Hud_CMD); Do OnClientPutInServer dodajemy g_bHud[client] = true; Teraz dodajemy kod do callbacku zarejestrowanej przez nas komendy. Aby wszystko poprawnie działało zmieniamy kod zawarty w timerze z na
  8. Siemka postanowiłem, że napiszę mały "poradniczek" jak zrobić aby w menu admina pokazywały się tylko mapy które są podane w maplist.txt 1. Wchodzimy w csgo/addons/sourcemod/configs/ i szukamy pliku maplists.cfg 2. znajdujemy 30 linijke i w "file" zmieniamy jedynie końcówkę 3. ja zmieniam w taki sposób np. w pliku csgo.txt mam dodaną mapę de_mirage i wygląda to tak
  9. Korzystanie z enum 1. Uproszczone wprowadzenie "enum" jest to struktura przez którą możemy zamiast surowych liczb wprowadzać nazwy i dzięki temu jest nam łatwiej z nich korzystać. Najprostszym przykładem będzie tutaj zbiór broni w sourcemodzie, gdzie każda broń ma swój numer i swoją enumową nazwę (zobacz też pełny temat dotyczący spisu broni): enum CSWeaponID { CSWeapon_NONE = 0, CSWeapon_P228, CSWeapon_GLOCK, CSWeapon_SCOUT, CSWeapon_HEGRENADE //(..) }; Powyższy kod jest deklaracją enuma o nazwie CSWeaponID, gdzie jego zawartością są kolejno CSWeapon_NONE, CSWeapon_P228 i tak dalej. Można zauważyć, że do pierwszej nazwy przypisane jest 0, i taką będzie przyjmować wartość CSWeapon_NONE = 0. Następne nazwy mają domyślnie wartość o jedną więcej, czyli jest to ciąg liczb naturalnych (0, 1, 2, 3, 4 i tak dalej). W taki sposób możemy korzystać z funkcji takich jak CS_GetWeaponPrice bez zapamiętywania poszczególnych numerów broni. Przykładowo osobiście nie mam pojęcia który numer w enumie ma broń AWP, ale mogę po prostu posłużyć się tym: CSWeapon_AWP. 2. Numeracja i licznik Jak już zostało wspomniane, domyślnie licznik enuma działa jak ciąg liczb naturalnych. Każda następna wartość jest większa od poprzedniej o jeden. Dlatego w tym przypadku: enum { wartosc1 = 0, wartosc2 = 10, wartosc3, wartosc4 = 30 } wartosc3 będzie równa 11. Dzieje się tak, ponieważ nawet jeśli manualnie "ustawimy" liczenie na co 10, kompilator nie będzie wiedział co mamy na myśli. Musimy użyć następującego sposobu: enum (+=10) { wartosc1 = 0, wartosc2, wartosc3 } W takim wypadku wartosc2 jest równa 10, a wartosc3 20. Możemy tak również zrobić w przypadku mnożenia na przykład: (*=2) Bibliografia https://amxx.pl/topic/1699-troche-o-enum/ https://forums.alliedmods.net/showthread.php?t=140103 i własne doświadczenie
  10. Źródło https://forums.alliedmods.net/showthread.php?t=312551 1. Hookujemy SDKHook_WeaponCanUse przy wejściu gracza na serwer. public void OnClientPutInServer(int client) { SDKHook(client, SDKHook_WeaponCanUse, Hook_WeaponCanUse); } 2. Następnie tworzymy funkcję do hooka Hook_WeaponCanUse public Action Hook_WeaponCanUse(int client, int weapon) { char classname[64]; GetEntityClassname(weapon, classname, sizeof classname); if (StrEqual(classname, "weapon_melee") || StrEqual(classname, "weapon_knife")) EquipPlayerWeapon(client, weapon); } 3. Teraz wystarczy dać graczowi broń. (Przykładowy plugin) 4. Pięści możemy dać graczowi takim sposobem. public void GiveFists(int client) { int fists = GivePlayerItem(client, "weapon_fists"); EquipPlayerWeapon(client, fists); }
  11. Jeżeli spędziłeś już trochę czasu na programowaniu w pawnie, na pewno natknąłeś się na funkcje, które pobierają dane z innych pluginów, np. cod_get_user_class, które zwróci ID postaci, na której aktualnie gramy. Warto zauważyć, że owe informacje wychodzą bezpośrednio z silnika cod moda - następuje więc tutaj bezpośrednia wymiana informacji pomiędzy pluginami. W tym poradniku zajmiemy się po krótce tym zagadnieniem. 1. Funkcje natywne Działanie "natywów" sprowadza się do zdalnego wywołania funkcji i ewentualnego zwrócenia rezultatu: 0 - Nasz plugin posiada funkcję natywną szarego pluginu I - zlecamy wykonanie funkcji II - szary plugin wykonuje polecenie III - szary plugin zwraca otrzymany rezultat IV - rezultat trafia do naszego pluginu Teraz pokażę jak takiego natywa z szarego pluginu stworzyć. Przede wszystkim natywy dodajemy w takiej funkcji: public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) Daje nam to pewność, że załadują się one w odpowiedniej chwili. Warto wspomnieć, że natywy możemy wczytywać pojedynczo, bądź jako biblioteka - tworzyć nasze własne pliki #include. W tym poradniku skupię się tylko na tej drugiej metodzie. Musimy więc najpierw zarejestrować naszą bibliotekę: RegPluginLibrary("NaszaNazwa"); następnie dodajemy natywy w następujący sposób: CreateNative("nazwa_natywu", Funkcja_Ktora_Ma_Sie_Wywolac); na samym końcu zwracamy: return APLRes_Success; Teraz pora na Funkcja_Ktora_Ma_Sie_Wywolac. Natywy pozwalają nam również na bezproblemowe przekazywanie argumentów, jednak funkcja, o której przed chwilą wspomniałem, nie może po prostu tych argumentów przyjmować w nawiasach () - działa to w trochę inny sposób: public int Funkcja_Ktora_Ma_Sie_Wykonac(Handle plugin, int numParams) { int liczba = GetNativeCell(1); return liczba * 2; } po pierwsze, argumenty muszą być takie, jak wyżej - Handle plugin (uchwyt to NASZEGO pluginu), a także numParams (ilość parametrów, które przekazaliśmy) Pobieranie argumentów odbywa się poprzez specjalne funkcje GetNative___ (polecam po prostu wpisać sobie "getnative" a także "setnative" w dokumentacji). Każdorazowo pierwszy argument GetNative__ to numer argumentu z naszego wywołania (wyjątkowo numerujemy tutaj od 1!) W przykładzie powyżej założyłem, że przekazany zostanie jedynie int, który zostanie przemnożony przez 2 i zwrócony. Przykładowe zastosowanie: Pamiętajmy jednak, że ciągle nie stworzyliśmy biblioteki GoCode! Będzie ona wyglądać mniej więcej tak: Tak przygotowany plik możemy zapisać jako GoCode.inc i wrzucić do naszego folderu include w kompilatorze. Koniec ? 2. Forwardy (coming soon)
  12. Czasami zdarza się, że nie mamy pojęcia z jakimi ilościami elementów będziemy mieli do czynienia (np. ile klas w CodMod'ie musimy załadować do pamięci). I owszem, możemy wówczas sprowadzić to do prostego ograniczenia, np.: #define MAX_ILOSC_KLAS 100 int ClassID[MAX_ILOSC_KLAS]; //... Niestety, rodzi to dwa zasadnicze problemy: 1. Jeśli aktualnie nie wykorzystujemy całej zaalokowanej pamięci, marnujemy bez potrzeby miejsce 2. Zawsze jesteśmy ograniczeni przez limit, który sami na siebie nałożyliśmy Rozwiązaniem naszego problemu są tablice dynamiczne Mimo, że działają one nieco wolniej od zwykłych tablic, pozwalają na całkowicie dynamiczną alokację pamięci - oznacza to, że w trakcie trwania programu możemy dowolnie manipulować ilością zarezerwowanej pamięci - zarówno alokując, jak i zwalniając zasoby Korzystanie z nich jest naprawdę proste i wymaga jedynie odrobinę wprawy Na samym początku tworzymy uchwyt (najczęściej globalnie): ArrayList MojUchwyt Do poprawnego działania konieczne jest jeszcze jego zainicjowanie - bez tego niemożliwe jest jego działanie, a serwer będzie sypał errorami. Najczęściej piszemy w OnPluginStart: MojUchwyt = CreateArray(block_size, init_size) Teraz omówię parametry block_size oraz init_size, posługując się strukturą tablicy dynamicznej. Możemy sobie ją przyrównać do takiej oto szafeczki: block_size mówi nam ile komórek zarezerwowanych jest dla jednego poziomu. Wartość raz tam podana pozostanie niezmienna dla danej tablicy (chyba, że użyjemy ResizeArray). Na naszym przykładzie dla każdego poziomu zarezerwowane zostaną 3 komórki. init_size określa ile poziomów zostanie zaalokowane na samym początku. Jego wartość ulega zmianie na przestrzeni trwania programu. Nasza szafeczka ma na start zaalokowane 12 poziomów, jednak w praktyce tego parametru najczęścieć nie stosuje się wogóle - wówczas na starcie nie ma żadnego poziomu i zostaną one dodane dopiero później. Jaką wartość podawać w block_size? Jeżeli wiemy, że w danej tablicy umieszczone zostaną jakieś ciągi znaków, które de facto wymagają więcej niż jednej komórki, wówczas podajemy tam wartość, która pozwoli na zapewnienie odpowiedniej ilości miejsca. Przykładowa inicjalizacja tablicy na imiona graczy może wyglądać na przykład tak: MojUchwyt = CreateArray(MAX_NAME_LENGTH+1) Z wykorzystaniem methodmap: MojUchwyt = new ArrayList(MAX_NAME_LENGTH+1); Polecenie zainicjuje pustą tablicę, która będzie posiadała na każdym poziomie ilość komórek równą wartość stałej MAX_NAME_LENGTH + 1 Niedobrze jest ograniczać się do konkretnych ilości danych (np. klas), jednak już dobrą praktyką jest ograniczanie dopuszczalnej ilości znaków, jak w przypadku limitu w nazwie gracza, zdefiniowanego przez podaną wyżej stałą. Gdyby zdarzyło się, że przekroczymy te ograniczenie, nadmiarowe znaki zostają usuwane... Skoro znamy już podstawy, możemy zapoznać się z procedurą zarządzania tablicą dynamiczną: 1/4. Dodawanie Każdorazowe dodanie elementu powoduje utworzenie się na samej górze nowego poziomu (patrz: przykład z półką) z zaalokowanymi komórkami w ilości block_size. Możemy tam wrzucić pojedyńczą komórkę, ciąg znaków, bądź tablicę. Przykład: 2/4. Modyfikowanie Każdy poziom możemy dowolnie edytować. Musimy jedynie wiedzieć na jakiej wysokości się on znajduje. Ważne jest, że poziomy numerujemy od zera! (dół - zero). Przykład: 3/4. Pobieranie Jeżeli pobieramy komórkę ( GetArrayCell ), zostanie ona zwrócona przez return. W przypadku stringa i tablicy, wartości zostaną skopiowane do podanych przez nas lokalizacji. Przykład: 4/4. Usuwanie Nic prostszego - wystarczy index poziomu. Po usunięciu wszystkie wyższe poziomy spadają o jeden w dół (troche jak ściąganie obrusu z zastawionego stołu ?) 5/4. Reszta SourceMod posiada dużo więcej funkcji obsługujących tablicy dynamiczne. Zapraszam tutaj: https://go-code.pl/dokumentacja-sourcemod/adt_array/ Przykład Poniższy kod dodaje do tablic dynamicznych nazwę i UserId każdego gracza, który wejdzie na serwer. Ponadto, komenda !show pozwala na wyświetlenie zawartości owych tablic, a po wybraniu gracza z menu plugin wypisuje jego status - czy gracz jest połączony, czy też nie (lub wykonał reconnect): To by było na tyle ? Jeżeli pojawią się jakieś pytania z radością na nie odpowiem, a w razie potrzeby zaktualizuję poradnik Pozdrawiam!
  13. KOMPEDIUM WIEDZY O BITACH, OPERACJACH BITOWYCH I ZASTOSOWANIU W PRAKTYCE Jeśli masz już jakieś doświadczenie z programowaniem, niezależnie czy jest to pawn, C czy inny język, na pewno na trafiłeś na takie pojęcie, jak "flaga" w pluginie/skrypcie etc. Masz też zapewne świadomość do czego taka flaga służy Przykładowo, w poradniku Vasto o timerach mamy do czynienia z flagami timera, np. UchwytNaszegoTimeru = CreateTimer(99999.9, FunkcjaKtoraSieWywola, INVALID_HANDLE, TIMER_REPEAT); czwarty argument funkcji CreateTimer mówi nam, że timer będzie cyklicznie wywoływał podaną funkcję dopóki go nie 'zabijemy' (KillTimer). Idea wydaje się być prosta - funkcja działa tak, jak jej rozkażemy. Nieco wyżej pojawia się jednak takie wywołanie: CreateTimer(1.0, FunkcjaKtoraSieWywola, GetClientUserId(client), TIMER_REPEAT|TIMER_FLAG_NO_MAPCHANGE|TIMER_DATA_HNDL_CLOSE); Czy potrafisz wyjaśnić, co tutaj się dzieje? Na pewno domyślasz się, że timer wykona się wraz ze wszystkimi trzema flagami, jednak rozłożenie tego na czynniki pierwsze wydaje się być dosyć enigmatyczne. Okazuje się jednak, że nie jest to wcale takie straszne. Wymaga to dobrego zrozumienia, jednak na pewno zapunktuje w przyszłości. Postaram się wyjaśnić to tak dobrze, na ile tylko będę w stanie. Przygotuj sobie jakieś jedzenie i do roboty! Część I - jak zapisujemy liczby? Podstawą tutaj jest dwubitowa architektura komputerów. Oznacza to, że dane w pamięci reprezentowane są w postaci logicznej prawdy (1 - jedynka logiczna), lub fałszu (0 - zero logiczne). Kiedyś nieodzownym było operowanie właśnie na pamięci w takiej formie, jednak z czasem zaczęły powstawać języki programowania, które pozwalały na bardziej abstrakcyjne podejście do tematu kodowania. Jednak pomimo tego, programowanie cały czas sprowadza się do tych samych operacji, co wiele lat temu, jednak w ładniejszym wydaniu (co niekonieczne oznacza, że lepszym). Pod tym, co widzi współczesny programista kodując w C++, w dalszym ciągu kryją się skomplikowane operacje bitowe. I tutaj mamy całe clue sprawy - flagi są reprezentowane bitowo! Od razu nasuwa się pytanie - dlaczego? Po co ktoś miałby używać takiego podejścia? Odpowiedź nasunie się sama w dalszej części poradnika W komputerach najmniejszą adresowalną jednostką jest bajt, który składa się z ośmiu bitów, gdzie każdy bit może zawierać 1 lub 0 (jak wspominałem wyżej). My nie posługujemy się "bajtami" - my działamy ma zmiennych, jak int, float, czy char. Otóż każda zmienna składa się ze stałej ilości bajtów (char 1 bajt, int i float 4 bajty w pawnie). Współczesne języki są jednak na tyle elastyczne, że możemy bez problemu posługiwać się znanymi nam dobrze operacjami dodawania, odejmowania, mnożenia, potęgowania itd bez potrzeby działania na bitach Nie oznacza to jednak, że jesteśmy od tych operacji odcięci ? programiści bardzo dobrze wykorzystują je do optymalizacji działania programów, w tym znanych nam już flag. Dzisiaj również nauczymy się, jak pisać takie flagi samemu. Skoro wiemy już, że pod takim "prostym" integerem(int) kryje się zlepek bitów, zajrzyjmy sobie do niego: Jak widzimy w powyższej jakże profesjonalnie wykonanej, tabeli pokazana jest tylko połowa integera - 2 bajty. Tyle jednak w zupełności wystarczy, żeby pokazać o co tutaj chodzi... Dowolną liczbę zapisaną w jednym systemie można zapisać w innym (np. z dziesiętnego na szesnastkowy). W tym przypadku interesuje nas zamiana liczby dziesiętnej na binarną(ciąg zer i jedynek). Ktoś kiedyś słusznie zauważył, że: i tak 1 to 2^0, 2 to 2^1 i tak dalej.... można zauważyć tutaj jeszcze jedną ciekawą zależność - wartość dziesiętna każdego bitu jest równa wartości wszystkich poprzednich bitów plus jeden, np. 2^6 = 64 = 32+16+8+4+2+1+1 Płynący z tego wniosek jest taki, że każda liczba dziesiętna ma swoją unikalną reprezentację w systemie dwójkowym. Jak zapisujemy liczby dwójkowo? Oto przykłady: 15 = 1111 (2^0 + 2^1 + 2^2 + 2^3) | 20 = 10100 (2^4 + 2^2) | 1024 = 10000000000 (2^10) Działanie odwrotne (liczba binarna na dziesiętną) prezentuje się następująco: 101 = 2^0*1 + 2^1*0 + 2^2*1 = 5 | 1000 = 2^0*0 + 2^1*0 + 2^2*0 + 2^3*1 = 8 Po więcej informacji odnośnie przeliczeń zapraszam tutaj. Skoro więc wiemy, że pod każdą zmienną kryją się bity, oraz że jest ich skończona ilość dla danej zmiennej, możemy również znaleźć największą wartość, jaką dany typ zmiennej może przechowywać. Dla uproszczenia nie będę się zagłębiał w liczby zmiennoprzecinkowe - dziś skupimy się tylko na liczbach całkowitym i na integerze. Przydatnym narzędziem systemowym jest kalkulator, który oferuje tryb programisty. Po wprowadzeniu do niego 32 jedynek binarnie (bo tyle bitów posiada integer), otrzymałem nastepujący wynik: Sporo, ale...nie jest to faktyczna wartość maksymalna integera. Musimy także pamiętać o tym, że liczba może być także ujemna! Jak określić tutaj ujemność liczby? W tej sytuacji poświęca się ostatni bit, którego wartośc określa, czy liczba jest dodatnia, czy ujemna. Skoro straciliśmy jeden bit, nasza liczba "skurczyła się" do 31 miejsc, a więc teraz jest to... 2 147 483 647, a konkretniej przedział < -2 147 483 647 ; 2 147 483 647 > Na tym etapie rozumiemy jak zapisywane są liczby w komputerze, co to bit, bajt i jak przechodzimy z systemu dziesiętnego do binarnego i odwrotnie. Teraz możemy zająć się operacjami bitowymi. Część II - operacje bitowe W systemie dziesiętnym wykonujemy operacje dodawania, odejmowania, mnożenia, dzielenia, potęgowania, pierwiastkowania i tak dalej. System binarny charakteryzuje się swoim własnym zestawem operacji. Każdą operację wykonujemy parami - pierwszy bit liczby numer 1 z pierwszym bitem numer 2 - czynność powtarzamy na wszystkich bitach. Omówię teraz po kolei wszystkie operacje. 1. AND (mnożenie - koniunkcja) -> Znak & Wynik operacji AND daje 1, jeśli obydwie liczby są jedynką Przykład: 1010 & 1001 = 1000 | 1100 & 0101 = 0100 2. OR (dodawanie - alternatywa) -> Znak | Wynik operacji daje 1, jeśli przynajmniej jedna z liczb jest jedynką Przykład: 1010 | 1001 = 1011 | 1100 | 0101 = 1101 3. XOR (alternatywa wykluczająca) | Znak ^ Wynik operacji daje 1, jeśli dwie liczby różnią się, a 0, jeśli są takie same Przykład: 1010 ^ 1001 = 0011 | 1100 ^ 0101 = 1001 4. NOT (negacja) | Znak ~ (tylda) Operacja odwraca wartość bitu Przykład: ~1010 = 0101 | ~1100 = 0011 Są to cztery podstawowe operacje logiczne. Do omówienia pozostały jeszcze przesunięcia bitowe: 5. Przesunięcie bitowe w lewo | Znak << Operacja powoduje, że wszystkie bity w liczbie zostają przesunięte w lewo. Wszystkie bity, które na skutek przesunięcia 'wypadły' poza liczbę zostają utracone. Bity, które 'pojawiają się z prawej strony są zapisywane zawsze jako zera. Po lewej stronie operandu zapisujemy liczbę, natomiast po prawej o ile miejsc przesuwamy. Przykład: 1010 << 1 = 0100 | 1100 << 1 = 1000 5. Przesunięcie bitowe w prawo | Znak >> Operacja powoduje, że wszystkie bity w liczbie zostają przesunięte w prawo. Wszystkie bity, które na skutek przesunięcia 'wypadły' poza liczbę zostają utracone. Bity, które 'pojawiają się z lewej strony są zapisywane zawsze jako zera. Po lewej stronie operandu zapisujemy liczbę, natomiast po prawej o ile miejsc przesuwamy. Przykład: 1010 >> 1 = 0101 | 1100 >> 1 = 0110 Wszystko wydaje się być dziwne i bezsensowne, jednak o tym jak potężne jest to narzędzie przekonasz się w części trzeciej ? Omówiliśmy już wszystkie operacje. Teraz, kiedy jesteśmy już gruntowo przygotowani pod względem teoretycznym, możemy wreszcie przejść do praktyki. Część III - flagi Rozpatrzmy teoretyczną sytuację, w której chcemy monitorować, czy gracz (dla uproszczenia jeden) posiada w nicku frazę "[GO-Code]", aby nagradzać go dodatkową ilością złota co rundę. Aby tego dokonać, musimy wykonać szereg operacji: znaleźć id gracza, na jego podstawie pobrać nick i umieścić w tablicy, sprawdzić, czy nazwa posiada szukaną frazę za pomocą StrContains, a następnie zwrócić rezultat. Mimo, że wszystkie te operacje trwają dość szybko (dla człowieka), to komputer nie ma świadomości tego, że już raz sprawdziliśmy nick gracza - dlatego w każdej kolejnej rundzie musi mieć jasną informację, czy ma przydzielać złoto z tytułu posiadanej frazy... Co byś w tej sytuacji zrobił? Najsensowniejszym rozwiązaniem wydaje się być zastosowanie zwykłej zmiennej, która domyślnie przyjmuje wartość 0 i zmienia się na 1, jeśli powyższe warunki zostaną spełnione. Teraz wystarczy, że serwer będzie na początku rundy sprawdzał zawartość zmiennej, zamiast każdorazowo wykonywać szereg kosztownych operacji - a nie zapominajmy, że przeważnie jest dużo więcej graczy do sprawdzenia ? Załóżmy teraz, że poza sprawdzeniem frazy [GO-Code] chcielibyśmy, by serwer dodatkowo sprawdzał, czy SteamID gracza to STEAM_0:1:1234567, gdyż jest to streamer, które gra na naszym serwerze i chcemy zapewnić mu dodatkowe przywileje. Aby to sprawdzać zapewne ponownie zastosujemy dodatkową zmienną, jednak musimy być świadomi, że teraz mamy już dwie zmienne, zamiast jednej. A co, jeżeli nasze dwie zmienne są lokalne i chcielibyśmy je przekazać do innej funkcji? Sprawa wygląda w miarę prosto, gdy mamy dwie zmienne, ale co w sytuacji, gdzie potrzebujemy takich zmiennych np. dziesięć? PrzekazStatystyki(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10) Pomyślmy, jak wyglądałaby deklaracja flag regexa? Co w sytuacji, gdybyśmy chcieli skorzystać tylko z jednej opcji? Mimo wszystko musielibyśmy wysyłać wszystkie zmienne (bez wyraźnej potrzeby) Dopiero teraz na wierzch wychodzi to, jak użyteczne są operacje bitowe Pamiętamy, że każdy int to zestaw 32 bitów. Każdy bit przyjmuje wartość 1 lub 0.... czyli właśnie to, czego potrzebujemy! 1 i 0 to właśnie nasza prawda i fałsz! daje nam to niesamowicie szeroki wachlarz odogodnień. Zamiast tworzyć dziesięć zmiennych: int x1, x2, x3, x4, x5, x6, x7, x8, x9; tworzymy jedną: int flags; Zamiast przekazywać do funkcji dziesięć zmiennych: PrzekazStatystyki(x1, x2, x3, x4, x5, x6, x7, x8, x9, x10) przekazujemy jedną: PrzekazStatystyki(flags) Teraz pozostaje nam nauczenie się, jak z tego korzystać, gdyż wszystkie operacje będą przeprowadzane na bitach. Powróćmy do naszego przykładu dwóch flag gracza. Gdybyśmy realizowali to standardową metodą, robilibyśmy to w taki sposób: Oraz funkcja przydzielająca atrybuty graczowi: Teraz napiszmy to samo z wykorzystaniem bitów  W naszym przykładzie posiadanie frazy w nicku będzie równoznaczne z ustawieniem pierwszego bitu (od prawej) w zmiennej na 1, natomiast streamer - drugiego bitu. Zauważmy tutaj ciekawą rzecz - pierwszy bit oznacza jedynkę (2^0), natomiast drugi bit dwójkę (2^1). Jeśli więc streamer będzie miał w nicku frazę "GO-Code" (obydwa przypadki spełnione), nasza flaga przyjmie wartość...3! Dlaczego? Oto wyjaśnienie (przypominam, że int ma 32 bity): int flags = 0000 0000 0000 0000 0000 0000 0000 0011 + 2^0 + 2^1 = 3 Prawda, że intuicyjne? Podobnie, gdybyśmy mieli dziesięć flag i chcielibyśmy sprawdzić, czy WSZYSTKIE są ustawione na true(1) wystarczyłoby porównanie wartości zmiennej flags do 1023 (ponieważ każdy bit to suma poprzedników plus jeden, a pod jedenastym bitem siedzi 2^10, czyli 1024. Oznacza to, że wartość 1024 to 00...100 0000 0000, a 1023 to 00...011 1111 1111) Dodatkowo, jeśli wykonujemy operacje arytmetyczne na liczbie dziesiętnej, i tak można ją przedstawić w postaci binarnej. Jeśli więc do zmiennej, która ma wartość 0, dodamy 2 (zmienna += 2) drugi bit tej zmiennej stanie się jedynką, a pozostałe będą zerami. My jednak nie będziemy wykonywać operacji arytmetycznych, gdyż operacja na np. dziesięciu zmiennych mogłaby przyprawić o zawrót głowy nawet największego wymiatacza  Aby dodać flagę do zmiennej (zmienić wartość bitu), będziemy posługiwać się logiczną operacją OR. Wiemy z części drugiej, że OR zwraca 1, jeśli przynajmniej jedna ze zmiennych jest prawdziwa. Jeśli więc wykonamy taką operację: flags |= 1 ( to samo, co flags = flags|1 ) spowodujemy, że pierwszy bit naszej zmiennej stanie się jedynką, natomiast pozostałe pozostaną bez zmian: ...101010 | ...000001(jedynka binarnie) = ...101011 prawda, że fajne? jedynym problemem jest dobre zrozumienie co tutaj się dzieje  podobnie, ustawienie drugiej flagi wiąże się z operacją OR, jednak tutaj posłużymy się już dwójką: ...010101 | ...000010(dwójka binarnie) = ...010111 Ale to jeszcze nie wszystko! Aby nie musieć poruszać się cały czas po potęgach dwójki, możemy również użyć przesunięć bitowych. Jak to działa? Zauważmy, że przesunięcie bitowe w lewo jest równoznaczne z pomnożeniem naszej liczby przez 2. Tak więc 2<<1 = 4. Widzimy też, że działa to dokładnie tak, jak nasza identyfikacja numeru bitu. Jeśli więc zrobimy coś takiego: flags |= (1<<1) efekt będzie taki sam, jak: flags |= 2 Sytuacja robi się jeszcze przyjemniejsza, gdy zdefiniujemy sobie takie makra: #define FRAZA (1<<0) #define STREAMER (1<<1) a później już tylko sama bajka: flags |= STREAMER; flags |= FRAZA; lub flags |= (STREAMER|FRAZA); I WŁAŚNIE TAKICH MAKR UŻYWA SIĘ WSZĘDZIE! Nie ważne, czy będzie to ADMIN_IMMUNITY z flag admina, czy PCRE_UTF8 z regexa - wszędzie odbywa się to w dokładnie ten sam sposób! Ostatnią rzeczą jest sprawdzenie, czy dana flaga jest aktywna, Tutaj posługujemy się logiczną operacją AND. Daje ona jedynkę tylko w sytuacji, gdy obydwie zmienne są prawdziwe. Możemy to obrócić na naszą korzyść. Aby sprawdzić, czy dany bit (tj nasza flaga) jest "włączona", robimy coś takiego: if(flags & FRAZA) jak wiemy, if "przepuszcza dalej", jeśli wartość wyrażenia jest różna od zera. A więc jeśli if otrzymał przykładowo: ...010101 & ...000001 = ...000001 = 1 (binarnie) ...musiał on przepuścić warunek sprawdzenia możemy łączyć: if(flags & FRAZA && flags & STREAMER) { // uprawnienia i kasiora prosze } pamiętajmy też o kolejności wykonywania działań! Z tego właśnie powodu stosuję nawiasy zarówno w warunku jak i przy deklaracji makr #define Tak oto przerobiliśmy cały materiał. Po zastosowaniu zdobytej wiedzy w praktyce, nasz poprzedni kod możemy napisać w następujący sposób: Oczywiście nie należy popadać w paranoję i stosować tych flag wszędzie, gdzie się tylko da xD. W małych pluginach możemy spokojnie korzystać ze zwykłych zmiennych, gdyż narzut pamięciowy jest i tak znikomy. Wierzę, że będziesz w stanie znaleźć do tego wiele zastosowań. Wooah.. ale poleciałem. Poradnik wyszedł strasznie długi, ale chciałem to zrobić w sposób rzetelny i w takiej formie, jaką sam bym chciał otrzymać lata temu, ponieważ nikt nie wytłumaczył tego w należyty sposób. Niech Wam dobrze służy Jeśli macie jeszcze jakieś pytania, czy wątpliwości, śmiało zadawajcie pytania, a jeśli okaże się to potrzebne, zaktualizuję poradnik. Oczywiście zachęcam do przeglądania innych poradników i ciągłego samodoskonalenia. Dzięki za uwagę i powodzenia w lekturze

O NAS Jesteśmy społecznością łączącą ludzi, którzy dzielą pasję poznawania CS:GO od strony programistycznej. Posiadamy duże zaplecze merytoryczne i zawsze cieszymy się, gdy dołączają do nas osoby gotowe do nauki. Gwarantujemy, że z odrobiną wytrwałości i otwartym umysłem bardzo szybko napiszesz swój pierwszy plugin. Zapraszamy! 🙂
Szablon wykonany z dużą ilością przez cyberpixelz / Mesharsky / Sitefuture
Forum dumnie napędzane przez: Invision Power Services, Inc.
×
×
  • Create New...