diff --git a/.gitignore b/.gitignore index ba80306d..f3fe1c97 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ claudeDocs/ toolchains/ dist/ NOTES.md +__pycache__/ +*.py[cod] diff --git a/config/ASSOC.INI b/config/ASSOC.INI index 00386929..3e7a4b31 100644 --- a/config/ASSOC.INI +++ b/config/ASSOC.INI @@ -22,7 +22,7 @@ FRM = apps/kpunch/dvxbasic/dvxbasic.app CLS = apps/kpunch/dvxbasic/dvxbasic.app CTL = apps/kpunch/dvxbasic/dvxbasic.app HLP = apps/kpunch/dvxhelp/dvxhelp.app -DHS = apps/kpunch/dvxhelp/helpedit.app +DHS = apps/kpunch/helpedit/helpedit.app DPK = apps/kpunch/software/software.app MOD = apps/kpunch/player/player.app S3M = apps/kpunch/player/player.app diff --git a/config/README.md b/config/README.md index 9a34d669..794b58aa 100644 --- a/config/README.md +++ b/config/README.md @@ -2,8 +2,8 @@ The settings files DVX ships and the files it shares with every program. The build copies them into `bin/` (text files converted to DOS line -endings, CR+LF, with sed): the settings into `bin/system/`, the themes and -wallpapers into `bin/shared/`. At its first start DVX copies `DVX.INI` to +endings, CR+LF, with sed): the settings into `bin/system/`, the themes, +wallpapers, and instrument banks into `bin/shared/`. At its first start DVX copies `DVX.INI` to the user folder (`USER`, or the folder `DVXUSER` names), where every change is saved; the shipped copy is never written. File types work the same way, except that the user's `ASSOC.INI` holds only their changes to @@ -24,6 +24,10 @@ the shipped list. | `wpaper/blueglow.jpg` | `bin/shared/wpaper/blueglow.jpg` | Wallpaper image | | `wpaper/swoop.jpg` | `bin/shared/wpaper/swoop.jpg` | Wallpaper image | | `wpaper/triangle.jpg` | `bin/shared/wpaper/triangle.jpg` | Wallpaper image | +| `banks/DMXOPL.OP2` | `bin/shared/banks/DMXOPL.OP2` | DMXOPL 1.11, a General MIDI instrument bank for FM music by ConSiGno (MIT): https://github.com/sneakernets/DMXOPL, branch master, commit 63bd4821c6adaff29074f1f6486261961c2014b4 (2017-08-22, "V1.11 Release"), GENMIDI.op2 unchanged (SHA-256 d3feb807e5102d7de3075c2351cced7e853ba2da8708eff98cc1c8ef8f240ce2) | +| `banks/DMXOPL.TXT` | `bin/shared/banks/DMXOPL.TXT` | Its licence and credits | +| `banks/DMXOPL3.OP2` | `bin/shared/banks/DMXOPL3.OP2` | DMXOPL3, its OPL3 successor, by Sneakernets (MIT): the same repository, branch DMXOPL3, commit e83c144a2e6d3c283e20f9d16d011126931892f5 (2018-07-13, after the v2.11d release), GENMIDI.op2 unchanged (SHA-256 92fc03c7734c45b1ece2bfad1f79d1abbd2b27ec6b45fbfb1c5a74f5ce0138c9); DVX plays its OPL3-only waveforms as the OPL2's four, as for every bank | +| `banks/DMXOPL3.TXT` | `bin/shared/banks/DMXOPL3.TXT` | Its licence and credits ("DMXOPL3 by Sneakernets", as the author asks) | | `keys/*.KEY` | `bin/system/KEYS/*.KEY` | DVX's repository key: packages it signs (DVX's own, and everything the package server publishes) are trusted as DVX's | The desktop sounds (`bin/shared/sounds/`) are made by mksounds and the diff --git a/src/addons/banks/dmxopl/SHARED/BANKS/KPUNCH/DMXOPL.OP2 b/config/banks/DMXOPL.OP2 similarity index 100% rename from src/addons/banks/dmxopl/SHARED/BANKS/KPUNCH/DMXOPL.OP2 rename to config/banks/DMXOPL.OP2 diff --git a/src/addons/banks/dmxopl/SHARED/BANKS/KPUNCH/DMXOPL.TXT b/config/banks/DMXOPL.TXT similarity index 100% rename from src/addons/banks/dmxopl/SHARED/BANKS/KPUNCH/DMXOPL.TXT rename to config/banks/DMXOPL.TXT diff --git a/src/addons/banks/dmxopl3/SHARED/BANKS/KPUNCH/DMXOPL3.OP2 b/config/banks/DMXOPL3.OP2 similarity index 100% rename from src/addons/banks/dmxopl3/SHARED/BANKS/KPUNCH/DMXOPL3.OP2 rename to config/banks/DMXOPL3.OP2 diff --git a/src/addons/banks/dmxopl3/SHARED/BANKS/KPUNCH/DMXOPL3.TXT b/config/banks/DMXOPL3.TXT similarity index 100% rename from src/addons/banks/dmxopl3/SHARED/BANKS/KPUNCH/DMXOPL3.TXT rename to config/banks/DMXOPL3.TXT diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html index 1a99ebb9..992a7e70 100644 --- a/docs/dvx_basic_reference.html +++ b/docs/dvx_basic_reference.html @@ -1258,6 +1258,7 @@ img { max-width: 100%; }
  • SYS_VIDEO_DEFAULT_WIDTH
  • SysFontGlyphRow
  • SysGetColor
  • +
  • SysLibraryLoaded
  • SysLoadTheme
  • SysMemoryFreeKb
  • SysMemoryTotalKb
  • @@ -4653,6 +4654,7 @@ fruit.Remove 1 ' removes Banana, now the first item

    SQL Functions

    DVX BASIC includes SQLite database support through the sql.bas include library. Add sql.bas to your project to gain access to these functions. All functions use database handles and result-set handles (Long integers) returned by SQLOpen and SQLQuery. Numeric parameters and results are Long, so row counts and integer column values keep their full 32-bit range.

    +

    SQLite is in the Databases part of DVX, which can be left out when DVX is installed. Without it the SQL functions do nothing: SQLOpen returns 0, and SQLError$ says that Databases are not installed. SysLibraryLoaded("dvxsql") (system.bas) tells beforehand.

    Opening and Closing Databases

    SQLOpen

    Opens a SQLite database file and returns a database handle (> 0) or 0 on failure. The file is created if it does not exist. A file that cannot be changed (for example one marked read-only) still opens, but only for reading. When SQLOpen returns 0, SQLError$(0) tells you why.

    @@ -4775,7 +4777,7 @@ If Clipboard.GetFormat(vbCFText) Then Text2.Text = Clipboard.GetText()

    sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, recording from a microphone or line input (RecordStart, RecordStop, RecordLevel, ...), the FM synthesizer chip, a wavetable card's voices and memory (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, WtAlloc, WtStart, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...) music.bas Music: play MOD, S3M, XM, IT, MIDI, WAV, and VOC files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); instruments from banks and SoundFonts, a sound card's own (MusicSetBank, MusicPatchSet$, MusicRomBank$); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$); what a MIDI keyboard plays, heard as it comes and recorded into MIDI files (MidiInOpen, MidiInMessage, MusicMidiSend, MidiRecordNew, MidiRecordSave, ...) fileops.bas Files and folders: folder listings for a file list, paths and names, the file dialogs' filter rule, attributes and the time a file was changed, sizes, free space and disk sizes, drive kinds, whether a folder can be written, the folder for scratch files, making a folder path, and copying, moving, and deleting whole folders at once or as a job with progress, Cancel, and questions (FileOpsList, FileOpsListRows$, FileOpsFilterMatch, FileOpsSetModified, FileOpsDriveType, FileOpsIsWritable, FileOpsMakePath, FileOpsJobCreate, FileOpsJobRunFor, ...) - system.bas What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper, text smoothing, and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl, and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, and the memory (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetTextSmoothing, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, ...) + system.bas What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper, text smoothing, and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl, and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, the memory, and whether a library is installed (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetTextSmoothing, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, SysLibraryLoaded, ...) font.bas Fonts: the font families DVX has, the sizes each comes in, whether one is fixed-pitch, and how wide text and how tall a line is in a font (FontFamilyCount, FontFamilyName$, FontFamilyFixed, FontSizeCount, FontSizeAt, FontNearestSize, FontTextWidth, FontLineHeight, FontAscent, FontRescan) richtext.bas RTF without a control: an RTF document as plain text and plain text as RTF (RtfToText$, TextToRtf$); a RichEdit control reads and writes RTF itself (TextRTF, SelRTF, LoadFile, SaveFile) layout.bas Control sizes in characters or in percent of the container instead of pixels (SizeChars, SizePercent) @@ -5387,7 +5389,7 @@ End Sub End Sub

    Add-on Packages

    With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed.

    -

    What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it. PkgFileOwner$(s, fileName) is the other way: the id of the package that put a file there (its name from the DVX folder, such as "SHARED\BANKS\KPUNCH\DMXOPL.OP2"; "" when none did).

    +

    What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it. PkgFileOwner$(s, fileName) is the other way: the id of the package that put a file there (its name from the DVX folder, such as "SHARED\FONTS\KPUNCH\ARIMO.DFN"; "" when none did).

    Installing. PkgOfferFile(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0, and returns the number of the one at path (-1 when it cannot be read). PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts the job, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is.

    offer = PkgOfferFile(s, path)
     If offer >= 0 And PkgPlan(s, CStr(offer)) Then
    @@ -5446,6 +5448,12 @@ body = "{" & JsonQuote$("name") & ": " &am
     

    Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower), and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys, and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS, and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back).

    The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse pointer is on the screen, and SysMouseButtons() the buttons held (vbLeftButton, vbRightButton, and vbMiddleButton bits) -- also while the pointer is over another program's window, where your MouseMove events do not see it. SysSetMousePos x, y moves the pointer (it is kept on the screen). SysShiftState() is the Shift, Ctrl, and Alt keys held now (vbShiftMask, vbCtrlMask, and vbAltMask bits). SysSendKey(keyCode, shift) types a key into the window that has the focus as if the user pressed it, like SendKeys in other BASICs: keyCode as a KeyDown event gets it -- a character code such as ASC("a") or 13 for Enter, or a vbKey constant such as vbKeyF1 -- and shift the vbShiftMask, vbCtrlMask, and vbAltMask bits held with it (SysSendKey ASC("s"), vbCtrlMask is Ctrl+S). The keys arrive in order, one at a time, once your code has returned to DVX; up to 64 can wait, and SysSendKey returns False when that many are waiting. A program cannot press the mouse buttons: to "click" a control, call its event procedure or set its value.

    Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told).

    +

    Libraries: some parts of DVX are optional (Setup's "What to install" page; Software lists each as one package), and so are their libraries: Networking has dvxnet, http, and pkgnet (net.bas, http.bas, pkgnet.bas), Databases has dvxsql (sql.bas), and Communications has conn and xfer (conn.bas, xfer.bas). SysLibraryLoaded(libName) is True when the library a DECLARE LIBRARY line names (any case) is installed and was started with DVX. A program that can do without such a library asks first and leaves out what needs it: calling a function of a library that is not there stops the program with "External function not found". Software, for one, keeps its package server pages off without pkgnet.

    +
    If SysLibraryLoaded("pkgnet") Then
    +    handle = PkgNetOpen("")
    +Else
    +    lblStatus.Caption = "The package server needs DVX's Networking part."
    +End If

    System font: SysFontWidth() and SysFontHeight() are the size of every character of the system font in pixels, and SysFontFirstChar() to SysFontFirstChar() + SysFontCharCount() - 1 the character codes it has. SysFontGlyphRow(code, row) is one row of a character's picture: a bit per pixel, &H80 the leftmost -- for drawing text dot by dot, enlarged or in a font editor of your own.

    Saving: SysColorsStore, SysWallpaperStore, SysScreenSaverStore, SysFontsStore, and SysMouseStore write the colors, the wallpaper, the screen saver, text smoothing, and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad, SysScreenSaverLoad, SysFontsLoad, and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.

    Dim snap As Long
    @@ -5511,7 +5519,7 @@ If FontAddFile(App.Path + "\TITLE.DFN") Then
         lblTitle.FontSize = 36
         lblTitle.FontBold = True
     End If
    -

    Every control and every form can have a font of its own, set with the properties FontName, FontSize (in pixels), FontBold, FontItalic, FontUnderline, and FontStrikethru, as in Visual Basic; they need no include file. A control that is given no font uses the font of the control it is in (a Frame, for example), else its form's, so setting a form's font changes every control on it that has none. Reading one of the properties gives the font the object draws with: a FontSize the family does not come in reads back as the nearest size it has, and a FontName DVX does not have reads back as "System". Setting one changes only that part of the font. Controls that edit text in columns (TextBox, TextArea, ComboBox, SpinButton, and Terminal) take only fixed-pitch families and show the System font in place of a proportional one. Print, TextWidth, TextHeight, PrintAccel, and TextWidthAccel on forms and picture boxes use the object's font. In the form designer, the Font row of the Properties window sets the six properties at once, and they are saved in the .frm file; DlgFont (see Font Dialog) lets the user choose a font while the program runs.

    +

    Every control and every form can have a font of its own, set with the properties FontName, FontSize (in pixels), FontBold, FontItalic, FontUnderline, and FontStrikethru, as in Visual Basic; they need no include file. A control that is given no font uses the font of the control it is in (a Frame, for example), else its form's, so setting a form's font changes every control on it that has none. Reading one of the properties gives the font the object draws with: a FontSize the family does not come in reads back as the nearest size it has, and a FontName DVX does not have reads back as "System". Setting one changes only that part of the font, keeping the size the font has: where a family's styles come in different sizes (a font the program carries in bold only, say), set FontBold and FontItalic before FontSize, so the size is picked from the style's own sizes. Controls that edit text in columns (TextBox, TextArea, ComboBox, SpinButton, and Terminal) take only fixed-pitch families and show the System font in place of a proportional one. Print, TextWidth, TextHeight, PrintAccel, and TextWidthAccel on forms and picture boxes use the object's font. In the form designer, the Font row of the Properties window sets the six properties at once, and they are saved in the .frm file; DlgFont (see Font Dialog) lets the user choose a font while the program runs.

    Sub Form1_Load ()
         Me.FontName = "DejaVu Sans"        ' every control without a font of its own
         Me.FontSize = 14
    @@ -5615,7 +5623,7 @@ Next i

    MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.

    MusicLoadMacro(macro) makes a music from a music macro string, the kind PLAY takes, with its notes on the General MIDI instruments its I commands name (the piano before any I): the tune can then be played in the background, paused, stopped, and made quieter like any music. MIDI music plays on the instruments chosen in Control Panel; MusicSetBank music, bank plays one music on another instrument bank, named as in the banks folders ("" is DVX's standard bank), before the music first plays (MusicError$ says when the bank is missing or damaged). A SoundFont (.SF2 or .SBK) gives the recorded instruments for a wavetable card or the built-in SoundFont synthesizer, as can a Gravis UltraSound's own patches: MusicPatchSet$() names them (their ULTRASND.INI, "" when there are none), and MusicRomBank$() names the instruments built into a Sound Blaster AWE card ("" when Creative's software is not installed).

    tune = MusicLoadMacro("T150 I41 O4 L8 CEG>C4")
    -MusicSetBank tune, "KPUNCH\DMXOPL.OP2"
    +MusicSetBank tune, "DMXOPL.OP2"
     MusicPlay tune, False
    Dim Shared tune As Long
     
    diff --git a/docs/dvx_help_viewer.html b/docs/dvx_help_viewer.html
    index 9e713119..a8da4cd0 100644
    --- a/docs/dvx_help_viewer.html
    +++ b/docs/dvx_help_viewer.html
    @@ -156,7 +156,7 @@ This is a warning message.
     
     

    Help Compiler (dvxhlpc)

    -

    The dvxhlpc tool runs on the host (Linux) and compiles .dhs source files into binary .hlp files for the viewer, and optionally into self-contained HTML.

    +

    The dvxhlpc tool runs on the host (Linux) and compiles .dhs source files into binary .hlp files for the viewer, and optionally into self-contained HTML. On DVX it is DVXHLPC.EXE in the SYSTEM folder, for the DOS prompt, which comes with the Help Editor in Developer Tools (the Help Editor compiles help itself, and DVX compiles the help of its libraries and widgets when it starts).

    Usage

    dvxhlpc -o output.hlp [-i imagedir] [--html out.html] [--quiet] input.dhs [@filelist] [...]

    Options

    diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html index 2f927e77..dc2625a2 100644 --- a/docs/dvx_system_reference.html +++ b/docs/dvx_system_reference.html @@ -245,6 +245,7 @@ img { max-width: 100%; }
  • Debugging: the Log and Crash Reports
  • Binary Interface Rules
  • Packaging and Installing
  • +
  • Making Fonts
  • @@ -447,6 +448,7 @@ img { max-width: 100%; }
  • Boot Sequence
  • BSD sockets
  • Build
  • +
  • Bundles
  • Button
  • Buttons
  • Canvas
  • @@ -725,6 +727,7 @@ img { max-width: 100%; }
  • dvxIntInputBox
  • dvxInvalidateRect
  • dvxInvalidateWindow
  • +
  • dvxLibraryLoaded
  • dvxLoadImage
  • dvxLoadImageAlpha
  • dvxLoadImageFromMemory
  • @@ -872,13 +875,16 @@ img { max-width: 100%; }
  • Folders
  • Folders
  • Font
  • +
  • Font add-on
  • Font families
  • Font files
  • Font licences
  • Font properties (C)
  • +
  • Font sizes
  • Font smoothing
  • Font smoothing
  • Fonts
  • +
  • fonts.ini
  • Forward
  • Frame
  • Frame Pacing
  • @@ -1066,6 +1072,7 @@ img { max-width: 100%; }
  • lwIP
  • make package
  • Makefile
  • +
  • Making Fonts
  • Master-Detail
  • Maximize
  • Memory
  • @@ -1091,6 +1098,7 @@ img { max-width: 100%; }
  • Minimized Windows
  • mkcd.sh
  • mkfont
  • +
  • mkfont
  • MOD
  • Modem
  • Modules
  • @@ -1163,6 +1171,7 @@ img { max-width: 100%; }
  • Open Dialog
  • OPL
  • Option Buttons
  • +
  • Optional parts of DVX
  • Out of Memory
  • Package Builder
  • Package server
  • @@ -1313,13 +1322,14 @@ img { max-width: 100%; }
  • prefsWheelDirChoices
  • Print
  • print
  • +
  • Print sizes
  • +
  • Print sizes
  • printDialog
  • printDocOpen
  • printDocPage
  • printDocPicture
  • PrintDocT
  • Printer
  • -
  • Printer Fonts
  • printFontOpen
  • PrintFontT
  • Printing
  • @@ -1639,6 +1649,7 @@ img { max-width: 100%; }
  • SysFontsStore
  • SysGetColor
  • SysIdleSeconds
  • +
  • SysLibraryLoaded
  • SysLoadTheme
  • SysMemoryFreeKb
  • SysMemoryTotalKb
  • @@ -2419,7 +2430,7 @@ img { max-width: 100%; }

    File > Page Setup... chooses how text is laid out on paper: the margins, how many lines fit on a page, how wide a line is, and a header and footer printed on every page (for example the file name and the page number). Your choices are kept for next time and used by every program that prints text.

    Pictures

    Pictures print as large as fits on the page: in color on a color printer (one with a color ribbon or cartridge), and in shades of grey on the others. Printing pictures needs your printer's model to be set (see Changing Settings); with the setting for text only, a program tells you so.

    -

    Some programs draw whole pages of their own, such as reports with charts. On a PostScript printer the printer draws those pages itself, as sharp as it can print. On other printers their text prints crisper with the Printer Fonts add-on installed (see Adding and Removing Software).

    +

    Some programs draw whole pages of their own, such as reports with charts. On a PostScript printer the printer draws those pages itself, as sharp as it can print. On other printers their text is made from DVX's fonts: DejaVu Sans, Sans Mono, and Serif have sizes made for printing, so they print crisp from 6 to 24 points; other fonts, and larger text, are enlarged and smoothed, a little softer.

    Running Programs

    @@ -2587,9 +2598,11 @@ img { max-width: 100%; }

    An empty floppy or CD-ROM drive

    When a program tries to read a drive that has no disk in it, or to write to a write-protected disk, the program simply reports that it could not read or write the file. DVX never stops at the DOS "Abort, Retry, Fail" question. Put the disk in and try again.

    Installing, upgrading, and removing DVX

    -

    DVX is installed with SETUP, from the DVX CD or from Disk 1 of the DVX floppy disks: change to that drive and type SETUP. After the licence, Setup asks what you would like to do: Install DVX, Upgrade or repair DVX, or Remove DVX. Click the picture or the button of your choice. To install, choose the folder for DVX (type it, or click Browse... to pick it; a folder that does not exist yet is made) and which fonts to install. Setup checks that there is room and unpacks DVX there, asking for each floppy disk it needs in turn. It can also make DVX start when the computer starts (it shows the line it adds to AUTOEXEC.BAT and keeps a copy of the old file as AUTOEXEC.DVX). Setup never writes to the CD or the floppy disks.

    -

    The fonts are up to you. None keeps DVX to its own font; the base set adds DejaVu Sans, Sans Mono, and Serif in all their styles (about 2 MB); and All adds every font that comes with DVX (about 8 MB). A program that asks for a font you did not install shows its text in DVX's own font instead, unless it carries that font itself (some programs do, such as Welcome and the DVX Demo). Every font file holds its own licence: the Licence button of the Font dialog shows it. With None or the base set, Setup does not ask for the last floppy disks, which hold only fonts.

    -

    To upgrade or repair DVX, run Setup again and choose Upgrade or repair DVX, with the folder DVX is in. (When DVX is in the usual folder, such as C:\DVX, Setup says so and suggests this choice.) Your settings and saved files (the USER folder) are kept, unless you tick Start over to remove them. Setup starts with the fonts that folder already has; choose others to add fonts or remove them. If you choose Install DVX for a folder that already has DVX, Setup offers to upgrade it instead.

    +

    DVX is installed with SETUP, from the DVX CD or from Disk 1 of the DVX floppy disks: change to that drive and type SETUP. After the licence, Setup asks what you would like to do: Install DVX, Upgrade or repair DVX, or Remove DVX. Click the picture or the button of your choice. To install, choose the folder for DVX (type it, or click Browse... to pick it; a folder that does not exist yet is made), then Next: what to install, that is the fonts, the parts of DVX, and the add-ons you want. Setup checks that there is room and unpacks DVX there, asking for each floppy disk it needs in turn. It can also make DVX start when the computer starts (it shows the line it adds to AUTOEXEC.BAT and keeps a copy of the old file as AUTOEXEC.DVX). Setup never writes to the CD or the floppy disks.

    +

    The fonts are up to you. None keeps DVX to its own font; the base set adds DejaVu Sans, Sans Mono, and Serif in all their styles, in sizes for the screen and for printing (about 8 MB). A program that asks for a font you did not install shows its text in DVX's own font instead, unless it carries that font itself (some programs do, such as Welcome and the DVX Demo). Every font file holds its own licence: the Licence button of the Font dialog shows it. With None, Setup does not ask for the last floppy disks, which hold only fonts; you can add the base set later from the package server in Software (as DVX Fonts), or by running Setup again. Software lists DVX and DVX Fonts among the installed software, and offers updates to them as they are published.

    +

    DVX comes in parts. The desktop and the everyday programs (Program Manager, File Manager, Notepad, Clock, the Image Viewer, the Help Viewer, Control Panel, and Software) are always installed; the other parts each have a box to tick: DVX BASIC (for writing your own programs, with BASCOMP to build them at the DOS prompt), Databases (for BASIC programs that keep their data in a database), Networking (network cards, the web, and the package server), Communications (the Terminal; it needs Networking), Media (the Media Player and the Sound Recorder), Developer Tools (the Icon Editor, the Resource Editor, the Help Editor, the Package Builder, and the Widget Showcase, with DVXRES and DVXHLPC for the DOS prompt), and Demos (the DVX Demo and the BASIC Demo). Point at one to see what it holds. A part you leave out takes no disk space and leaves more memory for your programs. To add or remove parts later, run Setup again. Software lists each part as one package, whose details name what it holds; without Networking, Software installs packages from files only.

    +

    The CD also has add-ons, which Setup offers with a box to tick for each: More Fonts (more than forty more typefaces, about 8 MB) and DVX General MIDI (instruments for MIDI music on a Gravis UltraSound or a Sound Blaster AWE32 or AWE64, about 4 MB). They are installed like any other add-on, so Software lists them and can remove them; to add one later, choose Install from File in Software and open it from the ADDONS folder of the CD. The floppy disks have no add-ons: get them from the package server in Software instead.

    +

    To upgrade or repair DVX, run Setup again and choose Upgrade or repair DVX, with the folder DVX is in. (When DVX is in the usual folder, such as C:\DVX, Setup says so and suggests this choice.) Your settings and saved files (the USER folder) are kept, unless you tick Start over to remove them. Setup starts with the fonts, parts, and add-ons that folder already has; choose others to add or remove them. A part you untick is removed, unless other software in the folder needs it (Setup then tells you it keeps it). If you choose Install DVX for a folder that already has DVX, Setup offers to upgrade it instead.

    To remove DVX, run Setup and choose Remove DVX, with the folder DVX is in. The SETUP in the DVX folder (for example C:\DVX\SETUP) can remove DVX too, from its own folder only. Files you added to the folder yourself are not removed, and the USER folder is only removed if you ask.

    Running DVX from a CD-ROM or a write-protected disk

    DVX also runs straight from a disk it cannot write to. It then keeps your settings and files in a DVXUSER folder in your TEMP folder, and tells you so once. To keep them somewhere of your choosing, set DVXUSER to a folder on your hard disk before starting DVX (see Changing Settings). Without a TEMP folder, DVX tells you that changed settings last only until you exit DVX. Either way the help files are used as they are. To keep everything on your hard disk, install DVX there with Setup.

    @@ -3086,7 +3099,7 @@ prefsClose(h);

    Font System

    -

    Every text call takes a BitmapFontT, and there are two kinds of them. The system font is the 8x16 VGA ROM font (code page 437, 256 glyphs) built into libdvx: 8-pixel cells, one byte per glyph row, so character positions are a multiplication, glyph lookup an index, and every scanline a single byte -- the hot paths of drawChar, drawTextN, and drawTermRow depend on it. A loaded font (dvxText.h) is one size of a font family from the FONTS folder (Setup installs none, the base set of DejaVu Sans, Sans Mono, and Serif, or all of them; a family that is not there opens as the system font): per-glyph advances and bitmap boxes, any width, proportional or fixed-pitch, and in up to four styles. The same calls draw and measure both; a loaded font's glyphs field is what sends them to the general paths.

    +

    Every text call takes a BitmapFontT, and there are two kinds of them. The system font is the 8x16 VGA ROM font (code page 437, 256 glyphs) built into libdvx: 8-pixel cells, one byte per glyph row, so character positions are a multiplication, glyph lookup an index, and every scanline a single byte -- the hot paths of drawChar, drawTextN, and drawTermRow depend on it. A loaded font (dvxText.h) is one size of a font family from the FONTS folder (Setup installs the base set of DejaVu Sans, Sans Mono, and Serif or none, and the More Fonts add-on the others; a family that is not there opens as the system font): per-glyph advances and bitmap boxes, any width, proportional or fixed-pitch, and in up to four styles. The same calls draw and measure both; a loaded font's glyphs field is what sends them to the general paths.

    BitmapFontT

      typedef struct {
           int32_t           charWidth;    // cell width (fixed pitch) or the digit 0's advance
    @@ -3138,10 +3151,10 @@ prefsClose(h);

    Rich Text

    Text in several fonts in one control is built on the text flow (dvxFlow.h): runs of text, each in a font and a color, laid out into wrapped, aligned lines as tall as the tallest font on them, with offsets for carets, hit tests, and selections. The RichLabel widget is one flow built by calls. The RichEdit widget keeps its document in the richtext library (styled text: runs of interned styles over code page 437 text, an alignment per paragraph, copy / replace / restyle, and RTF in and out) and lays each paragraph out as a flow of its own, so an edit lays out only the paragraphs it touched and repaints only the rows they hold (its quick repaint); a change of height repaints the widget. Its undo steps are pieces of styled text swapped back in, and Copy keeps a styled copy for a paste of the same text.

    Font Files (.DFN)

    -

    One file holds one family in one style at every size it comes in, made on the build host by mkfont (src/tools/mkfont.c) with FreeType from TrueType, OpenType, Type 1, BDF, and PCF fonts, as listed in src/fonts/fonts.ini. A .DFN is a pure format conversion: the converter never adds or changes a glyph, it only marks the characters a face lacks. Little-endian, every record read whole:

    +

    One file holds one family in one style at every size it comes in -- its screen sizes, their smoothed copies, and its print sizes (mkfont's print key; the SDK guide's Making Fonts) -- made on the build host by mkfont (src/tools/mkfont.c) with FreeType from TrueType, OpenType, Type 1, BDF, and PCF fonts, as listed in src/fonts/fonts.ini. Opening a font reads only the header and the strike table, so sizes nothing uses cost disk space, not memory. A .DFN is a pure format conversion: the converter never adds or changes a glyph, it only marks the characters a face lacks. Little-endian, every record read whole:

      Part                      Size            Holds
       ----                      ----            -----
    -  DfnHeaderT                60 bytes        "DVXF", format version (2), style, face flags (fixed pitch; box characters drawn by DVX), strike count (1 to 32), family name (31 characters), where the notice and licence texts are and their lengths
    +  DfnHeaderT                60 bytes        "DVXF", format version (2), style, face flags (fixed pitch; box characters drawn by DVX), strike count (1 to 64), family name (31 characters), where the notice and licence texts are and their lengths
       DfnStrikeT x count        24 bytes each   per size, smallest first: where its glyph table and bitmaps are, the bitmaps' size, the pixel size, ascent, descent, line height, widest advance
       FontGlyphT x 256          6 bytes each    per size, at its glyph table offset
       glyph bitmaps             as listed       per size, back to back in character order
    @@ -3738,7 +3751,7 @@ DVX_ABI_STAMP(mylib);

    dvxText.h -- Fonts

    -

    The font families DVX draws with: "System", the built-in 8x16 font, and the families in the FONTS folder. Which families that folder holds depends on the install -- Setup puts in none, the base set (DejaVu Sans, Sans Mono, and Serif), or all of them -- and dvxFontOpen of a family that is not there gives the system font. A family has up to four faces (regular, bold, italic, bold italic), each a .DFN file holding the face at the pixel sizes it was made at; the files are made on the build host from TrueType, Type 1, and bitmap fonts (src/tools/mkfont.c), so DVX only copies bitmaps. More files of the same family and style (up to four in all) add the sizes the face lacks, each size read from the file it came in: the printer-fonts add-on gives the DejaVu faces their large sizes that way, from FONTS\KPUNCH.

    +

    The font families DVX draws with: "System", the built-in 8x16 font, and the families in the FONTS folder. Which families that folder holds depends on the install -- Setup puts in the base set (DejaVu Sans, Sans Mono, and Serif) or none, and the More Fonts add-on the rest -- and dvxFontOpen of a family that is not there gives the system font. A family has up to four faces (regular, bold, italic, bold italic), each a .DFN file holding the face at the pixel sizes it was made at; the files are made on the build host from TrueType, Type 1, and bitmap fonts (src/tools/mkfont.c), so DVX only copies bitmaps. A face's file holds its screen sizes, their smoothed copies, and its print sizes together (the base set's have 25 to 100 pixels for printing; FONT_SIZES_MAX sizes at most). More files of the same family and style (up to four in all) add the sizes the face lacks, each size read from the file it came in: a program's own copy of a face adds its sizes that way.

    dvxFontOpen gives a BitmapFontT every text call draws and measures with. It never scales: it picks the face's size nearest the one asked for. A style the family has no face for is made from the nearest face -- bold by thickening each stroke a column (two from 24 pixels), italic by slanting a column per four rows -- and the font's flags say so. Every character of code page 437 draws: what a face lacks is filled in when the font loads. Box drawing, block, and shade characters are drawn to fill the character cell (the pitch, or the digit 0's width) so lines join, also in place of a face's own when its file asks; other characters come from FONT_FALLBACK_FAMILY (FONT_FALLBACK_FIXED for a fixed-pitch face, centered in its pitch) at the nearest size and the same style, or from the system font when that family is not installed.

    A face can also hold a size smoothed (anti-aliased): each pixel one of four levels, from none to full, drawn as that blend of the text color and what is under it. When smoothing is on (dvxFontSetSmooth) such a size opens smoothed, with FONT_FLAG_GRAY set and FONT_GRAY_BPP bits per pixel; other sizes and faces stay 1-bit. At 8 bits per pixel there are no colors to blend: the pixels of FONT_GRAY_HALF and more are drawn, the rest not.

    Fonts are shared and counted: the same family, size, and style opened again is the same font, and dvxFontClose gives one up. A few fonts no one holds stay loaded so reopening them is quick. A font's memory belongs to DVX, not to the program that opened it. The family list is read from DVX_FONTS_DIR the first time it is needed; a file that cannot be read is logged and skipped.

    @@ -3752,7 +3765,8 @@ DVX_ABI_STAMP(mylib);
    DVX_FONTS_DIR "FONTS" The fonts folder (dvxPlat.h), in SHARED and in the user folder FONT_SYSTEM_FAMILY "System" The built-in 8x16 font's family name FONT_FALLBACK_FAMILY "DejaVu Sans" Fills what a proportional face lacks - FONT_FALLBACK_FIXED "DejaVu Sans Mono" Fills what a fixed-pitch face lacks + FONT_FALLBACK_FIXED "DejaVu Sans Mono" Fills what a fixed-pitch face lacks + FONT_SIZES_MAX 64 The most sizes a face has (dvxFontSizes)

    dvxFontOpen

    BitmapFontT *dvxFontOpen(const char *family, int32_t pixelSize, uint32_t style);

    The font of family at the size nearest pixelSize in style. NULL, "", and a family there is none of give the system font (16 pixels; plain, it is dvxFont8x16 itself, drawn by the fast fixed paths).

    @@ -5193,6 +5207,9 @@ int32_t platformDosRun(const char *command, const char *folder, bool pause, cons bool platformPathAbsolute(const char *path, char *out, int32_t outSize);

    dvxRunDos runs a DOS program (an .EXE, .COM, or .BAT file, or a command DOS knows) with DVX stepped aside, and comes back. Full screen, menus, and drags end; program is made absolute (a bare name stays one, for the PATH) and given to DOS by its short name, with args after it, in folder (NULL or "": the program's own folder). platformDosRun then runs the suspend hooks (newest first), lets the keyboard hook and the timer service go (the timers are kept and stand still), sets text mode, gives the INT 24h handler and Ctrl+Break back to DOS, makes the folder current, and runs the command line with system() (as at the DOS prompt: a batch file through the command interpreter, PATH, redirection); with pause it prints "Press any key to return to DVX." and waits for a key. Everything is taken back in the opposite order, the desktop's mode (and palette) set again, and the resume hooks run (oldest first); dvxRunDos sets the mouse up again where it was, forgets held keys, and repaints the whole screen. DVX logs "DOS: running" and "DOS: back from" with the command line. Nothing of DVX runs meanwhile: close the other programs first (the shell's shellRunDos asks them). Returns the program's exit code, or -1 when it could not be run (errno: ENOENT, ENOMEM, E2BIG for a command line longer than DOS's 126 characters). DVX stays in memory: the program gets the conventional memory DVX's DOS part and the DPMI host leave. On the host, platformDosRun is system() in the folder with the hooks around it.

    platformPathAbsolute makes a path absolute against the current folder (it need not exist).

    +

    Optional Libraries: dvxLibraryLoaded

    +
    bool dvxLibraryLoaded(const char *name);
    +

    Whether the library (or widget) name, its file's base name in any case ("pkgnet", "dvxsql"), is loaded: installed and started with DVX, which loads every library and widget it has at start. It looks for the module's ABI stamp (DVX_ABI_STAMP: name and DVX_ABI_SUFFIX), which the loader checked before it took the module; a module that is not there, or did not load, has none. Some parts of DVX are optional (Setup's "What to install" page installs them as bundles: src/packages.ini), so a program that uses one of their libraries only when it is there asks first, and does not import it: an import of a missing library stops the program from loading. BASIC: SysLibraryLoaded (system.bas).

    Idle Time: dvxIdleMs

    uint32_t dvxIdleMs(const AppContextT *ctx);

    The milliseconds since the last key or mouse input: a key (pressed, released, or typed), the mouse moving, a button, or the wheel, injected input (a test script, SysSendKey) included, and input that came while DVX was busy and was thrown away. DVX coming back from a DOS program or from full-screen mode counts as input too. The shell's screen saver starts after DVX.INI [desktop] saverWait minutes of it (shellSaver.h); BASIC: SysIdleSeconds. The time is kept in AppContextT.lastInputTime (platformClock ticks).

    @@ -8745,7 +8762,8 @@ int32_t SysShiftState(void); int32_t SysSendKey(int32_t keyCode, int32_t shift); int32_t SysScreenshot(const char *path); int32_t SysMemoryTotalKb(void); -int32_t SysMemoryFreeKb(void); +int32_t SysMemoryFreeKb(void); +int32_t SysLibraryLoaded(const char *name);
      Function                     Calls
       --------                     -----
       SysVideoMode*                dvxGetVideoModes (0 out of range)
    @@ -8778,7 +8796,8 @@ int32_t     SysMemoryFreeKb(void);
    SysShiftState ctx->keyModifiers & KEY_MOD_ALL (KEY_MOD_SHIFT / CTRL / ALT = vbShiftMask / vbCtrlMask / vbAltMask) SysSendKey queues (up to 64) keyCode + shift as the BIOS stores the keystroke (platformBiosScanCode for combinations with codes of their own, KEY_EXT_FLAG keys by scan code, Ctrl+letter as its control character); a shell idle handler (shellRegisterIdle) injects one per idle frame with platformInjectKey, so each is read with its own modifiers, and releases them (platformInjectModifiers(0)) after the last SysScreenshot dvxScreenshot (BMP for .bmp, else PNG) - SysMemoryTotalKb / FreeKb platformGetMemoryInfo; -1 when unknown + SysMemoryTotalKb / FreeKb platformGetMemoryInfo; -1 when unknown + SysLibraryLoaded dvxLibraryLoaded (the module's ABI stamp, by its base name in any case)

    There is no binding for platformInjectMouse: on DOS it replaces the real mouse until the next call and has no way back to it, so a synthesized click would leave the pointer stuck. A program moves the pointer with SysSetMousePos and "clicks" a control by calling its event procedure or setting its value.

    system.bas also holds MOUSE_WHEEL_STEP_MIN / MAX / DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS, and MOUSE_SPEED_DEFAULT, and SYS_VIDEO_DEFAULT_WIDTH / HEIGHT / BPP (DVX_DEFAULT_VIDEO_W / H / BPP, the mode prefsGetVideoMode and safe mode fall back to), as CONSTs; the basdev test suite (caseIncludeConsts) checks each against dvxTypes.h.

    The Store / Load routines are the ones the shell uses at start (dvxMouseFromPrefs, dvxColorsFromPrefs, dvxWallpaperFromPrefs on DVX.INI) and Control Panel uses for OK and for its Cancel snapshot (an in-memory prefsCreate handle), so the INI keys are written in one place (dvxApp.c).

    @@ -9542,13 +9561,13 @@ End Sub ---- -------- newProject.sh Creates a project from a template sdk.mk Build rules every project Makefile includes - bin/ dvxres (resources), dvxhlpc (help compiler), bascomp (BASIC compiler), mkfont (fonts a program carries), dxeGen.sh (runs dxe3gen) + bin/ dvxres (resources), dvxhlpc (help compiler), bascomp (BASIC compiler), mkfont (fonts: see Making Fonts), dxeGen.sh (runs dxe3gen) include/ Public C headers, one directory per library, plus include/basic for BASIC declaration files templates/ The project templates samples/basic/ Example BASIC projects

    Requirements

    Creating a Project

    @@ -9602,6 +9621,7 @@ End Sub

    Debugging: the Log and Crash Reports

    Binary Interface Rules

    Packaging and Installing

    +

    Making Fonts

    Writing Games for DVX

    @@ -9623,7 +9643,7 @@ End Sub

    DVX calls appMain(DxeAppContextT *ctx) once. The template is a callback-only program (hasMainLoop = false): appMain creates a window with a File and Help menu, a label, and a button, and returns 0. From then on DVX calls the window's onClose and onMenu callbacks and the button's onClick. Destroying the program's last window ends it. A program that needs to keep working between events sets hasMainLoop = true, loops in appMain, and calls tsYield() often (see the Clock program's source for an example).

    ctx gives the program its paths: appPath (the .app file, used to read its own resources with dvxResOpen and dvxResRead), appDir, userDir (its folder in the user folder, for its settings and the files it saves: shellEnsureUserDir makes it, shellUserPath names a file there), and helpFile (preset to hello.hlp beside hello.app). The files it ships are in the DATA folder beside it (shellDataPath), which DVX never writes.

    A program writes only in its user folder, and never writes or installs anything into DVX's SHARED folder (DVX's own fonts, sounds, themes, and wallpapers): its own files stay with it, in its DATA folder or as resources in the .app, under its vendor and program folder, so two programs' files never collide.

    -

    A program can carry fonts of its own as font resources, so its text looks the same whatever fonts were installed: add a line such as "title font TITLE.DFN" to hello.res (dvxres checks that the file is a DVX font). DVX adds the program's fonts when it starts and forgets them when it ends; other programs never see them, and where a family is installed too, the program's sizes and styles come first (dvxText.h, Fonts a Program Carries). Make a .DFN with bin/mkfont from a TrueType, Type 1, or bitmap font -- its manifest names the sizes, whether they are smoothed too, and the characters to keep (mkfont.c's header documents every key; one size and only the characters the program shows keep the file small) -- or copy one from DVX's SHARED\FONTS folder. A .DFN carries the notice and licence of the font it was made from.

    +

    A program can carry fonts of its own as font resources, so its text looks the same whatever fonts were installed: add a line such as "title font TITLE.DFN" to hello.res (dvxres checks that the file is a DVX font). DVX adds the program's fonts when it starts and forgets them when it ends; other programs never see them, and where a family is installed too, the program's sizes and styles come first (dvxText.h, Fonts a Program Carries). Make a .DFN with bin/mkfont from a TrueType, Type 1, or bitmap font (see Making Fonts; one size and only the characters the program shows keep the file small), or copy one from DVX's SHARED\FONTS folder. A .DFN carries the notice and licence of the font it was made from.

    Run

      make install DVX_DIR=/path/to/dvx

    This copies hello.app and hello.hlp to APPS/LOCAL/HELLO/ in the DVX directory. Start DVX and run the program from Program Manager (File > Run), or add an icon for it. After a change, rebuild and install again; close the running copy first.

    @@ -9826,7 +9846,8 @@ End Sub ---- --------- ----- Program APPS/<VENDOR>/<NAME>/ NAME.app, NAME.hlp, DATA/ with the DATA files (if any) Widget WIDGETS/<VENDOR>/<NAME>/ NAME.wgt, NAME.DHS, NAME.BHS, NAME.dep (if any) - Library LIBS/<VENDOR>/<NAME>/ NAME.lib, NAME.DHS, NAME.dep (if any) + Library LIBS/<VENDOR>/<NAME>/ NAME.lib, NAME.DHS, NAME.dep (if any) + Any SYSTEM/ A program for the DOS prompt that goes with it (not in SYSTEM/KEYS or SYSTEM/PACKAGES)
    +
    +

    Making Fonts

    +

    DVX never draws an outline: every size it draws or prints is made beforehand by mkfont, from a TrueType, OpenType, Type 1, BDF, or PCF font, into a .DFN file. One file holds one family in one style (regular, bold, italic, or bold italic) at every size it comes in: the sizes for the screen, their smoothed copies, and the sizes for printing, all made from the same source in the same run, so the screen and the paper never show two versions of a font. When DVX starts it reads only each file's header and size list; a size's glyphs load when something draws or prints at that size, so sizes cost disk space, not memory.

    +

    Three Kinds of Size

    +
      Kind       Manifest key   What it is for
    +  ----       ------------   --------------
    +  Screen     sizes          The pixel sizes text is drawn at (8 to 24 for text faces); "native" for a bitmap face, which has its own
    +  Smoothed   smooth         Screen sizes also made anti-aliased (2 bits per pixel), drawn when text is smoothed (screens of more than 256 colors); about twice the room of the 1-bit size, and best from 12 pixels
    +  Print      print          Sizes made 1-bit for printing (25 and up); none of them a screen size, and never smoothed
    +

    A size is in pixels, which on paper are printer dots: a size in points is points x dpi / 72 dots, so 12 points is 50 dots at 300 dpi and 30 at 180. Printing picks the face's smallest size at least as large as the text in dots and scales it down a little; without one it scales the largest up, smoothed but softer. PostScript printers draw with their own faces. The base set's print sizes cover the usual point sizes at both resolutions:

    +
      Pixels   Points at 300 dpi   Points at 180 dpi
    +  ------   -----------------   -----------------
    +  25       6                   10
    +  29       7                   11.6
    +  33       8                   13.2
    +  38       9                   15.2
    +  42       10                  16.8
    +  50       12                  20
    +  58       14                  23.2
    +  67       16                  26.8
    +  75       18                  30
    +  83       20                  33.2
    +  100      24                  40
    +

    A file holds at most 64 strikes: its screen sizes, smoothed sizes, and print sizes together (DejaVu Sans has 12, 8, and 11, so 31). A size is at most 127 pixels. Every size also shows in the Font dialog and can be drawn on the screen, so a program can use the large ones for headings.

    +

    The Manifest

    +

    mkfont works from a manifest, an INI file: [fonts] lists the files to make (8.3 base name = family name), [licences] names each licence and the text files it is made of, and each font has a section of its own. src/fonts/fonts.ini is DVX's; src/tools/mkfont.c's header documents every key.

    +
    [fonts]
    +ACMESANS = Acme Sans
    +ACMESANB = Acme Sans
    +
    +[licences]
    +ACME     = acme/OFL.txt
    +
    +[ACMESANS]
    +style    = regular
    +licence  = ACME
    +source   = acme/AcmeSans-Regular.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
    +hinting  = native
    +
    +[ACMESANB]
    +style    = bold
    +licence  = ACME
    +source   = acme/AcmeSans-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
    +hinting  = native
    +
      Key        Meaning
    +  ---        -------
    +  style      regular, bold, italic, or bolditalic
    +  licence    A key of [licences]: the text every file carries
    +  source     The font file, under the source folder; source1, source2 ... more files for other sizes, each with its own sizes1, smooth1 ...
    +  sizes      Screen sizes, or native
    +  smooth     Sizes (or all) also made smoothed
    +  print      Sizes made for printing
    +  hinting    native (the font's own), auto (FreeType's), or none; italics often look better with auto
    +  graycut    Sizes whose 1-bit glyphs are cut from the smoothed rendering, for a face whose 1-bit hinting draws it too light
    +  boxdraw    true: DVX draws the box, block, and shade characters itself
    +  chars      Only these characters (hex codes and ranges), the rest marked missing
    +  weight     The weight of a variable font to make
    +  encoding   cp437 for a font that numbers its glyphs in code page 437
    +  set        DVX's own fonts only: base or more (where the file ships)
    +

    Names and Licences

    + +

    Building and Checking

    +

    In the SDK, bin/mkfont MANIFEST SOURCEDIR OUTDIR makes every font the manifest lists, and stops with a message naming the font for any mistake: a bad size, a size listed twice, more than 64 strikes, a missing licence text. It prints a line per font with the characters the font lacks (DVX fills those in from DejaVu Sans or the system font when it draws). bin/mkfont -p FONT.DFN SHEET.BMP draws every size of a font into a picture, with the characters it lacks marked in red: look at it before shipping a font.

    +

    In the DVX source tree, src/fonts/fonts.ini lists every font DVX ships, and the build makes them into bin/shared/fonts/kpunch (DVX's vendor folder) from sources src/fonts/installFonts.sh downloads (pinned versions). Each font names its set: base, the DVX Fonts package (kpunch.basefont, src/fonts/PACKAGE.INI), which Setup installs with DVX unless told to install no fonts and which the package server can update; or more, the More Fonts add-on. mkfont stops when a font names no set, and when a font of the base set has no print sizes, so neither a font nor its print sizes can be left out.

    +

    Where Fonts Go

    + +

    Checklist

    + +

    libtasks -- Cooperative Task Switching

    Credit-based cooperative (non-preemptive) multitasking library for DOS protected mode (DJGPP/DPMI). Each task receives (priority + 1) credits per scheduling round. Tasks run round-robin, consuming one credit per turn. When all credits are exhausted, every ready task is refilled. Higher-priority tasks run proportionally more often but never starve lower ones.

    @@ -10317,7 +10431,7 @@ if (rc != MUSIC_OK) { softwave the SoundFont software synthesizer (below); else the software synthesizer

    On the FM chip and the MPU-401 port the song is played by the timer service (500 times a second), so it keeps time however busy the programs are. On the FM chip every note takes a voice from audio.lib's shared pool; when none is free the note plays on the software synthesizer instead, which then runs beside the chip (its stream is always open for that). The software synthesizer runs in the stream's fill function (the shell's idle time), 64 frames between event checks, 150 ms ahead of what is heard; a program that keeps the desktop busy every frame should call audioPump once a frame, as for any stream.

    General MIDI on FM

    -

    Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion, and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank (MUSIC_INI_BANK) names another OP2 file instead: a bank in SHARED\BANKS or the user's BANKS folder by its name (an add-on package's as VENDOR\NAME), or any OP2 file by its path from the DVX folder, for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. Control Panel's Sounds page lists the banks to choose from; DVX's own add-ons offer DMXOPL and DMXOPL3 (MIT, by ConSiGno and Sneakernets) through Software. A bank that is not there falls back to the bundled one (MUSIC_BANK_DEFAULT). DVX.INI's choice is read as each MIDI music starts, so a new choice reaches the next one; a music keeps the bank it started with, and a bank is freed with the last music playing on it.

    +

    Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion, and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank (MUSIC_INI_BANK) names another OP2 file instead: a bank in SHARED\BANKS or the user's BANKS folder by its name (an add-on package's as VENDOR\NAME), or any OP2 file by its path from the DVX folder, for example a bank made with an OPL editor. DVX also ships DMXOPL and DMXOPL3 (MIT, by ConSiGno and Sneakernets) in SHARED\BANKS, made to sound like a Roland Sound Canvas. Control Panel's Sounds page lists the banks to choose from. A bank that is not there falls back to the bundled one (MUSIC_BANK_DEFAULT). DVX.INI's choice is read as each MIDI music starts, so a new choice reaches the next one; a music keeps the bank it started with, and a bank is freed with the last music playing on it.

    What is played: note on / off, velocity, program change, channel volume (CC7), expression (CC11), pan (CC10: left, both, or right on an OPL3 and the software synthesizer), sustain pedal (CC64), pitch bend with the bend range from RPN 0 (default 2 semitones), all notes off (CC123, CC120), reset controllers (CC121), and General MIDI System On (a reset). Channel 10 plays percussion. Loudness is velocity x volume x expression x the music's volume on a 40 log10 curve, added as attenuation to the voice's level. OP2 banks are tuned for DMX, which sounds note n where MIDI has note n + 12, so notes are played an octave up from the bank's offsets. Not played: modulation (CC1), aftertouch, the FM chip's own rhythm mode, four-operator voices.

    The Software Synthesizer

    When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo, and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time, and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.

    @@ -10377,6 +10491,7 @@ musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4");

    libxmp-lite (Extended Module Player Lite), (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr, MIT licence:

    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

    The DVX General MIDI SoundFont add-on (DVXGM.SF2) is made from FluidR3_GM by Frank Wen (MIT licence); its notice is installed beside it (SHARED\BANKS\KPUNCH\DVXGM.TXT).

    +

    The DMXOPL 1.11 instrument bank is by ConSiGno, and DMXOPL3 by Sneakernets (MIT licence); each one's licence and credits are installed beside it (SHARED\BANKS\DMXOPL.TXT and DMXOPL3.TXT).

    The GENMIDI instrument bank is from Freedoom 0.13.0, Copyright (C) 2001-2024 Contributors to the Freedoom project. All rights reserved. BSD 3-clause licence:

    Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the Freedoom project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

    @@ -10458,6 +10573,7 @@ APPS/ACME/CHESS/CHESS.APP = 61234 <SHA-256, 64 hex digits> saver SHARED/SAVERS/VENDOR (a screen saver: a program, so it needs abi) bundle no files: a set of packages, its dependencies system DVX itself (kpunch.dvx): anywhere but the user folder +

    Every kind but bundle may also add files to SYSTEM (a DOS program run at the prompt, say), but not to SYSTEM/KEYS (the keys DVX trusts) or SYSTEM/PACKAGES (the package records, PKG_DB_DIR), which only DVX itself writes. A file there that another package has is refused when the package is installed, as anywhere (pkgPlanInstall), so an add-on still cannot replace one of DVX's.

    Manifests (pkgManifest.h)

      Function                                       Description
       --------                                       -----------
    @@ -10513,8 +10629,10 @@ dvxpkg verify NAME.DPK [-s NAME.SIG] [KEY.KEY ...]
     dvxpkg info NAME.DPK
     dvxpkg keygen "Key name" BASE
     dvxpkg install NAME.DPK [-r DVXDIR] [-y]
    +dvxpkg record NAME.DPK DVXDIR [-n]
     dvxpkg remove VENDOR.NAME [-r DVXDIR] [-f]
     dvxpkg list [-r DVXDIR]
    +

    record writes a package's installed record into a DVX folder whose files came another way, and prints the record's name; with -n the package is recorded as needed by another (it goes when nothing needs it any more), not chosen. The release build records every package of DVX's in Setup's package that way (DVX itself, the base fonts, the optional bundles and their members: src/packages.ini), every file with its size and SHA-256, so an update from the package server replaces them as it would any package's, removing the files the new version dropped.

    On DOS, DVXPKG remove is the way out when an add-on keeps DVX from starting (DVX /SAFE, or holding Shift, also starts DVX without add-ons). The SDK's make package builds NAME.dpk from a project (sdk.mk's PKG_* settings), and the IDE's File > Make Package from a BASIC project.

    @@ -10616,8 +10734,8 @@ if (doc) {

    A fill (PrintFillE) is solid or a hatch of lines one dot wide every PRINT_HATCH_DOTS (8) dots, horizontal, vertical, either diagonal, or crossed, lined up across the page as BASIC's FillStyle hatches are. The pen (its color, width, and dashes) and the font are sent only when they change, and each of the printer's faces is made Latin-1 the first time the document uses it.

    A font opened for a vector page (printFontOpen with page.vector set) is the printer's own Helvetica, Times, or Courier in the style asked for (FONT_STYLE_BOLD, FONT_STYLE_ITALIC): Courier for a fixed-pitch family or one with Mono or Courier in its name, Times for one with Serif, Times, Roman, Tinos, or Ledger in it (but not Sans), else Helvetica. It is exactly the size asked for, and printFontWidth, printFontHeight (1.2 times the size), and printFontAscent measure the printer's letters by Adobe's published widths, so text placed with them lines up as it prints. It draws only with printVecText (printFontSpans and printFontDraw draw nothing with it); for DVX's own letters drawn in bands on a PostScript printer, open the font with vector cleared in a copy of the page.

    Text on a Page

    -

    A page a program draws can carry text in any font family (dvxText.h) at a size in points. printFontOpen(&page, family, decipoints, style) gives a PrintFontT for a page's resolution (its PrintPageInfoT): the size is in tenths of a point (120 for 12 points), the style any FONT_STYLE_ bits, underline and strike-through included. The font is the family's smallest strike at least as large as the size in printer dots (12 points is 50 dots at 300 dpi, 30 at 180), else its largest, scaled to the size across and down apart, since a 9-pin printer's dots are not square. Scaling is nearest pixel; scaling down, every pixel row and column of the strike still marks a dot, so thin strokes stay.

    -

    The base set's sizes go up to 24 pixels, so most printed sizes are scaled up from it and look blocky at 300 dpi. The Printer Fonts add-on (kpunch.prfonts) gives DejaVu Sans, Serif, and Sans Mono in every style at 25 to 100 pixels, about 6 to 24 points at 300 dpi and 10 to 40 at 180: installed, those families print crisp at those sizes with no change to the program.

    +

    A page a program draws can carry text in any font family (dvxText.h) at a size in points. printFontOpen(&page, family, decipoints, style) gives a PrintFontT for a page's resolution (its PrintPageInfoT): the size is in tenths of a point (120 for 12 points), the style any FONT_STYLE_ bits, underline and strike-through included. The font is the family's smallest strike at least as large as the size in printer dots (12 points is 50 dots at 300 dpi, 30 at 180), else its largest, scaled to the size across and down apart, since a 9-pin printer's dots are not square. Scaled down (or up one way only), a strike pixel becomes the dots it covers, and every pixel row and column of the strike still marks a dot, so thin strokes stay. Scaled up both ways, each dot is sampled between the four strike pixels around its centre and drawn when at least half covered, so edges come out smooth rather than in steps.

    +

    The base set's files carry print sizes beside their screen sizes (mkfont's print key; see Making Fonts): DejaVu Sans, Serif, and Sans Mono in every style at 25 to 100 pixels, about 6 to 24 points at 300 dpi and 10 to 40 at 180, so those families print crisp at those sizes. Other families, and larger text, are scaled up from their largest size.

      Function                                                    Description
       --------                                                    -----------
       printFontOpen(&page, family, decipoints, style)             The font; NULL when out of memory or for a size of 0 or less. printFontClose gives it up.
    diff --git a/screenshots/86box/apps-welcome.png b/screenshots/86box/apps-welcome.png
    index 8fca1d66..33129c1a 100644
    --- a/screenshots/86box/apps-welcome.png
    +++ b/screenshots/86box/apps-welcome.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    -size 21809
    +oid sha256:5ee0f12700291e4b6c2fb9655aea1f65e7054c67d24e5d0fe1313fb260da28ec
    +size 23216
    diff --git a/screenshots/86box/f1-h-format.png b/screenshots/86box/f1-h-format.png
    new file mode 100644
    index 00000000..315a925b
    --- /dev/null
    +++ b/screenshots/86box/f1-h-format.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:af11ec1ad13f8bef7b55f99be4257f5690d8328e8ff287012b671ed57c5a1b3b
    +size 22443
    diff --git a/screenshots/86box/f1-h-welcom.png b/screenshots/86box/f1-h-welcom.png
    index 8518787f..b367940f 100644
    --- a/screenshots/86box/f1-h-welcom.png
    +++ b/screenshots/86box/f1-h-welcom.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b1b3758c817e427e44cbc91837a48f04d88548435da0854d27dded272235584c
    -size 18884
    +oid sha256:c1052ad8ad913f3540601e56a55277a6834da1d787786323235d8fe93e063097
    +size 18903
    diff --git a/screenshots/86box/firstrun-fr-open.png b/screenshots/86box/firstrun-fr-open.png
    index 8fca1d66..33129c1a 100644
    --- a/screenshots/86box/firstrun-fr-open.png
    +++ b/screenshots/86box/firstrun-fr-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    -size 21809
    +oid sha256:5ee0f12700291e4b6c2fb9655aea1f65e7054c67d24e5d0fe1313fb260da28ec
    +size 23216
    diff --git a/screenshots/86box/instadd-add-font.png b/screenshots/86box/instadd-add-font.png
    new file mode 100644
    index 00000000..27e9ec9e
    --- /dev/null
    +++ b/screenshots/86box/instadd-add-font.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dbc32f5465cca9d6b7a9235a04d5b2de8ce94abd3f1d91975658fe8e1ac8e545
    +size 19792
    diff --git a/screenshots/86box/instadd-add-sw.png b/screenshots/86box/instadd-add-sw.png
    new file mode 100644
    index 00000000..87001408
    --- /dev/null
    +++ b/screenshots/86box/instadd-add-sw.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:14eba919cb53886f0496c00fb57379b238ca4754dd05bd0a13a49fd135e43599
    +size 21515
    diff --git a/screenshots/86box/instadd-su-copy.png b/screenshots/86box/instadd-su-copy.png
    new file mode 100644
    index 00000000..1fee6c29
    --- /dev/null
    +++ b/screenshots/86box/instadd-su-copy.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:fdbf49cb682467b8fd4f1f8e34fcb449fd5fff43cb0d334e1a33640ed77c0dcc
    +size 24503
    diff --git a/screenshots/86box/instadd-su-done.png b/screenshots/86box/instadd-su-done.png
    new file mode 100644
    index 00000000..65c53b03
    --- /dev/null
    +++ b/screenshots/86box/instadd-su-done.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:9dc12906f9d8052de2f6890597bf868e4c69ceaf5e0f2b46dc1d33d5794f2b25
    +size 22957
    diff --git a/screenshots/86box/instadd-su-fold.png b/screenshots/86box/instadd-su-fold.png
    new file mode 100644
    index 00000000..f0a9e279
    --- /dev/null
    +++ b/screenshots/86box/instadd-su-fold.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:a4eef0566bd81c39424f535e2100cdb7865dff89c98a56614a057cdcf194a929
    +size 17840
    diff --git a/screenshots/86box/instadd-su-lic.png b/screenshots/86box/instadd-su-lic.png
    new file mode 100644
    index 00000000..7dce1b99
    --- /dev/null
    +++ b/screenshots/86box/instadd-su-lic.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:22468405ed5beeeabb8c283c28adea6fffaa5887c6b9b5ea9a9fe14345421f81
    +size 20560
    diff --git a/screenshots/86box/instadd-su-start.png b/screenshots/86box/instadd-su-start.png
    new file mode 100644
    index 00000000..18e80052
    --- /dev/null
    +++ b/screenshots/86box/instadd-su-start.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:920606c7ed76ffa3dacd9be76ce1134da0d6e365efdf5caa107f0510903fc04b
    +size 20530
    diff --git a/screenshots/86box/instadd-su-unin.png b/screenshots/86box/instadd-su-unin.png
    new file mode 100644
    index 00000000..d2710d7e
    --- /dev/null
    +++ b/screenshots/86box/instadd-su-unin.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:62e86203241e98ef012cf0371c8faf08780ab83008ac5e79b948bd7ebeaf8863
    +size 18638
    diff --git a/screenshots/86box/instadd-su-what.png b/screenshots/86box/instadd-su-what.png
    new file mode 100644
    index 00000000..b3f2400e
    --- /dev/null
    +++ b/screenshots/86box/instadd-su-what.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:d928454dc96be2d801b4dca486336ca902d461895eaddb436544f9ef2eba25a8
    +size 21509
    diff --git a/screenshots/86box/install-in-welc.png b/screenshots/86box/install-in-welc.png
    index 8fca1d66..33129c1a 100644
    --- a/screenshots/86box/install-in-welc.png
    +++ b/screenshots/86box/install-in-welc.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    -size 21809
    +oid sha256:5ee0f12700291e4b6c2fb9655aea1f65e7054c67d24e5d0fe1313fb260da28ec
    +size 23216
    diff --git a/screenshots/86box/install-su-fold.png b/screenshots/86box/install-su-fold.png
    index 5b0363a6..f0a9e279 100644
    --- a/screenshots/86box/install-su-fold.png
    +++ b/screenshots/86box/install-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:2b6900ec5a8858fd1f12e78aca97d7c8e919c9194a2729b8cbe2727407033a7e
    -size 20564
    +oid sha256:a4eef0566bd81c39424f535e2100cdb7865dff89c98a56614a057cdcf194a929
    +size 17840
    diff --git a/screenshots/86box/install-su-what.png b/screenshots/86box/install-su-what.png
    new file mode 100644
    index 00000000..41a19b32
    --- /dev/null
    +++ b/screenshots/86box/install-su-what.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dfbc940dc69ee9cf1709927abf60c5f277233ae58db686e2402bb06ef7c95b6a
    +size 21461
    diff --git a/screenshots/86box/instbase-su-fold.png b/screenshots/86box/instbase-su-fold.png
    index 2f1d49cd..f0a9e279 100644
    --- a/screenshots/86box/instbase-su-fold.png
    +++ b/screenshots/86box/instbase-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c420a85d11918e5cf0fa2d9849c07cb0636af382a144db80a29b008ac67b363b
    -size 20553
    +oid sha256:a4eef0566bd81c39424f535e2100cdb7865dff89c98a56614a057cdcf194a929
    +size 17840
    diff --git a/screenshots/86box/instbase-su-lic.png b/screenshots/86box/instbase-su-lic.png
    index a5f80c35..7dce1b99 100644
    --- a/screenshots/86box/instbase-su-lic.png
    +++ b/screenshots/86box/instbase-su-lic.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:28003d2ec8561fcece2bec259abefa831b1b2ba1e38c446963a1872512e0315d
    -size 20521
    +oid sha256:22468405ed5beeeabb8c283c28adea6fffaa5887c6b9b5ea9a9fe14345421f81
    +size 20560
    diff --git a/screenshots/86box/instbase-su-what.png b/screenshots/86box/instbase-su-what.png
    new file mode 100644
    index 00000000..41a19b32
    --- /dev/null
    +++ b/screenshots/86box/instbase-su-what.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dfbc940dc69ee9cf1709927abf60c5f277233ae58db686e2402bb06ef7c95b6a
    +size 21461
    diff --git a/screenshots/86box/instcore-co-desk.png b/screenshots/86box/instcore-co-desk.png
    new file mode 100644
    index 00000000..d5611cf4
    --- /dev/null
    +++ b/screenshots/86box/instcore-co-desk.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dabc907a6179708f4427f6cf5b4f10e8b3875de21b60bfa0cb5c14ee1f23b494
    +size 16202
    diff --git a/screenshots/86box/instcore-co-sw.png b/screenshots/86box/instcore-co-sw.png
    new file mode 100644
    index 00000000..815a12a8
    --- /dev/null
    +++ b/screenshots/86box/instcore-co-sw.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:beb38279daa76343c1ae9f8ae54ec65b1b861246b07ba442dfd8de18537f6e19
    +size 15657
    diff --git a/screenshots/86box/instcore-co-welc.png b/screenshots/86box/instcore-co-welc.png
    new file mode 100644
    index 00000000..8edc7b80
    --- /dev/null
    +++ b/screenshots/86box/instcore-co-welc.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:ddc481d4a91560ce55e03ebf0898ba34b94661f63f7eb0d0c497468d1fd2ce80
    +size 21238
    diff --git a/screenshots/86box/upgdrop-ud-sw.png b/screenshots/86box/upgdrop-ud-sw.png
    new file mode 100644
    index 00000000..3ce54a2a
    --- /dev/null
    +++ b/screenshots/86box/upgdrop-ud-sw.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:1aaf22316b34f748e761e710a8c5fdfab34595916975dd8a8d4c7b74ec8b21ee
    +size 17154
    diff --git a/screenshots/86box/upgdrop-ud-welc.png b/screenshots/86box/upgdrop-ud-welc.png
    new file mode 100644
    index 00000000..8edc7b80
    --- /dev/null
    +++ b/screenshots/86box/upgdrop-ud-welc.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:ddc481d4a91560ce55e03ebf0898ba34b94661f63f7eb0d0c497468d1fd2ce80
    +size 21238
    diff --git a/screenshots/86box/upgrade-su-upgr.png b/screenshots/86box/upgrade-su-upgr.png
    index b58ae831..4f152a96 100644
    --- a/screenshots/86box/upgrade-su-upgr.png
    +++ b/screenshots/86box/upgrade-su-upgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:e93258d6519c57ad8f8ab4a4408f8e0e47970e288cbffdafca2d944948b308c9
    -size 21573
    +oid sha256:4ac7be8da792266b58ed2f9f782d39fdacbdc42e40a6ed5238469da0cc75b0d4
    +size 18852
    diff --git a/screenshots/86box/upgrade-su-what.png b/screenshots/86box/upgrade-su-what.png
    new file mode 100644
    index 00000000..cb123020
    --- /dev/null
    +++ b/screenshots/86box/upgrade-su-what.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:7cedd0ecc3f42417e5341284b096b64114f7ad54ade469e33945fcfac8ac6b83
    +size 21797
    diff --git a/screenshots/86box/welcome-wl-help.png b/screenshots/86box/welcome-wl-help.png
    index 8518787f..b367940f 100644
    --- a/screenshots/86box/welcome-wl-help.png
    +++ b/screenshots/86box/welcome-wl-help.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b1b3758c817e427e44cbc91837a48f04d88548435da0854d27dded272235584c
    -size 18884
    +oid sha256:c1052ad8ad913f3540601e56a55277a6834da1d787786323235d8fe93e063097
    +size 18903
    diff --git a/screenshots/86box/welcome-wl-open.png b/screenshots/86box/welcome-wl-open.png
    index 8fca1d66..33129c1a 100644
    --- a/screenshots/86box/welcome-wl-open.png
    +++ b/screenshots/86box/welcome-wl-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    -size 21809
    +oid sha256:5ee0f12700291e4b6c2fb9655aea1f65e7054c67d24e5d0fe1313fb260da28ec
    +size 23216
    diff --git a/screenshots/86box/welcome-wl-tour.png b/screenshots/86box/welcome-wl-tour.png
    index ff0fbc0a..7605f943 100644
    --- a/screenshots/86box/welcome-wl-tour.png
    +++ b/screenshots/86box/welcome-wl-tour.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c3eda8d3a118877e4417c8c9ed52fd4530e6cab356482ff0d8ce6cdc8cdeece1
    -size 24645
    +oid sha256:989ff22b897931288f1a4077b5b189b400caefe420692eb89f64e71ea50b881a
    +size 26013
    diff --git a/screenshots/86box/welcome-wl-tour2.png b/screenshots/86box/welcome-wl-tour2.png
    index e9c8d62f..b4cbb814 100644
    --- a/screenshots/86box/welcome-wl-tour2.png
    +++ b/screenshots/86box/welcome-wl-tour2.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:858c116aa5e55367589908c9f0c3dea6cf3adf0174d7f3876eb306c45db7a261
    -size 24465
    +oid sha256:ed2116d823aa2a9f1155a1e76e7fc40b97cbcafd1ba64587f3bb9fd4b1038030
    +size 25665
    diff --git a/screenshots/dosbox/apps-welcome.png b/screenshots/dosbox/apps-welcome.png
    index 8fca1d66..33129c1a 100644
    --- a/screenshots/dosbox/apps-welcome.png
    +++ b/screenshots/dosbox/apps-welcome.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    -size 21809
    +oid sha256:5ee0f12700291e4b6c2fb9655aea1f65e7054c67d24e5d0fe1313fb260da28ec
    +size 23216
    diff --git a/screenshots/dosbox/f1-h-format.png b/screenshots/dosbox/f1-h-format.png
    new file mode 100644
    index 00000000..315a925b
    --- /dev/null
    +++ b/screenshots/dosbox/f1-h-format.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:af11ec1ad13f8bef7b55f99be4257f5690d8328e8ff287012b671ed57c5a1b3b
    +size 22443
    diff --git a/screenshots/dosbox/f1-h-welcom.png b/screenshots/dosbox/f1-h-welcom.png
    index 8518787f..b367940f 100644
    --- a/screenshots/dosbox/f1-h-welcom.png
    +++ b/screenshots/dosbox/f1-h-welcom.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b1b3758c817e427e44cbc91837a48f04d88548435da0854d27dded272235584c
    -size 18884
    +oid sha256:c1052ad8ad913f3540601e56a55277a6834da1d787786323235d8fe93e063097
    +size 18903
    diff --git a/screenshots/dosbox/firstrun-fr-open.png b/screenshots/dosbox/firstrun-fr-open.png
    index 8fca1d66..33129c1a 100644
    --- a/screenshots/dosbox/firstrun-fr-open.png
    +++ b/screenshots/dosbox/firstrun-fr-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    -size 21809
    +oid sha256:5ee0f12700291e4b6c2fb9655aea1f65e7054c67d24e5d0fe1313fb260da28ec
    +size 23216
    diff --git a/screenshots/dosbox/instadd-add-font.png b/screenshots/dosbox/instadd-add-font.png
    new file mode 100644
    index 00000000..27e9ec9e
    --- /dev/null
    +++ b/screenshots/dosbox/instadd-add-font.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dbc32f5465cca9d6b7a9235a04d5b2de8ce94abd3f1d91975658fe8e1ac8e545
    +size 19792
    diff --git a/screenshots/dosbox/instadd-add-sw.png b/screenshots/dosbox/instadd-add-sw.png
    new file mode 100644
    index 00000000..87001408
    --- /dev/null
    +++ b/screenshots/dosbox/instadd-add-sw.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:14eba919cb53886f0496c00fb57379b238ca4754dd05bd0a13a49fd135e43599
    +size 21515
    diff --git a/screenshots/dosbox/instadd-su-copy.png b/screenshots/dosbox/instadd-su-copy.png
    new file mode 100644
    index 00000000..1fee6c29
    --- /dev/null
    +++ b/screenshots/dosbox/instadd-su-copy.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:fdbf49cb682467b8fd4f1f8e34fcb449fd5fff43cb0d334e1a33640ed77c0dcc
    +size 24503
    diff --git a/screenshots/dosbox/instadd-su-done.png b/screenshots/dosbox/instadd-su-done.png
    new file mode 100644
    index 00000000..65c53b03
    --- /dev/null
    +++ b/screenshots/dosbox/instadd-su-done.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:9dc12906f9d8052de2f6890597bf868e4c69ceaf5e0f2b46dc1d33d5794f2b25
    +size 22957
    diff --git a/screenshots/dosbox/instadd-su-fold.png b/screenshots/dosbox/instadd-su-fold.png
    new file mode 100644
    index 00000000..81fa3ae1
    --- /dev/null
    +++ b/screenshots/dosbox/instadd-su-fold.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:6dfe529072b9003fa2f16ea2c60c6c8c5c5cb54d8532e201784593ddc505f36f
    +size 17858
    diff --git a/screenshots/dosbox/instadd-su-lic.png b/screenshots/dosbox/instadd-su-lic.png
    new file mode 100644
    index 00000000..a5f80c35
    --- /dev/null
    +++ b/screenshots/dosbox/instadd-su-lic.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:28003d2ec8561fcece2bec259abefa831b1b2ba1e38c446963a1872512e0315d
    +size 20521
    diff --git a/screenshots/dosbox/instadd-su-start.png b/screenshots/dosbox/instadd-su-start.png
    new file mode 100644
    index 00000000..18e80052
    --- /dev/null
    +++ b/screenshots/dosbox/instadd-su-start.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:920606c7ed76ffa3dacd9be76ce1134da0d6e365efdf5caa107f0510903fc04b
    +size 20530
    diff --git a/screenshots/dosbox/instadd-su-unin.png b/screenshots/dosbox/instadd-su-unin.png
    new file mode 100644
    index 00000000..9e493f59
    --- /dev/null
    +++ b/screenshots/dosbox/instadd-su-unin.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:19d8eaf493f9134b8fe4fa89c916760c00e01f64d6f17ebcdcca2d2725069114
    +size 18617
    diff --git a/screenshots/dosbox/instadd-su-what.png b/screenshots/dosbox/instadd-su-what.png
    new file mode 100644
    index 00000000..b3f2400e
    --- /dev/null
    +++ b/screenshots/dosbox/instadd-su-what.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:d928454dc96be2d801b4dca486336ca902d461895eaddb436544f9ef2eba25a8
    +size 21509
    diff --git a/screenshots/dosbox/install-in-welc.png b/screenshots/dosbox/install-in-welc.png
    index 8fca1d66..33129c1a 100644
    --- a/screenshots/dosbox/install-in-welc.png
    +++ b/screenshots/dosbox/install-in-welc.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    -size 21809
    +oid sha256:5ee0f12700291e4b6c2fb9655aea1f65e7054c67d24e5d0fe1313fb260da28ec
    +size 23216
    diff --git a/screenshots/dosbox/install-su-disk.png b/screenshots/dosbox/install-su-disk.png
    index 9f856b23..05959812 100644
    --- a/screenshots/dosbox/install-su-disk.png
    +++ b/screenshots/dosbox/install-su-disk.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:2edb8d8a27f05a7b1bbba59b9dc18747da863e4e9150ac6723136914442ddb3a
    -size 21310
    +oid sha256:7404a86300b0a95292a80731f60d2bec6eab784f19a5c13b894a9a65f17baca6
    +size 21392
    diff --git a/screenshots/dosbox/install-su-fold.png b/screenshots/dosbox/install-su-fold.png
    index 05bb0a78..81fa3ae1 100644
    --- a/screenshots/dosbox/install-su-fold.png
    +++ b/screenshots/dosbox/install-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:13aba99db7b50efc2c665bb9f21436b3a5fed8b4e6059a95f4b03e8db5b082ba
    -size 20565
    +oid sha256:6dfe529072b9003fa2f16ea2c60c6c8c5c5cb54d8532e201784593ddc505f36f
    +size 17858
    diff --git a/screenshots/dosbox/install-su-what.png b/screenshots/dosbox/install-su-what.png
    new file mode 100644
    index 00000000..9faabb80
    --- /dev/null
    +++ b/screenshots/dosbox/install-su-what.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:a2206be3e91c9edfe36a1d0a9d34125d21c2f2b8dd0d9d87dbede817850b2868
    +size 20437
    diff --git a/screenshots/dosbox/install-su-wrong.png b/screenshots/dosbox/install-su-wrong.png
    index bfdae766..ad4f4a76 100644
    --- a/screenshots/dosbox/install-su-wrong.png
    +++ b/screenshots/dosbox/install-su-wrong.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:81136864ffb01f763ed7584e05e5645a0ef766283c6729957c8482d18d881827
    -size 22060
    +oid sha256:b963ebf84e6b3fc14a323bc60660a525e4e1a62c88ac090fad1e0752c09bd24a
    +size 22149
    diff --git a/screenshots/dosbox/instbase-su-fold.png b/screenshots/dosbox/instbase-su-fold.png
    index 2f1d49cd..81fa3ae1 100644
    --- a/screenshots/dosbox/instbase-su-fold.png
    +++ b/screenshots/dosbox/instbase-su-fold.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c420a85d11918e5cf0fa2d9849c07cb0636af382a144db80a29b008ac67b363b
    -size 20553
    +oid sha256:6dfe529072b9003fa2f16ea2c60c6c8c5c5cb54d8532e201784593ddc505f36f
    +size 17858
    diff --git a/screenshots/dosbox/instbase-su-what.png b/screenshots/dosbox/instbase-su-what.png
    new file mode 100644
    index 00000000..41a19b32
    --- /dev/null
    +++ b/screenshots/dosbox/instbase-su-what.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dfbc940dc69ee9cf1709927abf60c5f277233ae58db686e2402bb06ef7c95b6a
    +size 21461
    diff --git a/screenshots/dosbox/instcore-co-desk.png b/screenshots/dosbox/instcore-co-desk.png
    new file mode 100644
    index 00000000..d5611cf4
    --- /dev/null
    +++ b/screenshots/dosbox/instcore-co-desk.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:dabc907a6179708f4427f6cf5b4f10e8b3875de21b60bfa0cb5c14ee1f23b494
    +size 16202
    diff --git a/screenshots/dosbox/instcore-co-sw.png b/screenshots/dosbox/instcore-co-sw.png
    new file mode 100644
    index 00000000..815a12a8
    --- /dev/null
    +++ b/screenshots/dosbox/instcore-co-sw.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:beb38279daa76343c1ae9f8ae54ec65b1b861246b07ba442dfd8de18537f6e19
    +size 15657
    diff --git a/screenshots/dosbox/instcore-co-welc.png b/screenshots/dosbox/instcore-co-welc.png
    new file mode 100644
    index 00000000..8edc7b80
    --- /dev/null
    +++ b/screenshots/dosbox/instcore-co-welc.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:ddc481d4a91560ce55e03ebf0898ba34b94661f63f7eb0d0c497468d1fd2ce80
    +size 21238
    diff --git a/screenshots/dosbox/upgdrop-ud-sw.png b/screenshots/dosbox/upgdrop-ud-sw.png
    new file mode 100644
    index 00000000..3ce54a2a
    --- /dev/null
    +++ b/screenshots/dosbox/upgdrop-ud-sw.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:1aaf22316b34f748e761e710a8c5fdfab34595916975dd8a8d4c7b74ec8b21ee
    +size 17154
    diff --git a/screenshots/dosbox/upgdrop-ud-welc.png b/screenshots/dosbox/upgdrop-ud-welc.png
    new file mode 100644
    index 00000000..8edc7b80
    --- /dev/null
    +++ b/screenshots/dosbox/upgdrop-ud-welc.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:ddc481d4a91560ce55e03ebf0898ba34b94661f63f7eb0d0c497468d1fd2ce80
    +size 21238
    diff --git a/screenshots/dosbox/upgrade-su-upgr.png b/screenshots/dosbox/upgrade-su-upgr.png
    index b58ae831..21c3af01 100644
    --- a/screenshots/dosbox/upgrade-su-upgr.png
    +++ b/screenshots/dosbox/upgrade-su-upgr.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:e93258d6519c57ad8f8ab4a4408f8e0e47970e288cbffdafca2d944948b308c9
    -size 21573
    +oid sha256:425171823dd144f5069421bbe0376d9fe5570ce1924640565834ba9aebb015c3
    +size 18809
    diff --git a/screenshots/dosbox/upgrade-su-what.png b/screenshots/dosbox/upgrade-su-what.png
    new file mode 100644
    index 00000000..cb123020
    --- /dev/null
    +++ b/screenshots/dosbox/upgrade-su-what.png
    @@ -0,0 +1,3 @@
    +version https://git-lfs.github.com/spec/v1
    +oid sha256:7cedd0ecc3f42417e5341284b096b64114f7ad54ade469e33945fcfac8ac6b83
    +size 21797
    diff --git a/screenshots/dosbox/welcome-wl-help.png b/screenshots/dosbox/welcome-wl-help.png
    index 8518787f..b367940f 100644
    --- a/screenshots/dosbox/welcome-wl-help.png
    +++ b/screenshots/dosbox/welcome-wl-help.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:b1b3758c817e427e44cbc91837a48f04d88548435da0854d27dded272235584c
    -size 18884
    +oid sha256:c1052ad8ad913f3540601e56a55277a6834da1d787786323235d8fe93e063097
    +size 18903
    diff --git a/screenshots/dosbox/welcome-wl-open.png b/screenshots/dosbox/welcome-wl-open.png
    index 8fca1d66..33129c1a 100644
    --- a/screenshots/dosbox/welcome-wl-open.png
    +++ b/screenshots/dosbox/welcome-wl-open.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:84fe52aab2f648ad623509b2be7e39ff5d5809680566e1aaf6f14753b1af3057
    -size 21809
    +oid sha256:5ee0f12700291e4b6c2fb9655aea1f65e7054c67d24e5d0fe1313fb260da28ec
    +size 23216
    diff --git a/screenshots/dosbox/welcome-wl-tour.png b/screenshots/dosbox/welcome-wl-tour.png
    index ff0fbc0a..7605f943 100644
    --- a/screenshots/dosbox/welcome-wl-tour.png
    +++ b/screenshots/dosbox/welcome-wl-tour.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:c3eda8d3a118877e4417c8c9ed52fd4530e6cab356482ff0d8ce6cdc8cdeece1
    -size 24645
    +oid sha256:989ff22b897931288f1a4077b5b189b400caefe420692eb89f64e71ea50b881a
    +size 26013
    diff --git a/screenshots/dosbox/welcome-wl-tour2.png b/screenshots/dosbox/welcome-wl-tour2.png
    index e9c8d62f..40e1ecd9 100644
    --- a/screenshots/dosbox/welcome-wl-tour2.png
    +++ b/screenshots/dosbox/welcome-wl-tour2.png
    @@ -1,3 +1,3 @@
     version https://git-lfs.github.com/spec/v1
    -oid sha256:858c116aa5e55367589908c9f0c3dea6cf3adf0174d7f3876eb306c45db7a261
    -size 24465
    +oid sha256:2481fd396a25ee564549c351d911b2e0354ffe3c4f933e57ee0cd56125143b27
    +size 25688
    diff --git a/screenshots/masks/instadd-su-fold.mask b/screenshots/masks/instadd-su-fold.mask
    new file mode 100644
    index 00000000..ae082ecd
    --- /dev/null
    +++ b/screenshots/masks/instadd-su-fold.mask
    @@ -0,0 +1,4 @@
    +# Setup's "DVX needs about N MB" figure (grows with the package) and the rest of that line
    +202 346 176 16
    +# Blinking text caret after C:\DVX in the folder box
    +123 189 3 18
    diff --git a/screenshots/masks/instadd-su-lic.mask b/screenshots/masks/instadd-su-lic.mask
    new file mode 100644
    index 00000000..f635c0f2
    --- /dev/null
    +++ b/screenshots/masks/instadd-su-lic.mask
    @@ -0,0 +1,2 @@
    +# Blinking text caret at the start of the licence text
    +77 210 3 18
    diff --git a/screenshots/masks/instadd-su-unin.mask b/screenshots/masks/instadd-su-unin.mask
    new file mode 100644
    index 00000000..aecaa1d0
    --- /dev/null
    +++ b/screenshots/masks/instadd-su-unin.mask
    @@ -0,0 +1,2 @@
    +# Blinking text caret after C:\DVX in the folder box
    +123 189 3 18
    diff --git a/screenshots/masks/install-su-fold.mask b/screenshots/masks/install-su-fold.mask
    index dc53f889..84883b7f 100644
    --- a/screenshots/masks/install-su-fold.mask
    +++ b/screenshots/masks/install-su-fold.mask
    @@ -1,4 +1,6 @@
    -# Setup's "DVX needs about N MB" figure (grows with the package) and the rest of that line
    +# Setup's "DVX needs about N MB" figure (grows with the package) and the rest of that line:
    +# from floppies (DOSBox), and from the CD (86Box), where the add-ons push it down
     202 301 176 16
    +202 346 176 16
     # Blinking text caret after C:\DVX in the folder box
     123 189 3 18
    diff --git a/screenshots/masks/instbase-su-fold.mask b/screenshots/masks/instbase-su-fold.mask
    index dc53f889..ae082ecd 100644
    --- a/screenshots/masks/instbase-su-fold.mask
    +++ b/screenshots/masks/instbase-su-fold.mask
    @@ -1,4 +1,4 @@
     # Setup's "DVX needs about N MB" figure (grows with the package) and the rest of that line
    -202 301 176 16
    +202 346 176 16
     # Blinking text caret after C:\DVX in the folder box
     123 189 3 18
    diff --git a/screenshots/masks/instbase-su-lic.mask b/screenshots/masks/instbase-su-lic.mask
    new file mode 100644
    index 00000000..f635c0f2
    --- /dev/null
    +++ b/screenshots/masks/instbase-su-lic.mask
    @@ -0,0 +1,2 @@
    +# Blinking text caret at the start of the licence text
    +77 210 3 18
    diff --git a/screenshots/masks/upgrade-su-lic.mask b/screenshots/masks/upgrade-su-lic.mask
    new file mode 100644
    index 00000000..f635c0f2
    --- /dev/null
    +++ b/screenshots/masks/upgrade-su-lic.mask
    @@ -0,0 +1,2 @@
    +# Blinking text caret at the start of the licence text
    +77 210 3 18
    diff --git a/screenshots/masks/upgrade-su-upgr.mask b/screenshots/masks/upgrade-su-upgr.mask
    index dc53f889..ae082ecd 100644
    --- a/screenshots/masks/upgrade-su-upgr.mask
    +++ b/screenshots/masks/upgrade-su-upgr.mask
    @@ -1,4 +1,4 @@
     # Setup's "DVX needs about N MB" figure (grows with the package) and the rest of that line
    -202 301 176 16
    +202 346 176 16
     # Blinking text caret after C:\DVX in the folder box
     123 189 3 18
    diff --git a/server/README.md b/server/README.md
    index 2a2985f8..32587b1c 100644
    --- a/server/README.md
    +++ b/server/README.md
    @@ -109,7 +109,8 @@ and put it back.
     ## Publishing DVX itself
     
     `make` in the DVX tree writes `dist/DVXUPD.DPK`: DVX itself as the package `kpunch.dvx` (kind
    -`system`), every file of the release but the optional fonts, at this DVX's version and ABI. Packages
    +`system`), every file of the release but the optional fonts and DVX's optional parts, at this DVX's
    +version and ABI. Packages
     of the vendor `kpunch` are the operator's, so it is uploaded with the console token and approved
     like any other:
     
    @@ -119,17 +120,43 @@ like any other:
     
     Software then lists it first on its Updates page. DVX installs it only when the repository's
     signature checks against the key in its own `SYSTEM\KEYS`, and names the add-ons a new ABI would
    -leave unable to load before anything is installed.
    +leave unable to load before anything is installed. Setup installed DVX with its record listing every
    +file (the release build writes it into Setup's package), so the update also removes the files the
    +new version dropped.
     
    -The same build writes DVX's own add-ons to `dist/addons/` (`src/addons/README.md`): instrument
    -banks for MIDI music, kept out of the base install. They are uploaded and approved the same way:
    +The base set of fonts is a package too, `kpunch.basefont` ("DVX Fonts", `src/fonts/PACKAGE.INI`):
    +`make` writes it as `dist/BASEFONT.DPK`, at DVX's version. Setup installs and records it with DVX
    +unless told to install no fonts, so publishing a newer one updates every DVX that has the fonts,
    +and offers them to those that do not:
    +
    +    curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/octet-stream" \
    +         --data-binary @dist/BASEFONT.DPK https://packages.example.com/v1/upload
    +    calog tools/review.lua -- review.conf approve kpunch.basefont 1.0.1
    +
    +DVX's optional parts (`src/packages.ini`: DVX BASIC, Databases, Networking, Communications, Media,
    +Developer Tools, and Demos) are packages as well, one per module, each part a bundle that needs its
    +members. `make` writes them all to `dist/packages/` (`BASIC.DPK`, `DVXBASIC.DPK`, `BASDEV.DPK`, ...),
    +at DVX's version. Setup installs and records the parts chosen, so publishing newer ones updates
    +every DVX that has them, and offers the parts to those that do not; publish them with the DVX they
    +were built with:
    +
    +    for dpk in dist/packages/*.DPK; do
    +        curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/octet-stream" \
    +             --data-binary @"$dpk" https://packages.example.com/v1/upload
    +    done
    +    calog tools/review.lua -- review.conf approve kpunch.basic 1.0.1    # and each of the others
    +
    +The same build writes DVX's own add-ons to `dist/addons/` (`src/addons/README.md`): More Fonts and
    +the General MIDI SoundFont, kept out of the base install (the CD carries them too, signed when the
    +media are signed: with `DVX_SIGN_KEY=KEY.SEC`, else `~/.ssh/KPUNCH.SEC` when it is there). They are
    +uploaded and approved the same way:
     
         for dpk in dist/addons/*.DPK; do
             curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/octet-stream" \
                  --data-binary @"$dpk" https://packages.example.com/v1/upload
         done
    -    calog tools/review.lua -- review.conf approve kpunch.dmxopl 1.11.0
    -    calog tools/review.lua -- review.conf approve kpunch.dmxopl3 2.11.0
    +    calog tools/review.lua -- review.conf approve kpunch.morefont 1.0.0
    +    calog tools/review.lua -- review.conf approve kpunch.dvxgm 1.0.0
     
     ## Configuration
     
    diff --git a/src/addons/README.md b/src/addons/README.md
    index 52658a92..5e788828 100644
    --- a/src/addons/README.md
    +++ b/src/addons/README.md
    @@ -1,24 +1,42 @@
     # DVX's own add-ons
     
    -Add-on packages that DVX's operator publishes on the package server, kept
    -out of the base install to keep it small.  Each folder `KIND/NAME` holds a
    -package template (`PACKAGE.INI`, without `[files]`) and the files the
    -package installs, laid out as they go into the DVX folder.  Every build
    -(`src/installer/mkDist.sh`, run by `make`) packs each one into
    -`dist/addons/NAME.DPK`; they are uploaded and approved like any other
    -package (`server/README.md`).
    +Add-on packages DVX offers beside its base install: on the CD (its
    +`ADDONS` folder, where Setup offers them) and on the package server.  Each
    +folder `KIND/NAME` holds a package template (`PACKAGE.INI`, without
    +`[files]`) and the files the package installs, laid out as they go into
    +the DVX folder.  Every build (`src/installer/mkDist.sh`, run by `make`)
    +packs each one into `dist/addons/NAME.DPK` and copies it to the CD;
    +mkDist signs them (`NAME.SIG` beside each) with the secret key
    +`DVX_SIGN_KEY` names, else `~/.ssh/KPUNCH.SEC` when it is there; a
    +release's must be signed: DVX installs a `kpunch.*` package only when
    +signed by a key in its `SYSTEM\KEYS`, so Setup offers no unsigned ones.
    +On the server they are uploaded and approved like any other package
    +(`server/README.md`).
     
     Third-party content is kept unchanged, with its origin (the commit it came
     from and its SHA-256) in the comments of `PACKAGE.INI` and its licence in a
     text file the package installs beside it.
     
    -A folder with a `fonts.ini` instead holds a font manifest (as
    -`src/fonts/fonts.ini`): the build makes its `.DFN` files with mkfont into
    -`obj/addons/NAME/SHARED/FONTS/KPUNCH`, and mkDist packs that folder.  Each
    -`.DFN` carries its licence text.
    +An add-on whose files the build makes has them in `obj/addons/NAME`
    +(`src/tools/Makefile`), which mkDist packs instead of the folder:
    +
    +* More Fonts: the fonts of `src/fonts/fonts.ini`'s "more" set, copied
    +  from the build's fonts.  Every font DVX makes is listed in that one
    +  manifest (`set = base` for the DVX Fonts package, kpunch.basefont,
    +  which Setup installs with DVX: `src/fonts/PACKAGE.INI`; `set = more`
    +  for this add-on), so a font and its print sizes cannot be left out or
    +  drift apart; the SDK guide's Making Fonts says how to add one.
    +* A folder with a `soundfont.ini`: the SoundFont mksf makes from the
    +  pinned source, with the folder's own files.
    +
    +`version = dvx` in a template stands for DVX's own version (mkDist fills
    +it in): More Fonts is made from the same manifest as the base set, so
    +each DVX release carries its own.
     
     | Folder | Package | What it is |
     |---|---|---|
    -| `banks/dmxopl` | `kpunch.dmxopl` | DMXOPL 1.11, a General MIDI instrument bank by ConSiGno (MIT) |
    -| `banks/dmxopl3` | `kpunch.dmxopl3` | DMXOPL3, its OPL3 successor, by Sneakernets (MIT) |
    -| `fonts/prfonts` | `kpunch.prfonts` | Printer Fonts: DejaVu Sans, Serif, and Sans Mono in every style at 25 to 100 pixels, for crisp printed text (about 6 MB installed) |
    +| `banks/dvxgm` | `kpunch.dvxgm` | DVX General MIDI: a General MIDI SoundFont for wavetable cards (Gravis UltraSound, Sound Blaster AWE32 and AWE64), cut down from FluidR3_GM by Frank Wen (MIT); about 4 MB installed |
    +| `fonts/morefont` | `kpunch.morefont` | More Fonts: every font DVX makes beyond the base set (condensed DejaVu, Arimo, Tinos, Cousine, display faces, and pixel fonts), each with its licence; about 7 MB installed |
    +
    +DMXOPL and DMXOPL3, the FM instrument banks, were add-ons once; at 12 KB
    +together they ship in the base install (`config/banks`).
    diff --git a/src/addons/banks/dmxopl/PACKAGE.INI b/src/addons/banks/dmxopl/PACKAGE.INI
    deleted file mode 100644
    index 6885a53e..00000000
    --- a/src/addons/banks/dmxopl/PACKAGE.INI
    +++ /dev/null
    @@ -1,20 +0,0 @@
    -; DMXOPL 1.11 as an add-on package: a General MIDI instrument bank for
    -; MIDI music on the FM chip and DVX's FM software synthesizer.  mkDist
    -; builds it into dist/addons/DMXOPL.DPK for the package server.
    -;
    -; From https://github.com/sneakernets/DMXOPL, branch master, commit
    -; 63bd4821c6adaff29074f1f6486261961c2014b4 (2017-08-22, "V1.11 Release"):
    -; GENMIDI.op2, unchanged, as SHARED\BANKS\KPUNCH\DMXOPL.OP2 (SHA-256
    -; d3feb807e5102d7de3075c2351cced7e853ba2da8708eff98cc1c8ef8f240ce2).
    -; MIT licence; DMXOPL.TXT beside it carries the licence and the credits.
    -[package]
    -format    = 1
    -id        = kpunch.dmxopl
    -name      = DMXOPL
    -version   = 1.11
    -kind      = bank
    -summary   = General MIDI instruments for FM music, made to sound like a Roland Sound Canvas.
    -publisher = Kangaroo Punch Studios
    -author    = ConSiGno
    -licence   = MIT
    -homepage  = https://github.com/sneakernets/DMXOPL
    diff --git a/src/addons/banks/dmxopl3/PACKAGE.INI b/src/addons/banks/dmxopl3/PACKAGE.INI
    deleted file mode 100644
    index 5bfd5786..00000000
    --- a/src/addons/banks/dmxopl3/PACKAGE.INI
    +++ /dev/null
    @@ -1,23 +0,0 @@
    -; DMXOPL3 as an add-on package: a General MIDI instrument bank for MIDI
    -; music on the FM chip and DVX's FM software synthesizer.  mkDist builds
    -; it into dist/addons/DMXOPL3.DPK for the package server.
    -;
    -; From https://github.com/sneakernets/DMXOPL, branch DMXOPL3, commit
    -; e83c144a2e6d3c283e20f9d16d011126931892f5 (2018-07-13, after the v2.11d
    -; release): GENMIDI.op2, unchanged, as SHARED\BANKS\KPUNCH\DMXOPL3.OP2
    -; (SHA-256 92fc03c7734c45b1ece2bfad1f79d1abbd2b27ec6b45fbfb1c5a74f5ce0138c9).
    -; Made for the OPL3: DVX plays its OPL3-only waveforms as the OPL2's four,
    -; as it does for every bank.  MIT licence; DMXOPL3.TXT beside it carries
    -; the licence and the credits ("DMXOPL3 by Sneakernets", as the author
    -; asks).
    -[package]
    -format    = 1
    -id        = kpunch.dmxopl3
    -name      = DMXOPL3
    -version   = 2.11
    -kind      = bank
    -summary   = General MIDI instruments for FM music, the OPL3 successor to DMXOPL.
    -publisher = Kangaroo Punch Studios
    -author    = Sneakernets
    -licence   = MIT
    -homepage  = https://github.com/sneakernets/DMXOPL
    diff --git a/src/addons/fonts/morefont/PACKAGE.INI b/src/addons/fonts/morefont/PACKAGE.INI
    new file mode 100644
    index 00000000..2bd4db21
    --- /dev/null
    +++ b/src/addons/fonts/morefont/PACKAGE.INI
    @@ -0,0 +1,19 @@
    +; More Fonts as an add-on package: every font DVX makes but the base set
    +; (src/fonts/fonts.ini, set = more): the condensed DejaVu faces, Arimo,
    +; Tinos, Cousine, and the other outline, display, and pixel faces.  The
    +; build copies their .DFN files (src/tools/Makefile) into
    +; obj/addons/morefont/SHARED/FONTS/KPUNCH, and mkDist packs them into
    +; dist/addons/MOREFONT.DPK, for the CD's ADDONS folder and the package
    +; server.  Each file carries its own licence text.  version = dvx: the
    +; fonts are made from the same manifest as the base set, so the add-on
    +; takes DVX's version (mkDist fills it in).
    +[package]
    +format    = 1
    +id        = kpunch.morefont
    +name      = More Fonts
    +version   = dvx
    +kind      = font
    +summary   = More typefaces for documents, programs, and games: condensed DejaVu, Arimo, Tinos, Cousine, display faces, and pixel fonts.
    +publisher = Kangaroo Punch Studios
    +author    = The fonts' designers (each font names its own)
    +licence   = Open font licences (each font carries its own)
    diff --git a/src/addons/fonts/prfonts/PACKAGE.INI b/src/addons/fonts/prfonts/PACKAGE.INI
    deleted file mode 100644
    index 0819c958..00000000
    --- a/src/addons/fonts/prfonts/PACKAGE.INI
    +++ /dev/null
    @@ -1,18 +0,0 @@
    -; Printer Fonts as an add-on package: the DejaVu Sans, Serif, and Sans
    -; Mono faces DVX ships, in every style, at the large sizes text is
    -; printed at (25 to 100 pixels), so printed text is crisp instead of
    -; scaled up from the screen sizes.  The build makes the .DFN files from
    -; fonts.ini beside this file (mkfont, src/tools/Makefile) into
    -; obj/addons/prfonts, and mkDist packs them into dist/addons/PRFONTS.DPK
    -; for the package server.  Each file carries the DejaVu licence text.
    -[package]
    -format    = 1
    -id        = kpunch.prfonts
    -name      = Printer Fonts
    -version   = 2.37
    -kind      = font
    -summary   = Large sizes of the DejaVu fonts, for sharp printed text.
    -publisher = Kangaroo Punch Studios
    -author    = DejaVu fonts team
    -licence   = Bitstream Vera and Arev (DejaVu changes public domain)
    -homepage  = https://dejavu-fonts.github.io
    diff --git a/src/addons/fonts/prfonts/fonts.ini b/src/addons/fonts/prfonts/fonts.ini
    deleted file mode 100644
    index 1b20befa..00000000
    --- a/src/addons/fonts/prfonts/fonts.ini
    +++ /dev/null
    @@ -1,126 +0,0 @@
    -; fonts.ini -- the printer-fonts add-on (kpunch.prfonts): the DejaVu
    -; Sans, Serif, and Sans Mono faces DVX ships, at the large sizes text is
    -; printed at, made by mkfont (src/tools/mkfont.c documents every key)
    -; into SHARED\FONTS\KPUNCH.  Each file adds its sizes to the face of the
    -; same family and style in the base set, so printing finds a strike near
    -; the size in printer dots (12 points is 50 dots at 300 dpi, 30 at 180)
    -; and scales it only a little; without the add-on it scales the base
    -; set's largest (24 pixels).  Sizes: 25 to 100 pixels, about 6 to 24
    -; points at 300 dpi and 10 to 40 at 180.  Nothing smoothed: printers
    -; take dots.
    -;
    -; Names: P (printer), the family's short form (SANS, SERF, MONO), then
    -; B, I, or Z as in the base set.
    -
    -[fonts]
    -PSANS    = DejaVu Sans
    -PSANSB   = DejaVu Sans
    -PSANSI   = DejaVu Sans
    -PSANSZ   = DejaVu Sans
    -PSERF    = DejaVu Serif
    -PSERFB   = DejaVu Serif
    -PSERFI   = DejaVu Serif
    -PSERFZ   = DejaVu Serif
    -PMONO    = DejaVu Sans Mono
    -PMONOB   = DejaVu Sans Mono
    -PMONOI   = DejaVu Sans Mono
    -PMONOZ   = DejaVu Sans Mono
    -
    -[licences]
    -DEJAVU   = dejavu/dejavu-fonts-ttf-2.37/LICENSE
    -
    -[PSANS]
    -style    = regular
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = native
    -
    -[PSANSB]
    -style    = bold
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Bold.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = native
    -
    -[PSANSI]
    -style    = italic
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Oblique.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = auto
    -
    -[PSANSZ]
    -style    = bolditalic
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-BoldOblique.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = auto
    -
    -[PSERF]
    -style    = regular
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = native
    -
    -[PSERFB]
    -style    = bold
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-Bold.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = native
    -
    -[PSERFI]
    -style    = italic
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-Italic.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = auto
    -
    -[PSERFZ]
    -style    = bolditalic
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-BoldItalic.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = auto
    -
    -[PMONO]
    -style    = regular
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = native
    -
    -[PMONOB]
    -style    = bold
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-Bold.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = native
    -
    -[PMONOI]
    -style    = italic
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-Oblique.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = auto
    -
    -[PMONOZ]
    -style    = bolditalic
    -licence  = DEJAVU
    -boxdraw  = true
    -source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-BoldOblique.ttf
    -sizes    = 25 29 33 38 42 50 58 67 75 83 100
    -hinting  = auto
    diff --git a/src/apps/kpunch/Makefile b/src/apps/kpunch/Makefile
    index e20a4986..6fba55fd 100644
    --- a/src/apps/kpunch/Makefile
    +++ b/src/apps/kpunch/Makefile
    @@ -44,29 +44,23 @@ C_APPS      = $(sort $(foreach f,$(C_APP_MAINS),$(if $(filter $(notdir $(basenam
     
     BASCOMP   = ../../../bin/host/bascomp
     
    -# BASIC apps: each is a .dbp project.  An entry is the project's directory,
    -# or dir:out when the program goes into another program's folder (the Help
    -# Editor, dvxhelp/helpedit, installs beside the Help Viewer in dvxhelp).
    -# The program is bin/apps/kpunch//.app,  being the last
    -# part of the directory.
    -BASIC_APPS = iconed notepad imgview dvxhelp/helpedit:dvxhelp resedit basdemo widshow clock welcome fileMgr terminal software pkgbuild player recorder
    +# BASIC apps: each is a .dbp project, /.dbp, built into
    +# bin/apps/kpunch//.app (a folder of its own: each program is a
    +# package of its own, src/packages.ini).
    +BASIC_APPS = iconed notepad imgview helpedit resedit basdemo widshow clock welcome fileMgr terminal software pkgbuild player recorder
     
    -basicDir  = $(firstword $(subst :, ,$(1)))
    -basicName = $(notdir $(call basicDir,$(1)))
    -basicOut  = $(or $(word 2,$(subst :, ,$(1))),$(call basicName,$(1)))
    -basicApp  = $(BINDIR)/kpunch/$(call basicOut,$(1))/$(call basicName,$(1)).app
    +basicApp = $(BINDIR)/kpunch/$(1)/$(1).app
     
     # Everything a project is built from: the files in its directory and the
     # modules its .dbp names (include libraries elsewhere).
     basicDeps = $(wildcard $(1)/*.dbp $(1)/*.frm $(1)/*.bas $(1)/*.cls $(1)/*.BMP) \
    -	$(addprefix $(1)/,$(shell sed -n 's/^File[0-9]* *= *\(.*\.bas\)\r*$$/\1/p' $(1)/$(notdir $(1)).dbp))
    +	$(addprefix $(1)/,$(shell sed -n 's/^File[0-9]* *= *\(.*\.bas\)\r*$$/\1/p' $(1)/$(1).dbp))
     
    -BASIC_TARGETS = $(foreach a,$(BASIC_APPS),$(call basicName,$(a)))
    -BASIC_OUTS    = $(foreach a,$(BASIC_APPS),$(call basicApp,$(a)))
    +BASIC_OUTS = $(foreach a,$(BASIC_APPS),$(call basicApp,$(a)))
     
    -.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_TARGETS)
    +.PHONY: all clean $(C_APPS) dvxbasic $(BASIC_APPS)
     
    -all: $(C_APPS) dvxbasic $(BASIC_TARGETS)
    +all: $(C_APPS) dvxbasic $(BASIC_APPS)
     
     dvxbasic:
     	$(MAKE) -C dvxbasic
    @@ -91,24 +85,22 @@ appFonts   = $(if $(call appFontIni,$(1)),$(OBJDIR)/$(1)/fonts.stamp)
     appDeps = $(call appRes,$(1)) $(if $(call appRes,$(1)),$(wildcard $(1)/*.bmp)) $(call appFonts,$(1))
     
     # Data files a program installs beside itself, C or BASIC alike:
    -# APP_DATA_ lists them ( being the last part of the program's
    -# source directory, the files relative to it); they are copied into the
    -# program's folder under bin/apps/kpunch.
    +# APP_DATA_ lists them (relative to its source directory); they are
    +# copied into the program's folder under bin/apps/kpunch.
     APP_DATA_dvxdemo = logo.bmp new.bmp open.bmp sample.bmp save.bmp
     
    -appDataOut = $(addprefix $(BINDIR)/kpunch/$(2)/,$(APP_DATA_$(notdir $(1))))
    +appDataOut = $(addprefix $(BINDIR)/kpunch/$(1)/,$(APP_DATA_$(1)))
     
    -# $(1) = source directory, $(2) = output folder under bin/apps/kpunch,
    -# $(3) = the program's make target.
    +# $(1) = app directory name (also its make target and output folder).
     define APP_DATA_RULES
    -$(3): $(call appDataOut,$(1),$(2))
    +$(1): $(call appDataOut,$(1))
     
    -$(call appDataOut,$(1),$(2)): $(BINDIR)/kpunch/$(2)/%: $(1)/%
    +$(call appDataOut,$(1)): $(BINDIR)/kpunch/$(1)/%: $(1)/%
     	@mkdir -p $$(dir $$@)
     	cp $$< $$@
     endef
     
    -APP_DATA_OUTS = $(foreach a,$(C_APPS),$(call appDataOut,$(a),$(a))) $(foreach a,$(BASIC_APPS),$(call appDataOut,$(call basicDir,$(a)),$(call basicOut,$(a))))
    +APP_DATA_OUTS = $(foreach a,$(C_APPS) $(BASIC_APPS),$(call appDataOut,$(a)))
     
     # $(1) = app directory name.
     define C_APP_RULES
    @@ -135,7 +127,7 @@ endef
     
     $(foreach a,$(C_APPS),$(eval $(call C_APP_RULES,$(a))))
     $(foreach a,$(C_APPS),$(if $(call appFontIni,$(a)),$(eval $(call APP_FONT_RULES,$(a)))))
    -$(foreach a,$(C_APPS),$(if $(APP_DATA_$(a)),$(eval $(call APP_DATA_RULES,$(a),$(a),$(a)))))
    +$(foreach a,$(C_APPS),$(if $(APP_DATA_$(a)),$(eval $(call APP_DATA_RULES,$(a)))))
     
     C_APP_OBJS = $(foreach a,$(C_APPS),$(call appObjs,$(a)))
     
    @@ -145,18 +137,18 @@ C_APP_OBJS = $(foreach a,$(C_APPS),$(call appObjs,$(a)))
     # F1 needs no resource: the shell presets an app's help file to .hlp
     # beside the .app (compiled there from /.dhs by compile-help).
     
    -# $(1) = BASIC_APPS entry.
    +# $(1) = BASIC_APPS entry (the project's directory).
     define BASIC_APP_RULES
    -$(call basicName,$(1)): $(call basicApp,$(1))
    +$(1): $(call basicApp,$(1))
     
    -$(call basicApp,$(1)): $(call basicDeps,$(call basicDir,$(1))) $(call appFonts,$(call basicDir,$(1))) $$(BASCOMP) | dvxbasic
    +$(call basicApp,$(1)): $(call basicDeps,$(1)) $(call appFonts,$(1)) $$(BASCOMP) | dvxbasic
     	@mkdir -p $$(dir $$@)
    -	$$(BASCOMP) $(call basicDir,$(1))/$(call basicName,$(1)).dbp -o $$@ -release
    +	$$(BASCOMP) $(1)/$(1).dbp -o $$@ -release
     endef
     
     $(foreach a,$(BASIC_APPS),$(eval $(call BASIC_APP_RULES,$(a))))
    -$(foreach a,$(BASIC_APPS),$(if $(call appFontIni,$(call basicDir,$(a))),$(eval $(call APP_FONT_RULES,$(call basicDir,$(a))))))
    -$(foreach a,$(BASIC_APPS),$(if $(APP_DATA_$(call basicName,$(a))),$(eval $(call APP_DATA_RULES,$(call basicDir,$(a)),$(call basicOut,$(a)),$(call basicName,$(a))))))
    +$(foreach a,$(BASIC_APPS),$(if $(call appFontIni,$(a)),$(eval $(call APP_FONT_RULES,$(a)))))
    +$(foreach a,$(BASIC_APPS),$(if $(APP_DATA_$(a)),$(eval $(call APP_DATA_RULES,$(a)))))
     
     # Native host build (test harness): each C app becomes a plain ELF shared
     # object under obj/host/apps/.so with its resources appended by
    diff --git a/src/apps/kpunch/README.md b/src/apps/kpunch/README.md
    index 9731b783..33ca92e8 100644
    --- a/src/apps/kpunch/README.md
    +++ b/src/apps/kpunch/README.md
    @@ -15,7 +15,7 @@ the BASIC-based sample apps. Two flavours of app live side by side:
       host-side `bascomp`. The compiler links the BASIC bytecode into a
       copy of `basstub.app`, which acts as the runtime host. See
       `iconed/`, `notepad/`, `imgview/`, `basdemo/`, `resedit/`,
    -  `widshow/`, `dvxhelp/helpedit/`, `clock/`, `welcome/`, `fileMgr/`,
    +  `widshow/`, `helpedit/`, `clock/`, `welcome/`, `fileMgr/`,
       `terminal/`, `player/`, `recorder/`.
     
     The rest of this document covers writing a C app against the SDK.
    @@ -450,7 +450,7 @@ PNG (or BMP by extension) save.
     
     | | |
     |---|---|
    -| File | `apps/kpunch/dvxhelp/helpedit.app` |
    +| File | `apps/kpunch/helpedit/helpedit.app` |
     | Type | BASIC (main-loop via basstub) |
     | Multi-instance | No |
     
    @@ -458,7 +458,9 @@ Editor for `.dhs` help source files with syntax highlighting. Build >
     Compile writes `.hlp` next to the source; Build > Preview
     compiles and opens the result in the help viewer. Its own help is
     `helpedit.hlp`; Help > Help File Format opens the format reference in
    -the viewer's `dvxhelp.hlp`.
    +the viewer's `dvxhelp.hlp` (by its path under the programs folder: the
    +editor has a folder of its own, a package of Developer Tools with
    +`SYSTEM\DVXHLPC.EXE`).
     
     ### Resource Editor (resedit, BASIC)
     
    @@ -633,16 +635,17 @@ apps/kpunch/
         help.dhs             source for the dvxhelp app's own help
         sample.dhs           small sample source
         icon32.bmp
    -    helpedit/            BASIC project: help editor
    -      helpedit.dbp
    -      helpedit.frm
    -      ICON32.BMP
     
       # BASIC apps
       iconed/
         iconed.dbp
         iconed.frm
         ICON32.BMP
    +  helpedit/              help editor (Developer Tools)
    +    helpedit.dbp
    +    helpedit.frm
    +    helpedit.dhs
    +    ICON32.BMP
       notepad/
         notepad.dbp
         notepad.frm
    diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c
    index 9acb6e9f..409d3546 100644
    --- a/src/apps/kpunch/dvxbasic/formrt/formrt.c
    +++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c
    @@ -96,6 +96,7 @@ DVX_ABI_STAMP(basrt);
     #define BAS_ERR_TITLE        "BASIC Runtime Error"
     #define BAS_ERR_SUMMARY      "Runtime error"     // when the error names none
     #define BAS_ERR_UNKNOWN      "Unknown error"
    +#define BAS_SQL_MISSING      "Databases are not installed: add the Databases part of DVX with Setup (Upgrade or repair)."   // SQLError$ without SQLite
     
     // dlsym name buffer: DVX_SYM_PREFIX + the longest BASIC identifier.
     #define BAS_MANGLED_NAME_LEN (BAS_MAX_IDENT + 2)
    @@ -9925,7 +9926,7 @@ const char *SQLError(int32_t db) {
         const BasSqlCallbacksT *api = sqlApi();
     
         if (!api->sqlError) {
    -        return "";
    +        return BAS_SQL_MISSING;
         }
     
         const char *err = api->sqlError(db);
    diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs
    index f1c1cc47..7a58b616 100644
    --- a/src/apps/kpunch/dvxbasic/langref.dhs
    +++ b/src/apps/kpunch/dvxbasic/langref.dhs
    @@ -4002,6 +4002,8 @@ Through a variable declared As Collection the members are checked when the progr
     
     DVX BASIC includes SQLite database support through the sql.bas include library. Add sql.bas to your project to gain access to these functions. All functions use database handles and result-set handles (Long integers) returned by SQLOpen and SQLQuery. Numeric parameters and results are Long, so row counts and integer column values keep their full 32-bit range.
     
    +SQLite is in the Databases part of DVX, which can be left out when DVX is installed. Without it the SQL functions do nothing: SQLOpen returns 0, and SQLError$ says that Databases are not installed. SysLibraryLoaded("dvxsql") (system.bas) tells beforehand.
    +
     .h2 Opening and Closing Databases
     
     .h3 SQLOpen
    @@ -4284,7 +4286,7 @@ A page is 8 by 10 inches. Its coordinates are twips (1440 an inch) until ScaleMo
       CurrentX, CurrentY                              Where the next drawing starts.
       ForeColor, FillColor, FillStyle, DrawWidth, DrawStyle  As on a PictureBox (black, transparent fills, a solid pen 1 dot wide to start).
       ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight  The scale (vbTwips to start).
    -  FontName, FontSize, FontBold, FontItalic, FontUnderline, FontStrikethru  The font of Print's text; FontSize is in points (12 to start, in DejaVu Sans). Text prints crisp where the font has large sizes (the Printer Fonts add-on gives DejaVu Sans, Serif, and Sans Mono them), and blockier where a screen size is enlarged.
    +  FontName, FontSize, FontBold, FontItalic, FontUnderline, FontStrikethru  The font of Print's text; FontSize is in points (12 to start, in DejaVu Sans). Text prints crisp where the font has sizes made for printing (DejaVu Sans, Serif, and Sans Mono have them from 6 to 24 points at 300 dpi), and a little softer where a smaller size is enlarged.
       Copies                                          How many times each page prints (1 to 99); as PrintChoose chose when it is not set.
       Page                                            The page being drawn (1 first). Read only.
       Width, Height                                   The page's size in twips (11520 by 14400). Read only.
    @@ -5195,6 +5197,7 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
     .index SendKeys
     .index SysMemoryTotalKb
     .index SysMemoryFreeKb
    +.index SysLibraryLoaded
     .index SYS_VIDEO_DEFAULT_WIDTH
     .index PrefsKeyName$
     .index PrefsVideoWidth
    @@ -5278,7 +5281,7 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
       sound.bas      Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, recording from a microphone or line input (RecordStart, RecordStop, RecordLevel, ...), the FM synthesizer chip, a wavetable card's voices and memory (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, WtAlloc, WtStart, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
       music.bas      Music: play MOD, S3M, XM, IT, MIDI, WAV, and VOC files, and play notes yourself (MusicLoad, MusicPlay, MusicStop, MusicNoteOn, MusicInfo$, ...); instruments from banks and SoundFonts, a sound card's own (MusicSetBank, MusicPatchSet$, MusicRomBank$); raw bytes to the MIDI port (MidiAcquire, MidiPut, MidiRelease, MidiPortInfo$); what a MIDI keyboard plays, heard as it comes and recorded into MIDI files (MidiInOpen, MidiInMessage, MusicMidiSend, MidiRecordNew, MidiRecordSave, ...)
       fileops.bas    Files and folders: folder listings for a file list, paths and names, the file dialogs' filter rule, attributes and the time a file was changed, sizes, free space and disk sizes, drive kinds, whether a folder can be written, the folder for scratch files, making a folder path, and copying, moving, and deleting whole folders at once or as a job with progress, Cancel, and questions (FileOpsList, FileOpsListRows$, FileOpsFilterMatch, FileOpsSetModified, FileOpsDriveType, FileOpsIsWritable, FileOpsMakePath, FileOpsJobCreate, FileOpsJobRunFor, ...)
    -  system.bas     What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper, text smoothing, and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl, and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, and the memory (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetTextSmoothing, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, ...)
    +  system.bas     What Control Panel changes, at once for the whole desktop: the screen mode, the system colors and themes, the wallpaper, text smoothing, and the mouse (with DVX's limits and defaults, MOUSE_* and SYS_VIDEO_DEFAULT_*), and storing them in DVX's settings; the system font's character pictures; the mouse pointer and the Shift, Ctrl, and Alt keys anywhere on the screen, moving the pointer, typing keys into the window in front, a picture of the whole screen, the memory, and whether a library is installed (SysSetVideoMode, SysSetColor, SysLoadTheme, SysSetWallpaper, SysSetTextSmoothing, SysSetMouse, SysColorsStore, SysFontGlyphRow, SysMouseX, SysSendKey, SysScreenshot, SysMemoryFreeKb, SysLibraryLoaded, ...)
       font.bas       Fonts: the font families DVX has, the sizes each comes in, whether one is fixed-pitch, and how wide text and how tall a line is in a font (FontFamilyCount, FontFamilyName$, FontFamilyFixed, FontSizeCount, FontSizeAt, FontNearestSize, FontTextWidth, FontLineHeight, FontAscent, FontRescan)
       richtext.bas   RTF without a control: an RTF document as plain text and plain text as RTF (RtfToText$, TextToRtf$); a RichEdit control reads and writes RTF itself (TextRTF, SelRTF, LoadFile, SaveFile)
       layout.bas     Control sizes in characters or in percent of the container instead of pixels (SizeChars, SizePercent)
    @@ -5672,7 +5675,7 @@ End Sub
     
     With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed.
     
    -What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it. PkgFileOwner$(s, fileName) is the other way: the id of the package that put a file there (its name from the DVX folder, such as "SHARED\BANKS\KPUNCH\DMXOPL.OP2"; "" when none did).
    +What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it. PkgFileOwner$(s, fileName) is the other way: the id of the package that put a file there (its name from the DVX folder, such as "SHARED\FONTS\KPUNCH\ARIMO.DFN"; "" when none did).
     
     Installing. PkgOfferFile(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0, and returns the number of the one at path (-1 when it cannot be read). PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts the job, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is.
     
    @@ -5763,6 +5766,16 @@ The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse po
     
     Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told).
     
    +Libraries: some parts of DVX are optional (Setup's "What to install" page; Software lists each as one package), and so are their libraries: Networking has dvxnet, http, and pkgnet (net.bas, http.bas, pkgnet.bas), Databases has dvxsql (sql.bas), and Communications has conn and xfer (conn.bas, xfer.bas). SysLibraryLoaded(libName) is True when the library a DECLARE LIBRARY line names (any case) is installed and was started with DVX. A program that can do without such a library asks first and leaves out what needs it: calling a function of a library that is not there stops the program with "External function not found". Software, for one, keeps its package server pages off without pkgnet.
    +
    +.code
    +If SysLibraryLoaded("pkgnet") Then
    +    handle = PkgNetOpen("")
    +Else
    +    lblStatus.Caption = "The package server needs DVX's Networking part."
    +End If
    +.endcode
    +
     System font: SysFontWidth() and SysFontHeight() are the size of every character of the system font in pixels, and SysFontFirstChar() to SysFontFirstChar() + SysFontCharCount() - 1 the character codes it has. SysFontGlyphRow(code, row) is one row of a character's picture: a bit per pixel, &H80 the leftmost -- for drawing text dot by dot, enlarged or in a font editor of your own.
     
     Saving: SysColorsStore, SysWallpaperStore, SysScreenSaverStore, SysFontsStore, and SysMouseStore write the colors, the wallpaper, the screen saver, text smoothing, and the mouse setup into a prefs.bas handle, in the sections DVX reads when it starts; PrefsSetVideoMode does the same for a screen mode. Store them in PrefsLoad(PrefsDvxIni$()) and save it, and DVX starts with them next time. SysColorsLoad, SysWallpaperLoad, SysScreenSaverLoad, SysFontsLoad, and SysMouseLoad apply what a handle holds. A PrefsCreate handle filled with the Store calls is a snapshot in memory: load it again to undo every change, the way Cancel works in Control Panel.
    @@ -5844,7 +5857,7 @@ If FontAddFile(App.Path + "\TITLE.DFN") Then
     End If
     .endcode
     
    -Every control and every form can have a font of its own, set with the properties FontName, FontSize (in pixels), FontBold, FontItalic, FontUnderline, and FontStrikethru, as in Visual Basic; they need no include file. A control that is given no font uses the font of the control it is in (a Frame, for example), else its form's, so setting a form's font changes every control on it that has none. Reading one of the properties gives the font the object draws with: a FontSize the family does not come in reads back as the nearest size it has, and a FontName DVX does not have reads back as "System". Setting one changes only that part of the font. Controls that edit text in columns (TextBox, TextArea, ComboBox, SpinButton, and Terminal) take only fixed-pitch families and show the System font in place of a proportional one. Print, TextWidth, TextHeight, PrintAccel, and TextWidthAccel on forms and picture boxes use the object's font. In the form designer, the Font row of the Properties window sets the six properties at once, and they are saved in the .frm file; DlgFont (see Font Dialog) lets the user choose a font while the program runs.
    +Every control and every form can have a font of its own, set with the properties FontName, FontSize (in pixels), FontBold, FontItalic, FontUnderline, and FontStrikethru, as in Visual Basic; they need no include file. A control that is given no font uses the font of the control it is in (a Frame, for example), else its form's, so setting a form's font changes every control on it that has none. Reading one of the properties gives the font the object draws with: a FontSize the family does not come in reads back as the nearest size it has, and a FontName DVX does not have reads back as "System". Setting one changes only that part of the font, keeping the size the font has: where a family's styles come in different sizes (a font the program carries in bold only, say), set FontBold and FontItalic before FontSize, so the size is picked from the style's own sizes. Controls that edit text in columns (TextBox, TextArea, ComboBox, SpinButton, and Terminal) take only fixed-pitch families and show the System font in place of a proportional one. Print, TextWidth, TextHeight, PrintAccel, and TextWidthAccel on forms and picture boxes use the object's font. In the form designer, the Font row of the Properties window sets the six properties at once, and they are saved in the .frm file; DlgFont (see Font Dialog) lets the user choose a font while the program runs.
     
     .code
     Sub Form1_Load ()
    @@ -6008,7 +6021,7 @@ MusicLoadMacro(macro) makes a music from a music macro string, the kind PLAY tak
     
     .code
     tune = MusicLoadMacro("T150 I41 O4 L8 CEG>C4")
    -MusicSetBank tune, "KPUNCH\DMXOPL.OP2"
    +MusicSetBank tune, "DMXOPL.OP2"
     MusicPlay tune, False
     .endcode
     
    diff --git a/src/apps/kpunch/dvxhelp/help.dhs b/src/apps/kpunch/dvxhelp/help.dhs
    index d161e255..7ed0d885 100644
    --- a/src/apps/kpunch/dvxhelp/help.dhs
    +++ b/src/apps/kpunch/dvxhelp/help.dhs
    @@ -191,7 +191,7 @@ Use warning notes for important cautions the reader should be aware of.
     
     .h1 Help Compiler (dvxhlpc)
     
    -The dvxhlpc tool runs on the host (Linux) and compiles .dhs source files into binary .hlp files for the viewer, and optionally into self-contained HTML.
    +The dvxhlpc tool runs on the host (Linux) and compiles .dhs source files into binary .hlp files for the viewer, and optionally into self-contained HTML. On DVX it is DVXHLPC.EXE in the SYSTEM folder, for the DOS prompt, which comes with the Help Editor in Developer Tools (the Help Editor compiles help itself, and DVX compiles the help of its libraries and widgets when it starts).
     
     .h2 Usage
     
    diff --git a/src/apps/kpunch/dvxhelp/helpedit/ICON32.BMP b/src/apps/kpunch/helpedit/ICON32.BMP
    similarity index 100%
    rename from src/apps/kpunch/dvxhelp/helpedit/ICON32.BMP
    rename to src/apps/kpunch/helpedit/ICON32.BMP
    diff --git a/src/apps/kpunch/dvxhelp/helpedit/helpedit.dbp b/src/apps/kpunch/helpedit/helpedit.dbp
    similarity index 70%
    rename from src/apps/kpunch/dvxhelp/helpedit/helpedit.dbp
    rename to src/apps/kpunch/helpedit/helpedit.dbp
    index 363e3efb..207bd744 100644
    --- a/src/apps/kpunch/dvxhelp/helpedit/helpedit.dbp
    +++ b/src/apps/kpunch/helpedit/helpedit.dbp
    @@ -8,9 +8,10 @@ Description = DVX Help source editor with syntax highlighting and preview
     Icon = ICON32.BMP
     
     [Modules]
    -File0 = ../../../../include/basic/commdlg.bas
    -File1 = ../../../../include/basic/help.bas
    -Count = 2
    +File0 = ../../../include/basic/commdlg.bas
    +File1 = ../../../include/basic/help.bas
    +File2 = ../../../include/basic/shell.bas
    +Count = 3
     
     [Forms]
     File0 = helpedit.frm
    diff --git a/src/apps/kpunch/dvxhelp/helpedit/helpedit.dhs b/src/apps/kpunch/helpedit/helpedit.dhs
    similarity index 97%
    rename from src/apps/kpunch/dvxhelp/helpedit/helpedit.dhs
    rename to src/apps/kpunch/helpedit/helpedit.dhs
    index 4fcc9368..826b94a4 100644
    --- a/src/apps/kpunch/dvxhelp/helpedit/helpedit.dhs
    +++ b/src/apps/kpunch/helpedit/helpedit.dhs
    @@ -22,8 +22,8 @@
     
     # Help Editor help.  Compiled to helpedit.hlp next to helpedit.app; F1
     # and Help > Contents in the Help Editor open it.  The help source
    -# format itself is documented in the help viewer's own help (help.dhs),
    -# which Help > Help File Format opens.
    +# format itself is documented in the help viewer's own help
    +# (dvxhelp/help.dhs), which Help > Help File Format opens.
     
     .topic helpedit.overview
     .title Help Editor
    diff --git a/src/apps/kpunch/dvxhelp/helpedit/helpedit.frm b/src/apps/kpunch/helpedit/helpedit.frm
    similarity index 95%
    rename from src/apps/kpunch/dvxhelp/helpedit/helpedit.frm
    rename to src/apps/kpunch/helpedit/helpedit.frm
    index d94e6bf4..04c4d86c 100644
    --- a/src/apps/kpunch/dvxhelp/helpedit/helpedit.frm
    +++ b/src/apps/kpunch/helpedit/helpedit.frm
    @@ -93,12 +93,13 @@ End
     ' IN THE SOFTWARE.
     
     
    -' Help Editor's own help, and the help source format reference in the
    -' help viewer's help ("file topic", opened at that topic).
    -CONST HELP_FILE   = "helpedit.hlp"
    -CONST FORMAT_HELP = "dvxhelp.hlp help.format"
    -CONST HASH_MUL    = 31
    -CONST HASH_MOD    = 16777213
    +' Help Editor's own help, and the help source format reference: a topic
    +' of the Help Viewer's help, in its folder under the programs folder
    +CONST HELP_FILE    = "helpedit.hlp"
    +CONST VIEWER_HELP  = "\kpunch\dvxhelp\dvxhelp.hlp"
    +CONST FORMAT_TOPIC = "help.format"
    +CONST HASH_MUL     = 31
    +CONST HASH_MOD     = 16777213
     
     DIM SHARED currentFile AS STRING
     DIM SHARED hlpFile AS STRING
    @@ -371,7 +372,7 @@ END SUB
     
     
     SUB mnuHelpFormat_Click
    -    HelpView FORMAT_HELP
    +    HelpViewTopic ShellProgramsFolder$() + VIEWER_HELP, FORMAT_TOPIC
     END SUB
     
     
    diff --git a/src/apps/kpunch/pkgbuild/pkgbuild.dhs b/src/apps/kpunch/pkgbuild/pkgbuild.dhs
    index b349496d..14424e99 100644
    --- a/src/apps/kpunch/pkgbuild/pkgbuild.dhs
    +++ b/src/apps/kpunch/pkgbuild/pkgbuild.dhs
    @@ -98,7 +98,7 @@ Choose the folder whose files go into the package (Browse), and say how it is la
     
     .list
     .item The folder holds this package's own files: everything in the folder (and in the folders inside it) goes into the package's own folder in DVX, which depends on its kind and package name. The page shows where, and lists each file.
    -.item The folder is laid out like the DVX folder: each file goes in the same place in DVX as it has in the folder. Use this when the package puts files in more than one place, for example a program with its help in another folder.
    +.item The folder is laid out like the DVX folder: each file goes in the same place in DVX as it has in the folder. Use this when the package puts files in more than one place, for example a program with its help in another folder, or with a program for the DOS prompt in DVX's SYSTEM folder.
     .endlist
     
     Where each kind of package keeps its files, for a package named acme.chess:
    @@ -117,7 +117,7 @@ Where each kind of package keeps its files, for a package named acme.chess:
       saver                SHARED\SAVERS\ACME
     .endtable
     
    -A package may put files only in its own kind's folder, so it cannot change DVX or another publisher's files. Build Package says so when a file is somewhere else. A file named PACKAGE.INI or PACKAGE.SIG at the top of the folder is left out.
    +A package may put files only in its own kind's folder and in DVX's SYSTEM folder (but not in SYSTEM's KEYS and PACKAGES folders), and Software will not install one that has a file another package already has, so it cannot change DVX or another publisher's files. Build Package says so when a file is somewhere else. A file named PACKAGE.INI or PACKAGE.SIG at the top of the folder is left out.
     
     .topic pkgbuild.signing
     .title Signing a Package
    diff --git a/src/apps/kpunch/progman/userHelp.dhs b/src/apps/kpunch/progman/userHelp.dhs
    index 3d21cdd7..7f2c7bc0 100644
    --- a/src/apps/kpunch/progman/userHelp.dhs
    +++ b/src/apps/kpunch/progman/userHelp.dhs
    @@ -546,7 +546,7 @@ File > Page Setup... chooses how text is laid out on paper: the margins, how man
     
     Pictures print as large as fits on the page: in color on a color printer (one with a color ribbon or cartridge), and in shades of grey on the others. Printing pictures needs your printer's model to be set (see Changing Settings); with the setting for text only, a program tells you so.
     
    -Some programs draw whole pages of their own, such as reports with charts. On a PostScript printer the printer draws those pages itself, as sharp as it can print. On other printers their text prints crisper with the Printer Fonts add-on installed (see Adding and Removing Software).
    +Some programs draw whole pages of their own, such as reports with charts. On a PostScript printer the printer draws those pages itself, as sharp as it can print. On other printers their text is made from DVX's fonts: DejaVu Sans, Sans Mono, and Serif have sizes made for printing, so they print crisp from 6 to 24 points; other fonts, and larger text, are enlarged and smoothed, a little softer.
     
     .topic user.programs
     .title Running Programs
    @@ -920,11 +920,15 @@ When a program tries to read a drive that has no disk in it, or to write to a wr
     
     .h2 Installing, upgrading, and removing DVX
     
    -DVX is installed with SETUP, from the DVX CD or from Disk 1 of the DVX floppy disks: change to that drive and type SETUP. After the licence, Setup asks what you would like to do: Install DVX, Upgrade or repair DVX, or Remove DVX. Click the picture or the button of your choice. To install, choose the folder for DVX (type it, or click Browse... to pick it; a folder that does not exist yet is made) and which fonts to install. Setup checks that there is room and unpacks DVX there, asking for each floppy disk it needs in turn. It can also make DVX start when the computer starts (it shows the line it adds to AUTOEXEC.BAT and keeps a copy of the old file as AUTOEXEC.DVX). Setup never writes to the CD or the floppy disks.
    +DVX is installed with SETUP, from the DVX CD or from Disk 1 of the DVX floppy disks: change to that drive and type SETUP. After the licence, Setup asks what you would like to do: Install DVX, Upgrade or repair DVX, or Remove DVX. Click the picture or the button of your choice. To install, choose the folder for DVX (type it, or click Browse... to pick it; a folder that does not exist yet is made), then Next: what to install, that is the fonts, the parts of DVX, and the add-ons you want. Setup checks that there is room and unpacks DVX there, asking for each floppy disk it needs in turn. It can also make DVX start when the computer starts (it shows the line it adds to AUTOEXEC.BAT and keeps a copy of the old file as AUTOEXEC.DVX). Setup never writes to the CD or the floppy disks.
     
    -The fonts are up to you. None keeps DVX to its own font; the base set adds DejaVu Sans, Sans Mono, and Serif in all their styles (about 2 MB); and All adds every font that comes with DVX (about 8 MB). A program that asks for a font you did not install shows its text in DVX's own font instead, unless it carries that font itself (some programs do, such as Welcome and the DVX Demo). Every font file holds its own licence: the Licence button of the Font dialog shows it. With None or the base set, Setup does not ask for the last floppy disks, which hold only fonts.
    +The fonts are up to you. None keeps DVX to its own font; the base set adds DejaVu Sans, Sans Mono, and Serif in all their styles, in sizes for the screen and for printing (about 8 MB). A program that asks for a font you did not install shows its text in DVX's own font instead, unless it carries that font itself (some programs do, such as Welcome and the DVX Demo). Every font file holds its own licence: the Licence button of the Font dialog shows it. With None, Setup does not ask for the last floppy disks, which hold only fonts; you can add the base set later from the package server in Software (as DVX Fonts), or by running Setup again. Software lists DVX and DVX Fonts among the installed software, and offers updates to them as they are published.
     
    -To upgrade or repair DVX, run Setup again and choose Upgrade or repair DVX, with the folder DVX is in. (When DVX is in the usual folder, such as C:\DVX, Setup says so and suggests this choice.) Your settings and saved files (the USER folder) are kept, unless you tick Start over to remove them. Setup starts with the fonts that folder already has; choose others to add fonts or remove them. If you choose Install DVX for a folder that already has DVX, Setup offers to upgrade it instead.
    +DVX comes in parts. The desktop and the everyday programs (Program Manager, File Manager, Notepad, Clock, the Image Viewer, the Help Viewer, Control Panel, and Software) are always installed; the other parts each have a box to tick: DVX BASIC (for writing your own programs, with BASCOMP to build them at the DOS prompt), Databases (for BASIC programs that keep their data in a database), Networking (network cards, the web, and the package server), Communications (the Terminal; it needs Networking), Media (the Media Player and the Sound Recorder), Developer Tools (the Icon Editor, the Resource Editor, the Help Editor, the Package Builder, and the Widget Showcase, with DVXRES and DVXHLPC for the DOS prompt), and Demos (the DVX Demo and the BASIC Demo). Point at one to see what it holds. A part you leave out takes no disk space and leaves more memory for your programs. To add or remove parts later, run Setup again. Software lists each part as one package, whose details name what it holds; without Networking, Software installs packages from files only.
    +
    +The CD also has add-ons, which Setup offers with a box to tick for each: More Fonts (more than forty more typefaces, about 8 MB) and DVX General MIDI (instruments for MIDI music on a Gravis UltraSound or a Sound Blaster AWE32 or AWE64, about 4 MB). They are installed like any other add-on, so Software lists them and can remove them; to add one later, choose Install from File in Software and open it from the ADDONS folder of the CD. The floppy disks have no add-ons: get them from the package server in Software instead.
    +
    +To upgrade or repair DVX, run Setup again and choose Upgrade or repair DVX, with the folder DVX is in. (When DVX is in the usual folder, such as C:\DVX, Setup says so and suggests this choice.) Your settings and saved files (the USER folder) are kept, unless you tick Start over to remove them. Setup starts with the fonts, parts, and add-ons that folder already has; choose others to add or remove them. A part you untick is removed, unless other software in the folder needs it (Setup then tells you it keeps it). If you choose Install DVX for a folder that already has DVX, Setup offers to upgrade it instead.
     
     To remove DVX, run Setup and choose Remove DVX, with the folder DVX is in. The SETUP in the DVX folder (for example C:\DVX\SETUP) can remove DVX too, from its own folder only. Files you added to the folder yourself are not removed, and the USER folder is only removed if you ask.
     
    diff --git a/src/apps/kpunch/software/software.bas b/src/apps/kpunch/software/software.bas
    index 03c12f0a..737aaf15 100644
    --- a/src/apps/kpunch/software/software.bas
    +++ b/src/apps/kpunch/software/software.bas
    @@ -30,7 +30,9 @@
     ' it: swNet for the catalogue and fetching, swUpd for the updates, swMine
     ' for publishing and the account's list, so each list keeps its own
     ' answer; all three are opened again when the server's settings change
    -' (swServer.frm, which leaves swServerChanged True).  Installing asks first (swPlan.frm,
    +' (swServer.frm, which leaves swServerChanged True).  The server's
    +' library is in DVX's optional Networking part: without it (swOnline
    +' False) none is opened and nothing calls the library.  Installing asks first (swPlan.frm,
     ' which leaves its answer in swPlanAccepted) and removing asks about what
     ' needs the packages and what only they needed; both run as a job
     ' SwRunJob runs a slice at a time while the progress window (swProg.frm)
    @@ -39,14 +41,17 @@
     
     OPTION EXPLICIT
     
    -CONST SW_TITLE      = "Software"
    -CONST SW_TAB        = CHR$(9)          ' between the cells of a pkg.bas row
    -CONST SW_EOL        = CHR$(10)         ' after each row
    -CONST SW_DVX_ID     = "kpunch.dvx"     ' DVX itself: removed with Setup
    -CONST SW_FILTER     = "Packages (*.dpk)|All Files (*.*)"
    -CONST SW_KEY_FILTER = "Publisher Keys (*.key)|All Files (*.*)"
    -CONST SW_LIST_LEAD  = "    "           ' before each name in a message's list
    -CONST SW_SLICE_MS   = 60               ' job work between screen updates
    +CONST SW_TITLE       = "Software"
    +CONST SW_TAB         = CHR$(9)        ' between the cells of a pkg.bas row
    +CONST SW_EOL         = CHR$(10)       ' after each row
    +CONST SW_DVX_ID      = "kpunch.dvx"   ' DVX itself: removed with Setup
    +CONST SW_NET_LIB     = "pkgnet"       ' the package server's library (pkgnet.bas)
    +CONST SW_KIND_BUNDLE = "bundle"       ' a package that stands for the packages it needs
    +CONST SW_OFFLINE     = "The package server needs DVX's Networking part. To add it, run Setup from DVX's CD or Disk 1 and choose Upgrade or repair."
    +CONST SW_FILTER      = "Packages (*.dpk)|All Files (*.*)"
    +CONST SW_KEY_FILTER  = "Publisher Keys (*.key)|All Files (*.*)"
    +CONST SW_LIST_LEAD   = "    "         ' before each name in a message's list
    +CONST SW_SLICE_MS    = 60             ' job work between screen updates
     
     ' The job the progress window shows (swJob)
     CONST SW_JOB_NONE = 0
    @@ -60,6 +65,7 @@ DIM SHARED swNet AS LONG
     DIM SHARED swUpd AS LONG
     DIM SHARED swMine AS LONG
     DIM SHARED swServerChanged AS BOOLEAN
    +DIM SHARED swOnline AS BOOLEAN
     
     ' The install question's answer, the job running, the server's handle
     ' it is on, and whether its Cancel was pressed
    diff --git a/src/apps/kpunch/software/software.dbp b/src/apps/kpunch/software/software.dbp
    index 4dd9be32..b858a174 100644
    --- a/src/apps/kpunch/software/software.dbp
    +++ b/src/apps/kpunch/software/software.dbp
    @@ -15,8 +15,9 @@ File3 = ../../../include/basic/help.bas
     File4 = ../../../include/basic/pkg.bas
     File5 = ../../../include/basic/pkgnet.bas
     File6 = ../../../include/basic/shell.bas
    -File7 = software.bas
    -Count = 8
    +File7 = ../../../include/basic/system.bas
    +File8 = software.bas
    +Count = 9
     
     [Forms]
     File0 = software.frm
    diff --git a/src/apps/kpunch/software/software.dhs b/src/apps/kpunch/software/software.dhs
    index 3737e32b..19edef22 100644
    --- a/src/apps/kpunch/software/software.dhs
    +++ b/src/apps/kpunch/software/software.dhs
    @@ -51,6 +51,8 @@ Software has four pages: Installed (what is in DVX), Available (what the package
     
     The Installed list shows every package in DVX, DVX itself included. Each row has the package's name, version, kind, a short description, and its package name (such as acme.chess), which tells two packages with the same name apart. Click a column's heading to sort the list by it.
     
    +A bundle is a package that stands for a group of others, such as each part of DVX that Setup installed (DVX BASIC, Networking, Media, and so on). The list shows the bundle as one row; its details name the packages it holds, and removing it removes them.
    +
     .topic software.install
     .title Installing a Package
     .toc 1 Installing
    @@ -94,7 +96,7 @@ Software tells you why, for example:
     
     .h1 Removing a Package
     
    -Choose the package in the list, then Remove. If other packages need it, Software names them: they are removed with it, or nothing is removed. Packages that were installed only because the removed ones needed them are offered for removal too.
    +Choose the package in the list, then Remove. If other packages need it, Software names them: they are removed with it, or nothing is removed. Packages that were installed only because the removed ones needed them are offered for removal too, such as the programs and libraries of a part of DVX when you remove the part. Setup puts a part back (run it again and choose Upgrade or repair DVX).
     
     Files you changed after the package was installed (a data file you edited, say) are left in place, and Software asks whether to delete them as well. Folders the package made are removed when they end up empty.
     
    @@ -121,6 +123,8 @@ For a package on the Available or Updates page, Details shows what the package s
     
     .h1 Packages from the Package Server
     
    +The package server needs the Networking part of DVX. Without it, the Available, Updates, and Published pages say so and their buttons are off; Software still installs packages from files. To add Networking, run Setup again and choose Upgrade or repair DVX.
    +
     The Available page lists every package the package server offers: its name, version, kind, a short description, and the version you have, if any. The list is read from the server the first time you open the page; Refresh reads it again. Type in the Find box to show only the packages whose name or description has those words.
     
     Choose a package and Install. Software fetches it from the server with any packages it needs that you do not have, then shows what will be installed and installs it exactly as for a file (see Installing a Package). A fetch that is stopped, or cut off by the network, goes on from where it stopped the next time.
    diff --git a/src/apps/kpunch/software/software.frm b/src/apps/kpunch/software/software.frm
    index 760248b6..df9c6b26 100644
    --- a/src/apps/kpunch/software/software.frm
    +++ b/src/apps/kpunch/software/software.frm
    @@ -488,6 +488,11 @@ END SUB
     ' The Available page: the catalogue, read from the server the first time
     ' (or again when ask is True), shown as the Find box narrows it.
     SUB RefreshAvailable(BYVAL ask AS BOOLEAN)
    +    IF NOT swOnline THEN
    +        lblStatus.Caption = SW_OFFLINE
    +        EXIT SUB
    +    END IF
    +
         IF ask OR NOT swHaveCatalogue THEN
             IF NOT SwNetRun(swNet, PkgNetCatalogue(swNet), "Reading the package list") THEN
                 lblStatus.Caption = "The package server could not be reached."
    @@ -507,18 +512,46 @@ END SUB
     
     
     ' The installed packages, as the list shows them: name, version, kind,
    -' summary, id.
    +' summary, id.  A bundle's member installed only for it is left out: the
    +' bundle stands for it (and its details list it).
     SUB RefreshInstalled
         DIM row AS VARIANT
         DIM cells AS VARIANT
    +    DIM dep AS VARIANT
    +    DIM depCells AS VARIANT
    +    DIM installed AS STRING
    +    DIM members AS STRING
         DIM rows AS STRING
         DIM count AS LONG
     
    -    FOR EACH row IN Split(PkgInstalledRows$(swSession), SW_EOL)
    +    installed = PkgInstalledRows$(swSession)
    +
    +    ' The installed bundles' members, each between line ends
    +    members = SW_EOL
    +
    +    FOR EACH row IN Split(installed, SW_EOL)
             IF row <> "" THEN
                 cells = Split(row, SW_TAB)
    -            rows  = rows & cells(1) & SW_TAB & cells(2) & SW_TAB & cells(3) & SW_TAB & cells(4) & SW_TAB & cells(0) & SW_EOL
    -            count = count + 1
    +
    +            IF cells(3) = SW_KIND_BUNDLE THEN
    +                FOR EACH dep IN Split(PkgDependsRows$(swSession, cells(0)), SW_EOL)
    +                    IF dep <> "" THEN
    +                        depCells = Split(dep, SW_TAB)
    +                        members  = members & depCells(0) & SW_EOL
    +                    END IF
    +                NEXT dep
    +            END IF
    +        END IF
    +    NEXT row
    +
    +    FOR EACH row IN Split(installed, SW_EOL)
    +        IF row <> "" THEN
    +            cells = Split(row, SW_TAB)
    +
    +            IF InStr(members, SW_EOL & cells(0) & SW_EOL) = 0 OR PkgField$(swSession, cells(0), PKG_FIELD_EXPLICIT) <> "no" THEN
    +                rows  = rows & cells(1) & SW_TAB & cells(2) & SW_TAB & cells(3) & SW_TAB & cells(4) & SW_TAB & cells(0) & SW_EOL
    +                count = count + 1
    +            END IF
             END IF
         NEXT row
     
    @@ -530,6 +563,11 @@ END SUB
     ' The Published page: what the account has sent, each version with its
     ' review, read from the server now.
     SUB RefreshMine
    +    IF NOT swOnline THEN
    +        lblMine.Caption = SW_OFFLINE
    +        EXIT SUB
    +    END IF
    +
         IF PkgNetUser$() = "" THEN
             lblMine.Caption = "Give your account on the package server in File > Package Server to see what you have published."
             EXIT SUB
    @@ -550,6 +588,11 @@ END SUB
     ' The Updates page: what the server has that is newer than what is
     ' installed, asked for now.
     SUB RefreshUpdates
    +    IF NOT swOnline THEN
    +        lblUpdates.Caption = SW_OFFLINE
    +        EXIT SUB
    +    END IF
    +
         IF NOT SwNetRun(swUpd, PkgNetUpdates(swUpd), "Checking for updates") THEN
             lblUpdates.Caption = "The package server could not be reached."
             EXIT SUB
    @@ -596,6 +639,25 @@ FUNCTION SelectedId$() AS STRING
     END FUNCTION
     
     
    +' Without the package server's library: its menu commands and buttons
    +' off.
    +SUB ServerOff
    +    mnuPublish.Enabled     = False
    +    mnuServer.Enabled      = False
    +    mnuNetInstall.Enabled  = False
    +    mnuCheck.Enabled       = False
    +    mnuUpdateAll.Enabled   = False
    +    txtFind.Enabled        = False
    +    cmdRefresh.Enabled     = False
    +    cmdNetDetails.Enabled  = False
    +    cmdNetInstall.Enabled  = False
    +    cmdCheck.Enabled       = False
    +    cmdUpdateAll.Enabled   = False
    +    cmdMineRefresh.Enabled = False
    +    cmdPublish.Enabled     = False
    +END SUB
    +
    +
     ' Fill a list, its first row chosen (what Details, Install, and Remove
     ' act on until another is).
     SUB ShowRows(lv AS Control, BYVAL rows AS STRING)
    @@ -610,16 +672,26 @@ END SUB
     SUB SoftwareForm_Load
         swJob     = SW_JOB_NONE
         swSession = PkgOpen()
    -    swNet     = PkgNetOpen("")
    -    swUpd     = PkgNetOpen("")
    -    swMine    = PkgNetOpen("")
    +    swOnline  = SysLibraryLoaded(SW_NET_LIB)
     
    -    IF swSession = 0 OR swNet = 0 OR swUpd = 0 OR swMine = 0 THEN
    +    IF swOnline THEN
    +        swNet  = PkgNetOpen("")
    +        swUpd  = PkgNetOpen("")
    +        swMine = PkgNetOpen("")
    +    END IF
    +
    +    IF swSession = 0 OR (swOnline AND (swNet = 0 OR swUpd = 0 OR swMine = 0)) THEN
             SwError "There is not enough memory to start Software."
             Unload SoftwareForm
             EXIT SUB
         END IF
     
    +    ' Without the package server's library, what needs the server is off
    +    ' (its pages say why when shown)
    +    IF NOT swOnline THEN
    +        ServerOff
    +    END IF
    +
         lvInstalled.SetColumns SW_COLUMNS
         lvAvailable.SetColumns SW_NET_COLUMNS
         lvUpdates.SetColumns SW_UPD_COLUMNS
    @@ -641,13 +713,18 @@ SUB SoftwareForm_Unload
         Unload DetailForm
         Unload ProgForm
         Unload ServerForm
    -    PkgNetClose swMine
    -    PkgNetClose swUpd
    -    PkgNetClose swNet
    +
    +    IF swOnline THEN
    +        PkgNetClose swMine
    +        PkgNetClose swUpd
    +        PkgNetClose swNet
    +    END IF
    +
         PkgClose swSession
     END SUB
     
     
    +
     ' A page shown: the server's lists are read the first time they are
     ' wanted.
     SUB tabs_Click
    diff --git a/src/apps/kpunch/software/swServer.frm b/src/apps/kpunch/software/swServer.frm
    index c82f7368..2c8997b7 100644
    --- a/src/apps/kpunch/software/swServer.frm
    +++ b/src/apps/kpunch/software/swServer.frm
    @@ -129,10 +129,17 @@ SUB cmdServerOk_Click
     END SUB
     
     
    +' (Loaded with the program: without the package server's library, which
    +' Software then never shows this window for, there is nothing to read.)
     SUB ServerForm_Load
    +    txtPassword.PasswordChar = CHR$(DLG_PASSWORD_MASK)
    +
    +    IF NOT SysLibraryLoaded(SW_NET_LIB) THEN
    +        EXIT SUB
    +    END IF
    +
         txtServer.Text           = PkgNetServer$()
         txtPort.Text             = CStr(PkgNetPort())
         txtUser.Text             = PkgNetUser$()
    -    txtPassword.PasswordChar = CHR$(DLG_PASSWORD_MASK)
         txtPassword.Text         = PkgNetPassword$()
     END SUB
    diff --git a/src/apps/kpunch/welcome/welcome.dbp b/src/apps/kpunch/welcome/welcome.dbp
    index 6a08b6e9..b0e00e34 100644
    --- a/src/apps/kpunch/welcome/welcome.dbp
    +++ b/src/apps/kpunch/welcome/welcome.dbp
    @@ -8,9 +8,10 @@ Description = Introduces DVX when it starts for the first time
     Icon = ICON32.BMP
     
     [Modules]
    -File0 = ../../../include/basic/help.bas
    -File1 = ../../../include/basic/shell.bas
    -Count = 2
    +File0 = ../../../include/basic/fileops.bas
    +File1 = ../../../include/basic/help.bas
    +File2 = ../../../include/basic/shell.bas
    +Count = 3
     
     [Forms]
     File0 = welcome.frm
    diff --git a/src/apps/kpunch/welcome/welcome.dhs b/src/apps/kpunch/welcome/welcome.dhs
    index 6b250d83..5ef97c43 100644
    --- a/src/apps/kpunch/welcome/welcome.dhs
    +++ b/src/apps/kpunch/welcome/welcome.dhs
    @@ -50,6 +50,8 @@ The Welcome window appears when DVX starts for the first time. It shows what DVX
       Read the User Guide    Opens the DVX help, which explains the desktop, windows, menus, and the programs that come with DVX.
     .endtable
     
    +Write a program and See it in action are there when DVX BASIC and the Demos are installed: both are optional parts of DVX, which Setup adds (run it again and choose Upgrade or repair DVX).
    +
     Click a tile's picture or its button, or press Alt with the underlined letter. Under the tiles a tip about DVX changes every few seconds. Close closes the Welcome window.
     
     Press F1 at any time, in any program, to open the help for what you are doing.
    diff --git a/src/apps/kpunch/welcome/welcome.frm b/src/apps/kpunch/welcome/welcome.frm
    index ab2012c8..1ddd078a 100644
    --- a/src/apps/kpunch/welcome/welcome.frm
    +++ b/src/apps/kpunch/welcome/welcome.frm
    @@ -215,15 +215,15 @@ OPTION EXPLICIT
     CONST HELP_FILE = "welcome.hlp"
     
     ' The programs the tiles start, in DVX's programs folder
    -CONST APP_BASIC   = "\kpunch\dvxbasic\dvxbasic.app"
    -CONST APP_CPANEL  = "\kpunch\cpanel\cpanel.app"
    -CONST APP_FILES   = "\kpunch\filemgr\filemgr.app"
    -CONST APP_DEMO    = "\kpunch\dvxdemo\dvxdemo.app"
    -CONST APP_HELP    = "\kpunch\dvxhelp\dvxhelp.app"
    -CONST APP_CLOCK   = "\kpunch\clock\clock.app"
    -CONST APP_NOTEPAD = "\kpunch\notepad\notepad.app"
    -CONST APP_IMAGES  = "\kpunch\imgview\imgview.app"
    -CONST APP_ICONS   = "\kpunch\iconed\iconed.app"
    +CONST APP_BASIC    = "\kpunch\dvxbasic\dvxbasic.app"
    +CONST APP_CPANEL   = "\kpunch\cpanel\cpanel.app"
    +CONST APP_FILES    = "\kpunch\filemgr\filemgr.app"
    +CONST APP_DEMO     = "\kpunch\dvxdemo\dvxdemo.app"
    +CONST APP_HELP     = "\kpunch\dvxhelp\dvxhelp.app"
    +CONST APP_CLOCK    = "\kpunch\clock\clock.app"
    +CONST APP_NOTEPAD  = "\kpunch\notepad\notepad.app"
    +CONST APP_IMAGES   = "\kpunch\imgview\imgview.app"
    +CONST APP_SOFTWARE = "\kpunch\software\software.app"
     
     ' The banner's and the tour's gradient (red, green, blue at the top and
     ' at the bottom), the title's gold, the text's pale blue, the shadow
    @@ -286,6 +286,8 @@ SUB WelcomeForm_Load
         imgFiles.Picture = ProgramPath$(APP_FILES)
         imgDemo.Picture  = ProgramPath$(APP_DEMO)
         imgGuide.Picture = ProgramPath$(APP_HELP)
    +    tileCode.Visible = Installed(APP_BASIC)
    +    tileDemo.Visible = Installed(APP_DEMO)
         picBanner.AutoRedraw = True
         picTour.AutoRedraw   = True
         DrawBanner
    @@ -314,23 +316,37 @@ SUB SaveChoice
     END SUB
     
     
    -' A program of DVX's, by its place in the programs folder
    +'' A program of DVX's, by its place in the programs folder
     FUNCTION ProgramPath$(BYVAL relPath AS STRING)
         ProgramPath$ = ShellProgramsFolder$() + relPath
     END FUNCTION
     
     
    +' Whether a program of DVX's is installed: DVX BASIC and the DVX Demo are
    +' in optional parts of DVX.
    +FUNCTION Installed(BYVAL relPath AS STRING) AS BOOLEAN
    +    Installed = FileOpsGetAttr(ProgramPath$(relPath)) >= 0
    +END FUNCTION
    +
    +
    +' The icon a tour page shows for a program of DVX's: its own, or (not
    +' installed) this program's.
    +FUNCTION ProgramIcon$(BYVAL relPath AS STRING)
    +    ProgramIcon$ = IIF(Installed(relPath), ProgramPath$(relPath), ShellAppPath$())
    +END FUNCTION
    +
    +
     ' The tour's pages: each a title, the program whose icon it shows (or
     ' several, separated by "|": Program Manager has no icon of its own, its
     ' page shows a few of the programs it starts) and its text (wrapped to
     ' the page when shown).
     SUB FillTour
         SetPage 1, "A quick tour of DVX", ShellAppPath$(), "DVX turns your DOS computer into a desktop of windows, menus, and icons. This tour shows the main parts. Press Next to begin."
    -    SetPage 2, "Program Manager", ProgramPath$(APP_CLOCK) + "|" + ProgramPath$(APP_NOTEPAD) + "|" + ProgramPath$(APP_IMAGES) + "|" + ProgramPath$(APP_ICONS), "Your programs live in groups. Double-click an icon to start it, or type its name in Search. File > New Group makes groups of your own."
    -    SetPage 3, "Windows", ProgramPath$(APP_DEMO), "Every program runs in a window. Drag the title bar to move it, or an edge to change its size. Alt+Tab switches windows; Alt+F4 closes one."
    +    SetPage 2, "Program Manager", ProgramPath$(APP_CLOCK) + "|" + ProgramPath$(APP_NOTEPAD) + "|" + ProgramPath$(APP_IMAGES) + "|" + ProgramPath$(APP_SOFTWARE), "Your programs live in groups. Double-click an icon to start it, or type its name in Search. File > New Group makes groups of your own."
    +    SetPage 3, "Windows", ProgramIcon$(APP_DEMO), "Every program runs in a window. Drag the title bar to move it, or an edge to change its size. Alt+Tab switches windows; Alt+F4 closes one."
         SetPage 4, "Your files", ProgramPath$(APP_FILES), "File Manager shows your disks and folders. Copy, move, rename, and delete files with the mouse or the keys; double-click a file to open it."
         SetPage 5, "Make it yours", ProgramPath$(APP_CPANEL), "Control Panel sets colors and themes, wallpaper, the mouse, sounds, joysticks, printers, and the screen. Most changes show right away."
    -    SetPage 6, "Write your own programs", ProgramPath$(APP_BASIC), "DVX BASIC is all you need to build programs: draw a form, add buttons and lists, write some code, and press F5. Make an .app to share it."
    +    SetPage 6, "Write your own programs", ProgramIcon$(APP_BASIC), "DVX BASIC is all you need to build programs: draw a form, add buttons and lists, write some code, and press F5. Make an .app to share it." + IIF(Installed(APP_BASIC), "", " It is an optional part of DVX: run Setup again to add it.")
         SetPage 7, "Help is always there", ProgramPath$(APP_HELP), "Press F1 in any program for help about it. The Help Viewer holds every guide, with an index and a search. Have fun with DVX!"
     END SUB
     
    @@ -423,11 +439,13 @@ FUNCTION WrapText(BYVAL pic AS Control, BYVAL text AS STRING, BYVAL maxWidth AS
     END FUNCTION
     
     
    -' pic's text in Welcome's family at size, bold or not.
    +' pic's text in Welcome's family at size, bold or not: the style first,
    +' so the size is picked from the style's own (Welcome carries its bold at
    +' 24 and 36 pixels only).
     SUB UseFont(BYVAL pic AS Control, BYVAL size AS INTEGER, BYVAL bold AS BOOLEAN)
         pic.FontName = FONT_FAMILY
    -    pic.FontSize = size
         pic.FontBold = bold
    +    pic.FontSize = size
     END SUB
     
     
    diff --git a/src/fonts/PACKAGE.INI b/src/fonts/PACKAGE.INI
    new file mode 100644
    index 00000000..46b4fe60
    --- /dev/null
    +++ b/src/fonts/PACKAGE.INI
    @@ -0,0 +1,23 @@
    +; DVX Fonts, the base set of fonts (fonts.ini's "base" set: DejaVu Sans,
    +; Sans Mono, and Serif in every style, with their print sizes) as a
    +; package, kpunch.basefont (PKG_BASE_FONTS_ID, pkgManifest.h).  Setup
    +; installs these files with DVX (its package's second tier) unless told
    +; to install no fonts, and with them the package's record, which mkDist
    +; writes into that tier (dvxpkg record), so DVX knows the base set is
    +; installed: Software lists it, a newer version on the package server is
    +; offered as an update, and someone who installed no fonts can add it
    +; from the server.  mkDist also builds it as dist/BASEFONT.DPK for the
    +; server, from the same files.  version = dvx: it is made from the same
    +; manifest as the rest of DVX, so it takes DVX's version (mkDist fills it
    +; in).  Each .DFN carries the DejaVu licence text.
    +[package]
    +format    = 1
    +id        = kpunch.basefont
    +name      = DVX Fonts
    +version   = dvx
    +kind      = font
    +summary   = DejaVu Sans, Sans Mono, and Serif in every style, in sizes for the screen and for printing.
    +publisher = Kangaroo Punch Studios
    +author    = DejaVu fonts team
    +licence   = Bitstream Vera and Arev (DejaVu changes public domain)
    +homepage  = https://dejavu-fonts.github.io
    diff --git a/src/fonts/fonts.ini b/src/fonts/fonts.ini
    index 327e4f10..14fd85f5 100644
    --- a/src/fonts/fonts.ini
    +++ b/src/fonts/fonts.ini
    @@ -1,5 +1,9 @@
     ; fonts.ini -- the fonts DVX ships, made by mkfont (src/tools/mkfont.c,
    -; whose header documents every key) into bin/fonts at build time.
    +; whose header documents every key) into bin/shared/fonts/kpunch (DVX's
    +; vendor folder: the base set and More Fonts are font packages) at build
    +; time.
    +; The SDK guide's "Making fonts" chapter (sdkGuide.dhs) says how to add a
    +; face and what it needs.
     ;
     ; [fonts] lists the .DFN files in order: 8.3 base name = family name.
     ; [licences] names the licences and the texts under toolchains/fonts each
    @@ -19,10 +23,22 @@
     ; text without smoothing keeps their weight) and Sans Bold at 8.  Sans
     ; Mono Bold keeps its hinting (cut from gray, its small 0 fills in).
     ;
    -; set = base marks the base set: DejaVu Sans, Sans Mono, and Serif in
    -; every style (DVX draws a character a font lacks from the first two).
    -; Setup installs no fonts (DVX's own only), the base set, or all of them;
    -; the base set's files come first in the package (mkfont -l lists them).
    +; print lists the sizes made 1-bit for printing, in the same file as the
    +; screen sizes, so paper and screen always come from the same face.  The
    +; base set has them from 25 to 100 pixels: about 6 to 24 points at 300
    +; dpi and 10 to 40 at 180 (12 points is 50 dots at 300 dpi, 30 at 180).
    +; Printing picks the smallest size at least as large as the text in
    +; printer dots; a face without print sizes is scaled up from its largest
    +; screen size.  A file holds at most DFN_STRIKE_MAX (64) strikes -- its
    +; sizes, smoothed sizes, and print sizes together; the base faces have 31.
    +;
    +; set says where each file ships: base, the DVX Fonts package
    +; (kpunch.basefont, PACKAGE.INI beside this file: DejaVu Sans, Sans Mono,
    +; and Serif in every style; DVX draws a character a font lacks from the
    +; first two), which Setup installs with DVX unless told to install no
    +; fonts and the package server can update; more, the More Fonts add-on
    +; (MOREFONT.DPK, src/addons/fonts/morefont).  mkfont -l lists a set's files; it stops when a font names
    +; no set, or a base font has no print sizes.
     ;
     ; Names: an 8.3 base name is the family's short form plus B (bold), I
     ; (italic), or Z (bold italic).  Swiss, Roman, Schoolbook, Typewriter,
    @@ -203,6 +219,7 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = native
     
     [DJVSANSB]
    @@ -213,6 +230,7 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Bold.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = native
     graycut  = 8
     
    @@ -224,6 +242,7 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-Oblique.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = auto
     
     [DJVSANSZ]
    @@ -234,9 +253,11 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSans-BoldOblique.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = auto
     
     [DJVSCND]
    +set      = more
     style    = regular
     licence  = DEJAVU
     boxdraw  = true
    @@ -245,6 +266,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [DJVSCNDB]
    +set      = more
     style    = bold
     licence  = DEJAVU
     boxdraw  = true
    @@ -253,6 +275,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [DJVSCNDI]
    +set      = more
     style    = italic
     licence  = DEJAVU
     boxdraw  = true
    @@ -261,6 +284,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [DJVSCNDZ]
    +set      = more
     style    = bolditalic
     licence  = DEJAVU
     boxdraw  = true
    @@ -276,6 +300,7 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = native
     
     [DJVMONOB]
    @@ -286,6 +311,7 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-Bold.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = native
     
     [DJVMONOI]
    @@ -296,6 +322,7 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-Oblique.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = auto
     
     [DJVMONOZ]
    @@ -306,6 +333,7 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSansMono-BoldOblique.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = auto
     
     [DJVSERF]
    @@ -316,6 +344,7 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = native
     
     [DJVSERFB]
    @@ -326,6 +355,7 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-Bold.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = native
     graycut  = 12 13 14 15 16 18 20 24
     
    @@ -337,6 +367,7 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-Italic.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = auto
     
     [DJVSERFZ]
    @@ -347,9 +378,11 @@ boxdraw  = true
     source   = dejavu/dejavu-fonts-ttf-2.37/ttf/DejaVuSerif-BoldItalic.ttf
     sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
     hinting  = auto
     
     [DJVRCND]
    +set      = more
     style    = regular
     licence  = DEJAVU
     boxdraw  = true
    @@ -358,6 +391,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [DJVRCNDB]
    +set      = more
     style    = bold
     licence  = DEJAVU
     boxdraw  = true
    @@ -366,6 +400,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [DJVRCNDI]
    +set      = more
     style    = italic
     licence  = DEJAVU
     boxdraw  = true
    @@ -374,6 +409,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [DJVRCNDZ]
    +set      = more
     style    = bolditalic
     licence  = DEJAVU
     boxdraw  = true
    @@ -382,6 +418,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [ARIMO]
    +set      = more
     style    = regular
     licence  = CROSCORE
     boxdraw  = true
    @@ -390,6 +427,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [ARIMOB]
    +set      = more
     style    = bold
     licence  = CROSCORE
     boxdraw  = true
    @@ -398,6 +436,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [ARIMOI]
    +set      = more
     style    = italic
     licence  = CROSCORE
     boxdraw  = true
    @@ -406,6 +445,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [ARIMOZ]
    +set      = more
     style    = bolditalic
     licence  = CROSCORE
     boxdraw  = true
    @@ -414,6 +454,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [TINOS]
    +set      = more
     style    = regular
     licence  = CROSCORE
     boxdraw  = true
    @@ -422,6 +463,7 @@ sizes    = 16 18 20 24
     hinting  = native
     
     [TINOSB]
    +set      = more
     style    = bold
     licence  = CROSCORE
     boxdraw  = true
    @@ -430,6 +472,7 @@ sizes    = 16 18 20 24
     hinting  = native
     
     [TINOSI]
    +set      = more
     style    = italic
     licence  = CROSCORE
     boxdraw  = true
    @@ -438,6 +481,7 @@ sizes    = 16 18 20 24
     hinting  = native
     
     [TINOSZ]
    +set      = more
     style    = bolditalic
     licence  = CROSCORE
     boxdraw  = true
    @@ -446,6 +490,7 @@ sizes    = 16 18 20 24
     hinting  = native
     
     [COUSINE]
    +set      = more
     style    = regular
     licence  = CROSCORE
     boxdraw  = true
    @@ -454,6 +499,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [COUSINEB]
    +set      = more
     style    = bold
     licence  = CROSCORE
     boxdraw  = true
    @@ -462,6 +508,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [COUSINEI]
    +set      = more
     style    = italic
     licence  = CROSCORE
     boxdraw  = true
    @@ -470,6 +517,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [COUSINEZ]
    +set      = more
     style    = bolditalic
     licence  = CROSCORE
     boxdraw  = true
    @@ -478,6 +526,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [CLRSANS]
    +set      = more
     style    = regular
     licence  = CLEARSAN
     boxdraw  = true
    @@ -486,6 +535,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [CLRSANSB]
    +set      = more
     style    = bold
     licence  = CLEARSAN
     boxdraw  = true
    @@ -494,6 +544,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [CLRSANSI]
    +set      = more
     style    = italic
     licence  = CLEARSAN
     boxdraw  = true
    @@ -502,6 +553,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [CLRSANSZ]
    +set      = more
     style    = bolditalic
     licence  = CLEARSAN
     boxdraw  = true
    @@ -510,6 +562,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [GO]
    +set      = more
     style    = regular
     licence  = GOFONT
     boxdraw  = true
    @@ -518,6 +571,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [GOB]
    +set      = more
     style    = bold
     licence  = GOFONT
     boxdraw  = true
    @@ -526,6 +580,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [GOI]
    +set      = more
     style    = italic
     licence  = GOFONT
     boxdraw  = true
    @@ -534,6 +589,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [GOZ]
    +set      = more
     style    = bolditalic
     licence  = GOFONT
     boxdraw  = true
    @@ -542,6 +598,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [GOMONO]
    +set      = more
     style    = regular
     licence  = GOFONT
     boxdraw  = true
    @@ -550,6 +607,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [GOMONOB]
    +set      = more
     style    = bold
     licence  = GOFONT
     boxdraw  = true
    @@ -558,6 +616,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [GOMONOI]
    +set      = more
     style    = italic
     licence  = GOFONT
     boxdraw  = true
    @@ -566,6 +625,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [GOMONOZ]
    +set      = more
     style    = bolditalic
     licence  = GOFONT
     boxdraw  = true
    @@ -574,6 +634,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [DRDSERF]
    +set      = more
     style    = regular
     licence  = DROID
     boxdraw  = true
    @@ -582,6 +643,7 @@ sizes    = 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [DRDSERFB]
    +set      = more
     style    = bold
     licence  = DROID
     boxdraw  = true
    @@ -590,6 +652,7 @@ sizes    = 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [DRDSERFI]
    +set      = more
     style    = italic
     licence  = DROID
     boxdraw  = true
    @@ -598,6 +661,7 @@ sizes    = 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [DRDSERFZ]
    +set      = more
     style    = bolditalic
     licence  = DROID
     boxdraw  = true
    @@ -606,6 +670,7 @@ sizes    = 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [LEDGER]
    +set      = more
     style    = regular
     licence  = LEDGER
     boxdraw  = true
    @@ -626,6 +691,7 @@ source6  = x11bitstream/font-bitstream-75dpi-1.0.4/charR24.bdf
     sizes6   = native
     
     [LEDGERB]
    +set      = more
     style    = bold
     licence  = LEDGER
     boxdraw  = true
    @@ -646,6 +712,7 @@ source6  = x11bitstream/font-bitstream-75dpi-1.0.4/charB24.bdf
     sizes6   = native
     
     [LEDGERI]
    +set      = more
     style    = italic
     licence  = LEDGER
     boxdraw  = true
    @@ -666,6 +733,7 @@ source6  = x11bitstream/font-bitstream-75dpi-1.0.4/charI24.bdf
     sizes6   = native
     
     [LEDGERZ]
    +set      = more
     style    = bolditalic
     licence  = LEDGER
     boxdraw  = true
    @@ -686,6 +754,7 @@ source6  = x11bitstream/font-bitstream-75dpi-1.0.4/charBI24.bdf
     sizes6   = native
     
     [NOTOSRF]
    +set      = more
     style    = regular
     licence  = NOTOSERF
     boxdraw  = true
    @@ -694,6 +763,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [NOTOSRFB]
    +set      = more
     style    = bold
     licence  = NOTOSERF
     boxdraw  = true
    @@ -702,6 +772,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [NOTOSRFI]
    +set      = more
     style    = italic
     licence  = NOTOSERF
     boxdraw  = true
    @@ -710,6 +781,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [NOTOSRFZ]
    +set      = more
     style    = bolditalic
     licence  = NOTOSERF
     boxdraw  = true
    @@ -718,6 +790,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [RSLAB]
    +set      = more
     style    = regular
     licence  = RBTSLAB
     boxdraw  = true
    @@ -726,6 +799,7 @@ sizes    = 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [RSLABB]
    +set      = more
     style    = bold
     licence  = RBTSLAB
     boxdraw  = true
    @@ -734,6 +808,7 @@ sizes    = 9 10 11 12 13 14 15 16 18 20 24
     hinting  = auto
     
     [ATKINSN]
    +set      = more
     style    = regular
     licence  = ATKINSON
     boxdraw  = true
    @@ -742,6 +817,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [ATKINSNB]
    +set      = more
     style    = bold
     licence  = ATKINSON
     boxdraw  = true
    @@ -750,6 +826,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [ATKINSNI]
    +set      = more
     style    = italic
     licence  = ATKINSON
     boxdraw  = true
    @@ -758,6 +835,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [ATKINSNZ]
    +set      = more
     style    = bolditalic
     licence  = ATKINSON
     boxdraw  = true
    @@ -766,6 +844,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [FIRACOD]
    +set      = more
     style    = regular
     licence  = FIRACODE
     boxdraw  = true
    @@ -774,6 +853,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [FIRACODB]
    +set      = more
     style    = bold
     licence  = FIRACODE
     boxdraw  = true
    @@ -782,6 +862,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [JBMONO]
    +set      = more
     style    = regular
     licence  = JBMONO
     boxdraw  = true
    @@ -790,6 +871,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [JBMONOB]
    +set      = more
     style    = bold
     licence  = JBMONO
     boxdraw  = true
    @@ -798,6 +880,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [JBMONOI]
    +set      = more
     style    = italic
     licence  = JBMONO
     boxdraw  = true
    @@ -806,6 +889,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [JBMONOZ]
    +set      = more
     style    = bolditalic
     licence  = JBMONO
     boxdraw  = true
    @@ -814,6 +898,7 @@ sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
     hinting  = native
     
     [CNEUE]
    +set      = more
     style    = regular
     licence  = CNEUE
     boxdraw  = true
    @@ -823,6 +908,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [CNEUEB]
    +set      = more
     style    = bold
     licence  = CNEUE
     boxdraw  = true
    @@ -832,6 +918,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [CNEUEI]
    +set      = more
     style    = italic
     licence  = CNEUE
     boxdraw  = true
    @@ -841,6 +928,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [CNEUEZ]
    +set      = more
     style    = bolditalic
     licence  = CNEUE
     boxdraw  = true
    @@ -850,6 +938,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [CRELIEF]
    +set      = more
     style    = regular
     licence  = CRELIEF
     boxdraw  = true
    @@ -859,6 +948,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [CRELIEFB]
    +set      = more
     style    = bold
     licence  = CRELIEF
     boxdraw  = true
    @@ -868,6 +958,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [PATRICK]
    +set      = more
     style    = regular
     licence  = PATRICK
     boxdraw  = true
    @@ -877,6 +968,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [PANGOLN]
    +set      = more
     style    = regular
     licence  = PANGOLIN
     boxdraw  = true
    @@ -886,6 +978,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [PACIFIC]
    +set      = more
     style    = regular
     licence  = PACIFICO
     boxdraw  = true
    @@ -895,6 +988,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [ANTON]
    +set      = more
     style    = regular
     licence  = ANTON
     boxdraw  = true
    @@ -904,6 +998,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [BANGERS]
    +set      = more
     style    = regular
     licence  = BANGERS
     boxdraw  = true
    @@ -913,6 +1008,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [SPELITE]
    +set      = more
     style    = regular
     licence  = SPELITE
     boxdraw  = true
    @@ -922,6 +1018,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [SANCREK]
    +set      = more
     style    = regular
     licence  = SANCREEK
     boxdraw  = true
    @@ -931,6 +1028,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [PMARKER]
    +set      = more
     style    = regular
     licence  = PMARKER
     boxdraw  = true
    @@ -940,6 +1038,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [BUNGEE]
    +set      = more
     style    = regular
     licence  = BUNGEE
     boxdraw  = true
    @@ -949,6 +1048,7 @@ smooth   = 24 28 32 36
     hinting  = auto
     
     [EXO2]
    +set      = more
     style    = regular
     licence  = EXO2
     boxdraw  = true
    @@ -959,6 +1059,7 @@ hinting  = auto
     weight   = 400
     
     [EXO2B]
    +set      = more
     style    = bold
     licence  = EXO2
     boxdraw  = true
    @@ -969,6 +1070,7 @@ hinting  = auto
     weight   = 700
     
     [EXO2I]
    +set      = more
     style    = italic
     licence  = EXO2
     boxdraw  = true
    @@ -979,6 +1081,7 @@ hinting  = auto
     weight   = 400
     
     [EXO2Z]
    +set      = more
     style    = bolditalic
     licence  = EXO2
     boxdraw  = true
    @@ -989,6 +1092,7 @@ hinting  = auto
     weight   = 700
     
     [FIXED]
    +set      = more
     style    = regular
     licence  = MISCFIX
     source   = miscfixed/font-misc-misc-1.1.3/4x6.bdf
    @@ -1015,6 +1119,7 @@ source10 = miscfixed/font-misc-misc-1.1.3/10x20.bdf
     sizes10  = native
     
     [FIXEDB]
    +set      = more
     style    = bold
     licence  = MISCFIX
     source   = miscfixed/font-misc-misc-1.1.3/6x13B.bdf
    @@ -1027,12 +1132,14 @@ source3  = miscfixed/font-misc-misc-1.1.3/9x18B.bdf
     sizes3   = native
     
     [FIXEDI]
    +set      = more
     style    = italic
     licence  = MISCFIX
     source   = miscfixed/font-misc-misc-1.1.3/6x13O.bdf
     sizes    = native
     
     [SPLEEN]
    +set      = more
     style    = regular
     licence  = SPLEEN
     source   = spleen/spleen-2.2.0/spleen-5x8.bdf
    @@ -1052,6 +1159,7 @@ sizes5   = native
     encoding5 = cp437
     
     [GOHU]
    +set      = more
     style    = regular
     licence  = GOHU
     source   = gohufont/gohufont-2.1/gohufont-uni-11.pcf.gz
    @@ -1060,6 +1168,7 @@ source1  = gohufont/gohufont-2.1/gohufont-uni-14.pcf.gz
     sizes1   = native
     
     [GOHUB]
    +set      = more
     style    = bold
     licence  = GOHU
     source   = gohufont/gohufont-2.1/gohufont-uni-11b.pcf.gz
    @@ -1068,24 +1177,28 @@ source1  = gohufont/gohufont-2.1/gohufont-uni-14b.pcf.gz
     sizes1   = native
     
     [COZETTE]
    +set      = more
     style    = regular
     licence  = COZETTE
     source   = cozette/cozette.bdf
     sizes    = native
     
     [UNIFONT]
    +set      = more
     style    = regular
     licence  = UNIFONT
     source   = unifont/unifont-18.0.01.bdf
     sizes    = native
     
     [KIRSCH]
    +set      = more
     style    = regular
     licence  = KIRSCH
     source   = kirsch/kirsch.bdf
     sizes    = native
     
     [DEPARTM]
    +set      = more
     style    = regular
     licence  = DEPARTUR
     source   = departuremono/DepartureMono-1.500/DepartureMono-Regular.otf
    @@ -1093,6 +1206,7 @@ sizes    = 11 22
     hinting  = none
     
     [ARKPIX]
    +set      = more
     style    = regular
     licence  = ARKPIXEL
     source   = arkpixel/ark-pixel-10px-proportional-latin.bdf
    @@ -1101,6 +1215,7 @@ source1  = arkpixel/ark-pixel-12px-proportional-latin.bdf
     sizes1   = native
     
     [PIXOP]
    +set      = more
     style    = regular
     licence  = PIXOP
     source   = pixeloperator/PixelOperator.ttf
    @@ -1111,6 +1226,7 @@ sizes1   = 8
     hinting1 = none
     
     [PIXOPB]
    +set      = more
     style    = bold
     licence  = PIXOP
     source   = pixeloperator/PixelOperator-Bold.ttf
    @@ -1121,6 +1237,7 @@ sizes1   = 8
     hinting1 = none
     
     [PIXOPM]
    +set      = more
     style    = regular
     licence  = PIXOP
     source   = pixeloperator/PixelOperatorMono.ttf
    @@ -1131,6 +1248,7 @@ sizes1   = 8
     hinting1 = none
     
     [PIXOPMB]
    +set      = more
     style    = bold
     licence  = PIXOP
     source   = pixeloperator/PixelOperatorMono-Bold.ttf
    @@ -1141,6 +1259,7 @@ sizes1   = 8
     hinting1 = none
     
     [SILKSCR]
    +set      = more
     style    = regular
     licence  = SILKSCR
     source   = silkscreen/Silkscreen-Regular.ttf
    @@ -1148,6 +1267,7 @@ sizes    = 8 16
     hinting  = none
     
     [SILKSCRB]
    +set      = more
     style    = bold
     licence  = SILKSCR
     source   = silkscreen/Silkscreen-Bold.ttf
    @@ -1155,6 +1275,7 @@ sizes    = 8 16
     hinting  = none
     
     [TINY5]
    +set      = more
     style    = regular
     licence  = TINY5
     source   = tiny5/Tiny5[BLED,JITT,ROND,wdth,wght].ttf
    @@ -1162,6 +1283,7 @@ sizes    = 8 16
     hinting  = none
     
     [SWISS]
    +set      = more
     style    = regular
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/helvR08.bdf
    @@ -1188,6 +1310,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/helvR24.bdf
     sizes10  = native
     
     [SWISSB]
    +set      = more
     style    = bold
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/helvB08.bdf
    @@ -1214,6 +1337,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/helvB24.bdf
     sizes10  = native
     
     [SWISSI]
    +set      = more
     style    = italic
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/helvO08.bdf
    @@ -1240,6 +1364,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/helvO24.bdf
     sizes10  = native
     
     [SWISSZ]
    +set      = more
     style    = bolditalic
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/helvBO08.bdf
    @@ -1266,6 +1391,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/helvBO24.bdf
     sizes10  = native
     
     [ROMAN]
    +set      = more
     style    = regular
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/timR08.bdf
    @@ -1292,6 +1418,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/timR24.bdf
     sizes10  = native
     
     [ROMANB]
    +set      = more
     style    = bold
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/timB08.bdf
    @@ -1318,6 +1445,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/timB24.bdf
     sizes10  = native
     
     [ROMANI]
    +set      = more
     style    = italic
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/timI08.bdf
    @@ -1344,6 +1472,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/timI24.bdf
     sizes10  = native
     
     [ROMANZ]
    +set      = more
     style    = bolditalic
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/timBI08.bdf
    @@ -1370,6 +1499,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/timBI24.bdf
     sizes10  = native
     
     [SCHOOL]
    +set      = more
     style    = regular
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/ncenR08.bdf
    @@ -1396,6 +1526,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/ncenR24.bdf
     sizes10  = native
     
     [SCHOOLB]
    +set      = more
     style    = bold
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/ncenB08.bdf
    @@ -1422,6 +1553,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/ncenB24.bdf
     sizes10  = native
     
     [SCHOOLI]
    +set      = more
     style    = italic
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/ncenI08.bdf
    @@ -1448,6 +1580,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/ncenI24.bdf
     sizes10  = native
     
     [SCHOOLZ]
    +set      = more
     style    = bolditalic
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/ncenBI08.bdf
    @@ -1474,6 +1607,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/ncenBI24.bdf
     sizes10  = native
     
     [TYPEWR]
    +set      = more
     style    = regular
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/courR08.bdf
    @@ -1500,6 +1634,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/courR24.bdf
     sizes10  = native
     
     [TYPEWRB]
    +set      = more
     style    = bold
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/courB08.bdf
    @@ -1526,6 +1661,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/courB24.bdf
     sizes10  = native
     
     [TYPEWRI]
    +set      = more
     style    = italic
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/courO08.bdf
    @@ -1552,6 +1688,7 @@ source10 = x11adobe/font-adobe-100dpi-1.0.4/courO24.bdf
     sizes10  = native
     
     [TYPEWRZ]
    +set      = more
     style    = bolditalic
     licence  = X11ADOBE
     source   = x11adobe/font-adobe-75dpi-1.0.4/courBO08.bdf
    diff --git a/src/include/basic/pkg.bas b/src/include/basic/pkg.bas
    index c8acb84f..f47226da 100644
    --- a/src/include/basic/pkg.bas
    +++ b/src/include/basic/pkg.bas
    @@ -88,7 +88,7 @@ DECLARE LIBRARY "pkg"
         DECLARE FUNCTION PkgFilesRows$(BYVAL s AS LONG, BYVAL id AS STRING) AS STRING
     
         ' The id of the installed package that put a file there (its name from
    -    ' the DVX folder, such as "SHARED\BANKS\KPUNCH\DMXOPL3.OP2"), or "".
    +    ' the DVX folder, such as "SHARED\FONTS\KPUNCH\ARIMO.DFN"), or "".
         DECLARE FUNCTION PkgFileOwner$(BYVAL s AS LONG, BYVAL fileName AS STRING) AS STRING
     
         ' What it needs.  Rows: id, the oldest version that will do, the
    diff --git a/src/include/basic/sql.bas b/src/include/basic/sql.bas
    index 5878fc02..697338ca 100644
    --- a/src/include/basic/sql.bas
    +++ b/src/include/basic/sql.bas
    @@ -25,6 +25,11 @@
     ' Provides SQLite database access through DECLARE LIBRARY wrappers.
     ' Handles and cursors are LONG integers; strings are passed by value.
     '
    +' SQLite is in DVX's optional Databases part (Setup's "What to install"
    +' page).  Without it every SQL function does nothing: SQLOpen returns 0
    +' and SQLError$ says the Databases part is not installed;
    +' SysLibraryLoaded("dvxsql") (system.bas) tells beforehand.
    +'
     ' Usage: Add this file to your project.
     
     DECLARE LIBRARY "basrt"
    diff --git a/src/include/basic/system.bas b/src/include/basic/system.bas
    index 0081ccf2..ec2bc2c8 100644
    --- a/src/include/basic/system.bas
    +++ b/src/include/basic/system.bas
    @@ -252,6 +252,16 @@ DECLARE LIBRARY "dvxshell"
         DECLARE FUNCTION SysMemoryTotalKb() AS LONG
         DECLARE FUNCTION SysMemoryFreeKb() AS LONG
     
    +    ' ---- Libraries ----
    +
    +    ' True when the library DECLARE LIBRARY names as libName ("pkgnet",
    +    ' "dvxsql" ...; any case) is installed and was started with DVX.
    +    ' Some parts of DVX are optional (Setup's "What to install" page, and
    +    ' Software): a program using one of their libraries asks first, and
    +    ' leaves out what needs it, since calling a function of a library that
    +    ' is not there stops the program with "External function not found".
    +    DECLARE FUNCTION SysLibraryLoaded(BYVAL libName AS STRING) AS LONG
    +
         ' ---- System font ----
     
         ' The character cell of the system font in pixels: every character
    diff --git a/src/installer/Makefile b/src/installer/Makefile
    index 07a227cb..3d75978b 100644
    --- a/src/installer/Makefile
    +++ b/src/installer/Makefile
    @@ -24,8 +24,10 @@
     #
     #   bin/setup.exe     SETUP.EXE: one static DOS program -- the DVX core,
     #                     the widgets Setup uses, fileops, miniz's inflate, the
    -#                     Setup program, its fonts (setupFonts.ini), and its
    -#                     start page's icons (mkicon) --
    +#                     package library's install (and Monocypher's Ed25519,
    +#                     for the add-ons' signatures), the Setup program, its
    +#                     fonts (setupFonts.ini), and its start page's icons
    +#                     (mkicon) --
     #                     linked with --gc-sections so only what Setup reaches
     #                     is in it, then packed with UPX (it unpacks itself in
     #                     memory when it starts).  Also shipped in the package
    @@ -70,19 +72,20 @@ SETUP_ICONS = install upgrade remove
     # keeps the CWSDPMI stub.
     UPX_FLAGS  = -qq --best --nrv2e
     
    -INCLUDES   = -I. -I$(PKGLIB) -I$(MINIZ) -I$(OBJDIR) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty -I$(KPUNCH) -I$(KPUNCH)/libtasks -I$(WIDGETS)
    +INCLUDES   = -I. -I$(PKGLIB) -I$(MINIZ) -I$(MONO) -I$(OBJDIR) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty -I$(KPUNCH) -I$(KPUNCH)/libtasks -I$(WIDGETS)
     MINIZ_DOS  = $(PKG_MINIZ_FLAGS) -DMINIZ_NO_DEFLATE_APIS
     CFLAGS     = $(DOS_CFLAGS) -ffunction-sections -fdata-sections $(INCLUDES) $(MINIZ_DOS)
     LDFLAGS    = -s -Wl,--gc-sections -lm
     
     # Setup's own sources; the core, fileops, and the text and list helpers
     # as they are
    -SETUP_SRCS = setup.c setupCore.c pkgRead.c $(PKGLIB)/pkgDb.c $(PKGLIB)/pkgFormat.c $(PKGLIB)/pkgIni.c $(PKGLIB)/pkgManifest.c $(PKGLIB)/pkgZip.c $(MINIZ)/miniz.c
    +SETUP_SRCS = setup.c setupCore.c pkgRead.c $(PKGLIB)/pkgDb.c $(PKGLIB)/pkgDpk.c $(PKGLIB)/pkgFormat.c $(PKGLIB)/pkgIni.c $(PKGLIB)/pkgManifest.c $(PKGLIB)/pkgOp.c $(PKGLIB)/pkgPlan.c $(PKGLIB)/pkgSign.c $(PKGLIB)/pkgTrust.c $(PKGLIB)/pkgZip.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c $(KPUNCH)/serial/security/secHash.c $(KPUNCH)/serial/security/security.c
     CORE_SRCS  = $(wildcard $(LIBDVX)/*.c) $(LIBDVX)/platform/dvxPlatformDos.c $(LIBDVX)/platform/dvxPlatformIrq.c $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/platform/dvxMemTrack.c $(LIBDVX)/thirdparty/stb_ds_impl.c ../tools/dvxResWrite.c $(wildcard $(KPUNCH)/fileops/*.c) $(KPUNCH)/texthelp/textHelp.c $(KPUNCH)/listhelp/listHelp.c
     
     # The widgets Setup uses: /: (wgtRegister -> wgtRegister);
    -# the list box and the dropdown are the folder dialog's
    -SETUP_WGTS = box/widgetBox:Box button/widgetButton:Button checkbox/widgetCheckbox:Checkbox dropdown/widgetDropdown:Dropdown imageButton/widgetImageButton:ImageButton label/widgetLabel:Label listBox/widgetListBox:ListBox progressBar/widgetProgressBar:Progress radio/widgetRadio:Radio spacer/widgetSpacer:Spacer textInput/widgetTextInput:TextInput
    +# the list box and the dropdown are the folder dialog's, the wrap box the
    +# add-ons' row
    +SETUP_WGTS = box/widgetBox:Box button/widgetButton:Button checkbox/widgetCheckbox:Checkbox dropdown/widgetDropdown:Dropdown imageButton/widgetImageButton:ImageButton label/widgetLabel:Label listBox/widgetListBox:ListBox progressBar/widgetProgressBar:Progress radio/widgetRadio:Radio spacer/widgetSpacer:Spacer textInput/widgetTextInput:TextInput wrapBox/widgetWrapBox:WrapBox
     
     SETUP_OBJS = $(patsubst %.c,$(DOSOBJ)/%.o,$(notdir $(SETUP_SRCS)))
     CORE_OBJS  = $(patsubst %.c,$(DOSOBJ)/core/%.o,$(notdir $(CORE_SRCS)))
    diff --git a/src/installer/README.TXT b/src/installer/README.TXT
    index 61a8b86d..1eb5660b 100644
    --- a/src/installer/README.TXT
    +++ b/src/installer/README.TXT
    @@ -15,8 +15,10 @@ What you need
         ATI Mach64 cards
       * 4 MB of extended memory (8 MB or more recommended)
       * A two-button mouse and its DOS driver
    -  * About 12 MB of free space on the hard disk (14 MB with the base set
    -    of fonts, 22 MB with all the fonts)
    +  * About 14 MB of free space on the hard disk for all of DVX (10 MB
    +    without its optional parts), 8 MB more for the base set of fonts,
    +    and more for the add-ons on the CD: about 8 MB for More Fonts and
    +    4 MB for the General MIDI SoundFont
     
     
     Installing
    @@ -34,19 +36,47 @@ From floppy disks: put Disk 1 in drive A: and type
     
     After the licence, Setup asks what you would like to do: Install DVX,
     Upgrade or repair DVX, or Remove DVX. To install, choose the folder
    -(C:\DVX unless you change it; type it, or click Browse... to pick it)
    -and which fonts to install. Setup checks the free space and unpacks
    -DVX. With floppies it asks for each disk it needs in turn. Setup can
    -also add a line to AUTOEXEC.BAT so DVX starts with the computer; it
    -shows the line first and keeps a copy of the old file as AUTOEXEC.DVX.
    +(C:\DVX unless you change it; type it, or click Browse... to pick it),
    +then what to install: the fonts, the parts of DVX, and the add-ons you
    +want. Setup checks the free space and unpacks DVX. With floppies it asks for each disk it needs in
    +turn. Setup can also add a line to AUTOEXEC.BAT so DVX starts with the
    +computer; it shows the line first and keeps a copy of the old file as
    +AUTOEXEC.DVX.
     
     The fonts: None keeps DVX to its own font; the base set adds DejaVu
    -Sans, Sans Mono, and Serif in all their styles (about 2 MB); All adds
    -every font that comes with DVX (about 8 MB). A program that asks for a
    -font you did not install shows its text in DVX's own font, unless it
    -carries that font itself. Each font file holds the licence of the font
    -it was made from. With None or the base set, Setup does not ask for the
    -last floppy disks, which hold only fonts.
    +Sans, Sans Mono, and Serif in all their styles, in sizes for the screen
    +and for printing (about 8 MB). A program that asks for a font you did
    +not install shows its text in DVX's own font, unless it carries that
    +font itself. Each font file holds the licence of the font it was made
    +from. With None, Setup does not ask for the last floppy disks, which
    +hold only fonts; the base set can be added later from the package
    +server (Software lists it as DVX Fonts), or by running Setup again.
    +
    +The parts of DVX: the desktop, Program Manager, File Manager, Notepad,
    +the Help Viewer, Control Panel, Software, and the other everyday
    +programs are always installed. These parts are optional, all ticked to
    +start with: DVX BASIC (the editor, compiler, and debugger for writing
    +your own programs, and BASCOMP to build them at the DOS prompt), Databases (SQLite databases for BASIC programs, with
    +the Data control and the DBGrid), Networking (network cards, the web,
    +and the package server), Communications (the Terminal: modems, serial
    +and secure links, Telnet, and file transfers; it needs Networking),
    +Media (the Media Player and the Sound Recorder), Developer Tools (the
    +Icon Editor, Resource Editor, Help Editor, Package Builder, and Widget
    +Showcase, with DVXRES and DVXHLPC for the DOS prompt), and
    +Demos (the DVX Demo and the BASIC Demo). Point at a part to see what it
    +holds. A part left out takes no disk space and leaves more memory for
    +your programs. Software lists each part you install as one package; run
    +Setup again to add or remove parts.
    +
    +The add-ons, in the ADDONS folder of the CD: More Fonts (more than
    +forty more typefaces: condensed DejaVu, Arimo, Tinos, Cousine, display
    +faces, and pixel fonts; about 8 MB) and DVX General MIDI (instruments
    +for MIDI music on a Gravis UltraSound or a Sound Blaster AWE32 or
    +AWE64; about 4 MB). Setup installs the ones you tick. They are packages
    +like any other: Software, in Program Manager, lists them, removes them,
    +and installs them later from the CD (File, Install from File, then the
    +ADDONS folder) or from the package server. The floppy disks have no
    +add-ons.
     
     Setup never writes to the CD or the floppy disks.
     
    @@ -56,8 +86,9 @@ Upgrading or repairing
     
     Run Setup again, choose Upgrade or repair DVX, and choose the folder
     DVX is in. Your settings and saved files (the USER folder) are kept;
    -tick Start over to remove them instead. Setup starts with the fonts the
    -folder already has; choose others to add fonts or remove them.
    +tick Start over to remove them instead. Setup starts with the fonts,
    +parts, and add-ons the folder already has; choose others to add or
    +remove them. A part that other software in the folder needs is kept.
     
     
     Removing DVX
    @@ -81,13 +112,20 @@ DVX /SAFE, to start it in 640x480.
     Unattended install
     ------------------
     
    -    SETUP /AUTO=C:\DVX [/FONTS=NONE|BASE|ALL] [/LOG=C:\SETUP.LOG]
    +    SETUP /AUTO=C:\DVX [/FONTS=NONE|BASE] [/PARTS=ALL|NONE|names]
    +          [/ADDONS=ALL|NONE|names] [/LOG=C:\SETUP.LOG]
     
     installs without asking (the licence is taken as accepted, a folder
     that has DVX is upgraded with its USER folder kept, and AUTOEXEC.BAT is
     not changed). /FONTS chooses the fonts; without it Setup installs the
    -fonts the folder already has, or all of them in a new folder. The exit
    -code is 0 when DVX was installed.
    +fonts the folder already has, or the base set in a new folder. /PARTS
    +chooses the parts of DVX by name: BASIC, DATA, NET, COMM, MEDIA,
    +DEVTOOLS, and DEMOS, such as /PARTS=BASIC,COMM (a part brings the parts
    +it needs: COMM brings NET); without it Setup keeps the parts the folder
    +has, or installs them all in a new folder. /ADDONS
    +chooses the add-ons by their file names, such as /ADDONS=MOREFONT,DVXGM;
    +without it Setup keeps the add-ons the folder has. The exit code is 0
    +when DVX was installed.
     
     
     DVX is free software under the MIT licence (see the licence in Setup).
    diff --git a/src/installer/mkDist.sh b/src/installer/mkDist.sh
    index 3db17ed9..eb537c54 100755
    --- a/src/installer/mkDist.sh
    +++ b/src/installer/mkDist.sh
    @@ -26,25 +26,52 @@
     #
     # From a built bin/ (make all):
     #   dist/dvx.zip            the package (bin/ without the build machine's
    -#                           files: host tools, tests, SDK, map, logs), in
    -#                           tiers: DVX, then the base set of fonts (mkfont
    -#                           -l base), then the other fonts -- Setup installs
    -#                           the fonts of none, the first, or both
    +#                           files: host tools, tests, SDK, map, logs, and
    +#                           the fonts of the More Fonts add-on), in tiers:
    +#                           DVX, then the base set of fonts (mkfont -l
    +#                           base) -- Setup installs the fonts or not.
    +#                           Each tier carries its packages' installed
    +#                           records (dvxpkg record: the manifests of
    +#                           DVXUPD.DPK, BASEFONT.DPK, and dist/packages,
    +#                           every file with its size and SHA-256), so DVX,
    +#                           its fonts, and its optional parts arrive
    +#                           recorded, and update from the package server
    +#                           like any package
    +#   dist/addons/NAME.DPK    DVX's own add-ons, from src/addons/*/NAME
    +#                           (PACKAGE.INI and the files; files the build
    +#                           makes -- More Fonts' fonts, the SoundFont --
    +#                           from obj/addons/NAME; "version = dvx": DVX's
    +#                           version), for the CD and the package server.
    +#                           Signed with the secret key DVX_SIGN_KEY names,
    +#                           else ~/.ssh/KPUNCH.SEC when it is there (each
    +#                           NAME.SIG beside it, checked against the
    +#                           package's SYSTEM\KEYS); with neither (or
    +#                           DVX_SIGN_KEY set empty) they are unsigned, and
    +#                           Setup does not offer them (DVX installs only
    +#                           signed kpunch.* packages)
    +#   dist/packages/NAME.DPK  DVX's optional parts (src/packages.ini: the
    +#                           bundles and their members, needs worked out by
    +#                           mkPackages.py), for the package server; in the
    +#                           package too, which Setup unpacks but for the
    +#                           ones not chosen
    +#   dist/SETUP.INI          Setup's list of them (each bundle's name,
    +#                           summary, needs, members, folders, and size),
    +#                           beside SETUP.EXE on the CD and Disk 1
     #   dist/cd/SETUP.EXE       SETUP.EXE with the package appended
     #   dist/cd/README.TXT
    +#   dist/cd/ADDONS/         the add-ons (and their signatures), which
    +#                           Setup offers
     #   dist/DVX.ISO            ISO 9660 image of dist/cd
     #   dist/floppy/DISKnn.IMG  floppy images: Disk 1 = SETUP.EXE, README.TXT
    -#                           and DVX.P01; Disk n = DVX.P0n
    +#                           and DVX.P01; Disk n = DVX.P0n (no add-ons)
     #   dist/sdk.zip            the SDK (bin/sdk) in DVX-SDK/, with the
     #                           technical and BASIC references in docs/
     #   dist/DVXUPD.DPK         DVX's own update (kpunch.dvx): the package's
    -#                           files but the fonts (Setup's optional tiers),
    +#                           files but the fonts and the optional parts,
     #                           for the package server, where the operator
     #                           signs it (server/README.md)
    -#   dist/addons/NAME.DPK    DVX's own add-ons, from src/addons/*/NAME
    -#                           (PACKAGE.INI and the files; files the build
    -#                           makes, the fonts of one with a fonts.ini, from
    -#                           obj/addons/NAME), for the package server too
    +#   dist/BASEFONT.DPK       the base set of fonts (kpunch.basefont,
    +#                           src/fonts/PACKAGE.INI), for the server too
     #
     # FLOPPY: 360K, 720K, 1.2M, 1.44M (default), 2.88M -- images of that
     # format -- or a number of bytes: the room on each disk, and folders
    @@ -54,9 +81,9 @@
     # DVX_DIST_BIN / DVX_DIST_OUT replace bin/ and dist/ (the DOS test
     # harness builds from its bin snapshot into its run folder).
     #
    -# Needs mtools (mformat, mcopy, mdir), mkisofs and zip on the build
    -# machine (the DOS test harness already needs mtools; mkcd.sh mkisofs;
    -# zip keeps the SDK tools' execute bits).
    +# Needs mtools (mformat, mcopy, mdir), mkisofs, zip, and python3 on the
    +# build machine (the DOS test harness already needs mtools and python3;
    +# mkcd.sh mkisofs; zip keeps the SDK tools' execute bits).
     
     set -euo pipefail
     
    @@ -67,11 +94,27 @@ STAGE="${DIST}/stage"
     TOOL="${BIN}/host/dvxpkg"
     MKFONT="${BIN}/host/mkfont"
     FONT_LIST="${ROOT}/src/fonts/fonts.ini"
    -# bin/'s fonts folder (DVX_SHARED_DIR/DVX_FONTS_DIR, any case)
    +# bin/'s fonts folder (DVX_SHARED_DIR/DVX_FONTS_DIR, any case), and DVX's
    +# own fonts in it, in its vendor folder as a font package's must be
     FONTS_DIR="shared/fonts"
    +OWN_FONTS_DIR="${FONTS_DIR}/kpunch"
     SETUP="${BIN}/setup.exe"
     README="${ROOT}/src/installer/README.TXT"
     FLOPPY="${1:-1.44M}"
    +# The secret key that signs the add-ons: DVX_SIGN_KEY (set empty: none),
    +# else the repository's in ~/.ssh when it is there
    +DEFAULT_SIGN_KEY="${HOME}/.ssh/KPUNCH.SEC"
    +SIGN_KEY="${DVX_SIGN_KEY-}"
    +
    +if [ -z "${DVX_SIGN_KEY+set}" ] && [ -f "${DEFAULT_SIGN_KEY}" ]; then
    +    SIGN_KEY="${DEFAULT_SIGN_KEY}"
    +fi
    +
    +# Owner-only permissions of a secret key
    +KEY_MODE="600"
    +ADDONS_DIR="ADDONS"
    +# The package's trusted keys (DVX_KEYS_DIR in DVX_SYSTEM_DIR, any case)
    +KEYS_DIR="system/KEYS"
     
     export MTOOLS_SKIP_CHECK=1
     
    @@ -89,6 +132,13 @@ fail() {
     }
     
     
    +# dvxTemplate TEMPLATE OUT -- TEMPLATE with "version = dvx" made DVX's
    +# version (DVX_VERSION)
    +dvxTemplate() {
    +    sed "s/^version *= *dvx *\$/version   = ${DVX_VERSION}/" "$1" > "$2"
    +}
    +
    +
     # freeBytes IMAGE -- the free bytes mtools reports for a disk image
     freeBytes() {
         mdir -i "$1" :: 2>/dev/null | sed -n 's/^ *\([0-9 ]*\) bytes free.*/\1/p' | tr -d ' '
    @@ -108,27 +158,171 @@ for f in "${TOOL}" "${SETUP}" "${MKFONT}"; do
         [ -x "${f}" ] || [ -f "${f}" ] || fail "${f} is missing (run make first)"
     done
     
    +if [ -n "${SIGN_KEY}" ] && [ ! -f "${SIGN_KEY}" ]; then
    +    fail "DVX_SIGN_KEY: ${SIGN_KEY} is not a file"
    +fi
    +
    +if [ -n "${SIGN_KEY}" ] && [ "$(stat -c %a "${SIGN_KEY}")" != "${KEY_MODE}" ]; then
    +    echo "mkDist: ${SIGN_KEY} can be read by others: chmod ${KEY_MODE} ${SIGN_KEY}" >&2
    +fi
    +
     rm -rf "${DIST}"
     mkdir -p "${STAGE}" "${DIST}/cd" "${DIST}/floppy" "${DIST}/parts"
     
     tar -C "${BIN}" "${EXCLUDES[@]}" -cf - . | tar -C "${STAGE}" -xf -
     
    -# The tiers after DVX's own files: the base set of fonts (the folder and
    -# what mkfont lists), then every other font
    +# DVX's package template (this DVX's version and ABI), for DVX's update
    +# and the version of the add-ons that take DVX's
    +DVX_TEMPLATE="${DIST}/dvxupd.ini"
    +"${TOOL}" dvx "${DVX_TEMPLATE}" || fail "the package tool did not write DVX's package template"
    +DVX_VERSION="$(sed -n 's/^version *= *\([0-9.]*\).*/\1/p' "${DVX_TEMPLATE}")"
    +DVX_ABI="$(sed -n 's/^abi *= *\([0-9]*\).*/\1/p' "${DVX_TEMPLATE}")"
    +if [ -z "${DVX_VERSION}" ] || [ -z "${DVX_ABI}" ]; then
    +    fail "DVX's package template has no version or ABI"
    +fi
    +
    +# The More Fonts add-on's fonts are not in the package
    +MORE_FONTS=$("${MKFONT}" -l "${FONT_LIST}" more) || fail "mkfont could not list the More Fonts"
    +BASE_FONTS=$("${MKFONT}" -l "${FONT_LIST}" base) || fail "mkfont could not list the base set of fonts"
    +
    +for f in ${MORE_FONTS}; do
    +    rm -f "${STAGE}/${OWN_FONTS_DIR}/${f}"
    +done
    +
    +# DVX's optional packages (src/packages.ini): their templates, the
    +# members' folders, the bundles, and Setup's list of them (mkPackages.py)
    +GEN="${DIST}/packages.gen"
    +python3 -I "${ROOT}/src/installer/mkPackages.py" "${ROOT}/src/packages.ini" "${STAGE}" "${ROOT}/src" "${DVX_VERSION}" "${DVX_ABI}" "${GEN}" || fail "the packages of src/packages.ini could not be worked out"
    +mapfile -t MEMBERS < "${GEN}/members.lst"
    +mapfile -t BUNDLES < "${GEN}/bundles.lst"
    +
    +# A package's .DPK name: its id's name in capitals
    +dpkName() {
    +    echo "${1#*.}" | tr '[:lower:]' '[:upper:]'
    +}
    +
    +# DVX's own update: the staged release but its fonts and the optional
    +# packages' folders and paths (members.lst: "ID FOLDER [PATH ...]"), from
    +# DVX's template
    +UPD_STAGE="${DIST}/updstage"
    +UPD_EXCLUDES=(--exclude="./${FONTS_DIR}")
    +
    +for entry in "${MEMBERS[@]}"; do
    +    read -r -a fields <<< "${entry}"
    +
    +    for rel in "${fields[@]:1}"; do
    +        UPD_EXCLUDES+=(--exclude="./${rel}")
    +    done
    +done
    +
    +mkdir -p "${UPD_STAGE}"
    +tar -C "${STAGE}" "${UPD_EXCLUDES[@]}" -cf - . | tar -C "${UPD_STAGE}" -xf -
    +"${TOOL}" build "${DVX_TEMPLATE}" "${UPD_STAGE}" "${DIST}/DVXUPD.DPK" > /dev/null || fail "DVX's update package could not be built"
    +rm -rf "${UPD_STAGE}" "${DVX_TEMPLATE}"
    +
    +# The base set of fonts as a package (kpunch.basefont)
    +FONT_STAGE="${DIST}/fontstage"
    +FONT_TEMPLATE="${DIST}/basefont.ini"
    +mkdir -p "${FONT_STAGE}/${OWN_FONTS_DIR}"
    +
    +for f in ${BASE_FONTS}; do
    +    cp "${STAGE}/${OWN_FONTS_DIR}/${f}" "${FONT_STAGE}/${OWN_FONTS_DIR}/" || fail "the base font ${f} is missing"
    +done
    +
    +dvxTemplate "${ROOT}/src/fonts/PACKAGE.INI" "${FONT_TEMPLATE}"
    +"${TOOL}" build "${FONT_TEMPLATE}" "${FONT_STAGE}" "${DIST}/BASEFONT.DPK" > /dev/null || fail "the base fonts' package could not be built"
    +rm -rf "${FONT_STAGE}" "${FONT_TEMPLATE}"
    +
    +# The optional packages, for the package server: each member from its
    +# folder and paths, each bundle from none
    +PKG_DIR="${DIST}/packages"
    +MEMBER_STAGE="${DIST}/memberstage"
    +mkdir -p "${PKG_DIR}" "${DIST}/empty"
    +
    +for entry in "${MEMBERS[@]}"; do
    +    read -r -a fields <<< "${entry}"
    +    id="${fields[0]}"
    +    rm -rf "${MEMBER_STAGE}"
    +
    +    for rel in "${fields[@]:1}"; do
    +        mkdir -p "${MEMBER_STAGE}/$(dirname "${rel}")"
    +        cp -a "${STAGE}/${rel}" "${MEMBER_STAGE}/${rel}"
    +    done
    +
    +    "${TOOL}" build "${GEN}/${id}.ini" "${MEMBER_STAGE}" "${PKG_DIR}/$(dpkName "${id}").DPK" > /dev/null || fail "the package ${id} could not be built"
    +done
    +
    +for id in "${BUNDLES[@]}"; do
    +    "${TOOL}" build "${GEN}/${id}.ini" "${DIST}/empty" "${PKG_DIR}/$(dpkName "${id}").DPK" > /dev/null || fail "the bundle ${id} could not be built"
    +done
    +
    +rm -rf "${MEMBER_STAGE}" "${DIST}/empty"
    +
    +# Their records, each in its tier: DVX's with DVX's files, the fonts'
    +# (and so the fonts' only when they are installed) with theirs, each
    +# optional package's with its files (a bundle's members as needed by it,
    +# so they go with it); Setup skips the ones not chosen (SETUP.INI)
    +"${TOOL}" record "${DIST}/DVXUPD.DPK" "${STAGE}" > /dev/null || fail "DVX's record could not be written"
    +FONT_RECORD=$("${TOOL}" record "${DIST}/BASEFONT.DPK" "${STAGE}") || fail "the base fonts' record could not be written"
    +
    +for entry in "${MEMBERS[@]}"; do
    +    "${TOOL}" record "${PKG_DIR}/$(dpkName "${entry%% *}").DPK" "${STAGE}" -n > /dev/null || fail "the record of ${entry%% *} could not be written"
    +done
    +
    +for id in "${BUNDLES[@]}"; do
    +    "${TOOL}" record "${PKG_DIR}/$(dpkName "${id}").DPK" "${STAGE}" > /dev/null || fail "the record of ${id} could not be written"
    +done
    +
    +cp "${GEN}/SETUP.INI" "${DIST}/SETUP.INI"
    +rm -rf "${GEN}"
    +
    +# The tier after DVX's own files: the base set of fonts (the folders,
    +# what mkfont lists, and its record)
     TIER_BASE="${DIST}/tierbase.lst"
    -TIER_MORE="${DIST}/tiermore.lst"
    -{ echo "${FONTS_DIR}/"; "${MKFONT}" -l "${FONT_LIST}" base | sed "s|^|${FONTS_DIR}/|"; } > "${TIER_BASE}" || fail "mkfont could not list the base set of fonts"
    -echo "${FONTS_DIR}/*" > "${TIER_MORE}"
    -"${TOOL}" zip "${STAGE}" "${DIST}/dvx.zip" "${TIER_BASE}" "${TIER_MORE}"
    -rm -f "${TIER_BASE}" "${TIER_MORE}"
    +{ echo "${FONTS_DIR}/"; echo "${OWN_FONTS_DIR}/"; for f in ${BASE_FONTS}; do echo "${OWN_FONTS_DIR}/${f}"; done; echo "${FONT_RECORD}"; } > "${TIER_BASE}"
    +"${TOOL}" zip "${STAGE}" "${DIST}/dvx.zip" "${TIER_BASE}"
    +rm -f "${TIER_BASE}"
     sed 's/$/\r/' "${README}" > "${DIST}/README.TXT"
     
    +# DVX's own add-ons (src/addons/*/*/PACKAGE.INI), each from its folder,
    +# signed and checked when there is a key
    +mkdir -p "${DIST}/addons"
    +
    +for template in "${ROOT}"/src/addons/*/*/PACKAGE.INI; do
    +    folder="$(dirname "${template}")"
    +    base="$(basename "${folder}")"
    +    name="$(echo "${base}" | tr '[:lower:]' '[:upper:]')"
    +    dpk="${DIST}/addons/${name}.DPK"
    +    made="${DIST}/addons/${name}.INI"
    +
    +    # An add-on whose files the build makes (src/tools/Makefile) is packed
    +    # from obj/addons/NAME
    +    if [ -d "${ROOT}/obj/addons/${base}" ]; then
    +        folder="${ROOT}/obj/addons/${base}"
    +    fi
    +
    +    dvxTemplate "${template}" "${made}"
    +    "${TOOL}" build "${made}" "${folder}" "${dpk}" > /dev/null || fail "the add-on ${folder} could not be built"
    +    rm -f "${made}"
    +
    +    if [ -n "${SIGN_KEY}" ]; then
    +        "${TOOL}" sign "${dpk}" "${SIGN_KEY}" -d > /dev/null || fail "the add-on ${name}.DPK could not be signed"
    +        "${TOOL}" verify "${dpk}" -s "${DIST}/addons/${name}.SIG" "${STAGE}/${KEYS_DIR}"/*.KEY > /dev/null || fail "${name}.DPK's signature is not by a key the package trusts (${KEYS_DIR})"
    +    fi
    +done
    +
    +if [ -z "${SIGN_KEY}" ]; then
    +    echo "mkDist: the add-ons are not signed (no DVX_SIGN_KEY, no ${DEFAULT_SIGN_KEY}): Setup will not offer them" >&2
    +fi
    +
     SETUP_SIZE=$(stat -c %s "${SETUP}")
    -README_SIZE=$(stat -c %s "${DIST}/README.TXT")
    +README_SIZE=$(( $(stat -c %s "${DIST}/README.TXT") + $(stat -c %s "${DIST}/SETUP.INI") ))
     
     # CD
     "${TOOL}" cd "${SETUP}" "${DIST}/dvx.zip" "${DIST}/cd/SETUP.EXE"
    -cp "${DIST}/README.TXT" "${DIST}/cd/README.TXT"
    +cp "${DIST}/README.TXT" "${DIST}/SETUP.INI" "${DIST}/cd/"
    +mkdir -p "${DIST}/cd/${ADDONS_DIR}"
    +cp "${DIST}/addons/"* "${DIST}/cd/${ADDONS_DIR}/"
     mkisofs -quiet -iso-level 1 -V DVX -o "${DIST}/DVX.ISO" "${DIST}/cd"
     
     # Floppies
    @@ -137,8 +331,8 @@ if [ -n "${FORMATS[${FLOPPY}]:-}" ]; then
         format "${DIST}/floppy/DISK01.IMG" "${KB}" DVX1
         ROOM=$(freeBytes "${DIST}/floppy/DISK01.IMG")
     
    -    if ! mcopy -i "${DIST}/floppy/DISK01.IMG" "${SETUP}" ::SETUP.EXE 2>/dev/null || ! mcopy -i "${DIST}/floppy/DISK01.IMG" "${DIST}/README.TXT" ::README.TXT 2>/dev/null; then
    -        fail "SETUP.EXE (${SETUP_SIZE} bytes) and README.TXT (${README_SIZE} bytes) do not fit on a ${FLOPPY} disk (${ROOM} bytes free when empty). A smaller disk needs a bootstrap Setup (claudeDocs/installer.md)."
    +    if ! mcopy -i "${DIST}/floppy/DISK01.IMG" "${SETUP}" ::SETUP.EXE 2>/dev/null || ! mcopy -i "${DIST}/floppy/DISK01.IMG" "${DIST}/README.TXT" ::README.TXT 2>/dev/null || ! mcopy -i "${DIST}/floppy/DISK01.IMG" "${DIST}/SETUP.INI" ::SETUP.INI 2>/dev/null; then
    +        fail "SETUP.EXE (${SETUP_SIZE} bytes), README.TXT, and SETUP.INI (${README_SIZE} bytes) do not fit on a ${FLOPPY} disk (${ROOM} bytes free when empty). A smaller disk needs a bootstrap Setup (claudeDocs/installer.md)."
         fi
     
         ROOM1=$(freeBytes "${DIST}/floppy/DISK01.IMG")
    @@ -147,7 +341,7 @@ elif [[ "${FLOPPY}" =~ ^[0-9]+$ ]]; then
         ROOM1=$((FLOPPY - SETUP_SIZE - README_SIZE))
     
         if [ "${ROOM1}" -le 0 ]; then
    -        fail "SETUP.EXE (${SETUP_SIZE} bytes) and README.TXT (${README_SIZE} bytes) do not fit in ${FLOPPY} bytes."
    +        fail "SETUP.EXE (${SETUP_SIZE} bytes), README.TXT, and SETUP.INI (${README_SIZE} bytes) do not fit in ${FLOPPY} bytes."
         fi
     else
         fail "FLOPPY=${FLOPPY}: use 360K, 720K, 1.2M, 1.44M, 2.88M or a number of bytes"
    @@ -171,38 +365,11 @@ for ((n = 1; n <= DISKS; n++)); do
     
             if [ "${n}" -eq 1 ]; then
                 cp "${SETUP}" "${DIST}/floppy/${disk}/SETUP.EXE"
    -            cp "${DIST}/README.TXT" "${DIST}/floppy/${disk}/"
    +            cp "${DIST}/README.TXT" "${DIST}/SETUP.INI" "${DIST}/floppy/${disk}/"
             fi
         fi
     done
     
    -# DVX's own update: the staged release but its fonts, from the template
    -# the package tool writes (this DVX's version and ABI)
    -UPD_STAGE="${DIST}/updstage"
    -UPD_TEMPLATE="${DIST}/dvxupd.ini"
    -mkdir -p "${UPD_STAGE}"
    -tar -C "${STAGE}" --exclude="./${FONTS_DIR}" -cf - . | tar -C "${UPD_STAGE}" -xf -
    -"${TOOL}" dvx "${UPD_TEMPLATE}" || fail "the package tool did not write DVX's package template"
    -"${TOOL}" build "${UPD_TEMPLATE}" "${UPD_STAGE}" "${DIST}/DVXUPD.DPK" > /dev/null || fail "DVX's update package could not be built"
    -rm -rf "${UPD_STAGE}" "${UPD_TEMPLATE}"
    -
    -# DVX's own add-ons (src/addons/*/*/PACKAGE.INI), each from its folder
    -mkdir -p "${DIST}/addons"
    -
    -for template in "${ROOT}"/src/addons/*/*/PACKAGE.INI; do
    -    folder="$(dirname "${template}")"
    -    base="$(basename "${folder}")"
    -    name="$(echo "${base}" | tr '[:lower:]' '[:upper:]')"
    -
    -    # An add-on whose files the build makes (fonts.ini: src/tools/Makefile)
    -    # is packed from obj/addons/NAME
    -    if [ -d "${ROOT}/obj/addons/${base}" ]; then
    -        folder="${ROOT}/obj/addons/${base}"
    -    fi
    -
    -    "${TOOL}" build "${template}" "${folder}" "${DIST}/addons/${name}.DPK" > /dev/null || fail "the add-on ${folder} could not be built"
    -done
    -
     rm -rf "${STAGE}"
     
     # The SDK: bin/sdk and the references (make all writes docs/) in DVX-SDK/
    @@ -217,6 +384,8 @@ echo "mkDist: SETUP.EXE ${SETUP_SIZE} bytes; package $(stat -c %s "${DIST}/dvx.z
     echo "mkDist: CD: ${DIST}/DVX.ISO (SETUP.EXE $(stat -c %s "${DIST}/cd/SETUP.EXE") bytes with the package)"
     echo "mkDist: floppies (${FLOPPY}): ${DISKS} disk(s) in ${DIST}/floppy, ${ROOM1} bytes of the package on Disk 1"
     echo "mkDist: SDK: ${DIST}/sdk.zip ($(stat -c %s "${DIST}/sdk.zip") bytes)"
    -echo "mkDist: DVX's update: ${DIST}/DVXUPD.DPK ($(stat -c %s "${DIST}/DVXUPD.DPK") bytes)"
    +echo "mkDist: DVX's update: ${DIST}/DVXUPD.DPK ($(stat -c %s "${DIST}/DVXUPD.DPK") bytes); its fonts: ${DIST}/BASEFONT.DPK ($(stat -c %s "${DIST}/BASEFONT.DPK") bytes)"
     addonFiles=("${DIST}/addons/"*)
     echo "mkDist: add-ons: ${addonFiles[*]##*/}"
    +packageFiles=("${PKG_DIR}/"*)
    +echo "mkDist: DVX's optional packages (${#packageFiles[@]}, for the package server): ${packageFiles[*]##*/}"
    diff --git a/src/installer/mkPackages.py b/src/installer/mkPackages.py
    new file mode 100644
    index 00000000..f7343e4e
    --- /dev/null
    +++ b/src/installer/mkPackages.py
    @@ -0,0 +1,387 @@
    +#!/usr/bin/env python3
    +# The MIT License (MIT)
    +#
    +# Copyright (C) 2026 Scott Duensing
    +#
    +# Permission is hereby granted, free of charge, to any person obtaining a copy
    +# of this software and associated documentation files (the "Software"), to
    +# deal in the Software without restriction, including without limitation the
    +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
    +# sell copies of the Software, and to permit persons to whom the Software is
    +# furnished to do so, subject to the following conditions:
    +#
    +# The above copyright notice and this permission notice shall be included in
    +# all copies or substantial portions of the Software.
    +#
    +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
    +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    +# IN THE SOFTWARE.
    +
    +# mkPackages.py MANIFEST STAGE SRCDIR VERSION ABI OUTDIR
    +#
    +# The optional packages of a release (src/packages.ini) worked out from
    +# the staged release (STAGE, laid out as bin/), for mkDist.sh:
    +#   OUTDIR/ID.ini     each package's template (PACKAGE.INI without
    +#                     [files]): DVX's VERSION, the ABI for code, and
    +#                     [depends] -- a member's from the modules its code
    +#                     imports (dxeInfo.py), the libraries a BASIC program
    +#                     declares (its project's include files, under SRCDIR),
    +#                     and its uses; a bundle's from its members and needs
    +#   OUTDIR/members.lst  "ID FOLDER [PATH ...]" for each member: its folder
    +#                     and its files outside it (its manifest's paths, a
    +#                     DOS program in SYSTEM, say), as staged
    +#   OUTDIR/bundles.lst  "ID" for each bundle, in the manifest's order
    +#   OUTDIR/SETUP.INI  the components Setup offers: each bundle's name,
    +#                     summary, the bundles it needs, its members, their
    +#                     folders and paths, and the bytes and files they take
    +#
    +# Stops (exit 1, naming what is wrong) when a member's folder or one of
    +# its paths is missing, a path is two members' or in a member's folder,
    +# a member is in two bundles or none, a module of DVX itself needs an
    +# optional one (it could not be removed; a program of DVX's that uses one
    +# only when it is there is listed in the manifest's [kpunch.dvx]), or a
    +# member needs a member of another bundle its own bundle does not need.
    +
    +import configparser
    +import os
    +import re
    +import sys
    +
    +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'tools'))
    +from dxeInfo import DxeError, exeExports, moduleSyms
    +
    +CORE_ID      = 'kpunch.dvx'                     # PKG_SYSTEM_ID: DVX itself
    +VENDOR       = 'kpunch'
    +KIND_FOLDERS = {'app': 'apps', 'library': 'libs', 'widget': 'widgets'}
    +MODULE_EXTS  = ('.app', '.lib', '.wgt')
    +BUNDLE       = 'bundle'
    +PUBLISHER    = 'Kangaroo Punch Studios'
    +AUTHOR       = 'Scott Duensing'
    +LICENCE      = 'MIT'
    +FORMAT       = '1'
    +INCLUDE_DIR  = 'include/basic/'                 # a BASIC project's include files (.dbp [Modules])
    +DECLARE_RE   = re.compile(r'^\s*DECLARE\s+LIBRARY\s+"([^"]+)"', re.IGNORECASE)
    +
    +
    +def fail(msg):
    +    sys.stderr.write('mkPackages: %s\n' % msg)
    +    sys.exit(1)
    +
    +
    +def words(text):
    +    return [w for w in (text or '').split() if w]
    +
    +
    +# The file or folder (as wantDir says) under root whose path matches rel
    +# in any case (the build's folders are not all lower case), or None.
    +def findPath(root, rel, wantDir):
    +    path = root
    +
    +    for part in rel.split('/'):
    +        try:
    +            names = os.listdir(path)
    +        except OSError:
    +            return None
    +
    +        match = [n for n in names if n.lower() == part.lower()]
    +
    +        if not match:
    +            return None
    +
    +        path = os.path.join(path, match[0])
    +
    +    return path if (os.path.isdir(path) if wantDir else os.path.isfile(path)) else None
    +
    +
    +def findFolder(root, rel):
    +    return findPath(root, rel, True)
    +
    +
    +# Every module (.app .lib .wgt) under root, by its stage path
    +def modulesUnder(root, stage):
    +    found = []
    +
    +    for dirPath, _, files in os.walk(root):
    +        for f in files:
    +            if f.lower().endswith(MODULE_EXTS):
    +                found.append(os.path.relpath(os.path.join(dirPath, f), stage))
    +
    +    return found
    +
    +
    +# The libraries a BASIC program's project declares (DECLARE LIBRARY in
    +# its include files), by name; empty for a program that is not BASIC
    +def basicLibraries(srcDir, name):
    +    libs = set()
    +
    +    for dirPath, _, files in os.walk(os.path.join(srcDir, 'apps')):
    +        if os.path.basename(dirPath).lower() != name.lower():
    +            continue
    +
    +        for f in files:
    +            if not f.lower().endswith('.dbp'):
    +                continue
    +
    +            project = configparser.ConfigParser(strict=False, interpolation=None)
    +            project.read(os.path.join(dirPath, f))
    +
    +            for key, value in (project['Modules'].items() if project.has_section('Modules') else []):
    +                if key.lower() == 'count' or INCLUDE_DIR not in value.replace('\\', '/'):
    +                    continue
    +
    +                with open(os.path.normpath(os.path.join(dirPath, value)), errors='replace') as inc:
    +                    for line in inc:
    +                        m = DECLARE_RE.match(line)
    +
    +                        if m:
    +                            libs.add(m.group(1).lower())
    +
    +    return libs
    +
    +
    +def folderBytes(path):
    +    total = 0
    +    count = 0
    +
    +    for dirPath, _, files in os.walk(path):
    +        for f in files:
    +            total += os.path.getsize(os.path.join(dirPath, f))
    +            count += 1
    +
    +    return total, count
    +
    +
    +def template(pid, sec, kind, version, abi, depends):
    +    lines = ['[package]',
    +             'format    = ' + FORMAT,
    +             'id        = ' + pid,
    +             'name      = ' + sec['name'],
    +             'version   = ' + version,
    +             'kind      = ' + kind,
    +             'summary   = ' + sec['summary'],
    +             'publisher = ' + PUBLISHER,
    +             'author    = ' + AUTHOR,
    +             'licence   = ' + LICENCE]
    +
    +    if kind in KIND_FOLDERS:
    +        lines.append('abi       = ' + abi)
    +
    +    lines += ['', '[depends]'] + ['%s = %s' % (d, version) for d in sorted(depends)]
    +    return '\n'.join(lines) + '\n'
    +
    +
    +def main(argv):
    +    if len(argv) != 7:
    +        sys.stderr.write('usage: mkPackages.py MANIFEST STAGE SRCDIR VERSION ABI OUTDIR\n')
    +        return 2
    +
    +    manifestPath, stage, srcDir, version, abi, outDir = argv[1:]
    +    ini = configparser.ConfigParser(strict=True, interpolation=None)
    +    ini.read(manifestPath)
    +
    +    if not ini.has_section('packages'):
    +        fail('%s has no [packages] list' % manifestPath)
    +
    +    bundles = [pid for pid in ini['packages'] if ini['packages'][pid].strip() == BUNDLE]
    +    member  = {}                                    # member id -> its bundle
    +
    +    for b in bundles:
    +        if not ini.has_section(b):
    +            fail('the bundle %s has no section' % b)
    +
    +        for m in words(ini[b].get('members')):
    +            if m in member:
    +                fail('%s is in two bundles, %s and %s' % (m, member[m], b))
    +
    +            member[m] = b
    +
    +    for sec in ini.sections():
    +        if sec not in ('packages', CORE_ID) and sec not in bundles and sec not in member:
    +            fail('%s is in no bundle' % sec)
    +
    +    optionalUse = ini[CORE_ID] if ini.has_section(CORE_ID) else {}
    +
    +    # Each member's folder, paths, and modules
    +    folderOf  = {}
    +    pathsOf   = {}
    +    pathOwner = {}                                  # path (lower case) -> (member id, path as staged)
    +    owner     = {}                                  # module stage path -> member id
    +
    +    for m in member:
    +        if not ini.has_section(m):
    +            fail('the member %s has no section' % m)
    +
    +        kind = ini[m].get('kind', '')
    +
    +        if kind not in KIND_FOLDERS:
    +            fail('%s: kind %s is not app, library, or widget' % (m, kind))
    +
    +        rel  = '%s/%s/%s' % (KIND_FOLDERS[kind], VENDOR, m.split('.', 1)[1])
    +        path = findFolder(stage, rel)
    +
    +        if not path:
    +            fail('%s: its folder %s is not in the release' % (m, rel))
    +
    +        folderOf[m] = os.path.relpath(path, stage).replace(os.sep, '/')
    +        pathsOf[m]  = []
    +
    +        for mod in modulesUnder(path, stage):
    +            owner[mod] = m
    +
    +        for rel in words(ini[m].get('paths')):
    +            found = findPath(stage, rel, False)
    +
    +            if not found:
    +                fail('%s: its file %s is not in the release' % (m, rel))
    +
    +            staged = os.path.relpath(found, stage).replace(os.sep, '/')
    +
    +            if staged.lower() in pathOwner:
    +                fail('%s is in two packages, %s and %s' % (staged, pathOwner[staged.lower()][0], m))
    +
    +            pathOwner[staged.lower()] = (m, staged)
    +            pathsOf[m].append(staged)
    +
    +    for lower, (m, staged) in pathOwner.items():
    +        for other, folder in folderOf.items():
    +            if lower.startswith(folder.lower() + '/'):
    +                fail('%s (a path of %s) is in the folder of %s' % (staged, m, other))
    +
    +    # Who exports what (the modules of every package, DVX's own included)
    +    allModules = []
    +
    +    for kindDir in KIND_FOLDERS.values():
    +        top = findFolder(stage, kindDir)
    +
    +        if top:
    +            allModules += modulesUnder(top, stage)
    +
    +    dvxExe   = [f for f in os.listdir(stage) if f.lower() == 'dvx.exe']
    +    exported = set(exeExports(os.path.join(stage, dvxExe[0]))) if dvxExe else set()
    +    exporter = {}
    +    imports  = {}
    +    libOwner = {}                                   # library base name -> package id
    +
    +    try:
    +        for mod in allModules:
    +            exps, unres = moduleSyms(os.path.join(stage, mod))
    +            imports[mod] = unres
    +
    +            for e in exps:
    +                exporter.setdefault(e, mod)
    +
    +            if mod.lower().endswith('.lib'):
    +                libOwner[os.path.splitext(os.path.basename(mod))[0].lower()] = owner.get(mod, CORE_ID)
    +    except DxeError as e:
    +        fail(str(e))
    +
    +    # Each package's needs: its modules' imports, its BASIC programs'
    +    # libraries, its uses
    +    needs = {}
    +
    +    for mod in allModules:
    +        pid  = owner.get(mod, CORE_ID)
    +        want = needs.setdefault(pid, set())
    +
    +        for sym in imports[mod]:
    +            src = exporter.get(sym)
    +
    +            if src and sym.lstrip('_') not in exported and sym not in exported:
    +                want.add(owner.get(src, CORE_ID))
    +
    +    for mod in allModules:
    +        parts = mod.replace(os.sep, '/').split('/')
    +
    +        if not mod.lower().endswith('.app'):
    +            continue
    +
    +        pid   = owner.get(mod, CORE_ID)
    +        known = set(words(optionalUse.get(parts[2].lower()))) if pid == CORE_ID else set()
    +
    +        for lib in basicLibraries(srcDir, parts[2]):
    +            if lib not in libOwner:
    +                fail('%s declares the library %s, which the release does not have' % (mod, lib))
    +
    +            if libOwner[lib] not in known:
    +                needs.setdefault(pid, set()).add(libOwner[lib])
    +
    +    for m in member:
    +        needs.setdefault(m, set()).update(words(ini[m].get('uses')))
    +
    +    # DVX itself needs nothing optional; a member needs only its own
    +    # bundle's members, DVX, and what its bundle needs
    +    for pid, want in needs.items():
    +        want.discard(pid)
    +        optional = [w for w in want if w in member]
    +
    +        if pid == CORE_ID and optional:
    +            fail('DVX itself needs %s, so it cannot be optional' % ' '.join(sorted(optional)))
    +
    +        if pid == CORE_ID:
    +            continue
    +
    +        allowed = set()
    +        todo    = [member[pid]]
    +
    +        while todo:
    +            b = todo.pop()
    +
    +            if b not in allowed:
    +                allowed.add(b)
    +                todo += words(ini[b].get('needs'))
    +
    +        for w in optional:
    +            if member[w] not in allowed:
    +                fail('%s (%s) needs %s (%s), but %s does not need %s' % (pid, member[pid], w, member[w], member[pid], member[w]))
    +
    +    os.makedirs(outDir, exist_ok=True)
    +
    +    with open(os.path.join(outDir, 'members.lst'), 'w') as lst:
    +        for m in sorted(member):
    +            deps = needs.get(m, set()) | {CORE_ID}
    +
    +            with open(os.path.join(outDir, m + '.ini'), 'w') as t:
    +                t.write(template(m, ini[m], ini[m]['kind'], version, abi, deps))
    +
    +            lst.write(' '.join([m, folderOf[m]] + pathsOf[m]) + '\n')
    +
    +    comps = ['[components]'] + ['%s = %s' % (b, ini[b]['name']) for b in bundles]
    +
    +    with open(os.path.join(outDir, 'bundles.lst'), 'w') as lst:
    +        for b in bundles:
    +            members = words(ini[b].get('members'))
    +            deps    = set(members) | set(words(ini[b].get('needs'))) | {CORE_ID}
    +            total   = 0
    +            count   = 0
    +
    +            for m in members:
    +                n, c   = folderBytes(os.path.join(stage, folderOf[m]))
    +                total += n + sum(os.path.getsize(os.path.join(stage, f)) for f in pathsOf[m])
    +                count += c + len(pathsOf[m])
    +
    +            with open(os.path.join(outDir, b + '.ini'), 'w') as t:
    +                t.write(template(b, ini[b], BUNDLE, version, abi, deps))
    +
    +            lst.write(b + '\n')
    +            comps += ['',
    +                      '[%s]' % b,
    +                      'summary = ' + ini[b]['summary'],
    +                      'needs   = ' + ' '.join(words(ini[b].get('needs'))),
    +                      'members = ' + ' '.join(members),
    +                      'folders = ' + ' '.join(folderOf[m] + '/' for m in members),
    +                      'paths   = ' + ' '.join(f for m in members for f in pathsOf[m]),
    +                      'bytes   = %d' % total,
    +                      'files   = %d' % count]
    +
    +    with open(os.path.join(outDir, 'SETUP.INI'), 'w', newline='\r\n') as s:
    +        s.write('\n'.join(comps) + '\n')
    +
    +    return 0
    +
    +
    +if __name__ == '__main__':
    +    sys.exit(main(sys.argv))
    diff --git a/src/installer/pkgRead.c b/src/installer/pkgRead.c
    index 84f52aa5..4c796836 100644
    --- a/src/installer/pkgRead.c
    +++ b/src/installer/pkgRead.c
    @@ -56,6 +56,8 @@ struct PkgJobT {
         char                destDir[DVX_MAX_PATH];
         PkgAskDiskFnT       ask;
         void               *user;
    +    PkgSkipFnT          skip;               // pkgJobSkip
    +    void               *skipUser;
         FILE               *part;
         int32_t             disk;
         uint32_t            partLeft;           // bytes of the part not read yet
    @@ -69,7 +71,7 @@ struct PkgJobT {
         PkgZipEntryT        entry;
         char                shownName[PKG_NAME_MAX]; // name in native separators, for people
         char                outPath[DVX_MAX_PATH];
    -    FILE               *out;
    +    FILE               *out;                // NULL while one is skipped
         // totals
         uint64_t            bytesDone;          // files finished
         int32_t             filesDone;
    @@ -102,6 +104,7 @@ char *const        *pkgJobFiles(const PkgJobT *job);
     void                pkgJobFree(PkgJobT *job);
     const char         *pkgJobMessage(const PkgJobT *job);
     void                pkgJobProgress(const PkgJobT *job, PkgProgressT *out);
    +void                pkgJobSkip(PkgJobT *job, PkgSkipFnT skip, void *user);
     PkgStepE            pkgJobStep(PkgJobT *job);
     char *const        *pkgJobWritten(const PkgJobT *job);
     int32_t             pkgPartInDir(const char *dir);
    @@ -178,19 +181,22 @@ static PkgStepE entryData(PkgJobT *job) {
     }
     
     
    -// The file's data has all been read and checked: close it, date it.
    +// The file's data has all been read and checked: close it, date it (a
    +// skipped one was not written).
     static bool entryFinish(PkgJobT *job) {
         FILE *f = job->out;
     
         job->out = NULL;
     
    -    if (fclose(f) != 0) {
    +    if (f && fclose(f) != 0) {
             remove(job->outPath);
             fail(job, "Setup could not write %s. The disk may be full.", job->outPath);
             return false;
         }
     
    -    fileOpsSetTime(job->outPath, pkgZipDosTime(job->entry.dosDate, job->entry.dosTime));
    +    if (f) {
    +        fileOpsSetTime(job->outPath, pkgZipDosTime(job->entry.dosDate, job->entry.dosTime));
    +    }
     
         job->bytesDone += job->entry.size;
         job->filesDone++;
    @@ -224,6 +230,12 @@ static PkgStepE entryHeader(PkgJobT *job) {
             return fail(job, "The DVX package holds a name Setup does not accept: %s", job->shownName);
         }
     
    +    // Left out: a folder is passed over, a file's data read past
    +    if (job->skip && job->skip(job->skipUser, job->entry.name)) {
    +        job->state = job->entry.isDir ? JobHeaderE : JobDataE;
    +        return PkgStepMoreE;
    +    }
    +
         if (job->entry.isDir) {
             if (!ensureDirs(job, job->entry.name, true)) {
                 return PkgStepErrorE;
    @@ -285,11 +297,12 @@ static PkgStepE fail(PkgJobT *job, const char *fmt, ...) {
     }
     
     
    -// The reader's unpacked bytes, into the open file.
    +// The reader's unpacked bytes, into the open file (none for a skipped
    +// one).
     static bool fileWrite(void *ctx, const uint8_t *data, int32_t len) {
         PkgJobT *job = (PkgJobT *)ctx;
     
    -    if (fwrite(data, 1, (size_t)len, job->out) != (size_t)len) {
    +    if (job->out && fwrite(data, 1, (size_t)len, job->out) != (size_t)len) {
             fail(job, "Setup could not write %s. The disk may be full.", job->outPath);
             return false;
         }
    @@ -566,6 +579,12 @@ void pkgJobProgress(const PkgJobT *job, PkgProgressT *out) {
     }
     
     
    +void pkgJobSkip(PkgJobT *job, PkgSkipFnT skip, void *user) {
    +    job->skip     = skip;
    +    job->skipUser = user;
    +}
    +
    +
     PkgStepE pkgJobStep(PkgJobT *job) {
         if (job->failed) {
             return PkgStepErrorE;
    diff --git a/src/installer/pkgRead.h b/src/installer/pkgRead.h
    index 4c8606d4..84c1e9bc 100644
    --- a/src/installer/pkgRead.h
    +++ b/src/installer/pkgRead.h
    @@ -89,6 +89,10 @@ typedef struct {
     // cancel the job.
     typedef bool (*PkgAskDiskFnT)(void *user, int32_t disk, PkgDiskProblemE problem);
     
    +// Whether to leave out the file or folder name (relative, '/' between
    +// parts, a folder's without its '/'): Setup's parts not chosen.
    +typedef bool (*PkgSkipFnT)(void *user, const char *name);
    +
     typedef struct PkgJobT PkgJobT;
     
     // Look for the package of the program at exePath.
    @@ -117,6 +121,11 @@ const char  *pkgJobMessage(const PkgJobT *job);
     
     void         pkgJobProgress(const PkgJobT *job, PkgProgressT *out);
     
    +// Leave out what skip says yes to: read past (its CRC still checked), not
    +// written, made, or listed; it counts as done for the progress.  Call it
    +// before the first step.
    +void         pkgJobSkip(PkgJobT *job, PkgSkipFnT skip, void *user);
    +
     // Do a little work: at most one file header and PKG_STEP_BYTES of data.
     PkgStepE     pkgJobStep(PkgJobT *job);
     
    diff --git a/src/installer/setup.c b/src/installer/setup.c
    index 16c43b66..9814436c 100644
    --- a/src/installer/setup.c
    +++ b/src/installer/setup.c
    @@ -28,13 +28,20 @@
     // DVX.P01.. beside it on floppies (pkgRead.h).  Pages in one window:
     // licence; start, a tile (icon, button, and what it does) for each of
     // Install, Upgrade or repair, and Remove; the folder page, set up for the
    -// one chosen (the folder, with Browse...; the fonts and AUTOEXEC.BAT to
    -// install or upgrade; the user folder to start over or remove);
    -// progress; done.  The fonts are the package's later tiers: none, the
    -// base set, or all of them are unpacked (the disks after the last one
    -// chosen are never asked for), and an upgrade to fewer fonts removes the
    -// others.  Without a package (the SETUP.EXE installed in the DVX folder)
    -// it offers only Remove of its own folder.
    +// one chosen (the folder, with Browse...; AUTOEXEC.BAT to install or
    +// upgrade; the user folder to start over or remove); for Install and
    +// Upgrade, what to install (the fonts, DVX's optional parts, the add-ons,
    +// and the room they need); progress; done.  The base set of fonts is the
    +// package's later tier, unpacked or not (without it the disks after DVX's
    +// own are never asked for), and an upgrade without it removes it.  The
    +// optional parts (SETUP.INI beside SETUP.EXE: setupCore.h) are in the
    +// package too, a checkbox each; the files of a part not chosen are left
    +// out, and an upgrade removes a part unticked.  The add-ons are the
    +// signed packages in the medium's ADDONS folder, a checkbox each,
    +// installed after DVX by the package library; an upgrade starts with the
    +// parts and add-ons the folder has ticked, and one unticked is removed.
    +// Without a package (the SETUP.EXE installed in the DVX folder) it offers
    +// only Remove of its own folder.
     //
     // The window sits on a splash backdrop drawn into the desktop's wallpaper
     // buffer: a blue gradient with a large shadowed DVX title (the system
    @@ -50,17 +57,28 @@
     //                              taken as accepted, settings kept on an
     //                              upgrade, AUTOEXEC.BAT untouched); exit
     //                              code 0 when installed
    -//   SETUP /AUTO=.. /FONTS=BASE  NONE, BASE, or ALL fonts (else what the
    -//                              folder has; all on a new install); without
    -//                              /AUTO, the choice the folder page starts
    +//   SETUP /AUTO=.. /FONTS=BASE  NONE or BASE fonts (else what the folder
    +//                              has; the base set on a new install);
    +//                              without /AUTO, the choice the page starts
    +//                              with
    +//   SETUP /AUTO=.. /ADDONS=MOREFONT,DVXGM  the add-ons to have: ALL, NONE,
    +//                              or their names, the files' (else what the
    +//                              folder has); without /AUTO, what the page
    +//                              starts with
    +//   SETUP /AUTO=.. /PARTS=BASIC,COMM  DVX's optional parts to have: ALL,
    +//                              NONE, or their names (the ids' names, as in
    +//                              SETUP.INI; else what the folder has, all on
    +//                              a new install), each with the parts it
    +//                              needs; without /AUTO, what the page starts
     //                              with
     //   SETUP /LOG=C:\SETUP.LOG    write the log there (else it stays in memory)
     //   SETUP /AUTO=.. /SHOTS=C:\X  test only (the DOS test harness; not in the
     //                              user docs): also show every page an
     //                              install shows -- licence, start, the folder
     //                              page as Install or Upgrade (what /AUTO
    -//                              does), progress (before the unpacking
    -//                              starts), done, the folder page as Remove --
    +//                              does), what to install, progress (before
    +//                              the unpacking starts), done, the folder
    +//                              page as Remove --
     //                              and save each as C:\X\SU-*.BMP (sShotNames,
     //                              sActions); also ask the questions /AUTO
     //                              does not ask (disk prompts), save them and
    @@ -86,6 +104,7 @@
     #include "progressBar/progress.h"
     #include "radio/radio.h"
     #include "textInput/textInpt.h"
    +#include "wrapBox/wrapBox.h"
     #include "fileops/fileOps.h"
     #include "pkgRead.h"
     #include "setupCore.h"
    @@ -115,6 +134,7 @@
     #define SETUP_WIN_H        330      // under the splash title on a 640 x 480 screen
     #define SETUP_WIN_GAP      10       // between the splash title and the window
     #define SETUP_SPACING      6
    +#define SETUP_CHECK_GAP    12       // between the checkboxes of parts or add-ons, across and down
     #define SETUP_SLICE_MS     100
     #define SETUP_DEST_FOLDER  "DVX"
     #define SETUP_MSG_MAX      (DVX_MAX_PATH * 2 + SETUP_ERR_MAX)
    @@ -122,6 +142,9 @@
     #define SETUP_ARG_LOG      "/LOG="
     #define SETUP_ARG_SHOTS    "/SHOTS="
     #define SETUP_ARG_FONTS    "/FONTS="
    +#define SETUP_ARG_ADDONS   "/ADDONS="
    +#define SETUP_ARG_PARTS    "/PARTS="
    +#define SETUP_NEXT_LABEL   "&Next >"
     #define SETUP_BYTES_PER_MB (1024 * 1024)
     #define SETUP_EXIT_FAILED  1
     #define SETUP_DISK_POLL_MS 250
    @@ -132,7 +155,7 @@
     #define SETUP_BOARD_LINES  3
     #define SETUP_BOARD_PAD    10       // inside a billboard's edges
     #define SETUP_BOARD_GAP    6        // between its headline and its lines
    -#define SETUP_PERCENT      100      // sparkle positions are percentages of the board
    +#define SETUP_PERCENT      100      // sparkle positions are percentages of the board; the progress bar's full value
     #define SETUP_SPARKLE_CORE 3        // the square at a sparkle's centre, pixels across
     
     // The splash backdrop
    @@ -169,11 +192,13 @@ void wgtRegisterProgress(void);
     void wgtRegisterRadio(void);
     void wgtRegisterSpacer(void);
     void wgtRegisterTextInput(void);
    +void wgtRegisterWrapBox(void);
     
     typedef enum {
         PageLicenceE,
         PageStartE,
         PageFolderE,
    +    PageChoiceE,
         PageCopyE,
         PageDoneE,
         PageCountE
    @@ -199,6 +224,21 @@ typedef struct {
         const char    *shot;                      // /SHOTS: the folder page's picture
     } SetupActionT;
     
    +// The unpacking and the add-ons run in slices of SETUP_SLICE_MS, drawing
    +// between them: when the slice started, and when the billboard shown went
    +// up (sliceIdle)
    +typedef struct {
    +    PlatformTicksT sliceStart;
    +    PlatformTicksT boardStart;
    +} SliceT;
    +
    +// Which optional parts partsBytes counts
    +typedef enum {
    +    PartsAllE,
    +    PartsChosenE,
    +    PartsInstalledE
    +} PartsWhichE;
    +
     // A colour as red, green, blue (packColor makes the display's pixel)
     typedef struct {
         uint8_t r;
    @@ -257,9 +297,12 @@ typedef struct {
         WidgetT        *folderLabel;
         WidgetT        *dest;               // target folder
         WidgetT        *browseBtn;
    -    WidgetT        *fontsBox;           // the fonts: label, choice, and room needed
         WidgetT        *fontsGroup;         // the fonts choice: a radio button per SetupFontsE
    -    WidgetT        *spaceText;          // the room the choice of fonts needs
    +    WidgetT        *spaceText;          // the room the fonts, parts, and add-ons chosen need
    +    WidgetT       **addonChecks;        // stb_ds: addons[i]'s checkbox
    +    WidgetT       **partChecks;         // stb_ds: parts[i]'s checkbox
    +    WidgetT        *partsLabel;         // over them: to install, or (an upgrade) to have
    +    WidgetT        *choiceGoBtn;        // the choice page's Install or Upgrade
         WidgetT        *userBox;            // start over / remove the user folder
         WidgetT        *removeNote;         // what Remove leaves
         WidgetT        *autoexec;           // "start with the computer" (NULL without a boot drive)
    @@ -273,6 +316,11 @@ typedef struct {
         WidgetT        *doneRun;            // how to start it
         WidgetT        *startNow;
         PkgSourceT      src;
    +    SetupAddonT    *addons;             // stb_ds: the medium's (setupAddonsFind)
    +    SetupPartT     *parts;              // stb_ds: the optional parts (setupPartsRead)
    +    bool           *partsChosen;        // stb_ds: parts[i] chosen (partsPreset, the checkboxes)
    +    bool            upgrade;            // the folder has DVX (folderCheck)
    +    bool            keepSettings;       // ... and keeps its settings
         char            exePath[DVX_MAX_PATH];
         char            destDir[DVX_MAX_PATH];
         char            autoDest[DVX_MAX_PATH];
    @@ -281,6 +329,10 @@ typedef struct {
         SetupFontsE     fontsArg;           // /FONTS=
         bool            fontsGiven;
         bool            fontsBad;           // /FONTS= is none of sFontsArgs
    +    char            addonsArg[DVX_MAX_PATH];    // /ADDONS=
    +    bool            addonsGiven;
    +    char            partsArg[DVX_MAX_PATH];     // /PARTS=
    +    bool            partsGiven;
         bool            quit;
         bool            autoMode;
         bool            shots;              // /SHOTS with /AUTO: save every page
    @@ -306,14 +358,15 @@ static const char *const sFoundNames[] = { "appended", "in parts", "none", "dama
     
     // The fonts choices (SetupFontsE order): /FONTS= (and the log), and the
     // radio buttons (with the size each adds to DVX's own, but for none)
    -static const char *const sFontsArgs[SetupFontsCountE]   = { "NONE", "BASE", "ALL" };
    -static const char *const sFontsLabels[SetupFontsCountE] = { "&None: DVX's own font only", "Base s&et: DejaVu Sans, Sans Mono, and Serif", "A&ll fonts" };
    +static const char *const sFontsArgs[SetupFontsCountE]   = { "NONE", "BASE" };
    +static const char *const sFontsLabels[SetupFontsCountE] = { "&None: DVX's own font only", "Base s&et: DejaVu Sans, Sans Mono, and Serif" };
     
     // /SHOTS: each page's picture (8.3 names; the folder page's is its
     // action's)
     static const char *const sShotNames[PageCountE] = {
         [PageLicenceE] = "SU-LIC.BMP",
         [PageStartE]   = "SU-START.BMP",
    +    [PageChoiceE]  = "SU-WHAT.BMP",
         [PageCopyE]    = "SU-COPY.BMP",
         [PageDoneE]    = "SU-DONE.BMP"
     };
    @@ -375,6 +428,8 @@ static int32_t sBoardType = -1;
     // Prototypes
     // ============================================================
     
    +static bool               addonChosen(int32_t i);
    +static uint64_t           addonsSpace(void);
     static int32_t            ask(const char *msg, int32_t flags, const char *shot, int32_t disk);
     static bool               askDisk(void *user, int32_t disk, PkgDiskProblemE problem);
     static void               boardCalcMinSize(WidgetT *w, const BitmapFontT *font);
    @@ -382,20 +437,23 @@ static WidgetT           *boardCreate(WidgetT *parent, const SetupBoardT *boards
     static void               boardDestroy(WidgetT *w);
     static void               boardNext(WidgetT *w);
     static void               boardPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
    +static void               buildChoicePage(WidgetT *p);
     static void               buildFolderPage(WidgetT *p, char boot);
     static void               buildPages(WidgetT *root);
     static void               buildStartPage(WidgetT *p);
     static WidgetT           *buttonRow(WidgetT *page);
    +static WidgetT           *checkRow(WidgetT *page, const char *label, int32_t count, WidgetT ***checks, void (*onChange)(WidgetT *w));
     static void               drawShadowed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, int32_t offset, const SetupRgbT *color);
     static void               drawSparkle(DisplayT *d, const BlitOpsT *ops, int32_t cx, int32_t cy, int32_t radius, uint32_t color);
     static void               fillGradient(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, int32_t h, const SetupRgbT *top, const SetupRgbT *bottom);
    +static bool               folderCheck(const char *destDir, bool *upgrade, bool *keepSettings);
     static void               folderGet(char *out, int32_t outSize);
     static SetupFontsE        fontsChosen(const char *destDir);
     static void               fontsLoad(void);
     static SetupFontsE        fontsPreset(const char *destDir);
     static void               installAutoexec(const char *destDir);
    -static bool               installCheck(const char *destDir, SetupFontsE fonts, bool *upgrade, bool *keepSettings);
     static PkgStepE           installCopy(const char *destDir, SetupFontsE fonts, bool newInstall);
    +static void               installPackages(const char *destDir);
     static void               installRun(void);
     static uint64_t           megabytes(uint64_t bytes);
     static void               onAccept(WidgetT *w);
    @@ -403,22 +461,35 @@ static void               onAskIdle(void *ctx);
     static void               onBack(WidgetT *w);
     static void               onBrowse(WidgetT *w);
     static void               onCancel(WidgetT *w);
    +static void               onChoice(WidgetT *w);
     static void               onClose(WindowT *win);
     static void               onFinish(WidgetT *w);
    -static void               onFonts(WidgetT *w);
    +static void               onFolderGo(WidgetT *w);
     static void               onGo(WidgetT *w);
     static void               onNext(WidgetT *w);
    +static void               onPart(WidgetT *w);
     static void               onTile(WidgetT *w);
    +static PkgOpStateE        opRun(PkgOpT *op, const char *status);
     static void               paintBackdrop(void);
     static void               parseArgs(int argc, char *argv[]);
    +static void               partRemove(PkgDbT *db, const SetupPartT *part);
    +static uint64_t           partsBytes(PartsWhichE which);
    +static void               partsKeepNeeded(const char *destDir);
    +static void               partsLog(void);
    +static void               partsPreset(const char *destDir, bool newInstall);
    +static bool               partsSkip(void *user, const char *name);
     static void               report(const char *msg);
     static void               shootFile(const char *name);
     static void               shootPage(void);
    +static void               showChoice(const char *destDir);
     static void               showFolder(SetupActionE action);
     static void               showPage(PageE page);
     static void               showProgress(const PkgJobT *job);
     static void               showSpace(void);
     static void               showStart(void);
    +static void               sliceIdle(SliceT *s);
    +static bool               spaceCheck(const char *destDir, SetupFontsE fonts);
    +static uint64_t           spaceNeeded(SetupFontsE fonts, PartsWhichE which);
     static void               startDvx(void);
     static void               tileCreate(WidgetT *page, SetupActionE action);
     static const BitmapFontT *typeFont(SetupTypeE type);
    @@ -437,6 +508,30 @@ static const WidgetClassT sClassBoard = {
     };
     
     
    +// Whether add-on i is to be had: unattended, as /ADDONS= says (else what
    +// the folder has); else as its checkbox is.
    +static bool addonChosen(int32_t i) {
    +    if (sSetup.autoMode) {
    +        return setupAddonChosen(sSetup.addonsGiven ? sSetup.addonsArg : NULL, &sSetup.addons[i]);
    +    }
    +
    +    return wgtCheckboxIsChecked(sSetup.addonChecks[i]);
    +}
    +
    +
    +// Bytes the add-ons chosen need that the folder does not have yet (as
    +// setupAddonsInstalled last marked them)
    +static uint64_t addonsSpace(void) {
    +    uint64_t bytes = 0;
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.addons); i++) {
    +        bytes += (addonChosen(i) && !sSetup.addons[i].installed) ? sSetup.addons[i].bytes : 0;
    +    }
    +
    +    return bytes;
    +}
    +
    +
     // A question in a message box.  /SHOTS: onAskIdle saves it as shot and
     // answers it with its first button (Yes / OK); a disk prompt (disk > 0)
     // only once another disk is in the drive.
    @@ -593,10 +688,57 @@ static void boardPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma
     }
     
     
    +// The choice page's widgets, ticked for the folder by showChoice: the
    +// fonts, each with the room it adds to DVX's own (but for none); the
    +// optional parts by name (the room shows what each adds), each saying
    +// what it is when the pointer is on it; the add-ons (showChoice labels
    +// them); the room they need.
    +static void buildChoicePage(WidgetT *p) {
    +    const PkgHeaderT *info = &sSetup.src.info;
    +    char              text[SETUP_MSG_MAX];
    +
    +    wgtLabel(p, "Fonts to install:");
    +    sSetup.fontsGroup          = wgtRadioGroup(p);
    +    sSetup.fontsGroup->padding = wgtPixels(0);
    +
    +    for (int32_t f = 0; f < SetupFontsCountE; f++) {
    +        if (f == SetupFontsNoneE) {
    +            snprintf(text, sizeof(text), "%s", sFontsLabels[f]);
    +        } else {
    +            snprintf(text, sizeof(text), "%s (%" PRIu64 " MB)", sFontsLabels[f], megabytes(info->tiers[f].bytes - info->tiers[SetupFontsNoneE].bytes));
    +        }
    +
    +        wgtRadio(sSetup.fontsGroup, text);
    +    }
    +
    +    sSetup.fontsGroup->onChange = onChoice;
    +    sSetup.partsLabel           = checkRow(p, "", (int32_t)arrlen(sSetup.parts), &sSetup.partChecks, onPart);
    +    checkRow(p, "Add-ons to install:", (int32_t)arrlen(sSetup.addons), &sSetup.addonChecks, onChoice);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.parts); i++) {
    +        wgtSetText(sSetup.partChecks[i], sSetup.parts[i].name);
    +        sSetup.partChecks[i]->tooltip = sSetup.parts[i].summary;
    +    }
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.addons); i++) {
    +        sSetup.addonChecks[i]->tooltip = sSetup.addons[i].offer.manifest.summary;
    +    }
    +
    +    sSetup.spaceText = wgtLabel(p, "");
    +
    +    WidgetT *row = buttonRow(p);
    +
    +    wgtButton(row, "< &Back")->onClick = onBack;
    +    sSetup.choiceGoBtn          = wgtButton(row, "");
    +    sSetup.choiceGoBtn->onClick = onGo;
    +    wgtButton(row, "E&xit")->onClick = onCancel;
    +}
    +
    +
     // The folder page's widgets, set up per action by showFolder: the folder
    -// (/AUTO=, else DVX on the boot drive) with Browse..., the fonts, the
    -// user folder checkbox, what Remove leaves, AUTOEXEC.BAT (with a boot
    -// drive), and the note shown only without a package.
    +// (/AUTO=, else DVX on the boot drive) with Browse..., the user folder
    +// checkbox, what Remove leaves, AUTOEXEC.BAT (with a boot drive), and the
    +// note shown only without a package.
     static void buildFolderPage(WidgetT *p, char boot) {
         char text[SETUP_MSG_MAX];
     
    @@ -619,29 +761,6 @@ static void buildFolderPage(WidgetT *p, char boot) {
         }
     
         wgtSetText(sSetup.dest, text);
    -
    -    // The fonts, each with the room it adds to DVX's own (but for none)
    -    const PkgHeaderT *info = &sSetup.src.info;
    -
    -    sSetup.fontsBox          = wgtVBox(p);
    -    sSetup.fontsBox->padding = wgtPixels(0);
    -    sSetup.fontsBox->spacing = wgtPixels(SETUP_SPACING);
    -    wgtLabel(sSetup.fontsBox, "Fonts to install:");
    -    sSetup.fontsGroup          = wgtRadioGroup(sSetup.fontsBox);
    -    sSetup.fontsGroup->padding = wgtPixels(0);
    -
    -    for (int32_t f = 0; f < SetupFontsCountE; f++) {
    -        if (f == SetupFontsNoneE) {
    -            snprintf(text, sizeof(text), "%s", sFontsLabels[f]);
    -        } else {
    -            snprintf(text, sizeof(text), "%s (%" PRIu64 " MB)", sFontsLabels[f], megabytes(info->tiers[f].bytes - info->tiers[SetupFontsNoneE].bytes));
    -        }
    -
    -        wgtRadio(sSetup.fontsGroup, text);
    -    }
    -
    -    sSetup.fontsGroup->onChange = onFonts;
    -    sSetup.spaceText            = wgtLabel(sSetup.fontsBox, "");
         sSetup.userBox              = wgtCheckbox(p, "");
         sSetup.removeNote           = wgtLabel(p, "Files you added to the folder yourself are not removed.");
     
    @@ -656,7 +775,7 @@ static void buildFolderPage(WidgetT *p, char boot) {
         sSetup.backBtn            = wgtButton(row, "< &Back");
         sSetup.backBtn->onClick   = onBack;
         sSetup.goBtn              = wgtButton(row, "");
    -    sSetup.goBtn->onClick     = onGo;
    +    sSetup.goBtn->onClick     = onFolderGo;
         wgtButton(row, "E&xit")->onClick = onCancel;
     }
     
    @@ -685,13 +804,14 @@ static void buildPages(WidgetT *root) {
     
         WidgetT *row = buttonRow(p);
     
    -    sSetup.nextBtn          = wgtButton(row, "&Next >");
    +    sSetup.nextBtn          = wgtButton(row, SETUP_NEXT_LABEL);
         sSetup.nextBtn->onClick = onNext;
         wgtButton(row, "E&xit")->onClick = onCancel;
         wgtSetEnabled(sSetup.nextBtn, false);
     
         buildStartPage(sSetup.pages[PageStartE]);
         buildFolderPage(sSetup.pages[PageFolderE], platformBootDrive());
    +    buildChoicePage(sSetup.pages[PageChoiceE]);
     
         // Progress
         p             = sSetup.pages[PageCopyE];
    @@ -743,6 +863,33 @@ static WidgetT *buttonRow(WidgetT *page) {
     }
     
     
    +// Under label, count checkboxes side by side (as many to a row as fit, so
    +// the page keeps its height), each telling onChange (its index in
    +// userData), into *checks.  The label, or NULL: none (and no label) when
    +// count is 0.
    +static WidgetT *checkRow(WidgetT *page, const char *label, int32_t count, WidgetT ***checks, void (*onChange)(WidgetT *w)) {
    +    if (count == 0) {
    +        return NULL;
    +    }
    +
    +    WidgetT *title = wgtLabel(page, label);
    +    WidgetT *row   = wgtWrapBox(page);
    +
    +    row->padding = wgtPixels(0);
    +    row->spacing = wgtPixels(SETUP_CHECK_GAP);
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        WidgetT *check = wgtCheckbox(row, "");
    +
    +        check->onChange = onChange;
    +        check->userData = (void *)(intptr_t)i;
    +        arrput(*checks, check);
    +    }
    +
    +    return title;
    +}
    +
    +
     // Text scale times the font's size in color over a black shadow offset
     // pixels down and to the right.
     static void drawShadowed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t scale, int32_t offset, const SetupRgbT *color) {
    @@ -774,6 +921,36 @@ static void fillGradient(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y,
     }
     
     
    +// The folder against the action: Upgrade needs a DVX there, Install over
    +// one asks to upgrade it instead.  An upgrade keeps the settings unless
    +// Upgrade's "start over" is ticked.
    +static bool folderCheck(const char *destDir, bool *upgrade, bool *keepSettings) {
    +    char err[SETUP_ERR_MAX];
    +    char msg[SETUP_MSG_MAX];
    +
    +    if (!setupCheckDest(sSetup.src.partDir, destDir, err, sizeof(err))) {
    +        report(err);
    +        return false;
    +    }
    +
    +    *upgrade      = setupIsInstalled(destDir);
    +    *keepSettings = *upgrade && !(sSetup.action == ActionUpgradeE && wgtCheckboxIsChecked(sSetup.userBox));
    +
    +    if (!*upgrade && sSetup.action == ActionUpgradeE) {
    +        snprintf(msg, sizeof(msg), "DVX is not installed in %s.\n\nTo put DVX there, choose Back, then Install DVX.", destDir);
    +        report(msg);
    +        return false;
    +    }
    +
    +    if (*upgrade && sSetup.action == ActionInstallE && sSetup.asks) {
    +        snprintf(msg, sizeof(msg), "DVX is already installed in %s.\n\nUpgrade or repair it instead, keeping your settings and saved files?", destDir);
    +        return ask(msg, MB_YESNO | MB_ICONQUESTION, NULL, 0) == ID_YES;
    +    }
    +
    +    return true;
    +}
    +
    +
     // The folder the folder page names (/AUTO= put there), trimmed, with the
     // native separators.
     static void folderGet(char *out, int32_t outSize) {
    @@ -783,7 +960,7 @@ static void folderGet(char *out, int32_t outSize) {
     
     
     // The fonts to install in destDir: unattended fontsPreset, else the
    -// choice on the folder page.
    +// choice on the choice page.
     static SetupFontsE fontsChosen(const char *destDir) {
         return sSetup.autoMode ? fontsPreset(destDir) : (SetupFontsE)wgtRadioGroupGetSelected(sSetup.fontsGroup);
     }
    @@ -805,7 +982,7 @@ static void fontsLoad(void) {
     // The fonts first chosen for destDir: /FONTS=, else what the folder has
     // (all on a new install).
     static SetupFontsE fontsPreset(const char *destDir) {
    -    return sSetup.fontsGiven ? sSetup.fontsArg : setupFontsInstalled(destDir, &sSetup.src.info);
    +    return sSetup.fontsGiven ? sSetup.fontsArg : setupFontsInstalled(destDir);
     }
     
     
    @@ -841,77 +1018,26 @@ static void installAutoexec(const char *destDir) {
     }
     
     
    -// The folder; what is in it against the action (Upgrade needs a DVX
    -// there, Install over one asks to upgrade it instead); and the free space
    -// (on an upgrade, for more fonts than it has).  An upgrade keeps the
    -// settings unless Upgrade's "start over" is ticked.
    -static bool installCheck(const char *destDir, SetupFontsE fonts, bool *upgrade, bool *keepSettings) {
    -    char     err[SETUP_ERR_MAX];
    -    char     msg[SETUP_MSG_MAX];
    -    uint64_t needed = setupSpaceNeeded(&sSetup.src.info, fonts);
    -    uint64_t total  = 0;
    -    uint64_t avail  = 0;
    -
    -    if (!setupCheckDest(sSetup.src.partDir, destDir, err, sizeof(err))) {
    -        report(err);
    -        return false;
    -    }
    -
    -    *upgrade      = setupIsInstalled(destDir);
    -    *keepSettings = *upgrade && !(sSetup.action == ActionUpgradeE && wgtCheckboxIsChecked(sSetup.userBox));
    -
    -    if (!*upgrade && sSetup.action == ActionUpgradeE) {
    -        snprintf(msg, sizeof(msg), "DVX is not installed in %s.\n\nTo put DVX there, choose Back, then Install DVX.", destDir);
    -        report(msg);
    -        return false;
    -    }
    -
    -    if (*upgrade && sSetup.action == ActionInstallE && sSetup.asks) {
    -        snprintf(msg, sizeof(msg), "DVX is already installed in %s.\n\nUpgrade or repair it instead, keeping your settings and saved files?", destDir);
    -
    -        if (ask(msg, MB_YESNO | MB_ICONQUESTION, NULL, 0) != ID_YES) {
    -            return false;
    -        }
    -    }
    -
    -    // An upgrade replaces files of about the same size; only more fonts
    -    // than the folder has need room.
    -    if (*upgrade) {
    -        uint64_t have = setupSpaceNeeded(&sSetup.src.info, setupFontsInstalled(destDir, &sSetup.src.info));
    -
    -        needed = (needed > have) ? needed - have : 0;
    -    }
    -
    -    if (needed > 0 && platformDiskSpace(destDir, &total, &avail) && avail < needed) {
    -        char needText[FILEOPS_SIZE_TEXT_MAX];
    -        char freeText[FILEOPS_SIZE_TEXT_MAX];
    -
    -        fileOpsFormatSize(needed, needText, sizeof(needText));
    -        fileOpsFormatSize(avail, freeText, sizeof(freeText));
    -        snprintf(msg, sizeof(msg), "There is not enough free space for DVX on that disk.\n\nDVX needs about %s bytes; %s bytes are free.", needText, freeText);
    -        report(msg);
    -        return false;
    -    }
    -
    -    return true;
    -}
    -
    -
     // Unpack in slices, drawing progress and seeing Cancel between them.
     // Returns how it ended; a new install that did not finish is removed.
     static PkgStepE installCopy(const char *destDir, SetupFontsE fonts, bool newInstall) {
    -    PkgJobT       *job        = pkgJobCreate(&sSetup.src, destDir, (int32_t)fonts, askDisk, NULL);
    -    PlatformTicksT slice      = PLATFORM_MS_TO_TICKS(SETUP_SLICE_MS);
    -    PlatformTicksT sliceStart = platformClock();
    -    PlatformTicksT boardTime  = PLATFORM_MS_TO_TICKS(SETUP_BOARD_MS);
    -    PlatformTicksT boardStart = sliceStart;
    -    PkgStepE       step       = PkgStepErrorE;
    +    PkgJobT *job   = pkgJobCreate(&sSetup.src, destDir, (int32_t)fonts, askDisk, NULL);
    +    SliceT   slice = { platformClock(), platformClock() };
    +    PkgStepE step  = PkgStepErrorE;
    +    PkgDbT   before;
     
         if (!job) {
             report("There is not enough memory to run Setup.");
             return step;
         }
     
    +    pkgJobSkip(job, partsSkip, NULL);
    +
    +    // The folder's packages as they were, for what the new ones drop
    +    if (!pkgDbLoad(&before, destDir)) {
    +        dvxLog("Setup: the packages installed in %s could not all be read", destDir);
    +    }
    +
         // /SHOTS: the progress page before anything is unpacked, so the
         // picture is the same every run.
         if (sSetup.shots) {
    @@ -924,16 +1050,8 @@ static PkgStepE installCopy(const char *destDir, SetupFontsE fonts, bool newInst
                 pkgJobCancel(job);
             }
     
    -        if (platformClock() - sliceStart >= slice) {
    -            if (sSetup.copyBoard && platformClock() - boardStart >= boardTime) {
    -                boardNext(sSetup.copyBoard);
    -                boardStart = platformClock();
    -            }
    -
    -            showProgress(job);
    -            dvxUpdate(&sSetup.ctx);
    -            sliceStart = platformClock();
    -        }
    +        showProgress(job);
    +        sliceIdle(&slice);
         }
     
         showProgress(job);
    @@ -941,15 +1059,11 @@ static PkgStepE installCopy(const char *destDir, SetupFontsE fonts, bool newInst
         if (step == PkgStepDoneE) {
             char *const *files = pkgJobFiles(job);
     
    -        setupRemoveDropped(destDir, SETUP_FONTS_FOLDER, files, (int32_t)arrlen(files));
    +        setupRemoveDropped(destDir, &before, files, (int32_t)arrlen(files));
     
             if (!setupListWrite(destDir, files, (int32_t)arrlen(files))) {
                 dvxLog("Setup: could not write %s in %s", SETUP_LIST_NAME, destDir);
             }
    -
    -        if (!setupRecordDvx(destDir)) {
    -            dvxLog("Setup: could not record DVX as an installed package in %s", destDir);
    -        }
         } else {
             char *const *written = pkgJobWritten(job);
             char *const *dirs    = pkgJobCreatedDirs(job);
    @@ -969,24 +1083,104 @@ static PkgStepE installCopy(const char *destDir, SetupFontsE fonts, bool newInst
         }
     
         pkgJobFree(job);
    +    pkgDbFree(&before);
         return step;
     }
     
     
    +// After DVX: each part left out that the folder still has removed (one
    +// Software put there: the parts Setup put there went with its files);
    +// each add-on chosen installed (upgraded, repaired), each the folder has
    +// that is not chosen removed.  By the package library in slices like the
    +// unpacking, trusted as the DVX in destDir trusts it.  One that fails is
    +// reported and the others go on; Cancel stops them (DVX is installed).
    +static void installPackages(const char *destDir) {
    +    PkgDbT      db;
    +    PkgKeyringT ring;
    +    char        userDir[DVX_MAX_PATH] = "";
    +    char        msg[SETUP_MSG_MAX];
    +
    +    if (!pkgDbLoad(&db, destDir)) {
    +        dvxLog("Setup: the packages installed in %s could not all be read", destDir);
    +    }
    +
    +    platformUserDirOf(userDir, sizeof(userDir), destDir);
    +    pkgKeyringLoad(&ring, destDir, userDir);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.parts) && !sSetup.cancelRequested; i++) {
    +        if (!sSetup.partsChosen[i] && pkgDbFind(&db, sSetup.parts[i].id)) {
    +            partRemove(&db, &sSetup.parts[i]);
    +        }
    +    }
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.addons) && !sSetup.cancelRequested; i++) {
    +        const SetupAddonT *a      = &sSetup.addons[i];
    +        const char        *name   = a->offer.manifest.name;
    +        bool               chosen = addonChosen(i);
    +        PkgOpT            *op;
    +
    +        if (!setupAddonOp(&db, &ring, a, chosen, &op)) {
    +            continue;
    +        }
    +
    +        if (!op) {
    +            report("There is not enough memory to install the add-ons.");
    +            break;
    +        }
    +
    +        snprintf(msg, sizeof(msg), chosen ? "Installing %s" : "Removing %s", name);
    +
    +        PkgOpStateE state = opRun(op, msg);
    +
    +        if (state == PkgOpDoneE) {
    +            dvxLog("Setup: add-on %s (%s) %s", a->name, a->offer.manifest.id, chosen ? "installed" : "removed");
    +        } else if (state == PkgOpRunningE) {
    +            dvxLog("Setup: add-on %s cancelled", a->name);
    +            snprintf(msg, sizeof(msg), "Setup was cancelled: %s and the add-ons after it are not as you chose. Software (in Program Manager) can install and remove them.", name);
    +            report(msg);
    +        } else {
    +            snprintf(msg, sizeof(msg), chosen ? "%s could not be installed: %s" : "%s could not be removed: %s", name, pkgOpMessage(op));
    +            report(msg);
    +        }
    +
    +        pkgOpFree(op);
    +    }
    +
    +    pkgKeyringFree(&ring);
    +    pkgDbFree(&db);
    +}
    +
    +
     // Install or Upgrade clicked (or /AUTO): check, unpack, settings,
     // AUTOEXEC.BAT, done.
     static void installRun(void) {
    -    char destDir[DVX_MAX_PATH];
    -    char msg[SETUP_MSG_MAX];
    -    bool        upgrade      = false;
    -    bool        keepSettings = false;
    +    char        destDir[DVX_MAX_PATH];
    +    char        msg[SETUP_MSG_MAX];
         struct stat st;
     
         folderGet(destDir, sizeof(destDir));
     
    -    SetupFontsE fonts = fontsChosen(destDir);
    +    // Unattended, the folder is checked and the parts and add-ons set
    +    // here (else the folder page's Next did it)
    +    if (sSetup.autoMode) {
    +        if (!folderCheck(destDir, &sSetup.upgrade, &sSetup.keepSettings)) {
    +            sSetup.exitCode = SETUP_EXIT_FAILED;
    +            return;
    +        }
     
    -    if (!installCheck(destDir, fonts, &upgrade, &keepSettings)) {
    +        partsPreset(destDir, !sSetup.upgrade);
    +        setupAddonsInstalled(sSetup.addons, destDir);
    +    }
    +
    +    bool        upgrade      = sSetup.upgrade;
    +    bool        keepSettings = sSetup.keepSettings;
    +    SetupFontsE fonts        = fontsChosen(destDir);
    +
    +    if (upgrade) {
    +        partsKeepNeeded(destDir);
    +    }
    +
    +    if (!spaceCheck(destDir, fonts)) {
             sSetup.exitCode = SETUP_EXIT_FAILED;
             return;
         }
    @@ -1004,6 +1198,7 @@ static void installRun(void) {
         sSetup.cancelRequested = false;
         showPage(PageCopyE);
         dvxLog("Setup: installing in %s (%s), fonts %s", destDir, upgrade ? (keepSettings ? "upgrade, settings kept" : "settings replaced") : "new", sFontsArgs[fonts]);
    +    partsLog();
     
         PkgStepE result = installCopy(destDir, fonts, !upgrade);
     
    @@ -1014,7 +1209,7 @@ static void installRun(void) {
     
             dvxLog("Setup: %s", result == PkgStepCancelledE ? "cancelled" : "failed");
             sSetup.exitCode = SETUP_EXIT_FAILED;
    -        showPage(PageFolderE);
    +        showPage(PageChoiceE);
     
             if (result == PkgStepCancelledE && !sSetup.quit && !sSetup.autoMode) {
                 snprintf(msg, sizeof(msg), upgrade ? "Setup was cancelled. DVX in %s is incomplete now; run Setup again to repair it." : "Setup was cancelled. Nothing was installed in %s.", destDir);
    @@ -1024,6 +1219,8 @@ static void installRun(void) {
             return;
         }
     
    +    installPackages(destDir);
    +
         // The settings start over (the user folder removed, now that the new
         // files are in): start in the mode Setup runs in (it works here).
         if (upgrade && !keepSettings && !setupRemoveUser(destDir)) {
    @@ -1115,11 +1312,15 @@ static void onAskIdle(void *ctx) {
     }
     
     
    -// Back from the folder page to the start page, from there to the licence.
    +// Back from the choice page to the folder page, from there to the start
    +// page, from there to the licence.
     static void onBack(WidgetT *w) {
         (void)w;
     
    -    if (sSetup.page == PageFolderE) {
    +    if (sSetup.page == PageChoiceE) {
    +        showPage(PageFolderE);
    +        wgtSetFocused(sSetup.dest);
    +    } else if (sSetup.page == PageFolderE) {
             showStart();
         } else {
             showPage(PageLicenceE);
    @@ -1128,8 +1329,7 @@ static void onBack(WidgetT *w) {
     
     
     // Browse...: the folder dialog, starting at the folder typed (or, not
    -// made yet, the nearest one that is); on Upgrade the fonts follow the
    -// folder chosen.
    +// made yet, the nearest one that is).
     static void onBrowse(WidgetT *w) {
         char folder[DVX_MAX_PATH];
         char chosen[DVX_MAX_PATH];
    @@ -1143,11 +1343,6 @@ static void onBrowse(WidgetT *w) {
     
         wgtSetText(sSetup.dest, chosen);
         wgtSetFocused(sSetup.dest);
    -
    -    if (sSetup.action == ActionUpgradeE) {
    -        wgtRadioGroupSetSelected(sSetup.fontsGroup, (int32_t)fontsPreset(chosen));
    -        showSpace();
    -    }
     }
     
     
    @@ -1164,6 +1359,13 @@ static void onCancel(WidgetT *w) {
     }
     
     
    +// A fonts choice or an add-on ticked or not: the room needed again.
    +static void onChoice(WidgetT *w) {
    +    (void)w;
    +    showSpace();
    +}
    +
    +
     static void onClose(WindowT *win) {
         (void)win;
         sSetup.cancelRequested = true;
    @@ -1178,13 +1380,27 @@ static void onFinish(WidgetT *w) {
     }
     
     
    -static void onFonts(WidgetT *w) {
    +// The folder page's Next (Install, Upgrade: the folder checked, then what
    +// to install) or Remove (run from the main loop).
    +static void onFolderGo(WidgetT *w) {
    +    char folder[DVX_MAX_PATH];
    +
         (void)w;
    -    showSpace();
    +
    +    if (sSetup.action == ActionRemoveE) {
    +        sSetup.runRequested = true;
    +        return;
    +    }
    +
    +    folderGet(folder, sizeof(folder));
    +
    +    if (folderCheck(folder, &sSetup.upgrade, &sSetup.keepSettings)) {
    +        showChoice(folder);
    +    }
     }
     
     
    -// The folder page's Install, Upgrade, or Remove: run from the main loop.
    +// The choice page's Install or Upgrade: run from the main loop.
     static void onGo(WidgetT *w) {
         (void)w;
         sSetup.runRequested = true;
    @@ -1197,6 +1413,22 @@ static void onNext(WidgetT *w) {
     }
     
     
    +// A part ticked or not: the parts it needs ticked with it, or the parts
    +// that need it unticked with it; the room needed again.
    +static void onPart(WidgetT *w) {
    +    int32_t changed = (int32_t)(intptr_t)w->userData;
    +
    +    sSetup.partsChosen[changed] = wgtCheckboxIsChecked(w);
    +    setupPartsFix(sSetup.parts, sSetup.partsChosen, changed);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.parts); i++) {
    +        wgtCheckboxSetChecked(sSetup.partChecks[i], sSetup.partsChosen[i]);
    +    }
    +
    +    showSpace();
    +}
    +
    +
     // A start page tile's icon or button: the folder page for its action.
     // cppcheck-suppress constParameterCallback ; a widget onClick handler: WidgetT fixes the signature
     static void onTile(WidgetT *w) {
    @@ -1204,6 +1436,27 @@ static void onTile(WidgetT *w) {
     }
     
     
    +// Step op (status says what it does) in slices like the unpacking, until
    +// it is done or has failed, or Cancel stops it (PkgOpRunningE).
    +static PkgOpStateE opRun(PkgOpT *op, const char *status) {
    +    SliceT      slice = { platformClock(), platformClock() };
    +    PkgOpStateE state;
    +
    +    wgtSetText(sSetup.status, status);
    +
    +    for (state = pkgOpStep(op); state == PkgOpRunningE && !sSetup.cancelRequested; state = pkgOpStep(op)) {
    +        uint32_t done;
    +        uint32_t total;
    +
    +        pkgOpProgress(op, &done, &total);
    +        wgtProgressBarSetValue(sSetup.bar, total ? (int32_t)((uint64_t)done * SETUP_PERCENT / total) : SETUP_PERCENT);
    +        sliceIdle(&slice);
    +    }
    +
    +    return state;
    +}
    +
    +
     // The splash: the desktop's wallpaper buffer, made the screen's size in its
     // pixel format, gets the gradient, the shadowed title and the tagline.
     // Called before the first frame (dvxInit has the whole screen redrawn);
    @@ -1244,6 +1497,12 @@ static void parseArgs(int argc, char *argv[]) {
                 snprintf(sSetup.logPath, sizeof(sSetup.logPath), "%s", argv[i] + strlen(SETUP_ARG_LOG));
             } else if (strncasecmp(argv[i], SETUP_ARG_SHOTS, strlen(SETUP_ARG_SHOTS)) == 0) {
                 snprintf(sSetup.shotDir, sizeof(sSetup.shotDir), "%s", argv[i] + strlen(SETUP_ARG_SHOTS));
    +        } else if (strncasecmp(argv[i], SETUP_ARG_ADDONS, strlen(SETUP_ARG_ADDONS)) == 0) {
    +            snprintf(sSetup.addonsArg, sizeof(sSetup.addonsArg), "%s", argv[i] + strlen(SETUP_ARG_ADDONS));
    +            sSetup.addonsGiven = true;
    +        } else if (strncasecmp(argv[i], SETUP_ARG_PARTS, strlen(SETUP_ARG_PARTS)) == 0) {
    +            snprintf(sSetup.partsArg, sizeof(sSetup.partsArg), "%s", argv[i] + strlen(SETUP_ARG_PARTS));
    +            sSetup.partsGiven = true;
             } else if (strncasecmp(argv[i], SETUP_ARG_FONTS, strlen(SETUP_ARG_FONTS)) == 0) {
                 const char *value = argv[i] + strlen(SETUP_ARG_FONTS);
     
    @@ -1264,6 +1523,134 @@ static void parseArgs(int argc, char *argv[]) {
     }
     
     
    +// A part left out that the folder still has (Software put it there)
    +// removed: its bundle, then the members it alone needed (as Software
    +// removes it).
    +static void partRemove(PkgDbT *db, const SetupPartT *part) {
    +    const char           *ids[]   = { part->id };
    +    const PkgInstalledT **orphans = pkgPlanOrphans(db, ids, 1);
    +    char                **gone    = NULL;   // their ids: each removal changes db
    +    char                  status[SETUP_MSG_MAX];
    +    char                  msg[SETUP_MSG_MAX];
    +
    +    arrput(gone, strdup(part->id));
    +
    +    for (int32_t o = 0; o < (int32_t)arrlen(orphans); o++) {
    +        arrput(gone, strdup(orphans[o]->manifest.id));
    +    }
    +
    +    arrfree(orphans);
    +    snprintf(status, sizeof(status), "Removing %s", part->name);
    +
    +    for (int32_t g = 0; g < (int32_t)arrlen(gone) && !sSetup.cancelRequested; g++) {
    +        PkgOpT *op = gone[g] ? pkgOpRemove(db, gone[g], false) : NULL;
    +
    +        if (!op) {
    +            report("There is not enough memory to remove the parts left out.");
    +            break;
    +        }
    +
    +        PkgOpStateE state = opRun(op, status);
    +
    +        if (state == PkgOpDoneE) {
    +            dvxLog("Setup: %s removed (%s)", gone[g], part->id);
    +        } else if (state == PkgOpRunningE) {
    +            snprintf(msg, sizeof(msg), "Setup was cancelled: %s is not all removed. Software (in Program Manager) can remove it.", part->name);
    +            report(msg);
    +        } else {
    +            snprintf(msg, sizeof(msg), "%s could not be removed: %s", part->name, pkgOpMessage(op));
    +            report(msg);
    +        }
    +
    +        pkgOpFree(op);
    +    }
    +
    +    for (int32_t g = 0; g < (int32_t)arrlen(gone); g++) {
    +        free(gone[g]);
    +    }
    +
    +    arrfree(gone);
    +}
    +
    +
    +// Bytes of the parts which says: all of them, those chosen, or those the
    +// folder has (as setupPartsInstalled last marked them)
    +static uint64_t partsBytes(PartsWhichE which) {
    +    uint64_t bytes = 0;
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.parts); i++) {
    +        bool counted = which == PartsAllE || (which == PartsChosenE && sSetup.partsChosen[i]) || (which == PartsInstalledE && sSetup.parts[i].installed);
    +
    +        bytes += counted ? sSetup.parts[i].bytes : 0;
    +    }
    +
    +    return bytes;
    +}
    +
    +
    +// An upgrade: each part left out that other software in destDir needs is
    +// kept (chosen again), and the user told.
    +static void partsKeepNeeded(const char *destDir) {
    +    PkgDbT db;
    +    char   kept[SETUP_MSG_MAX];
    +    char   msg[SETUP_MSG_MAX];
    +
    +    if (!pkgDbLoad(&db, destDir)) {
    +        dvxLog("Setup: the packages installed in %s could not all be read", destDir);
    +    }
    +
    +    setupPartsKeepNeeded(sSetup.parts, sSetup.partsChosen, &db, kept, (int32_t)sizeof(kept));
    +    pkgDbFree(&db);
    +
    +    if (!kept[0]) {
    +        return;
    +    }
    +
    +    snprintf(msg, sizeof(msg), "Other software in %s needs these parts, so Setup keeps them: %s.", destDir, kept);
    +    dvxLog("Setup: %s", msg);
    +
    +    if (sSetup.asks) {
    +        ask(msg, MB_OK | MB_ICONINFO, NULL, 0);
    +    }
    +}
    +
    +
    +// The parts chosen, for the log
    +static void partsLog(void) {
    +    char text[SETUP_MSG_MAX] = "";
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.parts); i++) {
    +        if (sSetup.partsChosen[i]) {
    +            size_t len = strlen(text);
    +
    +            snprintf(text + len, sizeof(text) - len, "%s%s", len ? " " : "", sSetup.parts[i].id);
    +        }
    +    }
    +
    +    dvxLog("Setup: parts %s", text[0] ? text : "none");
    +}
    +
    +
    +// The parts first chosen for destDir: /PARTS=, else what the folder has
    +// (all on a new install), each with the parts it needs.
    +static void partsPreset(const char *destDir, bool newInstall) {
    +    setupPartsInstalled(sSetup.parts, destDir);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.parts); i++) {
    +        sSetup.partsChosen[i] = setupPartChosen(sSetup.partsGiven ? sSetup.partsArg : NULL, &sSetup.parts[i], newInstall);
    +    }
    +
    +    setupPartsFix(sSetup.parts, sSetup.partsChosen, -1);
    +}
    +
    +
    +// pkgJobSkip's filter: the files and records of the parts not chosen
    +static bool partsSkip(void *user, const char *name) {
    +    (void)user;
    +    return setupPartsSkip(sSetup.parts, sSetup.partsChosen, name);
    +}
    +
    +
     // An error: a message box, or (unattended) the log.
     static void report(const char *msg) {
         dvxLog("Setup: %s", msg);
    @@ -1291,18 +1678,56 @@ static void shootPage(void) {
     }
     
     
    -// The folder page set up for action: its label and button, the fonts
    -// (first what fontsPreset gives the folder) and AUTOEXEC.BAT but for
    -// Remove, the user folder checkbox (cleared) for Upgrade and Remove, and
    -// for Remove what it leaves.
    +// The choice page for destDir (folderCheck said whether it is an upgrade):
    +// the fonts, parts, and add-ons first as their presets give them (on an
    +// upgrade, a part unticked is removed: the label says so), each add-on
    +// with its size or that the folder has it; the button Install or
    +// Upgrade.
    +static void showChoice(const char *destDir) {
    +    char text[SETUP_MSG_MAX];
    +
    +    wgtRadioGroupSetSelected(sSetup.fontsGroup, (int32_t)fontsPreset(destDir));
    +    partsPreset(destDir, !sSetup.upgrade);
    +    setupAddonsInstalled(sSetup.addons, destDir);
    +
    +    if (sSetup.partsLabel) {
    +        wgtSetText(sSetup.partsLabel, sSetup.upgrade ? "Parts of DVX to have (an unticked one is removed):" : "Parts of DVX to install:");
    +    }
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.parts); i++) {
    +        wgtCheckboxSetChecked(sSetup.partChecks[i], sSetup.partsChosen[i]);
    +    }
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(sSetup.addons); i++) {
    +        const SetupAddonT *addon = &sSetup.addons[i];
    +
    +        if (addon->installed) {
    +            snprintf(text, sizeof(text), "%s (installed)", addon->offer.manifest.name);
    +        } else {
    +            snprintf(text, sizeof(text), "%s (%" PRIu64 " MB)", addon->offer.manifest.name, megabytes(addon->bytes));
    +        }
    +
    +        wgtSetText(sSetup.addonChecks[i], text);
    +        wgtCheckboxSetChecked(sSetup.addonChecks[i], setupAddonChosen(sSetup.addonsGiven ? sSetup.addonsArg : NULL, addon));
    +    }
    +
    +    wgtSetText(sSetup.choiceGoBtn, sActions[sSetup.upgrade ? ActionUpgradeE : ActionInstallE].button);
    +    showSpace();
    +    showPage(PageChoiceE);
    +    wgtSetFocused(sSetup.choiceGoBtn);
    +}
    +
    +
    +// The folder page set up for action: its label, its button (Next, or
    +// Remove), AUTOEXEC.BAT but for Remove, the user folder checkbox
    +// (cleared) for Upgrade and Remove, and for Remove what it leaves.
     static void showFolder(SetupActionE action) {
         const SetupActionT *a      = &sActions[action];
         bool                remove = action == ActionRemoveE;
     
         sSetup.action = action;
         wgtSetText(sSetup.folderLabel, a->folderLabel);
    -    wgtSetText(sSetup.goBtn, a->button);
    -    wgtSetVisible(sSetup.fontsBox, !remove);
    +    wgtSetText(sSetup.goBtn, remove ? a->button : SETUP_NEXT_LABEL);
         wgtSetVisible(sSetup.userBox, a->userLabel != NULL);
         wgtSetText(sSetup.userBox, a->userLabel ? a->userLabel : "");
         wgtCheckboxSetChecked(sSetup.userBox, false);
    @@ -1312,14 +1737,6 @@ static void showFolder(SetupActionE action) {
             wgtSetVisible(sSetup.autoexec, !remove);
         }
     
    -    if (!remove) {
    -        char folder[DVX_MAX_PATH];
    -
    -        folderGet(folder, sizeof(folder));
    -        wgtRadioGroupSetSelected(sSetup.fontsGroup, (int32_t)fontsPreset(folder));
    -        showSpace();
    -    }
    -
         showPage(PageFolderE);
         wgtSetFocused(sSetup.dest);
     }
    @@ -1353,11 +1770,11 @@ static void showProgress(const PkgJobT *job) {
     }
     
     
    -// The room the fonts chosen need, on the folder page
    +// The room the fonts, parts, and add-ons chosen need, on the choice page
     static void showSpace(void) {
         char text[SETUP_MSG_MAX];
     
    -    snprintf(text, sizeof(text), "DVX needs about %" PRIu64 " MB of disk space.", megabytes(setupSpaceNeeded(&sSetup.src.info, (SetupFontsE)wgtRadioGroupGetSelected(sSetup.fontsGroup))));
    +    snprintf(text, sizeof(text), "DVX needs about %" PRIu64 " MB of disk space.", megabytes(spaceNeeded((SetupFontsE)wgtRadioGroupGetSelected(sSetup.fontsGroup), PartsChosenE) + addonsSpace()));
         wgtSetText(sSetup.spaceText, text);
     }
     
    @@ -1382,6 +1799,68 @@ static void showStart(void) {
     
     // Run the installed DVX (Setup's screen is closed first; Setup's own
     // memory stays in use until DVX exits).
    +// Between steps of the unpacking or an add-on: at the end of a slice, the
    +// next billboard when the one shown has been up long enough, and the
    +// screen drawn (progress and Cancel seen).
    +static void sliceIdle(SliceT *s) {
    +    PlatformTicksT now = platformClock();
    +
    +    if (now - s->sliceStart < PLATFORM_MS_TO_TICKS(SETUP_SLICE_MS)) {
    +        return;
    +    }
    +
    +    if (sSetup.copyBoard && now - s->boardStart >= PLATFORM_MS_TO_TICKS(SETUP_BOARD_MS)) {
    +        boardNext(sSetup.copyBoard);
    +        s->boardStart = now;
    +    }
    +
    +    dvxUpdate(&sSetup.ctx);
    +    s->sliceStart = platformClock();
    +}
    +
    +
    +// Whether destDir's disk has room for the fonts, parts, and add-ons
    +// chosen: an upgrade replaces files of about the same size, so only what
    +// the folder does not have counts.
    +static bool spaceCheck(const char *destDir, SetupFontsE fonts) {
    +    uint64_t needed = spaceNeeded(fonts, PartsChosenE);
    +    uint64_t total  = 0;
    +    uint64_t avail  = 0;
    +
    +    if (sSetup.upgrade) {
    +        uint64_t have = spaceNeeded(setupFontsInstalled(destDir), PartsInstalledE);
    +
    +        needed = (needed > have) ? needed - have : 0;
    +    }
    +
    +    needed += addonsSpace();
    +
    +    if (needed > 0 && platformDiskSpace(destDir, &total, &avail) && avail < needed) {
    +        char needText[FILEOPS_SIZE_TEXT_MAX];
    +        char freeText[FILEOPS_SIZE_TEXT_MAX];
    +        char msg[SETUP_MSG_MAX];
    +
    +        fileOpsFormatSize(needed, needText, sizeof(needText));
    +        fileOpsFormatSize(avail, freeText, sizeof(freeText));
    +        snprintf(msg, sizeof(msg), "There is not enough free space for DVX on that disk.\n\nDVX needs about %s bytes; %s bytes are free.", needText, freeText);
    +        report(msg);
    +        return false;
    +    }
    +
    +    return true;
    +}
    +
    +
    +// Bytes DVX takes with fonts and the parts which says (the package's
    +// files, less the parts' it leaves out)
    +static uint64_t spaceNeeded(SetupFontsE fonts, PartsWhichE which) {
    +    uint64_t all   = setupSpaceNeeded(&sSetup.src.info, fonts);
    +    uint64_t parts = partsBytes(PartsAllE);
    +
    +    return (all > parts ? all - parts : 0) + partsBytes(which);
    +}
    +
    +
     static void startDvx(void) {
         char exe[DVX_MAX_PATH];
     
    @@ -1508,12 +1987,13 @@ int main(int argc, char *argv[]) {
         wgtRegisterRadio();
         wgtRegisterSpacer();
         wgtRegisterTextInput();
    +    wgtRegisterWrapBox();
         sBoardType = wgtRegisterClass(&sClassBoard);
         pkgFind(sSetup.exePath, &sSetup.src);
         dvxLog("Setup: %s, package %s (%d disk(s), %d files)", sSetup.exePath, sFoundNames[sSetup.src.found], (int)sSetup.src.info.diskCount, (int)sSetup.src.info.tiers[PKG_TIERS - 1].fileCount);
     
         if (sSetup.fontsBad) {
    -        fprintf(stderr, "DVX Setup: %s takes NONE, BASE, or ALL.\n", SETUP_ARG_FONTS);
    +        fprintf(stderr, "DVX Setup: %s takes %s or %s.\n", SETUP_ARG_FONTS, sFontsArgs[SetupFontsNoneE], sFontsArgs[SetupFontsBaseE]);
             return SETUP_EXIT_FAILED;
         }
     
    @@ -1546,6 +2026,11 @@ int main(int argc, char *argv[]) {
     
         WidgetT *root = wgtInitWindow(&sSetup.ctx, sSetup.win);
     
    +    sSetup.addons = setupAddonsFind(sSetup.src.partDir);
    +    sSetup.parts  = setupPartsRead(sSetup.src.partDir);
    +    arrsetlen(sSetup.partsChosen, arrlen(sSetup.parts));
    +    dvxLog("Setup: %d add-on(s) offered", (int)arrlen(sSetup.addons));
    +    dvxLog("Setup: %d part(s) offered", (int)arrlen(sSetup.parts));
         buildPages(root);
     
         // /SHOTS: the pointer out of the way in the bottom right corner (as
    @@ -1570,8 +2055,8 @@ int main(int argc, char *argv[]) {
         }
     
         // Unattended: install, or upgrade the DVX already there keeping its
    -    // settings.  /SHOTS: the pages before that, as Next and the tile show
    -    // them.
    +    // settings.  /SHOTS: the pages before that, as Next, the tile, and the
    +    // folder page's Next show them.
         if (sSetup.autoMode) {
             sSetup.action       = setupIsInstalled(sSetup.autoDest) ? ActionUpgradeE : ActionInstallE;
             sSetup.runRequested = true;
    @@ -1583,6 +2068,8 @@ int main(int argc, char *argv[]) {
             shootPage();
             showFolder(sSetup.action);
             shootPage();
    +        onFolderGo(NULL);
    +        shootPage();
         }
     
         while (!sSetup.quit && dvxUpdate(&sSetup.ctx)) {
    @@ -1597,6 +2084,11 @@ int main(int argc, char *argv[]) {
         }
     
         dvxShutdown(&sSetup.ctx);
    +    arrfree(sSetup.addonChecks);
    +    arrfree(sSetup.partChecks);
    +    arrfree(sSetup.partsChosen);
    +    setupAddonsFree(sSetup.addons);
    +    setupPartsFree(sSetup.parts);
     
         if (sSetup.startDvx) {
             startDvx();
    diff --git a/src/installer/setupCore.c b/src/installer/setupCore.c
    index 76e961c1..9426bfeb 100644
    --- a/src/installer/setupCore.c
    +++ b/src/installer/setupCore.c
    @@ -29,6 +29,8 @@
     #include "pkgDb.h"
     #include "stb_ds_wrap.h"
     
    +#include 
    +#include 
     #include 
     #include 
     #include 
    @@ -41,31 +43,62 @@
     // Line ending written into AUTOEXEC.BAT and SETUP.LST
     #define SETUP_EOL "\r\n"
     
    +// SETUP_PARTS_NAME's keys
    +#define PARTS_KEY_SUMMARY "summary"
    +#define PARTS_KEY_NEEDS   "needs"
    +#define PARTS_KEY_MEMBERS "members"
    +#define PARTS_KEY_FOLDERS "folders"
    +#define PARTS_KEY_PATHS   "paths"
    +#define PARTS_KEY_BYTES   "bytes"
    +#define PARTS_KEY_FILES   "files"
    +#define PARTS_WORD_SEP    " "
    +#define DECIMAL           10
    +
     // ============================================================
     // Prototypes
     // ============================================================
     
     static bool    autoexecRewrite(const char *path, const char *text, int32_t len, char *err, int32_t errSize);
    +static int     byAddonName(const void *a, const void *b);
     static int     byLengthDesc(const void *a, const void *b);
     static void    dirsAdd(char ***dirs, const char *destDir, const char *path);
    +static void    dropCandidate(char ***names, const char *name);
     static bool    lineMatches(char *l, const char *line);
     static void    listFree(char **names);
    +static bool    listHasName(const char *arg, const char *name);
     static bool    listNameIsValid(const char *name);
     static char  **listRead(const char *destDir);
    +static bool    nameIn(const char *name, char *const *names, int32_t count);
    +static void    partIds(const SetupPartT *part, char ***ids);
    +static int32_t partIndex(const SetupPartT *parts, const char *id);
    +static char  **partWords(PrefsHandleT *ini, const char *section, const char *key);
    +static bool    recordExists(const char *destDir, const char *id);
    +static bool    recordIs(const char *id, const char *name);
     static bool    removeFile(const char *path);
     static int32_t removePackages(const char *destDir, char ***dirs);
    +bool           setupAddonChosen(const char *arg, const SetupAddonT *addon);
    +bool           setupAddonOp(PkgDbT *db, const PkgKeyringT *ring, const SetupAddonT *addon, bool chosen, PkgOpT **op);
    +SetupAddonT   *setupAddonsFind(const char *srcDir);
    +void           setupAddonsFree(SetupAddonT *addons);
    +void           setupAddonsInstalled(SetupAddonT *addons, const char *destDir);
     bool           setupAutoexecAdd(const char *path, const char *line, char *err, int32_t errSize);
     bool           setupAutoexecHasLine(const char *path, const char *line);
     bool           setupAutoexecLine(const char *destDir, char *out, int32_t outSize);
     bool           setupAutoexecPath(char bootDrive, char *out, int32_t outSize);
     bool           setupAutoexecRemove(const char *path, const char *line, char *err, int32_t errSize);
     bool           setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t errSize);
    -SetupFontsE    setupFontsInstalled(const char *destDir, const PkgHeaderT *info);
    +SetupFontsE    setupFontsInstalled(const char *destDir);
     bool           setupHasSettings(const char *destDir);
     bool           setupIsInstalled(const char *destDir);
     bool           setupListWrite(const char *destDir, char *const *files, int32_t count);
    -bool           setupRecordDvx(const char *destDir);
    -void           setupRemoveDropped(const char *destDir, const char *folder, char *const *files, int32_t count);
    +bool           setupPartChosen(const char *arg, const SetupPartT *part, bool newInstall);
    +void           setupPartsFix(const SetupPartT *parts, bool *chosen, int32_t changed);
    +void           setupPartsFree(SetupPartT *parts);
    +void           setupPartsInstalled(SetupPartT *parts, const char *destDir);
    +void           setupPartsKeepNeeded(const SetupPartT *parts, bool *chosen, const PkgDbT *db, char *kept, int32_t keptSize);
    +SetupPartT    *setupPartsRead(const char *srcDir);
    +bool           setupPartsSkip(const SetupPartT *parts, const bool *chosen, const char *name);
    +void           setupRemoveDropped(const char *destDir, const PkgDbT *before, char *const *files, int32_t count);
     bool           setupRemoveUser(const char *destDir);
     void           setupRemoveWritten(char *const *written, int32_t writtenCount, char *const *dirs, int32_t dirCount);
     uint64_t       setupSpaceNeeded(const PkgHeaderT *info, SetupFontsE tier);
    @@ -98,6 +131,12 @@ static bool autoexecRewrite(const char *path, const char *text, int32_t len, cha
     }
     
     
    +// Add-ons in order of their file names
    +static int byAddonName(const void *a, const void *b) {
    +    return strcasecmp(((const SetupAddonT *)a)->name, ((const SetupAddonT *)b)->name);
    +}
    +
    +
     // Longest path first, so a folder comes after everything inside it.
     static int byLengthDesc(const void *a, const void *b) {
         size_t la = strlen(*(char *const *)a);
    @@ -120,6 +159,23 @@ static void dirsAdd(char ***dirs, const char *destDir, const char *path) {
     }
     
     
    +// name added to names (stb_ds, strdup'd), with '/' between its parts as
    +// SETUP.LST has them.
    +static void dropCandidate(char ***names, const char *name) {
    +    char *copy = strdup(name);
    +
    +    if (!copy) {
    +        return;
    +    }
    +
    +    for (char *p = copy; *p; p++) {
    +        *p = platformPathIsSep(*p) ? PKG_NAME_SEP : *p;
    +    }
    +
    +    arrput(*names, copy);
    +}
    +
    +
     // A line of a batch file (trimmed in place) is line: case ignored, and
     // '/' and '\\' alike (platformPathSame), so "C:/DVX/DVX.EXE" written by
     // hand is the same line.
    @@ -138,6 +194,34 @@ static void listFree(char **names) {
     }
     
     
    +// Whether arg (/ADDONS=, /PARTS=) names name: ALL, NONE, or a list
    +// (SETUP_ADDONS_SEP between the names), any case.
    +static bool listHasName(const char *arg, const char *name) {
    +    size_t len = strlen(name);
    +
    +    if (strcasecmp(arg, SETUP_ADDONS_ALL) == 0) {
    +        return true;
    +    }
    +
    +    if (strcasecmp(arg, SETUP_ADDONS_NONE) == 0) {
    +        return false;
    +    }
    +
    +    for (const char *p = arg; *p;) {
    +        const char *end = strchr(p, SETUP_ADDONS_SEP);
    +        size_t      n   = end ? (size_t)(end - p) : strlen(p);
    +
    +        if (n == len && strncasecmp(p, name, len) == 0) {
    +            return true;
    +        }
    +
    +        p += n + (end ? 1 : 0);
    +    }
    +
    +    return false;
    +}
    +
    +
     // A SETUP.LST line: a package name, or a folder's with a '/' after it.
     static bool listNameIsValid(const char *name) {
         char    bare[PKG_NAME_MAX];
    @@ -196,6 +280,92 @@ static char **listRead(const char *destDir) {
     }
     
     
    +// Whether name (relative, '/') is one of count names, any case.
    +static bool nameIn(const char *name, char *const *names, int32_t count) {
    +    for (int32_t i = 0; i < count; i++) {
    +        if (strcasecmp(names[i], name) == 0) {
    +            return true;
    +        }
    +    }
    +
    +    return false;
    +}
    +
    +
    +// part's bundle and members added to ids (stb_ds; the strings are part's).
    +static void partIds(const SetupPartT *part, char ***ids) {
    +    arrput(*ids, (char *)part->id);
    +
    +    for (int32_t m = 0; m < (int32_t)arrlen(part->members); m++) {
    +        arrput(*ids, part->members[m]);
    +    }
    +}
    +
    +
    +// The index of the part id in parts, or -1.
    +static int32_t partIndex(const SetupPartT *parts, const char *id) {
    +    for (int32_t i = 0; i < (int32_t)arrlen(parts); i++) {
    +        if (strcasecmp(parts[i].id, id) == 0) {
    +            return i;
    +        }
    +    }
    +
    +    return -1;
    +}
    +
    +
    +// The words of key in section of ini, each strdup'd (stb_ds array; NULL
    +// when there are none).
    +static char **partWords(PrefsHandleT *ini, const char *section, const char *key) {
    +    char **list = NULL;
    +    char  *text = strdup(prefsGetString(ini, section, key, ""));
    +
    +    for (char *w = text ? strtok(text, PARTS_WORD_SEP) : NULL; w; w = strtok(NULL, PARTS_WORD_SEP)) {
    +        char *copy = strdup(w);
    +
    +        if (copy) {
    +            arrput(list, copy);
    +        }
    +    }
    +
    +    free(text);
    +    return list;
    +}
    +
    +
    +// Whether destDir has id's record.
    +static bool recordExists(const char *destDir, const char *id) {
    +    char        path[DVX_MAX_PATH];
    +    struct stat st;
    +
    +    return pkgDbRecordPath(destDir, id, path, sizeof(path)) && stat(path, &st) == 0;
    +}
    +
    +
    +// Whether name (relative, either separator, any case) is the record of
    +// the package id.
    +static bool recordIs(const char *id, const char *name) {
    +    char path[DVX_MAX_PATH];
    +
    +    if (!pkgDbRecordPath("", id, path, sizeof(path))) {
    +        return false;
    +    }
    +
    +    const char *a = path;
    +    const char *b = name;
    +
    +    for (; *a && *b; a++, b++) {
    +        bool sameSep = platformPathIsSep(*a) && platformPathIsSep(*b);
    +
    +        if (!sameSep && tolower((unsigned char)*a) != tolower((unsigned char)*b)) {
    +            return false;
    +        }
    +    }
    +
    +    return *a == '\0' && *b == '\0';
    +}
    +
    +
     // Remove a file, read-only or not.  A file that is not there counts as
     // removed.
     static bool removeFile(const char *path) {
    @@ -239,6 +409,118 @@ static int32_t removePackages(const char *destDir, char ***dirs) {
     }
     
     
    +bool setupAddonChosen(const char *arg, const SetupAddonT *addon) {
    +    return arg ? listHasName(arg, addon->name) : addon->installed;
    +}
    +
    +
    +bool setupAddonOp(PkgDbT *db, const PkgKeyringT *ring, const SetupAddonT *addon, bool chosen, PkgOpT **op) {
    +    const char *id = addon->offer.manifest.id;
    +
    +    *op = NULL;
    +
    +    if (chosen) {
    +        *op = pkgOpInstall(db, addon->offer.where, ring, true);
    +        return true;
    +    }
    +
    +    if (!pkgDbFind(db, id)) {
    +        return false;
    +    }
    +
    +    *op = pkgOpRemove(db, id, false);
    +    return true;
    +}
    +
    +
    +SetupAddonT *setupAddonsFind(const char *srcDir) {
    +    char         dir[DVX_MAX_PATH];
    +    SetupAddonT *addons = NULL;
    +    PkgKeyringT  none;
    +
    +    memset(&none, 0, sizeof(none));
    +
    +    if (!platformPathJoin(dir, sizeof(dir), srcDir, SETUP_ADDONS_DIR)) {
    +        return NULL;
    +    }
    +
    +    DIR *d = opendir(dir);
    +
    +    if (!d) {
    +        return NULL;
    +    }
    +
    +    for (struct dirent *e = readdir(d); e; e = readdir(d)) {
    +        const char *dot = strrchr(e->d_name, '.');
    +        char        path[DVX_MAX_PATH];
    +        char        err[PKG_ERR_MAX];
    +        SetupAddonT a;
    +
    +        if (!dot || strcasecmp(dot, PKG_EXT) != 0 || dot - e->d_name >= SETUP_ADDON_NAME_MAX || !platformPathJoin(path, sizeof(path), dir, e->d_name)) {
    +            continue;
    +        }
    +
    +        memset(&a, 0, sizeof(a));
    +
    +        if (!pkgOfferRead(path, &none, &a.offer, err, sizeof(err))) {
    +            dvxLog("Setup: the add-on %s cannot be read (%s); not offered", path, err);
    +            continue;
    +        }
    +
    +        if (!a.offer.hasNamed) {
    +            dvxLog("Setup: the add-on %s is not signed; not offered", path);
    +            pkgOfferFree(&a.offer);
    +            continue;
    +        }
    +
    +        // As DOS shows it (DJGPP's readdir gives 8.3 names in lower case)
    +        snprintf(a.name, sizeof(a.name), "%.*s", (int)(dot - e->d_name), e->d_name);
    +
    +        for (char *p = a.name; *p; p++) {
    +            *p = (char)toupper((unsigned char)*p);
    +        }
    +
    +        for (int32_t f = 0; f < (int32_t)arrlen(a.offer.manifest.files); f++) {
    +            a.bytes += a.offer.manifest.files[f].size + SETUP_FILE_SLACK;
    +        }
    +
    +        arrput(addons, a);
    +    }
    +
    +    closedir(d);
    +
    +    if (addons) {
    +        qsort(addons, (size_t)arrlen(addons), sizeof(addons[0]), byAddonName);
    +    }
    +
    +    return addons;
    +}
    +
    +
    +void setupAddonsFree(SetupAddonT *addons) {
    +    for (int32_t i = 0; i < (int32_t)arrlen(addons); i++) {
    +        pkgOfferFree(&addons[i].offer);
    +    }
    +
    +    arrfree(addons);
    +}
    +
    +
    +void setupAddonsInstalled(SetupAddonT *addons, const char *destDir) {
    +    PkgDbT db;
    +
    +    if (!pkgDbLoad(&db, destDir)) {
    +        dvxLog("Setup: the packages installed in %s could not all be read", destDir);
    +    }
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(addons); i++) {
    +        addons[i].installed = pkgDbFind(&db, addons[i].offer.manifest.id) != NULL;
    +    }
    +
    +    pkgDbFree(&db);
    +}
    +
    +
     bool setupAutoexecAdd(const char *path, const char *line, char *err, int32_t errSize) {
         int32_t len = 0;
         char   *old = platformReadFile(path, &len);
    @@ -388,27 +670,12 @@ bool setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t
     }
     
     
    -SetupFontsE setupFontsInstalled(const char *destDir, const PkgHeaderT *info) {
    -    char **names = listRead(destDir);
    +SetupFontsE setupFontsInstalled(const char *destDir) {
    +    char        list[DVX_MAX_PATH];
    +    struct stat st;
    +    bool        listed = platformPathJoin(list, sizeof(list), destDir, SETUP_LIST_NAME) && stat(list, &st) == 0;
     
    -    if (!names) {
    -        return SetupFontsAllE;
    -    }
    -
    -    size_t   len   = strlen(SETUP_FONTS_FOLDER);
    -    uint32_t base  = info->tiers[SetupFontsBaseE].fileCount - info->tiers[SetupFontsNoneE].fileCount;
    -    uint32_t fonts = 0;
    -
    -    for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
    -        const char *name = names[i];
    -
    -        if (strncasecmp(name, SETUP_FONTS_FOLDER, len) == 0 && name[strlen(name) - 1] != PKG_NAME_SEP) {
    -            fonts++;
    -        }
    -    }
    -
    -    listFree(names);
    -    return (fonts == 0) ? SetupFontsNoneE : (fonts <= base) ? SetupFontsBaseE : SetupFontsAllE;
    +    return (!listed || recordExists(destDir, PKG_BASE_FONTS_ID)) ? SetupFontsBaseE : SetupFontsNoneE;
     }
     
     
    @@ -484,50 +751,268 @@ bool setupListWrite(const char *destDir, char *const *files, int32_t count) {
     }
     
     
    -bool setupRecordDvx(const char *destDir) {
    -    PkgDbT       db;
    -    PkgManifestT m;
    -    char         err[PKG_ERR_MAX];
    -    bool         ok;
    -
    -    memset(&db, 0, sizeof(db));
    -    snprintf(db.root, sizeof(db.root), "%s", destDir);
    -    pkgDvxManifest(&m);
    -
    -    ok = pkgDbPut(&db, &m, true, NULL, err, sizeof(err));
    -
    -    if (!ok) {
    -        dvxLog("Setup: %s", err);
    +bool setupPartChosen(const char *arg, const SetupPartT *part, bool newInstall) {
    +    if (arg) {
    +        return listHasName(arg, strchr(part->id, PKG_ID_SEP) ? strchr(part->id, PKG_ID_SEP) + 1 : part->id);
         }
     
    -    pkgDbFree(&db);
    -    pkgManifestFree(&m);
    -    return ok;
    +    return newInstall || part->installed;
     }
     
     
    -void setupRemoveDropped(const char *destDir, const char *folder, char *const *files, int32_t count) {
    -    char  **old  = listRead(destDir);
    -    char  **dirs = NULL;
    -    size_t  len  = strlen(folder);
    +void setupPartsFix(const SetupPartT *parts, bool *chosen, int32_t changed) {
    +    int32_t count = (int32_t)arrlen(parts);
    +    bool    more  = true;
     
    -    for (int32_t i = 0; i < (int32_t)arrlen(old); i++) {
    -        char path[DVX_MAX_PATH];
    -        bool kept = false;
    +    // Until nothing changes: a chosen part's needs chosen (all of them,
    +    // or what the part chosen brings), a part that needs one left out
    +    // left out (what the part left out takes)
    +    while (more) {
    +        more = false;
     
    -        for (int32_t j = 0; j < count && !kept; j++) {
    -            kept = strcasecmp(files[j], old[i]) == 0;
    +        for (int32_t i = 0; i < count; i++) {
    +            for (int32_t n = 0; n < (int32_t)arrlen(parts[i].needs); n++) {
    +                int32_t need = partIndex(parts, parts[i].needs[n]);
    +
    +                if (need < 0 || chosen[i] == chosen[need]) {
    +                    continue;
    +                }
    +
    +                if (changed >= 0 && !chosen[changed] && chosen[i]) {
    +                    chosen[i] = false;
    +                } else if (chosen[i]) {
    +                    chosen[need] = true;
    +                } else {
    +                    continue;
    +                }
    +
    +                more = true;
    +            }
    +        }
    +    }
    +}
    +
    +
    +void setupPartsFree(SetupPartT *parts) {
    +    for (int32_t i = 0; i < (int32_t)arrlen(parts); i++) {
    +        listFree(parts[i].needs);
    +        listFree(parts[i].members);
    +        listFree(parts[i].folders);
    +        listFree(parts[i].paths);
    +    }
    +
    +    arrfree(parts);
    +}
    +
    +
    +void setupPartsInstalled(SetupPartT *parts, const char *destDir) {
    +    char        path[DVX_MAX_PATH];
    +    struct stat st;
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(parts); i++) {
    +        SetupPartT *p = &parts[i];
    +
    +        p->installed = recordExists(destDir, p->id);
    +
    +        // (a folder's path without its separator: DOS does not take one)
    +        for (int32_t f = 0; !p->installed && f < (int32_t)arrlen(p->folders) && f < (int32_t)arrlen(p->members); f++) {
    +            if (!recordExists(destDir, p->members[f]) && pkgLocalPath(destDir, p->folders[f], path, sizeof(path))) {
    +                size_t len = strlen(path);
    +
    +                path[len - 1] = platformPathIsSep(path[len - 1]) ? '\0' : path[len - 1];
    +                p->installed  = stat(path, &st) == 0;
    +            }
    +        }
    +    }
    +}
    +
    +
    +void setupPartsKeepNeeded(const SetupPartT *parts, bool *chosen, const PkgDbT *db, char *kept, int32_t keptSize) {
    +    int32_t count = (int32_t)arrlen(parts);
    +    bool   *was   = NULL;               // chosen as it came
    +    bool    more  = true;
    +
    +    kept[0] = '\0';
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        arrput(was, chosen[i]);
    +    }
    +
    +    // Until nothing changes: the first part left out that a package
    +    // outside the parts left out (their bundles and members) needs,
    +    // chosen again with what it needs
    +    while (more) {
    +        char **left = NULL;
    +
    +        more = false;
    +
    +        for (int32_t i = 0; i < count; i++) {
    +            if (!chosen[i]) {
    +                partIds(&parts[i], &left);
    +            }
             }
     
    -        if (kept || strncasecmp(old[i], folder, len) != 0 || !pkgLocalPath(destDir, old[i], path, sizeof(path))) {
    +        for (int32_t i = 0; i < count && !more; i++) {
    +            char                **ids  = NULL;
    +            const PkgInstalledT **deps = NULL;
    +
    +            if (chosen[i]) {
    +                continue;
    +            }
    +
    +            partIds(&parts[i], &ids);
    +            deps = pkgPlanDependents(db, (const char *const *)ids, (int32_t)arrlen(ids));
    +
    +            for (int32_t d = 0; d < (int32_t)arrlen(deps) && !more; d++) {
    +                if (!nameIn(deps[d]->manifest.id, left, (int32_t)arrlen(left))) {
    +                    dvxLog("Setup: %s is kept: %s needs it", parts[i].id, deps[d]->manifest.id);
    +                    chosen[i] = true;
    +                    setupPartsFix(parts, chosen, -1);
    +                    more = true;
    +                }
    +            }
    +
    +            arrfree(deps);
    +            arrfree(ids);
    +        }
    +
    +        arrfree(left);
    +    }
    +
    +    for (int32_t i = 0; i < count; i++) {
    +        size_t len = strlen(kept);
    +
    +        if (chosen[i] && !was[i]) {
    +            snprintf(kept + len, (size_t)keptSize - len, "%s%s", len ? ", " : "", parts[i].name);
    +        }
    +    }
    +
    +    arrfree(was);
    +}
    +
    +
    +SetupPartT *setupPartsRead(const char *srcDir) {
    +    char        path[DVX_MAX_PATH];
    +    SetupPartT *parts = NULL;
    +
    +    if (!platformPathJoin(path, sizeof(path), srcDir, SETUP_PARTS_NAME)) {
    +        return NULL;
    +    }
    +
    +    PrefsHandleT *ini = prefsLoad(path);
    +
    +    if (!ini) {
    +        return NULL;
    +    }
    +
    +    for (int32_t i = 0; prefsKeyName(ini, SETUP_PARTS_SECTION, i); i++) {
    +        const char *id = prefsKeyName(ini, SETUP_PARTS_SECTION, i);
    +        SetupPartT  p;
    +
    +        memset(&p, 0, sizeof(p));
    +        snprintf(p.id, sizeof(p.id), "%s", id);
    +        snprintf(p.name, sizeof(p.name), "%s", prefsGetString(ini, SETUP_PARTS_SECTION, id, id));
    +        snprintf(p.summary, sizeof(p.summary), "%s", prefsGetString(ini, id, PARTS_KEY_SUMMARY, ""));
    +        p.needs   = partWords(ini, id, PARTS_KEY_NEEDS);
    +        p.members = partWords(ini, id, PARTS_KEY_MEMBERS);
    +        p.folders = partWords(ini, id, PARTS_KEY_FOLDERS);
    +        p.paths   = partWords(ini, id, PARTS_KEY_PATHS);
    +        p.bytes   = strtoull(prefsGetString(ini, id, PARTS_KEY_BYTES, "0"), NULL, DECIMAL) + (uint64_t)prefsGetInt(ini, id, PARTS_KEY_FILES, 0) * SETUP_FILE_SLACK;
    +        arrput(parts, p);
    +    }
    +
    +    prefsClose(ini);
    +    return parts;
    +}
    +
    +
    +bool setupPartsSkip(const SetupPartT *parts, const bool *chosen, const char *name) {
    +    size_t len = strlen(name);
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(parts); i++) {
    +        const SetupPartT *p = &parts[i];
    +
    +        if (chosen[i]) {
    +            continue;
    +        }
    +
    +        if (recordIs(p->id, name)) {
    +            return true;
    +        }
    +
    +        for (int32_t m = 0; m < (int32_t)arrlen(p->members); m++) {
    +            if (recordIs(p->members[m], name)) {
    +                return true;
    +            }
    +        }
    +
    +        if (nameIn(name, p->paths, (int32_t)arrlen(p->paths))) {
    +            return true;
    +        }
    +
    +        // In a folder of its, or that folder (named without its '/')
    +        for (int32_t f = 0; f < (int32_t)arrlen(p->folders); f++) {
    +            size_t n = strlen(p->folders[f]);
    +
    +            if (strncasecmp(name, p->folders[f], len < n ? len : n) == 0 && (len >= n || len == n - 1)) {
    +                return true;
    +            }
    +        }
    +    }
    +
    +    return false;
    +}
    +
    +
    +void setupRemoveDropped(const char *destDir, const PkgDbT *before, char *const *files, int32_t count) {
    +    char  **old  = listRead(destDir);
    +    char  **dirs = NULL;
    +    PkgDbT  now;
    +    char    path[DVX_MAX_PATH];
    +
    +    char  **ours   = NULL;                  // the packages Setup installed (ids, in before)
    +    int32_t listed = (int32_t)arrlen(old);
    +
    +    // The old list's names, then the files and records of the packages
    +    // Setup installed (their records in that list) as they were
    +    for (int32_t p = 0; p < (int32_t)arrlen(before->pkgs); p++) {
    +        const PkgInstalledT *pkg  = &before->pkgs[p];
    +        bool                 mine = false;
    +
    +        for (int32_t i = 0; i < listed && !mine; i++) {
    +            mine = recordIs(pkg->manifest.id, old[i]);
    +        }
    +
    +        if (!mine || !pkgDbRecordPath("", pkg->manifest.id, path, sizeof(path))) {
    +            continue;
    +        }
    +
    +        arrput(ours, (char *)pkg->manifest.id);
    +
    +        for (int32_t f = 0; f < (int32_t)arrlen(pkg->manifest.files); f++) {
    +            dropCandidate(&old, pkg->manifest.files[f].name);
    +        }
    +
    +        dropCandidate(&old, path);
    +    }
    +
    +    if (!pkgDbLoad(&now, destDir)) {
    +        dvxLog("Setup: the packages installed in %s could not all be read", destDir);
    +    }
    +
    +    for (int32_t i = 0; i < (int32_t)arrlen(old); i++) {
    +        const char          *name  = old[i];
    +        size_t               len   = strlen(name);
    +        bool                 dir   = len > 0 && name[len - 1] == PKG_NAME_SEP;
    +        const PkgInstalledT *owner = dir ? NULL : pkgDbOwner(&now, name, NULL);
    +
    +        if (nameIn(name, files, count) || (owner && !nameIn(owner->manifest.id, ours, (int32_t)arrlen(ours))) || !pkgLocalPath(destDir, name, path, sizeof(path))) {
                 continue;
             }
     
             // A folder: removed after the files, deepest first, when empty
    -        size_t pathLen = strlen(path);
    -
    -        if (platformPathIsSep(path[pathLen - 1])) {
    -            path[pathLen - 1] = '\0';
    +        if (dir) {
    +            path[strlen(path) - 1] = '\0';
                 arrput(dirs, strdup(path));
             } else {
                 removeFile(path);
    @@ -544,6 +1029,8 @@ void setupRemoveDropped(const char *destDir, const char *folder, char *const *fi
             rmdir(dirs[i]);
         }
     
    +    pkgDbFree(&now);
    +    arrfree(ours);
         listFree(dirs);
         listFree(old);
     }
    diff --git a/src/installer/setupCore.h b/src/installer/setupCore.h
    index 17269a28..7259b84e 100644
    --- a/src/installer/setupCore.h
    +++ b/src/installer/setupCore.h
    @@ -21,12 +21,21 @@
     // IN THE SOFTWARE.
     
     // setupCore.h  -- Setup's work without its window: the target folder,
    -// the list of installed files, removing DVX, DVX.INI and AUTOEXEC.BAT
    -// (host tested)
    +// the list of installed files, the add-ons on the medium, removing DVX,
    +// DVX.INI and AUTOEXEC.BAT (host tested)
     //
     // SETUP.EXE unpacks the DVX package (pkgRead.h) into the target folder:
    -// the program, and of the fonts none, the base set, or all (the package's
    -// tiers, pkgFormat.h).
    +// the program, and the base set of fonts or not (the package's tiers,
    +// pkgFormat.h).  Each tier carries its package's installed record (DVX
    +// itself, kpunch.dvx; the base fonts, PKG_BASE_FONTS_ID; DVX's optional
    +// parts, src/packages.ini: mkDist writes them), so they arrive recorded
    +// with every file, and the package server updates them as it does any
    +// package.  The optional parts (SETUP.INI beside SETUP.EXE) are chosen as
    +// a whole each: the files and records of one not chosen are left out.  The add-on packages on the medium (its ADDONS folder:
    +// More Fonts, the SoundFont) are installed after it, by the package
    +// library (pkgOp.h), as Software installs them, so Software lists,
    +// updates, and removes them; only signed ones are offered, and the DVX
    +// just installed (its SYSTEM\KEYS) decides whether it trusts them.
     // The package holds the program only (the DVX folder layout: dvxPlat.h);
     // everything written while DVX runs is in the user folder (USER, or the
     // folder DVXUSER names), which the package never holds and an upgrade
    @@ -43,6 +52,8 @@
     #define SETUP_CORE_H
     
     #include "pkgFormat.h"
    +#include "pkgOp.h"
    +#include "pkgPlan.h"
     #include "dvxPlat.h"
     
     #include 
    @@ -61,20 +72,16 @@
     #define SETUP_LIST_NAME      "SETUP.LST"
     
     // What Setup installs of the fonts: the last of the package's tiers it
    -// unpacks (tier 0 the program, 1 the base set of fonts, 2 the others)
    +// unpacks (tier 0 the program, 1 the base set of fonts; the others are
    +// the More Fonts add-on)
     typedef enum {
         SetupFontsNoneE,
         SetupFontsBaseE,
    -    SetupFontsAllE,
         SetupFontsCountE
     } SetupFontsE;
     
     _Static_assert(SetupFontsCountE == PKG_TIERS, "a fonts choice per package tier");
     
    -// The fonts folder as SETUP.LST names it (an upgrade with fewer fonts
    -// removes the ones it leaves out: setupRemoveDropped)
    -#define SETUP_FONTS_FOLDER   DVX_SHARED_DIR "/" DVX_FONTS_DIR "/"
    -
     // Room a file takes beyond its size on the target disk (half a 16 KB
     // cluster, typical of a FAT16 hard disk), for the free space check
     #define SETUP_FILE_SLACK     8192
    @@ -82,6 +89,103 @@ _Static_assert(SetupFontsCountE == PKG_TIERS, "a fonts choice per package tier")
     // Longest message the functions below return
     #define SETUP_ERR_MAX        320
     
    +// The add-ons Setup offers: NAME.DPK files in this folder beside
    +// SETUP.EXE, each signed (in it, or by NAME.SIG beside it).  /ADDONS=
    +// chooses them by NAME: all, none, or a list.
    +#define SETUP_ADDONS_DIR     "ADDONS"
    +#define SETUP_ADDONS_ALL     "ALL"
    +#define SETUP_ADDONS_NONE    "NONE"
    +#define SETUP_ADDONS_SEP     ','
    +#define SETUP_ADDON_NAME_MAX 9          // an 8.3 base name and its NUL
    +
    +// The optional parts of DVX Setup offers: SETUP_PARTS_NAME beside
    +// SETUP.EXE (mkDist writes it from src/packages.ini), [components] listing
    +// them (id = name), a section each: summary, needs (the parts it cannot
    +// work without), members (its packages), folders (theirs, each ending in
    +// '/'), paths (their files outside their folders, such as a DOS program
    +// in SYSTEM), bytes, files.  /PARTS= chooses them by their ids' names (BASIC,
    +// COMM ...), ALL, or NONE.
    +#define SETUP_PARTS_NAME     "SETUP.INI"
    +#define SETUP_PARTS_SECTION  "components"
    +#define SETUP_PARTS_ALL      SETUP_ADDONS_ALL
    +#define SETUP_PARTS_NONE     SETUP_ADDONS_NONE
    +
    +// A part: a bundle of DVX's packages, chosen or not as a whole
    +typedef struct {
    +    char     id[PKG_ID_MAX];
    +    char     name[PKG_TEXT_MAX];
    +    char     summary[PKG_TEXT_MAX];
    +    char   **needs;                     // stb_ds: the parts it needs (ids)
    +    char   **members;                   // stb_ds: its packages (ids)
    +    char   **folders;                   // stb_ds: their folders, members' order (relative, '/', ending in '/')
    +    char   **paths;                     // stb_ds: their files outside them (relative, '/')
    +    uint64_t bytes;                     // its files, with SETUP_FILE_SLACK each
    +    bool     installed;                 // the target folder has it (setupPartsInstalled)
    +} SetupPartT;
    +
    +// An add-on on the medium: its package, its file's base name in capitals
    +// (what /ADDONS= names), the room its files need, and whether the target
    +// folder has it (setupAddonsInstalled)
    +typedef struct {
    +    PkgOfferT offer;
    +    char      name[SETUP_ADDON_NAME_MAX];
    +    uint64_t  bytes;
    +    bool      installed;
    +} SetupAddonT;
    +
    +// Whether addon is chosen by /ADDONS= arg: ALL, NONE, or a list of names
    +// (SETUP_ADDONS_SEP between them, any case).  Without arg (NULL), what
    +// the target folder has.
    +bool     setupAddonChosen(const char *arg, const SetupAddonT *addon);
    +
    +// What to do to destDir's add-on (db: its packages; ring: the keys its
    +// DVX trusts): install it (upgrade or repair) when chosen, remove it when
    +// it is installed and not chosen.  False when there is nothing to do;
    +// else *op is the operation to step (pkgOpStep; NULL when out of memory).
    +bool     setupAddonOp(PkgDbT *db, const PkgKeyringT *ring, const SetupAddonT *addon, bool chosen, PkgOpT **op);
    +
    +// The signed add-ons in srcDir's ADDONS folder, by name (an stb_ds array:
    +// setupAddonsFree; NULL when there are none).  An unsigned or unreadable
    +// one is left out, and logged.
    +SetupAddonT *setupAddonsFind(const char *srcDir);
    +
    +void     setupAddonsFree(SetupAddonT *addons);
    +
    +// Mark which of addons destDir has.
    +void     setupAddonsInstalled(SetupAddonT *addons, const char *destDir);
    +
    +// Whether part is chosen by /PARTS= arg (ALL, NONE, or names, the ids'
    +// names, any case, SETUP_ADDONS_SEP between them).  Without arg (NULL):
    +// on a new install every part, else what the target folder has.
    +bool        setupPartChosen(const char *arg, const SetupPartT *part, bool newInstall);
    +
    +// Make chosen (chosen[i] for parts[i]) hold together after chosen[changed]
    +// changed: a part chosen brings the parts it needs, a part left out takes
    +// the parts that need it.  changed -1: every chosen part brings its needs.
    +void        setupPartsFix(const SetupPartT *parts, bool *chosen, int32_t changed);
    +
    +void        setupPartsFree(SetupPartT *parts);
    +
    +// Mark which of parts destDir has: its bundle recorded there, or (DVX
    +// installed before it was in parts) a member's folder there that has no
    +// record of its own (a member Software installed alone is not the part).
    +void        setupPartsInstalled(SetupPartT *parts, const char *destDir);
    +
    +// Choose again each part left out (chosen[i] false) that a package in db
    +// outside the parts left out needs, with the parts it needs
    +// (setupPartsFix): the names of all chosen again into kept (", " between
    +// them; "" when none).
    +void        setupPartsKeepNeeded(const SetupPartT *parts, bool *chosen, const PkgDbT *db, char *kept, int32_t keptSize);
    +
    +// The parts srcDir's SETUP_PARTS_NAME lists (stb_ds array:
    +// setupPartsFree; NULL when there is none).
    +SetupPartT *setupPartsRead(const char *srcDir);
    +
    +// Whether name (relative, '/'; a folder's without its '/') belongs to a
    +// part not chosen: in one of its folders, one of its paths, or one of its
    +// records.  For pkgJobSkip.
    +bool        setupPartsSkip(const SetupPartT *parts, const bool *chosen, const char *name);
    +
     // Add line (with a CR LF) to the end of the batch file at path, after
     // copying it to its SETUP_AUTOEXEC_BAK sibling.  A missing file is
     // created (nothing to back up).  The change is written to a scratch file
    @@ -112,10 +216,11 @@ bool     setupAutoexecRemove(const char *path, const char *line, char *err, int3
     // package is read from nor inside it.  False with a reason in err.
     bool     setupCheckDest(const char *srcDir, const char *destDir, char *err, int32_t errSize);
     
    -// What of the fonts the DVX in destDir has, judged from its SETUP.LST and
    -// info's tiers: none, at most the base set's files, or more.  A folder
    -// without a list (a new install) gets all of them.
    -SetupFontsE setupFontsInstalled(const char *destDir, const PkgHeaderT *info);
    +// What of the fonts the DVX in destDir has: the base set when the base
    +// fonts' package is recorded there (PKG_BASE_FONTS_ID: it comes in the
    +// package's fonts tier, and Software may have removed it since), else
    +// none.  A folder without a SETUP.LST (a new install) gets the base set.
    +SetupFontsE setupFontsInstalled(const char *destDir);
     
     // True when the DVX in destDir has the user's DVX.INI (platformUserDirOf).
     bool     setupHasSettings(const char *destDir);
    @@ -124,11 +229,6 @@ bool     setupHasSettings(const char *destDir);
     // there, or run from there) or SETUP.LST (an install partly removed).
     bool     setupIsInstalled(const char *destDir);
     
    -// Record DVX itself in destDir as the installed package kpunch.dvx (this
    -// version and ABI; no files, which SETUP.LST keeps), so add-on packages
    -// can need a version of DVX.  False when it cannot be written.
    -bool     setupRecordDvx(const char *destDir);
    -
     // Remove the user folder of the DVX in destDir (platformUserDirOf), its
     // settings and files, when it holds a DVX.INI (a folder that does not is
     // left alone, whatever DVXUSER names).  False only when removing failed.
    @@ -139,11 +239,15 @@ bool     setupRemoveUser(const char *destDir);
     // cannot be written.
     bool     setupListWrite(const char *destDir, char *const *files, int32_t count);
     
    -// Remove the files under folder (a name ending in '/', as SETUP.LST has
    -// it) that destDir's list has and files (the new install's) does not --
    -// what an upgrade with fewer fonts leaves out -- then those of its
    -// folders left empty.  Call it before setupListWrite.
    -void     setupRemoveDropped(const char *destDir, const char *folder, char *const *files, int32_t count);
    +// Remove what the DVX in destDir had before this install (before: its
    +// packages then) that files (the names the install wrote) does not have:
    +// the names of its SETUP.LST, and the files and records of the packages
    +// Setup installed (their records in that list: DVX itself, the base
    +// fonts, the optional parts) -- files DVX has dropped, a server update's
    +// included, and the base fonts or a part this install leaves out -- but
    +// a file another package has; then the folders left empty.  Call it
    +// before setupListWrite.
    +void     setupRemoveDropped(const char *destDir, const PkgDbT *before, char *const *files, int32_t count);
     
     // Remove what a cancelled new install wrote: the files, then the folders
     // it made (newest first) when they are empty.
    diff --git a/src/libs/kpunch/dvxshell/Makefile b/src/libs/kpunch/dvxshell/Makefile
    index a9856409..a1364623 100644
    --- a/src/libs/kpunch/dvxshell/Makefile
    +++ b/src/libs/kpunch/dvxshell/Makefile
    @@ -34,6 +34,7 @@ SYSTEMDIR = ../../../../bin/system
     SHAREDDIR = ../../../../bin/shared
     THEMEDIR  = $(SHAREDDIR)/themes
     WPAPERDIR = $(SHAREDDIR)/wpaper
    +BANKDIR   = $(SHAREDDIR)/banks
     KEYSDIR   = $(SYSTEMDIR)/KEYS
     
     SRCS   = shellMain.c shellApp.c shellBas.c shellSysBas.c shellInfo.c shellAssoc.c shellProg.c shellRun.c shellSaver.c pmModel.c pmModelBas.c
    @@ -46,11 +47,15 @@ TARGET    = $(TARGETDIR)/dvxshell.lib
     THEMES = $(THEMEDIR)/geos.thm $(THEMEDIR)/win31.thm $(THEMEDIR)/cde.thm $(THEMEDIR)/hotdog.thm
     WPAPERS = $(WPAPERDIR)/blueglow.jpg $(WPAPERDIR)/swoop.jpg $(WPAPERDIR)/triangle.jpg
     
    +# The instrument banks DVX ships beside music.lib's own (config/banks:
    +# each OP2 and the text with its licence, as they came)
    +BANKS = $(patsubst ../../../../config/banks/%,$(BANKDIR)/%,$(wildcard ../../../../config/banks/*))
    +
     # The repository's public keys (config/keys/*.KEY, made offline with
     # dvxpkg keygen): what DVX trusts as its own (pkgTrust.h)
     KEYS = $(patsubst ../../../../config/keys/%,$(KEYSDIR)/%,$(wildcard ../../../../config/keys/*.KEY))
     
    -all: $(TARGET) $(TARGETDIR)/dvxshell.dep $(SYSTEMDIR)/DVX.INI $(SYSTEMDIR)/ASSOC.INI $(SYSTEMDIR)/DOSPROG.BMP $(THEMES) $(WPAPERS) $(KEYS)
    +all: $(TARGET) $(TARGETDIR)/dvxshell.dep $(SYSTEMDIR)/DVX.INI $(SYSTEMDIR)/ASSOC.INI $(SYSTEMDIR)/DOSPROG.BMP $(THEMES) $(WPAPERS) $(BANKS) $(KEYS)
     
     $(TARGETDIR)/dvxshell.dep: dvxshell.dep | $(TARGETDIR)
     	sed 's/$$/\r/' $< > $@
    @@ -95,6 +100,12 @@ $(WPAPERDIR):
     $(WPAPERDIR)/%.jpg: ../../../../config/wpaper/%.jpg | $(WPAPERDIR)
     	cp $< $@
     
    +$(BANKDIR):
    +	mkdir -p $(BANKDIR)
    +
    +$(BANKDIR)/%: ../../../../config/banks/% | $(BANKDIR)
    +	cp $< $@
    +
     $(KEYSDIR):
     	mkdir -p $(KEYSDIR)
     
    @@ -105,4 +116,4 @@ $(KEYSDIR)/%.KEY: ../../../../config/keys/%.KEY | $(KEYSDIR)
     
     clean:
     	rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/dvxshell.dep $(TARGETDIR)
    -	rm -rf $(WPAPERDIR) $(THEMEDIR) $(KEYSDIR) $(SYSTEMDIR)/DVX.INI $(SYSTEMDIR)/ASSOC.INI $(SYSTEMDIR)/DOSPROG.BMP
    +	rm -rf $(WPAPERDIR) $(THEMEDIR) $(BANKDIR) $(KEYSDIR) $(SYSTEMDIR)/DVX.INI $(SYSTEMDIR)/ASSOC.INI $(SYSTEMDIR)/DOSPROG.BMP
    diff --git a/src/libs/kpunch/dvxshell/dvxshell.dhs b/src/libs/kpunch/dvxshell/dvxshell.dhs
    index 6ba8eddd..099754c6 100644
    --- a/src/libs/kpunch/dvxshell/dvxshell.dhs
    +++ b/src/libs/kpunch/dvxshell/dvxshell.dhs
    @@ -1189,6 +1189,7 @@ BASIC (shell.bas, DECLARE LIBRARY "dvxshell"): ShellGroupsLoad / Save / Free / S
     .index SysScreenshot
     .index SysMemoryTotalKb
     .index SysMemoryFreeKb
    +.index SysLibraryLoaded
     .index SysScreenSaver$
     .index SysSetScreenSaver
     .index SysScreenSaverStart
    @@ -1258,6 +1259,7 @@ int32_t     SysSendKey(int32_t keyCode, int32_t shift);
     int32_t     SysScreenshot(const char *path);
     int32_t     SysMemoryTotalKb(void);
     int32_t     SysMemoryFreeKb(void);
    +int32_t     SysLibraryLoaded(const char *name);
     .endcode
     
     .table
    @@ -1294,6 +1296,7 @@ int32_t     SysMemoryFreeKb(void);
       SysSendKey                   queues (up to 64) keyCode + shift as the BIOS stores the keystroke (platformBiosScanCode for combinations with codes of their own, KEY_EXT_FLAG keys by scan code, Ctrl+letter as its control character); a shell idle handler (shellRegisterIdle) injects one per idle frame with platformInjectKey, so each is read with its own modifiers, and releases them (platformInjectModifiers(0)) after the last
       SysScreenshot                dvxScreenshot (BMP for .bmp, else PNG)
       SysMemoryTotalKb / FreeKb    platformGetMemoryInfo; -1 when unknown
    +  SysLibraryLoaded             dvxLibraryLoaded (the module's ABI stamp, by its base name in any case)
     .endtable
     
     There is no binding for platformInjectMouse: on DOS it replaces the real mouse until the next call and has no way back to it, so a synthesized click would leave the pointer stuck. A program moves the pointer with SysSetMousePos and "clicks" a control by calling its event procedure or setting its value.
    diff --git a/src/libs/kpunch/dvxshell/shellApp.h b/src/libs/kpunch/dvxshell/shellApp.h
    index e1ed6f46..aa4a3ace 100644
    --- a/src/libs/kpunch/dvxshell/shellApp.h
    +++ b/src/libs/kpunch/dvxshell/shellApp.h
    @@ -604,7 +604,8 @@ void        ShellTaskManager(void);
     // read the input state the context holds; SysSendKey queues keystrokes
     // typed one per idle frame (platformInjectKey); SysSetMousePos warps the
     // pointer (platformMouseWarp); SysScreenshot is dvxScreenshot;
    -// SysMemory*Kb are platformGetMemoryInfo (-1 when unknown).
    +// SysMemory*Kb are platformGetMemoryInfo (-1 when unknown);
    +// SysLibraryLoaded is dvxLibraryLoaded.
     
     int32_t     SysColorCount(void);
     int32_t     SysColorId(const char *name);
    @@ -621,6 +622,7 @@ void        SysFontsStore(int32_t prefs);
     int32_t     SysFontWidth(void);
     int32_t     SysGetColor(int32_t id);
     int32_t     SysIdleSeconds(void);
    +int32_t     SysLibraryLoaded(const char *name);
     int32_t     SysLoadTheme(const char *path);
     int32_t     SysMemoryFreeKb(void);
     int32_t     SysMemoryTotalKb(void);
    diff --git a/src/libs/kpunch/dvxshell/shellAssoc.c b/src/libs/kpunch/dvxshell/shellAssoc.c
    index 5e4dadbc..6f682cca 100644
    --- a/src/libs/kpunch/dvxshell/shellAssoc.c
    +++ b/src/libs/kpunch/dvxshell/shellAssoc.c
    @@ -32,11 +32,13 @@
     
     #include "shellAssoc.h"
     #include "shellApp.h"
    +#include "shellProg.h"
     #include "dvxPrefs.h"
     
     #include 
     #include 
     #include 
    +#include 
     
     // ============================================================
     // Module state
    @@ -54,6 +56,7 @@ static ShellAssocLaunchFnT sLaunch      = shellLoadAppWithArgs;
     // ============================================================
     
     static void          assocDrop(void);
    +static bool          assocInstalled(const char *value);
     static void          assocProgramNative(char *value);
     static bool          assocSave(void);
     static PrefsHandleT *assocStore(void);
    @@ -81,6 +84,21 @@ static void assocDrop(void) {
     }
     
     
    +// Whether the program a stored value names is there: a DVX program (an
    +// .app) that is not (its optional part of DVX left out) opens nothing.  A
    +// DOS program can be a bare name found along the PATH.
    +static bool assocInstalled(const char *value) {
    +    char        program[DVX_MAX_PATH];
    +    const char *sep = strchr(value, SHELL_ASSOC_ARGS_SEP);
    +    int32_t     len = sep ? (int32_t)(sep - value) : (int32_t)strlen(value);
    +
    +    snprintf(program, sizeof(program), "%.*s", (int)len, value);
    +    dvxTrimRight(program);
    +    assocProgramNative(program);
    +    return !shellIsProgram(program) || access(program, F_OK) == 0;
    +}
    +
    +
     // A stored "program,arguments" value with its program part in native
     // separators (the shipped ASSOC.INI says "apps/kpunch/..."); the
     // arguments are left alone ("/doc" is a switch, not a path).
    @@ -251,7 +269,7 @@ bool shellAssocLookup(const char *extension, char *programPath, int32_t programP
         char        ext[SHELL_ASSOC_EXT_MAX];
         const char *value = shellAssocNormalize(extension, ext) ? prefsGetString(assocStore(), SHELL_ASSOC_SECTION, ext, NULL) : NULL;
     
    -    if (!value || !value[0] || !programPath) {
    +    if (!value || !value[0] || !programPath || !assocInstalled(value)) {
             return false;
         }
     
    diff --git a/src/libs/kpunch/dvxshell/shellAssoc.h b/src/libs/kpunch/dvxshell/shellAssoc.h
    index 324ccdd2..6872f84f 100644
    --- a/src/libs/kpunch/dvxshell/shellAssoc.h
    +++ b/src/libs/kpunch/dvxshell/shellAssoc.h
    @@ -78,7 +78,8 @@ int32_t shellAssocCount(void);
     bool shellAssocGet(int32_t index, char *extension, int32_t extensionSize, char *programPath, int32_t programPathSize);
     
     // Program for an extension (full stored value, as shellAssocGet).  False
    -// when the extension has no association.
    +// when the extension has no association, or its DVX program is not
    +// installed (an optional part of DVX left out: Setup, Software).
     bool shellAssocLookup(const char *extension, char *programPath, int32_t programPathSize);
     
     // Launch the program associated with documentPath's extension, passing the
    diff --git a/src/libs/kpunch/dvxshell/shellSysBas.c b/src/libs/kpunch/dvxshell/shellSysBas.c
    index 517a779d..8f1f8024 100644
    --- a/src/libs/kpunch/dvxshell/shellSysBas.c
    +++ b/src/libs/kpunch/dvxshell/shellSysBas.c
    @@ -91,6 +91,7 @@ void                         SysFontsStore(int32_t prefs);
     int32_t                      SysFontWidth(void);
     int32_t                      SysGetColor(int32_t id);
     int32_t                      SysIdleSeconds(void);
    +int32_t                      SysLibraryLoaded(const char *name);
     int32_t                      SysLoadTheme(const char *path);
     int32_t                      SysMemoryFreeKb(void);
     int32_t                      SysMemoryTotalKb(void);
    @@ -331,6 +332,12 @@ int32_t SysIdleSeconds(void) {
     }
     
     
    +// Whether the library name is loaded (dvxLibraryLoaded).
    +int32_t SysLibraryLoaded(const char *name) {
    +    return dvxLibraryLoaded(name) ? BAS_TRUE : BAS_FALSE;
    +}
    +
    +
     // Apply a theme file.  True when it was read.
     int32_t SysLoadTheme(const char *path) {
         return dvxLoadTheme(shellContext(), path) ? BAS_TRUE : BAS_FALSE;
    diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs
    index 6e15e5bc..eb13085a 100644
    --- a/src/libs/kpunch/libdvx/apiref.dhs
    +++ b/src/libs/kpunch/libdvx/apiref.dhs
    @@ -717,7 +717,7 @@ const BitmapFontT struct describing the 8x16 font, ready to pass to drawText/dra
     
     .h1 dvxText.h -- Fonts
     
    -The font families DVX draws with: "System", the built-in 8x16 font, and the families in the FONTS folder. Which families that folder holds depends on the install -- Setup puts in none, the base set (DejaVu Sans, Sans Mono, and Serif), or all of them -- and dvxFontOpen of a family that is not there gives the system font. A family has up to four faces (regular, bold, italic, bold italic), each a .DFN file holding the face at the pixel sizes it was made at; the files are made on the build host from TrueType, Type 1, and bitmap fonts (src/tools/mkfont.c), so DVX only copies bitmaps. More files of the same family and style (up to four in all) add the sizes the face lacks, each size read from the file it came in: the printer-fonts add-on gives the DejaVu faces their large sizes that way, from FONTS\KPUNCH.
    +The font families DVX draws with: "System", the built-in 8x16 font, and the families in the FONTS folder. Which families that folder holds depends on the install -- Setup puts in the base set (DejaVu Sans, Sans Mono, and Serif) or none, and the More Fonts add-on the rest -- and dvxFontOpen of a family that is not there gives the system font. A family has up to four faces (regular, bold, italic, bold italic), each a .DFN file holding the face at the pixel sizes it was made at; the files are made on the build host from TrueType, Type 1, and bitmap fonts (src/tools/mkfont.c), so DVX only copies bitmaps. A face's file holds its screen sizes, their smoothed copies, and its print sizes together (the base set's have 25 to 100 pixels for printing; FONT_SIZES_MAX sizes at most). More files of the same family and style (up to four in all) add the sizes the face lacks, each size read from the file it came in: a program's own copy of a face adds its sizes that way.
     
     dvxFontOpen gives a BitmapFontT every text call draws and measures with. It never scales: it picks the face's size nearest the one asked for. A style the family has no face for is made from the nearest face -- bold by thickening each stroke a column (two from 24 pixels), italic by slanting a column per four rows -- and the font's flags say so. Every character of code page 437 draws: what a face lacks is filled in when the font loads. Box drawing, block, and shade characters are drawn to fill the character cell (the pitch, or the digit 0's width) so lines join, also in place of a face's own when its file asks; other characters come from FONT_FALLBACK_FAMILY (FONT_FALLBACK_FIXED for a fixed-pitch face, centered in its pitch) at the nearest size and the same style, or from the system font when that family is not installed.
     
    @@ -742,6 +742,7 @@ A .DFN carries its notice (where the font came from and its copyright notices) a
       FONT_SYSTEM_FAMILY      "System"              The built-in 8x16 font's family name
       FONT_FALLBACK_FAMILY    "DejaVu Sans"         Fills what a proportional face lacks
       FONT_FALLBACK_FIXED     "DejaVu Sans Mono"    Fills what a fixed-pitch face lacks
    +  FONT_SIZES_MAX          64                    The most sizes a face has (dvxFontSizes)
     .endtable
     
     .h2 dvxFontOpen
    @@ -2818,6 +2819,7 @@ Install an icon that is already in display pixel format (for example from dvxLoa
     .index dvxExclusiveBegin
     .index dvxRunDos
     .index dvxIdleMs
    +.index dvxLibraryLoaded
     .index platformDosRun
     .index platformSuspendHookAdd
     .index platformPathAbsolute
    @@ -3767,6 +3769,14 @@ dvxRunDos runs a DOS program (an .EXE, .COM, or .BAT file, or a command DOS know
     
     platformPathAbsolute makes a path absolute against the current folder (it need not exist).
     
    +.h2 Optional Libraries: dvxLibraryLoaded
    +
    +.code
    +bool dvxLibraryLoaded(const char *name);
    +.endcode
    +
    +Whether the library (or widget) name, its file's base name in any case ("pkgnet", "dvxsql"), is loaded: installed and started with DVX, which loads every library and widget it has at start. It looks for the module's ABI stamp (DVX_ABI_STAMP: name and DVX_ABI_SUFFIX), which the loader checked before it took the module; a module that is not there, or did not load, has none. Some parts of DVX are optional (Setup's "What to install" page installs them as bundles: src/packages.ini), so a program that uses one of their libraries only when it is there asks first, and does not import it: an import of a missing library stops the program from loading. BASIC: SysLibraryLoaded (system.bas).
    +
     .h2 Idle Time: dvxIdleMs
     
     .code
    diff --git a/src/libs/kpunch/libdvx/arch.dhs b/src/libs/kpunch/libdvx/arch.dhs
    index e9991b28..ed6d5577 100644
    --- a/src/libs/kpunch/libdvx/arch.dhs
    +++ b/src/libs/kpunch/libdvx/arch.dhs
    @@ -734,7 +734,7 @@ Timestamp-based: two clicks on the same target (title bar, minimized icon, close
     
     .h1 Font System
     
    -Every text call takes a BitmapFontT, and there are two kinds of them. The system font is the 8x16 VGA ROM font (code page 437, 256 glyphs) built into libdvx: 8-pixel cells, one byte per glyph row, so character positions are a multiplication, glyph lookup an index, and every scanline a single byte -- the hot paths of drawChar, drawTextN, and drawTermRow depend on it. A loaded font (dvxText.h) is one size of a font family from the FONTS folder (Setup installs none, the base set of DejaVu Sans, Sans Mono, and Serif, or all of them; a family that is not there opens as the system font): per-glyph advances and bitmap boxes, any width, proportional or fixed-pitch, and in up to four styles. The same calls draw and measure both; a loaded font's glyphs field is what sends them to the general paths.
    +Every text call takes a BitmapFontT, and there are two kinds of them. The system font is the 8x16 VGA ROM font (code page 437, 256 glyphs) built into libdvx: 8-pixel cells, one byte per glyph row, so character positions are a multiplication, glyph lookup an index, and every scanline a single byte -- the hot paths of drawChar, drawTextN, and drawTermRow depend on it. A loaded font (dvxText.h) is one size of a font family from the FONTS folder (Setup installs the base set of DejaVu Sans, Sans Mono, and Serif or none, and the More Fonts add-on the others; a family that is not there opens as the system font): per-glyph advances and bitmap boxes, any width, proportional or fixed-pitch, and in up to four styles. The same calls draw and measure both; a loaded font's glyphs field is what sends them to the general paths.
     
     .h2 BitmapFontT
     
    @@ -818,12 +818,12 @@ Text in several fonts in one control is built on the text flow (dvxFlow.h): runs
     
     .h2 Font Files (.DFN)
     
    -One file holds one family in one style at every size it comes in, made on the build host by mkfont (src/tools/mkfont.c) with FreeType from TrueType, OpenType, Type 1, BDF, and PCF fonts, as listed in src/fonts/fonts.ini. A .DFN is a pure format conversion: the converter never adds or changes a glyph, it only marks the characters a face lacks. Little-endian, every record read whole:
    +One file holds one family in one style at every size it comes in -- its screen sizes, their smoothed copies, and its print sizes (mkfont's print key; the SDK guide's Making Fonts) -- made on the build host by mkfont (src/tools/mkfont.c) with FreeType from TrueType, OpenType, Type 1, BDF, and PCF fonts, as listed in src/fonts/fonts.ini. Opening a font reads only the header and the strike table, so sizes nothing uses cost disk space, not memory. A .DFN is a pure format conversion: the converter never adds or changes a glyph, it only marks the characters a face lacks. Little-endian, every record read whole:
     
     .table
       Part                      Size            Holds
       ----                      ----            -----
    -  DfnHeaderT                60 bytes        "DVXF", format version (2), style, face flags (fixed pitch; box characters drawn by DVX), strike count (1 to 32), family name (31 characters), where the notice and licence texts are and their lengths
    +  DfnHeaderT                60 bytes        "DVXF", format version (2), style, face flags (fixed pitch; box characters drawn by DVX), strike count (1 to 64), family name (31 characters), where the notice and licence texts are and their lengths
       DfnStrikeT x count        24 bytes each   per size, smallest first: where its glyph table and bitmaps are, the bitmaps' size, the pixel size, ascent, descent, line height, widest advance
       FontGlyphT x 256          6 bytes each    per size, at its glyph table offset
       glyph bitmaps             as listed       per size, back to back in character order
    diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c
    index e266a3c8..a47dd392 100644
    --- a/src/libs/kpunch/libdvx/dvxApp.c
    +++ b/src/libs/kpunch/libdvx/dvxApp.c
    @@ -62,6 +62,7 @@
     #include "thirdparty/stb_ds_wrap.h"
     
     #include 
    +#include 
     #include 
     #include 
     #include 
    @@ -90,6 +91,8 @@ DVX_ABI_STAMP(libdvx);
     #define SUBMENU_ARROW_HALF     3     // half-size of submenu arrow glyph
     #define TOOLTIP_DELAY_MS       500
     #define DVX_DOS_CMD_MAX        256   // a DOS command line as dvxRunDos builds it (DOS takes less)
    +#define LIBRARY_NAME_MAX       64    // a module's base name (dvxLibraryLoaded)
    +#define LIBRARY_SYM_MAX        (LIBRARY_NAME_MAX + 32)     // with the symbol prefix and the ABI suffix
     #define TOOLTIP_PAD            3
     #define POPUP_BEVEL_WIDTH      2     // popup menu border bevel thickness
     #define POPUP_ITEM_PAD_H       8     // extra horizontal padding in popup items
    @@ -3794,6 +3797,33 @@ void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) {
     }
     
     
    +// ============================================================
    +// dvxLibraryLoaded
    +// ============================================================
    +//
    +// A loaded module exports its ABI stamp (DVX_ABI_STAMP: its name, in
    +// lower case, and DVX_ABI_SUFFIX), which the loader checked as it loaded
    +// it; a module that did not load has none to find.
    +
    +bool dvxLibraryLoaded(const char *name) {
    +    char   lower[LIBRARY_NAME_MAX];
    +    char   sym[LIBRARY_SYM_MAX];
    +    size_t len = name ? strlen(name) : 0;
    +
    +    if (len == 0 || len >= sizeof(lower)) {
    +        return false;
    +    }
    +
    +    for (size_t i = 0; i <= len; i++) {
    +        lower[i] = (char)tolower((unsigned char)name[i]);
    +    }
    +
    +    int32_t n = snprintf(sym, sizeof(sym), "%s%s%s", DVX_SYM_PREFIX, lower, DVX_ABI_SUFFIX_STR);
    +
    +    return n > 0 && n < (int32_t)sizeof(sym) && dlsym(NULL, sym) != NULL;
    +}
    +
    +
     // ============================================================
     // dvxLoadImage
     // ============================================================
    diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h
    index b4e8c4c2..9cca1d15 100644
    --- a/src/libs/kpunch/libdvx/dvxApp.h
    +++ b/src/libs/kpunch/libdvx/dvxApp.h
    @@ -361,6 +361,13 @@ void dvxExclusiveWaitRetrace(void);
     // E2BIG for a command line longer than DOS takes).
     int32_t dvxRunDos(AppContextT *ctx, const char *program, const char *args, const char *folder, bool pause);
     
    +// Whether the library (or widget) name -- its file's base name, any case,
    +// as DECLARE LIBRARY names it: "pkgnet", "dvxsql" -- is loaded, that is
    +// installed and started with DVX (the loader loads every one at start).
    +// A program using a library of an optional part of DVX (Networking,
    +// Databases, ...) asks before it calls one of its functions.
    +bool dvxLibraryLoaded(const char *name);
    +
     // ============================================================
     // Idle time
     // ============================================================
    diff --git a/src/libs/kpunch/libdvx/dvxDialog.c b/src/libs/kpunch/libdvx/dvxDialog.c
    index 21b01e51..c5f566bc 100644
    --- a/src/libs/kpunch/libdvx/dvxDialog.c
    +++ b/src/libs/kpunch/libdvx/dvxDialog.c
    @@ -276,9 +276,9 @@ typedef struct {
         DvxFontChoiceT *out;
         DvxFontChoiceT  cur;
         const char    **families;                               // stb_ds; names owned by the font system
    -    const char     *sizeItems[DFN_STRIKE_MAX];
    -    char            sizeText[DFN_STRIKE_MAX][FN_SIZE_TEXT];
    -    int32_t         sizes[DFN_STRIKE_MAX];
    +    const char     *sizeItems[FONT_SIZES_MAX];
    +    char            sizeText[FONT_SIZES_MAX][FN_SIZE_TEXT];
    +    int32_t         sizes[FONT_SIZES_MAX];
         int32_t         sizeCount;
         WidgetT        *familyList;
         WidgetT        *styleList;
    @@ -2221,7 +2221,7 @@ static void fnRefreshSample(FontBoxStateT *fn) {
     static void fnRefreshSizes(FontBoxStateT *fn) {
         int32_t best = 0;
     
    -    fn->sizeCount = DVX_MIN(dvxFontSizes(fn->cur.family, fn->cur.style & FONT_STYLE_FACE, fn->sizes, DFN_STRIKE_MAX), DFN_STRIKE_MAX);
    +    fn->sizeCount = DVX_MIN(dvxFontSizes(fn->cur.family, fn->cur.style & FONT_STYLE_FACE, fn->sizes, FONT_SIZES_MAX), FONT_SIZES_MAX);
     
         for (int32_t i = 0; i < fn->sizeCount; i++) {
             snprintf(fn->sizeText[i], sizeof(fn->sizeText[i]), "%" PRId32, fn->sizes[i]);
    diff --git a/src/libs/kpunch/libdvx/dvxFontFmt.h b/src/libs/kpunch/libdvx/dvxFontFmt.h
    index 0fec02a8..48b2c585 100644
    --- a/src/libs/kpunch/libdvx/dvxFontFmt.h
    +++ b/src/libs/kpunch/libdvx/dvxFontFmt.h
    @@ -73,7 +73,7 @@
     #define DFN_EXT            ".DFN"
     #define DFN_CHAR_COUNT     256
     #define DFN_FAMILY_MAX     FONT_FAMILY_MAX
    -#define DFN_STRIKE_MAX     32   // sizes one file may hold
    +#define DFN_STRIKE_MAX     64   // strikes one file may hold (a smoothed size has two)
     
     // DfnStrikeT.flags
     #define DFN_STRIKE_GRAY2   0x0001   // glyph bitmaps of FONT_GRAY_BPP bits per pixel (anti-aliased)
    diff --git a/src/libs/kpunch/libdvx/dvxText.c b/src/libs/kpunch/libdvx/dvxText.c
    index e7f39afd..78ee7b7d 100644
    --- a/src/libs/kpunch/libdvx/dvxText.c
    +++ b/src/libs/kpunch/libdvx/dvxText.c
    @@ -61,6 +61,7 @@
     #include "dvxRes.h"
     #include "stb_ds_wrap.h"
     
    +#include 
     #include 
     #include 
     #include 
    @@ -68,7 +69,7 @@
     #include 
     
     #define FACE_STYLES         4           // regular, bold, italic, bold italic: indexed by style bits
    -#define FACE_FILES_MAX      4           // .DFN files one face's sizes may come from (its own, and add-ons with more sizes)
    +#define FACE_FILES_MAX      4           // .DFN files one face's sizes may come from (the installed one, a program's own)
     #define FONT_IDLE_MAX       8           // fonts no one holds kept loaded
     #define SYSTEM_FAMILY_INDEX 0
     #define NO_FAMILY           (-1)
    @@ -132,14 +133,16 @@ typedef struct {
         FontSourceT *src[FACE_FILES_MAX];   // held, srcCount of them; none: no face of this style (sizeCount 0)
         int32_t      srcCount;
         bool         installed;             // read from the font folders (a rescan reads it again)
    -    uint8_t      sizes[DFN_STRIKE_MAX]; // rising, each once
    -    uint8_t      sizeSrc[DFN_STRIKE_MAX];   // the src each size is read from
    +    uint8_t      sizes[FONT_SIZES_MAX]; // rising, each once
    +    uint8_t      sizeSrc[FONT_SIZES_MAX];   // the src each size is read from
         int32_t      sizeCount;
    -    uint32_t     grayMask;              // bit i: sizes[i] has a gray strike too
    +    uint64_t     grayMask;              // bit i: sizes[i] has a gray strike too
         uint16_t     style;                 // FONT_STYLE_BOLD / ITALIC
         uint16_t     faceFlags;             // DFN_FACE_*
     } FontFaceT;
     
    +_Static_assert(FONT_SIZES_MAX <= sizeof(((FontFaceT *)0)->grayMask) * CHAR_BIT, "FontFaceT.grayMask has a bit per size");
    +
     // A family: its faces by style, installed (OWNER_ALL) or added for one
     // program
     typedef struct {
    @@ -1021,7 +1024,7 @@ static bool faceGlyph(const BuildT *b, int32_t ch, FontGlyphT *g, const uint8_t
     static bool faceGray(const FontFaceT *face, int32_t size) {
         int32_t i = face ? sizeIndex(face, size) : -1;
     
    -    return sSmooth && i >= 0 && (face->grayMask & (1u << i)) != 0;
    +    return sSmooth && i >= 0 && (face->grayMask & ((uint64_t)1 << i)) != 0;
     }
     
     
    @@ -1033,7 +1036,7 @@ static bool faceMerge(FontFaceT *face, const DfnSourceT *dfn, const DfnFileT *fi
         const char  *name  = dfn->name ? dfn->name : "a font";
         int32_t      index = face->srcCount;
         int32_t      added = 0;
    -    bool         gray[DFN_STRIKE_MAX];
    +    bool         gray[FONT_SIZES_MAX];
         FontSourceT *made;
     
         if (index >= FACE_FILES_MAX) {
    @@ -1049,11 +1052,11 @@ static bool faceMerge(FontFaceT *face, const DfnSourceT *dfn, const DfnFileT *fi
         }
     
         for (int32_t i = 0; i < face->sizeCount; i++) {
    -        gray[i] = (face->grayMask & (1u << i)) != 0;
    +        gray[i] = (face->grayMask & ((uint64_t)1 << i)) != 0;
         }
     
         // Its 1-bit sizes the face lacks
    -    for (int32_t s = 0; s < file->header.strikeCount && face->sizeCount < DFN_STRIKE_MAX; s++) {
    +    for (int32_t s = 0; s < file->header.strikeCount && face->sizeCount < FONT_SIZES_MAX; s++) {
             int32_t size = file->strikes[s].pixelSize;
             int32_t at   = 0;
     
    @@ -1093,7 +1096,7 @@ static bool faceMerge(FontFaceT *face, const DfnSourceT *dfn, const DfnFileT *fi
         face->grayMask = 0;
     
         for (int32_t i = 0; i < face->sizeCount; i++) {
    -        face->grayMask |= gray[i] ? 1u << i : 0;
    +        face->grayMask |= gray[i] ? (uint64_t)1 << i : 0;
         }
     
         if (index == 0) {
    diff --git a/src/libs/kpunch/libdvx/dvxText.h b/src/libs/kpunch/libdvx/dvxText.h
    index 9c9c88da..3e7ac42a 100644
    --- a/src/libs/kpunch/libdvx/dvxText.h
    +++ b/src/libs/kpunch/libdvx/dvxText.h
    @@ -27,9 +27,9 @@
     // bold, italic, bold italic), each a .DFN file holding the face at the
     // pixel sizes it was made at (dvxFontFmt.h; made on the build host by
     // mkfont from TrueType, Type 1, and bitmap fonts).  More .DFN files of
    -// the same family and style add the sizes the face lacks (an add-on with
    -// large sizes for printing).  "System" is the
    -// built-in 8x16 font, always there.
    +// the same family and style add the sizes the face lacks (a program's
    +// own copy of a face with sizes of its own).  "System" is the built-in
    +// 8x16 font, always there.
     //
     // dvxFontOpen gives a BitmapFontT (dvxTypes.h) every text call draws and
     // measures with.  It never scales: it picks the size the face has that is
    @@ -68,6 +68,7 @@
     #define FONT_SYSTEM_FAMILY    "System"
     #define FONT_FALLBACK_FAMILY  "DejaVu Sans"         // fills what a proportional face lacks
     #define FONT_FALLBACK_FIXED   "DejaVu Sans Mono"    // fills what a fixed-pitch face lacks
    +#define FONT_SIZES_MAX        64                    // the most sizes a face has (dvxFontSizes)
     
     // The font of family (NULL, "", or a family there is none of: the system
     // font) at the size nearest pixelSize, in style: FONT_STYLE_BOLD and / or
    diff --git a/src/libs/kpunch/libdvx/dvxTextBas.c b/src/libs/kpunch/libdvx/dvxTextBas.c
    index d7b57237..879f5134 100644
    --- a/src/libs/kpunch/libdvx/dvxTextBas.c
    +++ b/src/libs/kpunch/libdvx/dvxTextBas.c
    @@ -32,7 +32,6 @@
     
     #include "dvxText.h"
     #include "dvxDraw.h"
    -#include "dvxFontFmt.h"
     #include "dvxMem.h"
     #include "dvxBas.h"
     
    @@ -144,10 +143,10 @@ void FontRescan(void) {
     
     // Size index (0 .. FontSizeCount - 1) of family in style; 0 out of range.
     int32_t FontSizeAt(const char *family, int32_t style, int32_t index) {
    -    int32_t sizes[DFN_STRIKE_MAX];
    -    int32_t count = dvxFontSizes(family, (uint32_t)style, sizes, DFN_STRIKE_MAX);
    +    int32_t sizes[FONT_SIZES_MAX];
    +    int32_t count = dvxFontSizes(family, (uint32_t)style, sizes, FONT_SIZES_MAX);
     
    -    return (index >= 0 && index < count && index < DFN_STRIKE_MAX) ? sizes[index] : 0;
    +    return (index >= 0 && index < count && index < FONT_SIZES_MAX) ? sizes[index] : 0;
     }
     
     
    diff --git a/src/libs/kpunch/libdvx/sdkGuide.dhs b/src/libs/kpunch/libdvx/sdkGuide.dhs
    index 8665c248..6ef0cdad 100644
    --- a/src/libs/kpunch/libdvx/sdkGuide.dhs
    +++ b/src/libs/kpunch/libdvx/sdkGuide.dhs
    @@ -45,7 +45,7 @@ The DVX SDK holds everything needed to build DVX modules outside the DVX source
       ----             --------
       newProject.sh    Creates a project from a template
       sdk.mk           Build rules every project Makefile includes
    -  bin/             dvxres (resources), dvxhlpc (help compiler), bascomp (BASIC compiler), mkfont (fonts a program carries), dxeGen.sh (runs dxe3gen)
    +  bin/             dvxres (resources), dvxhlpc (help compiler), bascomp (BASIC compiler), mkfont (fonts: see Making Fonts), dxeGen.sh (runs dxe3gen)
       include/         Public C headers, one directory per library, plus include/basic for BASIC declaration files
       templates/       The project templates
       samples/basic/   Example BASIC projects
    @@ -54,7 +54,7 @@ The DVX SDK holds everything needed to build DVX modules outside the DVX source
     .h2 Requirements
     
     .list
    -.item A Linux host (the tools in bin/ are Linux programs; on DVX itself they are SYSTEM/DVXRES.EXE, SYSTEM/DVXHLPC.EXE, and SYSTEM/BASCOMP.EXE).
    +.item A Linux host (the tools in bin/ are Linux programs; on DVX itself they are SYSTEM/BASCOMP.EXE, which comes with DVX BASIC, and SYSTEM/DVXRES.EXE and SYSTEM/DVXHLPC.EXE, which come with Developer Tools).
     .item The DJGPP cross compiler (i586-pc-msdosdjgpp-gcc and dxe3gen). Projects find it through DJGPP_PREFIX, from the environment or the make command line. Without it they use toolchains/djgpp of the DVX source tree the SDK sits in (SDK_DIR/../../toolchains/djgpp), which ./installToolchain.sh sets up.
     .endlist
     
    @@ -137,6 +137,7 @@ Nothing else is needed: DVX loads dvxnet.lib when it starts and connects the cal
     .link sdk.debug Debugging: the Log and Crash Reports
     .link sdk.abi Binary Interface Rules
     .link sdk.packaging Packaging and Installing
    +.link sdk.fonts Making Fonts
     .link games.intro Writing Games for DVX
     
     .topic sdk.firstapp
    @@ -178,7 +179,7 @@ ctx gives the program its paths: appPath (the .app file, used to read its own re
     
     A program writes only in its user folder, and never writes or installs anything into DVX's SHARED folder (DVX's own fonts, sounds, themes, and wallpapers): its own files stay with it, in its DATA folder or as resources in the .app, under its vendor and program folder, so two programs' files never collide.
     
    -A program can carry fonts of its own as font resources, so its text looks the same whatever fonts were installed: add a line such as "title font TITLE.DFN" to hello.res (dvxres checks that the file is a DVX font). DVX adds the program's fonts when it starts and forgets them when it ends; other programs never see them, and where a family is installed too, the program's sizes and styles come first (dvxText.h, Fonts a Program Carries). Make a .DFN with bin/mkfont from a TrueType, Type 1, or bitmap font -- its manifest names the sizes, whether they are smoothed too, and the characters to keep (mkfont.c's header documents every key; one size and only the characters the program shows keep the file small) -- or copy one from DVX's SHARED\FONTS folder. A .DFN carries the notice and licence of the font it was made from.
    +A program can carry fonts of its own as font resources, so its text looks the same whatever fonts were installed: add a line such as "title font TITLE.DFN" to hello.res (dvxres checks that the file is a DVX font). DVX adds the program's fonts when it starts and forgets them when it ends; other programs never see them, and where a family is installed too, the program's sizes and styles come first (dvxText.h, Fonts a Program Carries). Make a .DFN with bin/mkfont from a TrueType, Type 1, or bitmap font (see Making Fonts; one size and only the characters the program shows keep the file small), or copy one from DVX's SHARED\FONTS folder. A .DFN carries the notice and licence of the font it was made from.
     
     .h2 Run
     
    @@ -540,6 +541,8 @@ The DVX side of these rules (what bumps the number) is under Binary Interface Ve
     .index PKG_KEY
     .index PKG_VERSION
     .index dvxpkg
    +.index Optional parts of DVX
    +.index Bundles
     
     .h1 Packaging and Installing
     
    @@ -551,6 +554,7 @@ make package lays out pkg/ exactly as the files go into the DVX directory and ma
       Program    APPS///         NAME.app, NAME.hlp, DATA/ with the DATA files (if any)
       Widget     WIDGETS///      NAME.wgt, NAME.DHS, NAME.BHS, NAME.dep (if any)
       Library    LIBS///         NAME.lib, NAME.DHS, NAME.dep (if any)
    +  Any        SYSTEM/                       A program for the DOS prompt that goes with it (not in SYSTEM/KEYS or SYSTEM/PACKAGES)
     .endtable
     
     .list
    @@ -585,6 +589,12 @@ bin/dvxpkg info NAME.dpk                         # what the package says
     
     Give acme.KEY to the people who install your packages (Software, File > Trust a Publisher's Key) and keep acme.SEC private and backed up: anyone who has it can sign as you, and an update must be signed with the key that signed the version installed. bin/dvxpkg also signs and checks packages and lists their contents; on DVX, SYSTEM/DVXPKG.EXE does the same, and installs and removes packages. The package library and its file format are described under Package Library.
     
    +.h2 DVX's Own Packages and Optional Parts
    +
    +DVX is packages too: kpunch.dvx (the loader, SYSTEM, the shell, the core libraries and widgets, and the everyday programs), kpunch.basefont (the base fonts), and the optional parts Setup's "What to install" page offers. Each part is a bundle (kind bundle: a package with no files of its own that needs its members) and each member is one module in its own package: kpunch.basic (dvxbasic, basdev), kpunch.data (dvxsql, datactrl, dbgrid), kpunch.net (dvxnet, http, pkgnet), kpunch.comm (terminal, conn, xfer; it needs kpunch.net), kpunch.media (player, recorder), kpunch.devtools (iconed, resedit, helpedit, pkgbuild, widshow), and kpunch.demos (dvxdemo, basdemo). src/packages.ini lists them. The build (src/installer/mkPackages.py) works out what each member needs from the modules its code imports, the libraries its BASIC program declares, and the manifest's uses (a module reached by name), and stops when DVX itself, or a part, would need a part it does not name. A member's files outside its folder are its paths: the DOS tools in SYSTEM go with the part they serve (BASCOMP.EXE with kpunch.basdev, DVXRES.EXE with kpunch.resedit, and DVXHLPC.EXE with kpunch.helpedit, the Help Editor); DVXPKG.EXE stays in DVX itself, to install and remove packages at the DOS prompt.
    +
    +A module of yours that cannot work without a part's library names the library's package in PKG_NEEDS (PKG_NEEDS="kpunch.dvxnet=1.0.0"): Software then installs it only where that part is. One that can do without it does not import it: it asks dvxLibraryLoaded (BASIC: SysLibraryLoaded, system.bas) and reaches the library by name only when it is there, as Software does with pkgnet. BASIC's SQL functions do nothing without the Databases part (SQLOpen returns 0).
    +
     .h2 Publishing on the Package Server
     
     Software's Available and Updates pages list the packages on the DVX package server. To publish one there, make an account on the server (POST /v1/account/register, then the link in the mail), give its email and password in Software's File > Package Server, and choose File > Publish a Package; Software's Published page shows each version's review. From the build machine, sign in (POST /v1/account/login gives a session token) and upload the package with it:
    @@ -595,3 +605,129 @@ curl -H "Authorization: Bearer TOKEN" -H "Content-Type: application/octet-stream
     .endcode
     
     The server checks the package as DVX does before installing it and holds it for review; GET /v1/mine shows its state, and the note when it is turned down. Once the server's operator approves it, it is signed with DVX's repository key and listed, and every DVX that has an older version is offered the update. An id belongs to the account that first uploaded it, so later versions must come from the same account; ids of the vendor kpunch are DVX's own. The server's API and its rules are in server/README.md in the DVX source tree.
    +
    +.topic sdk.fonts
    +.title Making Fonts
    +.toc 1 Making Fonts
    +.index Making Fonts
    +.index mkfont
    +.index fonts.ini
    +.index Print sizes
    +.index Font add-on
    +.index Font sizes
    +
    +.h1 Making Fonts
    +
    +DVX never draws an outline: every size it draws or prints is made beforehand by mkfont, from a TrueType, OpenType, Type 1, BDF, or PCF font, into a .DFN file. One file holds one family in one style (regular, bold, italic, or bold italic) at every size it comes in: the sizes for the screen, their smoothed copies, and the sizes for printing, all made from the same source in the same run, so the screen and the paper never show two versions of a font. When DVX starts it reads only each file's header and size list; a size's glyphs load when something draws or prints at that size, so sizes cost disk space, not memory.
    +
    +.h2 Three Kinds of Size
    +
    +.table
    +  Kind       Manifest key   What it is for
    +  ----       ------------   --------------
    +  Screen     sizes          The pixel sizes text is drawn at (8 to 24 for text faces); "native" for a bitmap face, which has its own
    +  Smoothed   smooth         Screen sizes also made anti-aliased (2 bits per pixel), drawn when text is smoothed (screens of more than 256 colors); about twice the room of the 1-bit size, and best from 12 pixels
    +  Print      print          Sizes made 1-bit for printing (25 and up); none of them a screen size, and never smoothed
    +.endtable
    +
    +A size is in pixels, which on paper are printer dots: a size in points is points x dpi / 72 dots, so 12 points is 50 dots at 300 dpi and 30 at 180. Printing picks the face's smallest size at least as large as the text in dots and scales it down a little; without one it scales the largest up, smoothed but softer. PostScript printers draw with their own faces. The base set's print sizes cover the usual point sizes at both resolutions:
    +
    +.table
    +  Pixels   Points at 300 dpi   Points at 180 dpi
    +  ------   -----------------   -----------------
    +  25       6                   10
    +  29       7                   11.6
    +  33       8                   13.2
    +  38       9                   15.2
    +  42       10                  16.8
    +  50       12                  20
    +  58       14                  23.2
    +  67       16                  26.8
    +  75       18                  30
    +  83       20                  33.2
    +  100      24                  40
    +.endtable
    +
    +A file holds at most 64 strikes: its screen sizes, smoothed sizes, and print sizes together (DejaVu Sans has 12, 8, and 11, so 31). A size is at most 127 pixels. Every size also shows in the Font dialog and can be drawn on the screen, so a program can use the large ones for headings.
    +
    +.h2 The Manifest
    +
    +mkfont works from a manifest, an INI file: [fonts] lists the files to make (8.3 base name = family name), [licences] names each licence and the text files it is made of, and each font has a section of its own. src/fonts/fonts.ini is DVX's; src/tools/mkfont.c's header documents every key.
    +
    +.code
    +[fonts]
    +ACMESANS = Acme Sans
    +ACMESANB = Acme Sans
    +
    +[licences]
    +ACME     = acme/OFL.txt
    +
    +[ACMESANS]
    +style    = regular
    +licence  = ACME
    +source   = acme/AcmeSans-Regular.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
    +hinting  = native
    +
    +[ACMESANB]
    +style    = bold
    +licence  = ACME
    +source   = acme/AcmeSans-Bold.ttf
    +sizes    = 8 9 10 11 12 13 14 15 16 18 20 24
    +smooth   = 12 13 14 15 16 18 20 24
    +print    = 25 29 33 38 42 50 58 67 75 83 100
    +hinting  = native
    +.endcode
    +
    +.table
    +  Key        Meaning
    +  ---        -------
    +  style      regular, bold, italic, or bolditalic
    +  licence    A key of [licences]: the text every file carries
    +  source     The font file, under the source folder; source1, source2 ... more files for other sizes, each with its own sizes1, smooth1 ...
    +  sizes      Screen sizes, or native
    +  smooth     Sizes (or all) also made smoothed
    +  print      Sizes made for printing
    +  hinting    native (the font's own), auto (FreeType's), or none; italics often look better with auto
    +  graycut    Sizes whose 1-bit glyphs are cut from the smoothed rendering, for a face whose 1-bit hinting draws it too light
    +  boxdraw    true: DVX draws the box, block, and shade characters itself
    +  chars      Only these characters (hex codes and ranges), the rest marked missing
    +  weight     The weight of a variable font to make
    +  encoding   cp437 for a font that numbers its glyphs in code page 437
    +  set        DVX's own fonts only: base or more (where the file ships)
    +.endtable
    +
    +.h2 Names and Licences
    +
    +.list
    +.item A file's 8.3 base name is the family's short form plus B (bold), I (italic), or Z (bold italic): ACMESANS, ACMESANB, ACMESANI, ACMESANZ. The four files of a family share its family name (at most 31 characters).
    +.item Ship only fonts whose licence lets them be converted and given away (the SIL Open Font Licence, Apache, MIT, the Bitstream Vera licence, public domain), and read what it says about the font's name in a converted copy: some keep the name for the original. DVX's X11 bitmap faces ship under neutral names (Swiss, Roman, Schoolbook, Typewriter) because their own names are trademarks.
    +.item Every .DFN carries its notice (the family, style, and the files it was made from, with their copyright notices) and its licence text, so the licence goes wherever the font does; the Font dialog's Licence button shows it.
    +.endlist
    +
    +.h2 Building and Checking
    +
    +In the SDK, bin/mkfont MANIFEST SOURCEDIR OUTDIR makes every font the manifest lists, and stops with a message naming the font for any mistake: a bad size, a size listed twice, more than 64 strikes, a missing licence text. It prints a line per font with the characters the font lacks (DVX fills those in from DejaVu Sans or the system font when it draws). bin/mkfont -p FONT.DFN SHEET.BMP draws every size of a font into a picture, with the characters it lacks marked in red: look at it before shipping a font.
    +
    +In the DVX source tree, src/fonts/fonts.ini lists every font DVX ships, and the build makes them into bin/shared/fonts/kpunch (DVX's vendor folder) from sources src/fonts/installFonts.sh downloads (pinned versions). Each font names its set: base, the DVX Fonts package (kpunch.basefont, src/fonts/PACKAGE.INI), which Setup installs with DVX unless told to install no fonts and which the package server can update; or more, the More Fonts add-on. mkfont stops when a font names no set, and when a font of the base set has no print sizes, so neither a font nor its print sizes can be left out.
    +
    +.h2 Where Fonts Go
    +
    +.list
    +.item In the FONTS folder, for every program: as an add-on package of kind font, its files under SHARED/FONTS/VENDOR/ (see Packaging and Installing), installed with Software. A face may come in several files of the same family and style (up to four), each adding the sizes the others lack, but one file per face is the way to ship: its sizes cannot drift apart.
    +.item In a program, for it alone: as font resources (a .res line "name font FILE.DFN", or a BASIC project's Fonts). The program's sizes and styles come first and the installed family fills in the rest.
    +.endlist
    +
    +.h2 Checklist
    +
    +.list
    +.item The licence allows converting and giving the font away, and its full text is in [licences].
    +.item Each style the family has is made; a missing bold or italic is made by DVX, thickened or slanted.
    +.item Screen sizes from 8 to 24 pixels for a text face, smoothed from 12.
    +.item Print sizes from 25 to 100 pixels for any face meant for documents (a base-set font must have them).
    +.item At most 64 strikes a file; 8.3 file names; family names of 31 characters at most.
    +.item The missing-character line and the mkfont -p picture of each font looked at.
    +.item As an add-on: kind = font, files under SHARED/FONTS/VENDOR/, a higher version for every change, signed.
    +.endlist
    diff --git a/src/libs/kpunch/music/music.dhs b/src/libs/kpunch/music/music.dhs
    index 5a4b851e..b6a5a279 100644
    --- a/src/libs/kpunch/music/music.dhs
    +++ b/src/libs/kpunch/music/music.dhs
    @@ -129,7 +129,7 @@ On the FM chip and the MPU-401 port the song is played by the timer service (500
     
     .h2 General MIDI on FM
     
    -Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion, and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank (MUSIC_INI_BANK) names another OP2 file instead: a bank in SHARED\BANKS or the user's BANKS folder by its name (an add-on package's as VENDOR\NAME), or any OP2 file by its path from the DVX folder, for example a bank made with an OPL editor, or another freely licensed bank such as DMXOPL. Control Panel's Sounds page lists the banks to choose from; DVX's own add-ons offer DMXOPL and DMXOPL3 (MIT, by ConSiGno and Sneakernets) through Software. A bank that is not there falls back to the bundled one (MUSIC_BANK_DEFAULT). DVX.INI's choice is read as each MIDI music starts, so a new choice reaches the next one; a music keeps the bank it started with, and a bank is freed with the last music playing on it.
    +Instruments come from an OP2 bank (the DMX "GENMIDI" format: 128 melodic instruments and 47 percussion instruments for notes 35 to 81, each one or two two-operator voices, with a note offset, a fixed note for percussion, and a detune for the second voice). DVX ships Freedoom's GENMIDI bank (see Licences below); DVX.INI [audio] midiBank (MUSIC_INI_BANK) names another OP2 file instead: a bank in SHARED\BANKS or the user's BANKS folder by its name (an add-on package's as VENDOR\NAME), or any OP2 file by its path from the DVX folder, for example a bank made with an OPL editor. DVX also ships DMXOPL and DMXOPL3 (MIT, by ConSiGno and Sneakernets) in SHARED\BANKS, made to sound like a Roland Sound Canvas. Control Panel's Sounds page lists the banks to choose from. A bank that is not there falls back to the bundled one (MUSIC_BANK_DEFAULT). DVX.INI's choice is read as each MIDI music starts, so a new choice reaches the next one; a music keeps the bank it started with, and a bank is freed with the last music playing on it.
     
     What is played: note on / off, velocity, program change, channel volume (CC7), expression (CC11), pan (CC10: left, both, or right on an OPL3 and the software synthesizer), sustain pedal (CC64), pitch bend with the bend range from RPN 0 (default 2 semitones), all notes off (CC123, CC120), reset controllers (CC121), and General MIDI System On (a reset). Channel 10 plays percussion. Loudness is velocity x volume x expression x the music's volume on a 40 log10 curve, added as attenuation to the voice's level. OP2 banks are tuned for DMX, which sounds note n where MIDI has note n + 12, so notes are played an octave up from the bank's offsets. Not played: modulation (CC1), aftertouch, the FM chip's own rhythm mode, four-operator voices.
     
    @@ -234,6 +234,8 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
     
     The DVX General MIDI SoundFont add-on (DVXGM.SF2) is made from FluidR3_GM by Frank Wen (MIT licence); its notice is installed beside it (SHARED\BANKS\KPUNCH\DVXGM.TXT).
     
    +The DMXOPL 1.11 instrument bank is by ConSiGno, and DMXOPL3 by Sneakernets (MIT licence); each one's licence and credits are installed beside it (SHARED\BANKS\DMXOPL.TXT and DMXOPL3.TXT).
    +
     The GENMIDI instrument bank is from Freedoom 0.13.0, Copyright (C) 2001-2024 Contributors to the Freedoom project. All rights reserved. BSD 3-clause licence:
     
     Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the Freedoom project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    diff --git a/src/libs/kpunch/pkg/pkg.dhs b/src/libs/kpunch/pkg/pkg.dhs
    index b66fa399..d34fe636 100644
    --- a/src/libs/kpunch/pkg/pkg.dhs
    +++ b/src/libs/kpunch/pkg/pkg.dhs
    @@ -83,6 +83,8 @@ Every file is listed with its size and SHA-256 and every listed file is in the p
       system                DVX itself (kpunch.dvx): anywhere but the user folder
     .endtable
     
    +Every kind but bundle may also add files to SYSTEM (a DOS program run at the prompt, say), but not to SYSTEM/KEYS (the keys DVX trusts) or SYSTEM/PACKAGES (the package records, PKG_DB_DIR), which only DVX itself writes. A file there that another package has is refused when the package is installed, as anywhere (pkgPlanInstall), so an add-on still cannot replace one of DVX's.
    +
     .h2 Manifests (pkgManifest.h)
     
     .table
    @@ -164,8 +166,11 @@ dvxpkg verify NAME.DPK [-s NAME.SIG] [KEY.KEY ...]
     dvxpkg info NAME.DPK
     dvxpkg keygen "Key name" BASE
     dvxpkg install NAME.DPK [-r DVXDIR] [-y]
    +dvxpkg record NAME.DPK DVXDIR [-n]
     dvxpkg remove VENDOR.NAME [-r DVXDIR] [-f]
     dvxpkg list [-r DVXDIR]
     .endcode
     
    +record writes a package's installed record into a DVX folder whose files came another way, and prints the record's name; with -n the package is recorded as needed by another (it goes when nothing needs it any more), not chosen. The release build records every package of DVX's in Setup's package that way (DVX itself, the base fonts, the optional bundles and their members: src/packages.ini), every file with its size and SHA-256, so an update from the package server replaces them as it would any package's, removing the files the new version dropped.
    +
     On DOS, DVXPKG remove is the way out when an add-on keeps DVX from starting (DVX /SAFE, or holding Shift, also starts DVX without add-ons). The SDK's make package builds NAME.dpk from a project (sdk.mk's PKG_* settings), and the IDE's File > Make Package from a BASIC project.
    diff --git a/src/libs/kpunch/pkg/pkgCli.c b/src/libs/kpunch/pkg/pkgCli.c
    index 9fef22f3..6f139bc6 100644
    --- a/src/libs/kpunch/pkg/pkgCli.c
    +++ b/src/libs/kpunch/pkg/pkgCli.c
    @@ -48,6 +48,9 @@
     #define ARGS_CHECK          3
     #define ARGS_KEYGEN         4
     #define ARGS_DVX            3
    +#define ARGS_RECORD         4
    +#define ARGS_RECORD_MAX     5
    +#define OPT_NEEDED          "-n"        // record: needed by another package, not chosen (goes with it)
     #define OPT_DETACHED        "-d"
     #define OPT_SIG             "-s"
     #define OPT_ROOT            "-r"        // install / remove / list: the DVX folder
    @@ -77,6 +80,7 @@ static int   cmdInfo(const char *path);
     static int   cmdInstall(const CliOptsT *o);
     static int   cmdKeygen(const char *name, const char *base, PkgRandomFnT random);
     static int   cmdList(const CliOptsT *o);
    +static int   cmdRecord(const char *path, const char *root, bool explicitly);
     static int   cmdRemove(const CliOptsT *o);
     static int   cmdSign(int argc, char *argv[]);
     static int   cmdVerify(int argc, char *argv[]);
    @@ -347,6 +351,39 @@ static int cmdList(const CliOptsT *o) {
     }
     
     
    +// The package at path recorded as installed in the DVX folder root, as
    +// if installed there (its files are put there some other way: Setup's
    +// package carries the records of DVX's packages with their files),
    +// explicitly or as needed by another (removed with it: a bundle's
    +// member).  Prints the record's name relative to root.
    +static int cmdRecord(const char *path, const char *root, bool explicitly) {
    +    PkgDpkInfoT info;
    +    PkgDbT      db;
    +    char        err[PKG_ERR_MAX];
    +    char        rel[DVX_MAX_PATH];
    +    int         result = EXIT_FAILED;
    +
    +    if (!pkgDpkReadInfo(path, &info, err, sizeof(err))) {
    +        said(path, err);
    +        return EXIT_FAILED;
    +    }
    +
    +    memset(&db, 0, sizeof(db));
    +    snprintf(db.root, sizeof(db.root), "%s", root);
    +
    +    if (!pkgDbPut(&db, &info.manifest, explicitly, NULL, err, sizeof(err))) {
    +        said(root, err);
    +    } else if (pkgDbRecordPath("", info.manifest.id, rel, sizeof(rel))) {
    +        printf("%s\n", rel);
    +        result = EXIT_OK;
    +    }
    +
    +    pkgDbFree(&db);
    +    pkgDpkInfoFree(&info);
    +    return result;
    +}
    +
    +
     // Remove the installed package o->arg from the DVX folder o->root.  What
     // needs it, and files changed since they were installed, go only with -f.
     static int cmdRemove(const CliOptsT *o) {
    @@ -563,6 +600,10 @@ int pkgCliRun(int argc, char *argv[], PkgRandomFnT random, bool *handled) {
             return cmdDvx(argv[ARG_FIRST]);
         }
     
    +    if (strcmp(cmd, "record") == 0 && (argc == ARGS_RECORD || (argc == ARGS_RECORD_MAX && strcmp(argv[ARGS_RECORD], OPT_NEEDED) == 0))) {
    +        return cmdRecord(argv[ARG_FIRST], argv[ARG_FIRST + 1], argc == ARGS_RECORD);
    +    }
    +
         if (strcmp(cmd, "info") == 0 && argc == ARGS_INFO) {
             return cmdInfo(argv[ARG_FIRST]);
         }
    @@ -599,6 +640,7 @@ void pkgCliUsage(FILE *f) {
         fprintf(f, "       dvxpkg keygen \"Key name\" BASE\n");
         fprintf(f, "       dvxpkg dvx TEMPLATE.INI    (DVX's own package template)\n");
         fprintf(f, "       dvxpkg install NAME.DPK [-r DVXDIR] [-y]\n");
    +    fprintf(f, "       dvxpkg record NAME.DPK DVXDIR [-n] (as installed there, -n: needed by another; prints the record's name)\n");
         fprintf(f, "       dvxpkg remove VENDOR.NAME [-r DVXDIR] [-f]\n");
         fprintf(f, "       dvxpkg list [-r DVXDIR]\n");
     }
    diff --git a/src/libs/kpunch/pkg/pkgDb.h b/src/libs/kpunch/pkg/pkgDb.h
    index 67566c70..90a7ec00 100644
    --- a/src/libs/kpunch/pkg/pkgDb.h
    +++ b/src/libs/kpunch/pkg/pkgDb.h
    @@ -45,7 +45,6 @@
     #include 
     #include 
     
    -#define PKG_DB_DIR      "PACKAGES"      // in SYSTEM
     #define PKG_DB_EXT      ".INI"
     
     typedef struct {
    diff --git a/src/libs/kpunch/pkg/pkgFormat.c b/src/libs/kpunch/pkg/pkgFormat.c
    index 17679f14..e566598f 100644
    --- a/src/libs/kpunch/pkg/pkgFormat.c
    +++ b/src/libs/kpunch/pkg/pkgFormat.c
    @@ -39,6 +39,9 @@
     #define HDR_TIERS       32
     #define HDR_TIER_SIZE   16
     #define HDR_CRC         (HDR_TIERS + HDR_TIER_SIZE * PKG_TIERS)
    +#define HDR_CRC_SIZE    4
    +
    +_Static_assert(PKG_HEADER_SIZE == HDR_CRC + HDR_CRC_SIZE, "PKG_HEADER_SIZE is the header's fields and its CRC-32");
     
     // A tier's fields, from its start
     #define TIER_END        0
    diff --git a/src/libs/kpunch/pkg/pkgFormat.h b/src/libs/kpunch/pkg/pkgFormat.h
    index 6818a8a1..06b78490 100644
    --- a/src/libs/kpunch/pkg/pkgFormat.h
    +++ b/src/libs/kpunch/pkg/pkgFormat.h
    @@ -36,9 +36,9 @@
     //
     // Tiers: the files come in PKG_TIERS groups, in this order in the ZIP.
     // Tier 0 is always installed; a later tier is installed with every tier
    -// before it (DVX: the program, the base set of fonts, the other fonts), so
    -// Setup stops reading at the end of the last tier chosen and never asks
    -// for the disks after it.  A file of a later tier carries its tier as its
    +// before it (DVX: the program, then the base set of fonts; the other
    +// fonts are the More Fonts add-on), so Setup stops reading at the end of
    +// the last tier chosen and never asks for the disks after it.  A file of a later tier carries its tier as its
     // ZIP file comment, one digit (pkgBuildZip writes it, pkgBuildInfo reads
     // it).
     //
    @@ -60,13 +60,13 @@
     
     // Part header: magic, format version, then the fields of PkgHeaderT and
     // a CRC-32 of everything before it.  Little-endian.
    -#define PKG_HEADER_SIZE      84
    +#define PKG_HEADER_SIZE      68
     #define PKG_MAGIC            "DVXPKG\032"
     #define PKG_MAGIC_SIZE       8
     #define PKG_FORMAT_VERSION   2
     
     // Tiers of files (see above), and the length of a later tier's comment
    -#define PKG_TIERS            3
    +#define PKG_TIERS            2
     #define PKG_TIER_COMMENT_LEN 1
     
     // Part file names: DVX.P01 .. DVX.P99 (PKG_PART_NAME_MAX with the NUL)
    diff --git a/src/libs/kpunch/pkg/pkgManifest.c b/src/libs/kpunch/pkg/pkgManifest.c
    index 6bc36a75..309f7573 100644
    --- a/src/libs/kpunch/pkg/pkgManifest.c
    +++ b/src/libs/kpunch/pkg/pkgManifest.c
    @@ -96,7 +96,8 @@ static const char *const sKindNames[PkgKindCountE] = { "app", "library", "widget
     
     // Each kind's folders; help goes in its program's folder, or beside a
     // library or widget of its name (pkgFileAllowed); bundle has none; system
    -// has the whole program tree.
    +// has the whole program tree.  Every kind with files may also add to
    +// SYSTEM, but for sSystemOwnDirs.
     static const KindFolderT sKindFolders[PkgKindCountE] = {
         [PkgKindAppE]       = { DVX_APPS_DIR,                     true  },
         [PkgKindLibraryE]   = { DVX_LIBS_DIR,                     true  },
    @@ -112,6 +113,9 @@ static const KindFolderT sKindFolders[PkgKindCountE] = {
         [PkgKindSystemE]    = { NULL,                             false }
     };
     
    +// SYSTEM's folders only DVX itself writes
    +static const char *const sSystemOwnDirs[] = { PKG_DB_DIR, PKG_KEYS_DIR };
    +
     // ============================================================
     // Prototypes
     // ============================================================
    @@ -125,6 +129,7 @@ static bool        inFolder(const char *name, const char *root, const char *vend
     static bool        parseFileLine(PkgManifestT *m, const PkgIniT *ini, char *err, int32_t errSize);
     static bool        parsePackageKey(PkgManifestT *m, const PkgIniT *ini, uint32_t *seen, char *err, int32_t errSize);
     static bool        parseText(const char *text, int32_t len, bool template, PkgManifestT *m, char *err, int32_t errSize);
    +static const char *pathUnder(const char *name, const char *dir);
     void               pkgDvxManifest(PkgManifestT *m);
     bool               pkgFileAllowed(const PkgManifestT *m, const char *name);
     void               pkgIdPartFrom(const char *text, char *out);
    @@ -146,6 +151,7 @@ bool               pkgTemplateRead(const char *path, PkgManifestT *m, char *err,
     int32_t            pkgVersionCompare(PkgVersionT a, PkgVersionT b);
     void               pkgVersionFormat(PkgVersionT v, char *out, int32_t outSize);
     bool               pkgVersionParse(const char *text, PkgVersionT *v);
    +static bool        systemFileAllowed(const char *name);
     static void        textAdd(TextT *t, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
     
     
    @@ -214,7 +220,7 @@ static bool inFolder(const char *name, const char *root, const char *vendor, con
         char    prefix[PREFIX_MAX];
         int32_t n = folderText(prefix, sizeof(prefix), root, vendor, pkgName);
     
    -    return n > 0 && strncasecmp(name, prefix, (size_t)n) == 0 && name[n] == PKG_NAME_SEP && name[n + 1] != '\0';
    +    return n > 0 && pathUnder(name, prefix) != NULL;
     }
     
     
    @@ -384,6 +390,15 @@ static bool parseText(const char *text, int32_t len, bool template, PkgManifestT
     }
     
     
    +// The rest of name after dir and its '/' (any case), or NULL when name is
    +// not in dir (dir itself is not).
    +static const char *pathUnder(const char *name, const char *dir) {
    +    size_t n = strlen(dir);
    +
    +    return (strncasecmp(name, dir, n) == 0 && name[n] == PKG_NAME_SEP && name[n + 1] != '\0') ? name + n + 1 : NULL;
    +}
    +
    +
     void pkgDvxManifest(PkgManifestT *m) {
         pkgManifestInit(m);
         snprintf(m->id, sizeof(m->id), "%s", PKG_SYSTEM_ID);
    @@ -418,13 +433,21 @@ bool pkgFileAllowed(const PkgManifestT *m, const char *name) {
             return false;
         }
     
    +    if (m->kind == PkgKindBundleE) {
    +        return false;
    +    }
    +
    +    if (systemFileAllowed(name)) {
    +        return true;
    +    }
    +
         if (m->kind == PkgKindHelpE) {
             return inFolder(name, DVX_APPS_DIR, vendor, pkgName) || inFolder(name, DVX_LIBS_DIR, vendor, pkgName) || inFolder(name, DVX_WIDGETS_DIR, vendor, pkgName);
         }
     
         const KindFolderT *f = &sKindFolders[m->kind];
     
    -    return f->root && inFolder(name, f->root, vendor, f->perName ? pkgName : NULL);
    +    return inFolder(name, f->root, vendor, f->perName ? pkgName : NULL);
     }
     
     
    @@ -779,6 +802,27 @@ bool pkgVersionParse(const char *text, PkgVersionT *v) {
     }
     
     
    +// Whether name is in SYSTEM, and neither one of the folders there only
    +// DVX itself writes nor in one.
    +static bool systemFileAllowed(const char *name) {
    +    const char *rest = pathUnder(name, DVX_SYSTEM_DIR);
    +
    +    if (!rest) {
    +        return false;
    +    }
    +
    +    for (int32_t i = 0; i < DVX_ARRAY_LEN(sSystemOwnDirs); i++) {
    +        size_t n = strlen(sSystemOwnDirs[i]);
    +
    +        if (strncasecmp(rest, sSystemOwnDirs[i], n) == 0 && (rest[n] == PKG_NAME_SEP || rest[n] == '\0')) {
    +            return false;
    +        }
    +    }
    +
    +    return true;
    +}
    +
    +
     static void textAdd(TextT *t, const char *fmt, ...) {
         char    line[LINE_MAX];
         va_list ap;
    diff --git a/src/libs/kpunch/pkg/pkgManifest.h b/src/libs/kpunch/pkg/pkgManifest.h
    index c575f0d8..c5e6c326 100644
    --- a/src/libs/kpunch/pkg/pkgManifest.h
    +++ b/src/libs/kpunch/pkg/pkgManifest.h
    @@ -44,9 +44,13 @@
     // Every file in the package is listed with its size and SHA-256 and every
     // listed file is in the package, so the one signature over these bytes
     // covers everything.  Each kind keeps to its own folders (pkgFileAllowed),
    -// so an add-on cannot write over DVX or another vendor's files; only
    -// `system` (DVX itself, kpunch.dvx) may write anywhere in the program
    -// tree, and nothing writes the user folder.  Lines end in CR LF as
    +// so an add-on cannot write over DVX or another vendor's files; any
    +// package but a bundle may also add files to SYSTEM (a DOS program run at
    +// the prompt, say), but not to its KEYS and PACKAGES folders (the keys
    +// DVX trusts, the package records), and a file there that another
    +// package has is refused as anywhere (pkgPlan.h); only `system` (DVX
    +// itself, kpunch.dvx) may write anywhere in the program tree, and nothing
    +// writes the user folder.  Lines end in CR LF as
     // written by pkgManifestFormat; a reader takes LF too.  Keys this DVX does
     // not know are ignored; a `format` above PKG_MANIFEST_FORMAT is refused
     // (it needs a newer DVX).
    @@ -59,6 +63,12 @@
     #include 
     #include 
     
    +// SYSTEM's folders that only DVX itself (kind system) puts files in: the
    +// package records (pkgDb.h) and the keys DVX trusts (pkgTrust.h; the
    +// user folder has a KEYS folder too)
    +#define PKG_DB_DIR          "PACKAGES"
    +#define PKG_KEYS_DIR        "KEYS"
    +
     #define PKG_MANIFEST_NAME   "PACKAGE.INI"
     #define PKG_MANIFEST_FORMAT 1
     #define PKG_EXT             ".DPK"
    @@ -69,6 +79,7 @@
     #define PKG_VERSION_MAX     18                              // "65535.65535.65535" and NUL
     #define PKG_SHA256_SIZE     32
     #define PKG_SYSTEM_ID       "kpunch.dvx"                    // the one `system` package: DVX itself
    +#define PKG_BASE_FONTS_ID   "kpunch.basefont"               // DVX's base set of fonts (src/fonts/PACKAGE.INI)
     #define PKG_DVX_NAME        "DVX"                           // its manifest (pkgDvxManifest)
     #define PKG_DVX_SUMMARY     "The DVX desktop and its programs."
     #define PKG_DVX_PUBLISHER   "Kangaroo Punch Studios"
    diff --git a/src/libs/kpunch/pkg/pkgTrust.h b/src/libs/kpunch/pkg/pkgTrust.h
    index 3e7465c4..17d70c14 100644
    --- a/src/libs/kpunch/pkg/pkgTrust.h
    +++ b/src/libs/kpunch/pkg/pkgTrust.h
    @@ -44,7 +44,6 @@
     #include 
     #include 
     
    -#define PKG_KEYS_DIR    "KEYS"          // in SYSTEM and in the user folder
     #define PKG_DVX_VENDOR  "kpunch"        // DVX's own packages' vendor
     
     typedef enum {
    diff --git a/src/libs/kpunch/print/print.dhs b/src/libs/kpunch/print/print.dhs
    index dd7ceaf9..7442bff7 100644
    --- a/src/libs/kpunch/print/print.dhs
    +++ b/src/libs/kpunch/print/print.dhs
    @@ -35,7 +35,7 @@
     .index printModelPage
     .index printFontOpen
     .index PrintFontT
    -.index Printer Fonts
    +.index Print sizes
     .index PrintDocT
     .index printVecBegin
     .index Vector Pages
    @@ -182,9 +182,9 @@ A font opened for a vector page (printFontOpen with page.vector set) is the prin
     
     .h2 Text on a Page
     
    -A page a program draws can carry text in any font family (dvxText.h) at a size in points. printFontOpen(&page, family, decipoints, style) gives a PrintFontT for a page's resolution (its PrintPageInfoT): the size is in tenths of a point (120 for 12 points), the style any FONT_STYLE_ bits, underline and strike-through included. The font is the family's smallest strike at least as large as the size in printer dots (12 points is 50 dots at 300 dpi, 30 at 180), else its largest, scaled to the size across and down apart, since a 9-pin printer's dots are not square. Scaling is nearest pixel; scaling down, every pixel row and column of the strike still marks a dot, so thin strokes stay.
    +A page a program draws can carry text in any font family (dvxText.h) at a size in points. printFontOpen(&page, family, decipoints, style) gives a PrintFontT for a page's resolution (its PrintPageInfoT): the size is in tenths of a point (120 for 12 points), the style any FONT_STYLE_ bits, underline and strike-through included. The font is the family's smallest strike at least as large as the size in printer dots (12 points is 50 dots at 300 dpi, 30 at 180), else its largest, scaled to the size across and down apart, since a 9-pin printer's dots are not square. Scaled down (or up one way only), a strike pixel becomes the dots it covers, and every pixel row and column of the strike still marks a dot, so thin strokes stay. Scaled up both ways, each dot is sampled between the four strike pixels around its centre and drawn when at least half covered, so edges come out smooth rather than in steps.
     
    -The base set's sizes go up to 24 pixels, so most printed sizes are scaled up from it and look blocky at 300 dpi. The Printer Fonts add-on (kpunch.prfonts) gives DejaVu Sans, Serif, and Sans Mono in every style at 25 to 100 pixels, about 6 to 24 points at 300 dpi and 10 to 40 at 180: installed, those families print crisp at those sizes with no change to the program.
    +The base set's files carry print sizes beside their screen sizes (mkfont's print key; see Making Fonts): DejaVu Sans, Serif, and Sans Mono in every style at 25 to 100 pixels, about 6 to 24 points at 300 dpi and 10 to 40 at 180, so those families print crisp at those sizes. Other families, and larger text, are scaled up from their largest size.
     
     .table
       Function                                                    Description
    diff --git a/src/libs/kpunch/print/print.h b/src/libs/kpunch/print/print.h
    index 7b32bdc6..96abefe9 100644
    --- a/src/libs/kpunch/print/print.h
    +++ b/src/libs/kpunch/print/print.h
    @@ -448,8 +448,9 @@ typedef void (*PrintSpanFnT)(void *ctx, int32_t x0, int32_t x1, int32_t y);
     // tenths of a point in style (FONT_STYLE_*, lines too) on page (its dots
     // per inch: printModelPage or printDocInfo), and given up.  The family's
     // smallest strike at least that many dots is scaled to it, else its
    -// largest: text is crisp where the family has large sizes (the
    -// printer-fonts add-on) and blockier where it is scaled up.  On a vector
    +// largest: text is crisp where the family has print sizes (the base
    +// set's: mkfont's print key) and softer where it is scaled up (smoothed:
    +// printFont.c).  On a vector
     // page (page->vector) the font is instead the printer's own face nearest
     // the family (Courier for a fixed-pitch one, Times for a serif one, else
     // Helvetica), which measures as it prints and is drawn with printVecText
    diff --git a/src/libs/kpunch/print/printFont.c b/src/libs/kpunch/print/printFont.c
    index 3fdf1e66..adf7afbb 100644
    --- a/src/libs/kpunch/print/printFont.c
    +++ b/src/libs/kpunch/print/printFont.c
    @@ -27,10 +27,14 @@
     // a page's resolution, across and down apart (a 9-pin printer's dots are
     // not square).  The strike scaled is the family's smallest at least as
     // large as the size in dots, else its largest, so text is scaled down or
    -// as little up as the family allows (the printer-fonts add-on gives the
    -// DejaVu families sizes up to 24 points at 300 dpi).  Scaling is nearest
    -// pixel: a strike pixel becomes the dots it covers, and scaling down
    -// every strike row and column still marks a dot, so thin strokes stay.
    +// as little up as the family allows (the base set's DejaVu faces carry
    +// print sizes up to 100 pixels, 24 points at 300 dpi: mkfont's print
    +// key).  Scaled down (or up one way only), a strike pixel becomes the
    +// dots it covers, and every strike row and column still marks a dot, so
    +// thin strokes stay.  Scaled up both ways (a face without print sizes,
    +// the system font, text larger than the largest size), each dot is
    +// sampled between the four strike pixels around its centre (bilinear,
    +// ink from half covered), so edges come out smooth instead of in steps.
     //
     // Text goes out as spans (runs of dots on a row) to a PrintSpanFnT;
     // printFontDraw paints them into an RGB band.  Positions are worked out
    @@ -50,12 +54,13 @@
     
     #define POINTS_PER_INCH     72
     #define DECIPOINTS_PER_INCH (POINTS_PER_INCH * 10)
    -#define FONT_SIZES_MAX      64          // sizes a family may list (dvxFontSizes)
     #define RGB_BYTES           3
     #define RGB_RED_SHIFT       16
     #define RGB_GREEN_SHIFT     8
     #define RGB_MASK            0xFF
     #define ROUND_HALF          0.5
    +#define FIX_ONE             65536       // glyphSmoothSpans: strike positions in 16.16 fixed point
    +#define FIX_HALF            (FIX_ONE / 2)
     
     // One glyph as a bitmap: the system font's cells and a loaded font's
     // glyphs alike
    @@ -83,8 +88,11 @@ typedef struct {
     // ============================================================
     
     static void    bandSpan(void *ctx, int32_t x0, int32_t x1, int32_t y);
    +static int64_t fixFloor(int64_t v);
     static bool    glyphInk(const GlyphT *g, const uint8_t *row, int32_t col);
    +static int32_t glyphLevel(const GlyphT *g, int32_t col, int32_t row);
     static bool    glyphOf(const BitmapFontT *font, uint8_t ch, GlyphT *g);
    +static void    glyphSmoothSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx);
     static void    glyphSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx);
     static void    lineSpans(const PrintFontT *pf, int32_t x, int32_t y, int32_t row, int32_t thick, int32_t width, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx);
     int32_t        printFontAscent(const PrintFontT *font);
    @@ -112,6 +120,12 @@ static void bandSpan(void *ctx, int32_t x0, int32_t x1, int32_t y) {
     }
     
     
    +// The whole strike pixels at or below v (16.16), as 16.16.
    +static int64_t fixFloor(int64_t v) {
    +    return (v >= 0) ? v & ~(FIX_ONE - 1) : -((-v + FIX_ONE - 1) & ~(FIX_ONE - 1));
    +}
    +
    +
     // Whether the glyph pixel at col of a row is ink (a smoothed one from
     // half covered).
     static bool glyphInk(const GlyphT *g, const uint8_t *row, int32_t col) {
    @@ -123,6 +137,23 @@ static bool glyphInk(const GlyphT *g, const uint8_t *row, int32_t col) {
     }
     
     
    +// How much ink the glyph pixel at col, row holds, 0 .. FONT_GRAY_FULL (a
    +// 1-bit glyph's ink is full); 0 outside the glyph.
    +static int32_t glyphLevel(const GlyphT *g, int32_t col, int32_t row) {
    +    if (col < 0 || row < 0 || col >= g->width || row >= g->height) {
    +        return 0;
    +    }
    +
    +    const uint8_t *bits = g->bits + row * g->rowBytes;
    +
    +    if (g->gray) {
    +        return FONT_GRAY_LEVEL(bits, col);
    +    }
    +
    +    return (bits[FONT_PIXEL_BYTE(col)] & FONT_PIXEL_BIT(col)) ? FONT_GRAY_FULL : 0;
    +}
    +
    +
     // Character ch of font as a bitmap; false when the font has no glyph for
     // it (outside its range).
     static bool glyphOf(const BitmapFontT *font, uint8_t ch, GlyphT *g) {
    @@ -158,6 +189,55 @@ static bool glyphOf(const BitmapFontT *font, uint8_t ch, GlyphT *g) {
     }
     
     
    +// A glyph scaled up both ways, as glyphSpans: each dot's centre taken
    +// back to the strike, its ink the four pixels around it weighed by
    +// nearness (bilinear), a dot at least half covered drawn.  The strike
    +// position of a row is worked out once; across, it steps by the strike
    +// pixels a dot covers.
    +static void glyphSmoothSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx) {
    +    int32_t rowTop  = pf->font->ascent - g->top;
    +    int32_t colLeft = pen + g->left;
    +    int32_t y0      = y + scaleY(pf, rowTop) - 1;
    +    int32_t y1      = y + scaleY(pf, rowTop + g->height) + 1;
    +    int32_t x0      = x + scaleX(pf, colLeft) - 1;
    +    int32_t x1      = x + scaleX(pf, colLeft + g->width) + 1;
    +    int64_t stepX   = (int64_t)pf->size * FIX_ONE / pf->dotsX;
    +    int64_t startU  = (int64_t)(2 * (x0 - x) + 1) * pf->size * FIX_ONE / (2 * pf->dotsX) - (int64_t)colLeft * FIX_ONE - FIX_HALF;
    +    int64_t full    = (int64_t)FONT_GRAY_FULL * FIX_ONE * FIX_ONE;
    +
    +    y0 = y0 < top ? top : y0;
    +    y1 = y1 > bottom ? bottom : y1;
    +
    +    for (int32_t dy = y0; dy <= y1; dy++) {
    +        int64_t v     = (int64_t)(2 * (dy - y) + 1) * pf->size * FIX_ONE / (2 * pf->dotsY) - (int64_t)rowTop * FIX_ONE - FIX_HALF;
    +        int64_t vBase = fixFloor(v);
    +        int32_t r     = (int32_t)(vBase / FIX_ONE);
    +        int64_t fy    = v - vBase;
    +        int64_t u     = startU;
    +        int32_t run   = -1;
    +
    +        for (int32_t dx = x0; dx <= x1; dx++, u += stepX) {
    +            int64_t uBase = fixFloor(u);
    +            int32_t c     = (int32_t)(uBase / FIX_ONE);
    +            int64_t fx    = u - uBase;
    +            int64_t ink   = (FIX_ONE - fx) * (FIX_ONE - fy) * glyphLevel(g, c, r) + fx * (FIX_ONE - fy) * glyphLevel(g, c + 1, r) + (FIX_ONE - fx) * fy * glyphLevel(g, c, r + 1) + fx * fy * glyphLevel(g, c + 1, r + 1);
    +            bool    on    = 2 * ink >= full;
    +
    +            if (on && run < 0) {
    +                run = dx;
    +            } else if (!on && run >= 0) {
    +                fn(ctx, run, dx - 1, dy);
    +                run = -1;
    +            }
    +        }
    +
    +        if (run >= 0) {
    +            fn(ctx, run, x1, dy);
    +        }
    +    }
    +}
    +
    +
     // A glyph's dots, its pen pen strike pixels from the text's start at x,
     // the top of its line at y: each run of ink on a strike row, scaled.
     static void glyphSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx) {
    @@ -311,7 +391,8 @@ PrintFontT *printFontOpen(const PrintPageInfoT *page, const char *family, int32_
             return NULL;
         }
     
    -    pf->size = pf->font->pixelSize > 0 ? pf->font->pixelSize : pick;
    +    pf->size   = pf->font->pixelSize > 0 ? pf->font->pixelSize : pick;
    +    pf->smooth = pf->dotsX > pf->size && pf->dotsY > pf->size;
         return pf;
     }
     
    @@ -334,7 +415,12 @@ void printFontSpans(const PrintFontT *font, int32_t x, int32_t y, const char *te
                 continue;
             }
     
    -        glyphSpans(font, &g, x, y, pen, top, bottom, fn, ctx);
    +        if (font->smooth) {
    +            glyphSmoothSpans(font, &g, x, y, pen, top, bottom, fn, ctx);
    +        } else {
    +            glyphSpans(font, &g, x, y, pen, top, bottom, fn, ctx);
    +        }
    +
             pen += g.advance;
         }
     
    diff --git a/src/libs/kpunch/print/printInt.h b/src/libs/kpunch/print/printInt.h
    index 3e3f8931..7293a829 100644
    --- a/src/libs/kpunch/print/printInt.h
    +++ b/src/libs/kpunch/print/printInt.h
    @@ -141,6 +141,7 @@ struct PrintFontS {
         int32_t      dotsY;                 // ... and down
         int32_t      psFace;                // printPsFaces index of a printer's face, else -1
         uint32_t     style;                 // FONT_STYLE_* (a printer's face draws the lines itself)
    +    bool         smooth;                // scaled up both ways: each dot sampled between strike pixels
         double       psSizeX;               // a printer's face: the size in dots across (not rounded)
         double       psSizeY;               // ... and down
     };
    diff --git a/src/packages.ini b/src/packages.ini
    new file mode 100644
    index 00000000..0616bed4
    --- /dev/null
    +++ b/src/packages.ini
    @@ -0,0 +1,185 @@
    +; packages.ini -- how DVX's release is split into packages.
    +;
    +; DVX itself (kpunch.dvx: the loader, the shell, the core libraries and
    +; widgets, the everyday programs, SYSTEM, and SHARED) is everything this
    +; file does not give to another package.  The base fonts are kpunch.basefont
    +; (src/fonts/PACKAGE.INI) and the CD's add-ons are in src/addons.
    +;
    +; The optional parts are bundles (kind = bundle: no files, only the
    +; packages they hold), each a feature Setup offers on its "What to install"
    +; page and Software lists: members = the packages it holds.  needs = other
    +; bundles it cannot work without (installing it installs them; removing
    +; one of them removes it).
    +;
    +; A member is one module (the package format allows one program, library,
    +; or widget per code package, in a folder of its own: pkgManifest.c
    +; pkgFileAllowed): kind = app, library, or widget; its id's name is its
    +; folder's (APPS\KPUNCH\NAME, LIBS\KPUNCH\NAME, WIDGETS\KPUNCH\NAME).
    +; paths = its files outside that folder, as the release has them (any
    +; package but a bundle may add files to SYSTEM: a DOS program run at the
    +; prompt goes with the part it serves).  What a member needs is worked
    +; out by the build (src/installer/
    +; mkPackages.py): the modules its code imports, the libraries a BASIC
    +; program declares (its project's include files), and uses = the members
    +; it reaches by name at run time, which neither shows.
    +;
    +; Every package takes DVX's version.
    +;
    +; [kpunch.dvx] names DVX's own programs that use an optional part only when
    +; it is installed, and work without it: program folder = the packages.
    +
    +[packages]
    +kpunch.basic    = bundle
    +kpunch.data     = bundle
    +kpunch.net      = bundle
    +kpunch.comm     = bundle
    +kpunch.media    = bundle
    +kpunch.devtools = bundle
    +kpunch.demos    = bundle
    +
    +[kpunch.dvx]
    +software = kpunch.dvxnet kpunch.pkgnet
    +
    +; ---- Bundles ----
    +
    +[kpunch.basic]
    +name    = DVX BASIC
    +summary = Write your own programs: the BASIC editor, compiler, and debugger, with its guides.
    +members = kpunch.dvxbasic kpunch.basdev
    +
    +[kpunch.data]
    +name    = Databases
    +summary = SQLite databases for BASIC programs, with the Data control and the DBGrid that show them.
    +members = kpunch.dvxsql kpunch.datactrl kpunch.dbgrid
    +
    +[kpunch.net]
    +name    = Networking
    +summary = Network cards (with their DOS packet driver), the web, and the package server.
    +members = kpunch.dvxnet kpunch.http kpunch.pkgnet
    +
    +[kpunch.comm]
    +name    = Communications
    +summary = The Terminal: modems, serial and secure links, Telnet, and file transfers.
    +members = kpunch.terminal kpunch.conn kpunch.xfer
    +needs   = kpunch.net
    +
    +[kpunch.media]
    +name    = Media
    +summary = The Media Player and the Sound Recorder.
    +members = kpunch.player kpunch.recorder
    +
    +[kpunch.devtools]
    +name    = Developer Tools
    +summary = The Icon, Resource, and Help Editors, Package Builder, and Widget Showcase, with DVXRES and DVXHLPC for the DOS prompt.
    +members = kpunch.iconed kpunch.resedit kpunch.helpedit kpunch.pkgbuild kpunch.widshow
    +
    +[kpunch.demos]
    +name    = Demos
    +summary = The DVX Demo and the BASIC Demo.
    +members = kpunch.dvxdemo kpunch.basdemo
    +
    +; ---- Members ----
    +
    +[kpunch.dvxbasic]
    +kind    = app
    +name    = DVX BASIC IDE
    +summary = The BASIC editor, form designer, and debugger.
    +
    +[kpunch.basdev]
    +kind    = library
    +name    = BASIC Compiler
    +summary = The BASIC compiler the IDE builds programs with, and BASCOMP for the DOS prompt.
    +paths   = system/BASCOMP.EXE
    +
    +[kpunch.dvxsql]
    +kind    = library
    +name    = SQLite
    +summary = SQLite databases for BASIC programs.
    +
    +[kpunch.datactrl]
    +kind    = widget
    +name    = Data Control
    +summary = The Data control: a table or query of a database, record by record.
    +
    +[kpunch.dbgrid]
    +kind    = widget
    +name    = DBGrid
    +summary = The DBGrid control: a Data control's records as a grid.
    +uses    = kpunch.datactrl
    +
    +[kpunch.dvxnet]
    +kind    = library
    +name    = Network
    +summary = TCP/IP over a network card's DOS packet driver.
    +
    +[kpunch.http]
    +kind    = library
    +name    = HTTP
    +summary = Web requests over the network.
    +
    +[kpunch.pkgnet]
    +kind    = library
    +name    = Package Server
    +summary = Software's link to the package server: updates and new software.
    +
    +[kpunch.terminal]
    +kind    = app
    +name    = Terminal
    +summary = Modems, serial and secure links, Telnet, and file transfers.
    +
    +[kpunch.conn]
    +kind    = library
    +name    = Connections
    +summary = Serial, modem, secure-link, and Telnet connections for programs.
    +
    +[kpunch.xfer]
    +kind    = library
    +name    = File Transfer
    +summary = XMODEM, YMODEM, and ZMODEM file transfers.
    +
    +[kpunch.player]
    +kind    = app
    +name    = Media Player
    +summary = Plays music modules, MIDI files, and WAV and VOC sounds.
    +
    +[kpunch.recorder]
    +kind    = app
    +name    = Sound Recorder
    +summary = Records and plays sounds and MIDI.
    +
    +[kpunch.iconed]
    +kind    = app
    +name    = Icon Editor
    +summary = Draws icons and pictures for programs.
    +
    +[kpunch.resedit]
    +kind    = app
    +name    = Resource Editor
    +summary = Looks at and changes the resources in DVX programs and files, also with DVXRES at the DOS prompt.
    +paths   = system/DVXRES.EXE
    +
    +[kpunch.helpedit]
    +kind    = app
    +name    = Help Editor
    +summary = Writes help files: edits help source, compiles it, and shows it in the Help Viewer; DVXHLPC compiles at the DOS prompt.
    +paths   = system/DVXHLPC.EXE
    +
    +[kpunch.pkgbuild]
    +kind    = app
    +name    = Package Builder
    +summary = Makes add-on packages of programs, fonts, and more.
    +
    +[kpunch.widshow]
    +kind    = app
    +name    = Widget Showcase
    +summary = Every kind of control at work.
    +
    +[kpunch.dvxdemo]
    +kind    = app
    +name    = DVX Demo
    +summary = A tour of what DVX's windows and controls can do.
    +
    +[kpunch.basdemo]
    +kind    = app
    +name    = BASIC Demo
    +summary = What DVX BASIC programs can do, in one program.
    diff --git a/src/sdk/README.TXT b/src/sdk/README.TXT
    index 23f46a6c..0be67119 100644
    --- a/src/sdk/README.TXT
    +++ b/src/sdk/README.TXT
    @@ -97,8 +97,8 @@ Requirements
         ../../toolchains/djgpp from this directory: inside a DVX source tree
         that is the toolchain ./installToolchain.sh sets up.
       - A Linux host for the tools in bin/.  On DVX itself the same tools are
    -    SYSTEM/DVXRES.EXE, SYSTEM/DVXHLPC.EXE, SYSTEM/BASCOMP.EXE, and
    -    SYSTEM/DVXPKG.EXE.
    +    SYSTEM/DVXRES.EXE and SYSTEM/DVXHLPC.EXE (with Developer Tools),
    +    SYSTEM/BASCOMP.EXE (with DVX BASIC), and SYSTEM/DVXPKG.EXE.
     
     Deployment
     ----------
    diff --git a/src/test/abi/abiSizes.txt b/src/test/abi/abiSizes.txt
    index 2d926e78..da6e363c 100644
    --- a/src/test/abi/abiSizes.txt
    +++ b/src/test/abi/abiSizes.txt
    @@ -95,7 +95,7 @@ PkgDependT            24
     PkgDpkFileT           8
     PkgDpkInfoT           904
     PkgFileT              40
    -PkgHeaderT            68
    +PkgHeaderT            52
     PkgInstalledT         832
     PkgKeyT               104
     PkgKeyringT           8
    diff --git a/src/test/coverage/__pycache__/covReport.cpython-312.pyc b/src/test/coverage/__pycache__/covReport.cpython-312.pyc
    deleted file mode 100644
    index 3bf3314e..00000000
    Binary files a/src/test/coverage/__pycache__/covReport.cpython-312.pyc and /dev/null differ
    diff --git a/src/test/dos/__pycache__/audioCmp.cpython-312.pyc b/src/test/dos/__pycache__/audioCmp.cpython-312.pyc
    deleted file mode 100644
    index 41ae142d..00000000
    Binary files a/src/test/dos/__pycache__/audioCmp.cpython-312.pyc and /dev/null differ
    diff --git a/src/test/dos/__pycache__/joyDrive.cpython-312.pyc b/src/test/dos/__pycache__/joyDrive.cpython-312.pyc
    deleted file mode 100644
    index ac1843e9..00000000
    Binary files a/src/test/dos/__pycache__/joyDrive.cpython-312.pyc and /dev/null differ
    diff --git a/src/test/dos/golden/prdjcol/prdjcol.prn b/src/test/dos/golden/prdjcol/prdjcol.prn
    index ec4bf0cf..e6cdb911 100644
    Binary files a/src/test/dos/golden/prdjcol/prdjcol.prn and b/src/test/dos/golden/prdjcol/prdjcol.prn differ
    diff --git a/src/test/dos/golden/prlj300/prlj300.prn b/src/test/dos/golden/prlj300/prlj300.prn
    index 9cac0acd..b703b4c9 100644
    Binary files a/src/test/dos/golden/prlj300/prlj300.prn and b/src/test/dos/golden/prlj300/prlj300.prn differ
    diff --git a/src/test/dos/runDos.sh b/src/test/dos/runDos.sh
    index 1db2c41a..2ded8de5 100755
    --- a/src/test/dos/runDos.sh
    +++ b/src/test/dos/runDos.sh
    @@ -86,7 +86,9 @@ FIXTURES_DIR="${HERE}/fixtures"
     MKMEDIA_PY="${HERE}/mkMedia.py"   # Media Player's test files (stageTree)
     NVR_DIR="${HERE}/nvr"
     IMGCMP="${DVX_ROOT}/obj/test/dos/imgCmp"
    -BIN_DIR="${DVX_ROOT}/bin"
    +# The build to run: bin/, or the folder DVX_TEST_BIN names (a cut-down
    +# copy, say, to measure what some modules cost)
    +BIN_DIR="${DVX_TEST_BIN:-${DVX_ROOT}/bin}"
     # Every script of a run is staged from this private copy of bin/, taken
     # once the build is done (see takeSnapshot), never from bin/ itself.
     SNAPSHOT_DIR="${DVX_ROOT}/obj/test/dos/bin.$$"
    @@ -199,6 +201,9 @@ PKTDRV_ARGS="0x60 3 0x300"    # software interrupt, IRQ, I/O base
     NET_PID=""
     NIC_TEXT=""
     EXPECTS=()
    +FILE_EXPECTS=()
    +NOFILE_EXPECTS=()
    +PRESENT_LIST="present.lst"     # out/: the FILE / NOFILE paths the run left
     KEEP_WELCOME=0
     KEEP_SOUNDS=0
     SCRIPT_VIDEO_W=""
    @@ -292,7 +297,10 @@ DOSBOX_MAPPER_LINE=""
     # get a staged C:\DVX (with "upgrade": it does, and Setup installs over
     # it).  `make dist` (src/installer/mkDist.sh, from the bin snapshot)
     # builds the release media into /dist and Setup runs before DVX.EXE
    -# with /AUTO=C:\DVX: the script then drives the installed copy.
    +# with /AUTO=C:\DVX: the script then drives the installed copy.  The
    +# media's add-ons are signed with a key made for the run (MEDIA.KEY, in
    +# the package's SYSTEM\KEYS while it is built), so Setup offers them as
    +# it does a release's; "# SETUPARGS: /ADDONS=..." chooses them.
     #   cd      86Box: DVX.ISO in an ATAPI CD-ROM drive (primary slave),
     #           FreeDOS loads HIMEMX.EXE and UDVD2.SYS (FDCONFIG.SYS; UDVD2
     #           needs XMS) and SHSUCDX (drive D:), then D:\SETUP.EXE.  DOSBox:
    @@ -309,18 +317,27 @@ DOSBOX_MAPPER_LINE=""
     # /SHOTS=C:\SUSHOTS: it shows each of its pages and questions (upgrade,
     # disk prompts) and saves them there (SU-*.BMP); those pictures are copied
     # out with the script's own and compared with the goldens the same way
    -# (screenshots//