Skocz do zawartości

  • Zaloguj korzystając z Facebooka Zaloguj korzystając z Twittera Zaloguj przez Steam Zaloguj poprzez Google      Logowanie »   
  • Rejestracja

Witamy w Nieoficjalnym polskim support'cie AMX Mod X

Witamy w Nieoficjalnym polskim support'cie AMX Mod X, jak w większości społeczności internetowych musisz się zarejestrować aby móc odpowiadać lub zakładać nowe tematy, ale nie bój się to jest prosty proces w którym wymagamy minimalnych informacji.

  • Rozpoczynaj nowe tematy i odpowiedaj na inne
  • Zapisz się do tematów i for, aby otrzymywać automatyczne uaktualnienia
  • Dodawaj wydarzenia do kalendarza społecznościowego
  • Stwórz swój własny profil i zdobywaj nowych znajomych
  • Zdobywaj nowe doświadczenia

Dołączona grafika Dołączona grafika

Guest Message by DevFuse
 

Zdjęcie

Plugin + baza danychOpis dobrego projektu bazy i dobrego używania jej w pluginie.


  • Nie możesz napisać tematu
  • Zaloguj się, aby dodać odpowiedź
37 odpowiedzi w tym temacie

#1 GwynBleidD

    Godlike

  • Administrator

Reputacja: 1849
Godlike

  • Postów:3066
  • Steam:steam
  • Lokalizacja:Przemyśl
Offline

Napisano 30.10.2012 01:39

*
Popularny

Na wstępie zaznaczę, że NIE jest to poradnik o używaniu MySQL w AMX, ale o DOBRYM jego używaniu. Kto czytał mój poradnik o dobrych nawykach tworzenia menu, ten wie o co chodzi :)

Powstaje coraz więcej pluginów z użyciem MySQL, ale czy są to dobrze napisane pluginy? Na pewno nie wszystkie. O ile w większości przypadków do samych realizacji funkcji pluginu na serwerze nie można się przyczepić, o tyle komunikacja z bazą danych pozostawia wiele do życzenia. Pół biedy, gdy mamy serwer SQL na tej samej maszynie, albo na maszynie w jednej sieci... Albo gdy używamy SQLite. Ale większość serwerów posiada bazę SQL w zupełnie innej lokalizacji. Często serwer gry jest w Polsce, a SQL we Francji, Niemczech... Czym to owocuje?

Popatrzmy.. Sami wiemy, że serwery zagraniczne do grania mixów się nie nadają raczej, zbyt wysoki ping. Podobny ping do takiego serwera ma dowolny serwer gry postawiony w Polsce, potrafi się on wahać od 100 do 500 milisekund, czyli aż do pół sekundy! To już całkiem sporo jak na przetworzenie zapytania... O ile samo przetworzenie nie zajmuje dużo, przy dobrze skonfigurowanym serwerze nie jest to nawet 1 milisekunda, o tyle wysłanie go i odebranie wyniku już trochę trwa...

Popatrzmy na taki prosty przykład (przyjmijmy średni ping na 150 ms do zagranicznych serwerów), AmxBans, dowolna ich wersja... Gracz wchodzi na serwer, wykonywane są po kolei następujące zapytania:
  • Sprawdzenie, czy gracz nie posiada bana.
  • Jeśli ban został znaleziony:
    • Jest aktywny: zwiększenie ilości kicków w danych o banie (dotyczy GmBans), dalsze zapytania niewykonywane
    • Jeśli nie jest aktywny: przesunięcie go do archiwum
  • Sprawdzenie, czy gracz nie został oflagowany
  • Sprawdzenie, czy gracz nie posiadał wcześniej żadnych banów
  • Jeśli używamy bazy sql zamiast users.ini - sprawdzenie, czy gracz nie jest adminem
W pesymistycznym przypadku - 5 zapytań SQL, w optymistycznym (aktywny ban) 2. W normalnym (brak aktywnych banów): 4. Dosyć sporo. A wszystkie, prócz ostatniego można ograniczyć do TYLKO jednego zapytania. Jakby się postarać to i ostatnie można podłączyć do tego zapytania, ale nie ma to sensu bo nie dalibyśmy wtedy użytkownikowi wyboru czy chce używać users.ini czy bazy danych do przechowywania adminów.

Założyliśmy, że średni ping wynosi 150ms, czyli w uproszczeniu daje nam to 150ms na jedno zapytanie (w rzeczywistości trwa to jednak dłużej), więc 750ms przy pesymistycznej wersji będzie trwało ustalenie z kim mamy do czynienia, prawie sekunda! a graczy możemy mieć nawet 32, przy zmianie mapy te wszystkie zapytania się wykonują! W tym artykule pomogę Wam takich koszmarków unikać...


Projekt bazy danych.
Pierwszą rzeczą, którą musimy uczynić przy tworzeniu pluginu wykorzystującego bazę danych, jest odpowiedni projekt samej bazy danych. Najpierw musimy wiedzieć co zapisujemy. Czy chcemy stworzyć exp moda? Może jakieś statystyki dla graczy zapisywać? A może wszystkie serwery sieci zebrać w jednej bazie danych na potrzeby pluginu takiego, jak xRedirect?

Gdy to już wiemy, następna rzecz to to, co chcemy na dany temat w bazie umieścić. Przyjmijmy dla przykładu, że tworzymy plugin zliczający czas gry na naszym serwerze każdego gracza, który nań wejdzie. Dla uproszczenia przyjmijmy również, że serwer jest Steam Only (nie mam zamiaru igrać z prawem tutaj :D), dzięki czemu każdego gracza po SteamID można rozpoznać.

Więc co musimy w bazie zapisać? Na pewno jego SteamID oraz czas gry na serwerze. Czyli mamy już 2 kolumny w bazie danych, dopiszemy jeszcze trzecią, id. Czym będzie ID? Identyfikatorem ułatwiającym nam operację na bazie danych :) Szybciej w bazie jest wyszukiwać po liczbach, niż po napisach. Więc będziemy mieli 3 kolumny:
  • `id` INT(11) NOT NULL auto_increment
  • `sid` VARCHAR(24) NOT NULL
  • `time` INT(11) NOT NULL
Przy ID dodajemy auto_increment. Dzięki temu każdy id będzie większy o 1 od poprzedniego dodanego do bazy. Dalej mamy string `sid` długości 24 znaków. 20 znaków zajmuje najdłuższy spotkany przeze mnie Steam ID. Dajmy dodatkowe znaki, jakby w przyszłości się rozrosły. Time jest typu INT, czyli liczbą. Wszak czas możemy zapisać w formie ilości sekund przegranej na serwerze. Tak najprościej go jest przechować i przetwarzać (dodawać, odejmować...)

Mamy 3 kolumny, ale to jeszcze nie wszystko. Dodamy klucze! Czym są klucze? Dają nam one unikalność danych w kolumnie, szybkość wyszukiwania i definiują również jaka kolumna jest indeksem w tabeli. Dodamy 2 klucze, ponieważ ID ma być indeksem tabeli (PRIMARY KEY), a sid musi być koniecznie unikalne. Możemy to pominąć, ale zobaczycie jak to ułatwi później budowę zapytania :) Więc indeksy:
  • PRIMARY KEY (`id`)
  • UNIQUE KEY `sid` (`sid`)
No dobrze, ale co gdy mamy sieć serwerów, a czas chcemy dla każdego serwera zapisywać osobno? Trzeba każdemu serwerowi nadać jakieś id (najlepiej w formie liczby i najlepiej też zapisać je po prostu w osobnej tabeli w bazie danych, w której będą dane odnośnie serwerów). Wtedy dodamy tylko kolumnę oznaczającą nasz serwer
`server` INT(11) NOT NULL
oraz zmodyfikujemy indeks `sid` na taki:
UNIQUE KEY `server_sid` (`server`, `sid`)
Jak ten indeks zadziała? Ano nie pozwoli on, aby w bazie znajdował się wpis z takim samym Steam ID oraz takim samym numerem serwera. Czyli gdy mamy 2 takie same Steam ID, ale inne serwery to wpisy te mogą istnieć, tak samo gdy 2 inne SteamID, a 2 takie same serwery.

Cała struktura tabeli, w języku SQL będzie wyglądała tak:
CREATE TABLE IF NOT EXISTS `godziny` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `server` int(11) NOT NULL,
  `sid` varchar(24) NOT NULL,
  `time` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `server_sid` (`server`,`sid`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 COLLATE=utf8_polish_ci;


Wstawianie i aktualizacja danych
Teraz przejdziemy do tego, co na serwerze. Pomyślmy co musimy zrobić w naszym przykładzie od strony serwera? Otóż wstawić nowy rekord w bazie z danymi gracza, lub go aktualizować jeśli już istnieje. Jak można to zrobić? Sprawdzić czy rekord istnieje, jeśli tak to zaktualizować, jeśli nie to wstawić nowy... czyli 2 zapytania dla każdego gracza by się musiały wykonać, ale czy muszą? Pomyślmy... A jakby zapytanie samo zdecydowało czy wstawić, czy zaktualizować? A da się tak? A da :D

Ale pozostaje jeszcze problem przy aktualizacji. Musimy odpowiednio zwiększyć starą wartość czasu, a nie wstawiać nową bezmyślnie, podmieniając starą... Możemy pobrać po prostu starą wartość, dodać i wpisać nową... NIE, tak nie zrobimy! Bo to też oznacza 2 zapytania (co prawda niekoniecznie w ciągu, jedno po drugim, ale dwa!).

Więc mamy do wyboru: Odpowiedni warunek używając SELECT, UPDATE oraz INSERT INTO w jednym zapytaniu... Długie ono niestety wyjdzie i ciężko jest je skonstruować. Drugim rozwiązaniem jest REPLACE INTO. Działa ono tak, że stary rekord usuwa, gdy taki istnieje, a następnie tworzy nowy. Tutaj niestety nie możemy się do starej wartości odwołać. Mamy jeszcze jakieś rozwiązania?

Tak, mamy. Dzięki odpowiednio skonstruowanym kluczom możemy po prostu użyć INSERT INTO. No nie tak po prostu, dopiszemy na końcu ON DUPLICATE KEY UPDATE, co spowoduje, że gdy zapytanie INSERT się nie powiedzie, z powodu istniejących już wartości w polach unikalnych, zostanie wykonane UPDATE. Niestety to UPDATE, jest z pewnych powodów trochę uproszczone w porównaniu do klasycznego.

Najpierw skonstruujmy samego INSERTa:
INSERT INTO `czasy` (`server`, `sid`, `time`)
VALUES (%d, '%s', %d)

Jak widać proste zapytanie :) Dla czytelności podzieliłem na 2 linie. Teraz nasze UPDATE:
INSERT INTO `czasy` (`server`, `sid`, `time`)
VALUES (%d, '%s', %d)
ON DUPLICATE KEY UPDATE
`time` = `time`+%d
Oto i nasz update. Tak, tak się da, serwer SQL wtedy ładnie zwiększy `time` o wartość przez nas podaną. Jednakże napiszemy to troszkę inaczej.
INSERT INTO `czasy` (`server`, `sid`, `time`)
VALUES (%d, '%s', %d)
ON DUPLICATE KEY UPDATE
`time` = `time`+VALUES(`time`)
Ta funkcja spowoduje pobranie wartości, którą podaliśmy przy INSERT. Niby wychodzi troszkę większa ilość znaków, ale za chwilę zobaczycie dlaczego tak.


Odpowiednie umiejscowienie w kodzie pluginu
Teraz rzecz na której ludzie najczęściej się potykają. Kiedy wysyłać dane na serwer? Najprościej by było gdy gracz się rozłączy i pod koniec mapy, co sprowadza się do jednego: client_disconnect. A to jest błąd, duży błąd... Gdyż oznacza to przy pełnym 32 slotowym serwerze wysłanie 32 zapytań pod koniec mapy. Może i niedużo, ale jednak coś. Pytania nie są wysyłane na raz, ale po kolei. Nie mam tu na myśli, że zakończy się jedno, a wyśle drugie. Wysyłane są po kolei, a odbierane w kolejności wysłania. Więc jeśli np 15 zapytanie utknie, to reszta musi czekać. Jeśli odpowiedź z 7 utknie to znów reszta na odbiór musi czekać. Przydałoby się to upchnąć w plugin_end w jednym zbiorczym, np tak:
INSERT INTO `czasy` (`server`, `sid`, `time`) VALUES
(%d, '%s', %d),
(%d, '%s', %d),
(%d, '%s', %d),
(%d, '%s', %d)
ON DUPLICATE KEY UPDATE
`time` = `time`+VALUES(`time`)
Wszak można wrzucić od razu wszystkich graczy w ten sposób (tu jest tylko 4). Ale hmm.. Najpierw się wykonują wszystkie client_disconnect, a dopiero później plugin_end. Więc trzeba jakoś to "przechwycić". Dodatkowym problemem jest to, że będzie potrzebna bardzo duża tablica, aby to wszystko zmieścić. Najpierw poradźmy sobie z 2 problemem, co nam częściowo rozwiąże 1. Zauważasz pewnie tutaj dlaczego wcześniej polecilem użyć tego "magicznego" VALUES. Otóż gdy dodajemy kilka rekordów to nie mamy już możliwości wpisania tam konkretnej wartości, a ta funkcja zadba o to, aby dla każdego wpisu była użyta wartość podana przy próbie jego zainsertowania ;)

Otóż AMX ma to do siebie (jak duża część języków programowania), że lepiej przyjmuje duże tablice, gdy są one tablicami globalnymi, niż lokalnymi. Czyli zdefiniujmy sobie query na początku pliku sma, a nie w samej funkcji tej tablicy używającej. Wielkość query sobie trzeba policzyć. Tutaj mamy na pierwszą linię 53 znaki (wraz z enterem). Na lnii z danymi graczy mamy wstawione zmienne. trzeba policzyć jaką długość każda z nich zajmie. Znaczy liczyć nie trzeba, mamy to w bazie danych: 11, 24 i 11. W sumie jest to 46. do tego 2 przecinki, 2 nawiasy, 2 spacje (dla optymalizacji można je usunąć), 2 apostrofy, przecinek na końcu i enter. 10 znaków, czyli łącznie z tymi wstawionymi ze zmiennych mamy 56. Przemnóżmy to razy 32, wychodzi 1792. Teraz 2 ostatnie linie, mają one odopwiednio 24 i 31 znaków (łącznie z enterem i nullem na ich końcach). Sumujemy i wychodzi: 1900 (o, jaka równa liczba :D). Taką właśnie tablicę musimy sobie zadeklarować. To jest oczywiście przypadek pesymistyczny, więc pewnie do końca jej nigdy nie zapełnimy :)

Teraz problem numer 1. Jak go rozwiązać? W bardzo prosty sposób, w client_disconnect zamiast wykonywać zapytania, będziemy dopisywać do głównego naszego zapytania odpowiednie linie dla każdego "wychodzącego" gracza i wyślemy to zapytanie w plugin_end. Dodatkowo co jakiś czas (proponuję 60 sekund) będzie wykonywany task, który sprawdzi, czy jakiś gracz nie rozłączył się w trakcie trwania mapy i jeśli się rozłaczył (i dopisał do głównego zapytania) to to zapytanie wyślemy :) Pamiętać trzeba o kilku rzeczach: zanim zaczniemy zbierać do zapytania wartości, należy dodać początek, czyli pierwszą linię. Druga rzecz, tuż przed wysłaniem zapytania musimy skasować przecinek z ostatnio wpisanej pozycji (nie przewidzimy przecież wcześniej, że więcej już pozycji nie będzie, a jeśli precinek zostawimy to zapytanie się nie wykona, gdyż będzie błędne!) i dopisanie końcówki (ON DUPLICATE....). Początek najlepiej wpisać przy plugin_init oraz po każdym wysłaniu zapytania (oczywiście nadpisać nim cały napis, a nie dopisywać). Końcówkę tuż przy wysyłaniu :) Dodatkowo jeszcze musimy zadbać o to, aby nie wysyłać zapytania bez wstawionego żadnego wiersza. W tym celu najlepiej utworzyć sobie licznik, inkrementować go przy każdym dodaniu do zapytania wiersza, sprawdzić go przed wysłaniem zapytania, czy nie wynosi zero, a po wysłaniu wyzerować.

Dodatkowo możemy w tasku przejechać się pętlą po wszystkich graczach na serwerze i też wysłać ich dane, dzięki czemu przy crashu nie stracą oni dorobku z całej mapy, ale tylko najwyżej z ostatniej minuty (jeśli co minutę task się wykonuje oczywiście).

Jest jeszcze jedna rzecz. Twórcy AMX, a ściślej biblioteki sqlx przestrzegają przed używaniem ThreadedQuery w plugin_end. Dlatego polecam użyć tutaj tzw trybu liniowego (synchronicznego). Nie spowoduje to "widocznego" laga na serwerze, gdyż gracze w tym momencie będą czekać na zmianę mapy, a czas trwania tej zmiany tak samo się przez to wydłuży, jak przy ThreadedQuery (serwer przed zmianą mapy czeka, aż wszystkie zapytania ThreadedQuery zostaną zakończone).

Druga część poradnika w #12 poście w tym temacie.
  • +
  • -
  • 22
NIE pomagam na PW. Nie trudź się, na zlecenia nie odpiszę... Od pomagania jest forum.
NIE zaglądam w tematy wysłane na PW. Jeśli są na forum to prędzej czy później je przeczytam. Jeśli mam co w nich odpisać, to odpiszę.
 
1988650.png?theme=dark

#2 Jak się nazwać

    Wszechmogący

  • Power User

Reputacja: 170
Profesjonalista

  • Postów:617
  • Imię:a
  • Lokalizacja:a
Offline

Napisano 31.10.2012 09:49

Czyli żeby zabezpieczyć się przed dublowaniem wpisów przy bazie o konstrukcji:
CREATE TABLE IF NOT EXISTS `ZombieDane` (name varchar(32),exp INT(11))
Wystarczy zmienić na
CREATE TABLE IF NOT EXISTS `ZombieDane` (name varchar(32),exp INT(11) UNIQUE KEY `name` (`name`)
)
Tak? Bo do tej pory zabespieczałem się zmiennymi przy insert (true/false żeby tylko 1 zapytanie wyszło). Jak coś źle napisałem to popraw zapytanie bo za niedługo robię reset i przyda mi się to :P
  • +
  • -
  • 1
Pisze na zamówienie statystyki pod nvault. GG: 15600964

#3 Portek

    Kończymy zabawę, permanentna emerytura!

  • Przyjaciel

Reputacja: 976
Master

  • Postów:3007
  • GG:
  • Steam:steam
  • Imię:Michał
  • Lokalizacja:Częstochowa
Offline

Napisano 31.10.2012 13:52

http://dev.mysql.com...-duplicate.html
  • +
  • -
  • 0

Dołączona grafika
IP: ts3.cserwerek.pl


#4 GwynBleidD

    Godlike

  • Autor tematu
  • Administrator

Reputacja: 1849
Godlike

  • Postów:3066
  • Steam:steam
  • Lokalizacja:Przemyśl
Offline

Napisano 04.11.2012 22:58

:arrow: Portek, wiem że osoby, które coś programują powinny wyznawać zasadę "Manual ponad wszystko", ale takie podsuwanie jedynie linku do manuala jest trochę niegrzeczne. Temat powstał po to, aby osobom, które raczkują w łączeniu SQLa z AMXem pokazać jak to robić dobrze, a osobom, które już to robią dłużej wskazać błędy. Prosiłbym o nie pisanie w tym temacie postów w tym stylu, ale raczej umieszczanie rzetelnych odpowiedzi, które będą dobrym uzupełnieniem poradnika.

:arrow: Jak Się Nazwać, tyle wystarczy. Zauważ jak działają klucze w bazach. W MySQL jest kilka typów kluczy. Index przyspiesza wyszukiwanie po danej kolumnie w bazie danych (przydatne przy naprawdę dużej ilości rekordów w bazie, np jakbyś chciał wszystkich graczy przechowywać). Unique powoduje, że wartość lub kombinacja wartości będą unikalne całkowicie w całej tabeli. Primary jest to klucz główny, powinien (ale nie musi, choć niektóre mechanizmy baz danych to wymuszają) być numeryczny i jest unikalny. Obejmuje tylko jedną kolumnę, jest to tzw ID i powinna ta kolumna brać główny udział w relacjach między bazami danych.

Więc gdy uczynisz jakąś kolumnę lub kombinację kolumn unikalnymi, serwer nie pozwoli utworzyć 2ch wierszy o tej samej zawartości w tej/tych kolumnie/ach. Jednak pamiętaj, że zwykłe INSERT INTO zwróci Ci błąd, przez co będziesz miał zaspamione error logi. Więc wtedy zrób to z:


ON DUPLICATE KEY UPDATE `kolumna`=`kolumna`

jeśli nie chcesz nic zmieniać. Insert ignore nie używaj, gdyż ukryje przed tobą inne błędy, nie związane z kluczem. słowo kolumna zamień na nazwę istniejącej kolumny w Twojej tabeli, w obu miejscach na to samo.


Użytkownik GwynBleidD edytował ten post 10.10.2013 18:50

  • +
  • -
  • 3
NIE pomagam na PW. Nie trudź się, na zlecenia nie odpiszę... Od pomagania jest forum.
NIE zaglądam w tematy wysłane na PW. Jeśli są na forum to prędzej czy później je przeczytam. Jeśli mam co w nich odpisać, to odpiszę.
 
1988650.png?theme=dark

#5 PimP517

    Zaawansowany

  • Zbanowany

Reputacja: 52
Pomocny

  • Postów:129
  • Lokalizacja: / home / pimp517
Offline

Napisano 07.11.2012 08:08

A czy np. to doda serwer jak już będzie istniał w bazie?
"INSERT IGNORE INTO `serwery` (`nazwa`, `ip`, `port`, `dodany`) VALUES ('%s','%s','%s' %d)"
Czy może lepiej zastosować?
"REPLACE INTO `serwery` (`nazwa`, `ip`, `port`, `dodany`) VALUES ('%s','%s','%s' %d)"

Zdejmin te warny lub zablokuj mi konto!!! WCM!!!


#6 GwynBleidD

    Godlike

  • Autor tematu
  • Administrator

Reputacja: 1849
Godlike

  • Postów:3066
  • Steam:steam
  • Lokalizacja:Przemyśl
Offline

Napisano 07.11.2012 11:51

Jak już napisałem lepiej unikać INSERT IGNORE (maskuje on nie tylko błąd duplikujących się kluczy, ale też kilka innych kompletnie z tym nie związanych... Praktycznie wszystkie NIE związane ze składnią zapytania).

Jak już też pisałem: wszystko zależy od kluczy. Jeśli posiadasz unikalny klucz ze zdefiniowaną parą kolumn ip,port lub ze zdefiniowaną nazwą (lub oba, ale tego drugiego klucza nie polecam bo czasem nam się nazwa może zduplikować) to pierwsze zapytanie nie zrobi nic, jeśli serwer już istnieje, drugie skasuje stary wpis i utworzy nowy (co może zaowocować również wysokimi wartościami w kolumnie z auto_increment, więc średnio polecam, ale jeśli takiej nie posiadasz to możesz stosować... lepiej jednak posiadać, bo raczej używasz tego w relacjach również, a nie tylko żeby mieć listę serwerów w bazie). Najlepszym rozwiązaniem będzie tutaj INSERT INTO .... ON DUPLICATE KEY UPDATE (co wałkujemy właściwie przez cały temat). Jeśli nie chcesz nic aktualizować gdy istnieje już wpis, dodaj coś nie zmieniającego w update np. `nazwa`=`nazwa`. Całość zapytania gdy NIE chcesz nic zmieniać:
INSERT INTO `serwery` (`nazwa`, `ip`, `port`, `dodany`) VALUES ('%s','%s','%s' %d) ON DUPLICATE KEY UPDATE `nazwa`=`nazwa`
gdy jednak chcesz coś zmienić (a na to wskazuje ogólne zastosowanie takiej tabeli):
INSERT INTO `serwery` (`nazwa`, `ip`, `port`, `dodany`) VALUES ('%s','%s','%s' %d) ON DUPLICATE KEY UPDATE `nazwa`=VALUES(`nazwa`)
Oczywiście zmieniać możesz co chcesz ja założyłem, że ip i port dla każdego serwera są stałe raczej (klucz unique), a ta magiczna kolumna `dodany` raczej nie powinna być zmieniana przez serwer.
  • +
  • -
  • 2
NIE pomagam na PW. Nie trudź się, na zlecenia nie odpiszę... Od pomagania jest forum.
NIE zaglądam w tematy wysłane na PW. Jeśli są na forum to prędzej czy później je przeczytam. Jeśli mam co w nich odpisać, to odpiszę.
 
1988650.png?theme=dark

#7 PimP517

    Zaawansowany

  • Zbanowany

Reputacja: 52
Pomocny

  • Postów:129
  • Lokalizacja: / home / pimp517
Offline

Napisano 08.11.2012 03:09

Dzięki.
Widać że się napracowałeś. ++ Za to.

Zdejmin te warny lub zablokuj mi konto!!! WCM!!!


#8 PimP517

    Zaawansowany

  • Zbanowany

Reputacja: 52
Pomocny

  • Postów:129
  • Lokalizacja: / home / pimp517
Offline

Napisano 08.11.2012 06:55

Nie mogłem edytować postu.
Dodam jeszcze że funkcja SQL_GetInsertId nie zwróci poprawnego wyniku.

Użytkownik PimP517 edytował ten post 08.11.2012 06:56

Zdejmin te warny lub zablokuj mi konto!!! WCM!!!


#9 sebul

    Godlike

  • Junior Admin

Reputacja: 2016
Godlike

  • Postów:5411
  • Steam:steam
  • Imię:Sebastian
  • Lokalizacja:Ostrołęka
Offline

Napisano 08.11.2012 07:20

Nie mogłem edytować postu.
Dodam jeszcze że funkcja SQL_GetInsertId nie zwróci poprawnego wyniku.

Jeśli główny klucz składa się z jednej kolumny, to po wykonaniu INSERTa na pewno zwraca poprawną wartość.
  • +
  • -
  • 1
Posiadam TBM (inaczej PTB), które działa dużo lepiej niż zwykłe PTB, nawet na modach z lvlami. Zainteresowany? Proszę bardzo
Generator tabeli expa - aż do 103600 poziomu

#10 PimP517

    Zaawansowany

  • Zbanowany

Reputacja: 52
Pomocny

  • Postów:129
  • Lokalizacja: / home / pimp517
Offline

Napisano 08.11.2012 09:16

Mam tak.
formatex(mysqlCache,511,"INSERT INTO `serwery` (`nazwa`, `mod`, `ip`, `port`) VALUES ('%s','%s','%s','%d') ON DUPLICATE KEY UPDATE `nazwa`=VALUES(`nazwa`),`mod`=VALUES(`mod`)",SerwerNazwa, SerwerMod, SerwerIP, SerwerPort);
SQL_ThreadQuery(Polaczenie, "sql_DodajSerwer", mysqlCache);
public sql_DodajSerwer(iFailState, Handle:Zapytanie)
{
switch(iFailState)
{
  case TQUERY_CONNECT_FAILED: log_amx("Blad nie mozna polaczyc sie z baza");
  case TQUERY_QUERY_FAILED: log_amx("Blad nie mozna dodac serwera");
  default:{}
}
SerwerID = SQL_GetInsertId(Zapytanie);
}
Przy jednym rekordzie w bazie zwraca 2.
Przy 2 rekordach zwraca 3.
W sumie to niby dobrze zwraca

Użytkownik PimP517 edytował ten post 08.11.2012 09:19

Zdejmin te warny lub zablokuj mi konto!!! WCM!!!


#11 GwynBleidD

    Godlike

  • Autor tematu
  • Administrator

Reputacja: 1849
Godlike

  • Postów:3066
  • Steam:steam
  • Lokalizacja:Przemyśl
Offline

Napisano 08.11.2012 11:51

No i to jest jeszcze jedna zaleta tego zapytania... Praktycznie zawsze działa to GetInsertId prawidłowo... Przy zapytaniu warunkowym (if, select then update else insert tak w uproszczeniu) raz miałem w GetInsertId właściwy ID, a raz jedynkę (czyli ilość edytowanych kolumn...). Przy Replace zwraca niby prawidłowo, ale o wadach replace już się rozpisywałem.
  • +
  • -
  • 0
NIE pomagam na PW. Nie trudź się, na zlecenia nie odpiszę... Od pomagania jest forum.
NIE zaglądam w tematy wysłane na PW. Jeśli są na forum to prędzej czy później je przeczytam. Jeśli mam co w nich odpisać, to odpiszę.
 
1988650.png?theme=dark

#12 GwynBleidD

    Godlike

  • Autor tematu
  • Administrator

Reputacja: 1849
Godlike

  • Postów:3066
  • Steam:steam
  • Lokalizacja:Przemyśl
Offline

Napisano 15.11.2012 00:10

*
Popularny

Odcinek numer 2 - Triggery

No i druga część mojego poradnika, teraz zajmiemy się troszkę innym zagadnieniem, niż zliczanie czasu na serwerach... Do trigerów najlepszym przykładem będzie baza banów ;)

No więc najpierw zaprezentuję skromną bazę danych na potrzeby składowania naszych banów. Zakładamy dalej, iż mamy do czynienia z serwerem Steam, więc w bazie pojawią się takie rzeczy, jak: nick banowanego, SteamID banowanego (na to będzie ban), IP (na to NIE będzie bana... nie chcemy przecież banować całego osiedla na którym mieszka cziter, albo przypadkowych osób, gdy ma zmienne IP. Może to posłużyć do automatycznej informacji o prawdopodobnym banie, gdy wejdzie ten ktoś na stronę banów), nick banującego, SteamID banującego, IP banującego, długość bana (w minutach), czas zbanowania, powód bana, id serwera, informacje o odbanowaniu. Więc i projekt:
CREATE TABLE IF NOT EXISTS `bany` (
  `bid`			int(11)					NOT NULL AUTO_INCREMENT	COMMENT 'ID Banów',
  `name`		varchar(32) collate utf8_polish_ci	NOT NULL		COMMENT 'Nick zbanowanego gracza',
  `ip`			varchar(18) collate utf8_polish_ci	default NULL		COMMENT 'Adres IP zbanowanego gracza - na niego nie ma bana!',
  `steamid`		varchar(32) collate utf8_polish_ci	NOT NULL		COMMENT 'SteamID zbanowanego gracza',
  `a_name`		varchar(32) collate utf8_polish_ci	default NULL		COMMENT 'Nick banującego',
  `a_ip`		varchar(18) collate utf8_polish_ci	NOT NULL		COMMENT 'Adres IP banującego',
  `a_steamid`		varchar(32) collate utf8_polish_ci	default NULL		COMMENT 'SteamID banującego',
  `time`		int(11)					NOT NULL		COMMENT 'timestamp utworzenia bana, wiadomo <img src='http://amxx.pl/public/style_emoticons/<#EMO_DIR#>/smile.png' class='bbc_emoticon' alt=':)' />',
  `duration`		int(11)					NOT NULL default '-1'	COMMENT 'Czas (w minutach) bana. 0 - perm, wartości ujemne dla akcji specjalnych',
  `reason`		varchar(64) collate utf8_polish_ci	NOT NULL		COMMENT 'Powód bana',
  `sid`			int(11)					NOT NULL		COMMENT 'ID serwera',
  `ub`			int(11)					NOT NULL default '0'	COMMENT 'Odbanowanie (jeśli gracz odbanowany, zostanie tu przepisane ID bana, a nie 1 czy inna wartość!)',
  `ub_nick`		varchar(32) collate utf8_polish_ci	default NULL		COMMENT 'Nick odbanowującego',
  `ub_ip`		varchar(18) collate utf8_polish_ci	default NULL		COMMENT 'Adres IP odbanowującego',
  `ub_steamid`		varchar(32) collate utf8_polish_ci	default NULL		COMMENT 'SteamID odbanowującego',
  PRIMARY KEY		(`bid`),
  UNIQUE KEY `steamid`  (`steamid`,`ub`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_polish_ci

Struktura mam nadzieję zrozumiała... Wszystko w komentarzach opisałem w kodzie. Teraz wyjaśnienia, zobaczmy na klucze: mamy oczywiście primary key z id bana. To ID może się wyświetlać zbanowanemu, dla uproszczenia odbanowywania ;) Drugi klucz pilnuje, żeby gracz nie posiadał dwóch aktywnych banów, bo to tylko powody tworzy. Dodane zostało do niego pole ub, domyślnie wynoszące 0 (co oznacza, że ban jest aktywny). Daje to możliwość przechowywania wygasłych czy odbanowanych rekordów tuż obok aktywnego bana. Ale jak to działa? Otóż gdy odbanujemy gracza, przepisujemy do pola ub id bana, czyli bid. Dzięki temu mamy pewność, że steamid będzie unikalny nawet, gdy gracz będzie posiadał kilka nieaktywnych banów. Sprytne rozwiązanie, prawda? ;)

Zapytania do banowania i odbanowywania sobie możecie skonstruować sami na podstawie pierwszej części, one nie są przedmiotem tej części artykułu.

Teraz zrobimy drugą tabelę, w której byśmy chcieli przechowywać zmiany w banach. Np jakiś admin edytuje bana czy odbanuje gracza, chcemy przechować kto i co z banem zrobił. Oto projekt drugiej tabeli, jest prawie identyczna z dwiema różnicami:
CREATE TABLE IF NOT EXISTS `bany_log` (
  `action`		enum('update', 'remove')		NOT NULL		COMMENT 'Typ akcji (edycja/usunięcie)',
  `action_time`		int(11)					NOT NULL		COMMENT 'Czas wykonania akcji',
  `bid`			int(11)					NOT NULL		COMMENT 'ID Banów',
  `name`		varchar(32) collate utf8_polish_ci	NOT NULL		COMMENT 'Nick zbanowanego gracza',
  `ip`			varchar(18) collate utf8_polish_ci	default NULL		COMMENT 'Adres IP zbanowanego gracza - na niego nie ma bana!',
  `steamid`		varchar(32) collate utf8_polish_ci	NOT NULL		COMMENT 'SteamID zbanowanego gracza',
  `a_name`		varchar(32) collate utf8_polish_ci	default NULL		COMMENT 'Nick banującego',
  `a_ip`		varchar(18) collate utf8_polish_ci	NOT NULL		COMMENT 'Adres IP banującego',
  `a_steamid`		varchar(32) collate utf8_polish_ci	default NULL		COMMENT 'SteamID banującego',
  `time`		int(11)					NOT NULL		COMMENT 'timestamp utworzenia bana, wiadomo <img src='http://amxx.pl/public/style_emoticons/<#EMO_DIR#>/smile.png' class='bbc_emoticon' alt=':)' />',
  `duration`		int(11)					NOT NULL default '-1'	COMMENT 'Czas (w minutach) bana. 0 - perm, wartości ujemne dla akcji specjalnych',
  `reason`		varchar(64) collate utf8_polish_ci	NOT NULL		COMMENT 'Powód bana',
  `sid`			int(11)					NOT NULL		COMMENT 'ID serwera',
  `ub`			int(11)					NOT NULL default '0'	COMMENT 'Odbanowanie (jeśli gracz odbanowany, zostanie tu przepisane ID bana, a nie 1 czy inna wartość!)',
  `ub_nick`		varchar(32) collate utf8_polish_ci	default NULL		COMMENT 'Nick odbanowującego',
  `ub_ip`		varchar(18) collate utf8_polish_ci	default NULL		COMMENT 'Adres IP odbanowującego',
  `ub_steamid`		varchar(32) collate utf8_polish_ci	default NULL		COMMENT 'SteamID odbanowującego',
  INDEX			(`bid`),
  INDEX `steamid`	(`steamid`,`ub`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_polish_ci
Tabela jak widać będzie się nazywała bany_log, w niej będziemy przechowywać stare wartości. Czyli gdy ktoś edytuje ban, stare wartości zostaną skopiowane do nowego rekordu w tej tabeli, a w tabeli banów zostanie to zaktualizowane. Różnice widać: doszło pole oznaczające akcję, czyli czy ktoś edytował, czy bana usunął oraz pole przechowujące czas wykonania tej akcji, usunięte zostało autoincrement i unikalność kluczy (zamiast unikalności są zwykłe indeksy). Reszta struktyry jest dokładnie taka sama, co daje nam prostotę w kopiowaniu do tej tabeli danych. Teraz tylko pytanie jak to zrobić na serwerze? Możemy pobrać dane i wykonać zapytanie, ale jak już w 1 części wspomniałem, zamykamy całą pojedyńczą operację w JEDNYM zapytaniu do bazy danych, którym odczytamy i zapiszemy to, co chcemy ;) Więc jak to można inaczej zrobić? co do zapytania dopisać?

.... Ano nic :) Pluginu na serwerze nawet nie trzeba zmieniać, żeby dodać log akcji na banie. Wystarczą odpowiednie triggery... Czym są triggery w takim razie? Są to specjalne zapytania, które zostaną zapisane w bazie tak, jak dane. Baza danych wywoła je automatycznie, gdy zostanie wykonane jakieś zapytanie. Daje to nam niebywałą wygodę! Dodatkowo jeśli zabezpieczymy odpowiednio dostęp do tabeli logu (usuniemy WSZYSTKIM użytkownikom w bazie możliwość edycji i usuwania rekordów, zostawimy wyłącznie możliwość dodawania) to nie będziemy mieli absolutnie problemu ze "znikającymi" banami (czyli gdy jakiś admin odbanowuje swoich kolegów). Ślad zawsze po tym zostanie.

Więc jak zbudować taki trigger? Oto przykład:
CREATE TRIGGER au_bany BEFORE DELETE ON bany FOR EACH ROW
  INSERT INTO bany_log (`action`, `action_time`, `bid`, `name`, `ip`, `steamid`, `a_name`, `a_ip`, `a_steamid`, `time`, `duration`, `reason`, `sid`, `ub`,  `ub_nick`, `ub_ip`, `ub_steamid`)
  VALUES('delete', UNIX_TIMESTAMP(NOW()),OLD.`bid`, OLD.`name`, OLD.`ip`, OLD.`steamid`, OLD.`a_name`, OLD.`a_ip`, OLD.`a_uid`, OLD.`time`, OLD.`duration`, OLD.`reason`, OLD.`sid`, OLD.`ub`, OLD.`ub_nick`, OLD.`ub_ip`, OLD.`ub_steamid`);

Teraz wyjaśnienie, dużo nie trzeba. Tworzymy trigger, nazwany au_bany (każdy trigger musi mieć unikalną nazwę!), dla tabeli banów, akcji delete, który przed każdą akcją delete, dla każdego wiersza (w delete, update i insert może być ich kilka w jednym zapytaniu przecież) wykona insert do bany_log. Jak widzicie używam tu "pseudotabeli" OLD, która jest po prostu aliasem dla starych wartości. Można też użyć new, ale dla delete by nie miały sensu. Z triggerem do UPDATE sobie już chyba poradzicie. Tam używacie również OLD, aby zapisać STARĄ wartość, a nie nową. Do insert też możemy podpiąć wydarzenie, ale czy jest sens?

Teraz już wszelkie akcje na tabeli banów zostaną zapisane w bany_log. Jak widać sam silnik SQL potrafi za nas bardzo dużo rzeczy zrobić :)

Bonus

Na koniec drobny bonus, opisujący kilka rzeczy. Jak widzicie, użyłem UNIX_TIMESTAMP(NOW()). Daje to tyle samo, ile get_systime() w AMX. Dlaczego jednak używam funkcji z SQL? Często na serwerach gier jest błędna godzina ustawiona (nie mamy na to wpływu, gdyż odpowiada za to bezpośrednio hosting). Pół biedy, gdy na wszystkich serwerach jest błędna, ale ta sama. A co, gdy są różne? Może się okazać, że gracz na jednym serwerze dostał bana na godzinę, ale na drugim już po nadaniu ten ban jest uważany za wygasły. Dzięki UNIX_TIMESTAMP(NOW()) mamy pewność, że wszystkie serwery będą pracowały na tym samym czasie, czasie w którym działa serwer MySQL. Te zazwyczaj mają ustawiony poprawny czas i nie tworzy to problemów. Dzięki temu nie musimy używać jakichś timezone offset spotykanych w amxbans czy innych systemach banów.

Druga sprawa: często ludzie stosują 3literowe nazwy kolumn w pluginach, dlaczego? Może usłyszeli, że to jest optymalniejsze. Nie jest (jeśli jest to nieznacznie i to bardzo). Jedyne co tym zyskujemy, to mniejszą długość zapytania, więc możemy utworzyć do tego mniejszą tablicę. Jednak zawsze może być to tablica globalna w pluginie, która już nie jest tak czuła na duże rozmiary, bo nie jest co chwile tworzona i usuwana. Moim więc zdaniem takie praktyki nie mają zbyt dużego sensu, no chyba że zapytanie jest tak długie, że już globalna tablica jest zbyt duża, żeby plugin poprawnie działał. Wtedy takie oszczędności na pamięci się przydadzą, ale zmniejszają znacznie czytelność bazy danych.

Jak widzicie, użyłem w zapytaniach tworzących bazę danych komentarzy. Polecam umieszczanie takich rzeczy, bo kiedyś możemy zapomnieć co to było a_ip albo ub_name. Komentarz nam szybko przypomni, a zajmuje nam miejsce tylko i wyłącznie w zapytaniu tworzącym tabelę.

Dziękuję za wytrwałość w czytaniu moich "bzdurek" ;) Życzę miłego kodzenia... Być może się pojawi 3ci odcinek... :)
  • +
  • -
  • 12
NIE pomagam na PW. Nie trudź się, na zlecenia nie odpiszę... Od pomagania jest forum.
NIE zaglądam w tematy wysłane na PW. Jeśli są na forum to prędzej czy później je przeczytam. Jeśli mam co w nich odpisać, to odpiszę.
 
1988650.png?theme=dark

#13 GT Team

    Ten lepszy xD

  • Zbanowany

Reputacja: 321
Wszechpomocny

  • Postów:1435
  • GG:
  • Imię:Tomasz i Grzegorz
  • Lokalizacja:Wojkowice
Offline

Napisano 08.05.2013 18:48

Chłopie jesteś Geniuszem !

Nowa wersja Tower Defense 0.2 Alpha | Inne Mody -> Nowości

 


#14 mrdrifter

    Początkujący

  • Użytkownik

Reputacja: -3
Mniej niż zer0.

  • Postów:11
  • Imię:Piotrek
Offline

Napisano 09.05.2013 12:11

Tutaj są nawet fajne poradniki + dla Ciebie za błędy.
  • +
  • -
  • 0

#15 sebul

    Godlike

  • Junior Admin

Reputacja: 2016
Godlike

  • Postów:5411
  • Steam:steam
  • Imię:Sebastian
  • Lokalizacja:Ostrołęka
Offline

Napisano 23.07.2014 16:09

Tak się już trochę zastanawiałem, czy dało by radę połączyć insert, on duplicate key oraz select, bo chociażby mając klucze obce w tabeli, to jak pobrać id (klucz primary) z innej tabeli wykonując tylko jedno zapytanie do bazy. Np.
//pseudo tabele
gracz (gid, nazwa)
bron (bid, nazwa)
posiada (gid, bid)
no i chcę do tabeli "posiada" dodać kilka rekordów, znając tylko nazwy graczy i broni. Jest w ogóle coś takiego możliwe? Przy jednym rekordzie, to można by próbować tak
INSERT INTO posiada (gid,bid)
SELECT (SELECT g.gid FROM gracz AS g WHERE g.nazwa='%s'), (SELECT b.bid FROM bron AS b WHERE b.nazwa='%s');
i prawdopodobnie coś takiego zadziała, ale to tylko jeden rekord.
  • +
  • -
  • 0
Posiadam TBM (inaczej PTB), które działa dużo lepiej niż zwykłe PTB, nawet na modach z lvlami. Zainteresowany? Proszę bardzo
Generator tabeli expa - aż do 103600 poziomu

#16 G[o]Q

    I'm G[o]Q

  • Przyjaciel

Reputacja: 1339
Godlike

  • Postów:3556
  • Steam:steam
  • Imię:Krzysiek
  • Lokalizacja:C: / program Files / Valve / Cstrike / G[o]Q.dem
Offline

Napisano 23.07.2014 16:39

możesz zrobić joina ale wtedy musisz miec kolumny którymi mógłbyś połączyc te relacje (chyba ze polaczysz 3 tabele)


  • +
  • -
  • 0
Manual ponad wszystko, konsola ponad manual :D :&

Chcesz wysłać do mnie PW ? użyj nazwy GoQ zamiast G[o]Q
Chcesz Kupić moduł płatności via Pukawka,Tserwery, Gamesol, Zabijaka do mojego sklepu? napisz PW cena to tylko 10 zł/sztuka

GG:6022845 (nie pomagam za free osobom ponizej rangi MoD) :D

#17 sebul

    Godlike

  • Junior Admin

Reputacja: 2016
Godlike

  • Postów:5411
  • Steam:steam
  • Imię:Sebastian
  • Lokalizacja:Ostrołęka
Offline

Napisano 23.07.2014 16:40

Ale, że jak mogę zrobić joina? :D
Ja nie pytam jak połączyć tabelę (to wiem), tylko jak dodać kilka nowych rekordów do jednej z tabel, nie posiadając id kluczy obcych, tylko same nazwy.
  • +
  • -
  • 0
Posiadam TBM (inaczej PTB), które działa dużo lepiej niż zwykłe PTB, nawet na modach z lvlami. Zainteresowany? Proszę bardzo
Generator tabeli expa - aż do 103600 poziomu

#18 GwynBleidD

    Godlike

  • Autor tematu
  • Administrator

Reputacja: 1849
Godlike

  • Postów:3066
  • Steam:steam
  • Lokalizacja:Przemyśl
Offline

Napisano 23.07.2014 20:32

Tak, to zadziała.
  • +
  • -
  • 0
NIE pomagam na PW. Nie trudź się, na zlecenia nie odpiszę... Od pomagania jest forum.
NIE zaglądam w tematy wysłane na PW. Jeśli są na forum to prędzej czy później je przeczytam. Jeśli mam co w nich odpisać, to odpiszę.
 
1988650.png?theme=dark

#19 G[o]Q

    I'm G[o]Q

  • Przyjaciel

Reputacja: 1339
Godlike

  • Postów:3556
  • Steam:steam
  • Imię:Krzysiek
  • Lokalizacja:C: / program Files / Valve / Cstrike / G[o]Q.dem
Offline

Napisano 23.07.2014 21:01


Ale, że jak mogę zrobić joina? :D

 

normalnie? laczysz 1 z 3 i 2 no albo podzapytania które podałeś

 

dla uproszczenia chodzi Ci o cos takiego że masz zapisanych graczy w jednej relacji a bronie w drugiej i wysylasz dane w postaci {nazwaGracza,nazwaBroni} a do relacji 3 maja się zapisać id przekazanych rzeczy jeśli tak to po co kilka rekordów skoro to są relacje 1:1 (a przynajmniej powinny)


  • +
  • -
  • 0
Manual ponad wszystko, konsola ponad manual :D :&

Chcesz wysłać do mnie PW ? użyj nazwy GoQ zamiast G[o]Q
Chcesz Kupić moduł płatności via Pukawka,Tserwery, Gamesol, Zabijaka do mojego sklepu? napisz PW cena to tylko 10 zł/sztuka

GG:6022845 (nie pomagam za free osobom ponizej rangi MoD) :D

#20 GwynBleidD

    Godlike

  • Autor tematu
  • Administrator

Reputacja: 1849
Godlike

  • Postów:3066
  • Steam:steam
  • Lokalizacja:Przemyśl
Offline

Napisano 23.07.2014 22:12

1:1? od kiedy?

mamy ewidentne multi:multi... przecież każdy gracz może z każdą bronią być powiązany... Bez tej 3 tabeli przecież nie przejdzie.

//edit

Właściwie to zapytanie jest trochę nadmiarowe, bo mamy aż 3 selecty. Join będzie tutaj szybszy... No ale jak połączyć 2 tabele w jednym select, gdy nie mamy powiązania między nimi (bo właśnie je tworzymy)?

Prosto :)

INSERT INTO posiada (gid,bid)
SELECT g.gid, b.bid FROM gracz AS g INNER JOIN bron AS b ON 1=1 WHERE g.nazwa='%s' AND b.nazwa='%s';
Jak to zadziała? Ano tak, że wybierze WSZYSTKIE możliwe kombinacje połączenia tabeli graczy i broni, a z tego zbioru wybierze ten wpis, w którym nazwa gracza jest taka i nazwa broni taka. Jeśli chcesz do tabeli dodać jakieś jeszcze pola, nie zawierające ID z innych tabel, możesz po prostu podać je jako stałą w select:

INSERT INTO posiada (gid,bid, exp, quest)
SELECT g.gid, b.bid, 123, 'bardzo trudny' FROM gracz AS g INNER JOIN bron AS b ON 1=1 WHERE g.nazwa='%s' AND b.nazwa='%s';

  • +
  • -
  • 0
NIE pomagam na PW. Nie trudź się, na zlecenia nie odpiszę... Od pomagania jest forum.
NIE zaglądam w tematy wysłane na PW. Jeśli są na forum to prędzej czy później je przeczytam. Jeśli mam co w nich odpisać, to odpiszę.
 
1988650.png?theme=dark




Użytkownicy przeglądający ten temat: 0

0 użytkowników, 0 gości, 0 anonimowych