// // CSL.cpp // Clock Signal // // Created by Thomas Harte on 12/06/2024. // Copyright © 2024 Thomas Harte. All rights reserved. // #include "CSL.hpp" #include #include #include #include #include #include "../../Machines/AmstradCPC/Keyboard.hpp" using namespace Storage::Automation; namespace { bool append_typed(std::vector &down, std::vector &up, std::istringstream &stream) { const auto press = [&](uint16_t key) { CSL::KeyEvent event; event.key = key; event.down = true; down.push_back(event); event.down = false; up.push_back(event); }; const auto shift = [&](uint16_t key) { CSL::KeyEvent event; event.key = AmstradCPC::Key::KeyShift; event.down = true; down.push_back(event); press(key); event.down = false; up.push_back(event); }; const auto next = stream.get(); if(stream.eof()) return false; switch(next) { default: throw CSL::InvalidArgument; case '\'': return false; case '}': return false; case 'A': press(AmstradCPC::Key::KeyA); break; case 'B': press(AmstradCPC::Key::KeyB); break; case 'C': press(AmstradCPC::Key::KeyC); break; case 'D': press(AmstradCPC::Key::KeyD); break; case 'E': press(AmstradCPC::Key::KeyE); break; case 'F': press(AmstradCPC::Key::KeyF); break; case 'G': press(AmstradCPC::Key::KeyG); break; case 'H': press(AmstradCPC::Key::KeyH); break; case 'I': press(AmstradCPC::Key::KeyI); break; case 'J': press(AmstradCPC::Key::KeyJ); break; case 'K': press(AmstradCPC::Key::KeyK); break; case 'L': press(AmstradCPC::Key::KeyL); break; case 'M': press(AmstradCPC::Key::KeyM); break; case 'N': press(AmstradCPC::Key::KeyN); break; case 'O': press(AmstradCPC::Key::KeyO); break; case 'P': press(AmstradCPC::Key::KeyP); break; case 'Q': press(AmstradCPC::Key::KeyQ); break; case 'R': press(AmstradCPC::Key::KeyR); break; case 'S': press(AmstradCPC::Key::KeyS); break; case 'T': press(AmstradCPC::Key::KeyT); break; case 'U': press(AmstradCPC::Key::KeyU); break; case 'V': press(AmstradCPC::Key::KeyV); break; case 'W': press(AmstradCPC::Key::KeyW); break; case 'X': press(AmstradCPC::Key::KeyX); break; case 'Y': press(AmstradCPC::Key::KeyY); break; case 'Z': press(AmstradCPC::Key::KeyZ); break; case ' ': press(AmstradCPC::Key::KeySpace); break; case '0': press(AmstradCPC::Key::Key0); break; case '1': press(AmstradCPC::Key::Key1); break; case '2': press(AmstradCPC::Key::Key2); break; case '3': press(AmstradCPC::Key::Key3); break; case '4': press(AmstradCPC::Key::Key4); break; case '5': press(AmstradCPC::Key::Key5); break; case '6': press(AmstradCPC::Key::Key6); break; case '7': press(AmstradCPC::Key::Key7); break; case '8': press(AmstradCPC::Key::Key8); break; case '9': press(AmstradCPC::Key::Key9); break; case '"': shift(AmstradCPC::Key::Key2); break; case '\\': { if(stream.peek() != '(') { press(AmstradCPC::Key::KeyBackSlash); break; } stream.get(); std::string name; while(stream.peek() != ')') { name.push_back(char(stream.get())); } stream.get(); static const std::unordered_map names = { {"ESC", AmstradCPC::Key::KeyEscape}, {"TAB", AmstradCPC::Key::KeyTab}, {"CAP", AmstradCPC::Key::KeyCapsLock}, {"SHI", AmstradCPC::Key::KeyShift}, {"CTR", AmstradCPC::Key::KeyControl}, {"COP", AmstradCPC::Key::KeyCopy}, {"CLR", AmstradCPC::Key::KeyClear}, {"DEL", AmstradCPC::Key::KeyDelete}, {"RET", AmstradCPC::Key::KeyReturn}, {"ENT", AmstradCPC::Key::KeyEnter}, {"ARL", AmstradCPC::Key::KeyLeft}, {"ARR", AmstradCPC::Key::KeyRight}, {"ARU", AmstradCPC::Key::KeyUp}, {"ARD", AmstradCPC::Key::KeyDown}, {"FN0", AmstradCPC::Key::KeyF0}, {"FN1", AmstradCPC::Key::KeyF1}, {"FN2", AmstradCPC::Key::KeyF2}, {"FN3", AmstradCPC::Key::KeyF3}, {"FN4", AmstradCPC::Key::KeyF4}, {"FN5", AmstradCPC::Key::KeyF5}, {"FN6", AmstradCPC::Key::KeyF6}, {"FN7", AmstradCPC::Key::KeyF7}, {"FN8", AmstradCPC::Key::KeyF8}, {"FN9", AmstradCPC::Key::KeyF9}, //TODO: { } \ ' KOF }; const auto name_pair = names.find(name); if(name_pair == names.end()) { throw CSL::InvalidArgument; } press(name_pair->second); } break; case '{': while(append_typed(down, up, stream)); break; } return true; } } std::vector CSL::parse(const std::string &file_name) { std::vector instructions; std::ifstream file; file.open(file_name); using Type = Instruction::Type; static const std::unordered_map keywords = { {"csl_version", Type::Version}, {"reset", Type::Reset}, {"crtc_select", Type::CRTCSelect}, {"disk_insert", Type::DiskInsert}, {"disk_dir", Type::SetDiskDir}, {"tape_insert", Type::TapeInsert}, {"tape_dir", Type::SetTapeDir}, {"tape_play", Type::TapeInsert}, {"tape_stop", Type::TapeStop}, {"tape_rewind", Type::TapeRewind}, {"snapshot_load", Type::LoadSnapshot}, {"snapshot_dir", Type::SetSnapshotDir}, {"key_delay", Type::KeyDelay}, {"key_output", Type::KeyOutput}, {"key_from_file", Type::KeyFromFile}, {"wait", Type::Wait}, {"wait_driveonoff", Type::WaitDriveOnOff}, {"wait_ssm0000", Type::WaitSSM0000}, {"screenshot_name", Type::SetScreenshotName}, {"screenshot_dir", Type::SetScreenshotDir}, {"screenshot", Type::Screenshot}, {"snapshot_name", Type::SetSnapshotDir}, {"csl_load", Type::LoadCSL}, }; for(std::string line; std::getline(file, line); ) { // Ignore comments and empty lines. if(line.empty() || line[0] == ';') { continue; } std::istringstream stream(line); std::string keyword; stream >> keyword; // Second way for a line to be empty: purely whitespace. if(keyword.empty()) { continue; } const auto key_pair = keywords.find(keyword); if(key_pair == keywords.end()) { throw InvalidKeyword; } Instruction instruction; instruction.type = key_pair->second; // TODO: strings are encoded specially in order to capture whitespace. // They're surrounded in single quotes with some special keys escaped. const auto require = [&](auto &&target) { stream >> target; if(stream.fail()) { throw InvalidArgument; } }; switch(instruction.type) { // Keywords with no argument. case Type::TapePlay: case Type::TapeStop: case Type::TapeRewind: case Type::WaitVsyncOnOff: case Type::WaitSSM0000: break; // Keywords with a single string mandatory argument // that can be directly captured as a string. case Type::Version: { std::string argument; require(argument); instruction.argument = argument; } break; // Keywords with a single string mandatory argument // that is within quotes but otherwise directly usable // as a string. case Type::LoadCSL: case Type::SetScreenshotDir: case Type::SetScreenshotName: case Type::SetSnapshotDir: case Type::SetSnapshotName: case Type::LoadSnapshot: case Type::SetTapeDir: case Type::TapeInsert: case Type::SetDiskDir: case Type::KeyFromFile: { std::string argument; char next; stream >> next; if(next != '\'') { throw InvalidArgument; } while(true) { next = static_cast(stream.get()); if(stream.eof()) break; // Take a bit of a random guess about what's escaped // in regular string arguments. if(next == '\\' && stream.peek() == '(') { stream.get(); if(stream.peek() != '\'') { argument.push_back('\\'); argument.push_back('('); continue; } } if(next == '\'') { break; } argument.push_back(next); } instruction.argument = argument; } break; // Keywords with a single number mandatory argument. case Type::WaitDriveOnOff: case Type::Wait: { uint64_t argument; require(argument); instruction.argument = argument; } break; // Miscellaneous: case Type::Snapshot: case Type::Screenshot: { std::string vsync; stream >> vsync; if(stream.fail()) { instruction.argument = ScreenshotOrSnapshot::Now; break; } if(vsync != "vsync") { throw InvalidArgument; } instruction.argument = ScreenshotOrSnapshot::WaitForVSync; } break; case Type::Reset: { std::string type; stream >> type; if(!stream.fail()) { if(type != "soft" && type != "hard") { throw InvalidArgument; } instruction.argument = (type == "soft") ? Reset::Soft : Reset::Hard; } } break; case Type::CRTCSelect: { std::string type; require(type); static const std::set allowed_types = { "0", "1", "1A", "1B", "2", "3", "4", }; if(allowed_types.find(type) == allowed_types.end()) { throw InvalidArgument; } instruction.argument = static_cast(std::stoi(type)); } break; case Type::DiskInsert: { std::string name; require(name); // Crop the assumed opening and closing quotes. name.erase(name.end() - 1); name.erase(name.begin()); DiskInsert argument; if(name.size() == 1) { argument.drive = toupper(name[0]) - 'A'; require(name); } argument.file = name; instruction.argument = argument; } break; case Type::KeyOutput: { std::vector argument; char next; stream >> next; if(next != '\'') { throw InvalidArgument; } std::vector down; std::vector up; while(append_typed(down, up, stream)) { std::copy(down.begin(), down.end(), std::back_inserter(argument)); std::copy(up.begin(), up.end(), std::back_inserter(argument)); down.clear(); up.clear(); } instruction.argument = argument; } break; case Type::KeyDelay: { KeyDelay argument; require(argument.press_delay); uint64_t interpress_delay; stream >> interpress_delay; if(!stream.fail()) { argument.interpress_delay = argument.press_delay; } uint64_t carriage_return_delay; stream >> carriage_return_delay; if(!stream.fail()) { argument.carriage_return_delay = carriage_return_delay; } instruction.argument = argument; } break; } instructions.push_back(std::move(instruction)); } return instructions; }