diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cbced30 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +### +## Hacky makefile which does too much recompilation. +## +## "go build" / "go install" will do the right thing, unless +## you're changing the CCP or modifying the static binaries we +## added. +## +### + + +ALL: ccp static cpmulator + + +# +# CCP is fast to build. +# +.PHONY: ccp +ccp: $(wildcard ccp/*.ASM) + cd ccp && make + +# +# Static helpers are fast to build. +# +.PHONY: static +static: $(wildcard ccp/*.z80) + cd static && make + + +# +# cpmulator is fast to build. +# +cpmulator: $(wildcard *.go */*.go) + go build . diff --git a/README.md b/README.md index f606e33..f5e086e 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ This emulator is written using golang, so if you have a working golang toolchain go install github.com/skx/cpmulator@latest ``` -If you were to clone this repository to your local system you could then build and install by running: +If you were to clone this repository to your local system you could build, and install it, by running: ``` go build . @@ -104,8 +104,7 @@ You can terminate the CCP by typing `EXIT`. The following built-in commands are * `CLS` * Clear the screen. * `DIR` - * List files, by default this uses "`*.*`". - * Try "`DIR *.COM`" if you want to see something more specific, for example. + * Try "`DIR *.COM`" if you want to see only executables, for example. * `EXIT` / `HALT` / `QUIT` * Terminate the CCP. * `ERA` @@ -122,7 +121,7 @@ There are currently a pair of CCP implementations included within the emulator, * "ccp" * This is the default, but you can choose it explicitly via `cpmulator -ccp=ccp ..`. - * The original/default one, from Digital Research + * The original/default one, from Digital Research. * "ccpz" * Launch this via `cpmulate -ccp=ccpz ..` * An enhanced one with extra built-in commands. @@ -136,7 +135,11 @@ You can also launch a binary directly by specifying it's path upon the command-l $ cpmulator /path/to/binary [optional-args] ``` -Other options are shown in the output of `cpmulator -help`, but in brief: + + +## Command Line Flags + +There are several command-line options which are shown in the output of `cpmulator -help`, but in brief: * `-cd /path/to/directory` * Change to the given directory before running. @@ -151,7 +154,9 @@ Other options are shown in the output of `cpmulator -help`, but in brief: * `-syscalls` * Dump the list of implemented BDOS and BIOS syscalls. * `-version` - * Show our version number. + * Show the version number of the emulator, and exit. + +Note that some of these options can be changed at runtime. @@ -161,7 +166,7 @@ When the CCP is launched for interactive execution, we allow commands to be exec * If `SUBMIT.COM` **and** `AUTOEXEC.SUB` exist on A: * Then the contents of `AUTOEXEC.SUB` will be executed. -* We secretly run "`SUBMIT AUTOEXEC`" to achieve this. + * We secretly run "`SUBMIT AUTOEXEC`" to achieve this. This allows you to customize the emulator, or perform other "one-time" setup via the options described in the next section. @@ -171,24 +176,26 @@ This allows you to customize the emulator, or perform other "one-time" setup via There are a small number of [extensions](EXTENSIONS.md) added to the BIOS functionality we provide, and these extensions allow changing the behaviour of the emulator at runtime. +The behaviour changing is achieved by having a small number of .COM files invoke the extension functions, and these binaries are embedded within our emulator to improve ease of use, via the [static/](static/) directory in our source-tree - This means no matter what you'll always find some binaries installed on A:, despite not being present in reality. + ### CCP Handling -We default to loading the Digital Research CCP, but allow the CCPZ to be selected via the `-ccp` command-line flag. The binary `samples/ccp.com` lets you change CCP at runtime. +We default to loading the Digital Research CCP, but allow the CCPZ to be selected via the `-ccp` command-line flag. The binary `A:CCP.COM` lets you change CCP at runtime. ### Ctrl-C Handling Traditionally pressing `Ctrl-C` would reload the CCP, via a soft boot. I think that combination is likely to be entered by accident, so in `cpmulator` we default to requiring you to press Ctrl-C _twice_ in a row to reboot the CCP. -I've added a binary `samples/ctrlc.com` which lets you change this at runtime. Run `ctrlc 0` to disable the Ctrl-C behaviour, or `ctrlc N` to require N consecutive Ctrl-C keystrokes to trigger the restart-behaviour (max: 9). +The binary `A:CTRLC.COM` which lets you change this at runtime. Run `A:CTRLC 0` to disable the Ctrl-C behaviour, or `A:CTRLC N` to require N consecutive Ctrl-C keystrokes to trigger the restart-behaviour (max: 9). ### Console Output -We default to pretending our output device is an ADM-3A terminal, this can be changed via the `-console` command-line flag at startup. Additionally it can be changed at runtime via the `samples/console.com` command. +We default to pretending our output device is an ADM-3A terminal, this can be changed via the `-console` command-line flag at startup. Additionally it can be changed at runtime via `A:CONSOLE.COM`. -Run `console ansi` to disable the output emulation, or `console adm-3a` to restore it. +Run `A:CONSOLE ansi` to disable the output emulation, or `A:CONSOLE adm-3a` to restore it. You'll see that the [cpm-dist](https://github.com/skx/cpm-dist) repository contains a version of Wordstar, and that behaves differently depending on the selected output handler. Changing the handler at run-time is a neat bit of behaviour. @@ -197,7 +204,7 @@ You'll see that the [cpm-dist](https://github.com/skx/cpm-dist) repository conta When CCP is soft/warm-booted it prints a banner showing the currently active CCP, and the console-output device which is in-use. -Running `samples/quiet.com` will silence this output, essentially enabling "quiet mode". +Running `A:QUIET 1` will silence this output, essentially enabling "quiet mode", running with no arguments will show the current state, and running `A:QUIET 0` will restore the default behaviour. @@ -342,36 +349,23 @@ For reference the memory map of our CP/M looks like this: -# Sample Programs - -You'll see some Z80 assembly programs beneath [samples](samples/) which are used to check my understanding. If you have the `pasmo` compiler enabled you can build them all by running "make", in case you don't I've also committed the generated binaries. - - +# Credits and References -# Credits - * Much of the functionality of this repository comes from the [excellent Z80 emulator library](https://github.com/koron-go/z80) it is using, written by [@koron-go](https://github.com/koron-go). -* The CCP comes from [my fork](https://github.com/skx/z80-playground-cpm-fat/) of the original [cpm-fat](https://github.com/z80playground/cpm-fat/) +* The default CCP comes from [my fork](https://github.com/skx/z80-playground-cpm-fat/) of the original [cpm-fat](https://github.com/z80playground/cpm-fat/) * However this is largely unchanged from the [original CCP](http://www.cpm.z80.de/source.html) from Digital Research, although I did add the `CLS`, `EXIT`, `HALT` & `QUIT` built-in commands. - -When I was uncertain of how to implement a specific system call the following two emulators were also useful: - -* [https://github.com/ivanizag/iz-cpm](https://github.com/ivanizag/iz-cpm) - * Portable CP/M emulation to run CP/M 2.2 binaries for Z80. +* Reference Documentation + * [CP/M BDOS function reference](https://www.seasip.info/Cpm/bdos.html). + * [CP/M BIOS function reference](https://www.seasip.info/Cpm/bios.html). +* Other emulators which were useful resources when some functionality was unclear: + * [https://github.com/ivanizag/iz-cpm](https://github.com/ivanizag/iz-cpm) + * Portable CP/M emulation to run CP/M 2.2 binaries for Z80. * Has a handy "download" script to fetch some CP/M binaries, including BASIC, Turbo Pascal, and WordStar. - * Written in Rust. -* [https://github.com/jhallen/cpm](https://github.com/jhallen/cpm) - * Run CP/M commands in Linux/Cygwin with this Z80 / BDOS / ADM-3A emulator. - * Written in C. - - - - -# References - -* [Digital Research - CP/M Operating System Manual](http://www.gaby.de/cpm/manuals/archive/cpm22htm/) - * Particularly the syscall reference in [Section 5: CP/M 2 System Interface](http://www.gaby.de/cpm/manuals/archive/cpm22htm/ch5.htm). + * Written in Rust. + * [https://github.com/jhallen/cpm](https://github.com/jhallen/cpm) + * Run CP/M commands in Linux/Cygwin with this Z80 / BDOS / ADM-3A emulator. + * Written in C. @@ -394,8 +388,6 @@ The testing that I should do before a release: * [ ] Test BE.COM * [ ] Test STAT.COM * [ ] Test some built-in shell-commands; ERA, TYPE, and EXIT. -* [ ] Test `samples/INTEST.COM` `samples/READ.COM`, `samples/WRITE.COM`. - * These demonstrate core primitives are not broken. diff --git a/cpm/cpm.go b/cpm/cpm.go index d9eb1be..286b730 100644 --- a/cpm/cpm.go +++ b/cpm/cpm.go @@ -8,6 +8,7 @@ package cpm import ( "context" + "embed" "errors" "fmt" "log/slog" @@ -108,6 +109,10 @@ type CPM struct { // files is the cache we use for File handles. files map[uint16]FileCache + // virtual contains a reference to a static filesystem which + // is embedded within our binary, if any. + static embed.FS + // input is our interface for reading from the console. // // This needs to take account of echo/no-echo status. @@ -841,6 +846,11 @@ func (cpm *CPM) RunAutoExec() { cpm.input.StuffInput("SUBMIT AUTOEXEC") } +// SetStaticFilesystem allows adding a reference to an embedded filesyste,. +func (cpm *CPM) SetStaticFilesystem(fs embed.FS) { + cpm.static = fs +} + // SetDrives enables/disables the use of subdirectories upon the host system // to represent CP/M drives. // diff --git a/cpm/cpm_bdos.go b/cpm/cpm_bdos.go index ab5429d..e07bba8 100644 --- a/cpm/cpm_bdos.go +++ b/cpm/cpm_bdos.go @@ -9,9 +9,11 @@ package cpm import ( "fmt" "io" + "io/fs" "log/slog" "os" "path/filepath" + "sort" "strings" "github.com/skx/cpmulator/consolein" @@ -381,7 +383,37 @@ func SysCallFileOpen(cpm *CPM) error { // Ensure the filename is qualified fileName = filepath.Join(path, fileName) - // Now we open.. + // Remapped file + x := filepath.Base(fileName) + x = filepath.Join(string(cpm.currentDrive+'A'), x) + + // Can we open this file from our embedded filesystem? + virt, er := cpm.static.ReadFile(x) + if er == nil { + + // Yes we can! + // Save the file handle in our cache. + cpm.files[ptr] = FileCache{name: fileName, handle: nil} + + // Get file size, in blocks + fLen := uint8(len(virt) / blkSize) + + // Set record-count + if fLen > maxRC { + fcbPtr.RC = maxRC + } else { + fcbPtr.RC = fLen + } + + // Update the FCB in memory. + cpm.Memory.SetRange(ptr, fcbPtr.AsBytes()...) + + // Return success + cpm.CPU.States.AF.Hi = 0x00 + return nil + } + + // Now we open from the filesystem file, err := os.OpenFile(fileName, os.O_RDWR, 0644) if err != nil { @@ -464,6 +496,13 @@ func SysCallFileClose(cpm *CPM) error { return fmt.Errorf("tried to close a file that wasn't open") } + // Close of a virtual file. + if obj.handle == nil { + // Record success + cpm.CPU.States.AF.Hi = 0x00 + return nil + } + // Is this a $-file? if strings.Contains(obj.name, "$") { @@ -531,12 +570,40 @@ func SysCallFindFirst(cpm *CPM) error { return nil } + // Add on any virtual files, by merging the drive. + _ = fs.WalkDir(cpm.static, string(cpm.currentDrive+'A'), + func(path string, d fs.DirEntry, err error) error { + if err != nil { + return nil + } + if d.IsDir() { + return nil + } + + // Does the entry match the glob? + if fcbPtr.DoesMatch(filepath.Base(path)) { + + // If so append + res = append(res, fcb.FCBFind{ + Host: path, + Name: filepath.Base(path)}) + } + + return nil + }) + // No matches? Return an error if len(res) < 1 { cpm.CPU.States.AF.Hi = 0xFF return nil } + // Sort the list, since we've added the embedded files + // onto the end and that will look weird. + sort.Slice(res, func(i, j int) bool { + return res[i].Name < res[j].Name + }) + // Here we save the results in our cache, // dropping the first cpm.findFirstResults = res[1:] @@ -706,14 +773,6 @@ func SysCallRead(cpm *CPM) error { return nil } - // Get the next read position - offset := fcbPtr.GetSequentialOffset() - - _, err := obj.handle.Seek(int64(offset), io.SeekStart) - if err != nil { - return fmt.Errorf("cannot seek to position %d: %s", offset, err) - } - // Temporary area to read into data := make([]byte, blkSize) @@ -722,6 +781,53 @@ func SysCallRead(cpm *CPM) error { data[i] = 0x1A } + // Get the next read position + offset := fcbPtr.GetSequentialOffset() + + // Are we reading from a virtual file? + if obj.handle == nil { + + // Remap + p := filepath.Join(string(cpm.currentDrive+'A'), filepath.Base(obj.name)) + + // open + file, err := fs.ReadFile(cpm.static, p) + if err != nil { + fmt.Printf("error on readfile for virtual path (%s):%s\n", p, err) + } + i := 0 + + // default to being successful + cpm.CPU.States.AF.Hi = 0x00 + + // copy each appropriate byte into the data-area + for i < blkSize { + if int(offset)+i < len(file) { + data[i] = file[int(offset)+i] + } else { + cpm.CPU.States.AF.Hi = 0x01 + } + i++ + } + + // Copy the data to the DMA area + cpm.Memory.SetRange(cpm.dma, data...) + + // Update the next read position + fcbPtr.IncreaseSequentialOffset() + + // Update the FCB in memory + cpm.Memory.SetRange(ptr, fcbPtr.AsBytes()...) + + // All done + return nil + } + + _, err := obj.handle.Seek(int64(offset), io.SeekStart) + if err != nil { + return fmt.Errorf("cannot seek to position %d: %s", offset, err) + } + // Read from the file, now we're in the right place _, err = obj.handle.Read(data) if err != nil && err != io.EOF { @@ -773,6 +879,11 @@ func SysCallWrite(cpm *CPM) error { return nil } + // A virtual handle, from our embedded resources. + if obj.handle == nil { + return fmt.Errorf("fatal error SysCallWrite against an embedded resource %v", obj) + } + // Get the next write position offset := fcbPtr.GetSequentialOffset() @@ -1118,6 +1229,11 @@ func SysCallReadRand(cpm *CPM) error { return nil } + // A virtual handle, from our embedded resources. + if obj.handle == nil { + return fmt.Errorf("fatal error SysCallReadRand against an embedded resource %v", obj) + } + // Get the record to read record := int(int(fcbPtr.R2)<<16) | int(int(fcbPtr.R1)<<8) | int(fcbPtr.R0) @@ -1163,6 +1279,11 @@ func SysCallWriteRand(cpm *CPM) error { return nil } + // A virtual handle, from our embedded resources. + if obj.handle == nil { + return fmt.Errorf("fatal error SysCallWriteRand against an embedded resource %v", obj) + } + // Get the data range from the DMA area data := cpm.Memory.GetRange(cpm.dma, 128) diff --git a/dist/README.md b/dist/README.md index 48ad35e..defd6b0 100644 --- a/dist/README.md +++ b/dist/README.md @@ -4,8 +4,10 @@ This directory contains a single binary which is used to test the emulator: * [The lighthouse of Doom](https://github.com/skx/lighthouse-of-doom/) -The top-level [samples/](../samples) directory contains some sample code, -along with the corresponding Z80 assembly-language source code. +Other CP/M code within the repository includes: + +* The top-level [samples/](../samples/) directory contains some code which was useful to me to test my understanding, when writing the emulator. +* The top-level [static/](../static/) directory contains some binaries which are always available when launching our emulator. diff --git a/fcb/fcb.go b/fcb/fcb.go index 8d0d6ef..0ea5eaa 100644 --- a/fcb/fcb.go +++ b/fcb/fcb.go @@ -274,6 +274,52 @@ func FromBytes(bytes []uint8) FCB { return tmp } +// DoesMatch returns true if the filename specified matches the pattern in the FCB. +func (f *FCB) DoesMatch(name string) bool { + t := string(f.Type[0]) + string(f.Type[1]) + string(f.Type[2]) + if t == "" || t == " " { + t = "???" + } + + // Having a .extension is fine, but if the + // suffix is longer than three characters we're + // not going to use it. + parts := strings.Split(name, ".") + if len(parts) == 2 { + // filename is over 8 characters + if len(parts[0]) > 8 { + return false + } + // suffix is over 3 characters + if len(parts[1]) > 3 { + return false + } + } + + // Create a temporary FCB for the specified filename. + tmp := FromString(name) + + // Now test if the name we've got matches that in the + // search-pattern: Name. + // + // Either a literal match, or a wildcard match with "?". + for i, c := range f.Name { + if (tmp.Name[i] != c) && (f.Name[i] != '?') { + return false + } + } + + // Repeat for the suffix. + for i, c := range f.Type { + if (tmp.Type[i] != c) && (f.Type[i] != '?') { + return false + } + } + + // Got a match + return true +} + // GetMatches returns the files matching the pattern in the given FCB record. // // We try to do this by converting the entries of the named directory into FCBs diff --git a/main.go b/main.go index 098fa68..510aa98 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( cpmccp "github.com/skx/cpmulator/ccp" "github.com/skx/cpmulator/cpm" + "github.com/skx/cpmulator/static" cpmver "github.com/skx/cpmulator/version" ) @@ -173,6 +174,10 @@ func main() { } } + // Load any embedded files within our binary + files := static.Content + obj.SetStaticFilesystem(files) + // Default to not using subdirectories for drives obj.SetDrives(false) diff --git a/samples/Makefile b/samples/Makefile index 25e2cf9..997767e 100644 --- a/samples/Makefile +++ b/samples/Makefile @@ -2,9 +2,9 @@ # # The files we wish to generate. # -all: ccp.com cli-args.com console.com create.com \ - ctrlc.com delete.com drive.com find.com \ - intest.com quiet.com read.com ret.com test.com \ +all: cli-args.com create.com \ + delete.com drive.com find.com \ + intest.com read.com ret.com test.com \ user-num.com version.com write.com diff --git a/samples/README.md b/samples/README.md index 21d4fc9..98cd629 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,40 +1,19 @@ # Sample CP/M Binaries -This directory contains some sample code, written in Z80 assembly. +This directory contains some sample code, written in Z80 assembly, which was useful in testing my understanding when writing the emulator. -The top-level [dist/](../dist) directory contains a complete program used to test the emulator, and our sister repository contains a curated collection of binaries known to work well under this emulator: +Other CP/M code within the repository includes: -* https://github.com/skx/cpm-dist - - - -## cpmulator Specific Utilities +* The top-level [dist/](../dist) directory contains a complete program used to test the emulator. +* The top-level [static/](../static/) directory contains some binaries which are always available when launching our emulator. -I've implemented [a custom BIOS function](../EXTENSIONS.md), with a number of extensions, and these extensions are useful within the CCP. - -The following binaries are designed to work with the extensions and make functional/useful changes to the CCP environment: - -* [ccp.z80](ccp.asm) - * Change the CCP in-use at runtime. -* [console.z80](console.z80) - * Toggle between ADM-3A and ANSI console output. -* [ctrlc.z80](ctrlc.z80) - * By default we reboot the CCP whenever the user presses Ctrl-C twice in a row. - * Here you can tweak that behaviour to change the number of consecutive Ctrl-Cs that will reboot. - * Require only a single Ctrl-C (`ctrlc 1`) - * Disable the Ctrl-C reboot behaviour entirely (`ctrlc 0`) -* [test.z80](test.z80) - * A program that determines whether it is running under cpmulator. - * If so it shows the version banner. -* [quiet.z80](quiet.z80) - * Disable the startup banner. - * "cpmulator unreleased loaded CCP ccpz, with adm-3a output driver", or similar. +More significant programs are available within the sister-repository: +* https://github.com/skx/cpm-dist -## Other Testing-Commands -These were primarily written to compare behaviour across emulators, or test my understanding of how things were supposed to work. +## Contents * [cli-args.z80](cli-args.z80) * Shows command-line arguments passed to binaries launched from CCP diff --git a/samples/ccp.com b/static/A/CCP.COM similarity index 100% rename from samples/ccp.com rename to static/A/CCP.COM diff --git a/samples/console.com b/static/A/CONSOLE.COM similarity index 100% rename from samples/console.com rename to static/A/CONSOLE.COM diff --git a/samples/ctrlc.com b/static/A/CTRLC.COM similarity index 100% rename from samples/ctrlc.com rename to static/A/CTRLC.COM diff --git a/samples/quiet.com b/static/A/QUIET.COM similarity index 100% rename from samples/quiet.com rename to static/A/QUIET.COM diff --git a/static/Makefile b/static/Makefile new file mode 100644 index 0000000..d0e2870 --- /dev/null +++ b/static/Makefile @@ -0,0 +1,20 @@ + +# +# The files we wish to generate. +# +all: A/CCP.COM A/CONSOLE.COM A/CTRLC.COM A/QUIET.COM + +# +# How to build them all - repetitive. +# +A/CCP.COM: + pasmo ccp.z80 A/CCP.COM + +A/CONSOLE.COM: + pasmo console.z80 A/CONSOLE.COM + +A/CTRLC.COM: + pasmo ctrlc.z80 A/CTRLC.COM + +A/QUIET.COM: + pasmo quiet.z80 A/QUIET.COM diff --git a/static/README.md b/static/README.md new file mode 100644 index 0000000..744f790 --- /dev/null +++ b/static/README.md @@ -0,0 +1,36 @@ +# Embedded Resources + +This directory contains some binaries which are embedded into the main `cpmulator` binary, the intention is that these binaries will always +appear present upon the local disc. These make use of our [custom BIOS functions](../EXTENSIONS.md), making them tied to this emulator and useless without it. + +Other CP/M code within the repository includes: + +* The top-level [dist/](../dist) directory contains a complete program used to test the emulator. +* The top-level [samples/](../samples/) directory contains some code which was useful to me to test my understanding, when writing the emulator. + +More significant programs are available within the sister-repository: + +* https://github.com/skx/cpm-dist + + + +## Contents + +The embedded resources do not have 100% full functionality, you cannot bundle a game such as ZORK, because not all I/O primitives work upon them, but simple binaries to be executed by the CCP work just fine. + + +* [ccp.z80](ccp.z80) + * Change the CCP in-use at runtime. +* [console.z80](console.z80) + * Toggle between ADM-3A and ANSI console output. +* [ctrlc.z80](ctrlc.z80) + * By default we reboot the CCP whenever the user presses Ctrl-C twice in a row. + * Here you can tweak that behaviour to change the number of consecutive Ctrl-Cs that will reboot. + * Require only a single Ctrl-C (`ctrlc 1`) + * Disable the Ctrl-C reboot behaviour entirely (`ctrlc 0`) +* [test.z80](test.z80) + * A program that determines whether it is running under cpmulator. + * If so it shows the version banner. +* [quiet.z80](quiet.z80) + * Disable the startup banner. + * "cpmulator unreleased loaded CCP ccpz, with adm-3a output driver", or similar. diff --git a/samples/ccp.z80 b/static/ccp.z80 similarity index 100% rename from samples/ccp.z80 rename to static/ccp.z80 diff --git a/samples/console.z80 b/static/console.z80 similarity index 100% rename from samples/console.z80 rename to static/console.z80 diff --git a/samples/ctrlc.z80 b/static/ctrlc.z80 similarity index 100% rename from samples/ctrlc.z80 rename to static/ctrlc.z80 diff --git a/samples/quiet.z80 b/static/quiet.z80 similarity index 100% rename from samples/quiet.z80 rename to static/quiet.z80 diff --git a/static/static.go b/static/static.go new file mode 100644 index 0000000..916d78e --- /dev/null +++ b/static/static.go @@ -0,0 +1,11 @@ +// Package static is a hierarchy of files that are added to +// the generated emulator. +// +// The intention is that we can ship a number of binary CP/M +// files within our emulator. +package static + +import "embed" + +//go:embed */* +var Content embed.FS