Wprowadzenie:
Ten poradnik jest przeznaczony dla skrypterów, którzy mają przynajmniej podstawowe doświadczenie w programowaniu proceduralnym w innych językach. Jeśli nigdy wcześniej nie pisałeś żadnych skryptów lub programów, sugeruję abyś zaczął od nauki języka BASIC lub Pascal na zajęciach w szkole, lub zastanowił się nadkupnem prostej książki o programowaniu w lokalnej księgarni. Zdobyta w ten sposób wiedza i umiejętności pozwolą Ci dużo szybciej uczyć się pisać skrypty w języku eScript. Z drugiej strony, może się zdarzyć, że pewne rzeczy przeze mnie napisane mogą się wydawać trywialne. Jeżeli tak, to przepraszam, jednak starałem się napisać poradnik tak, aby był zrozumiały dla wszystkich.
Rozdział 2: Krótki przegląd struktur danych
Rozdział 6: Wbudowane właściwości i POL Object Reference
Rozdział 7: Wiele hałasu o CProps i głupie nazwy dla rozdziałów
Rozdział 8: Użycie i dostęp do plików konfiguracyjnych
Rozdział 11: Zaawansowane typy danych i funkcje
Dodatek X: Powtórka z historii
Deklaracja i przypisanie zmiennych
Zmienne w eScript nie mają stałych typów, ale można je rzutować (konwertować) na kilka sposobów: integer (liczby całkowite), real (float; liczby z rozwinięciem dziesiętnym), i string (tekst). W prosty sposób można zadeklarować nową zmienną:
var my_variable;
Dostęp do zmiennej zależy od tego, gdzie zadeklarujesz zmienną w kodzie. Nazywa się to zasięgiem zmiennych. Jeżeli zadeklarujesz zmienną poza blokiem instrukcji lub ciałem funkcji, zmienna ta będzie widoczna dla wszystkich funkcji w programie (zmienna globalna). Zmienna zadeklarowana w ciele funkcji jest dostępna w całym ciele funkcji. W blokach instrukcji (takich jak If - EndIf, For - EndFor i podobne) deklaracja zmiennej ma ważność tylko do końca danego bloku. Zauważ, że w starszych stryptach możesz spotkać zmienne zadeklarowane przedrostkiem local i global. Te słowa kluczone odpowiadają zasięgowi zmiennych. Pierwsze znaczy to samo co var wewnątrz funkcji, a drugie to co var na zewnątrz (poza całem jakiejkolwiek funkcji czy bloku instrukcji). Jednakże do deklaracji zmiennych poleca się uzywanie var zamiast tych przestarzałych słów kluczownych.
Dobrze, teraz mając kilka zmiennych, możemy przypisać im pewne wartości by wykożystać je w skrypcie.
my_variable := 10;
Operator := (dwukropek + znak równości) przypisuje wartość z prawej strony do zmiennej po lewej stronie. Prawą stronę może stanowić liczba, inna zmienna, lub pełne wyrażenie (różne działania, funkcje). Możesz to zrobić już w trakcie deklaracji:
var number_of_pies := 15;
Aby zadeklarować stałą lub zmienną tylko do odczytu, musisz to zrobić na samym początku pliku przed wszystkimi funkcjami, tam gdzie deklarujesz zmienne globalne (stałe także mają zasięg globalny). Gdy kompilator zapotka stałą w wyrażeniu, jej nazwę automatycznie zastąpi wartością. Deklaracja stałej wygląda tak:
const MY_CONSTANT := 47;
Użycie stałych jest kluczem pisania poprawnego i czytelnego kodu, zapobiegają niejasnościom wynikającym z uzywania surowych liczb, nadając im znaczące nazwy. Dobrym zwyczajem jest dawanie stałym nazw pisanych wielkimi literami dla odróżnienia od zwykłych zmiennych, tak jak to czyni większość programistów
Przykładowy wycinek kodu pokazuje sposób przypisania zmiennej pewnej wartości za pomocą stałej:
const SIZE_OF_TRAY := 15;
var pies_in_tray := SIZE_OF_TRAY;
Ta częsć jest prosta, wyrażenie po lewej stronie operatora przyrównania jest po prostu obliczana, a wynik jest zapisywany do zmiennej po lewej stronie operatora. Lista matematycznych operatorów w eScript:
| Operator | Znaczenie |
| + | Dodawanie |
| - | Odejmowanie |
| * | Mnożenie |
| / | Dzielenie |
| % | Modulo |
| >> | Przedunięcie bitowe w prawo |
| << | Przesunięcie bitowe w lewo |
Wyrażenia obliczane są najpierw w nawiasach potem od lewej do prawej:
var pies_used := pies_used + 1; // Inkrementacja 'pies_used' o 1
var pies_left := number_of_trays * (pies_in_tray – pies_used);
Trzeba wspomnieć co nieco o typach zmiennych w arytmetyce, zwłaszcza przy dzieleniu. Jeżeli dzielisz przez siebie dwie liczby całkowite typu integer, podczas którego otrzymujesz część ułamkową, POL zwróci tylko część całkowitą. Aby mieć pewność że wynik jest rzeczywisty (real - z częścią ułamkową), upewnij się aby przynajmniej jeden z operandów był typu rzeczywistego. Możesz to zrobić wpisując liczby z rozwinięciem dziesiętnym (np. 100.0), lub "rzutując" je do typu rzeczywistego (konwertując na inny typ). Jest kilka funkcji rzytowania w eScript: CInt(), CStr(), CDbl().
| CInt(100.3) | // Wynik całkowity 100 |
| CStr(100); | // Wynik tekstowy "100" // W takiej postaci nie może być użyty dalej do operacji matematycznych |
| CDbl(100); | // Wynik rzeczywisty 100.0 |
Dodatkowe funkcje rzutowania z biblioteki basic.em
| Hex(100); | // Wynik tekstowy w formacie hexodecymalnym "0x64" |
| Bin(100); | // Wynik tekstowy w formacie binarnym "1100100" // CInt() nie przekszatałci tej funkcji do postaci decymalnej poprawnie // (potraktuje ją jako dużą liczbę dziesiętną a nie binarną. |
| CAsc("A"); | // Wynik całkowiny 65 (numer odpowiadający znakowi w zestawie ASCII) |
| CChr(65); | // Wynik tekstowy "A" (odwrotnie do funkcji CAsc()) |
Komentowanie kodu jest jedną z najważniejszych rzeczy. Komentarze pozwalają na dodanie dowolnego tekstu w celu wytłumaczenia intencji i środków programisty, oraz pewnych zawiłości kodu, bez zakłucania jego działania i obniżania funkcjonalności. Komentarze są również przydatne przy wyłączaniu części kodu bez trwałego ich usuwania. W eScript jest kilka sposobów pisania komentarzy:
Komentarz liniowy: Tekst po // jest traktowany jako komentarz do końca aktualnej linii. Następna linia nie jest już komentarzem (aby kontynuować komentarz należy znowu wpisać // ). To jest najlepszy sposób pisania krótkich komentarzy.
Komentarz blokowy: Tekst pomiędzy /* a */ również jest traktowany jako komentarz, jednak tekst pomiędzy może obejmować wiele linii, jak również można tak wyłączyć cały blok instrukcji. Komentarz kończy się gdy kompilator napotka pierwsze wystąpienie */. Dlatego nie można zagnieżdżać komentarzy blokowych, gdyż każde kolejne wystąpienie */ bez rozpoczęcia nowego komentarza będzie traktowane jako błąd. Dlatego też poleca się stosowanie komentarzy liniowych.
Krótki przegląd struktur danych
Dobrze, teraz umiemy tworzyć zmienne całkowite, rzeczywiste oraz tekstowe. Jednak do pisania jakichkolwiek przydatnych skryptów potrzebne nam będą pewne struktury danych. Mowa tu o tablicach, strukturach i słownikach.
Tablice są jeden-based kolekcją obiektów o swobodnym dostępie. Inaczej mówiąc, możesz dostać się w każde miejsce tablicy od pierwszego (z indeksem 1) do ostatniego (z indeksem X). Popatrzmy najpierm jak zadeklarować tablice.
Aby stworzyć zmienną tablicową, zadeklaruj:
|
|
|
Tablica może być przypisana do każdej innej zmiennej, nawet gdy ta zmienna nie została zadeklarowana jako tablica:
var a := array{2, 4, 6, 8};
var b;
b := a;
Podobnie, jeśli jakaś funkcjia zwraca tablice (będziemy jeszcze mówić o tym później), nie jest potrzebna żadna specjalna deklaracja:
var a;
a := FunctionThatReturnsAnArray();
W wielu innych językach programowania, nie ma możliwości zapisu lub odczytu z komórek o indeksach wychodzących poza zadeklarowany rozmiar tablicy. W eScript, tablice rosną automatycznie bez żadnych błędów dostępu:
var a := array;
a[1] := 4;
a[4] := 7;
Komórki tablic mogą być zmiennymi dowolnego typu włączając to inne tablice:
var a := array{};
var b := array{};
a[1] := 5;
b[1] := a;
b[2] := 6;
Poniżej zamieściłem kilka równoważnych metod przejścia po wszystkich elementach tablicy. Konstrukcja foreach jest dużo wydajniejsza jak i praktyczniejsza. Więcej o pętlach i innych konstrukcjach w następnym rozdziale.
var a := array{2,4,6,8};
var i;
for(i:=1; i<=a.Size(); i:=i+1)
Print(a[i]);
endfor
foreach i in ( a )
Print(i);
endforeach
Możesz uzywać tablic gdziekolwiek ich potrzebujesz, jak na przykład przesyłanie wielu zmiennych spowrotem do funkcji (funkcje omówimy później), lub kiedy funkcja jądra tego wymaga.
Jest również kilka specjalnych funkcji operujących na tablicach, które omówimy później (jak na przykład array.Insert()).
Struktury to obiektowo zorientowane typy danych ze zmiennymi elementami lub właściwościami, zawierającymi dane. Aby odnieść się do elementu struktury, należy użyć operatora dostępu . (kropka):
Print(a.height);
Print(a.width);
Aby stworzyć zmienną strukturalną, zadeklaruj:
|
|
|
Aby dodać nowy element do struktury, należy użyć operatora dodawania + :
var a:= struct;
a.+height := 7;
a.+width;
// Przypisz wartość do istniejącego elementu
a.width := 5;
Słowniki to tablice, których elementy mają nazwy (klucze). Często są nazywane tablicami asocjacyjnymi.
Print(a["height"]);
Print(a["width"]);
Aby stworzyć taką strukturę danych, zadeklaruj:
|
|
|
Jeżeli odniesiemy się do nieistniejącego klucza w słowniku, zostanie on automatycznie utworzony.
var a:= dictionary;
a["height"] := 7;
a["width"] := 100;
Warunki i pętle
W tym rozdziale zajmiemy się dwoma tematami: konstrukcjami warunkowymi IF oraz pętlami.
Konstrukcje If są absolutnie potrzebne w każdym skrypcie. Pozwalają jeden podejmować decyzje w skryptach na bazie pewnych przyjętych cryteriów. Podstawowa składnia wygląda tak:
if(warunek)
// kod
elseif(warunek)
// kod
elseif(warunek)
// kod
else
// kod
endif
Zauważ że możesz użyć dowolną ilość razy (w tym wogóle) konstrukcję elseif pomiędzy if a opcjonalnym else .
Kluczową częścią skłądni powyżej jest "warunek". Gdy skrypt będzie wykonywany, najpierw zostanie wykonany kod "warunku" i obliczona jego wartość, aby sprawdzić czy wynosi "prawdę" czy "fałsz".
Jeżeli wyrażenie nie będzie "prawdziwe", instrukcje zostaną pominięte aż do następnego "warunku", jeżeli zostanie znaleziony taki, którego wartość wynosi "prawda", lub do instrukcji po else (jeśli znajdzie taką konstrukcję).
Składnia "warunku":
zmienna1 {operator zmienna2} ...
Kilka przykładów wyrażeń o wartości "prawda":
var var1 := 10;
var var2 := 5;
if (var1 > var2) //TRUE
if ((var1 – 5) == var2) //TRUE
if (var2 < var1) //TRUE
if ((var2+5) >= var2) //TRUE
if (var1 != var2) //TRUE
Lista operatorów porównania w eScript (evaluated left-to-right):
| Operator | Znaczenie |
| == | Jest równe |
| != | Jest nierówne |
| > | Jest większe |
| < | Jest mniejsze |
| >= | Jest większe lub równe |
| <= | Jest mniejsze lub równe |
Również możesz używać operatorów logicznych:
| Operator | Znaczenie |
&& ( and ) | Koniukcja (ilocznyn logiczny) |
|| ( or ) | Alternatywa (suma logiczna) |
! ( not ) | Negacja (odwrotność logiczna) |
Przykład:
if ( (var1 > var2) || (var1 == 5) )
if ( (var1 > var2) or (var1 == 5) )
//PRAWDA jeżeli przynajmniej jeden z warunków jest spełniony.
if ( (var1 > var2) && (var1 == 5) )
if ( (var1 > var2) and (var1 == 5) )
//PRAWDA, jeżeli oba warunki są spełnione
Konstrukcje If będziesz uzywać we wszystkich prostszych skryptach. Musisz zastanowić się co chcesz zrobić, zanim połączysz warunki, inaczej możesz się łatwo pogubić:
if ( score < 60 )
Print("Ty got less than 60");
elseif ( score == 50 )
Print("Ty got 50!");
endif
W tym skrypcie linia elseif(score == 50) nigdy nie będzie wykonana, nawet jeżeli jest to bardziej precyzyjnie określony warunek, ponieważ, jeśli score jest równe 50, pierwszy warunek będzie spełniony.
W eScript wszystkie wyrażenia, których wartość jest równa 0 są uważane za "fałsz", a wszystkie inne za "prawdę". Możesz więc uzywać konstrukcji If bez żadnych operatorów porównawczych:
var valid := 1;
if( valid )
// kod
else
// kod
endif
Albo:
if ( !valid ) // FAŁSZ jeżeli valid jest różne od zera
// kod
endif
Często się zdarza w pisaniu skryptów, ze musisz wykonać ten sam kod dla wielu możliwych warunków. Dokonywanie tego typu wyboru za pomocą konstrukcji If - ElseIf byłoby całkiem nieczytelne jak i mało praktyczne. Dlatego w eScript wprowadzono specjalną konstrukcję:
// Dekladacja:
const BLUE := 1;
const YELLOW := 2;
const RED := 3;
const MAUVE := 4;
// Sposób 1 - konstrukcja if-elseif:
function FunctionOne()
var answer := WhatIsYourFavoriteColor();
if ( answer == BLUE )
// do stuff
elseif ( answer == YELLOW )
// do stuff
elseif ( answer == RED )
// do stuff
elseif ( answer == MAUVE )
// do stuff
else
// do something else
endfunction
// Sposób 2 - konstrukcja case:
function FunctionTwo()
var answer := WhatIsYourFavoriteColor();
case ( answer )
BLUE:
//do stuff
YELLOW:
//do stuff
RED:
//do stuff
MAUVE:
//do stuff
default:
//do something else
endcase
endfunction
Podstawowa skłania konstrukcji case:
case ( wyrażenie )
wartość_porównywana:
//kod
break;
wartość_porównywana:
//kod
break;
...
default:
//gdy powyższe nie pasują, kod domyślny (opcjonalne)
break;
endcase
Wyrażenie może być obliczane w trakcie wykonywania skryptu, ale wartość_porównywana nie. To oznacza, że kontrukcja case jest przydatna tylko w momencie porównywania wielu stałych, przewidzianych wartości. Wartość_porównywana nie może być żadnym typem wyrażenia, które musi być obliczane w trakcie wykonywania konstrukcji, jak na przykład matematycznych obliczeń, musi być również precyzyjna (nie dopuszcza się żadnych operatorów porównania jak większy czy mniejszy). Wyrażenie break; jest opcjonalne, z jednym wyjątkiem gdy poza nim nie ma żadnych innych instrukcji dla danego zdarzenia. Jeżeli natomiast po jednej wartość_porównywana pojawi się następna, wtedy dla obu z nich zostanie wykonany ten sam kod (następujący po ostatniej wartości z rzędu).
Przykład:
function FunctionThree()
var answer := WhatIsYourFavoriteColor();
case ( answer )
BLUE:
YELLOW:
//kod
RED:
//kod
default:
//kod
endcase
endfunction
Dla zdarzenia BLUE zostanie wykonany ten sam fragment kodu, który następuje po YELLOW.Dla RED zostanie wykonany odzielny kod, a dla czegokolwiek innego jeszcze inny fragment kodu. Jeśli nie chcesz aby po zdarzeniu BLUE był wykonywany jakikolwiek kod, musisz po nim umieścić instrukcję break;. Instrukcja break; powoduje natychmiastowy przeskok do instrukcji zaraz po wyjściu z danego bloku instrukcji (w tym wypadku end_case).
Zachowanie skryptu podczas zdarzenia z pustym blokiem instrukcji (jak w znarzeniu BLUE) to tak zwane fall through.
W poprzednim rozdziale wspomniałem o przejściu po wszystkich elementach tablicy. Jest to bardzo często używana konstrukcja w większości skryptów. Są na to dwie popularne metody. Jeszcze raz przydocze kod z wcześniejszego rozdziału:
var a := array{2,4,6,8};Oto co wypisze ten fragment kodu:
var i;
for ( i:=1; i<=a.Size(); i:=i+1 )
Print(a[i]);
endfor
2
4
6
8
Pętla for powinna być dobrze znana każdemu, kto programował kiedykolwiek przedtem. Jej podstawowa składnia wygląda tak:
for ( inicjalizacja ; wyrażenie ; inkrementacja)
// kod
endfor
Pomimo że możesz umieścić dowolny kod w inicjalizacji i inkrementacji, nazwałem je tak, ponieważ to jest ich najbardziej typowa funkcja. Zwykle pętla for jest count-checker. Przy każdym przejściu przez pętle, automatycznie wykonuje kod inkrementacji, i sprawdza czy warunek jest spełniony (pętla jest powtarzana) czy nie (pętla jest przerywana). W powyższym przykładzie zmienna i jest zadeklarowana. Przed pierwszym przejściem pętli, jest zainicjowana przez przypisanie jej wartości 1. Jeżeli i jest mniejsze lub równe długości tablicy a, wykonywany jest kod w ciele pętli. Pod koniec, wykonywany jest kod inkrementacji, do wartości i jest dodany 1 i nowa wartości zapisana w i. Wtedy znowu i jest porównywany z długością tablicy a (w tym przykładzie wynosi ona 4). i jest w dalszym ciągu mniejsze, więc cała pętla jest wykonywana ponownie, aż do momentu gdy i jest większe niż długość tablicy a, wtedy następuje skok do instrukcji umieszczonej zaraz po end_for.
Inną, praktyczniejszą metodą jest foreach :
foreach i in ( a )
Print(i);
endforeach
Tutaj nie musiy inicjalizować licznika i, gdyż pętla foreach (dla każdego) wykonuje to za nas. Ta pętla robi dokładnie do samo co pędla for w poprzednim przykładzie. ale w dużo bardziej zwięzły i efektywny sposób. Podstawowa składnia jest taka:
foreach zmienna in ( tablica )
{kod}
endforeach
Zmienna iteracyjna:
Gdy uzywasz konstrukcji foreach, zostaje utworzona zmienna _(var)_iter o zasięgu lokalnym, zawierająca informację w numerze aktualnie wykonywanej iteracji.
Ten kod wypisze:
var a := array{"A", 1000};
foreach i in ( a )
Print(_i_iter);
Print(i);
endforeach
1
A
2
1000
Ostatnim typem pętli for jest konstrukcja "starego typu" dla tej pętli (znana tym którzy programowali w języku Pascal).
for i:=0 to 3
Print(i);
endforeach
Skrypt wypisze:
0
1
2
3
Innym typem pętlni w eScript jest pętla while :
while ( warunek )
// kod
endwhile
Jest bardzo prosta w użyciu; jak długo spełniony jest warunek, tak długo kod w ciele pętli jest wykonywany. Prawdopodobnie będzież uzywać i zmieniać jedną lub więcej zmiennych w warunku, albo nigdy nie będziesz opuszczać pętli (są pewne wyjątki od regóły omówione poniżej).
Podobnia do konstrukcji while jest pętla reprat - until. Różnica polega na tym, że warunek jest sprawdzany na końcu pętli, a nie na początku, co daje gwarancję że ta pętla wykona się przynajmniej raz:
repeat
// kod
until ( warunek );
Możesz także użyć konstrukcji do - while tak aby warunek był sprawdzany pod koniec pętli, za pomocą instrukcji do :
do
// kod
dowhile ( warunek );
Czasam może zajść taka potrzeba, że będziesz chciał opuścić pętle w pewnym (dowolnym) momencie, bez względu na to czy warunek jest spełniony czy nie. Do natychmiastowego wyjścia z pętli (skoku do pierwszej instrukcji kodu po instrukcji kończącej pętle) służy instrukcja break. Ta instrukcja działa w każdym bloku instrukcji (a więc w konstrukcjach typu for, while, repeat - until, i innych). Również, jeżeli chcesz przerwać wykonywanie kodu pętli, ale pozostać w niej (wykonać skok do początku pętli, następnego jej przejścia i sprawdzania warunku), musisz użyć instrukcji continue.
while ( warunek != 0 )
if ( warunek == 42 )
//oh geez! Przerwij!
break;
elseif ( warunek == 13 )
//nie wykonuj kodu w ciele pętli
continue;
endif
warunek := warunek * coś;
endwhile
Jak dotąd, wszytskie fragmenty kodu, które pokazałem, nie mogłą się wykonać same z siebie. Cały kod, który napiszesz, musi być umieszczony w ciele funkcji. Jeśli pojęcie funkcji nie jest Ci znane, musisz wiedzieć, że są jeden jednymi z fundamentalnych konstrukcji programowania. W eScript są dwa rodzaje funkcji: function i program. Jeśli programowałeś wcześniej, możesz porównywać funkcję program z funkcją "main" aplikacji. Oznacza to, ze gdy scrypt uruchamiany, pierwszym wykonywanym blokiem instrukcji (punktem wejścia) jest pierwsza funkcja program. Podstawowa składnia dla obu funkcji jest taka:
program NazwaProgramu(argument, ...)
// kod
endprogram
function FunctionName(argument, ...)
// kod
endfunction
endprogram i endfunction słowa kluczowe identyfikują koniec funckcji. Są absolutnie potrzebne i Ty będziesz otrzymywać błędy kompilatora, jeśli nie będą obecne (więcej o kompilacji później).
Nazwą funkcji może być cokolwiek, tak długo jak będzie to jedno słowo zaczynające się od litery.
Lista argument to wartości przekazywane do funkcji w miejscu jej wywołania przez blok instrukcji wywołujący daną funkcję. Argument może być dowolną zmienną, także tablicą czy strukturą. Zauważ, że to co czyni funkcję program specjalną, to to że to nie Ty ją wywołujesz więc nie masz wpływu na to jakie argumenty zostaną jej przekazane. Zalezy to od skryptu, jaki w którym znajduje się funkcja (więcej o tym później). Na razie przyjmijmy, że funkcja prorgam nie otrzymuje żadnych argumentów. Każda funkcja może mieć jeden lub więcej argumentów. Oto przykład tworzenia funkcji:
program Main()
var var1 := "dudes";
var var2 := "oingo";
var var3 := "boingo";
var var4 := "let is";
var var5 := 42;
MyFunction(var1, var4); // 1
MyFunction(var2, var3); // 2
Myfunction(var5, var1); // 3
MyFunction(var3); // 4
MyFunction(); // 5
endprogram
function MyFunction(a := "Yo yo", b := "hey hey")
Print(a + " " + b);
endfunction
Oto co wypisze ten fragment kodu:
dudes let is
oingo boingo
42 dudes
boingo hey hey
Yo yo hey hey
Zauważ, że w deklaracji funkcji MyFunction mamy różne nazwy argumentów i przypisania do nich. Oznacza to, że jeżeli funkcja zostanie wywołana z mniejszą ilością parametrów lub wogóle bez nich, argumentom zostanie przypisana wartość domyślna, tak jak w przypadku czwartego i piątego wywołania funkcji MyFunction w ciele funkcji Main.
Jest to bardzo prosty przykład, i nie pokazuje dlaczego warto dzielić kod na kilka oddzielnych funkcji zamiast oprzeć skrypt na jednej wielkiej funkcji. Jest to po prostu dobry styl pisania programów i skryptów (tak samo jak umieszczanie częstych komentarzy), aby podzielić problem na mniejsze, łatwiejsze do ogarnięcia i zrozumienia zadania, które potem łatwiej prześledzić i w miare potrzeb zmienić.
Funkcje mogą zwracać jedną wartość do funkcji wywołującej zanim zakończą działanie. W tym celu należy użyć instrukcji return. Funkcja wywołująca odbiera tę wartość za pomocą zmiennej po lewej stronie operatora przypisania, a po prawej funkcję zwracającą wartość. W praktyce można powiedzieć, że po wykonaniu funkcji, wartość zwracana jest "podstawiana" za nazwę funkcji w kodzie wywołującym tą funkcję, a więc można użyć tego mechanizmu również w wyrażeniach i warunkach pętli:
program Main()
var hejaz;
hejaz := SmellsLike();
Print(hejaz);
endprogram
function SmellsLike()
var smell := "Teen Spirit";
return smell;
endfunction
Wypisze na wyjściu: Teen Spirit
Aby zwrócić więcej niż jedną wartość, przypisz wartości do zmiennej zaawansowanego typu struktury danych (tablica, struktura, słownik) i zwróć tę zmienną.
program Main()
var hejaz;
hejaz := SmellsLike();
Print(hejaz[1]);
Print(hejaz[2]);
endprogram
function SmellsLike()
var smell := array{"teen", "spirit"};
return smell;
endfunction
Wypisze na wyjściu:
Teen
Spirit
Dołączanie kodu innych plików (include)
Do tej pory cały kod z którym ekperymentowaliśmy, mieścił się w jednym pliku. Aby zaimportować kod z innych plików (specjalnie do tego służą pliki zwane "plikami dołączanymi", które zazwyczaj posiadają rozszeżenie ".inc". Są specjalne, ponieważ nie posiadają funkcji program, a jedynie szereg zwykłych funkcji), musisz umieścić na początku skryptu (przez deklaracjami zmiennych czy funkcji) taką linijkę kodu:
include "Nazwa_Pliku_Bez_.inc";
Teraz możesz w swoim skrypcie (plik z rozszeżeniem ".scr") wywoływać każdą funkcję zdefiniowaną w dołączanym pliku. Zauważ, że każda zmienna globalna zadeklarowana w pliku dołączanym jest również widoczna w skrypcie (możesz traktować dołączanie plików jako wstawienie ich w miejsce instrukcji dołączania include). To oznacza, że musisz uważać na to co deklarujesz po dałączeniu pliku, ponieważ każda próba zadeklarowania zmiennej lub funkcji o tej samej nazwie co już istniejąca w pliku dołączanym zakończy się błędem kompilatora, a takie błędy są bardzo trudne do wyśledzenia.
Powyższa instrukcja include będzie działać tylko dla plików znajdujących się w tym samym katalogu co plik skryptu, do którego chcesz dołączyć plik. Jeśli chcesz dołączyć standardowe pliki dołączane (znajdujące się w katalogu /scripts/include), musisz użyć składni:
include "include/NazwaPliku";
Jeśli chcesz dołączyć plik będący w package którego chcesz uzyć:
include ":NazwaPkg:NazwaPliku";
RADUJMY SIĘ! Teraz część przeznaczona dla UO i POL: pliki *.em i ich funkcje
Cieszę się, że przebrnąłeś przez całą tę dyskusję o składni i nie przeskoczyłeś od razu do tej części. Jeśli tak zrobiłeś - powodzenia, ponieważ nie mamzamiaru powtarzać tego tematu.
No dobrze, teraz zaczniemy naukę pisania prawdziwych skryptów dla środowiska UO-POL. Do jej pory mogłeś pisać proste skrypty wypisujące coś na ekranie konsoli, ale prawdopodobnie nic więcej. Aby pisać naprawdę przydatne skrypty robiące cokolwiek, musisz miec możliwość dostępu do danych o świecie gry. POL zapewnia taki interfejs poprzez szereg "funkcji jądra" zdefiniowanych w plikach ".em", które znajdują się w podkatalogu "/scripts" katalogu głownego POLa. Zauważ, że pliki "*.em" są często nazywane "modułami".
Ale najpierw, musimy pogadać o typach skryptów w środowisku POL, kiedy są one wykonywane, oraz z jakimi parametrami. Tutaj zamieściłem krótką listę (Zauważ że nazwy parametrów są dowolne, te które użyłem, jak na przykład "klikający" są czysto przykładowe, a starałem się tak dobrać nazwy, aby były w miare opisowe. Jednynie kolejność tych parametrów jest istotna):
CharRef = odniesienie do postaci uruchamiającej skrypt
MobRef = Odniesienie do mobilnego obiektu (np. NPC, postaci)
ObjRef = Odniesienie do przedmiotu
Skrypt użycia: Uruchomienie poprzez dwuklik w oknie UO na przedmiocie, do którego jest przypisany skrypt.
Parametry: 1: CharRef klikający , 2: ObjRef kliknięty_przedmiot
Skrypt przejścia: Uruchomienie poprzez przejście postacią na przedmiocie, do którego jest przypisany skrypt.
Parametry: 1: CharRef przechodzący , 2: ObjRef nadepnięty_przedmiod
Skrypt komendy: Uruchomienie poprzez wpisanie komendy ".NazwaSkryptu" w oknie UO przez postać mającą stosowne przywileje.
Parametry: 1: CharRef postać , 2: Tekst po komendzie (argumenty), np. wpisanie ".komenda blabla" spowoduje przekazanie skryptowi argumentu tekstowego "blabla"
Skrypt czaru: Uruchomienie podczas wybrania przez postać czaru z księgi zaklęć
Parametry: 1: CharRef rzucający_zaklęcie
Skrypt kontroli: Uruchomienie przy tworzeniu przedmiotu, do którego został przypisany skrypt, oraz przy resetowaniu serwera.
Parametry: 1: ObjRef kontrolowany_przedmiot
Skrypt AI: Skrypty sztucznej inteligencji, kontrola zachowań NPC, skrypty te nie powinny się kończyć.
Parametry: 1: MobRef NPC
Skrypt zdolności: Uruchomienie gdy postać używa zdolności, do której przypisany jest skrypt.
Parametry: 1: CharRef postać_używająca_zdolności
Zacznijmy od skryptów komend, gdyż są one najłatwiejsze do uruchomienia. Komenda tekstowa jest uruchamiana wtedy, gdy postać w oknie UO wpisze komendę w postaci nazwy skryptu poprzedzonej kropką (.), i tylko wtedy gdy postać wywołująca komendę ma wystarczające uprawnienia ("command level"). Upewnij się że postać, na której testujesz skrypt ma uprawnienia "GM" (zazwyczaj poziom 4) lub większe. Będziemy umieszczać wszystkie skrypty w katalogu "/scripts/textcmd/gm".
Zacznijmy od prostego skryptu. Powiedzmy, że chcesz napisać skrypt wysyłający wiadomość do wszystkich graczy na serwerze. Oto kod skryptu "bcast.src":
/*1*/ use uo;
/*2*/
/*3*/ program MyBroadcast(speaker, text)
/*4*/
/*4*/ foreach character in ( EnumerateOnlineCharacters() )
/*5*/ SendSysmessage(character, text);
/*6*/ endforeach
/*7*/
/*8*/ endprogram
Linia 1: Instrukcja use uo powiadamia kompilator, że będziemy kożystać z funkcji z modułu "uo.em" (w tym wypadku EnumerateOnlineCharacters() oraz SendSysMessage).
Linia 3: Od tej linii zaczyna się skrypt komendy, POL automatycznie przekaże odnośnik do postaci wywołującej daną komendę oraz tekst wpisany po nazwie komendy. W tym przypadku są to zmienne speaker (postać) i text (tekst, argument do komendy).
Linia 4: Funkcja EnumerateOnlineCharacters() zwraca tablicę odnośników do wszystkich postaci na serwerze. Pętla foreach każdy kolejny element tej tablicy przypisuje do zmiennej character, oraz wykonuje kod w ciele pętli (następna linijka) dla każdego z nich.
Linia 5: Funkcja SendSysMessage() wysyła wiadomośc tekstową zmiennej text w lewy dolny róg okna UO postaci, do której odnościk znajduje się w zmiennej character.
Linia 6: Sygnał końca pętli foreach
Linia 8: Sygnał Końca funkcji.
Skrypt wysyła tekst do wszystkich postaci na serwerze, jednak teraz się może okazać że GMowi nie podoba się fakt, iż on też dostaje swoją własną wiadomość. Możesz łatwo zmienić skrypt tak, by to nie miało miejsca, poprzez sprawdzenie przed wywołaniem funkcji SendSysMessage() czy zmienna character nie odnosi się do tej samej postaci (nie jest taka sama) jak zmienna speaker (przechowująca odnośnik do postaci wywołującej komendę):
/*1*/ use uo;
/*2*/
/*3*/ program MyBroadcast(speaker, text)
/*4*/
/*5*/ foreach character in ( EnumerateOnlineCharacters() )
/*6*/ if ( character != speaker )
/*7*/ SendSysmessage(character, text);
/*8*/ endif
/*9*/ endforeach
/*10*/
/*11*/endprogram
Teraz wiadomość nie zostanie wysłana do osoby która ją wysłała
W porządku, teraz nadszedł czas na uruchomienie pierwszego skryptu. POL nie może uruchamiać skryptów w postaci kodu źródłowego (eScript), w jakim zostały napisane, skrypt musi zostać skompilowany do odpowiedniego formatu przed uruchomieniem w środowisku POLa.
Zauważ, że gdy zmienisz plik "*.inc", musisz przekompilować wszystkie pliki "*.src", do których został on dołączony, aby je uaktualnić.
Do kompilacji służy program "ecompile.exe" dołączony do instalacji POLa. Używa się go z linii poleceń powłoki systemu operacyjnego. Zakładam, że masz doświadczenie z używaniem komend powłoki w Twoim systemie operacyjnym.
Ja używam systemu Windows oraz jego linii poleceń.
Po pierwsze, zapisz kod powyżej do pliku "bcast.src" i umieść go w katalogu "pol/scripts/textcmd/gm". Teraz otwórz konsole powłoki (wiersz poleceń) i przejdź do katalogu POLa, do podkatalogu "/scripts". Wpisz "ecompile /?". W oknie konsoli powinna wyświetlić się lista opcji użycia kompilatora eScript.
Nam nie są potrzebne żadne z tych skomplikowanych flag, tylko podstawowe opcje kompilacji: "ecompile <NazwaPliku.src>"
Musimy jednakże powiedzieć kompilatorowi gdzie znajduje się plik, który chcemy skompilować, więc wpisz w linii poleceń:
ecompile.exe txtcmd/gm/bcast.src
Powinieneć zobaczyć coś takiego:
EScript Compiler v1.05
Copyright (C) 1994-2006 Eric N. Swanson
Compiling: D:\pol\scripts\textcmd\gm\bcast.src
Writing: D:\pol\scripts\textcmd\gm\bcast.ecl
Kiedy "ecompile" wypisze, że zapisał plik ".ecl" oznacza, że kompilacja się powiodła i skrypt może zostać uruchomiony.
Aby to zrobić, upewnij się że POL działa, i że jesteś zalogowany w UO na swoim serwerze jako postać o przywilejach GM (lub wyżej, jak Admin). Teraz wpisz w oknie UO .bcast <komunikat>. Jeśli wszystko zrobiłeś poprawnie, nie powinieneś nic zauważyć - ponieważ napisaliśmy w skrypcie warunek, aby nie wysyłał wiadomości do postaci, która tę wiadomość napisała. Więc dla testów, zaproś kilku znajomych na swój serwer i zapytaj ich czy dostali wiadomość. Jeśli tak, to gratulacje, właśnie napisałeś i uruchomiłeś swój pierwszy skrypt!
Wbudowane właściwości i POL Object Reference
Wszystkie informacje o grze w POL stanowią obiekty: postacie, konta, przedmioty, zwloki, NPCe (ang. Non-Player Character - postacie nie kontrolowane przez gracza), i inne.
POL uzywa hierarchi klas by pozwolić na dziedziczenie właściwości pomiędzy obiektami. Dla przykładu, w świecie UO, wszystkie obiekty mają swoje współrzędne x, y i z określające położenie. Dotyczy to zarówno przedmiotów leżących na ziemi jak i obiektów mobilnych (ang. mobiles - NPCe wraz z całą fauną świata UO). Możesz sprawdzić w Object Reference Chart (Dodatek A) (ten dodatek nie został dołączony w trakcie tłumaczenia, można go znaleźć pod adresem http://docs.polserver.com/pol096/objref.php - przyp. tłumacza), że obie klasy - Item Object i Mobile Object są potomkami klasy UObject. Zauważ, że w klasie UObject jest zdefiniowanych kilka właściwości, włączając w to x, y, z, serial (numer identyfikacyjny), objtype (typ obiektu), color, i inne. Te właściwości są dziedziczone przez wszystkie klasy, będące niżej w drzewie hierarchi klas, tym samym gwarantuje to ich występowanie ww wszystkich tych obiektach w świecie UO.
Niektóre klasy posiadają nawet metody (funkcje), jak w typowym obiektowo-zorientowanym projektowaniu, np. klasa drzwi posiada metodę door.Open() oraz door.Close(), które odpowiednio otwierają i zamykają drzwi. Zauważ, w jaki sposób należy się odnieść do tych metod, czy "funkcji wewnętrznych" - za pomocą operatora dostępu (kropki) .. W zależności od tego, nad jakim obiektem pracujesz, pewne właściwości czy metody mogę się zmieniać, czasami niektóre z nich mogą wogóle nie występować. Dla przykładu, obiekt "Door" (drzwi) nie będzie posiadał właściwości "quality" (jakość), jaką posiadają obiekty klasy "Equipment" (wyposarzenie), ale w obu z nich wystąpią koordynaty x,y i z. Zauważ, że niektóre właściwości mogą być tylko odczytane a przypisanie im jakiejkolwiek wartości spowoduje błąd, inne możesz odczytać i zmodyfikować. Jest to spowodowane bezpieczeństwem serwera. (Dla przykładu właściwościami tylko do odczytu są character.dead lub character.acct - ich zmiana, bez wykonania dodatkowego kodu spowodowała by natychmiast bałagan, który mógłby doprowadzić nawet do załamania się serwera - sięgnij do dokumentacji POLa po hierarchię obiektów po więcej informacji dotyczących wbudowanych właściwości i metod.)
Aby dowiedzieć się więcej, zobacz POL Object Reference Chart.
Wiele hałasu o CProps i głupie nazwy dla rozdziałów
CProps (ang. Custom Properties - zmienne właściwości) są tym, co czyli pisanie skryptów w eScript tak elastycznym. Obok wszystkiego co mówiliśmy w poprzednim rodziale o wbudowanych właściwościach, CProps pozwalają Ci przechowywać w obiektach dowolną ilość dowolnych informacji. Te informacje mogą zostać przypisane obiektowi przez dowolny skrypt i być odczytane przez dowolny (w tym inny) skrypt, któremu zostanie przekazany odnośnik do tego obiektu. Możesz przechowywać każdy typ danych - teksty, liczby całkowite, rzeczywiste, tablice, itp. Powinieneś już się domyślać, że możliwość przypisania i odczytywania dowolnych danych jest bardzo przydatna. Poniżej zamieszczam kilka przykładów skryptów, wykożystujących CProps jak i pokazujących kilka innych aspektów skryptowania.
Powiedzmy, że chcesz stworzyć przedmiot jednorazowego użytku, który pozwala graczowi przenieść się spowrotem do jego zwłok gdy zostanie wskrzeszony. Aby to osiągnąć, musimy zrobić kilka rzeczy: stworzyć opis konkretnego obiektu, któremu przypiszemy taką możliwość, napisać skrypt kontrolujący zachowanie tego przedmiotu, oraz napisać kod, który zapamięta koordynaty zwłok danego gracza w chwili śmierci. Po pierwsze, krótka lekcja tworzenia nowych przedmiotów:
Otwórz dowolny plik o nazwie "itemdesc.cfg". Plik ten zawiera definicje każdego obiektu, który coś robi w świecie. Musimy dodać nasz przedmiot do jednego z tych plików. Tym razem użyjemy pliku "/config/itemdesc.cfg". Oto definicja naszego obiektu oraz opis każdego parametru:
Item 0xABCD
{
| Name | ring_of_returning | ||
| Desc | Pierscien Powrotu | ||
| Graphic | 0x108A | ||
| Script | return_ring |
Item 0xABCD :
Rozpoczyna definicje nowego obiektu. Identyfikator przedmiotu może być dowolny, ale musi być unikalny. Zakres numerów przedmiotów własnych to 0x5000 do 0xFFFF.
Name ring_of_returning :
To jest wewnętrzna nazwa przedmiotu, możesz z niej skorzystać np. tworząc przedmiot używając komendy ".create ring_of_returning" zamiast ".create 0xABCD".
Desc Pierscien Powrotu
Jest to opis pokazujący się po najechaniu i kliknięciu myszką na przedmiocie.
Graphic 0x108A
Jest to identyfikator grafiki, jaka zostanie przypisana do przedmiotu. Znalazłem tę grafikę i jej ID zapomocą programu "InsideUO".
Script return_ring
Tutaj jest nazwa skryptu, który będzie odpowiadał za działanie przedmiotu (skrypt będzie uruchamiany poprzez dwuklik na przedmiocie).
Teraz przyjrzyjmy się funkcjom obsługującym CProp, które będziemy używać, znajdujących się w module "uo.em":
SetObjProperty(odnośnik_do_obiekt, nazwa_właściwości, wartość);
Zapisuje do danego obiektu CProp o dowolnej nazwie i przypisuje jej kontretną wartość.
GetObjProperty(odnośnik_do_obiektu, nazwa_właściwości);
Zwraca wartość CProp o danej nazwie z obiektu. Jeżeli taka CProp nie istnieje, zwraca błąd.
EraseObjProperty(odnośnik_do_obiektu, nazwa_właściwości);
Usuwa CProp o danej nazwie z konkretnego obiektu. Jeżeli taka CProp nie istnieje, zwraca błąd.
Teraz napiszmy kod, który zapisze pozycje postaci w grze podczas śmierci. Skrypt, który uruchamia się gdy postać ginie to "/scripts/misc/chrdeath.src/". Przyjżyjmy się mu trochę. Jest tam już cała masa kodu, do wykorzystania w innych celach. Na razie możesz go zignorować. Możemy zobaczyć, że parametry głównej funkcji wyglądają tak:
program ChrDeath(corpse, ghost)
Parametr corpse to odnośnik do zwłok postaci (przedmiotu). Przypomnij sobie, że każdy przedmiot to obiekt, który dziedziczy wszystkie właściwości z klasy UObject, włączając w to koordynaty x, y i z. Możemy mieć dostęp do tych właściwości za pomocą operatora dostępu (kropki). Nam zależy na tym aby zapamiętać koordynaty x,y i z zwłok do obiektu gracza by później skrypt pierścienia mógł je odczytać. Parametr ghost to odnośnik do obiektu zmarłej postaci (mobile). Nie jest istotne że to duch, dalej mozemy w normalny sposób mieć dostęp do danych zapisanych w tym obiekcie. W "chrdeath.src" musisz dopisać:
SetObjProperty(ghost, "x_corpse", corpse.x);
SetObjProperty(ghost, "y_corpse", corpse.y);
SetObjProperty(ghost, "z_corpse", corpse.z);
Teraz mamy już wszystko co potrzeba aby napisać skrypt Pierścienia Powrotu. Tworzymy plik "return_ring.src" w katalogu "/scripts/items/". Tutaj mamy listę co rzeczy które musi skrypt robić:
A oto kod:
use uo;
program ReturnRing(gracz, pierscien)
// pamiętaj, że ten skrypt jest skryptem użycia, i parametrami funkcji głównej
// są odnośnik do postaci i odnośnik do uzytego przedmiotu
var x,y,z;
x := GetObjProperty(postac, "x_corpse");
y := GetObjProperty(postac, "y_corpse");
z := GetObjProperty(postac, "z_corpse");
if ( (x == error) or (y == error) or (z == error) )
SendSysMessage(postac, "Nie można znaleźć Twych zwłok!");
return 0; // zakończenie pracy i wyjście ze skryptu
endif
MoveCharacterToLocation(postac, x, y, z);
EraseObjProperty(postac, "x_corpse");
EraseObjProperty(postac, "y_corpse");
EraseObjProperty(postac, "z_corpse");
DestroyItem(pierscien);
endprogram
Proste, mam rację? =) Teraz pozostało skompilować skrypty, odśmieżyć skrypt chrdeath, jeżeli Twój serwer już działał ( .unload chrdeath ), stworzyć nowy przedmiot i przetestować. Możesz się zastanawiać "Jeśli postać miałą pierścień ze sobą w momencie śmierci, zostanie on w jej zwłokach, więc nie pomoże jej do nich powrócić". Oczywiście masz rację, i jest kilka rzeczy jakie możesz zrobić, by tego uniknąć:
Dobrze jest wiedzieć, jakie POL przechowuje CPropy w obiektach. Gdy użyjesz komendy .props na obiekcie, np. na naszej nieszczęsnej postaci, zobaczysz coś w rodzaju: . Zwróć uwagę na 'i' przed wartością CPropa. To jest informacja jakiego typu jest dana wartość. Więcej o tym w następnym rozdziale.
Możesz również przechowywać informacje w kontach (częściej niż w postaciach należących do nich), ale służy do tego inna składnia. Inaczej niż w przypadku zwykłego obiektu SetObjProperty(konto, nazwa, wartość) , możesz użyć metody klasy Account do zapamiętania i odczytania CProp w kontach:
account.SetProp(nazwa, wartość);
account.GetProp(nazwa);
Użycie i dostęp do plików konfiguracyjnych
Pliki konfiguracyjne (z rozszerzeniem ".cfg") POLa zawierają statyczne dane, które mogą być odczytywane podczas działania serwera i mogą być zmieniane bez potrzeby ponownego uruchamiania całego serwera czy rekompilacji. Dlatego też są dobrym miejscem na przechowywanie danych, które się często zmieniają, a także dla łatwej zmiany pewnych parametrów dla skryptów, które można zmienić bez potrzeby rekompilowania danego skryptu. Są dwa rodzaje plików cfg w POLu: część jest używana przez samo jądro, inne są odczytywane przez skrypty. Pierwszymi się nie będziemy zajmować, są to m. in. plik konfiguracji serwera "pol.cfg", konfiguracji czarów "spells.cfg", ustawień zdolności "skills.cfg" itp. Są pliki używane zarówno przez jądro jak i przez skrypty, jak np. "itemdesc.cfg" i "npcdesc.cfg" ( by the core executable, and ones that are read in by scripts. The former we won't go into, but some examples include the system configuration, pol.cfg; spell configurations, spells.cfg; skill configurations, skills.cfg. There are some cfg files that are used both by the core and other scripts, such as itemdesc.cfg and npcdesc.cfg (both are read by their respective Create core functions, but other scripts can access them for additional data, as we will see).
Config files have several pieces to them which you should be familiar with. A configuration file consists of zero or more elements, each element has a type, a key, and zero or more properties. An example follows with the parts labeled:BowcraftData 0x13B2
{
Name
Bow
Material 16
Difficulty
30
PointValue 20
}
BowcraftData is the element type. It is not used by the system and only serves to give the scripter an idea about what the element is related to.
0x13B2 is the element key or simply key. This must be a string (or number) which is used to find the element of interest in a file with many elements like the one shown above.
Name Bow
Material 16
Difficulty 30
PointValue 20
These above, are all the
element's properties. This is where the data you are interested lives.
So the steps to find a specific piece of data is as follows (must use cfgfile.em):
As a quick example, let's suppose that element above is in a file called "bowery.cfg" (in the /config directory) and we want the value of the "Material" property of a normal bow.
use cfgfile;
function GetMaterial(item)
// assume that
'item' is an itemref to a normal bow item
var cfgfile, element,
propvalue;
cfgfile := ReadConfigFile("bowery");
element := FindConfigElem(cfgfile, item.objtype);
propvalue := GetConfigInt(element,
"material");
endfunction
What you put in a config file is up to you, but they are best suited for large amounts of data that differs depending on the element key. The key is often an objtype, or just sequentially numbered, or structured however you want.
For config files that are used by both the core and other scripts, there are properties that are expected to exist for both parts. For example, in an NPC description, the core expects the normal properties of an NPC, like stats, color, graphic, etc. You can also add other properties that are not read by the core, but perhaps are read by the AI script to direct it exactly how to behave (for example, to run away from players or to attack). There is a third option, you can add CProps to config files. Look at this example of an NPC template:
NpcTemplate shade
{
| Name | a shade | ||
| script | killpcs | ||
| ObjType | 0x1a | ||
| Color | 0 | ||
| . | |||
| . | |||
| . | |||
| lootgroup | 29 | ||
| Magicitemchance | 1 | ||
| provoke | 67 | ||
| CProp | Undead i1 |
Please note:
Number 4 is very important; if you define CProps in a config file like npcdesc.cfg or itemdesc.cfg, that CProp is automatically set on every instance of that item that is created. This brings up the age-old dilemma of "Space versus Time". CProps in config files are set on every one of those items, which takes up extra memory, but is faster to access. Reading the same data from a config file takes a little more time to do, but the data is stored in only one place instead of many. Using CProps in a file with those bowery entries, for example, would not do anything, since no item is created from those elements.
Packages
You'll notice that up until now we've been putting files in the 'standard' places, like /scripts/items for itemuse scripts, /config for config files, etc. That's considered poor practice as it makes upgrading very difficult. To try to help that problem, POL uses a package system where all the files to do a specific purpose can be placed in a directory named for that purpose. For example, if you wrote a system for a new skill, you could place all the files needed for that new skill in a package: all the script source files, the compiled source files, support config files, readme files, etc. In POL, there are two types of packages, the 'standard' packages which are enabled by default (in /pkg/std/), which are normal skill systems, spawner, spells, etc. Then there are the 'optional' packages which are off by default, but can be used if desired (in /pkg/opt/). Normally, enabling an optional package requires some instructions which are normally supplied with the package.
Each package must include a package descriptor file, pkg.cfg, which has the following format (note # denotes a comment):
# Example package definition file
Enabled 1
#
Enabled 0/1 Should this package be
enabled?
Name template
# Name of package,
should
match directory name
Version 1.3
# Version
v0.v1..vn Version number for this package
Requires
spawner 1.2
# Requires pkgname
{version}
# Other package(s) other than
this one are required
# in order to
function
# More than one of these can
occur.
# Format: requires package-name
{version)
Conflicts some-package
# Conflicts pkgname
# This package cannot co-exist with a specific
package.
# Note that version cannot be specified
###
Everything below this line is currently ignored, but are a very
### good idea
to include in your pkg.cfg for imformation to users
#
CoreRequired ver note no leading '0', which would indicate octal
CoreRequired
96
# Your name
Maintainer John Q. Public
# Your
email
Email johnq@public.com
In the previous examples that use the ReadConfigFile function, the addition of packages complicate things somewhat. How can you know if you want to read the itemdesc.cfg in a specific package, the standard one in /config, or all of the files combined into one (for easy searching) ? This is handled in the filename format you pass to the ReadConfigFile function. The formats are:
ReadConfigFile("cfgname")
- for a script not in
a package, looks for /config/[cfgname].cfg
- for a script
in a package, looks for [cfgname].cfg in the same package
ReadConfigFile(":*:cfgname")
- Reads in every config file that matches that name.
ReadConfigFile(":pkgname:cfgname")
- looks for [pkgdir]/[cfgname].cfg
ReadConfigFile("::cfgname")
- always looks in
pol/config/[cfgname].cfg
Note for the special files itemdesc.cfg, npcdesc.cfg, skills.cfg, and spells.cfg, the first of the three formats will return the "composite" config file that includes the concatenation of the contents of all the files in all enabled packages and the standard file.
There are a few other times this format is used, such as the start_script() and the UnloadConfigFile functions. This is why the syntax for the .unloadcfg command is ".unloadcfg :pkgname:cfgname". That brings up another point: config files are cached by the system and must be unloaded for any online change to be seen.
Debugging
Many books have been written on the subject of finding and fixing software errors and it would be wasteful to repeat their content. This chapter will show some POL/eScript specific ways of finding bugs in your scripts. Let's first look at compile-time scripts. Ecompile, the eScript compiler does well at giving you good hints to where your script's syntax errors are. I say hints because of one of the rules of programming: never trust the compiler's error messages. They are often very useful, but often can lead you astray if you take their messages as gospel. The error message will contain a line number around where the error is. It may be above, on or below that line. The rest of the error message is usually correct, if not sometimes somewhat vague. Here are some examples:
Don't know what to do with Unknown Token: (280,8,'elseif') in
SmartParser::parseToken
Error compiling statement at
D:\pd\pol\scripts\items\torch.src, Line 4
- this error was from a
missing semicolon in an if-block on line 5
Warning: Equals test result ignored. Did you mean := for
assign?
near: item.graphic = 0xa12;
File:
D:\pd\pol\scripts\items\torch.src, Line 5
- ecompile guesses right
here: we used an equals test when we wanted an assign statement
Warning! possible incorrect assignment.
Near: if(item.graphic :=
0x0f64)
- ecompile catches this too, normally you wouldn't want to do
an assignment in an if-condition. It is not an illegal statement, so ecompile
completes the compile but warns you about it.
Unhandled reserved word: 'endprogram'
Error compiling statement at
D:\pd\pol\scripts\items\torch.src, Line 7
Error in IF statement starting at
File: D:\pd\pol\scripts\items\torch.src, Line 4
- this error was we
forgot an 'endif' keyword. The compile ran into the 'endprogram' keyword before
'endif', which is a syntax error.
Token 'item' cannot follow token ')'
Error compiling statement at
D:\pd\pol\scripts\items\torch.src, Line 2
- this error was from an
unmatched open-parenthesis in an if statement (on line 4)
Error compiling statement at D:\pd\pol\scripts\items\torch.src, Line
2
Error detected in program body.
- this is a nasty one, because it
does not give you any idea what is wrong. I've only come across this error when
a variable is declared with the same name as a keyword, in this case, 'for'.
Run-Time errors are much harder to find. These are errors that are syntactically correct, but produce incorrect results. The compiler will not catch these, nor will it help you find them. Your best bet for finding these errors is to notice where and when the script behaves incorrectly and go to that portion of the source code and poke around. To nail down the specific problem, you have several ways to go about it:
1. print() variables that you think are in doubt every now and then. Narrow
down the exact spot where the bug is.
2. Many core functions return 'error'
if something went wrong (i.e. tried to create an NPC in an illegal location). If
you test the return value of these functions against 'error', you can catch them
(i.e. if(CreateNPCAtLocation == error)...)
3. Some functions also return an
'.errortext' member that gives you additional information. Such as:
var house := CreateMultiAtLocation(...);
if(house == error)
Print(house.errortext);
endif
4. If none of these work for you, you could build the script with debug turned on. What this does is POL will print each line of code as it executes to the console. This is not always helpful, because if many instances of that script are running, they'll all print to the console. Best bet with this method is to debug on a local server where a minimum of other scripts are running. To turn on debug mode, make sure you 'use os;' before you add the line
set_debug(1);
under the 'use' lines at the top of your function. Then, you need to tell ecompile to compile with debug on. Do this with the '-i' switch. I.e. > ecompile –i test.src .
Advanced Data Types and Functions
On the variable side, in this chapter we'll go into more detail on structs, describe dictionaries and error types, explain the concept of persistance, and define global properties. On the functions side, we'll describe pass by reference and use of the "parms" array to get past limitations on what can be passed.
By now, you're familiar with CProps- custom properties. You're an old hand at applying them to items, to people, heck, even to accounts. But, what if you don't want to apply it to anything at all? Is there a way to save information and let it just float in space, accessible from everywhere?
Yes, you can, and to do so you use global properties, otherwise known as GProps. You manipulate them in much the same way as you do CProps, except you don't need to tell it where to go. These are the three fundamental functions you'll need to use GProps:
GetGlobalProperty("PropertyName");
SetGlobalProperty("PropertyName",
"PropertyValue");
EraseGlobalProperty("PropertyName");
And as always, you can use variables to define the name or value of the Global Property.
var globname := "Prop1";
SetGlobalProperty(globname ,
"No");
The above code would set the value "No" to the global property "Prop1". You can also but more than just simple variables as the value, for both GProps and CProps. Here's an example:
var propname := "Property1";
var propvalue := array{0 , 1 , "two"};
SetGlobalProperty(propname, propvalue);
And viola, Property1 now has as its value an array containing 0, 1, and "two". Later, if you were to do the following:
var ourprop := GetGlobalProperty("Property1");
print
outprop[1];
the output would be "0". This also works for CProps.
So now we have an interesting question- if simple variables and arrays can both be stored in props, what else can? To answer that question, we'll take a look at the concept of persistance.
If a variable type can be stored in a prop, it is said to be Persistable. As
of POL090, there are no data types that cannot be persisted, so this section is
mostly so you'll know what was meant when you read old changes.txts and see
notices that certain things can now be persisted. In POL089, there was one type
of data that could not be persisted- the mysterious ERROR type. This was the
type you got if you tried to go something that should have returned a value but
failed- instead you got
I'll go into a little more detail on data types you've already seen, now.
The prefered way to initialize an empty array is to use:
var a := array;
This works the same as
var a := {};
used to.
Arrays have a couple of methods that go with them- functions you can use to manipulate them directly. They are:
array.Size() - This returns the number of elements in the
array.
array.Insert(index , value) - inserts a new element, value, at the
specified index.
array.Erase(index) - deletes the element with the
specified index.
array.Shrink(nelems) - erases all but the first nelems
elements in the array.
array.Append(value) - adds this element to the end
of the array.
array.Reverse() - reverses the order of the
array.
array.Sort() - sorts the array.
Here's an example:
1 var colours := array;
2 colours[1] :=
"green";
3 colours[2] := "blue";
4 var csize :=
colours.Size();
5 colours[5] := "shiny";
6 csize :=
colours.Size();
So, let's take this step by step. Line 1 defines the array. Lines 2 and 3 put the first elements into it. At line 4, the variables look like this:
|
|
Line 5 adds a new elements at position 5, which EScript handles just fine because it's cool like that. So what do the variables look like as of line 6?
|
|
Note that when it counts how many things are in the array, it includes the "empty" spaces! In other words, array.Size() could more accurately be said to return the last valid array index. I'll also point out that if you attempt to look at an array position off the end of the array (colours[10], for instance) it will also return <uninitialized object> on you.
Now, let's continue the previous example.
7 colours.Insert(3, "bronze");
8 csize := colours.Size();
What happens here? We're inserting the value "bronze" at position 3 in the array. Here's what the array looks like after that:
|
|
Note that an insert pushes the remaining elements down one slot, EVEN IF it is inserted into a position that was empty. If you don't want to push like that, don't use insert, just use an assignment: colours[3] := "bronze".
9 colours.Erase(3);
This will return the array to what it looked like before the insert.
10 colours.Erase(3);
This removes one of the empty slots in the array. Using the erase method pulls everything up one slot- just the opposite of insert. So after line 10, we have:
|
|
11 colours.shrink(3);
This will reduce the array to the first 3 elements- in this case,
|
|
12 colours.Append("brown");
13 colours.Reverse();
This will end up yielding:
|
|
14 colours.shrink(2);
15 colours.reverse();
|
|
Quite frankly, I'm not entirely sure how sort works. I'll provide two examples from my testing and let people experiment with it on their own.
a := array;
a[1] := 4;
a[2] := 7;
a[3] := "show";
a[4] :=
"tunes";
a.Sort();
printing out the array elements in order yielded:
|
|
However...
a := array;
a[1] := 4;
a[2] := 7;
a[3] := "show";
a[5] :=
"tunes";
a.Sort();
printing this out in order gave me:
|
|
Note also that an array element can be any data type- I can hold a number there, or a struct, or an error, or another array. Try to sort an array of arrays at your own risk.
A struct is similar to an array in that it contains a collection of values rather than just one. However, rather than an ordered list, structs are stored in, well, a structure. To create one, first you have to initialize it:
var a := struct;
This lets the compiler know that it is going to be of data type struct and treats it accordingly. The nice thing about structs is that they can be treated like a lot of the internal objects. For instance, a player has certain elements, like his position. These are accessed with the '.' - player.x, player.y, and player.z, for instance. Similarly, we can assign the struct elements that are accessed the same way. Here's an example:
|
|
|
|
|
// It's on the first floor. // We'll keep an array of the people on it, using // append and erase to keep track of them. // It's in the third shaft from the left. |
So, someone gets on and pushes 3.
|
|
|
Dictionaries are similar to both structs and arrays- they are, sort of, a bridge between the two types. You create a dictionary, unsurprisingly enough, with this syntax:
var thing := dictionary;
Then, you treat it like an array except, instead of just ordered numbers, a dictionary can contains words or numbers as its keys.
thing["green"] := "blue";
thing["number"] := 4;
thing[3] := array{1 ,
3};
Internally, structs are actually dictionaries- so, basically, these do the same thing:
var thing1 := struct;
thing.+first;
thing.first :=
"one";
var thing2 := dictionary;
thing2["first"] := "one";
In addition, it means that the dictionary methods work equally well on both. These methods are:
dictionary.Size() - returns the number of elements.
dictionary.Erase(key) - erases an element.
| dictionary.Insert(key , value) - | adds an element. Dictionaries are not really ordered, so it's not
entirely accurate to say the item is "inserted". This is the same as doing dictionary["key"] := value; |
Functions, for the most part, return one value if any. Frequently, we want to pass back more than one piece of data, we can return an array, or struct, or dictionary.
This also works the other way. The start_script os method only
takes two parameters- the name of the script, and one thing to pass it. What if
you want the script to take in multiple variables for information? You send it
an array, of course, and custom has it that this array ends up getting named
"parms".
Here's an example. Let's say you have a script, called testscript. It wants as parameters who called it as well as two items that the user has clicked on. If you wanted to call it from another script, you could do the following:
start_script("testscript" , array{who, target1, target2});
or,
var parms := array;
parms[1] := who;
parms[2] :=
target1;
parms[3] := target2;
start_script("testscript" , parms);
Then, in testscript.src:
program testscript(parms)
if ( parms[2] ) //
tests if it got passed an array
char :=
parms[1];
firsttarget :=
parms[2];
secondtarget :=
parms[3];
else
char
:= parms;
endif
// do
stuff
endprogram
The program knows how to react whether it was simply sent a scalar or if it was sent an array.
The final section of this chapter will cover passing variables to functions by reference rather than by value. What's the difference?
Normally, we pass variables by value. Which is to say, it sends the contents of the original variable to the new one (copy), and the new one doesn't care anymore about the old one. So, when we have:
var first := "one";
var second := 2;
FallFunction(first ,
second);
and...
function CallFunction (this, that)
print
this; // will print "one"
print
that; // will print "2"
this :=
"green";
endfunction
when all is said and done, the values of first and second do not change.
However, if we pass by REFERENCE, what we are actually sending to the function is the location in memory of the variable itself, rather than just sending along its contents. You do this by prefixing the variable name with "byref" in the function declaration. So for instance:
var a := 4;
var b := 6;
var c := 9;
Foo(a, b, c);
function
Foo(pa, byref pb
, pc)
pa :=
3;
pb := 5;
pc :=
8;
endfunction
After the call to function foo is completed, a and c are unchanged but the value of b has become 5, because pb is a reference to the variable b itself, not a copy of its contents.
You have to be careful with this, as it is easy to change a variable you didn't intend to, but it is very powerful. It is also more efficient than pass by value, because it does not have to make and store a copy of the variable's value, and then destroy it when the function is done.
| Tags | Syntax/Description | |
| ---- | ------------------ | |
| noclose | [NONE] | |
| gump can't be closed by clicking the right mousebutton. Selection via an exit enabled button must be made. | ||
| nomove | [NONE] | |
| The gump can't be moved around the screen. | ||
| nodispose | [NONE] | |
| The gump can't be closed by hitting ESC. If this setting isn't specified and the user hits ESC, then the dialog is closed but no message is sent to the server. The server never thinks the player is done with the gump. Therefore ALL gump dialogs should specify 'nodispose'. | ||
| tilepic | [X] [Y] [T] | |
| Displays a tile on the
gump. [X] -> X coord of the tile. [Y] -> Y coord of the tile. [T] -> Tile (see insideUO) | ||
| resizepic | [X1] [Y1] [G] [X2] [Y2] | |
| Defines the gump
layout background. [X1] -> Start X coord of the gump. [Y1] -> Start Y coord of the gump. [G] -> Graphics to be used as gump background. [Y1] -> End X coord of the gump. [Y2] -> End Y coord of the gump. Notes: Be careful, not all gumps 'stretch' and have to be used with the correct sizing. 5100 = stretchable gray background gump. | ||
| page | [N] | |
| [N] ->
Pagenumber. Notes: Defines the page layout. Page 0 affects all following pages (items will not be removed when moving to other pages), i.e. the 'background' that is always displayed. page 1 is what will be displayed when the gump first opens (on top of what is defined in page 0). | ||
| button | [X] [Y] [G1] [G2] [E] [P] [R] | |
| X -> X coord of the
button. Y -> Y coord of the button. G1 -> Normal graphics. G2 -> Clicked graphics. E -> Exit? (10) P -> Pagenumber (jump to page) R -> Returnvalue. [EXIT] = button will make a menuexit and return a value. (01) [PAGENUMBER] = button will change to page #. (#) [RETURNVALUE] = Returnvalue of button. (See EXIT) (#) if you have radio buttons on the screen with return values, those return values will be used instead. | ||
| radio | [X],[Y],[GUMPGFX],[CLICKGUMPGFX],[PREVALUE],[RETURNVALUE] | |
| Only one radio button
can be selected on a page. RETURNVALUE
will be returned when the gump exits. (See also BUTTON). | ||
| tilepic | [X],[Y],[TILENUMBER] | |
| Display the selected tile (normal item art). | ||
| text | [X],[Y],[COLORCODE],[STRINGNUMBER] | |
| Display
text The string numbering requires some thought however, UO considers the first string passed to it as 0 (zero) but POL considers the first one in an array to be 1 (one) so this is the way to do it: text 0 0 32 0 <---> array[1] := "This I want printed". | ||
| gumppic | [X],[Y],[GUMPGFX] | |
| Display the selected gump. | ||
| textentry | [X] [Y] [WIDTH] [HEIGHT] [COLOR] [RETURNVALUE] [INITSTRINGNUMBER] | |
| Display an editable text
entry line [RETURNVALUE] is the key to use to retrieve the string [INITSTRINGNUMBER] is the initial string to use, usual base 0<>1 stuff | ||
| checkbox | [X],[Y],[GUMPGFX],[CLICKGUMPGFX],[PREVALUE],[RETURNVALUE] | |
| RETURNVALUE of all the checked boxes can be found in result.keys[]. | ||
v0.1: 6/27/2000
Initial preliminary
document
v0.2: 7/19/2000
Added
sections on Case, built-in props, Object Ref chart
v0.3:
7/21/2000
Added Gump Tag descriptions,
CProp Chapter, Package Chapter
v0.4:
7/22/2000
Added Debugging
Chapter
v0.5: 7/25/2000
Cleaned up,
added contact info, first public version.
v0.6:
11/01/2000
Turned into HTML. Corrected an
error in the CASE section. Added info about CProps and accounts. Added Chapter
11. -Madman
v0.6a: 11/06/2000
Added
Table of Contents, cell borders on some tables. -Madman
v0.6b: 15/06/2006
Fixed some eScript comparisons and
assignments. Removed some 'local's. Added 'do..dowhile()'. -Shinigami
v0.6c: 7/07/2006
Removed object property/method lists,
updated data structures and iterations and trillions of smaller things. -Austin