2016-08-29 12:48:49 +00:00
|
|
|
//
|
|
|
|
// AcornAnalyser.cpp
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 29/08/2016.
|
2018-05-13 19:19:52 +00:00
|
|
|
// Copyright 2016 Thomas Harte. All rights reserved.
|
2016-08-29 12:48:49 +00:00
|
|
|
//
|
|
|
|
|
2016-09-15 23:24:59 +00:00
|
|
|
#include "StaticAnalyser.hpp"
|
2016-08-29 12:48:49 +00:00
|
|
|
|
2016-09-19 01:18:11 +00:00
|
|
|
#include "Disk.hpp"
|
2016-08-30 01:53:06 +00:00
|
|
|
#include "Tape.hpp"
|
2018-03-09 21:07:29 +00:00
|
|
|
#include "Target.hpp"
|
2016-08-30 01:53:06 +00:00
|
|
|
|
2024-05-22 01:49:30 +00:00
|
|
|
#include "../../../Numeric/StringSimilarity.hpp"
|
|
|
|
|
2021-01-31 04:32:04 +00:00
|
|
|
#include <algorithm>
|
2024-05-22 01:49:30 +00:00
|
|
|
#include <map>
|
2021-01-31 04:32:04 +00:00
|
|
|
|
2018-01-25 02:48:44 +00:00
|
|
|
using namespace Analyser::Static::Acorn;
|
2016-08-29 12:48:49 +00:00
|
|
|
|
2018-01-24 03:18:16 +00:00
|
|
|
static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
|
|
|
AcornCartridgesFrom(const std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> &cartridges) {
|
|
|
|
std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>> acorn_cartridges;
|
2016-08-29 12:48:49 +00:00
|
|
|
|
2017-12-02 21:01:30 +00:00
|
|
|
for(const auto &cartridge : cartridges) {
|
|
|
|
const auto &segments = cartridge->get_segments();
|
2016-08-29 12:48:49 +00:00
|
|
|
|
|
|
|
// only one mapped item is allowed
|
|
|
|
if(segments.size() != 1) continue;
|
|
|
|
|
2018-01-16 02:27:45 +00:00
|
|
|
// which must be 8 or 16 kb in size
|
2018-01-17 03:27:41 +00:00
|
|
|
const Storage::Cartridge::Cartridge::Segment &segment = segments.front();
|
2018-01-16 02:27:45 +00:00
|
|
|
if(segment.data.size() != 0x4000 && segment.data.size() != 0x2000) continue;
|
2016-08-29 12:48:49 +00:00
|
|
|
|
|
|
|
// is a copyright string present?
|
2020-05-10 03:42:42 +00:00
|
|
|
const uint8_t copyright_offset = segment.data[7];
|
2016-08-29 12:48:49 +00:00
|
|
|
if(
|
|
|
|
segment.data[copyright_offset] != 0x00 ||
|
|
|
|
segment.data[copyright_offset+1] != 0x28 ||
|
|
|
|
segment.data[copyright_offset+2] != 0x43 ||
|
|
|
|
segment.data[copyright_offset+3] != 0x29
|
|
|
|
) continue;
|
|
|
|
|
|
|
|
// is the language entry point valid?
|
|
|
|
if(!(
|
|
|
|
(segment.data[0] == 0x00 && segment.data[1] == 0x00 && segment.data[2] == 0x00) ||
|
|
|
|
(segment.data[0] != 0x00 && segment.data[2] >= 0x80 && segment.data[2] < 0xc0)
|
|
|
|
)) continue;
|
|
|
|
|
|
|
|
// is the service entry point valid?
|
|
|
|
if(!(segment.data[5] >= 0x80 && segment.data[5] < 0xc0)) continue;
|
|
|
|
|
|
|
|
// probability of a random binary blob that isn't an Acorn ROM proceeding to here:
|
|
|
|
// 1/(2^32) *
|
|
|
|
// ( ((2^24)-1)/(2^24)*(1/4) + 1/(2^24) ) *
|
|
|
|
// 1/4
|
2018-05-13 19:34:31 +00:00
|
|
|
// = something very improbable, around 1/16th of 1 in 2^32, but not exactly.
|
2016-08-29 12:48:49 +00:00
|
|
|
acorn_cartridges.push_back(cartridge);
|
|
|
|
}
|
|
|
|
|
|
|
|
return acorn_cartridges;
|
|
|
|
}
|
|
|
|
|
2024-05-22 02:06:09 +00:00
|
|
|
Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType) {
|
2024-05-14 01:42:38 +00:00
|
|
|
auto target8bit = std::make_unique<ElectronTarget>();
|
|
|
|
auto targetArchimedes = std::make_unique<ArchimedesTarget>();
|
2016-09-01 00:24:13 +00:00
|
|
|
|
2024-03-04 15:13:57 +00:00
|
|
|
// Copy appropriate cartridges to the 8-bit target.
|
|
|
|
target8bit->media.cartridges = AcornCartridgesFrom(media.cartridges);
|
2016-08-29 12:48:49 +00:00
|
|
|
|
2024-02-22 18:51:44 +00:00
|
|
|
// If there are any tapes, attempt to get data from the first.
|
2018-07-11 00:00:46 +00:00
|
|
|
if(!media.tapes.empty()) {
|
2017-08-17 14:48:29 +00:00
|
|
|
std::shared_ptr<Storage::Tape::Tape> tape = media.tapes.front();
|
2018-01-24 03:18:16 +00:00
|
|
|
std::vector<File> files = GetFiles(tape);
|
2017-07-13 01:34:08 +00:00
|
|
|
tape->reset();
|
2016-09-01 00:24:13 +00:00
|
|
|
|
|
|
|
// continue if there are any files
|
2018-07-11 00:00:46 +00:00
|
|
|
if(!files.empty()) {
|
2016-09-01 00:24:13 +00:00
|
|
|
bool is_basic = true;
|
|
|
|
|
2021-01-31 04:10:59 +00:00
|
|
|
// If a file is execute-only, that means *RUN.
|
|
|
|
if(files.front().flags & File::Flags::ExecuteOnly) is_basic = false;
|
2016-09-01 00:24:13 +00:00
|
|
|
|
|
|
|
// check also for a continuous threading of BASIC lines; if none then this probably isn't BASIC code,
|
|
|
|
// so that's also justification to *RUN
|
2017-11-11 20:28:40 +00:00
|
|
|
std::size_t pointer = 0;
|
2020-05-10 03:42:42 +00:00
|
|
|
uint8_t *const data = &files.front().data[0];
|
|
|
|
const std::size_t data_size = files.front().data.size();
|
2017-03-26 18:34:47 +00:00
|
|
|
while(1) {
|
|
|
|
if(pointer >= data_size-1 || data[pointer] != 13) {
|
2016-09-01 00:24:13 +00:00
|
|
|
is_basic = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if((data[pointer+1]&0x7f) == 0x7f) break;
|
|
|
|
pointer += data[pointer+3];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Inspect first file. If it's protected or doesn't look like BASIC
|
|
|
|
// then the loading command is *RUN. Otherwise it's CHAIN"".
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n";
|
2016-09-01 00:24:13 +00:00
|
|
|
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->media.tapes = media.tapes;
|
2016-09-01 00:24:13 +00:00
|
|
|
}
|
2016-08-30 01:10:38 +00:00
|
|
|
}
|
2016-09-01 00:24:13 +00:00
|
|
|
|
2018-07-11 00:00:46 +00:00
|
|
|
if(!media.disks.empty()) {
|
2017-08-17 14:48:29 +00:00
|
|
|
std::shared_ptr<Storage::Disk::Disk> disk = media.disks.front();
|
2016-09-25 21:46:11 +00:00
|
|
|
std::unique_ptr<Catalogue> dfs_catalogue, adfs_catalogue;
|
2024-05-14 02:25:02 +00:00
|
|
|
|
|
|
|
// Get any sort of catalogue that can be found.
|
2016-09-25 21:46:11 +00:00
|
|
|
dfs_catalogue = GetDFSCatalogue(disk);
|
|
|
|
if(dfs_catalogue == nullptr) adfs_catalogue = GetADFSCatalogue(disk);
|
2024-05-14 02:25:02 +00:00
|
|
|
|
|
|
|
// 8-bit options: DFS and Hugo-style ADFS.
|
2024-04-18 02:44:55 +00:00
|
|
|
if(dfs_catalogue || (adfs_catalogue && !adfs_catalogue->has_large_sectors && adfs_catalogue->is_hugo)) {
|
2021-01-31 04:20:43 +00:00
|
|
|
// Accept the disk and determine whether DFS or ADFS ROMs are implied.
|
2021-01-31 18:12:59 +00:00
|
|
|
// Use the Pres ADFS if using an ADFS, as it leaves Page at &EOO.
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->media.disks = media.disks;
|
|
|
|
target8bit->has_dfs = bool(dfs_catalogue);
|
|
|
|
target8bit->has_pres_adfs = bool(adfs_catalogue);
|
2016-09-19 12:29:23 +00:00
|
|
|
|
2021-01-31 04:20:43 +00:00
|
|
|
// Check whether a simple shift+break will do for loading this disk.
|
2016-09-29 01:28:34 +00:00
|
|
|
Catalogue::BootOption bootOption = (dfs_catalogue ?: adfs_catalogue)->bootOption;
|
2021-01-31 04:20:43 +00:00
|
|
|
if(bootOption != Catalogue::BootOption::None) {
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->should_shift_restart = true;
|
2021-01-31 04:20:43 +00:00
|
|
|
} else {
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->loading_command = "*CAT\n";
|
2021-01-31 04:20:43 +00:00
|
|
|
}
|
|
|
|
|
2021-01-31 04:32:04 +00:00
|
|
|
// Check whether adding the AP6 ROM is justified.
|
|
|
|
// For now this is an incredibly dense text search;
|
|
|
|
// if any of the commands that aren't usually present
|
|
|
|
// on a stock Electron are here, add the AP6 ROM and
|
|
|
|
// some sideways RAM such that the SR commands are useful.
|
|
|
|
for(const auto &file: dfs_catalogue ? dfs_catalogue->files : adfs_catalogue->files) {
|
|
|
|
for(const auto &command: {
|
|
|
|
"AQRPAGE", "BUILD", "DUMP", "FORMAT", "INSERT", "LANG", "LIST", "LOADROM",
|
|
|
|
"LOCK", "LROMS", "RLOAD", "ROMS", "RSAVE", "SAVEROM", "SRLOAD", "SRPAGE",
|
|
|
|
"SRUNLOCK", "SRWIPE", "TUBE", "TYPE", "UNLOCK", "UNPLUG", "UROMS",
|
|
|
|
"VERIFY", "ZERO"
|
|
|
|
}) {
|
|
|
|
if(std::search(file.data.begin(), file.data.end(), command, command+strlen(command)) != file.data.end()) {
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->has_ap6_rom = true;
|
|
|
|
target8bit->has_sideways_ram = true;
|
2021-01-31 04:32:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-03-04 15:13:57 +00:00
|
|
|
} else if(adfs_catalogue) {
|
2024-05-14 02:25:02 +00:00
|
|
|
// Archimedes options, implicitly: ADFS, non-Hugo.
|
2024-03-04 15:13:57 +00:00
|
|
|
targetArchimedes->media.disks = media.disks;
|
2024-05-14 02:25:02 +00:00
|
|
|
|
|
|
|
// Also look for the best possible startup program name, if it can be discerned.
|
2024-05-22 02:12:55 +00:00
|
|
|
std::multimap<double, std::string, std::greater<double>> options;
|
2024-05-14 02:25:02 +00:00
|
|
|
for(const auto &file: adfs_catalogue->files) {
|
|
|
|
// Skip non-Pling files.
|
|
|
|
if(file.name[0] != '!') continue;
|
|
|
|
|
|
|
|
// Take whatever else comes with a preference for things that don't
|
2024-05-22 02:12:55 +00:00
|
|
|
// have 'boot' or 'read' in them (the latter of which will tend to be
|
|
|
|
// read_me or read_this or similar).
|
2024-05-14 02:25:02 +00:00
|
|
|
constexpr char read[] = "read";
|
2024-05-22 02:12:55 +00:00
|
|
|
constexpr char boot[] = "boot";
|
|
|
|
const auto has = [&](const char *begin, const char *end) {
|
|
|
|
return std::search(
|
2024-05-14 02:25:02 +00:00
|
|
|
file.name.begin(), file.name.end(),
|
2024-05-22 02:12:55 +00:00
|
|
|
begin, end - 1, // i.e. don't compare the trailing NULL.
|
2024-05-14 02:25:02 +00:00
|
|
|
[](char lhs, char rhs) {
|
|
|
|
return std::tolower(lhs) == rhs;
|
|
|
|
}
|
|
|
|
) != file.name.end();
|
2024-05-22 02:12:55 +00:00
|
|
|
};
|
|
|
|
const auto has_read = has(std::begin(read), std::end(read));
|
|
|
|
const auto has_boot = has(std::begin(boot), std::end(boot));
|
2024-05-14 02:25:02 +00:00
|
|
|
|
2024-05-22 02:23:53 +00:00
|
|
|
const auto probability =
|
|
|
|
Numeric::similarity(file.name, adfs_catalogue->name) +
|
|
|
|
Numeric::similarity(file.name, file_name) -
|
|
|
|
((has_read || has_boot) ? 0.2 : 0.0);
|
2024-05-22 01:49:30 +00:00
|
|
|
options.emplace(probability, file.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!options.empty()) {
|
|
|
|
targetArchimedes->main_program = options.begin()->second;
|
2024-05-14 02:25:02 +00:00
|
|
|
}
|
2016-09-19 12:29:23 +00:00
|
|
|
}
|
2016-09-19 01:18:11 +00:00
|
|
|
}
|
2016-09-01 00:24:13 +00:00
|
|
|
|
2021-01-31 18:12:59 +00:00
|
|
|
// Enable the Acorn ADFS if a mass-storage device is attached;
|
|
|
|
// unlike the Pres ADFS it retains SCSI logic.
|
|
|
|
if(!media.mass_storage_devices.empty()) {
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->has_pres_adfs = false; // To override a floppy selection, if one was made.
|
|
|
|
target8bit->has_acorn_adfs = true;
|
2021-02-01 02:41:11 +00:00
|
|
|
|
|
|
|
// Assume some sort of later-era Acorn work is likely to happen;
|
|
|
|
// so ensure *TYPE, etc are present.
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->has_ap6_rom = true;
|
|
|
|
target8bit->has_sideways_ram = true;
|
2021-02-01 02:41:11 +00:00
|
|
|
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->media.mass_storage_devices = media.mass_storage_devices;
|
2021-02-01 02:03:09 +00:00
|
|
|
|
|
|
|
// Check for a boot option.
|
2024-03-04 15:13:57 +00:00
|
|
|
const auto sector = target8bit->media.mass_storage_devices.front()->get_block(1);
|
2021-02-01 02:03:09 +00:00
|
|
|
if(sector[0xfd]) {
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->should_shift_restart = true;
|
2021-02-01 02:03:09 +00:00
|
|
|
} else {
|
2024-03-04 15:13:57 +00:00
|
|
|
target8bit->loading_command = "*CAT\n";
|
2021-02-01 02:03:09 +00:00
|
|
|
}
|
2021-01-31 18:12:59 +00:00
|
|
|
}
|
|
|
|
|
2018-04-14 16:12:12 +00:00
|
|
|
TargetList targets;
|
2024-03-04 15:13:57 +00:00
|
|
|
if(!target8bit->media.empty()) {
|
|
|
|
targets.push_back(std::move(target8bit));
|
|
|
|
}
|
|
|
|
if(!targetArchimedes->media.empty()) {
|
|
|
|
targets.push_back(std::move(targetArchimedes));
|
2018-03-09 21:07:29 +00:00
|
|
|
}
|
2018-04-14 16:12:12 +00:00
|
|
|
return targets;
|
2016-08-30 01:10:38 +00:00
|
|
|
}
|