Dnešní příspěvek bude o portaci hry Brány Skeldalu, která vyšla v roce 1998. To že to nebyla snadná cesta už dokazuje to, že tento článek vznikl. Pokusím se vypíchnout hlavně různé nástrahy, které si vynutily speciální zacházení
Hra Brány Skeldalu byla poprvé vydána na podzim v 1998. Za produkcí stojí Napoleon Games, což je (nebo spíš byla) společnost založenou Jindřichem Rohlíkem (který si časem změnil příjmení na Skeldal). Má role v tomhle projeku bylo naprogramovat kód. Nebyl to sice můj první projekt, ale rozhodně to byl první velký projekt který se dostal do povědomí širší veřejnosti. A asi leckoho překvapí, že to byl první projekt napsaný v C. Já jsem do té doby programoval Pascalu. Na jazyk C jsem postupně přecházel
Jak jsem už předeslal, kód hry byl napsán v jazyce C. V roce 1996, kdy vývoj začal, byla cílovým operačným systémem MS-DOS s využitím DPMI. Proto byl zvolen Watcom C, který byl dodáván s DOS4/GW. Tento extender umožňoval využít až 64MB paměti. Prostě 640KB paměti určitě nedostačuje každému. Důvodem bylo mimo jiné volba SVGA rozlišení s podporou flat framebuffer (pamětníci si jistě vzpomenou na přepínání bank) a z počátku se počítalo s UniVBE. S postupem vývoje se ale objevovalo víc a víc karet, které podporovaly VESA 2.0, takže se od požadavku na UniVBE odstoupilo. Tento ovladač se pak stal jen jako doporučení pro majitelé karet, kde hra nefungovala, nebyl součástí distribuce.
V roce 2000 vyšla hra v distribuci Game4U
Přeskočme do roku 2004 a vývoj se posunul, stačilo pouhých 6 let a platforma se s hrou stalá obsolete. Všichni jsme měli Windows a podpora nebyla dobrá. Takže jsem se rozhodl, že zkusím hru z vlastní iniciativy ve svém volném čase přeportovat do Windows s použitím DirectX. To se nakonec nějak povedlo, a po několikaměsíčním, ne zrovna jednoduchém, jednání z Jindrou vyšla hra v podobě patche pro majitele oficiálního CD zdarma a dále pak jako vydání pro časopis LEVEL.
V roce 2008 byl zdroják uvolnění jako open source a hodně dlouho ležel ladem na SourceForge
V roce 2014 vybral Jindra peníze na Startovači, aby mohl vydat hru na platformy iOS a Android. Tento kód portoval sám Jindra s mou pomocí. Já jsem připravoval kusy kódu v C a on to pak nějak slepoval v marmeládě (Marmalade SDK).
A dostáváme se do roku 2025, kdy hra vychází na Steamu.
Záměr vydání na Steamu mi Jindra oznámil někdy na přelomu roku s tím, jestli mu nepomůžu to nějak upravit. Souhlasil jsem, ale měl jsem zásadní podmínku – tou bylo kompletní generálka kódu s tím, že cílem je úplná přenositelnost mezi platformami. Primární focus byl Windows a Linux, ale kód hry měl být odprostěn od platformově závislých věcí. Případně potřeby měl být platformově závislý kód oddělen jednotným API a umístěn do separátního stromu v projektu
Proč tahle podmínka? Stál za tím pragmatický důvod. V té době prakticky neexistovala funkční verze. Verze pro Windows z roku 2005 už (prý) nefunguje ve Windows 11. Aby bylo možné do hry přidávat nějaké další funkce, jako třeba Steam achievementy, bylo potřeba, aby se kód dal přeložit. S tím souvisí to, že já ve Windows už prakticky neprogramuju a na drobné věci používám pouze vscode. Dávno nemám možnost například pracovat se sln
a vcproj
soubory a nechtělo se mi do toho zabředávat. Takže portování na Linux byla logická volba.
Ono se to ukázalo jako dobré rozhodnutí. Každý překladač (Gcc, Clang, MSVC) si v kódu našel jinou část, na které si rád postěžoval, a tak se hodně upravovalo do „čistější“ struktury. Stejné pak nasazení různých sanitizérů (MSVC address sanitizer vs valgrind) také odhalilo různé problémy při běhu kódu. Podrobně o tom v dalším odstavci.
Dalším cílem bylo trošku oživit kód pro open source. Mám svůj vlastní github repozitář, kde vystvavuji prakticky veškerou mou práci. A i když ne všechno je 100% funkční, spoustu projektů jsou dnes již zastaralé, tak bych rád udržel nějakou úroveň „standardu“. Tak například jedním z pravidel je snadná instalace ze zdrojáků. To v případě C/C++ vyžaduje aspoň nasazení CMake. Žádne vcproj
ani sln
do mého repozitáře nepatří. CMake si je případně umí vytvořit během sestavení. Takže tento cíl je zřejmý, pokud má být projekt na stránce mého githubu, bylo potřeba ho revidovat.
Stránky projektu: ondra-novak/gates_of_skeldal.
K tomu ještě poznámku. Někdy kolem roku 2011 se objevil zájemce, který chtěl přeportovat hru do Linuxu. Jeho repozitář najdete zde: nextghost/skeldal. Zdá se že je pořád aktivní, má tam issue z roku 2024. Já se tedy přiznám, že s nim nejsem v kontaktu, jeho port jsem nepoužil a ani nevím v jakém je stavu. Každopádně bych mu rád připsal nějaký kredit. Trochu je mi ho líto, že jsem mu takhle vzal vítr z plachet
Začnu obecným pohledem na starý kód, kromě věcí, které proberu v dalších odstavcích podrobněji. První věc co mne napadne, když se na kód dívám je:
Už vím proč nesnáším C
Nicméně jedním dechem dodávám, že Rust to nezachrání. Jsem holt už hodně dlouho až po uši v C++, a jakmile člověk má něco někde opravit v C, připadá si, že se ocitl na pustém ostrově bez nástrojů. Já chci objekty, konstruktory, destruktory, automatické kopírování, já chci šablony. Místo toho nesmím zapomínat ke každému malloc psát free. Taková pitomost jako std::vector
tam nemám, kód na spoustě míst řeší seznamy různě. Například jako spojové seznamy, nebo jako rozšiřující se pole přes realloc
. Nebo jako pole pointerů. Každá tahle věc má jiné API.
Samotný kód byl rozšiřován o podporu grafiky, hudby, komunikace se steamem, lokalizace, všechny nové věci jsem napsal v C++ a do původního kódu přenesl přes C rozhraní, které objekty exportovalo jako pointery na typové struktury. Například:
typedef struct ini_config_tag INI_CONFIG; typedef struct ini_config_section_tag INI_CONFIG_SECTION; INI_CONFIG *ini_open(const char *filename); void ini_close(INI_CONFIG *config); const INI_CONFIG_SECTION *ini_section_open(const INI_CONFIG *cfg, const char *section); const char *ini_find_key(const INI_CONFIG_SECTION *section, const char *key); const char *ini_get_string(const INI_CONFIG_SECTION *section, const char *key, const char *defval); long ini_get_int(const INI_CONFIG_SECTION *section, const char *key, long defval); int ini_get_double(const INI_CONFIG_SECTION *section, const char *key, double defval); int ini_get_boolean(const INI_CONFIG_SECTION *section, const char *key, int defval);
Implementace těchto dvou objektů je samozřejmě v C++, protože používá std::map, std::vector, std::string, atd. Kdybych tohle měl psát v C,… asi bych začal křičet: vraťte mi někdo ruce!
Na jeden návrhový vzor jsem však v C++ dávno zapomněl. Týká se to struktur alokovaných v jednom paměťovém bloku. Hra ze svých dat načítá často komplikované datové struktury. Například herní obchodníci mají krámky, ty mají zboží, je to taková databáze jak v miniaturním e-shopu. Ale organizované je to ve strukturách, které linkují další struktury. Kód si nejprve spočítá, kolik potřebuje paměti, následně celý blok alokuje a následně realizuje celou tuhle databázi v tomhle bloku s tím, že všechny pointery ukazují do tohoto bloku. Proč se to takhle dělalo? Jednoduše, na konci se celý blok uvolnil. Nebylo nutné rekurzivně procházet všechny závislosti a uvolňovat závislé objekty.
Mno, přesto si nejsem jist, jestli bych tenhle návrhový vzor chtěl dneska oprašovat. Možná pokud budu potřebovat vysoký výkon.
Pro zajištění maximální přenositelnosti jsem zvolil knihovnu SDL2. Prvotní cílem bylo emulovat původní frame buffer přímo v SDL surface, ale nakonec jsem zvolil jiné řešení. Hra si zachovává vlastní frame buffer tak jak bylo navrženo pro DOS, a aktualizace se řeší přenášením pravoúhelníkových oblastí přes SDL_UpdateTexture. Tato jedna velká textura se pak kreslí do render targetu. Používá se originální barevné schéma 1555 (ARGB), pouze s tím, že původní hra měla význam A=0 ~ viditelné, A=1 ~ transparentní – takže se všem bodům přehazuje nejvyšší bit.
Hra si také sama řešila zobrazování kurzoru myši, v této verzi se kurzor myši řeší v SDL jako další textura, která se zobrazuje z-indexu nad hlavním texturou
Kromě toho SDL část sama řeší různé grafické efekty jako zvětšování výřezu při chození a otáčení – tento kód byl v původní hře napsán v asembleru pro každou grafickou kartu zvlášť. Zvláštní zacházení si vyžádalo i zobrazování přídě loďky, která se ve hře vyskytuje a která se maluje v z-indexu mezi myší a hlavní texturou
Celkem se s SDL po grafické stránce pracovalo dobře. Pomocí jednoduchých efektů jsem také realizoval volitelný „CRT Filter“, což je celoobrazovkový filtr, který simuluje zobrazení na CRT monitoru. Vypadá to pěkně, dýchne na mě nostalgie
Detail CRT filtru
Dodatek: Možná někoho napadlo, hra velice využívá zvětšování a zmenšování grafiky ve výhledu, aby se simuloval efekt 3D pomocí 2D obrázků. Toto by se v SDL dalo dělat s podporou HW a třeba se zapnutým lineárním vyhlazování, takže by výsledný efekt nevytvářel kostrbaté artefakty. Musím vás zklamat, i v této verzi to počítají původní softwarové rutiny, byly akorát přepsané z assembleru do C. Tam by bylo potřeba kompletně přepsat správu scény, a na to bylo v danou chvíli málo času. Ale třeba tuto možnost v budoucnu prozkoumám, uvidíme.
Co se týče audia, v tom mne SDL maličko zklamalo. Audio u SDL2 je řešeno jako jeden vícekanálový stream, který hraje trvale a aplikace musí v callbacku plnit buffer daty, které se mají přehrát. Oproti DirectX portu je to krok zpět, v DirectSound má člověk možnost pracovat s vícero streamy současně a využívá se hardwarové mixování. SDL sice nabízí SDL_Mixer, ale jak jsem pochopil, jedná se od nadstavbu. Softwarové mixování jsem si tedy zajistil sám pomocí několika objektů a šablon v C++.(wave_mixer.h a závislé soubory)
Originální hra z velké části používala assembler x86 pro řešení kritických částí programu. Například veškeré vykreslování bylo napsáno v assembleru. Je nutné si uvědomit, že hra běžela v rozlišení 640×480×32768 barev. (tedy 2 bajty na bod). V té době kolem roku 1996 ze začínalo opouštět oblíbené rozlišení 320×200×256 a objevovaly se hry s vyšším rozlišení. A my nechtěli zůstat pozadu. Je třeba si uvědomit, mnou vybraný grafický režim znamenal 10× víc dat, kterých se musel přenést na obrazovku, než u 320×200. Kdo tehdy hrál 3D hru se softwarovým renderingem na 60fps, ten by v našem rozlišení měl 6fps.
To byl důvod, proč byl zvolen assembler. Kromě toho je potřeba přidat poznatek, že optimalizace C v roce 1996 nebyla na dobré úrovni. Dnes bych si netroufnul programovat přímo assembleru. Překladače v tomhle dělají velmi dobrou práci. Už jen znát všechny aspekty superskalárního procesoru a jak za sebou poskládat instrukce, aby z toho byl optimální kód… Že některé posloupnosti instrukcí jsou ve skutečnosti pomalejší, než by se zdálo logické. Dnešní doba je prostě jinde.
Logickým krokem tedy bylo veškerý assembler přepsat do C. Myslel jsem si, že seženu nějaký dobrý dekompilátor, ale nezadařilo se (pokud o něčem víte?). Použil jsem i umělou inteligenci, ale tvrdě jsem narazil, AI prostě dělala chyby, výsledkem bylo vždy něco jiného. Nakonec jsem to tedy řešil ručně. Nějak takto:
int input_code(const void *source,int32_t *bitepos,int bitsize,int mask) { const uint8_t *esi = source; // mov esi,source int32_t *edi = bitepos; // mov edi,bitepos int ebx = bitsize; // mov ebx,bitsize int edx = mask; // mov edx,mask int ecx = *edi; // mov ecx,[edi] int eax = ecx; // mov eax,ecx eax >>=3; // shr eax,3 eax = esi[eax] | (esi[eax+1] << 8) | (esi[eax+2] << 16) | (esi[eax+3] << 24); // mov eax,[esi+eax] ecx &= 7; // and cl,7 eax >>= ecx; // shr eax,cl eax &= edx; // and eax,edx (*edi) += ebx; // add [edi],ebx return eax; }
Jedním z cílem bylo přeložit hru na x64. V kódu člověk našel i takovéhle konstrukce
short **item_map = malloc(mapsize*4); //32bit pointer má 4 bajty memset(item_map, 0, 4*mapsize)
Najít všechna ta místa a nahradit to minimálně sizeof() bylo náročné. Možná teď napadne někoho, proč se tento problém neprojevil u předchozích portů. Všechny ty porty byly 32bitové. Verze na Steamu je tedy poprvé 64bitová
Velký problém byl s alignmentem struktur. V době vývoje se s aligmentem nepočítalo, Watcom C ukládal jednotlivé pole struktur za sebou bez vycpávek. Většina datových souborů, která v té době vznikala tak měla formát v podobě binárně serializovaných struktur s nealignovanými prvky. Takové struktury se v aktuálních verzích překladačů načítají špatně, protože u struktur se aplikuje align.
Tohle by mohlo vyřešit #pragma pack(1), ale tím bych si zavřel možnost portace na platformy, kde nealignovaný přístup k proměnné znamená chybu. Takže všechny soubory s binárním formátem se musí po načtení upravit, vložit vycpávky, tam kde mají být, aby to přesně sedělo na struktury s aligmentem. Týká se to všech map, definicí předmětů, nestvůr, akcí, skriptů (které jsou také v binárním formátu).
Příklad:
typedef struct tproduct { short item; //cislo predmetu ktere nabizi /* short padding1*/ int cena; //cena za jeden short trade_flags; //vlajky /* short padding2*/ int pocet; //pocet predmetu na sklade int max_pocet; }TPRODUCT;
Funkce, která deserializuje tuto strukturu (původně se to mapovalo 1:1)
static __inline void copy_data(const char **src, void *target, int size) { memcpy(target, *src, size); (*src)+=size; } static const char * load_TPRODUCT(const char *binary, TPRODUCT *target) { copy_data(&binary, &target->item, 2); copy_data(&binary, &target->cena, 4); copy_data(&binary, &target->trade_flags, 2); copy_data(&binary, &target->pocet, 4); copy_data(&binary, &target->max_pocet, 4); return binary; }
V době, kdy hra vycházela mělo běžné PC asi 8MB. Někdo měl 16MB, někdo 32MB. Kompletní level v paměti zabere v resourců až 24MB, jedná se o grafiky, animace a zvuky. A opět se dostáváme k tomu, že DOS/DPMI nebyl operační systém. Když došla paměť, tak… došla paměť. Toto vyžadovalo opatření z mé strany, nějaký správce resourců
Předně, místo malloc()
se používala mnou napsaná funkce getmem()
– reference na Pascal – tato funkce kromě alokace zvládla vyřešit problém nedostatku paměti. Pokud alokace selhala, našla se nějaká nepotřebná paměť a ta se uvolnila. Něco jako garbage collector. Nicméně to co se mohlo uvolnit byly většinou resource v paměti právě v podobě grafiky a zvuků. Ty se totiž daly kdykoliv zase do paměti načíst.
Tyto resource se registrovaly v tabulce podle ID a evidovalo se, zda jsou načtené v paměti nebo ne. Pokud program potřeboval nějakou resource, zavolal funkci ablock(id)
– jako access block. A obdržel pointer na načtenou resource. Samozřejmě pokud resource nebyla v paměti, tak se pro resource alokovala paměť a resource se načetla z disku.
Funkce ablock garantovala platnost pointeru do dalšího ablock nebo getmem(). Pokud jsem potřeboval garantovat pointer déle, existovala dvojice funkcí alock
a aunlock
, které bránily uvolnění konkrétní resource z paměti v případě, že paměti byl nedostatek. Manager paměti si musel vybrat jinou resource.
V nastavení hry je volba, která se jmenuje „nahrát vše do paměti“. Hráči s větší paměti tak mohli trochu zrychlit odezvu hry přes loadovací obrazovku. Při přechodu mezi levely se objevila obrazovka s progress barem, kdy se hra snažila načíst co nejvíc resourců do paměti. Během procházení bludištěm se pak už nenačítalo. Tohle asi mělo smysl hlavně pokud se hra hrála přímo z CD, které bylo relativně pomalé. Hráči si hru ale stejně většinou instalovali na HDD. Pokud volba „nahrát vše do paměti“ nebyla aktivní, pak přechody mezi levely sice byla rychlejší, ale pokud na vás z poza rohu vyskočil skřet, chvili bylo doslova vidět, jak se každé jeho animační políčko načítá z disku nebo z CD.
V současném portu byla většina těchto funkci zrušena, protože hra se plně vejde celá do libovolného současného PC. Hlavní soubor obsahující všechny resource má 100MB. Tento soubor se při spuštění mapuje do paměti pomocí mmap
(resp MapViewOfFile
) a pak se do resourců přistupuje přímým přístupem. Volba „nahrát vše do paměti“ ztratila význam a neprovádí se. Přechody mezi levely jsou tak rychlé, že hráč to často nemusí postřehnout, žádná nahrávací obrazovka tam není. Přesto v kódu zůstaly funkce ablock
, alock
, aunlock
, i když významně zredukované.
Následující část popisuje message system, který byl vyvinut pro hru a přidružené nástroje. Je třeba se na to dívat tak, že nejprve vznikaly nástroje, jako editor map, a ty vznikaly pro DOS bez podpory nějakého známého grafického UI – tedy UI bylo komplet mé vlastní.
Když jsem připravoval tuhle sekci, dlouze jsem hledal, odkud přišla inspirace na tenhle systém, protože nápadně připomíná Windows API. Problém je, že ve Win32 jsem se začal učit až v roce 1999. S pomocí umělé inteligence jsem to dal dohromady. Mezi přechodem od Borland Pascalu k jazyku C jsem měl pár let, kdy jsem si „hrál“ ve Windows 3.1 prostřednictvím mnoha vizuálních SW. Vizuální programování bylo tehdy buzz word. A tak jsem se zřejmě nechal ovlivnit Visual Basicem a Microsoft Accessem. Jinak přísahám, na low level Windows API jsem tehdy nesahal!
Message system (v event.h
a event.c
) má na starost distribuci různých událostí, které mohou vzniknou ve hře, ať už v důsledku interakce uživatele s klávesnicí, myší, ale i časovače, a lze samozřejmě generovat vlastní události a programovat handlery událostí. Nezapomínejme, že jsme pořád v DOSu, kde klávesnici řídil BIOS pomoci patřičné INT 16h instrukce a myš se instalovala jako rezidentní program a také měla vlastní INT 33h. Časovač byl napojen na INT 08h. Žádná message queue, žádný dispatcher.
K dispozici je jedna funkce
send_message(int msgid, ...);
Tato funkce „pošle“ zprávu msgid
a parametry (...)
někomu, kdo na zprávu může reagovat. Existují dvě vyhrazené zprávy
send_message(E_ADD, msgid, handler)
send_message(E_DONE, msgid, handler)
Na jedné události může poslouchat víc posluchačů. V celém systému pak je definovaných několik dalších zpráv
Zpravidla se na E_WATCH registroval handler na klávesnici a myš. Pokud se událost detekovala, vznikla událost E_KEYBOARD nebo E_MOUSE. Navíc se kontrolovaly hodiny a pokud nastal „tick“, zavolala se událost E_TIMER.
Handler měl tento prototyp:
typedef struct event_msg { int32_t msg; va_list args; //původní deklarace: void *args; } EVENT_MSG; typedef void (*EV_PROC)(const EVENT_MSG *,void **) ;
Při příchodu eventu obdržel každý handler pointer na zprávu a pointer na pointer, který byl asociovaný s handlerem (s jeho registrací). Tam si mohl každý handler uložit libovolnou informaci. Typicky se při E_INIT provedla alokace paměti a pointer se uložil do pointeru druhého parametru. Pak tato paměť byla přístupná pro každý event. Při příchodu události E_DONE mohl handler po sobě uklidit. Pokud tam nechal platný pointer, automaticky na něm udělalo free()
Problémem zde byl přístup k argumentům. V 32-bit systému se parametry ukládají do zásobníku a tedy stačilo místo va_list
předat pointer na první parametr. Tohle nejde použít u 64-bit systému. Většina argumentů se předává přes registry. Norma C na toto myslí, a proto byly zavedeny funkce va_start
, va_arg
a va_end
, a také va_copy
.
Všechny místa, kde se k parametrům přistupuje přes pointer na args se musely přepsat na va_arg
, tato funkce (původně makro) vždy vyzvedne další argument. Problém je, že každý handler si chtěl přečíst stejná data, ale va_arg
vždy posune vnitřní ukazatel na další argument. K tomuto účelu právě existuje va_copy
. Pro každý handler se tedy provedla kopie přes va_copy
. Toto se musel dělat pro každou kopii zprávy – v jazyce, kde není copy constructor :)
Protože message systém je protkán celou hrou, těch míst, kde se takto pracuje s argumenty je opravdu hodně a navíc je bylo těžké odhalit. Tak snad se mi podařilo upravit ve všech handlerech
Následuje (zkrácená) ukázka pro ovládání konzoli (ve hře je možno otevřít ladící konzoli a do ní psát příkazy)
static void console_keyboard(EVENT_MSG *msg, void **_) { if (msg->msg == E_KEYBOARD) { int code = va_arg(msg->data, int); int c = code & 0xFF; //nula dole je extended code if (c) { int len = strlen(console_input_line); if (c == 0x1B) { //escape, zavřít konzoli } if (c == '\b') { //backspace, smaž poslední znak } else if (c == '\r') { //enter } else if (c == 3 && !get_shift_key_state()) { console_copy_to_clipboard(); } else if (c >=32 && len < console_max_characters) { //přidej další znak do příkazové řádky } } else { switch (code >> 8) { //....kurzorové klávesy //.... } } //tímto způsobem se zabrání distribuci zprávy //dalším listenerům msg->msg = -1; } } //instalace send_message(E_ADD,E_KEYBOARD, console_keyboard); //odinstalace send_message(E_DONE,E_KEYBOARD, console_keyboard);
Výčet specialit v tomhle podivném kódu nekončí, podíváme se na korutiny. To totiž byl další prvek, který z počátku velmi bránil jakékoliv portaci. Pamatuji si, jak mi kamarád ukazoval přepínání zásobníků na ZX Spectrum s tím, že se jedná o „multitasking“. Jako člověk, co vyrůstal na ZX mne to tehdy zaujalo ale chtěl jsem si to vyzkoušet na PC. První pokusy byly v Pascalu v reálném režimu.
Do kódu hry byly přidané korutiny v pozdější fázi vývoje. Doposud jsem si vystačil se systémem zpráv, ale některé věci se v tom realizovaly obtížně. Jeden reprezentativní příklad jsou ingame animace. Když hráč útočí zbraní nebo kouzlem, přehraje se ve výhledu animace. Kód, který přehrává animace je stejný kód, který přehrává i úvodní a závěrečnou animace a používá stejný formát. Ten formát jsem vymýšlel sám a je inspirován formátem FLI/FLC (Autodesk animator), ale používá LZW kompresi grafiky (jako gif).
Problém je, že celý přehravač je napsán jako smyčka, která dostane parametrem callback funkci, která dostává informace o eventech v animačím formátu. Jsou tam přitom eventy „přehraj zvuk“ a „zobraz obraz“, podle toho, co se zrovna ze streamu dekódovalo. Veškerý stav přehravače je v zásobníků. Bez dalšího opatření by to znamenalo, že po dobu přehrávání animace by se celý život hry zastavil. Animace by se klíčovala na statický obraz. Ale to nebylo ono. Chtělo to, aby se stav animace zachoval mezi jednotlivými hernímy kroky.
A tak namísto přepisování kódu přehravače – jsme v C, vytvořit si stavový objekt je ohromné množství práce – jsem zavedl korutiny. V našem případě se jmenovaly „task“
typedef void (*TaskerFunctionName)(va_list); int add_task(int stack,TaskerFunctionName fcname,...); void term_task(int id_num); void unsuspend_task(EVENT_MSG *msg); void task_sleep(void); void task_sleep_for(unsigned int time_ms); EVENT_MSG *task_wait_event(int32_t event_number); int q_any_task(void); char q_is_running(int id_num); char q_is_mastertask(void); int q_current_task(void);
A funguje to přesně tak jak se dá očekávat. Pokud chci spustit korutinu, volám funkcí add_task
, předám jí velikost zásobníků (musel jsem odhadnout) a funkci, která se má spustit a její parametry, které se předají jako va_list. Funkce se ihned spustí, ale může ve svém těle zavolat task_sleep
, čímž se řízení vrátí do tzv master_task, tak se označuje kód v hlavní zásobníku. Pokud master_task zavolá task_sleep
, jsou probuzeny všechny uspané korutiny v round-robin schématu.
Korutina ale může zavolat task_wait_event
čímž je uspána dokud někdo pošle zprávu send_message(event_number,...)
. Pak je korutina probuzena a funkce vrátí pointer na EVENT_MSG
. Korutina tedy může čekat jen na jednu událost současně. V novém portu přibyla i funkce sleep_for
, která umožňuje uspat korutinu na určitou doby. Korutina se automaticky ukončí, když doběhne do svého konce, ale lze jí taky zabít přes term_task
. Nicméně neprovede žádný úklid (jen uvolní zásobník).
Problém je, že korutiny nejsou nikam přenositelné. Dlouhou dobu tak jakákoliv portace hry byla tabu, protože představa, že si budu měnit zásobník v operačním systém s chraněným režimem, byla neprůchozí. Teprve až když jsem objevil Windows Fibers se tato možnost otevřela. Windows port z roku 2004 tak používá právě Fibers pro přepínání korutin.
A jak je to v současném portu?
Používají se vlákna. Při přepnutí korutiny dojde k předání „peška“. Vlákno, které drží peška běží, ostatní vlákna čekají na futexu (je to atomic::wait, kdyby to někoho zajímalo) na předání peška. Bohužel jsem nenašel lepší přenositelnější řešení. Naštestí od C++11 jsou vlákna standardní součástí a tak je snažší je použít.
Jestli se ptáte, zda šlo použít C++20 korutiny tak nešlo. Korutiny v kódu počítají se stackful implementací
Nahradit korutiny vlákny je skutečně funkční řešení, i když asi nemusí být nejefektivnější. Naštěstí tato hra nepotřebuje maximální výkon na nejmodernějších počítačích, takže to není něco, co by mne trápilo. Problém jsem měl s SDL.
Kód hry nedělá rozdíl mezi tím, který task (korutina) může kreslit po obrazovce, u SDL tohle bohužel hraje zásadní roli, protože korutiny se emulují vlákny. SDL (speciálně na linuxu) vyžaduje, aby ke grafickému subsystému přistupovalo jedno konkrétní vlákno, zpravidla vlákno, které vytvořilo daný renderer. To se samozřejmě týká všech jeho resourců jako jsou textury, surface ale třeba i okna. Pokud toto není dodrženo, většina API selhává.
Toto jsem nakonec vyřešil tak, že jsem pro SDL a ovládání UI, jako rozměry okna, maximalizace nebo přepínání do fullscreen (ALT+ENTER), svěřil separátnímu vláknu. Pokud kterákoliv část programu posílá aktualizaci obrazovky, předává se to přes frontu. Je tam doslova buffer, který funguje jako fronta. Aktualizace výřezu znamená, že se do bufferu uloží hlavička zprávy a data, která se mají aktualizovat a pošle se do SDL vlákna signál. Na to vlákno reaguje, vyzvedne zprávu a data a provede aktualizaci a překreslení. Tomuto vláknu říkám display server.
Použití display serveru umožňuje v budoucnu portovat hru na headless systémy, které by mohly svůj obrazový obsah posílat na display přes rouru nebo TCP spojení??? Je to vtip, ale šlo by to, proč ne :)
Hlavní výhodou separátního vlákna je, že je ovládání okna responzivní bez ohledu na stav hry. Pomohlo to i při ladění, protože SDL v Linuxu vytváří problémy při ladění události kliknutí myši. Typicky dojde k blokaci myši, dokud je program laděn. Jakmile jsem ale založil SDL vlákno, pak při ladění při použití non-stop gdb mode k těmto problémům nedochází.
Může mít tahle věc ještě nějakou budoucnost?
Byl zveřejněn nový repozitář. Kód hry je nyní pod licenci MIT. Nebyly přeportovány nástroje pro tvorbu nových kampaní. Ty existující kampaně, jako třeba Magika a Skřetí říše by ale měla fungovat, a pokud ne, bude dalším cílem odstraňování chyb v této části kódu. Jestli budou přeportovány nástroje je samozřejmě otázkou budoucnosti.
Na místě je třeba přiznat, že na správu repozitáře nebudu mít tolik času, jak bych si přál. Ty tam jsou časy, kdy jsem u PC trávil 16 hodin nad jedním projektem. Pracuji pro víc klientů, mám rodinu, dvě pubertální děti, dvě kočky, barák, řeším kotel, řeším střechu, solárka, rekonstrukci koupelny. Takže bych touto cestou rád oslovil i zájemce, kteří by chtěli se kódem zabývat. Upřímně v době, kdy hra vyšla, jsem od toho nečekal velký zájem. Přesto mne i po 27 letech překvapilo, kolik lidí si na hru vzpomíná v dobrém, a kolik lidí se těšilo na nové vydání.
Hra nově vychází i v anglické jazykové mutaci – jen tak mimochodem, způsob, jakým je realizována jazyková mutace je asi na další článek.
A to je asi všechno.
Určitě prostudujte příkazovou řádku (parametr -h), a konfigurační soubor skeldal.ini
. Jsou tam nějaké další možnosti nastavení grafiky. Pokud máte problém se zobrazením, lze v konfiguračním souboru vypnout HW akceleraci a vypnout třeba vyhlazování a filtry.
Nově lze hru ovládat pomocí „WASD“ fungující jako šipky. Klávesou X se spojuje skupina. Nově přibyly některé operace přes Ctrl+Click (rozdělování skupiny, rychlé obchodování, atd)
Hra má také nově podporu pro ovladač – tedy já osobně to ladil na PS4 ovladači přes BT. O podporu ovladače se stará SDL2 pomocí SDL_JoystickOpen. Očekávejte od toho ale jen jiný způsob ovládání kurzoru myši a mačkání tlačítek. Mapování tlačítek se dá také změnit ve skeldal.ini. K dispozici je jedno MOD tlačítko, takže na každé tlačítko lze namapovat 2 funkce. Pokud budete hledat tlč. na rozdělování skupiny, tak mačkat pravou páčku nad portrétem.
Dík za zajímavé podrobnosti portaci. Kdysi jsi říkal, že bys to přepsal do JS, z toho asi sešlo, že? (asi by tedy bylo lepší TS nebo Elm, ale to je asi už jedno).
no pokud to poběží s SDL2, tak přes Emscripten by to mohlo být prakticky zadarmo. Já to zkoušel na našem programu v C (jo čisté C, tuším co si říkáš :-p) a v podstatě to bylo za půl hodiny hotový, jen jsem si přidal novej cíl do Makefile.
Úžasný článek, díky... Komplexita (nutných) znalostí programátora na konci devadesátek je udivující. Psaní nízkoúrovňových funkcí, vlastní řešení datových struktur, správa paměti, grafika, animace, kurzor, assembler... To už se dneska asi moc nevidí :-)
Holt, to bola kliatba ms-dosu. Ani si nepamätám, že by bol voľne dostupný engine na takéto veci. Obdivujem autora, že mal chuť na portovanie, ja by som to celé zahodil a napísal odznova v Ru... pardón C++.
Kupodivu mi to přišlo jako víc práce, přeci jen implementace herních mechanik zůstala víceméně netknuta, až na aligmenty. Psát to celé znova a testovat jestli se to chová stejně ...
Eclipse (CDT) používám většinu času, líp se v tom debuguje než ve vscode. To mám často na web věci, poslední dobou na copilota. Často mám otevřený projekt v obou současně :)
Kdyby to někdo chtěl zkoušet, tak soubory z toho DVD Levelu nejsou pro tenhle port vhodné.
Např. požadovaný soubor POPISY.TXT se na něm nikde nenachází.
No, on je to soubor POPISY.ENC
To je tak jednoduše zakódovaný POPISY.TXT.
Program otevře POPISY.TXT - neexistuje, hledá POPISY.ENC - něco se nepovede, a vrátí se do původní funkce a ohlásí chybu s původním názvem.
POPISY.TXT/ENC obsahují texty k UI. Většinou je ale problém v nastavení cest.
Zrovna před pár dny jsem si vzpomněl, jak jsem koukával přes rameno spolužákovi, který Brány Skedalu tehdy hrál a hodil jsem si to do TODO :)
Tak jsem teď vletěl na Steam, ale tam vidím jen verzi pri Windows 10 :/
nejsem bohužel správce steam účtu , tak netuším jak to tam je nastavený ale prý to lidi hrajou i na steamdecku. Linuxova verze každopádně jde vybuildit ze zdrojaku a dokonce budou fungovat v linuxovem steamu achievementy pokud člověk přidá linuxovy bin projektu do rootu hry (na steamu) a přepíše exe obsahem sh souboru)
Není potřeba, ledaže byste chtěli nabídnout nativní verzi na streamu. Na Linuxu mám v Steam klientovi compatibility pro všechny tituly zapnutou a tuto hru jsem zakoupil a jede bez obtíží i se zvukem. Překvapivě podporuje Steam cloud pro saves.
Zkoušel jsem i spustit tuto hru na tabletu přes Steam link k tomu linuxovému stroji. Tam akorát by bylo lepší mít myš přes bluetooth protože tam touchscreen nefunguje a to onscreen ovládání, tlačítka a křížový ovladač, není s touto hrou kompatibilní. Podpora ukazatele přes touchscreen tabletu je pak pomalá a otravná.
Díky za port a rady. Linuxovou binárku se mi podařilo vybuildit a už mi jede ve Steamu. Jen jsem při cmake narazil na toto:
!!! Steam is not installed
!!! compiling without steam and without achievements support
!!! To enable steam, set STEAMWORKS_SDK_DIR to correct value
Mohu poprosit pro postup pro nás lamy neprogramátory, jak to rozchodit i s achievementy?
Pri pouziti __attribute__((__packed__)) u struktury generuje jak GCC tak Clang kod, ktery nevyvola SIGBUS, protoze vi, ze struktura neni zarovnana.
Presne to me napadlo pri cteni - pokud se nebude pretypovavat (coz obecne smerem k delsim typum je vzdy zadost o prusvih), tak toto musi v C fungovat. Nejake to zpomaleni v dusledku nezarovnani na dnesnich masinach bude zanedbatelne. Ta rucni prace musela byt peklo. Kazdopadne kloboucek za to se tomu takto povenovat.
Obecně ARM procesory jsou náchylnější na aligned access, to je pravda. Na x86/x64 je to akorát pomalejší, ale na některých procesorech je to spolehlivá cesta k SIGBUS.
To je nesmysl. __attribute__((__packed__)) nastaví alignment všech prvku objektu na 1, takže překladač musí použít pouze instrukce, které umožňují unaligned access.
To co působí problémy jsou pointry na packed objekty. Dodnes netuším jestli je vůbec nějaký univerzální způsob, jak je deklarovat v přenositelné formě.
mozna jste se ani nemel divat na stary kod a napsat to uplne znova jen s pouzitim puvodnich obrazku a zvuku.
Pán Novák mi napísal, že cieľom bolo zachovať herné mechaniky, aby sa to správalo rovnako.
V tomto mu dávam zapravdu, že je jednoduchšie portnúť nie celkom ideálne spravený starý engine, než sa pokúšať ohýbať lepšie naprogramovaný nový engine. Ak nemá človek dosť času a armádu testerov, je to úplne pochopiteľné.
Avšak zjistit, co konkrétně testovat – a že těch test casů bude požehnaně – bude dost možná víc práce než portování s použitím původního kódu. To sice taky vyžaduje jistou míru testování oproti originálu, ale půjde spíš o odhalování chyb v programování, než v tom, že si autor verze pro novou platformu nějakou herní mechaniku domyslel.
Já si vždycky vzpomenu, jak CD Projekt vyházel testery a nechal otestovat Cyberpunk 2077 externí firmou, kde cíleli na maximum ticketů. Hra byla tak zabugovaná, že ji museli z digitálních obchodů odstranit. Spousta misí ani nešla dohrát.
Paradny clanok,
paci sa mi ako ste prepisali ASM, nenarazili ste v nom na prerusenia alebo nieco, co neslo prepisat?
(ta nostalgia, ked si spomeniem ako som doarabal podporu mysi do Borland Pascalu cez ASM.)
Inac ja mam C radsej ako C++, hoci uznavam, ze mu strasne chybaju generika a defer blok.
Naštěstí, žádné přerušení jsem nepoužíval, tedy ne v téhle části. Přerušení bylo používáno na timer, sound blaster a i na covox. Ale to jsou věci, které se plně nahradily funkcemi operačních systémů.
Všechno přepsat šlo, jen byl problém to přepsat přesně tak,aby to dělalo totéž. To se týká občas některých věcí, které jsou jinak UB, například různé triky zaměřené na přetejkání dat z registru a podobně. To je pak třeba explicitně řešit v C, aby pochopil, že to není UB, že přetejkání je ok, jinak je schopen to zoptimalizovat až moc.
Pak samozřejmě nealignovaný přístup, viz ukázka v textu, jak tam čtu 4 bajty, a nečtu to jako 1x long
Uffff, ono je na jednu stranu výhodné, že jsme zakysli na 64-bitových OS na věky a fullhd i barvy tady s námi vydrží asi i dalších 10 let. Tolik práce.....na nadčasovosti SW by se mělo začít mnohem víc uvažovat i díky tomu zpomalení vývoje HW obecně, které to ulehčuje.
C++ je sice mocnější nástroj než samotné C, jenže zrovna jeho parametr nadčasovosti, kdy mají programátoři v dlouhodobějších projektech pocit "že jim to doslova hnije pod rukama" je horší než u C. O něco víc než v samotný Rust apod. svaté grály věřím ve vliv UI specializované na programování. Však kdo se někdy při programování necítil jak robot - pro ně je to přirozenější než pro biologické formy života.
Budu si to muset přečíst ještě jednou jako odstrašující příklad nevděčné a pomíjivé práce programátora v dávných časech, kdy hardware letěl dopředu jak splašený kůň.
co teprve programovat v době arkáde her, nedavno jsem viděl dokument, jak vznikala v Nintendu donkey kong hra. Běželo to ba zilogu 80, vývojáři programovaly na papir a střídali se u jednoho stroje, který to uměl ladit. Buildy se pak vypalovaly do Rom přes děrované pásky. muselo to být hotovo za půl roku. za roku už to bylo zastaralé
Jsem ještě udělal test jestli se hra spustí na starším laptopu, vyhodilo to chybu a spalo to. Předpokládám že je to chyba driveru, protože pro grafiku R7 M340 už podpora skončila.
Tak jsem nahodil Ubuntu Mate + Steam a zapnul kompatibilitu s Proton 10beta a světe div se ono to naskočilo i se zvukem.
Víc lidí si stěžuje na nějaké ovladače, zkusím se ještě podivat jestli se sdl neda provozovat čistě softwarove. Musím to prostudovat
Já se tedy přiznám, že s nim nejsem v kontaktu, jeho port jsem nepoužil a ani nevím v jakém je stavu. Každopádně bych mu rád připsal nějaký kredit. Trochu je mi ho líto, že jsem mu takhle vzal vítr z plachet
Původní hru s buildem od nextghosta lze dohrát po aplikaci pár patchů, hra padá v pozdějších fázích hry. Záplaty jsem původně psal před několika lety, ale k jejich odeslání sešlo z právě časových priorit; některé chyby dost možná vychytal už nextghost. Krom záplat k původní hře mám i k některým kampaním/rozšířením; původní hru na jeho buildu se mi podařílo dohrát až teď - trochu mě vyprovokovala existence repa s MIT portem.
K samotnému portu od nextghosta - je 64 bit, zčásti C++ (<20), používá SDL1.2 a SDL mixer (s SDL compat lze spustit s SDL2) a snaží se být i pro ostatní platformy, jednu dobu se hlásil snad i tester pro amigu. Windows jsem nezkoušel. Lze spouštět kampaně/rozšíření, v masteru je ale zatím verze, která jede jen v 640x480 okně, a pro chůzi nestačí držet soustavně klávesu, musí se pořád co krok mačkat.
Záplaty později vystavím ve forku repa od nextghosta, pokusím se je začlenit; pokud to půjde, tak zkusím použít něco i do MIT portu, pokud získám souhlas.
v masteru je ale zatím verze [...]
... a i pro tyto nepříjemnosti mám záplaty, ale spíš jen k testování (fullscreen jde, okno si roztáhnete sami/správcem oken, držení klávesy pro chůzi je hack)
Přispívat do repo lze standardně, poslat pull request, pokud to bude rozumný, tak to tam dám.
Je teď ještě nejasné, jak to bude buildem na steam. Je dost možný, že ne vše se bude moci dostat oficiálně do steam distribuce, tam o tom nerozhoduju. Nicméně Jindra sám navrhoval udělat si fork s věcma pro komunitu a pro vlastní potřebu si tam aplikovat úprav kolik člověk chce.
Koupil jsem za necelé €4 na Steamu. Zdrojáky zkompilované za minutu a už mi běží nativní verze pro linux. Díky!
Škoda, že pan Rohlík nedal na Steam i verzi pro linux, když je to tak hezky připravené.
Editor map není open-source?
Editor map jsem ještě nepřeportoval. Musíte hodně žadonit. Vidím tam víc problémů kolem message systému a v celém tom UI a samozřejmě s algnmentem
Proč tam nedal verzi pro linux nevím, ale něco mi říká, že linux je pro něj španělská vesnice. Netuší kde roste. Ale binárky dostal i návod z chatgpt :)
Skvely clanek, dekuji moc za nej i za hru!
Prvni Skeldal sem hral kdysi davno kdyz vysel a doted na nej vzpominam a ted sem koupil novou verzi na Steamu a rovnou k tomu pridal i 2ku.
Podilel jste se i na 2ce? Prosla nebo projde i 2ka podobnou portovaci iniciativou?
Pokud ano, myslim ze clanek o detailech vyvoje druheho dilu by byl moc zajimavy :-)
Dekuji!
Bohužel ne. Dvojku programoval Hlouškův Centauri Production. A trojku (sedm mágů) ani nevím kdo - neví to ani Wikipedie.
Intenzivně se zabývám programováním zejména v jazyce C++. Vyvíjím vlastní knihovny, vzory, techniky, používám šablony, to vše proto, aby se mi usnadnil život při návrhu aplikací. Pracoval jsem jako programátor ve společnosti Seznam.cz. Nyní jsem se usadil v jednom startupu, kde vyvíjím serverové komponenty a informační systémy v C++
Přečteno 53 156×
Přečteno 25 156×
Přečteno 23 545×
Přečteno 22 043×
Přečteno 19 779×