Tutorial LPC: Różnice pomiędzy wersjami
Linia 1505: | Linia 1505: | ||
==== Instrukcja #include ==== | ==== Instrukcja #include ==== | ||
− | Jest to | + | Jest to najczęściej używana dyrektywa preprocesora. Mówi ona |
− | mu, aby | + | mu, aby zastąpił linie w której ona się znajduje zawartością '''całego''' |
pliku, o nazwie podanej po instrukcji. | pliku, o nazwie podanej po instrukcji. | ||
− | Dane, | + | Dane, które umieszczasz we włączanym pliku to takie, których raczej nie |
− | + | będziesz nigdy zmieniał i takie, które będziesz włączał w wiele plików. | |
− | Zamiast wpisywania tego samego tekstu w | + | Zamiast wpisywania tego samego tekstu w różne pliki w nieskończoność i |
− | co za tym idzie | + | co za tym idzie zwiększania szansy na jakieś kretyńskie błędy, po prostu |
− | zbierasz | + | zbierasz powtarzające się dane w jeden lub kilka plików i włączasz(include) |
− | je w te programy, w | + | je w te programy, w które trzeba. |
− | + | Składnia jest bardzo prosta: | |
#include <plik_standardowy> | #include <plik_standardowy> | ||
#include "plik_dodatkowy" | #include "plik_dodatkowy" | ||
− | '''UWAGA!''' | + | '''UWAGA!''' Zauważ, że nie ma ‘;’ na końcu linii! |
− | Dwa | + | Dwa różne sposoby zapisywania nazw plików zależą od tego, gdzie się |
− | one | + | one znajdują. Jest spora liczba standardowych bibliotek w grze, |
− | rozrzuconych po sporej liczbie | + | rozrzuconych po sporej liczbie katalogów. Zamiast zapamiętywania dokładnie |
− | gdzie one | + | gdzie one są, wystarczy że podasz sama nazwę pliku, który chcesz włączyć. |
#include <stdproperties.h> | #include <stdproperties.h> | ||
#include <adverbs.h> | #include <adverbs.h> | ||
− | Gdy chcesz | + | Gdy chcesz włączyć jakieś pliki własne, które nie są żadną standardową |
− | + | biblioteką, to musisz dokładnie podać ich lokacje. Możesz to zrobić | |
− | + | zarówno poprzez podanie ścieżki od katalogu, gdzie się znajduje program, | |
− | albo poprzez podanie | + | albo poprzez podanie pełnej, absolutnej ścieżki od głównego katalogu. |
#include "/d/Standard/login/login.h" | #include "/d/Standard/login/login.h" | ||
#include "moje_def.h" | #include "moje_def.h" | ||
− | #include "/sys/adverbs.h" // Ten sam, co | + | #include "/sys/adverbs.h" // Ten sam, co krótszy przykład powyżej |
− | Gdy chcesz | + | Gdy chcesz włączyć standardowe biblioteki, to zawsze używaj notacji < >. |
− | (czyli podawaj sama | + | (czyli podawaj sama nazwę biblioteki, ujętą w nawiasy < > ). Powodem nie |
− | jest tylko to, | + | jest tylko to, że tak jest krócej, ale to, że gdy biblioteki zostaną |
− | przemieszczone gdzie indziej, | + | przemieszczone gdzie indziej, twój program przestanie działać. Gdy |
− | + | użyjesz notacji <> to zawsze zostaną znalezione. | |
− | + | Włączane pliki mogą mieć dowolną nazwę, ale ustalono, że będą miały | |
− | + | końcówkę ‘.h’, żeby moc je jasno odróżnić od innych plików. | |
− | Jest nawet | + | Jest nawet możliwe włączenie plików ‘c’, tzn. całych plików zawierających |
− | programy. | + | programy. Jednakże, jest to bardzo zła rzecz. '''Nie''' rób tego '''NIGDY'''! |
− | Czemu? Po pierwsze programy | + | Czemu? Po pierwsze programy śledzące błędy gubią numeracje linii we |
− | + | włączanych plikach i przez to podają złe numery linii. | |
− | Po drugie, gdy | + | Po drugie, gdy włączasz nieskompilowany kod w wiele różnych obiektów, |
− | marnujesz | + | marnujesz pamięć oraz CPU, gdyż ten sam plik musi być wielokrotnie |
− | kompilowany i przechowywany osobno, dla | + | kompilowany i przechowywany osobno, dla każdego obiektu, który go używa. |
− | A poza tym samo czytanie takiego programu | + | A poza tym samo czytanie takiego programu może być istną torturą. |
− | Co ma | + | Co ma więc tak naprawdę rozszerzenie nazwy pliku do jego zawartości? |
− | Tak | + | Tak naprawdę to nic nie ma... Jednakże jest przyjęte, że kod i funkcje |
− | + | są przechowywane w plikach z rozszerzeniem ‘.c’, a definicje | |
− | z rozszerzeniem ‘.h’. Mudlib zazwyczaj korzysta z tego | + | z rozszerzeniem ‘.h’. Mudlib zazwyczaj korzysta z tego rozdziału i może nie |
− | + | rozpoznawać jako kodu źródłowego niczego, poza plikami z końcówką ‘c’. | |
==== Instrukcja #define ==== | ==== Instrukcja #define ==== | ||
− | Jest to bardzo | + | Jest to bardzo potężne „makro”, komenda preprocesora, która jest ciągle |
− | + | nadużywana. Mądrze postąpisz, jeśli będziesz używał jej ostrożnie | |
i tylko do prostych rzeczy. | i tylko do prostych rzeczy. | ||
− | Ma ona | + | Ma ona następującą składnię: |
− | #define <identyfikator> <tekst | + | #define <identyfikator> <tekst zastępujący> |
#undef <identyfikator> | #undef <identyfikator> | ||
Dowolny taki sam tekst w pliku jak ‘<identyfikator>’ zostanie | Dowolny taki sam tekst w pliku jak ‘<identyfikator>’ zostanie | ||
− | + | zastąpiony ‘<tekstem zastępującym>’, jeszcze przed kompilacją. | |
− | ‘#define’ jest | + | ‘#define’ jest ważne od linii, w której zostało zdefiniowane do końca |
− | pliku, albo do momentu wykonania komendy ‘#undef’, | + | pliku, albo do momentu wykonania komendy ‘#undef’, która usuwa makro. |
− | Pomimo tego, | + | Pomimo tego, że makrem może być dowolny tekst, jest w zwyczaju(rób tak!), |
− | + | że nazwę makra pisze się wyłącznie dużymi literami. Jest to po to, żeby | |
− | + | można było wyróżnić makra w tekście, w którym każdy(ty tez!) pisze nazwy | |
− | funkcji i zmiennych | + | funkcji i zmiennych małymi literami. |
− | Umieszczaj wszystkie definicje na | + | Umieszczaj wszystkie definicje na początku pliku, albo biedny koleś, |
− | + | który będzie ci pomagał w wyłapaniu błędów, będzie miał prawdziwe | |
− | + | piekiełko z poszukiwaniem pochowanych definicji. Jeśli to będzie ktoś, | |
− | kogo sam | + | kogo sam poprosiłeś o pomoc (gdyż masz błędy powstałe z powodu brzydko |
− | napisanego kodu), to najprawdopodobniej powie ci | + | napisanego kodu), to najprawdopodobniej powie ci żebyś sobie wsadził taki |
− | program w wiadome miejsce i | + | program w wiadome miejsce i wrócił dopiero wtedy, jak się nauczysz poprawnie |
− | i | + | i ładnie pisać. |
− | Prostymi definicjami | + | Prostymi definicjami są na przykład ścieżki, nazwy i wszystkie inne |
− | stale dowolnego rodzaju, | + | stale dowolnego rodzaju, których nie chcesz w kółko zapisywać, albo |
− | chcesz | + | chcesz mieć możliwość łatwej ich modyfikacji, bez zmieniania tego samego |
− | w | + | w dziesiątkach miejsc. |
MAX_LOGIN 100 /* Maksymalna liczba zalogowanych | MAX_LOGIN 100 /* Maksymalna liczba zalogowanych | ||
Linia 1603: | Linia 1603: | ||
POWIT_TEKST "Witamy!" /* Komunikat logowania */ | POWIT_TEKST "Witamy!" /* Komunikat logowania */ | ||
− | Gdziekolwiek | + | Gdziekolwiek wystąpi identyfikator makra, zostanie on zastąpiony tym, |
− | co | + | co się znajduje w definicji pomiędzy identyfikatorem, a końcem linii. |
− | + | Podchodzą pod to także komentarze, które jednak i tak zostaną później | |
wyrzucone. | wyrzucone. | ||
tell_object(gracz, POWIT_TEKST + "\n"); | tell_object(gracz, POWIT_TEKST + "\n"); | ||
− | Komentarz typu ‘//’ nie jest w takiej | + | Komentarz typu ‘//’ nie jest w takiej sytuacji dobra rzeczą, gdyż kończy |
− | + | się on dopiero na końcu linii. | |
POWIT_TEKST "Witamy!" // Komunikat logowania | POWIT_TEKST "Witamy!" // Komunikat logowania | ||
− | ... | + | ...będzie zamienione w poprzednim przykładzie na: |
tell_object(gracz, "Witamy!" // Komunikat logowania + "\n"); | tell_object(gracz, "Witamy!" // Komunikat logowania + "\n"); | ||
− | ...co spowoduje | + | ...co spowoduje ujęcie w komentarz wszystkiego, co wystąpi po ‘//’, aż |
− | do | + | do końca linii. |
− | Gdy makro wychodzi poza linie, | + | Gdy makro wychodzi poza linie, możesz przedłużyć ją za pomocą znaku ‘\’ |
− | + | który zaznacza, że definicja jest kontynuowana w następnej linii. | |
− | + | Jednakże musisz zakończyć linie tuż po ‘\’ i '''NIE''' nie może być za nim | |
− | + | żadnych spacji, ani innych znaków. | |
− | DLUGA_DEFINICJA " | + | DLUGA_DEFINICJA "początek stringa \ |
i jego koniec." | i jego koniec." | ||
− | Definicje | + | Definicje naśladujące funkcje są często stosowane i nieraz nadużywane. |
− | Jedyna | + | Jedyna naprawdę ważną zasadą, obowiązującą przy pisaniu takich makr jest |
− | to, | + | to, że każdy argument '''musi''' być ujęty w nawiasy. Jeśli napiszesz inaczej, |
− | to | + | to możesz otrzymać bardzo dziwne wyniki. |
− | 1: POMNOZ_TO(a, b) a * b /* | + | 1: POMNOZ_TO(a, b) a * b /* Źle */ |
− | 2: POMNOZ_TO(a, b) (a * b) /* Nie | + | 2: POMNOZ_TO(a, b) (a * b) /* Nie wystarczająco */ |
− | 3: POMNOZ_TO(a, b) ((a) * (b)) /* | + | 3: POMNOZ_TO(a, b) ((a) * (b)) /* Właściwie */ |
− | Co za | + | Co za różnica pewnie zapytasz? Spójrz w takim razie na ten przykład: |
wynik = POMNOZ_TO(2 + 3, 4 * 5) / 5; | wynik = POMNOZ_TO(2 + 3, 4 * 5) / 5; | ||
− | Po podstawieniu | + | Po podstawieniu wygląda to tak: |
− | 1: wynik = 2 + 3 * 4 * 5 / 5; // = 14, | + | 1: wynik = 2 + 3 * 4 * 5 / 5; // = 14, Zły |
− | 2: wynik = (2 + 3 * 4 * 5) / 5 // = 12, | + | 2: wynik = (2 + 3 * 4 * 5) / 5 // = 12, Też zły |
− | 3: wynik = ((2 + 3) * (4 * 5)) / 5 // = 20, | + | 3: wynik = ((2 + 3) * (4 * 5)) / 5 // = 20, Właściwy! |
− | + | Nadużycia definicji polegają zazwyczaj na złym ich ułożeniu i użyciu | |
− | skomplikowanych makr | + | skomplikowanych makr wewnątrz innych makr (co czyni kod prawie niemożliwym |
− | do zrozumienia). | + | do zrozumienia). Podstawową zasadą jest pisanie krótkich i prostych makr. |
− | + | Rób tak, albo marny będzie twój koniec ;) | |
==== Instrukcje #if, #ifdef, #ifndef, #else i #elseif ==== | ==== Instrukcje #if, #ifdef, #ifndef, #else i #elseif ==== | ||
− | + | Są to dyrektywy preprocesora, które służą selekcjonowaniu określonych | |
− | + | części kodu i usuwaniu innych w zależności od stanu zmiennych preprocesora. | |
− | + | Dzięki nim część kodu może zostać uniewidoczniona dla kompilatora – coś | |
jak inteligentne komentarze. | jak inteligentne komentarze. | ||
− | Instrukcja ‘#if’ jest bardzo podobna do | + | Instrukcja ‘#if’ jest bardzo podobna do zwykłej instrukcji if. Jest |
− | tylko | + | tylko trochę inaczej zapisywana. |
− | + | Załóżmy, że '''możesz''' mieć gdzieś następującą definicję: | |
CODE_VAR 2 | CODE_VAR 2 | ||
Linia 1673: | Linia 1673: | ||
CODE_VAR 3 | CODE_VAR 3 | ||
− | Wtedy | + | Wtedy możesz napisać: |
#if CODE_VAR == 2 | #if CODE_VAR == 2 | ||
− | <kod ten | + | <kod ten będzie dopuszczony do kompilacji tylko jeśli CODE_VAR == 2> |
#else | #else | ||
− | <kod ten | + | <kod ten będzie zachowany tylko wtedy gdy CODE_VAR != 2> |
#endif | #endif | ||
− | + | Możesz w ogóle nie pisać instrukcji ‘#else’ jeśli tego nie chcesz. | |
− | Wystarczy | + | Wystarczy napisać następującą instrukcje, żeby zaistniała dana definicja |
preprocesora: | preprocesora: | ||
CODE_VAR /* Definiuje istnienie CODE_VAR */ | CODE_VAR /* Definiuje istnienie CODE_VAR */ | ||
− | I teraz | + | I teraz możesz napisać: |
#ifdef CODE_VAR | #ifdef CODE_VAR | ||
− | <Kod, | + | <Kod, który będzie zachowany tylko, jeśli CODE_VAR istnieje> |
#else | #else | ||
− | <kod, | + | <kod, który będzie zachowany tylko wtedy, gdy CODE_VAR nie jest |
zdefiniowane> | zdefiniowane> | ||
#endif | #endif | ||
Linia 1700: | Linia 1700: | ||
#ifndef CODE_VAR | #ifndef CODE_VAR | ||
− | <kod, | + | <kod, który będzie zachowany tylko wtedy, gdy CODE_VAR *nie* jest |
zdefiniowane> | zdefiniowane> | ||
#else | #else | ||
− | <kod, | + | <kod, który będzie zachowany, jeśli CODE_VAR istnieje> |
#endif | #endif | ||
− | I ponownie, instrukcje ‘#else’ | + | I ponownie, instrukcje ‘#else’ można ominąć. |
− | Komendy preprocesora ‘#if/#ifdef/#ifndef’ | + | Komendy preprocesora ‘#if/#ifdef/#ifndef’ są prawie wyłącznie używane |
− | do dodania | + | do dodania odpluskwiającego kodu, który nie powinien być cały czas aktywny |
− | lub do pisania rzeczy, | + | lub do pisania rzeczy, które będą różnie pracowały w zależności od bardzo |
− | rzadko | + | rzadko zmieniających się parametrów. |
− | + | Cała konfiguracja mudliba jest sprawdzana w ten sposób. Na przykład | |
− | jest definicja MET_ACTIVE, | + | jest definicja MET_ACTIVE, która służy ustalaniu czy system met/nonmet |
− | (nie | + | (nie każdy zna każdego gracza) ma być włączony czy nie. Gdy administrator |
− | muda zdecyduje | + | muda zdecyduje się na wyłączenie tego, wystarczy że w jednym pliku |
konfiguracyjnym zamieni ‘#define MET_ACTIVE’ na ‘#undef MET_ACTIVE’. | konfiguracyjnym zamieni ‘#define MET_ACTIVE’ na ‘#undef MET_ACTIVE’. | ||
(W Arkadii wiele takich definicji znajdziesz w ‘/config/sys/local.h’). | (W Arkadii wiele takich definicji znajdziesz w ‘/config/sys/local.h’). |
Wersja z 18:16, 13 gru 2018
- Wersja tekstowa z wyszczególnionym autorstwem rozdziałów znajduje się na stronie MUD-a Barsawia. Tu znajduje się ładnie, w założeniu, sformatowana i w wielu miejscach poprawiona wersja HTML.
This is a tutorial for LPC and basic LPmud mudlib. The document was written for the Genesis driver and Genesis mudlib. Gamedriver version CD.04.02, Mudlib CD.00.31 though in all likelyhood it will still be usable for three-four versions further. Copyright (C) 1996 Ronny Wikh The use of this manual in any commercial venture is expressly forbidden, neither may it be offered as inducement to purchase other services or products. Permission is granted to make and distribute verbatim copies of this manual provided the copyright notice and this permission notice are preserved in full on all copies. Permission is granted to copy and distribute modified versions of this manual under the conditions for verbatim copying, provided that the entire resulting derived work is distributed under the terms of a permission notice identical to this one. Permission is granted to copy and distribute translations of this manual into another language, under the above conditions for modified versions, except that this permission notice may be stated in a translation approved by the holders of the above copyright.
Przetłumaczyli na polski / Translated into polish:
- Alvin.Arkadia [arkadia (@) arkadia.rpg.pl] (25/08/96)
- Kael.Barsawia [barsawia (@) irc.pl] (28/02/03)
Przedmowa
LPC
Granie w LPMudy jest bardzo ekscytującym zajęciem, a ich tworzenie jeszcze bardziej. Jest bardzo niewiele rzeczy, których nie da się zakodować. Nieraz trzeba jednak używać różnych sztuczek. Największą zaletą MUD-ów jest naturalnie to, ze może w nie grać naraz wielu graczy; widok setek, a nawet tysięcy ludzi używających i czerpiących radość z Twojego kodu sprawia wielką przyjemność.
Ten podręcznik ma na celu nauczenie Cię niezbędnych podstaw potrzebnych do tworzenia kodu w LPMudach i na Genesis w szczególności. Nie jest to jednak łatwa lektura, wiec spodziewaj się, ze zajmie Ci kilka ładnych dni przyswojenie sobie tego, czego spróbuje Cię nauczyć. Pomimo tego groźnie brzmiącego ostrzeżenia, życzę Ci przyjemnego kodowania, po tym, jak już przełamiesz pierwsze lody.
-- Ronny Wikh, 25 stycznia 1996
Warunki rozpowszechniania
Podręcznik ten został napisany w dobrej wierze dla ludzi, którzy piszą MUD-y dla własnej przyjemności. Oznacza to, ze nie będę wymagał żadnych opłat za używanie i rozprowadzanie tego tekstu, pod warunkiem, ze taki właśnie jest cel jego użytkowania.
W szczególności chce się upewnić, że ci, którzy czerpią korzyści finansowe z prowadzenia MUD-a nie używają tego dokumentu jako pomocy w zarabianiu pieniędzy.
Notki tłumaczy
Alvin
Tłumaczenie może być miejscami niezręczne. Jest tez spora szansa, ze napotkasz jakieś błędy. W obu przypadkach wal z nimi proszę do mnie, czyli do Alvina.
Mam nadzieje, ze Ci się na coś zda to tłumaczenie.
Kael
Wszelkie, błędy, merytoryczne uwagi i propozycje dotyczące tego wydania proszę zgłaszać na adres: rafal.dorociak (małpa) wp.pl
Starałem się używać w miarę przystępnego języka, w partiach, które tłumaczyłem, nierzadko odbiegając nieco od oryginału, tudzież dodając własne uwagi.
Życzę miłego korzystania i wielu przyjemnych chwil z kodem. :)
Wstęp
Na początku moim zamierzeniem było, by dowolna osoba mogła korzystać i uczyć się z tego dokumentu. Teraz jednak wydaje mi się to raczej niemożliwe, więc skoryguje to troszeczkę. To jest podręcznik, z którego może korzystać każdy, kto ma przynajmniej blade pojecie o programowaniu oraz chęci do nauki. Nie musisz znać C zanim zaczniesz i mam nadzieję, że nawet kompletni laicy będą w stanie się nauczyć kodowania, choć oczywiście będą mieli o wiele więcej roboty.
Doświadczeni programiści, a nawet ci, którzy pisali już kiedyś mudy będą musieli zaznajomić się z tym podręcznikiem, gdyż wyjaśnia on pojęcia charakterystyczne tylko dla tej gry, aczkolwiek będą mogli po prostu przejrzeć większość tekstu i skupić się na bardziej zawiłych fragmentach. Tobie, czytelniku, zostawiam wybór tego co jest dla Ciebie ważne, a co nie, gdyż chyba tylko Ty wiesz czego się jeszcze powinieneś nauczyć.
Jako, ze język LPC w aktualnej postaci jest mocno powiązany z mudlibem, który jest w nim napisany, zahaczę również o jego część. Jednakże nie będę Cie uczył szczegółowo jak z niego korzystać – będziesz musiał przeczytać inne podręczniki. Wszystko to czyni ten dokument wyspecjalizowanym na LPMudy, a na Genesis w szczególności. Pamiętaj o tym, jeśli piszesz pod innym mudem.
Mam nadzieję, że ten podręcznik okaże się pomocny i warty wysiłku, który trzeba poswięcić na jego przeczytanie. Z pewnością napisanie go nie było takie proste, więc niech lepiej jego lektura nie będzie bezowocna! :)
Podziękowania
Chciałbym zacząć od podziękowań dla Thorstena Lockerta, Christiana Markusa i Bobby'ego Bodenheimera (znanych raczej jako Plugh, Olorin i Plarry), za pomoc przy korekcie tekstu, wartościowe sugestie i ogólnie za wsparcie i pomocna dłoń w czasie tworzenia tego podręcznika.
Bez nich realizacja tego ciągnącego się projektu zajęłaby jeszcze więcej czasu (niewiarygodne, ale bardzo prawdziwe), a końcowy efekt byłby znacznie gorszy.
Mnóstwo ludzi na Genesisie przyczyniła się do kształtu tego tutoriala i z wdzięcznością wspominam o ich cennych sugestiach i pytaniach, ukazujących co ludzie chcieliby wiedzieć o LPC.
Forma podręcznika
Podręcznik jest podzielony na trzy rozdziały, o rosnącym stopniu zaawansowania. Pierwszy rozdział wyjaśni podstawy kodowania i LPC bez zagłębiania się w szczegóły.
Drugi rozdział traktuje o trudniejszych sprawach; wyjaśni w pełni wszystkie aspekty funkcji i operatorów, które mogły zostać troszeczkę zbyt pobieżnie potraktowane w rozdziale pierwszym.
Trzeci, końcowy rozdział traktuje o tym, co mogło zostać pominięte w dwóch poprzednich. Tak naprawdę, to nie o wszystkim; podręcznik nie wyjaśni wszystkich zawiłości gamedrivera i mudliba. Jeśli szukasz informacji o tworzeniu własnego mudliba albo o robieniu innych bardzo zaawansowanych rzeczy, będziesz musiał sam do tego dojść, czytając kod źródłowy.
Jeśli jesteś początkującym wizardem, możesz być trochę przytłoczony na początku, patrząc na ten obszerny podręcznik. Jednakże jest ważne, żebyś przeczytał wszystko i nie zostawiał niczego na później. Bez wątpienia będziesz musiał poznać zagadnienia ze wszystkich trzech rozdziałów, choć przeważnie będziesz korzystał z wiedzy zawartej w dwóch pierwszych. Kurcze! Jest wielu starych wizardów, którzy ledwo co opanowali pierwszy rozdział! Jest to jeden z podstawowych powodów, dla których pisze ten podręcznik...
Ten podręcznik jest dość rozległy, pomimo tego, że jest przeznaczony tylko do nauki kodowania w domenach. Oznacza to, że nie jest to kompletna instrukcja LPC. Kilka rzadziej używanych efunkcji jest pominiętych, gdyż są one używane tylko przez ludzi piszących oraz ulepszających mudliba i gamedrivera. Przykro mi, jeśli właśnie tego tu szukałeś – będziesz się musiał rozejrzeć za jakimś innym źródłem informacji.
Mała uwaga o płci. Przez cały czas używam męskiego rodzaju. Nie dlatego, że dyskryminuje programistki, ale dlatego, że język angielski jest nastawiony na męski rodzaj. (przyp. tłum. hmm.. nie jestem pewien jak z polskim, ale to co autor dalej pisze to prawda) Myślę, że mógłbym za każdym razem dopisywać też żeńską formę, lecz uważam, ze byłoby to trochę głupawe. Dopóki angielski nie stanie się w pełni neutralnym płciowo językiem, będę się trzymał męskiego rodzaju.
Rozdziały, które szczegółowo opisują efunkcje/sfunkcje/lfunkcje/makra, będą miały wymienione w nawiasach odwołania do nazw powiązanych z omawianymi rzeczami, aby ułatwić Ci późniejsze wyszukiwanie tych informacji.
Historia LPC
LPC jest językiem programowania MUD-ów, stworzonym przez Larsa Pensjoego na potrzeby własnego pomysłu LPMuda, interaktywnego, wielużytkownikowego środowiska, przeznaczonego do kilku różnych celów, wśród których gra nie była jedynym. Od czasu pierwszego pojawienia się go, w 1988 roku, język znacznie się zmienił.
Prace nad nim zostały przejęte około 1990 roku, przez innych ludzi z Chalmers Datofrerening, głównie przez Jakoba Hallena, Lennarta Augustssona i Andersa Chrigstroema. Rozszerzyli i udoskonalili znacznie język, aczkolwiek, jak nazwa LPC wskazuje, wciąż wykazuje on powiązania z językiem ‘C’. Różnice głównie polegają na dołączeniu struktury obiektowej, jak również na dodaniu kilku typów danych, w celu uprzystępnienia programowania. LPC nie jest tak swobodnym językiem jak klasyczny ‘C’, choć z drugiej strony bardziej się nadaje do celu, dla jakiego został stworzony – do pisania środowiska gry.
Gamedriver/Mudlib
Definicja różnicy pomiędzy gamedriverem i mudlibem może się zdawać trudna do określenia, lecz tak naprawdę jest bardzo prosta. Gamedriver jest to program, który jest uruchomiony na serwerze muda. Jest interpreterem poleceń połączonym z jądrem zarządzania obiektami (object management kernel). Można nawet powiedzieć, ze jest to swoisty system operacyjny. Definiuje i rozumie język LPC, interpretuje i wykonuje instrukcje podane mu poprzez obiekty LPC. Gamedriver zajmuje się tylko obliczeniami i wszelka treść MUD-a jest mu podawana z zewnątrz do uruchomienia.
Mudlib jest biblioteką podstawowych obiektów LPC wspólnych dla całej gry. Zawiera zestaw obiektów LPC, które są używane w grze: ogólny obiekt gracza, potwora, pokoju itd. To co Ty piszesz, czyli kod domenowy, jest przechowywane i obsługiwane osobno i korzysta z podstawowych elementów środowiska gry umieszczonych w mudlibie.
Struktura administracyjna
Można powiedzieć, że gra jest podzielona na trzy charakterystyczne części, tak jak to już zostało powyżej zasugerowane; Gamedriver, mudlib i „kod domenowy”. Gamedriver i mudlib już zostały opisane. Domeny są sposobem na zorganizowanie pisania kodu. Domena to grupa wizardów, pracujących nad osiągnięciem z góry określonego celu. Może nim być jakaś ograniczona przestrzeń, fragment świata gry, lub rzeczy takie jak gildie bądż sekty, których członkami gracze mogą zostać.
W każdej domenie jest jeden szef, Lord domeny. Jest on swego rodzaju nadzorcą. Decyduje co się w domenie ma dziać i w jakim kierunku prace powinny zdążać. W domenie cały kod może być łatwo wymieniany i jest mocno ze sobą powiązany.
Oczywiście, są również więzy pomiędzy różnymi domenami, aczkolwiek są one zazwyczaj słabsze, a kod jest rzadko wymieniany.
Jako początkujący wizard, będziesz próbował znaleźć sobie jakąś domenę, w której praca wyda ci się interesująca i inspirująca.
Pisanie kodu
Może się wydać nieco przedwczesnym mówienie Ci jak kod powinien wyglądać, zanim nawet nie nauczyłeś się jak go pisać. Jednakże jest to fundamentalna sprawa o wielkiej ważności. Ktoś fajnie powiedział, że właściwe pisanie kodu uczyni Twe zęby bielszymi, włosy ciemniejszymi i pozytywnie wpłynie na twoje życie seksualne. Tego chyba nie mogę zagwarantować, ale na pewno drastycznie polepszy to jakość, tego co piszesz, przy bardzo niskich nakładach pracy. Polega to głównie na samodyscyplinie.
Oto kilka dobrych argumentów na pisanie poprawnego kodu:
- Czyni Twój kod znacznie bardziej przejrzystym, nie tylko dla innych, lecz również dla Ciebie, szczególnie jeśli sam będziesz musiał przeczytać i poprawić go sześć miesięcy po tym, jak go napisałeś.
- Jeśli jest on bardziej przejrzysty dla innych, to łatwiej im będzie zrozumieć to, co skleciłeś i co za tym idzie łatwiej będzie im pomóc ci w razie problemów.
- Właściwe pisanie kodu czyni go lepszym, możesz wierzyć lub nie. Przyczyna jest prosta: niepoprawne pisanie programu zwiększa bardzo szanse na to, że przeoczysz jakieś banalne błędy, schowane pośród ściśniętego kodu.
- Może ci być naprawdę ciężko znaleźć ludzi, którzy będą chcieli odpluskwić źle sformatowany kod. Ja osobiście nie pomogę osobom w usunięciu błędów z programu, który wygląda okropnie. Przyczyna jest prosta: jest to gra nie warta świeczki. W źle wyglądającym kodzie kryje się wiele głupich błędów (przeważnie brakujące, albo przemieszczone nawiasy), które wyjdą na wierzch od razu po tym, jak właściwie sformatujesz program.
Teraz będzie trochę przydługa instrukcja mówiąca o tym, jak należy układać kod w czasie pisania. Przeczytaj to, nawet jeśli w pełni nie rozumiesz o czym jest mowa, a później wróć i przeczytaj raz jeszcze po tym, jak zdobędziesz niezbędne umiejętności. W ten sposób utrwali Ci się właściwy sposób pisania programów.
- Jedno wcięcie ma długość 4 spacji, nie mniej, nie więcej. Nowe wcięcie zaczyna się na początku każdego bloku.
- Pomiędzy słowami kluczowymi a otwierającym nawiasem ‘(’, jeśli takowy jest, powinna być jedna spacja.
while (test) wyrażenie;
- Nawiasy otwierające i zamykające ten sam blok powinny być w tej samej kolumnie; w kolumnie pierwszej litery wyrażenia otwierającego blok.
if (to) { wyrażenie; } else if (tamto) { inne_wyrażenie; } else { standardowe_wyrażenie; }
Natrafiamy teraz na punkt o prawie religijnym znaczeniu dla niektórych programistów. Przedstawiciele innej sekty wyznają, że nawias otwierąjacy blok powinien znajdować się na końcu linii z wyrażeniem otwierającym blok. Rób tak jak ja mówię, albo utkniesz w tym cholernym COBOLu na zawsze :) Teraz na serio, wybierz jedna metodę i trzymaj się jej. Jest naprawdę bardzo ważną rzeczą to, abyś utrzymywał tę samą długość wcięcia przez cały czas. Zmienne długości wcięć to coś, co nie może być tolerowane.
- Liczne argumenty oddzielone od siebie przecinkami, mają po każdym takowym jedną spację. Listy argumentów oddzielone ‘;’ i operatory numeryczne maja po jednej spacji przed i po operatorze.
int a, b, c; for (a = 0; a < 10; a++) { b = function_call(a, b * 2); c = b * 3 / 4; }
- Jeśli wyrażenie pętli jest puste, umieść kończący ‘;’ w oddzielnej linii.
while (!(zmienna = funkcja(zmienna)))
;
Przyczyną jest to, ze jakbyś umieszczał ‘;’ w tej samej linii, byłoby bardzo łatwo przeoczyć prawdziwe błędy takie jak ten, wynikający z czystego lenistwa.
for (i = 0 ; i < 100 ; i++); { <ten blok jest wykonywany tylko raz, i to za każdym razem> }
- Wszystkie wyrażenia ‘#define’ i ‘#include’ powinny być umieszczone na samym początku pliku. Możliwe jest ich rozrzucenie, lecz to tylko gmatwa.
- To samo z prototypami funkcji i zmiennymi typu global/static. Zbierz je do kupy, z odpowiednim komentarzem w nagłówku i umieść na początku pliku. Oczywiście możesz je rozrzucić po całym pliku, ale jakże łatwe będzie przeoczenie ich później...
- Przy deklaracji funkcji, typ zwracanej wartości umieszczaj w linii przed nazwą funkcji.
public void moja_funkcja(int a, int b) { < kod > }
- Łam długie linie kodu w odpowiednich miejscach tak, żeby nie wychodziły poza 80-znakowy ekran. Wygląda to potwornie i ciężko się to czyta, nie mówiąc o problemach związanych z późniejszym drukowaniem.
- Plik powinien się zaczynać następującym nagłówkiem:
/* * <nazwa pliku> * * <Krótki opis tego co plik robi, nie więcej niż 5-7 linii. * ... * ... > * * Copyright (C): <twoje imię, nazwisko i rok> * */
Już TERAZ przeczytaj prawa autorskie, by wiedzieć jakie zasady obowiązują co do kodu, który piszesz dla gry. Powinny one się znajdować w pliku ‘/doc/COPYRIGHT’. Jeśli ich tam nie ma, to po prostu spytaj jednego z administratorów gry. (przyp. tłum. U nas jeszcze nie ma czegoś takiego i nie będzie do czasu aż ktoś nie napisze ;) )
- Każda funkcję zaczynaj od nagłówka, który wygląda tak:
/* * Nazwa funkcji: <Nazwa funkcji> * Opis: <Krótki opis tego, co funkcja robi, zazwyczaj * nie więcej niż trzy linie. > * Argumenty <Lista wszystkich argumentów, jeden na linie * arg1 - opis nie dłuższy niż jedna linia. * arg2 - następny argument, itd. > * Zwraca: <Co funkcja zwraca> */
Jeśli funkcja nie potrzebuje żadnych argumentów, lub nie zwraca niczego, to po prostu usuń te linie z nagłówka. - Umieść stosowne komentarze przy kodzie tu i ówdzie, tam gdzie uznasz, ze może on być niezrozumiały. Pamiętaj również o tym, że na twoim (zakładanym) poziomie kompetencji wiele rzeczy może się wydawać niezrozumiałymi :) Czyń według własnego uznania.
/* * Komentarz poprzedzający kod, * opisujący co on robi */ < kod >
- Upewnij się, że nazwy funkcji i zmienne lokalne są zapisane małymi literami alfabetu, z ewentualnymi odstępami pomiędzy wyrazami w postaci podkreślenia (np. ‘nazwa_funkcji()’). Zmienne globalne powinny mieć duże pierwsze litery słów (np. ‘int GlobalTime;’). ‘#define’ powinny być zapisane dużymi literami (np. ‘#define AMEBA "jednokomórkowe żyjątko"’). W ten sposób łatwo będzie rozpoznać, jaki jest to rodzaj symbolu.
Najprostszym sposobem załapania w jaki sposób należy pisać, jest używanie edytora emacs, ustawionego na tryb c++. Taki tryb rozumie operatory ‘::’, aczkolwiek wymaga kilku podpowiedzi w materii tabulacji itp. Umieść te linie w pliku .emacs i wszystko powinno działać jak powinno:
- W oryginalnym tutorialu ta lista jest dłuższa, nie znam się na emacsie więc nie wiem czy to istotne.
;; emacs lisp script start (setq auto-mode-alist (append '( ("\\.l" . my-c++-mode) ("\\.y" . my-c++-moe) ("\\.c" . my-c++-mode) ("\\.h" . my-c++-mode)) auto-mode-alist)) (defun my-c++-mode () (interactive) (c++-mode) (setq c-indent-level 4) (setq c-brace-offset 0) (setq c-label-offset -4) (setq c-continued-brace-offset -4) (setq c-continued-statement-offset 4)) ;; emacs end
Emacs ma jeszcze wbudowaną możliwość, która się przydaje przy odpluskwianiu wypocin innych ludzi. Poprawienie wcięć w kodzie jest tak proste, jak wstukanie ‘M-<’, ‘M->’, ‘M-x indent-region’.
Wprowadzenie do LPC
Ten rozdział nauczy cię zupełnych podstaw programowania, potrzebnych do zrozumienia całości. Oprócz tego wprowadzi cię w programowanie obiektowe oraz opisze kawałek mudliba.
Podstawowe zagadnienia związane z programowaniem
Zaczynamy od podstawowych zagadnień związanych z programowaniem, strukturą i środowiskiem LPC.
Co to jest programowanie?
To jest całkiem filozoficzne pytanie. Trzymajmy się jednakże praktycznej strony i zostawmy cały bitowy zen tym, którzy się tym zajmują.
W zasadzie programowanie to sztuka identyfikowania problemu i zamieniania rozwiązania w symbole, które komputer jest w stanie zrozumieć. Dobry programista ma wysoko rozwiniętą zdolność widzenia, jak problem może być podzielony na mniejsze problemiki, z których każdy można rozwiązać na kilka sposobów. Wie on także, które z rozwiązań jest najefektywniejsze i powinno być użyte w danej sytuacji, biorąc pod uwagę szybkość działania i zużycie pamięci.
Programista, jak już poprzednio zasugerowałem, mówi komputerowi jak rozwiązać problem. Maszyna nie może sama wymyślić rozwiązania. Jednakże jest ona o wiele szybsza od człowieka, więc problemy które możesz rozwiązać, ale które zajęłyby ci mnóstwo czasu, są szybciutko rozwiązywane przez komputer.
To, czego musisz się nauczyć, to sposób myślenia, który umożliwi ci ‘rozbijanie’ problemów na kroki, które musisz wykonać od stanu początkowego do rozwiązanego problemu. Musisz także poznać metody czynienia tych kroków. Naturalnie, ten podręcznik nie nauczy cię jak programować, a jedynie przedstawi ci język, za pomocą którego, będziesz mógł wpisać program.
Kto w takim razie nauczy cię programowania, jeśli nie wiesz jak? Hmm.. W pierwszym rzędzie inni wizardzi oraz ty sam. Innymi słowy ciężka praca. Niestety nigdy nie ma żadnych skrótów, niezależnie od tego, czego potrzebujesz się nauczyć. Jednakże ponieważ jest to świetna zabawa, miejmy nadzieję że zdobywanie tych umiejętności sprawi Ci wielką frajdę.
Kompilowany/interpretowany kod
Programy są niczym więcej, jak tylko plikami zawierającymi zbiór instrukcji zrozumiałych dla komputera. Programować oznacza wpisywać listy komend w taki sposób, by komputer doszedł do zamierzonego przez nas celu. Zazwyczaj program jest kompilowany – tłumaczony – na niskopoziomowy kod wyrażony w symbolach binarnych (wysokie i niskie stany napięć w pamięci komputera), które maszyna rozumie bez problemu. Język, w którym programujesz jest po prostu wygodnym kompromisem, który rozumiesz zarówno Ty jak i komputer. Przyczyna dla której kompilujesz, jest to, ze tłumaczenie kodu jest dość skomplikowane i zajmuje sporo czasu. Lepiej zrobić to raz, przechować wyniki i odwoływać się bezpośrednio do nich w razie potrzeby.
LPC jednakże nie jest kompilowany – jest interpretowany. Instrukcje są czytane i tłumaczone jedna po drugiej, wykonane i zapomniane. W sumie nie jest to tak do końca prawda. W rzeczywistości gamedriver tłumaczy program w LPC do prostej, przejściowej postaci kodu instrukcji. Ten zestaw kodów komend tworzy tak zwany ‘master object’ i jest przechowywany w pamięci komputera. Kiedy uruchamiasz program LPC, instrukcje sa przeglądane linia po linii, tak jak zostało to powyżej opisane, powodując wykonanie poprzednio określonego zestawu czynności zdefiniowanego przez instrukcje.
Różnica pomiędzy posiadaniem zinterpretowanego kodu, a posiadaniem skompilowanego polega na tym, ze o ile skompilowany kod jest szybszy, to zinterpretowana wersja jest o wiele łatwiejsza do zmodyfikowania. Jeśli chcesz dokonać zmiany w skompilowanej wersji, musisz zmienić kod źródłowy, zrekompilować, przechować nowa wersje i dopiero wypróbować. Gdy masz zinterpretowany kod, wystarczy ze zmienisz źródło i uruchomisz go. W LPC musisz jeszcze poinstruować gamedrivera, aby zniszczył stary ‘master object’, ale o tym później.
Programy
Programy LPC, jak zostało wyżej powiedziane, mają postać plików zawierających instrukcje dla komputera, napisane w języku LPC. Ich nazwy muszą się kończyć literami ‘.c’ (np. ‘test.c’), dzięki czemu gamedriver wie z czym ma do czynienia. Nazwa pliku może być dowolnym łańcuchem znakowym o długości mniejszej niż 32 znaki, zaczynającym się od litery alfabetu. W praktyce jednakże, lepiej jest ograniczać nazwy plików do 16 znaków, na które składają się tylko małe litery alfabetu. Jeśli chcesz by nazwa składała się z dwóch słów, możesz je oddzielić za pomocą znaku ‘_’ (np. ‘moja_nazwa_pliku.c’).
Obiekty
„Obiekt” w LPC jest po prostu załadowaną do pamięci komputera kopią istniejącego programu – jest jednym ucieleśnieniem jakiegoś kodu. Kiedy program jest ładowany do pamięci, by utworzyć „master object”, kod jest kompilowany i wytwarza poprzednio opisany zestaw instrukcji. Dołączany przy tym jest także mały obszar pamięci przeznaczony dla „zmiennych globalnych” (opisane dalej). Kiedy kopia, „klon” programu jest tworzona, specjalny odnośnik zwany „wskaźnikiem obiektu” (object pointer) jest kreowany. Dostaje on adres do master objectu oraz unikalny obszar pamięci. Kiedy klonujesz obiekt jeszcze raz, tworzony jest nowy wskaźnik i przydzielana jest nowa pamięć. Gdy obiekt jest niszczony, zaalokowana pamięć zostaje uwolniona, tak aby inne obiekty mogły z niej korzystać. Sam master object, czyli wyżej opisana lista instrukcji zostaje nienaruszona. Obiekt zawsze jest klonowany z master objectu. Jeśli chcesz klonować nowa wersję obiektu, najpierw musisz uaktualnić (update) master object, aby gamedriver wiedział, ze nowa lista instrukcji ma być kompilowana ze zmienionego kodu źródłowego.
W takiej sytuacji, istniejące klony nie będą zmieniane tylko z tego powodu, ze master object uległ zmianie. Będą miały odniesienie do starej listy instrukcji. Ważne jest abyś pamiętał, że zachowanie starych klonów nie zmienia się tylko dlatego, że uaktualniłeś master object. Jak widzisz jest możliwe posiadanie klonów obiektów, które się zachowują różnie, po prostu dlatego, że są one skompilowane z różnych źródeł, sklonowane pomiędzy uaktualnieniami i zmianami w kodzie. Może to być przyczyną dużych nieporozumień, więc pamiętaj o tym.
Struktura obiektu
Obiekt jest złożony z „funkcji” i „zmiennych”. Funkcja to zestaw instrukcji, do których można się odwołać za pomocą nazwy. Zmienna to swego rodzaju pojemnik, w którym można przechowywać dane do użytku przez funkcje. Kilka funkcji jest zdefiniowane z góry w gamedriverze i są one nazywane „funkcjami zewnętrznymi” (external functions), albo „efunkcjami” (efuns). Funkcje zdefiniowane w kodzie LPC są nazywane „lokalnymi funkcjami” (local functions), albo „lfunkcjami&rbdquo; (lfuns). Żeby jeszcze bardziej pogmatwać sprawę są funkcje, które zachowują się jak efunkcje, ale są napisane w kodzie LPC. Są one zwane „symulowanymi efunkcjami” (simulated efuns), albo „sfunkcjami” (sfuns).
Efunkcja to taka, której się nie da stworzyć w LPC. Na przykład funkcja write(), która umożliwia wyświetlenie tekstu na ekranie gracza. Niemożliwe jest stworzenie jej z innych funkcji LPC, więc musi ona być w gamedriverze. Ta efunkcja jest dostępna dla wszystkich programów LPC. Efunkcje nie wiedzą w jakim środowisku są używane i nie martwią się o to, czy są stosowane do symulowania smaku truskawki, czy jako część gry.
Funkcja taka jak ‘add_exit()’, która dodaje wyjście z pokoju jest dostępna tylko obiektom typu pokój i napisano ja w LPC. Lfunkcje z zasady są częścią struktury środowiska w jakim obiekty są użyte. Nasz przykładowy ‘add_exit()’ świetnie radzi sobie z takimi rzeczami, jak kierunki wyjść i koszty podróży (zmęczenie), ale jest ograniczony tylko do tego – nie potrafi nic więcej.
Funkcja ‘creator()’ jest dobrym przykładem trzeciego rodzaju. Jest ona dostępna wszędzie. Zwraca kto stworzył podany obiekt. Ta informacja jest jednak bardzo charakterystyczna dla środowiska, ponieważ używa takich pojęć jak lokalizacja kodu. Taki rodzaj funkcji jest łatwy do napisania w LPC, ale z drugiej strony musi być dostępny we wszystkich obiektach, tak jak by to były efunkcje. Z tej przyczyny został stworzony specjalny obiekt ‘/secure/simul_efun.c’, który jest dostępny ze wszystkich innych obiektów w grze. Znajdziesz tam wszystkie sfunkcje. To wszystko jest w sumie niewidzialne dla ciebie; ty po prostu używasz ich jako efunkcji, bez zawracania sobie głowy, że jest to jakaś sfunkcja.
Podstawy LPC
LPC jest bardzo podobny do języka C, choć można się dopatrzeć kilku różnic. Jak doświadczony programista zapewne dostrzeże, jest trochę uproszczony poprzez dodanie dla wygody kilku nowych typów i zestawu funkcji obsługujących je. Różnice nie są jednak na tyle poważne, by mogły spowodować jakieś problemy, o ile się o nich pamięta.
Komentarze
Może to trochę dziwnie wyglądać, że zaczynam akurat od tego, ale komentarze występują wszędzie, więc musisz umieć rozpoznawać je od samego początku.
Są dwa typy komentarzy:
<kod> // To jest komentarz trwający do końca tej linii. <kod> /* To jest komentarz ograniczony */ <kod>
Jak widzisz, pierwszy typ komentarzy zaczyna się od znaków ‘//’ i trwa aż do końca linii. Jeśli chcesz mieć więcej linii komentarzy, to musisz na początku każdej napisać ‘//’.
Drugi typ jest taki, że ma określoną długość. Zaczyna się od znaków ‘/*’ i kończy znakami ‘*/’. Ten rodzaj komentarzy jest użyteczny, gdy chcesz zawrzeć tekst, który zajmuje wiele linii.
UWAGA! Komentarz ‘/* */’ nie może być zagnieżdżony, tzn nie możesz zrobić czegoś takiego jak w tym przykładzie:
/* Komentarz /* Zagnieżdżony komentarz */ kontynuacja pierwszego */
W takiej sytuacji komentarz skończy się na pierwszym napotkanym ‘*/’, pozostawiając tekst ‘kontynuacja pierwszego */’ kompilatorowi, który będzie to próbował zinterpretować tak, jakby to był kod LPC. Oczywiście coś takiego nie zadziała i otrzymasz komunikat błędu.
Typy danych
Obiekty przechowują informacje w „zmiennych”. Jak sama nazwa wskazuje, są one oznaczonymi schowkami, które mogą magazynować informacje, które się zmieniają od czasu do czasu. Obiekt operuje na nich poprzez funkcje, które to zarówno używają jak i zwracają dane rożnych typów.
W zasadzie tylko jeden typ danych jest potrzebny, coś w rodzaju uniwersalnego pojemnika, który może przechowywać cokolwiek zechcesz. W rzeczywistości jest o wiele lepiej, jeśli rozróżnisz różne typy informacji od siebie. Może się wydawać, że to tylko przysporzy ci jeszcze więcej problemów, ale tak naprawdę to redukuje to ryzyko błędu, podnosi czytelność oraz znacznie przyspiesza kodowanie i odpluskwianie obiektu.
W LPC jest możliwość używania tylko danych ‘ogólnego użytku’, o których wcześniej mówiłem. We wcześniejszych wersjach języka, był to jedyny dostępny typ. Jednakże w LPC, którego używamy dziś, wielce korzystne jest unikanie tego jak się tylko da. Zaczynaj swe programy tą instrukcją:
#pragma strict_types
Mówi ona gamedriverowi by sprawdził wszystkie funkcje, czy są dostosowane do sytuacji w jakiej są używane i co za tym idzie, czy nie powodują żadnych błędów. Jest to wielka pomoc we wczesnym wykrywaniu błędów i dzięki temu nie będziesz musiał się później głowić, kiedy program nie będzie działał do końca tak, jak byś chciał.
Istnieją następujące typy danych:
- void
- ‘nic’
- Ten typ danych jest używany w funkcjach, które nie zwracają żadnych danych.
- int
- ‘liczba całkowita’
- Dowolna liczba całkowita z szerokiego zakresu, zależnego od mocy komputera, np. od -2147483648 do 2147483647. Może to być choćby 3, 17, -32, 999.
- float
- ‘liczba zmiennoprzecinkowa’
- Dowolna liczba wymierna mieszcząca się w bardzo szerokim zakresie, zależnym od mocy komputera, np. od 1.17549435e-38 do 3.40282347e+38, przykładowo 1.7, -348.4, 4.53e+4.
- Jeśli są tu jacyś miłośnicy FORTRANa, to niech uważają na to, ze numery typu ‘1.’ albo ‘.4711’ nie sa rozpoznawane jako zmiennoprzecinkowe. Musicie podać zarówno całkowitą, jak i ułamkową część, nawet jeśli któraś z nich jest zerowa.
- string
- ‘łańcuch znaków’
- Stringi to po prostu łańcuchy znaków (liczb, liter – na przykład słowa), zawarte w angielskich cudzysłowach, np. "x", "łańcuch", "Kolejny długi łańcuch z numerkiem 5 w środku". Stringi mogą zawierać specjalne znaki, takie jak znak nowej linii ("\n"). Wiele wyrażeń języka LPC może obsługiwać stringi bezpośrednio, w przeciwieństwie do C. Czyni to je bardzo użytecznymi i łatwymi w obsłudze.
- mapping
- ‘Lista powiązań’
- Mappingi to kolejny bardzo wygodny wynalazek LPC (uwaga, bardzo pamięciożerny, używaj go z umiarem). Mapping jest to lista połączonych ze sobą wartości. Załóżmy, że chcesz przechować wiek kilku osób, np. ze Olek ma 23 lata, Piotr 54, a Ania 17. W LPC można to zapisać w ten sposób:
([ "Olek":23, "Piotr":54, "Ania":17 ])
- Jak widzisz wartość po lewej stronie została powiązana z wartością po prawej. Możesz wyłowić jakieś powiązanie, poprzez podanie lewej strony.
- object
- ‘wskaźnik obiektu’
- Jest to odnośnik do klonu kodu LPC, który jest załadowany do pamięci.
- function
- ‘wskaźnik funkcji’
- Odnośnik do funkcji.
- Array
- ‘tablica’
- Wszystkie powyższe typy mogą występować jako tablice. Wtedy, w definicji, przed nazwą każdej zmiennej wstawia się ‘*’, np. ‘int *a, b;’ definiuje dwie zmienne: tablice a oraz pojedynczą zmienną b.
- W LPC tablice wyglądają bardziej jak listy, niż jak prawdziwe tablice. Istnieje wiele funkcji, które zostały napisane by przyspieszyć i ułatwić ich obsługę.
- mixed
- No i na koniec ogólny typ, który zastępuje wszystkie inne, swego rodzaju klucz uniwersalny. Pod zmienne mixed można podstawić wartość dowolnego innego typu. Jeszcze raz powtórzę, że używanie go poza sytuacjami, kiedy to jest absolutnie konieczne, tylko prowokuje błędy.
- Hmm, jak mi zwrócono uwagę, to co napisałem może brzmieć ciut za surowo. Typ mixed jest całkiem często używany. Chodziło mi o to, ze o ile da się zastosować jakiś normalny typ, to się go powinno użyć. Nie zastępuj go zmienną typu mixed tylko dlatego, ze jesteś leniwy.
Deklaracje zmiennych
Zmienna jest to łańcuch znaków identyfikujący ‘skrytkę’, w której dane są przechowywane. Skrytka ma podana nazwę, składającą się z maks. 32 znaków, gdzie pierwszy z nich musi być literą alfabetu. Ustalono, że wszystkie zmienne zdefiniowane wewnątrz funkcji będą się składały z samych małych liter. Globalne zmienne zaś, będą miały pierwszą literę dużą i resztę małą. Ustalono również, że nie będzie się oddzielało wyrazów znakami innymi niż ‘_’. Nazwy zmiennych zawsze powinny odzwierciedlać to, do czego są używane. Zmienne deklarujemy w następujący sposób:
<typ danych> <nazwa zmiennej>, <nast. zmienna>, ..., <ostatnia zmienna>; np: int licznik; float wysokość, waga; mapping map_wieku;
Zmienne muszą być zadeklarowane na początku bloku (tuż po znaku otwierającym blok ‘{’), przed kodem właściwym. Zmienne globalne, czyli takie, które są dostępne w całym programie, powinny być zadeklarowane na początku pliku.
Kiedy deklaracje są wykonywane w czasie uruchamiania programu, są one ustawiane na 0, a NIE na ich ‘puste’(null) wartości. Innymi słowy np. mappingi, tablice i stringi będą zawsze ustawiane na 0, a nie na ‘([])’, ‘({})’ i ‘""’ tak, jak byś mógł przypuszczać. Możliwe jest zainicjalizowanie zmiennych już w samej deklaracji. Jest to nawet bardzo dobry zwyczaj.
A robi się to tak:
<typ danych> <nazwa zmiennej> = <wartość>, itp. np. int licznik = 8; float wysokość = 3.0, waga = 1.2; mapping map_wieku = ([]); object *potwory = ({});
Powodem dla którego tablice i mappingi powinny być inicjalizowane na swoje wartości ‘puste’ (czyli kolejno ‘({})’ i ‘([])’) jest to, ze inaczej będą ustawione na 0, co może być przyczyną niekompatybilności typów i może spowodować później jakieś problemy.
Definicje funkcji
Funkcja musi dać znać jaki typ danych zwraca, o ile w ogóle coś zwraca. Tak jak zmienne, funkcje to są nazwy składające się z maks. 32 znaków, gdzie pierwszy z nich to litera. Przyjęte jest, że nazwy wszystkich funkcji będą pisane małymi literami, a do oddzielania wyrazów będzie się używalo tylko znaku ‘_’. Nazywaj funkcje tak, aby jasno odzwierciedlały to co robią. Deklaracja funkcji wygląda tak:
/* * Nazwa funkcji: <Nazwa funkcji> * Opis: <Co ona robi> * Argumenty: <Lista i krótki opis argumentów> * Zwraca: <Co funkcja zwraca> */ <typ zwracanej wartości> <nazwa funkcji>(<lista argumentów>) { <kod funkcji> }
/* * Function name: oblicz_srednice * Description: Oblicza średnicę okręgu przy podanym obwodzie * Variables: obwod - obwód okręgu * name - nazwa dana okręgowi * Returns: Srednice. */ float oblicz_srednice(float obwod, string nazwa) { float rval; // Obwod = pi * srednica rval = obwod / 3.141592643; write("Srednica okregu " + nazwa + " wynosi " + ftoa(rval) + "\n"); return rval; }
Argumenty są od siebie oddzielone przecinkami, tak jak w deklaracjach zmiennych. Ustalasz tu jaki typ danych będzie wysłany do funkcji i kojarzysz nazwę zmiennej z tym typem, do późniejszego użytku przez funkcje. Dane z argumentów, będą mogły być użyte tylko wewnątrz funkcji, no chyba, że wyślesz je na zewnątrz poprzez wywołanie innej funkcji.
(Żeby zachować trochę miejsca oraz zwiększyć przejrzystość podręcznika nie będę umieszczał nagłówka przed wszystkimi moimi krótkimi przykładami funkcji).
Funkcja, która nie zwraca żadnych danych, powinna mieć typ zadeklarowany jako ‘void’.
void write_all(string komunikat) { users()->catch_msg(komunikat); }
Instrukcje i wyrażenia
Żebym mógł cokolwiek wyjaśnić, wpierw określę co oznacza „instrukcja” (statement), a co „wyrażenie” (expression).
Instrukcje
Instrukcja jest swego rodzaju zdaniem, stworzonym z jednego lub więcej wyrażeń. Instrukcje zazwyczaj zajmują jedna linie kodu. Czasem zachodzi potrzeba złamania ich gdy są za długie, w celu zwiększenia czytelności. W większości instrukcji wystarczy, że złamiesz je w przerwie pomiędzy dwoma wyrazami. Inaczej jest w łańcuchach znakowych – w takim przypadku musisz wstawić backslash (‘\’) na końcu pierwszej łamanej linii. Robi się to po to, by gamedriver wiedział co jest grane.
write("To jest przykład \ złamanego łańcucha(stringa).\n");
Jednakże łamanie stringów za pomocą backslasha wygląda bardzo nieładnie i czyni tekst trudnym do przeczytania. Zazwyczaj jest możliwość łamania linii naturalnie, na końcu stringa, albo nawet podzielenia łańcucha w połowie i dodaniu obu części za pomocą operatora ‘+’. Tak naprawdę, jedynym miejscem gdzie backslash jest naprawdę niezbędny, to instrukcja ‘#define’ – ale o tym później.
write("To jest lepszy sposób " + "łamania stringów.\n");
Instrukcje w LPC zazwyczaj są zakończone średnikiem ‘;’. Jest to również dobre miejsce na zakończenie linii. Nic nie wstrzymuje cię przed zaczynaniem kolejnej instrukcji zaraz po poprzedniej, w tej samej linii, tylko że wygląda to potwornie.
Wyrażenia
Wyrażenie jest to opis jednej lub więcej czynności, których wykonanie powoduje uzyskanie danych jakiegoś rodzaju. Na przykład taki ‘+’. Używa on dwóch wyrażeń, które w sumie dają jakiś wynik. Zmienna też jest wyrażeniem, ponieważ przechowuje ona dane jako wynik. Następująca kombinacja dwóch wyrażeń i operatora tez jest poprawnym wyrażeniem: ‘a + b’ – gdzie ‘a’ oraz ‘b’ sa zmiennymi (wyrażeniami), a ‘+’ jest operatorem. ‘a = b + c;’ jest pełną instrukcją kończącą się na średniku ‘;’.
Wywołania funkcji są poprawnymi wyrażeniami. Są zapisane w postaci nazwy oraz pary nawiasów, w których znajdują się argumenty, które funkcja wykorzystuje wewnątrz. Na przykład funkcja ‘max()’, która zwraca wyższy z dwóch podanych argumentów. Aby znaleźć wyższą liczbę, np spośród ‘4’ i ‘10’, trzeba napisać ‘max(4, 10)’, co zwróci wartość ‘10’ i dzięki temu możemy to nazwać wyrażeniem. Oczywiście warto by jakoś ten wynik przechować.
Instrukcje grupujące (bloki)
Jest wiele instrukcji, na przykład instrukcje warunkowe, które w określonych warunkach tylko raz wykonają podaną instrukcję. Załóżmy, że w takim wypadku chcesz wykonać kilka instrukcji, a nie tylko jedną. Do tego celu istnieje specjalna instrukcja zwana blokiem (instrukcja grupująca). Znak ‘{’ definiuje początek bloku, a ‘}’ jego koniec. Wewnątrz tych nawiasów możesz umieścić tyle instrukcji (włączając w to definicje zmiennych), ile tylko zechcesz. Instrukcja grupująca nie kończy się średnikiem ‘;’ i nic nie zmieni, jeśli przez przypadek umieścisz jeden.
Instrukcja ‘;’
Jak już powiedziałem, ‘;’ jest przeważnie używany to zakańczania instrukcji, ale powinieneś wiedzieć, że średnik ‘;’ jest również instrukcją samą w sobie.
Sam ‘;’ będzie po prostu pustą instrukcją, która nie powoduje niczego. Jest ona użyteczna, gdy potrzebujesz zastosować pętle testowe (pętle będą opisane później), które będą się tylko wykonywały (i nie będą robiły przy tym żadnych innych rzeczy, jak to zwykle bywa z pętlami).
Zasięg i prototypy
„Zasięg” to termin określający gdzie funkcja lub deklaracja zmiennej jest poprawna. Ponieważ programy są czytane od góry do dołu, od lewej do prawej (zupełnie tak, jak ty czytasz tę stronę), zadeklarowane funkcje
i zmienne są dostępne na prawo i poniżej ich deklaracji. Zasięg ich, może być jednak ograniczony.
Zmienna zadeklarowana wewnątrz funkcji, jest dostępna aż do znaku zakończenia bloku ‘}’, w którym to ta zmienna jest zdefiniowana.
< Początek pliku > int GlownyLicznik; // Tylko GlownyLicznik jest tu dostępny void var_func(int arg) { int zmienna_1; // GlownyLicznik, arg oraz zmienna_1 są tu dostępne < kod > { string zmienna_2; // GlownyLicznik, arg, zmienna_1 i zmienna_2 są dostępne w tym // bloku < kod > } // GlownyLicznik, arg i zmienna_1 są tu dostępne < kod > { int zmienna_2; mapping zmienna_3; // GlownyLicznik, arg, zmienna_1, zmienna_2 oraz zmienna_3 są tu // dostępne // *UWAGA* zmienna_2 jest NOWA i nie ma nic wspólnego z tą // zdefiniowana funkcje wcześniej < kod > } // GlownyLicznik, arg oraz zmienna_1 są tu dostępne < kod > } // Tu tylko GlownyLicznik (i funkcja var_func) jest tu dostpny
Deklaracje funkcji obowiązuje ta sama zasada. Nie możesz tylko zadeklarować jednej funkcji wewnątrz drugiej. Wyobraż sobie jednak sytuacje, gdzie masz dwie funkcje i jedna z nich korzysta z drugiej.
int /* Definicja func_1. */ func_1() { < kod > func_2("test"); /* Wywołanie func_2 z argumentem "test". */ } void /* Definicja func_2. */ func_2(string dane) { < kod > }
I tu natrafiasz na problem, ponieważ pierwsza funkcja próbuje wykorzystać drugą, zanim tamta jest zadeklarowana. Jeśli poinstruowałeś gamedriver, żeby wymagał zgodności typów poprzez napisanie ‘#pragma strict_types’, to wyskoczy ci komunikat błędu. Aby tego uniknąć możesz przekomponować program w taki sposób, by ‘func_2’ była zadeklarowana _przed_ ‘func_1’. Czasem jednak nie zawsze jest to możliwe, a poza tym może na tym ucierpieć wygląd programu. Jest jeszcze inny, lepszy sposób. Polega on na napisaniu „prototypu funkcji”. Powinien on zostać umieszczony na początku pliku, tuz po instrukcjach ‘inherit’ i ‘#include’. Powinien wyglądać identycznie jak deklaracja funkcji.
A więc mamy:
< początek pliku, instrukcje `inherit' i `#include' > void func_2(string dane); // <- to jest prototyp funkcji func_2 < definicje zmiennych globalnych, itp. >
void func_1() { < kod func_1() > func_2("jakiś string"); // wywołanie func_2() }
void func_2(string dane) { < kod funkcji func_2 > }
Operatory (operator expressions)
W języku LPC jest zdefiniowana spora grupa operatorów, czyli takich cosików, które operują na wyrażeniach. Będę używał pewnego skrótowego sposobu notacji, dzięki któremu zaoszczędzi się sporo miejsca.
- ‘W’
- Będzie oznaczało dowolne wyrażenie, nawet bardzo pogmatwane.
- ‘Z’
- Będzie oznaczało jakąś zmienna.
Operatory różne
- (W)
- W jest obliczane przed robieniem czegokolwiek poza nawiasami. Jest to użyteczne w izolowaniu wyrażeń, które trzeba wykonać w jakiejś określonej kolejności, albo gdy nie jesteś pewien jaka jest kolejność działań (o tym później).
- W1, W2
- W1 jest wykonywane jako pierwsze i jego rezultat zostaje zapamiętany, wtedy wykonywane jest W2, a jego rezultat jest wyrzucany. Na koniec przechowany rezultat W1 jest zwracany jako wynik całego wyrażenia.
- Instrukcja ‘a = 1, 2, 3;’ ustawi wartość ‘a’ na ‘1’.
- Z = W
- Zmienna ma przypisywaną wartość W. Rezultatem całego wyrażenia jest również wartość W.
- ‘a = b = 4;’ ustali wartość a oraz b na 4. Może to być tez zapisane w ten sposób:
- ‘a = (b = 4);’, co ilustruje kolejność wykonywania.
Operatory arytmetyczne
- W1 + W2
- Wyrażenia są obliczane i ich rezultaty zostają do siebie dodane.
- Możesz sumować ze sobą integery, floaty, stringi, tablice i mappingi. Stringi, tablice i mappingi są po prostu ze sobą wiązane – początek drugiego jest przyłączany do końca pierwszego argumentu.
- Istnieje także możliwość dodawania integerów do stringów. W takiej sytuacji integer będzie przekonwertowany w stringa i doklejony do niego.
- W1 - W2
- W2 jest odejmowane od W1.
- Możesz odejmować integery, floaty i dowolne tablice, które mają ten sam typ. Jeśli element z odejmowanej tablicy istnieje w tej, od której się odejmuje, to jest on po prostu usuwany. Jeśli nie ma takowego, to tablica pozostaje nienaruszona.
- W1 * W2
- W1 jest mnożone przez W2.
- Działa tylko na integerach i floatach.
- W1 / W2
- W1 jest dzielone przez W2.
- Działa tylko na integerach i floatach.
- W1 % W2
- Reszta z dzielenia ‘W1 / W2’ jest zwracana.
- Działa to tylko dla integerow.
- ‘14 % 3’ zwróci 2, ponieważ ‘14 / 3 - 2 = 0’.
- -W
- Zwróci W z odwróconym znakiem.
- Działa to tylko dla integerow i floatów.
- W++ (lub ++W)
- Zwiększa W o 1.
- Jeśli plusy znajdują się przed wyrażeniem, to wyrażenie zwraca nowa wartość. Jeśli są za wyrażeniem, to zwraca jeszcze starą.
- W-- (lub --W)
- Zmniejsza W o 1.
- Jeśli plusy znajdują się przed wyrażeniem, to wyrażenie zwraca nowa wartość. Jeśli są za wyrażeniem, to zwraca jeszcze starą.
Operatory booleanowskie (binarne)
Operatorów booleanowskich można używać tylko na integerach. Wyjątkiem jest operator ‘&’, który może być zastosowany również na tablicach. Integer ma długość 32 bitów. Jednakże w przykładach będę ukazywał tylko 10 ostatnich bitów, jako ze pozostałe będą miały wartość 0 i będą one mogły być przez to zignorowane, z wyjątkiem operatora ‘~’.
1. W1 & W2 W1 i W2. (iloczyn logiczny)
1011101001 (= 745) 1000100010 & (= 546) ------------ 1000100000 (= 544) => 745 & 546 = 544
Użyty na dwóch tablicach, zwróci nam nowa tablice zawierającą wszystkie elementy, które wstępują w obu tablicach jednocześnie.
2. W1 | W2 W1 lub W2. (suma logiczna)
1011101001 (= 745) 1000100010 | (= 546) ------------ 1011101011 (= 747) => 745 | 546 = 747
3. W1 ^ W2 W1 xor (exclusive or - nierównoważność; LUB wykluczające) W2.
1011101001 (= 745) 1000100010 ^ (= 546) ------------ 0011001011 (= 203) => 745 ^ 546 = 203
4. ~W 1-complement of E (odwrócenie W).
00000000000000000000001011101001 ~ (= 745) ---------------------------------- 11111111111111111111110100010110 (= -746) => ~745 = -746
5. W1 << W2 W1 jest przesuwane w lewo o W2 kroków.
5 << 4 => 101(b) << 4 = 1010000(b) = 80
6. W1 >> W2 W1 jest przesuwane w prawo o W2 kroków.
1054 >> 5 => 10000011110(b) >> 5 = 100000(b) = 32
Operatory warunkowe (logiczne)
1. W1 || W2 Zwraca prawdę jeśli W1 lub W2 zwraca prawdę. Nie obliczy W2 jeśli W1 zwraca prawdę.
2. W1 && W2 Zwraca prawdę, jeśli zarówno W1 jak i W2 zwraca prawdę. Nie wykona W2 jeśli W1 zwraca fałsz.
3. !E Zwraca prawdę, jeśli W jest fałszywe i vice versa.
Operatory porównawcze
1. W1 == W2 Zwraca prawdę, jeśli W1 jest równe W2. Może być użyty na wszystkich typach. Obejrzyj specjalny rozdział o tablicach i mappingach, gdyż ten operator działa na nich inaczej, niż by ci się mogło wydawać.
2. W1 != W2 Zwraca prawdę jeśli W1 jest różne od W2. Może być użyty na wszystkich typach. Obejrzyj specjalny rozdział o tablicach i mappingach, gdyż ten operator działa na nich inaczej, niż by ci się mogło wydawać.
3. W1 > W2 Zwraca prawdę, jeśli W1 jest większe od W2. Może być użyte na wszystkich typach poza tablicami i mappingami.
4. W1 < W2 Zwraca prawdę, jeśli W1 jest mniejsze od W2. Może być użyte na wszystkich typach z wyjątkiem tablic i mappingów.
5. W1 >= W2 Zwraca prawdę, jeśli W1 jest większe lub równe W2. Może być użyte na wszystkich typach poza tablicami i mappingami.
6. W1 <= W2 Zwraca prawdę, jeśli W1 jest mniejsze lub równe W2. Może być użyte na wszystkich typach z wyjątkiem tablic i mappingów.
Podwójne operatory przypisania
Wszystkie arytmetyczne i booleanowskie operatory można zapisać w prostszy sposób, o ile chcesz jedynie obliczyć działanie jednej zmiennej z jakimś wyrażeniem, a wynik przechować w tej zmiennej.
Powiedzmy, że chcesz wykonać ‘a = a + 5;’. Lepiej można to zapisać tak: ‘a += 5;’. Wartość drugiego wyrażenia jest dodawana do pierwszego i w nim przechowana (czyli w tym przypadku w zmiennej a).
Wszystkie inne operatory można zapisać w ten sam sposób, tj. najpierw zmienna, potem ‘=’ poprzedzone operatorem, a na końcu wyrażenie.
a >>= 5; /* a = a >> 5; */ b %= d + 4; /* b = b % (d + 4); */ c ^= 44 & q; /* c = c ^ (44 & q); */
Priorytet operatorów i kolejność wykonywania działań
W poniższej tabeli zawarte są reguły dotyczące kolejności i łączenia wszystkich operatorów, włączając w to te, o których jeszcze nie było mowy. Operatory będące w tej samej linii w tabeli mają taki sam priorytet, a operatory w kolejnych rzędach mają malejąca ważność. Na przykład ‘*’, ‘/’ i ‘%’ mają taki sam priorytet i jest on wyższy od tego, który mają ‘+’ i ‘-’.
Zauważ, że priorytet logicznych operatorów bitowych ‘&’, ‘^’ i ‘|’ jest niższy od priorytetu ‘==’ i ‘!=’. Powoduje to, że wyrażenia testujące bity, takie jak
if ((x & MASKA) == 0) ...
powinny być całe ujęte w nawiasy, żeby dały poprawne wyniki.
1. () [] Od lewej do prawej 2. ! ~ ++ -- - (typ) * & Od prawej to lewej
3. * / % Od lewej do prawej
4. + - Od lewej do prawej
5. << >> L->P
6. < <= > >= L->P
7. == != L->P
8. & L->P
9. ^ L->P
10. | L->P
11. && L->P
12. || L->P
13. ?: L->P
14. = += == etc. P->L
15. , L->P
Instrukcje warunkowe
W LPC często korzysta się z instrukcji warunkowych. Jest wiele metod ich zapisywania. Bardzo ważną rzeczą jest to, ze w testach zero ‘0’ jest traktowane jako „falsz”, zaś dowolna inna wartość jako „prawda”. Oznacza to, iż puste tablice ‘({})’, puste stringi ‘""’ oraz puste mappingi ‘([])’ również są „prawda”, ponieważ nie są ‘0’. Jeśli chcesz poznać ich rozmiar, albo zbadać ich zawartość, to musisz się posłuzyć specjalnymi funkcjami - o nich będzie mowa później.
Instrukcje if/else
‘if’ stanowi najczęściej spotykana instrukcję warunkową. Jest łatwa w użyciu i może być łączona z instrukcją ‘else’ w razie potrzeby obsłużenia obu wyników testu (tj. fałszu i prawdy). Stosuje się ją w taki sposób:
if (wyrażenie) instrukcja; np. if (a == 5) a -= 4;
Jeśli chcesz obsłużyć fałszywy wynik testu (jest on wtedy, gdy wyrażenie w nawiasie zwróciło fałsz), to musisz dodać intrukcję ‘else’:
if (wyrażenie) instrukcja_prawda else instrukcja_fałsz; np. if (a == 5) a -= 4; else a += 18;
Jeśi zmienna ‘a’ będzie się równała 5, to od niej zostanie odjęte 4, w przeciwnym wypadku, do zmiennej ‘a’ zostanie dodane 18.
Pamiętaj, że możesz w miejsce instrukcji wstawić również jakiś blok.
Instrukcja switch
Jeślibyś chciał przetestować jedną zmienną na wiele różnych wartości, to skończyłoby się to na dosyć długiej liście instrukcji ‘if-else-if-else’. Jednakże, jeśli typ wartości na które testujesz zmienną jest integerem, floatem lub stringiem to możesz użyc o wiele lepszej i bardziej skondensowanej metody testowania. Załóżmy, że chcesz napisać następujący kod:
if (imie == "lewy") // jeśli imie == "lewy" to... { miasto = "lu"; opis = "wesoły"; } else if (imie == "alvin") // w przeciwnym wypadku, jeśli imie == "alvin" { miasto = "wa"; opis = "bursztynooki"; } else if (imie == "athmagor") // jeśli nie "alvin" i nie "lewy", to może "athmagor" { miasto = "lu"; opis = "piekny"; } else { miasto = "x"; opis = "nieznany"; }
Lepszym sposobem na zapisanie tego jest:
switch (imię) { case "lewy": miasto = "lu"; opis = "wesoły"; break; case "alvin": miasto = "wa"; opis = "bursztynooki"; break; case "athmagor": miasto = "lu"; opis = "piękny"; break; default: miasto = "x"; opis = "nieznany"; }
Ta instrukcja działa bardzo prosto: ‘switch’ bierze wyrażenie spomiędzy nawiasów i porównuje je z każdym z wyrażeń występujących po ‘case’.
- UWAGA Wyrażenie po ‘case’ musi być wartością stałą. Nie może to być
zmienna, wywołanie funkcji, ani żadne inne wyrażenie tego typu.
Po tym, jak kompilator stwierdzi, że wartość w nawiasie równa się wartości po ‘case’, wszystkie instrukcje występujące po ‘case’ zostaną wykonane, aż do momentu kiedy natrafi na instrukcję ‘break’. Jeśli nie znajdzie żadnych równających się wartości, wykonywane zostaną instrukcje po ‘default’.
- UWAGA! Nie ma przymusu pisania części ‘default’. Jest to jednak bardzo
polecane, gdyż wykonanie jej zazwyczaj oznacza, że stało się coś, co nie było przewidziane w czasie pisania programu. Jeśli uważasz, że testowana zmienna będzie przyjmowała jakiś ograniczony zestaw wartości, to warto mieć w zapleczu jakiś komunikat błędu, który zostanie wyświetlony użytkownikowi, gdy stanie się coś nieoczekiwanego.
Gdy zapomnisz umieścić komendy ‘break’, kolejne wyrażenia ‘case’ będą wykonywane. Może to nie brzmieć tak, jakbyś chciał, ale na przykład jeśli powyższe imiona ‘alvin’ i ‘lewy’ miałyby wygenerować wspólne czynności, to mógłbyś napisać:
case "lewy": /* nie ma break */ case "alvin": < kod > break;
... i oszczędzić trochę miejsca. Używanie switcha nie czyni kodu szybszym, jedynie zwiększa czytelność i zmniejsza ryzyko popełnienia jakiegoś błędu w czasie pisania. Pamiętaj umieścić jakiś komentarz, w miejscu gdzie powinien być break, gdyż potem możesz zapomnieć o tym, iż specjalnie go nie umieściłeś. Dobrym pomysłem jest wstawianie jednej pustej linii po każdym breaku, żeby dać ‘chwile oddechu’, co zwiększy czytelność.
Wyrażenie ?:
Jest to bardzo skondensowana forma pisania instrukcji ‘if/else’ i zwracania różnych wartości w zależności od tego, jak wypadł test. Oczywiście nie jest to instrukcja, lecz wyrażenie, ponieważ zwraca jakąś wartość. Nie mówiłem o tym jak omawiałem wyrażenia, bo było by mi ciężko to wytłumaczyć, przed wyjaśnianiem instrukcji ‘if/else’.
Załóżmy, że chcesz napisać coś takiego:
if (testowane_wyrażenie) zmienna = wyrażenie_if; else zmienna = wyrażenie_else;
Możesz to napisać w znacznie bardziej skondensowany sposób:
zmienna = testowane_wyrazenie ? wyrazenie_if : wyrazenie_else; np. nazwa = dzień == 2 ? "wtorek" : "inny dzień";
Jest kwestią sporną, czy ten sposób zapisywania czyni kod bardziej, czy mniej czytelnym. Ale myślę, że jedno wyrażenie tego typu ułatwia czytanie kodu, zaś kombinacja kilku pogarsza tylko czytelność. Coś takiego, jak w następującym przykładzie z pewnosciś nie jest ulepszeniem:
nazwa = dzień == 2 ? godzina == 18 ? "czas na kwas" : "wtorek" : "inny dzień";
Pętle
Pętla to rodzaj instrukcji, która wykonuje określone czynności dopóki nie zostaną spełnione pewne wcześniej zaprogramowane warunki. Są dwa rodzaje pętli i oba używają instrukcji warunkowych.
Instrukcja while
Instrukcja while jest bardzo prosta w użyciu. Już z samej nazwy (while oznacza „dopoki”) można łatwo wywnioskować co ona robi. Będzie ona cały czas wykonywała inna instrukcje, dopóki wyrażenie kontrolne nie zwróci fałszu. Ma ona następującą składnię:
while (<wyrazenie_kontrolne>) instrukcja_wykonywana;
Pamiętaj, że wyrażenie kontrolne jest sprawdzane na samym początku, jeszcze przed wywołaniem instrukcji po raz pierwszy. Jeśli już na samym początku wyrażenie kontrolne zwróci fałsz, to instrukcja nigdy nie zostanie wykonana.
a = 0; while (a != 4) { a += 5; a /= 2; }
Dopóki a ma wartość różną od 4, jest do niego dodawane 5, a potem jest dzielone przez 2. (Wiem, wiem, ten przykład nie ma najmniejszego sensu :)).
Instrukcja for
Jeśli potrzebujesz zwykłego licznika, to powinieneś użyć instrukcji ‘for’. Ma ona następującą składnię:
for (instrukcja_inicjalizujaca; wyrazenie_kontrolne; instrukcja_konca_cyklu) instrukcja_powtarzana;
Całą instrukcję można zapisać w inny równoważny sposób, za pomocą instrukcji while:
instrukcja_inicjalizujaca; while (wyrazenie_kontrolne) { instrukcja_powtarzana; instrukcja_konca_cyklu; }
Na samym początku, instrukcja ‘for’ wykonuje instrukcję inicjalizującą. Stosuje się ją do początkowego ustawienia liczników, lub innych wartości używanych w czasie wykonywania pętli. Od tego momentu trwa jej właściwa część. Każdy cykl zaczyna się od obliczenia „wyrażenia_kontrolnego” i sprawdzenia jego wyniku. Jeśli jest prawdziwy, czyli wyrażenie zwróci od razu po niej instrukcja_konca_cyklu. W instrukcji_powtarzanej zawarte jest zazwyczaj to, co się chce by pętla cyklicznie wykonywała, a w instrukcji_konca_cyklu zazwyczaj zwiększa lub zmniejsza się liczniki.
W poprzednim ustępie wiele razy użyłem słowa zazwyczaj. Zrobiłem to dlatego, że nie musisz robić tego w ten sposób; nie ma niczego co by cię przymuszało do takiego wykorzystania tej instrukcji, jaki podałem.
Załóżmy, że chcesz obliczyć sumę wszystkich liczb całkowitych (integerów) z przedziału od 7 do 123 i nie znasz na to wzoru ((x1^2 + x2^2) / 2). Najłatwiejszą (co nie oznacza najefektywniejszą) metodą, jest użycie pętli.
wynik = 0; for (licznik = 7 ; licznik < 124 ; licznik++) wynik += licznik;
Na początku wynik jest zerowany. Po tym następuje właściwa część instrukcji ‘for’. Zaczyna się od ustawienia zmiennej licznik na 7. Teraz następuje wejście do pętli i test, czy licznik (równy 7) jest mniejszy niż 124. Jest tak, więc wartość licznika jest dodawana do zmiennej. Zmienna licznik jest zwiększana o jeden i następuje ponowne wejście do pętli. Dzieje się tak, do momentu aż wartość licznika osiągnie 124. Ponieważ nie jest on wtedy mniejszy od 124, wykonywanie pętli zostaje zakończone.
- UWAGA! Wartość licznika po wykonaniu instrukcji ‘for’ jest równa 124,
a nie 123, jak niektórzy ludzie sądzą. wyrazenie_kontrolne musi zwrócić fałsz, żeby pętla mogła się skończyć i co za tym idzie licznik musi mieć wartość 124.
Instrukcje break i continue
Czasem, podczas wykonywania instrukcji ‘switch’, ‘for’ lub ‘while’ zachodzi potrzeba wyjścia z pętli i kontynuacji poza nią. W tym celu używa się instrukcji ‘break’.
while (warunek_koncowy < 9999) { // Jeśli funkcja time() zwróci 29449494, to przerwij wykonywanie // pętli if (time() == 29449494) break; < kod > } // Kompilator wykonuje ten kod zarówno po użyciu instrukcji `break', // jak i gdy pętla się skończy. < kod >
Nieraz chcesz, by pętla wykonała się dodatkowo podczas użycia instrukcji ‘for’ lub ‘while’. Do tego celu używa się instrukcji ‘continue’.
// Dodaje wszystkie parzyste liczby suma = 0; for (i = 0 ; i < 10000 ; i++) { // Zacznij pętle od początku, gdy `i' zwraca nieparzysta liczbę if (i % 2) continue; suma += i; }
Każde użycie continue w tym przykładzie powoduje przeskoczenie jednego cyklu, gdyż licznik zostaje zwiększony, ale zasadnicza część kodu pozostaje nie wykonana, bo instrukcja ‘continue’ znajduje się przed nią.
Tablice i Mappingi
Nadszedł czas na zagłębienie się w dwa specjalne typy: „tablice” i „mappingi”. Ich zastosowanie może wyglądać podobnie, lecz w rzeczywistości bardzo się od siebie różnią.
Dla obu typów napisano wiele użytecznych efunkcji, które operują i pobierają z nich informacje. Jednakże zostaną opisane one później, a na razie przedstawiś tylko kilka z nich.
Jak używać i deklarować tablice
Tablice w LPC to tak naprawdę nie są prawdziwe tablice. Lepiej określa je to, że są to listy z określonym porządkiem. Różnica może się wydawać niewielka, lecz dla informatyków jest ogromna.
Tablice mają określony typ, co oznacza, że mogą przyjmować tylko takie elementy, u których jest on zgodny z tablicą. Innym ograniczeniem jest to, że tablice mogą być tylko jednowymiarowe. Istnieje jednak możliwość obejścia tego, gdyż tablice z elementami typu mixed mogą przyjmować wartości dowolnego typu, czyli nawet inne tablice. Powinieneś jednak pamiętać o ostrożnym posługiwaniu się typami tablic, żeby nie popełnić jakiegoś błędu.
Tablice definiuje się w ten sposób:
<typ> *<nazwa_tablicy>; np. int *moja_tab, *twoja_tab; float *inna_tab; object *tab_ob;
Początkową wartością tych tablic jest ‘0’, a nie pusta tablica. Powtarzam: są one ustawiane na zero – ‘0’, nie na pustą tablicę – ‘({ })’. Pamiętaj o tym!
Tablice alokuje i inicjalizuje się w ten sposób:
<tablica> = ({ elem1, elem2, elem3, ..., elemN }); np. moja_tab = ({ 1, 383, 5, 391, -4, 6 });
Dostęp do elementów tablicy można uzyskać dzięki podaniu numeru indeksu w nawiasach, po nazwie zmiennej zawierającej tablice. (Załóżmy, że zmienna wart jest zadeklarowana jako integer).
<zmienna> = <tablica>[<indeks>]; np. wart = moja_tab[3];
LPC, tak samo jak C, zaczyna liczenie od 0 i przez to czwarty element ma numer 3.
Żeby ustawić wartość już istniejącego elementu na nową, używa się operatora ‘=’.
moja_tab[3] = 22; // => ({ 1, 383, 5, 22, -4, 6 }) moja_tab[3] = 391; // => ({ 1, 383, 5, 391, -4, 6 })
W celu pobrania wycinka tablicy, wystarczy w nawiasach podać od którego, do którego elementu chce się wyciąć.
<tablica> = <inna_tablica>[<odkąd>..<dokąd>]; np. twoja_tab = moja_tab[1..3];
... spowoduje, że nowa tablica ‘twoja_tab’ będzie miała wartość ‘({ 383, 5, 391 });’. Jeśli starej tablicy przypiszesz nową wartość, poprzednia zawartość zostanie stracona.
np.
moja_tab = ({ });
... spowoduje, że ‘moja_tab’ stanie się pustą tablicą. Stara zostanie zdealokowana, a pamięć przez nią zajęta uwolniona.
Jeśli podasz indeks, który wyjdzie poza rozmiar tablicy, to wyskoczy błąd i wykonywanie obiektu zostanie przerwane. Jednakże, gdy indeks przy podawaniu zasięgu, np. indeks dokąd wyjdzie poza tablice, to błąd nie zostanie zakomunikowany, zasięg zaś zostanie tak ograniczony, by się zmieścił w rozmiarze tablicy.
Jeśli chcesz stworzyć pustą tablice, z wartościami ustawionymi na 0 (typ nie gra roli) o określonej długości, to możesz użyć efunkcji ‘allocate()’.
<tablica> = allocate(<wielkość>); np. twoja_tab = allocate(3); // => twoja_tab = ({ 0, 0, 0 });
Dodawanie tablic do siebie daje się łatwo dokonać za pomocą operatora ‘+’. Wystarczy, że zsumujesz je tak, jak się sumuje liczby. Operator ‘+=’ również do tego celu świetnie pasuje.
moja_tab = ({ 9, 3 }) + ({ 5, 10, 3 }); // => ({ 9, 3, 5, 10, 3 })
Użycie operatorów ‘-’ i ‘-=’ to najłatwiejszy sposób na usuwanie elementów z tablicy. Uważaj jednakże, gdyż operator ten usunie wszystkie elementy, które będą miały wartość taką samą jak elementy, które chcesz usunąć.
moja_tab -= ({ 3, 10 }); // => ({ 9, 5 })
I jeszcze mały przykład na wycinanie i doklejanie elementów tablic.
moja_tab = ({ 9, 3, 5, 10, 3 }); moja_tab = moja_tab[2..4] + moja_tab[0..0]; // => ({ 5, 10, 3, 9 })
<tablica> moja_tab[0..0] // = ({ 9 }) <int> moja_tab[0] // = 9
UWAGA! Zwróć uwagę na róznicę!!! W pierwszej linii jest tablica, a w drugiej integer!
Jak deklarować i używać Mappingi
Mapping to lista powiązanych par wartości. Mają one wszystkie typ ‘mixed’. Oznacza to, że część indeksowa pary nie musi mieć przez cały czas tego samego typu. Jest to jednak bardzo doradzane, z powodu błędów jakie nieostrożne używanie typu ‘mixed’ może spowodować.
Zaröwno indeks, jak i wartość może przyjmować dowolny typ. Istnieje tylko jedno ograniczenie tyczące się indeksów – nie może być dwóch indeksów o takiej samej wartości w jednym mappingu.
Może to trochę pogmatwanie brzmieć z początku, ale tak naprawdę to to jest bardzo banalne. Chyba łatwiej zrozumieć będzie jednak, jeśli od razu przystąpie do ilustrowania tego przykładami.
Mappingi deklaruje się tak jak wszystkie inne zmienne. Zacznijmy więc od kilku prostych deklaracji.
mapping moj_map; int wartość;
Alokować i inicjalizować można na trzy różne sposoby.
1: <mapping> = ([ <indeks1>:<wartosc1>, <indeks2>:<wartosc2>, ... ]); 2: <mapping>[<indeks>] = wartość; 3: <mapping> = mkmapping(<tablica indeksów>, <tablica wartości>);
Pierwszy sposób jest prosty i naturalny.
1: my_map = ([ "adam":5, "brunon":8, "czeslaw":-4 ]);
Działanie drugiego sposobu polega na tym, że gdy nie istnieje w mappingu para o podanym indeksie – to jest tworzona, jeśli istnieje – to wartość pary jest zastępowana nową.
2: my_map["adam"] = 1; // Tworzy parę "adam":1 my_map["brunon"] = 8; // Tworzy parę "brunon":8 my_map["adam"] = 5; // Zamienia starą wartość w "adam" na 5. ...
Trzeci sposób wymaga dwóch tablic – jednej zawierającej indeksy, a drugiej przechowującej wartości. Tworzenie tablic zostalo opisane w poprzednim rozdziale.
3: moj_map = mkmapping( ({ "adam", "brunon", "czeslaw" }), ({ 5, 8, -4 }) );
W przeciwieństwie do tablic, mappingi nie mają ustalonego porządku. Wartości są poukładane w taki sposöb, by maksymalnie skrócić czas ich wyszukania. Istnieją funkcje, które umożliwiają pobranie listy elementów (indeksów, lub wartości) z mappinga. Pamiętaj tylko, że mogą one bbyć yc w dowolnej kolejności i nigdy nie ma pewności, że będą tak samo ułożone pomiędzy dwoma wywołaniami funkcji „pobierających”. W praktyce jednak, kolejność jest zmieniana tylko w czasie dodawania lub usuwania elementów.
Mappingi łączy się za pomocą operatorów ‘+’ i ‘+=’, zupełnie tak samo jak w przypadku tablic.
moj_map += ([ "dobroslawa":5, "edmund":33 ]);
Usuwanie elementów z mappingu już nie jest takie proste. Służy do tego specjalny efun ‘m_delete()’ (również opisany dalej).
moj_map = m_delete(moj_map, "brunon"); moj_map = m_delete(moj_map, "dobroslawa");
Jak widzisz, elementy usuwa się po kolei, podając indeks jako identyfikator pary. Inna rzeczą, którą sobie musisz szybko uświadomić jest to, że indeksy muszą być unikalne, nie może być dwóch takich samych „uchwytów” do par. Jednakże wartości oczywiście mogą być takie same.
Poszczególne wartości można pobrać poprzez zwykłe podanie indeksu.
wartość = moj_map["czeslaw"]; // => -4
Próba pobrania elementu o nieistniejącym indeksie nie wywoła żadnego błędu, a jedynie zwróci 0. Musisz więc być bardzo ostrożny, gdyż 0 może oznaczać różne rzeczy, tzn. wartość 0 może oznaczać, że element o podanym indeksie nie ma części z wartością, ale może oznaczać też, że wartością jest 0.
wartość = moj_map["rgojhijaeh"]; // => 0
Preprocesor
Preprocesor nie jest częścią języka LPC. Jest to specjalny proces, który jest uruchamiany przed rozpoczęciem kompilacji. Można go postrzegać jako bardzo sprytnego „podstawiacza” stringów; Określone łańcuchy znakowe są zastępowane innymi.
Pierwszym znakiem każdej dyrektywy preprocesora jest ‘#’. Poza tym muszą się one zaczynać w pierwszej kolumnie (na samym początku linii). Możesz je umieścić gdziekolwiek w kodzie, acz jak się dowiesz później, większość z nich jest przypisana początkowi programu.
Instrukcja #include
Jest to najczęściej używana dyrektywa preprocesora. Mówi ona mu, aby zastąpił linie w której ona się znajduje zawartością całego pliku, o nazwie podanej po instrukcji.
Dane, które umieszczasz we włączanym pliku to takie, których raczej nie będziesz nigdy zmieniał i takie, które będziesz włączał w wiele plików. Zamiast wpisywania tego samego tekstu w różne pliki w nieskończoność i co za tym idzie zwiększania szansy na jakieś kretyńskie błędy, po prostu zbierasz powtarzające się dane w jeden lub kilka plików i włączasz(include) je w te programy, w które trzeba.
Składnia jest bardzo prosta:
#include <plik_standardowy> #include "plik_dodatkowy"
UWAGA! Zauważ, że nie ma ‘;’ na końcu linii!
Dwa różne sposoby zapisywania nazw plików zależą od tego, gdzie się one znajdują. Jest spora liczba standardowych bibliotek w grze, rozrzuconych po sporej liczbie katalogów. Zamiast zapamiętywania dokładnie gdzie one są, wystarczy że podasz sama nazwę pliku, który chcesz włączyć.
#include <stdproperties.h> #include <adverbs.h>
Gdy chcesz włączyć jakieś pliki własne, które nie są żadną standardową biblioteką, to musisz dokładnie podać ich lokacje. Możesz to zrobić zarówno poprzez podanie ścieżki od katalogu, gdzie się znajduje program, albo poprzez podanie pełnej, absolutnej ścieżki od głównego katalogu.
#include "/d/Standard/login/login.h" #include "moje_def.h" #include "/sys/adverbs.h" // Ten sam, co krótszy przykład powyżej
Gdy chcesz włączyć standardowe biblioteki, to zawsze używaj notacji < >. (czyli podawaj sama nazwę biblioteki, ujętą w nawiasy < > ). Powodem nie jest tylko to, że tak jest krócej, ale to, że gdy biblioteki zostaną przemieszczone gdzie indziej, twój program przestanie działać. Gdy użyjesz notacji <> to zawsze zostaną znalezione.
Włączane pliki mogą mieć dowolną nazwę, ale ustalono, że będą miały końcówkę ‘.h’, żeby moc je jasno odróżnić od innych plików.
Jest nawet możliwe włączenie plików ‘c’, tzn. całych plików zawierających programy. Jednakże, jest to bardzo zła rzecz. Nie rób tego NIGDY! Czemu? Po pierwsze programy śledzące błędy gubią numeracje linii we włączanych plikach i przez to podają złe numery linii. Po drugie, gdy włączasz nieskompilowany kod w wiele różnych obiektów, marnujesz pamięć oraz CPU, gdyż ten sam plik musi być wielokrotnie kompilowany i przechowywany osobno, dla każdego obiektu, który go używa. A poza tym samo czytanie takiego programu może być istną torturą.
Co ma więc tak naprawdę rozszerzenie nazwy pliku do jego zawartości? Tak naprawdę to nic nie ma... Jednakże jest przyjęte, że kod i funkcje są przechowywane w plikach z rozszerzeniem ‘.c’, a definicje z rozszerzeniem ‘.h’. Mudlib zazwyczaj korzysta z tego rozdziału i może nie rozpoznawać jako kodu źródłowego niczego, poza plikami z końcówką ‘c’.
Instrukcja #define
Jest to bardzo potężne „makro”, komenda preprocesora, która jest ciągle nadużywana. Mądrze postąpisz, jeśli będziesz używał jej ostrożnie i tylko do prostych rzeczy.
Ma ona następującą składnię:
#define <identyfikator> <tekst zastępujący> #undef <identyfikator>
Dowolny taki sam tekst w pliku jak ‘<identyfikator>’ zostanie zastąpiony ‘<tekstem zastępującym>’, jeszcze przed kompilacją. ‘#define’ jest ważne od linii, w której zostało zdefiniowane do końca pliku, albo do momentu wykonania komendy ‘#undef’, która usuwa makro.
Pomimo tego, że makrem może być dowolny tekst, jest w zwyczaju(rób tak!), że nazwę makra pisze się wyłącznie dużymi literami. Jest to po to, żeby można było wyróżnić makra w tekście, w którym każdy(ty tez!) pisze nazwy funkcji i zmiennych małymi literami.
Umieszczaj wszystkie definicje na początku pliku, albo biedny koleś, który będzie ci pomagał w wyłapaniu błędów, będzie miał prawdziwe piekiełko z poszukiwaniem pochowanych definicji. Jeśli to będzie ktoś, kogo sam poprosiłeś o pomoc (gdyż masz błędy powstałe z powodu brzydko napisanego kodu), to najprawdopodobniej powie ci żebyś sobie wsadził taki program w wiadome miejsce i wrócił dopiero wtedy, jak się nauczysz poprawnie i ładnie pisać.
Prostymi definicjami są na przykład ścieżki, nazwy i wszystkie inne stale dowolnego rodzaju, których nie chcesz w kółko zapisywać, albo chcesz mieć możliwość łatwej ich modyfikacji, bez zmieniania tego samego w dziesiątkach miejsc.
MAX_LOGIN 100 /* Maksymalna liczba zalogowanych graczy */ LOGIN_OB "/std/login" /* Obiekt logowania */ POWIT_TEKST "Witamy!" /* Komunikat logowania */
Gdziekolwiek wystąpi identyfikator makra, zostanie on zastąpiony tym, co się znajduje w definicji pomiędzy identyfikatorem, a końcem linii. Podchodzą pod to także komentarze, które jednak i tak zostaną później wyrzucone.
tell_object(gracz, POWIT_TEKST + "\n");
Komentarz typu ‘//’ nie jest w takiej sytuacji dobra rzeczą, gdyż kończy się on dopiero na końcu linii.
POWIT_TEKST "Witamy!" // Komunikat logowania
...będzie zamienione w poprzednim przykładzie na:
tell_object(gracz, "Witamy!" // Komunikat logowania + "\n");
...co spowoduje ujęcie w komentarz wszystkiego, co wystąpi po ‘//’, aż do końca linii.
Gdy makro wychodzi poza linie, możesz przedłużyć ją za pomocą znaku ‘\’ który zaznacza, że definicja jest kontynuowana w następnej linii. Jednakże musisz zakończyć linie tuż po ‘\’ i NIE nie może być za nim żadnych spacji, ani innych znaków.
DLUGA_DEFINICJA "początek stringa \ i jego koniec."
Definicje naśladujące funkcje są często stosowane i nieraz nadużywane. Jedyna naprawdę ważną zasadą, obowiązującą przy pisaniu takich makr jest to, że każdy argument musi być ujęty w nawiasy. Jeśli napiszesz inaczej, to możesz otrzymać bardzo dziwne wyniki.
1: POMNOZ_TO(a, b) a * b /* Źle */ 2: POMNOZ_TO(a, b) (a * b) /* Nie wystarczająco */ 3: POMNOZ_TO(a, b) ((a) * (b)) /* Właściwie */
Co za różnica pewnie zapytasz? Spójrz w takim razie na ten przykład:
wynik = POMNOZ_TO(2 + 3, 4 * 5) / 5;
Po podstawieniu wygląda to tak:
1: wynik = 2 + 3 * 4 * 5 / 5; // = 14, Zły 2: wynik = (2 + 3 * 4 * 5) / 5 // = 12, Też zły 3: wynik = ((2 + 3) * (4 * 5)) / 5 // = 20, Właściwy!
Nadużycia definicji polegają zazwyczaj na złym ich ułożeniu i użyciu skomplikowanych makr wewnątrz innych makr (co czyni kod prawie niemożliwym do zrozumienia). Podstawową zasadą jest pisanie krótkich i prostych makr. Rób tak, albo marny będzie twój koniec ;)
Instrukcje #if, #ifdef, #ifndef, #else i #elseif
Są to dyrektywy preprocesora, które służą selekcjonowaniu określonych części kodu i usuwaniu innych w zależności od stanu zmiennych preprocesora.
Dzięki nim część kodu może zostać uniewidoczniona dla kompilatora – coś jak inteligentne komentarze.
Instrukcja ‘#if’ jest bardzo podobna do zwykłej instrukcji if. Jest tylko trochę inaczej zapisywana.
Załóżmy, że możesz mieć gdzieś następującą definicję:
CODE_VAR 2 lub CODE_VAR 3
Wtedy możesz napisać:
#if CODE_VAR == 2 <kod ten będzie dopuszczony do kompilacji tylko jeśli CODE_VAR == 2> #else <kod ten będzie zachowany tylko wtedy gdy CODE_VAR != 2> #endif
Możesz w ogóle nie pisać instrukcji ‘#else’ jeśli tego nie chcesz.
Wystarczy napisać następującą instrukcje, żeby zaistniała dana definicja preprocesora:
CODE_VAR /* Definiuje istnienie CODE_VAR */
I teraz możesz napisać:
#ifdef CODE_VAR <Kod, który będzie zachowany tylko, jeśli CODE_VAR istnieje> #else <kod, który będzie zachowany tylko wtedy, gdy CODE_VAR nie jest zdefiniowane> #endif
lub
#ifndef CODE_VAR <kod, który będzie zachowany tylko wtedy, gdy CODE_VAR *nie* jest zdefiniowane> #else <kod, który będzie zachowany, jeśli CODE_VAR istnieje> #endif
I ponownie, instrukcje ‘#else’ można ominąć.
Komendy preprocesora ‘#if/#ifdef/#ifndef’ są prawie wyłącznie używane do dodania odpluskwiającego kodu, który nie powinien być cały czas aktywny lub do pisania rzeczy, które będą różnie pracowały w zależności od bardzo rzadko zmieniających się parametrów.
Cała konfiguracja mudliba jest sprawdzana w ten sposób. Na przykład jest definicja MET_ACTIVE, która służy ustalaniu czy system met/nonmet (nie każdy zna każdego gracza) ma być włączony czy nie. Gdy administrator muda zdecyduje się na wyłączenie tego, wystarczy że w jednym pliku konfiguracyjnym zamieni ‘#define MET_ACTIVE’ na ‘#undef MET_ACTIVE’. (W Arkadii wiele takich definicji znajdziesz w ‘/config/sys/local.h’).
Podstawy Mudliba i LPC
W rozdziale tym nauczysz sie wszystkiego, czego potrzebujesz by pisac kod w srodowisku gry. Bede unikal bardziej skomplikowanych tematow, pozostawiajac ich omowienie na trzeci rozdzial. Dowiesz sie tu wielu rzeczy na temat mudliba i pracy gamedrivera – wiedzy niezbednej do pisania dzialajacego i efektywnego kodu.
Małe wybiegnięcie w przyszłość
Aby moc pokazac ci przyklady tego, czego bede cie probowal nauczyc, musze ci wpierw z wyprzedzeniem wyjasnic kilka funkcji. Bedzie to powtorzone w odpowiednim kontekscie pozniej. Tutaj jest zamieszczony jedynie krotki przeglad, zebys wiedzial co robie.
Zeby pokazac pokazac jakis tekst na ekranie gracza, uzywa sie efunkcji ‘write()’. Istnieja dwa specjalne znaki, ktore sa dosc czesto uzywane do formatowania tekstu. Sa to: znak „tabulacji” i znak „nowej linii”. Zapisuje je sie w ten sposob: (kolejno) ‘\t’ i ‘\n’. Znak „tabulacji” wstawia osiem spacji, a znak „nowej linii”, jak sama nazwa wskazuje lamie linie, czyli konczy stara i przechodzi do nowej.
void write(string tekst) np. write("Siemanko!\n"); write("\tTo jest wciety string.\n"); write("Ten string\njest rozlozony na kilka linii\n\ti czesciowo" + "\nwciety.\n"); /* Wynikiem bedzie: Siemanko! To jest wciety string. Ten string jest rozlozony na kilka linii i czesciowo wciety. */
Jesli masz tablice, mapping albo zwykla zmienna dowolnego typu i chcesz wyswietlic to na ekranie, np. zeby pomoc sobie w wyszukiwaniu bledow, to sfunkcja ‘dump_array()’ moze sie okazac bardzo pomocna. Podajesz zmienna, ktora chcesz wyswietlic jako argument i jej zawartosc zostanie ukazana na ekranie.
void dump_array(mixed dane) np. string *imie = ({ "franek", "zdzichu", "bolo" }); dump_array(imie); /* Wynikiem bedzie (Array) [0] = (string) "franek" [1] = (string) "zdzichu" [2] = (string) "bolo" */
Drugi rzut oka na LPC
Wytlumacze ci teraz pominiete w poprzednim rozdziale informacje o LPC. Ta wiedza jest ci potrzebna do tworzenia dzialajacych obiektow w srodowisku gry.
Wywołania funkcji
Sa dwa rodzaje wywolan funkcji: wewnetrzne i zewnetrzne. Jedynym rodzajem o jakim dotad mowilismy byly wywolania wewnetrzne, choc zewnetrzne tez sie znalazly w kilku miejscach.
Tworzenie wewnątrzobiektowych wywołań funkcji
[call_self]
Robienie wewnetrznych wywolan funkcji jest tak proste, jak pisanie nazwy funkcji i argumentow w nawiasach po niej. Lista argumentow jest albo lista wyrazen, albo wogole jej nie ma. Wywolanie funkcji jest oczywiscie wyrazeniem rowniez.
<funkcja>(<lista argumentow>); np. zmienna = jakas_funkcja(1.0) * 4;
Jest inny sposob na zrobienie tego. Jesli nazwe funkcji masz przechowana w jakiejs zmiennej i chcesz ja wywolac, to pomocna moze sie okazac funkcja ‘call_self()’:
call_self(<"nazwa funkcji">, <lista argumentow>); np. zmienna = call_self("jakas_funkcja", 1.0) * 4;
Jesli uzywajac ‘call_self()’ podasz nazwe nieistniejacej funkcji, wyskoczy blad i wykonywanie obiektu zostanie przerwane.
Tworzenie prostych zewnątrzobiektowych wywołań funkcji
[call_other]
Wywolanie zewnetrzne, to wywolanie funkcji z innego obiektu. Zeby moc to zrobic, potrzebujesz odnosnika do obiektu w ktorym chcesz ja wywolywac. Nie mowilismy jeszcze o tym, jak zdobyc odnosnik do obiektu, ale zalozmy ze juz go masz.
mixed <odnosnik do obiektu/sciezka do obiektu>-><funkcja>(<lista argumentow>); mixed call_other(<odn ob/sciezka ob>, "<funkcja>", <lista argumentow>);
np.
/* * Zalozmy, ze chce wywolac funckje 'oblicz_cos' w obiekcie * "/d/Mojadomena/wiz/obiekt" i ze mam jakis wlasciwy wskaznik * obiektu przechowany w zmiennej 'obiekcik' */ zmienna = obiekcik->oblicz_cos(1.0); zmienna = "/d/Mojadomena/wiz/obiekt"->oblicz_cos(1.0); zmienna = call_other(obiekcik, "oblicz_cos", 1.0); zmienna = call_other("/d/Mojadomena/wiz/obiekt", "oblicz_cos", 1.0); /* * Te cztery linijki robia dokladnie to samo */
Jak widzisz, efunkcja ‘call_other()’ dziala analogicznie do ‘call_self()’.
Kiedy zewnetrznie wywolujesz funkcje poslugujac sie sciezka do obiektu, tak zwany „master object” zostaja wywolany. Jesli obiekt, w ktorym wywolujesz nie zostal jeszcze zaladowany do pamieci, to zostanie. Gdy sprobujesz wywolac nieistniejaca funkcje, zostanie zwrocone 0 bez zadnych komunikatow bledu. Jesli wywolasz funkcje z obiektu, ktory ma jakies bledy, to zostanie wyswietlony komunikat bledu i dzialanie obiektu ktory wykonal wywolanie zostanie przerwane.
Po co wiec ten ‘call_self()’ w takim razie? Przeciez moznaby uzywac zamiast niego ‘call_other()’ przez caly czas z takim samym skutkiem? Nie do konca tak. ‘call_other()’ miesza jeszcze sporo w czyms co sie nazywa dostepem do funkcji w obiekcie – o tym jeszcze nie mowilem. Roznica polega na tym, ze ‘call_self’ rzeczywiscie dziala jak jakiekolwiek wywolanie wewnetrzne (ma dostep do wszystkich funkcji w danym obiekcie), podczas gdy ‘call_other()’ jest typowym wywolaniem zewnetrznym. Pamietaj o tym, kiedy bede omawial roznice pomiedzy dostepem w wywolaniach wewnetrznych i zewnetrznych.
Tworzenie wielokrotnych zewnątrzobiektowych wywołań funkcji
Mozesz wywolywac wiele obiektow na raz, tak samo prosto jakbys to robil z jednym. Jesli masz tablice stringow ze sciezkami, albo wskaznikow do obiektow lub mapping, w ktorym wartosci sa sciezkami do plikow lub wskaznikami do obiektow, to mozesz w jednej instrukcji wywolac je wszystkie. Rezultatem bedzie albo tablica z wynikami, jesli wywolywales za pomoca tablicy, albo mapping z takimi samymi indeksami z jakimi wywolywales, jesli korzystales z mappinga.
(tablica/mapping) <tablica/mapping>-><funkcja>(<lista argumentow>); np. /* * Potrzebuje mappingu, gdzie indeksami beda imiona graczy, a * wartosciami ich hitpointy. */ object *ludzie; mapping hp_map; string *imiona; // Przypisuje liste wszystkich graczy. ludzie = users(); // Bierze ich imiona. imiona = ludzie->query_real_name(); // Tworzy mapping, przy pomocy ktorego beda robione wywolania. // Element = imie:wskaznik hp_map = mkmapping(imiona, ludzie) // Zastepuje wskazniki wartosciami hitpoint. hp_map = hp_map->query_hp(); // A wszystko to mogloby byc zrobione prosciej w ten sposob: hp_map = mkmapping(users()->query_real_name(), users()->query_hp());
Dziedziczenie obiektów
Zalozmy, ze chcesz zakodowac jakas rzecz, drzwi na przyklad. Musisz wiec zaprogramowac mozliwosc otwierania i zamykania przejscia pomiedzy dwoma pokojami. Byc moze chcesz rowniez moc otwierac i zamykac zamek w drzwiach. O wszystko to musisz zadbac w swoim kodzie. Na dodatek, bedziesz musial kopiowac ten sam kod za kazdym razem, kiedy dodasz nowe drzwi i bedziesz chcial zeby roznily sie tylko opisem.
Po jakims czasie wkurzysz sie na to wszystko, czesciowo dlatego, ze odkryjesz ze inni wizardzi napisali wlasne drzwi, ktore dzialaja prawie - lecz nie do konca – tak jak twoje, przez co twoje swietne obiekty beda bezuzyteczne poza twoja domena.
Myslenie zorientowane obiektowe polega na tym, ze zamiast powtarzac takie pisanie w kolko, tworzy sie podstawowy obiekt drzwi, ktory moze robic wszystko to, co uwazasz, ze typowe drzwi powinny moc. Wtedy dziedziczy sie go w naszych konkretnych drzwiach, konfiguruje sie je (nie obiekt dziedziczony), korzystajac z mozliwosci drzwi „rodzica”.
Jest nawet mozliwe dziedziczenie kilku obiektow, choc „wielokrotne dziedziczenie” niesie ze soba kilka trudnych problemow do rozwiazania, wiec staraj sie unikac tego.
Skladnia na dziedziczenie obiektow jest bardzo prosta. Na poczatku pliku pisze sie cos takiego:
inherit "<sciezka do pliku>"; np. inherit "/std/door"; // (door oznacza drzwi) inherit "/std/room.c";
UWAGA! To NIE jest dyrektywa preprocesora, a zwykla instrukcja, wiec stawia sie ‘;’ na koncu i NIE pisze sie ‘#’ przed nia. Jesli chcesz, mozesz wyszczegolnic w sciezce, ze chodzi o plik c, aczkolwiek to nie jest konieczne.
„Dziecko” dziedziczy wszystkie funkcje i wszystkie zmienne, ktore sa zadeklarowane w sposob pozwalajacy na ich dziedziczenie. Jesli masz funkcje o takiej samej nazwie jak w obiekcie „rodzicu”, to twoja „zamaskuje” funkcje „rodzica”. Kiedy funkcja bedzie wolana poprzez wywolanie zewnetrzne, to wykonana zostanie twoja funkcja. Wewnetrzne wywolania w „rodzicu” beda sie jednak odnosily do jego funkcji. Nieraz potrzebujesz wywolac funkcje „rodzica” z „dziecka” – robi sie to poprzez dodanie ‘::’ przed wewnetrznym wywolaniem funkcji.
void moja_funkcja() { /* * Ta funkcja istnieje i w naszym obiekcie(dziecku) i w obiekcie * ktory dziedziczymy(rodzicu), ale potrzebujemy ja * wywolac z rodzica. */ ::moja_funkcja(); // Wywoluje moja_funkcje w rodzicu. }
Nie da sie wywolac zamaskowanej funkcji w rodzicu poprzez wywolanie zewnetrzne, mozliwe jest to tylko z rodzica. Gdy obiekt dziedziczy inny, ktory z kolei dziedziczy jeszcze inny, np. C dziedziczy B, a ten dziedziczy A, to wtedy maskowana funkcja z A, jest mozliwa do wywolania tylko w B, a w C juz nie.
Cienie : Maskowanie funkcji w czasie wykonywania obiektu
Jest cos takiego w LPC, co jest zwane „cieniowaniem”. Purytanie wola jednak nazywac to „obrzydlistwem” i przy kazdej okazji krzycza zeby to usunac, gdyz wystepuje to prawie przeciwko wszystkiemu, czego uczy sie o dobrym programowaniu. „Cieniowanie” jest raczej uzyteczne w grze, aczkolwiek moze spowodowac troche problemow glownie zwiazanych z bezpieczenstwem. Uzywaj tego z rozwaga!
Gdy jeden obiekt cieniuje drugi, wszystkie powtarzajace sie funkcje i zmienne z cieniujacego obiektu, zamaskuja te z cieniowanego obiektu. Wywolania cieniowanych funkcji, spowoduja wykonanie zamiast nich, tych z cieni. Cien praktycznie ‘stanie’ sie obiektem, ktory cieniuje. Od zwyklego maskowania rozni je to, ze funkcje sa maskowane w czasie kompilacji. Obiekty-cienie zas sa dodawane do obiektow juz w czasie ich dzialania. Mozna w ten sposob dodac do obiektu jakies wlasciwosci, ktore nie sa przewidziane w kodzie.
To wszystko na razie na ten temat. Sposob tworzenia i zastosowanie cieni beda szczegolowo omowione pozniej. Jak na razie wiesz wszystko, co potrzebujesz wiedziec.
Identyfikacja typów
[intp, floatp, functionp, stringp, objectp, mappingp, pointerp]
Ze wzgledu na fakt, ze wszystkie zmienne sa na poczatku ustawiane na 0 i ze wiele funkcji zwraca 0 w razie bledu, warto moc zbadac jaki typ wartosci odebralismy. Rowniez, gdy uzywa sie typu ‘mixed’, niezbedna jest informacja jaki typ wartosci zawiera zmienna. Do tego celu istnieja specjalne funkcje kontrolne, ktore zwracaja 1 (prawde) jesli testowane wartosci sa danego typu, a 0 jesli nie.
int intp(mixed)
Sprawdza, czy podana wartosc jest typu integer
int floatp(mixed)
Sprawdza, czy podana wartosc jest typu float
functionp(mixed)
Sprawdza, czy podana wartosc jest wskaznikiem funkcji (typ function)
int stringp(mixed)
Sprawdza, czy podana wartosc jest stringiem
int objectp(mixed)
Sprawdza, czy podana wartosc jest wskaznikiem obiektu (typ object)
int mappingp(mixed)
Sprawdza, czy podana wartosc jest mappingiem
int pointerp(mixed)
Sprawdza, czy podana wartosc jest tablica
- UWAGA! Funkcje te sprawdzaja typ wartosci, a NIE sama wartosc
w znaczeniu poprawnosci dzialania. Innymi slowy ‘intp(0)’ zawsze zwroci prawde, tak jak to zrobi ‘mappingp(([]))’.
Kwalifikatory typów
Typy, ktore przypisujesz zmienym i funkcjom moga miec rozne kwalifikatory, zmieniajace sposob ich dzialania. Bardzo wazne jest to, by o nich caly czas pamietac i uzywac w odpowiednich miejscach. Wiekszosc z nich inaczej dziala na zmiennych, a inaczej na funkcjach, wiec male klopoty zwiazane z ich uzywaniem sa dosyc powszechne wsrod programistow. Postaraj sie przejsc przez to teraz, a nie bedziesz mial pozniej zadnych problemow.
Kwalifikator zmiennej ‘static’
Jest to dosyc klopotliwy kwalifikator, gdyz dziala on inaczej nawet na rozne zmienne, w zaleznosci od tego gdzie one sa! Zmienne globalne (jak na pewno wiesz) definiowane sa na poczatku pliku, na zewnatrz funkcji. Sa one dostepne we wszystkich funkcjach, tzn. ich „zasieg” rozciaga sie na caly obiekt i nie jest ograniczony do jednej funkcji.
Istnieje mozliwosc nagrania wszystkich zmiennych globalnych jakiegos obiektu, za pomoca pewnej efunkcji (opisana bedzie dalej). Jednakze, jesli zmienna globalna jest zdefiniowana jako ‘static’, to nie bedzie nagrana.
static string JakasNazwa; // Nie nagrywana zmienna globalna.
Kwalifikator funkcji ‘static’
Do funkcji, ktore sa zadeklarowane z uzyciem kwalifikatora ‘static’ nie mozna stosowac zewnetrznych wywolywan, tylko wewnetrzne. Czyni to taka funkcje ‘niewidzialna’ i niedostepna dla innych obiektow. Miedzy innymi tu jest wlasnie ta roznica pomiedzy wewnetrznymi a zewnetrznymi wywolaniami funkcji, o ktorej wczesniej mowilem.
Kwalifikator funkcji/zmiennej ‘private’
Zmienne lub funkcje, ktore zostaly zadeklarowana jako ‘private’ nie beda mogly byc dziedziczone przez inne obiekty. Dostep do nich ma tylko obiekt, ktory je definiuje.
Kwalifikator funkcji/zmiennej ‘nomask’
Funkcje i zmienne, ktore sa zadeklarowane jako ‘nomask’ nie moga byc maskowane w zaden sposob, ani przez cieniowanie, ani przez dziedziczenie. Jesli sprobujesz zamaskowac takowa, to otrzymasz komunikat bledu.
Kwalifikator funkcji/zmiennej ‘public’
Jest to standardowy kwalifikator dla wszystkich funkcji i zmiennych. Oznacza to, ze nie narzuca on zadnych innych ograniczen ponad te, ktore sa narzucone przez jezyk.
Kwalifikator funkcji ‘varargs’
Funkcje, ktore sa zdefiniowane jako ‘varargs’ beda mogly otrzymywac rozna liczbe argumentow. W takim przypadku nie ma potrzeby podawania ich wszystkich przy wywolywaniu, zeby bylo ono prawidlowe. Moga byc ustalone standardowe wartosci dla argumentow, ktore nie otrzymaly zadnych wartosci.
varargs void mojafun(int a, string str = "pelle", float c = 3.0); { }
Jak widzisz, kilka argumentow dostalo standardowe wartosci, ktore beda uzyte w przypadku gdy dany argument nie zostanie podany. Jesli nie ustalisz standardowej wartosci, to wartosc nie podanego argumentu zostanie ustawiona na 0.
Drugi rzut oka na typy danych
Jeden typ danych byl dotad mniej lub bardziej ignorowany. Jest nim typ ‘function’. Jako, ze obiekty maja swoj wlasny typ, funkcje maja taki rowniez. Dzieki niemu mozesz miec zmienne, poprzez ktore bedziesz mogl wywolywac funkcje. Przewaznie jest on jest uzywany w polaczeniu z innymi funkcjami, ktore uzywaja go jako parametru.
Zmienna tego typu definiuje sie tak jak kazda inna:
<typ danych> <nazwa zmiennej>, <inna zmienna>, ..., <ost. zmienna>; np. function moja_fun, *tablica_fun;
Przypisywanie odnosnikow funkcji do nich nie jest jednak takie proste. Mozesz przypisac dowolny typ funkcji do zmiennej tego typu: czy efunkcja, czy sfunkcja, czy lfunkcja nie czyni zadnej roznicy. Istnieje nawet mozliwosc przypisania odwolania do funkcji zewnetrznej.
Przypisanie wskaznika do funkcji wymaga tego, by dana funkcja byla zdefiniowana, albo poprzez prototyp, albo poprzez deklaracje. Zalozmy, ze jak na razie interesuja cie tylko proste wskazniki do funkcji.
<zmienna typu `function'> = <nazwa funkcji>; <zmienna typu `function'> = &<nazwa funkcji>(); np. moja_fun = allocate; moja_fun = &allocate();
Wskaznika do funkcji uzywa sie tak samo jak zwyklego wywolania funkcji.
int *i_tabl;
i_tabl = allocate(5); // Korzystamy z powyzszego i_tabl = moja_fun(5); // ... przypisania wskaznika funkcji.
Wystarczy tego na teraz. Pozniej wyjasnie jak tworzyc bardziej skomplikowane struktury funkcji.
Drugi rzut oka na instrukcję switch/case
Instrukcja switch jest bardzo sprytna i mozna w niej stosowac testowac, czy jakas wartosc typu integer miesci sie w podanym zakresie:
public void kolo_fortuny() { int i; i = random(10); // Przypisuje losowa liczbe z zakresu 0-9 switch (i) { case 0..4: write("Sprobuj jeszcze raz, frajerze!\n"); break; case 5..6: write("Ekstra, masz trzecie miejsce!\n"); break; case 7..8: write("Tak! Drugie miejsce!\n"); break; case 9: write("OOPS! Udalo ci sie!\n"); break; default: write("Ktos grzebal przy kole... Wykrec 997!\n"); break; } }
catch/throw: Obsługa blędów w czasie działania programu
Czasami zachodzi potrzeba wywolania funkcji, o ktorej wiadomo, ze moze zwrocic blad runtime (czyli taki, ktory wynikl po kompilacji, juz w czasie dzialania programu). Na przyklad w swoim programie mozesz chciec sklonowac jakis obiekt (opisane pozniej) albo zaladowac plik. Jesli nie ma takiego pliku, lub gdy masz niewystarczajace przywileje, to wyskoczy blad runtime i wykonanie programu zostanie zatrzymane. W tych okolicznosciach przydalaby sie mozliwosc przechwycenia bladu i wyswietlenia wlasnego komunikatu o bledzie, lub tez wykonania innej czynnosci. Jest to mozliwe dzieki funkcji ‘catch()’. Jako argument podaje sie w niej zmienna typu ‘function’. Jesli ‘catch()’ zwroci 1 (prawde), to bedzie to oznaczalo, ze wystapil blad w czasie wykonywania podanej funkcji. Jesli wszystko z podana funkcja bedzie ok, to zwroci 0.
int catch(function) np. if (catch(tail("/d/Relikwa/fatty/tajna_mapa_do_ukrytych_orzeszkow"))) { write("Przykro mi, nie ma mozliwosci przeczytania tego "+ "pliku.\n"); return; }
Jest takze mozliwe wywolanie bledu i co za tym idze przerwanie programu. Jest to uzyteczne, gdy chcesz powiadomic uzytkownika o nieplanowanym zdazeniu, ktore wystapilo w czasie dzialania programu. Zazwyczaj sie to umieszcza w czesci ‘default’ komendy switch, o ile (oczywiscie) nie uzywasz tej czesci jako pewnego rodzaju lapacza wszelkich pozostalych mozliwosci.
throw(mixed info) np. if (test < 5) throw("BLAD: Zmienna 'test' ma wartosc mniejsza niz 5.\n");
Wskaźniki do tablic i Mappingów
W terminologii informatycznej tablice i mappingi sa uzywane jako "odniesienie poprzez wskaznik", podczas gdy inne zmienne jako "odniesienie poprzez wartosc". Oznacza to, ze tablice i mappingi, w przeciwienstwie do innnych rodzajow zmiennych w czasie kopiowania nie sa dublowane. Zamiast nich, dublowany jest tylko wskaznik do oryginalnej tablicy czy mappinga. Co to wszystko oznacza?
Hmm... Po prostu to:
object *tab, *tab_2; tab = ({ 1, 2, 3, 4 }); // Jakas tablica. tab_2 = tab; // Zalozmy (zle), ze 'tab_2' // staje sie kopia tablicy 'tab'. // Zmieniamy pierwszy element (1) na 5. tab_2[0] = 5;
... I teraz ... logicznie rzecz biorac wydawaloby sie, ze wartosc pierwszego elementu tab_2 rowna jest 5, podczas gdy ten sam element w ‘tab’ wynosi 1. Nie jest tak jednak, gdyz to co zostalo skopiowane do zmiennej tab_2 nie bylo tablica, tylko wskaznikiem do tej samej tablicy co ‘tab’. Oznacza to, ze nasze dzialanie zmienilo pierwszy element w oryginalnej tablicy, do ktorej obie zmienne maja wskazniki. Wydawalo by sie, ze ‘tab_2’ i ‘tab’ obie sie zmienily, podczas gdy w rzeczywistosci zmianie ulegla tylko tablica, do ktorej obie zmienne sie odwolywaly.
Dokladnie to samo sie stanie w przypadku mappingow, gdyz pod tym wzgledem dzialaja one tak samo.
Wiec.. jak w takim razie obejsc to? Czasem na prawde zachodzi potrzeba pracy na kopii, a nie na oryginale tablicy czy mappingu. Rozwiazanie jest bardzo proste. Trzeba sie tylko upewnic, ze kopia zostala stworzona z innej tablicy lub mappingu.
_ To jest po prostu pusta tablica. / tab_2 = ({ }) + tab; \_ A to jest ta szczegolna, o ktora nam chodzi.
W tym przykladzie ‘tab_2’ staje sie suma tablicy pustej i tablicy `tab', stworzona jako zupelnie nowa tablica. Pozostawi to w przyszlosci oryginal bez zmian, dokladnie tak jak chcielismy. Mozesz uczynic dokladnie to samo z mappingami. Nie gra roli to, czy pusta tablice lub mapping dodajesz z przodu, czy z tylu, o ile wogole dodajesz.
Interfejs LPC/Mudliba
Jest sporo rzeczy, ktore mozesz zechciec zrobic, ktorych nie da sie wyrazic tylko za pomoca elementow jezyka LPC. Jest to na przyklad obsluga lancuchow znakowych czy tez zapisywanie do plikow. Rzeczy te sa bowiem czescia ‘standardowego zestawu funkcji’, ktory zawiera wiekszosc jezykow programowania. Ten rozdzial ma nauczyc cie podstaw robienia wszystkich tego typu rzeczy, potrzebnych do tworzenia obiektow LPC.
Istnieje okreslony zestaw globalnych wlasciwosci, co do ktorych mozesz miec pewnosc, ze beda dostepne zawsze w dowolnym obiekcie. Oto one:
- creator
- Kazdy obiekt jest tworzony przez kogos. Tozsamosc autora zalezy od lokacji zrodla (pliku zrodlowego, czyli takiego, z ktorego obiekt jest wykonywany) w systemie plikow. Jesli obiekt znajduje sie w katalogu zwyklego czarodzieja, wtedy jako jego autora podaje sie imie tego czarodzieja. W innym przypadku podawana jest nazwa domeny. Dla obiektow mudliba, autorem jest ‘root’ w przypadku obiektow o prawach administratora, albo ‘backbone’ w przypadku reszty.
- uid/euid
- "uid" (User ID – czyli identyfikator uzytkownika) obiektu okresla najwyzszy mozliwy poziom przywilejow dla danego obiektu. Samo uid jest uzywane tylko do ograniczania na "euid" (Effective User ID – czyli rzeczywisty identyfikator uztykownika) tego samego lub innego obiektu. Euid jest uzywane w sytuacjach, gdy przywileje obiektu musza byc sprawdzone, np. dostep do pliku (czytanie/zapisywanie/usuwanie), badz tworzenie obiektu.
- living
- Zeby obiekt mogl przyjmowac badz wydawac komendy, musi byc livingiem.
Definicja obiektów standardowych i bibliotecznych
Jak juz poprzednio mowilem, gamedriver prawie nic nie wie o grze. A conajwyzej tak malo, jak to mozliwe. To mudlib musi sie tym wszystkim zaopiekowac. Zajmiemy sie teraz podstawowymi sprawami z nim zwiazanymi, np. poruszaniem sie, poziomami oswietlenia, tym jak obiekty powinny sie kontaktowac z graczami, itp. Po tym stworzymy obiekty, ktore beda wykorzystywaly omowione funkcje.
Zwykly czarodziej nie musi przesiadywac dlugich godzin zastanawiajac sie jak napisac obiekt, bedacy w prawidlowej relacji z innymi i rozpatrujac jeszcze tysiace innych, klopotliwych spraw. Zamiast tego dziedziczy odpowiadajacy mu, jeden ze standardowych obiektow. Pozniej tylko dorzuca don kilka ustawien, czyniac go unikalnym wzgledem innych obiektow tego typu.
Konsekwencja tego jest sytuacja w ktorej wszystkie obiekty w grze w 100% polegaja na fakcie, ze okreslone typy obiektow (pokoj, potwor, bron) maja okreslone zestawy powszechnych cech i mozliwosci. Po prostu musza miec, by mogly wspolpracowac w ustalony sposob. Jesliby nie mogly, jesliby kazdy czlowiek mial inne rozwiazanie tego samego problemu, obiekty dzialalyby tylko na terenie danego czarodzieja i nigdzie poza nim. Nie byloby mozliwe uzywanie miecza w calej grze. Ba! Nawet nie moznaby go nigdzie przemiescic. Oczywiscie oznacza to, ze musimy wymuszac jednomyslnosc, gdyz niemozliwe jest stworzenie (i uzytkowanie) obiektow, ktore nie dziedzicza tych specjalnych obiektow. Jak sam pozniej zobaczysz mozliwym byloby napisanie miecza, ktory nie bedzie dziedziczyc standardowego obiektu broni, tyle ze wtedy dzierzenie go staloby sie bariera nie do przebycia...
Istnieja rozne standardowe obiekty uzywane do roznych celow, ale najwazniejszym z nich jest ‘/std/object.c’.
Podstawowy obiekt: /std/object.c
Ten obiekt jest wielozadaniowy. WSZYSTKIE obiekty majace ‘fizyczna’ postac dziedzicza go. Dziedziczac jeden ze standardowych obiektow mozesz byc pewien, ze posrednio dziedziczysz takze ‘object.c’, gdyz jest on dziedziczony przez przewazna wiekszosc standardowych obiektow.
Standardowy obiekt definiuje nastepujace rzeczy:
- zawartosc (inventory)
- Obiekt moze "zawierac" inne obiekty. W rzeczywistosci jest to zwykla lista obiektow, ktore sa trzymane wewnatrz obiektu posiadajacego te liste. Jednakze bardzo proste jest uwidocznienie tego, jako ekwipunku gracza, wnetrza torby, pokoju, pudelka itp.
- srodowisko (environment)
- Obiekt "otaczajacy" inny obiekt, odwrotnosc zawartosci. Jesli obiekt A jest w ‘ekwipunku’ obiektu B, to obiekt B otacza obiekt A. Obiekt moze miec wiele innych obiektow w swoim ‘ekwipunku’, lecz tylko jeden obiekt-srodowisko. Wszystkie nowo utworzone obiekty nie maja zadnego srodowiska.
- lista komend
- Lista polecen powiazanych z funkcjami. Obiekt udostepnia polecenia wszystkim "zyjacym" obiektom, ktore sie znajduja zarowno w jego ekwipunku jak i w srodowisku. Zyjace obiekty moga wydac takie polecenie i obiekt udostepniajacy je wywola powiazana z poleceniem funkcje.
- wlasciwosci (properties)
- Wlasciwosci sa czysto umowna sprawa. Jest to najzwyklejszy w swiecie mapping, ktorego indeksy sa jakimis zarezerwowanymi nazwami, a wartosci zmiennymi, ktore wplywaja na jakies ogolnie dostepne stany. Typowymi wlasciwosciami sa waga, cena, poziom swiatla i nieco bardziej abstrakcyjne pojecia takie jak mozliwosc upuszczenia, wziecia czy sprzedania obiektu. Mozliwy do uzycia zestaw wlasciwosci zalezy juz od konkretnych obiektow.
- poziom swiatla
- Obiekt ma okreslony poziom swiatla. Zazwyczaj nie wplywa on na otoczenie, choc mozliwe sa do utworzenia zarowno zrodla swiatla, jak i zrodla ciemnosci. Poziom swiatla jest jedna z wlasciwosci.
- waga/objetosc
- Wartosci te okreslaja, jak duzo obiekt wazy i ile miejsca zajmuje. W przypadku ‘pojemnikow’ takich jak torby definiuje to takze ich pojemnosc.
- widzialnosc
- Niektore obiekty moga byc latwiejsze do znalezenia niz inne.
- nazwy i opisy
- Jak obiekt sie nazywa i jakim widzi go gracz.
Standardowe klasy obiektów
Istnieje spora liczba standardowych obiektow, ktorych sie uzywa (stosuje sie termin ‘dziedziczy’) w roznych sytuacjach. By dowiedziec sie nieco wiecej o kazdym z nich, bedziesz potrzebowal osobnej dokumentacji. Ten spis dostepnych obiektow standardowych ma za zadanie przynajmniej ukierunkowac cie w poszukiwaniach.
Jak juz wczesniej mowilem, wiekszosci z nich nie wykorzystuje sie na codzien. Z kilkoma jednak wkrotce staniesz sie za pan brat, gdyz opisywanych przez nie typow obiektow nie da sie napisac nie dziedziczac ich.
- /std/armour.c
- Dowolne zbroje
- /std/board.c
- Tablice ogloszeniowe
- /std/book.c
- Ksiazka ktora mozesz otwierac i zamykac, ze stronami, ktore mozesz przewracac i czytac.
- /std/coins.c
- Podstawa wszystkich rodzajow pieniedzy
- /std/container.c
- Dowolny obiekt, ktory moze zawierac w sobie inne (pojemnik)
- /std/corpse.c
- Cialo martwych potworow/graczy/NPC-ów (NPC – Non Player Charater, czyli wszystkie zyjace istoty, ktore nie sa graczami)
- /std/creature.c
- Proste zyjace istoty
- /std/domain_link.c
- Dziedziczone przez obiekty uczestniczace w tzw. ‘preloadingu’ (po kazdej Apokalipsie, przed dopuszczeniem graczy do gry, wazniejsze obiekty sa od razu wczytywane do pamieci, by zredukowac laga)
- /std/door.c
- Drzwi, ktore lacza dwa pokoje
- /std/food.c
- Jedzenie roznego rodzaj
- /std/guild (katalog)
- Obiekty zwiazane z gildiami (gildie i shadowy)
- /std/heap.c
- Obiekty dowolnego rodzaju, ktore moga byc umieszczone w stosach
- /std/herb.c
- Ziola
- /std/key.c
- Klucze do drzwi
- /std/leftover.c
- Pozostalosci po rozlozonych cialach
- /std/living.c
- Zyjace obiekty
- /std/mobile.c
- Przemieszczajace sie zyjace obiekty
- /std/monster.c
- Dowolne potwory
- /std/npc.c
- Sprytniejsze istoty humanoidalne
- /std/object.c
- Podstawowy obiekt
- /std/poison_effect.c
- Obsluguje dzialanie roznych trucizn
- /std/potion.c
- Mikstury
- /std/receptacle.c
- Dowolnego rodzaju pojemniki, ktore mozna otwierac/zamykac
- /std/resistance.c
- Obsluguje odpornosci na rozne rzeczy
- /std/room.c
- Dowolny rodzaj lokacji
- /std/scroll.c
- Pergaminy, kartki itp
- /std/shadow.c
- Uzywane jako podstawa w tworzeniu obiektow typu ‘shadow’
- /std/spells.c
- Obiekty czarow, tomow itp
- /std/torch.c
- Pochodnie/lampy itp.
- /std/weapon.c
- Bronie wszelakiej masci
Standardowe obiekty biblioteczne
To sa pomocnicze pliki. Uzywa sie ich w polaczeniu z obiektami z katalogu ‘/std/’. Na przyklad chcac zakodowac karczme, dziedziczymy ‘/std/room.c’ oraz ‘/lib/pub.c’.
- /lib/bank.c
- Dowolny rodzaj banku
- /lib/cache.c
- ‘Cache’ uzywany w celu przyspieszenia regularnie wykorzystywanych obiektow
- /lib/guild_support.c
- Dodatkowe funkcje dla gildii
- /lib/herb_support.c
- Dodatkowe funkcje do ziol
- /lib/more.c
- Mozliwosc ‘more’ (przegladania plikow)
- /lib/pub.c
- Funkcje przydatne przy pisaniu karczm
- /lib/shop.c
- Sklepy dowolnego rodzaju
- /lib/skill_raise.c
- Trenowanie umiejetnosci
- /lib/store_support.c
- Pomocne funkcje w magazynach (niezbedny dodatek do sklepow)
- /lib/time.c
- Funkcje obslugujace czas
- /lib/trade.c
- Pomocne we wszystkim co jest zwiazane z handlem
Jak zdobyć odnośniki do obiektów
Obiekty, jak juz wczesniej mowilem, wystepuja w dwoch rodzajach – twz. „master object” i „klon”. W wiekszosci przypadkow operuje sie na sklonowanych obiektach. Sa to miedzy innymi takie, ktore mozesz przesunac, dotknac, obejrzec itp, lub dowolne inne, ktore wystepuja w wiecej niz jednej kopii. Operowanie na „master objectach” ogranicza sie zazwyczaj do pokoi i souli (soule to obiekty wykorzystywane przez obiekty graczy, ktore dodaja im rozne komendy).
Kazdy zaladowany obiekt ma swoj „master object”, ktory jest reprezentacja tego obiektu w pamieci, a zarazem pierwszym klonem. Zawiera on informacje o mechanizmie dzialania obiektu. Klony skladaja sie jedynie z zestawu zmiennych i odnosnika do swojego „master objectu”. Ow zestaw zmiennych odroznia poszczegolne klony od siebie (np. dwa miecze tego samego typu moga sie roznic stopniem stepienia).
Jesli zniszczysz „master object”, jego klony istniejace juz w grze pozostana (informacja o mechanizmie dzialania obiektu gdzies pozostanie w pamieci). Zwiazany jest z tym blad w mysleniu wielu poczatkujacych czarodziejów: Uwazaja, ze przeladowujac „master object” do nowej postaci (po zmianach ktore wprowadzili w kodzie), zmienia sie wszystkie juz istniejace klony. Nic bardziej mylnego – po przeladowaniu (updatowaniu) „master objectu” dopiero nowe klony beda zawieraly wprowadzone zmiany.
Ladowanie „master objectu” do pamieci odbywa sie poprzez wywolanie na sciezce do jego pliku dowolnej, nieistniejacej funkcji (sciezka do pliku stanowi jeden ze sposobow na odniesienie sie do „master objectu”).
Przeladowywanie obiektu (zwane rowniez jego uaktualnianiem lub updatowaniem) polega usunieciu z pamieci starego „master objectu” i zaladowaniu nowego.
Jak w takim razie zdobyc odnosnik do obiektu? To zalezy. Odnosnikiem do obiektu moze byc zarowno wskaznik do obiektu, jak i sciezka do zrodla obiektu w systemie plikow. Metody ich uzyskiwania sa rozne w roznych sytuacjach. Wytlumacze teraz je wszystkie.
Odnośniki do aktualnego obiektu
[this_object, previous_object, calling_object]
Kazdy obiekt zawsze moze zdobyc odnosnik do samego siebie. Sluzy do tego efunkcja ‘this_object()’:
object this_object() np. object ja; ja = this_object();
Zeby zbadac ktory obiekt wywolal aktualnie uruchomiona funkcje za pomoca wywolania zewnetrznego, mozna uzyc efunkcji ‘previous_object()’:
object previous_object(void|int glebokosc) np. object p_ob, pp_ob; p_ob = previous_object(); // Obiekt, ktory wywolal te funkcje pp_ob = previous_object(-2); // Obiekt, ktory wywolal inny obiekt, // ktory wywolal te funkcje
Jesli nie podasz zadnego argumentu, albo podasz -1, funkcja zwroci obiekt, ktory ja wywolal. Zmniejszanie argumentu spowoduje podanie dalszych poprzednikow, np. ‘previous_object(-4)’ zwroci obiekt, ktory wywolal obiekt, ktory wywolal obiekt, ktory wywolal twoj obiekt. O ile lancuch wywolan byl na tyle dlugi. Jesli glebokoscia przekroczy sie dlugosc lancucha wywolan, to funkcja zwroci 0.
Mam nadzieje, ze zauwazyles, ze funkcja ta sprawdza tylko zewnetrzne wywolania. Istnieje inna efunkcja, ktora dziala tak samo, tyle ze dla wszystkich rodzajow wywolan (wewnetrzne lub zewnetrzne):
object calling_object(void|int glebokosc)
Uzywa sie jej tak samo jak poprzedniej
A wiec... skad mozesz wiedziec czy ktory odnosnik wlasnie dostales jest wlasciwy czy nie (tzn. ze to nie jest np. zero albo cos innego)? Jesli jeszcze pamietasz, istnieje pewna efunkcja zwana ‘objectp()’? Doskonale by sie do tego celu nadawala – zwroci 1, jesli podany argument bedzie prawidlowym wskaznikiem do obiektu.
int objectp(mixed ob) np. if (objectp(calling_object(-2))) write("Tak, ob ktory wywolal ob, ktory nas wywolal " + "istnieje!\n"); else write("Nie ma takiego.\n");
Tworzenie obiektów
[setuid, getuid, seteuid, geteuid, creator, set_auth, query_auth,
clone_object]
Wpierw musisz sie upewnic, ze obiekt, ktory probuje stworzyc inny ma do tego wystarczajace przywileje. Reguly sa bardzo proste: Obiekt, ktory ma wlasciwe euid moze klonowac dowolny inny obiekt. Wlasciwe euid, to dowolne, rozne od 0. Uid i Euid jest standardowo ustawiane na 0 we wszystkich nowych obiektach i oznacza, ze obiekt wogole nie ma przywilejow.
Zazwyczaj masz ograniczony wybor euid, ktory mozesz ustawic obiektom. Jesli jestes zwyklym wizardem ogranicza sie on tylko do twojego imienia. Lord moze jako euid wstawic swoje imie, lub dowolnego innego wizarda w swojej domenie (wyjatkiem sa tylko Archowie). I oczywiscie obiekty z uid ‘root’ moga ustawic dowolne euid jakie chca.
A wiec... uid jest nadrzedne w stosunku do euid. Uid ogranicza euid jakie mozesz ustawic. Stanowi maksymalny poziom, jaki mozna ustawic euid. Standardowa wartosc uid ustawia sie poprzez wywolanie sfunkcji setuid():
void setuid() np. setuid();
Proste, nie? Wykonanie tej komendy spowoduje przypisanie uid uzyskanego z pozycji zrodla pliku w systemie plikow. Dziala to na takich samych zasadach jak w pobieraniu wartosci zwracanej przez sfunkcje creator(), co zostalo opisane wczesniej.
string creator(mixed odnosnik) e.g. string moj_tworca; moj_tworca = creator(this_object());
Aby poznac wartosc aktualnego uid, uzywa sie sfunkcji ‘getuid()’.
string getuid() np. string aktualny_uid; aktualny_uid = getuid();
Wiec... uid ma juz stawiony maksymalny priorytet, taki jaki ma klonujacy. Euid jednakze jest caly czas rowne 0. Poniewaz euid ustala rzczywiste przywileje, bedzie to oznaczalo, ze obiekt nie ma jeszcze zadnych praw.
Do nadawania wartosci euid uzywa sie sfunkcji ‘seteuid()’, gdzie jako argument podaje sie nowa wartosc euid, o ile ma sie prawa, zeby tak ustawic (jest to sprawdzane). Funkcja zwraca 0 w razie niepowodzenia. Nie podanie zadnego argumentu spowoduje wyzerowanie euid.
int seteuid(void|string przyw) np. if (seteuid("lewy")) write("Wlasnie TAK! Jestem panem WSZECHSWIATA!\n"); else write("Ehhhhh...\n");
Jak zwykle jest podobna sfunkcja, ktora zwraca aktualne euid:
string geteuid() np. write("Aktualnym euid jest " + geteuid() + .\n");
Wszystkie sfunkcje ‘setuid()’, ‘getuid()’, ‘seteuid()’ i ‘geteuid()’ korzystaja z efunkcji ‘set_auth()’ i ‘get_auth()’. Uzywa sie ich do zmieniania specjalnej zmiennej z prawami w danym obiekcie. W razie proby uzycia ‘set_auth()’ gamedriver wywoluje specjalna funkcje kontrolna w master object, ktora sprawdza czy ma sie do tego odpowiednie prawa. Jest tak, gdyz tej zmiennej mozna przypisac dowolny string, a sposob w jaki jej uzywamy jest sztucznie ustalony, tak jak uznalismy, ze najlepiej bedzie rozwiazac to w systemie bezpieczenstwa.
Kiedy probujesz wykonac operacje, ktora wymaga odpowiednich przywilejow, taka jak zapisywanie do pliku, czy klonowanie obiektu, to gamedriver wywoluje inne specjalne funkcje w master object, zeby sie upewnic czy masz odpowiednie prawa. Wszystko to zalezy od tego, czy informacje zawarte w zmiennej z prawami maja scisle okreslona, wymagana postac. W zwiazku z tym nie jestes w stanie zdzialac nic wiecej za pomoca ‘set_auth()’, niz bys mogl z ‘setuid()’ i ‘seteuid()’. ‘query_auth()’ nie jest zabezpieczone przed wywolywaniem, lecz nie znajdziesz tam zadnych ciekawych informacji.
Tak prawde mowiac, informacja zawarta w zmiennej z prawami, ktorej to wartosc zwraca ‘query_auth()’ ma po prostu postac uid i euid oddzielonych dwukropkiem.
Skoro juz wiemy jak nadawac przywileje innym obiektom, sprobujmy korzystajac z nich cos sklonowac. Efunkcja do tego celu sluzaca nazywa sie ‘clone_object()’. Laduje i tworzy obiekt z pliku zrodlowego. Gdy operacja klonowania sie nie uda, na przyklad ze wzgledu na jakies bledy w kodzie pliku zrodlowego, zostanie wyswietlony komunikat bledu zas wykonanie obiektu przerwane.
object clone_object(string sciezka) np. object magiczny_pierscien; // Tak sie ustawia prawa obiektu do klonowania setuid(); // pobiera uid na podstawie katalogu seteuid(getuid()); // jako euid podstawia pobrane uid // No i samo klonowanie magiczny_pierscien = clone_object("/d/Domena/wiz/mag_pierscien");
Oczywiscie wystarczy, ze ustawisz uid/euid obiektu RAZ – pozniej juz nie musisz tego robic przed wykonaniem kazdej operacji, ktora wymaga odpowiednich praw. Najbardziej rozpowszechniona metoda jest umieszczenie wywolan funkcji ustawiajacych uid/euid w funkcji, ktora jest wywolywana w momencie tworzenia obiektu – ale o tym pozniej.
A teraz... tablice i mappingi istnieja tak dlugo, jak dlugo korzystaja z nich jakies zmienne. Gdy ostatnia zmienna odnoszaca sie do nich zostaje wyzerowana, to dane zawarte w nich sa rowniez czyszczone. Czy jest tak samo z obiektami? NIE! Obiekt pozostanie w grze tak dlugo, jak gamedriver jest uruchomiony lub dopoki doraznie go nie zniszczysz.
Odnajdywanie odnośników do innych obiektów
[file_name, find_object, object_clones, find_living, set_living_name,
MASTER_OB, IS_CLONE]
Jak juz mowilem odnosniki do obiektow moga byc zarowno stringami jak i wskaznikami do obiektow. Zamienianie odnosnika ze wskaznika w lancuch znakowy robi sie przy pomocy efunkcji ‘file_name()’:
string file_name(object ob) np. write("To jest obiekt: " + file_name(this_object()) + ".\n");
String, zwracany przez ‘file_name()’ jest tekstowa reprezentacja wskaznika do obiektu. Ma postac ‘<sciezka do pliku>#<numer obiektu>’, na przyklad ‘"/d/Domena/wiz/mag_mikstura#2321"’. Ten lancuch jest wlasciwym odnosnikiem do danego obiektu.
Aby zamienic tekstowy odnosnik do pliku we wskaznik, uzywa sie efunkcji ‘find_object()’.
object find_object(string odn_ob) np. object obiekt; // Master object obiekt = find_object("/d/Domena/wiz/mag_mikstura"); // Konkretny klon obiekt = find_object("/d/Domena/wiz/mag_mikstura#2321");
Jesli funkcja nie znajdzie obiektu (moze byc podana zla sciezka, dany klon moze nie istniec lub obiekt moze byc nie zaladowany), to zwraca 0.
Czasem przydaje sie miec odnosniki do wszystkich klonow danego obiektu. Do znalezienia ich sluzy efunkcja ‘object_clones()’. Zwroci ona tablice zawierajaca wskazniki wszystkich klonow master objectu, na ktory wskazuje odnosnik podany jako argument. Oznacza to, ze mozesz podac zarowno wskaznik do master objectu, jak i do jednego z jego klonow – nie robi to roznicy. Jesli funkcja nie bedzie w stanie znalezc zadnych klonow, to zwroci pusta tablice.
object *object_clones(object odn_ob) np. object *lista_klonow; lista_klonow = object_clones(find_object("/d/Domena/wiz/mag_mikstura"));
Niektore obiekty nazywa sie "zyjacymi" (living). W grze objawia sie to miedzy innymi tym, ze moga one zostac zaatakowane i (byc moze) zabite. Zyjace obiekty rejestruja sie na specjalnej liscie w gamedriverze. Robia to po to, by mozna je latwiej znalezc. Specjalna efunkcja ‘find_living()’ szuka w tej liscie podanej nazwy zyjacego obiektu.
object *find_living(string imie, void|int 1) np. object balrog_ob, *balrogowie; // Szuka potwora 'balrog' w grze. balrog_ob = find_living("balrog");
Jesli podasz ‘1’ jako drugi argument, efunkcja zwroci liste wszystkich obiektow z ta nazwa.
balrogowie = find_living("balrog", 1);
Jesli efunkcja nie bedzie mogla znalezc obiektu z podanym imieniem, zwroci 0.
Zeby obiekt znalazl sie na liscie imion, musi w sobie wywolac efunkcje ‘set_living_name()’.
void set_living_name(string imie) np. // Jest to czesc funkcji create() w kodzie balroga. set_living_name("balrog");
Pamietaj o tym, ze jesli masz wiele obiektow z ta sama nazwa, to ‘find_living()’ zwroci losowo jeden z nich.
Zeby zdobyc odnosnik do master_objectu obiektu do ktorego masz wskaznik, musisz zamienic go na stringa i wtedy oderwac od niego numer klonu. Istnieje juz jednak makro znajdujace sie w standardowym pakiecie ‘/sts/macros.h’ robiace to za ciebie. Po prostu dodaj na poczatku pliku linijke ‘#include <macros.h>’ i uzyj makra ‘MASTER_OB’.
string MASTER_OB(object ob) np. string master; // Zalozmy ze /sys/macros.h juz zostalo przylaczone do tego pliku. master = MASTER_OB(find_living("balrog"));
Makro zwraca odnosnik do master objectu balroga w postaci tekstowej, wiec jesli chcesz miec wskaznik do master objectu musisz posluzyc sie funkcja ‘find_object()’, jako argument podajac swiezo uzyskany string.
Klon najlatwiej odroznic od master objectu poprzez porownywanie tekstowego odnosnika. W tym celu mozna uzyc makra IS_CLONE. Jest ono rowniez dostepne w ‘/sys/macros.h’. Korzysta ono z ‘this_object()’ i nie wymaga zadnego argumentu.
int IS_CLONE np. if (IS_CLONE) write("Jestem klonem!\n");
Odnośniki do interaktywnych obiektów
[find_player, this_interactive, this_player]
Jesli szukasz jakiegos konkretnego gracza, to do cego celu moglbys uzyc ‘find_living()’, a potem upewnic sie, ze zwrocona wartosc jest interaktywnym obiektem. Jednakze o wiele szybciej jest uzyc efunkcji ‘find_player()’, ktora robi dokladnie to samo, z jednym wyjatkiem – moze byc tylko jeden gracz o podanym imieniu w grze. Jesli bedzie on aktualnie zalogowany, to otrzymasz do niego odnosnik.
object *find_player(string imie) np. object lewy; lewy = find_player("lewy"); if (pointerp(lewy)) lewy->catch_msg("Siemanko, bracie!\n"); else write("Eh.. chyba go nie ma.\n");
Bardzo czesto potrzeba wiedziec kto wydal komende, ktora zapoczatkowala wykonanie jakiejs okreslonej funkcji. Efunkcja ‘this_interactive()’ zwroci do niego wskaznik. Jesli lancuch komend zostal zapoczatkowany przez jakis niezalezny, nieinterakcyjny obiekt, to zwroci ona 0.
Czesciej jednak nie interesuje cie kto zapoczatkowal lancuch, lecz raczej na kogo dany obiekt kieruje swoja uwage. Ten wlasnie obiekt zwroci efunkcja ‘this_player()’. Innymi slowy, podczas gdy obiekt powinien zwracac uwage (listy komend, komunikaty, itp) na obiekt podany przez this_player(), inny gracz zwrocony przez ‘this_interactive()’ mogl stac sie przyczyna wykonania lancucha komend w aktualnym obiekcie. Wartosc ‘this_interactive()’ nigdy nie moze byc zmieniana, w przeciwienstwie do wartosci ‘this_player()’. Wiecej o tym bedzie pozniej.
object this_player(); object this_interactive(); np. object tp, ti; tp = this_player(); ti = this_interactive(); if (objectp(ti)) { if (ti != tp) { tp->catch_msg("Zapppp!\n"); // Ale genialne tlumaczenie, ti->catch_msg("Zapnales go!\n"); // nie? :-) } else ti->catch_msg("Fzzzzz...\n"); }
Niszczenie obiektów
[destruct, remove_object]
Predzej czy pozniej bedziesz chcial sie pozbyc jakiegos obiektu. Sluzy do tego efunkcja ‘destruct()’. Jednakze gamedriver pozwoli tylko na zniszczenie samego siebie, tzn. na wywolanie destruct() tylko z obiektu ktory ma byc zniszczony – zadne wywolanie zewnetrzne nie wchodzi w rachube. To oznacza, ze kazdy obiekt potrzebuje funkcji, ktora wywoluje destruct(), ktora moglbys wywolac z innego obiektu, zeby moc zniszczyc go z zewnatrz. Jesli jednak obiekt nie zawiera funkcji korzystajacej z ‘destruct()’, to nie bedziesz w stanie go ruszyc w ten sposob.
Istnieje wyjscie z tego, ktore umozliwia ci zniszczenie dowolnego obiektu, ale jest nim komenda, ktora musisz wydac wlasnorecznie. Nie mozesz uzyc jej z wewnatrz programu.
Standardowy obiekt, ktory pozniej bedzie bardziej szczegolowo omowiony, definiuje funkcje ‘remove_object()’, ktora mozesz wykonac, aby zniszczyc obiekt. Poniewaz wszystkie obiekty w grze MUSZA dziedziczyc go, mozesz miec pewnosc, ze w kazdym znajdziesz te funkcje. Mozliwe jest zamaskowanie jej i co za tym idzie zablokowanie. Robienie tego, bedzie jednak zwyklym sabotazem, wiec nawet nie mysl o robieniu czegos takiego. Pozwolilismy na jej maskowanie, dlatego zebys mogl dodac tam jakis kod, ktory w razie niszczenia obiektu zrobi kilka innych rzeczy, a nie po to bys mogl pisac niezniszczalne obiekty.
void remove_object() np. void usun_balroga(string imie_bal) { object bal; bal = find_living(imie_bal); if (objectp(bal)) bal->remove_object(); }
Gdy bezposrednio uzywasz ‘destruct()’ albo wywolujesz w tym samym obiekcie ‘remove_object()’, DWA razy sie upewnij, ze zaden kod nie jest po tym wykonywany. Widzisz, wykonywanie nie jest przerywane od razu po komendzie do zniszczenia – obiekt jest tylko oznaczany jako „juz zniszczony”, a sama destrukcja dzieje sie po skonczeniu wykonywania go. Oznacza to, ze wywolania funkcji lub komendy wydane po komendzie destrukcji moga spowodowac bledy runtime w innych obiektach.
void destruct() np. void zniszcz_mnie() { write("Zegnaj, okrutny swiecie!\n"); destruct(); }
Gdy obiekt jest niszczony, WSZYSTKIE wskazniki do niego (nie odnosniki tekstowe) w grze sa ustawiane na 0. Ze wzgledu na ten fakt, warto sie upewnic, czy stary wskaznik do obiektu jest wciaz wlasciwy, przed robieniem z nim czegokolwiek. Nigdy nie masz pewnosci – moze zostal usuniety w miedzyczasie.
Jesli niszczony obiekt zawiera jakies inne obiekty, to w czasie destrukcji one sa rowniez usuwane. Wyjatkiem sa obiekty interakcyjne, gracze. Kiedy uaktualniasz (update) pokoj, skutecznie go niszczysz. Jesli sa w nim jacys gracze, to zostana oni przemieszczeni do startowych lokacji. Jesli dany pokoj jest startowa lokacja lub jest jakis problem z przemieszczaniem ich tam (bledny kod lokacji, albo niemoznosc przemieszczenia), to ci gracze zostana rowniez usunieci.
Obsługa poleceń przez obiekty
[init, add_action, enable_commands, disable_commands, living, command,
commands, get_localcmd, query_verb, notify_fail, update_actions]
Jak na razie wiesz na pewno, ze nic w tej grze nie jest proste. Aby pogmatwac wydawanie polecen, mamy ich dwa rodzaje. Jednym z nich jest taki, o ktorym juz wczesniej mowilismy, czyli polecenia definiowane przez ‘fizyczne’ obiekty. Drugim rodzajem sa komendy twz. soul (duszowe?). Sa one wymyslem czysto mudlibowym. Bedzie o nich mowa w trzecim rozdziale.
Polecenia dodane przez fizyczne obiekty dzialaja w ten sposob: Po wejsciu takiego obiektu w kontakt z innym obiektem, np. graczem (czyli:
- albo 1:obiekt wchodzi w ekwipunek ("zawartosc" obiektu) gracza,
- albo 2:gracz i obiekt znajduja sie we wspolnym srodowisku, np. w tym samym pokoju,
- albo 3:gracz wchodzi w "zawartosc" obiektu np do pokoju),
w obiekcie ktory wszedl wolana jest lfunkcja ‘init()’. Jest ona zwykla funkcja, taka jak kazda inna, lecz przeznaczona jest do dodawania polecen poprzez efunkcje ‘add_action()’. Innymi slowy, gdy wchodzisz do jakiegos obiektu, do listy twoich polecen zostaja dodane polecenia z nowego srodowiska, z twojego ekwipunku oraz polecenia innych obiektow bedacych w tym samym srodowisku co ty.
Funkcja ‘add_action()’ przypisuje polecenie jakiejs funkcji. Po wpisaniu slowa-polecenia, zostaje wykonana polaczona funkcja, z argumentem stanowiacym reszte wpisanego polecenia (np. gdyby cale polecenie brzmialo ‘zabij zielonego smoka’, to slowem-poleceniem byloby ‘zabij’, a argumentem ‘zielonego smoka’). Oprocz poprzednich dwoch argumentow(tj. funkcja oraz slowo-polecenie), add_action() moze przyjac rowniez trzeci. Jesli rowna sie on 1, to przypisana funkcja bedzie wykonana, nawet gdy komenda bedzie stanowila tylko czesc zdefiniowanego w add_action() polecenia. Na przyklad, jesli zdefiniowane polecenie brzmialoby ‘zabij’, to wywolanie przypisanej funkcji mogloby byc dokonane juz przez komende "za" albo "zab".
add_action(function funkcja, string slowo_polecenie, void|int 1) np. init() { /* * Funkcje 'zrob_uklon()' i 'wyjdz_z_gry()' sa zdefiniowane * gdzies w tym obiekcie. Jednakze, jesli ich deklaracje * znajduja sie za ta funkcja, to w naglowku programu musza * byc ich prototypy. */ add_action(zrob_uklon, "uklon"); // Przyzwyczaj sie lepiej add_action(&wyjdz_z_gry(), "quit"); // do roznych rodzajow // deklaracji odnosnikow // do funkcji. }
Czy jest to prawda dla kazdego rodzaju obiektu? Czy kazdy obiekt otrzyma ten zestaw komend? Ano nie. Tylko "zyjace" obiekty. Obiekt moze zostac uczyniony "zyjacym" poprzez efunkcje ‘enable_commands()’, a martwym badz bezwladnym poprzez efunkcje ‘disable_commands()’. Zauwaz, ze „zyjacy” obiekt dla gamedrivera oznacza „taki, ktory moze przyjmowac i wydawac polecenia” – dla mudliba znaczy to troche wiecej.
Mozesz uzywac tych efunkcji za kazdym razem, gdy zechcesz wlaczyc lub wylaczyc zdolnosc to obslugiwania polecen w obiekcie. Pamietaj tylko o tym, ze gdy "ozywisz" obiekt, to nie przybedzie mu komend z racji tego, ze znajduje sie np w pokoju ktory dodaje polecenia. Sa one dodawane tylko przy wchodzeniu do srodowiska pokoju. Zeby je uzyskal bedziesz musial wiec przeniesc go z, a potem spowrotem do pomieszczenia.
Mozesz sprawdzic, czy obiekt jest "zyjacy" przy pomocy efunkcji ‘living()’.
enable_commands() disable_commands() int living(object ob) np. public void zmien_stan_ozywienia() { if (living(this_object())) disable_commands(); else enable_commands(); }
Polecenia moga byc dodawane i obslugiwane tylko przez obiekty, ktore sie znajduja w srodowisku lub zawartosci danego obiektu. Kiedy jest on usuwany z zasiegu obiektow dodajacych polecenia, mozliwosc ich wykonania jest rowniez usuwana.
Jak widzisz, gamedriver oczekuje, ze funkcja ‘init()’ jest zdefiniowana w kazdym obiekcie, ktory chce dodac polecenia zyjacym obiektom. Badz jednak ostrozny w czasie uzywania tej funkcji. Jesli na przyklad uzywasz ‘init()’ do sprawdzania, czy wchodzacy obiekt ma prawo sie tam znalezc i przenosi go gdzies jesli nie ma prawa, to najprawdopodobniej bedziesz mial klopoty. Bedzie tak dlatego, ze gdy sprobujesz dodac polecenia juz po przesunieciu, to w rzeczywistosci bedziesz to robil na nieobecnym obiekcie. Gamedriver spostrzeze to i skonczy sie to bledem. Chcialbym ci doradzic, bys nie uzywal funkcji ‘init()’ do zadnych innych celow, niz do dodawania polecen. Mozesz ewentualnie sprawdzac czy obiekt, ktory wywolal ‘init()’ powinien otrzymac polecenia, czy nie (jesli chcesz ograniczyc dostep do niektorych polecen), ale nie wrzucaj tam niczego innego.
W wiekszosci obiektow, ktore dziedzicza posrednio lub bezposrednio podstawowy obiekt, musisz wywolac takze rodzica ‘init()’, gdyz bez tego twoj init() moze stracic kilka bardzo waznych rzeczy. Po prostu umieszczaj instrukcje ‘::init();’ na poczatku kazdego swojego init'a, jeszcze przed instrukcjami ‘add_action()’, a wszystko bedzie w porzadku.
Aby wykonac jakies polecenie w zyjacym obiekcie, uzyj efunkcji ‘command()’.
int command(string komenda) np. command("kichnij"); command("zaloz helm");
Aby otrzymac liste dostepnych polecen, uzywa sie efukcji ‘commands()’ lub ‘get_localcmd()’ w zaleznosci od tego, jakiego typu informacji sie potrzebuje. ‘commands()’ zwraca tablice tablic, zawierajacych wszystkie komendy dostepne dla okreslonego obiektu plus te, ktore obiekt sam definiuje wraz z funkcjami, ktore sa w razie ich wykonania wywolywane. ‘get_localcmd()’ jest prostsza i zwraca tylko tablice z slowami-poleceniami. Gdy zaden obiekt nie jest wyszczegolniony, uzywa sie jako standardowej wartosci ‘this_object()’. Obejrzyj manuala dla ‘commands()’ by dowiedziec sie jaki format ma zwracana tablica.
mixed commands(void|object ob) string *get_localcmd(void|object ob)
Gdy uzywasz jednej funkcji do wielu slow-polecen, zachodzi potrzeba zbadania, ktore konkretnie zostalo uzyte. Do tego uzywa sie efunkcji ‘query_verb()’.
string query_verb() np. init() { ::init(); add_action(&moja_funkc(), "apa"); // wszystkie trzy add_action(moja_funkc, "bepa"); // polecenia wywoluja add_action(&moja_funkc(), "cepa"); // te same funkcje... } public int moja_funkc() { switch (query_verb()) // ...ale kazde polecenie jest w niej { // rozpatrywane oddzielnie case "apa": < kod > break; case "bepa": < kod > break; case "cepa": < kod > break; } return 1; }
Funkcje obslugujace polecenia powinny zwracac 1, gdy zostaly poprawnie uzyte, tzn kiedy zostala wywolana wlasciwa funkcja, z wlasciwym argumentem. Kiedy zwrocisz 0, gamedriver bedzie szukal innych funkcji przypisanych do identycznego slowa-polecenia, do czasu az ktoras z nich zwroci 1, albo nie bedzie juz zadnych innych. Jest specjalna efunkcja zwana ‘notify_fail()’, ktorej mozesz uzyc do przechowania komunikatu bledu, ktory zostanie wyswietlony gdy zadna z funkcji nie ‘przygarnie’ polecenia. Zamiast standardowego, bezsensownego komunikatu bledu ‘What?’, mozesz dac graczowi wydajacemu polecenie jakas lepsza informacje (na Arkadii ‘Slucham?’). W przypadku gdy wszystkie funkcje powiazane z identycznym slowem poleceniem zwroca 0, to ostatnio zdefiniowany ‘notify_fail()’ zostanie uzyty do wyswietlenia komunikatu bledu.
notify_fail(string komunikat) np. public void init() { ::init(); add_action(&zrob_uklon(), "uklon"); } public int zrob_uklon(string komu) { if (!find_player(komu)) { notify_fail("Nie tu nikogo takiego!\n"); return 0; } < kod uklonu > return 1; }
Jesli jestes absolutnie pewien, ze polecenie odnosilo sie do twojego obiektu i chcesz przerwac wykonanie nawet jesli twoj obiekt znalazl w nim blad (zle argumenty, kontekst, lub cokolwiek innego), to mozesz zwrocic 1. Pamietaj jednak, ze wtedy musisz uzyc innej metody wyswietlenia komunikatu bledu na ekranie niz ‘notify_fail()’, gdyz ta funkcja ma szanse byc uzyta tylko gdy zwrocisz 0.
Gdy twoj obiekt zmienia dostepne polecenia w czasie swojego dzialania i chcesz, by okoliczne zyjace obiekty uaktualnily swoj zestaw komend, musisz wywolac efunkcje ‘update_actions()’ na swoim obiekcie. Jesli nie sprecyzujesz w argumencie zadnego obiektu, to domyslnie zostanie przyjety ‘this_object()’. Wszystkie okoliczne zyjace obiekty uniewaznia stary zestaw komend od twojego obiektu i wywolaja sobie jeszcze raz ‘init()’ z niego, zeby pobrac nowy zestaw.
update_actions(object ob)
Alarmy: Niesynchroniczne wywołania funkcji
[set_alarm, remove_alarm, get_alarm, get_all_alarms]
Gamedriver oblicza tak zwany „evaluation cost”, albo prosciej „eval cost” (koszt wykonania). Jest to zwykly sposob mierzenia, jak bardzo obiekt obciaza CPU. Kazdy obiekt ma nadany limit kosztu wykonania. Kiedy jest on wyczerpany, wykonanie obiektu zostaje przerwane. Ograniczenie to zostalo narzucane, by gra nie byla za dlugo przeciazana. Istnienie tego pociaga za soba jednak pewne konsekwencje. Jakies duze obliczenia moga sie po prostu nie zmiescic w limicie eval costu, wiec w takiej sytuacji potrzeba rozbic program na mniejsze czesci.
Czasem przydaje sie opoznic wykonanie jakiejs funkcji, a czasem warto, by funkcja byla wykonywana cyklicznie. Pozwala na to wykorzystanie alarmow. Ustawia sie je efunkcja ‘set_alarm()’.
int set_alarm(float opoznienie, float cykl, function funkcja_alarm) remove_alarm(int alarm_id) mixed get_alarm(int alarm_id) mixed get_all_alarms()
Funkcja zwraca unikalny dla tego obiektu numer alarmu, ktory mozesz pozniej wykorzystywac do manipulowania nim. Informacje o danym alarmie uzyskuje sie wywolujac funkcje ‘get_alarm()’, jako argument podajac numer alarmu. Do usuwania ich sluzy efunkcja ‘remove_alarm()’, a do zdobycia informacji o wszystkich alarmach sluzy efunkcja ‘get_all_alarms()’. Ta ostatnia jest przewaznie wykorzystywana w sytuacjach, gdy zapomni sie przechowac gdzies identyfikator alarmu, albo chce sie wyswietlic informacje o obiekcie. Efunkcja ‘set_alarm()’ pozwala na ustalenie opoznienia, po jakim podana funkcja zostanie wykonana po raz pierwszy, oraz opoznienia pomiedzy kolejnymi cyklicznymi wywolaniami. Kazde wywolanie alarmu zaczyna sie z eval costem rownym 0. Poniewaz funkcje sa wywolywanie przez alarm niesynchronicznie wzgledem uzytkownika obiektu, ‘this_player()’ i ‘this_interactive()’ zwroca 0. Pamietaj o tym, gdyz niektore efunkcje korzystaja z wartosci zwracanej przez ‘this_player()’.
- UWAGA! PRZECZYTAJ TO UWAZNIE!
- Bardzo latwo wpasc w nawyk dzielenia programu na wiele ‘alarmowych’
wywolan w malych odstepach czasowych. Jednakze NIE do tego sluza alarmy. Smiertelnym grzechem jest robienie funkcji alarmowej, ktora tworzy powtarzajace sie alarmy wewnatrz powtarzajacych sie alarmow. Ilosc alarmow wtedy gwaltownie wzrasta i CALA GRA momentalnie staje. Jest to tak nieprawdopodobnie kretynskie, ze grozi natychmiastowym demotem, wiec upewnij sie, ze wszytko jest W PORZADKU za pierwszym razem. Ogolnie rzecz biorac, odstepy pomiedzy cyklicznymi alarmami, powinny byc dluzsze niz jedna dwie sekundy, tak samo jak odstepy przed pojedynczymi wywolaniami.
Funkcje alarmowe beda szerzej opisane i zademonstrowane w trzecim rozdziale.
Środowisko i zawartość
[move_object, move, enter_inv, enter_env, leave_inv, leave_env,
environment, all_inventory, deep_inventory, id, present]
Jak juz wczesniej mowielem, kazdy obiekt ma zarowno „wnetrze”, jak i „otoczenie”. Otoczenie, lub inaczej mowiac „srodowisko” moze byc tylko jednym obiektem, podczas gdy we wnetrzu, w „zawartosci” moze sie znajdowac wiele obiektow.
Swiezo sklonowany obiekt znajduje sie w swego rodzaju pustce, gdyz nie ma zadnego srodowiska. Zeby mogl znalezc sie w fizycznym swiecie gry, musi byc tam przesuniety. Jednakze, nie wszystkie obiekty moga byc przemieszczane. Zeby DOWOLNY obiekt, ktory chce byc gdzies umieszczony, albo chce samemu zawierac jakies inne obiekty dzialal, MUSI dziedziczyc ‘/std/object.c’ gdzies w swoim lancuchu dziedziczen. Po co to ograniczenie? Dlatego, ze standardowy obiekt definiuje spora liczbe pomocnych funkcji i inne obiekty polegaja na tym, ze beda one w twoim obiekcie.
Oto najwazniejsze sposrod nich:
- move()
- Przemieszcza obiekt do innego, obslugujac rachunek wagi/objetosci. Zwraca, czy przesuniecie powiodlo sie. Jest odpowiedzialna za wywolywanie nastepujacych funkcji:
- enter_inv()
- Wywolywana jest w obiekcie, gdy inny obiekt wchodzi w jego zawartosc.
- leave_inv()
- Wywolywana w obiekcie, gdy inny obiekt wchodzi w jego zawartosc.
- enter_env()
- (!!!)
- leave_env()
- (!!!)
UWAGA! Powyzsze funkcje beda wywolane TYLKO wtedy gdy to wlasnie ‘move()’ zostanie uzyty do przesuniecia. Dlatego tak wazne jest to, zebys przemieszczal w ten sposob, a nie poprzez efunkcje, ktora robi to bezposrednio.
Lfunkcja ‘move()’ korzysta z efunkcji ‘move_object()’. ALE pamietaj, gdy odwolasz sie bezposrednio do tej drugiej, to stany obiektu takie jak oswietlenie, waga, czy objetosc nie zostana uaktualnione. Jak juz poprzednio mowilem, fiaskiem skonczy sie proba przeniesienia obiektu do innego przy pomocy ‘move_object()’, gdy ktorys z nich nie dziedziczy ‘/std/object.c’. Dodatkowo, efunkcja moze byc wywolana tylko z obiektu, ktory chce byc przesuniety. To samo dotyczy oczywiscie lfunkcji ‘move()’.
W celu uzyskania odnosnika do obiektu srodowiska, uzywa sie efunkcji ‘environment()’. Jak juz wczesniej powiedzialem, zaden obiekt nie ma zdefiniowanego srodowiska zaraz po utworzeniu – otrzymuje je dopiero, gdy jest gdzies przenoszony. Kiedy obiekt chociaz raz opusci ‘pustke’, w ktorej sie znajduje na poczatku, to juz nigdy nie moze tam wrocic, tzn. nie mozna przeniesc obiektu do ‘0’. Obiektami, po ktorych mozesz sie spodziewac, ze nie beda mialy zadnego srodowiska sa pokoje, dusze, cienie i obiekty daemon.
Masz do wyboru dwie funkcje, sposrod ktorych mozesz wybierac, gdy chcesz zdobyc sklad obiektu. Efunkcja ‘all_inventory()’ zwraca tablice ze wszystkimi obiektami bedacymi zawartoscia podanego obiektu. Efunkcja ‘deep_inventory()’ zwraca tablice, zawierajaca nie tylko to, co ‘all_inventory()’, ale rowniez obiekty, ktore sa we wnetrzu obiektow, ktore sa we wnetrzu podanego obiektu, itd.
object *all_inventory(object ob) object *deep_inventory(object ob) np. /* * Funkcja wyswietla ekwpipunek Fatty na ekranie. Od argumentu bedzie * zalezalo, czy bedzie to tylko ekwipunek widoczny na pierwszy rzut * oka, czy caly. */ void fatty_powiedz_aaaaa(int wszystko) { object fatty_ob, *listaob; if (!objectp((fatty_ob = find_player("fatty")))) { write("Przykro mi, Fatty nie ma dzis w grze.\n"); return 0; } listaob = wszystko ? deep_inventory(fatty_ob) : all_inventory(fatty_ob); write("Oto " + (wszystko ? "cala " : "") + " zawartosc wielkiego brzucha Fatty:\n"); dump_array(listaob); }
Co powiesz na sprawdzenie, czy dany obiekt jest obecny w zawartosci innego? Bazowy obiekt ‘/std/object.c’ definiuje zarowno nazwe jak i opis w obiektach. Rowniez w nim znajduje sie lfunkcja ‘id()’, ktora sprawdza czy podany argument jest jedna z nazw danego obiektu. Jesli jest to zwraca 1(prawde). Efunkcja ‘present()’ przeszukuje zawartosci obiektow, sprawdzajac czy jest tam obiekt o podanym odnosniku lub nazwie. Gdy w argumencie wpiszesz to drugie, to ‘present()’ skorzysta z poprzednio omowionej funkcji ‘id()’ do sprawdzenia, czy w przeszukiwanym aktualnie obiekcie istnieje takowa nazwa. Wykonanie funkcji skonczy sie tak szybko, jak szybko znajdzie ona pierwszy pasujacy do opisu obiekt. Oznacza to, ze jesli jest wiecej takich obiektow, to funkcja zwroci ci tylko jeden z nich.
object present(object ob|string odnob, object *listaob|object ob|void) np. /* * Szuka orzeszkow u Fatty */ void znajdz_orzeszek() { object fatty_ob; fatty_ob = find_player("fatty"); // Nie mozna znalezc Fatty! if (!objectp(fatty_ob)) { write("Fatty chwilowo nie ma, sprobuj pozniej.\n"); return; } if (present("orzeszek", fatty_ob)) write("Tak, Fatty wydaje sie byc bardzo zadowolony z zycia.\n"); else write("Na twoim miejscu trzymalbym sie z dala od " + "Fatty, dopoki nie zaspokoi glodu.\n"); }
Jesli nie podasz drugiego argumentu w ‘present()’, to funkcja poszuka obiektu w zawartosci ‘this_object()’, czyli tego obiektu, z ktorego zostala wywolana. Gdy w drugim argumencie podasz tablice, to funkcja przeszuka wszystkie obiekty z listy. Jesli nie znajdzie niczego, to zwroci 0.
Funkcje obsługujące łancuchy znakowe
[break_string, capitalize, lower_case, sprintf, strlen, wildmatch]
W srodowisku gry opartym na tekscie, naturalne jest to, ze tworcy zadali sobie troche trudu w stworzeniu latwych i wszechstronnych funkcji obslugujacych lancuchy znakowe. Jak juz wiesz, stringi mozna sumowac przy pomocy operatora ‘+’, a nawet laczyc je z liczbami calkowitymi bez zadnych klopotow. Floaty i wskazniki do obiektow musza juz jednak byc konwertowane. Te pierwsze przy pomocy efunkcji ‘ftoa()’ (opisanej pozniej), a te drugie poprzez juz opisana efunkcje ‘file_name()’.
Jedna z najczesciej sprawdzana rzecza w stringach, poza tym co zawieraja, jest ich dlugosc. Uzyskuje sie ja przy pomocy efunkcji ‘strlen()’. Jako argument mozna podac rowniez liczby calkowite (zwroci wtedy 0), dzieki czemu mozna ja wykorzystywac takze do sprawdzania czy zmienna typu string zostala juz zainicjalizowana.
int strlen(string str) np. string str = "Fatty jest spasionym, zatwardzialym szowinista"; write("Dlugosc stringa '" + str + "' wynosi " + strlen(str) + " znakow.\n");
Nieraz zachodzi potrzeba takiego przeformatowania stringa, by zaczynal sie z duzej litery. Sluzy do tego efunkcja ‘capitalize()’. Oprocz tego istnieje efunkcja ‘lower_case()’, ktora zamienia wszystkie litery w podanym stringu na male.
string capitalize(string str) string lower_case(string str)
np.
void // Wyswietli podane imie, odpowiednio sformatowane. wyswietl_ladne_imie(string imie) { string nowe_imie; // Zalozmy, ze imie = "fAttY" nowe_imie = lower_case(imie); // Teraz imie = "fatty" nowe_imie = captialize(imie); write("Imie brzmi: " + imie + "\n"); /* Efektem jest: Imie brzmi: Fatty */ }
Czasem przydaloby sie polamac stringa na mniejsze kawalki(np dlugosci linijki ekranu), by ladniej wygladal i nadawal sie do wyswietlenia. Sluzy do tego efunkcja ‘break_string()’. Dzieki niej mozesz nawet dodac spacje na poczatku polamanych lancuchow znakowych. Jej dzialanie polega na wstawianiu znaku nowej linii po odpowiedniej ilosci slow, tak by jedna czesc zmiescila sie w podanym limicie znakow. Trzeci argument mowiacy ile ma byc spacji wciecia, albo podajacy string wstawiany na poczatku kazdego fragmentu jest opcjonalny.
string break_string(string str, int dlug_lam, int dlugosc_wciecia|string wstawiany_string|void)
np.
string str = "To jest string, ktory chce przedstawic na rozne " + "sposoby."; write(break_string(str, 26) + "\n"); write(break_string(str, 26, 5) + "\n"); write(break_string(str, 26, "Fatty mowi: ") + "\n"); /* Efektem bedzie:
To jest string, ktory chce przedstawic na rozne sposoby. To jest string, ktory chce przedstawic na rozne sposoby. Fatty mowi: To jest string, ktory chce Fatty mowi: przedstawic na rozne Fatty mowi: sposoby. */
Bardzo czesto bedziesz chcial przedstawic zawartosc zmiennej na ekranie. Jak juz pokazalem, mozesz to zrobic poprzez zamienienie wartosci zmiennej w stringa i wyswietlenie jej. Integerow nawet nie trzeba konwertowac - wystarczy, ze dodasz go przy pomocy operatora ‘+’. Otrzymasz jednak cos, co nie bedzie sformatowane i byc moze bedzie wymagalo jakis przerobek. Czasem mozesz chciec wyswietlic tresc zmiennych w postaci tabelki i bedziesz wtedy musial bawic sie w rozne duperele takie jak uzaleznianie ilosci spacji od dlugosci zmiennej itp. Zamiast tego, mozesz skorzystac z efunkcji ‘sprintf()’.
‘sprintf()’ pobiera dwa stringi. Pierwszy to jest ten, ktory chcesz przeformatowac. Drugi zas zawiera wskazowki, wedlug ktorych formatowanie ma sie odbywac. Wynikiem jest gotowy lancuch znakow, ktory mozesz wyswietlic np. przy pomocy ‘write()’.
Wszystkie znaki z drugiego stringa, poza specjalnymi zaczynajacymi sie od ‘%’ beda przekopiowane do wynikowego stringa. Znaki kontrolne maja postac: "%<wyznacznik szerokosci><wyznacznik typu>".
Szerokosc jest liczba calkowita, oznaczajaca dlugosc „okienka” w ktorym dane beda wyswietlane oraz czy dana ta ma byc rownana do lewej czy do prawej strony. Dodatni numer oznacza, ze do prawej, zas ujemny, ze do lewej. Jesli nie podasz szerokosci, to zmienna zostanie wlozona w okienko o dlugosci rownej dlugosci zmiennej. Wyznacznik typu sklada sie z jednej lub wiecej liter, okreslajacych jaki typ zmiennej bedzie tu uzyty.
A oto lista wyznacznikow typu:
- d
- i
- Argument jest liczba calkowita.
string str; int a; a = 7; str = sprintf("test: >%-3d%i<", 1, a); write(str + "\n"); // Efektem jest: // test: >1 7<
- s
- Argument jest lancuchem znakow.
- c
- Podany argument jest numerem ASCII, znaku ktory ma byc wyswietlony.
- o
- Liczba ma byc przedstawiona w systemie osemkowym.
- x
- Liczba ma byc przedstawiona w systemie szesnastkowym.
- X
- Liczba ma byc przedstawiona w systemie szesnastkowym (duzymi literami).
- O
- Argument jest typem danych LPC. Jest to swietna rzecz do wyszukiwania bledow (odpluskwiania), gdyz mozesz wyswietlic dzieki niej zawartosc DOWOLNEJ zmiennej.
np.
write(sprintf("1:%d 2:%s 3:%c 4:%o\n5:%x 6:%X 7:%O\n", 5, "hupp happ", 85, 584, 32434, 85852, strlen)); // Efektem bedzie: // 1:5 2:hupp happ 3:U 4:1110 // 5:7eb2 6:14F5C 7:<<FUNCTION &strlen()>>
Specyfikator ten jest jak na razie jedynym, w ktorym da sie wyswietlic floaty.
To byla lista wszystkich wyznacznikow typow. Do wyznacznikow szerokosci mozna jeszcze dodac te elementy:
- ' '
- Liczbowy argument zostanie poprzedzony jedna spacja, o ile jest dodatni. Pozwala to na robienie fajnych tabel bez zawracania sobie glowy tym, ze liczby z minusem zajmuja o jeden znak wiecej.
- +
- Dodatnie argumenty liczbowe zostana poprzedzone plusem.
- 'X'
- Znak(i) w apostrofach bedzie poprzedzal argument.
- |
- Argument zostanie wycentrowany w okienku.
write((sprintf(">%19|s<\n", "Fatty grubas")));
// Efektem bedzie: // > Fatty grubas <
- #
- Oznacza to tryb tablicowy. Efektem bedzie lista slow oddzielonych ‘\n’ w tabeli o szerokosci okienka. Oczywiscie mozna uzyc tego tylko do stringow.
- =
- Ten wyznacznik jest poprawny tylko dla stringow. Wyswietla rezultat w kolumnach, o ile argument jest szerszy od okienka.
- *
- Argument obok gwiazdki jest rozmiarem okienka. Jesli polaczysz to z trybem tablicowym, to otrzymasz fajne tabelki.
- @
- Argumentem jest tablica. Oczywiscie musisz polaczyc to z wyznacznikiem typu, zaznaczajac typ elementow.
Bardzo czesto zachodzi potrzeba sprawdzenia, czy dany string jest czescia jakiego innego. Jesli nie jestes zainteresowany informacja gdzie on dokladnie wystapil, a tylko czy wogole, to efunkcja ‘wildmatch()’ bedzie czyms w sam raz dla ciebie. Po prostu zwraca 1, jesli podany string wystapil gdzies, w jakims innym, wiekszym stringu. Mniejszy string moze skladac sie tez z prostych symboli-masek.
- *
- Odpowiada dowolnej liczbie dowolnych znakow (użyteczne np. przy szukaniu stringa typu „obojętnie kto mowi: ty draniu obojętnie co”)
- ?
- Odpowiada jednemu dowolnemu znakowi
- [xyz]
- Porownuje dowolne znaki sposrod tych w nawiasach kwadratowych
- [^xyz]
- Porownuje dowolne znaki nie bedace pomiedzy nawiasami kwadratowymi
- \c
- Porownuje c, nawet jesli jest to znak specjalny
int wildmatch(string matryca, string str);
np.
// Cokolwiek, co sie konczy na .foo wildmatch("*.foo", "bar.foo") == 1 // Cokolwiek zaczynajacego sie od a, b lub c i zawierajacego // conajmniej jeden znak wiecej wildmatch("[abc]?*", "axy") == 1 wildmatch("[abc]?*", "dxy") == 0 wildmatch("[abc]?*", "a") == 0
Funkcje obsługujące znaczniki bitowe
[clear_bit, set_bit, test_bit]
Nieraz zachodzi potrzeba przechowania sporej liczby informacji typu ‘tak/nie’. Bardzo prostym i przy okazji niezbyt dobrym sposobem byloby stworzenie sporej liczby integerow, po jednym na kazda informacje i wstawianie w nie 0 albo 1, zeby przedstawic jakis stan. Daje to latwy dostep i jest zrozumiale, ale jak przychodzi do przechowania wiekszej ilosci informacji, to sie zaczynaja problemy z straszna pamieciozernoscia tej metody.
Zamiast tego, mozna uzywac stringow, gdzie kazdy bit w znaku (jest ich 8 na znak) moze przechowywac informacje typu tak/nie. Maksymalna liczba bitow w lancuchu wynosi okolo 1200 = dlugosc stringa okolo 150 znakow. Choc raczej watpie, ze wykorzystasz je wszystkie.
Poszczegolne bity ustawia sie przy pomocy efunkcji ‘set_bit()’, ktora wymaga dwoch argumentow. Pierwszym jest zmienna typu string, w ktorej bit ma byc ustawiony, drugim zas numer bitu, ktory chcesz wlaczyc. ‘clear_bit()’ dziala analogicznie do ‘set_bit()’, tylko ze zeruje(wylacza) podany bit. Jesli chcesz sprawdzic jaka wartosc zawiera dany bit, to powinienes uzyc efunkcji ‘test_bit()’.
Nie musisz inicjalizowac stringow, ktore chcesz wykorzystac do przechowywania bitow. Zarowno ‘set_bit()’ jak i ‘clear_bit()’ zwracaja zmodyfikowany string, a w przypadku gdy nie jest on wystarczajaco szeroki to zostanie rozszerzony przez ‘set_bit()’. ‘clear_bit()’ jednakze nie skroci stringa.
string set_bit(string bitstr, int numer_bitu) string clear_bit(string bitstr, int numer_bitu) int test_bit(string bitstr, int numer_bitu) np. // Wlacza 23 bit string bf; bf = ""; bf = set_bit(bf, 22); // Zeruje 93 bit bf = clear_bit(bf, 92); // Sprawdza 3 bit if (test_bit(bf, 2)) write("Wlaczony!\n"); else write("Wyzerowany!\n");
Funkcje obsługujące czas
[time, ctime, file_time, last_reference_time, object_time]
Z jakiejs nieznanej przyczyny wszystkie pomiary czasu w UNIXe, a co za tym idzie w mudzie, zaczynaja sie od 1 stycznia 1970 roku. Byc moze tworcy tego systemu wymyslili sobie, ze z komputerowego punktu widzenia nie ma powodu, by chciec ustawic jakas wczesniejsza date. W kazdym razie tak jest i nic na to nie mozna poradzic. Mierniki czasu sa integerami i zliczaja czas od wyzej wymienionej daty w sekundkach.
Efunkcja ‘time()’ zwraca aktualny czas. Mozesz wykorzystac ja w tej postaci, albo przekonwertowac zwracana wartosc w jakis zrozumialy string przy pomocy efunkcji ‘ctime()’. W celu otrzymania czasu, w ktorym plik zostal utworzony, uzywa sie efunkcji ‘file_time()’. Istnieje analogiczna efunkcja odnoszaca sie do obiektow – ‘object_time()’.
Warto czasem wiedziec kiedy ostatni raz obiekt byl uzywany, tzn. kiedy ostatni raz byla wywolana w nim jakas funkcja. Jesli jako pierwsza instrukcje wywolasz ‘last_reference_time()’ to otrzymasz ten czas. Pamietaj jednakze, ze po wykonaniu tej funkcji, czas ostatniego wywolania przyjmie wartosc czasu biezacego.
int time() string ctime(int tm) int file_time(string obref) int object_time(object ob) np. // last_reference_time() wywolujemy jako pierwsze write("Ten obiekt ostatni raz byl uzyty " + ctime(last_reference_time()) + "\n"); write("Aktualny czas: " + ctime(time()) + ".\n"); write("Ten obiekt istnieje juz " + (time() - object_time(this_object())) + " sekund.\n");
Funkcje obsługujące konwersję tablice-stringi
[explode, implode]
Istnieje mozliwosc podzielenia stringa na mniejsze kawalki na podstawie jakiegos innego lancucha, albo sklejenia roznych stringow umieszczonych w tablicy w jeden. Do tego celu sluza efunkcje ‘explode()’ i ‘implode()’.
Efunkcja ‘explode()’ wymaga dwoch argumentow: pierwszym jest string, ktory chce sie podzielic, drugim zas jakis inny string, ktorego ‘explode()’ szuka w wiekszym jako znacznika, gdzie go podzielic (np. explode(jakis_tekst, " ") zwroci jakis_tekst podzielony na slowa;
znacznikiem dzielacym jest tutaj spacja).
Efunckja zwraca tablice skladajaca sie z podzielonych stringow. ‘implode()’ jako argumentow wymaga tablicy i stringa, a zwraca string skladajacy sie z posklejanych elementow tablicy, polaczonych stringiem z drugiego argumentu.
string *explode(string str, string matryca) string implode(string *lista_str, string str_laczacy) np. string owoce = "jablko i banan i ananas " + "i pomarancz i fatty ktory je to wszystko"; string *lista_owocow; lista_owocow = explode(owoce, " i "); dump_array(lista_owocow); /* Efektem bedzie: (Array) [0] = (string) "jablko" [1] = (string) "banan" [2] = (string) "ananas" [3] = (string) "pomarancz" [4] = (string) "fatty ktory je to wszystko" */ owoce = implode(lista_owocow, ", "); write(owoce + "\n"); // Efektem bedzie: // jablko, banan, ananas, pomarancz, fatty ktory je to wszystko
Funkcje obsługujące tablice
[allocate, member_array, sizeof, pointerp]
Zaczne od malej powtorki z tablic. Moga one zawierac dowolne typy danych, wlaczajac w to inne tablice. Pamietaj o tym, ze tablice w przeciwienstwie do typow danych (poza mappingami) sa kopiowane poprzez odnosnik, a nie poprzez wartosc. Oznacza to, ze gdy przypisujesz tablice zmiennej, nie kopiujesz jej, a jedynie przechowujesz odnosnik, wskaznik do tablicy w zmiennej.
np. string *arr1, *arr2; arr1 = ({ 1, 2, 3 }); arr2 = arr1; arr2[1] = 5; dump_array(arr1); /* * Efektem jest: * * (Array) * [0] = (int) 1 * [1] = (int) 5 * [2] = (int) 3 */
A wiec jak widzisz, zmiana zawartosci tablicy ‘arr2’ zmienia rowniez zawartosc tablicy ‘arr1’. Zeby uczynic ja unikalna, musisz wpierw wykonac kopie ‘arr1’, na przyklad poprzez dodanie do niej pustej tablicy ‘({ })’.
Jak juz wiesz, tablice beda automatycznie zaalokowane poprzez zwykle wpisanie czegos w nie badz poprzez dodanie elementu lub innej tablicy. Jesli jednak chcesz natychmiast zaalokowac tablice do odpowiedniego rozmiaru to mozesz uzyc efunkcji ‘allocate()’. Rozmiar podaje sie jako jedyny argument. Funkcja zainicjalizuje podana liczbe elementow i ustawi je wszystkie na 0, niezaleznie od typu tablicy.
mixed *allocate(int rozmiar) np. string *str_tabl; str_tabl = allocate(3); str_tabl[1] = "Fatty jest sflaczalym szowinista"; dump_array(str_tabl); /* Efektem jest:
(Array) [0] = (int) 0 [1] = (string) "Fatty jest sflaczalym szowinista" [2] = (int) 0 */
Jesli chcesz sprawdzic, czy dane wyrazenie jest elementem tablicy i jesli tak, to jaki jest indeks tego elementu, to mozesz skorzystac z efunkcji ‘member_array()’, podajac jako argumenty tablice i szukany element. Funkcja zwroci numer indeksu, jesli znajdzie element, albo -1 gdy poszukiwania zakoncza sie niepowodzeniem. Jesli w tablicy bedzie wiecej odpowiadajacych elementow, to ‘member_array()’ zwroci indeks pierwszego z nich.
int member_array(mixed element, mixed tablica)
np.
int *tab = ({ 1, 55443, 123, -3, 5, 828, 120398, 5, 12 }); int indeks; // Wszystkie '5' zostana zastapione przez '33' while ((indeks = member_array(5, tablica)) != -1) tab[indeks] = 33;
Bardzo wazna efunkcja dotyczaca tablic jest ‘sizeof()’. Zwraca ona rozmiar podanej tablicy, tzn. liczbe elementow znajdujacych sie w niej. Czesto zachodzi potrzeba napisania petli oblatujacej wszystkie elementy tablicy, albo po prostu znalezienia indeksu ostatniego elementu i wtedy ta efunkcja sie bardzo przydaje.
UWAGA! Indeks ostatniego elementu wynosi rozmiar_tablicy - 1 : (sizeof(tablica) - 1), gdyz numeracja indeksow zaczyna sie od 0.
int sizeof(mixed tab) np. string *tab = ({ "Fatty", "szownita" }); write(implode(tab, " ") + " jest zle.\n"); tab[sizeof(tab) - 1] = "szowinista"; write(implode(tab, " ") + " jest poprawnie.\n");
Efunkcja ‘pointerp()’ moze byc zastosowana do sprawdzenia, czy zmienna zawiera tablice (dowolnego typu), czy nie. Jest bardzo przydata, jesli masz doczynienia z funkcjami, ktore moga zwrocic 0 (wartosc NULL), gdy cos pojdzie nie tak jak powinno.
int pointerp(mixed tab) np. string *tab; if (pointerp((tab = find_player("zdzichu")->pobierz_gildie()))) write("Zdzichu nalezy do: " + implode(tab, ", ") + ".\n"); else write("Zdzichu nie nalezy do zadnej gildii!.\n");
Funkcje obsługujące Mappingi
[mkmapping, mappingp, m_sizeof, m_delete, m_indices, m_values, m_restore_object, m_save_object]
Tak jak to wczesniej mowilem, mappingi sa listami indeksow powiazanych z wartoscami. Podajac indeks, otrzymujesz przypisana do niego wartosc. Zawartosc mappingow jest ulozona w specjalnie posortowany sposob, dzieki czemu dostep do nich jest bardzo szybki. Jednakze maja pewna wade – sa strasznie pamieciozerne i zuzywaja bardzo duzo miejsca w porownaniu do tablic.
Jak sie alokuje mappingi? Jest to bardzo proste. Wystarczy, ze zadeklarujesz go, a potem przypiszesz jedna wartosc do indeksu. Jesli indeks bedzie juz istnial, wartosc przy nim zostanie zastapiona nowa. A jesli nie, to zostanie dodana nowa para. Do tworzenia mappingow mozna tez uzyc efunkcji ‘mkmapping()’ i jako argumenty podac dwie tablice, jedna z samymi indeksami, a druga z wartosciami. Pamietaj tylko, ze musza miec one taki sam rozmiar.
mapping mkmapping(mixed tab_ind, mixed tab_war) np. string *ind_tab, *wart_tab; mapping mp; mp["lewy"] = "wielki"; mp["alvin"] = "straszny"; mp["fatty"] = "grubas"; // ... jest tym samym co ... ind_tab = ({ "lewy", "alvin", "fatty" }); wart_tab = ({ "wielki", "unikalny", "grubas" }); mp = mkmapping(ind_tab, wart_tab); // Mozesz oczywiscie podac te tablice bezposrednio, // bez poslugiwania sie zmiennymi
Tak jak w tablicach, tu tez jest funkcja sprawdzajaca czy dana zmienna zawiera mapping, czy nie. Jest nia ‘mappingp()’. Uzywaj jej do tego samego celu, tzn. gdy jakas funkcja moze, ale nie musi zwracac mappingu, a ty chcesz miec pewnosc zanim zaczniesz indeksowac zwracana wartosc.
Do znajdywania rozmiaru mappingu moze ci posluzyc efunkcja ‘m_sizeof()’. Dziala dokladnie tak samo, jak odpowiednik u tablic, zwracajac liczbe elementow (par) w mappingu.
W mappingach usuwanie elementow juz nie jest takie proste, jak w tablicach. Sluzy do tego efunkcja ‘m_delete()’. Jej dzialanie nie polega na bezposrednim usunieciu elementu, tylko na stworzeniu nowego mappingu i przekopiowaniu tam zawartosci starego bez podanego elementu.
mapping m_delete(mapping map, mixed elem) np. mapping mp, m_nowy; mp["lewy"] = "wielki"; mp["alvin"] = "straszny"; mp["fatty"] = "grubas"; m_nowy = m_delete(mp, "fatty"); dump_array(m_nowy); /* Wynik: * * (Mapping) ([ * "lewy":"wielki" * "alvin":"straszny" * ]) */
A jak zdobyc wszystkie elementy mappingu? Na przyklad chcielibysmy jakiejs odwrotnosci ‘mkmapping()’. Do tego sluza dwie funkcje: ‘m_indices()’ oraz ‘m_values()’, ktore zwracaja (kolejno) wszystkie indeksy oraz wszystkie wartosci danego mappingu.
Doszlismy do dosyc niestabilnej kwestii – kolejnosci elementow w mappingach. Jak wczesniej mowilem, mappingi nie maja jej na stale zdefiniowanej. Tzn. maja, ale nie jest to temat ktorym powinienes sobie zawracac glowe. Zmienia sie ona gdy dodajesz, albo usuwasz jakas pare. W kazdym razie wazne jest to, ze gdy pobierzesz wszystkie indeksy i wartosci z jakiegos mappinga (za pomoca m_indices() i m_values()), to elementy otrzymanych tablic beda sobie odpowiadaly o ile pomiedzy pobieraniami nie wykonywales zadnych operacji na tym mappingu.
mixed m_indices(mapping mapp); mixed m_values(mapping mapp); np. // Funkcja wyswietla mapping i jego zawartosc void dump_mapping(mapping mp) { int i, sz; mixed ind, war; ind = m_indices(mp); // Pomiedzy tymi dwoma intrukcjami, nie war = m_values(mp); // powinno byc zadnych dzialan na mappingu. sz = sizeof(ind); for (i = 0 ; i < sz ; i++) write(sprintf("%O", ind[i]) + " przypisane do " + sprintf("%O", war[i]) + "\n"); } /* Na przyklad uruchamiamy: dump_mapping(([ "fatty" : "grubas", * "lewy" : "wielki", * "alvin" : "straszny" * ])); * Otrzymujemy: * * "alvin" przypisane do "straszny" * "fatty" przypisane do "grubas" * "lewy" przypisane do "wielki" */
Sa, albo beda dwie funkcje, ktore zapisuja i odtwarzaja dane obiektu. Niestety, jak na razie maja one jeszcze bledy i nie dzialaja dokladnie tak, jakbysmy sobie tego zyczyli. Powinny one funkcjonowac w ten sposob: ‘m_save_object()’ stworzy mapping, zawierajacy wszystkie globalne, nie-statyczne zmienne, ktorych nazwy beda indeksami. Bedziesz mogl wtedy zgrac go bezposrednio do pliku, albo przekazac go dalej, jako argument jakiejs funkcji. Odwrotnoscia tego bedzie funkcja ‘m_restore_object()’. Bedzie ona przyjmowala mapping jako argument, rozkladala jego elementy ustawiajac zmiennym globalnym odpowiadajace wartosci.
Konwersja typów
[atoi, atof, ftoa, itof, ftoi, str2val, val2str, sscanf]
Wiekszosc rzeczy wpisywanych przez gracza to stringi; wpisujesz cos i gra powinna odpowiednio na to zareagowac. Wymaga to istnienia funkcji, ktore zanalizuja twoje komendy i przetlumacza to na wartosci, ktorych mozesz uzyc w swoich programach. Analiza skladni komend, jest lekko mowiac bardzo skomplikowana i zostawiam to na trzeci rozdzial. Teraz skupmy sie tylko na konwertowaniu samych wartosci. Jak mowilem, to co gracze wpisuja jest w formie stringow, przez co zamienianie stringow w integery oraz floaty i vice versa moze byc dosyc pozyteczna umiejetnoscia.
Zacznijmy od integerow. Zalozmy, ze otrzymales string zawierajacy jakas wartosc numeryczna i chcesz jej uzyc do jakis obliczen. Musisz wiec przeksztalcic stringa w integera. Sluzy do tego efunkcja ‘atoi()’; podajesz string jako argument i otrzymujesz integer – bardzo proste. Lancuch nie moze jednak zawierac zadnych znakow innych niz cyfry. W przeciwnym wypadku funkcja zwroci 0.
int atoi(string str) np. int war; war = atoi("23"); write("23 + 3 = " + (war + 3) + "\n");
Liczby zmiennopozycyjne (floaty) maja analogiczna efunkcje, ‘atof()’, ktora przeksztalca stringa we floata. Jak juz wiesz, floaty nie moga byc przekonwertowane w stringi w ten sam sposob, jak integery, czyli poprzez dodanie ich do innego stringa. Funkcja ‘ftoa()’ zamienia floata w stringa. Tak samo jak w przypadku ‘atoi()’, jesli w ‘atof()’ podasz jakis string, w ktorym beda znaki nie-numeryczne, to zwroci 0 (wyjatkiem beda specjalne znaki, takie jak np. ‘.’.
Do zamiany pomiedzy integerem a floatem sluza efunkcje ‘itof()’ oraz ‘ftoi()’. Pamietaj tylko, ze gdy zamieniasz floata w integera, to czesc dziesietna nie jest zaokraglana, tylko obcinana.
Jest wiele momentow, w ktorych mozesz chciec przechowac wartosc w stringu, a potem przemienic ja spowrotem. Sluza do tego efunkcje ‘val2str()’ oraz ‘str2val()’. Da sie wyswietlic zawartosc ‘val2str()’, ale nie do tego zostala ona stworzona. Mozesz przechowac dowolna wartosc zmiennej, korzystajac z tych funkcji
Najczesciej jednak uzywanym konwerterem danych jest efunkcja ‘sscanf()’. Mozesz sprecyzowac, gdzie ma ona szukac wartosci (podobnie jak w ‘sprintf()’), w celu pobrania ich i przechowania w jakiejs zmiennej. Jest ona charakterystyczna, gdyz ustawia wartosci zmiennych podanych w argumencie, wiec niemozliwe jest pobranie jej adresu. Poza tym, dziala ona bardzo prosto. Podaje sie matryce, string do przeszukania i zmienne, w ktorych dane powiny byc przechowane. Funkcja zwraca liczbe znalezionych wartosci.
String, ktory podasz jako matryce, jest interpretowany doslownie, z wyjatkiem ponizszych lancuchow kontrolnych.
- %d
- porownuje liczbe calkowita.
- %s
- porownuje lancuch znakow.
- %f
- porownuje liczbe zmiennopozycyjna.
- %%
- porownuje znak `%'.
int sscanf(string str, string matryca, <zmienne>...); np. int szer; float waga; string orgstr; string typ_szer, typ_wagi; /* * Zalozmy, ze zadawane jest pytanie : * "Jak ci sie wydaje, jak ciezki i szeroki jest Fatty?". * Oprocz tego zalozmy, ze odpowiedz jest dana w formie * '<ilosc> <rodzaj> i <ilosc> <rodzaj>', na przyklad: * '4 metry i 3.2 tony'. Przyjmijmy, ze pierwsza wartosc moze byc * tylko integerem, a trzecia tylko floatem. * * Zalozmy jeszcze, ze odpowiedz jest podana w zmiennej `orgstr' */ if (sscanf(orgstr, "%d %s i %f %s", szer, typ_szer, waga, typ_wagi) != 4) { write("Podaj pelna odpowiedz!\n"); return; } write("Aha, uwazasz, ze Fatty jest szeroki na " + szer + " " + typ_szer + " i wazy " + ftoa(waga) + " " + typ_wagi ".\n");
Funkcje matematyczne
[random, rnd, sin, cos, tan, asin, acos, atan, atan2, exp, log, pow,
sinh, cosh, tanh, asinh, acosh, atanh, abs, fact, sqrt]
Efunkcja ‘random()’ zwraca dowolna liczbe calkowita z zakresu od 0, do podanej w argumencie liczby minus jeden. Na przyklad ‘random(8)’ zwroci losowa liczbe z zakresu 0-7.
Reszta funkcji matematycznych pobiera jako argumenty i zwraca floaty. Funkcje trygonometryczne uzywaja radianow, nie stopni. Pamietaj o tym.
Oto pelna lista funkcji matematycznych:
- float rnd()
- Zwraca losowa liczbe z zakresu od 0 do 1
- float sin(float)
- Oblicza sinus podanego kata.
- float cos(float)
- Oblicza cosinus podanego kata.
- float tan(float)
- Oblicza tangens podanego kata.
- float asin(float)
- Oblicza arcus sinus z zakresu od -pi/2 do pi/2.
- float acos(float)
- Oblicza arcus cosinus z zakresu od 0 do pi.
- float atan(float)
- Oblicza arcus tangens z zakresu od -pi/2 do pi/2.
- float atan2(float x, float y)
- Compute the argument (phase) of a rectangular coordinate in the range -p to p.
- (!!!) – Hmm, ktos wie co to moze znaczyc?
- float exp(float)
- Compute the exponential function using the natural logarithm e as base
- (!!!) – Rety, nastepny kwiatek... ln do potegi ?
- float log(float)
- Oblicza logarytm naturalny.
- float sinh(float)
- Oblicza wartosc sinusa hiperbolicznego.
- float cosh(float)
- Oblicza wartosc cosinusa hiperbolicznego.
- float tanh(float)
- Oblicza wartosc tangensa hiperbolicznego.
- float asinh(float)
- Oblicza wartosc arcusa sinusa hiperbolicznego.
- float acosh(float)
- Oblicza wartosc arcus cosinusa hiperbolicznego.
- float atanh(float)
- Oblicza wartosc arcus tangensa hiperbolicznego.
- float abs(float)
- Oblicza wartosc absolutna argumentu (modul).
- float fact(float)
- Oblicza silnie (funkcja gamma) podanego argumentu.
- float sqrt(float)
- Oblicza pierwiastek kwadratowy podanego argumentu.
Obsługa plików
[save_object, restore_object, save_map, restore_map, write_bytes,
read_bytes, write_file, read_file, file_size, file_time, rename, rm, ed]
Przechowywania informacji w plikach jest bardzo wazne. Zaraz pokaze ci kilka funkcji, ktore ci w tym pomoga. Pozwol mi jednak strzelic male kazanie, na temat zuzycia CPU:
Odczyt i zapis plikow jest sprawa bardzo obciazajaca CPU. Moze nie w porownaniu do innych procesow, ale podczas operacji dyskowej CPU nie moze robic nic innego. Innymi slowy, wczytywanie i zapisywanie duzych porcji danych znacznie spowalnia gre. Zeby ograniczyc zbyt duze obciazanie pamieci, dysku i CPU, narzucono limit nie pozwalajacy obluzyc wiecej niz 50 kb danych na raz. Pliki moga byc wieksze, ale ty nie mozesz zapisywac ni wczytywac fragmentow wiekszych niz ten limit. Oznacza to, ze musisz podzielic prace nad wiekszymi plikami na kawalki wykonywane sekwencyjnie, z ewentualna przerwa pomiedzy nimi, zeby reszta gry tez miala czas na zrobienie czegos. Wiec, prosze pamietaj o tym, ze to ograniczenie nie zostalo stworzone, by ci utrudnic zycie i zeby bylo obchodzone roznymi kretymi sposobami, tylko aby ci przypominac, ze obciazasz zasoby muda i ze powinienes innym tez dac zyc. Amen.
Zacznijmy od bardzo prostej sprawy: nagrywania i odtwarzania obiektow. Zazwyczaj, chce sie przechowac zmienne globalne obiektu w pliku, po to by je pozniej odtworzyc. Do tego celu sluza efunkcje ‘save_object()’ oraz ‘restore_object()’. W obu musisz podac sciezke wraz z nazwa pliku jako argument i oczywiscie miec prawa do zapisu lub do odczytu. Wynikiem ‘save_object()’ bedzie plik, ktorego nazwa bedzie miala koncowke ‘.o’. Przy odtwarzaniu musisz pamietac o tym, by ja podac. Przy nagrywaniu to nie jest konieczne – funkcja sama doda rozszerzenie ‘.o’, gdy ty o tym zapomnisz. ‘restore_object()’ zwraca 1, gdy odtwarzanie zakonczylo sie sukcesem, zas w przeciwnym wypadku zwraca 0. Zawartoscia nagranego pliku jest lista wszystkich zmiennych globalnych z ich wartosciami, oddzielonymi spacjami. Format zapisu przechowanej zmiennej jest taki sam, jak w przypadku juz omowionej funkcji ‘val2str()’.
Mappingi sa najbardziej wygodnym typem do przechowywania zmiennych. Wystarczy umiescic dane w mappingu, gdzie indeksami sa nazwy zmiennych, a potem nagrac mapping przy pomocy efunkcji ‘save_map()’, by pozniej odtworzyc go poprzez ‘restore_map()’. Przewaga tej metody nad ‘save/restore_object()’ jest to, ze nie jestes ograniczony wylacznie do nie-statycznych zmiennych i mozesz przechowac co ci sie zywnie podoba. Minusem jest to, ze odtwarzanie danych z mappingu jest juz ciut bardziej skomplikowane.
void save_object(string sciezka_nagr); int restore_object(string sciezka_odcz); void save_map(mapping mapp, string sciezka_nagr); mapping restore_map(string sciezka_odcz); np. /* * Zalozmy, ze mamy takie definicje zmiennych globalnych: * * string imie, *opis; * int flip; * mapping dane_map, smap; * * Zalozmy, ze interesuje nas przechowanie zmiennych imie, opis, * flip i dane_map */ // Ustawienie dziedzicznych przywilejow poprzez nadanie obiektowi // euid tworcy pliku setuid(); seteuid(getuid()); // Metoda nagrywania 1 save_object("mojplik"); // Metoda odtwarzania 1 if (restore_object("mojplik")) write("Tak!\n"); else write("Nieeee..\n"); // Metoda nagrywania 2 smap = ([ "imie" : imie, "opis" : opis, "flip" : flip, "dmap" : dane_map ]); save_map(smap, "mojplik"); // Metoda odtwarzania 2 smap = restore_map("mojplik"); if (m_sizeof(smap)) { imie = smap["imie"]; // Odtwarza imie opis = smap["opis"]; // Odtwarza opis flip = smap["flip"]; // Odtwarza flip dane_map = smap["dmap"]; // Odtwarza dane_map write("Tak!\n"); } else write("Nieeee..\n");
Musisz zapamietac, ze format zapisu uzywany przez ‘save_object()’ i przez ‘save_map()’ jest taki sam, dzieki czemu mozna odtworzyc plik zapisany przez ‘save_object()’ przy pomocy ‘restore_map()’ i wybrac z otrzymanego mappinga, tylko te elementy, na ktorych nam zalezy. Zalozmy, ze bys chcial odtworzyc tylko zmienna ‘opis’ w powyzszym przykladzie. Nie martwil bys sie wtedy innymi zmiennymi i uzylbys wtedy okrojonej drugiej metody odtwarzania. Uwazaj na odczytywanie danych, ktore zostaly nagrane przez ‘save_map()’ instrukcja ‘restore_object()’. Zeby wszystko bylo poprawnie, indeksy nagranego mappingu musialy by miec takie same nazwy, jak zadeklarowane w obiekcie zmienne globalne. W powyzszym przykladzie sa roznice pomiedzy nimi, wiec odtworzenie metoda 1, danych zgranych metoda 2 skonczyloby sie bledem. W odwrotnej sytuacji, czyli przy wczytywaniu metoda 2 danych przechowanych metoda 1 nie wyskoczy zaden blad, ale nie uda sie odtworzyc zmiennej ‘dane_map’.
To byly wszystkie metody przechowywania zmiennych w plikach. Nieraz moze zajsc potrzeba nagrania danych w wolnej formie, a nie tylko w zmiennych. Do tego celu sluza efunkcje ‘write_bytes()’ i ‘read_bytes()’, albo ‘write_file()’ i ‘read_file()’. W zasadzie obie pary funkcji dzialaja bardzo podobnie, tzn wczytuja i nagrywana stringi o okreslonej dlugosci. Jedyna roznica jest to, ze ‘write_bytes()’ moze byc uzyte do nagrywania na stare dane w pliku (overwriting), podczas gdy ‘write_file()’ jest w stanie zapisywac tylko na koncu. No i ‘read_bytes()’ operuje na bajtach, a ‘read_file()’ na pelnych liniach. Obie zapisujace funkcje zwracaja 1 w przypadku powodzenia, zas 0 w gdy cos sie nie uda. Obie wczytujace funkcje zwracaja string, zawierajacy odczytana porcje danych w przypadku sukcesu, a 0 w przypadku niepowodzenia, wiec sprawdzaj rezultat przy pomocy ‘stringp()’, zeby sie upewnic, ze wszystko bylo ok.
int write_bytes(string sciezka, int poz, string tekst); string read_bytes(string sciezka, void|int poz, void|int num); int write_file(string sciezka, string tekst); string read_file(string sciezka, void|int poz, void|int num);
O pliku rowniez da sie uzyskac informacje. W poznaniu rozmiaru pliku moze dopomoc efunkcja ‘file_size()’. Mozna ja tez wykorzystac do sprawdzenia obecnosci pliku. Jesli liczba zwrocona przez funkcje jest dodatnia, to oznacza to, ze jest to rozmiar. Gdy nie ma pliku, funkcja zwraca -1, a gdy plik jest katalogiem, -2. Aby poznac czas ostatniej modyfikacji mozna uzyc efunkcji ‘file_time()’.
int file_size(string sciezka); np. void plik_info(string sciezka) { int typ, tm; rozm = file_size(sciezka); tm = file_time(sciezka); write("Plik '" + sciezka + "' "); switch (rozm) { case -1: write("nie istnieje.\n"); break; case -2: write("jest katalogiem, ostatni raz zmodyfikowanym " + ctime(tm) + ".\n"); break; default: write("ma rozmiar " + rozmiar + " bajtow, ostatni raz " + "byl zmodyfikowany " + ctime(tm) + ".\n"); break; } }
Jesli chcesz zmienic nazwe albo przemiescic plik, to efunkcja ‘rename()’ jest w sam raz dla ciebie. Ale uwazaj, gdyz jej dzialanie polega na skopiowaniu pliku, a potem skasowaniu starej wersji. Moze byc rowniez uzyta do przesuniecia katalogow. Jesli zyczysz sobie usunac caly plik, to mozesz skorzystac z efunkcji ‘rm()’. Zwraca ona 1 w przypadku sukcesu, a 0 w przypadku niepowodzenia. Uwazaj, bo ‘rename()’ dziala dokladnie odwrotnie, gdyz zwraca 1 w przypadku niepowodzenia, a 0 jak wszystko jest ok.
int rename(string stara_sciezka, string nowa_sciezka); int rm(string sciezka); np. if (rm("mojplik")) write("Ok, skasowane.\n"); else write("Sorki, cos nie idzie.\n"); if (rename("plik_stary", "plik_nowy")) write("Niestety, wciaz po staremu...\n"); else write("Ok!\n");
Wewnetrzny edytor ‘ed’ jest efunkcja, ktora operuje na plikach. Mozesz go uzywac do czegokolwiek, lecz pamietaj, ze wiekszosc ludzi nie wie jak z niego korzystac. Pamietaj takze, ze efunkcja ‘ed()’ moze byc uzyta do stworzenia nowego pliku, albo edycji starego, na podstawie praw obiektu, ktory ja wywolal. Jesli podasz wskaznik do funkcji, to przy wyjsciu z ‘ed()’ zostanie ona wywolana. Jesli nie podasz sciezki, uzytkownik bedzie musial sam wpisac sciezke i nazwe pliku wewnatrz edytora.
void ed(void|string sciezka, void|function fun_wyjsciowa);
Obsługa katalogów
[mkdir, rename, rmdir, get_dir]
Tworzenie, zmienianie nazwy i usuwanie katalogow robi sie przy uzyciu efunkcji (kolejno) ‘mkdir()’, ‘rename()’, ‘rmdir()’. Aby je wykonac musisz oczywiscie miec prawa do zapisu w katalogu, w ktorym to robisz. ‘mkdir()‘ i ‘rmdir()’ zwracaja 1 w przypadku sukcesu, a 0 w razie niepowodzenia. ‘rename()’ jak juz mowilem dziala odwrotnie i zwraca 1 w przypadku niepowodzenia. ‘rmdir()’ kasuje tylko puste katalogi, tzn takie, ktore nie zawieraja zadnych plikow ani katalogow.
int mkdir(string sciezka); int rename(string stara_sciezka, string nowa_sciezka); int rmdir(string sciezka);
W celu pobrania zawartosci katalogu uzywa sie efunkcji ‘get_dir()’. Zwraca ona tablice albo zawierajaca wszystkie pliki w podanym katalogu w przypadku sukcesu, albo pusta w przypadku niepowodzenia.
string *get_dir(string sciezka); np. string *zawartosc_kat; int i, sz; zawartosc_kat = get_dir("/d/Domena/fatty"); for (i = 0, sz = sizeof(zawartosc_kat) ; i < sz ; i++) { // Obejrzyj kod funkcji plik_info w poprzednim przykladzie file_info(zawartosc_kat[i]); }
Wejście/wyjście konsoli
[write, write_socket, cat, tail, input_to]
Juz zapewne jestes za pan brat z efunkcja ‘write()’, ktora wyswietla dane na ekranie osoby zdefiniowanej jako sluchacz w danym obiekcie. Moze nim byc gracz, ale moze rowniez byc nim jakis inny obiekt. Zazwyczaj funkcja wystarcza, gdy masz pelna kontrole nad tym co i do kogo piszesz. Jednakze istnieje pewna podobna funkcja, ktora czasem staje sie potrzebna. Nazywa sie ona ‘write_socket()’. Dziala ona prawie tak jak ‘write()’, tylko ze wypisuje teksty wylacznie na ekranie interakcyjnego uzytkownika. Jesli nie ma takowego, to zamiast do niego, pisze w glownym logu bledow. W czasie kodowania zwyklych obiektow nie bedziesz musial jej uzywac. Przewaznie, lub nawet zawsze jest wykorzystywana w konkretnych obiektach mudliba, czesciowo tych, ktore maja doczynienia z logujacymi sie graczami.
Wypisywanie write'm jest niezle, ale czasem mozesz miec chrapke na blyskawiczne wyswietlenie plikow, lub przynajmniej ich czesci. Do tego sluzy efunkcja ‘cat()’. Ukaze ona na ekranie okreslona porcje podanego pliku na ekranie. Istnieje tez inna efunkcja zwana ‘tail()’, ktora wyswietli ostatnie 1080 bajtow pliku. ‘cat()’ zwraca liczbe ukazanych linii, zas ‘tail()’ zwraca 1 w przypadku sukcesu, a 0 w przypadku niepowodzenia.
int cat(string sciezka, int start, int dlug); int tail(string sciezka);
np.
// Wyswietla PLIKTEST, od 20 do 100 linii cat("PLIKTEST", 20, 80) // Wyswietla koncowke tego samego pliku tail("PLIKTEST");
Wiekszosc rzeczy wpisywanych w grze przez graczy przychodzi w formie argumentow do funkcji. Czasem jednak potrzeba zapytac gracza o cos, a on musi odpowiedziec. Pojawia sie tu problem, gdyz wykonanie obiektow w gamedriverze jest sekwencyjne; jesli sie zatrzymasz z wykonywaniem czekajac na odpowiedz, cala gra zatrzyma sie wraz z toba, do czasu az leniwy gracz sie namysli (o ile kiedykolwiek) i odpowie. Z pewnoscia taka sytuacja nie jest dobrym rozwiazaniem. Zamiast tego, mozna wywolac efunkcje ‘input_to()’, ktora pozwala ci ustalic ktora funkcja zostanie wywolana majac za argument jakakolwiek odpowiedz gracza, po ukonczeniu wykonywania aktualnej funkcji. Brzmi to troche skomplikowanie, ale na prawde wcale takie nie jest. Spojrz tylko na przyklad:
void input_to(string funkcja, void|int bezecha, void|mixed arg);
np.
string imie, plec, zajecie; pytajaca_fun() { write("Wpisz swoje imie: "); input_to("fun_2"); } fun_2(string wpis) { imie = wpis; write("\nWpisz swoja plec (Facet/Kobitka): "); input_to("fun_3"); } fun_3(string wpis) { plec = wpis; write("\nCzym sie zajmujesz?: "); input_to("fun_4"); } fun_4(string wpis) { zajecie = wpis; write("\n\nDzieki za wspolprace!\n"); }
Jesli jako drugi argument do ‘input_to’ podasz 1, to cokolwiek gracz wpisze, nie zostanie wyswietlone na ekranie. Uzywa sie tego przy haslach i innch waznych informacjach. Trzeci, opcjonalny argument jest przekazywany do funkcji, ktora podales jako odbiorce.
Niektóre komendy MUD-a
Moze to wygladac nieco dziwnie, ze rozdzial ten umiescilem tak nisko. Problem ten podobny jest do zagadnienia : co bylo pierwsze jajko czy kura. Uzywanie tych komend jest niemozliwe, zanim nie zrozumie sie na czym to wsystko naprawde polega. By to zrozumiec zawsze mozna empirycznie eksperymentowac z komendami... Jesli rozpoczales lekture tego manualu przed eksperymentowaniem, to rozdzial ten na pewno ci sie przyda.
Prosilbym przede wszystkim bys skupil sie na swoich umiejetnosciach edytorskich. Naucz sie obslugiwac ed, ktory jest jedynym wewnatrzmudowym edytorem, lub uzywaj zewnetrznego edytora i pliki slij przez ftp. Polecam edytor emacs i ftp. Pamietaj, ze pliki mozesz tworzyc takze pod Windowsem np. w Notatniku, ale zanim wyslesz pliki na serwer musisz je konwertowac na format UNIXa.
Ponizsze komendy sa dobrze opisane w mudzie, po prostu wpisz ?<komenda>, by uzyskac pelen opis. To co ja napisalem to zwykle podsumowanie i streszczenie tego.
load: Ladowanie obiektu do pamieci clone: Ladowanie i klonowanie obiektu do gry destruct: Niszczenie sklonowanego obiektu update: Aktualizacja obiektu odnow: Odnawianie obiektu
Kompilacja i ładowanie obiektów do pamięci gamedrivera
Komenda load nakazujesz gamedriverowi, by sprobowal skompilowac i zaladowac dany plik do pamieci. Komendy tej uzywa sie zazwyczaj, by sprawdzic czy dany plik w ogole sie zaladuje. Istnieja takze obiekty, ktore nie sa przeznaczone do klonowania (np. pokoje), a dzieki load udostepniamy je dla innych obiektow.
Jezeli plik sie nie zaladuje, komunikat o bledzie laduje w dwoch miejscach: glownym /lplog oraz w logu bledow nalezacym do domeny/czarodzieja, ktory stworzyl tenze obiekt. Dla domen sciezka do ostatniego ma ksztalt /d/<Domena>/log/errors, zas dla czarodziei /d/<Domena>/<czarodziej>/log/errors.
Niestety komunikaty o bledach sa nierzadko trudne do zrozumienia. Nie moge zatem w tej materii sluzyc pomoca, bo po prostu jest ogromna ilosc roznych typow takich komunikatow, zreszta zapewne z wiekszoscia sie spotkacie :) Wiele komunikatow latwo jest zrozumiec, a z czasem kazdy koder nauczy sie rozpoznawac i zapobiegac tym ciekawszym...
Komenda load moze obslugiwac takze wieksza ilosc plikow jednoczesnie w porcjach w odstepach czasowych. Ladowanie jednakze, to raczej obciazajaca proces operacja, wiec lepiej unikac masowego ladowania plikow.
CIEKAWOSTKA:
Uzywajac ls z opcja F (tzn. ls -F) widzisz w liscie plikow i katalogow , ktore zostaly juz zaladowane do pamieci (sa one oznaczone gwiazdka). Warto zatem uzywajac komendy <alias ls ls -F> nadpisac te komende.
Kompilacja, ładowanie i klonowanie obiektów do gry
Jezeli dany obiekt zostal zaladowany do pamieci, mozna go sklonowac. Zreszta jesli potrzeba komenda clone laduje i klonuje jednoczesnie. Jesli natkniesz sie na problemy pamietaj by sprawdzic logi bledow.
Niszczenie sklonowanego obiektu
Komendy destruct uzywamy, by usunac sklonowany obiekt z pamieci. Komenda ta niszczy jedynie dany obiekt nic wiecej.
Aktualizacja załadowanego obiektu
Komendy update uzywamy w przypadku gdy w pliku zaszla zmiana i chcemy, aby gamediver ponownie go skompilowal.
Odnawianie obieku
Warto takze zwrocic uwage na komende odnow. Komenda ta laczy w sobie cztery inne – niszczy stary klon (destruct), przeladowuje obiekt na nowa wersje (update, load) i klonuje z powrotem nowa wersje obiektu (clone) w miejsce, gdzie byl stary klon. W przypadku, gdy poda sie sciezke zamiast nazwy obiektu, komenda przeladuje podany obiekt i sklonuje jego nowa wersje.
Narzędzie Tracer
Wbudowany zestaw narzedzi czarodzieja, zwany Tracerem, moze okazac sie nieodlaczny przy oddzialywaniu na wnetrze obiektu, ktory juz zaladowano do gry. Pozwala bowiem wywolywac przerozne funkcje w dowolnym obiekcie, bez wzgledu na to gdze sie znajduje, pozwala takze na wyswietlenie inwentazra obiektu, a nawet przenoszenie obiektow z roznych miejsc w inne. Warto poznac ten zestaw narzedzi doglebnie.
Niektorzy skarza sie, ze Tracer jest dosyc ciezki w uzyciu. Jest tak dlatego, bo nie chcialo im sie poczytac wiecej o Tracerze w manualu gry. Wewnetrzy manual gry jest napisany trudnym jezykiem, wiec postanowilem blizej zajac sie Tracerem.
Warto zwrocic uwage, ze kazda komenda z Tracera zaczyna sie wielka litera. Sluzy to odroznieniu od zwyklych komend, z ktorych niektore nazywaja sie tozsamo. Komendy Tracera sa dostepne jedynie dla pelnych wizardow (Praktykant i dalej).
Na poczatek przyjrzyjmy sie temu, jak tracer widzi obiekty. Podam jak odwolywac sie do danego obiektu w danej sytuacji.
- nazwa
W ten sposob mozna odwolywac sie do obiektow znajdujacych sie w ekwipunku lub w twoim srodowisku (np. na tej samej lokacji). Uwaga! Wiele obiektow moze dzielic te same nazwy (np. ‘czlowiek’, ‘zbroja’, ‘miecz’). (uzycie np. In czlowiek ...)
- "opis"
Tu uzywamy krotkiego opisu obiektu, znajdujacego sie w ekwipunku lub w tym samym srodowisku (np. tej samej lokacji). Zazwyczaj podaje sie dokladniejszy opis, nie tylko sama nazwe. (uzycie np. Destruct "wysoki elf")
- sciezka
Tu podajemy pelna sciezke Jezeli chcesz operowac na okreslonym pojedynczym obiekcie, dodajesz numer klonowania. Np. sama sciezka ~Ansalon/guild/society/obj/nametag, albo ~Ansalon/guild/society/obj/nametag#22144 dla okreslonego obiektu.
- @nazwa
W tym przypadku operujemy na istocie zywej (potworze NPC), ktora znajduje sie gdziekolwiek w grze. Zauwazmy, ze tracer znajdzie nazwe ustawiona poprzez wywolanie efunkcji set_living_name() (opisana wczesniej w manualu), a nie przez nazwe okreslona przez set_name(), czy tez ustaw_nazwe(), ustaw_imie(), itp. Istota zywa (gracz) jest automatycznie dodawana do listy livingow.
- *nazwa
This specifies the name of a player, and nothing else.
- here
W tym przypadku dzialamy na srodowisku, w ktorym sie znalezlismy, np. na pokoju gdzie stoimy.
- me
Tutaj natomiast na samym sobie, swoim obiekcie gracza.
- #numer
Tutaj okreslamy numer obiektu. Jezeli np. wiesz, ze miecz, ktory masz przy sobie jest trzecim obiektem, wowczas odwolanie ma ksztalt #3. Nalezy pamietac, ze kolejnosc obiektow moze zmienic sie bez ostrzezenia, wowczas obiekt #3 bedzie zupelnie innym niz chcielismy. Zamiast tego nalezy uzywac zmiennych tracera (opisanych przy komendzie Set), by problemu uniknac..
- $zmienna
Okresla zawartosc zmiennej tracera (opisane przy komendzie Set). Obiekty czesto istnieja w roznych rodzajach srodowisk. Czasem w tym samym miejscu co inne, czasem wewnatrz innych, itp. W celu okreslenia relacji typu „drugi obiekt wewnatrz misia w tym samym pokoju, w ktorym stoje” podaje sie liste specyfikacji obiektow rozdzielona dwukropkiem (:). Srodowisko danego obiektu jest wowczas okreslone przez :^.
Przykladowo, poprzedni opis o misiu wygladalby tak:^mis:#2. To naprawde nie jest az tak skomplikowane jak wyglada.
Inny przyklad: „miecz w torbie trzymanej przez Adama”: *adam:torba:miecz.
At: Wykonanie polecenia w srodowisku gracza Call: Wywolanie funkcji obiektu/na obiekcie Cat: Podglad pliku zwiazanego z danym obiektem Clean: Niszczenie wszystkich nie zywych przedmioto w obiekcie Destruct: Niszczenie okreslonego obiektu Dump: Wyswietlenie przeroznych informacji o obiekcie Ed: Edycja pliku zwiazanego z obiektem Goto: Wejscie do srodowiska obiektu In: Wykonanie komendy w innym obiekcie Inventory: Wyswietlenie inwentarza obiektu Items: Przegladanie itemow zawartych w danym obiekcie/srodowisku Light: Wyswietlenie listy obiektow danego srodowiska More: Przegladanie pliku zwiazanego z obiektem w porcjach Move: Przemieszczenie obiektu do miejsca docelowego Set: Ustawienie zmiennej tracera Tail: Przegladanie porcjami pliku zwiazanego z obiektem, od konca
Wykonywanie komend w środowisku gracza
Komenda ta tak naprawde przenosi wywolujacego do srodowiska w ktorym przebywa dany gracz w celu wykonania danej komendy. Przeniesienie to odbywa sie w sposob bezwzgledny, tzn. zadne sprawdzenia mozliwosci przeniesienia nie sa sprawdzane.
Skladnia: At <imie_gracza_w_mianowniku> <polecenie>
Zazwyczaj istnieja lepsze sposoby do osiagniecia podobnego celu, jednak dla pewnych kwestii komenda At okazuje sie nieodzowna. Dobrym na to przykladem jest sytuacja, gdy chcemy spojrzec na pokoj, w ktorym znajduje sie dany gracz, np. Adam. W tym celu wykonujemy:
At adam sp
UWAGA! Warto zachowac ostroznosc przy wykonywaniu tego wobec smiertelnikow, istnieja sytuacje, w ktorych wywolujacy moze okazac sie widoczny dla gracza. Warto przeto uzyc ‘invis’ nim wywolamy podobna komende.
Wywoływanie funkcji na obiekcie
Komenda Call jest szczegolnie uzyteczna. Dzieki niej mozesz wyolywac funckje na obiekcie podajac dowolne parametry [w praktyce jednak nie kazdy typ argumentu da sie ta droga bezproblemowo przekazac].
Skladnia: Call <dany_obiekt> arg1%%arg2%%...
Przykladowo, aby ustawic wlasnosc OBJ_I_VOLUME na 55 w drugim obiekcie z ekwipunku Adama wykonujemy:
Call *adam:#2 add_prop _obj_i_volume%%55
Warto zauwazyc, ze odwolano sie bezposrednio do stringu "_obj_i_volume", albowiem jest on dopiero na poziomie <stdproperties.h> definiowany jako zamiennik dla OBJ_I_VOLUME, ale tego Call niestety nie uwzglednia.
Przegladanie zawartości pliku związanego z danym obiektem
Komenda Cat, wspolnie z More jest dosyc ciekawa i czesto uzyteczna komenda. Zwraca ona zrodlo pliku zwiazanego z danym obiektem, o ile mamy prawa odczytu dla niego. Oczywiscie Cat zwraca jedynie 100 pierwszych linijek kodu, by przejrzec calosc nalezy uzyc stronnicowania [zobacz komende More].
Skladnia: Cat <dany_obiekt>
Zniszczenie wszystkich nieinteraktywnych zawartych w obiekcie docelowym
Clear jest uzyteczna komenda dla czyszczenia zawartosci danego obiektu. Czesto uzywa sie jej, gdy np. mamy duzo obiektow w danej lokacji i chcemy szybkim ruchem je usunac albo gdy nierozwazny czarodziej sklonuje obiekt, ktorego dzialania nie do konca przewidzial i przykladowo sparalizuje go on, wowczas dowolny inny czarodziej moze go szybko uratowac uzywajac tej komendy ;)
Skladnia: Clean <dany_obiekt>
UWAGA! Wszystkie nieinteraktywne obiekty, jak np przedmioty i npce, ulegna zniszczeniu.
Zniszczenie określonego obiektu
W celu zniszczenia konkretnego obiektu, uzywamy komendy Destruct. Czasami np. gdy funkcja leave_inv() obiektu zawiera wadliwy kod, dany obiekt moze nie ulec zniszczeniu od razu. Nalezy wtedy uzyc opcji -D, czyli np. Destruct -D miecz, by wymusic usuniecie danego obiektu.
Skladnia: Destruct [-D] <dany_obiekt>
UWAGA! Nalezy unikac uzywania opcji -D i stosowac ja wylacznie wtedy, gdy jest to konieczne. Zaleznie od wersji mudliba moze wystepowac opcja -f, dzialajac tozsamo jak -D.
Pobieranie szczegółowych informacji o danym obiekcie
Komendy Dump uzywamy by pobrac specyficzne informacje z danego obiektu. Wybor jest dosyc duzy.
Skladnia: Dump <dany_obiekt> <jakie_informacje_chcemy>
Mozliwe informacje:
- <nic>
Komenda Dump <dany_obiekt> pobiera nazwe, sciezke, uid tworcy oraz euid docelowego obiektu.
- <zmienna>
O ile zmienna nie jest rownoznaczna z jednym z ponizej opisanych parametrow, to jest interpretowana jako zmienna w badanym obiekcie. Komenda zwraca nam tedy wartosc tejze zmiennej.
UWAGA! Jest to naprawde uzyteczny sposob uzycia tej komendy. Przydac sie moze przy roznego rodzaju debugowaniu kodu. Nie trzeba bowiem ingerowac w kod obiektu by dowiedziec sie jaka wartosc ma zmienna w danym obiekcie. Oczywiscie wciaz sporo jest przypadkow, w ktorych to nie wystarczy i trzeba uzyc wlasnych funkcji debugu.
- alarms
- Zwraca wszystkie biezace alarmy „biegajace po” danym obiekcie.
- cpu
- Parametr ten zwraca ile czasu procesora zostalo zuzyte przez badany obiekt. Dziala wylacznie gdy wlaczono PROFILE_OBJS w config.h drivera. Opcji tej uzywa sie generalnie przy rozwijaniu mudliba lub tez badaniu jego wydajnosci.
- flags
- Zwraca wszystkie pochodzace z drivera flagi zwiazane z badanym obiektem, wraz z innymi informacjami o stanie obiektu. Informacje te sa uzyteczne jedynie dla osob pracujacych nad udoskonaleniem drivera.
- functions
- Zwraca wszystkie funkcje zdefiniowane w danym obiekcie.
- UWAGA! Zwraca jedynie funkcje z obiektu polozonego najwyzej w hierarchii dziedziczenia. Tzn. nie podaje funkcji, ktore obiekt nabyl z innego dziedziczonego obiektu.
- info
- Parametr ten zwraca pewne podstawowe zwiazane z driverem informacje przyporzadkowane do danego obiektu. Ponownie, informacje te sa uzyteczne jedynie dla osob pracujacych nad driverem.
- inherits
- Uzyteczny parametr zwracajacy liste obiektow, po ktorych dany obiekt dziedziczy.
- inv | inventory
- Zwraca inwentarz/ekwipunek/zawartosc obiektu badanego wraz z numerami odpowiadajacymi zawartym tam obiektom, ktorych mozna uzyc przy dalszych wywolaniach, np. funkcji na mieczu, ktory jest w ekwipunku gracza X. [patrz: opis odwolan do obiektow w 2.5]
W niektorych wersjach mudliba wystepuje jako osobna komenda tracera Inventory [I]
- items
- Zwraca liste wszystkich pseudo-przedmiotow, np. takich, ktore jedynie dodaja opis, lub komendy, ktore dodano do badanego obiektu.
- light
- Ten parametr zwraca liste stanow swiatla dla danego obiektu i informuje o efektach dla obiektow w nim zawartych. Poda zatem obecny stan oswietlenia i czy obiekt generuje lub pobiera swiatlo.
- profile
- Dziala wylacznio gdy skompilowany driver ma wlaczona flage PROFILE_FUNS. Zwraca wszystkie zapisane informacje profilowania powiazanych z danym obiektem. Uzywane glownie przy pracy nad optymalizacja i rozszerzeniem drivera.
- props | properties
- Zwraca wszystkie wlasnosci [propy] danego obiektu.
- shadows
- Zwraca wszystkie shadowy powiazane z danym obiektem.
- vars | variables
- Zwraca liste wszystkich zmiennych danego obiektu.
- wizinfo
- Zwraca specjalna informacje zapisano w obiekcie poprzez wlasnosc OBJ_S_WIZINFO. Glownie zawiera wazne informacje dla innych czarodziei, jak zajmowac sie danym obiektem, czego z nim robic sie nie powinno, itd.
UWAGA! To czy taka informacja znajdzie sie w obiekcie zalezy wylacznie od kreatora tego obiektu i jego zastosowania.
Ed(ycja) pliku powiązanego z obiektem
Komenda Ed uruchamia (dość toporny) edytor ed z otwartym plikiem, do ktorego odwołuje sie badany obiekt.
Skladnia: Ed <dany_obiekt>
Przenoszenie istoty żywej/obiektu do danego środowiska
Komenda Move pozwala na przeniesienie dowolnego obiektu do innego okreslonego srodowiska. W przypadku przenoszenia istoty zywej uzyte zostanie odwolanie do funkcji move_living() z flaga teleportacji, w przypadku niepowodzenie przy przenoszeniu istoty zywej podejmowana jest proba przez odwolanie do funkcji move(). Dla obiektow zawsze uzywana jast funkcja move(). Gdy uzyjemy opcji -f, wymuszajacej przeniesienie, wowczas zawsze wykonywane jest move( ,1).
Skladnia: Move [-f] <dany_obiekt> [<obiekt_docelowy>]
/ w [] podano parametry opcjonalne /
Wykonywanie komendy wewnątrz innego obiektu
Komenda In dziala podobnie do komendy At, z ta roznice, ze jej dzialanie odbywa sie w inwentarzu dowolnego obiektu, gdzie to wykonujacy jest na chwile przenoszony. Stosowac z rozwaga i wylacznie gdy to konieczne. Nalezy zwrocic uwage, ze wiekszosc obiektow nie jest przeznaczonych do przyjmowania obiektow gracza wewnatrz siebie.
Skladnia: In <dany_obiekt> <komenda>
More – przeglądanie pliku powiązanego z obiektem
More to bardzo uzyteczna komenda, stanowiaca swoiste rozszerzenie Cat w tryb stronnicowany. Zezwala na wygodne przejrzenie kodu danego obiektu, ktory dzielony jest w zaleznosci od potrzeb, na kilka stron.
Skladnia: More <dany obiekt>
Set – ustawianie zmiennej narzędziowej tracera
W tym miejscu odwolam sie do tego co napisano wczesniej. Jednym z najwiekszych niebezpieczenst z pojawiajacych sie przy stosowaniu narzedzi tracera jest zmiana liczby porzadkujacej, okreslajacej pozycje obiektu na ktorym dzialamy, w inwentarzu jego srodowiska.
Skladnia: Set $<zmienna> <dany_obiekt>
Zalozmy, by ulatwic omowienie problemu, ze chcemy odnowic stworzony przez nas miecz, znajdujacy sie w ekwipunku gracza Adama. Przyjmijmy, ze miecz ten zawieral w poprzedniej wersji istotne wady i chcemy go zastapic nowa wersja, ale tak by Adam w ogole sie nie spostrzegl. Aby znalezc dokladna liczbe okreslajaca pozycje miecza w ekwipunku Adama uzywamy Dump *adam inv [lub I *adam], niech bedzie to liczba 5. Nastepnie wykonujemy komende Destruct *adam:#5. W sytuacji gdy Adam w miedzyczasie odlozyl miecz, lub np. schowal go do plecaka, i zamiast miecza zniszczylismy jego cenne 3000 mithrylowych monet! To na pewno by mu sie nie spodobalo :)
Aby uniknac takich problemow nalezy zastosowac nastepujaca czynnosc. Pobieramy odwolanie do obiektu, jak w przykladzie powyzej i zapisujemy to w zmiennej. W przykladzie mamy: miecz posiada w chwili obecnej numer 5 w ekwipunku Adama, wykonujemy przeto Set $miecz *adam:#5. Po tym jak nastapia przetasowania w ekwipunku Adama tracer poinformuje nas, ze zmienna sword wskazuje teraz na stos mithrylowych monet. Zalozmy, ze miecz spadl na pozycje 4. Ustawiamy Set $miecz *adam:#4 i na powrot zmienna wskazuje wlasciwy obiekt. Wykonujemy Destruct $miecz i miecza juz nie ma. Prawda, ze proste? Teraz wystarczy go podmienic nowym.
Operacje odnowienia tego miecza, daloby sie wykonac na wiele innych, latwiejszych i na pewno mniej ryzykownych sposobow, powyzszy przyklad mial za zadanie jedynie zilustrowac mechanizm dzialania komendy Set.
Mozna ustawic dowolna liczbe zmiennych, nalezy jednak pamietac, ze po wylowaniu sie przepadna one. Podobnie, gdy obiekt ulegnie zniszczeniu, ten sam los spotka wskazujaca nan zmienna.
Set bez zadnych argumentow zwroci nam pelna liste ustawionych przez nas zmiennych.
Ciekawostka jest fakt, ze ostatni obiekt na ktorym wywolano komende tracera zostaje przypisany pod zmienna $. Czyli po wywolaniu na obiekcie jakiejs funkcji tracera, nastepna mozna wywolac juz na zmiennej $ (<Komenda_tracera> $ <opcje>), a zostanie ona wywolana na tym samym obiekcie.
Aktualizacja, przeładowanie, sklonowanie i zastąpienie danego obiektu nową wersją
Komenda Replace moze okazac sie bardzo uzyteczna. [Nie zostala uwzgledniona wsrod narzedzi tracera w mudlibie CDpl.01.00, porownaj ?odnow]
Przy jej uzyciu mozesz zaktualizowac obiekt nadrzedny danego obiektu, ponownie sklonowac nowa wersja i nadpisac ja na stara, ktora ulegnie degradacji.
Jesli nowa wersja nie da sie przeniesc w sposob bezposredni, kolejna proba przeniesienia nastepuje na zasadzie wymuszenia.
Jesli wystapi blad krytyczny podczas odnawiania, proces zostanie przerwany, a stara wersja nie zostanie nadpisana.
Skladnia: Reload <dany obiekt>
Tail – przeglądanie powiązanego z obiektem pliku od końca
Komenda Tail dziala podobnie jak Cat i More, z ta roznica, ze wyswietla ostatnie linijki kodu.
Zaawansowany LPC i Mudlib
W rozdziale tym zajmiemy sie nieco bardziej zaawansowanymi sprawami dotyczacymi LPC. Musisz naprawde zrozumiec podstawy LPC, ktore wylozylem w poprzednich rozdzialach, zeby moc przyswoic sobie to, co tu opisuje.
Nie miej zadnych oporow przed przegladaniem poprzednich rozdzialow w razie potrzeby, czytajac ten tekst.
Funkcja jako typ danych, część 3
Typ funkcyjny (function) jako taki nie byl dotychczas szczegolowo omawiany w tym podreczniku, choc czasami uzywany. Jednak, ze wzgledu na jego wage nalezy go omowic. Dokladna znajomosc jego wlasnosci pozwoli ci stworzy czesto bardzo skomplikowane i zlozone, lecz jednoczesnie efktywne i optymalne partie kodu. Warto wiedziec, ze wszystkie funkcje ktore zwykly przyjmowac nazwy funckji jako stringi, w chwili obecnej sa zdolne do pobrania wskaznika do danej funkcji jako argumentu, co jest efektywniejsze.
Podstawowe informacje o typie funkcyjnym
Dostep do funkcji mozna uzyskac przez wskazniki funkcji. Jak pokazano wczesniej, kazda wywolanie funkcji sprowadza sie do wskaznika funkcji powiazanego z lista argumentow. Rozpatrzmy to na prostym przykladzie.
void moja_funkcja(string str, int value) { write("Jako string podano: '" + str + "', zas jako liczbe: " + value + ".\n"); return; }
Wywolanie funkcji nastepuje przez odwolanie sie do niej za posrednictwem jej nazwy i nastepujacych po niej argumentow podanych w nawiasie.
Przykladowo: moja_funkcja("smurf", 1000);
Teraz rozszerzymy to zgodnie z pomyslem typu funkcyjnego, gdzie mozna pobrac adres funkcji i nastepnie przypisac innej zmiennej.
Przykladowo:
function nowa_funkcja;
nowa_funkcja = moja_funkcja; // lub rownowaznie nowa_funkcja = &moja_funkcja();
moja_funkcja("smurf", 1000); // lub rownowaznie nowa_funkcja("smurf", 1000);
Zwrocmy uwage, ze nim zmiennej nowa_funkcja przypisano poprawna wartosc, nie odwolywala sie ona do zadnej funkcji. Uzycie jej w takim przypadku doprowadzilo by do bledu run-time.
Okrojona lista argumentów
W poprzednim rozdziale moglismy zaobserwowac, ze mozliwe jest przypisanie odwolania do jednej funkcji zmiennej o innej nazwie. Innym przydatnym zastosowaniem moze okazac sie mozliwosc takiego przypisania, ze niektore argumenty dla oryginalnej funkcji jest traktowana jako stala, pozostawiajac reszte jako zmienne. Zilustrujemy to przykladem:
void tell_player(object player, string mess) { player->catch_msg(mess + "\n"); }
void moja_funkcja() { function mod_tell;
mod_tell = &tell_player(this_player(), );
mod_tell("Witaj!"); // Rownowazne wzgledem
tell_player(this_player(), "Witaj!"); }
Dziala to dobrze, bez wzgledu na ilosc argumentow. Pozostale argumenty zostana wypelnione od lewej do prawej.
Przykladowo biorac funkcje o naglowku void func(int a, int b, int c, int d, int e) przy zmiennej okreslonej nastepujaco function moja_funkcja = func(1, , 3, 4,); otrzymamy, ze wywolanie moja_funkcja(100, 101); dziala tak samo jak wywolanie func(1, 100, 3, 4, 101);
Złożenia funkcji i kompleksowe struktury
W tym miejscu wazne jest, by czytelnik poruszal sie zrecznie w plaszczyznach problemow omowionych w poprzednich rozdzialach. Pojawia sie pytanie: ‘Co mozna umiescic w wywolanie funkcji tego typu?’.
Odpowiedz brzmi: ‘Prawie wszystko’.
Jest to oczywiscie zalezne od umiejetnosci wlasnych i gibkosci poruszania sie w ramach kodu.
Wiele osob miewa problemy ze zrozumieniem operatorow. Wsrod nich mamy: +, -, *, /, %, &, |, ^, >>, <<, <, >, <=, >=, ==, !=, [].
Dosyc czesto programista chce wykonac jedna operacje i jej wynik przeslac bezposrednio jako argument dla innej.
Typ funkcyjny obsluguje superpozycje [zlozenie] funkcji poprzez operator @.
Dziala on jak matematyczne zlozenie, od prawej do lewej! Podobnie jak operator.
Oto przyklad, ktory, miejmy nadzieje, rozjasni wszystko:
Niech pewna tablica zawiera imiona pewnych graczy:
string *arr = ({ "Galarel", "Adren", "Kael", "Aendill", "Thorin", "Hanor" });
Przyjmijmy, ze chcemy docelowo pobrac z tablicy imiona, ktore skladaja sie z wiecej niz pieciu liter. Mozna to oczywiscie zrobic za pomoca petli, jak ponizej:
string * func(string *arr) { int i, sz; string *result = ({}); for (i = 0, sz = len(arr); i < sz; i++) { if (strlen(arr[i]) > 5) result += ({ arr[i] }) } return result; }
Oczywiscie, mozna uzyc efunkcji filter:
int filterfunc(string item) { return strlen(item); } string * func(string *arr) { return filter(arr, filter_func); }
Ale w takim przypadku musze pisac osobna funkcje filtrujaca. Na szczescie mozna pojsc jeszcze dalej, do efektownego zapisu jednolinijkowego !
// ... tu mamy zdefiniowana tablice arr result = filter(arr, &operator(<)(5) @ strlen);
UWAGA! Niezmiernie wazne jest tu wywolanie operator(). Jak powiedzielismy przebiega ono od prawej do lewej, przeto dochodzi do momentu, gdy program ma do czynienia z nastepujacym porownaniem: 5 < strlen(name).
Teraz nieco skomplikujemy sytuacje. Chcemy pojsc dalej i pobrac z tablicy wylacznie Smiertelnikow [ktorych imiona skladaja sie z conajwyzej pieciu znakow.]
Oczywiscie jest to proste i wyglada nastepujaco:
// ... tu mamy zdefiniowana tablice arr
result = filter(filter(arr, &operator(<)(5) @ strlen), &operator(==)(0) @ SECURITY->query_wiz_rank);
Tutaj rozszerzono po prostu wywolanie o kolejne zlozenie, przy wczesniejszym pobraniu osob o imionach o dlugosci do 5 znakow.
Pojdzmy dalej. Jak myslisz co sie stanie gdy wykonasz nastepujace polecenie? Co pojawi sie na ekranie?
exec return implode(sort_array(map(filter(users(), sizeof @ &filter(, &call_other(, "id", "tarcza")) @ deep_inventory)->query_real_name(), capitalize)), ", ");
Na pewno wiekszosci czytelnikow wydaje sie to prosciutkie.
Opisze, co sie tu dzialo. Po pierwsze, przebiegamy przez wszystkich graczy [w tym czarodziejow] i przeszukujemy cale ich ekwipunki. Filtruje osoby, ktore posiadaja tarcze i pobieram ich imiona. Tablice wynikow sortuje, przeksztalcam by byly napisane z wielkiej litery i tworze zen jeden string z przecinikiem (,) po kazdym imieniu. W rezultacie na ekranie pojawia sie ten wlasnie string.
Innymi slowy: otrzymuje spis osob, ktore maja przy sobie tarcze.
- UWAGA! Czas na przestroge. Oczywiscie gladko otrzymalem liste osob, majacych w ekwipunku tarcze (nawet jesli jest w plecaku). Jednak zwrocmy uwage, ze przy duzej liczbie graczy liczba obiektow, ktore maja przy sobie osiaga wielkie pulapy, moze nawet kilka tysiecy! I teraz na kazdym z nich wywolujemy funkcje sprawdzania czy to tarcza. Jak widac cala operacja jest niezwykle pamieciozerna, na szczescie potrzeba przeprowadzenia podobnej operacji rzadko sie zdarza.
Chodzi glownie o to by juz na etapie pisania kodu przewidywac jakie beda konsekwencje jego uzycia.
3.2 Pisanie efektywnego kodu
Ten temat jest blisko powiazany z tym co powiedzialem wczesniej, ze nie bede wyjasniac jak programowac. Wycofam swe slowa – troszeczke – i opowiem o kilku sprawach co robic, albo nawet co wazniejsze, czego nie robic.
Efektywne pętle
Ten temat moze sie wydawac raczej trywialny. W koncu na ile sposobow mozna zapisac petle? Raczej na niewiele. Zacznijmy od najczestszego bledu. Zalozmy, ze mamy jakas duza tablice, nazwijmy ja ‘wielka_tab’ i zalozmy, ze chcemy ‘przeleciec’ petla przez wszystkie jej elementy. Co robisz w takiej sytuacji? „Proste!” wyjasniasz, „Oczywiscie, ze zwykla petla ‘for’!”. Pewnie... Ale jak to zapiszesz? Najczesciej ludzie robia to w ten sposob:
int i; for (i = 0 ; i < sizeof(wielka_tab) ; i++) { // Tu robimy cos z tablica }
No tak... a wiec co jest zle? Jesli przejrzysz rozdzial mowiacy o dzialaniu instrukcji ‘for’, to zobaczysz, ze wykonywane sa trzy czesci w okraglych nawiasach oddzielone srednikami. Pierwsza tylko na poczatku, druga (srodkowa) za kazdym cyklem petli i trzecia rowniez za kazdym razem na koncu kazdego cyklu.
Oznacza to, ze funkcja ‘sizeof()’ zostaje wykonana za kazdym cyklem petli. Jest to raczej marnotrastwo, biorac pod uwage fakt, ze tablice rzadko zmieniaja rozmiar. Jesli by robily to czesto, bylaby to inna para kaloszy, ale jako ze nie robia... Nie. Napisz to w ten sposob:
int i, sz; for (i = 0, sz = sizeof(wielka_tab) ; i < sz ; i++) { // Tu robimy cos z tablica. }
Widzisz? Zmienne ‘i’ oraz ‘sz’ tylko na poczatku dzialania petli maja przypisywane wartosci. Licznik ‘i’ zostaje ustawiony na 0, a zmiennej ‘sz’ zostaje przypisany rozmiar tablicy. Przez caly czas dzialania petli porownywane sa same zmienne, zamiast ciaglego obliczania nie zmieniajacego sie rozmiaru tablicy.
Mozesz mi wierzyc albo nie, ale to jest bardzo czesty blad, prawie kazdy go robi. Oszczednosci w moim sposobie moga sie nie wydawac tak wielkie... ale... pomnoz to razy wszystkie petle w mudzie i przez liczbe razy kiedy sa wykonywane, a otrzymasz calkiem niezla liczbe. Kosztem drugiego sposobu w porownaniu do pierwszego jest dodanie jednej zmiennej lokalnej i to jest wystarczajaco mala cena.
Pamietaj o tym problemie nie tylko w przypadku tablic, ale rowniez w mappingach lub innych ogolnych pojemnikach, zawierajacych rzeczy, przez ktore chcesz ‘przeleciec’ petla. Rozwiazanie, z wyjatkiem malych roznic w rozpoznawaniu rozmiaru pojemnika jest zawsze takie same.
Słów kilka o makrodefinicjach
Czesto popelnianym bledem jest umieszczanie DUZYCH tablic i/lub mappingow jako makrodefinicji. Wyobrazmy sobie jeden wielki mapping zawierajacy definicje rang gildiowych, opisy, przerozne limity umiejetnosci mozliwych tamze do wytrenowania, modyfikatory, etc, gdzie ranga gildiowa spelnia funkcje indeksu. Bardzo czesto trzeba sie bedzie do niego odwolywac, przez centralny obiekt administracyjny gildii, etc. Mamy cos takiego:
// Gorna partia pliku #define GUILD_MAP ([ 0: ({ "poczatkujacy", "troche", 3, 2343, ... }), \ 1: ({ .... \ ... /* i dalej okolo 20 lub wiecej podobnych linijek */ \ ]) // kodowy przyklad uzycia write("Masz obecnie range: " + GUILD_MAP[rank][1] + "\n"); // dalszy kod
Spojrz na to czytelniku uwaznie. Przypomne w tym miejscu co daja makrodefinicje: otoz dzialaja nastepujaca, ilekroc podczas wykonywania programu system napotka na wzor podany przy makrodefinicji (tu: GUILD_MAP) zamienia go przez rozwiniecie podane w makrodefinicji (tu: caly ten mapping). Krotko: ilekroc uzyjemy odwolania do GUILD_MAP wstawiany jest calutki mapping. Za kazdym razem gdy go wstawiamy driver musi ponownie interpretowac jego zawartosc, sortowac i indeksowac. Jest to zatem strasznie nieoptymalne, pamieciozerne i czasochlonne.
Zamiast makrodefinicji o wiele efektywniej mozna to zrobic tak, uzywajac zmiennej:
// Gorna partia pliku mapping GuildMap; create_object() { // kod GuildMap = ([ 0: ({ "poczatkujacy", "troche", 3, 2343, ... }), \ 1: ({ .... \ ... /* i dalej okolo 20 lub wiecej podobnych linijek */ \ ]);
} // kodowy przyklad uzycia write("Masz obecnie range: " + GuildMap[rank][1] + "\n"); // dalszy kod...
Pułapki i niuanse
Przy kodowaniu nie trudno o pomylke. Czesto kod wyglada na nasze oko ladnie, ale tak naprawde jest szalenie nieefektywny. W tym rozdziale ponownie zajmiemy sie czesto popelnianymi bledami.
Wiele z tych bledow juz opisano we wczesniejszych rozdzialach, ale nie zaszkodzi powtorzyc.
Mappingi/Tablice – bezpieczeństwo
Problem jak wspomniano w rozdziale 2.2.9 polega na tym, ze mappingi i tablice nie sa kopiowane, za kazdym razem gdy sa przenoszone. Operacje odbywaja sie jedynie na wskazniku. Moze to byc podstawa do wielu niebezpiecznych luk w kodzie.
Przyjrzyjmy sie przykladowi obiektu gildiowego, ktory zajmuje sie czlonkami gildii. Globalna zmienna Rada, ktora jest zapisywana przez save_object() zawiera rade gildii.
string *Rada; public string zwroc_rade() { return Rada; }
Wyglada to na prawidlowe, lecz tak naprawde zwraca jedynie wskaznik do oryginalnej tablicy. Jesli ktos inny zechce dodac czlonka do rady gildii moze to zrobic! wystarczy, taka sztuczka:
void moja_funkcja() { string *rada_po_moich_poprawkach; rada_po_moich_poprawkach = OBIEKT_GILDIOWY->zwroc_rade(); rada_po_moich_poprawkach += ({ "olorin" }); // I dodaje Olorina do rady gildii i kazdy // moze to zrobic! }
Jak to zatem poprawic? Wystarczy zmodyfikowac funkcje zwroc_rade, by zwracala return Rada + ({}); i wszystko bedzie w porzadku. Latwo przegapic, a jakiez to wazne!
Zapętlanie alarmów
Przyjrzyjmy sie nastepujacej funkcji:
public void moje_wlasne_alarmy(int ile) { set_alarm(1.0, 1.0, moje_wlasne_alarmy(ile + 1)); tell_object(find_player("<twoje_imie>"), "Buu! " + ile + "\n"); }
Co bedzie efektem wywolania tej funkcji? Otoz, co sekunde bedzie generowany nowy alarm, wywolujacy siebie samego co sekunde. Co to oznacza? Spojrzmy na opis efektow:
1 sekunda: Buu! 0 (oryginalne wywolanie) 2 sekunda: Buu! 1 (powtorzenie 1 sek 0) Buu! 1 (nowe od 1 sek 0) 3 sekunda: Buu! 1 (powtorzenie 1 sek 0) Buu! 2 (powtorzenie 2 sek 1) Buu! 2 (powtorzenie 2 sek 1) Buu! 2 (nowe od 2 sek 1) Buu! 2 (nowe od 2 sek 1) 4 sekunda: Buu! 1 (powtorzenie 1 sek 0) Buu! 2 (nowe od 3 sek 1) Buu! 2 (powtorzenie 2 sek 1) Buu! 2 (powtorzenie 2 sek 1) Buu! 3 (powtorzenie 3 sek 2) Buu! 3 (powtorzenie 3 sek 2) Buu! 3 (powtorzenie 3 sek 2) Buu! 3 (powtorzenie 3 sek 2) Buu! 3 (nowe od 3 sek 2) Buu! 3 (nowe od 3 sek 2) Buu! 3 (nowe od 3 sek 2) Buu! 3 (nowe od 3 sek 2) ... itd.
Jak widac przyrost jest tutaj lawinowy, co w krotkim czasie doprowadzi do padu gry. Taka praktyka jest tak glupia ze konsekwencje dla czarodzieja, ktory by cos takiego lub podobnego zrobil bylyby bardzo ostre. Oczywiscie na obecnym etapie rozwoju wiekszosc LPMudów jest zabezpieczona przed uzyciem partykularnie tozsamej konstrukcji.
Korzystanie z wewnętrznej dokumentacji
Na zakonczenie slow kilka o wewnetrznej dokumentacji muda, gdzie znalezc mozna zazwyczaj mnostwo przydatnych rzeczy [zalezy to glownie od administratorow muda i osob zajmujacych sie dokumentacja ;)]
Efunkcje, sfunkcje i lfuns opisano w manulach dostepnych pod komenda ‘man’. Mozna oczywiscie wyszukiwac za pomoca odpowiedniej opcji interesujace nas funkcje. Ponadto w kilku rozdzialach opisano rozne przydatne rzeczy. [szczegoly ?man]
Przykladowo chcemy pobrac user id obiektu, ale nie pamietamy dokladnie jaka funkcja jest za to odpowiedzialna. Pamietamy jednak, ze jej nazwa konczyla sie literami ‘id’!
> man -k *id --- simul_efun: export_uid geteuid getuid seteuid setuid
Widzimy zatem, ze wyszukiwarka znalazla piec funkcji odpowiadajacych podanemu kryterium. Wyswietlono wszyskie pasujace wyniki, po przeszukaniu wszystkich stron man.
Wiemy juz, ze chodzilo o getuid. By poczytac o niej wpisujemy man getuid (lub nawet man simul_efun getuid – efekt bedzie w tym przypadku taki sam).
Doswiadczeni programisci LPC znaja na pamiec powiazania funkcji i wiedza co dziedziczyc by uzyskac do porzadany efekt. Wie takze mniej wiecej jakie funkcje zawarte sa w danym obiekcie, oczywiscie nie zna wszystkich. Kazdy koder przeto powinien czesto korzystac z komendy ‘sman’. Stanowi on kolekcje naglowkow funkcji zawartych w kodzie z /cmd, /lib, /obj, /secure, /std i /sys.
Takze tutaj mozliwe jest przeszukiwanie smanu wg podanych kryteriow [szczegoly ?sman].
Przykladowo, chcemy znalezc funkcje ktora zwraca nazwe gildii zawodowej do ktorej dany gracz nalezy. Nie wiemy jednak jaki obiekt definiuje ja ani jak sie ta funkcja zwie.
> sman -k *guild* --- /lib/guild_support: create_guild_support init_guild_support --- /secure/master: add_guild_master query_guild_type guild_command query_guild_type_int guild_filter_type query_guild_type_long_string guild_sort_styles query_guild_type_string load_guild_defaults query_guilds query_guild_domain remove_guild_master query_guild_is_master set_guild_domain query_guild_long_name set_guild_long_name query_guild_masters set_guild_phase query_guild_phase set_guild_style query_guild_short_name set_guild_type query_guild_style --- /std/guild/guild_base: list_major_guilds query_guild_not_allow_join_guild query_guild_keep_player query_guild_skill_name query_guild_leader query_guild_style query_guild_member --- /std/guild/guild_lay_sh: query_guild_incognito_lay query_guild_tax_lay query_guild_leader_lay query_guild_title_lay query_guild_member_lay query_guild_trainer_lay query_guild_name_lay query_guild_type query_guild_not_allow_join_lay remove_guild_lay query_guild_style_lay --- /std/guild/guild_occ_sh: query_guild_incognito_occ query_guild_tax_occ query_guild_leader_occ query_guild_title_occ query_guild_member_occ query_guild_trainer_occ query_guild_name_occ query_guild_type query_guild_not_allow_join_occ remove_guild_occ query_guild_style_occ --- /std/guild/guild_race_sh: query_guild_family_name query_guild_style_race query_guild_incognito_race query_guild_tax_race query_guild_leader_race query_guild_title_race query_guild_member_race query_guild_trainer_race query_guild_name_race query_guild_type query_guild_not_allow_join_race remove_guild_race --- /std/living: clear_guild_stat set_guild_pref query_guild_pref_total set_guild_stat
W gaszczu funkcji odnajdujemy te jedna, a szczegoly odczytamy uzywajac komendy sman /std/guild/guild_occ_sh query_guild_name_occ.
Z czasem lepiej będziesz orientować się w Mudlibie i łatwiej będzie ci tak formułować zapytanie, żeby otrzymać wystarczająco zawężoną odpowiedź za pierwszym razem. Nie żeby było coś złego w parokrotnym przeszukiwaniu, od tego w końcu jest sman!
- Koniec czesci glownej Manualu LPC ...
- Uzupelnienie by Kael
DODATEK A. Formanty odmiany polskiej
na podstawie /sys/pl.h 1) odmiana przez przypadki
PL_MIA 0 - mianownik PL_DOP 1 - dopelniacz PL_CEL 2 - celownik PL_BIE 3 - biernik PL_NAR 4 - nadrzednik PL_MIE 5 - miejscownik
np. chcac poznac imie gracza w bierniku stosujemy
this_player()->query_name(PL_BIE);
lub
this_player()->query_name(3);
2) rodzaje gramatyczne
PL_MESKI_OS 0 - wskazuje na odmiane meska osobowa obiektu (osoba tu oznacza istote zywa myslaca, np. elf, ogr ale nie kot) PL_MESKI_NOS_ZYW 1 - wskazuje na odmiane nieosobowa dla istoty zywej (np. kota) PL_MESKI_NOS_NZYW 2 - wskazuje na odmiane nieosobowa dla przedmiotu (np. dlugopis) PL_MESKI_NZYW 2 - to samo co PL_MSESKI_NOS_NZYW
PL_ZENSKI 3 - wskazuje na odmiane zenska
PL_NIJAKI_OS 4 - wskazuje na odmiane nijaka osobowa PL_NIJAKI_NOS 5 - wskazuje na odmiane nijaka nieosobowa
DODATEK B. Rodzaje obrażeń
W_IMPALE rany klute W_SLASH rany ciete W_BLUDGEON obuchowe
W_NO_DT brak obrazen MAGIC_DT obrazenia magiczne