Jednym z kluczowych elementów skryptów są identyfikatory. To nazwy oznaczające jakieś byty w kodzie - klasy, prototypy, funkcje, zmienne, stałe. Wszystkie te rzeczy, po wywaleniu których moglibyśmy dostać dobrze znany komunikat "Unknown identifier", o ile były gdzieś używane.
Problemem osób piszących skrypty w Daedalusie jest to, że zwykle nie odróżniają identyfikatorów od obiektów na które wskazują. Na pytanie czym jest "pc_hero", większość odpowiedziałaby pewnie że obiektem klasy c_npc. Niestety nie jest to do końca prawda. To identyfikator który na taki obiekt wskazuje w trakcie gry. Ta różnica jest ważniejsza niż może się wydawać, o czym można łatwo przekonać się próbując dokonać przypisania jednego do drugiego:
var c_npc bezimienny; bezimienny = pc_hero;
Taki kod spowoduje otrzymanie komunikatu o niezgodności typów. Aby zrozumieć o co chodzi, trzeba zapoznać się z tym co parser robi z identyfikatorami.
Parser przetwarzając skrypt, na podstawie każdego napotkanego identyfikatora tworzy nowy obiekt klasy zCPar_Symbol i zapisuje wskaźnik na niego pod kolejnym indeksem w tablicy zCParser.symtab_table_array. Pełną definicję klasy zCPar_Symbol z opisem można obecnie znaleźć w Ikarusie, wygląda ona w dużym skrócie tak:
class zCPar_Symbol
{
var string name; // identyfikator
var int content; // zawartość lub wskaźnik na zawartość
var int offset; // wskaźnik związany z zawartością
};
Przykładowo, zakładając że parser przetworzył dotąd 1000 identyfikatorów i napotkał taki kod:
class JakasKlasa { };
func void JakasFunkcja (var int argumentFunkcji) { var string zmiennaLokalna; };
instance JakisNpc (C_NPC) { };
const int stalaGlobalna = 42;
Stworzy kolejne symbole zapisze je pod odpowiednimi indeksami:
JakasKlasa -> 1001
JakasFunkcja -> 1002
JakasFunkcja.argumentFunkcji -> 1003
JakasFunkcja.zmiennaLokalna -> 1004
JakisNpc -> 1005
stalaGlobalna -> 1006
i tak dalej.
Symbole różnią się od siebie w zależności od tego co oznaczały, w przypadku większości rodzajów symboli można odczytać ich indeksy... tak po prostu:
print(IntToString(pc_hero));
W praktyce wszyscy to robią, po prostu zwykle nie zwracają uwagi na to że to czym operują to właściwie liczby, a nie obiekty. Przykładem są wszelkie wywołania funkcji Hlp_GetNpc czy CreateInvItems. Obie jako argumenty przyjmują liczby (indeksy symboli) - można to łatwo sprawdzić w ich deklaracji.
W przypadku symboli instancji, istnieje też elegancki sposób na otrzymanie ich indeksów - funkcja Hlp_GetInstanceID.
Symbole oprócz swoich numerów i identyfikatorów, mają też zawartość. Będzie ona różnego rodzaju w zależności od tego co symbol reprezentuje.
Stałe i zmienne:
- content będzie zawierało wartość lub wskaźnik na wartość
- offset będzie zawierało przesunięcie wewnątrz klasy, o ile dana zmienna należy do jakiejś klasy
Funkcje:
- content będzie zawierało wskaźnik na kod na stosie
- offset będzie zawierało wskaźnik na wynik
Prototypy (są w pewnym sensie funkcjami inicjalizującymi nowy obiekt wybranym zestawem danych):
- content będzie, tak jak przy funkcjach, zawierało wskaźnik na kod na stosie
Instancje są specyficznym przypadkiem. Mają trochę ze zmiennych i prototypów, ale oba ich składniki są opcjonalne i mogą być puste
- content może zawierać wskaźnik na kod inicjalizujący (jeśli istnieje) (jak przy prototypach)
- offset może zawierać wskaźnik na obiekt stworzony na ich podstawie (zwykle ostatni) (jeśli istnieje) (trochę jak przy zmiennych)
Przykładowo:
instance jasio (c_npc) { name = "Jan"; };
Zakładając że nie stworzymy tego npc w grze, symbol o nazwie "jasio" będzie zawierał wskaźnik na kod inicjalizacyjny w polu content, ale pole offset będzie zawierało null pointera (bo nie istnieje żaden obiekt stworzony na podstawie tej instancji).
instance self (c_npc);
Taka deklaracja rzeczywiście znajduje się w constants.d. W przeciwieństwie do jasia, self nie posiada kodu inicjalizacyjnego - więc pole content będzie zawierało null pointera. Z drugiej strony, silnik przypisuje mu jako zawartość npc na których operuje - więc offset będzie zawierało wskaźnik na npc z którym ostatnio coś się działo.
instance nikt (c_npc);
Ten tutaj z kolei, będzie miał null pointery zarówno w content, jak i offset - nie ma funkcji inicjalizacyjnej i nie istnieje też żaden obiekt stworzony na podstawie tej instancji.
Normalnie nie mamy bezpośredniego dostępu do zawartości symboli, ale funkcje zewnętrzne którym podamy je jako argumenty, będą próbowały uzyskać do nich dostęp.
Przykładowo, Wld_InsertNpc oczekuje jako argumentu indeksu symbolu oznaczającego obiekt klasy c_npc. Z założenia jednak nie oczekuje ona że taki obiekt już istnieje, więc nie próbuje odczytać tego na co wskazuje "offset". Zamiast tego tworzy nowy obiekt i w jego kontekście wywołuje kod na który wskazuje wskaźnik w "content". Można to sprawdzić wywołując tą funkcję i podając self jako argument. Ponieważ ta instancja nie ma kodu inicjalizacyjnego, powinniśmy otrzymać crash. Z kolei podając jako argument naszego jasia, powinno obejść się bez problemów, pomimo że wcześniej nie istniał obiekt na który ten symbol mógłby wskazywać.
Przykładem odwrotnego zachowania jest funkcja Npc_IsDead. Ona z kolei nie potrzebuje tworzyć nowego npc, a jedynie odczytać wskaźnik na obiekt już istniejący. Dlatego możemy bez crasha wywołać Npc_IsDead(self). Wywołanie jej z nigdy nie stworzonym jasiem mogłoby spowodować crash, ale tego akurat nie jestem pewien.
Odkąd napisałem ten artykuł w 2011, dwa razy przepisywałem jego treść. Obecna wersja zawiera informacje zawdzięczane Sektenspinnerowi - cały kawałek o zawartości symboli. To co ja pierwotnie zauważyłem i o czym napisałem, to samo odróżnianie obiektów od identyfikatorów i posługiwanie się indeksami symboli