NXI/SCR Viewer 1.1
7. 6. 2026

Calm Commander umí načítat externí viewer pluginy z adresáře c:/CalmCommander/plugin. Plugin není samostatná aplikace s vlastním loaderem, ale malý binární modul .ccp, který Calm Commander nahraje do paměti, předá mu kontext aktuálního souboru a po návratu zase obnoví file manager.

Calm Commander: jak psát viewer pluginy

Praktický tutorial k plugin systému Calm Commanderu pro ZX Spectrum Next.

Calm Commander umí načítat externí viewer pluginy z adresáře c:/CalmCommander/plugin. Plugin není samostatná aplikace s vlastním loaderem, ale malý binární modul .ccp, který Calm Commander nahraje do paměti, předá mu kontext aktuálního souboru a po návratu zase obnoví file manager.

Tenhle text je zaměřený na psaní vlastního pluginu. Nebudeme tedy řešit obecnou myšlenku modularity, ale konkrétní ABI: kam se plugin skládá, co dostane v registrech, co je v plugin_api.i.asm, jak číst data souboru a jak vrátit řízení zpět do Calm Commanderu.

Aktuální viewer pluginy v projektu:
  • text.ccp - textový viewer s režimy text/hex/dec a hledáním
  • zxscreen.ccp - prohlížeč klasických 6912b ZX Spectrum obrazovek
  • nxi.ccp - viewer NXI obrázků
  • pt2test.ccp, pt3test.ccp, stctest.ccp, stptest.ccp, sqtest.ccp - hudební test/player pluginy
  • HelloWord.ccp - malá referenční ukázka plugin contractu

Co musí plugin splňovat

Plugin se assemblí jako kód od adresy $C000. Calm Commander ho načítá do samostatné banky a volá jeho první instrukci právě na této adrese. Velikost pluginu je teď omezená na 4096 bajtů, tedy rozsah $C000-$CFFF.

DEVICE ZXSPECTRUMNEXT
        org VIEW_PLUGIN_ADDRESS

        include "plugin_api.i.asm"

plugin_start
        ; HL = pointer na PluginContext
        ; DE = pointer na service table
        ...

plugin_end
        assert plugin_end - plugin_start <= VIEW_PLUGIN_SIZE
        SAVEBIN "plugin/myviewer.ccp", VIEW_PLUGIN_ADDRESS, VIEW_PLUGIN_SIZE

Vstupní bod není pojmenovaný symbol, který by loader vyhledával. Je to jednoduše začátek binárky na $C000. Proto je dobré mít hned na začátku plugin_start a za ním inicializaci kontextu.

plugin_api.i.asm

Soubor plugin/plugin_api.i.asm je malý, ale je to hlavní veřejné rozhraní mezi Calm Commanderem a pluginem. Plugin by si neměl opisovat konstanty ručně, ale includovat právě tenhle soubor.

VIEW_PLUGIN_ADDRESS  equ 49152
VIEW_DATA_ADDRESS    equ 57344
VIEW_PLUGIN_SIZE     equ 4096

VIEWCTX_ABI          equ 0
VIEWCTX_TYPE         equ 1
VIEWCTX_FILENAME     equ 2
VIEWCTX_SIZE_LO      equ 4
VIEWCTX_SIZE_HI      equ 6
VIEWCTX_DATA_PAGE    equ 8
VIEWCTX_DATA_ADDR    equ 9
VIEWCTX_READ_LEN     equ 11
VIEWCTX_PAGE_COUNT   equ 13
VIEWCTX_DATA_PAGES   equ 14
VIEWCTX_SERVICES     equ 16

SERVICE_PRINT        equ 0
SERVICE_INKEY        equ 2
SERVICE_WINDOW       equ 4
SERVICE_LAYER0       equ 6
SERVICE_INPUT_NOWAIT equ 8

Volání pluginu

Calm Commander před voláním připraví data vybraného souboru, nahraje plugin, vyplní context a zavolá adresu $C000. Při vstupu platí:

  • HL ukazuje na PluginContext
  • DE ukazuje na tabulku služeb Calm Commanderu
  • A obsahuje ABI verzi, aktuálně 1
  • kód pluginu je namapovaný na $C000
  • aktuální datová stránka souboru se mapuje na $E000

Plugin se vrací obyčejným ret. Návratová hodnota je v registru A: 0 znamená zavřít viewer a vrátit se do Calm Commanderu, 1 znamená zavřít viewer a požádat Calm Commander o posun na další soubor.

PluginContext

Context je malá struktura, kterou si plugin typicky uloží do vlastní proměnné a čte ji přes IX. Není potřeba znát vnitřnosti Calm Commanderu, stačí offsety z plugin_api.i.asm.

plugin_start
        ld (ctxPtr),hl
        ld (svcPtr),de
        call patch_services

        ld ix,(ctxPtr)
        ld a,(ix+VIEWCTX_ABI)
        cp 1
        jr nz,.unsupported

        ld e,(ix+VIEWCTX_FILENAME)
        ld d,(ix+VIEWCTX_FILENAME+1)
        ; DE teď ukazuje na jméno souboru ukončené nulou

VIEWCTX_SIZE_LO a VIEWCTX_SIZE_HI tvoří 32bitovou velikost souboru. Menším viewerům často stačí nižších 16 bitů, ale textový viewer např. kontroluje i horních 16 bitů a při velkém souboru pracuje jen s načtenou částí.

VIEWCTX_READ_LEN říká, kolik bajtů je dostupných v prvním načteném bloku. VIEWCTX_PAGE_COUNT a VIEWCTX_DATA_PAGES popisují seznam 8K stránek, které Calm Commander načetl pro aktuální soubor.

Čtení dat souboru

Nejjednodušší plugin může číst první blok přímo z VIEW_DATA_ADDRESS. To je vhodné např. pro jednoduché formáty, které se vejdou do první stránky nebo mají hlavičku na začátku souboru.

        ld hl,VIEW_DATA_ADDRESS
        ld a,(hl)
        ; A = první bajt načteného souboru

Pro větší soubory je důležitý seznam stránek. VIEWCTX_DATA_PAGES ukazuje na pole MMU page čísel. Plugin si vybere příslušnou stránku, namapuje ji do MMU slotu pro $E000 a potom čte z VIEW_DATA_ADDRESS.

        ld ix,(ctxPtr)
        ld l,(ix+VIEWCTX_DATA_PAGES)
        ld h,(ix+VIEWCTX_DATA_PAGES+1)
        ld a,(hl)              ; první datová MMU page
        nextreg $57,a           ; MMU7 = $E000-$FFFF
        ld hl,VIEW_DATA_ADDRESS

Calm Commander dnes připravuje až osm 8K stránek. Textový viewer toho využívá pro soubory větší než prvních 8 KB, zatímco jednodušší viewery si vystačí s aktuálně namapovanou datovou adresou.

Service table

Plugin nemá volat náhodné adresy z Calm Commanderu. Dostane tabulku služeb v DE a z ní si opíše adresy rutin, které potřebuje. V ukázkových pluginech se to dělá tak, že se lokální call 0 placeholdery opatchují skutečnými adresami.

patch_services
        ld ix,(svcPtr)
        ld l,(ix+SERVICE_PRINT)
        ld h,(ix+SERVICE_PRINT+1)
        ld (call_print+1),hl
        ld l,(ix+SERVICE_WINDOW)
        ld h,(ix+SERVICE_WINDOW+1)
        ld (call_window+1),hl
        ld l,(ix+SERVICE_INPUT_NOWAIT)
        ld h,(ix+SERVICE_INPUT_NOWAIT+1)
        ld (call_input+1),hl
        ret

call_print
        call 0
        ret

call_window
        call 0
        ret

call_input
        call 0
        ret

SERVICE_PRINT používá stejnou konvenci jako interní tisk Calm Commanderu: DE ukazuje na nulou ukončený text, HL nese pozici jako H = sloupec, L = řádek a A je atribut/barva. SERVICE_WINDOW kreslí okno, kde HL je pozice a BC velikost.

Pro vstup je nejlepší používat SERVICE_INPUT_NOWAIT. Je neblokující a přitom nechává běžet obsluhu myši. Viewer díky tomu nezmrazí kurzor ani zbytek vstupní logiky.

Minimální plugin

Tohle je zkrácená kostra podle plugin/HelloWord.asm. V reálném pluginu si pravděpodobně uložíte stack, vykreslíte vlastní obrazovku, počkáte na puštění spouštěcí klávesy a potom budete v input loopu reagovat na klávesy nebo myš.

DEVICE ZXSPECTRUMNEXT
        org VIEW_PLUGIN_ADDRESS
        include "plugin_api.i.asm"

PLUGIN_STACK equ $DFFE

plugin_start
        ld (ctxPtr),hl
        ld (svcPtr),de
        call patch_services

        ld (savedSp),sp
        ld sp,PLUGIN_STACK

        ld hl,0*256+5
        ld bc,78*256+20
        ld a,16
        call call_window

        ld de,titleText
        ld hl,2*256+6
        ld a,16
        call call_print

.input_loop
        halt
        call call_input
        or a
        jr z,.input_loop
        cp 1
        jr z,.stop
        cp 2
        jr z,.next
        jr .input_loop

.stop
        ld sp,(savedSp)
        xor a
        ret

.next
        ld sp,(savedSp)
        ld a,1
        ret

titleText defb "My viewer",0
ctxPtr    defw 0
svcPtr    defw 0
savedSp   defw 0

; sem patří patch_services a call_* wrappery

plugin_end
        assert plugin_end - plugin_start <= VIEW_PLUGIN_SIZE
        SAVEBIN "plugin/myviewer.ccp", VIEW_PLUGIN_ADDRESS, VIEW_PLUGIN_SIZE

Ovládání vstupu

Hodnoty z SERVICE_INPUT_NOWAIT závisí na typu vieweru. Společné pravidlo je, že 0 znamená žádný vstup a 1 ukončení vieweru. U pluginů, které umí procházet soubory, se používá také 2 jako požadavek na další soubor. Textový viewer navíc reaguje na hodnoty pro posun po řádcích a stránkách.

U obrázkových a hudebních viewerů je dobrý vzor HelloWord.asm: po startu počkat na puštění původní klávesy, krátce "armed delay" a teprve potom přijímat ovládání. Bez toho se může stát, že plugin okamžitě zareaguje na klávesu, kterou byl spuštěn.

Přidání pluginu do menu

Samotný .ccp soubor nestačí, pokud má být plugin dostupný z výběrového menu Calm Commanderu. Loader má tabulku v functions/viewer.asm, kde jsou přiřazené typy, názvy souborů a popisky menu.

viewPluginMenuTable
        defb VIEWTYPE_TEXT     : defw viewTextPluginName     : defw viewPluginMenuTextTxt
        defb VIEWTYPE_ZXSCREEN : defw viewZxScreenPluginName : defw viewPluginMenuZxTxt
        defb VIEWTYPE_NXI      : defw viewNxiPluginName      : defw viewPluginMenuNxiTxt
        ; další položky...

K novému typu tedy patří tři změny: přidat VIEWTYPE_* konstantu, přidat jméno souboru pluginu ukončené hodnotou 255 a přidat text do menu ukončený nulou. Pokud má Calm Commander plugin vybírat automaticky, je potřeba ještě doplnit rozpoznání souboru ve výběrové logice.

Sestavení

Pluginy v repozitáři se skládají přes sjasmplus. Vzor je stejný jako u existujících souborů v adresáři plugin: zdroj includuje plugin_api.i.asm, na konci má assert velikosti a SAVEBIN vytvoří výsledný .ccp.

sjasmplus plugin/myviewer.asm

Po sestavení musí být výsledný soubor v NextZXOS dostupný v c:/CalmCommander/plugin. V repozitáři se generuje do plugin/*.ccp, odkud ho lze zkopírovat do instalace Calm Commanderu.

Praktické poznámky

  • Držte se limitu VIEW_PLUGIN_SIZE. Pokud plugin přeroste 4 KB, assert musí spadnout.
  • Uložte a obnovte SP, pokud použijete vlastní stack. Ukázkové pluginy často dávají stack pod konec plugin prostoru.
  • Pro tisk a okna používejte service table, ne pevné adresy z hlavního programu.
  • Pro čtení dat používejte context a seznam MMU stránek. Nespoléhejte na to, že celý soubor leží souvisle v paměti.
  • Pokud plugin mění zobrazení, musí po sobě uklidit tolik, aby se Calm Commander mohl vrátit do svého UI. Loader po návratu obnovuje panely, ale plugin by neměl nechávat hardware v náhodném stavu.
  • Jako referenci berte hlavně HelloWord.asm pro ABI, text.asm pro práci s více stránkami dat a zxscreen.asm/nxi.asm pro grafické viewery.

Shrnutí

Viewer plugin pro Calm Commander je malý modul na $C000, který dostane v HL context, v DE service table a data souboru dostupná přes $E000 a seznam MMU stránek. Základní workflow je: includovat plugin_api.i.asm, uložit context, opatchovat služby, zpracovat data, reagovat na vstup a vrátit v A výsledek.

Nejlepší start pro nový viewer je zkopírovat strukturu z HelloWord.asm, přejmenovat výstupní .ccp, doplnit vlastní čtení dat a až potom plugin přidat do tabulky v functions/viewer.asm. Tím zůstane hlavní Calm Commander malý a specializované formáty mohou žít jako samostatné rozšíření.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *