Files
CLK/Analyser/Static/ZXSpectrum/StaticAnalyser.cpp
T
2026-10-21 22:21:43 -04:00

283 lines
8.6 KiB
C++

//
// StaticAnalyser.cpp
// Clock Signal
//
// Created by Thomas Harte on 17/03/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "StaticAnalyser.hpp"
#include "Storage/Disk/Parsers/CPM.hpp"
#include "Storage/Disk/Encodings/MFM/Parser.hpp"
#include "Storage/Tape/Parsers/Spectrum.hpp"
#include "Target.hpp"
#include <array>
#include <algorithm>
namespace {
enum class KnownGame {
Aquaplane,
BCsQuestForTyres, // The real game is spelt the American way, but I can't bring myself to type it.
DarkStar,
FantasyWorldDizzy,
MatchDayII,
SabreWulf,
};
namespace Signatures {
// Text used on Aquaplane's title screen.
static constexpr std::string_view aquaplane = "A Q U A P L A N E";
// The status line.
static constexpr std::string_view bcs_quest_for_tyres = "WHEELS SPEED SCORE HIGH SCOREP1 00P2 00GAMEOVER";
// Part of the instructions.
static constexpr std::string_view dark_star =
"If it moves shoot it\r\rIf it doesn't shoot it anyway\r\rIf it's square fly through it !";
// Dialogue revealing that Daisy has been eggnapped.
static constexpr std::array fantasy_world_dizzy{
0x44, 0x41, 0x49, 0x53, 0x59, 0x3b, 0x53, 0x3a, 0x42, 0x45, 0x45, 0x4e,
0x3a, 0x45, 0x47, 0x47, 0x4e, 0x41, 0x50, 0x50, 0x45, 0x44, 0x88, 0x50,
0x41, 0x4e, 0x44, 0x3a, 0x49, 0x53, 0x3a, 0x42, 0x45, 0x49, 0x4e, 0x47,
0x3a, 0x48, 0x45, 0x4c, 0x44, 0x3a, 0x49, 0x4e, 0x3a, 0x54, 0x48, 0x45,
0x8a, 0x58, 0x57, 0x49, 0x5a, 0x41, 0x52, 0x44, 0x3b, 0x53, 0x3a, 0x43,
0x4c, 0x4f, 0x55, 0x44, 0x3a, 0x43, 0x41, 0x53, 0x54, 0x4c, 0x45, 0x3c,
};
// Text (with specialised control characters) including the author's names and some football-related words.
static constexpr std::array match_day_ii{
0x82, 0x86, 0x1a, 0x00, 0x42, 0x45, 0x52, 0x4e, 0x49, 0x45, 0x06, 0x18,
0x02, 0x44, 0x52, 0x55, 0x4d, 0x4d, 0x4f, 0x4e, 0x44, 0x06, 0x00, 0x00,
0x4a, 0x4f, 0x4e, 0x06, 0x00, 0x02, 0x52, 0x49, 0x54, 0x4d, 0x41, 0x4e,
0xff, 0x20, 0x48, 0x41, 0x4c, 0x46, 0xff, 0x54, 0x49, 0x4d, 0x45, 0xff,
0x20, 0x45, 0x58, 0x54, 0x52, 0x41, 0x20, 0xff, 0x46, 0x49, 0x52, 0x53,
0x54, 0xa3, 0xff, 0x53, 0x45, 0x43, 0x4f, 0x4e, 0x44,
};
// Likely to be part of the graphics.
static constexpr std::array sabre_wulf{
0x5c, 0x3a, 0x5c, 0x3a, 0x5c, 0x3a, 0x5c, 0x3a, 0x5c, 0x1f, 0x5c, 0x42,
0x52, 0xd5, 0x5b, 0xd5, 0x5b, 0xd5, 0x5b, 0xd5, 0x5b, 0xd5, 0x5b, 0xc4,
0x5b, 0x42, 0x52, 0x42, 0x52, 0x30, 0x51, 0x30, 0x51, 0x30, 0x51, 0x30,
0x51, 0x30, 0x51, 0x30, 0x51, 0x30, 0x51, 0x30, 0x51, 0x30, 0x51, 0x30,
0x51, 0x30, 0x51, 0x30, 0x51, 0x30, 0x51, 0x30, 0x51, 0x30, 0x51, 0x30,
0x51, 0x2f, 0x52, 0x2f, 0x52, 0x2f, 0x52, 0x2f, 0x52, 0x2f, 0x52, 0x2f,
0x52, 0x2f, 0x52, 0x2f, 0x52, 0x2f, 0x52, 0x2f, 0x52, 0x2f, 0x52, 0x2f,
0x52, 0x2f, 0x52, 0x2f, 0x52, 0x2f, 0x52, 0x2f,
};
}
std::optional<KnownGame> known_game(const std::vector<uint8_t> &code) {
const auto contains = [&](auto &&signature) {
return std::search(code.begin(), code.end(), signature.begin(), signature.end()) != code.end();
};
if(contains(Signatures::aquaplane)) {
return KnownGame::Aquaplane;
}
if(contains(Signatures::fantasy_world_dizzy)) {
return KnownGame::FantasyWorldDizzy;
}
if(contains(Signatures::bcs_quest_for_tyres)) {
return KnownGame::BCsQuestForTyres;
}
if(contains(Signatures::sabre_wulf)) {
return KnownGame::SabreWulf;
}
if(contains(Signatures::match_day_ii)) {
return KnownGame::MatchDayII;
}
if(contains(Signatures::dark_star)) {
return KnownGame::DarkStar;
}
return std::nullopt;
}
std::optional<KnownGame> known_game(Storage::Tape::TapeSerialiser &tape) {
using Parser = Storage::Tape::ZXSpectrum::Parser;
using BlockType = Storage::Tape::ZXSpectrum::BlockType;
Parser parser(Parser::MachineType::ZXSpectrum);
while(true) {
const auto block = parser.find_block(tape);
if(!block) break;
// Check for a Spectrum contents block.
if(block->type == BlockType::Body) {
const auto game = known_game(parser.get_block_body(tape));
if(game.has_value()) return game;
}
}
return std::nullopt;
}
bool is_tape(Storage::Tape::TapeSerialiser &tape) {
using Parser = Storage::Tape::ZXSpectrum::Parser;
using BlockType = Storage::Tape::ZXSpectrum::BlockType;
Parser parser(Parser::MachineType::ZXSpectrum);
while(true) {
const auto block = parser.find_block(tape);
if(!block) break;
if(block->type == BlockType::Program) {
return true;
}
}
return false;
}
bool is_disk(const Storage::Disk::Disk &disk) {
Storage::Encodings::MFM::Parser parser(Storage::Encodings::MFM::Density::Double, disk);
// Grab absolutely any sector from the first track to determine general encoding.
const Storage::Encodings::MFM::Sector *any_sector = parser.any_sector(0, 0);
if(!any_sector) return false;
// Determine the sector base and get logical sector 1.
const uint8_t sector_base = any_sector->address.sector & 0xc0;
const Storage::Encodings::MFM::Sector *boot_sector = parser.sector(0, 0, sector_base + 1);
if(!boot_sector) return false;
Storage::Disk::CPM::ParameterBlock cpm_format{};
switch(sector_base) {
case 0x40: cpm_format = Storage::Disk::CPM::ParameterBlock::cpc_system_format(); break;
case 0xc0: cpm_format = Storage::Disk::CPM::ParameterBlock::cpc_data_format(); break;
default: {
// Check the first ten bytes of the first sector for the disk format; if these are all
// the same value then instead substitute a default format.
std::array<uint8_t, 10> format;
std::copy_n(boot_sector->samples[0].begin(), 10, format.begin());
if(std::all_of(format.begin(), format.end(), [&](const uint8_t v) { return v == format[0]; })) {
format = {0x00, 0x00, 0x28, 0x09, 0x02, 0x01, 0x03, 0x02, 0x2a, 0x52};
}
// Parse those ten bytes as:
//
// Byte 0: disc type
// Byte 1: sidedness
// bits 0-6: arrangement
// 0 => single sided
// 1 => double sided, flip sides
// 2 => double sided, up and over
// bit 7: double-track
// Byte 2: number of tracks per side
// Byte 3: number of sectors per track
// Byte 4: Log2(sector size) - 7
// Byte 5: number of reserved tracks
// Byte 6: Log2(block size) - 7
// Byte 7: number of directory blocks
// Byte 8: gap length (read/write)
// Byte 9: gap length(format)
cpm_format.sectors_per_track = format[3];
cpm_format.tracks = format[2];
cpm_format.block_size = 128 << format[6];
cpm_format.first_sector = sector_base + 1;
cpm_format.reserved_tracks = format[5];
// i.e. bits set downward from 0x4000 for as many blocks as form the catalogue.
cpm_format.catalogue_allocation_bitmap = 0x8000 - (0x8000 >> format[7]);
} break;
}
// If the boot sector sums to 3 modulo 256 then this is a Spectrum disk.
const auto byte_sum = static_cast<uint8_t>(
std::accumulate(boot_sector->samples[0].begin(), boot_sector->samples[0].end(), 0));
if(byte_sum == 3) {
return true;
}
// ... otherwise read a CPM directory and look for a BASIC program called "DISK".
const auto catalogue = Storage::Disk::CPM::GetCatalogue(disk, cpm_format, false);
return catalogue && catalogue->is_zx_spectrum_booter();
}
}
Analyser::Static::TargetList Analyser::Static::ZXSpectrum::GetTargets(
const Media &media,
const std::filesystem::path &,
TargetPlatform::IntType,
bool
) {
TargetList destination;
auto target = std::make_unique<Target>();
target->confidence = 0.5;
std::optional<KnownGame> game;
if(!media.tapes.empty()) {
bool has_spectrum_tape = false;
for(auto &tape: media.tapes) {
auto serialiser = tape->serialiser();
const bool is_spectrum = is_tape(*serialiser);
has_spectrum_tape |= is_spectrum;
if(is_spectrum) {
serialiser->reset();
const auto tape_game = known_game(*serialiser);
if(tape_game.has_value()) {
game = tape_game;
}
}
}
if(has_spectrum_tape) {
target->media.tapes = media.tapes;
if(game) {
switch(*game) {
using enum KnownGame;
case Aquaplane:
case BCsQuestForTyres:
case DarkStar:
case MatchDayII:
case SabreWulf:
target->model = Target::Model::FortyEightK;
break;
case FantasyWorldDizzy:
target->model = Target::Model::Plus2a;
break;
}
} else {
target->model = Target::Model::OneTwoEightK;
}
}
}
if(!media.disks.empty()) {
bool has_spectrum_disk = false;
for(auto &disk: media.disks) {
has_spectrum_disk |= is_disk(*disk);
}
if(has_spectrum_disk) {
target->media.disks = media.disks;
target->model = Target::Model::Plus3;
}
}
// If any media survived, add the target.
if(!target->media.empty()) {
target->should_hold_enter = true; // To force entry into the 'loader' and thereby load the media.
target->loading_command = L"J\"\"\n";; // On a 16kb or 48kb, to load the [implicitly a] tape.
destination.push_back(std::move(target));
}
return destination;
}