diff --git a/src/Chip8.Replay/QuirksCodec.cs b/src/Chip8.Replay/QuirksCodec.cs new file mode 100644 index 0000000..51cfa79 --- /dev/null +++ b/src/Chip8.Replay/QuirksCodec.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Chip8.Core; + +namespace Chip8.Replay; + +public static class QuirksCodec +{ + const int shiftUsesVyBit = 0; + const int jumpWithVxBit = 1; + const int loadStoreIncrementsIBit = 2; + const int resetVfOnLogicBit = 3; + const int clipSpritesBit = 4; + const int addToIndexSetsCarryBit = 5; + + static readonly (string token, int bit)[] tokens = + { + ("shiftVy", shiftUsesVyBit), + ("jumpVx", jumpWithVxBit), + ("incI", loadStoreIncrementsIBit), + ("resetVf", resetVfOnLogicBit), + ("clip", clipSpritesBit), + ("carryI", addToIndexSetsCarryBit), + }; + + public static byte toByte(Quirks quirks) + { + int packed = 0; + if (quirks.shiftUsesVy) packed |= 1 << shiftUsesVyBit; + if (quirks.jumpWithVx) packed |= 1 << jumpWithVxBit; + if (quirks.loadStoreIncrementsI) packed |= 1 << loadStoreIncrementsIBit; + if (quirks.resetVfOnLogic) packed |= 1 << resetVfOnLogicBit; + if (quirks.clipSprites) packed |= 1 << clipSpritesBit; + if (quirks.addToIndexSetsCarry) packed |= 1 << addToIndexSetsCarryBit; + return (byte)packed; + } + + public static Quirks fromByte(byte packed) => new Quirks + { + shiftUsesVy = (packed & (1 << shiftUsesVyBit)) != 0, + jumpWithVx = (packed & (1 << jumpWithVxBit)) != 0, + loadStoreIncrementsI = (packed & (1 << loadStoreIncrementsIBit)) != 0, + resetVfOnLogic = (packed & (1 << resetVfOnLogicBit)) != 0, + clipSprites = (packed & (1 << clipSpritesBit)) != 0, + addToIndexSetsCarry = (packed & (1 << addToIndexSetsCarryBit)) != 0, + }; + + public static string toTokens(Quirks quirks) + { + byte packed = toByte(quirks); + var active = new List(); + foreach (var (token, bit) in tokens) + { + if ((packed & (1 << bit)) != 0) + { + active.Add(token); + } + } + return active.Count == 0 ? "none" : string.Join("+", active); + } + + public static Quirks fromTokens(string text) + { + byte packed = 0; + string trimmed = text.Trim(); + if (trimmed == "none" || trimmed.Length == 0) + { + return fromByte(0); + } + + foreach (string chunk in trimmed.Split('+')) + { + string token = chunk.Trim(); + if (token.Length == 0) + { + continue; + } + + bool matched = false; + foreach (var (candidate, bit) in tokens) + { + if (string.Equals(candidate, token, StringComparison.OrdinalIgnoreCase)) + { + packed |= (byte)(1 << bit); + matched = true; + break; + } + } + + if (!matched) + { + throw new FormatException("unknown quirk token '" + token + "'"); + } + } + + return fromByte(packed); + } + + public static Quirks clone(Quirks quirks) => fromByte(toByte(quirks)); + + public static bool sameFlags(Quirks left, Quirks right) => toByte(left) == toByte(right); +} diff --git a/src/Chip8.Replay/RomLibrary.cs b/src/Chip8.Replay/RomLibrary.cs new file mode 100644 index 0000000..fcca86b --- /dev/null +++ b/src/Chip8.Replay/RomLibrary.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Chip8.Replay; + +public sealed class RomLibrary +{ + readonly string romDirectory; + readonly Dictionary cache = new Dictionary(StringComparer.Ordinal); + + public RomLibrary(string romDirectory) + { + this.romDirectory = romDirectory; + } + + public string directory => romDirectory; + + public static RomLibrary locate() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !Directory.Exists(Path.Combine(dir.FullName, "roms"))) + { + dir = dir.Parent; + } + + if (dir is null) + { + throw new DirectoryNotFoundException("could not locate a 'roms' directory above " + AppContext.BaseDirectory); + } + + return new RomLibrary(Path.Combine(dir.FullName, "roms")); + } + + public bool has(string name) => File.Exists(pathOf(name)); + + public string pathOf(string name) => Path.Combine(romDirectory, name); + + public byte[] load(string name) + { + if (cache.TryGetValue(name, out byte[]? cached)) + { + return cached; + } + + string path = pathOf(name); + if (!File.Exists(path)) + { + throw new FileNotFoundException("rom '" + name + "' not found under " + romDirectory, path); + } + + byte[] bytes = File.ReadAllBytes(path); + cache[name] = bytes; + return bytes; + } + + public IReadOnlyList available() + { + var names = new List(); + if (!Directory.Exists(romDirectory)) + { + return names; + } + + foreach (string path in Directory.GetFiles(romDirectory, "*.ch8")) + { + names.Add(Path.GetFileName(path)); + } + names.Sort(StringComparer.Ordinal); + return names; + } +}