2017-07-31 01:15:20 +00:00
|
|
|
//
|
|
|
|
// AmstradCPC.cpp
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 30/07/2017.
|
2018-05-13 19:19:52 +00:00
|
|
|
// Copyright 2017 Thomas Harte. All rights reserved.
|
2017-07-31 01:15:20 +00:00
|
|
|
//
|
|
|
|
|
|
|
|
#include "StaticAnalyser.hpp"
|
2017-08-11 22:46:39 +00:00
|
|
|
|
2017-11-10 03:04:49 +00:00
|
|
|
#include <algorithm>
|
|
|
|
#include <cstring>
|
|
|
|
|
2018-01-25 02:48:44 +00:00
|
|
|
#include "../../../Storage/Disk/Parsers/CPM.hpp"
|
|
|
|
#include "../../../Storage/Disk/Encodings/MFM/Parser.hpp"
|
2021-03-11 03:02:10 +00:00
|
|
|
#include "../../../Storage/Tape/Parsers/Spectrum.hpp"
|
2017-07-31 01:15:20 +00:00
|
|
|
|
2021-03-22 23:53:51 +00:00
|
|
|
#include "Target.hpp"
|
|
|
|
|
2021-03-11 03:02:10 +00:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
bool strcmp_insensitive(const char *a, const char *b) {
|
2017-11-11 20:28:40 +00:00
|
|
|
if(std::strlen(a) != std::strlen(b)) return false;
|
2017-08-11 20:23:00 +00:00
|
|
|
while(*a) {
|
2017-11-11 20:28:40 +00:00
|
|
|
if(std::tolower(*a) != std::tolower(*b)) return false;
|
2017-08-11 20:23:00 +00:00
|
|
|
a++;
|
|
|
|
b++;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-03-11 03:02:10 +00:00
|
|
|
bool is_implied_extension(const std::string &extension) {
|
2017-08-17 02:24:37 +00:00
|
|
|
return
|
|
|
|
extension == " " ||
|
|
|
|
strcmp_insensitive(extension.c_str(), "BAS") ||
|
|
|
|
strcmp_insensitive(extension.c_str(), "BIN");
|
|
|
|
}
|
|
|
|
|
2021-03-11 03:02:10 +00:00
|
|
|
void right_trim(std::string &string) {
|
2017-08-17 17:21:48 +00:00
|
|
|
string.erase(std::find_if(string.rbegin(), string.rend(), [](int ch) {
|
2018-05-05 23:32:20 +00:00
|
|
|
return !std::isspace(ch);
|
|
|
|
}).base(), string.end());
|
2017-08-17 17:21:48 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 03:02:10 +00:00
|
|
|
std::string RunCommandFor(const Storage::Disk::CPM::File &file) {
|
2017-08-17 02:24:37 +00:00
|
|
|
// Trim spaces from the name.
|
|
|
|
std::string name = file.name;
|
2017-08-17 17:21:48 +00:00
|
|
|
right_trim(name);
|
2017-08-17 02:24:37 +00:00
|
|
|
|
|
|
|
// Form the basic command.
|
|
|
|
std::string command = "run\"" + name;
|
|
|
|
|
|
|
|
// Consider whether the extension is required.
|
|
|
|
if(!is_implied_extension(file.type)) {
|
2017-08-17 17:21:48 +00:00
|
|
|
std::string type = file.type;
|
|
|
|
right_trim(type);
|
|
|
|
command += "." + type;
|
2017-08-17 02:24:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add a newline and return.
|
|
|
|
return command + "\n";
|
2017-08-14 13:48:56 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 03:02:10 +00:00
|
|
|
void InspectCatalogue(
|
2017-08-17 01:55:31 +00:00
|
|
|
const Storage::Disk::CPM::Catalogue &catalogue,
|
2018-03-09 21:07:29 +00:00
|
|
|
const std::unique_ptr<Analyser::Static::AmstradCPC::Target> &target) {
|
2017-09-17 00:15:06 +00:00
|
|
|
|
|
|
|
std::vector<const Storage::Disk::CPM::File *> candidate_files;
|
|
|
|
candidate_files.reserve(catalogue.files.size());
|
2018-05-01 02:23:57 +00:00
|
|
|
for(const auto &file : catalogue.files) {
|
2017-09-17 00:15:06 +00:00
|
|
|
candidate_files.push_back(&file);
|
|
|
|
}
|
2017-08-17 16:48:15 +00:00
|
|
|
|
|
|
|
// Remove all files with untypable characters.
|
2017-08-17 02:11:49 +00:00
|
|
|
candidate_files.erase(
|
2017-09-17 00:15:06 +00:00
|
|
|
std::remove_if(candidate_files.begin(), candidate_files.end(), [](const Storage::Disk::CPM::File *file) {
|
2018-05-01 02:23:57 +00:00
|
|
|
for(const auto c : file->name + file->type) {
|
2017-09-16 23:10:17 +00:00
|
|
|
if(c < 32) return true;
|
|
|
|
}
|
2017-08-17 16:48:15 +00:00
|
|
|
return false;
|
2017-08-17 02:11:49 +00:00
|
|
|
}),
|
|
|
|
candidate_files.end());
|
|
|
|
|
2017-08-17 16:48:15 +00:00
|
|
|
// If that leaves a mix of 'system' (i.e. hidden) and non-system files, remove the system files.
|
|
|
|
bool are_all_system = true;
|
2018-05-01 02:23:57 +00:00
|
|
|
for(const auto &file : candidate_files) {
|
2017-09-17 00:15:06 +00:00
|
|
|
if(!file->system) {
|
2017-08-17 16:48:15 +00:00
|
|
|
are_all_system = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!are_all_system) {
|
|
|
|
candidate_files.erase(
|
2017-09-17 00:15:06 +00:00
|
|
|
std::remove_if(candidate_files.begin(), candidate_files.end(), [](const Storage::Disk::CPM::File *file) {
|
|
|
|
return file->system;
|
2017-08-17 16:48:15 +00:00
|
|
|
}),
|
|
|
|
candidate_files.end());
|
|
|
|
}
|
|
|
|
|
2017-08-11 14:47:12 +00:00
|
|
|
// If there's just one file, run that.
|
2017-08-17 02:11:49 +00:00
|
|
|
if(candidate_files.size() == 1) {
|
2018-01-25 03:35:54 +00:00
|
|
|
target->loading_command = RunCommandFor(*candidate_files[0]);
|
2017-08-11 14:47:12 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-08-11 20:23:00 +00:00
|
|
|
// If only one file is [potentially] BASIC, run that one; otherwise if only one has a suffix
|
|
|
|
// that AMSDOS allows to be omitted, pick that one.
|
2017-08-11 14:47:12 +00:00
|
|
|
int basic_files = 0;
|
2017-08-11 20:23:00 +00:00
|
|
|
int implicit_suffixed_files = 0;
|
|
|
|
|
2017-11-11 20:28:40 +00:00
|
|
|
std::size_t last_basic_file = 0;
|
|
|
|
std::size_t last_implicit_suffixed_file = 0;
|
2017-08-11 20:23:00 +00:00
|
|
|
|
2017-11-11 20:28:40 +00:00
|
|
|
for(std::size_t c = 0; c < candidate_files.size(); c++) {
|
2017-08-11 22:59:38 +00:00
|
|
|
// Files with nothing but spaces in their name can't be loaded by the user, so disregard them.
|
2017-09-17 00:15:06 +00:00
|
|
|
if(candidate_files[c]->type == " " && candidate_files[c]->name == " ")
|
2017-08-11 22:59:38 +00:00
|
|
|
continue;
|
|
|
|
|
2017-08-11 16:27:50 +00:00
|
|
|
// Check for whether this is [potentially] BASIC.
|
2017-09-17 00:15:06 +00:00
|
|
|
if(candidate_files[c]->data.size() >= 128 && !((candidate_files[c]->data[18] >> 1) & 7)) {
|
2017-08-11 14:47:12 +00:00
|
|
|
basic_files++;
|
|
|
|
last_basic_file = c;
|
|
|
|
}
|
2017-08-11 16:27:50 +00:00
|
|
|
|
|
|
|
// Check suffix for emptiness.
|
2017-09-17 00:15:06 +00:00
|
|
|
if(is_implied_extension(candidate_files[c]->type)) {
|
2017-08-11 20:23:00 +00:00
|
|
|
implicit_suffixed_files++;
|
|
|
|
last_implicit_suffixed_file = c;
|
2017-08-11 16:27:50 +00:00
|
|
|
}
|
2017-08-11 14:47:12 +00:00
|
|
|
}
|
2017-08-11 20:23:00 +00:00
|
|
|
if(basic_files == 1 || implicit_suffixed_files == 1) {
|
2017-11-11 20:28:40 +00:00
|
|
|
std::size_t selected_file = (basic_files == 1) ? last_basic_file : last_implicit_suffixed_file;
|
2018-01-25 03:35:54 +00:00
|
|
|
target->loading_command = RunCommandFor(*candidate_files[selected_file]);
|
2017-08-11 14:47:12 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-08-11 14:47:45 +00:00
|
|
|
|
2017-08-17 17:21:48 +00:00
|
|
|
// One more guess: if only one remaining candidate file has a different name than the others,
|
|
|
|
// assume it is intended to stand out.
|
|
|
|
std::map<std::string, int> name_counts;
|
2017-11-11 20:28:40 +00:00
|
|
|
std::map<std::string, std::size_t> indices_by_name;
|
|
|
|
std::size_t index = 0;
|
2018-05-01 02:23:57 +00:00
|
|
|
for(const auto &file : candidate_files) {
|
2017-09-17 00:15:06 +00:00
|
|
|
name_counts[file->name]++;
|
|
|
|
indices_by_name[file->name] = index;
|
2017-08-17 17:21:48 +00:00
|
|
|
index++;
|
|
|
|
}
|
|
|
|
if(name_counts.size() == 2) {
|
2018-05-01 02:23:57 +00:00
|
|
|
for(const auto &pair : name_counts) {
|
2017-08-17 17:21:48 +00:00
|
|
|
if(pair.second == 1) {
|
2018-01-25 03:35:54 +00:00
|
|
|
target->loading_command = RunCommandFor(*candidate_files[indices_by_name[pair.first]]);
|
2017-08-17 17:21:48 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-11 14:47:45 +00:00
|
|
|
// Desperation.
|
2018-01-25 03:35:54 +00:00
|
|
|
target->loading_command = "cat\n";
|
2017-08-11 14:47:12 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 03:02:10 +00:00
|
|
|
bool CheckBootSector(const std::shared_ptr<Storage::Disk::Disk> &disk, const std::unique_ptr<Analyser::Static::AmstradCPC::Target> &target) {
|
2017-08-11 22:46:39 +00:00
|
|
|
Storage::Encodings::MFM::Parser parser(true, disk);
|
2017-09-30 00:08:36 +00:00
|
|
|
Storage::Encodings::MFM::Sector *boot_sector = parser.get_sector(0, 0, 0x41);
|
2018-03-22 22:52:43 +00:00
|
|
|
if(boot_sector != nullptr && !boot_sector->samples.empty() && boot_sector->samples[0].size() == 512) {
|
2017-08-17 01:55:31 +00:00
|
|
|
// Check that the first 64 bytes of the sector aren't identical; if they are then probably
|
|
|
|
// this disk was formatted and the filler byte never replaced.
|
|
|
|
bool matched = true;
|
2017-11-11 20:28:40 +00:00
|
|
|
for(std::size_t c = 1; c < 64; c++) {
|
2017-11-01 01:32:28 +00:00
|
|
|
if(boot_sector->samples[0][c] != boot_sector->samples[0][0]) {
|
2017-08-17 01:55:31 +00:00
|
|
|
matched = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-11 22:46:39 +00:00
|
|
|
// This is a system disk, then launch it as though it were CP/M.
|
2017-08-17 01:55:31 +00:00
|
|
|
if(!matched) {
|
2018-01-25 03:35:54 +00:00
|
|
|
target->loading_command = "|cpm\n";
|
2017-08-17 18:28:16 +00:00
|
|
|
return true;
|
2017-08-17 01:55:31 +00:00
|
|
|
}
|
2017-08-11 22:46:39 +00:00
|
|
|
}
|
2017-08-17 01:55:31 +00:00
|
|
|
|
2017-08-17 18:28:16 +00:00
|
|
|
return false;
|
2017-08-11 14:47:12 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 03:02:10 +00:00
|
|
|
bool IsAmstradTape(const std::shared_ptr<Storage::Tape::Tape> &tape) {
|
|
|
|
// Limited sophistication here; look for a CPC-style file header, that is
|
|
|
|
// any Spectrum-esque block with a synchronisation character of 0x2c.
|
|
|
|
//
|
|
|
|
// More could be done here: parse the header, look for 0x16 data records.
|
|
|
|
using Parser = Storage::Tape::ZXSpectrum::Parser;
|
|
|
|
Parser parser(Parser::MachineType::AmstradCPC);
|
|
|
|
|
|
|
|
while(true) {
|
|
|
|
const auto block = parser.find_block(tape);
|
|
|
|
if(!block) break;
|
|
|
|
|
|
|
|
if(block->type == 0x2c) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
2020-05-30 05:06:43 +00:00
|
|
|
Analyser::Static::TargetList Analyser::Static::AmstradCPC::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) {
|
2018-04-14 16:12:12 +00:00
|
|
|
TargetList destination;
|
2019-12-22 04:52:04 +00:00
|
|
|
auto target = std::make_unique<Target>();
|
2018-03-07 19:24:52 +00:00
|
|
|
target->confidence = 0.5;
|
2017-08-05 23:20:38 +00:00
|
|
|
|
2018-03-09 21:07:29 +00:00
|
|
|
target->model = Target::Model::CPC6128;
|
2017-08-11 14:47:12 +00:00
|
|
|
|
2018-03-22 22:52:43 +00:00
|
|
|
if(!media.tapes.empty()) {
|
2021-03-11 03:02:10 +00:00
|
|
|
bool has_cpc_tape = false;
|
|
|
|
for(auto &tape: media.tapes) {
|
|
|
|
has_cpc_tape |= IsAmstradTape(tape);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(has_cpc_tape) {
|
|
|
|
target->media.tapes = media.tapes;
|
2018-03-22 22:52:43 +00:00
|
|
|
|
2021-03-11 03:02:10 +00:00
|
|
|
// Ugliness flows here: assume the CPC isn't smart enough to pause between pressing
|
|
|
|
// enter and responding to the follow-on prompt to press a key, so just type for
|
|
|
|
// a while. Yuck!
|
2021-03-13 04:04:45 +00:00
|
|
|
target->loading_command = "|tape\nrun\"\n123";
|
2021-03-11 03:02:10 +00:00
|
|
|
}
|
2017-08-11 14:47:12 +00:00
|
|
|
}
|
2017-08-05 23:20:38 +00:00
|
|
|
|
2018-03-22 22:52:43 +00:00
|
|
|
if(!media.disks.empty()) {
|
2017-08-11 14:47:12 +00:00
|
|
|
Storage::Disk::CPM::ParameterBlock data_format;
|
|
|
|
data_format.sectors_per_track = 9;
|
|
|
|
data_format.tracks = 40;
|
|
|
|
data_format.block_size = 1024;
|
|
|
|
data_format.first_sector = 0xc1;
|
|
|
|
data_format.catalogue_allocation_bitmap = 0xc000;
|
|
|
|
data_format.reserved_tracks = 0;
|
|
|
|
|
2018-03-22 22:52:43 +00:00
|
|
|
Storage::Disk::CPM::ParameterBlock system_format;
|
|
|
|
system_format.sectors_per_track = 9;
|
|
|
|
system_format.tracks = 40;
|
|
|
|
system_format.block_size = 1024;
|
|
|
|
system_format.first_sector = 0x41;
|
|
|
|
system_format.catalogue_allocation_bitmap = 0xc000;
|
|
|
|
system_format.reserved_tracks = 2;
|
|
|
|
|
2018-05-01 02:23:57 +00:00
|
|
|
for(auto &disk: media.disks) {
|
2018-03-22 22:52:43 +00:00
|
|
|
// Check for an ordinary catalogue.
|
|
|
|
std::unique_ptr<Storage::Disk::CPM::Catalogue> data_catalogue = Storage::Disk::CPM::GetCatalogue(disk, data_format);
|
|
|
|
if(data_catalogue) {
|
|
|
|
InspectCatalogue(*data_catalogue, target);
|
|
|
|
target->media.disks.push_back(disk);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Failing that check for a boot sector.
|
2018-03-23 22:39:37 +00:00
|
|
|
if(CheckBootSector(disk, target)) {
|
2018-03-22 22:52:43 +00:00
|
|
|
target->media.disks.push_back(disk);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Failing that check for a system catalogue.
|
2018-03-23 22:39:37 +00:00
|
|
|
std::unique_ptr<Storage::Disk::CPM::Catalogue> system_catalogue = Storage::Disk::CPM::GetCatalogue(disk, system_format);
|
2018-03-22 22:52:43 +00:00
|
|
|
if(system_catalogue) {
|
|
|
|
InspectCatalogue(*system_catalogue, target);
|
|
|
|
target->media.disks.push_back(disk);
|
|
|
|
continue;
|
2017-08-11 14:47:12 +00:00
|
|
|
}
|
|
|
|
}
|
2017-08-10 21:10:21 +00:00
|
|
|
}
|
|
|
|
|
2018-03-22 22:52:43 +00:00
|
|
|
// If any media survived, add the target.
|
|
|
|
if(!target->media.empty())
|
|
|
|
destination.push_back(std::move(target));
|
2018-04-14 16:12:12 +00:00
|
|
|
|
|
|
|
return destination;
|
2017-07-31 01:15:20 +00:00
|
|
|
}
|