Odpowiedz
Autor Wiadomość
Kobieta
PostWysłany: 06 Maj 2012, 22:41 
Moderator
Awatar użytkownika
Dołączenie:
Luty 2012
Posty: 530
Skąd: Tychy
nick w SL: PanteraPolnocy
Ostatnio pewien klient prosił mnie o napisanie dość zaawansowanego skryptu obsługującego zamówienia, z danymi przechowywanymi w bazie danych MySQL. Jako, że komuś być może będzie potrzebna kiedyś komunikacja dwukierunkowa na linii Second Life <=> PHP/MySQL, to krótko spiszę co trzeba zrobić. :3

Po co takie coś w ogóle omawiać? Wyobraźmy sobie wiele obiektów tego samego typu, na wielu regionach, oddalonych od siebie znacznie, a więc wszelka normalna (llSay, llShout, llRegionSay) komunikacja nie jest możliwa... i każdy z nich musi mieć te same dane. Idealnym przykładem byłby tutaj rotacyjny system reklam pobierający co jakiś czas listę tekstur z centralnego serwera, ale aby uprościć przykład założę, że potrzebuję armii zwykłych kostek.

Każda z tych kostek będzie pobierać co 5 minut z osobnego, zewnętrznego serwera z obsługą PHP/MySQL trzy wartości: wektor koloru, wartość blasku oraz ciąg, który zostanie wstawiony w tekst ponad tą kostką (hover text). Jednocześnie po kliknięciu na każdej z tych kostek będzie można w czacie, na kanale 123 "powiedzieć" te wartości w jednym ciągu, oddzielone średnikami, aby wysłać te dane na hosta. Przy następnym odświeżeniu kostki więc będą mogły się przestawić na nowy kolor, blask i tekst. Wszystkie, we wszystkich regionach.

Co będzie potrzebne? Host, w miarę szybki, z obsługą języka PHP i baz danych MySQL. Może być darmowy, może być nawet z reklamami - od siebie polecam http://cba.pl - naturalnie najlepszym rozwiązaniem jest własny skromny host dedykowany. Poza tym podstawowa wiedza z zakresu pisania w językach LSL i PHP. Zacznę od opisywania skryptu od strony zewnętrznego hosta.

Baza danych... na potrzeby tego przykładu będzie to osobna tablica "kostki" z czterema kolumnami nazwanymi "nazwa", "kolor", "blask" oraz "tekst" - i jednym wierszem, tymi danymi właśnie. Jeśli by się uprzeć, to można taki wiersz, naturalnie w innym formacie (nazwa-wartość) wstawić w tablice WordPressa czy phpBB... ale to tylko taka wskazówka. ;)

Załączniki 01-struct.jpg, 02-struct.jpg i 03-struct.jpg pokazują strukturę i umiejscowienie tablicy, a także jeden wiersz z danymi. Aby stworzyć taką tablicę oraz przykładowy wiersz należy wprowadzić w okienku SQL zapytanie:

Cytuj:
CREATE TABLE IF NOT EXISTS `kostki` (
`nazwa` varchar(255) NOT NULL,
`kolor` varchar(255) NOT NULL,
`blask` varchar(255) NOT NULL,
`tekst` varchar(255) NOT NULL,
PRIMARY KEY (`nazwa`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

INSERT INTO `kostki` (`nazwa`, `kolor`, `blask`, `tekst`) VALUES
('dane_kostek', '<1.0,1.0,1.0>', '1.0', 'Dowolny tekst');

Zapytanie to tworzy tablicę w wybranej bazie o odpowiedniej strukturze z głównym, unikalnym kluczem przypisanym do kolumny "nazwa" - nie jest to ważne w przypadku jednego wiersza, który się tam będzie znajdować w tym przypadku, ale w przypadku większych baz danych klucze sprawiają, że serwer MySQL wyszukuje dane szybciej. Silnik to MyISAM, zestaw znaków latin1, wszystkie pola to varchar (żeby już za bardzo nie zaciemniać innymi typami danych). Po strukturze jest też naturalnie instrukcja wstawiająca ten jeden wspomniany wiersz.

327
328
329

Skrypt w PHP wygląda z kolei następująco:
(o wiele czytelniejsza wersja na pastebin, w kolorze: http://pastebin.com/1SvEpPYY )

Cytuj:
<?php

// ============== Inicjalizacja

ob_start("ob_gzhandler");
error_reporting(0);

// ============== Konfiguracja

$klucz_walidacyjny = '1a2b3c4';

$database = array(
'host' => '123.456.789.012',
'user' => 'nazwa_uzytkownika',
'pass' => 'haslo_uzytkownika',
'database' => 'nazwa_bazy'
);

// ============== Wspolne funkcje

function die_mysql() {
mysql_close();
die('blad-mysql');
}

function sql_filter($dane) {
if(get_magic_quotes_gpc()) {
$dane = stripslashes($dane);
}
if(function_exists('mysql_real_escape_string')) {
return mysql_real_escape_string($dane);
} elseif(function_exists('addslashes')) {
return addslashes($dane);
} else {
mysql_close();
die('brak-filtra');
}
}

// ============== Znacznik rozpoczecia

echo '111---tutaj-zacznij---';

// ============== Glowna czesc skryptu

if(!empty($_POST['klucz_walidacyjny'])) {
if($_POST['klucz_walidacyjny'] == $klucz_walidacyjny) {
if(mysql_connect($database['host'], $database['user'], $database['pass']) and mysql_select_db($database['database'])) {

if($_POST['akcja'] == 'odczytaj') {

$querydata = mysql_query("SELECT * FROM `kostki` WHERE `nazwa` = 'dane_kostek'") or die_mysql();
while($row = mysql_fetch_assoc($querydata)) {
$pobrane['kolor'] = $row['kolor'];
$pobrane['blask'] = $row['blask'];
$pobrane['tekst'] = $row['tekst'];
}

if(!empty($pobrane['kolor']) && !empty($pobrane['blask']) && !empty($pobrane['tekst'])) {
echo implode(';', $pobrane);
} else {
echo 'dane-zle';
}

} else if($_POST['akcja'] == 'ustaw') {

mysql_query(sprintf("UPDATE `kostki` SET `kolor` = '%s', `blask` = '%s', `tekst` = '%s' WHERE `nazwa` = 'dane_kostek'", sql_filter($_POST['kolor']), sql_filter($_POST['blask']), sql_filter($_POST['tekst']))) or die_mysql();
echo 'zaktualizowane';

} else {

echo 'nieznana-akcja';

}

mysql_close();
} else {
echo 'blad-polaczenia';
}
} else {
echo 'klucz-zly';
}
} else {
echo 'klucz-pusty';
}

// ============== Znacznik zakonczenia

echo '---tutaj-zakoncz---111';

?>

A teraz, po kolei wyjaśniając...

Cytuj:
ob_start("ob_gzhandler");
error_reporting(0);


Uruchomienie kompresowanego w locie bufora, do którego będą wpakowywane dane wyświetlane potem na ekranie - a więc i wysyłane do skryptu. Następuje także wyłączenie domyślnego wyświetlania błędów serwera PHP.

Cytuj:
$klucz_walidacyjny = '1a2b3c4';


Losowy ciąg znaków dodatkowo zabezpieczający hosta przed pobraniem danych przez osoby z zewnątrz. Identyczny ciąg musi być w skrypcie LSL, wewnątrz świata Second Life.

Cytuj:
$database = array(
'host' => '123.456.789.012',
'user' => 'nazwa_uzytkownika',
'pass' => 'haslo_uzytkownika',
'database' => 'nazwa_bazy'
);


Dane dostępowe do hosta MySQL, zależą one od właściciela hosta. W przypadku zrzutów ekranów zawartych w załącznikach moja nazwa bazy danych to "web_db", a host - "localhost". Użytkownika i hasła naturalnie nie zdradzę. :P

Cytuj:
function die_mysql() {
mysql_close();
die('blad-mysql');
}


Pierwsza ze wspólnych funkcji, która w razie wystąpienia błędu kulturalnie zamyka połączenie z hostem zanim skrypt zostanie zatrzymany.

Cytuj:
function sql_filter($dane) {
if(get_magic_quotes_gpc()) {
$dane = stripslashes($dane);
}
if(function_exists('mysql_real_escape_string')) {
return mysql_real_escape_string($dane);
} elseif(function_exists('addslashes')) {
return addslashes($dane);
} else {
mysql_close();
die('brak-filtra');
}
}


Filtr uniemożliwiający przeprowadzenie ataku na bazę danych przy pomocy metody SQL Injection.

Cytuj:
echo '111---tutaj-zacznij---';

...

echo '---tutaj-zakoncz---111';


To będzie miejsce, dzięki któremu skrypt w LSL w świecie Second Life będzie wiedział, że od tego miejsca zaczynają się (lub kończą) właściwe dane, jakimi ma się zająć - a więc reszta badziewia, takie jak doklejki reklamowe serwerów przed i po, ma być ignorowana. Ciągi "111" są konieczne dla prawidłowego indeksowania listy w skrypcie LSL (który z góry zakłada, że jakieś śmieci tam są).

Cytuj:
if(!empty($_POST['klucz_walidacyjny'])) {

...

} else {
echo 'klucz-pusty';
}


Pierwsze co musi zrobić skrypt, to sprawdzić, czy klucz walidacyjny przesyłany przez skrypt LSL nie jest pusty. Jeśli jest - wypluwa 'klucz-pusty', a więc razem ze znacznikami kończa i początku: '---tutaj-zacznij---klucz-pusty---tutaj-zakoncz---'.

Cytuj:
if($_POST['klucz_walidacyjny'] == $klucz_walidacyjny) {

...

} else {
echo 'klucz-zly';
}


Jeśli przesyłany przez skrypt LSL klucz nie jest pusty pora sprawdzić, czy zgadza się z tym zapisanym w PHP. Jeśli nie - strona zwraca komunikat, że klucz jest zły.

Cytuj:
if(mysql_connect($database['host'], $database['user'], $database['pass']) and mysql_select_db($database['database'])) {

...

} else {
echo 'blad-polaczenia';
}


Następnie, jeśli wszystko jest okej, następuje próba połączenia z bazą danych za pomocą wartości podanych wcześniej. Jeśli host za długo nie odpowiada, dane są niewłaściwe albo wystąpiła inna anomalia - zwracane jest 'blad-polaczenia'.

Cytuj:
if($_POST['akcja'] == 'odczytaj') {

...

} else if($_POST['akcja'] == 'ustaw') {

...

} else {

echo 'nieznana-akcja';

}


Jeśli połączenie jest w porządku należy sprawdzić, jaki rozkaz przesyła skrypt. Jeśli to 'odczytaj' wykonana zostanie operacja znajdująca się w pierwszej klamerce, jeśli 'ustaw', to w drugiej. Jeśli rozkaz ma inną treść albo jest pusty, to zostanie zwrócony ciąg 'nieznana-akcja'.

Cytuj:
$querydata = mysql_query("SELECT * FROM `kostki` WHERE `nazwa` = 'dane_kostek'") or die_mysql();

while($row = mysql_fetch_assoc($querydata)) {
$pobrane['kolor'] = $row['kolor'];
$pobrane['blask'] = $row['blask'];
$pobrane['tekst'] = $row['tekst'];
}
Cytuj:
if(!empty($pobrane['kolor']) && !empty($pobrane['blask']) && !empty($pobrane['tekst'])) {
echo implode(';', $pobrane);
} else {
echo 'dane-zle';
}


Podczas odczytywania zostają wybrane z tablicy 'kostki' wszystkie wiersze i powiązane z nimi dane, które w kolumnie 'nazwa' posiadają wartość 'dane_kostek'. Przy niepowodzeniu zostanie wywołana "kulturalna" funkcja die_mysql. ;) Jeśli błędu nie ma, to za pomocą wewnętrznej funkcji mysql_fetch_assoc i pętli while zostaje stworzona mała tablica w pamięci, której wartości 'kolor', 'blask' i 'tekst' odpowiadają tym wybranym z bazy danych. Następnie za pomocą funkcji !empty(), a więc zaprzeczeniu funkcji empty() sprawdzającej, czy wartość jest pusta, zostaje sprawdzona każda z tych wartości - czy nie jest pusta. Jeśli wszystkie zawierają jakieś dane - wtedy tablica jest łączona za pomocą średnika i implode(), dając ciąg 'kolor;blask;tekst'. Jeśli którakolwiek z nich jest pusta - skrypt zwraca 'dane-zle'.

Uwaga! Ponieważ łączymy wartości znakiem średnika, to w samym tekście NIE MOŻE się on znajdować!

Cytuj:
mysql_query(sprintf("UPDATE `kostki` SET `kolor` = '%s', `blask` = '%s', `tekst` = '%s' WHERE `nazwa` = 'dane_kostek'", sql_filter($_POST['kolor']), sql_filter($_POST['blask']), sql_filter($_POST['tekst']))) or die_mysql();

echo 'zaktualizowane';


Zapisywanie jest dużo prostszym procesem. Następuje aktualizacja tablicy 'kostki': ustawione zostają 'kolor', 'blask' oraz 'tekst' tym, co skrypt PHP otrzyma od skryptu LSL, dla wiersza o nazwie 'dane_kostek'. Wszystkie te wartości zostają najpierw przepuszczone przez funkcję sql_filter() zapobiegającą SQL injection. Jeśli nie wystąpi błąd, to skrypt zwraca wartość 'zaktualizowane'.

----------------------------------
Tyle po stronie hosta. Teraz pora na skrypt z drugiej strony "kabla", znajdujący się w świecie Second Life.
(ponownie - kolorowa wersja tutaj: http://pastebin.com/pJ09rrrv )

Cytuj:
string host_url = "http://moj-host.pl/plik_ze_skryptem.php";
string klucz_walidacyjny = "1a2b3c4";
list bodyLista;
key queryPage;
string trybPracy;

vector kolor = <1, 1, 1>;
float blask = 0;
string tekst = "";

default {
state_entry() {
llSetTimerEvent(300);
llListen(123, "", llGetOwner(), "");
}
on_rez(integer start_param) {
llResetScript();
}
timer() {
trybPracy = "odczytaj";
queryPage = llHTTPRequest(host_url, [HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/x-www-form-urlencoded"], "klucz_walidacyjny="+klucz_walidacyjny+"&akcja=odczytaj");
}
listen(integer channel, string name, key id, string message) {
if(channel == 123 && id == llGetOwner()) {
llSetTimerEvent(0);
trybPracy = "ustaw";
bodyLista = llParseString2List(message, [";"], []);
kolor = llList2Vector(bodyLista, 0);
blask = llList2Float(bodyLista, 1);
tekst = llList2String(bodyLista, 2);
queryPage = llHTTPRequest(host_url, [HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/x-www-form-urlencoded"], "klucz_walidacyjny="+klucz_walidacyjny+"&kolor="+(string)kolor+"&blask="+(string)blask+"&tekst="+tekst+"&akcja=ustaw");
}
}
touch_start(integer number) {
if(llDetectedKey(0) == llGetOwner()) {
llOwnerSay("Wektor koloru (<0-1, 0-1, 0-1>); Blask (0-1) ; Tekst (do 255 znakow, BEZ SREDNIKOW). Na kanale 123. Na przyklad:");
llOwnerSay("/123 <0.5, 1.0, 0.75>;0.1;Testowy tekst.");
}
}
http_response(key request_id, integer status, list metadata, string body) {
if(request_id == queryPage) {
bodyLista = llParseString2List(body, ["---tutaj-zacznij---", "---tutaj-zakoncz---"], []);
body = llList2String(bodyLista, 1);
if(body == "blad-mysql") {
llOwnerSay("Wystapil blad podczas odpytywania bazy danych.");
} else if(body == "brak-filtra") {
llOwnerSay("Na serwerze nie wykryto filtra MySQL.");
} else if(body == "klucz-pusty") {
llOwnerSay("Klucz walidacyjny zostal uznany za pusty.");
} else if(body == "klucz-zly") {
llOwnerSay("Klucz walidacyjny zostal uznany za nieprawidlowy.");
} else if(body == "blad-polaczenia") {
llOwnerSay("Blad polaczenia z serwerem MySQL.");
} else if(body == "nieznana-akcja") {
llOwnerSay("Skrypt na serwerze nie potrafi odczytac przeslanego rozkazu.");
} else if(body == "zaktualizowane") {
llOwnerSay("Dane na serwerze zewnetrznym zostaly zaktualizowane pomyslnie.");
} else if(body == "dane-zle") {
llOwnerSay("Kolor, blask lub tekst zwrocone z serwera nie sa prawidlowe.");
} else if(body == "") {
llOwnerSay("Serwer przeslal pusta odpowiedz.");
} else {
llOwnerSay("Odpowiedz z serwera otrzymana ("+body+").");
if(trybPracy == "odczytaj") {
bodyLista = llParseString2List(body, [";"], []);
kolor = llList2Vector(bodyLista, 0);
blask = llList2Float(bodyLista, 1);
tekst = llList2String(bodyLista, 2);
llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_GLOW, ALL_SIDES, blask, PRIM_TEXT, tekst, kolor, 1.0, PRIM_COLOR, ALL_SIDES, kolor, 1.0]);
} else {
llSetTimerEvent(300);
}
}
}
}
}


I ponownie rozkładając na czynniki pierwsze:

Cytuj:
string host_url = "http://moj-host.pl/plik_ze_skryptem.php";
string klucz_walidacyjny = "1a2b3c4";
list bodyLista;
key queryPage;
string trybPracy;


Doklaracja podstawowych wartości i określenie typów danych skryptu - bez tego nie będzie w stanie działać. Tutaj znaleźć też można klucz walidacyjny i adres do pliku skryptu PHP znajdującego się na zewnętrznym serwerze.
Cytuj:
vector kolor = <1, 1, 1>;
float blask = 0;
string tekst = "";


Wstępna deklaracja wartości dla koloru, blasku i tekstu.

Cytuj:
default {

...

}


Stan podstawowy. W nim znajduje się cały skrypt.

Cytuj:
state_entry() {
llSetTimerEvent(300);
llListen(123, "", llGetOwner(), "");
}


Gdy skrypt zostanie załadowany do pamięci zostanie włączone wykonywanie polecenia timer() { ... } co 300 sekund (co 5 minut) oraz rozpocznie się nasłuch dowolnego ciągu tekstu na kanale 123 dla osoby, która jest właścicielem obiektu (funkcja llGetOwner() odpowiada za pobranie odpowiedniego klucza awatara).

Cytuj:
on_rez(integer start_param) {
llResetScript();
}


Po zrezzowaniu obiektu skrypt dla danej konkretnej kostki zostanie zrestartowany.

Cytuj:
timer() {
trybPracy = "odczytaj";
queryPage = llHTTPRequest(host_url, [HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/x-www-form-urlencoded"], "klucz_walidacyjny="+klucz_walidacyjny+"&akcja=odczytaj");
}


Polecenie timer(), wykonywane co 5 minut. Zmienna trybPracy zostaje ustawiona na 'odczytaj', a funkcja llHTTPRequest() dostaje polecenie wysłania w adresie pliku (post) zmiennej 'klucz_walidacyjny' o wartości zmiennej pliku LSL 'klucz_walidacyjny' oraz zmiennej 'akcja' o wartości 'odczytaj'. Co spowoduje kontakt z serwerem zewnętrznym, z plikiem PHP. W tym miejscu nie jest jeszcze sprawdzane, co serwer odpowiedział.

Cytuj:
listen(integer channel, string name, key id, string message) {
if(channel == 123 && id == llGetOwner()) {
llSetTimerEvent(0);
trybPracy = "ustaw";
bodyLista = llParseString2List(message, [";"], []);
kolor = llList2Vector(bodyLista, 0);
blask = llList2Float(bodyLista, 1);
tekst = llList2String(bodyLista, 2);
queryPage = llHTTPRequest(host_url, [HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/x-www-form-urlencoded"], "klucz_walidacyjny="+klucz_walidacyjny+"&kolor="+(string)kolor+"&blask="+(string)blask+"&tekst="+tekst+"&akcja=ustaw");
}
}


To polecenie odpowiada za nasłuch zainicjowany funkcją llListen(). Wprowadziłam tutaj dodatkowe sprawdzenie, czy kanał to na pewno 123 i id awatara to na pewno ten właściciela. W zasadzie nie jest to potrzebne, ale czasem lepiej dmuchać na zimne. Tak czy inaczej jeśli te dwa założenia są prawdziwe, to następuje wstrzymanie odliczania w poleceniu timer() (a nuż akurat się wykona w tym samym momencie - i skrypt może przyciąć), zmiana zmiennej trybPracy na 'ustaw', pocięcie wiadomości na kawałki, po średnikach, za pomocą funkcji llParseString2List() (KonwertujCiągNaListę). Następnie zmienna koloru, blasku i tekstu zostają odpowiednio przyporządkowane do tych pociętych kawałków (0 to pierwsza pozycja w liście, 1 to druga, 2 to trzecia). Na koniec zostaje wywołane polecenie llHTTPRequest() z odpowiednio przygotowanym adresem, który zrozumie skrypt PHP na serwerze. Akcja to 'ustaw'.

Cytuj:
touch_start(integer number) {
if(llDetectedKey(0) == llGetOwner()) {
llOwnerSay("Wektor koloru (<0-1, 0-1, 0-1>); Blask (0-1) ; Tekst (do 255 znakow, BEZ SREDNIKOW). Na kanale 123. Na przyklad:");
llOwnerSay("/123 <0.5, 1.0, 0.75>;0.1;Testowy tekst.");
}
}


Jeśli osoba klikająca na kostkę ( llDetectedKey(0) ) to właściciel ( llGetOwner() ), wtedy zostaje mu pokazany tekst taki, jak powyżej. Krótka instrukcja tego, jak powinien konstruować to, co ma powiedzieć na kanale 123, aby skrypt nie wywołał błędu.

Cytuj:
http_response(key request_id, integer status, list metadata, string body) {
if(request_id == queryPage) {

...

}
}


Polecenie http_response odpowiada za odbieranie tego, co zostaje zwrócone skryptowi przez stronę internetową po wysłaniu danych poprzez funkcję llHTTPRequest().

Cytuj:
bodyLista = llParseString2List(body, ["---tutaj-zacznij---", "---tutaj-zakoncz---"], []);
body = llList2String(bodyLista, 1);


Odpowiedź zostaje pocięta na kawałki po ciągach znaków '---tutaj-zacznij---' oraz '---tutaj-zakoncz---', wobec czego lista utworzona w ten sposób ma postać:

0 - Wszystko, co przed tutaj-zacznij, np. jakieś reklamy
1 - Dane, które nas interesują
2 - Wszystko, co po tutaj-zakoncz

A zatem za pomocą llList2String() wybieramy wartość 1.

Cytuj:
if(body == "xxx") {
...
} else if(body == "xxx") {
...
} else if(body == "xxx") {


Tutaj jest sprawdzana obecność tych wszystkich różnych kodów błędu, jakie może zwrócić skrypt w PHP.

Cytuj:
} else {
llOwnerSay("Odpowiedz z serwera otrzymana ("+body+").");
if(trybPracy == "odczytaj") {
bodyLista = llParseString2List(body, [";"], []);
kolor = llList2Vector(bodyLista, 0);
blask = llList2Float(bodyLista, 1);
tekst = llList2String(bodyLista, 2);
llSetLinkPrimitiveParamsFast(LINK_THIS, [PRIM_GLOW, ALL_SIDES, blask, PRIM_TEXT, tekst, kolor, 1.0, PRIM_COLOR, ALL_SIDES, kolor, 1.0]);
} else {
llSetTimerEvent(300);
}
}


Jeśli żaden z kodów błędu nie jest obecny w odpowiedzi serwera - skrypt wyświetla co przesłał serwer. Jeśli tryb pracy to 'odczytaj', to potnij odpowiedź na kawałki po średniku, ustaw zmienne koloru, blasku i tekstu na odpowiednie wartości, a następnie za pomocą llSetLinkPrimitiveParamsFast() ustaw to wszystko tak, aby było ładnie. :P Jeśli tryb jest inny, niż 'odczytaj' (a więc 'ustaw'), to nie rób nic z odpowiedzią i uruchom timer() wstrzymany wcześniej w poleceniu listen { ... }.

---------------------------

Mam nadzieję, że w powyższym miniporadniczku nie ma błędów... a gdyby jakieś były, to poprawię, o. Pisane na sucho, bez testowania.

_________________
Pantera Północy: https://my.secondlife.com/panterapolnocy
Don't walk in front of me - I may not follow. Don't walk behind me - I may not lead. Walk beside me and be my friend.
http://pantera-polnocy.deviantart.com
http://www.firestormviewer.org


Profil E-mail GGOffline

Wyświetl posty z poprzednich:  Sortuj według  

Odpowiedz



Kto jest na forum

Użytkownicy przeglądający to forum: Brak zarejestrowanych użytkowników oraz 4 gości

Panel
Góra
Skocz do:  
SecondLife.pl designed by CvX! Powered by phpBB © phpBB Group - tłumaczenie
SecondLife.pl nie jest oficjalną stroną Second Life. SecondLife.pl is an unofficial Second Life website. SecondLife, SL logo and Second Life related graphics are trademarks of Linden Lab.

Entropia Universe , Planet Calypso