This post is long overdue and was originally going to feature my code for playing sounds in the Z-machine. That will have to wait however as I've recently been making some significant changes to my Z-machine library and the sound code faces some significant rewriting.
Supporting all eight Z-machine versions in one codebase is an interesting challenge. Back when I first started this project, I had one class which encompassed every version. It was filled with hundreds of if/else statements and switches which directed behavior based on the machine version. This worked well enough, but was a mess to look at and maintain. After some time I decided to use an inheritance model to separate the behavior of each version. This helped me clean up the code as well as fix a few bugs which were hidden before.
One thing that became clear to me as I refactored my code was that my original design, in the interest of typing less code, had actually created a sort of hybrid machine where capabilities of later versions are actually available to earlier ones. An interpreter can get away with this sort of design by relying on the fact that earlier games will not try to use these capabilities, as I mentioned during my last post. Indeed, I think this approach is fairly common among Z-machine interpreters. While this is fine from a practical point of view of running games. I've recently come to the conclusion that it is poor from a code-as-documentation standpoint. If we had to reverse engineer existing interpreters to reproduce the Z-machine standards document, a hybrid machine would give us a badly distorted picture. Theoretically, it could also encourage the development of games that use capabilities not properly belonging to a given version. Because of this I've decided to adopt a more strict interpretation of the specification and make the appropriate changes to my library.
Moving to an inheritance model has helped me improve my code a lot, but it hasn't all been a cure all. There are a few methods, namely converting between Z-characters and ZSCII which don't lend themselves easily to inheritance. Key pieces of the algorithms change between versions, which makes it difficult to implement them as virtual/override methods and leads to blocks of repeated code. This is the exception however, and the overall improvement in the code has been substantial.
I still have some refactoring to do to implement my latest design changes, but I hope to make my source code available soon.