speccy.pl
Facebook Like


SPECCY.PL

[SPECCY.PL PARTY 2022.1]

[WIKI SPECCY.PL]
Polecamy

Polskie Komputery
KOMITET SPOŁECZNY KRONIKA POLSKIEJ DEMOSCENY
PIXEL HEAVEN 2020
AYGOR
Forum ZX Spectrum
Zawartość panelu chwilowo niedostępna
Archiwum plików ZX Spectrum
Nawigacja
Oldschool demomaking, część 2

Oldschool demomaking


Scrollery - część 2


W przykładach z pierwszej części cyklu scrollery używały standardowego zestawu znaków z ROMu Spectrum. Rozwiązanie takie jest wygodne ponieważ nie trzeba przygotowywać zestawu znaków co samo w sobie - jeśli chcemy to zrobić dobrze - może zająć dużo czasu. W prawdziwych demach używamy jednak zwykle własnych zestawów znaków stąd kolejny artykuł zaczniemy od wskazówek jak zabrać się do tworzenia zestawu znaków w stylu "old school".

Własna czcionka

Nie ma oczywiście problemu, żeby w praktyce użyć któregoś z dostępnych na PC (głównie pod Windows) programów do edycji grafiki dla Spectrum albo jakiegoś specjalizowanego edytora do czcionek. W najprostszej jednak wersji do przygotowania czcionki wystarczy nam dowolny program graficzny na Spectrum (np. Artstudio czy The Artist). Dla ułatwienia pracy można użyć paru prostych programików w BASICu. Pierwszy z nich przygotowuje nam szablon do edycji w programie graficznym tak, żebyśmy się nie pogubili przy tworzeniu czcionki.

10 for i=0 to 5
20 for j=0 to 15
30 let c=32+i*16+j
40 print at i*2,j*2; chr$ c
50 next j
60 next i

Szablon i cały zestaw znaków przygotujemy dla scrollera o znakach o rozmiarze 16x16 pikseli a wynikiem działania programu jest taki:

Obrazek ten zapisujemy jako normalny zrzut ekranu (SAVE "szablon" SCREEN$) i możemy go zachować na przyszłość. Po załadowaniu do programu graficznego musimy w odpowiednie kwadraty 16x16 pikseli wyrysować naszą czcionkę - tutaj potrzebna jest odrobina zmysłu artystycznego, której mojej czcionce przykładowej niestety brakuje. Na zakończenie procedury powinniśmy uzyskać obrazek zbliżony do takiego:

Taka forma nie nadaje się do praktycznego zastosowania - czcionka musi zostać zamieniona z organizacji pamięci ekranu Spectrum na postać liniową (kolejne bajty jednego znaku w kolejnych komórkach pamięci). Można to zrobić na przykład takim programem:

10 clear 32767
20 load "" screen$
30 let a=32768
40 for t=0 to 1
50 for i=0 to 3
60 for j=0 to 15
70 for l=0 to 1
80 for p=0 to 7
90 for b=0 to 1
100 let s=16384+t*2048+i*64+j*2+l*32+p*256+b
110 poke a,peek s
120 poke s,255
130 let a=a+1
140 next b
150 next p
160 next l
170 next j
180 next i
190 next t

Obrazek ładowany jest na ekran w linii 20 a następnie wykonywany jest szereg pętli "linearyzujących" nasz zestaw znaków. Kolejne liczniki pętli składane są w linii 100 w adres bajtu w pamięci ekranu. Wyrażenie tam zawarte ma następujące składowe:


  • 16384 - początek pamięci ekranu

  • t*2048 - tercja ekranu - podzielony jest on na trzy fragmenty po 2048 bajtów każdy z identyczną organizacją w każdym fragmencie

  • i*64 - kolejne wiersze zestawu znaków - każdy znak zajmuje na wysokość dwa wiersze ekranu stąd licznik wierszy znaków mnożony jest przez 64 (2*32)

  • j*2 - kolejne kolumny zestawu znaków - każdy znak ma 16 pikseli czyli 2 bajty szerokości

  • l*32 - to już fizyczne wiersze ekranu w ramach logicznego wiersza ekranu

  • p*256 - kolejne linie w wierszu ekranu - przy projektowaniu układu ULA organizacja ekranu została wybrana tak aby w ramach dostępnych do wyboru możliwości uprościć dostęp do pamięci przy drukowaniu standardowych znaków dlatego odstęp między kolejnymi liniami w ramach wiersza to 256 bajtów - przeskok do kolejnej linii to zwiększenie starszego bajtu adresu

  • b - numer kolejnego bajtu w pojedynczej linii znaku


Program jest napisany w taki sposób aby połączyć logiczną strukturę znaków z fizyczną organizacją ekranu. Łatwo byłoby go również zmodyfikować dla znaków o innym niż 16x16 rozmiarze.

Po wyliczeniu adresu w pamięci ekranu w linii 110 jest on przepisywany do pamięci zaczynając od adresu 32768 a w jego miejsce na ekranie wpisywane jest 255. Linię 120 można pominąć - służy ona wyłącznie do wizualnego upewnienia się czy wyliczane adresy są poprawne.

Po zakończeniu działania programu mamy w pamięci przygotowany do dalszego użycia zestaw znaków. Program przepisuje dane z nadmiarem, ale dodawanie licznika znaków niepotrzebnie by go skomplikowałoby i spowolniło jego wykonanie.

Dane wygenerowane przez program zajmują 3072 bajty zaczynając od adresu 32768 - należy je zapisać (np. SAVE "znaki" CODE 32768,3072).

16x16....

Scroller 16x16 pikseli nie różni się jakoś specjalnie od tych prezentowanych wcześniej. Zarówno pętla główna jak i wskaźnik do tekstu realizowane są identycznie jak poprzednio a jedyna różnica to licznik - tym razem ustawiany (przy move_text) na 15.

        org 32768

main_loop:
        halt
        call one_scroll
        ld bc,$7ffe
        in a,(c)
        and 1
        jr nz, main_loop
        ret

one_scroll:
        ld a,(bpos)
        or a
        jr z,move_text
        dec a
        ld (bpos),a
        jr do_scroll
move_text:
        ld a,15
        ld (bpos),a
text_adr: equ $+1
        ld hl,text
        ld a,(hl)
        inc hl
        ld (text_adr),hl
        cp 13
        jr nz,get_char
        ld hl,text
        ld a,(hl)
        inc hl
        ld (text_adr),hl

Istotna różnica to oczywiście kopiowanie znaku do bufora.

get_char:
        sub 32
        ld h,0
        ld l,a
        add hl,hl
        add hl,hl
        add hl,hl
        add hl,hl
        add hl,hl
        ld de,font
        add hl,de
        ld de,buf

        rept 32
          ldi
        endm

Jak widać od kodu znaku odejmowane jest 32 (kod spacji) a następnie jest on mnożony przez 32 (każde z kolejnych dodawań HL do siebie to oczywiście mnożenie przez 2) i modyfikowany o początek zestawu znaków. Z wyliczonego adresu przerzucamy 32 bajty do bufora ciągiem rozkazów LDI.

Użycie takiej konstrukcji może się wydawać dziwne - można by pomyśleć, że LDIR będzie wygodniejszy. Problem w tym, że wygoda okupiona jest znacznymi różnicami w czasie wykonania. Pojedyncze LDI kopiuje jeden bajt i jego wykonanie trwa 16 taktów. LDIR kopiuje blok o długości podanej w BC ale dla każdego oprócz ostatniego bajtu wykonanie trwa 21 taktów. O ile przy kilkudziesięciu bajtach może się wydawać, że nie ma to znaczenia dobrze jest przyzwyczaić się do takich oszczędności bo w praktycznych zastosowaniach mają one kolosalne znaczenie.
Używana tutaj konstrukcja z dyrektywą rept to składnia specyficzna dla assemblera Pasmo - powtórzenie podaną jako parametr liczbę razy bloku kodu zawartego między rept i endm.

Właściwy kod scrollera trochę różni się od poprzednich. Początek jest taki sam:

do_scroll:
        ld hl,20672+31
        ld de,buf+1
        ld b,2
loop0:
        push bc
        push hl
        ld b,8

Ustawiamy pozycję scrollera, otwieramy zewnętrzną pętlę (dwa przebiegi - dwa wiersze po 8 linii ekranu), zapamiętujemy jej licznik i pozycję na ekranie i inicjujemy licznik pętli wewnętrznej. W związku z tym, że znak ma teraz 2 bajty szerokości wskaźnik do bufora znaków w DE ustawiany jest na drugi bajt pierwszej linii znaku.

Wewnętrzna pętla musi oczywiście najpierw przesunąć dwa bajty w buforze i wysunąć najstarszy bit do CY.

loop1:
        push hl
        ld c,32
        ld a,(de)
        rla
        ld (de),a
        dec de
        ld a,(de)
        rla
        ld (de),a
        inc de

Na początek zapamiętywana jest pozycja na ekranie w ramach wiersza i ustawiany licznik pętli przesuwającej pojedynczą linię. Potem przesuwany jest pierwszy (prawy) bajt znaku z bufora, zmniejszany wskaźnik i przesuwany drugi (lewy) bajt. Na koniec wskaźnik do bufora jest znowu zwiększany - można to zrobić tutaj albo po zamknięciu pętli. Na zakończenie w CY znajduje się wartość bitu do wsunięcia w aktualną linię ekranu.
Sama pętla przesuwająca linię jest identyczna jak poprzednio:

loop2:
    ld a,(hl)
    rla
    ld (hl),a
    dec hl
    dec c
    jr nz,loop2

Ostatnia różnica to zwiększenie wskaźnika do bufora o 2 związane oczywiście z większym rozmiarem znaku.

        inc de
        inc de
        pop hl
        inc h
        djnz loop1
        pop hl
        ld bc,32
        add hl,bc
        pop bc
        djnz loop0
        ret

Domknięcie obu pętli zewnętrznych jest identyczne jak poprzednio - najpierw HL zwiększane jest o 256 w każdym przebiegu przeskakując co linię a na zewnątrz zwiększane jest (po podniesieniu ze stosu) o 32 przeskakując do kolejnego wiersza.

Na koniec jeszcze tylko zmienne:

buf:    ds 32
bpos:   db 0
text:   defm "Scrollery, czesc 2 (c) 2012 mat for speccy.pl...   "
        db 13
font:   incbin "chars.bin"
        end 32768

Bufor ma oczywiście większy rozmiar. Dodatkowo pojawia się deklaracja etykiety "font" z poleceniem incbin - powoduje ono wczytanie w miejscu swego wystąpienia binarnego pliku podanego jako parametr - w tym wypadku "chars.bin" zawiera przygotowany wcześniej zestaw znaków.

Działający scroller wygląda tak:

Czcionka ze zmienną szerokością liter

Stosunkowo prostą, choć wymagającą pewnych przygotowań, modyfikacją tego scrollera jest wersja używająca czcionki zmiennej szerokości. Przygotowania te to zmiana samej czcionki tak, żeby na obrazie wyjściowym przed przetworzeniem na postać liniową wszystkie znaki były dosunięte do lewego brzegu swojej przestrzeni. Efekt w wypadku czcionki przykładowej (część znaków została też zwężona) wygląda tak:

Czcionkę generujemy i zapisujemy dokładnie tak jak poprzednio. Dodatkowo musimy przygotować tablicę szerokości poszczególnych znaków - trzeba to zrobić ręcznie zapisując szerokość w pikselach każdego z 96 znaków ze stosownym marginesem tak aby znaki się nie zlewały. Po przygotowaniu czcionki i tablicy szerokości możemy przystąpić do modyfikacji kodu.

Pierwsza zmiana to usunięcie inicjalizacji licznika z procedury przesuwającej wskaźnik teksu. Po modyfikacji fragment kodu wygląda tak:

one_scroll:
        ld a,(bpos)
        or a
        jr z,move_text
        dec a
        ld (bpos),a
        jr do_scroll
move_text:
text_adr: equ $+1
        ld hl,text
        ld a,(hl)

Druga zmiana to procedura wyliczająca adres znaku - tutaj przeniosła się inicjalizacja licznika.

get_char:
        sub 32
        ld h,0
        ld l,a
        push hl
        ld de,size_tab
        add hl,de
        ld a,(hl)
        dec a
        ld (bpos),a

        pop hl
        add hl,hl
        add hl,hl
        add hl,hl
        add hl,hl
        add hl,hl
        ld de,font
        add hl,de
        ld de,buf

Jak widać podstawowa różnica do kod, który indeksując za pomocą HL (zawierającego numer aktualnego znaku) tabelę "size_tab" i pobiera z niej bajt, który (po zmniejszeniu o jeden - można to oczywiście zrobić przygotowując tabelę) zapisywany jest do głównego licznika. Dalszy ciąg kodu jest identyczny jak poprzednio.
Ostatnia zmiana to oczywiście dodanie tablicy szerokości na końcu kodu tuż przez incbin wczytującym plik z czcionką.

size_tab:
        db 10, 4, 7,14,15,15,15, 4, 5, 5,12,12, 4,12, 4,15
        db 15, 6,15,15,15,15,15,15,15,15, 4, 4, 9,12, 9,13
        db 15,15,15,15,15,15,15,15,15, 4,15,15,15,15,15,15
        db 15,15,15,15,14,15,15,15,15,14,15, 5,15, 5,11,15
        db  1,12,12,12,12,12, 7,12,12, 4, 8,12, 6,14,12,12
        db 12,12,12,12, 7,12,12,14,12,12,12, 6, 4, 6,15,15

Oczywiście przygotowując własną czcionkę musimy w tym miejscu umieścić własną tablicę szerokości.

Jak widać poniżej czcionka zmiennej szerokości nadaje działającemu scrollerowi bardziej "elegancki" wygląd.



Jeszcze większe litery.... 32x32!

Kolejnym realizowanym bezpośrednio w pamięci ekranu scrollerem jest używany dość często w starych demach scroller ze znakami o rozmiarze 32x32 piksele w którym znaki ze standardowej matrycy 8x8 pikseli są powiększane czterokrotnie a cały scroller przesuwa się o 4 piksele na ramkę.

Rozwiązanie to wykorzystuje operujący na połówkach bajtów w pamięci rozkaz procesora Z80 RLD. Jest to stosunkowo skomplikowany a przy tym dość szybki (wykonuje się w 18 taktów) rozkaz dokonujący zamiany połówek bajtów - wskazywanego przez HL i akumulatora.
Zamiana ta wygląda następująco:


  • cztery najmłodsze bity bajtu pamięci wskazywanego przez HL są przepisywane do czterech starszych bitów tego bajtu

  • cztery starsze bity są przepisywane do czterech najmłodszych bitów akumulatora

  • cztery najmłodsze bity akumulatora są przepisywane do czterech najmłodszych bitów bajtu pamięci


Jak widać sposób działania tego rozkazu idealnie nadaje się do przesuwania zawartości ekranu Spectrum o 4 piksele na raz.

Nasz scroller zaczyna się identycznie jak wszystkie wcześniejsze.

    org 32768

main_loop:
    halt
    call one_scroll
    ld bc,$7ffe
    in a,(c)
    and 1
    jr nz, main_loop
    ret

one_scroll:
    ld a,(bpos)
    or a
    jr z,move_text
    dec a
    ld (bpos),a
    jr do_scroll
move_text:
    ld a,7
    ld (bpos),a
text_adr: equ $+1
    ld hl,text
    ld a,(hl)
    inc hl
    ld (text_adr),hl
    cp 13
    jr nz,get_char
    ld hl,text
    ld a,(hl)
    inc hl
    ld (text_adr),hl
get_char:
    ld h,0
    ld l,a
    add hl,hl
    add hl,hl
    add hl,hl
    ld de,15360
    add hl,de
    ld de,buf

    rept 8
      ldi
    endm

Sama pętla przesuwająca zawartość ekranu ma trzy poziomy. Pierwszy to oczywiście licznik linii znaku - tutaj wygląda to jak normalny scroller 8x8.

do_scroll:
    ld hl,20608+31
    ld de,buf
    ld b,8

Następny fragment różni się od wcześniejszych.

loop1:
    ld a,(de)
    rla
    ld (de),a
    jr c,is_set
    ld a,0
    jr is_reset
is_set:
    ld a,15
is_reset:

Bajt znaku z bufora jest przesuwany jak dotychczas a następnie zależnie od jego stanu do akumulatora ładowane jest 0 (wyzerowane cztery najmłodsze bity) albo 15 (ustawione cztery najmłodsze bity).

    push bc
    ld b,4
loop2:
    push af
    push hl
    ld c,32

Otwarcie drugiego i trzeciego poziomu pętli jest dość typowe. Drugi poziom to cztery przebiegi przesuwające cztery kolejne linie ekranu tworzące jedną linię scrollera. Trzeci poziom to przesunięcie 32 bajtów pojedynczej linii. Przed rozpoczęciem tej pętli zapamiętywany jest adres linii ekranu i stan bitu ustawionego wcześniej w akumulatorze.

loop3:
    rld
    dec l
    dec c
    jr nz,loop3
    pop hl

Sama pętla jest bardzo prosta - zamieniamy bity, zmniejszamy wskaźnik ekranu (wystarczy dec l - pojedyncza linia ekranu nie wykracza poza zakres 256 bajtów) i zamykamy pętlę.

Kolejny fragment to bardzo przydatna procedura wyliczająca adres bajtu ekranu znajdujący się linię poniżej adresu wskazywanego przez HL.

    inc     h
    ld      a,h
    and     7
    jr      nz,skip
    ld      a,l
    add     a,32
    ld      l,a
    jr      c,skip
    ld      a,h
    sub     8
    ld      h,a
skip:

Procedura przelicza adres zgodnie z dość skomplikowaną organizacją pamięci ekranu używając do tego akumulatora i HL, które na koniec zawiera adres następnej linii ekranu. Procedury tej można by uniknąć komplikując jednak niepotrzebnie pętle przesuwające.

Domknięcie pętli zewnętrznych jest standardowe - odtwarza tylko najpierw zachowany stan pierwszego punktu.

    pop af
    djnz loop2
    pop bc
    inc de
    djnz loop1
    ret

Deklaracje również są standardowe:

buf:    ds 8
bpos:   db 0
text:   defm "Scrollery, czesc 2 (c) 2012 mat for speccy.pl...   "
        db 13
    end 32768

Działający scroller wygląda tak:

Scroller ten mimo niewielkiego skomplikowania zajmuje jednak dość dużo czasu procesora ponieważ przesuwa 1024 bajty ekranu co samo w sobie zabiera ponad 18000 taktów. Czas wykonania całej procedury powoduje, że jeśli scroller przesunęlibyśmy na górę ekranu zmieniając w "do_scroll" adres na 16384+31 widać będzie wyraźnie, że scroller "ścina się" - efekt ten to typowy problem powodowany tym, że część scrollera wykonywana jest zanim raster wyświetlający zdąży dojść do aktualnie obrabianego a reszta dopiero po przejściu rastra - scroller jest "rozrywany" między dwie ramki. Aby tego uniknąć należałoby wprowadzić pewne opóźnienie przed wywołaniem procedury scrollera - w normalnych warunkach można na początek pętli głównej wstawić na przykład wywołanie procedury odtwarzającej muzykę itp.
Aby przekonać wizualnie się ile czasu zajmuje wykonanie danej procedury można użyć prostego triku i zmodyfikować główną pętlę:

main_loop:
    halt
    ld a,1
    out ($fe),a
    call one_scroll
    ld a,7
    out ($fe),a
    ld bc,$7ffe
    in a,(c)
    and 1
    jr nz, main_loop
    ret

Jak widać po zakończeniu procedury obsługi przerwań kolor ramki ekranu jest zmieniany na niebieski a następnie wywoływana jest procedura scrollera i ramka z powrotem zmieniana jest na białą. Dzięki temu po uruchomieniu programu wyraźnie widać w którym mniej więcej miejscu znajduje się raster w momencie zakończenia procedury scrollera. W podobny sposób używając różnych kolorów ramki można ocenić jak dużo czasu zajmuje wykonanie poszczególnych elementów dzięki czemu można też na przykład zmienić kolejność wykonywania pewnych procedur tak aby uniknąć konfliktu między naszym kodem a rastrem.

Atrybuty, atrybuty!

Ostatni wykonywany bezpośrednio w pamięci ekranu scroller to scroller "na atrybutach". Scroller ten wyświetla duże znaki - zwykle w matrycy 8x8 z punktem o rozmiarze 8x8 pikseli i przesuwa się z prędkością 8 pikseli na klatkę.
Kod aż do samej procedury przesuwającej wygląda identycznie jak scroller 8x8:

    org 32768

main_loop:
    halt
    call one_scroll
    ld bc,$7ffe
    in a,(c)
    and 1
    jr nz, main_loop
    ret

one_scroll:
    ld a,(bpos)
    or a
    jr z,move_text
    dec a
    ld (bpos),a
    jr do_scroll
move_text:
    ld a,7
    ld (bpos),a
text_adr: equ $+1
    ld hl,text
    ld a,(hl)
    inc hl
    ld (text_adr),hl
    cp 13
    jr nz,get_char
    ld hl,text
    ld a,(hl)
    inc hl
    ld (text_adr),hl
get_char:
    ld h,0
    ld l,a
    add hl,hl
    add hl,hl
    add hl,hl
    ld de,15360
    add hl,de
    ld de,buf
    rept 8
      ldi
    endm

Pętla przesuwająca jest tym razem tylko jedna - odliczająca linie ekranu. Na początku ustawiamy wskaźnik do pamięci atrybutów - tym razem jednak do początku linii - 23040 to ósmy od dołu wiersz ekranu.

do_scroll:
    ld hl,23040
    ld de,buf
    ld b,8

Wewnątrz pętli najpierw przesuwana jest zawartość bufora i - podobnie jak w poprzednim scrollerze - na podstawie stanu CY ustawiana jest zawartość akumulatora na odpowiednią wartość atrybutu - 56 to biały kwadrat, 0 - czarny.

loop1:
    ld a,(de)
    rla
    ld (de),a
    jr c,is_set
    ld a,56
    jr is_reset
is_set:
    ld a,0
is_reset:

Przesuwanie zawartości pamięci tym razem wykonywane jest jako proste kopiowanie danych:

    push de
    push bc
    push hl
    pop de
    inc hl
    rept 31
      ldi
    endm
    ld (de),a
    pop bc
    pop de
    inc de
    djnz loop1
    ret

Wskaźnik do bufora (DE) i licznik pętli (BC) są zapamiętywane a następnie do DE ładowana jest zawartość HL a HL jest zwiększane. Na koniec wykonywane jest 31 razy LDI co powoduje przesunięcie zawartości pamięci ekranu o jeden bajt "w dół" a na koniec HL wskazuje na pierwszy bajt następnej linii atrybutów a DE na ostatni bajt aktualnej linii. Po przesunięciu danych pod DE ładowana jest ustawiona wcześniej wartość "piksela" z akumulatora, odtwarzany jest licznik i wskaźnik do bufora a następnie pętla jest zamykana co kończy procedurę.

Reszta kodu wygląda standardowo:

buf:    ds 8
bpos:   db 0
text:   defm "Scrollery, czesc 2 (c) 2012 mat for speccy.pl...   "
        db 13
        end 32768

Wykonanie programu generuje czarny scroller na białym tle. Efekt ten można w bardzo prosty sposób "upiększyć" dodając kratkę jako tło oraz ustawiając odpowiednio atrybuty. Początku kodu zmieniamy następująco:

    org 32768

    xor a
    out ($fe),a
    ld hl,22528
    ld de,22529
    ld bc,767
    ld a,7
    ld (hl),a
    ldir

    ld hl,20480
    ld de,20481
    ld bc,2047
    ld a,128
    ld (hl),a
    ldir

    ld hl,20480
    ld de,20481
    ld bc,255
    ld a,255
    ld (hl),a
    ldir

main_loop:

Pierwszy blok ustawia kolor ramki na czarny oraz atrybuty całego ekranu na biały INK i czarny PAPER. Drugi blok zapisuje do całej dolnej tercji ekranu bajt 128 (ustawiony lewy piksel w każdym bajcie) - w efekcie dostajemy na ekranie pionowe paski co 8 pikseli. Trzeci blok zapisuje do pierwszych 256 bajtów dolnej tercji wartość 255 (ustawione wszystkie piksele w bajcie) co dzięki organizacji ekranu powoduje utworzenie poziomych linii co 8 pikseli ekranu a w połączeniu z poprzednim blokiem daje nam białą kratkę na czarnym tle.

Druga modyfikacja to zmiana wartości atrybutów dla ustawionego i skasowanego piksela znaku:

loop1:
    ld a,(de)
    rla
    ld (de),a
    jr c,is_set
    ld a,7
    jr is_reset
is_set:
    ld a,63
is_reset:

Piksel wyzerowany to atrybut 7 (biały INK, czarny PAPER), piksel ustawiony to atrybut 63 (biały INK, biały PAPER). Efekt uruchomienia programu jest taki:


Archiwum z kodami źródłowymi oraz plikami .tap do pobrania.

Brak komentarzy. Może czas dodać swój?
Dodaj komentarz
Zaloguj się, aby móc dodać komentarz.
Oceny
Tylko zarejestrowani użytkownicy mogą oceniać zawartość strony
Zaloguj się , żeby móc zagłosować.

Brak ocen. Może czas dodać swoją?