diff --git a/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs b/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs index fcce1118cf..935df8122f 100644 --- a/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs +++ b/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs @@ -99,6 +99,9 @@ namespace BizHawk.Client.ApiHawk case "ZXSpectrum": return CoreSystem.ZXSpectrum; + case "AmstradCPC": + return CoreSystem.AmstradCPC; + case "VB": case "NGP": case "DNGP": @@ -211,6 +214,9 @@ namespace BizHawk.Client.ApiHawk case CoreSystem.ZXSpectrum: return "ZXSpectrum"; + case CoreSystem.AmstradCPC: + return "AmstradCPC"; + default: throw new IndexOutOfRangeException(string.Format("{0} is missing in convert list", value.ToString())); } diff --git a/BizHawk.Client.Common/Api/CoreSystem.cs b/BizHawk.Client.Common/Api/CoreSystem.cs index 9bdc7d8232..d76625b963 100644 --- a/BizHawk.Client.Common/Api/CoreSystem.cs +++ b/BizHawk.Client.Common/Api/CoreSystem.cs @@ -30,6 +30,7 @@ Libretro, VirtualBoy, NeoGeoPocket, - ZXSpectrum + ZXSpectrum, + AmstradCPC } } diff --git a/BizHawk.Client.Common/Global.cs b/BizHawk.Client.Common/Global.cs index db1ccedf94..0969dbc0b6 100644 --- a/BizHawk.Client.Common/Global.cs +++ b/BizHawk.Client.Common/Global.cs @@ -153,6 +153,8 @@ namespace BizHawk.Client.Common return SystemInfo.NeoGeoPocket; case "ZXSpectrum": return SystemInfo.ZXSpectrum; + case "AmstradCPC": + return SystemInfo.AmstradCPC; } } } diff --git a/BizHawk.Client.Common/RomGame.cs b/BizHawk.Client.Common/RomGame.cs index f225c96458..0a69964793 100644 --- a/BizHawk.Client.Common/RomGame.cs +++ b/BizHawk.Client.Common/RomGame.cs @@ -67,7 +67,7 @@ namespace BizHawk.Client.Common RomData = FileData; } else if (file.Extension == ".DSK" || file.Extension == ".TAP" || file.Extension == ".TZX" || - file.Extension == ".PZX" || file.Extension == ".CSW" || file.Extension == ".WAV") + file.Extension == ".PZX" || file.Extension == ".CSW" || file.Extension == ".WAV" || file.Extension == ".CDT") { // these are not roms. unforunately if treated as such there are certain edge-cases // where a header offset is detected. This should mitigate this issue until a cleaner solution is found diff --git a/BizHawk.Client.Common/RomLoader.cs b/BizHawk.Client.Common/RomLoader.cs index 9470689b1d..ef007eeac6 100644 --- a/BizHawk.Client.Common/RomLoader.cs +++ b/BizHawk.Client.Common/RomLoader.cs @@ -25,6 +25,7 @@ using BizHawk.Emulation.DiscSystem; using GPGX64 = BizHawk.Emulation.Cores.Consoles.Sega.gpgx; using BizHawk.Emulation.Cores.Consoles.Sega.Saturn; using BizHawk.Emulation.Cores.Consoles.NEC.PCFX; +using BizHawk.Emulation.Cores.Computers.AmstradCPC; namespace BizHawk.Client.Common { @@ -674,7 +675,22 @@ namespace BizHawk.Client.Common (ZXSpectrum.ZXSpectrumSyncSettings)GetCoreSyncSettings(), Deterministic); break; - case "PSX": + case "AmstradCPC": + + List cpcGI = new List(); + foreach (var a in xmlGame.Assets) + { + cpcGI.Add(new GameInfo { Name = Path.GetFileNameWithoutExtension(a.Key) }); + } + + nextEmulator = new AmstradCPC( + nextComm, + xmlGame.Assets.Select(a => a.Value), //.First(), + cpcGI, // GameInfo.NullInstance, + (AmstradCPC.AmstradCPCSettings)GetCoreSettings(), + (AmstradCPC.AmstradCPCSyncSettings)GetCoreSyncSettings()); + break; + case "PSX": var entries = xmlGame.AssetFullPaths; var discs = new List(); var discNames = new List(); @@ -1023,7 +1039,11 @@ namespace BizHawk.Client.Common Deterministic); nextEmulator = zx; break; - case "GBA": + case "AmstradCPC": + var cpc = new AmstradCPC(nextComm, Enumerable.Repeat(rom.RomData, 1), Enumerable.Repeat(rom.GameInfo, 1).ToList(), GetCoreSettings(), GetCoreSyncSettings()); + nextEmulator = cpc; + break; + case "GBA": if (Global.Config.GBA_UsemGBA) { core = CoreInventory.Instance["GBA", "mGBA"]; diff --git a/BizHawk.Client.Common/SystemInfo.cs b/BizHawk.Client.Common/SystemInfo.cs index 2d65f1729c..dc9e7be92b 100644 --- a/BizHawk.Client.Common/SystemInfo.cs +++ b/BizHawk.Client.Common/SystemInfo.cs @@ -193,14 +193,19 @@ namespace BizHawk.Client.Common /// public static SystemInfo ZXSpectrum { get; } = new SystemInfo("ZX Spectrum", CoreSystem.ZXSpectrum, 2); - #endregion Get SystemInfo + /// + /// Gets the instance for AmstradCPC + /// + public static SystemInfo AmstradCPC { get; } = new SystemInfo("Amstrad CPC", CoreSystem.AmstradCPC, 2); - /// - /// Get a by its - /// - /// you're looking for - /// - public static SystemInfo FindByCoreSystem(CoreSystem system) + #endregion Get SystemInfo + + /// + /// Get a by its + /// + /// you're looking for + /// + public static SystemInfo FindByCoreSystem(CoreSystem system) { return _allSystemInfos.Find(s => s.System == system); } diff --git a/BizHawk.Client.Common/config/PathEntry.cs b/BizHawk.Client.Common/config/PathEntry.cs index 5182bb28d8..9a95354253 100644 --- a/BizHawk.Client.Common/config/PathEntry.cs +++ b/BizHawk.Client.Common/config/PathEntry.cs @@ -301,6 +301,12 @@ namespace BizHawk.Client.Common new PathEntry { System = "ZXSpectrum", SystemDisplayName = "Sinclair ZX Spectrum", Type = "Screenshots", Path = Path.Combine(".", "Screenshots"), Ordinal = 4 }, new PathEntry { System = "ZXSpectrum", SystemDisplayName = "Sinclair ZX Spectrum", Type = "Cheats", Path = Path.Combine(".", "Cheats"), Ordinal = 5 }, + new PathEntry { System = "AmstradCPC", SystemDisplayName = "Amstrad CPC", Type = "Base", Path = Path.Combine(".", "AmstradCPC"), Ordinal = 0 }, + new PathEntry { System = "AmstradCPC", SystemDisplayName = "Amstrad CPC", Type = "ROM", Path = ".", Ordinal = 1 }, + new PathEntry { System = "AmstradCPC", SystemDisplayName = "Amstrad CPC", Type = "Savestates", Path = Path.Combine(".", "State"), Ordinal = 2 }, + new PathEntry { System = "AmstradCPC", SystemDisplayName = "Amstrad CPC", Type = "Screenshots", Path = Path.Combine(".", "Screenshots"), Ordinal = 4 }, + new PathEntry { System = "AmstradCPC", SystemDisplayName = "Amstrad CPC", Type = "Cheats", Path = Path.Combine(".", "Cheats"), Ordinal = 5 }, + new PathEntry { System = "PSX", SystemDisplayName = "Playstation", Type = "Base", Path = Path.Combine(".", "PSX"), Ordinal = 0 }, new PathEntry { System = "PSX", SystemDisplayName = "Playstation", Type = "ROM", Path = ".", Ordinal = 1 }, new PathEntry { System = "PSX", SystemDisplayName = "Playstation", Type = "Savestates", Path = Path.Combine(".", "State"), Ordinal = 2 }, diff --git a/BizHawk.Client.Common/movie/PlatformFrameRates.cs b/BizHawk.Client.Common/movie/PlatformFrameRates.cs index 40ad9b22ff..11b25b77a8 100644 --- a/BizHawk.Client.Common/movie/PlatformFrameRates.cs +++ b/BizHawk.Client.Common/movie/PlatformFrameRates.cs @@ -61,17 +61,18 @@ namespace BizHawk.Client.Common ["C64_DREAN"] = PALNCarrier * 2 / 7 / 312 / 65, ["INTV"] = 59.92, - ["ZXSpectrum_PAL"] = 50.080128205 + ["ZXSpectrum_PAL"] = 50.080128205, + ["AmstradCPC_PAL"] = 50.08012820512821, - // according to ryphecha, using - // clocks[2] = { 53.693182e06, 53.203425e06 }; //ntsc console, pal console - // lpf[2][2] = { { 263, 262.5 }, { 314, 312.5 } }; //ntsc,pal; noninterlaced, interlaced - // cpl[2] = { 3412.5, 3405 }; //ntsc mode, pal mode - // PAL PS1: 0, PAL Mode: 0, Interlaced: 0 --- 59.826106 (53.693182e06/(263*3412.5)) - // PAL PS1: 0, PAL Mode: 0, Interlaced: 1 --- 59.940060 (53.693182e06/(262.5*3412.5)) - // PAL PS1: 1, PAL Mode: 1, Interlaced: 0 --- 49.761427 (53.203425e06/(314*3405)) - // PAL PS1: 1, PAL Mode: 1, Interlaced: 1 --- 50.000282(53.203425e06/(312.5*3405)) - }; + // according to ryphecha, using + // clocks[2] = { 53.693182e06, 53.203425e06 }; //ntsc console, pal console + // lpf[2][2] = { { 263, 262.5 }, { 314, 312.5 } }; //ntsc,pal; noninterlaced, interlaced + // cpl[2] = { 3412.5, 3405 }; //ntsc mode, pal mode + // PAL PS1: 0, PAL Mode: 0, Interlaced: 0 --- 59.826106 (53.693182e06/(263*3412.5)) + // PAL PS1: 0, PAL Mode: 0, Interlaced: 1 --- 59.940060 (53.693182e06/(262.5*3412.5)) + // PAL PS1: 1, PAL Mode: 1, Interlaced: 0 --- 49.761427 (53.203425e06/(314*3405)) + // PAL PS1: 1, PAL Mode: 1, Interlaced: 1 --- 50.000282(53.203425e06/(312.5*3405)) + }; public double this[string systemId, bool pal] { diff --git a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj index 10e149c0a6..ad52178d1d 100644 --- a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj +++ b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj @@ -199,6 +199,30 @@ BizBoxInfoControl.cs + + Form + + + AmstradCPCCoreEmulationSettings.cs + + + Form + + + AmstradCPCAudioSettings.cs + + + Form + + + AmstradCPCPokeMemory.cs + + + Form + + + AmstradCPCNonSyncSettings.cs + Component @@ -1298,6 +1322,18 @@ BizBoxInfoControl.cs + + AmstradCPCCoreEmulationSettings.cs + + + AmstradCPCAudioSettings.cs + + + AmstradCPCPokeMemory.cs + + + AmstradCPCNonSyncSettings.cs + AnalogRangeConfigControl.cs diff --git a/BizHawk.Client.EmuHawk/FileLoader.cs b/BizHawk.Client.EmuHawk/FileLoader.cs index bbe8b9d62e..b130bb5c1b 100644 --- a/BizHawk.Client.EmuHawk/FileLoader.cs +++ b/BizHawk.Client.EmuHawk/FileLoader.cs @@ -51,7 +51,7 @@ namespace BizHawk.Client.EmuHawk return new[] { ".NES", ".FDS", ".UNF", ".SMS", ".GG", ".SG", ".GB", ".GBC", ".GBA", ".PCE", ".SGX", ".BIN", ".SMD", ".GEN", ".MD", ".SMC", ".SFC", ".A26", ".A78", ".LNX", ".COL", ".ROM", ".M3U", ".CUE", ".CCD", ".SGB", ".Z64", ".V64", ".N64", ".WS", ".WSC", ".XML", ".DSK", ".DO", ".PO", ".PSF", ".MINIPSF", ".NSF", - ".EXE", ".PRG", ".D64", "*G64", ".CRT", ".TAP", ".32X", ".MDS", ".TZX", ".PZX", ".CSW", ".WAV" + ".EXE", ".PRG", ".D64", "*G64", ".CRT", ".TAP", ".32X", ".MDS", ".TZX", ".PZX", ".CSW", ".WAV", ".CDT" }; } diff --git a/BizHawk.Client.EmuHawk/MainForm.Designer.cs b/BizHawk.Client.EmuHawk/MainForm.Designer.cs index 5a7a619df1..746d1ebd37 100644 --- a/BizHawk.Client.EmuHawk/MainForm.Designer.cs +++ b/BizHawk.Client.EmuHawk/MainForm.Designer.cs @@ -393,6 +393,15 @@ this.ZXSpectrumDisksSubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.zxt2ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.ZXSpectrumExportSnapshotMenuItemMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.amstradCPCToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.amstradCPCCoreEmulationSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.AmstradCPCAudioSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.AmstradCPCPokeMemoryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.AmstradCPCMediaToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.AmstradCPCTapesSubMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.cpct1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.AmstradCPCDisksSubMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.cpcd1ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.Atari7800HawkCoreMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.MainStatusBar = new StatusStripEx(); this.DumpStatusButton = new System.Windows.Forms.ToolStripDropDownButton(); @@ -465,6 +474,7 @@ this.ShowMenuContextMenuSeparator = new System.Windows.Forms.ToolStripSeparator(); this.ShowMenuContextMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.timerMouseIdle = new System.Windows.Forms.Timer(this.components); + this.AmstradCPCNonSyncSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.MainformMenu.SuspendLayout(); this.MainStatusBar.SuspendLayout(); this.MainFormContextMenu.SuspendLayout(); @@ -503,7 +513,8 @@ this.virtualBoyToolStripMenuItem, this.neoGeoPocketToolStripMenuItem, this.zXSpectrumToolStripMenuItem, - this.HelpSubMenu}); + this.HelpSubMenu, + this.amstradCPCToolStripMenuItem}); this.MainformMenu.LayoutStyle = System.Windows.Forms.ToolStripLayoutStyle.Flow; this.MainformMenu.Location = new System.Drawing.Point(0, 0); this.MainformMenu.Name = "MainformMenu"; @@ -3498,6 +3509,79 @@ this.ZXSpectrumExportSnapshotMenuItemMenuItem.Size = new System.Drawing.Size(159, 22); this.ZXSpectrumExportSnapshotMenuItemMenuItem.Text = "Export Snapshot"; this.ZXSpectrumExportSnapshotMenuItemMenuItem.Click += new System.EventHandler(this.ZXSpectrumExportSnapshotMenuItemMenuItem_Click); + // + // amstradCPCToolStripMenuItem + // + this.amstradCPCToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.amstradCPCCoreEmulationSettingsToolStripMenuItem, + this.AmstradCPCAudioSettingsToolStripMenuItem, + this.AmstradCPCNonSyncSettingsToolStripMenuItem, + this.AmstradCPCPokeMemoryToolStripMenuItem, + this.AmstradCPCMediaToolStripMenuItem}); + this.amstradCPCToolStripMenuItem.Name = "amstradCPCToolStripMenuItem"; + this.amstradCPCToolStripMenuItem.Size = new System.Drawing.Size(90, 19); + this.amstradCPCToolStripMenuItem.Text = "Amstrad CPC"; + // + // amstradCPCCoreEmulationSettingsToolStripMenuItem + // + this.amstradCPCCoreEmulationSettingsToolStripMenuItem.Name = "amstradCPCCoreEmulationSettingsToolStripMenuItem"; + this.amstradCPCCoreEmulationSettingsToolStripMenuItem.Size = new System.Drawing.Size(201, 22); + this.amstradCPCCoreEmulationSettingsToolStripMenuItem.Text = "Core Emulation Settings"; + this.amstradCPCCoreEmulationSettingsToolStripMenuItem.Click += new System.EventHandler(this.amstradCPCCoreEmulationSettingsToolStripMenuItem_Click); + // + // AmstradCPCAudioSettingsToolStripMenuItem + // + this.AmstradCPCAudioSettingsToolStripMenuItem.Name = "AmstradCPCAudioSettingsToolStripMenuItem"; + this.AmstradCPCAudioSettingsToolStripMenuItem.Size = new System.Drawing.Size(201, 22); + this.AmstradCPCAudioSettingsToolStripMenuItem.Text = "Audio Settings"; + this.AmstradCPCAudioSettingsToolStripMenuItem.Click += new System.EventHandler(this.AmstradCPCAudioSettingsToolStripMenuItem_Click); + // + // AmstradCPCPokeMemoryToolStripMenuItem + // + this.AmstradCPCPokeMemoryToolStripMenuItem.Name = "AmstradCPCPokeMemoryToolStripMenuItem"; + this.AmstradCPCPokeMemoryToolStripMenuItem.Size = new System.Drawing.Size(201, 22); + this.AmstradCPCPokeMemoryToolStripMenuItem.Text = "POKE Memory"; + this.AmstradCPCPokeMemoryToolStripMenuItem.Click += new System.EventHandler(this.AmstradCPCPokeMemoryToolStripMenuItem_Click); + // + // AmstradCPCMediaToolStripMenuItem + // + this.AmstradCPCMediaToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.AmstradCPCTapesSubMenu, + this.AmstradCPCDisksSubMenu}); + this.AmstradCPCMediaToolStripMenuItem.Name = "AmstradCPCMediaToolStripMenuItem"; + this.AmstradCPCMediaToolStripMenuItem.Size = new System.Drawing.Size(201, 22); + this.AmstradCPCMediaToolStripMenuItem.Text = "Media"; + this.AmstradCPCMediaToolStripMenuItem.DropDownOpened += new System.EventHandler(this.AmstradCPCMediaToolStripMenuItem_DropDownOpened); + // + // AmstradCPCTapesSubMenu + // + this.AmstradCPCTapesSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.cpct1ToolStripMenuItem}); + this.AmstradCPCTapesSubMenu.Name = "AmstradCPCTapesSubMenu"; + this.AmstradCPCTapesSubMenu.Size = new System.Drawing.Size(105, 22); + this.AmstradCPCTapesSubMenu.Text = "Tapes"; + this.AmstradCPCTapesSubMenu.DropDownOpened += new System.EventHandler(this.AmstradCPCTapesSubMenu_DropDownOpened); + // + // cpct1ToolStripMenuItem + // + this.cpct1ToolStripMenuItem.Name = "cpct1ToolStripMenuItem"; + this.cpct1ToolStripMenuItem.Size = new System.Drawing.Size(103, 22); + this.cpct1ToolStripMenuItem.Text = "cpct1"; + // + // AmstradCPCDisksSubMenu + // + this.AmstradCPCDisksSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.cpcd1ToolStripMenuItem}); + this.AmstradCPCDisksSubMenu.Name = "AmstradCPCDisksSubMenu"; + this.AmstradCPCDisksSubMenu.Size = new System.Drawing.Size(105, 22); + this.AmstradCPCDisksSubMenu.Text = "Disks"; + this.AmstradCPCDisksSubMenu.DropDownOpened += new System.EventHandler(this.AmstradCPCDisksSubMenu_DropDownOpened); + // + // cpcd1ToolStripMenuItem + // + this.cpcd1ToolStripMenuItem.Name = "cpcd1ToolStripMenuItem"; + this.cpcd1ToolStripMenuItem.Size = new System.Drawing.Size(106, 22); + this.cpcd1ToolStripMenuItem.Text = "cpcd1"; // // Atari7800HawkCoreMenuItem // @@ -4125,6 +4209,13 @@ this.timerMouseIdle.Interval = 2000; this.timerMouseIdle.Tick += new System.EventHandler(this.TimerMouseIdle_Tick); // + // AmstradCPCNonSyncSettingsToolStripMenuItem + // + this.AmstradCPCNonSyncSettingsToolStripMenuItem.Name = "AmstradCPCNonSyncSettingsToolStripMenuItem"; + this.AmstradCPCNonSyncSettingsToolStripMenuItem.Size = new System.Drawing.Size(201, 22); + this.AmstradCPCNonSyncSettingsToolStripMenuItem.Text = "Non-Sync Settings"; + this.AmstradCPCNonSyncSettingsToolStripMenuItem.Click += new System.EventHandler(this.AmstradCPCNonSyncSettingsToolStripMenuItem_Click); + // // MainForm // this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; @@ -4589,17 +4680,27 @@ private System.Windows.Forms.ToolStripMenuItem SMSControllerLightPhaserToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem SMSControllerSportsPadToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem SMSControllerKeyboardToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem zXSpectrumToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem ZXSpectrumControllerConfigurationMenuItem; - private System.Windows.Forms.ToolStripMenuItem ZXSpectrumCoreEmulationSettingsMenuItem; - private System.Windows.Forms.ToolStripMenuItem ZXSpectrumNonSyncSettingsMenuItem; - private System.Windows.Forms.ToolStripMenuItem ZXSpectrumAudioSettingsMenuItem; - private System.Windows.Forms.ToolStripMenuItem ZXSpectrumPokeMemoryMenuItem; - private System.Windows.Forms.ToolStripMenuItem ZXSpectrumMediaMenuItem; - private System.Windows.Forms.ToolStripMenuItem ZXSpectrumTapesSubMenu; - private System.Windows.Forms.ToolStripMenuItem ZXSpectrumDisksSubMenu; - private System.Windows.Forms.ToolStripMenuItem zxt1ToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem zxt2ToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem ZXSpectrumExportSnapshotMenuItemMenuItem; + private System.Windows.Forms.ToolStripMenuItem zXSpectrumToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem ZXSpectrumControllerConfigurationMenuItem; + private System.Windows.Forms.ToolStripMenuItem ZXSpectrumCoreEmulationSettingsMenuItem; + private System.Windows.Forms.ToolStripMenuItem ZXSpectrumNonSyncSettingsMenuItem; + private System.Windows.Forms.ToolStripMenuItem ZXSpectrumAudioSettingsMenuItem; + private System.Windows.Forms.ToolStripMenuItem ZXSpectrumPokeMemoryMenuItem; + private System.Windows.Forms.ToolStripMenuItem ZXSpectrumMediaMenuItem; + private System.Windows.Forms.ToolStripMenuItem ZXSpectrumTapesSubMenu; + private System.Windows.Forms.ToolStripMenuItem ZXSpectrumDisksSubMenu; + private System.Windows.Forms.ToolStripMenuItem zxt1ToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem zxt2ToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem ZXSpectrumExportSnapshotMenuItemMenuItem; + private System.Windows.Forms.ToolStripMenuItem amstradCPCToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem amstradCPCCoreEmulationSettingsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem AmstradCPCAudioSettingsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem AmstradCPCPokeMemoryToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem AmstradCPCMediaToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem AmstradCPCTapesSubMenu; + private System.Windows.Forms.ToolStripMenuItem cpct1ToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem AmstradCPCDisksSubMenu; + private System.Windows.Forms.ToolStripMenuItem cpcd1ToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem AmstradCPCNonSyncSettingsToolStripMenuItem; } } diff --git a/BizHawk.Client.EmuHawk/MainForm.Events.cs b/BizHawk.Client.EmuHawk/MainForm.Events.cs index 39cb20e235..24eda1315d 100644 --- a/BizHawk.Client.EmuHawk/MainForm.Events.cs +++ b/BizHawk.Client.EmuHawk/MainForm.Events.cs @@ -2,6 +2,7 @@ using System; using System.Drawing; using System.IO; using System.Windows.Forms; +using System.Collections.Generic; using BizHawk.Emulation.Common; using BizHawk.Emulation.Common.IEmulatorExtensions; @@ -26,7 +27,7 @@ using BizHawk.Client.ApiHawk; using BizHawk.Emulation.Cores.Computers.Commodore64; using BizHawk.Emulation.Cores.Nintendo.Gameboy; using BizHawk.Emulation.Cores.Computers.SinclairSpectrum; -using System.Collections.Generic; +using BizHawk.Emulation.Cores.Computers.AmstradCPC; namespace BizHawk.Client.EmuHawk { @@ -2601,6 +2602,101 @@ namespace BizHawk.Client.EmuHawk #endregion + #region AmstradCPC + + private void amstradCPCCoreEmulationSettingsToolStripMenuItem_Click(object sender, EventArgs e) + { + new AmstradCPCCoreEmulationSettings().ShowDialog(); + } + + private void AmstradCPCAudioSettingsToolStripMenuItem_Click(object sender, EventArgs e) + { + new AmstradCPCAudioSettings().ShowDialog(); + } + + private void AmstradCPCPokeMemoryToolStripMenuItem_Click(object sender, EventArgs e) + { + new AmstradCPCPokeMemory().ShowDialog(); + } + + private void AmstradCPCMediaToolStripMenuItem_DropDownOpened(object sender, EventArgs e) + { + if (Emulator is AmstradCPC) + { + AmstradCPCTapesSubMenu.Enabled = ((AmstradCPC)Emulator)._tapeInfo.Count > 0; + AmstradCPCDisksSubMenu.Enabled = ((AmstradCPC)Emulator)._diskInfo.Count > 0; + } + } + + private void AmstradCPCTapesSubMenu_DropDownOpened(object sender, EventArgs e) + { + AmstradCPCTapesSubMenu.DropDownItems.Clear(); + + if (Emulator is AmstradCPC) + { + var ams = (AmstradCPC)Emulator; + var currSel = ams._machine.TapeMediaIndex; + + for (int i = 0; i < ams._tapeInfo.Count; i++) + { + string name = ams._tapeInfo[i].Name; + + var menuItem = new ToolStripMenuItem + { + Name = i + "_" + name, + Text = i + ": " + name, + Checked = currSel == i + }; + + int dummy = i; + menuItem.Click += (o, ev) => + { + ams._machine.TapeMediaIndex = dummy; + }; + + AmstradCPCTapesSubMenu.DropDownItems.Add(menuItem); + } + } + } + + private void AmstradCPCDisksSubMenu_DropDownOpened(object sender, EventArgs e) + { + AmstradCPCDisksSubMenu.DropDownItems.Clear(); + + if (Emulator is AmstradCPC) + { + var ams = (AmstradCPC)Emulator; + var currSel = ams._machine.DiskMediaIndex; + + for (int i = 0; i < ams._diskInfo.Count; i++) + { + string name = ams._diskInfo[i].Name; + + var menuItem = new ToolStripMenuItem + { + Name = i + "_" + name, + Text = i + ": " + name, + Checked = currSel == i + }; + + int dummy = i; + menuItem.Click += (o, ev) => + { + ams._machine.DiskMediaIndex = dummy; + }; + + AmstradCPCDisksSubMenu.DropDownItems.Add(menuItem); + } + } + } + + private void AmstradCPCNonSyncSettingsToolStripMenuItem_Click(object sender, EventArgs e) + { + new AmstradCPCNonSyncSettings().ShowDialog(); + } + + #endregion + #region Help private void HelpSubMenu_DropDownOpened(object sender, EventArgs e) diff --git a/BizHawk.Client.EmuHawk/MainForm.cs b/BizHawk.Client.EmuHawk/MainForm.cs index 5055177fc3..02c6c1026b 100644 --- a/BizHawk.Client.EmuHawk/MainForm.cs +++ b/BizHawk.Client.EmuHawk/MainForm.cs @@ -1735,6 +1735,7 @@ namespace BizHawk.Client.EmuHawk neoGeoPocketToolStripMenuItem.Visible = false; pCFXToolStripMenuItem.Visible = false; zXSpectrumToolStripMenuItem.Visible = false; + amstradCPCToolStripMenuItem.Visible = false; switch (system) { @@ -1839,6 +1840,9 @@ namespace BizHawk.Client.EmuHawk #else ZXSpectrumExportSnapshotMenuItemMenuItem.Visible = false; #endif + break; + case "AmstradCPC": + amstradCPCToolStripMenuItem.Visible = true; break; } } @@ -2101,7 +2105,7 @@ namespace BizHawk.Client.EmuHawk if (VersionInfo.DeveloperBuild) { return FormatFilter( - "Rom Files", "*.nes;*.fds;*.unf;*.sms;*.gg;*.sg;*.pce;*.sgx;*.bin;*.smd;*.rom;*.a26;*.a78;*.lnx;*.m3u;*.cue;*.ccd;*.mds;*.exe;*.gb;*.gbc;*.gba;*.gen;*.md;*.32x;*.col;*.int;*.smc;*.sfc;*.prg;*.d64;*.g64;*.crt;*.tap;*.sgb;*.xml;*.z64;*.v64;*.n64;*.ws;*.wsc;*.dsk;*.do;*.po;*.vb;*.ngp;*.ngc;*.psf;*.minipsf;*.nsf;*.tzx;*.pzx;*.csw;*.wav;%ARCH%", + "Rom Files", "*.nes;*.fds;*.unf;*.sms;*.gg;*.sg;*.pce;*.sgx;*.bin;*.smd;*.rom;*.a26;*.a78;*.lnx;*.m3u;*.cue;*.ccd;*.mds;*.exe;*.gb;*.gbc;*.gba;*.gen;*.md;*.32x;*.col;*.int;*.smc;*.sfc;*.prg;*.d64;*.g64;*.crt;*.tap;*.sgb;*.xml;*.z64;*.v64;*.n64;*.ws;*.wsc;*.dsk;*.do;*.po;*.vb;*.ngp;*.ngc;*.psf;*.minipsf;*.nsf;*.tzx;*.pzx;*.csw;*.wav;*.cdt;%ARCH%", "Music Files", "*.psf;*.minipsf;*.sid;*.nsf", "Disc Images", "*.cue;*.ccd;*.mds;*.m3u", "NES", "*.nes;*.fds;*.unf;*.nsf;%ARCH%", @@ -2130,6 +2134,7 @@ namespace BizHawk.Client.EmuHawk "Virtual Boy", "*.vb;%ARCH%", "Neo Geo Pocket", "*.ngp;*.ngc;%ARCH%", "Sinclair ZX Spectrum", "*.tzx;*.tap;*.dsk;*.pzx;*.csw;*.wav;%ARCH%", + "Amstrad CPC", "*.cdt;*.dsk;%ARCH%", "All Files", "*.*"); } @@ -2781,9 +2786,14 @@ namespace BizHawk.Client.EmuHawk { var core = (Emulation.Cores.Computers.SinclairSpectrum.ZXSpectrum)Emulator as Emulation.Cores.Computers.SinclairSpectrum.ZXSpectrum; CoreNameStatusBarButton.ToolTipText = core.GetMachineType(); + } - } - } + if (Emulator.SystemId == "AmstradCPC") + { + var core = (Emulation.Cores.Computers.AmstradCPC.AmstradCPC)Emulator as Emulation.Cores.Computers.AmstradCPC.AmstradCPC; + CoreNameStatusBarButton.ToolTipText = core.GetMachineType(); + } + } private void ToggleKeyPriority() { diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCAudioSettings.Designer.cs b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCAudioSettings.Designer.cs new file mode 100644 index 0000000000..2b11c1ee0c --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCAudioSettings.Designer.cs @@ -0,0 +1,185 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class AmstradCPCAudioSettings + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AmstradCPCAudioSettings)); + this.OkBtn = new System.Windows.Forms.Button(); + this.CancelBtn = new System.Windows.Forms.Button(); + this.label1 = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.panTypecomboBox1 = new System.Windows.Forms.ComboBox(); + this.lblBorderInfo = new System.Windows.Forms.Label(); + this.tapeVolumetrackBar = new System.Windows.Forms.TrackBar(); + this.label3 = new System.Windows.Forms.Label(); + this.label5 = new System.Windows.Forms.Label(); + this.ayVolumetrackBar = new System.Windows.Forms.TrackBar(); + ((System.ComponentModel.ISupportInitialize)(this.tapeVolumetrackBar)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.ayVolumetrackBar)).BeginInit(); + this.SuspendLayout(); + // + // OkBtn + // + this.OkBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.OkBtn.Location = new System.Drawing.Point(247, 245); + this.OkBtn.Name = "OkBtn"; + this.OkBtn.Size = new System.Drawing.Size(60, 23); + this.OkBtn.TabIndex = 3; + this.OkBtn.Text = "&OK"; + this.OkBtn.UseVisualStyleBackColor = true; + this.OkBtn.Click += new System.EventHandler(this.OkBtn_Click); + // + // CancelBtn + // + this.CancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.CancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.CancelBtn.Location = new System.Drawing.Point(313, 245); + this.CancelBtn.Name = "CancelBtn"; + this.CancelBtn.Size = new System.Drawing.Size(60, 23); + this.CancelBtn.TabIndex = 4; + this.CancelBtn.Text = "&Cancel"; + this.CancelBtn.UseVisualStyleBackColor = true; + this.CancelBtn.Click += new System.EventHandler(this.CancelBtn_Click); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 14); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(140, 13); + this.label1.TabIndex = 17; + this.label1.Text = "Amstrad CPC Audio Settings"; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(12, 172); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(135, 13); + this.label2.TabIndex = 23; + this.label2.Text = "AY-3-8912 Panning Config:"; + // + // panTypecomboBox1 + // + this.panTypecomboBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.panTypecomboBox1.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.panTypecomboBox1.FormattingEnabled = true; + this.panTypecomboBox1.Location = new System.Drawing.Point(12, 188); + this.panTypecomboBox1.Name = "panTypecomboBox1"; + this.panTypecomboBox1.Size = new System.Drawing.Size(157, 21); + this.panTypecomboBox1.TabIndex = 22; + // + // lblBorderInfo + // + this.lblBorderInfo.Font = new System.Drawing.Font("Lucida Console", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.lblBorderInfo.Location = new System.Drawing.Point(175, 181); + this.lblBorderInfo.Name = "lblBorderInfo"; + this.lblBorderInfo.Size = new System.Drawing.Size(196, 37); + this.lblBorderInfo.TabIndex = 24; + this.lblBorderInfo.Text = "Selects a particular panning configuration for the 3ch AY-3-8912 Programmable Sou" + + "nd Generator"; + this.lblBorderInfo.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // tapeVolumetrackBar + // + this.tapeVolumetrackBar.Location = new System.Drawing.Point(12, 60); + this.tapeVolumetrackBar.Maximum = 100; + this.tapeVolumetrackBar.Name = "tapeVolumetrackBar"; + this.tapeVolumetrackBar.Size = new System.Drawing.Size(359, 45); + this.tapeVolumetrackBar.TabIndex = 25; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(12, 44); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(73, 13); + this.label3.TabIndex = 26; + this.label3.Text = "Tape Volume:"; + // + // label5 + // + this.label5.AutoSize = true; + this.label5.Location = new System.Drawing.Point(12, 108); + this.label5.Name = "label5"; + this.label5.Size = new System.Drawing.Size(98, 13); + this.label5.TabIndex = 30; + this.label5.Text = "AY-3-8912 Volume:"; + // + // ayVolumetrackBar + // + this.ayVolumetrackBar.Location = new System.Drawing.Point(12, 124); + this.ayVolumetrackBar.Maximum = 100; + this.ayVolumetrackBar.Name = "ayVolumetrackBar"; + this.ayVolumetrackBar.Size = new System.Drawing.Size(359, 45); + this.ayVolumetrackBar.TabIndex = 29; + // + // AmstradCPCAudioSettings + // + this.AcceptButton = this.OkBtn; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.CancelBtn; + this.ClientSize = new System.Drawing.Size(385, 280); + this.Controls.Add(this.label5); + this.Controls.Add(this.ayVolumetrackBar); + this.Controls.Add(this.label3); + this.Controls.Add(this.tapeVolumetrackBar); + this.Controls.Add(this.lblBorderInfo); + this.Controls.Add(this.label2); + this.Controls.Add(this.panTypecomboBox1); + this.Controls.Add(this.label1); + this.Controls.Add(this.CancelBtn); + this.Controls.Add(this.OkBtn); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.Name = "AmstradCPCAudioSettings"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Audio Settings"; + this.Load += new System.EventHandler(this.IntvControllerSettings_Load); + ((System.ComponentModel.ISupportInitialize)(this.tapeVolumetrackBar)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.ayVolumetrackBar)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button OkBtn; + private System.Windows.Forms.Button CancelBtn; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.ComboBox panTypecomboBox1; + private System.Windows.Forms.Label lblBorderInfo; + private System.Windows.Forms.TrackBar tapeVolumetrackBar; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.Label label5; + private System.Windows.Forms.TrackBar ayVolumetrackBar; + } +} \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCAudioSettings.cs b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCAudioSettings.cs new file mode 100644 index 0000000000..1cd8a83fec --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCAudioSettings.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Windows.Forms; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Cores.Computers.AmstradCPC; +using System.Text; + +namespace BizHawk.Client.EmuHawk +{ + public partial class AmstradCPCAudioSettings : Form + { + private AmstradCPC.AmstradCPCSettings _settings; + + public AmstradCPCAudioSettings() + { + InitializeComponent(); + } + + private void IntvControllerSettings_Load(object sender, EventArgs e) + { + _settings = ((AmstradCPC)Global.Emulator).GetSettings().Clone(); + + // AY panning config + var panTypes = Enum.GetNames(typeof(AY38912.AYPanConfig)); + foreach (var val in panTypes) + { + panTypecomboBox1.Items.Add(val); + } + panTypecomboBox1.SelectedItem = _settings.AYPanConfig.ToString(); + + // tape volume + tapeVolumetrackBar.Value = _settings.TapeVolume; + + // ay volume + ayVolumetrackBar.Value = _settings.AYVolume; + + + } + + private void OkBtn_Click(object sender, EventArgs e) + { + bool changed = + _settings.AYPanConfig.ToString() != panTypecomboBox1.SelectedItem.ToString() + || _settings.TapeVolume != tapeVolumetrackBar.Value + || _settings.AYVolume != ayVolumetrackBar.Value; + + if (changed) + { + _settings.AYPanConfig = (AY38912.AYPanConfig)Enum.Parse(typeof(AY38912.AYPanConfig), panTypecomboBox1.SelectedItem.ToString()); + + _settings.TapeVolume = tapeVolumetrackBar.Value; + _settings.AYVolume = ayVolumetrackBar.Value; + + GlobalWin.MainForm.PutCoreSettings(_settings); + + DialogResult = DialogResult.OK; + Close(); + } + else + { + DialogResult = DialogResult.OK; + Close(); + } + } + + private void CancelBtn_Click(object sender, EventArgs e) + { + GlobalWin.OSD.AddMessage("Misc settings aborted"); + DialogResult = DialogResult.Cancel; + Close(); + } + } +} diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCAudioSettings.resx b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCAudioSettings.resx new file mode 100644 index 0000000000..ca821b54f8 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCAudioSettings.resx @@ -0,0 +1,624 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAwAMDAQAAAABABoBgAAxgAAACAgEAAAAAQA6AIAAC4HAAAYGBAAAAAEAOgBAAAWCgAAEBAQAAAA + BAAoAQAA/gsAADAwAAAAAAgAqA4AACYNAAAgIAAAAAAIAKgIAADOGwAAGBgAAAAACADIBgAAdiQAABAQ + AAAAAAgAaAUAAD4rAAAwMAAAAAAgAKglAACmMAAAICAAAAAAIACoEAAATlYAABgYAAAAACAAiAkAAPZm + AAAQEAAAAAAgAGgEAAB+cAAAKAAAADAAAABgAAAAAQAEAAAAAACABAAAAAAAAAAAAAAQAAAAEAAAAAAA + AAAAAIAAAIAAAACAgACAAAAAgACAAICAAACAgIAAwMDAAAAA/wAA/wAAAP//AP8AAAD/AP8A//8AAP// + /wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAHR3AAAAAAAAAAAAAAAAAAAAAAAAAAAAdHdEcAAAAAAAAAAAAAAAAA + AAAAAAAAAHd0d3QAAAAAAAAAAAAAAAAAAAAAAAAAAEd8d3UAAAAAAAAAAAAAAAAAAAAAAAAAB3yHfHZw + AAAAAAAAAAAAAAAAAAAAAAAAd3fIyHVwAAAAAAAAAAAAAAAAAAAAAAAAfHh3jIxwAAAAAAAAAAAAAAAA + AAAAAAAHd8jIyHdgAAAAAAAAAAAAAAAAAAAAAAAHd4yHfIdAAAAAAAAAAAAAAAAAAAAAAAAHyMjIyMhQ + AAAAAAAAAAAAAAAAAAAAAAB3d3eMh4dgAAAAAAAAAAAAAAAAAAAAAAB8jIyIfIdQAAAAAAAAAAAAAAAA + AAAAAAB3h4jIiMh3AAAAAAAAAAAAAAAAAAAAAAB8jIeHeIjHAAAAAAAAAAAAAAAAAAAAAAeIiHh4eMiE + AAAAAAAAAAAAB0dHcAAAAAd8h4eIiIiHcAAAAAAAAAB0d3d3RwAAAAeIeIiIiIh3RwAAAAAAAHR3d8h3 + dAAAAAfIh4iIiHiIx0cAAAAAdHh3eIeHhwAAAAeHiIiIiIiId3R3dHR0eHd4h4eHhAAAAAd4eIiIiIiH + x3d2d3eId4iIiIiIhwAAAAd4eIiI+IiIh3d3eHh3iIiIiIeHwAAAAAfIjHeIiIiIyIeHh4iIiIiIiIiI + cAAAAAeIQ0R3h3iIiMiIiIiIiIiIiIiEAAAAAAfIR3d3d0iIiIh4iIeIiIiIiHhAAAAAAAB4d3d3SHiI + h4fTiIi3iIiIeIwAAAAAAAB3h4d3eIeIiHiJiIuIiIh4jHAAAAAAAAAHyId3h3h4iIh4iIiIiIiHeAAA + AAAAAAAAB8iMiMjIiIiIh4h3aMjHAAAAAAAAAAAAAAdYyIeIiIiMjId6d4eAAAAAAAAAAAAAAAAHdsjH + eIeH6MiId3AAAAAAAAAAAAAAAIiIh4V8jIh4eIfHcAAAAAAAAAAAAACIiIh3AAAHd3h3fHcAAAAAAAAA + AAAAAAiIjHgAAAAAAHx8eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///////8AAP///////wAA////////AAD///////8AAP///////wAA//////// + AAD///////8AAP///////wAA//h/////AAD/4D////8AAP/AP////wAA/8A/////AAD/gB////8AAP8A + H////wAA/wAf////AAD+AB////8AAP4AH////wAA/gAf////AAD8AB////8AAPwAH////wAA/AAP//// + AAD8AA////8AAPgAD//+BwAA+AAH//ADAAD4AAP/wAMAAPgAAP8AAwAA+AAAAAADAAD4AAAAAAMAAPgA + AAAABwAA+AAAAAAHAAD4AAAAAA8AAPgAAAAAHwAA/AAAAAA/AAD8AAAAAH8AAP4AAAAA/wAA/4AAAAP/ + AAD/4AAAB/8AAP/4AAAf/wAA/8AAAH//AAD8A+AD//8AAPgP/A///wAA////////AAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAAoAAAAIAAAAEAAAAABAAQAAAAAAAAC + AAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAICAgADAwMAAAAD/AAD/ + AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdwAAAAAAAAAAAAAAAA + AAd0dAAAAAAAAAAAAAAAAAB3x3cAAAAAAAAAAAAAAAAAd3fHcAAAAAAAAAAAAAAAB3yMh3AAAAAAAAAA + AAAAAAfIeMdwAAAAAAAAAAAAAAAHjIyHQAAAAAAAAAAAAAAAfId4yHAAAAAAAAAAAAAAAHjIyIdQAAAA + AAAAAAAAAAB3iId4YAAAAAAAAAdwAAAAjIiIiIUAAAAAAHd3dAAAB4iIiHh8cAAAAHd3x4dwAAd4iIiI + h3Z3d3R3yIh4cAAHh4iIiIfHd3d4iIiIh3AAB3jHiIiIiHeHiIiIiIwAAAh3dXh4iMiIiIiIiIhwAAAA + yGd0d4iIeIi4iIiMAAAAAIeHd4iIh32IiIiIcAAAAAAAd4jIyIiIiHeHyAAAAAAAAAB3h4iIh8h3dwAA + AAAAAAAIh8fIh4eIaAAAAAAAAACIiHAAB8jIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////// + ////////////////////n////g////wP///8B///+Af///gH///4B///8Af///AH///wB//n8AP/A+AB + /AHgAAAB4AAAAeAAAAPgAAAH8AAAD/AAAB/8AAA//wAA//4AA//weA////////////////////////// + //8oAAAAGAAAADAAAAABAAQAAAAAACABAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAgAAAgAAAAICAAIAA + AACAAIAAgIAAAICAgADAwMAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHRwAAAAAAAAAAAAB3dAAAAAAAAAAAAA + d8dwAAAAAAAAAAAAfId3AAAAAAAAAAAHeMjHAAAAAAAAAAAHyHh3AAAAAAAAAAAHh3eEAAAAAAAAAAAI + yIiHAAAAAHd2cAAIiIiIQAAAd3d4UACHiIiId3d3eHiIcACHh4iIyHeHiIiIcAAIR3d4iIiIiIiMAAAH + d3eIh3iIiIhwAAAAeMh4iIiHiMAAAAAAAHfIiMh4aAAAAAAAiIgHyIfIAAAAAAAIgAAAAIAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8A////AP///wD8f/8A+H//APB/ + /wDwP/8A4D//AOA//wDgP/8A4D/BAOAfAQDAAAEAwAABAOAAAwDgAAcA8AAfAPwAPwDwgP8A5/f/AP// + /wD///8A////ACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACA + AAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAd1AAAAAAAAB8cAAAAAAAB4eAAAAAAAAHyMgAAAAAAAiIhwAAAHcACI + iHcAd3hwAIz4jIeIiIAAd3eIiIiIAACHeIiIiHAAAACMeMh4AAAAiAAIgAAAAAAAAAAAAAAAAAAAAAAA + AAD//wAA//8AAP//AADj/wAA4/8AAMP/AADB/wAAwfkAAMDBAADAAQAAwAMAAMAHAADwDwAAzn8AAP// + AAD//wAAKAAAADAAAABgAAAAAQAIAAAAAAAACQAAAAAAAAAAAAAAAQAAAAEAAAAAAAA9OzsAZD8/AGg8 + PABtPj4AQkNDAEZIRwBWQkIAV0REAF5AQABbRkYAVklJAFxPTwBTU1MAXFJSAF5ZWQBkQEAAYUREAGZF + RQBqQkEAYEtLAGNPTwBwQUEAfUZGAHJKSgB2SUkAfU9PAGBRUQBgVFQAZlZWAGZYWABqWVkAclZWAHpU + VAB9W1oAbmJiAGtoaABtaWkAcWdnAHdnZwB8Y2MAe2pqAHJxcQB+dHQAd3l5AHl6egCGT08AiU9PAIFP + UACGU1MAjVFRAIlWVgCMV1cAg1xbAIxaWQCQUlIAlVJSAJFXVgCXVVUAmVVVAJZaWQCSXV0AlV9eAJpZ + WgCeW1sAml5eAKBZWgCgXFwAql9fAIRmZQCIZWQAhWtrAI5ragCTYmEAnGBhAJ9kYwCaZmYAk25uAJ1s + awCFdHQAiXd3AIt+fgCWd3cAmHR0AJV5eQCbfHwAo2JhAKZhYQChZWUApGVkAKplZACsZGQAqmhnAKZr + agCnbGsAqmloAKlubQCsbW0AtGZnALhsbACxb3AAv29wAKVxcACrc3IAr35+ALN0cwC5c3MAvXBxALR4 + dgC1fHsAunt6AMNtbgDGb3AAw3FyAMZwcQDGdXUAyHR1AMp3eADBeXkAxnt7AMB/fgDLensANLBSAEWf + TgBBtFwAPMdnADHkdgDciiIAvoF/AISrdwDln0sA35lhAN2XfADgmmEA8LdlAO61cAArWPIALWT+AEh5 + +gDOf4AAfoCAAHiA1ABZv9wAZrnUAGK+2ABxnv4Ad6P/ADPX/QBw0OcAW+D7AIKEgwCPgoIAjI2NAJuC + ggCUiIgAmYqKAJGSkgCjhIQAqoKCAKKLiwC+hIMAsoqKALaSgQCum5sAsZubALqqlQCdgr4Ar6ytALGh + oAC6pKQAwoSDAMyBggDGiIYAyYiHAMWMigDMjIoA0ISFANKHiADUjIwA2Y6NAMCUjQDIk44A0JCPANaP + kADHlZQAzpSSAMScmwDUkpIA2ZSVANWYlgDampcA2ZeYANWcnADam5sA4p2cAMChjwDeoJ4A5aCFAOaj + jQDlpJoA2p6hAMOkowDOoaEAy62tANegoADdoqEA2aGpANGsrwDdq6kAwbG4ANGysQDdtLQA2ri3AOGk + owDjqKYA66ylAOGnqADjq6oA6a2rAOOwrwDssK4A5K+wAOaztADttLIA57i2AO24tgDmurgA6rq6APC1 + swDyuLYA9Ly5APi+uwD1wL0A+cC9AKKMwACkk8QAqprMALSayACptsEAlaDkAOy/wACRxtQAgOv9AJnr + 9wDEwsoA5sbGAOzCwgDuyMcA7MzMAPPEwgDxy8oA9dPTAPja2gAAAAAAAAAAAP///wAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AAAAAAAAAADLytDi9tOemQAAAAAAUy9EecLEsa1uPTUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPj11Mme + VakAAAAAAAAAAAAATS84M0akAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///////8AAP///////wAA////////AAD///////8AAP///////wAA//////// + AAD///////8AAP///////wAA//h/////AAD/4D////8AAP/AP////wAA/8A/////AAD/gB////8AAP8A + H////wAA/wAf////AAD+AB////8AAP4AH////wAA/gAf////AAD8AB////8AAPwAH////wAA/AAP//// + AAD8AA////8AAPgAD//+BwAA+AAH//ADAAD4AAP/wAMAAPgAAP8AAwAA+AAAAAADAAD4AAAAAAMAAPgA + AAAABwAA+AAAAAAHAAD4AAAAAA8AAPgAAAAAHwAA/AAAAAA/AAD8AAAAAH8AAP4AAAAA/wAA/4AAAAP/ + AAD/4AAAB/8AAP/4AAAf/wAA/8AAAH//AAD8A+AD//8AAPgP/A///wAA////////AAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAAoAAAAIAAAAEAAAAABAAgAAAAAAAAE + AAAAAAAAAAAAAAABAAAAAQAAAAAAAFFNTQBRUlIAU1RUAGJHRwBiT08Aa0lIAGJTUwBrVlYAYllZAGZc + XABpWloAb1xbAHNTUwB7V1YAc1hXAHFbWwBkZWUAaWFhAG5kZABpamkAcGFhAHlubgB2cHAAf3V1AH55 + eQB8fX0AgUpKAI1PTwCLWFcAhlhYAI9ZWQCKXFsAm1ZWAJJZWQCWWVgAmlpbAJtcWwCiXFwAl2BfAIBg + YACAZ2YAgG9vAI9oaACWZWQAmGBhAJ5kZACcaWoAmm9vAIV0dACNcHAAiXZ2AIB8fACac3IAm3V0AJ51 + dQCZfHwAnHx8AKNmZgCnZmYAqmJiAK5jYwCvb24AtWVmALBtbgC5bW0AvmxtAKx+fQCxcnIAtHBwALZz + dACydXQAtnd2ALlwcAC5dnYAt3p5ALh5eAC8fHsAun18ALx+fQDGb3AAxnBxAMdzdADAd3YAyHJzAMlz + dADJdXYAynd4AMd/fwDMe3wAzXx9AHunbwBhvHIAYsN4ANuLOwC2hn4A4Zt5APC3ZABte9sAX47+AHWM + 5QAl0foAY+P8AIeDgwCFhoYAioSEAJOIiACWi4sAmpKRAKGCgQCmhYUAqYGBAKuDhACniooApYyMAKiO + jQCyhYMAvoWEALeNjQCrj5AAr5eXALSVlAC9lJMAmbCEAK6RugDBgYAAwoSCAMWDhADChoQAxYeFAM6A + gQDFiIYAxoqIAMqIiQDMi4oAy4yKAMiPjQDPj44A0ISFANKJigDUi4wA04+NANWNjgDKkY8A0JCOANud + iQDWj5AAzJSTAM2XlgDGm5oA1pGSANOUkgDVl5EA1pOUANiVlgDYmJUA2ZeYANKenADbmpsA3pmYANuc + mgDbn5wA1aacAN6gngDqqZoA3Z+gAMyjowDCra0AxqysAMqpqQDboaAA3qKiAN6logDbp6UA3aWkANer + qgDWsbMA0rW0ANe0tADfs7IA4aSiAOGlpQDkp6UA46imAOWopgDsraIA6qimAOGoqADhrqwA6a2rAOqv + rADpsK4A7LGuAOGzswDlsbEA7bKxAO+1sgDotrYA5rm3AO+4twDot7sA6bq5AOu9uwDrv70A8bazAPG2 + tADxuLUA9Lm2APC9uwD2vboA9L+9APi+uwD4v7wA8sC+APXAvgD5wL0AkILJAKqXzACsu8cAqr/LALLV + 3QDawMIA48XFAOvDwQDswMAA7cTDAO/ExQDgxsgA8cbEAPTGxADwyskA9MvJAPLNzQD21dYA+NjZAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAMEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqHCEcBQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAayU9PSYbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdQlBSQiJpAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAM0pSUlJQPRcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnUlJSUlJGFQAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAFJSUlJSUkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzUlJSWVJZfxAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAC5XWYqKioqGDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASoqMkpqa + mqAsAAAAAAAAAAAAAAAAAABoNAAAAAAAAACMjJyuvLy2toYHAAAAAAAAAAAAABcOIDouBgAAAAAAc4yc + tsHKysPAriIKAAAAAAAAABYgRk1LTX+DEAAAAABukqXB4ejo4dHPQCIEChcXEwggTXV/k66unKMpAAAA + AG6Srsro6ero0dN/Rk1NRk2Dg4STrsbh4cHAt2sAAAAAbpKuOXPe6ajW15KGg4OGk528yuHo5eHPz882 + AAAAAAB4jCkDAxSoMabXt5yjt8ro3ePo5dbT09HTdAAAAAAAAABGcBFoGgFwdtfDwHxi2dpmZcrX09HP + z0MAAAAAAAAAAHh/qWwaOa6cz9PNZGPYsdzbzc3DwLk2AAAAAAAAAAAAAAAvhpKakoyg19HNyKS5wHtb + orZ/cwAAAAAAAAAAAAAAAAAANkaKWVm5zb1gYV6cXVxfNgAAAAAAAAAAAAAAAAAAALGvlTIuP1K5tqCR + l4xfLwAAAAAAAAAAAAAAAAAAsbPBenkAAAAAcCVYjE0scwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////////////////////+f///+D////A////wH + ///4B///+Af///gH///wB///8Af///AH/+fwA/8D4AH8AeAAAAHgAAAB4AAAA+AAAAfwAAAP8AAAH/wA + AD//AAD//gAD//B4D////////////////////////////ygAAAAYAAAAMAAAAAEACAAAAAAAQAIAAAAA + AAAAAAAAAAEAAAABAAAAAAAAWlJSAHBJSQB1SEgAe1dXAHdYWAB5WlkAel1dAGBiYgB1bGwAfWtrAHh2 + dgB9fn4Ag01NAIRXVwCIV1cAhV9eAItbWgCgX14ApV1dAJhgXwCNYGAAnWtqAJhtbQCCdnYAh3x8AI15 + eACeensAqGBgAKhoZwCga2oArGpqALNqagCzb28AtG1tALltbQCxb3AApnVzAKlzcwCqdHMApnp6AKd+ + fgCpensAq3x7ALZ3dgC8dHQAvH59AMZvcADGcHEAxXN0AMhycwDJdncAynh5AMx5egDNfn8Ajo1wAOek + VgDGgH8A4p53AEZ2+gB8u4AAd8PaAIuEhACOh4cAjo6OAJ+DggCejo4Ao4SEAKSIiACsi4sAqo2MAK6P + jgC+gYAAvoaGAL+KiACskJAAtJeXALWenQC5np4At6iOAKmyjgC9nroAwYSDAMaGhADOhoYAxomHAMiK + iQDJjYwA0oeIANOOjwDUjY0A2ZiPANaPkADGkZEAx5eXAMySkADGnZwA1ZOSANeTlADWl5YA2JSVANGZ + mADan50A3J6dAOCcmwDVoJ8A7K2fAMOtrQDXo6IA3aCgAN+kpADVq6oA3ay3AMu0tADPtrYA3L+/AOCi + oQDhpqUA5KelAOinpgDlq6gA46usAOOvrQDqrqwA7LGuAOayswDjtrQA5re1AOqysQDts7EA57y6AO+8 + ugDrvL0A8LOwAPC1sgDwtrQA87q3APS6twD2vboA8b69APi/vAD2wb4A+cC9AJmTzwDHqMMAu8PMAIHf + 8QDByNAA7cLCAO3FwwDvxsQA5cjIAOzOzgDwxcQA9cbEAPPP0AD10tIAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + BQMJAAAAAAAAAAAAAAAAAAAAAAAAAAAPHBMNAAAAAAAAAAAAAAAAAAAAAAAAABojLy8TAAAAAAAAAAAA + AAAAAAAAAAAAAB0wMDAiPgAAAAAAAAAAAAAAAAAAAAAAQjAwMDAtGAAAAAAAAAAAAAAAAAAAAAAAFzIy + NTU5CgAAAAAAAAAAAAAAAAAAAAAAIjZYWFxcBwAAAAAAAAAAAAAAAAAAAAAANlxtdW11JQAAAAAAAAAA + PgcRDgkAAAAAXG1/lISAZgMAAAAAABkVLC5SVhcAAABNY3WWnJuLfB8UBAcQHkhWaX91dSsAAABNY2BM + mJeCiVJSVl9laX+WloSJgEIAAAAAXAEIC0tGjnR0dJaRk5qNjIyJQwAAAAAAJkNADBtdjIaPO1GSPYuJ + hnVEAAAAAAAAAClISWRcd4xwkGp8UE90VwAAAAAAAAAAAAAAKSQ1NYZ7OjhbPDdGAAAAAAAAAAAAAHNv + YGsAKyJoXFYmRwAAAAAAAAAAAAAAcnIAAAAAAAAATgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP// + /wD///8A////APx//wD4f/8A8H//APA//wDgP/8A4D//AOA//wDgP8EA4B8BAMAAAQDAAAEA4AADAOAA + BwDwAB8A/AA/APCA/wDn9/8A////AP///wD///8AKAAAABAAAAAgAAAAAQAIAAAAAAAAAQAAAAAAAAAA + AAAAAQAAAAEAAAAAAABjZGQAdmRjAHtpaQB/eHgAgU9PAKBaWgCFbm0AlWtqAKptbgCwZ2cAsGhoAKxw + cACteHkAvnJyAMZvcADGcHEAy3l5AMx9fgCFmXQAwIB/ANeUfQDhoX8AlIqJAJWMjACYiIgAoIaGAK2K + igCxh4cAvoGAALKKigC4iYgAuJWVAL2cnACss50AuqKhAL+mpgDLgoIAxImHAMeNjADLkI8AxpWTANCS + kQDYlZUA1J6dANqZmgDdnp4A1J+oAMaiogDOr68AzLKyANi5uADhpaIA4qypAOWtqADrrqsA4bKwAOay + sgDtuLYA57++AOy4uADxtLIA8be0APa9ugDswL4A9sG+ALCcxwC5ncIA06zBALnH0QC2ytQA7sPDAPLS + 0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAZBgUAAAAAAAAAAAAAAAAACw8KAAAAAAAAAAAAAAAAGhAQDgAAAAAAAAAAAAAAAAkRESUYAAAA + AAAAAAAAAAAlKy4uBwAAAAAAAAcDAAAAKzlHPCYCAAAYCB0oKgAAAC0wSDs0FB0nLDlAOiwAAAANAQQb + Pi9DRkVBPzUAAAAAJB4cKz5EQjMiNSkAAAAAAAAAHwwRNxYVEyQAAAAAAAAxMgAAACEgAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP//AAD//wAA4/8AAOP/AADD/wAAwf8AAMH5 + AADAwQAAwAEAAMADAADABwAA8A8AAM5/AAD//wAA//8AACgAAAAwAAAAYAAAAAEAIAAAAAAAgCUAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAkAAAAJAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAUAAAAOAEBAVUAAABUAAAANQAAABAAAAABAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAkFBSUvGRl5TCkpwlYuLtxDJCTQFw0NmQAA + AEkAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACGAwMKE8rK6V6RET2klJR/5ZS + U/+OT0//ZDc38B0QEJoAAAAyAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYDAwYVzAwoopP + T/ygXVz/oFtb/55ZWf+bWFf/k1NT/1UvL9wGAwNcAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AARNKipxhk5O+adkY/+uZWX/tWdo/7VmZ/+qYWH/nltb/3hERPcfERGCAAAAFgAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAADEZGS1zQ0LXqGdm/7ptbf/Fb3D/x3Bx/8hwcf/BbW7/q2Vl/4hPT/82HR2gAAAAIAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAB1gxMYyYXl3/vXFx/8Zwcf/HcHH/x3Bx/8dwcf/HcHH/uG1t/5NY + V/9EJia2AAAAKQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPB8fNH1MS+K4cnH/x3Fy/8dwcf/HcHH/x3Bx/8dw + cf/HcHH/wHBx/51gX/9PLCzGAAAAMwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AAMAAAAAAAAAAAAAAAAAAAAALhAQFIZXVs/RjIz/1Y2O/9qYmP/eoaL/46qr/+aysv/ot7f/6rm5/+m4 + uf/otbX/5q+v/+uvrf/jqab/wYeF/28/P/QhEhKvAAAAXwAAACgAAAANAAAABQAAAAMAAAACAAAAAwAA + AAUAAAAKAAAAFQAAADAdDg9oSSkptHZHRu2dYmL+t3Z1/758e/+6enn/tnh3/7d5eP+8fn3/w4SD/7Z6 + ef9eODfbBgICTgAAAAgAAAAAAAAAAAAAAAAAAAAAPhwcJJVjYuPXkZH/2JOU/92fn//iqqr/57O0/+u8 + vP/uwsL/78XG/+/Exf/twMD/67i4/+60sv/wtrP/zZKQ/5taWv9xQED2MRsaxAgEBIcAAABaAAAAQQAA + ADcAAAA2AAAAOwAAAEUEAgJZHA4OfUcnJ7l5SkntqGxr/8CAfv/DgoH/vH59/7p+ff/DiIb/zZGP/9GT + kf/UlJP/1peV/9eZl/+GVlbuGQsLVwAAAAcAAAAAAAAAAAAAAAAAAAAARiIiLZ9rauvZk5P/2peY/+Ck + pP/lsLD/6ru7/+/Fxf/yzMz/9NDQ//PPz//xycr/7sDA//K5tv/1u7j/36Kg/6dmZf+mZWX/j1ZW/WM6 + OutDJSXQNBwcvDAaGrQ0HBy1PiIivUwsLMtkPDzfh1VU9a1xcP/EhIP/xIWE/7+Cgf/Ch4b/zZST/9mk + ov/grq3/4a6t/96lo//eoJ7/36Kg/+Cjof+IWVjnGwwMQwAAAAIAAAAAAAAAAAAAAAAAAAAARyQkL6Br + auzZk5P/25qb/+GnqP/ntLT/7cDA//LLy//209T/+NjY//fX1//00ND/8cbG//W9u//4vrz/46ak/7d0 + c/+vb27/s3Jy/7d2df+ucXD/pWpp/6Npaf+nbWz/sHVz/7p9fP/EhYT/yImI/8WIhv/DiIb/ypGP/9eg + n//hr63/57q5/+rCwP/rwsD/6bq4/+evrf/nq6n/6q6r/9qgnv9wRkbDBwAAHgAAAAAAAAAAAAAAAAAA + AAAAAAAASCQkLZ1nZuvYkpP/25uc/+Opqv/qtrf/7cHB//TOzv/52Nj/+tzc//na2v/xz9D/8MfH//fA + vv/6wb7/6a6r/8OBgP/DgoD/vX58/7h7ev+8fn3/woOC/8aHhv/HiYj/xoqJ/8aLif/Ijoz/zZST/9eg + nv/hrav/6Lm3/+zCwf/uyMf/78nH/+/Dwf/uvLr/7ba0/+60sf/vtLL/8ri1/7J+fflMKSltAAAABAAA + AAAAAAAAAAAAAAAAAAAAAAAAQyEhI5JcXOPWj5D/3Juc/8qVlf+BZmb/bl5e/4l4eP/AqKj/8tPT//LO + zv+5p6b/w6qq//fBv//7wr//8LWy/86Ojf/Ojoz/0ZGP/9GSkP/OkY//zpOR/9GamP/VoJ//2qel/+Gv + rf/nt7X/6727/+3Dwf/wycf/8czL//LLyf/yxsT/8cC+//G7uf/yubf/87m3//S7uP/4vrv/1J6c/3JH + RrAdCgsWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANRcXEYJNTcvPiIn/15aW/2VNTf85Ojr/Q0VF/0JF + RP9dXFz/n5GR/+S/v/+bh4f/hXp6/+25uP/7wr//9bu4/9qcmv/Zmpj/252b/96gnf/ipKH/5q+s/+u+ + vP/vycf/8srI/+3Hxv/wysj/9c7M//TNy//0ysj/9MbE//TBv//1vrz/9r26//e9u//4vrv/+L+8//vB + vv/hqqf/g1ZVzDwcHC4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAW4+Ppq/env/05OT/2ZX + V/9rbm7/fX9//3l6ev99f3//cHJy/5F9ff+ff3//XFhY/9eop//8wr//+L+8/+Wppv/ipaP/5qil/96i + pP/Kmaz/1qi1//LGxP/tyMf/qb3J/23E3P9kw9//vMTN//jDwP/3wb//+MC9//i/vf/5v73/+b+8//i/ + vP/3vrv/+L68/92mo/+IWlnRRSMjOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFcv + L0mbX1/y15GS/6GAgP9XV1b/iYuL/4CBgf98fX3/cnR0/1dPT/++j4//km9w/9Sfnv/6wL3/+cC9/+6z + sP/ssK3/0Z+u/4OH1P9YffD/QGPs/7KYyv/Ct7z/Ytrz/3Ts//8s2f//cbvU//m+u//4v7z/+L67//e9 + uv/1vLn/9Lq3//O5tv/zuLX/0puZ/4RVVctGIyM4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAADIXFwdrPDySq2ts/diZmf/ApKT/sKur/4CBgP95enr/iYiI/49zdP/do6P/36Ch/96e + nv/zuLX/+sK///W7uP/1ubT/qZC//2qY+/9tnf//MGT6/56FxP/esK//nMbS/57n8/9+z+T/ybG3//a6 + t//zubb/8re0//C1s//utLH/7rKw/+qvrP++iIb9dklJtkMgISoAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABHIyMSazw8kZ5hYvXNjI3/2aSk/7OMjP+bd3f/sIKC/9KV + lv/cnJz/2peY/9aRkf/koqL/+sG+//nAvf/5v7z/4amw/6qZx/+aouP/qpvP/+mxtv/2urj/6rGv/+S6 + u//ptrX/466n/+Ovqf/ssK7/6q6s/+isqv/oq6n/2J2b/6JubfFoPT2NOxoaFwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOBoaCFowMFd7SEjAomZm9sWC + gv/XkZL/25SV/9iSk//Wj5D/1IyN/9KHiP/UiIj/8bOx//rCv//3vbv/9ru4//O3s//xuLX/7q6e/+ej + hf/npIn/7bCp/+Otp/+KsX3/ULdm/1WjWv+7oYz/5KWk/9uenP+4gH79glJRzVYuLlQgCAkGAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAA8HBwQVy4uS3FBQaCPV1fjsG5v/cmAgf/ShYb/0YKD/85+f//LeXr/2I2M//e8uf/1vLn/7rOx/+2y + sP/lpJX/5qFY/+6xXP/djS3/35h9/86gl/9SwW7/Nd90/0WxXP+vlH//wYSE/49cW+VlOTmBQR4eHAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAGk7OhqIWFd8oG5u8J5qav+eX2D/tmts/8Z0df/KdHX/yXJz/92T + k//3vLn/7LGu/+Snpf/dm5L/4Z1q/+61dP/fmmX/15WM/9eYlv/Bm43/r6uR/6uNgP+WYWDtbkBAnUwn + JzQVAQECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiFJSBnhC + QgpqNDQJWSUlB08dHQdfKisKfENDFJJWViinbGtRvYOCjtOcm8/pt7X157y6/7eOjfhxRUW7aTk5m4RK + StehWlr6uGdo/8Zwcf/dkpH/8bSx/+OnpP/YmZj/1ZWT/9ealP/Vl5X/0JCP/8eIhv+zdnb/lFtc6nA/ + QKRSKio/JQwNBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADTn6AB2qioDMuUlCHBhYU8voCAWcCBgXTEhoaLzZGQqdeensngrKvn47Sz/NOop/+yiIfyi2Bgs2k+ + PlZXKysPAAAAAUYlJRxcMTFYcj4+pYpMTeWmXF3+xnl5/9+Zl//dnJr/z46M/8KCgf+vc3L/ll9e831L + S8hlOTl/TigoMy0REQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABzQUIDnmprDriGhifHlpZMzp6eeNCgoZ7On5+2yJqaybuPj9WnfHzVj2RkunVJ + SYNbLy8/PRQUCgAAAAAAAAAAAAAAAAAAAAAAAAAAKRUVBU0pKSphNDRtd0BAsotNTd2ZW1vrkVlY4HtJ + Sb5lOTmCUysrQTsbGxEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWCwsA2Y4OA5xQkImdkhIRHhKSll0R0dibUBAWWI2 + NkNUKCgoOhISDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMhkZB0km + Jh5LJiYsRSEhITATFAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAD/+H////8AAP/gH////wAA/8Af//// + AAD/gA////8AAP+AD////wAA/wAP////AAD/AA////8AAP4AB////wAA/gAH////AAD8AAf///8AAPwA + B////wAA/AAH////AAD8AAf///8AAPgAB////wAA+AAH//4HAAD4AAP/8AEAAPgAAf/AAQAA8AAA/wAA + AADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAEAAPAAAAAAAQAA8AAAAAADAADwAAAAAAcAAPAA + AAAADwAA+AAAAAAfAAD4AAAAAD8AAPwAAAAAfwAA/gAAAAD/AAD/gAAAA/8AAP/gAAAH/wAAgAAAAB// + AAAAAAAAf/8AAAAD4AP//wAAgB/8H///AAD///////8AAP///////wAA////////AAD///////8AAP// + /////wAA////////AAAoAAAAIAAAAEAAAAABACAAAAAAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAYAAAAZAAAAGQAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAARCQkYOh8fb0ooKK80HByiCQUFTAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAIhERFmA2Np2ITUz3lVNT/4dLS/5IKCi9AAAALwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAANjODiBllhY+61kZP+vY2P/pV5e/3xHRvEhEhJfAAAAAgAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAASSgoN41VVeS6bW3/xW9w/8dwcf+9bG3/klZW/jogIIEAAAAGAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZ1RkWcs2xs/8dxcv/HcHH/x3Bx/8Zwcf+iYWH/SSkpmAAA + AAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUC0tMZtgX+fGcnP/x3Bx/8dwcf/HcHH/x3Fy/61q + av9UMTGqAAAAEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxRER1tm9v/8hxcv/HcHH/x3Bx/8dw + cf/HcnP/tnRz/185OboAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAACIxXV7TEdHT/yHJz/8l1 + dv/Kd3j/ynd4/8p4eP/Bf37/bURDywAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNKysjo2Zm4Mt4 + ef/NfH3/z4GC/9GFhf/RhYb/0YWF/82Mi/9+UVHeCAICOwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAJAAAACwAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAGc+ + Pkm1c3P30IGC/9OJiv/XkZL/2ZaW/9mWl//YlJX/2JmY/5hnZfMeEBBrAAAABwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAA0FAgItHhAQWzAbG4IqFxeHDQcHWwAAABkAAAAAAAAAAAAA + AAAAAAAAek1MdMN/f//VjI3/2piZ/9+io//hqKn/4qmp/+Clpf/jpqT/wImH/04xMLwAAAA6AAAABQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAABEbDg5GRygokW5CQs+MVlbxnGJh/JdfXvxnPz7hHA8PbgAA + AAwAAAAAAAAAAAAAAACMW1qbz4qK/9qXl//gpqb/5rKz/+q6u//rvLz/6La2/+qxr//epKL/j1lZ+DUc + HLACAQFPAAAAHQAAAA8AAAAPAAAAEwAAACIbDg5MVDExnYZUU+SpbWz+uXl4/7x+fP/AgoD/xoeF/72A + f/9fOzu1AAAAHAAAAAAAAAAAAAAABJhkZK/VkZH/3Z+g/+axsf/twMD/8svL//LNzf/vxcX/8Lq4/+6z + sf+1dHP/j1VU+144N9g7IiKqMhwclDcfH5RGKSmiYTw7v4tZWOiydXT+woOC/8aKiP/Ol5X/2aWj/9ui + of/cnpz/2pyb/35TUrgAAAAVAAAAAAAAAAAAAAAFmmVkstaTk//hpaX/7Lm6//TLy//419f/+NnZ//TP + z//1wb//9Lq3/8aGhP+1dHP/s3Rz/6xwb/+pb27+rnNy/7Z7ev/BhIL/yY2L/8+WlP/apqT/5be2/+vB + v//rvrz/6bKw/+uvrf/Um5n/bUVEgAAAAAMAAAAAAAAAAAAAAAOTXV2q1ZGR/9CYmP+dfX7/o4yM/9e8 + vP/z0tL/zLOz/+u8u//5v7z/1peV/8uLif/Ki4r/yoyL/86Ukv/TnJv/2qSi/+Gtq//nuLb/7cPB//DJ + x//xxsT/8b+9//G6t//zubf/77az/6d1dM89Hx8lAAAAAAAAAAAAAAAAAAAAAIJOTojNiIn/jGlp/01O + Tv9UVlb/dnNz/7uhof+Pfn7/xJ+e//zCv//lqKb/3J2b/+Chnv/hpaT/7Ly5/+vHxv/MxMn/0MjN//LK + yf/1x8X/9sLA//a/vP/3vrv/+L+8//S7uP+5hoXhYTo5RwAAAAAAAAAAAAAAAAAAAAAAAAAAaTs7RrVz + dPKmfn7/cXJx/4SGhv97fX3/b2Zm/516ev+7kJD/+sG+//C2s//lqqr/rpbA/3aB2/+ql83/tMHK/2jc + 9P9OzOz/2r3B//q/vP/3vrv/9ry6//a8uf/ss7D/tYGA32c+Pk0AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAvEhIHg01Njbp9fvrCn5//nI+P/4R7ev+fgID/2Jyd/9ybnP/ytrT/+b+8/+ewtf+Mld3/ZI36/5eI + zv/Ttrn/sNLc/6/Czv/stLT/8re0/++0sf/tsq//2qCe/6Rxb8phODg+AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABCIB8MeUZGbqRpata8gYH8x4mJ/9eTk//YkpP/04qL/+Cbmv/5wL3/9726/+Sw + t//Zrrn/56qY/+2smf/lr6n/nLWJ/4Gtdf/Pppn/3qGf/7yEg/KJWViYTyoqIAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQh0dGXJAQGOXXl7NtnR1/8V7fP/MfH3/znt8/+il + o//0urj/7LCu/+Whg//rq13/35VX/9Kek/9yvXz/ZbNv/6iCdfqYY2O/aj4+TCUJCgcAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAACcamsBjFRVB4FERAh9PT0JjU1ND6VnZx+/hINF0JqZiNOjoty0iIf2hFBQw5lX + V8+wY2P4xXR0/+aioP/oq6j/2pqT/92fif/Vlor/yYqJ/7N8efiVZmPGdERFYkEfHxIAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAALiFhgXFkJEdx5CQSMqSknbNlZWbz5uaws2cnOXBlJPnqH18r4dc + XFFULy8OSCUlFm07O0+FSUmeoV1d3sF9fPrGhoX/snZ295xkZNiFUlKbbD4+T0UdHxIAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc0JDA5FgYRKdbm46onR0Zp9ycnuWampzhFlZVmY6 + OikvDAwHAAAAAAAAAAAAAAAAAAAAAB0ODgRULCwhbjo7UXhERGVrPDxHTCYmGxAAAQMAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP//////////////////////D////gf///wH///4A///+AP///AD///wA///8AP//+AD + ///gA//D4AH+AeAA+ADgAAAAwAAAAMAAAADAAAAB4AAAA+AAAAfgAAAP8AAAH/wAAD8AAAD/AAAD/wB4 + D//H////////////////////KAAAABgAAAAwAAAAAQAgAAAAAABgCQAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAABMAAAAtAAAAEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAgIO1cwMM1qOjrsHhAQmwAA + ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAATCgogfUhI6ahgYP6lXV3+f0hI9wIBAT0AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsGBgFPLy6kuW1t/sZv + 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 + + + \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCCoreEmulationSettings.Designer.cs b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCCoreEmulationSettings.Designer.cs new file mode 100644 index 0000000000..359f6f2dba --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCCoreEmulationSettings.Designer.cs @@ -0,0 +1,213 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class AmstradCPCCoreEmulationSettings + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AmstradCPCCoreEmulationSettings)); + this.OkBtn = new System.Windows.Forms.Button(); + this.CancelBtn = new System.Windows.Forms.Button(); + this.label4 = new System.Windows.Forms.Label(); + this.MachineSelectionComboBox = new System.Windows.Forms.ComboBox(); + this.label1 = new System.Windows.Forms.Label(); + this.lblMachineNotes = new System.Windows.Forms.Label(); + this.determEmucheckBox1 = new System.Windows.Forms.CheckBox(); + this.lblAutoLoadText = new System.Windows.Forms.Label(); + this.autoLoadcheckBox1 = new System.Windows.Forms.CheckBox(); + this.lblBorderInfo = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.borderTypecomboBox1 = new System.Windows.Forms.ComboBox(); + this.SuspendLayout(); + // + // OkBtn + // + this.OkBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.OkBtn.Location = new System.Drawing.Point(249, 432); + this.OkBtn.Name = "OkBtn"; + this.OkBtn.Size = new System.Drawing.Size(60, 23); + this.OkBtn.TabIndex = 3; + this.OkBtn.Text = "&OK"; + this.OkBtn.UseVisualStyleBackColor = true; + this.OkBtn.Click += new System.EventHandler(this.OkBtn_Click); + // + // CancelBtn + // + this.CancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.CancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.CancelBtn.Location = new System.Drawing.Point(315, 432); + this.CancelBtn.Name = "CancelBtn"; + this.CancelBtn.Size = new System.Drawing.Size(60, 23); + this.CancelBtn.TabIndex = 4; + this.CancelBtn.Text = "&Cancel"; + this.CancelBtn.UseVisualStyleBackColor = true; + this.CancelBtn.Click += new System.EventHandler(this.CancelBtn_Click); + // + // label4 + // + this.label4.AutoSize = true; + this.label4.Location = new System.Drawing.Point(12, 46); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(98, 13); + this.label4.TabIndex = 15; + this.label4.Text = "Emulated Machine:"; + // + // MachineSelectionComboBox + // + this.MachineSelectionComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.MachineSelectionComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.MachineSelectionComboBox.FormattingEnabled = true; + this.MachineSelectionComboBox.Location = new System.Drawing.Point(12, 62); + this.MachineSelectionComboBox.Name = "MachineSelectionComboBox"; + this.MachineSelectionComboBox.Size = new System.Drawing.Size(363, 21); + this.MachineSelectionComboBox.TabIndex = 13; + this.MachineSelectionComboBox.SelectionChangeCommitted += new System.EventHandler(this.MachineSelectionComboBox_SelectionChangeCommitted); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 14); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(159, 13); + this.label1.TabIndex = 17; + this.label1.Text = "Amstrad CPC Emulation Settings"; + // + // lblMachineNotes + // + this.lblMachineNotes.Font = new System.Drawing.Font("Lucida Console", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.lblMachineNotes.Location = new System.Drawing.Point(15, 95); + this.lblMachineNotes.Name = "lblMachineNotes"; + this.lblMachineNotes.Size = new System.Drawing.Size(358, 204); + this.lblMachineNotes.TabIndex = 20; + this.lblMachineNotes.Text = "null\r\n"; + // + // determEmucheckBox1 + // + this.determEmucheckBox1.AutoSize = true; + this.determEmucheckBox1.Location = new System.Drawing.Point(12, 373); + this.determEmucheckBox1.Name = "determEmucheckBox1"; + this.determEmucheckBox1.Size = new System.Drawing.Size(135, 17); + this.determEmucheckBox1.TabIndex = 21; + this.determEmucheckBox1.Text = "Deterministic Emulation"; + this.determEmucheckBox1.UseVisualStyleBackColor = true; + // + // lblAutoLoadText + // + this.lblAutoLoadText.Font = new System.Drawing.Font("Lucida Console", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.lblAutoLoadText.Location = new System.Drawing.Point(172, 390); + this.lblAutoLoadText.Name = "lblAutoLoadText"; + this.lblAutoLoadText.Size = new System.Drawing.Size(196, 30); + this.lblAutoLoadText.TabIndex = 27; + this.lblAutoLoadText.Text = "When enabled CPCHawk will automatically start and stop the tape whenever the tape" + + " motor state changes"; + this.lblAutoLoadText.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // autoLoadcheckBox1 + // + this.autoLoadcheckBox1.AutoSize = true; + this.autoLoadcheckBox1.Location = new System.Drawing.Point(12, 396); + this.autoLoadcheckBox1.Name = "autoLoadcheckBox1"; + this.autoLoadcheckBox1.Size = new System.Drawing.Size(128, 17); + this.autoLoadcheckBox1.TabIndex = 26; + this.autoLoadcheckBox1.Text = "Auto Tape Start/Stop"; + this.autoLoadcheckBox1.UseVisualStyleBackColor = true; + // + // lblBorderInfo + // + this.lblBorderInfo.Font = new System.Drawing.Font("Lucida Console", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.lblBorderInfo.Location = new System.Drawing.Point(175, 331); + this.lblBorderInfo.Name = "lblBorderInfo"; + this.lblBorderInfo.Size = new System.Drawing.Size(196, 21); + this.lblBorderInfo.TabIndex = 30; + this.lblBorderInfo.Text = "null"; + this.lblBorderInfo.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(12, 315); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(118, 13); + this.label2.TabIndex = 29; + this.label2.Text = "Rendered Border Type:"; + // + // borderTypecomboBox1 + // + this.borderTypecomboBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.borderTypecomboBox1.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.borderTypecomboBox1.FormattingEnabled = true; + this.borderTypecomboBox1.Location = new System.Drawing.Point(12, 331); + this.borderTypecomboBox1.Name = "borderTypecomboBox1"; + this.borderTypecomboBox1.Size = new System.Drawing.Size(159, 21); + this.borderTypecomboBox1.TabIndex = 28; + // + // AmstradCPCCoreEmulationSettings + // + this.AcceptButton = this.OkBtn; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.CancelBtn; + this.ClientSize = new System.Drawing.Size(387, 467); + this.Controls.Add(this.lblBorderInfo); + this.Controls.Add(this.label2); + this.Controls.Add(this.borderTypecomboBox1); + this.Controls.Add(this.lblAutoLoadText); + this.Controls.Add(this.autoLoadcheckBox1); + this.Controls.Add(this.determEmucheckBox1); + this.Controls.Add(this.lblMachineNotes); + this.Controls.Add(this.label1); + this.Controls.Add(this.label4); + this.Controls.Add(this.MachineSelectionComboBox); + this.Controls.Add(this.CancelBtn); + this.Controls.Add(this.OkBtn); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.Name = "AmstradCPCCoreEmulationSettings"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Core Emulation Settings"; + this.Load += new System.EventHandler(this.IntvControllerSettings_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button OkBtn; + private System.Windows.Forms.Button CancelBtn; + private System.Windows.Forms.Label label4; + private System.Windows.Forms.ComboBox MachineSelectionComboBox; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label lblMachineNotes; + private System.Windows.Forms.CheckBox determEmucheckBox1; + private System.Windows.Forms.Label lblAutoLoadText; + private System.Windows.Forms.CheckBox autoLoadcheckBox1; + private System.Windows.Forms.Label lblBorderInfo; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.ComboBox borderTypecomboBox1; + } +} \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCCoreEmulationSettings.cs b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCCoreEmulationSettings.cs new file mode 100644 index 0000000000..c01722fc5d --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCCoreEmulationSettings.cs @@ -0,0 +1,118 @@ +using System; +using System.Linq; +using System.Windows.Forms; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Cores.Computers.AmstradCPC; +using System.Text; +using static BizHawk.Emulation.Cores.Computers.AmstradCPC.AmstradCPC; + +namespace BizHawk.Client.EmuHawk +{ + public partial class AmstradCPCCoreEmulationSettings : Form + { + private AmstradCPC.AmstradCPCSyncSettings _syncSettings; + + public AmstradCPCCoreEmulationSettings() + { + InitializeComponent(); + } + + private void IntvControllerSettings_Load(object sender, EventArgs e) + { + _syncSettings = ((AmstradCPC)Global.Emulator).GetSyncSettings().Clone(); + + // machine selection + var machineTypes = Enum.GetNames(typeof(MachineType)); + foreach (var val in machineTypes) + { + MachineSelectionComboBox.Items.Add(val); + } + MachineSelectionComboBox.SelectedItem = _syncSettings.MachineType.ToString(); + UpdateMachineNotes((MachineType)Enum.Parse(typeof(MachineType), MachineSelectionComboBox.SelectedItem.ToString())); + + // border selecton + var borderTypes = Enum.GetNames(typeof(AmstradCPC.BorderType)); + foreach (var val in borderTypes) + { + borderTypecomboBox1.Items.Add(val); + } + borderTypecomboBox1.SelectedItem = _syncSettings.BorderType.ToString(); + UpdateBorderNotes((AmstradCPC.BorderType)Enum.Parse(typeof(AmstradCPC.BorderType), borderTypecomboBox1.SelectedItem.ToString())); + + // deterministic emulation + determEmucheckBox1.Checked = _syncSettings.DeterministicEmulation; + + // autoload tape + autoLoadcheckBox1.Checked = _syncSettings.AutoStartStopTape; + } + + private void OkBtn_Click(object sender, EventArgs e) + { + bool changed = + _syncSettings.MachineType.ToString() != MachineSelectionComboBox.SelectedItem.ToString() + || _syncSettings.BorderType.ToString() != borderTypecomboBox1.SelectedItem.ToString() + || _syncSettings.DeterministicEmulation != determEmucheckBox1.Checked + || _syncSettings.AutoStartStopTape != autoLoadcheckBox1.Checked; + + if (changed) + { + _syncSettings.MachineType = (MachineType)Enum.Parse(typeof(MachineType), MachineSelectionComboBox.SelectedItem.ToString()); + _syncSettings.BorderType = (AmstradCPC.BorderType)Enum.Parse(typeof(AmstradCPC.BorderType), borderTypecomboBox1.SelectedItem.ToString()); + _syncSettings.DeterministicEmulation = determEmucheckBox1.Checked; + _syncSettings.AutoStartStopTape = autoLoadcheckBox1.Checked; + + GlobalWin.MainForm.PutCoreSyncSettings(_syncSettings); + + DialogResult = DialogResult.OK; + Close(); + } + else + { + DialogResult = DialogResult.OK; + Close(); + } + } + + private void CancelBtn_Click(object sender, EventArgs e) + { + GlobalWin.OSD.AddMessage("Core emulator settings aborted"); + DialogResult = DialogResult.Cancel; + Close(); + } + + private void MachineSelectionComboBox_SelectionChangeCommitted(object sender, EventArgs e) + { + ComboBox cb = sender as ComboBox; + UpdateMachineNotes((MachineType)Enum.Parse(typeof(MachineType), cb.SelectedItem.ToString())); + } + + private void UpdateMachineNotes(MachineType type) + { + lblMachineNotes.Text = CPCMachineMetaData.GetMetaString(type); + } + + private void borderTypecomboBox1_SelectedIndexChanged(object sender, EventArgs e) + { + ComboBox cb = sender as ComboBox; + UpdateBorderNotes((AmstradCPC.BorderType)Enum.Parse(typeof(AmstradCPC.BorderType), cb.SelectedItem.ToString())); + } + + private void UpdateBorderNotes(AmstradCPC.BorderType type) + { + switch (type) + { + case AmstradCPC.BorderType.Uniform: + lblBorderInfo.Text = "Attempts to equalise the border areas"; + break; + case AmstradCPC.BorderType.Uncropped: + lblBorderInfo.Text = "Pretty much the signal the gate array is generating (looks pants)"; + break; + + case AmstradCPC.BorderType.Widescreen: + lblBorderInfo.Text = "Top and bottom border removed so that the result is *almost* 16:9"; + break; + } + } + } +} diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCCoreEmulationSettings.resx b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCCoreEmulationSettings.resx new file mode 100644 index 0000000000..ca821b54f8 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCCoreEmulationSettings.resx @@ -0,0 +1,624 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAwAMDAQAAAABABoBgAAxgAAACAgEAAAAAQA6AIAAC4HAAAYGBAAAAAEAOgBAAAWCgAAEBAQAAAA + BAAoAQAA/gsAADAwAAAAAAgAqA4AACYNAAAgIAAAAAAIAKgIAADOGwAAGBgAAAAACADIBgAAdiQAABAQ + AAAAAAgAaAUAAD4rAAAwMAAAAAAgAKglAACmMAAAICAAAAAAIACoEAAATlYAABgYAAAAACAAiAkAAPZm + AAAQEAAAAAAgAGgEAAB+cAAAKAAAADAAAABgAAAAAQAEAAAAAACABAAAAAAAAAAAAAAQAAAAEAAAAAAA + AAAAAIAAAIAAAACAgACAAAAAgACAAICAAACAgIAAwMDAAAAA/wAA/wAAAP//AP8AAAD/AP8A//8AAP// + /wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAHR3AAAAAAAAAAAAAAAAAAAAAAAAAAAAdHdEcAAAAAAAAAAAAAAAAA + AAAAAAAAAHd0d3QAAAAAAAAAAAAAAAAAAAAAAAAAAEd8d3UAAAAAAAAAAAAAAAAAAAAAAAAAB3yHfHZw + AAAAAAAAAAAAAAAAAAAAAAAAd3fIyHVwAAAAAAAAAAAAAAAAAAAAAAAAfHh3jIxwAAAAAAAAAAAAAAAA + AAAAAAAHd8jIyHdgAAAAAAAAAAAAAAAAAAAAAAAHd4yHfIdAAAAAAAAAAAAAAAAAAAAAAAAHyMjIyMhQ + AAAAAAAAAAAAAAAAAAAAAAB3d3eMh4dgAAAAAAAAAAAAAAAAAAAAAAB8jIyIfIdQAAAAAAAAAAAAAAAA + AAAAAAB3h4jIiMh3AAAAAAAAAAAAAAAAAAAAAAB8jIeHeIjHAAAAAAAAAAAAAAAAAAAAAAeIiHh4eMiE + AAAAAAAAAAAAB0dHcAAAAAd8h4eIiIiHcAAAAAAAAAB0d3d3RwAAAAeIeIiIiIh3RwAAAAAAAHR3d8h3 + dAAAAAfIh4iIiHiIx0cAAAAAdHh3eIeHhwAAAAeHiIiIiIiId3R3dHR0eHd4h4eHhAAAAAd4eIiIiIiH + x3d2d3eId4iIiIiIhwAAAAd4eIiI+IiIh3d3eHh3iIiIiIeHwAAAAAfIjHeIiIiIyIeHh4iIiIiIiIiI + cAAAAAeIQ0R3h3iIiMiIiIiIiIiIiIiEAAAAAAfIR3d3d0iIiIh4iIeIiIiIiHhAAAAAAAB4d3d3SHiI + h4fTiIi3iIiIeIwAAAAAAAB3h4d3eIeIiHiJiIuIiIh4jHAAAAAAAAAHyId3h3h4iIh4iIiIiIiHeAAA + AAAAAAAAB8iMiMjIiIiIh4h3aMjHAAAAAAAAAAAAAAdYyIeIiIiMjId6d4eAAAAAAAAAAAAAAAAHdsjH + eIeH6MiId3AAAAAAAAAAAAAAAIiIh4V8jIh4eIfHcAAAAAAAAAAAAACIiIh3AAAHd3h3fHcAAAAAAAAA + AAAAAAiIjHgAAAAAAHx8eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///////8AAP///////wAA////////AAD///////8AAP///////wAA//////// + AAD///////8AAP///////wAA//h/////AAD/4D////8AAP/AP////wAA/8A/////AAD/gB////8AAP8A + H////wAA/wAf////AAD+AB////8AAP4AH////wAA/gAf////AAD8AB////8AAPwAH////wAA/AAP//// + AAD8AA////8AAPgAD//+BwAA+AAH//ADAAD4AAP/wAMAAPgAAP8AAwAA+AAAAAADAAD4AAAAAAMAAPgA + AAAABwAA+AAAAAAHAAD4AAAAAA8AAPgAAAAAHwAA/AAAAAA/AAD8AAAAAH8AAP4AAAAA/wAA/4AAAAP/ + AAD/4AAAB/8AAP/4AAAf/wAA/8AAAH//AAD8A+AD//8AAPgP/A///wAA////////AAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAAoAAAAIAAAAEAAAAABAAQAAAAAAAAC + AAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAICAgADAwMAAAAD/AAD/ + AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdwAAAAAAAAAAAAAAAA + AAd0dAAAAAAAAAAAAAAAAAB3x3cAAAAAAAAAAAAAAAAAd3fHcAAAAAAAAAAAAAAAB3yMh3AAAAAAAAAA + AAAAAAfIeMdwAAAAAAAAAAAAAAAHjIyHQAAAAAAAAAAAAAAAfId4yHAAAAAAAAAAAAAAAHjIyIdQAAAA + AAAAAAAAAAB3iId4YAAAAAAAAAdwAAAAjIiIiIUAAAAAAHd3dAAAB4iIiHh8cAAAAHd3x4dwAAd4iIiI + h3Z3d3R3yIh4cAAHh4iIiIfHd3d4iIiIh3AAB3jHiIiIiHeHiIiIiIwAAAh3dXh4iMiIiIiIiIhwAAAA + yGd0d4iIeIi4iIiMAAAAAIeHd4iIh32IiIiIcAAAAAAAd4jIyIiIiHeHyAAAAAAAAAB3h4iIh8h3dwAA + AAAAAAAIh8fIh4eIaAAAAAAAAACIiHAAB8jIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////// + ////////////////////n////g////wP///8B///+Af///gH///4B///8Af///AH///wB//n8AP/A+AB + /AHgAAAB4AAAAeAAAAPgAAAH8AAAD/AAAB/8AAA//wAA//4AA//weA////////////////////////// + //8oAAAAGAAAADAAAAABAAQAAAAAACABAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAgAAAgAAAAICAAIAA + AACAAIAAgIAAAICAgADAwMAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHRwAAAAAAAAAAAAB3dAAAAAAAAAAAAA + d8dwAAAAAAAAAAAAfId3AAAAAAAAAAAHeMjHAAAAAAAAAAAHyHh3AAAAAAAAAAAHh3eEAAAAAAAAAAAI + yIiHAAAAAHd2cAAIiIiIQAAAd3d4UACHiIiId3d3eHiIcACHh4iIyHeHiIiIcAAIR3d4iIiIiIiMAAAH + d3eIh3iIiIhwAAAAeMh4iIiHiMAAAAAAAHfIiMh4aAAAAAAAiIgHyIfIAAAAAAAIgAAAAIAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8A////AP///wD8f/8A+H//APB/ + /wDwP/8A4D//AOA//wDgP/8A4D/BAOAfAQDAAAEAwAABAOAAAwDgAAcA8AAfAPwAPwDwgP8A5/f/AP// + /wD///8A////ACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACA + AAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAd1AAAAAAAAB8cAAAAAAAB4eAAAAAAAAHyMgAAAAAAAiIhwAAAHcACI + iHcAd3hwAIz4jIeIiIAAd3eIiIiIAACHeIiIiHAAAACMeMh4AAAAiAAIgAAAAAAAAAAAAAAAAAAAAAAA + AAD//wAA//8AAP//AADj/wAA4/8AAMP/AADB/wAAwfkAAMDBAADAAQAAwAMAAMAHAADwDwAAzn8AAP// + AAD//wAAKAAAADAAAABgAAAAAQAIAAAAAAAACQAAAAAAAAAAAAAAAQAAAAEAAAAAAAA9OzsAZD8/AGg8 + PABtPj4AQkNDAEZIRwBWQkIAV0REAF5AQABbRkYAVklJAFxPTwBTU1MAXFJSAF5ZWQBkQEAAYUREAGZF + RQBqQkEAYEtLAGNPTwBwQUEAfUZGAHJKSgB2SUkAfU9PAGBRUQBgVFQAZlZWAGZYWABqWVkAclZWAHpU + VAB9W1oAbmJiAGtoaABtaWkAcWdnAHdnZwB8Y2MAe2pqAHJxcQB+dHQAd3l5AHl6egCGT08AiU9PAIFP + UACGU1MAjVFRAIlWVgCMV1cAg1xbAIxaWQCQUlIAlVJSAJFXVgCXVVUAmVVVAJZaWQCSXV0AlV9eAJpZ + WgCeW1sAml5eAKBZWgCgXFwAql9fAIRmZQCIZWQAhWtrAI5ragCTYmEAnGBhAJ9kYwCaZmYAk25uAJ1s + awCFdHQAiXd3AIt+fgCWd3cAmHR0AJV5eQCbfHwAo2JhAKZhYQChZWUApGVkAKplZACsZGQAqmhnAKZr + agCnbGsAqmloAKlubQCsbW0AtGZnALhsbACxb3AAv29wAKVxcACrc3IAr35+ALN0cwC5c3MAvXBxALR4 + dgC1fHsAunt6AMNtbgDGb3AAw3FyAMZwcQDGdXUAyHR1AMp3eADBeXkAxnt7AMB/fgDLensANLBSAEWf + TgBBtFwAPMdnADHkdgDciiIAvoF/AISrdwDln0sA35lhAN2XfADgmmEA8LdlAO61cAArWPIALWT+AEh5 + +gDOf4AAfoCAAHiA1ABZv9wAZrnUAGK+2ABxnv4Ad6P/ADPX/QBw0OcAW+D7AIKEgwCPgoIAjI2NAJuC + ggCUiIgAmYqKAJGSkgCjhIQAqoKCAKKLiwC+hIMAsoqKALaSgQCum5sAsZubALqqlQCdgr4Ar6ytALGh + oAC6pKQAwoSDAMyBggDGiIYAyYiHAMWMigDMjIoA0ISFANKHiADUjIwA2Y6NAMCUjQDIk44A0JCPANaP + kADHlZQAzpSSAMScmwDUkpIA2ZSVANWYlgDampcA2ZeYANWcnADam5sA4p2cAMChjwDeoJ4A5aCFAOaj + jQDlpJoA2p6hAMOkowDOoaEAy62tANegoADdoqEA2aGpANGsrwDdq6kAwbG4ANGysQDdtLQA2ri3AOGk + owDjqKYA66ylAOGnqADjq6oA6a2rAOOwrwDssK4A5K+wAOaztADttLIA57i2AO24tgDmurgA6rq6APC1 + swDyuLYA9Ly5APi+uwD1wL0A+cC9AKKMwACkk8QAqprMALSayACptsEAlaDkAOy/wACRxtQAgOv9AJnr + 9wDEwsoA5sbGAOzCwgDuyMcA7MzMAPPEwgDxy8oA9dPTAPja2gAAAAAAAAAAAP///wAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAoIJQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAACYXODs4BCUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + KTNDQ0M7OAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALllbYmJZQBcAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYYWNwcHBwWy8mAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAFFLanBwcHBwYz0eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAABpqcHBwcHBwZVkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAl11w + cHBwcHBwcGcSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIXdwcHBwcHBwcGkSAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPXBwcHBwcHBwd2wYAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAACXbnBwdXB5dXl0eW4hAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAid3R5eXl5eXl5q6wzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9eXV5 + i7CxsbGxsblLKgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABndYuwsbm8uby5vMFnHgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJt3q7G3vMHB1cLBwdWuEgAAAAAAAAAAAAAAAAAA + AAAAAAAeEhMSCiUAAAAAAAAAAEexsbm/1dXZ2dnZ1da5ZgwAAAAAAAAAAAAAAAAAAAAjEjNZaW5qXRMl + AAAAAAAAADW5s7/V2N7i4uLi3dzZrQQPAAAAAAAAAAAAAAAAHxhZbm5uaWltd6ASAAAAAAAAAEmzvMLZ + 3uP29/fw4uTkuUAWCy0AAAAAAAAAAB4YYXd3gG13vbm5vb8zAAAAAAAAAE6xwdXd4/b6+/r38OTl1Vlc + OAMIFAweFBQSM2mtrYB3vdXT0NXExNU1AAAAAAAAAE65wtXe8Pr7/Pz79+fn1WphZ25pXV1mbHetrXd3 + tdXT4vXw49nZ3NYgAAAAAAAAAEu3wdje9vv7/Pz79+fn34B3d2xtoHeud66uudXT4vD39/Dj49zk5G0A + AAAAAAAAAD2xwcwoH0/L/Pukyenp5K27u7m5uczM0Nve4vb3+vr56OPl5eXl1igAAAAAAAAAADWxwQgB + BQYNmveZK/Dp6cG/wcTV2eP3+vr6+/r6+ejm5ufn5+nkIgAAAAAAAAAAAJmruR4sjC2WLFCdDd3p6dXW + 1tXI3vn67pCO9Ojp6efo5+fm59wiAAAAAAAAAAAAAABLsZ0FmC0qKgHMRcjp6dzc1Y2KiO3RlfKTj+np + 5ubm5eXk1SIAAAAAAAAAAAAAAACdab/Lp5aWnEfV1cHm6ebk6pGSiabZ8fOU0uXl5eTk3NyuRQAAAAAA + AAAAAAAAAAAAn0ux0KFTaMHBv7nC6efp3Ovv7OTm3OPl3Nzc3NfW1U6fAAAAAAAAAAAAAAAAAAAAAABF + Wa25t7yxs7Gw5+fn5Obk18XG3NyBfHvD1cSgNQAAAAAAAAAAAAAAAAAAAAAAAAAAAFUzarGwsHl5sefn + 39zEgoZ/hL19fnqirj2jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATj09ZXV0cLzn3NXChYeDub+1pbQ9 + VQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0rXj+rpInTBDcHCz5NW/ucG5u7GAM1QAAAAAAAAAAAAAAAAA + AAAAAAAAAADLytDi9tOemQAAAAAAUy9EecLEsa1uPTUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPj11Mme + VakAAAAAAAAAAAAATS84M0akAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///////8AAP///////wAA////////AAD///////8AAP///////wAA//////// + AAD///////8AAP///////wAA//h/////AAD/4D////8AAP/AP////wAA/8A/////AAD/gB////8AAP8A + H////wAA/wAf////AAD+AB////8AAP4AH////wAA/gAf////AAD8AB////8AAPwAH////wAA/AAP//// + AAD8AA////8AAPgAD//+BwAA+AAH//ADAAD4AAP/wAMAAPgAAP8AAwAA+AAAAAADAAD4AAAAAAMAAPgA + AAAABwAA+AAAAAAHAAD4AAAAAA8AAPgAAAAAHwAA/AAAAAA/AAD8AAAAAH8AAP4AAAAA/wAA/4AAAAP/ + AAD/4AAAB/8AAP/4AAAf/wAA/8AAAH//AAD8A+AD//8AAPgP/A///wAA////////AAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAAoAAAAIAAAAEAAAAABAAgAAAAAAAAE + AAAAAAAAAAAAAAABAAAAAQAAAAAAAFFNTQBRUlIAU1RUAGJHRwBiT08Aa0lIAGJTUwBrVlYAYllZAGZc + XABpWloAb1xbAHNTUwB7V1YAc1hXAHFbWwBkZWUAaWFhAG5kZABpamkAcGFhAHlubgB2cHAAf3V1AH55 + eQB8fX0AgUpKAI1PTwCLWFcAhlhYAI9ZWQCKXFsAm1ZWAJJZWQCWWVgAmlpbAJtcWwCiXFwAl2BfAIBg + YACAZ2YAgG9vAI9oaACWZWQAmGBhAJ5kZACcaWoAmm9vAIV0dACNcHAAiXZ2AIB8fACac3IAm3V0AJ51 + dQCZfHwAnHx8AKNmZgCnZmYAqmJiAK5jYwCvb24AtWVmALBtbgC5bW0AvmxtAKx+fQCxcnIAtHBwALZz + dACydXQAtnd2ALlwcAC5dnYAt3p5ALh5eAC8fHsAun18ALx+fQDGb3AAxnBxAMdzdADAd3YAyHJzAMlz + dADJdXYAynd4AMd/fwDMe3wAzXx9AHunbwBhvHIAYsN4ANuLOwC2hn4A4Zt5APC3ZABte9sAX47+AHWM + 5QAl0foAY+P8AIeDgwCFhoYAioSEAJOIiACWi4sAmpKRAKGCgQCmhYUAqYGBAKuDhACniooApYyMAKiO + jQCyhYMAvoWEALeNjQCrj5AAr5eXALSVlAC9lJMAmbCEAK6RugDBgYAAwoSCAMWDhADChoQAxYeFAM6A + gQDFiIYAxoqIAMqIiQDMi4oAy4yKAMiPjQDPj44A0ISFANKJigDUi4wA04+NANWNjgDKkY8A0JCOANud + iQDWj5AAzJSTAM2XlgDGm5oA1pGSANOUkgDVl5EA1pOUANiVlgDYmJUA2ZeYANKenADbmpsA3pmYANuc + mgDbn5wA1aacAN6gngDqqZoA3Z+gAMyjowDCra0AxqysAMqpqQDboaAA3qKiAN6logDbp6UA3aWkANer + qgDWsbMA0rW0ANe0tADfs7IA4aSiAOGlpQDkp6UA46imAOWopgDsraIA6qimAOGoqADhrqwA6a2rAOqv + rADpsK4A7LGuAOGzswDlsbEA7bKxAO+1sgDotrYA5rm3AO+4twDot7sA6bq5AOu9uwDrv70A8bazAPG2 + tADxuLUA9Lm2APC9uwD2vboA9L+9APi+uwD4v7wA8sC+APXAvgD5wL0AkILJAKqXzACsu8cAqr/LALLV + 3QDawMIA48XFAOvDwQDswMAA7cTDAO/ExQDgxsgA8cbEAPTGxADwyskA9MvJAPLNzQD21dYA+NjZAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAMEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqHCEcBQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAayU9PSYbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdQlBSQiJpAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAM0pSUlJQPRcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnUlJSUlJGFQAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAFJSUlJSUkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzUlJSWVJZfxAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAC5XWYqKioqGDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASoqMkpqa + mqAsAAAAAAAAAAAAAAAAAABoNAAAAAAAAACMjJyuvLy2toYHAAAAAAAAAAAAABcOIDouBgAAAAAAc4yc + tsHKysPAriIKAAAAAAAAABYgRk1LTX+DEAAAAABukqXB4ejo4dHPQCIEChcXEwggTXV/k66unKMpAAAA + AG6Srsro6ero0dN/Rk1NRk2Dg4STrsbh4cHAt2sAAAAAbpKuOXPe6ajW15KGg4OGk528yuHo5eHPz882 + AAAAAAB4jCkDAxSoMabXt5yjt8ro3ePo5dbT09HTdAAAAAAAAABGcBFoGgFwdtfDwHxi2dpmZcrX09HP + z0MAAAAAAAAAAHh/qWwaOa6cz9PNZGPYsdzbzc3DwLk2AAAAAAAAAAAAAAAvhpKakoyg19HNyKS5wHtb + orZ/cwAAAAAAAAAAAAAAAAAANkaKWVm5zb1gYV6cXVxfNgAAAAAAAAAAAAAAAAAAALGvlTIuP1K5tqCR + l4xfLwAAAAAAAAAAAAAAAAAAsbPBenkAAAAAcCVYjE0scwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////////////////////+f///+D////A////wH + ///4B///+Af///gH///wB///8Af///AH/+fwA/8D4AH8AeAAAAHgAAAB4AAAA+AAAAfwAAAP8AAAH/wA + AD//AAD//gAD//B4D////////////////////////////ygAAAAYAAAAMAAAAAEACAAAAAAAQAIAAAAA + AAAAAAAAAAEAAAABAAAAAAAAWlJSAHBJSQB1SEgAe1dXAHdYWAB5WlkAel1dAGBiYgB1bGwAfWtrAHh2 + dgB9fn4Ag01NAIRXVwCIV1cAhV9eAItbWgCgX14ApV1dAJhgXwCNYGAAnWtqAJhtbQCCdnYAh3x8AI15 + eACeensAqGBgAKhoZwCga2oArGpqALNqagCzb28AtG1tALltbQCxb3AApnVzAKlzcwCqdHMApnp6AKd+ + fgCpensAq3x7ALZ3dgC8dHQAvH59AMZvcADGcHEAxXN0AMhycwDJdncAynh5AMx5egDNfn8Ajo1wAOek + VgDGgH8A4p53AEZ2+gB8u4AAd8PaAIuEhACOh4cAjo6OAJ+DggCejo4Ao4SEAKSIiACsi4sAqo2MAK6P + jgC+gYAAvoaGAL+KiACskJAAtJeXALWenQC5np4At6iOAKmyjgC9nroAwYSDAMaGhADOhoYAxomHAMiK + iQDJjYwA0oeIANOOjwDUjY0A2ZiPANaPkADGkZEAx5eXAMySkADGnZwA1ZOSANeTlADWl5YA2JSVANGZ + mADan50A3J6dAOCcmwDVoJ8A7K2fAMOtrQDXo6IA3aCgAN+kpADVq6oA3ay3AMu0tADPtrYA3L+/AOCi + oQDhpqUA5KelAOinpgDlq6gA46usAOOvrQDqrqwA7LGuAOayswDjtrQA5re1AOqysQDts7EA57y6AO+8 + ugDrvL0A8LOwAPC1sgDwtrQA87q3APS6twD2vboA8b69APi/vAD2wb4A+cC9AJmTzwDHqMMAu8PMAIHf + 8QDByNAA7cLCAO3FwwDvxsQA5cjIAOzOzgDwxcQA9cbEAPPP0AD10tIAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + BQMJAAAAAAAAAAAAAAAAAAAAAAAAAAAPHBMNAAAAAAAAAAAAAAAAAAAAAAAAABojLy8TAAAAAAAAAAAA + AAAAAAAAAAAAAB0wMDAiPgAAAAAAAAAAAAAAAAAAAAAAQjAwMDAtGAAAAAAAAAAAAAAAAAAAAAAAFzIy + NTU5CgAAAAAAAAAAAAAAAAAAAAAAIjZYWFxcBwAAAAAAAAAAAAAAAAAAAAAANlxtdW11JQAAAAAAAAAA + PgcRDgkAAAAAXG1/lISAZgMAAAAAABkVLC5SVhcAAABNY3WWnJuLfB8UBAcQHkhWaX91dSsAAABNY2BM + mJeCiVJSVl9laX+WloSJgEIAAAAAXAEIC0tGjnR0dJaRk5qNjIyJQwAAAAAAJkNADBtdjIaPO1GSPYuJ + hnVEAAAAAAAAAClISWRcd4xwkGp8UE90VwAAAAAAAAAAAAAAKSQ1NYZ7OjhbPDdGAAAAAAAAAAAAAHNv + YGsAKyJoXFYmRwAAAAAAAAAAAAAAcnIAAAAAAAAATgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP// + /wD///8A////APx//wD4f/8A8H//APA//wDgP/8A4D//AOA//wDgP8EA4B8BAMAAAQDAAAEA4AADAOAA + BwDwAB8A/AA/APCA/wDn9/8A////AP///wD///8AKAAAABAAAAAgAAAAAQAIAAAAAAAAAQAAAAAAAAAA + AAAAAQAAAAEAAAAAAABjZGQAdmRjAHtpaQB/eHgAgU9PAKBaWgCFbm0AlWtqAKptbgCwZ2cAsGhoAKxw + cACteHkAvnJyAMZvcADGcHEAy3l5AMx9fgCFmXQAwIB/ANeUfQDhoX8AlIqJAJWMjACYiIgAoIaGAK2K + igCxh4cAvoGAALKKigC4iYgAuJWVAL2cnACss50AuqKhAL+mpgDLgoIAxImHAMeNjADLkI8AxpWTANCS + kQDYlZUA1J6dANqZmgDdnp4A1J+oAMaiogDOr68AzLKyANi5uADhpaIA4qypAOWtqADrrqsA4bKwAOay + sgDtuLYA57++AOy4uADxtLIA8be0APa9ugDswL4A9sG+ALCcxwC5ncIA06zBALnH0QC2ytQA7sPDAPLS + 0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAZBgUAAAAAAAAAAAAAAAAACw8KAAAAAAAAAAAAAAAAGhAQDgAAAAAAAAAAAAAAAAkRESUYAAAA + AAAAAAAAAAAlKy4uBwAAAAAAAAcDAAAAKzlHPCYCAAAYCB0oKgAAAC0wSDs0FB0nLDlAOiwAAAANAQQb + Pi9DRkVBPzUAAAAAJB4cKz5EQjMiNSkAAAAAAAAAHwwRNxYVEyQAAAAAAAAxMgAAACEgAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP//AAD//wAA4/8AAOP/AADD/wAAwf8AAMH5 + AADAwQAAwAEAAMADAADABwAA8A8AAM5/AAD//wAA//8AACgAAAAwAAAAYAAAAAEAIAAAAAAAgCUAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAkAAAAJAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAUAAAAOAEBAVUAAABUAAAANQAAABAAAAABAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAkFBSUvGRl5TCkpwlYuLtxDJCTQFw0NmQAA + AEkAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACGAwMKE8rK6V6RET2klJR/5ZS + U/+OT0//ZDc38B0QEJoAAAAyAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYDAwYVzAwoopP + T/ygXVz/oFtb/55ZWf+bWFf/k1NT/1UvL9wGAwNcAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AARNKipxhk5O+adkY/+uZWX/tWdo/7VmZ/+qYWH/nltb/3hERPcfERGCAAAAFgAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAADEZGS1zQ0LXqGdm/7ptbf/Fb3D/x3Bx/8hwcf/BbW7/q2Vl/4hPT/82HR2gAAAAIAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAB1gxMYyYXl3/vXFx/8Zwcf/HcHH/x3Bx/8dwcf/HcHH/uG1t/5NY + V/9EJia2AAAAKQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPB8fNH1MS+K4cnH/x3Fy/8dwcf/HcHH/x3Bx/8dw + cf/HcHH/wHBx/51gX/9PLCzGAAAAMwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AAMAAAAAAAAAAAAAAAAAAAAALhAQFIZXVs/RjIz/1Y2O/9qYmP/eoaL/46qr/+aysv/ot7f/6rm5/+m4 + uf/otbX/5q+v/+uvrf/jqab/wYeF/28/P/QhEhKvAAAAXwAAACgAAAANAAAABQAAAAMAAAACAAAAAwAA + AAUAAAAKAAAAFQAAADAdDg9oSSkptHZHRu2dYmL+t3Z1/758e/+6enn/tnh3/7d5eP+8fn3/w4SD/7Z6 + ef9eODfbBgICTgAAAAgAAAAAAAAAAAAAAAAAAAAAPhwcJJVjYuPXkZH/2JOU/92fn//iqqr/57O0/+u8 + vP/uwsL/78XG/+/Exf/twMD/67i4/+60sv/wtrP/zZKQ/5taWv9xQED2MRsaxAgEBIcAAABaAAAAQQAA + ADcAAAA2AAAAOwAAAEUEAgJZHA4OfUcnJ7l5SkntqGxr/8CAfv/DgoH/vH59/7p+ff/DiIb/zZGP/9GT + kf/UlJP/1peV/9eZl/+GVlbuGQsLVwAAAAcAAAAAAAAAAAAAAAAAAAAARiIiLZ9rauvZk5P/2peY/+Ck + pP/lsLD/6ru7/+/Fxf/yzMz/9NDQ//PPz//xycr/7sDA//K5tv/1u7j/36Kg/6dmZf+mZWX/j1ZW/WM6 + OutDJSXQNBwcvDAaGrQ0HBy1PiIivUwsLMtkPDzfh1VU9a1xcP/EhIP/xIWE/7+Cgf/Ch4b/zZST/9mk + ov/grq3/4a6t/96lo//eoJ7/36Kg/+Cjof+IWVjnGwwMQwAAAAIAAAAAAAAAAAAAAAAAAAAARyQkL6Br + auzZk5P/25qb/+GnqP/ntLT/7cDA//LLy//209T/+NjY//fX1//00ND/8cbG//W9u//4vrz/46ak/7d0 + c/+vb27/s3Jy/7d2df+ucXD/pWpp/6Npaf+nbWz/sHVz/7p9fP/EhYT/yImI/8WIhv/DiIb/ypGP/9eg + n//hr63/57q5/+rCwP/rwsD/6bq4/+evrf/nq6n/6q6r/9qgnv9wRkbDBwAAHgAAAAAAAAAAAAAAAAAA + AAAAAAAASCQkLZ1nZuvYkpP/25uc/+Opqv/qtrf/7cHB//TOzv/52Nj/+tzc//na2v/xz9D/8MfH//fA + vv/6wb7/6a6r/8OBgP/DgoD/vX58/7h7ev+8fn3/woOC/8aHhv/HiYj/xoqJ/8aLif/Ijoz/zZST/9eg + nv/hrav/6Lm3/+zCwf/uyMf/78nH/+/Dwf/uvLr/7ba0/+60sf/vtLL/8ri1/7J+fflMKSltAAAABAAA + AAAAAAAAAAAAAAAAAAAAAAAAQyEhI5JcXOPWj5D/3Juc/8qVlf+BZmb/bl5e/4l4eP/AqKj/8tPT//LO + zv+5p6b/w6qq//fBv//7wr//8LWy/86Ojf/Ojoz/0ZGP/9GSkP/OkY//zpOR/9GamP/VoJ//2qel/+Gv + rf/nt7X/6727/+3Dwf/wycf/8czL//LLyf/yxsT/8cC+//G7uf/yubf/87m3//S7uP/4vrv/1J6c/3JH + RrAdCgsWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANRcXEYJNTcvPiIn/15aW/2VNTf85Ojr/Q0VF/0JF + RP9dXFz/n5GR/+S/v/+bh4f/hXp6/+25uP/7wr//9bu4/9qcmv/Zmpj/252b/96gnf/ipKH/5q+s/+u+ + vP/vycf/8srI/+3Hxv/wysj/9c7M//TNy//0ysj/9MbE//TBv//1vrz/9r26//e9u//4vrv/+L+8//vB + vv/hqqf/g1ZVzDwcHC4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAW4+Ppq/env/05OT/2ZX + V/9rbm7/fX9//3l6ev99f3//cHJy/5F9ff+ff3//XFhY/9eop//8wr//+L+8/+Wppv/ipaP/5qil/96i + pP/Kmaz/1qi1//LGxP/tyMf/qb3J/23E3P9kw9//vMTN//jDwP/3wb//+MC9//i/vf/5v73/+b+8//i/ + vP/3vrv/+L68/92mo/+IWlnRRSMjOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFcv + L0mbX1/y15GS/6GAgP9XV1b/iYuL/4CBgf98fX3/cnR0/1dPT/++j4//km9w/9Sfnv/6wL3/+cC9/+6z + sP/ssK3/0Z+u/4OH1P9YffD/QGPs/7KYyv/Ct7z/Ytrz/3Ts//8s2f//cbvU//m+u//4v7z/+L67//e9 + uv/1vLn/9Lq3//O5tv/zuLX/0puZ/4RVVctGIyM4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAADIXFwdrPDySq2ts/diZmf/ApKT/sKur/4CBgP95enr/iYiI/49zdP/do6P/36Ch/96e + nv/zuLX/+sK///W7uP/1ubT/qZC//2qY+/9tnf//MGT6/56FxP/esK//nMbS/57n8/9+z+T/ybG3//a6 + t//zubb/8re0//C1s//utLH/7rKw/+qvrP++iIb9dklJtkMgISoAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABHIyMSazw8kZ5hYvXNjI3/2aSk/7OMjP+bd3f/sIKC/9KV + lv/cnJz/2peY/9aRkf/koqL/+sG+//nAvf/5v7z/4amw/6qZx/+aouP/qpvP/+mxtv/2urj/6rGv/+S6 + u//ptrX/466n/+Ovqf/ssK7/6q6s/+isqv/oq6n/2J2b/6JubfFoPT2NOxoaFwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOBoaCFowMFd7SEjAomZm9sWC + gv/XkZL/25SV/9iSk//Wj5D/1IyN/9KHiP/UiIj/8bOx//rCv//3vbv/9ru4//O3s//xuLX/7q6e/+ej + hf/npIn/7bCp/+Otp/+KsX3/ULdm/1WjWv+7oYz/5KWk/9uenP+4gH79glJRzVYuLlQgCAkGAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAA8HBwQVy4uS3FBQaCPV1fjsG5v/cmAgf/ShYb/0YKD/85+f//LeXr/2I2M//e8uf/1vLn/7rOx/+2y + sP/lpJX/5qFY/+6xXP/djS3/35h9/86gl/9SwW7/Nd90/0WxXP+vlH//wYSE/49cW+VlOTmBQR4eHAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAGk7OhqIWFd8oG5u8J5qav+eX2D/tmts/8Z0df/KdHX/yXJz/92T + k//3vLn/7LGu/+Snpf/dm5L/4Z1q/+61dP/fmmX/15WM/9eYlv/Bm43/r6uR/6uNgP+WYWDtbkBAnUwn + JzQVAQECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiFJSBnhC + QgpqNDQJWSUlB08dHQdfKisKfENDFJJWViinbGtRvYOCjtOcm8/pt7X157y6/7eOjfhxRUW7aTk5m4RK + StehWlr6uGdo/8Zwcf/dkpH/8bSx/+OnpP/YmZj/1ZWT/9ealP/Vl5X/0JCP/8eIhv+zdnb/lFtc6nA/ + QKRSKio/JQwNBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADTn6AB2qioDMuUlCHBhYU8voCAWcCBgXTEhoaLzZGQqdeensngrKvn47Sz/NOop/+yiIfyi2Bgs2k+ + PlZXKysPAAAAAUYlJRxcMTFYcj4+pYpMTeWmXF3+xnl5/9+Zl//dnJr/z46M/8KCgf+vc3L/ll9e831L + S8hlOTl/TigoMy0REQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABzQUIDnmprDriGhifHlpZMzp6eeNCgoZ7On5+2yJqaybuPj9WnfHzVj2RkunVJ + SYNbLy8/PRQUCgAAAAAAAAAAAAAAAAAAAAAAAAAAKRUVBU0pKSphNDRtd0BAsotNTd2ZW1vrkVlY4HtJ + Sb5lOTmCUysrQTsbGxEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWCwsA2Y4OA5xQkImdkhIRHhKSll0R0dibUBAWWI2 + NkNUKCgoOhISDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMhkZB0km + Jh5LJiYsRSEhITATFAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAD/+H////8AAP/gH////wAA/8Af//// + AAD/gA////8AAP+AD////wAA/wAP////AAD/AA////8AAP4AB////wAA/gAH////AAD8AAf///8AAPwA + B////wAA/AAH////AAD8AAf///8AAPgAB////wAA+AAH//4HAAD4AAP/8AEAAPgAAf/AAQAA8AAA/wAA + AADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAEAAPAAAAAAAQAA8AAAAAADAADwAAAAAAcAAPAA + AAAADwAA+AAAAAAfAAD4AAAAAD8AAPwAAAAAfwAA/gAAAAD/AAD/gAAAA/8AAP/gAAAH/wAAgAAAAB// + AAAAAAAAf/8AAAAD4AP//wAAgB/8H///AAD///////8AAP///////wAA////////AAD///////8AAP// + /////wAA////////AAAoAAAAIAAAAEAAAAABACAAAAAAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAYAAAAZAAAAGQAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAARCQkYOh8fb0ooKK80HByiCQUFTAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAIhERFmA2Np2ITUz3lVNT/4dLS/5IKCi9AAAALwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAANjODiBllhY+61kZP+vY2P/pV5e/3xHRvEhEhJfAAAAAgAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAASSgoN41VVeS6bW3/xW9w/8dwcf+9bG3/klZW/jogIIEAAAAGAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZ1RkWcs2xs/8dxcv/HcHH/x3Bx/8Zwcf+iYWH/SSkpmAAA + AAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUC0tMZtgX+fGcnP/x3Bx/8dwcf/HcHH/x3Fy/61q + av9UMTGqAAAAEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxRER1tm9v/8hxcv/HcHH/x3Bx/8dw + cf/HcnP/tnRz/185OboAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAACIxXV7TEdHT/yHJz/8l1 + dv/Kd3j/ynd4/8p4eP/Bf37/bURDywAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNKysjo2Zm4Mt4 + ef/NfH3/z4GC/9GFhf/RhYb/0YWF/82Mi/9+UVHeCAICOwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAJAAAACwAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAGc+ + Pkm1c3P30IGC/9OJiv/XkZL/2ZaW/9mWl//YlJX/2JmY/5hnZfMeEBBrAAAABwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAA0FAgItHhAQWzAbG4IqFxeHDQcHWwAAABkAAAAAAAAAAAAA + AAAAAAAAek1MdMN/f//VjI3/2piZ/9+io//hqKn/4qmp/+Clpf/jpqT/wImH/04xMLwAAAA6AAAABQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAABEbDg5GRygokW5CQs+MVlbxnGJh/JdfXvxnPz7hHA8PbgAA + AAwAAAAAAAAAAAAAAACMW1qbz4qK/9qXl//gpqb/5rKz/+q6u//rvLz/6La2/+qxr//epKL/j1lZ+DUc + HLACAQFPAAAAHQAAAA8AAAAPAAAAEwAAACIbDg5MVDExnYZUU+SpbWz+uXl4/7x+fP/AgoD/xoeF/72A + f/9fOzu1AAAAHAAAAAAAAAAAAAAABJhkZK/VkZH/3Z+g/+axsf/twMD/8svL//LNzf/vxcX/8Lq4/+6z + sf+1dHP/j1VU+144N9g7IiKqMhwclDcfH5RGKSmiYTw7v4tZWOiydXT+woOC/8aKiP/Ol5X/2aWj/9ui + of/cnpz/2pyb/35TUrgAAAAVAAAAAAAAAAAAAAAFmmVkstaTk//hpaX/7Lm6//TLy//419f/+NnZ//TP + z//1wb//9Lq3/8aGhP+1dHP/s3Rz/6xwb/+pb27+rnNy/7Z7ev/BhIL/yY2L/8+WlP/apqT/5be2/+vB + v//rvrz/6bKw/+uvrf/Um5n/bUVEgAAAAAMAAAAAAAAAAAAAAAOTXV2q1ZGR/9CYmP+dfX7/o4yM/9e8 + vP/z0tL/zLOz/+u8u//5v7z/1peV/8uLif/Ki4r/yoyL/86Ukv/TnJv/2qSi/+Gtq//nuLb/7cPB//DJ + x//xxsT/8b+9//G6t//zubf/77az/6d1dM89Hx8lAAAAAAAAAAAAAAAAAAAAAIJOTojNiIn/jGlp/01O + Tv9UVlb/dnNz/7uhof+Pfn7/xJ+e//zCv//lqKb/3J2b/+Chnv/hpaT/7Ly5/+vHxv/MxMn/0MjN//LK + yf/1x8X/9sLA//a/vP/3vrv/+L+8//S7uP+5hoXhYTo5RwAAAAAAAAAAAAAAAAAAAAAAAAAAaTs7RrVz + dPKmfn7/cXJx/4SGhv97fX3/b2Zm/516ev+7kJD/+sG+//C2s//lqqr/rpbA/3aB2/+ql83/tMHK/2jc + 9P9OzOz/2r3B//q/vP/3vrv/9ry6//a8uf/ss7D/tYGA32c+Pk0AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAvEhIHg01Njbp9fvrCn5//nI+P/4R7ev+fgID/2Jyd/9ybnP/ytrT/+b+8/+ewtf+Mld3/ZI36/5eI + zv/Ttrn/sNLc/6/Czv/stLT/8re0/++0sf/tsq//2qCe/6Rxb8phODg+AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABCIB8MeUZGbqRpata8gYH8x4mJ/9eTk//YkpP/04qL/+Cbmv/5wL3/9726/+Sw + t//Zrrn/56qY/+2smf/lr6n/nLWJ/4Gtdf/Pppn/3qGf/7yEg/KJWViYTyoqIAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQh0dGXJAQGOXXl7NtnR1/8V7fP/MfH3/znt8/+il + o//0urj/7LCu/+Whg//rq13/35VX/9Kek/9yvXz/ZbNv/6iCdfqYY2O/aj4+TCUJCgcAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAACcamsBjFRVB4FERAh9PT0JjU1ND6VnZx+/hINF0JqZiNOjoty0iIf2hFBQw5lX + V8+wY2P4xXR0/+aioP/oq6j/2pqT/92fif/Vlor/yYqJ/7N8efiVZmPGdERFYkEfHxIAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAALiFhgXFkJEdx5CQSMqSknbNlZWbz5uaws2cnOXBlJPnqH18r4dc + XFFULy8OSCUlFm07O0+FSUmeoV1d3sF9fPrGhoX/snZ295xkZNiFUlKbbD4+T0UdHxIAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc0JDA5FgYRKdbm46onR0Zp9ycnuWampzhFlZVmY6 + OikvDAwHAAAAAAAAAAAAAAAAAAAAAB0ODgRULCwhbjo7UXhERGVrPDxHTCYmGxAAAQMAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP//////////////////////D////gf///wH///4A///+AP///AD///wA///8AP//+AD + ///gA//D4AH+AeAA+ADgAAAAwAAAAMAAAADAAAAB4AAAA+AAAAfgAAAP8AAAH/wAAD8AAAD/AAAD/wB4 + D//H////////////////////KAAAABgAAAAwAAAAAQAgAAAAAABgCQAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAABMAAAAtAAAAEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAgIO1cwMM1qOjrsHhAQmwAA + ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAATCgogfUhI6ahgYP6lXV3+f0hI9wIBAT0AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsGBgFPLy6kuW1t/sZv + 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 + + + \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCNonSyncSettings.Designer.cs b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCNonSyncSettings.Designer.cs new file mode 100644 index 0000000000..fb077a8279 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCNonSyncSettings.Designer.cs @@ -0,0 +1,135 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class AmstradCPCNonSyncSettings + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ZXSpectrumNonSyncSettings)); + this.OkBtn = new System.Windows.Forms.Button(); + this.CancelBtn = new System.Windows.Forms.Button(); + this.label1 = new System.Windows.Forms.Label(); + this.lblOSDVerbinfo = new System.Windows.Forms.Label(); + this.label4 = new System.Windows.Forms.Label(); + this.osdMessageVerbositycomboBox1 = new System.Windows.Forms.ComboBox(); + this.SuspendLayout(); + // + // OkBtn + // + this.OkBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.OkBtn.Location = new System.Drawing.Point(247, 142); + this.OkBtn.Name = "OkBtn"; + this.OkBtn.Size = new System.Drawing.Size(60, 23); + this.OkBtn.TabIndex = 3; + this.OkBtn.Text = "&OK"; + this.OkBtn.UseVisualStyleBackColor = true; + this.OkBtn.Click += new System.EventHandler(this.OkBtn_Click); + // + // CancelBtn + // + this.CancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.CancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.CancelBtn.Location = new System.Drawing.Point(313, 142); + this.CancelBtn.Name = "CancelBtn"; + this.CancelBtn.Size = new System.Drawing.Size(60, 23); + this.CancelBtn.TabIndex = 4; + this.CancelBtn.Text = "&Cancel"; + this.CancelBtn.UseVisualStyleBackColor = true; + this.CancelBtn.Click += new System.EventHandler(this.CancelBtn_Click); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 14); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(185, 13); + this.label1.TabIndex = 17; + this.label1.Text = "ZX Spectrum Misc Non-Sync Settings"; + // + // lblOSDVerbinfo + // + this.lblOSDVerbinfo.Font = new System.Drawing.Font("Lucida Console", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.lblOSDVerbinfo.Location = new System.Drawing.Point(175, 107); + this.lblOSDVerbinfo.Name = "lblOSDVerbinfo"; + this.lblOSDVerbinfo.Size = new System.Drawing.Size(196, 21); + this.lblOSDVerbinfo.TabIndex = 28; + this.lblOSDVerbinfo.Text = "null"; + this.lblOSDVerbinfo.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // label4 + // + this.label4.AutoSize = true; + this.label4.Location = new System.Drawing.Point(12, 91); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(125, 13); + this.label4.TabIndex = 27; + this.label4.Text = "OSD Message Verbosity:"; + // + // osdMessageVerbositycomboBox1 + // + this.osdMessageVerbositycomboBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.osdMessageVerbositycomboBox1.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.osdMessageVerbositycomboBox1.FormattingEnabled = true; + this.osdMessageVerbositycomboBox1.Location = new System.Drawing.Point(12, 107); + this.osdMessageVerbositycomboBox1.Name = "osdMessageVerbositycomboBox1"; + this.osdMessageVerbositycomboBox1.Size = new System.Drawing.Size(157, 21); + this.osdMessageVerbositycomboBox1.TabIndex = 26; + this.osdMessageVerbositycomboBox1.SelectionChangeCommitted += new System.EventHandler(this.OSDComboBox_SelectionChangeCommitted); + // + // ZXSpectrumNonSyncSettings + // + this.AcceptButton = this.OkBtn; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.CancelBtn; + this.ClientSize = new System.Drawing.Size(385, 177); + this.Controls.Add(this.lblOSDVerbinfo); + this.Controls.Add(this.label4); + this.Controls.Add(this.osdMessageVerbositycomboBox1); + this.Controls.Add(this.label1); + this.Controls.Add(this.CancelBtn); + this.Controls.Add(this.OkBtn); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.Name = "ZXSpectrumNonSyncSettings"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Other Non-Sync Settings"; + this.Load += new System.EventHandler(this.IntvControllerSettings_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button OkBtn; + private System.Windows.Forms.Button CancelBtn; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label lblOSDVerbinfo; + private System.Windows.Forms.Label label4; + private System.Windows.Forms.ComboBox osdMessageVerbositycomboBox1; + } +} \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCNonSyncSettings.cs b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCNonSyncSettings.cs new file mode 100644 index 0000000000..63dc150f70 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCNonSyncSettings.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using System.Windows.Forms; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Cores.Computers.AmstradCPC; +using System.Text; + +namespace BizHawk.Client.EmuHawk +{ + public partial class AmstradCPCNonSyncSettings : Form + { + private AmstradCPC.AmstradCPCSettings _settings; + + public AmstradCPCNonSyncSettings() + { + InitializeComponent(); + } + + private void IntvControllerSettings_Load(object sender, EventArgs e) + { + _settings = ((AmstradCPC)Global.Emulator).GetSettings().Clone(); + + + + // OSD Message Verbosity + var osdTypes = Enum.GetNames(typeof(AmstradCPC.OSDVerbosity)); + foreach (var val in osdTypes) + { + osdMessageVerbositycomboBox1.Items.Add(val); + } + osdMessageVerbositycomboBox1.SelectedItem = _settings.OSDMessageVerbosity.ToString(); + UpdateOSDNotes((AmstradCPC.OSDVerbosity)Enum.Parse(typeof(AmstradCPC.OSDVerbosity), osdMessageVerbositycomboBox1.SelectedItem.ToString())); + } + + private void OkBtn_Click(object sender, EventArgs e) + { + bool changed = + _settings.OSDMessageVerbosity.ToString() != osdMessageVerbositycomboBox1.SelectedItem.ToString(); + + if (changed) + { + _settings.OSDMessageVerbosity = (AmstradCPC.OSDVerbosity)Enum.Parse(typeof(AmstradCPC.OSDVerbosity), osdMessageVerbositycomboBox1.SelectedItem.ToString()); + + GlobalWin.MainForm.PutCoreSettings(_settings); + + DialogResult = DialogResult.OK; + Close(); + } + else + { + DialogResult = DialogResult.OK; + Close(); + } + } + + private void CancelBtn_Click(object sender, EventArgs e) + { + GlobalWin.OSD.AddMessage("Misc settings aborted"); + DialogResult = DialogResult.Cancel; + Close(); + } + + private void UpdateOSDNotes(AmstradCPC.OSDVerbosity type) + { + switch (type) + { + case AmstradCPC.OSDVerbosity.Full: + lblOSDVerbinfo.Text = "Show all OSD messages"; + break; + case AmstradCPC.OSDVerbosity.Medium: + lblOSDVerbinfo.Text = "Only show machine/device generated messages"; + break; + case AmstradCPC.OSDVerbosity.None: + lblOSDVerbinfo.Text = "No core-driven OSD messages"; + break; + } + } + + private void OSDComboBox_SelectionChangeCommitted(object sender, EventArgs e) + { + ComboBox cb = sender as ComboBox; + UpdateOSDNotes((AmstradCPC.OSDVerbosity)Enum.Parse(typeof(AmstradCPC.OSDVerbosity), cb.SelectedItem.ToString())); + } + } +} diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCNonSyncSettings.resx b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCNonSyncSettings.resx new file mode 100644 index 0000000000..ca821b54f8 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCNonSyncSettings.resx @@ -0,0 +1,624 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAwAMDAQAAAABABoBgAAxgAAACAgEAAAAAQA6AIAAC4HAAAYGBAAAAAEAOgBAAAWCgAAEBAQAAAA + BAAoAQAA/gsAADAwAAAAAAgAqA4AACYNAAAgIAAAAAAIAKgIAADOGwAAGBgAAAAACADIBgAAdiQAABAQ + AAAAAAgAaAUAAD4rAAAwMAAAAAAgAKglAACmMAAAICAAAAAAIACoEAAATlYAABgYAAAAACAAiAkAAPZm + AAAQEAAAAAAgAGgEAAB+cAAAKAAAADAAAABgAAAAAQAEAAAAAACABAAAAAAAAAAAAAAQAAAAEAAAAAAA + AAAAAIAAAIAAAACAgACAAAAAgACAAICAAACAgIAAwMDAAAAA/wAA/wAAAP//AP8AAAD/AP8A//8AAP// + /wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAHR3AAAAAAAAAAAAAAAAAAAAAAAAAAAAdHdEcAAAAAAAAAAAAAAAAA + AAAAAAAAAHd0d3QAAAAAAAAAAAAAAAAAAAAAAAAAAEd8d3UAAAAAAAAAAAAAAAAAAAAAAAAAB3yHfHZw + AAAAAAAAAAAAAAAAAAAAAAAAd3fIyHVwAAAAAAAAAAAAAAAAAAAAAAAAfHh3jIxwAAAAAAAAAAAAAAAA + AAAAAAAHd8jIyHdgAAAAAAAAAAAAAAAAAAAAAAAHd4yHfIdAAAAAAAAAAAAAAAAAAAAAAAAHyMjIyMhQ + AAAAAAAAAAAAAAAAAAAAAAB3d3eMh4dgAAAAAAAAAAAAAAAAAAAAAAB8jIyIfIdQAAAAAAAAAAAAAAAA + AAAAAAB3h4jIiMh3AAAAAAAAAAAAAAAAAAAAAAB8jIeHeIjHAAAAAAAAAAAAAAAAAAAAAAeIiHh4eMiE + AAAAAAAAAAAAB0dHcAAAAAd8h4eIiIiHcAAAAAAAAAB0d3d3RwAAAAeIeIiIiIh3RwAAAAAAAHR3d8h3 + dAAAAAfIh4iIiHiIx0cAAAAAdHh3eIeHhwAAAAeHiIiIiIiId3R3dHR0eHd4h4eHhAAAAAd4eIiIiIiH + x3d2d3eId4iIiIiIhwAAAAd4eIiI+IiIh3d3eHh3iIiIiIeHwAAAAAfIjHeIiIiIyIeHh4iIiIiIiIiI + cAAAAAeIQ0R3h3iIiMiIiIiIiIiIiIiEAAAAAAfIR3d3d0iIiIh4iIeIiIiIiHhAAAAAAAB4d3d3SHiI + h4fTiIi3iIiIeIwAAAAAAAB3h4d3eIeIiHiJiIuIiIh4jHAAAAAAAAAHyId3h3h4iIh4iIiIiIiHeAAA + AAAAAAAAB8iMiMjIiIiIh4h3aMjHAAAAAAAAAAAAAAdYyIeIiIiMjId6d4eAAAAAAAAAAAAAAAAHdsjH + eIeH6MiId3AAAAAAAAAAAAAAAIiIh4V8jIh4eIfHcAAAAAAAAAAAAACIiIh3AAAHd3h3fHcAAAAAAAAA + AAAAAAiIjHgAAAAAAHx8eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///////8AAP///////wAA////////AAD///////8AAP///////wAA//////// + AAD///////8AAP///////wAA//h/////AAD/4D////8AAP/AP////wAA/8A/////AAD/gB////8AAP8A + H////wAA/wAf////AAD+AB////8AAP4AH////wAA/gAf////AAD8AB////8AAPwAH////wAA/AAP//// + AAD8AA////8AAPgAD//+BwAA+AAH//ADAAD4AAP/wAMAAPgAAP8AAwAA+AAAAAADAAD4AAAAAAMAAPgA + AAAABwAA+AAAAAAHAAD4AAAAAA8AAPgAAAAAHwAA/AAAAAA/AAD8AAAAAH8AAP4AAAAA/wAA/4AAAAP/ + AAD/4AAAB/8AAP/4AAAf/wAA/8AAAH//AAD8A+AD//8AAPgP/A///wAA////////AAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAAoAAAAIAAAAEAAAAABAAQAAAAAAAAC + AAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAICAgADAwMAAAAD/AAD/ + AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdwAAAAAAAAAAAAAAAA + AAd0dAAAAAAAAAAAAAAAAAB3x3cAAAAAAAAAAAAAAAAAd3fHcAAAAAAAAAAAAAAAB3yMh3AAAAAAAAAA + AAAAAAfIeMdwAAAAAAAAAAAAAAAHjIyHQAAAAAAAAAAAAAAAfId4yHAAAAAAAAAAAAAAAHjIyIdQAAAA + AAAAAAAAAAB3iId4YAAAAAAAAAdwAAAAjIiIiIUAAAAAAHd3dAAAB4iIiHh8cAAAAHd3x4dwAAd4iIiI + h3Z3d3R3yIh4cAAHh4iIiIfHd3d4iIiIh3AAB3jHiIiIiHeHiIiIiIwAAAh3dXh4iMiIiIiIiIhwAAAA + yGd0d4iIeIi4iIiMAAAAAIeHd4iIh32IiIiIcAAAAAAAd4jIyIiIiHeHyAAAAAAAAAB3h4iIh8h3dwAA + AAAAAAAIh8fIh4eIaAAAAAAAAACIiHAAB8jIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////// + ////////////////////n////g////wP///8B///+Af///gH///4B///8Af///AH///wB//n8AP/A+AB + /AHgAAAB4AAAAeAAAAPgAAAH8AAAD/AAAB/8AAA//wAA//4AA//weA////////////////////////// + //8oAAAAGAAAADAAAAABAAQAAAAAACABAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAgAAAgAAAAICAAIAA + AACAAIAAgIAAAICAgADAwMAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHRwAAAAAAAAAAAAB3dAAAAAAAAAAAAA + d8dwAAAAAAAAAAAAfId3AAAAAAAAAAAHeMjHAAAAAAAAAAAHyHh3AAAAAAAAAAAHh3eEAAAAAAAAAAAI + yIiHAAAAAHd2cAAIiIiIQAAAd3d4UACHiIiId3d3eHiIcACHh4iIyHeHiIiIcAAIR3d4iIiIiIiMAAAH + d3eIh3iIiIhwAAAAeMh4iIiHiMAAAAAAAHfIiMh4aAAAAAAAiIgHyIfIAAAAAAAIgAAAAIAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8A////AP///wD8f/8A+H//APB/ + /wDwP/8A4D//AOA//wDgP/8A4D/BAOAfAQDAAAEAwAABAOAAAwDgAAcA8AAfAPwAPwDwgP8A5/f/AP// + /wD///8A////ACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACA + AAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAd1AAAAAAAAB8cAAAAAAAB4eAAAAAAAAHyMgAAAAAAAiIhwAAAHcACI + iHcAd3hwAIz4jIeIiIAAd3eIiIiIAACHeIiIiHAAAACMeMh4AAAAiAAIgAAAAAAAAAAAAAAAAAAAAAAA + AAD//wAA//8AAP//AADj/wAA4/8AAMP/AADB/wAAwfkAAMDBAADAAQAAwAMAAMAHAADwDwAAzn8AAP// + AAD//wAAKAAAADAAAABgAAAAAQAIAAAAAAAACQAAAAAAAAAAAAAAAQAAAAEAAAAAAAA9OzsAZD8/AGg8 + PABtPj4AQkNDAEZIRwBWQkIAV0REAF5AQABbRkYAVklJAFxPTwBTU1MAXFJSAF5ZWQBkQEAAYUREAGZF + RQBqQkEAYEtLAGNPTwBwQUEAfUZGAHJKSgB2SUkAfU9PAGBRUQBgVFQAZlZWAGZYWABqWVkAclZWAHpU + VAB9W1oAbmJiAGtoaABtaWkAcWdnAHdnZwB8Y2MAe2pqAHJxcQB+dHQAd3l5AHl6egCGT08AiU9PAIFP + UACGU1MAjVFRAIlWVgCMV1cAg1xbAIxaWQCQUlIAlVJSAJFXVgCXVVUAmVVVAJZaWQCSXV0AlV9eAJpZ + WgCeW1sAml5eAKBZWgCgXFwAql9fAIRmZQCIZWQAhWtrAI5ragCTYmEAnGBhAJ9kYwCaZmYAk25uAJ1s + awCFdHQAiXd3AIt+fgCWd3cAmHR0AJV5eQCbfHwAo2JhAKZhYQChZWUApGVkAKplZACsZGQAqmhnAKZr + agCnbGsAqmloAKlubQCsbW0AtGZnALhsbACxb3AAv29wAKVxcACrc3IAr35+ALN0cwC5c3MAvXBxALR4 + dgC1fHsAunt6AMNtbgDGb3AAw3FyAMZwcQDGdXUAyHR1AMp3eADBeXkAxnt7AMB/fgDLensANLBSAEWf + TgBBtFwAPMdnADHkdgDciiIAvoF/AISrdwDln0sA35lhAN2XfADgmmEA8LdlAO61cAArWPIALWT+AEh5 + +gDOf4AAfoCAAHiA1ABZv9wAZrnUAGK+2ABxnv4Ad6P/ADPX/QBw0OcAW+D7AIKEgwCPgoIAjI2NAJuC + ggCUiIgAmYqKAJGSkgCjhIQAqoKCAKKLiwC+hIMAsoqKALaSgQCum5sAsZubALqqlQCdgr4Ar6ytALGh + oAC6pKQAwoSDAMyBggDGiIYAyYiHAMWMigDMjIoA0ISFANKHiADUjIwA2Y6NAMCUjQDIk44A0JCPANaP + kADHlZQAzpSSAMScmwDUkpIA2ZSVANWYlgDampcA2ZeYANWcnADam5sA4p2cAMChjwDeoJ4A5aCFAOaj + jQDlpJoA2p6hAMOkowDOoaEAy62tANegoADdoqEA2aGpANGsrwDdq6kAwbG4ANGysQDdtLQA2ri3AOGk + owDjqKYA66ylAOGnqADjq6oA6a2rAOOwrwDssK4A5K+wAOaztADttLIA57i2AO24tgDmurgA6rq6APC1 + swDyuLYA9Ly5APi+uwD1wL0A+cC9AKKMwACkk8QAqprMALSayACptsEAlaDkAOy/wACRxtQAgOv9AJnr + 9wDEwsoA5sbGAOzCwgDuyMcA7MzMAPPEwgDxy8oA9dPTAPja2gAAAAAAAAAAAP///wAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AAAAAAAAAADLytDi9tOemQAAAAAAUy9EecLEsa1uPTUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPj11Mme + VakAAAAAAAAAAAAATS84M0akAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///////8AAP///////wAA////////AAD///////8AAP///////wAA//////// + AAD///////8AAP///////wAA//h/////AAD/4D////8AAP/AP////wAA/8A/////AAD/gB////8AAP8A + H////wAA/wAf////AAD+AB////8AAP4AH////wAA/gAf////AAD8AB////8AAPwAH////wAA/AAP//// + AAD8AA////8AAPgAD//+BwAA+AAH//ADAAD4AAP/wAMAAPgAAP8AAwAA+AAAAAADAAD4AAAAAAMAAPgA + AAAABwAA+AAAAAAHAAD4AAAAAA8AAPgAAAAAHwAA/AAAAAA/AAD8AAAAAH8AAP4AAAAA/wAA/4AAAAP/ + AAD/4AAAB/8AAP/4AAAf/wAA/8AAAH//AAD8A+AD//8AAPgP/A///wAA////////AAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAAoAAAAIAAAAEAAAAABAAgAAAAAAAAE + AAAAAAAAAAAAAAABAAAAAQAAAAAAAFFNTQBRUlIAU1RUAGJHRwBiT08Aa0lIAGJTUwBrVlYAYllZAGZc + XABpWloAb1xbAHNTUwB7V1YAc1hXAHFbWwBkZWUAaWFhAG5kZABpamkAcGFhAHlubgB2cHAAf3V1AH55 + eQB8fX0AgUpKAI1PTwCLWFcAhlhYAI9ZWQCKXFsAm1ZWAJJZWQCWWVgAmlpbAJtcWwCiXFwAl2BfAIBg + YACAZ2YAgG9vAI9oaACWZWQAmGBhAJ5kZACcaWoAmm9vAIV0dACNcHAAiXZ2AIB8fACac3IAm3V0AJ51 + dQCZfHwAnHx8AKNmZgCnZmYAqmJiAK5jYwCvb24AtWVmALBtbgC5bW0AvmxtAKx+fQCxcnIAtHBwALZz + dACydXQAtnd2ALlwcAC5dnYAt3p5ALh5eAC8fHsAun18ALx+fQDGb3AAxnBxAMdzdADAd3YAyHJzAMlz + dADJdXYAynd4AMd/fwDMe3wAzXx9AHunbwBhvHIAYsN4ANuLOwC2hn4A4Zt5APC3ZABte9sAX47+AHWM + 5QAl0foAY+P8AIeDgwCFhoYAioSEAJOIiACWi4sAmpKRAKGCgQCmhYUAqYGBAKuDhACniooApYyMAKiO + jQCyhYMAvoWEALeNjQCrj5AAr5eXALSVlAC9lJMAmbCEAK6RugDBgYAAwoSCAMWDhADChoQAxYeFAM6A + gQDFiIYAxoqIAMqIiQDMi4oAy4yKAMiPjQDPj44A0ISFANKJigDUi4wA04+NANWNjgDKkY8A0JCOANud + iQDWj5AAzJSTAM2XlgDGm5oA1pGSANOUkgDVl5EA1pOUANiVlgDYmJUA2ZeYANKenADbmpsA3pmYANuc + mgDbn5wA1aacAN6gngDqqZoA3Z+gAMyjowDCra0AxqysAMqpqQDboaAA3qKiAN6logDbp6UA3aWkANer + qgDWsbMA0rW0ANe0tADfs7IA4aSiAOGlpQDkp6UA46imAOWopgDsraIA6qimAOGoqADhrqwA6a2rAOqv + rADpsK4A7LGuAOGzswDlsbEA7bKxAO+1sgDotrYA5rm3AO+4twDot7sA6bq5AOu9uwDrv70A8bazAPG2 + tADxuLUA9Lm2APC9uwD2vboA9L+9APi+uwD4v7wA8sC+APXAvgD5wL0AkILJAKqXzACsu8cAqr/LALLV + 3QDawMIA48XFAOvDwQDswMAA7cTDAO/ExQDgxsgA8cbEAPTGxADwyskA9MvJAPLNzQD21dYA+NjZAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAMEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqHCEcBQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAayU9PSYbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdQlBSQiJpAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAM0pSUlJQPRcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnUlJSUlJGFQAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAFJSUlJSUkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzUlJSWVJZfxAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAC5XWYqKioqGDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASoqMkpqa + mqAsAAAAAAAAAAAAAAAAAABoNAAAAAAAAACMjJyuvLy2toYHAAAAAAAAAAAAABcOIDouBgAAAAAAc4yc + tsHKysPAriIKAAAAAAAAABYgRk1LTX+DEAAAAABukqXB4ejo4dHPQCIEChcXEwggTXV/k66unKMpAAAA + AG6Srsro6ero0dN/Rk1NRk2Dg4STrsbh4cHAt2sAAAAAbpKuOXPe6ajW15KGg4OGk528yuHo5eHPz882 + AAAAAAB4jCkDAxSoMabXt5yjt8ro3ePo5dbT09HTdAAAAAAAAABGcBFoGgFwdtfDwHxi2dpmZcrX09HP + z0MAAAAAAAAAAHh/qWwaOa6cz9PNZGPYsdzbzc3DwLk2AAAAAAAAAAAAAAAvhpKakoyg19HNyKS5wHtb + orZ/cwAAAAAAAAAAAAAAAAAANkaKWVm5zb1gYV6cXVxfNgAAAAAAAAAAAAAAAAAAALGvlTIuP1K5tqCR + l4xfLwAAAAAAAAAAAAAAAAAAsbPBenkAAAAAcCVYjE0scwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////////////////////+f///+D////A////wH + ///4B///+Af///gH///wB///8Af///AH/+fwA/8D4AH8AeAAAAHgAAAB4AAAA+AAAAfwAAAP8AAAH/wA + AD//AAD//gAD//B4D////////////////////////////ygAAAAYAAAAMAAAAAEACAAAAAAAQAIAAAAA + AAAAAAAAAAEAAAABAAAAAAAAWlJSAHBJSQB1SEgAe1dXAHdYWAB5WlkAel1dAGBiYgB1bGwAfWtrAHh2 + dgB9fn4Ag01NAIRXVwCIV1cAhV9eAItbWgCgX14ApV1dAJhgXwCNYGAAnWtqAJhtbQCCdnYAh3x8AI15 + eACeensAqGBgAKhoZwCga2oArGpqALNqagCzb28AtG1tALltbQCxb3AApnVzAKlzcwCqdHMApnp6AKd+ + fgCpensAq3x7ALZ3dgC8dHQAvH59AMZvcADGcHEAxXN0AMhycwDJdncAynh5AMx5egDNfn8Ajo1wAOek + VgDGgH8A4p53AEZ2+gB8u4AAd8PaAIuEhACOh4cAjo6OAJ+DggCejo4Ao4SEAKSIiACsi4sAqo2MAK6P + jgC+gYAAvoaGAL+KiACskJAAtJeXALWenQC5np4At6iOAKmyjgC9nroAwYSDAMaGhADOhoYAxomHAMiK + iQDJjYwA0oeIANOOjwDUjY0A2ZiPANaPkADGkZEAx5eXAMySkADGnZwA1ZOSANeTlADWl5YA2JSVANGZ + mADan50A3J6dAOCcmwDVoJ8A7K2fAMOtrQDXo6IA3aCgAN+kpADVq6oA3ay3AMu0tADPtrYA3L+/AOCi + oQDhpqUA5KelAOinpgDlq6gA46usAOOvrQDqrqwA7LGuAOayswDjtrQA5re1AOqysQDts7EA57y6AO+8 + ugDrvL0A8LOwAPC1sgDwtrQA87q3APS6twD2vboA8b69APi/vAD2wb4A+cC9AJmTzwDHqMMAu8PMAIHf + 8QDByNAA7cLCAO3FwwDvxsQA5cjIAOzOzgDwxcQA9cbEAPPP0AD10tIAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + BQMJAAAAAAAAAAAAAAAAAAAAAAAAAAAPHBMNAAAAAAAAAAAAAAAAAAAAAAAAABojLy8TAAAAAAAAAAAA + AAAAAAAAAAAAAB0wMDAiPgAAAAAAAAAAAAAAAAAAAAAAQjAwMDAtGAAAAAAAAAAAAAAAAAAAAAAAFzIy + NTU5CgAAAAAAAAAAAAAAAAAAAAAAIjZYWFxcBwAAAAAAAAAAAAAAAAAAAAAANlxtdW11JQAAAAAAAAAA + PgcRDgkAAAAAXG1/lISAZgMAAAAAABkVLC5SVhcAAABNY3WWnJuLfB8UBAcQHkhWaX91dSsAAABNY2BM + mJeCiVJSVl9laX+WloSJgEIAAAAAXAEIC0tGjnR0dJaRk5qNjIyJQwAAAAAAJkNADBtdjIaPO1GSPYuJ + hnVEAAAAAAAAAClISWRcd4xwkGp8UE90VwAAAAAAAAAAAAAAKSQ1NYZ7OjhbPDdGAAAAAAAAAAAAAHNv + YGsAKyJoXFYmRwAAAAAAAAAAAAAAcnIAAAAAAAAATgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP// + /wD///8A////APx//wD4f/8A8H//APA//wDgP/8A4D//AOA//wDgP8EA4B8BAMAAAQDAAAEA4AADAOAA + BwDwAB8A/AA/APCA/wDn9/8A////AP///wD///8AKAAAABAAAAAgAAAAAQAIAAAAAAAAAQAAAAAAAAAA + AAAAAQAAAAEAAAAAAABjZGQAdmRjAHtpaQB/eHgAgU9PAKBaWgCFbm0AlWtqAKptbgCwZ2cAsGhoAKxw + cACteHkAvnJyAMZvcADGcHEAy3l5AMx9fgCFmXQAwIB/ANeUfQDhoX8AlIqJAJWMjACYiIgAoIaGAK2K + igCxh4cAvoGAALKKigC4iYgAuJWVAL2cnACss50AuqKhAL+mpgDLgoIAxImHAMeNjADLkI8AxpWTANCS + kQDYlZUA1J6dANqZmgDdnp4A1J+oAMaiogDOr68AzLKyANi5uADhpaIA4qypAOWtqADrrqsA4bKwAOay + sgDtuLYA57++AOy4uADxtLIA8be0APa9ugDswL4A9sG+ALCcxwC5ncIA06zBALnH0QC2ytQA7sPDAPLS + 0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAZBgUAAAAAAAAAAAAAAAAACw8KAAAAAAAAAAAAAAAAGhAQDgAAAAAAAAAAAAAAAAkRESUYAAAA + AAAAAAAAAAAlKy4uBwAAAAAAAAcDAAAAKzlHPCYCAAAYCB0oKgAAAC0wSDs0FB0nLDlAOiwAAAANAQQb + Pi9DRkVBPzUAAAAAJB4cKz5EQjMiNSkAAAAAAAAAHwwRNxYVEyQAAAAAAAAxMgAAACEgAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP//AAD//wAA4/8AAOP/AADD/wAAwf8AAMH5 + AADAwQAAwAEAAMADAADABwAA8A8AAM5/AAD//wAA//8AACgAAAAwAAAAYAAAAAEAIAAAAAAAgCUAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAkAAAAJAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAUAAAAOAEBAVUAAABUAAAANQAAABAAAAABAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAkFBSUvGRl5TCkpwlYuLtxDJCTQFw0NmQAA + AEkAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACGAwMKE8rK6V6RET2klJR/5ZS + U/+OT0//ZDc38B0QEJoAAAAyAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYDAwYVzAwoopP + T/ygXVz/oFtb/55ZWf+bWFf/k1NT/1UvL9wGAwNcAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AARNKipxhk5O+adkY/+uZWX/tWdo/7VmZ/+qYWH/nltb/3hERPcfERGCAAAAFgAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAADEZGS1zQ0LXqGdm/7ptbf/Fb3D/x3Bx/8hwcf/BbW7/q2Vl/4hPT/82HR2gAAAAIAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAB1gxMYyYXl3/vXFx/8Zwcf/HcHH/x3Bx/8dwcf/HcHH/uG1t/5NY + V/9EJia2AAAAKQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPB8fNH1MS+K4cnH/x3Fy/8dwcf/HcHH/x3Bx/8dw + cf/HcHH/wHBx/51gX/9PLCzGAAAAMwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACXjU1h6NnZv/Fc3T/x3Bx/8dw + cf/HcHH/x3Bx/8dwcf/HcHH/w3Jz/6ZoZ/9ZMzPTAQAAPQAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyFxccektK0b12 + dv/HcHH/x3Bx/8dwcf/HcHH/x3Bx/8dwcf/HcHH/xXR0/69wb/9jOjneBwMDSQAAAAUAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AABNKSlNlmBf9sh3d//HcHH/x3Bx/8dwcf/HcHH/x3Bx/8dwcf/HcHH/xnd3/7Z4d/9sQUDnDgcHVQAA + AAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABkOjqKsXFw/8lyc//HcXL/yHJz/8l0df/JdXb/yXV2/8l1dv/JdHX/ynt7/7+B + f/94SknvFgsLZQAAAAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAACILCxB7TUzDwXd3/8lyc//KdXb/y3h5/8x7fP/NfX7/zX5+/819 + fv/NfH3/zoOC/8iJiP+GVVX3Hg8QegAAABIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEMiIi+SXl3oynp7/8t4ef/NfX7/z4GC/9GE + hf/Sh4j/04iJ/9KIiP/Rhof/04uK/8+RkP+XY2L9KxcXlwAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAABwAA + AA0AAAAPAAAACwAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFUvL1enbW37zn5+/85/ + gP/Rhob/1IuM/9aPkP/XkpP/2JOU/9iTlP/XkZH/15OT/9eZl/+rdHP/QSUlvAAAADwAAAAFAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAACQAA + ABgAAAAvAgEBSwcDA2EFAgJoAAAAWAAAADYAAAARAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGU8 + O4W5eXn/0IKD/9KIif/Wj5D/2ZWW/9ubm//dnp//3qCg/92foP/cnZ3/3Jyc/9+in//CiYf/Zj8/4wYC + AnAAAAAbAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAA + AA4AAAAnCQQEUCISEoQ+IiKzVzEx1mU6OuZiOTnmRigo0hgNDZsAAABMAAAAEAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAABnVJSK/HhIP/04eI/9aQkf/amJn/3qCh/+Gmp//jq6v/5Kyt/+OsrP/iqan/4aal/+ap + p//Umpj/nmxr/C8ZGboAAABXAAAAGAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAIAAAAOAQAALRkNDWY+IiKpZDo63YZRUfigZGP/sHBv/7V0c/+xcnH/oWZm/2k+PvEfEBCcAAAAMQAA + AAMAAAAAAAAAAAAAAAAAAAAALhAQFIZXVs/RjIz/1Y2O/9qYmP/eoaL/46qr/+aysv/ot7f/6rm5/+m4 + uf/otbX/5q+v/+uvrf/jqab/wYeF/28/P/QhEhKvAAAAXwAAACgAAAANAAAABQAAAAMAAAACAAAAAwAA + AAUAAAAKAAAAFQAAADAdDg9oSSkptHZHRu2dYmL+t3Z1/758e/+6enn/tnh3/7d5eP+8fn3/w4SD/7Z6 + ef9eODfbBgICTgAAAAgAAAAAAAAAAAAAAAAAAAAAPhwcJJVjYuPXkZH/2JOU/92fn//iqqr/57O0/+u8 + vP/uwsL/78XG/+/Exf/twMD/67i4/+60sv/wtrP/zZKQ/5taWv9xQED2MRsaxAgEBIcAAABaAAAAQQAA + ADcAAAA2AAAAOwAAAEUEAgJZHA4OfUcnJ7l5SkntqGxr/8CAfv/DgoH/vH59/7p+ff/DiIb/zZGP/9GT + kf/UlJP/1peV/9eZl/+GVlbuGQsLVwAAAAcAAAAAAAAAAAAAAAAAAAAARiIiLZ9rauvZk5P/2peY/+Ck + pP/lsLD/6ru7/+/Fxf/yzMz/9NDQ//PPz//xycr/7sDA//K5tv/1u7j/36Kg/6dmZf+mZWX/j1ZW/WM6 + OutDJSXQNBwcvDAaGrQ0HBy1PiIivUwsLMtkPDzfh1VU9a1xcP/EhIP/xIWE/7+Cgf/Ch4b/zZST/9mk + ov/grq3/4a6t/96lo//eoJ7/36Kg/+Cjof+IWVjnGwwMQwAAAAIAAAAAAAAAAAAAAAAAAAAARyQkL6Br + auzZk5P/25qb/+GnqP/ntLT/7cDA//LLy//209T/+NjY//fX1//00ND/8cbG//W9u//4vrz/46ak/7d0 + c/+vb27/s3Jy/7d2df+ucXD/pWpp/6Npaf+nbWz/sHVz/7p9fP/EhYT/yImI/8WIhv/DiIb/ypGP/9eg + n//hr63/57q5/+rCwP/rwsD/6bq4/+evrf/nq6n/6q6r/9qgnv9wRkbDBwAAHgAAAAAAAAAAAAAAAAAA + AAAAAAAASCQkLZ1nZuvYkpP/25uc/+Opqv/qtrf/7cHB//TOzv/52Nj/+tzc//na2v/xz9D/8MfH//fA + vv/6wb7/6a6r/8OBgP/DgoD/vX58/7h7ev+8fn3/woOC/8aHhv/HiYj/xoqJ/8aLif/Ijoz/zZST/9eg + nv/hrav/6Lm3/+zCwf/uyMf/78nH/+/Dwf/uvLr/7ba0/+60sf/vtLL/8ri1/7J+fflMKSltAAAABAAA + AAAAAAAAAAAAAAAAAAAAAAAAQyEhI5JcXOPWj5D/3Juc/8qVlf+BZmb/bl5e/4l4eP/AqKj/8tPT//LO + zv+5p6b/w6qq//fBv//7wr//8LWy/86Ojf/Ojoz/0ZGP/9GSkP/OkY//zpOR/9GamP/VoJ//2qel/+Gv + rf/nt7X/6727/+3Dwf/wycf/8czL//LLyf/yxsT/8cC+//G7uf/yubf/87m3//S7uP/4vrv/1J6c/3JH + RrAdCgsWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANRcXEYJNTcvPiIn/15aW/2VNTf85Ojr/Q0VF/0JF + RP9dXFz/n5GR/+S/v/+bh4f/hXp6/+25uP/7wr//9bu4/9qcmv/Zmpj/252b/96gnf/ipKH/5q+s/+u+ + vP/vycf/8srI/+3Hxv/wysj/9c7M//TNy//0ysj/9MbE//TBv//1vrz/9r26//e9u//4vrv/+L+8//vB + vv/hqqf/g1ZVzDwcHC4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAW4+Ppq/env/05OT/2ZX + V/9rbm7/fX9//3l6ev99f3//cHJy/5F9ff+ff3//XFhY/9eop//8wr//+L+8/+Wppv/ipaP/5qil/96i + pP/Kmaz/1qi1//LGxP/tyMf/qb3J/23E3P9kw9//vMTN//jDwP/3wb//+MC9//i/vf/5v73/+b+8//i/ + vP/3vrv/+L68/92mo/+IWlnRRSMjOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFcv + L0mbX1/y15GS/6GAgP9XV1b/iYuL/4CBgf98fX3/cnR0/1dPT/++j4//km9w/9Sfnv/6wL3/+cC9/+6z + sP/ssK3/0Z+u/4OH1P9YffD/QGPs/7KYyv/Ct7z/Ytrz/3Ts//8s2f//cbvU//m+u//4v7z/+L67//e9 + uv/1vLn/9Lq3//O5tv/zuLX/0puZ/4RVVctGIyM4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAADIXFwdrPDySq2ts/diZmf/ApKT/sKur/4CBgP95enr/iYiI/49zdP/do6P/36Ch/96e + nv/zuLX/+sK///W7uP/1ubT/qZC//2qY+/9tnf//MGT6/56FxP/esK//nMbS/57n8/9+z+T/ybG3//a6 + t//zubb/8re0//C1s//utLH/7rKw/+qvrP++iIb9dklJtkMgISoAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABHIyMSazw8kZ5hYvXNjI3/2aSk/7OMjP+bd3f/sIKC/9KV + lv/cnJz/2peY/9aRkf/koqL/+sG+//nAvf/5v7z/4amw/6qZx/+aouP/qpvP/+mxtv/2urj/6rGv/+S6 + u//ptrX/466n/+Ovqf/ssK7/6q6s/+isqv/oq6n/2J2b/6JubfFoPT2NOxoaFwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOBoaCFowMFd7SEjAomZm9sWC + gv/XkZL/25SV/9iSk//Wj5D/1IyN/9KHiP/UiIj/8bOx//rCv//3vbv/9ru4//O3s//xuLX/7q6e/+ej + hf/npIn/7bCp/+Otp/+KsX3/ULdm/1WjWv+7oYz/5KWk/9uenP+4gH79glJRzVYuLlQgCAkGAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAA8HBwQVy4uS3FBQaCPV1fjsG5v/cmAgf/ShYb/0YKD/85+f//LeXr/2I2M//e8uf/1vLn/7rOx/+2y + sP/lpJX/5qFY/+6xXP/djS3/35h9/86gl/9SwW7/Nd90/0WxXP+vlH//wYSE/49cW+VlOTmBQR4eHAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAGk7OhqIWFd8oG5u8J5qav+eX2D/tmts/8Z0df/KdHX/yXJz/92T + k//3vLn/7LGu/+Snpf/dm5L/4Z1q/+61dP/fmmX/15WM/9eYlv/Bm43/r6uR/6uNgP+WYWDtbkBAnUwn + JzQVAQECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiFJSBnhC + QgpqNDQJWSUlB08dHQdfKisKfENDFJJWViinbGtRvYOCjtOcm8/pt7X157y6/7eOjfhxRUW7aTk5m4RK + StehWlr6uGdo/8Zwcf/dkpH/8bSx/+OnpP/YmZj/1ZWT/9ealP/Vl5X/0JCP/8eIhv+zdnb/lFtc6nA/ + QKRSKio/JQwNBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADTn6AB2qioDMuUlCHBhYU8voCAWcCBgXTEhoaLzZGQqdeensngrKvn47Sz/NOop/+yiIfyi2Bgs2k+ + PlZXKysPAAAAAUYlJRxcMTFYcj4+pYpMTeWmXF3+xnl5/9+Zl//dnJr/z46M/8KCgf+vc3L/ll9e831L + S8hlOTl/TigoMy0REQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABzQUIDnmprDriGhifHlpZMzp6eeNCgoZ7On5+2yJqaybuPj9WnfHzVj2RkunVJ + SYNbLy8/PRQUCgAAAAAAAAAAAAAAAAAAAAAAAAAAKRUVBU0pKSphNDRtd0BAsotNTd2ZW1vrkVlY4HtJ + Sb5lOTmCUysrQTsbGxEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWCwsA2Y4OA5xQkImdkhIRHhKSll0R0dibUBAWWI2 + NkNUKCgoOhISDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMhkZB0km + Jh5LJiYsRSEhITATFAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAD/+H////8AAP/gH////wAA/8Af//// + AAD/gA////8AAP+AD////wAA/wAP////AAD/AA////8AAP4AB////wAA/gAH////AAD8AAf///8AAPwA + B////wAA/AAH////AAD8AAf///8AAPgAB////wAA+AAH//4HAAD4AAP/8AEAAPgAAf/AAQAA8AAA/wAA + AADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAEAAPAAAAAAAQAA8AAAAAADAADwAAAAAAcAAPAA + AAAADwAA+AAAAAAfAAD4AAAAAD8AAPwAAAAAfwAA/gAAAAD/AAD/gAAAA/8AAP/gAAAH/wAAgAAAAB// + AAAAAAAAf/8AAAAD4AP//wAAgB/8H///AAD///////8AAP///////wAA////////AAD///////8AAP// + /////wAA////////AAAoAAAAIAAAAEAAAAABACAAAAAAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAYAAAAZAAAAGQAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAARCQkYOh8fb0ooKK80HByiCQUFTAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAIhERFmA2Np2ITUz3lVNT/4dLS/5IKCi9AAAALwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAANjODiBllhY+61kZP+vY2P/pV5e/3xHRvEhEhJfAAAAAgAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAASSgoN41VVeS6bW3/xW9w/8dwcf+9bG3/klZW/jogIIEAAAAGAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZ1RkWcs2xs/8dxcv/HcHH/x3Bx/8Zwcf+iYWH/SSkpmAAA + AAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUC0tMZtgX+fGcnP/x3Bx/8dwcf/HcHH/x3Fy/61q + av9UMTGqAAAAEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxRER1tm9v/8hxcv/HcHH/x3Bx/8dw + cf/HcnP/tnRz/185OboAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAACIxXV7TEdHT/yHJz/8l1 + dv/Kd3j/ynd4/8p4eP/Bf37/bURDywAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNKysjo2Zm4Mt4 + ef/NfH3/z4GC/9GFhf/RhYb/0YWF/82Mi/9+UVHeCAICOwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAJAAAACwAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAGc+ + Pkm1c3P30IGC/9OJiv/XkZL/2ZaW/9mWl//YlJX/2JmY/5hnZfMeEBBrAAAABwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAA0FAgItHhAQWzAbG4IqFxeHDQcHWwAAABkAAAAAAAAAAAAA + AAAAAAAAek1MdMN/f//VjI3/2piZ/9+io//hqKn/4qmp/+Clpf/jpqT/wImH/04xMLwAAAA6AAAABQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAABEbDg5GRygokW5CQs+MVlbxnGJh/JdfXvxnPz7hHA8PbgAA + AAwAAAAAAAAAAAAAAACMW1qbz4qK/9qXl//gpqb/5rKz/+q6u//rvLz/6La2/+qxr//epKL/j1lZ+DUc + HLACAQFPAAAAHQAAAA8AAAAPAAAAEwAAACIbDg5MVDExnYZUU+SpbWz+uXl4/7x+fP/AgoD/xoeF/72A + f/9fOzu1AAAAHAAAAAAAAAAAAAAABJhkZK/VkZH/3Z+g/+axsf/twMD/8svL//LNzf/vxcX/8Lq4/+6z + sf+1dHP/j1VU+144N9g7IiKqMhwclDcfH5RGKSmiYTw7v4tZWOiydXT+woOC/8aKiP/Ol5X/2aWj/9ui + of/cnpz/2pyb/35TUrgAAAAVAAAAAAAAAAAAAAAFmmVkstaTk//hpaX/7Lm6//TLy//419f/+NnZ//TP + z//1wb//9Lq3/8aGhP+1dHP/s3Rz/6xwb/+pb27+rnNy/7Z7ev/BhIL/yY2L/8+WlP/apqT/5be2/+vB + v//rvrz/6bKw/+uvrf/Um5n/bUVEgAAAAAMAAAAAAAAAAAAAAAOTXV2q1ZGR/9CYmP+dfX7/o4yM/9e8 + vP/z0tL/zLOz/+u8u//5v7z/1peV/8uLif/Ki4r/yoyL/86Ukv/TnJv/2qSi/+Gtq//nuLb/7cPB//DJ + x//xxsT/8b+9//G6t//zubf/77az/6d1dM89Hx8lAAAAAAAAAAAAAAAAAAAAAIJOTojNiIn/jGlp/01O + Tv9UVlb/dnNz/7uhof+Pfn7/xJ+e//zCv//lqKb/3J2b/+Chnv/hpaT/7Ly5/+vHxv/MxMn/0MjN//LK + yf/1x8X/9sLA//a/vP/3vrv/+L+8//S7uP+5hoXhYTo5RwAAAAAAAAAAAAAAAAAAAAAAAAAAaTs7RrVz + dPKmfn7/cXJx/4SGhv97fX3/b2Zm/516ev+7kJD/+sG+//C2s//lqqr/rpbA/3aB2/+ql83/tMHK/2jc + 9P9OzOz/2r3B//q/vP/3vrv/9ry6//a8uf/ss7D/tYGA32c+Pk0AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAvEhIHg01Njbp9fvrCn5//nI+P/4R7ev+fgID/2Jyd/9ybnP/ytrT/+b+8/+ewtf+Mld3/ZI36/5eI + zv/Ttrn/sNLc/6/Czv/stLT/8re0/++0sf/tsq//2qCe/6Rxb8phODg+AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABCIB8MeUZGbqRpata8gYH8x4mJ/9eTk//YkpP/04qL/+Cbmv/5wL3/9726/+Sw + t//Zrrn/56qY/+2smf/lr6n/nLWJ/4Gtdf/Pppn/3qGf/7yEg/KJWViYTyoqIAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQh0dGXJAQGOXXl7NtnR1/8V7fP/MfH3/znt8/+il + o//0urj/7LCu/+Whg//rq13/35VX/9Kek/9yvXz/ZbNv/6iCdfqYY2O/aj4+TCUJCgcAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAACcamsBjFRVB4FERAh9PT0JjU1ND6VnZx+/hINF0JqZiNOjoty0iIf2hFBQw5lX + V8+wY2P4xXR0/+aioP/oq6j/2pqT/92fif/Vlor/yYqJ/7N8efiVZmPGdERFYkEfHxIAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAALiFhgXFkJEdx5CQSMqSknbNlZWbz5uaws2cnOXBlJPnqH18r4dc + XFFULy8OSCUlFm07O0+FSUmeoV1d3sF9fPrGhoX/snZ295xkZNiFUlKbbD4+T0UdHxIAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc0JDA5FgYRKdbm46onR0Zp9ycnuWampzhFlZVmY6 + OikvDAwHAAAAAAAAAAAAAAAAAAAAAB0ODgRULCwhbjo7UXhERGVrPDxHTCYmGxAAAQMAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP//////////////////////D////gf///wH///4A///+AP///AD///wA///8AP//+AD + ///gA//D4AH+AeAA+ADgAAAAwAAAAMAAAADAAAAB4AAAA+AAAAfgAAAP8AAAH/wAAD8AAAD/AAAD/wB4 + D//H////////////////////KAAAABgAAAAwAAAAAQAgAAAAAABgCQAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAABMAAAAtAAAAEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAgIO1cwMM1qOjrsHhAQmwAA + ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAATCgogfUhI6ahgYP6lXV3+f0hI9wIBAT0AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsGBgFPLy6kuW1t/sZv + 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 + + + \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCPokeMemory.Designer.cs b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCPokeMemory.Designer.cs new file mode 100644 index 0000000000..63593d8e8a --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCPokeMemory.Designer.cs @@ -0,0 +1,164 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class AmstradCPCPokeMemory + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AmstradCPCPokeMemory)); + this.OkBtn = new System.Windows.Forms.Button(); + this.CancelBtn = new System.Windows.Forms.Button(); + this.label1 = new System.Windows.Forms.Label(); + this.label4 = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.numericUpDownAddress = new System.Windows.Forms.NumericUpDown(); + this.label3 = new System.Windows.Forms.Label(); + this.numericUpDownByte = new System.Windows.Forms.NumericUpDown(); + ((System.ComponentModel.ISupportInitialize)(this.numericUpDownAddress)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.numericUpDownByte)).BeginInit(); + this.SuspendLayout(); + // + // OkBtn + // + this.OkBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.OkBtn.Location = new System.Drawing.Point(150, 109); + this.OkBtn.Name = "OkBtn"; + this.OkBtn.Size = new System.Drawing.Size(60, 23); + this.OkBtn.TabIndex = 3; + this.OkBtn.Text = "&OK"; + this.OkBtn.UseVisualStyleBackColor = true; + this.OkBtn.Click += new System.EventHandler(this.OkBtn_Click); + // + // CancelBtn + // + this.CancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.CancelBtn.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.CancelBtn.Location = new System.Drawing.Point(216, 109); + this.CancelBtn.Name = "CancelBtn"; + this.CancelBtn.Size = new System.Drawing.Size(60, 23); + this.CancelBtn.TabIndex = 4; + this.CancelBtn.Text = "&Cancel"; + this.CancelBtn.UseVisualStyleBackColor = true; + this.CancelBtn.Click += new System.EventHandler(this.CancelBtn_Click); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(12, 14); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(273, 13); + this.label1.TabIndex = 17; + this.label1.Text = "Enter an address to POKE along with a single byte value"; + // + // label4 + // + this.label4.AutoSize = true; + this.label4.Location = new System.Drawing.Point(12, 52); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(93, 13); + this.label4.TabIndex = 27; + this.label4.Text = "Address (0-65535)"; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(12, 27); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(254, 13); + this.label2.TabIndex = 29; + this.label2.Text = "(This will always target the 64K RAM address space)"; + // + // numericUpDownAddress + // + this.numericUpDownAddress.Location = new System.Drawing.Point(15, 69); + this.numericUpDownAddress.Maximum = new decimal(new int[] { + 65535, + 0, + 0, + 0}); + this.numericUpDownAddress.Name = "numericUpDownAddress"; + this.numericUpDownAddress.Size = new System.Drawing.Size(90, 20); + this.numericUpDownAddress.TabIndex = 30; + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Location = new System.Drawing.Point(123, 52); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(70, 13); + this.label3.TabIndex = 31; + this.label3.Text = "Value (0-255)"; + // + // numericUpDownByte + // + this.numericUpDownByte.Location = new System.Drawing.Point(126, 68); + this.numericUpDownByte.Maximum = new decimal(new int[] { + 255, + 0, + 0, + 0}); + this.numericUpDownByte.Name = "numericUpDownByte"; + this.numericUpDownByte.Size = new System.Drawing.Size(67, 20); + this.numericUpDownByte.TabIndex = 32; + // + // AmstradCPCPokeMemory + // + this.AcceptButton = this.OkBtn; + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.CancelButton = this.CancelBtn; + this.ClientSize = new System.Drawing.Size(288, 144); + this.Controls.Add(this.numericUpDownByte); + this.Controls.Add(this.label3); + this.Controls.Add(this.numericUpDownAddress); + this.Controls.Add(this.label2); + this.Controls.Add(this.label4); + this.Controls.Add(this.label1); + this.Controls.Add(this.CancelBtn); + this.Controls.Add(this.OkBtn); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.Name = "AmstradCPCPokeMemory"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Poke Memory"; + ((System.ComponentModel.ISupportInitialize)(this.numericUpDownAddress)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.numericUpDownByte)).EndInit(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button OkBtn; + private System.Windows.Forms.Button CancelBtn; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label4; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.NumericUpDown numericUpDownAddress; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.NumericUpDown numericUpDownByte; + } +} \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCPokeMemory.cs b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCPokeMemory.cs new file mode 100644 index 0000000000..ba99cd60c5 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCPokeMemory.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using System.Windows.Forms; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Cores.Computers.AmstradCPC; +using System.Text; + +namespace BizHawk.Client.EmuHawk +{ + public partial class AmstradCPCPokeMemory : Form + { + private AmstradCPC.AmstradCPCSettings _settings; + + public AmstradCPCPokeMemory() + { + InitializeComponent(); + } + + private void OkBtn_Click(object sender, EventArgs e) + { + var ams = (AmstradCPC)Global.Emulator; + var addr = (ushort)numericUpDownAddress.Value; + var val = (byte)numericUpDownByte.Value; + + ams.PokeMemory(addr, val); + + DialogResult = DialogResult.OK; + Close(); + } + + private void CancelBtn_Click(object sender, EventArgs e) + { + GlobalWin.OSD.AddMessage("POKE memory aborted"); + DialogResult = DialogResult.Cancel; + Close(); + } + } +} diff --git a/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCPokeMemory.resx b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCPokeMemory.resx new file mode 100644 index 0000000000..ca821b54f8 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/AmstradCPC/AmstradCPCPokeMemory.resx @@ -0,0 +1,624 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + AAABAAwAMDAQAAAABABoBgAAxgAAACAgEAAAAAQA6AIAAC4HAAAYGBAAAAAEAOgBAAAWCgAAEBAQAAAA + BAAoAQAA/gsAADAwAAAAAAgAqA4AACYNAAAgIAAAAAAIAKgIAADOGwAAGBgAAAAACADIBgAAdiQAABAQ + AAAAAAgAaAUAAD4rAAAwMAAAAAAgAKglAACmMAAAICAAAAAAIACoEAAATlYAABgYAAAAACAAiAkAAPZm + AAAQEAAAAAAgAGgEAAB+cAAAKAAAADAAAABgAAAAAQAEAAAAAACABAAAAAAAAAAAAAAQAAAAEAAAAAAA + AAAAAIAAAIAAAACAgACAAAAAgACAAICAAACAgIAAwMDAAAAA/wAA/wAAAP//AP8AAAD/AP8A//8AAP// + /wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAHR3AAAAAAAAAAAAAAAAAAAAAAAAAAAAdHdEcAAAAAAAAAAAAAAAAA + AAAAAAAAAHd0d3QAAAAAAAAAAAAAAAAAAAAAAAAAAEd8d3UAAAAAAAAAAAAAAAAAAAAAAAAAB3yHfHZw + AAAAAAAAAAAAAAAAAAAAAAAAd3fIyHVwAAAAAAAAAAAAAAAAAAAAAAAAfHh3jIxwAAAAAAAAAAAAAAAA + AAAAAAAHd8jIyHdgAAAAAAAAAAAAAAAAAAAAAAAHd4yHfIdAAAAAAAAAAAAAAAAAAAAAAAAHyMjIyMhQ + AAAAAAAAAAAAAAAAAAAAAAB3d3eMh4dgAAAAAAAAAAAAAAAAAAAAAAB8jIyIfIdQAAAAAAAAAAAAAAAA + AAAAAAB3h4jIiMh3AAAAAAAAAAAAAAAAAAAAAAB8jIeHeIjHAAAAAAAAAAAAAAAAAAAAAAeIiHh4eMiE + AAAAAAAAAAAAB0dHcAAAAAd8h4eIiIiHcAAAAAAAAAB0d3d3RwAAAAeIeIiIiIh3RwAAAAAAAHR3d8h3 + dAAAAAfIh4iIiHiIx0cAAAAAdHh3eIeHhwAAAAeHiIiIiIiId3R3dHR0eHd4h4eHhAAAAAd4eIiIiIiH + x3d2d3eId4iIiIiIhwAAAAd4eIiI+IiIh3d3eHh3iIiIiIeHwAAAAAfIjHeIiIiIyIeHh4iIiIiIiIiI + cAAAAAeIQ0R3h3iIiMiIiIiIiIiIiIiEAAAAAAfIR3d3d0iIiIh4iIeIiIiIiHhAAAAAAAB4d3d3SHiI + h4fTiIi3iIiIeIwAAAAAAAB3h4d3eIeIiHiJiIuIiIh4jHAAAAAAAAAHyId3h3h4iIh4iIiIiIiHeAAA + AAAAAAAAB8iMiMjIiIiIh4h3aMjHAAAAAAAAAAAAAAdYyIeIiIiMjId6d4eAAAAAAAAAAAAAAAAHdsjH + eIeH6MiId3AAAAAAAAAAAAAAAIiIh4V8jIh4eIfHcAAAAAAAAAAAAACIiIh3AAAHd3h3fHcAAAAAAAAA + AAAAAAiIjHgAAAAAAHx8eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///////8AAP///////wAA////////AAD///////8AAP///////wAA//////// + AAD///////8AAP///////wAA//h/////AAD/4D////8AAP/AP////wAA/8A/////AAD/gB////8AAP8A + H////wAA/wAf////AAD+AB////8AAP4AH////wAA/gAf////AAD8AB////8AAPwAH////wAA/AAP//// + AAD8AA////8AAPgAD//+BwAA+AAH//ADAAD4AAP/wAMAAPgAAP8AAwAA+AAAAAADAAD4AAAAAAMAAPgA + AAAABwAA+AAAAAAHAAD4AAAAAA8AAPgAAAAAHwAA/AAAAAA/AAD8AAAAAH8AAP4AAAAA/wAA/4AAAAP/ + AAD/4AAAB/8AAP/4AAAf/wAA/8AAAH//AAD8A+AD//8AAPgP/A///wAA////////AAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAAoAAAAIAAAAEAAAAABAAQAAAAAAAAC + AAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAICAgADAwMAAAAD/AAD/ + AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdwAAAAAAAAAAAAAAAA + AAd0dAAAAAAAAAAAAAAAAAB3x3cAAAAAAAAAAAAAAAAAd3fHcAAAAAAAAAAAAAAAB3yMh3AAAAAAAAAA + AAAAAAfIeMdwAAAAAAAAAAAAAAAHjIyHQAAAAAAAAAAAAAAAfId4yHAAAAAAAAAAAAAAAHjIyIdQAAAA + AAAAAAAAAAB3iId4YAAAAAAAAAdwAAAAjIiIiIUAAAAAAHd3dAAAB4iIiHh8cAAAAHd3x4dwAAd4iIiI + h3Z3d3R3yIh4cAAHh4iIiIfHd3d4iIiIh3AAB3jHiIiIiHeHiIiIiIwAAAh3dXh4iMiIiIiIiIhwAAAA + yGd0d4iIeIi4iIiMAAAAAIeHd4iIh32IiIiIcAAAAAAAd4jIyIiIiHeHyAAAAAAAAAB3h4iIh8h3dwAA + AAAAAAAIh8fIh4eIaAAAAAAAAACIiHAAB8jIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////// + ////////////////////n////g////wP///8B///+Af///gH///4B///8Af///AH///wB//n8AP/A+AB + /AHgAAAB4AAAAeAAAAPgAAAH8AAAD/AAAB/8AAA//wAA//4AA//weA////////////////////////// + //8oAAAAGAAAADAAAAABAAQAAAAAACABAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAgAAAgAAAAICAAIAA + AACAAIAAgIAAAICAgADAwMAAAAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHRwAAAAAAAAAAAAB3dAAAAAAAAAAAAA + d8dwAAAAAAAAAAAAfId3AAAAAAAAAAAHeMjHAAAAAAAAAAAHyHh3AAAAAAAAAAAHh3eEAAAAAAAAAAAI + yIiHAAAAAHd2cAAIiIiIQAAAd3d4UACHiIiId3d3eHiIcACHh4iIyHeHiIiIcAAIR3d4iIiIiIiMAAAH + d3eIh3iIiIhwAAAAeMh4iIiHiMAAAAAAAHfIiMh4aAAAAAAAiIgHyIfIAAAAAAAIgAAAAIAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8A////AP///wD8f/8A+H//APB/ + /wDwP/8A4D//AOA//wDgP/8A4D/BAOAfAQDAAAEAwAABAOAAAwDgAAcA8AAfAPwAPwDwgP8A5/f/AP// + /wD///8A////ACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACA + AAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD///8AAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAd1AAAAAAAAB8cAAAAAAAB4eAAAAAAAAHyMgAAAAAAAiIhwAAAHcACI + iHcAd3hwAIz4jIeIiIAAd3eIiIiIAACHeIiIiHAAAACMeMh4AAAAiAAIgAAAAAAAAAAAAAAAAAAAAAAA + AAD//wAA//8AAP//AADj/wAA4/8AAMP/AADB/wAAwfkAAMDBAADAAQAAwAMAAMAHAADwDwAAzn8AAP// + AAD//wAAKAAAADAAAABgAAAAAQAIAAAAAAAACQAAAAAAAAAAAAAAAQAAAAEAAAAAAAA9OzsAZD8/AGg8 + PABtPj4AQkNDAEZIRwBWQkIAV0REAF5AQABbRkYAVklJAFxPTwBTU1MAXFJSAF5ZWQBkQEAAYUREAGZF + RQBqQkEAYEtLAGNPTwBwQUEAfUZGAHJKSgB2SUkAfU9PAGBRUQBgVFQAZlZWAGZYWABqWVkAclZWAHpU + VAB9W1oAbmJiAGtoaABtaWkAcWdnAHdnZwB8Y2MAe2pqAHJxcQB+dHQAd3l5AHl6egCGT08AiU9PAIFP + UACGU1MAjVFRAIlWVgCMV1cAg1xbAIxaWQCQUlIAlVJSAJFXVgCXVVUAmVVVAJZaWQCSXV0AlV9eAJpZ + WgCeW1sAml5eAKBZWgCgXFwAql9fAIRmZQCIZWQAhWtrAI5ragCTYmEAnGBhAJ9kYwCaZmYAk25uAJ1s + awCFdHQAiXd3AIt+fgCWd3cAmHR0AJV5eQCbfHwAo2JhAKZhYQChZWUApGVkAKplZACsZGQAqmhnAKZr + agCnbGsAqmloAKlubQCsbW0AtGZnALhsbACxb3AAv29wAKVxcACrc3IAr35+ALN0cwC5c3MAvXBxALR4 + dgC1fHsAunt6AMNtbgDGb3AAw3FyAMZwcQDGdXUAyHR1AMp3eADBeXkAxnt7AMB/fgDLensANLBSAEWf + TgBBtFwAPMdnADHkdgDciiIAvoF/AISrdwDln0sA35lhAN2XfADgmmEA8LdlAO61cAArWPIALWT+AEh5 + +gDOf4AAfoCAAHiA1ABZv9wAZrnUAGK+2ABxnv4Ad6P/ADPX/QBw0OcAW+D7AIKEgwCPgoIAjI2NAJuC + ggCUiIgAmYqKAJGSkgCjhIQAqoKCAKKLiwC+hIMAsoqKALaSgQCum5sAsZubALqqlQCdgr4Ar6ytALGh + oAC6pKQAwoSDAMyBggDGiIYAyYiHAMWMigDMjIoA0ISFANKHiADUjIwA2Y6NAMCUjQDIk44A0JCPANaP + kADHlZQAzpSSAMScmwDUkpIA2ZSVANWYlgDampcA2ZeYANWcnADam5sA4p2cAMChjwDeoJ4A5aCFAOaj + jQDlpJoA2p6hAMOkowDOoaEAy62tANegoADdoqEA2aGpANGsrwDdq6kAwbG4ANGysQDdtLQA2ri3AOGk + owDjqKYA66ylAOGnqADjq6oA6a2rAOOwrwDssK4A5K+wAOaztADttLIA57i2AO24tgDmurgA6rq6APC1 + swDyuLYA9Ly5APi+uwD1wL0A+cC9AKKMwACkk8QAqprMALSayACptsEAlaDkAOy/wACRxtQAgOv9AJnr + 9wDEwsoA5sbGAOzCwgDuyMcA7MzMAPPEwgDxy8oA9dPTAPja2gAAAAAAAAAAAP///wAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AAAAAAAAAADLytDi9tOemQAAAAAAUy9EecLEsa1uPTUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPj11Mme + VakAAAAAAAAAAAAATS84M0akAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///////8AAP///////wAA////////AAD///////8AAP///////wAA//////// + AAD///////8AAP///////wAA//h/////AAD/4D////8AAP/AP////wAA/8A/////AAD/gB////8AAP8A + H////wAA/wAf////AAD+AB////8AAP4AH////wAA/gAf////AAD8AB////8AAPwAH////wAA/AAP//// + AAD8AA////8AAPgAD//+BwAA+AAH//ADAAD4AAP/wAMAAPgAAP8AAwAA+AAAAAADAAD4AAAAAAMAAPgA + AAAABwAA+AAAAAAHAAD4AAAAAA8AAPgAAAAAHwAA/AAAAAA/AAD8AAAAAH8AAP4AAAAA/wAA/4AAAAP/ + AAD/4AAAB/8AAP/4AAAf/wAA/8AAAH//AAD8A+AD//8AAPgP/A///wAA////////AAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAAoAAAAIAAAAEAAAAABAAgAAAAAAAAE + AAAAAAAAAAAAAAABAAAAAQAAAAAAAFFNTQBRUlIAU1RUAGJHRwBiT08Aa0lIAGJTUwBrVlYAYllZAGZc + XABpWloAb1xbAHNTUwB7V1YAc1hXAHFbWwBkZWUAaWFhAG5kZABpamkAcGFhAHlubgB2cHAAf3V1AH55 + eQB8fX0AgUpKAI1PTwCLWFcAhlhYAI9ZWQCKXFsAm1ZWAJJZWQCWWVgAmlpbAJtcWwCiXFwAl2BfAIBg + YACAZ2YAgG9vAI9oaACWZWQAmGBhAJ5kZACcaWoAmm9vAIV0dACNcHAAiXZ2AIB8fACac3IAm3V0AJ51 + dQCZfHwAnHx8AKNmZgCnZmYAqmJiAK5jYwCvb24AtWVmALBtbgC5bW0AvmxtAKx+fQCxcnIAtHBwALZz + dACydXQAtnd2ALlwcAC5dnYAt3p5ALh5eAC8fHsAun18ALx+fQDGb3AAxnBxAMdzdADAd3YAyHJzAMlz + dADJdXYAynd4AMd/fwDMe3wAzXx9AHunbwBhvHIAYsN4ANuLOwC2hn4A4Zt5APC3ZABte9sAX47+AHWM + 5QAl0foAY+P8AIeDgwCFhoYAioSEAJOIiACWi4sAmpKRAKGCgQCmhYUAqYGBAKuDhACniooApYyMAKiO + jQCyhYMAvoWEALeNjQCrj5AAr5eXALSVlAC9lJMAmbCEAK6RugDBgYAAwoSCAMWDhADChoQAxYeFAM6A + gQDFiIYAxoqIAMqIiQDMi4oAy4yKAMiPjQDPj44A0ISFANKJigDUi4wA04+NANWNjgDKkY8A0JCOANud + iQDWj5AAzJSTAM2XlgDGm5oA1pGSANOUkgDVl5EA1pOUANiVlgDYmJUA2ZeYANKenADbmpsA3pmYANuc + mgDbn5wA1aacAN6gngDqqZoA3Z+gAMyjowDCra0AxqysAMqpqQDboaAA3qKiAN6logDbp6UA3aWkANer + qgDWsbMA0rW0ANe0tADfs7IA4aSiAOGlpQDkp6UA46imAOWopgDsraIA6qimAOGoqADhrqwA6a2rAOqv + rADpsK4A7LGuAOGzswDlsbEA7bKxAO+1sgDotrYA5rm3AO+4twDot7sA6bq5AOu9uwDrv70A8bazAPG2 + tADxuLUA9Lm2APC9uwD2vboA9L+9APi+uwD4v7wA8sC+APXAvgD5wL0AkILJAKqXzACsu8cAqr/LALLV + 3QDawMIA48XFAOvDwQDswMAA7cTDAO/ExQDgxsgA8cbEAPTGxADwyskA9MvJAPLNzQD21dYA+NjZAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAMEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAqHCEcBQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAayU9PSYbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdQlBSQiJpAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAM0pSUlJQPRcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnUlJSUlJGFQAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAFJSUlJSUkoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzUlJSWVJZfxAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAC5XWYqKioqGDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASoqMkpqa + mqAsAAAAAAAAAAAAAAAAAABoNAAAAAAAAACMjJyuvLy2toYHAAAAAAAAAAAAABcOIDouBgAAAAAAc4yc + tsHKysPAriIKAAAAAAAAABYgRk1LTX+DEAAAAABukqXB4ejo4dHPQCIEChcXEwggTXV/k66unKMpAAAA + AG6Srsro6ero0dN/Rk1NRk2Dg4STrsbh4cHAt2sAAAAAbpKuOXPe6ajW15KGg4OGk528yuHo5eHPz882 + AAAAAAB4jCkDAxSoMabXt5yjt8ro3ePo5dbT09HTdAAAAAAAAABGcBFoGgFwdtfDwHxi2dpmZcrX09HP + z0MAAAAAAAAAAHh/qWwaOa6cz9PNZGPYsdzbzc3DwLk2AAAAAAAAAAAAAAAvhpKakoyg19HNyKS5wHtb + orZ/cwAAAAAAAAAAAAAAAAAANkaKWVm5zb1gYV6cXVxfNgAAAAAAAAAAAAAAAAAAALGvlTIuP1K5tqCR + l4xfLwAAAAAAAAAAAAAAAAAAsbPBenkAAAAAcCVYjE0scwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////////////////////+f///+D////A////wH + ///4B///+Af///gH///wB///8Af///AH/+fwA/8D4AH8AeAAAAHgAAAB4AAAA+AAAAfwAAAP8AAAH/wA + AD//AAD//gAD//B4D////////////////////////////ygAAAAYAAAAMAAAAAEACAAAAAAAQAIAAAAA + AAAAAAAAAAEAAAABAAAAAAAAWlJSAHBJSQB1SEgAe1dXAHdYWAB5WlkAel1dAGBiYgB1bGwAfWtrAHh2 + dgB9fn4Ag01NAIRXVwCIV1cAhV9eAItbWgCgX14ApV1dAJhgXwCNYGAAnWtqAJhtbQCCdnYAh3x8AI15 + eACeensAqGBgAKhoZwCga2oArGpqALNqagCzb28AtG1tALltbQCxb3AApnVzAKlzcwCqdHMApnp6AKd+ + fgCpensAq3x7ALZ3dgC8dHQAvH59AMZvcADGcHEAxXN0AMhycwDJdncAynh5AMx5egDNfn8Ajo1wAOek + VgDGgH8A4p53AEZ2+gB8u4AAd8PaAIuEhACOh4cAjo6OAJ+DggCejo4Ao4SEAKSIiACsi4sAqo2MAK6P + jgC+gYAAvoaGAL+KiACskJAAtJeXALWenQC5np4At6iOAKmyjgC9nroAwYSDAMaGhADOhoYAxomHAMiK + iQDJjYwA0oeIANOOjwDUjY0A2ZiPANaPkADGkZEAx5eXAMySkADGnZwA1ZOSANeTlADWl5YA2JSVANGZ + mADan50A3J6dAOCcmwDVoJ8A7K2fAMOtrQDXo6IA3aCgAN+kpADVq6oA3ay3AMu0tADPtrYA3L+/AOCi + oQDhpqUA5KelAOinpgDlq6gA46usAOOvrQDqrqwA7LGuAOayswDjtrQA5re1AOqysQDts7EA57y6AO+8 + ugDrvL0A8LOwAPC1sgDwtrQA87q3APS6twD2vboA8b69APi/vAD2wb4A+cC9AJmTzwDHqMMAu8PMAIHf + 8QDByNAA7cLCAO3FwwDvxsQA5cjIAOzOzgDwxcQA9cbEAPPP0AD10tIAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + BQMJAAAAAAAAAAAAAAAAAAAAAAAAAAAPHBMNAAAAAAAAAAAAAAAAAAAAAAAAABojLy8TAAAAAAAAAAAA + AAAAAAAAAAAAAB0wMDAiPgAAAAAAAAAAAAAAAAAAAAAAQjAwMDAtGAAAAAAAAAAAAAAAAAAAAAAAFzIy + NTU5CgAAAAAAAAAAAAAAAAAAAAAAIjZYWFxcBwAAAAAAAAAAAAAAAAAAAAAANlxtdW11JQAAAAAAAAAA + PgcRDgkAAAAAXG1/lISAZgMAAAAAABkVLC5SVhcAAABNY3WWnJuLfB8UBAcQHkhWaX91dSsAAABNY2BM + mJeCiVJSVl9laX+WloSJgEIAAAAAXAEIC0tGjnR0dJaRk5qNjIyJQwAAAAAAJkNADBtdjIaPO1GSPYuJ + hnVEAAAAAAAAAClISWRcd4xwkGp8UE90VwAAAAAAAAAAAAAAKSQ1NYZ7OjhbPDdGAAAAAAAAAAAAAHNv + YGsAKyJoXFYmRwAAAAAAAAAAAAAAcnIAAAAAAAAATgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AP// + /wD///8A////APx//wD4f/8A8H//APA//wDgP/8A4D//AOA//wDgP8EA4B8BAMAAAQDAAAEA4AADAOAA + BwDwAB8A/AA/APCA/wDn9/8A////AP///wD///8AKAAAABAAAAAgAAAAAQAIAAAAAAAAAQAAAAAAAAAA + AAAAAQAAAAEAAAAAAABjZGQAdmRjAHtpaQB/eHgAgU9PAKBaWgCFbm0AlWtqAKptbgCwZ2cAsGhoAKxw + cACteHkAvnJyAMZvcADGcHEAy3l5AMx9fgCFmXQAwIB/ANeUfQDhoX8AlIqJAJWMjACYiIgAoIaGAK2K + igCxh4cAvoGAALKKigC4iYgAuJWVAL2cnACss50AuqKhAL+mpgDLgoIAxImHAMeNjADLkI8AxpWTANCS + kQDYlZUA1J6dANqZmgDdnp4A1J+oAMaiogDOr68AzLKyANi5uADhpaIA4qypAOWtqADrrqsA4bKwAOay + sgDtuLYA57++AOy4uADxtLIA8be0APa9ugDswL4A9sG+ALCcxwC5ncIA06zBALnH0QC2ytQA7sPDAPLS + 0gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAZBgUAAAAAAAAAAAAAAAAACw8KAAAAAAAAAAAAAAAAGhAQDgAAAAAAAAAAAAAAAAkRESUYAAAA + AAAAAAAAAAAlKy4uBwAAAAAAAAcDAAAAKzlHPCYCAAAYCB0oKgAAAC0wSDs0FB0nLDlAOiwAAAANAQQb + Pi9DRkVBPzUAAAAAJB4cKz5EQjMiNSkAAAAAAAAAHwwRNxYVEyQAAAAAAAAxMgAAACEgAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP//AAD//wAA4/8AAOP/AADD/wAAwf8AAMH5 + AADAwQAAwAEAAMADAADABwAA8A8AAM5/AAD//wAA//8AACgAAAAwAAAAYAAAAAEAIAAAAAAAgCUAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAkAAAAJAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAUAAAAOAEBAVUAAABUAAAANQAAABAAAAABAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAkFBSUvGRl5TCkpwlYuLtxDJCTQFw0NmQAA + AEkAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACGAwMKE8rK6V6RET2klJR/5ZS + U/+OT0//ZDc38B0QEJoAAAAyAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYDAwYVzAwoopP + T/ygXVz/oFtb/55ZWf+bWFf/k1NT/1UvL9wGAwNcAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AARNKipxhk5O+adkY/+uZWX/tWdo/7VmZ/+qYWH/nltb/3hERPcfERGCAAAAFgAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAADEZGS1zQ0LXqGdm/7ptbf/Fb3D/x3Bx/8hwcf/BbW7/q2Vl/4hPT/82HR2gAAAAIAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAB1gxMYyYXl3/vXFx/8Zwcf/HcHH/x3Bx/8dwcf/HcHH/uG1t/5NY + V/9EJia2AAAAKQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPB8fNH1MS+K4cnH/x3Fy/8dwcf/HcHH/x3Bx/8dw + cf/HcHH/wHBx/51gX/9PLCzGAAAAMwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AAMAAAAAAAAAAAAAAAAAAAAALhAQFIZXVs/RjIz/1Y2O/9qYmP/eoaL/46qr/+aysv/ot7f/6rm5/+m4 + uf/otbX/5q+v/+uvrf/jqab/wYeF/28/P/QhEhKvAAAAXwAAACgAAAANAAAABQAAAAMAAAACAAAAAwAA + AAUAAAAKAAAAFQAAADAdDg9oSSkptHZHRu2dYmL+t3Z1/758e/+6enn/tnh3/7d5eP+8fn3/w4SD/7Z6 + ef9eODfbBgICTgAAAAgAAAAAAAAAAAAAAAAAAAAAPhwcJJVjYuPXkZH/2JOU/92fn//iqqr/57O0/+u8 + vP/uwsL/78XG/+/Exf/twMD/67i4/+60sv/wtrP/zZKQ/5taWv9xQED2MRsaxAgEBIcAAABaAAAAQQAA + ADcAAAA2AAAAOwAAAEUEAgJZHA4OfUcnJ7l5SkntqGxr/8CAfv/DgoH/vH59/7p+ff/DiIb/zZGP/9GT + kf/UlJP/1peV/9eZl/+GVlbuGQsLVwAAAAcAAAAAAAAAAAAAAAAAAAAARiIiLZ9rauvZk5P/2peY/+Ck + pP/lsLD/6ru7/+/Fxf/yzMz/9NDQ//PPz//xycr/7sDA//K5tv/1u7j/36Kg/6dmZf+mZWX/j1ZW/WM6 + OutDJSXQNBwcvDAaGrQ0HBy1PiIivUwsLMtkPDzfh1VU9a1xcP/EhIP/xIWE/7+Cgf/Ch4b/zZST/9mk + ov/grq3/4a6t/96lo//eoJ7/36Kg/+Cjof+IWVjnGwwMQwAAAAIAAAAAAAAAAAAAAAAAAAAARyQkL6Br + auzZk5P/25qb/+GnqP/ntLT/7cDA//LLy//209T/+NjY//fX1//00ND/8cbG//W9u//4vrz/46ak/7d0 + c/+vb27/s3Jy/7d2df+ucXD/pWpp/6Npaf+nbWz/sHVz/7p9fP/EhYT/yImI/8WIhv/DiIb/ypGP/9eg + n//hr63/57q5/+rCwP/rwsD/6bq4/+evrf/nq6n/6q6r/9qgnv9wRkbDBwAAHgAAAAAAAAAAAAAAAAAA + AAAAAAAASCQkLZ1nZuvYkpP/25uc/+Opqv/qtrf/7cHB//TOzv/52Nj/+tzc//na2v/xz9D/8MfH//fA + vv/6wb7/6a6r/8OBgP/DgoD/vX58/7h7ev+8fn3/woOC/8aHhv/HiYj/xoqJ/8aLif/Ijoz/zZST/9eg + nv/hrav/6Lm3/+zCwf/uyMf/78nH/+/Dwf/uvLr/7ba0/+60sf/vtLL/8ri1/7J+fflMKSltAAAABAAA + AAAAAAAAAAAAAAAAAAAAAAAAQyEhI5JcXOPWj5D/3Juc/8qVlf+BZmb/bl5e/4l4eP/AqKj/8tPT//LO + zv+5p6b/w6qq//fBv//7wr//8LWy/86Ojf/Ojoz/0ZGP/9GSkP/OkY//zpOR/9GamP/VoJ//2qel/+Gv + rf/nt7X/6727/+3Dwf/wycf/8czL//LLyf/yxsT/8cC+//G7uf/yubf/87m3//S7uP/4vrv/1J6c/3JH + RrAdCgsWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANRcXEYJNTcvPiIn/15aW/2VNTf85Ojr/Q0VF/0JF + RP9dXFz/n5GR/+S/v/+bh4f/hXp6/+25uP/7wr//9bu4/9qcmv/Zmpj/252b/96gnf/ipKH/5q+s/+u+ + vP/vycf/8srI/+3Hxv/wysj/9c7M//TNy//0ysj/9MbE//TBv//1vrz/9r26//e9u//4vrv/+L+8//vB + vv/hqqf/g1ZVzDwcHC4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAW4+Ppq/env/05OT/2ZX + V/9rbm7/fX9//3l6ev99f3//cHJy/5F9ff+ff3//XFhY/9eop//8wr//+L+8/+Wppv/ipaP/5qil/96i + pP/Kmaz/1qi1//LGxP/tyMf/qb3J/23E3P9kw9//vMTN//jDwP/3wb//+MC9//i/vf/5v73/+b+8//i/ + vP/3vrv/+L68/92mo/+IWlnRRSMjOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFcv + L0mbX1/y15GS/6GAgP9XV1b/iYuL/4CBgf98fX3/cnR0/1dPT/++j4//km9w/9Sfnv/6wL3/+cC9/+6z + sP/ssK3/0Z+u/4OH1P9YffD/QGPs/7KYyv/Ct7z/Ytrz/3Ts//8s2f//cbvU//m+u//4v7z/+L67//e9 + uv/1vLn/9Lq3//O5tv/zuLX/0puZ/4RVVctGIyM4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAADIXFwdrPDySq2ts/diZmf/ApKT/sKur/4CBgP95enr/iYiI/49zdP/do6P/36Ch/96e + nv/zuLX/+sK///W7uP/1ubT/qZC//2qY+/9tnf//MGT6/56FxP/esK//nMbS/57n8/9+z+T/ybG3//a6 + t//zubb/8re0//C1s//utLH/7rKw/+qvrP++iIb9dklJtkMgISoAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABHIyMSazw8kZ5hYvXNjI3/2aSk/7OMjP+bd3f/sIKC/9KV + lv/cnJz/2peY/9aRkf/koqL/+sG+//nAvf/5v7z/4amw/6qZx/+aouP/qpvP/+mxtv/2urj/6rGv/+S6 + u//ptrX/466n/+Ovqf/ssK7/6q6s/+isqv/oq6n/2J2b/6JubfFoPT2NOxoaFwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOBoaCFowMFd7SEjAomZm9sWC + gv/XkZL/25SV/9iSk//Wj5D/1IyN/9KHiP/UiIj/8bOx//rCv//3vbv/9ru4//O3s//xuLX/7q6e/+ej + hf/npIn/7bCp/+Otp/+KsX3/ULdm/1WjWv+7oYz/5KWk/9uenP+4gH79glJRzVYuLlQgCAkGAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAA8HBwQVy4uS3FBQaCPV1fjsG5v/cmAgf/ShYb/0YKD/85+f//LeXr/2I2M//e8uf/1vLn/7rOx/+2y + sP/lpJX/5qFY/+6xXP/djS3/35h9/86gl/9SwW7/Nd90/0WxXP+vlH//wYSE/49cW+VlOTmBQR4eHAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAGk7OhqIWFd8oG5u8J5qav+eX2D/tmts/8Z0df/KdHX/yXJz/92T + k//3vLn/7LGu/+Snpf/dm5L/4Z1q/+61dP/fmmX/15WM/9eYlv/Bm43/r6uR/6uNgP+WYWDtbkBAnUwn + JzQVAQECAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiFJSBnhC + QgpqNDQJWSUlB08dHQdfKisKfENDFJJWViinbGtRvYOCjtOcm8/pt7X157y6/7eOjfhxRUW7aTk5m4RK + StehWlr6uGdo/8Zwcf/dkpH/8bSx/+OnpP/YmZj/1ZWT/9ealP/Vl5X/0JCP/8eIhv+zdnb/lFtc6nA/ + QKRSKio/JQwNBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADTn6AB2qioDMuUlCHBhYU8voCAWcCBgXTEhoaLzZGQqdeensngrKvn47Sz/NOop/+yiIfyi2Bgs2k+ + PlZXKysPAAAAAUYlJRxcMTFYcj4+pYpMTeWmXF3+xnl5/9+Zl//dnJr/z46M/8KCgf+vc3L/ll9e831L + S8hlOTl/TigoMy0REQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABzQUIDnmprDriGhifHlpZMzp6eeNCgoZ7On5+2yJqaybuPj9WnfHzVj2RkunVJ + SYNbLy8/PRQUCgAAAAAAAAAAAAAAAAAAAAAAAAAAKRUVBU0pKSphNDRtd0BAsotNTd2ZW1vrkVlY4HtJ + Sb5lOTmCUysrQTsbGxEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWCwsA2Y4OA5xQkImdkhIRHhKSll0R0dibUBAWWI2 + NkNUKCgoOhISDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMhkZB0km + Jh5LJiYsRSEhITATFAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////8AAP// + /////wAA////////AAD///////8AAP///////wAA////////AAD/+H////8AAP/gH////wAA/8Af//// + AAD/gA////8AAP+AD////wAA/wAP////AAD/AA////8AAP4AB////wAA/gAH////AAD8AAf///8AAPwA + B////wAA/AAH////AAD8AAf///8AAPgAB////wAA+AAH//4HAAD4AAP/8AEAAPgAAf/AAQAA8AAA/wAA + AADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAEAAPAAAAAAAQAA8AAAAAADAADwAAAAAAcAAPAA + AAAADwAA+AAAAAAfAAD4AAAAAD8AAPwAAAAAfwAA/gAAAAD/AAD/gAAAA/8AAP/gAAAH/wAAgAAAAB// + AAAAAAAAf/8AAAAD4AP//wAAgB/8H///AAD///////8AAP///////wAA////////AAD///////8AAP// + /////wAA////////AAAoAAAAIAAAAEAAAAABACAAAAAAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAYAAAAZAAAAGQAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAARCQkYOh8fb0ooKK80HByiCQUFTAAAAAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAIhERFmA2Np2ITUz3lVNT/4dLS/5IKCi9AAAALwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAANjODiBllhY+61kZP+vY2P/pV5e/3xHRvEhEhJfAAAAAgAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAASSgoN41VVeS6bW3/xW9w/8dwcf+9bG3/klZW/jogIIEAAAAGAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZ1RkWcs2xs/8dxcv/HcHH/x3Bx/8Zwcf+iYWH/SSkpmAAA + AAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUC0tMZtgX+fGcnP/x3Bx/8dwcf/HcHH/x3Fy/61q + av9UMTGqAAAAEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxRER1tm9v/8hxcv/HcHH/x3Bx/8dw + cf/HcnP/tnRz/185OboAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAACIxXV7TEdHT/yHJz/8l1 + dv/Kd3j/ynd4/8p4eP/Bf37/bURDywAAACQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABNKysjo2Zm4Mt4 + ef/NfH3/z4GC/9GFhf/RhYb/0YWF/82Mi/9+UVHeCAICOwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAJAAAACwAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAGc+ + Pkm1c3P30IGC/9OJiv/XkZL/2ZaW/9mWl//YlJX/2JmY/5hnZfMeEBBrAAAABwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAA0FAgItHhAQWzAbG4IqFxeHDQcHWwAAABkAAAAAAAAAAAAA + AAAAAAAAek1MdMN/f//VjI3/2piZ/9+io//hqKn/4qmp/+Clpf/jpqT/wImH/04xMLwAAAA6AAAABQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAABEbDg5GRygokW5CQs+MVlbxnGJh/JdfXvxnPz7hHA8PbgAA + AAwAAAAAAAAAAAAAAACMW1qbz4qK/9qXl//gpqb/5rKz/+q6u//rvLz/6La2/+qxr//epKL/j1lZ+DUc + HLACAQFPAAAAHQAAAA8AAAAPAAAAEwAAACIbDg5MVDExnYZUU+SpbWz+uXl4/7x+fP/AgoD/xoeF/72A + f/9fOzu1AAAAHAAAAAAAAAAAAAAABJhkZK/VkZH/3Z+g/+axsf/twMD/8svL//LNzf/vxcX/8Lq4/+6z + sf+1dHP/j1VU+144N9g7IiKqMhwclDcfH5RGKSmiYTw7v4tZWOiydXT+woOC/8aKiP/Ol5X/2aWj/9ui + of/cnpz/2pyb/35TUrgAAAAVAAAAAAAAAAAAAAAFmmVkstaTk//hpaX/7Lm6//TLy//419f/+NnZ//TP + z//1wb//9Lq3/8aGhP+1dHP/s3Rz/6xwb/+pb27+rnNy/7Z7ev/BhIL/yY2L/8+WlP/apqT/5be2/+vB + v//rvrz/6bKw/+uvrf/Um5n/bUVEgAAAAAMAAAAAAAAAAAAAAAOTXV2q1ZGR/9CYmP+dfX7/o4yM/9e8 + vP/z0tL/zLOz/+u8u//5v7z/1peV/8uLif/Ki4r/yoyL/86Ukv/TnJv/2qSi/+Gtq//nuLb/7cPB//DJ + x//xxsT/8b+9//G6t//zubf/77az/6d1dM89Hx8lAAAAAAAAAAAAAAAAAAAAAIJOTojNiIn/jGlp/01O + Tv9UVlb/dnNz/7uhof+Pfn7/xJ+e//zCv//lqKb/3J2b/+Chnv/hpaT/7Ly5/+vHxv/MxMn/0MjN//LK + yf/1x8X/9sLA//a/vP/3vrv/+L+8//S7uP+5hoXhYTo5RwAAAAAAAAAAAAAAAAAAAAAAAAAAaTs7RrVz + dPKmfn7/cXJx/4SGhv97fX3/b2Zm/516ev+7kJD/+sG+//C2s//lqqr/rpbA/3aB2/+ql83/tMHK/2jc + 9P9OzOz/2r3B//q/vP/3vrv/9ry6//a8uf/ss7D/tYGA32c+Pk0AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAvEhIHg01Njbp9fvrCn5//nI+P/4R7ev+fgID/2Jyd/9ybnP/ytrT/+b+8/+ewtf+Mld3/ZI36/5eI + zv/Ttrn/sNLc/6/Czv/stLT/8re0/++0sf/tsq//2qCe/6Rxb8phODg+AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAABCIB8MeUZGbqRpata8gYH8x4mJ/9eTk//YkpP/04qL/+Cbmv/5wL3/9726/+Sw + t//Zrrn/56qY/+2smf/lr6n/nLWJ/4Gtdf/Pppn/3qGf/7yEg/KJWViYTyoqIAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQh0dGXJAQGOXXl7NtnR1/8V7fP/MfH3/znt8/+il + o//0urj/7LCu/+Whg//rq13/35VX/9Kek/9yvXz/ZbNv/6iCdfqYY2O/aj4+TCUJCgcAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAACcamsBjFRVB4FERAh9PT0JjU1ND6VnZx+/hINF0JqZiNOjoty0iIf2hFBQw5lX + V8+wY2P4xXR0/+aioP/oq6j/2pqT/92fif/Vlor/yYqJ/7N8efiVZmPGdERFYkEfHxIAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAALiFhgXFkJEdx5CQSMqSknbNlZWbz5uaws2cnOXBlJPnqH18r4dc + XFFULy8OSCUlFm07O0+FSUmeoV1d3sF9fPrGhoX/snZ295xkZNiFUlKbbD4+T0UdHxIAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAc0JDA5FgYRKdbm46onR0Zp9ycnuWampzhFlZVmY6 + OikvDAwHAAAAAAAAAAAAAAAAAAAAAB0ODgRULCwhbjo7UXhERGVrPDxHTCYmGxAAAQMAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAP//////////////////////D////gf///wH///4A///+AP///AD///wA///8AP//+AD + ///gA//D4AH+AeAA+ADgAAAAwAAAAMAAAADAAAAB4AAAA+AAAAfgAAAP8AAAH/wAAD8AAAD/AAAD/wB4 + D//H////////////////////KAAAABgAAAAwAAAAAQAgAAAAAABgCQAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAABMAAAAtAAAAEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAgIO1cwMM1qOjrsHhAQmwAA + ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAATCgogfUhI6ahgYP6lXV3+f0hI9wIBAT0AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsGBgFPLy6kuW1t/sZv + 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 + + + \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/ControllerConfig.cs b/BizHawk.Client.EmuHawk/config/ControllerConfig.cs index f17a7e1afe..4b54d570be 100644 --- a/BizHawk.Client.EmuHawk/config/ControllerConfig.cs +++ b/BizHawk.Client.EmuHawk/config/ControllerConfig.cs @@ -174,7 +174,7 @@ namespace BizHawk.Client.EmuHawk tt.TabPages[pageidx].Controls.Add(createpanel(settings, cat.Value, tt.Size)); // zxhawk hack - it uses multiple categoryLabels - if (Global.Emulator.SystemId == "ZXSpectrum") + if (Global.Emulator.SystemId == "ZXSpectrum" || Global.Emulator.SystemId == "AmstradCPC") pageidx++; } @@ -182,7 +182,7 @@ namespace BizHawk.Client.EmuHawk if (buckets[0].Count > 0) { // ZXHawk needs to skip this bit - if (Global.Emulator.SystemId == "ZXSpectrum") + if (Global.Emulator.SystemId == "ZXSpectrum" || Global.Emulator.SystemId == "AmstradCPC") return; string tabname = (Global.Emulator.SystemId == "C64") ? "Keyboard" : "Console"; // hack @@ -272,7 +272,16 @@ namespace BizHawk.Client.EmuHawk pictureBox1.Size = Properties.Resources.ZXSpectrumKeyboards.Size; tableLayoutPanel1.ColumnStyles[1].Width = Properties.Resources.ZXSpectrumKeyboards.Width; } - } + + if (controlName == "AmstradCPC Controller") + { + /* + pictureBox1.Image = Properties.Resources.ZXSpectrumKeyboards; + pictureBox1.Size = Properties.Resources.ZXSpectrumKeyboards.Size; + tableLayoutPanel1.ColumnStyles[1].Width = Properties.Resources.ZXSpectrumKeyboards.Width; + */ + } + } // lazy methods, but they're not called often and actually // tracking all of the ControllerConfigPanels wouldn't be simpler diff --git a/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs b/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs index 7b016a8cab..379f4540ee 100644 --- a/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs +++ b/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs @@ -52,8 +52,9 @@ namespace BizHawk.Client.EmuHawk { "GBC", "Game Boy Color" }, { "PCFX", "PC-FX" }, { "32X", "32X" }, - { "ZXSpectrum", "ZX Spectrum" } - }; + { "ZXSpectrum", "ZX Spectrum" }, + { "AmstradCPC", "Amstrad CPC" } + }; public string TargetSystem = null; diff --git a/BizHawk.Client.EmuHawk/tools/MultiDiskBundler/MultiDiskBundler.Designer.cs b/BizHawk.Client.EmuHawk/tools/MultiDiskBundler/MultiDiskBundler.Designer.cs index 5616d71e02..f6b2a00d07 100644 --- a/BizHawk.Client.EmuHawk/tools/MultiDiskBundler/MultiDiskBundler.Designer.cs +++ b/BizHawk.Client.EmuHawk/tools/MultiDiskBundler/MultiDiskBundler.Designer.cs @@ -144,7 +144,8 @@ "PCFX", "PSX", "SAT", - "ZXSpectrum"}); + "ZXSpectrum", + "AmstradCPC"}); this.SystemDropDown.Location = new System.Drawing.Point(425, 75); this.SystemDropDown.Name = "SystemDropDown"; this.SystemDropDown.Size = new System.Drawing.Size(69, 21); diff --git a/BizHawk.Emulation.Common/BizHawk.Emulation.Common.csproj b/BizHawk.Emulation.Common/BizHawk.Emulation.Common.csproj index d6d61bbf3b..0a8aa6ec09 100644 --- a/BizHawk.Emulation.Common/BizHawk.Emulation.Common.csproj +++ b/BizHawk.Emulation.Common/BizHawk.Emulation.Common.csproj @@ -79,6 +79,7 @@ + @@ -143,4 +144,4 @@ --> - + \ No newline at end of file diff --git a/BizHawk.Emulation.Common/DSKIdentifier.cs b/BizHawk.Emulation.Common/DSKIdentifier.cs new file mode 100644 index 0000000000..b40c6ffdf5 --- /dev/null +++ b/BizHawk.Emulation.Common/DSKIdentifier.cs @@ -0,0 +1,421 @@ + +using System.Linq; +using System.Text; + +namespace BizHawk.Emulation.Common +{ + /// + /// A slightly convoluted way of determining the required System based on a *.dsk file + /// This is here because (for probably good reason) there does not appear to be a route + /// to BizHawk.Emulation.Cores from Bizhawk.Emultion.Common + /// + public class DSKIdentifier + { + /// + /// Default fallthrough to AppleII + /// + public string IdentifiedSystem = "AppleII"; + private string PossibleIdent = ""; + + private byte[] data; + + // dsk header + public byte NumberOfTracks { get; set; } + public byte NumberOfSides { get; set; } + public int[] TrackSizes { get; set; } + + // state + public int CylinderCount; + public int SideCount; + public int BytesPerTrack; + + public Track[] Tracks = null; + + public DSKIdentifier(byte[] imageData) + { + data = imageData; + ParseDskImage(); + } + + private void ParseDskImage() + { + string ident = Encoding.ASCII.GetString(data, 0, 16).ToUpper(); + if (ident.Contains("MV - CPC")) + { + ParseDSK(); + } + else if (ident.Contains("EXTENDED CPC DSK")) + { + ParseEDSK(); + } + else + { + // fall through + return; + } + + CalculateFormat(); + } + + private void CalculateFormat() + { + // uses some of the work done here: https://github.com/damieng/DiskImageManager + var trk = Tracks[0]; + + // look for standard speccy bootstart + if (trk.Sectors[0].SectorData != null && trk.Sectors[0].SectorData.Length > 0) + { + if (trk.Sectors[0].SectorData[0] == 0 && trk.Sectors[0].SectorData[1] == 0 + && trk.Sectors[0].SectorData[2] == 40) + { + PossibleIdent = "ZXSpectrum"; + } + } + + // search for PLUS3DOS string + foreach (var t in Tracks) + { + foreach (var s in t.Sectors) + { + if (s.SectorData == null || s.SectorData.Length == 0) + continue; + + string str = Encoding.ASCII.GetString(s.SectorData, 0, s.SectorData.Length).ToUpper(); + if (str.Contains("PLUS3DOS")) + { + IdentifiedSystem = "ZXSpectrum"; + return; + } + } + } + + // check for bootable status + if (trk.Sectors[0].SectorData != null && trk.Sectors[0].SectorData.Length > 0) + { + int chksm = trk.Sectors[0].GetChecksum256(); + + switch(chksm) + { + case 3: + IdentifiedSystem = "ZXSpectrum"; + return; + case 1: + case 255: + // different Amstrad PCW boot records + // just return CPC for now + IdentifiedSystem = "AmstradCPC"; + return; + } + + switch(trk.GetLowestSectorID()) + { + case 65: + case 193: + IdentifiedSystem = "AmstradCPC"; + return; + } + } + + // at this point the disk is not standard bootable + // try format analysis + if (trk.Sectors.Length == 9 && trk.Sectors[0].SectorSize == 2) + { + switch(trk.GetLowestSectorID()) + { + case 1: + switch(trk.Sectors[0].GetChecksum256()) + { + case 3: + IdentifiedSystem = "ZXSpectrum"; + return; + case 1: + case 255: + // different Amstrad PCW checksums + // just return CPC for now + IdentifiedSystem = "AmstradCPC"; + return; + } + break; + case 65: + case 193: + IdentifiedSystem = "AmstradCPC"; + return; + } + } + + // could be an odd format disk + switch (trk.GetLowestSectorID()) + { + case 1: + if (trk.Sectors.Length == 8) + { + // CPC IBM + IdentifiedSystem = "AmstradCPC"; + return; + } + break; + case 65: + case 193: + // possible CPC custom + PossibleIdent = "AmstradCPC"; + break; + } + + // other custom ZX Spectrum formats + if (NumberOfSides == 1 && trk.Sectors.Length == 10) + { + if (trk.Sectors[0].SectorData.Length > 10) + { + if (trk.Sectors[0].SectorData[2] == 42 && trk.Sectors[0].SectorData[8] == 12) + { + switch (trk.Sectors[0].SectorData[5]) + { + case 0: + if (trk.Sectors[1].SectorID == 8) + { + switch (Tracks[1].Sectors[0].SectorID) + { + case 7: + IdentifiedSystem = "ZXSpectrum"; + return; + default: + PossibleIdent = "ZXSpectrum"; + break; + } + } + else + { + PossibleIdent = "ZXSpectrum"; + } + break; + case 1: + if (trk.Sectors[1].SectorID == 8) + { + switch (Tracks[1].Sectors[0].SectorID) + { + case 7: + case 1: + IdentifiedSystem = "ZXSpectrum"; + return; + } + } + else + { + PossibleIdent = "ZXSpectrum"; + } + break; + } + } + + if (trk.Sectors[0].SectorData[7] == 3 && + trk.Sectors[0].SectorData[9] == 23 && + trk.Sectors[0].SectorData[2] == 40) + { + IdentifiedSystem = "ZXSpectrum"; + return; + } + } + } + + // last chance. use the possible value + if (IdentifiedSystem == "AppleII" && PossibleIdent != "") + { + IdentifiedSystem = "ZXSpectrum"; + } + } + + private void ParseDSK() + { + NumberOfTracks = data[0x30]; + NumberOfSides = data[0x31]; + TrackSizes = new int[NumberOfTracks * NumberOfSides]; + Tracks = new Track[NumberOfTracks * NumberOfSides]; + int pos = 0x32; + for (int i = 0; i < NumberOfTracks * NumberOfSides; i++) + { + TrackSizes[i] = (ushort)(data[pos] | data[pos + 1] << 8); + } + pos = 0x100; + for (int i = 0; i < NumberOfTracks * NumberOfSides; i++) + { + if (TrackSizes[i] == 0) + { + Tracks[i] = new Track(); + Tracks[i].Sectors = new Sector[0]; + continue; + } + int p = pos; + Tracks[i] = new Track(); + Tracks[i].TrackIdent = Encoding.ASCII.GetString(data, p, 12); + p += 16; + Tracks[i].TrackNumber = data[p++]; + Tracks[i].SideNumber = data[p++]; + p += 2; + Tracks[i].SectorSize = data[p++]; + Tracks[i].NumberOfSectors = data[p++]; + Tracks[i].GAP3Length = data[p++]; + Tracks[i].FillerByte = data[p++]; + int dpos = pos + 0x100; + Tracks[i].Sectors = new Sector[Tracks[i].NumberOfSectors]; + for (int s = 0; s < Tracks[i].NumberOfSectors; s++) + { + Tracks[i].Sectors[s] = new Sector(); + + Tracks[i].Sectors[s].TrackNumber = data[p++]; + Tracks[i].Sectors[s].SideNumber = data[p++]; + Tracks[i].Sectors[s].SectorID = data[p++]; + Tracks[i].Sectors[s].SectorSize = data[p++]; + Tracks[i].Sectors[s].Status1 = data[p++]; + Tracks[i].Sectors[s].Status2 = data[p++]; + Tracks[i].Sectors[s].ActualDataByteLength = (ushort)(data[p] | data[p + 1] << 8); + p += 2; + if (Tracks[i].Sectors[s].SectorSize == 0) + { + Tracks[i].Sectors[s].ActualDataByteLength = TrackSizes[i]; + } + else if (Tracks[i].Sectors[s].SectorSize > 6) + { + Tracks[i].Sectors[s].ActualDataByteLength = TrackSizes[i]; + } + else if (Tracks[i].Sectors[s].SectorSize == 6) + { + Tracks[i].Sectors[s].ActualDataByteLength = 0x1800; + } + else + { + Tracks[i].Sectors[s].ActualDataByteLength = 0x80 << Tracks[i].Sectors[s].SectorSize; + } + Tracks[i].Sectors[s].SectorData = new byte[Tracks[i].Sectors[s].ActualDataByteLength]; + for (int b = 0; b < Tracks[i].Sectors[s].ActualDataByteLength; b++) + { + Tracks[i].Sectors[s].SectorData[b] = data[dpos + b]; + } + dpos += Tracks[i].Sectors[s].ActualDataByteLength; + } + pos += TrackSizes[i]; + } + } + + private void ParseEDSK() + { + NumberOfTracks = data[0x30]; + NumberOfSides = data[0x31]; + TrackSizes = new int[NumberOfTracks * NumberOfSides]; + Tracks = new Track[NumberOfTracks * NumberOfSides]; + int pos = 0x34; + for (int i = 0; i < NumberOfTracks * NumberOfSides; i++) + { + TrackSizes[i] = data[pos++] * 256; + } + pos = 0x100; + for (int i = 0; i < NumberOfTracks * NumberOfSides; i++) + { + if (TrackSizes[i] == 0) + { + Tracks[i] = new Track(); + Tracks[i].Sectors = new Sector[0]; + continue; + } + int p = pos; + Tracks[i] = new Track(); + Tracks[i].TrackIdent = Encoding.ASCII.GetString(data, p, 12); + p += 16; + Tracks[i].TrackNumber = data[p++]; + Tracks[i].SideNumber = data[p++]; + Tracks[i].DataRate = data[p++]; + Tracks[i].RecordingMode = data[p++]; + Tracks[i].SectorSize = data[p++]; + Tracks[i].NumberOfSectors = data[p++]; + Tracks[i].GAP3Length = data[p++]; + Tracks[i].FillerByte = data[p++]; + int dpos = pos + 0x100; + Tracks[i].Sectors = new Sector[Tracks[i].NumberOfSectors]; + for (int s = 0; s < Tracks[i].NumberOfSectors; s++) + { + Tracks[i].Sectors[s] = new Sector(); + + Tracks[i].Sectors[s].TrackNumber = data[p++]; + Tracks[i].Sectors[s].SideNumber = data[p++]; + Tracks[i].Sectors[s].SectorID = data[p++]; + Tracks[i].Sectors[s].SectorSize = data[p++]; + Tracks[i].Sectors[s].Status1 = data[p++]; + Tracks[i].Sectors[s].Status2 = data[p++]; + Tracks[i].Sectors[s].ActualDataByteLength = (ushort)(data[p] | data[p + 1] << 8); + p += 2; + Tracks[i].Sectors[s].SectorData = new byte[Tracks[i].Sectors[s].ActualDataByteLength]; + for (int b = 0; b < Tracks[i].Sectors[s].ActualDataByteLength; b++) + { + Tracks[i].Sectors[s].SectorData[b] = data[dpos + b]; + } + if (Tracks[i].Sectors[s].SectorSize <= 7) + { + int specifiedSize = 0x80 << Tracks[i].Sectors[s].SectorSize; + if (specifiedSize < Tracks[i].Sectors[s].ActualDataByteLength) + { + if (Tracks[i].Sectors[s].ActualDataByteLength % specifiedSize != 0) + { + Tracks[i].Sectors[s].ContainsMultipleWeakSectors = true; + } + } + } + dpos += Tracks[i].Sectors[s].ActualDataByteLength; + } + pos += TrackSizes[i]; + } + } + + #region Internal Classes + + public class Track + { + public string TrackIdent { get; set; } + public byte TrackNumber { get; set; } + public byte SideNumber { get; set; } + public byte DataRate { get; set; } + public byte RecordingMode { get; set; } + public byte SectorSize { get; set; } + public byte NumberOfSectors { get; set; } + public byte GAP3Length { get; set; } + public byte FillerByte { get; set; } + public Sector[] Sectors { get; set; } + + public byte GetLowestSectorID() + { + byte res = 0xFF; + foreach (var s in Sectors) + { + if (s.SectorID < res) + res = s.SectorID; + } + return res; + } + } + + public class Sector + { + public byte TrackNumber { get; set; } + public byte SideNumber { get; set; } + public byte SectorID { get; set; } + public byte SectorSize { get; set; } + public byte Status1 { get; set; } + public byte Status2 { get; set; } + public int ActualDataByteLength { get; set; } + public byte[] SectorData { get; set; } + public bool ContainsMultipleWeakSectors { get; set; } + + public int GetChecksum256() + { + int res = 0; + for (int i = 0; i < SectorData.Length; i++) + { + res += SectorData[i] % 256; + } + return res; + } + } + + #endregion + + + } +} diff --git a/BizHawk.Emulation.Common/Database/Database.cs b/BizHawk.Emulation.Common/Database/Database.cs index 5fcf3ce89f..1cd73d701e 100644 --- a/BizHawk.Emulation.Common/Database/Database.cs +++ b/BizHawk.Emulation.Common/Database/Database.cs @@ -310,7 +310,11 @@ namespace BizHawk.Emulation.Common game.System = "ZXSpectrum"; break; - case ".TAP": + case ".CDT": + game.System = "AmstradCPC"; + break; + + case ".TAP": byte[] head = romData.Take(8).ToArray(); if (System.Text.Encoding.Default.GetString(head).Contains("C64-TAPE")) game.System = "C64"; @@ -342,14 +346,9 @@ namespace BizHawk.Emulation.Common break; case ".DSK": - byte[] head2 = romData.Take(20).ToArray(); - string ident = System.Text.Encoding.Default.GetString(head2); - if (ident.ToUpper().Contains("EXTENDED CPC DSK") || - ident.ToUpper().Contains("MV - CPC")) - game.System = "ZXSpectrum"; - else - game.System = "AppleII"; - break; + var dId = new DSKIdentifier(romData); + game.System = dId.IdentifiedSystem; + break; case ".PO": case ".DO": diff --git a/BizHawk.Emulation.Common/SystemLookup.cs b/BizHawk.Emulation.Common/SystemLookup.cs index 4c6c692e46..0e4cc90d06 100644 --- a/BizHawk.Emulation.Common/SystemLookup.cs +++ b/BizHawk.Emulation.Common/SystemLookup.cs @@ -33,7 +33,8 @@ namespace BizHawk.Emulation.Common new SystemInfo { SystemId = "C64", FullName = "Commodore 64" }, new SystemInfo { SystemId = "AppleII", FullName = "Apple II" }, new SystemInfo { SystemId = "INTV", FullName = "Intellivision" }, - new SystemInfo { SystemId = "ZXSpectrum", FullName = "Sinclair ZX Spectrum" } + new SystemInfo { SystemId = "ZXSpectrum", FullName = "Sinclair ZX Spectrum" }, + new SystemInfo { SystemId = "AmstradCPC", FullName = "Amstrad CPC" } }; public SystemInfo this[string systemId] diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj index 7c5b552b65..e2050b3c10 100644 --- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj +++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj @@ -125,6 +125,64 @@ TI83.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AppleII.cs @@ -1440,6 +1498,7 @@ + @@ -1452,7 +1511,12 @@ + + + + + diff --git a/BizHawk.Emulation.Cores/CPUs/Z80A/Interrupts.cs b/BizHawk.Emulation.Cores/CPUs/Z80A/Interrupts.cs index 6bc915c296..8b7852812f 100644 --- a/BizHawk.Emulation.Cores/CPUs/Z80A/Interrupts.cs +++ b/BizHawk.Emulation.Cores/CPUs/Z80A/Interrupts.cs @@ -30,6 +30,10 @@ namespace BizHawk.Emulation.Cores.Components.Z80A public Action IRQCallback = delegate () { }; public Action NMICallback = delegate () { }; + // this will be a few cycles off for now + // it should suffice for now until Alyosha returns from hiatus + public Action IRQACKCallback = delegate () { }; + private void NMI_() { cur_instr = new ushort[] @@ -47,7 +51,7 @@ namespace BizHawk.Emulation.Cores.Components.Z80A BUSRQ = new ushort[] { 0, SPh, 0, 0, SPh, 0, 0, PCh, 0, 0, 0 }; MEMRQ = new ushort[] { 0, SPh, 0, 0, SPh, 0, 0, PCh, 0, 0, 0 }; - } + } // Mode 0 interrupts only take effect if a CALL or RST is on the data bus // Otherwise operation just continues as normal @@ -67,7 +71,9 @@ namespace BizHawk.Emulation.Cores.Components.Z80A BUSRQ = new ushort[] { PCh, 0, 0, PCh, 0, 0, 0 }; MEMRQ = new ushort[] { PCh, 0, 0, PCh, 0, 0, 0 }; - } + + IRQACKCallback(); + } // Just jump to $0038 private void INTERRUPT_1() @@ -89,7 +95,9 @@ namespace BizHawk.Emulation.Cores.Components.Z80A BUSRQ = new ushort[] { I, 0, 0, SPh, 0, 0, SPh, 0, 0, PCh, 0, 0, 0 }; MEMRQ = new ushort[] { I, 0, 0, SPh, 0, 0, SPh, 0, 0, PCh, 0, 0, 0 }; - } + + IRQACKCallback(); + } // Interrupt mode 2 uses the I vector combined with a byte on the data bus private void INTERRUPT_2() @@ -117,7 +125,9 @@ namespace BizHawk.Emulation.Cores.Components.Z80A BUSRQ = new ushort[] { I, 0, 0, SPh, 0, 0, SPh, 0, 0, W, 0, 0, W, 0 ,0 ,PCh, 0, 0, 0 }; MEMRQ = new ushort[] { I, 0, 0, SPh, 0, 0, SPh, 0, 0, W, 0, 0, W, 0, 0, PCh, 0, 0, 0 }; - } + + IRQACKCallback(); + } private void ResetInterrupts() { diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.Controllers.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.Controllers.cs new file mode 100644 index 0000000000..709e9b8e34 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.Controllers.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPCHawk: Core Class + /// * Controllers * + /// + public partial class AmstradCPC + { + /// + /// The one CPCHawk ControllerDefinition + /// + public ControllerDefinition AmstradCPCControllerDefinition + { + get + { + ControllerDefinition definition = new ControllerDefinition(); + definition.Name = "AmstradCPC Controller"; + + // joysticks + List joys1 = new List + { + // P1 Joystick + "P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 Fire1", "P1 Fire2", "P1 Fire3" + }; + + foreach (var s in joys1) + { + definition.BoolButtons.Add(s); + definition.CategoryLabels[s] = "J1"; + } + + List joys2 = new List + { + // P2 Joystick + "P2 Up", "P2 Down", "P2 Left", "P2 Right", "P2 Fire", + }; + + foreach (var s in joys2) + { + definition.BoolButtons.Add(s); + definition.CategoryLabels[s] = "J2"; + } + + // keyboard + List keys = new List + { + /// http://www.cpcwiki.eu/index.php/Programming:Keyboard_scanning + /// http://www.cpcwiki.eu/index.php/File:Grimware_cpc464_version3_case_top.jpg + + // Keyboard - row 1 + "Key ESC", "Key 1", "Key 2", "Key 3", "Key 4", "Key 5", "Key 6", "Key 7", "Key 8", "Key 9", "Key 0", "Key Dash", "Key Hat", "Key CLR", "Key DEL", + // Keyboard - row 2 + "Key TAB", "Key Q", "Key W", "Key E", "Key R", "Key T", "Key Y", "Key U", "Key I", "Key O", "Key P", "Key @", "Key LeftBracket", "Key RETURN", + // Keyboard - row 3 + "Key CAPSLOCK", "Key A", "Key S", "Key D", "Key F", "Key G", "Key H", "Key J", "Key K", "Key L", "Key Colon", "Key SemiColon", "Key RightBracket", + // Keyboard - row 4 + "Key SHIFT", "Key Z", "Key X", "Key C", "Key V", "Key B", "Key N", "Key M", "Key Comma", "Key Period", "Key ForwardSlash", "Key BackSlash", + // Keyboard - row 5 + "Key SPACE", "Key CONTROL", + // Keyboard - Cursor + "Key CURUP", "Key CURDOWN", "Key CURLEFT", "Key CURRIGHT", "Key COPY", + // Keyboard - Numpad + "Key NUM0", "Key NUM1", "Key NUM2", "Key NUM3", "Key NUM4", "Key NUM5", "Key NUM6", "Key NUM7", "Key NUM8", "Key NUM9", "Key NUMPERIOD", "KEY ENTER" + }; + + foreach (var s in keys) + { + definition.BoolButtons.Add(s); + definition.CategoryLabels[s] = "Keyboard"; + } + + // Power functions + List power = new List + { + // Power functions + "Reset", "Power" + }; + + foreach (var s in power) + { + definition.BoolButtons.Add(s); + definition.CategoryLabels[s] = "Power"; + } + + // Datacorder (tape device) + List tape = new List + { + // Tape functions + "Play Tape", "Stop Tape", "RTZ Tape", "Record Tape", "Insert Next Tape", + "Insert Previous Tape", "Next Tape Block", "Prev Tape Block", "Get Tape Status" + }; + + foreach (var s in tape) + { + definition.BoolButtons.Add(s); + definition.CategoryLabels[s] = "Datacorder"; + } + + // Datacorder (tape device) + List disk = new List + { + // Tape functions + "Insert Next Disk", "Insert Previous Disk", /*"Eject Current Disk",*/ "Get Disk Status" + }; + + foreach (var s in disk) + { + definition.BoolButtons.Add(s); + definition.CategoryLabels[s] = "Amstrad Disk Drive"; + } + + return definition; + } + } + } + + /// + /// The possible joystick types + /// + public enum JoystickType + { + NULL, + Joystick1, + Joystick2 + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IDebuggable.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IDebuggable.cs new file mode 100644 index 0000000000..917a08865b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IDebuggable.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPCHawk: Core Class + /// * IDebugggable * + /// + public partial class AmstradCPC : IDebuggable + { + public IDictionary GetCpuFlagsAndRegisters() + { + return new Dictionary + { + ["A"] = _cpu.Regs[_cpu.A], + ["AF"] = _cpu.Regs[_cpu.F] + (_cpu.Regs[_cpu.A] << 8), + ["B"] = _cpu.Regs[_cpu.B], + ["BC"] = _cpu.Regs[_cpu.C] + (_cpu.Regs[_cpu.B] << 8), + ["C"] = _cpu.Regs[_cpu.C], + ["D"] = _cpu.Regs[_cpu.D], + ["DE"] = _cpu.Regs[_cpu.E] + (_cpu.Regs[_cpu.D] << 8), + ["E"] = _cpu.Regs[_cpu.E], + ["F"] = _cpu.Regs[_cpu.F], + ["H"] = _cpu.Regs[_cpu.H], + ["HL"] = _cpu.Regs[_cpu.L] + (_cpu.Regs[_cpu.H] << 8), + ["I"] = _cpu.Regs[_cpu.I], + ["IX"] = _cpu.Regs[_cpu.Ixl] + (_cpu.Regs[_cpu.Ixh] << 8), + ["IY"] = _cpu.Regs[_cpu.Iyl] + (_cpu.Regs[_cpu.Iyh] << 8), + ["L"] = _cpu.Regs[_cpu.L], + ["PC"] = _cpu.Regs[_cpu.PCl] + (_cpu.Regs[_cpu.PCh] << 8), + ["R"] = _cpu.Regs[_cpu.R], + ["Shadow AF"] = _cpu.Regs[_cpu.F_s] + (_cpu.Regs[_cpu.A_s] << 8), + ["Shadow BC"] = _cpu.Regs[_cpu.C_s] + (_cpu.Regs[_cpu.B_s] << 8), + ["Shadow DE"] = _cpu.Regs[_cpu.E_s] + (_cpu.Regs[_cpu.D_s] << 8), + ["Shadow HL"] = _cpu.Regs[_cpu.L_s] + (_cpu.Regs[_cpu.H_s] << 8), + ["SP"] = _cpu.Regs[_cpu.Iyl] + (_cpu.Regs[_cpu.Iyh] << 8), + ["Flag C"] = _cpu.FlagC, + ["Flag N"] = _cpu.FlagN, + ["Flag P/V"] = _cpu.FlagP, + ["Flag 3rd"] = _cpu.Flag3, + ["Flag H"] = _cpu.FlagH, + ["Flag 5th"] = _cpu.Flag5, + ["Flag Z"] = _cpu.FlagZ, + ["Flag S"] = _cpu.FlagS + }; + } + + public void SetCpuRegister(string register, int value) + { + switch (register) + { + default: + throw new InvalidOperationException(); + case "A": + _cpu.Regs[_cpu.A] = (ushort)value; + break; + case "AF": + _cpu.Regs[_cpu.F] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.A] = (ushort)(value & 0xFF00); + break; + case "B": + _cpu.Regs[_cpu.B] = (ushort)value; + break; + case "BC": + _cpu.Regs[_cpu.C] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.B] = (ushort)(value & 0xFF00); + break; + case "C": + _cpu.Regs[_cpu.C] = (ushort)value; + break; + case "D": + _cpu.Regs[_cpu.D] = (ushort)value; + break; + case "DE": + _cpu.Regs[_cpu.E] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.D] = (ushort)(value & 0xFF00); + break; + case "E": + _cpu.Regs[_cpu.E] = (ushort)value; + break; + case "F": + _cpu.Regs[_cpu.F] = (ushort)value; + break; + case "H": + _cpu.Regs[_cpu.H] = (ushort)value; + break; + case "HL": + _cpu.Regs[_cpu.L] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.H] = (ushort)(value & 0xFF00); + break; + case "I": + _cpu.Regs[_cpu.I] = (ushort)value; + break; + case "IX": + _cpu.Regs[_cpu.Ixl] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.Ixh] = (ushort)(value & 0xFF00); + break; + case "IY": + _cpu.Regs[_cpu.Iyl] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.Iyh] = (ushort)(value & 0xFF00); + break; + case "L": + _cpu.Regs[_cpu.L] = (ushort)value; + break; + case "PC": + _cpu.Regs[_cpu.PCl] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.PCh] = (ushort)(value & 0xFF00); + break; + case "R": + _cpu.Regs[_cpu.R] = (ushort)value; + break; + case "Shadow AF": + _cpu.Regs[_cpu.F_s] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.A_s] = (ushort)(value & 0xFF00); + break; + case "Shadow BC": + _cpu.Regs[_cpu.C_s] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.B_s] = (ushort)(value & 0xFF00); + break; + case "Shadow DE": + _cpu.Regs[_cpu.E_s] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.D_s] = (ushort)(value & 0xFF00); + break; + case "Shadow HL": + _cpu.Regs[_cpu.L_s] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.H_s] = (ushort)(value & 0xFF00); + break; + case "SP": + _cpu.Regs[_cpu.SPl] = (ushort)(value & 0xFF); + _cpu.Regs[_cpu.SPh] = (ushort)(value & 0xFF00); + break; + } + } + + public IMemoryCallbackSystem MemoryCallbacks { get; } + + public bool CanStep(StepType type) => false; + + [FeatureNotImplemented] + public void Step(StepType type) + { + throw new NotImplementedException(); + } + + public long TotalExecutedCycles => _cpu.TotalExecutedCycles; + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IEmulator.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IEmulator.cs new file mode 100644 index 0000000000..23a2bfb521 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IEmulator.cs @@ -0,0 +1,83 @@ +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPCHawk: Core Class + /// * IEmulator * + /// + public partial class AmstradCPC : IEmulator + { + public IEmulatorServiceProvider ServiceProvider { get; } + + public ControllerDefinition ControllerDefinition { get; set; } + + public void FrameAdvance(IController controller, bool render, bool renderSound) + { + _controller = controller; + + bool ren = render; + bool renSound = renderSound; + + if (DeterministicEmulation) + { + ren = true; + renSound = true; + } + + _isLag = true; + + if (_tracer.Enabled) + { + _cpu.TraceCallback = s => _tracer.Put(s); + } + else + { + _cpu.TraceCallback = null; + } + + _machine.ExecuteFrame(ren, renSound); + + if (_isLag) + { + _lagCount++; + } + } + + public int Frame + { + get + { + if (_machine == null) + return 0; + else + return _machine.FrameCount; + } + } + + public string SystemId => "AmstradCPC"; + + private bool deterministicEmulation; + public bool DeterministicEmulation + { + get { return deterministicEmulation; } + } + + public void ResetCounters() + { + _machine.FrameCount = 0; + _lagCount = 0; + _isLag = false; + } + + public CoreComm CoreComm { get; } + + public void Dispose() + { + if (_machine != null) + { + _machine = null; + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IMemoryDomains.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IMemoryDomains.cs new file mode 100644 index 0000000000..01b844ff3c --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IMemoryDomains.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPCHawk: Core Class + /// * Memory Domains * + /// + public partial class AmstradCPC + { + internal IMemoryDomains memoryDomains; + private readonly Dictionary _byteArrayDomains = new Dictionary(); + private bool _memoryDomainsInit = false; + + private void SetupMemoryDomains() + { + var domains = new List + { + new MemoryDomainDelegate("System Bus", 0x10000, MemoryDomain.Endian.Little, + (addr) => + { + if (addr < 0 || addr >= 65536) + { + throw new ArgumentOutOfRangeException(); + } + return _machine.ReadBus((ushort)addr); + }, + (addr, value) => + { + if (addr < 0 || addr >= 65536) + { + throw new ArgumentOutOfRangeException(); + } + _machine.WriteBus((ushort)addr, value); + }, 1) + }; + + SyncAllByteArrayDomains(); + + memoryDomains = new MemoryDomainList(_byteArrayDomains.Values.Concat(domains).ToList()); + (ServiceProvider as BasicServiceProvider).Register(memoryDomains); + + _memoryDomainsInit = true; + } + + private void SyncAllByteArrayDomains() + { + SyncByteArrayDomain("ROMLower", _machine.ROMLower); + SyncByteArrayDomain("ROM0", _machine.ROM0); + SyncByteArrayDomain("ROM7", _machine.ROM7); + SyncByteArrayDomain("RAM0", _machine.RAM0); + SyncByteArrayDomain("RAM1", _machine.RAM1); + SyncByteArrayDomain("RAM2", _machine.RAM2); + SyncByteArrayDomain("RAM3", _machine.RAM3); + SyncByteArrayDomain("RAM4", _machine.RAM4); + SyncByteArrayDomain("RAM5", _machine.RAM5); + SyncByteArrayDomain("RAM6", _machine.RAM6); + SyncByteArrayDomain("RAM7", _machine.RAM7); + } + + private void SyncByteArrayDomain(string name, byte[] data) + { + if (_memoryDomainsInit || _byteArrayDomains.ContainsKey(name)) + { + var m = _byteArrayDomains[name]; + m.Data = data; + } + else + { + var m = new MemoryDomainByteArray(name, MemoryDomain.Endian.Little, data, true, 1); + _byteArrayDomains.Add(name, m); + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.ISettable.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.ISettable.cs new file mode 100644 index 0000000000..eda36aace2 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.ISettable.cs @@ -0,0 +1,329 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; +using System.ComponentModel; +using System.Text; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPCHawk: Core Class + /// * ISettable * + /// + public partial class AmstradCPC : ISettable + { + internal AmstradCPCSettings Settings = new AmstradCPCSettings(); + internal AmstradCPCSyncSettings SyncSettings = new AmstradCPCSyncSettings(); + + public AmstradCPCSettings GetSettings() + { + return Settings.Clone(); + } + + public AmstradCPCSyncSettings GetSyncSettings() + { + return SyncSettings.Clone(); + } + + public bool PutSettings(AmstradCPCSettings o) + { + + // restore user settings to devices + if (_machine != null && _machine.AYDevice != null) + { + ((AY38912)_machine.AYDevice as AY38912).PanningConfiguration = o.AYPanConfig; + _machine.AYDevice.Volume = o.AYVolume; + } + if (_machine != null && _machine.TapeBuzzer != null) + { + ((Beeper)_machine.TapeBuzzer as Beeper).Volume = o.TapeVolume; + } + + + Settings = o; + + return false; + } + + public bool PutSyncSettings(AmstradCPCSyncSettings o) + { + bool ret = AmstradCPCSyncSettings.NeedsReboot(SyncSettings, o); + SyncSettings = o; + return ret; + } + + public class AmstradCPCSettings + { + [DisplayName("AY-3-8912 Panning Config")] + [Description("Set the PSG panning configuration.\nThe chip has 3 audio channels that can be outputed in different configurations")] + [DefaultValue(AY38912.AYPanConfig.ABC)] + public AY38912.AYPanConfig AYPanConfig { get; set; } + + [DisplayName("AY-3-8912 Volume")] + [Description("The AY chip volume")] + [DefaultValue(75)] + public int AYVolume { get; set; } + + [DisplayName("Core OSD Message Verbosity")] + [Description("Full: Display all GUI messages\nMedium: Display only emulator/device generated messages\nNone: Show no messages")] + [DefaultValue(OSDVerbosity.Medium)] + public OSDVerbosity OSDMessageVerbosity { get; set; } + + [DisplayName("Tape Loading Volume")] + [Description("The buzzer volume when the tape is playing")] + [DefaultValue(50)] + public int TapeVolume { get; set; } + + public AmstradCPCSettings Clone() + { + return (AmstradCPCSettings)MemberwiseClone(); + } + + public AmstradCPCSettings() + { + BizHawk.Common.SettingsUtil.SetDefaultValues(this); + } + } + + public class AmstradCPCSyncSettings + { + [DisplayName("Deterministic Emulation")] + [Description("If true, the core agrees to behave in a completely deterministic manner")] + [DefaultValue(true)] + public bool DeterministicEmulation { get; set; } + + [DisplayName("CPC Model")] + [Description("The model of Amstrad CPC machine to be emulated")] + [DefaultValue(MachineType.CPC464)] + public MachineType MachineType { get; set; } + + [DisplayName("Auto Start/Stop Tape")] + [Description("If true, CPCHawk will automatically start and stop the tape when the tape motor is triggered")] + [DefaultValue(true)] + public bool AutoStartStopTape { get; set; } + + [DisplayName("Border type")] + [Description("Select how to show the border area")] + [DefaultValue(BorderType.Uniform)] + public BorderType BorderType { get; set; } + + public AmstradCPCSyncSettings Clone() + { + return (AmstradCPCSyncSettings)MemberwiseClone(); + } + + public AmstradCPCSyncSettings() + { + BizHawk.Common.SettingsUtil.SetDefaultValues(this); + } + + public static bool NeedsReboot(AmstradCPCSyncSettings x, AmstradCPCSyncSettings y) + { + return !DeepEquality.DeepEquals(x, y); + } + } + + /// + /// Verbosity of the CPCHawk generated OSD messages + /// + public enum OSDVerbosity + { + /// + /// Show all OSD messages + /// + Full, + /// + /// Only show machine/device generated messages + /// + Medium, + /// + /// No core-driven OSD messages + /// + None + } + + /// + /// Provides information on each emulated machine + /// + public class CPCMachineMetaData + { + public MachineType MachineType { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Released { get; set; } + public string CPU { get; set; } + public string Memory { get; set; } + public string Video { get; set; } + public string Audio { get; set; } + public string Media { get; set; } + public string OtherMisc { get; set; } + + + public static CPCMachineMetaData GetMetaObject(MachineType type) + { + CPCMachineMetaData m = new CPCMachineMetaData(); + m.MachineType = type; + + switch (type) + { + case MachineType.CPC464: + m.Name = "Amstrad CPC 464"; + m.Description = "The CPC 464 was the first personal home computer built by Amstrad in 1984. "; + m.Description += "The 464 was popular with consumers for various reasons. Aside from the joystick port, the computer, keyboard, and tape deck were all combined into one unit."; + m.Released = "1984"; + m.CPU = "Zilog Z80A @ 4MHz"; + m.Memory = "64KB RAM / 32KB ROM"; + m.Video = "Amstrad Gate Array @ 16Mhz & CRCT @ 1Mhz"; + m.Audio = "General Instruments AY-3-8912 PSG (3ch)"; + m.Media = "Cassette Tape (via built-in Datacorder)"; + break; + case MachineType.CPC6128: + m.Name = "Amstrad CPC 6128"; + m.Description = "The CPC6128 features 128 KB RAM and an internal 3-inch floppy disk drive. "; + m.Description += "Aside from various hardware and firmware improvements, one of the CPC6128's most prominent features is the compatibility with the CP/M+ operating system that rendered it attractive for business uses."; + m.Released = "1985"; + m.CPU = "Zilog Z80A @ 4MHz"; + m.Memory = "64KB RAM / 32KB ROM"; + m.Video = "Amstrad Gate Array @ 16Mhz & CRCT @ 1Mhz"; + m.Audio = "General Instruments AY-3-8912 PSG (3ch)"; + m.Media = "3\" Floppy Disk (via built-in Floppy Drive) & Cassette Tape (via external cassette player)"; + break; + /* + case MachineType.ZXSpectrum48: + m.Name = "Sinclair ZX Spectrum 48K / 48K+"; + m.Description = "The original ZX Spectrum 48K RAM version. 2 years later a 'plus' version was released that had a better keyboard. "; + m.Description += "Electronically both the 48K and + are identical, so ZXHawk treats them as the same emulated machine. "; + m.Description += "These machines dominated the UK 8-bit home computer market throughout the 1980's so most non-128k only games are compatible."; + m.Released = "1982 (48K) / 1984 (48K+)"; + m.CPU = "Zilog Z80A @ 3.5MHz"; + m.Memory = "16KB ROM / 48KB RAM"; + m.Video = "ULA @ 7MHz - PAL (50.08Hz Interrupt)"; + m.Audio = "Beeper (HW 1ch. / 10oct.) - Internal Speaker"; + m.Media = "Cassette Tape (via 3rd party external tape player)"; + break; + case MachineType.ZXSpectrum128: + m.Name = "Sinclair ZX Spectrum 128"; + m.Description = "The first Spectrum 128K machine released in Spain in 1985 and later UK in 1986. "; + m.Description += "With an updated ROM and new memory paging system to work around the Z80's 16-bit address bus. "; + m.Description += "The 128 shipped with a copy of the 48k ROM (that is paged in when required) and a new startup menu with the option of dropping into a '48k mode'. "; + m.Description += "Even so, there were some compatibility issues with older Spectrum games that were written to utilise some of the previous model's intricacies. "; + m.Description += "Many games released after 1985 supported the new AY-3-8912 PSG chip making for far superior audio. The extra memory also enabled many games to be loaded in all at once (rather than loading each level from tape when needed)."; + m.Released = "1985 / 1986"; + m.CPU = "Zilog Z80A @ 3.5469 MHz"; + m.Memory = "32KB ROM / 128KB RAM"; + m.Video = "ULA @ 7.0938MHz - PAL (50.01Hz Interrupt)"; + m.Audio = "Beeper (HW 1ch. / 10oct.) & General Instruments AY-3-8912 PSG (3ch) - RF Output"; + m.Media = "Cassette Tape (via 3rd party external tape player)"; + break; + case MachineType.ZXSpectrum128Plus2: + m.Name = "Sinclair ZX Spectrum +2"; + m.Description = "The first Sinclair Spectrum 128K machine that was released after Amstrad purchased Sinclair in 1986. "; + m.Description += "Electronically it was almost identical to the 128, but with the addition of a built-in tape deck and 2 Sinclair Joystick ports."; + m.Released = "1986"; + m.CPU = "Zilog Z80A @ 3.5469 MHz"; + m.Memory = "32KB ROM / 128KB RAM"; + m.Video = "ULA @ 7.0938MHz - PAL (50.01Hz Interrupt)"; + m.Audio = "Beeper (HW 1ch. / 10oct.) & General Instruments AY-3-8912 PSG (3ch) - RF Output"; + m.Media = "Cassette Tape (via built-in Datacorder)"; + break; + case MachineType.ZXSpectrum128Plus2a: + m.Name = "Sinclair ZX Spectrum +2a"; + m.Description = "The +2a looks almost identical to the +2 but is a variant of the +3 machine that was released the same year (except with the same built-in datacorder that the +2 had rather than a floppy drive). "; + m.Description += "Memory paging again changed significantly and this (along with memory contention timing changes) caused more compatibility issues with some older games. "; + m.Description += "Although functionally identical to the +3, it does not contain floppy disk controller."; + m.Released = "1987"; + m.CPU = "Zilog Z80A @ 3.5469 MHz"; + m.Memory = "64KB ROM / 128KB RAM"; + m.Video = "ULA @ 7.0938MHz - PAL (50.01Hz Interrupt)"; + m.Audio = "Beeper (HW 1ch. / 10oct.) & General Instruments AY-3-8912 PSG (3ch) - RF Output"; + m.Media = "Cassette Tape (via built-in Datacorder)"; + break; + case MachineType.ZXSpectrum128Plus3: + m.Name = "Sinclair ZX Spectrum +3"; + m.Description = "Amstrad released the +3 the same year as the +2a, but it featured a built-in floppy drive rather than a datacorder. An external cassette player could still be connected though as in the older 48k models. "; + m.Description += "Memory paging again changed significantly and this (along with memory contention timing changes) caused more compatibility issues with some older games. "; + m.Description += "Currently ZXHawk does not emulate the floppy drive or floppy controller so the machine reports as a +2a on boot."; + m.Released = "1987"; + m.CPU = "Zilog Z80A @ 3.5469 MHz"; + m.Memory = "64KB ROM / 128KB RAM"; + m.Video = "ULA @ 7.0938MHz - PAL (50.01Hz Interrupt)"; + m.Audio = "Beeper (HW 1ch. / 10oct.) & General Instruments AY-3-8912 PSG (3ch) - RF Output"; + m.Media = "3\" Floppy Disk (via built-in Floppy Drive)"; + break; + */ + } + return m; + } + + public static string GetMetaString(MachineType type) + { + var m = GetMetaObject(type); + + StringBuilder sb = new StringBuilder(); + + sb.Append(m.Name); + sb.Append("\n"); + sb.Append("-----------------------------------------------------------------\n"); + // Release + sb.Append("Released:"); + sb.Append(" "); + sb.Append(m.Released); + sb.Append("\n"); + // CPU + sb.Append("CPU:"); + sb.Append(" "); + sb.Append(m.CPU); + sb.Append("\n"); + // Memory + sb.Append("Memory:"); + sb.Append(" "); + sb.Append(m.Memory); + sb.Append("\n"); + // Video + sb.Append("Video:"); + sb.Append(" "); + sb.Append(m.Video); + sb.Append("\n"); + // Audio + sb.Append("Audio:"); + sb.Append(" "); + sb.Append(m.Audio); + sb.Append("\n"); + // Audio + sb.Append("Media:"); + sb.Append(" "); + sb.Append(m.Media); + sb.Append("\n"); + + sb.Append("-----------------------------------------------------------------\n"); + // description + sb.Append(m.Description); + if (m.OtherMisc != null) + sb.Append("\n" + m.OtherMisc); + + return sb.ToString(); + + } + } + + /// + /// The size of the Spectrum border + /// + public enum BorderType + { + /// + /// Attempts to equalise the border areas + /// + Uniform, + + /// + /// Pretty much the signal the gate array is generating (looks shit) + /// + Uncropped, + + /// + /// Top and bottom border removed so that the result is *almost* 16:9 + /// + Widescreen, + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IStatable.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IStatable.cs new file mode 100644 index 0000000000..910e618a17 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.IStatable.cs @@ -0,0 +1,101 @@ +using System.IO; +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPCHawk: Core Class + /// * IStatable * + /// + public partial class AmstradCPC : IStatable + { + + public bool BinarySaveStatesPreferred + { + get { return true; } + } + + public void SaveStateText(TextWriter writer) + { + SyncState(new Serializer(writer)); + } + + public void LoadStateText(TextReader reader) + { + SyncState(new Serializer(reader)); + } + + public void SaveStateBinary(BinaryWriter bw) + { + SyncState(new Serializer(bw)); + } + + public void LoadStateBinary(BinaryReader br) + { + SyncState(new Serializer(br)); + } + + public byte[] SaveStateBinary() + { + MemoryStream ms = new MemoryStream(); + BinaryWriter bw = new BinaryWriter(ms); + SaveStateBinary(bw); + bw.Flush(); + return ms.ToArray(); + } + + private void SyncState(Serializer ser) + { + byte[] core = null; + if (ser.IsWriter) + { + var ms = new MemoryStream(); + ms.Close(); + core = ms.ToArray(); + } + + if (ser.IsWriter) + { + ser.SyncEnum("_machineType", ref _machineType); + + _cpu.SyncState(ser); + ser.BeginSection("AmstradCPC"); + _machine.SyncState(ser); + ser.Sync("Frame", ref _machine.FrameCount); + ser.Sync("LagCount", ref _lagCount); + ser.Sync("IsLag", ref _isLag); + ser.EndSection(); + } + + if (ser.IsReader) + { + var tmpM = _machineType; + ser.SyncEnum("_machineType", ref _machineType); + if (tmpM != _machineType && _machineType.ToString() != "72") + { + string msg = "SAVESTATE FAILED TO LOAD!!\n\n"; + msg += "Current Configuration: " + tmpM.ToString(); + msg += "\n"; + msg += "Saved Configuration: " + _machineType.ToString(); + msg += "\n\n"; + msg += "If you wish to load this SaveState ensure that you have the correct machine configuration selected, reboot the core, then try again."; + CoreComm.ShowMessage(msg); + _machineType = tmpM; + } + else + { + _cpu.SyncState(ser); + ser.BeginSection("AmstradCPC"); + _machine.SyncState(ser); + ser.Sync("Frame", ref _machine.FrameCount); + ser.Sync("LagCount", ref _lagCount); + ser.Sync("IsLag", ref _isLag); + ser.EndSection(); + + SyncAllByteArrayDomains(); + } + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.InputPollable.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.InputPollable.cs new file mode 100644 index 0000000000..6d5bd4ea90 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.InputPollable.cs @@ -0,0 +1,29 @@ +using System; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPCHawk: Core Class + /// * IInputPollable * + /// + public partial class AmstradCPC : IInputPollable + { + public int LagCount + { + get { return _lagCount; } + set { _lagCount = value; } + } + + public bool IsLagFrame + { + get { return _isLag; } + set { _isLag = value; } + } + + public IInputCallbackSystem InputCallbacks { get; } + + private int _lagCount = 0; + private bool _isLag = false; + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.Messaging.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.Messaging.cs new file mode 100644 index 0000000000..b80977c5c2 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.Messaging.cs @@ -0,0 +1,523 @@ +using System; +using System.Linq; +using System.Text; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPCHawk: Core Class + /// * Handles all messaging (OSD) operations * + /// + public partial class AmstradCPC + { + /// + /// Writes a message to the OSD + /// + /// + /// + public void SendMessage(string message, MessageCategory category) + { + if (!CheckMessageSettings(category)) + return; + + StringBuilder sb = new StringBuilder(); + + switch (category) + { + case MessageCategory.Tape: + sb.Append("DATACORDER: "); + sb.Append(message); + break; + case MessageCategory.Input: + sb.Append("INPUT DETECTED: "); + sb.Append(message); + break; + case MessageCategory.Disk: + sb.Append("DISK DRIVE: "); + sb.Append(message); + break; + case MessageCategory.Emulator: + case MessageCategory.Misc: + sb.Append("CPCHAWK: "); + sb.Append(message); + break; + } + + CoreComm.Notify(sb.ToString()); + } + + #region Input Message Methods + + /// + /// Called when certain input presses are detected + /// + /// + public void OSD_FireInputMessage(string input) + { + StringBuilder sb = new StringBuilder(); + sb.Append(input); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Input); + } + + #endregion + + #region DiskDevice Message Methods + + /// + /// Disk message that is fired on core init + /// + public void OSD_DiskInit() + { + StringBuilder sb = new StringBuilder(); + if (_machine.diskImages != null && _machine.UPDDiskDevice != null) + { + sb.Append("Disk Media Imported (count: " + _machine.diskImages.Count() + ")"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Emulator); + } + } + + /// + /// Disk message that is fired when a new disk is inserted into the drive + /// + public void OSD_DiskInserted() + { + StringBuilder sb = new StringBuilder(); + + if (_machine.UPDDiskDevice == null) + { + sb.Append("No Drive Present"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); + return; + } + + sb.Append("DISK INSERTED (" + _machine.DiskMediaIndex + ": " + _diskInfo[_machine.DiskMediaIndex].Name + ")"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); + } + + /// + /// Tape message that prints the current status of the tape device + /// + public void OSD_ShowDiskStatus() + { + StringBuilder sb = new StringBuilder(); + + if (_machine.UPDDiskDevice == null) + { + sb.Append("No Drive Present"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); + return; + } + + if (_diskInfo.Count == 0) + { + sb.Append("No Disk Loaded"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); + return; + } + + if (_machine.UPDDiskDevice != null) + { + if (_machine.UPDDiskDevice.DiskPointer == null) + { + sb.Append("No Disk Loaded"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); + return; + } + + sb.Append("Disk: " + _machine.DiskMediaIndex + ": " + _diskInfo[_machine.DiskMediaIndex].Name); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); + sb.Clear(); + /* + string protection = "None"; + protection = Enum.GetName(typeof(ProtectionType), _machine.UPDDiskDevice.DiskPointer.Protection); + if (protection == "None") + protection += " (OR UNKNOWN)"; + + sb.Append("Detected Protection: " + protection); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); + sb.Clear(); + */ + + sb.Append("Status: "); + + if (_machine.UPDDiskDevice.DriveLight) + sb.Append("READING/WRITING DATA"); + else + sb.Append("UNKNOWN"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Disk); + sb.Clear(); + } + } + + #endregion + + #region TapeDevice Message Methods + + /// + /// Tape message that is fired on core init + /// + public void OSD_TapeInit() + { + if (_tapeInfo.Count == 0) + return; + + StringBuilder sb = new StringBuilder(); + sb.Append("Tape Media Imported (count: " + _tapeInfo.Count() + ")"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Emulator); + } + + /// + /// Tape message that is fired when tape is playing + /// + public void OSD_TapeMotorActive() + { + if (_tapeInfo.Count == 0) + return; + + StringBuilder sb = new StringBuilder(); + sb.Append("MOTOR ON (" + _machine.TapeMediaIndex + ": " + _tapeInfo[_machine.TapeMediaIndex].Name + ")"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when tape is playing + /// + public void OSD_TapeMotorInactive() + { + if (_tapeInfo.Count == 0) + return; + + StringBuilder sb = new StringBuilder(); + sb.Append("MOTOR OFF (" + _machine.TapeMediaIndex + ": " + _tapeInfo[_machine.TapeMediaIndex].Name + ")"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when tape is playing + /// + public void OSD_TapePlaying() + { + if (_tapeInfo.Count == 0) + return; + + StringBuilder sb = new StringBuilder(); + sb.Append("PLAYING MANUAL (" + _machine.TapeMediaIndex + ": " + _tapeInfo[_machine.TapeMediaIndex].Name + ")"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when tape is stopped + /// + public void OSD_TapeStopped() + { + if (_tapeInfo.Count == 0) + return; + + StringBuilder sb = new StringBuilder(); + sb.Append("STOPPED MANUAL (" + _machine.TapeMediaIndex + ": " + _tapeInfo[_machine.TapeMediaIndex].Name + ")"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when tape is rewound + /// + public void OSD_TapeRTZ() + { + if (_tapeInfo.Count == 0) + return; + + StringBuilder sb = new StringBuilder(); + sb.Append("REWOUND (" + _machine.TapeMediaIndex + ": " + _tapeInfo[_machine.TapeMediaIndex].Name + ")"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when a new tape is inserted into the datacorder + /// + public void OSD_TapeInserted() + { + if (_tapeInfo.Count == 0) + return; + + StringBuilder sb = new StringBuilder(); + sb.Append("TAPE INSERTED (" + _machine.TapeMediaIndex + ": " + _tapeInfo[_machine.TapeMediaIndex].Name + ")"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + + /// + /// Tape message that is fired when a tape is stopped automatically + /// + public void OSD_TapeStoppedAuto() + { + StringBuilder sb = new StringBuilder(); + + if (_tapeInfo.Count == 0) + { + sb.Append("No Tape Loaded"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + return; + } + + + sb.Append("STOPPED (Auto Tape Trap Detected)"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when a tape is started automatically + /// + public void OSD_TapePlayingAuto() + { + StringBuilder sb = new StringBuilder(); + + if (_tapeInfo.Count == 0) + { + sb.Append("No Tape Loaded"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + return; + } + + + sb.Append("PLAYING (Auto Tape Trap Detected)"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when a new block starts playing + /// + public void OSD_TapePlayingBlockInfo(string blockinfo) + { + StringBuilder sb = new StringBuilder(); + + if (_tapeInfo.Count == 0) + { + sb.Append("No Tape Loaded"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + return; + } + + + sb.Append("...Starting Block " + blockinfo); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when a tape block is skipped (because it is empty) + /// + public void OSD_TapePlayingSkipBlockInfo(string blockinfo) + { + StringBuilder sb = new StringBuilder(); + + if (_tapeInfo.Count == 0) + { + sb.Append("No Tape Loaded"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + return; + } + + + sb.Append("...Skipping Empty Block " + blockinfo); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when a tape is started automatically + /// + public void OSD_TapeEndDetected(string blockinfo) + { + StringBuilder sb = new StringBuilder(); + + if (_tapeInfo.Count == 0) + { + sb.Append("No Tape Loaded"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + return; + } + + + sb.Append("...Skipping Empty Block " + blockinfo); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when user has manually skipped to the next block + /// + public void OSD_TapeNextBlock(string blockinfo) + { + StringBuilder sb = new StringBuilder(); + + if (_tapeInfo.Count == 0) + { + sb.Append("No Tape Loaded"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + return; + } + + + sb.Append("Manual Skip Next " + blockinfo); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when user has manually skipped to the next block + /// + public void OSD_TapePrevBlock(string blockinfo) + { + StringBuilder sb = new StringBuilder(); + + if (_tapeInfo.Count == 0) + { + sb.Append("No Tape Loaded"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + return; + } + + + sb.Append("Manual Skip Prev " + blockinfo); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that prints the current status of the tape device + /// + public void OSD_ShowTapeStatus() + { + StringBuilder sb = new StringBuilder(); + + if (_tapeInfo.Count == 0) + { + sb.Append("No Tape Loaded"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + return; + } + + sb.Append("Status: "); + + if (_machine.TapeDevice.TapeIsPlaying) + sb.Append("PLAYING"); + else + sb.Append("STOPPED"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + sb.Clear(); + + sb.Append("Tape: " + _machine.TapeMediaIndex + ": " + _tapeInfo[_machine.TapeMediaIndex].Name); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + sb.Clear(); + + sb.Append("Block: "); + sb.Append("(" + (_machine.TapeDevice.CurrentDataBlockIndex + 1) + + " of " + _machine.TapeDevice.DataBlocks.Count() + ") " + + _machine.TapeDevice.DataBlocks[_machine.TapeDevice.CurrentDataBlockIndex].BlockDescription); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + sb.Clear(); + + sb.Append("Block Pos: "); + + int pos = _machine.TapeDevice.Position; + int end = _machine.TapeDevice.DataBlocks[_machine.TapeDevice.CurrentDataBlockIndex].DataPeriods.Count; + double p = 0; + if (end != 0) + p = ((double)pos / (double)end) * (double)100; + + sb.Append(p.ToString("N0") + "%"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + sb.Clear(); + + // get position within the tape itself + sb.Append("Tape Pos: "); + var ind = _machine.TapeDevice.CurrentDataBlockIndex; + int cnt = 0; + for (int i = 0; i < ind; i++) + { + cnt += _machine.TapeDevice.DataBlocks[i].DataPeriods.Count; + } + // now we are at our current block + int ourPos = cnt + pos; + cnt += end; + // count periods in the remaining blocks + for (int i = ind + 1; i < _machine.TapeDevice.DataBlocks.Count; i++) + { + cnt += _machine.TapeDevice.DataBlocks[i].DataPeriods.Count; + } + // work out overall position within the tape + p = 0; + p = ((double)ourPos / (double)cnt) * (double)100; + sb.Append(p.ToString("N0") + "%"); + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + #endregion + + /// + /// Checks whether message category is allowed to be sent + /// + /// + /// + public bool CheckMessageSettings(MessageCategory category) + { + switch (Settings.OSDMessageVerbosity) + { + case OSDVerbosity.Full: + return true; + case OSDVerbosity.None: + return false; + case OSDVerbosity.Medium: + switch (category) + { + case MessageCategory.Disk: + case MessageCategory.Emulator: + case MessageCategory.Tape: + case MessageCategory.Misc: + return true; + default: + return false; + } + default: + return true; + } + } + + /// + /// Defines the different message categories + /// + public enum MessageCategory + { + /// + /// No defined category as such + /// + Misc, + /// + /// User generated input messages (at the moment only tape/disk controls) + /// + Input, + /// + /// Tape device generated messages + /// + Tape, + /// + /// Disk device generated messages + /// + Disk, + /// + /// Emulator generated messages + /// + Emulator + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.Util.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.Util.cs new file mode 100644 index 0000000000..3078893fdd --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.Util.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPCHawk: Core Class + /// * Misc Utilities * + /// + public partial class AmstradCPC + { + /// + /// Helper method that returns a single INT32 from a BitArray + /// + /// + /// + public static int GetIntFromBitArray(BitArray bitArray) + { + if (bitArray.Length > 32) + throw new ArgumentException("Argument length shall be at most 32 bits."); + + int[] array = new int[1]; + bitArray.CopyTo(array, 0); + return array[0]; + } + + /// + /// POKEs a memory bus address + /// + /// + /// + public void PokeMemory(ushort addr, byte value) + { + _machine.WriteBus(addr, value); + } + + public string GetMachineType() + { + string m = ""; + switch (SyncSettings.MachineType) + { + case MachineType.CPC464: + m = "(Amstrad) CPC 464 (64K)"; + break; + case MachineType.CPC6128: + m = "(Amstrad) CPC 6464 (128K)"; + break; + } + + return m; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.cs new file mode 100644 index 0000000000..a11785c72d --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/AmstradCPC.cs @@ -0,0 +1,220 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components.Z80A; +using BizHawk.Emulation.Cores.Properties; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPCHawk: Core Class + /// * Main Initialization * + /// + [Core( + "CPCHawk", + "Asnivor", + isPorted: false, + isReleased: false)] + public partial class AmstradCPC : IRegionable, IDriveLight + { + public AmstradCPC(CoreComm comm, IEnumerable files, List game, object settings, object syncSettings) + { + var ser = new BasicServiceProvider(this); + ServiceProvider = ser; + InputCallbacks = new InputCallbackSystem(); + MemoryCallbacks = new MemoryCallbackSystem(new[] { "System Bus" }); + CoreComm = comm; + _gameInfo = game; + _cpu = new Z80A(); + _tracer = new TraceBuffer { Header = _cpu.TraceHeader }; + _files = files?.ToList() ?? new List(); + + if (settings == null) + settings = new AmstradCPCSettings(); + if (syncSettings == null) + syncSettings = new AmstradCPCSyncSettings(); + + PutSyncSettings((AmstradCPCSyncSettings)syncSettings ?? new AmstradCPCSyncSettings()); + PutSettings((AmstradCPCSettings)settings ?? new AmstradCPCSettings()); + + deterministicEmulation = ((AmstradCPCSyncSettings)syncSettings as AmstradCPCSyncSettings).DeterministicEmulation; + + switch (SyncSettings.MachineType) + { + case MachineType.CPC464: + ControllerDefinition = AmstradCPCControllerDefinition; + Init(MachineType.CPC464, _files, ((AmstradCPCSyncSettings)syncSettings as AmstradCPCSyncSettings).AutoStartStopTape, + ((AmstradCPCSyncSettings)syncSettings as AmstradCPCSyncSettings).BorderType); + break; + case MachineType.CPC6128: + ControllerDefinition = AmstradCPCControllerDefinition; + Init(MachineType.CPC6128, _files, ((AmstradCPCSyncSettings)syncSettings as AmstradCPCSyncSettings).AutoStartStopTape, ((AmstradCPCSyncSettings)syncSettings as AmstradCPCSyncSettings).BorderType); + break; + default: + throw new InvalidOperationException("Machine not yet emulated"); + } + + _cpu.MemoryCallbacks = MemoryCallbacks; + + HardReset = _machine.HardReset; + SoftReset = _machine.SoftReset; + + _cpu.FetchMemory = _machine.ReadMemory; + _cpu.ReadMemory = _machine.ReadMemory; + _cpu.WriteMemory = _machine.WriteMemory; + _cpu.ReadHardware = _machine.ReadPort; + _cpu.WriteHardware = _machine.WritePort; + _cpu.FetchDB = _machine.PushBus; + _cpu.IRQACKCallback = _machine.GateArray.IORQA; + //_cpu.OnExecFetch = _machine.CPUMon.OnExecFetch; + + ser.Register(_tracer); + ser.Register(_cpu); + ser.Register(_machine.GateArray); + + // initialize sound mixer and attach the various ISoundProvider devices + SoundMixer = new SoundProviderMixer((int)(32767 / 10), "Tape Audio", (ISoundProvider)_machine.TapeBuzzer); + if (_machine.AYDevice != null) + SoundMixer.AddSource(_machine.AYDevice, "AY-3-3912"); + + // set audio device settings + if (_machine.AYDevice != null && _machine.AYDevice.GetType() == typeof(AY38912)) + { + ((AY38912)_machine.AYDevice as AY38912).PanningConfiguration = ((AmstradCPCSettings)settings as AmstradCPCSettings).AYPanConfig; + _machine.AYDevice.Volume = ((AmstradCPCSettings)settings as AmstradCPCSettings).AYVolume; + } + + if (_machine.TapeBuzzer != null) + { + ((Beeper)_machine.TapeBuzzer as Beeper).Volume = ((AmstradCPCSettings)settings as AmstradCPCSettings).TapeVolume; + } + + ser.Register(SoundMixer); + + HardReset(); + SetupMemoryDomains(); + } + + public Action HardReset; + public Action SoftReset; + + private readonly Z80A _cpu; + private readonly TraceBuffer _tracer; + public IController _controller; + public CPCBase _machine; + + public List _gameInfo; + public List _tapeInfo = new List(); + public List _diskInfo = new List(); + + private SoundProviderMixer SoundMixer; + + private readonly List _files; + + private byte[] GetFirmware(int length, params string[] names) + { + // Amstrad licensed ROMs are free to distribute and shipped with BizHawk + byte[] embeddedRom = new byte[length]; + bool embeddedFound = true; + switch (names.FirstOrDefault()) + { + // CPC 464 ROMS + case "OS464ROM": + embeddedRom = Util.DecompressGzipFile(new MemoryStream(Resources.OS_464_ROM)); + break; + case "BASIC1-0ROM": + embeddedRom = Util.DecompressGzipFile(new MemoryStream(Resources.CPC_BASIC_1_0_ROM)); + break; + + // CPC 6128 ROMS + case "OS6128ROM": + embeddedRom = Util.DecompressGzipFile(new MemoryStream(Resources.CPC_OS_6128_ROM)); + break; + case "BASIC1-1ROM": + embeddedRom = Util.DecompressGzipFile(new MemoryStream(Resources.CPC_BASIC_1_1_ROM)); + break; + case "AMSDOS0-5ROM": + embeddedRom = Util.DecompressGzipFile(new MemoryStream(Resources.CPC_AMSDOS_0_5_ROM)); + break; + default: + embeddedFound = false; + break; + } + + if (embeddedFound) + return embeddedRom; + + // Embedded ROM not found, maybe this is a peripheral ROM? + var result = names.Select(n => CoreComm.CoreFileProvider.GetFirmware("AmstradCPC", n, false)).FirstOrDefault(b => b != null && b.Length == length); + if (result == null) + { + throw new MissingFirmwareException($"At least one of these firmwares is required: {string.Join(", ", names)}"); + } + + return result; + } + + private MachineType _machineType; + + private void Init(MachineType machineType, List files, bool autoTape, BorderType bType) + { + _machineType = machineType; + + // setup the emulated model based on the MachineType + switch (machineType) + { + case MachineType.CPC464: + _machine = new CPC464(this, _cpu, files, autoTape, bType); + List roms64 = new List(); + roms64.Add(RomData.InitROM(MachineType.CPC464, GetFirmware(0x4000, "OS464ROM"), RomData.ROMChipType.Lower)); + roms64.Add(RomData.InitROM(MachineType.CPC464, GetFirmware(0x4000, "BASIC1-0ROM"), RomData.ROMChipType.Upper, 0)); + _machine.InitROM(roms64.ToArray()); + break; + + case MachineType.CPC6128: + _machine = new CPC6128(this, _cpu, files, autoTape, bType); + List roms128 = new List(); + roms128.Add(RomData.InitROM(MachineType.CPC6128, GetFirmware(0x4000, "OS6128ROM"), RomData.ROMChipType.Lower)); + roms128.Add(RomData.InitROM(MachineType.CPC6128, GetFirmware(0x4000, "BASIC1-1ROM"), RomData.ROMChipType.Upper, 0)); + roms128.Add(RomData.InitROM(MachineType.CPC6128, GetFirmware(0x4000, "AMSDOS0-5ROM"), RomData.ROMChipType.Upper, 7)); + _machine.InitROM(roms128.ToArray()); + break; + } + } + + + #region IRegionable + + public DisplayType Region => DisplayType.PAL; + + #endregion + + #region IDriveLight + + public bool DriveLightEnabled + { + get + { + return true; + } + } + + public bool DriveLightOn + { + get + { + if (_machine != null && + (_machine.TapeDevice != null && _machine.TapeDevice.TapeIsPlaying) || + (_machine.UPDDiskDevice != null && _machine.UPDDiskDevice.DriveLight)) + return true; + + return false; + } + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IBeeperDevice.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IBeeperDevice.cs new file mode 100644 index 0000000000..2bca3c69eb --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IBeeperDevice.cs @@ -0,0 +1,29 @@ +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Represents a beeper/buzzer device + /// + public interface IBeeperDevice + { + /// + /// Initialisation + /// + /// + /// + void Init(int sampleRate, int tStatesPerFrame); + + /// + /// Processes an incoming pulse value and adds it to the blipbuffer + /// + /// + void ProcessPulseValue(bool pulse); + + /// + /// State serialization + /// + /// + void SyncState(Serializer ser); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IFDDHost.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IFDDHost.cs new file mode 100644 index 0000000000..89c99e3eb7 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IFDDHost.cs @@ -0,0 +1,30 @@ + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Defines an object that can load a floppy disk image + /// + public interface IFDDHost + { + /// + /// The currently inserted diskimage + /// + FloppyDisk Disk { get; set; } + + /// + /// Parses a new disk image and loads it into this floppy drive + /// + /// + void FDD_LoadDisk(byte[] diskData); + + /// + /// Ejects the current disk + /// + void FDD_EjectDisk(); + + /// + /// Signs whether the current active drive has a disk inserted + /// + bool FDD_IsDiskLoaded { get; } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IJoystick.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IJoystick.cs new file mode 100644 index 0000000000..e1c100a42b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IJoystick.cs @@ -0,0 +1,38 @@ + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Represents a spectrum joystick + /// + public interface IJoystick + { + /// + /// The type of joystick + /// + JoystickType JoyType { get; } + + /// + /// Array of all the possibly button press names + /// + string[] ButtonCollection { get; set; } + + /// + /// The player number that this controller is currently assigned to + /// + int PlayerNumber { get; set; } + + /// + /// Sets the joystick line based on key pressed + /// + /// + /// + void SetJoyInput(string key, bool isPressed); + + /// + /// Gets the state of a particular joystick binding + /// + /// + /// + bool GetJoyInput(string key); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IKeyboard.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IKeyboard.cs new file mode 100644 index 0000000000..9c78f826d4 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IKeyboard.cs @@ -0,0 +1,58 @@ +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Represents a spectrum keyboard + /// + public interface IKeyboard + { + /// + /// The calling spectrumbase class + /// + CPCBase _machine { get; } + + /// + /// The keyboard matrix for a particular CPC model + /// + string[] KeyboardMatrix { get; set; } + + /// + /// Other keyboard keys that are not in the matrix + /// (usually keys derived from key combos) + /// + string[] NonMatrixKeys { get; set; } + + /// + /// Represents the spectrum key state + /// + bool[] KeyStatus { get; set; } + + /// + /// The currently selected line + /// + int CurrentLine { get; set; } + + /// + /// Reads the current line status + /// + /// + byte ReadCurrentLine(); + + /// + /// Sets the CPC key status + /// + /// + /// + void SetKeyStatus(string key, bool isPressed); + + /// + /// Gets the status of a CPC key + /// + /// + /// + bool GetKeyStatus(string key); + + void SyncState(Serializer ser); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IPSG.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IPSG.cs new file mode 100644 index 0000000000..826cf338b1 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IPSG.cs @@ -0,0 +1,71 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Represents a PSG device (in this case an AY-3-891x) + /// + public interface IPSG : ISoundProvider + { + /// + /// Initlization routine + /// + /// + /// + void Init(int sampleRate, int tStatesPerFrame); + + void SetFunction(int data); + + //void ClockCycle(); + + /// + /// Activates a register + /// + int SelectedRegister { get; set; } + + /// + /// Writes to the PSG + /// + /// + void PortWrite(int value); + + /// + /// Reads from the PSG + /// + int PortRead(); + + + /// + /// Resets the PSG + /// + void Reset(); + + /// + /// The volume of the AY chip + /// + int Volume { get; set; } + + /// + /// Called at the start of a frame + /// + void StartFrame(); + + /// + /// called at the end of a frame + /// + void EndFrame(); + + /// + /// Updates the sound based on number of frame cycles + /// + /// + void UpdateSound(int frameCycle); + + /// + /// IStatable serialization + /// + /// + void SyncState(Serializer ser); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IPortIODevice.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IPortIODevice.cs new file mode 100644 index 0000000000..9394bf1d90 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Abstraction/IPortIODevice.cs @@ -0,0 +1,25 @@ + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Represents a device that utilizes port IN & OUT + /// + public interface IPortIODevice + { + /// + /// Device responds to an IN instruction + /// + /// + /// + /// + bool ReadPort(ushort port, ref int result); + + /// + /// Device responds to an OUT instruction + /// + /// + /// + /// + bool WritePort(ushort port, int result); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Datacorder/DatacorderDevice.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Datacorder/DatacorderDevice.cs new file mode 100644 index 0000000000..7bc4c9c247 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Datacorder/DatacorderDevice.cs @@ -0,0 +1,841 @@ +using BizHawk.Common; +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Represents the tape device + /// + public class DatacorderDevice + { + #region Construction + + private CPCBase _machine; + private Z80A _cpu => _machine.CPU; + private IBeeperDevice _buzzer => _machine.TapeBuzzer; + + /// + /// Default constructor + /// + public DatacorderDevice(bool autoTape) + { + _autoPlay = autoTape; + } + + /// + /// Initializes the datacorder device + /// + /// + public void Init(CPCBase machine) + { + _machine = machine; + } + + #endregion + + #region State Information + + /// + /// Signs whether the tape motor is running + /// + private bool tapeMotor; + public bool TapeMotor + { + get { return tapeMotor; } + set + { + if (tapeMotor == value) + return; + + tapeMotor = value; + if (tapeMotor) + { + _machine.CPC.OSD_TapeMotorActive(); + + if (_autoPlay) + { + Play(); + } + } + + else + { + _machine.CPC.OSD_TapeMotorInactive(); + + if (_autoPlay) + { + Stop(); + } + } + + } + } + + /// + /// Internal counter used to trigger tape buzzer output + /// + private int counter = 0; + + /// + /// The index of the current tape data block that is loaded + /// + private int _currentDataBlockIndex = 0; + public int CurrentDataBlockIndex + { + get + { + if (_dataBlocks.Count() > 0) { return _currentDataBlockIndex; } + else { return -1; } + } + set + { + if (value == _currentDataBlockIndex) { return; } + if (value < _dataBlocks.Count() && value >= 0) + { + _currentDataBlockIndex = value; + _position = 0; + } + } + } + + /// + /// The current position within the current data block + /// + private int _position = 0; + public int Position + { + get + { + if (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count) { return 0; } + else { return _position; } + } + } + + /// + /// Signs whether the tape is currently playing or not + /// + private bool _tapeIsPlaying = false; + public bool TapeIsPlaying + { + get { return _tapeIsPlaying; } + } + + /// + /// A list of the currently loaded data blocks + /// + private List _dataBlocks = new List(); + public List DataBlocks + { + get { return _dataBlocks; } + set { _dataBlocks = value; } + } + + /// + /// Stores the last CPU t-state value + /// + private long _lastCycle = 0; + + /// + /// Edge + /// + private int _waitEdge = 0; + + /// + /// Current tapebit state + /// + private bool currentState = false; + + #endregion + + #region Datacorder Device Settings + + /// + /// Signs whether the device should autodetect when the Z80 has entered into + /// 'load' mode and auto-play the tape if neccesary + /// + private bool _autoPlay; + + #endregion + + #region Emulator + + /// + /// Should be fired at the end of every frame + /// Primary purpose is to detect tape traps and manage auto play (if/when this is ever implemented) + /// + public void EndFrame() + { + //MonitorFrame(); + } + + public void StartFrame() + { + //_buzzer.ProcessPulseValue(currentState); + } + + #endregion + + #region Tape Controls + + /// + /// Starts the tape playing from the beginning of the current block + /// + public void Play() + { + if (_tapeIsPlaying) + return; + + if (!_autoPlay) + _machine.CPC.OSD_TapePlaying(); + + _machine.CPC.OSD_TapeMotorActive(); + + // update the lastCycle + _lastCycle = _cpu.TotalExecutedCycles; + + // reset waitEdge and position + _waitEdge = 0; + _position = 0; + + if ( + _dataBlocks.Count > 0 && // data blocks are present && + _currentDataBlockIndex >= 0 // the current data block index is 1 or greater + ) + { + while (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count) + { + // we are at the end of a data block - move to the next + _position = 0; + _currentDataBlockIndex++; + + // are we at the end of the tape? + if (_currentDataBlockIndex >= _dataBlocks.Count) + { + break; + } + } + + // check for end of tape + if (_currentDataBlockIndex >= _dataBlocks.Count) + { + // end of tape reached. Rewind to beginning + AutoStopTape(); + RTZ(); + return; + } + + // update waitEdge with the current position in the current block + _waitEdge = _dataBlocks[_currentDataBlockIndex].DataPeriods[_position]; + + // sign that the tape is now playing + _tapeIsPlaying = true; + } + } + + /// + /// Stops the tape + /// (should move to the beginning of the next block) + /// + public void Stop() + { + if (!_tapeIsPlaying) + return; + + _machine.CPC.OSD_TapeStopped(); + + // sign that the tape is no longer playing + _tapeIsPlaying = false; + + if ( + _currentDataBlockIndex >= 0 && // we are at datablock 1 or above + _position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count - 1 // the block is still playing back + ) + { + // move to the next block + _currentDataBlockIndex++; + + if (_currentDataBlockIndex >= _dataBlocks.Count()) + { + _currentDataBlockIndex = -1; + } + + // reset waitEdge and position + _waitEdge = 0; + _position = 0; + + if ( + _currentDataBlockIndex < 0 && // block index is -1 + _dataBlocks.Count() > 0 // number of blocks is greater than 0 + ) + { + // move the index on to 0 + _currentDataBlockIndex = 0; + } + } + + // update the lastCycle + _lastCycle = _cpu.TotalExecutedCycles; + } + + /// + /// Rewinds the tape to it's beginning (return to zero) + /// + public void RTZ() + { + Stop(); + _machine.CPC.OSD_TapeRTZ(); + _currentDataBlockIndex = 0; + } + + /// + /// Performs a block skip operation on the current tape + /// TRUE: skip forward + /// FALSE: skip backward + /// + /// + public void SkipBlock(bool skipForward) + { + int blockCount = _dataBlocks.Count; + int targetBlockId = _currentDataBlockIndex; + + if (skipForward) + { + if (_currentDataBlockIndex == blockCount - 1) + { + // last block, go back to beginning + targetBlockId = 0; + } + else + { + targetBlockId++; + } + } + else + { + if (_currentDataBlockIndex == 0) + { + // already first block, goto last block + targetBlockId = blockCount - 1; + } + else + { + targetBlockId--; + } + } + + var bl = _dataBlocks[targetBlockId]; + + StringBuilder sbd = new StringBuilder(); + sbd.Append("("); + sbd.Append((targetBlockId + 1) + " of " + _dataBlocks.Count()); + sbd.Append(") : "); + //sbd.Append("ID" + bl.BlockID.ToString("X2") + " - "); + sbd.Append(bl.BlockDescription); + if (bl.MetaData.Count > 0) + { + sbd.Append(" - "); + sbd.Append(bl.MetaData.First().Key + ": " + bl.MetaData.First().Value); + //sbd.Append("\n"); + //sbd.Append(bl.MetaData.Skip(1).First().Key + ": " + bl.MetaData.Skip(1).First().Value); + } + + if (skipForward) + _machine.CPC.OSD_TapeNextBlock(sbd.ToString()); + else + _machine.CPC.OSD_TapePrevBlock(sbd.ToString()); + + CurrentDataBlockIndex = targetBlockId; + } + + /// + /// Inserts a new tape and sets up the tape device accordingly + /// + /// + public void LoadTape(byte[] tapeData) + { + // instantiate converters + CdtConverter cdtSer = new CdtConverter(this); + + // CDT + if (cdtSer.CheckType(tapeData)) + { + // this file has a tzx header - attempt serialization + try + { + cdtSer.Read(tapeData); + // reset block index + CurrentDataBlockIndex = 0; + return; + } + catch (Exception ex) + { + // exception during operation + var e = ex; + throw new Exception(this.GetType().ToString() + + "\n\nTape image file has a valid CDT header, but threw an exception whilst data was being parsed.\n\n" + e.ToString()); + } + } + } + + /// + /// Resets the tape + /// + public void Reset() + { + RTZ(); + } + + #endregion + + #region Tape Device Methods + + /// + /// Is called every cpu cycle but runs every 50 t-states + /// This enables the tape devices to play out even if the spectrum itself is not + /// requesting tape data + /// + public void TapeCycle() + { + if (TapeMotor) + { + counter++; + + if (counter > 20) + { + counter = 0; + bool state = GetEarBit(_machine.CPU.TotalExecutedCycles); + _buzzer.ProcessPulseValue(state); + } + } + } + + /// + /// Simulates the spectrum 'EAR' input reading data from the tape + /// + /// + /// + public bool GetEarBit(long cpuCycle) + { + // decide how many cycles worth of data we are capturing + long cycles = cpuCycle - _lastCycle; + + // check whether tape is actually playing + if (tapeMotor == false) + { + // it's not playing. Update lastCycle and return + _lastCycle = cpuCycle; + return false; + } + + // check for end of tape + if (_currentDataBlockIndex < 0) + { + // end of tape reached - RTZ (and stop) + RTZ(); + return currentState; + } + + // process the cycles based on the waitEdge + while (cycles >= _waitEdge) + { + // decrement cycles + cycles -= _waitEdge; + + if (_position == 0 && tapeMotor) + { + // start of block - take care of initial pulse level for PZX + switch (_dataBlocks[_currentDataBlockIndex].BlockDescription) + { + case BlockType.PULS: + // initial pulse level is always low + if (currentState) + FlipTapeState(); + break; + case BlockType.DATA: + // initial pulse level is stored in block + if (currentState != _dataBlocks[_currentDataBlockIndex].InitialPulseLevel) + FlipTapeState(); + break; + case BlockType.PAUS: + // initial pulse level is stored in block + if (currentState != _dataBlocks[_currentDataBlockIndex].InitialPulseLevel) + FlipTapeState(); + break; + } + + // most of these amstrad tapes appear to have a pause block at the start + // skip this if it is the first block + switch (_dataBlocks[_currentDataBlockIndex].BlockDescription) + { + case BlockType.PAUS: + case BlockType.PAUSE_BLOCK: + case BlockType.Pause_or_Stop_the_Tape: + if (_currentDataBlockIndex == 0) + { + // this is the first block on the tape + SkipBlock(true); + } + else + { + // there may be non-data blocks before this + bool okToSkipPause = true; + for (int i = _currentDataBlockIndex; i >= 0; i--) + { + switch (_dataBlocks[i].BlockDescription) + { + case BlockType.Archive_Info: + case BlockType.BRWS: + case BlockType.Custom_Info_Block: + case BlockType.Emulation_Info: + case BlockType.Glue_Block: + case BlockType.Hardware_Type: + case BlockType.Message_Block: + case BlockType.PZXT: + case BlockType.Text_Description: + break; + default: + okToSkipPause = false; + break; + } + + if (!okToSkipPause) + break; + } + + if (okToSkipPause) + { + SkipBlock(true); + } + } + break; + } + + // notify about the current block + var bl = _dataBlocks[_currentDataBlockIndex]; + + StringBuilder sbd = new StringBuilder(); + sbd.Append("("); + sbd.Append((_currentDataBlockIndex + 1) + " of " + _dataBlocks.Count()); + sbd.Append(") : "); + //sbd.Append("ID" + bl.BlockID.ToString("X2") + " - "); + sbd.Append(bl.BlockDescription); + if (bl.MetaData.Count > 0) + { + sbd.Append(" - "); + sbd.Append(bl.MetaData.First().Key + ": " + bl.MetaData.First().Value); + } + _machine.CPC.OSD_TapePlayingBlockInfo(sbd.ToString()); + } + + + // increment the current period position + _position++; + + if (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count()) + { + // we have reached the end of the current block + + if (_dataBlocks[_currentDataBlockIndex].DataPeriods.Count() == 0) + { + // notify about the current block (we are skipping it because its empty) + var bl = _dataBlocks[_currentDataBlockIndex]; + StringBuilder sbd = new StringBuilder(); + sbd.Append("("); + sbd.Append((_currentDataBlockIndex + 1) + " of " + _dataBlocks.Count()); + sbd.Append(") : "); + //sbd.Append("ID" + bl.BlockID.ToString("X2") + " - "); + sbd.Append(bl.BlockDescription); + if (bl.MetaData.Count > 0) + { + sbd.Append(" - "); + sbd.Append(bl.MetaData.First().Key + ": " + bl.MetaData.First().Value); + } + _machine.CPC.OSD_TapePlayingSkipBlockInfo(sbd.ToString()); + + } + + // skip any empty blocks (and process any command blocks) + while (_position >= _dataBlocks[_currentDataBlockIndex].DataPeriods.Count()) + { + // check for any commands + var command = _dataBlocks[_currentDataBlockIndex].Command; + var block = _dataBlocks[_currentDataBlockIndex]; + bool shouldStop = false; + switch (command) + { + case TapeCommand.STOP_THE_TAPE: + case TapeCommand.STOP_THE_TAPE_48K: + throw new Exception("spectrum tape command found in CPC tape"); + + /* + // Stop the tape command found - if this is the end of the tape RTZ + // otherwise just STOP and move to the next block + case TapeCommand.STOP_THE_TAPE: + + _machine.CPC.OSD_TapeStoppedAuto(); + shouldStop = true; + + if (_currentDataBlockIndex >= _dataBlocks.Count()) + RTZ(); + else + { + Stop(); + } + + _monitorTimeOut = 2000; + + break; + case TapeCommand.STOP_THE_TAPE_48K: + if (is48k) + { + _machine.CPC.OSD_TapeStoppedAuto(); + shouldStop = true; + + if (_currentDataBlockIndex >= _dataBlocks.Count()) + RTZ(); + else + { + Stop(); + } + + _monitorTimeOut = 2000; + } + break; + */ + default: + break; + } + + if (shouldStop) + break; + + _position = 0; + _currentDataBlockIndex++; + + if (_currentDataBlockIndex >= _dataBlocks.Count()) + { + break; + } + } + + // check for end of tape + if (_currentDataBlockIndex >= _dataBlocks.Count()) + { + _currentDataBlockIndex = -1; + RTZ(); + return currentState; + } + } + + // update waitEdge with current position within the current block + _waitEdge = _dataBlocks[_currentDataBlockIndex].DataPeriods[_position]; + + // flip the current state + FlipTapeState(); + + } + + // update lastCycle and return currentstate + _lastCycle = cpuCycle - (long)cycles; + + // play the buzzer + //_buzzer.ProcessPulseValue(false, currentState); + + return currentState; + } + + private void FlipTapeState() + { + currentState = !currentState; + } + + #endregion + + #region TapeMonitor + + + + public void AutoStopTape() + { + if (!_tapeIsPlaying) + return; + + if (!_autoPlay) + return; + + Stop(); + _machine.CPC.OSD_TapeStoppedAuto(); + } + + public void AutoStartTape() + { + if (_tapeIsPlaying) + return; + + if (!_autoPlay) + return; + + Play(); + _machine.CPC.OSD_TapePlayingAuto(); + } + + /* + public int MaskableInterruptCount = 0; + + private void MonitorFrame() + { + if (_tapeIsPlaying && _autoPlay) + { + if (DataBlocks.Count > 1 || + (_dataBlocks[_currentDataBlockIndex].BlockDescription != BlockType.CSW_Recording && + _dataBlocks[_currentDataBlockIndex].BlockDescription != BlockType.WAV_Recording)) + { + // we should only stop the tape when there are multiple blocks + // if we just have one big block (maybe a CSW or WAV) then auto stopping will cock things up + _monitorTimeOut--; + } + + if (_monitorTimeOut < 0) + { + if (_dataBlocks[_currentDataBlockIndex].BlockDescription != BlockType.PAUSE_BLOCK && + _dataBlocks[_currentDataBlockIndex].BlockDescription != BlockType.PAUS) + { + AutoStopTape(); + } + + return; + } + + // fallback in case usual monitor detection methods do not work + + // number of t-states since last IN operation + long diff = _machine.CPU.TotalExecutedCycles - _lastINCycle; + + // get current datablock + var block = DataBlocks[_currentDataBlockIndex]; + + // is this a pause block? + if (block.BlockDescription == BlockType.PAUS || block.BlockDescription == BlockType.PAUSE_BLOCK) + { + // dont autostop the tape here + return; + } + + // pause in ms at the end of the current block + int blockPause = block.PauseInMS; + + // timeout in t-states (equiv. to blockpause) + int timeout = ((_machine.GateArray.FrameLength * 50) / 1000) * blockPause; + + // dont use autostop detection if block has no pause at the end + if (timeout == 0) + return; + + // dont autostop if there is only 1 block + if (DataBlocks.Count == 1 || _dataBlocks[_currentDataBlockIndex].BlockDescription == BlockType.CSW_Recording || + _dataBlocks[_currentDataBlockIndex].BlockDescription == BlockType.WAV_Recording + ) + { + return; + } + + if (diff >= timeout * 2) + { + // There have been no attempted tape reads by the CPU within the double timeout period + // Autostop the tape + AutoStopTape(); + _lastCycle = _cpu.TotalExecutedCycles; + } + } + } + */ + + #endregion + + #region IPortIODevice + + /// + /// Mask constants + /// + private const int TAPE_BIT = 0x40; + private const int EAR_BIT = 0x10; + private const int MIC_BIT = 0x08; + + /// + /// Device responds to an IN instruction + /// + /// + public bool ReadPort() + { + if (TapeIsPlaying) + { + GetEarBit(_cpu.TotalExecutedCycles); + } + /* + if (currentState) + { + result |= TAPE_BIT; + } + else + { + result &= ~TAPE_BIT; + } + */ + + if (!TapeIsPlaying) + { + //if (_machine.UPDDiskDevice == null || !_machine.UPDDiskDevice.FDD_IsDiskLoaded) + //MonitorRead(); + } + //if (_machine.UPDDiskDevice == null || !_machine.UPDDiskDevice.FDD_IsDiskLoaded) + //MonitorRead(); + + return true; + } + + /// + /// Device responds to an OUT instruction + /// + /// + /// + public void WritePort(bool state) + { + // not implemented + + /* + if (!TapeIsPlaying) + { + currentState = ((byte)result & 0x10) != 0; + } + */ + } + + #endregion + + #region State Serialization + + /// + /// Bizhawk state serialization + /// + /// + public void SyncState(Serializer ser) + { + ser.BeginSection("DatacorderDevice"); + ser.Sync("counter", ref counter); + ser.Sync("_currentDataBlockIndex", ref _currentDataBlockIndex); + ser.Sync("_position", ref _position); + ser.Sync("_tapeIsPlaying", ref _tapeIsPlaying); + ser.Sync("_lastCycle", ref _lastCycle); + ser.Sync("_waitEdge", ref _waitEdge); + ser.Sync("currentState", ref currentState); + ser.Sync("TapeMotor", ref tapeMotor); + ser.EndSection(); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/CHRN.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/CHRN.cs new file mode 100644 index 0000000000..2eb7f7c5be --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/CHRN.cs @@ -0,0 +1,180 @@ + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Used for the sector CHRN structure + /// + public class CHRN + { + /// + /// Track + /// + public byte C { get; set; } + + /// + /// Side + /// + public byte H { get; set; } + + /// + /// Sector ID + /// + public byte R { get; set; } + + /// + /// Sector Size + /// + public byte N { get; set; } + + /// + /// Status register 1 + /// + private byte _flag1; + public byte Flag1 + { + get { return _flag1; } + set { _flag1 = value; } + } + + /// + /// Status register 2 + /// + private byte _flag2; + public byte Flag2 + { + get { return _flag2; } + set { _flag2 = value; } + } + + /// + /// Used to store the last transmitted/received data bytes + /// + public byte[] DataBytes { get; set; } + + /// + /// ID for the read/write data command + /// + public int DataID { get; set; } + + #region Helper Methods + + /// + /// Missing Address Mark (Sector_ID or DAM not found) + /// + public bool ST1MA + { + get { return NECUPD765.GetBit(0, _flag1); } + set + { + if (value) { NECUPD765.SetBit(0, ref _flag1); } + else { NECUPD765.UnSetBit(0, ref _flag1); } + } + } + + /// + /// No Data (Sector_ID not found, CRC fail in ID_field) + /// + public bool ST1ND + { + get { return NECUPD765.GetBit(2, _flag1); } + set + { + if (value) { NECUPD765.SetBit(2, ref _flag1); } + else { NECUPD765.UnSetBit(2, ref _flag1); } + } + } + + /// + /// Data Error (CRC-fail in ID- or Data-Field) + /// + public bool ST1DE + { + get { return NECUPD765.GetBit(5, _flag1); } + set + { + if (value) { NECUPD765.SetBit(5, ref _flag1); } + else { NECUPD765.UnSetBit(5, ref _flag1); } + } + } + + /// + /// End of Track (set past most read/write commands) (see IC) + /// + public bool ST1EN + { + get { return NECUPD765.GetBit(7, _flag1); } + set + { + if (value) { NECUPD765.SetBit(7, ref _flag1); } + else { NECUPD765.UnSetBit(7, ref _flag1); } + } + } + + /// + /// Missing Address Mark in Data Field (DAM not found) + /// + public bool ST2MD + { + get { return NECUPD765.GetBit(0, _flag2); } + set + { + if (value) { NECUPD765.SetBit(0, ref _flag2); } + else { NECUPD765.UnSetBit(0, ref _flag2); } + } + } + + /// + /// Bad Cylinder (read/programmed track-ID different and read-ID = FF) + /// + public bool ST2BC + { + get { return NECUPD765.GetBit(1, _flag2); } + set + { + if (value) { NECUPD765.SetBit(1, ref _flag2); } + else { NECUPD765.UnSetBit(1, ref _flag2); } + } + } + + /// + /// Wrong Cylinder (read/programmed track-ID different) (see b1) + /// + public bool ST2WC + { + get { return NECUPD765.GetBit(4, _flag2); } + set + { + if (value) { NECUPD765.SetBit(4, ref _flag2); } + else { NECUPD765.UnSetBit(4, ref _flag2); } + } + } + + /// + /// Data Error in Data Field (CRC-fail in data-field) + /// + public bool ST2DD + { + get { return NECUPD765.GetBit(5, _flag2); } + set + { + if (value) { NECUPD765.SetBit(5, ref _flag2); } + else { NECUPD765.UnSetBit(5, ref _flag2); } + } + } + + /// + /// Control Mark (read/scan command found sector with deleted DAM) + /// + public bool ST2CM + { + get { return NECUPD765.GetBit(6, _flag2); } + set + { + if (value) { NECUPD765.SetBit(6, ref _flag2); } + else { NECUPD765.UnSetBit(6, ref _flag2); } + } + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.Definitions.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.Definitions.cs new file mode 100644 index 0000000000..ff1bacb223 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.Definitions.cs @@ -0,0 +1,826 @@ +using BizHawk.Common; +using System; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Definitions + /// + #region Attribution + /* + Implementation based on the information contained here: + http://www.cpcwiki.eu/index.php/765_FDC + and here: + http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf + */ + #endregion + public partial class NECUPD765 + { + #region Enums + + /// + /// Defines the current phase of the controller + /// + private enum Phase + { + /// + /// FDC is in an idle state, awaiting the next initial command byte + /// + Idle, + + /// + /// FDC is in a state waiting for the next command instruction + /// A command consists of a command byte (eventually including the MF, MK, SK bits), and up to eight parameter bytes + /// + Command, + + /// + /// During this phase, the actual data is transferred (if any). Usually that are the data bytes for the read/written sector(s), except for the Format Track Command, + /// in that case four bytes for each sector are transferred + /// + Execution, + + /// + /// Returns up to seven result bytes (depending on the command) that are containing status information. The Recalibrate and Seek Track commands do not return result bytes directly, + /// instead the program must wait until the Main Status Register signalizes that the command has been completed, and then it must (!) send a + /// Sense Interrupt State command to 'terminate' the Seek/Recalibrate command. + /// + Result + } + + /// + /// The lifecycle of an instruction + /// Similar to phase, this describes the current 'sub-phase' we are in when dealing with an instruction + /// + private enum InstructionState + { + /// + /// FDC has received a command byte and is currently reading parameter bytes from the data bus + /// + ReceivingParameters, + + /// + /// All parameter bytes have been received. This phase allows any neccessary setup before instruction execution starts + /// + PreExecution, + + /// + /// The start of instruction execution. This may end up with the FDC moving into result phase, + /// but also may also prepare the way for further processing to occur later in execution phase + /// + StartExecute, + + /// + /// Data is read or written in execution phase + /// + ExecutionReadWrite, + + /// + /// Execution phase is well under way. This state primarily deals with data transfer between CPU and FDC + /// + ExecutionWrite, + + /// + /// Execution phase is well under way. This state primarily deals with data transfer between FDC and CPU + /// + ExecutionRead, + + /// + /// Execution has finished and results bytes are ready to be read by the CPU + /// Initial result setup + /// + StartResult, + + /// + /// Result processing + /// + ProcessResult, + + /// + /// Results are being sent + /// + SendingResults, + + /// + /// Final cleanup tasks when the instruction has fully completed + /// + Completed + + } + + /// + /// Represents internal interrupt state of the FDC + /// + public enum InterruptState + { + /// + /// There is no interrupt + /// + None, + /// + /// Execution interrupt + /// + Execution, + /// + /// Result interrupt + /// + Result, + /// + /// Ready interrupt + /// + Ready, + /// + /// Seek interrupt + /// + Seek + } + + /// + /// Possible main states that each drive can be in + /// + public enum DriveMainState + { + /// + /// Drive is not doing anything + /// + None, + /// + /// Seek operation is in progress + /// + Seek, + /// + /// Recalibrate operation is in progress + /// + Recalibrate, + /// + /// A scan data operation is in progress + /// + Scan, + /// + /// A read ID operation is in progress + /// + ReadID, + /// + /// A read data operation is in progress + /// + ReadData, + /// + /// A read diagnostic (read track) operation is in progress + /// + ReadDiagnostic, + /// + /// A write id (format track) operation is in progress + /// + WriteID, + /// + /// A write data operation is in progress + /// + WriteData, + } + + /// + /// State information during a seek/recalibration operation + /// + public enum SeekSubState + { + /// + /// Seek hasnt started yet + /// + Idle, + /// + /// Delayed + /// + Wait, + /// + /// Setup for head move + /// + MoveInit, + /// + /// Seek is currently happening + /// + HeadMove, + /// + /// Head move with no delay + /// + MoveImmediate, + /// + /// Ready to complete + /// + PerformCompletion, + /// + /// Seek operation has completed + /// + SeekCompleted + } + + /// + /// Seek int code + /// + public enum SeekIntStatus + { + Normal, + Abnormal, + DriveNotReady, + } + + /// + /// The direction of a specific command + /// + private enum CommandDirection + { + /// + /// Data flows from UPD765A to Z80 + /// + OUT, + /// + /// Data flows from Z80 to UPD765A + /// + IN + } + + /// + /// Enum defining the different types of result that can be returned + /// + private enum ResultType + { + /// + /// Standard 7 result bytes are returned + /// + Standard, + /// + /// 1 byte returned - ST3 + /// (used for SenseDriveStatus) + /// + ST3, + /// + /// 1 byte returned - ST0 + /// (used for version & invalid) + /// + ST0, + /// + /// 2 bytes returned for sense interrupt status command + /// ST0 + /// CurrentCylinder + /// + Interrupt + } + + /// + /// Possible list of encountered drive status errors + /// + public enum Status + { + /// + /// No error detected + /// + None, + /// + /// An undefined error has been detected + /// + Undefined, + /// + /// Drive is not ready + /// + DriveNotReady, + /// + /// Invalid command received + /// + Invalid, + /// + /// The disk has its write protection tab enabled + /// + WriteProtected, + /// + /// The requested sector has not been found + /// + SectorNotFound + } + + /// + /// Represents the direction that the head is moving over the cylinders + /// Increment: Track number increasing (head moving from outside of disk inwards) + /// Decrement: Track number decreasing (head moving from inside of disk outwards) + /// + public enum SkipDirection + { + Increment, + Decrement + } + + #endregion + + #region Constants + + // Command Instruction Constants + // Designates the default postitions within the cmdbuffer array + + public const int CM_HEAD = 0; + /// + /// C - Track + /// + public const int CM_C = 1; + /// + /// H - Side + /// + public const int CM_H = 2; + /// + /// R - Sector ID + /// + public const int CM_R = 3; + /// + /// N - Sector size + /// + public const int CM_N = 4; + /// + /// EOT - End of track + /// + public const int CM_EOT = 5; + /// + /// GPL - Gap length + /// + public const int CM_GPL = 6; + /// + /// DTL - Data length + /// + public const int CM_DTL = 7; + /// + /// STP - Step + /// + public const int CM_STP = 7; + + // Result Instruction Constants + // Designates the default postitions within the cmdbuffer array + + /// + /// Status register 0 + /// + public const int RS_ST0 = 0; + /// + /// Status register 1 + /// + public const int RS_ST1 = 1; + /// + /// Status register 2 + /// + public const int RS_ST2 = 2; + /// + /// C - Track + /// + public const int RS_C = 3; + /// + /// H - Side + /// + public const int RS_H = 4; + /// + /// R - Sector ID + /// + public const int RS_R = 5; + /// + /// N - Sector size + /// + public const int RS_N = 6; + + // Main Status Register Constants + // Designates the bit positions within the Main status register + + /// + /// FDD0 Busy (seek/recalib active, until succesful sense intstat) + /// FDD number 0 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command. + /// + public const int MSR_D0B = 0; + /// + /// FDD1 Busy (seek/recalib active, until succesful sense intstat) + /// FDD number 1 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command. + /// + public const int MSR_D1B = 1; + /// + /// FDD2 Busy (seek/recalib active, until succesful sense intstat) + /// FDD number 2 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command. + /// + public const int MSR_D2B = 2; + /// + /// FDD3 Busy (seek/recalib active, until succesful sense intstat) + /// FDD number 3 is in the seek mode. If any of the DnB bits IS set FDC will not accept read or write command. + /// + public const int MSR_D3B = 3; + /// + /// FDC Busy (still in command-, execution- or result-phase) + /// A Read or Write command is in orocess. (FDC Busy) FDC will not accept any other command + /// + public const int MSR_CB = 4; + /// + /// Execution Mode (still in execution-phase, non_DMA_only) + /// This bit is set only during execution ohase (Execution Mode) in non-DMA mode When DB5 goes low, execution phase has ended and result phase has started.It operates only during + /// non-DMA mode of operation + /// + public const int MSR_EXM = 5; + /// + /// Data Input/Output (0=CPU->FDC, 1=FDC->CPU) (see b7) + /// Indicates direction of data transfer between FDC and data regrster If DIO = 1, then transfer is from data register to the + /// processor.If DIO = 0, then transfer is from the processor to data register + /// + public const int MSR_DIO = 6; + /// + /// Request For Master (1=ready for next byte) (see b6 for direction) + /// ndicates data register IS ready to send or receive data to or from the processor Both bits DIO and RQM should be + /// used to perform the hand-shaking functions of “ready” and “directron” to the processor + /// + public const int MSR_RQM = 7; + + // Status Register 0 Constants + // Designates the bit positions within the status register 0 + + /// + /// Unit Select (driveno during interrupt) + /// This flag IS used to indicate a drive unit number at interrupt + /// + public const int SR0_US0 = 0; + + /// + /// Unit Select (driveno during interrupt) + /// This flag IS used to indicate a drive unit number at interrupt + /// + public const int SR0_US1 = 1; + + /// + /// Head Address (head during interrupt) + /// State of the head at interrupt + /// + public const int SR0_HD = 2; + + /// + /// Not Ready (drive not ready or non-existing 2nd head selected) + /// Not Ready - When the FDD IS in the not-ready state and a Read or Write command IS Issued, this + /// flag IS set If a Read or Write command is issued to side 1 of a single-sided drive, then this flag IS set + /// + public const int SR0_NR = 3; + + /// + /// Equipment Check (drive failure or recalibrate failed (retry)) + /// Equipment check - If a fault srgnal IS received from the FDD, or if the track 0 srgnal fails to occur after 77 + /// step pulses(Recalibrate Command) then this flag is set + /// + public const int SR0_EC = 4; + + /// + /// Seek End (Set if seek-command completed) + /// Seek end - When the FDC completes the Seek command, this flag IS set lo 1 (high) + /// + public const int SR0_SE = 5; + + /// + /// Interrupt Code (low byte) + /// Interrupt Code (0=OK, 1=aborted:readfail/OK if EN, 2=unknown cmd + /// or senseint with no int occured, 3=aborted:disc removed etc.) + /// + public const int SR0_IC0 = 6; + + /// + /// Interrupt Code (high byte) + /// Interrupt Code (0=OK, 1=aborted:readfail/OK if EN, 2=unknown cmd + /// or senseint with no int occured, 3=aborted:disc removed etc.) + /// + public const int SR0_IC1 = 7; + + // Status Register 1 Constants + // Designates the bit positions within the status register 1 + + /// + /// Missing Address Mark (Sector_ID or DAM not found) + /// Missing address mark - This bit is set i f the FDC does not detect the IDAM before 2 index pulses It is also set if + /// the FDC cannot find the DAM or DDAM after the IDAM i s found.MD bit of ST2 is also set at this time + /// + public const int SR1_MA = 0; + + /// + /// Not Writeable (tried to write/format disc with wprot_tab=on) + /// Not writable (write protect) - During execution of Write Data, Write Deleted Data or Write ID command. if the FDC + /// detect: a write protect srgnal from the FDD.then this flag is Set + /// + public const int SR1_NW = 1; + + /// + /// No Data + /// No Data (Sector_ID not found, CRC fail in ID_field) + /// + /// During execution of Read Data. Read Deleted Data Write Data.Write Deleted Data or Scan command, if the FDC cannot find + /// the sector specified in the IDR(2)Register, this flag i s set. + /// + /// During execution of the Read ID command. if the FDC cannot read the ID field without an error, then this flag IS set + /// + /// During execution of the Read Diagnostic command. if the starting sector cannot be found, then this flag is set + /// + public const int SR1_ND = 2; + + /// + /// Over Run (CPU too slow in execution-phase (ca. 26us/Byte)) + /// Overrun - If the FDC i s not serviced by the host system during data transfers within a certain time interval.this flaa i s set + /// + public const int SR1_OR = 4; + + /// + /// Data Error (CRC-fail in ID- or Data-Field) + /// Data error - When the FDC detects a CRC(1) error in either the ID field or the data field, this flag is set + /// + public const int SR1_DE = 5; + + /// + /// End of Track (set past most read/write commands) (see IC) + /// End of cylinder - When the FDC tries to access a sector beyond the final sector of a cylinder, this flag I S set + /// + public const int SR1_EN = 7; + + // Status Register 2 Constants + // Designates the bit positions within the status register 2 + + /// + /// Missing Address Mark in Data Field (DAM not found) + /// Missing address mark - When data IS read from the medium, i f the FDC cannot find a data address mark or deleted + /// data address mark, then this flag is set + /// + public const int SR2_MD = 0; + + /// + /// Bad Cylinder (read/programmed track-ID different and read-ID = FF) + /// Bad cylinder - This bit i s related to the ND bit. and when the contents of C on the medium is different + /// from that stored i n the IDR and the contents of C IS FFH.then this flag IS set + /// + public const int SR2_BC = 1; + + /// + /// Scan Not Satisfied (no fitting sector found) + /// Scan not satisfied - During execution of the Scan command, i f the F D cannot find a sector on the cylinder + /// which meets the condition.then this flag i s set + /// + public const int SR2_SN = 2; + + /// + /// Scan Equal Hit (equal) + /// Scan equal hit - During execution of the Scan command. i f the condition of “equal” is satisfied, this flag i s set + /// + public const int SR2_SH = 3; + + /// + /// Wrong Cylinder (read/programmed track-ID different) (see b1) + /// Wrong cylinder - This bit IS related to the ND bit, and when the contents of C(3) on the medium is different + /// from that stored i n the IDR.this flag is set + /// + public const int SR2_WC = 4; + + /// + /// Data Error in Data Field (CRC-fail in data-field) + /// Data error in data field - If the FDC detects a CRC error i n the data field then this flag is set + /// + public const int SR2_DD = 5; + + /// + /// Control Mark (read/scan command found sector with deleted DAM) + /// Control mark - During execution of the Read Data or Scan command, if the FDC encounters a sector + /// which contains a deleted data address mark, this flag is set Also set if DAM is + /// found during Read Deleted Data + /// + public const int SR2_CM = 6; + + // Status Register 3 Constants + // Designates the bit positions within the status register 3 + + /// + /// Unit select 0 + /// Unit Select (pin 28,29 of FDC) + /// + public const int SR3_US0 = 0; + + /// + /// Unit select 1 + /// Unit Select (pin 28,29 of FDC) + /// + public const int SR3_US1 = 1; + + /// + /// Head address (side select) + /// Head Address (pin 27 of FDC) + /// + public const int SR3_HD = 2; + + /// + /// Two Side (0=yes, 1=no (!)) + /// Two-side - This bit IS used to indicate the status of the two-side signal from the FDD + /// + public const int SR3_TS = 3; + + /// + /// Track 0 (on track 0 we are) + /// Track 0 - This bit IS used to indicate the status of the track 0 signal from the FDD + /// + public const int SR3_T0 = 4; + + /// + /// Ready - status of the ready signal from the fdd + /// Ready (drive ready signal) + /// + public const int SR3_RY = 5; + + /// + /// Write Protected (write protected) + /// Write protect - status of the wp signal from the fdd + /// + public const int SR3_WP = 6; + + /// + /// Fault - This bit is used to indicate the status of the fault signal from the FDD + /// Fault (if supported: 1=Drive failure) + /// + public const int SR3_FT = 7; + + // Interrupt Code Masks + + /// + /// 1 = aborted:readfail / OK if EN (end of track) + /// + public const byte IC_OK = 0x00; + + /// + /// 1 = aborted:readfail / OK if EN (end of track) + /// + public const byte IC_ABORTED_RF_OKEN = 0x40; + + /// + /// 2 = unknown cmd or senseint with no int occured + /// + public const byte IC_NO_INT_OCCURED = 0x80; + + /// + /// 3 = aborted:disc removed etc + /// + public const byte IC_ABORTED_DISCREMOVED = 0xC0; + + // command code constants + public const int CC_READ_DATA = 0x06; + public const int CC_READ_ID = 0x0a; + public const int CC_SPECIFY = 0x03; + public const int CC_READ_DIAGNOSTIC = 0x02; + public const int CC_SCAN_EQUAL = 0x11; + public const int CC_SCAN_HIGHOREQUAL = 0x1d; + public const int CC_SCAN_LOWOREQUAL = 0x19; + public const int CC_READ_DELETEDDATA = 0x0c; + public const int CC_WRITE_DATA = 0x05; + public const int CC_WRITE_ID = 0x0d; + public const int CC_WRITE_DELETEDDATA = 0x09; + public const int CC_SEEK = 0x0f; + public const int CC_RECALIBRATE = 0x07; + public const int CC_SENSE_INTSTATUS = 0x08; + public const int CC_SENSE_DRIVESTATUS = 0x04; + public const int CC_VERSION = 0x10; + public const int CC_INVALID = 0x00; + + // drive seek state constants + public const int SEEK_IDLE = 0; + public const int SEEK_SEEK = 1; + public const int SEEK_RECALIBRATE = 2; + // seek interrupt + public const int SEEK_INTACKNOWLEDGED = 3; + public const int SEEK_NORMALTERM = 4; + public const int SEEK_ABNORMALTERM = 5; + public const int SEEK_DRIVENOTREADY = 6; + + #endregion + + #region Classes & Structs + + /// + /// Class that holds information about a specific command + /// + private class Command + { + /// + /// Mask to remove potential parameter bits (5,6, and or 7) in order to identify the command + /// + //public int BitMask { get; set; } + /// + /// The command code after bitmask has been applied + /// + public int CommandCode { get; set; } + /// + /// The number of bytes that make up the full command + /// + public int ParameterByteCount { get; set; } + /// + /// The number of result bytes that will be generated from the command + /// + public int ResultByteCount { get; set; } + /// + /// The command direction + /// IN - Z80 to UPD765A + /// OUT - UPD765A to Z80 + /// + public CommandDirection Direction { get; set; } + /// + /// Command makes use of the MT bit + /// + public bool MT; + /// + /// Command makes use of the MF bit + /// + public bool MF; + /// + /// Command makes use of the SK bit + /// + public bool SK; + /// + /// Read/Write command that is READ + /// + public bool IsRead; + /// + /// Read/Write command that is WRITE + /// + public bool IsWrite; + + /// + /// Delegate function that is called by this command + /// bool 1: EXECUTE - if TRUE the command will be executed. if FALSE the method will instead parse commmand parameter bytes + /// bool 2: RESULT - if TRUE + /// + public Action CommandDelegate { get; set; } + } + + /// + /// Storage for command parameters + /// + public class CommandParameters + { + /// + /// The requested drive + /// + public byte UnitSelect; + /// + /// The requested physical side + /// + public byte Side; + /// + /// The requested track (C) + /// + public byte Cylinder; + /// + /// The requested head (H) + /// + public byte Head; + /// + /// The requested sector (R) + /// + public byte Sector; + /// + /// The specified sector size (N) + /// + public byte SectorSize; + /// + /// The end of track or last sector value (EOT) + /// + public byte EOT; + /// + /// Gap3 length (GPL) + /// + public byte Gap3Length; + /// + /// Data length (DTL) - When N is defined as 00, DTL stands for the data length + /// which users are going to read out or write into the sector + /// + public byte DTL; + + /// + /// Clear down + /// + public void Reset() + { + UnitSelect = 0; + Side = 0; + Cylinder = 0; + Head = 0; + Sector = 0; + SectorSize = 0; + EOT = 0; + Gap3Length = 0; + DTL = 0; + } + + public void SyncState(Serializer ser) + { + ser.BeginSection("ActiveCmdParams"); + + ser.Sync("UnitSelect", ref UnitSelect); + ser.Sync("Side", ref Side); + ser.Sync("Cylinder", ref Cylinder); + ser.Sync("Head", ref Head); + ser.Sync("Sector", ref Sector); + ser.Sync("SectorSize", ref SectorSize); + ser.Sync("EOT", ref EOT); + ser.Sync("Gap3Length", ref Gap3Length); + ser.Sync("DTL", ref DTL); + + ser.EndSection(); + } + } + + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.FDC.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.FDC.cs new file mode 100644 index 0000000000..54510a10fe --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.FDC.cs @@ -0,0 +1,2841 @@ +using BizHawk.Common.NumberExtensions; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// FDC State and Methods + /// + #region Attribution + /* + Implementation based on the information contained here: + http://www.cpcwiki.eu/index.php/765_FDC + and here: + http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf + */ + #endregion + public partial class NECUPD765 + { + #region Controller State + + /// + /// Signs whether the drive is active + /// + public bool DriveLight; + + /// + /// Collection of possible commands + /// + private List CommandList; + + /// + /// State parameters relating to the Active command + /// + public CommandParameters ActiveCommandParams = new CommandParameters(); + + /// + /// The current active phase of the controller + /// + private Phase ActivePhase = Phase.Command; + + /// + /// The currently active interrupt + /// + private InterruptState ActiveInterrupt = InterruptState.None; + /// + /// Command buffer + /// This does not contain the initial command byte (only parameter bytes) + /// + private byte[] CommBuffer = new byte[9]; + + /// + /// Current index within the command buffer + /// + private int CommCounter = 0; + + /// + /// Initial command byte flag + /// Bit7 Multi Track (continue multi-sector-function on other head) + /// + private bool CMD_FLAG_MT; + + /// + /// Initial command byte flag + /// Bit6 MFM-Mode-Bit (Default 1=Double Density) + /// + private bool CMD_FLAG_MF; + + /// + /// Initial command byte flag + /// Bit5 Skip-Bit (set if secs with deleted DAM shall be skipped) + /// + private bool CMD_FLAG_SK; + + /// + /// Step Rate Time (supplied via the specify command) + /// SRT stands for the steooino rate for the FDD ( 1 to 16 ms in 1 ms increments). + /// Stepping rate applies to all drives(FH= 1ms, EH= 2ms, etc.). + /// + private int SRT; + + /// + /// Keeps track of the current SRT state + /// + private int SRT_Counter; + + /// + /// Head Unload Time (supplied via the specify command) + /// HUT stands for the head unload time after a Read or Write operation has occurred + /// (16 to 240 ms in 16 ms Increments) + /// + private int HUT; + + /// + /// Keeps track of the current HUT state + /// + private int HUT_Counter; + + /// + /// Head load Time (supplied via the specify command) + /// HLT stands for the head load time in the FDD (2 to 254 ms in 2 ms Increments) + /// + private int HLT; + + /// + /// Keeps track of the current HLT state + /// + private int HLT_Counter; + + /// + /// Non-DMA Mode (supplied via the specify command) + /// ND stands for operation in the non-DMA mode + /// + private bool ND; + + /// + /// In lieu of actual timing, this will count status reads in execution phase + /// where the CPU hasnt actually read any bytes + /// + private int OverrunCounter; + + /// + /// Contains result bytes in result phase + /// + private byte[] ResBuffer = new byte[7]; + + /// + /// Contains sector data to be written/read in execution phase + /// + private byte[] ExecBuffer = new byte[0x8000]; + + /// + /// Interrupt result buffer + /// Persists (and returns when needed) the last result data when a sense interrupt status command happens + /// + private byte[] InterruptResultBuffer = new byte[2]; + + /// + /// Current index within the result buffer + /// + private int ResCounter = 0; + + /// + /// The byte length of the currently active command + /// This may or may not be the same as the actual command resultbytes value + /// + private int ResLength = 0; + + /// + /// Index for sector data within the result buffer + /// + private int ExecCounter = 0; + + /// + /// The length of the current exec command + /// + private int ExecLength = 0; + + /// + /// The last write byte that was received during execution phase + /// + private byte LastSectorDataWriteByte = 0; + + /// + /// The last read byte to be sent during execution phase + /// + private byte LastSectorDataReadByte = 0; + + /// + /// The last parameter byte that was written to the FDC + /// + private byte LastByteReceived = 0; + + /// + /// Delay for reading sector + /// + private int SectorDelayCounter = 0; + + /// + /// The phyical sector ID + /// + private int SectorID = 0; + + /// + /// Counter for index pulses + /// + private int IndexPulseCounter; + + /// + /// Specifies the index of the currently selected command (in the CommandList) + /// + public int CMDIndex + { + get { return _cmdIndex; } + set + { + _cmdIndex = value; + ActiveCommand = CommandList[_cmdIndex]; + } + } + private int _cmdIndex; + + /// + /// The currently active command + /// + private Command ActiveCommand; + + /// + /// Main status register (accessed via reads to port 0x2ffd) + /// + /* + b0..3 DB FDD0..3 Busy (seek/recalib active, until succesful sense intstat) + b4 CB FDC Busy (still in command-, execution- or result-phase) + b5 EXM Execution Mode (still in execution-phase, non_DMA_only) + b6 DIO Data Input/Output (0=CPU->FDC, 1=FDC->CPU) (see b7) + b7 RQM Request For Master (1=ready for next byte) (see b6 for direction) + */ + private byte StatusMain; + + /// + /// Status Register 0 + /// + /* + b0,1 US Unit Select (driveno during interrupt) + b2 HD Head Address (head during interrupt) + b3 NR Not Ready (drive not ready or non-existing 2nd head selected) + b4 EC Equipment Check (drive failure or recalibrate failed (retry)) + b5 SE Seek End (Set if seek-command completed) + b6,7 IC Interrupt Code (0=OK, 1=aborted:readfail/OK if EN, 2=unknown cmd + or senseint with no int occured, 3=aborted:disc removed etc.) + */ + private byte Status0; + + /// + /// Status Register 1 + /// + /* + b0 MA Missing Address Mark (Sector_ID or DAM not found) + b1 NW Not Writeable (tried to write/format disc with wprot_tab=on) + b2 ND No Data (Sector_ID not found, CRC fail in ID_field) + b3,6 0 Not used + b4 OR Over Run (CPU too slow in execution-phase (ca. 26us/Byte)) + b5 DE Data Error (CRC-fail in ID- or Data-Field) + b7 EN End of Track (set past most read/write commands) (see IC) + */ + private byte Status1; + + /// + /// Status Register 2 + /// + /* + b0 MD Missing Address Mark in Data Field (DAM not found) + b1 BC Bad Cylinder (read/programmed track-ID different and read-ID = FF) + b2 SN Scan Not Satisfied (no fitting sector found) + b3 SH Scan Equal Hit (equal) + b4 WC Wrong Cylinder (read/programmed track-ID different) (see b1) + b5 DD Data Error in Data Field (CRC-fail in data-field) + b6 CM Control Mark (read/scan command found sector with deleted DAM) + b7 0 Not Used + */ + private byte Status2; + + /// + /// Status Register 3 + /// + /* + b0,1 US Unit Select (pin 28,29 of FDC) + b2 HD Head Address (pin 27 of FDC) + b3 TS Two Side (0=yes, 1=no (!)) + b4 T0 Track 0 (on track 0 we are) + b5 RY Ready (drive ready signal) + b6 WP Write Protected (write protected) + b7 FT Fault (if supported: 1=Drive failure) + */ + private byte Status3; + + #endregion + + #region UPD Internal Functions + + #region READ Commands + + /// + /// Read Data + /// COMMAND: 8 parameter bytes + /// EXECUTION: Data transfer between FDD and FDC + /// RESULT: 7 result bytes + /// + private void UPD_ReadData() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + ParseParamByteStandard(CommCounter); + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + // all parameter bytes received - setup for execution phase + + // clear exec buffer and status registers + ClearExecBuffer(); + Status0 = 0; + Status1 = 0; + Status2 = 0; + Status3 = 0; + + // temp sector index + byte secIdx = ActiveCommandParams.Sector; + + // hack for when another drive (non-existent) is being called + if (ActiveDrive.ID != 0) + DiskDriveIndex = 0; + + // do we have a valid disk inserted? + if (!ActiveDrive.FLAG_READY) + { + // no disk, no tracks or motor is not on + SetBit(SR0_IC0, ref Status0); + SetBit(SR0_NR, ref Status0); + + CommitResultCHRN(); + CommitResultStatus(); + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + + int buffPos = 0; + int sectorSize = 0; + int maxTransferCap = 0; + + // calculate requested size of data required + if (ActiveCommandParams.SectorSize == 0) + { + // When N=0, then DTL defines the data length which the FDC must treat as a sector. If DTL is smaller than the actual + // data length in a sector, the data beyond DTL in the sector is not sent to the Data Bus. The FDC reads (internally) + // the complete sector performing the CRC check and, depending upon the manner of command termination, may perform + // a Multi-Sector Read Operation. + sectorSize = ActiveCommandParams.DTL; + + // calculate maximum transfer capacity + if (!CMD_FLAG_MF) + maxTransferCap = 3328; + } + else + { + // When N is non - zero, then DTL has no meaning and should be set to ffh + ActiveCommandParams.DTL = 0xFF; + + // calculate maximum transfer capacity + switch (ActiveCommandParams.SectorSize) + { + case 1: + if (CMD_FLAG_MF) + maxTransferCap = 6656; + else + maxTransferCap = 3840; + break; + case 2: + if (CMD_FLAG_MF) + maxTransferCap = 7680; + else + maxTransferCap = 4096; + break; + case 3: + if (CMD_FLAG_MF) + maxTransferCap = 8192; + else + maxTransferCap = 4096; + break; + } + + sectorSize = 0x80 << ActiveCommandParams.SectorSize; + } + + // get the current track + var track = ActiveDrive.Disk.DiskTracks.Where(a => a.TrackNumber == ActiveDrive.CurrentTrackID).FirstOrDefault(); + + if (track == null || track.NumberOfSectors <= 0) + { + // track could not be found + SetBit(SR0_IC0, ref Status0); + SetBit(SR0_NR, ref Status0); + + CommitResultCHRN(); + CommitResultStatus(); + + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + + FloppyDisk.Sector sector = null; + + // sector read loop + for (;;) + { + bool terminate = false; + + // lookup the sector + sector = GetSector(); + + if (sector == null) + { + // sector was not found after two passes of the disk index hole + SetBit(SR1_ND, ref Status1); + SetBit(SR0_IC0, ref Status0); + UnSetBit(SR0_IC1, ref Status0); + + // result requires the actual track id, rather than the sector track id + ActiveCommandParams.Cylinder = track.TrackNumber; + + CommitResultCHRN(); + CommitResultStatus(); + ActivePhase = Phase.Result; + break; + } + + // sector ID was found on this track + + // get status regs from sector + Status1 = sector.Status1; + Status2 = sector.Status2; + + // we dont need EN + UnSetBit(SR1_EN, ref Status1); + + // If SK=1, the FDC skips the sector with the Deleted Data Address Mark and reads the next sector. + // The CRC bits in the deleted data field are not checked when SK=1 + if (CMD_FLAG_SK && Status2.Bit(SR2_CM)) + { + if (ActiveCommandParams.Sector != ActiveCommandParams.EOT) + { + // increment the sector ID and search again + ActiveCommandParams.Sector++; + continue; + } + else + { + // no execution phase + SetBit(SR0_IC0, ref Status0); + UnSetBit(SR0_IC1, ref Status0); + + // result requires the actual track id, rather than the sector track id + ActiveCommandParams.Cylinder = track.TrackNumber; + + CommitResultCHRN(); + CommitResultStatus(); + ActivePhase = Phase.Result; + break; + } + } + + // read the sector + for (int i = 0; i < sector.DataLen; i++) + { + ExecBuffer[buffPos++] = sector.ActualData[i]; + } + + // mark the sector read + sector.SectorReadCompleted(); + + // any CRC errors? + if (Status1.Bit(SR1_DE) || Status2.Bit(SR2_DD)) + { + SetBit(SR0_IC0, ref Status0); + UnSetBit(SR0_IC1, ref Status0); + terminate = true; + } + + if (!CMD_FLAG_SK && Status2.Bit(SR2_CM)) + { + // deleted address mark was detected with NO skip flag set + ActiveCommandParams.EOT = ActiveCommandParams.Sector; + SetBit(SR2_CM, ref Status2); + SetBit(SR0_IC0, ref Status0); + UnSetBit(SR0_IC1, ref Status0); + terminate = true; + } + + if (sector.SectorID == ActiveCommandParams.EOT || terminate) + { + // this was the last sector to read + // or termination requested + + SetBit(SR1_EN, ref Status1); + + int keyIndex = 0; + for (int i = 0; i < track.Sectors.Length; i++) + { + if (track.Sectors[i].SectorID == sector.SectorID) + { + keyIndex = i; + break; + } + } + + if (keyIndex == track.Sectors.Length - 1) + { + // last sector on the cylinder, set EN + SetBit(SR1_EN, ref Status1); + + // increment cylinder + ActiveCommandParams.Cylinder++; + + // reset sector + ActiveCommandParams.Sector = sector.SectorID; // 1; + ActiveDrive.SectorIndex = 0; + } + else + { + ActiveDrive.SectorIndex++; + } + + UnSetBit(SR0_IC1, ref Status0); + if (terminate) + SetBit(SR0_IC0, ref Status0); + else + UnSetBit(SR0_IC0, ref Status0); + + SetBit(SR0_IC0, ref Status0); + + // result requires the actual track id, rather than the sector track id + ActiveCommandParams.Cylinder = track.TrackNumber; + + CommitResultCHRN(); + CommitResultStatus(); + ActivePhase = Phase.Execution; + break; + } + else + { + // continue with multi-sector read operation + ActiveCommandParams.Sector++; + //ActiveDrive.SectorIndex++; + } + } + + if (ActivePhase == Phase.Execution) + { + ExecLength = buffPos; + ExecCounter = buffPos; + + DriveLight = true; + } + } + + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + + var index = ExecLength - ExecCounter; + + LastSectorDataReadByte = ExecBuffer[index]; + + OverrunCounter--; + ExecCounter--; + + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Read Deleted Data + /// COMMAND: 8 parameter bytes + /// EXECUTION: Data transfer between the FDD and FDC + /// RESULT: 7 result bytes + /// + private void UPD_ReadDeletedData() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + ParseParamByteStandard(CommCounter); + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + // all parameter bytes received - setup for execution phase + + // clear exec buffer and status registers + ClearExecBuffer(); + Status0 = 0; + Status1 = 0; + Status2 = 0; + Status3 = 0; + + // temp sector index + byte secIdx = ActiveCommandParams.Sector; + + // do we have a valid disk inserted? + if (!ActiveDrive.FLAG_READY) + { + // no disk, no tracks or motor is not on + SetBit(SR0_IC0, ref Status0); + SetBit(SR0_NR, ref Status0); + + CommitResultCHRN(); + CommitResultStatus(); + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + + int buffPos = 0; + int sectorSize = 0; + int maxTransferCap = 0; + + // calculate requested size of data required + if (ActiveCommandParams.SectorSize == 0) + { + // When N=0, then DTL defines the data length which the FDC must treat as a sector. If DTL is smaller than the actual + // data length in a sector, the data beyond DTL in the sector is not sent to the Data Bus. The FDC reads (internally) + // the complete sector performing the CRC check and, depending upon the manner of command termination, may perform + // a Multi-Sector Read Operation. + sectorSize = ActiveCommandParams.DTL; + + // calculate maximum transfer capacity + if (!CMD_FLAG_MF) + maxTransferCap = 3328; + } + else + { + // When N is non - zero, then DTL has no meaning and should be set to ffh + ActiveCommandParams.DTL = 0xFF; + + // calculate maximum transfer capacity + switch (ActiveCommandParams.SectorSize) + { + case 1: + if (CMD_FLAG_MF) + maxTransferCap = 6656; + else + maxTransferCap = 3840; + break; + case 2: + if (CMD_FLAG_MF) + maxTransferCap = 7680; + else + maxTransferCap = 4096; + break; + case 3: + if (CMD_FLAG_MF) + maxTransferCap = 8192; + else + maxTransferCap = 4096; + break; + } + + sectorSize = 0x80 << ActiveCommandParams.SectorSize; + } + + // get the current track + var track = ActiveDrive.Disk.DiskTracks.Where(a => a.TrackNumber == ActiveDrive.CurrentTrackID).FirstOrDefault(); + + if (track == null || track.NumberOfSectors <= 0) + { + // track could not be found + SetBit(SR0_IC0, ref Status0); + SetBit(SR0_NR, ref Status0); + + CommitResultCHRN(); + CommitResultStatus(); + + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + + FloppyDisk.Sector sector = null; + + // sector read loop + for (;;) + { + bool terminate = false; + + // lookup the sector + sector = GetSector(); + + if (sector == null) + { + // sector was not found after two passes of the disk index hole + SetBit(SR1_ND, ref Status1); + SetBit(SR0_IC0, ref Status0); + UnSetBit(SR0_IC1, ref Status0); + + // result requires the actual track id, rather than the sector track id + ActiveCommandParams.Cylinder = track.TrackNumber; + + CommitResultCHRN(); + CommitResultStatus(); + ActivePhase = Phase.Result; + break; + } + + // sector ID was found on this track + + // get status regs from sector + Status1 = sector.Status1; + Status2 = sector.Status2; + + // we dont need EN + UnSetBit(SR1_EN, ref Status1); + + // invert CM for read deleted data command + if (Status2.Bit(SR2_CM)) + UnSetBit(SR2_CM, ref Status2); + else + SetBit(SR2_CM, ref Status2); + + // skip flag is set and no DAM found + if (CMD_FLAG_SK && Status2.Bit(SR2_CM)) + { + if (ActiveCommandParams.Sector != ActiveCommandParams.EOT) + { + // increment the sector ID and search again + ActiveCommandParams.Sector++; + continue; + } + else + { + // no execution phase + SetBit(SR0_IC0, ref Status0); + UnSetBit(SR0_IC1, ref Status0); + + // result requires the actual track id, rather than the sector track id + ActiveCommandParams.Cylinder = track.TrackNumber; + + CommitResultCHRN(); + CommitResultStatus(); + ActivePhase = Phase.Result; + break; + } + } + // we can read this sector + else + { + // if DAM is not set this will be the last sector to read + if (Status2.Bit(SR2_CM)) + { + ActiveCommandParams.EOT = ActiveCommandParams.Sector; + } + + if (!CMD_FLAG_SK && !Status2.Bit(SR2_CM) && + ActiveDrive.Disk.Protection == ProtectionType.PaulOwens) + { + ActiveCommandParams.EOT = ActiveCommandParams.Sector; + SetBit(SR2_CM, ref Status2); + SetBit(SR0_IC0, ref Status0); + UnSetBit(SR0_IC1, ref Status0); + terminate = true; + } + + // read the sector + for (int i = 0; i < sectorSize; i++) + { + ExecBuffer[buffPos++] = sector.ActualData[i]; + } + + // mark the sector read + sector.SectorReadCompleted(); + + if (sector.SectorID == ActiveCommandParams.EOT) + { + // this was the last sector to read + + SetBit(SR1_EN, ref Status1); + + int keyIndex = 0; + for (int i = 0; i < track.Sectors.Length; i++) + { + if (track.Sectors[i].SectorID == sector.SectorID) + { + keyIndex = i; + break; + } + } + + if (keyIndex == track.Sectors.Length - 1) + { + // last sector on the cylinder, set EN + SetBit(SR1_EN, ref Status1); + + // increment cylinder + ActiveCommandParams.Cylinder++; + + // reset sector + ActiveCommandParams.Sector = 1; + ActiveDrive.SectorIndex = 0; + } + else + { + ActiveDrive.SectorIndex++; + } + + UnSetBit(SR0_IC1, ref Status0); + if (terminate) + SetBit(SR0_IC0, ref Status0); + else + UnSetBit(SR0_IC0, ref Status0); + + SetBit(SR0_IC0, ref Status0); + + // result requires the actual track id, rather than the sector track id + ActiveCommandParams.Cylinder = track.TrackNumber; + + // remove CM (appears to be required to defeat Alkatraz copy protection) + UnSetBit(SR2_CM, ref Status2); + + CommitResultCHRN(); + CommitResultStatus(); + ActivePhase = Phase.Execution; + break; + } + else + { + // continue with multi-sector read operation + ActiveCommandParams.Sector++; + //ActiveDrive.SectorIndex++; + } + } + } + + if (ActivePhase == Phase.Execution) + { + ExecLength = buffPos; + ExecCounter = buffPos; + DriveLight = true; + } + } + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + var index = ExecLength - ExecCounter; + + LastSectorDataReadByte = ExecBuffer[index]; + + OverrunCounter--; + ExecCounter--; + + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Read Diagnostic (read track) + /// COMMAND: 8 parameter bytes + /// EXECUTION: Data transfer between FDD and FDC. FDC reads all data fields from index hole to EDT + /// RESULT: 7 result bytes + /// + private void UPD_ReadDiagnostic() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + ParseParamByteStandard(CommCounter); + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + // all parameter bytes received - setup for execution phase + + // clear exec buffer and status registers + ClearExecBuffer(); + Status0 = 0; + Status1 = 0; + Status2 = 0; + Status3 = 0; + + // temp sector index + byte secIdx = ActiveCommandParams.Sector; + + // do we have a valid disk inserted? + if (!ActiveDrive.FLAG_READY) + { + // no disk, no tracks or motor is not on + SetBit(SR0_IC0, ref Status0); + SetBit(SR0_NR, ref Status0); + + CommitResultCHRN(); + CommitResultStatus(); + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + + int buffPos = 0; + int sectorSize = 0; + int maxTransferCap = 0; + + // calculate requested size of data required + if (ActiveCommandParams.SectorSize == 0) + { + // When N=0, then DTL defines the data length which the FDC must treat as a sector. If DTL is smaller than the actual + // data length in a sector, the data beyond DTL in the sector is not sent to the Data Bus. The FDC reads (internally) + // the complete sector performing the CRC check and, depending upon the manner of command termination, may perform + // a Multi-Sector Read Operation. + sectorSize = ActiveCommandParams.DTL; + + // calculate maximum transfer capacity + if (!CMD_FLAG_MF) + maxTransferCap = 3328; + } + else + { + // When N is non - zero, then DTL has no meaning and should be set to ffh + ActiveCommandParams.DTL = 0xFF; + + // calculate maximum transfer capacity + switch (ActiveCommandParams.SectorSize) + { + case 1: + if (CMD_FLAG_MF) + maxTransferCap = 6656; + else + maxTransferCap = 3840; + break; + case 2: + if (CMD_FLAG_MF) + maxTransferCap = 7680; + else + maxTransferCap = 4096; + break; + case 3: + if (CMD_FLAG_MF) + maxTransferCap = 8192; + else + maxTransferCap = 4096; + break; + } + + sectorSize = 0x80 << ActiveCommandParams.SectorSize; + } + + // get the current track + var track = ActiveDrive.Disk.DiskTracks.Where(a => a.TrackNumber == ActiveDrive.CurrentTrackID).FirstOrDefault(); + + if (track == null || track.NumberOfSectors <= 0) + { + // track could not be found + SetBit(SR0_IC0, ref Status0); + SetBit(SR0_NR, ref Status0); + + CommitResultCHRN(); + CommitResultStatus(); + + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + + FloppyDisk.Sector sector = null; + ActiveDrive.SectorIndex = 0; + + int secCount = 0; + + // read the whole track + for (int i = 0; i < track.Sectors.Length; i++) + { + if (secCount >= ActiveCommandParams.EOT) + { + break; + } + + var sec = track.Sectors[i]; + for (int b = 0; b < sec.ActualData.Length; b++) + { + ExecBuffer[buffPos++] = sec.ActualData[b]; + } + + // mark the sector read + sec.SectorReadCompleted(); + + // end of sector - compare IDs + if (sec.TrackNumber != ActiveCommandParams.Cylinder || + sec.SideNumber != ActiveCommandParams.Head || + sec.SectorID != ActiveCommandParams.Sector || + sec.SectorSize != ActiveCommandParams.SectorSize) + { + SetBit(SR1_ND, ref Status1); + } + + secCount++; + ActiveDrive.SectorIndex = i; + } + + if (secCount == ActiveCommandParams.EOT) + { + // this was the last sector to read + // or termination requested + + int keyIndex = 0; + for (int i = 0; i < track.Sectors.Length; i++) + { + if (track.Sectors[i].SectorID == track.Sectors[ActiveDrive.SectorIndex].SectorID) + { + keyIndex = i; + break; + } + } + + if (keyIndex == track.Sectors.Length - 1) + { + // last sector on the cylinder, set EN + SetBit(SR1_EN, ref Status1); + + // increment cylinder + ActiveCommandParams.Cylinder++; + + // reset sector + ActiveCommandParams.Sector = 1; + ActiveDrive.SectorIndex = 0; + } + else + { + ActiveDrive.SectorIndex++; + } + + UnSetBit(SR0_IC1, ref Status0); + UnSetBit(SR0_IC0, ref Status0); + + CommitResultCHRN(); + CommitResultStatus(); + ActivePhase = Phase.Execution; + } + + if (ActivePhase == Phase.Execution) + { + ExecLength = buffPos; + ExecCounter = buffPos; + + DriveLight = true; + } + } + + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + + var index = ExecLength - ExecCounter; + + LastSectorDataReadByte = ExecBuffer[index]; + + OverrunCounter--; + ExecCounter--; + + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Read ID + /// COMMAND: 1 parameter byte + /// EXECUTION: The first correct ID information on the cylinder is stored in the data register + /// RESULT: 7 result bytes + /// + private void UPD_ReadID() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + ParseParamByteStandard(CommCounter); + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + DriveLight = true; + + // all parameter bytes received + ClearResultBuffer(); + Status0 = 0; + Status1 = 0; + Status2 = 0; + Status3 = 0; + + // set unit select + //SetUnitSelect(ActiveDrive.ID, ref Status0); + + // HD should always be 0 + UnSetBit(SR0_HD, ref Status0); + + if (!ActiveDrive.FLAG_READY) + { + // no disk, no tracks or motor is not on + // it is at this point the +3 detects whether a disk is present + // if not (and after another readid and SIS) it will eventually proceed to loading from tape + SetBit(SR0_IC0, ref Status0); + SetBit(SR0_NR, ref Status0); + + // setup the result buffer + ResBuffer[RS_ST0] = Status0; + for (int i = 1; i < 7; i++) + ResBuffer[i] = 0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + + var track = ActiveDrive.Disk.DiskTracks.Where(a => a.TrackNumber == ActiveDrive.CurrentTrackID).FirstOrDefault(); + + if (track != null && track.NumberOfSectors > 0 && track.TrackNumber != 0xff) + { + // formatted track + + // is the index out of bounds? + if (ActiveDrive.SectorIndex >= track.NumberOfSectors) + { + // reset the index + ActiveDrive.SectorIndex = 0; + } + + if (ActiveDrive.SectorIndex == 0 && ActiveDrive.Disk.DiskTracks[ActiveDrive.CurrentTrackID].Sectors.Length > 1) + { + // looks like readid always skips the first sector on a track + ActiveDrive.SectorIndex++; + } + + // read the sector data + var data = track.Sectors[ActiveDrive.SectorIndex]; //.GetCHRN(); + ResBuffer[RS_C] = data.TrackNumber; + ResBuffer[RS_H] = data.SideNumber; + ResBuffer[RS_R] = data.SectorID; + ResBuffer[RS_N] = data.SectorSize; + + ResBuffer[RS_ST0] = Status0; + + // check for DAM & CRC + //if (data.Status2.Bit(SR2_CM)) + //SetBit(SR2_CM, ref ResBuffer[RS_ST2]); + + + // increment the current sector + ActiveDrive.SectorIndex++; + + // is the index out of bounds? + if (ActiveDrive.SectorIndex >= track.NumberOfSectors) + { + // reset the index + ActiveDrive.SectorIndex = 0; + } + } + else + { + // unformatted track? + CommitResultCHRN(); + + SetBit(SR0_IC0, ref Status0); + ResBuffer[RS_ST0] = Status0; + ResBuffer[RS_ST1] = 0x01; + } + + ActivePhase = Phase.Result; + } + + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + #endregion + + #region WRITE Commands + + /// + /// Write Data + /// COMMAND: 8 parameter bytes + /// EXECUTION: Data transfer between FDC and FDD + /// RESULT: 7 result bytes + /// + private void UPD_WriteData() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + ParseParamByteStandard(CommCounter); + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + // all parameter bytes received - setup for execution phase + + // clear exec buffer and status registers + ClearExecBuffer(); + Status0 = 0; + Status1 = 0; + Status2 = 0; + Status3 = 0; + + // temp sector index + byte secIdx = ActiveCommandParams.Sector; + + // hack for when another drive (non-existent) is being called + if (ActiveDrive.ID != 0) + DiskDriveIndex = 0; + + // do we have a valid disk inserted? + if (!ActiveDrive.FLAG_READY) + { + // no disk, no tracks or motor is not on + SetBit(SR0_IC0, ref Status0); + SetBit(SR0_NR, ref Status0); + + CommitResultCHRN(); + CommitResultStatus(); + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + + // check write protect tab + if (ActiveDrive.FLAG_WRITEPROTECT) + { + SetBit(SR0_IC0, ref Status0); + SetBit(SR1_NW, ref Status1); + + CommitResultCHRN(); + CommitResultStatus(); + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + else + { + + // calculate the number of bytes to write + int byteCounter = 0; + byte startSecID = ActiveCommandParams.Sector; + byte endSecID = ActiveCommandParams.EOT; + bool lastSec = false; + + // get the first sector + var track = ActiveDrive.Disk.DiskTracks[ActiveCommandParams.Cylinder]; + int secIndex = 0; + for (int s = 0; s < track.Sectors.Length; s++) + { + if (track.Sectors[s].SectorID == endSecID) + lastSec = true; + + for (int i = 0; i < 0x80 << ActiveCommandParams.SectorSize; i++) + { + byteCounter++; + + if (i == (0x80 << ActiveCommandParams.SectorSize) - 1 && lastSec) + { + break; + } + } + + if (lastSec) + break; + } + + ExecCounter = byteCounter; + ExecLength = byteCounter; + ActivePhase = Phase.Execution; + DriveLight = true; + break; + } + } + + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + + var index = ExecLength - ExecCounter; + + ExecBuffer[index] = LastSectorDataWriteByte; + + OverrunCounter--; + ExecCounter--; + + if (ExecCounter <= 0) + { + int cnt = 0; + + // all data received + byte startSecID = ActiveCommandParams.Sector; + byte endSecID = ActiveCommandParams.EOT; + bool lastSec = false; + var track = ActiveDrive.Disk.DiskTracks[ActiveCommandParams.Cylinder]; + int secIndex = 0; + + for (int s = 0; s < track.Sectors.Length; s++) + { + if (cnt == ExecLength) + break; + + ActiveCommandParams.Sector = track.Sectors[s].SectorID; + + if (track.Sectors[s].SectorID == endSecID) + lastSec = true; + + int size = 0x80 << track.Sectors[s].SectorSize; + + for (int d = 0; d < size; d++) + { + track.Sectors[s].SectorData[d] = ExecBuffer[cnt++]; + } + + if (lastSec) + break; + } + + SetBit(SR0_IC0, ref Status0); + SetBit(SR1_EN, ref Status1); + + CommitResultCHRN(); + CommitResultStatus(); + } + + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Write ID (format write) + /// COMMAND: 5 parameter bytes + /// EXECUTION: Entire track is formatted + /// RESULT: 7 result bytes + /// + private void UPD_WriteID() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + ParseParamByteStandard(CommCounter); + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + // all parameter bytes received - setup for execution phase + DriveLight = true; + + // clear exec buffer and status registers + ClearExecBuffer(); + Status0 = 0; + Status1 = 0; + Status2 = 0; + Status3 = 0; + + // temp sector index + byte secIdx = ActiveCommandParams.Sector; + + // hack for when another drive (non-existent) is being called + if (ActiveDrive.ID != 0) + DiskDriveIndex = 0; + + // do we have a valid disk inserted? + if (!ActiveDrive.FLAG_READY) + { + // no disk, no tracks or motor is not on + SetBit(SR0_IC0, ref Status0); + SetBit(SR0_NR, ref Status0); + + CommitResultCHRN(); + CommitResultStatus(); + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + + // check write protect tab + if (ActiveDrive.FLAG_WRITEPROTECT) + { + SetBit(SR0_IC0, ref Status0); + SetBit(SR1_NW, ref Status1); + + CommitResultCHRN(); + CommitResultStatus(); + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + else + { + // not implemented yet + SetBit(SR0_IC0, ref Status0); + SetBit(SR1_NW, ref Status1); + + CommitResultCHRN(); + CommitResultStatus(); + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + } + + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Write Deleted Data + /// COMMAND: 8 parameter bytes + /// EXECUTION: Data transfer between FDC and FDD + /// RESULT: 7 result bytes + /// + private void UPD_WriteDeletedData() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + ParseParamByteStandard(CommCounter); + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + // all parameter bytes received - setup for execution phase + + // clear exec buffer and status registers + ClearExecBuffer(); + Status0 = 0; + Status1 = 0; + Status2 = 0; + Status3 = 0; + + // temp sector index + byte secIdx = ActiveCommandParams.Sector; + + // hack for when another drive (non-existent) is being called + if (ActiveDrive.ID != 0) + DiskDriveIndex = 0; + + // do we have a valid disk inserted? + if (!ActiveDrive.FLAG_READY) + { + // no disk, no tracks or motor is not on + SetBit(SR0_IC0, ref Status0); + SetBit(SR0_NR, ref Status0); + + CommitResultCHRN(); + CommitResultStatus(); + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + + // check write protect tab + if (ActiveDrive.FLAG_WRITEPROTECT) + { + SetBit(SR0_IC0, ref Status0); + SetBit(SR1_NW, ref Status1); + + CommitResultCHRN(); + CommitResultStatus(); + //ResBuffer[RS_ST0] = Status0; + + // move to result phase + ActivePhase = Phase.Result; + break; + } + else + { + + // calculate the number of bytes to write + int byteCounter = 0; + byte startSecID = ActiveCommandParams.Sector; + byte endSecID = ActiveCommandParams.EOT; + bool lastSec = false; + + // get the first sector + var track = ActiveDrive.Disk.DiskTracks[ActiveCommandParams.Cylinder]; + int secIndex = 0; + for (int s = 0; s < track.Sectors.Length; s++) + { + if (track.Sectors[s].SectorID == endSecID) + lastSec = true; + + for (int i = 0; i < 0x80 << ActiveCommandParams.SectorSize; i++) + { + byteCounter++; + + if (i == (0x80 << ActiveCommandParams.SectorSize) - 1 && lastSec) + { + break; + } + } + + if (lastSec) + break; + } + + ExecCounter = byteCounter; + ExecLength = byteCounter; + ActivePhase = Phase.Execution; + DriveLight = true; + break; + } + } + + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + + var index = ExecLength - ExecCounter; + + ExecBuffer[index] = LastSectorDataWriteByte; + + OverrunCounter--; + ExecCounter--; + + if (ExecCounter <= 0) + { + int cnt = 0; + + // all data received + byte startSecID = ActiveCommandParams.Sector; + byte endSecID = ActiveCommandParams.EOT; + bool lastSec = false; + var track = ActiveDrive.Disk.DiskTracks[ActiveCommandParams.Cylinder]; + int secIndex = 0; + + for (int s = 0; s < track.Sectors.Length; s++) + { + if (cnt == ExecLength) + break; + + ActiveCommandParams.Sector = track.Sectors[s].SectorID; + + if (track.Sectors[s].SectorID == endSecID) + lastSec = true; + + int size = 0x80 << track.Sectors[s].SectorSize; + + for (int d = 0; d < size; d++) + { + track.Sectors[s].SectorData[d] = ExecBuffer[cnt++]; + } + + if (lastSec) + break; + } + + SetBit(SR0_IC0, ref Status0); + SetBit(SR1_EN, ref Status1); + + CommitResultCHRN(); + CommitResultStatus(); + } + + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + #endregion + + #region SCAN Commands + + /// + /// Scan Equal + /// COMMAND: 8 parameter bytes + /// EXECUTION: Data compared between the FDD and FDC + /// RESULT: 7 result bytes + /// + private void UPD_ScanEqual() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Scan Low or Equal + /// COMMAND: 8 parameter bytes + /// EXECUTION: Data compared between the FDD and FDC + /// RESULT: 7 result bytes + /// + private void UPD_ScanLowOrEqual() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Scan High or Equal + /// COMMAND: 8 parameter bytes + /// EXECUTION: Data compared between the FDD and FDC + /// RESULT: 7 result bytes + /// + private void UPD_ScanHighOrEqual() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + #endregion + + #region OTHER Commands + + /// + /// Specify + /// COMMAND: 2 parameter bytes + /// EXECUTION: NO execution phase + /// RESULT: NO result phase + /// + /// Looks like specify command returns status 0x80 throughout its lifecycle + /// so CB is NOT set + /// + private void UPD_Specify() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + byte currByte = CommBuffer[CommCounter]; + BitArray bi = new BitArray(new byte[] { currByte }); + + switch (CommCounter) + { + // SRT & HUT + case 0: + SRT = 16 - (currByte >> 4) & 0x0f; + HUT = (currByte & 0x0f) << 4; + if (HUT == 0) + { + HUT = 255; + } + break; + // HLT & ND + case 1: + if (bi[0]) + ND = true; + else + ND = false; + + HLT = currByte & 0xfe; + if (HLT == 0) + { + HLT = 255; + } + break; + } + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + // all parameter bytes received + ActivePhase = Phase.Idle; + } + + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Seek + /// COMMAND: 2 parameter bytes + /// EXECUTION: Head is positioned over proper cylinder on disk + /// RESULT: NO result phase + /// + private void UPD_Seek() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + byte currByte = CommBuffer[CommCounter]; + switch (CommCounter) + { + case 0: + ParseParamByteStandard(CommCounter); + break; + case 1: + ActiveDrive.SeekingTrack = currByte; + break; + } + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + // all parameter bytes received + DriveLight = true; + ActivePhase = Phase.Execution; + ActiveCommand.CommandDelegate(); + } + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + // set seek flag + ActiveDrive.SeekStatus = SEEK_SEEK; + + if (ActiveDrive.CurrentTrackID == CommBuffer[CM_C]) + { + // we are already on the correct track + ActiveDrive.SectorIndex = 0; + } + else + { + // immediate seek + ActiveDrive.CurrentTrackID = CommBuffer[CM_C]; + + ActiveDrive.SectorIndex = 0; + + if (ActiveDrive.Disk.DiskTracks[ActiveDrive.CurrentTrackID].Sectors.Length > 1) + { + // always read the first sector + //ActiveDrive.SectorIndex++; + } + } + + // skip execution mode and go directly to idle + // result is determined by SIS command + ActivePhase = Phase.Idle; + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Recalibrate (seek track 0) + /// COMMAND: 1 parameter byte + /// EXECUTION: Head retracted to track 0 + /// RESULT: NO result phase + /// + private void UPD_Recalibrate() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + ParseParamByteStandard(CommCounter); + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + // all parameter bytes received + DriveLight = true; + ActivePhase = Phase.Execution; + ActiveCommand.CommandDelegate(); + } + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + + // immediate recalibration + ActiveDrive.TrackIndex = 0; + ActiveDrive.SectorIndex = 0; + + // recalibrate appears to always skip the first sector + //if (ActiveDrive.Disk.DiskTracks[ActiveDrive.TrackIndex].Sectors.Length > 1) + //ActiveDrive.SectorIndex++; + + // set seek flag + ActiveDrive.SeekStatus = SEEK_RECALIBRATE; + + // skip execution mode and go directly to idle + // result is determined by SIS command + ActivePhase = Phase.Idle; + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Sense Interrupt Status + /// COMMAND: NO parameter bytes + /// EXECUTION: NO execution phase + /// RESULT: 2 result bytes + /// + private void UPD_SenseInterruptStatus() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + // SIS should return 2 bytes if sucessfully sensed an interrupt + // 1 byte otherwise + + // it seems like the +3 ROM makes 3 SIS calls for each seek/recalibrate call for some reason + // possibly one for each drive??? + // 1 - the interrupt is acknowleged with ST0 = 32 and track number + // 2 - second sis returns 1 ST0 byte with 192 + // 3 - third SIS call returns standard 1 byte 0x80 (unknown cmd or SIS with no interrupt occured) + // for now I will assume that the first call is aimed at DriveA, the second at DriveB (which we are NOT implementing) + + // check active drive first + if (ActiveDrive.SeekStatus == SEEK_RECALIBRATE || + ActiveDrive.SeekStatus == SEEK_SEEK) + { + // interrupt has been raised for this drive + // acknowledge + ActiveDrive.SeekStatus = SEEK_IDLE;// SEEK_INTACKNOWLEDGED; + + // result length 2 + ResLength = 2; + + // first byte ST0 0x20 + Status0 = 0x20; + ResBuffer[0] = Status0; + // second byte is the current track id + ResBuffer[1] = ActiveDrive.CurrentTrackID; + } + /* + else if (ActiveDrive.SeekStatus == SEEK_INTACKNOWLEDGED) + { + // DriveA interrupt has already been acknowledged + ActiveDrive.SeekStatus = SEEK_IDLE; + + ResLength = 1; + Status0 = 192; + ResBuffer[0] = Status0; + } + */ + else if (ActiveDrive.SeekStatus == SEEK_IDLE) + { + // SIS with no interrupt + ResLength = 1; + Status0 = 0x80; + ResBuffer[0] = Status0; + } + + ActivePhase = Phase.Result; + + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Sense Drive Status + /// COMMAND: 1 parameter byte + /// EXECUTION: NO execution phase + /// RESULT: 1 result byte + /// + /// The ZX spectrum appears to only specify drive 1 as the parameter byte, NOT drive 0 + /// After the final param byte is received main status changes to 0xd0 + /// Data register (ST3) result is 0x51 if drive/disk not available + /// 0x71 if disk is present in 2nd drive + /// + private void UPD_SenseDriveStatus() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + // store the parameter in the command buffer + CommBuffer[CommCounter] = LastByteReceived; + + // process parameter byte + ParseParamByteStandard(CommCounter); + + // increment command parameter counter + CommCounter++; + + // was that the last parameter byte? + if (CommCounter == ActiveCommand.ParameterByteCount) + { + // all parameter bytes received + ActivePhase = Phase.Execution; + UPD_SenseDriveStatus(); + } + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + // one ST3 byte required + + // set US + Status3 = (byte)ActiveDrive.ID; + + if (Status3 != 0) + { + // we only support 1 drive + SetBit(SR3_FT, ref Status3); + } + else + { + // HD - only one side + UnSetBit(SR3_HD, ref Status3); + + // write protect + if (ActiveDrive.FLAG_WRITEPROTECT) + SetBit(SR3_WP, ref Status3); + + // track 0 + if (ActiveDrive.FLAG_TRACK0) + SetBit(SR3_T0, ref Status3); + + // rdy + if (ActiveDrive.Disk != null) + SetBit(SR3_RY, ref Status3); + } + + ResBuffer[0] = Status3; + ActivePhase = Phase.Result; + + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + break; + } + } + + /// + /// Version + /// COMMAND: NO parameter bytes + /// EXECUTION: NO execution phase + /// RESULT: 1 result byte + /// + private void UPD_Version() + { + switch (ActivePhase) + { + case Phase.Idle: + case Phase.Command: + case Phase.Execution: + case Phase.Result: + UPD_Invalid(); + break; + } + } + + /// + /// Invalid + /// COMMAND: NO parameter bytes + /// EXECUTION: NO execution phase + /// RESULT: 1 result byte + /// + private void UPD_Invalid() + { + switch (ActivePhase) + { + //---------------------------------------- + // FDC is waiting for a command byte + //---------------------------------------- + case Phase.Idle: + break; + + //---------------------------------------- + // Receiving command parameter bytes + //---------------------------------------- + case Phase.Command: + break; + + //---------------------------------------- + // FDC in execution phase reading/writing bytes + //---------------------------------------- + case Phase.Execution: + // no execution phase + ActivePhase = Phase.Result; + UPD_Invalid(); + break; + + //---------------------------------------- + // Result bytes being sent to CPU + //---------------------------------------- + case Phase.Result: + ResBuffer[0] = 0x80; + break; + } + } + + #endregion + + #endregion + + #region Controller Methods + + /// + /// Called when a status register read is required + /// This can be called at any time + /// The main status register appears to be queried nearly all the time + /// so needs to be kept updated. It keeps the CPU informed of the current state + /// + private byte ReadMainStatus() + { + SetBit(MSR_RQM, ref StatusMain); + + switch (ActivePhase) + { + case Phase.Idle: + UnSetBit(MSR_DIO, ref StatusMain); + UnSetBit(MSR_CB, ref StatusMain); + UnSetBit(MSR_EXM, ref StatusMain); + break; + case Phase.Command: + UnSetBit(MSR_DIO, ref StatusMain); + SetBit(MSR_CB, ref StatusMain); + UnSetBit(MSR_EXM, ref StatusMain); + break; + case Phase.Execution: + if (ActiveCommand.Direction == CommandDirection.OUT) + SetBit(MSR_DIO, ref StatusMain); + else + UnSetBit(MSR_DIO, ref StatusMain); + + SetBit(MSR_EXM, ref StatusMain); + SetBit(MSR_CB, ref StatusMain); + + // overrun detection + OverrunCounter++; + if (OverrunCounter >= 64) + { + // CPU has read the status register 64 times without reading the data register + // switch the current command into result phase + ActivePhase = Phase.Result; + + // reset the overun counter + OverrunCounter = 0; + } + + break; + case Phase.Result: + SetBit(MSR_DIO, ref StatusMain); + SetBit(MSR_CB, ref StatusMain); + UnSetBit(MSR_EXM, ref StatusMain); + break; + } + + //if (!CheckTiming()) + //{ + // UnSetBit(MSR_EXM, ref StatusMain); + //} + + return StatusMain; + } + private int testCount = 0; + /// + /// Handles CPU reading from the data register + /// + /// + private byte ReadDataRegister() + { + // default return value + byte res = 0xff; + + // check RQM flag status + if (!GetBit(MSR_RQM, StatusMain)) + { + // FDC is not ready to return data + return res; + } + + // check active direction + if (!GetBit(MSR_DIO, StatusMain)) + { + // FDC is expecting to receive, not send data + return res; + } + + switch (ActivePhase) + { + case Phase.Execution: + // reset overrun counter + OverrunCounter = 0; + + // execute read + ActiveCommand.CommandDelegate(); + + res = LastSectorDataReadByte; + + if (ExecCounter <= 0) + { + // end of execution phase + ActivePhase = Phase.Result; + } + + return res; + + case Phase.Result: + + DriveLight = false; + + ActiveCommand.CommandDelegate(); + + // result byte reading + res = ResBuffer[ResCounter]; + + // increment result counter + ResCounter++; + + if (ResCounter >= ResLength) + { + ActivePhase = Phase.Idle; + } + + break; + } + + return res; + } + + /// + /// Handles CPU writing to the data register + /// + /// + private void WriteDataRegister(byte data) + { + if (!GetBit(MSR_RQM, StatusMain) || GetBit(MSR_DIO, StatusMain)) + { + // FDC will not receive and process any bytes + return; + } + + // store the incoming byte + LastByteReceived = data; + + // process incoming bytes + switch (ActivePhase) + { + //// controller is idle awaiting the first command byte of a new instruction + case Phase.Idle: + ParseCommandByte(data); + break; + //// we are in command phase + case Phase.Command: + // attempt to process this parameter byte + //ProcessCommand(data); + ActiveCommand.CommandDelegate(); + break; + //// we are in execution phase + case Phase.Execution: + // CPU is going to be sending data bytes to the FDC to be written to disk + + // store the byte + LastSectorDataWriteByte = data; + ActiveCommand.CommandDelegate(); + + if (ExecCounter <= 0) + { + // end of execution phase + ActivePhase = Phase.Result; + } + + break; + //// result phase + case Phase.Result: + // data register will not receive bytes during result phase + break; + } + } + + /// + /// Processes the first command byte (within a command instruction) + /// Returns TRUE if successful. FALSE if otherwise + /// Called only in idle phase + /// + /// + /// + /// + private bool ParseCommandByte(byte cmdByte) + { + // clear counters + CommCounter = 0; + ResCounter = 0; + + // get the first 4 bytes + byte cByte = (byte)(cmdByte & 0x0f); + + // get MT, MD and SK states + CMD_FLAG_MT = cmdByte.Bit(7); + CMD_FLAG_MF = cmdByte.Bit(6); + CMD_FLAG_SK = cmdByte.Bit(5); + + cmdByte = cByte; + + // lookup the command + var cmd = CommandList.Where(a => a.CommandCode == cmdByte).FirstOrDefault(); + + if (cmd == null) + { + // no command found - use invalid + CMDIndex = CommandList.Count() - 1; + } + else + { + // valid command found + CMDIndex = CommandList.FindIndex(a => a.CommandCode == cmdByte); + + // check validity of command byte flags + // if a flag is set but not valid for this command then it is invalid + bool invalid = false; + + if (!ActiveCommand.MT) + if (CMD_FLAG_MT) + invalid = true; + if (!ActiveCommand.MF) + if (CMD_FLAG_MF) + invalid = true; + if (!ActiveCommand.SK) + if (CMD_FLAG_SK) + invalid = true; + + if (invalid) + { + // command byte included spurious bit 5,6 or 7 flags + CMDIndex = CommandList.Count() - 1; + } + + /* + if ((CMD_FLAG_MF && !ActiveCommand.MF) || + (CMD_FLAG_MT && !ActiveCommand.MT) || + (CMD_FLAG_SK && !ActiveCommand.SK)) + { + // command byte included spurious bit 5,6 or 7 flags + CMDIndex = CommandList.Count() - 1; + } + */ + } + + CommCounter = 0; + ResCounter = 0; + + // there will now be an active command set + // move to command phase + ActivePhase = Phase.Command; + + /* + // check for invalid SIS + if (ActiveInterrupt == InterruptState.None && CMDIndex == CC_SENSE_INTSTATUS) + { + CMDIndex = CC_INVALID; + //ActiveCommand.CommandDelegate(InstructionState.StartResult); + } + */ + + // set reslength + ResLength = ActiveCommand.ResultByteCount; + + // if there are no expected param bytes to receive - go ahead and run the command + if (ActiveCommand.ParameterByteCount == 0) + { + ActivePhase = Phase.Execution; + ActiveCommand.CommandDelegate(); + } + + return true; + } + + /// + /// Parses the first 5 command argument bytes that are of the standard format + /// + /// + private void ParseParamByteStandard(int index) + { + byte currByte = CommBuffer[index]; + BitArray bi = new BitArray(new byte[] { currByte }); + + switch (index) + { + // HD & US + case CM_HEAD: + if (bi[2]) + ActiveCommandParams.Side = 1; + else + ActiveCommandParams.Side = 0; + + ActiveCommandParams.UnitSelect = (byte)(GetUnitSelect(currByte)); + DiskDriveIndex = ActiveCommandParams.UnitSelect; + break; + + // C + case CM_C: + ActiveCommandParams.Cylinder = currByte; + break; + + // H + case CM_H: + ActiveCommandParams.Head = currByte; + break; + + // R + case CM_R: + ActiveCommandParams.Sector = currByte; + break; + + // N + case CM_N: + ActiveCommandParams.SectorSize = currByte; + break; + + // EOT + case CM_EOT: + ActiveCommandParams.EOT = currByte; + break; + + // GPL + case CM_GPL: + ActiveCommandParams.Gap3Length = currByte; + break; + + // DTL + case CM_DTL: + ActiveCommandParams.DTL = currByte; + break; + + default: + break; + } + } + + /// + /// Clears the result buffer + /// + public void ClearResultBuffer() + { + for (int i = 0; i < ResBuffer.Length; i++) + { + ResBuffer[i] = 0; + } + } + + /// + /// Clears the result buffer + /// + public void ClearExecBuffer() + { + for (int i = 0; i < ExecBuffer.Length; i++) + { + ExecBuffer[i] = 0; + } + } + + /// + /// Populates the result status registers + /// + private void CommitResultStatus() + { + // check for read diag + if (ActiveCommand.CommandCode == 0x02) + { + // commit to result buffer + ResBuffer[RS_ST0] = Status0; + ResBuffer[RS_ST1] = Status1; + return; + } + + // check for error bits + if (GetBit(SR1_DE, Status1) || + GetBit(SR1_MA, Status1) || + GetBit(SR1_ND, Status1) || + GetBit(SR1_NW, Status1) || + GetBit(SR1_OR, Status1) || + GetBit(SR2_BC, Status2) || + GetBit(SR2_CM, Status2) || + GetBit(SR2_DD, Status2) || + GetBit(SR2_MD, Status2) || + GetBit(SR2_SN, Status2) || + GetBit(SR2_WC, Status2)) + { + // error bits set - unset end of track + UnSetBit(SR1_EN, ref Status1); + } + + // check for data errors + if (GetBit(SR1_DE, Status1) || + GetBit(SR2_DD, Status2)) + { + // unset control mark + UnSetBit(SR2_CM, ref Status2); + } + else if (GetBit(SR2_CM, Status2)) + { + // DAM found - unset IC and US0 + UnSetBit(SR0_IC0, ref Status0); + UnSetBit(SR0_US0, ref Status0); + } + + // commit to result buffer + ResBuffer[RS_ST0] = Status0; + ResBuffer[RS_ST1] = Status1; + ResBuffer[RS_ST2] = Status2; + + } + + /// + /// Populates the result CHRN values + /// + private void CommitResultCHRN() + { + ResBuffer[RS_C] = ActiveCommandParams.Cylinder; + ResBuffer[RS_H] = ActiveCommandParams.Head; + ResBuffer[RS_R] = ActiveCommandParams.Sector; + ResBuffer[RS_N] = ActiveCommandParams.SectorSize; + } + + /// + /// Moves active phase into idle + /// + public void SetPhase_Idle() + { + ActivePhase = Phase.Idle; + + // active direction + UnSetBit(MSR_DIO, ref StatusMain); + // CB + UnSetBit(MSR_CB, ref StatusMain); + // RQM + SetBit(MSR_RQM, ref StatusMain); + + CommCounter = 0; + ResCounter = 0; + } + + /// + /// Moves to result phase + /// + public void SetPhase_Result() + { + ActivePhase = Phase.Result; + + // active direction + SetBit(MSR_DIO, ref StatusMain); + // CB + SetBit(MSR_CB, ref StatusMain); + // RQM + SetBit(MSR_RQM, ref StatusMain); + // EXM + UnSetBit(MSR_EXM, ref StatusMain); + + CommCounter = 0; + ResCounter = 0; + } + + /// + /// Moves to command phase + /// + public void SetPhase_Command() + { + ActivePhase = Phase.Command; + + // default 0x80 - just RQM + SetBit(MSR_RQM, ref StatusMain); + UnSetBit(MSR_DIO, ref StatusMain); + UnSetBit(MSR_CB, ref StatusMain); + UnSetBit(MSR_EXM, ref StatusMain); + CommCounter = 0; + ResCounter = 0; + } + + /// + /// Moves to execution phase + /// + public void SetPhase_Execution() + { + ActivePhase = Phase.Execution; + + // EXM + SetBit(MSR_EXM, ref StatusMain); + // CB + SetBit(MSR_CB, ref StatusMain); + // RQM + UnSetBit(MSR_RQM, ref StatusMain); + + CommCounter = 0; + ResCounter = 0; + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.FDD.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.FDD.cs new file mode 100644 index 0000000000..ef04817a68 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.FDD.cs @@ -0,0 +1,893 @@ +using BizHawk.Common; +using BizHawk.Common.NumberExtensions; +using System; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Floppy drive related stuff + /// + #region Attribution + /* + Implementation based on the information contained here: + http://www.cpcwiki.eu/index.php/765_FDC + and here: + http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf + */ + #endregion + public partial class NECUPD765 : IFDDHost + { + #region Drive State + + /// + /// FDD Flag - motor on/off + /// + public bool FDD_FLAG_MOTOR; + + /// + /// The index of the currently active disk drive + /// + public int DiskDriveIndex + { + get { return _diskDriveIndex; } + set + { + // when index is changed update the ActiveDrive + _diskDriveIndex = value; + ActiveDrive = DriveStates[_diskDriveIndex]; + } + } + private int _diskDriveIndex = 0; + + /// + /// The currently active drive + /// + private DriveState ActiveDrive; + + /// + /// Array that holds state information for each possible drive + /// + private DriveState[] DriveStates = new DriveState[4]; + + #endregion + + #region FDD Methods + + /// + /// Initialization / reset of the floppy drive subsystem + /// + private void FDD_Init() + { + for (int i = 0; i < 4; i++) + { + DriveState ds = new DriveState(i, this); + DriveStates[i] = ds; + } + } + + /// + /// Searches for the requested sector + /// + /// + private FloppyDisk.Sector GetSector() + { + FloppyDisk.Sector sector = null; + + // get the current track + var trk = ActiveDrive.Disk.DiskTracks[ActiveDrive.TrackIndex]; + + // get the current sector index + int index = ActiveDrive.SectorIndex; + + // make sure this index exists + if (index > trk.Sectors.Length) + { + index = 0; + } + + // index hole count + int iHole = 0; + + // loop through the sectors in a track + // the loop ends with either the sector being found + // or the index hole being passed twice + while (iHole <= 2) + { + // does the requested sector match the current sector + if (trk.Sectors[index].SectorIDInfo.C == ActiveCommandParams.Cylinder && + trk.Sectors[index].SectorIDInfo.H == ActiveCommandParams.Head && + trk.Sectors[index].SectorIDInfo.R == ActiveCommandParams.Sector && + trk.Sectors[index].SectorIDInfo.N == ActiveCommandParams.SectorSize) + { + // sector has been found + sector = trk.Sectors[index]; + + UnSetBit(SR2_BC, ref Status2); + UnSetBit(SR2_WC, ref Status2); + break; + } + + // check for bad cylinder + if (trk.Sectors[index].SectorIDInfo.C == 255) + { + SetBit(SR2_BC, ref Status2); + } + // check for no cylinder + else if (trk.Sectors[index].SectorIDInfo.C != ActiveCommandParams.Cylinder) + { + SetBit(SR2_WC, ref Status2); + } + + // incrememnt sector index + index++; + + // have we reached the index hole? + if (trk.Sectors.Length <= index) + { + // wrap around + index = 0; + iHole++; + } + } + + // search loop has completed and the sector may or may not have been found + + // bad cylinder detected? + if (Status2.Bit(SR2_BC)) + { + // remove WC + UnSetBit(SR2_WC, ref Status2); + } + + // update sectorindex on drive + ActiveDrive.SectorIndex = index; + + return sector; + } + + #endregion + + #region IFDDHost + + // IFDDHost methods that fall through to the currently active drive + + /// + /// Parses a new disk image and loads it into this floppy drive + /// + /// + public void FDD_LoadDisk(byte[] diskData) + { + // we are only going to load into the first drive + DriveStates[0].FDD_LoadDisk(diskData); + } + + /// + /// Ejects the current disk + /// + public void FDD_EjectDisk() + { + DriveStates[0].FDD_EjectDisk(); + } + + /// + /// Signs whether the current active drive has a disk inserted + /// + public bool FDD_IsDiskLoaded + { + get { return DriveStates[DiskDriveIndex].FDD_IsDiskLoaded; } + } + + /// + /// Returns the disk object from drive 0 + /// + public FloppyDisk DiskPointer + { + get { return DriveStates[0].Disk; } + } + + public FloppyDisk Disk { get; set; } + + #endregion + + #region Drive Status Class + + /// + /// Holds specfic state information about a drive + /// + private class DriveState : IFDDHost + { + #region State + + /// + /// The drive ID from an FDC perspective + /// + public int ID; + + /// + /// Signs whether this drive ready + /// TRUE if both drive exists and has a disk inserted + /// + public bool FLAG_READY + { + get + { + if (!FDD_IsDiskLoaded || Disk.GetTrackCount() == 0 || !FDC.FDD_FLAG_MOTOR) + return false; + else + return true; + } + } + + /// + /// Disk is write protected (TRUE BY DEFAULT) + /// + public bool FLAG_WRITEPROTECT = false; + + /// + /// Storage for seek steps + /// One step for each indexpulse (track index) until seeked track + /// + public int SeekCounter; + + /// + /// Seek status + /// + public int SeekStatus; + + /// + /// Age counter + /// + public int SeekAge; + + /// + /// The current side + /// + public byte CurrentSide; + + /// + /// The current track index in the DiskTracks array + /// + public byte TrackIndex; + + /// + /// The track ID of the current cylinder + /// + public byte CurrentTrackID + { + get + { + // default invalid track + int id = 0xff; + + if (Disk == null) + return (byte)id; + + if (Disk.DiskTracks.Count() == 0) + return (byte)id; + + if (TrackIndex >= Disk.GetTrackCount()) + TrackIndex = 0; + else if (TrackIndex < 0) + TrackIndex = 0; + + var track = Disk.DiskTracks[TrackIndex]; + + id = track.TrackNumber; + + return (byte)id; + } + set + { + for (int i = 0; i < Disk.GetTrackCount(); i++) + { + if (Disk.DiskTracks[i].TrackNumber == value) + { + TrackIndex = (byte)i; + break; + } + } + } + } + + + /// + /// The new track that the drive is seeking to + /// (used in seek operations) + /// + public int SeekingTrack; + + /// + /// The current sector index in the Sectors array + /// + public int SectorIndex; + + /// + /// The currently loaded floppy disk + /// + public FloppyDisk Disk { get; set; } + + /// + /// The parent controller + /// + private NECUPD765 FDC; + + #endregion + + #region Lookups + + /// + /// TRUE if we are on track 0 + /// + public bool FLAG_TRACK0 + { + get + { + if (TrackIndex == 0) { return true; } + else { return false; } + } + } + + #endregion + + #region Public Methods + /* + /// + /// Moves the head across the disk cylinders + /// + /// + /// + public void MoveHead(SkipDirection direction, int cylinderCount) + { + // get total tracks + int trackCount = Disk.DiskTracks.Count(); + + int trk = 0; + + switch (direction) + { + case SkipDirection.Increment: + trk = (int)CurrentTrack + cylinderCount; + if (trk >= trackCount) + { + // past the last track + trk = trackCount - 1; + } + else if (trk < 0) + trk = 0; + break; + case SkipDirection.Decrement: + trk = (int)CurrentTrack - cylinderCount; + if (trk < 0) + { + // before the first track + trk = 0; + } + else if (trk >= trackCount) + trk = trackCount - 1; + break; + } + + // move the head + CurrentTrack = (byte)trk; + } + */ + + /* + + /// + /// Finds a supplied sector + /// + /// + /// + /// + /// + public FloppyDisk.Sector FindSector(ref byte[] resBuffer, CommandParameters prms) + { + int index =CurrentSector; + int lc = 0; + FloppyDisk.Sector sector = null; + + bool found = false; + + do + { + sector = Disk.DiskTracks[CurrentTrack].Sectors[index]; + if (sector != null && sector.SectorID == prms.Sector) + { + // sector found + // check for data errors + if ((sector.Status1 & 0x20) != 0 || (sector.Status2 & 0x20) != 0) + { + // data errors found + } + found = true; + break; + } + + // sector doesnt match + var c = Disk.DiskTracks[CurrentTrack].Sectors[index].TrackNumber; + if (c == 255) + { + // bad cylinder + resBuffer[RS_ST2] |= 0x02; + } + else if (prms.Cylinder != c) + { + // cylinder mismatch + resBuffer[RS_ST2] |= 0x10; + } + + // increment index + index++; + + if (index >= Disk.DiskTracks[CurrentTrack].NumberOfSectors) + { + // out of bounds + index = 0; + lc++; + } + + } while (lc < 2); + + if ((resBuffer[RS_ST2] & 0x02) != 0) + { + // bad cylinder set - remove no cylinder + UnSetBit(SR2_WC, ref resBuffer[RS_ST2]); + } + + // update current sector + CurrentSector = index; + + if (found) + return sector; + else + return null; + } + + + /// + /// Populates a result buffer + /// + /// + /// + public void FillResult(ref byte[] resBuffer, CHRN chrn) + { + // clear results + resBuffer[RS_ST0] = 0; + resBuffer[RS_ST1] = 0; + resBuffer[RS_ST2] = 0; + resBuffer[RS_C] = 0; + resBuffer[RS_H] = 0; + resBuffer[RS_R] = 0; + resBuffer[RS_N] = 0; + + if (chrn == null) + { + // no chrn supplied + resBuffer[RS_ST0] = ST0; + resBuffer[RS_ST1] = 0; + resBuffer[RS_ST2] = 0; + resBuffer[RS_C] = 0; + resBuffer[RS_H] = 0; + resBuffer[RS_R] = 0; + resBuffer[RS_N] = 0; + } + } + + + + /// + /// Populates the result buffer with ReadID data + /// + /// + public void ReadID(ref byte[] resBuffer) + { + if (CheckDriveStatus() == false) + { + // drive not ready + resBuffer[RS_ST0] = ST0; + return; + } + + var track = Disk.DiskTracks.Where(a => a.TrackNumber == CurrentTrack).FirstOrDefault(); + + if (track != null && track.NumberOfSectors > 0) + { + // formatted track + + // get the current sector + int index = CurrentSector; + + // is the index out of bounds? + if (index >= track.NumberOfSectors) + { + // reset the index + index = 0; + } + + // read the sector data + var data = track.Sectors[index]; + resBuffer[RS_C] = data.TrackNumber; + resBuffer[RS_H] = data.SideNumber; + resBuffer[RS_R] = data.SectorID; + resBuffer[RS_N] = data.SectorSize; + + resBuffer[RS_ST0] = ST0; + + // increment the current sector + CurrentSector = index + 1; + return; + } + else + { + // unformatted track? + resBuffer[RS_C] = FDC.CommBuffer[CM_C]; + resBuffer[RS_H] = FDC.CommBuffer[CM_H]; + resBuffer[RS_R] = FDC.CommBuffer[CM_R]; + resBuffer[RS_N] = FDC.CommBuffer[CM_N]; + + SetBit(SR0_IC0, ref ST0); + resBuffer[RS_ST0] = ST0; + resBuffer[RS_ST1] = 0x01; + return; + } + } + */ + + /* + + /// + /// The drive performs a seek operation if necessary + /// Return value TRUE indicates seek complete + /// + public void DoSeek() + { + if (CurrentState != DriveMainState.Recalibrate && + CurrentState != DriveMainState.Seek) + { + // no seek/recalibrate has been asked for + return; + } + + if (GetBit(ID, FDC.StatusMain)) + { + // drive is already seeking + return; + } + + RunSeekCycle(); + } + + /// + /// Runs a seek cycle + /// + public void RunSeekCycle() + { + for (;;) + { + switch (SeekState) + { + // seek or recalibrate has been requested + case SeekSubState.Idle: + + if (CurrentState == DriveMainState.Recalibrate) + { + // recalibrate always seeks to track 0 + SeekingTrack = 0; + } + SeekState = SeekSubState.MoveInit; + + // mark drive as busy + // this should be cleared by SIS command + SetBit(ID, ref FDC.StatusMain); + + break; + + // setup for the head move + case SeekSubState.MoveInit: + + if (CurrentTrack == SeekingTrack) + { + // we are already at the required track + if (CurrentState == DriveMainState.Recalibrate && + !FLAG_TRACK0) + { + // recalibration fail + SeekIntState = SeekIntStatus.Abnormal; + + // raise seek interrupt + FDC.ActiveInterrupt = InterruptState.Seek; + + // unset DB bit + UnSetBit(ID, ref FDC.StatusMain); + + // equipment check + SetBit(SR0_EC, ref FDC.Status0); + + SeekState = SeekSubState.PerformCompletion; + break; + } + + if (CurrentState == DriveMainState.Recalibrate && + FLAG_TRACK0) + { + // recalibration success + SeekIntState = SeekIntStatus.Normal; + + // raise seek interrupt + FDC.ActiveInterrupt = InterruptState.Seek; + + // unset DB bit + UnSetBit(ID, ref FDC.StatusMain); + + SeekState = SeekSubState.PerformCompletion; + break; + } + } + + // check for error + if (IntStatus >= IC_ABORTED_DISCREMOVED || Disk == null) + { + // drive not ready + FLAG_READY = false; + + // drive not ready + SeekIntState = SeekIntStatus.DriveNotReady; + + // cancel any interrupt + FDC.ActiveInterrupt = InterruptState.None; + + // unset DB bit + UnSetBit(ID, ref FDC.StatusMain); + + SeekState = SeekSubState.PerformCompletion; + break; + } + + if (SeekCounter > 1) + { + // not ready to seek yet + SeekCounter--; + return; + } + + if (FDC.SRT < 1 && CurrentTrack != SeekingTrack) + { + SeekState = SeekSubState.MoveImmediate; + break; + } + + // head move + SeekState = SeekSubState.HeadMove; + + break; + + case SeekSubState.HeadMove: + + // do the seek + SeekCounter = FDC.SRT; + + if (CurrentTrack < SeekingTrack) + { + // we are seeking forward + var delta = SeekingTrack - CurrentTrack; + MoveHead(SkipDirection.Increment, 1); + } + else if (CurrentTrack > SeekingTrack) + { + // we are seeking backward + var delta = CurrentTrack - SeekingTrack; + MoveHead(SkipDirection.Decrement, 1); + } + + // should the seek be completed now? + if (CurrentTrack == SeekingTrack) + { + SeekState = SeekSubState.PerformCompletion; + break; + } + + // seek not finished yet + return; + + // seek emulation processed immediately + case SeekSubState.MoveImmediate: + + if (CurrentTrack < SeekingTrack) + { + // we are seeking forward + var delta = SeekingTrack - CurrentTrack; + MoveHead(SkipDirection.Increment, delta); + + } + else if (CurrentTrack > SeekingTrack) + { + // we are seeking backward + var delta = CurrentTrack - SeekingTrack; + MoveHead(SkipDirection.Decrement, delta); + } + + SeekState = SeekSubState.PerformCompletion; + break; + + case SeekSubState.PerformCompletion: + SeekDone(); + SeekState = SeekSubState.SeekCompleted; + break; + + case SeekSubState.SeekCompleted: + // seek has already completed + return; + } + } + } + + /// + /// Called when a seek operation has completed + /// + public void SeekDone() + { + SeekCounter = 0; + SeekingTrack = CurrentTrack; + + // generate ST0 register data + + // get only the IC bits + IntStatus &= IC_ABORTED_DISCREMOVED; + + // drive ready? + if (!FLAG_READY) + { + SetBit(SR0_NR, ref IntStatus); + SetBit(SR0_EC, ref IntStatus); + + // are we recalibrating? + if (CurrentState == DriveMainState.Recalibrate) + { + SetBit(SR0_EC, ref IntStatus); + } + } + + // set seek end + SetBit(SR0_SE, ref IntStatus); + /* + // head address + if (CurrentSide > 0) + { + SetBit(SR0_HD, ref IntStatus); + + // drive only supports 1 head + // set the EC bit + SetBit(SR0_EC, ref IntStatus); + } + */ + /* + // UnitSelect + SetUnitSelect(ID, ref IntStatus); + + // move to none state + //CurrentState = DriveMainState.None; + + //SeekState = SeekSubState.SeekCompleted; + + // set the seek interrupt flag for this drive + // this will be cleared at the next successful senseint + FLAG_SEEK_INTERRUPT = true; + + //CurrentState = DriveMainState.None; + + } + */ + + #endregion + + #region Construction + + public DriveState(int driveID, NECUPD765 fdc) + { + ID = driveID; + FDC = fdc; + } + + #endregion + + #region IFDDHost + + /// + /// Parses a new disk image and loads it into this floppy drive + /// + /// + public void FDD_LoadDisk(byte[] diskData) + { + // try dsk first + FloppyDisk fdd = null; + bool found = false; + + foreach (DiskType type in Enum.GetValues(typeof(DiskType))) + { + switch (type) + { + case DiskType.CPCExtended: + fdd = new CPCExtendedFloppyDisk(); + found = fdd.ParseDisk(diskData); + break; + case DiskType.CPC: + fdd = new CPCFloppyDisk(); + found = fdd.ParseDisk(diskData); + break; + } + + if (found) + { + Disk = fdd; + break; + } + } + + if (!found) + { + throw new Exception(this.GetType().ToString() + + "\n\nDisk image file could not be parsed. Potentially an unknown format."); + } + } + + /// + /// Ejects the current disk + /// + public void FDD_EjectDisk() + { + Disk = null; + //FLAG_READY = false; + } + + /// + /// Signs whether the current active drive has a disk inserted + /// + public bool FDD_IsDiskLoaded + { + get + { + if (Disk != null) + return true; + else + return false; + } + } + + #endregion + + #region StateSerialization + + public void SyncState(Serializer ser) + { + ser.Sync("ID", ref ID); + ser.Sync("FLAG_WRITEPROTECT", ref FLAG_WRITEPROTECT); + //ser.Sync("FLAG_DISKCHANGED", ref FLAG_DISKCHANGED); + //ser.Sync("FLAG_RECALIBRATING", ref FLAG_RECALIBRATING); + //ser.Sync("FLAG_SEEK_INTERRUPT", ref FLAG_SEEK_INTERRUPT); + //ser.Sync("IntStatus", ref IntStatus); + //ser.Sync("ST0", ref ST0); + //ser.Sync("RecalibrationCounter", ref RecalibrationCounter); + ser.Sync("SeekCounter", ref SeekCounter); + ser.Sync("SeekStatus", ref SeekStatus); + ser.Sync("SeekAge", ref SeekAge); + ser.Sync("CurrentSide", ref CurrentSide); + //ser.Sync("CurrentTrack", ref CurrentTrack); + ser.Sync("TrackIndex", ref TrackIndex); + ser.Sync("SeekingTrack", ref SeekingTrack); + //ser.Sync("CurrentSector", ref CurrentSector); + ser.Sync("SectorIndex", ref SectorIndex); + //ser.Sync("RAngles", ref RAngles); + //ser.Sync("DataPointer", ref DataPointer); + //ser.SyncEnum("CurrentState", ref CurrentState); + //ser.SyncEnum("SeekState", ref SeekState); + //ser.SyncEnum("SeekIntState", ref SeekIntState); + } + + #endregion + } + +#endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.IPortIODevice.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.IPortIODevice.cs new file mode 100644 index 0000000000..6a2c89f506 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.IPortIODevice.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// IPortIODevice + /// + #region Attribution + /* + Implementation based on the information contained here: + http://www.cpcwiki.eu/index.php/765_FDC + and here: + http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf + */ + #endregion + public partial class NECUPD765 : IPortIODevice + { + #region Dev Logging + + public string outputfile = @"D:\Dropbox\Dropbox\_Programming\TASVideos\BizHawk\output\zxhawkio-" + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".csv"; + public string outputString = "STATUS,WRITE,READ,CODE,MT,MF,SK,CMDCNT,RESCNT,EXECCNT,EXECLEN\r\n"; + public bool writeDebug = false; + + public List dLog = new List + { + "STATUS,WRITE,READ,CODE,MT,MF,SK,CMDCNT,RESCNT,EXECCNT,EXECLEN" + }; + + + /* + * Status read + * Data write + * Data read + * CMD code + * CMD string + * MT flag + * MK flag + * SK flag + * */ + private string[] workingArr = new string[3]; + + private void BuildCSVLine() + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 3; i++) + { + sb.Append(workingArr[i]); + sb.Append(","); + workingArr[i] = ""; + } + + sb.Append(ActiveCommand.CommandCode).Append(","); + + sb.Append(CMD_FLAG_MT).Append(","); + sb.Append(CMD_FLAG_MF).Append(","); + sb.Append(CMD_FLAG_SK).Append(","); + + sb.Append(CommCounter).Append(","); + sb.Append(ResCounter).Append(","); + sb.Append(ExecCounter).Append(","); + sb.Append(ExecLength); + + //sb.Append("\r\n"); + + //outputString += sb.ToString(); + dLog.Add(sb.ToString()); + } + + #endregion + + public void ReadStatus(ref int data) + { + // read main status register + // this can happen at any time + data = ReadMainStatus(); + if (writeDebug) + { + //outputString += data + ",,," + ActiveCommand.CommandCode + "\r\n"; + workingArr[0] = data.ToString(); + BuildCSVLine(); + //System.IO.File.WriteAllText(outputfile, outputString); + } + } + + public void ReadData(ref int data) + { + // Z80 is trying to read from the data register + data = ReadDataRegister(); + if (writeDebug) + { + workingArr[2] = data.ToString(); + //outputString += ",," + data + "," + ActiveCommand.CommandCode + "\r\n"; + BuildCSVLine(); + } + } + + public void WriteData(int data) + { + // Z80 is attempting to write to the data register + WriteDataRegister((byte)data); + if (writeDebug) + { + //outputString += "," + data + ",," + ActiveCommand.CommandCode + "\r\n"; + workingArr[1] = data.ToString(); + BuildCSVLine(); + //System.IO.File.WriteAllText(outputfile, outputString); + } + } + + public void Motor(int data) + { + // set disk motor on/off + if (data > 0) + FDD_FLAG_MOTOR = true; + else + FDD_FLAG_MOTOR = false; + } + + /// + /// Device responds to an IN instruction + /// + /// + /// + /// + public bool ReadPort(ushort port, ref int data) + { + BitArray bits = new BitArray(new byte[] { (byte)data }); + + if (port == 0x3ffd) + { + // Z80 is trying to read from the data register + data = ReadDataRegister(); + if (writeDebug) + { + workingArr[2] = data.ToString(); + //outputString += ",," + data + "," + ActiveCommand.CommandCode + "\r\n"; + BuildCSVLine(); + } + + return true; + } + + if (port == 0x2ffd) + { + // read main status register + // this can happen at any time + data = ReadMainStatus(); + if (writeDebug) + { + //outputString += data + ",,," + ActiveCommand.CommandCode + "\r\n"; + workingArr[0] = data.ToString(); + BuildCSVLine(); + //System.IO.File.WriteAllText(outputfile, outputString); + } + + return true; + } + + return false; + } + + /// + /// Device responds to an OUT instruction + /// + /// + /// + /// + public bool WritePort(ushort port, int data) + { + BitArray bits = new BitArray(new byte[] { (byte)data }); + + if (port == 0x3ffd) + { + // Z80 is attempting to write to the data register + WriteDataRegister((byte)data); + if (writeDebug) + { + //outputString += "," + data + ",," + ActiveCommand.CommandCode + "\r\n"; + workingArr[1] = data.ToString(); + BuildCSVLine(); + //System.IO.File.WriteAllText(outputfile, outputString); + } + + return true; + } + + if (port == 0x1ffd) + { + // set disk motor on/off + FDD_FLAG_MOTOR = bits[3]; + return true; + } + return false; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.Timing.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.Timing.cs new file mode 100644 index 0000000000..9daed05605 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.Timing.cs @@ -0,0 +1,121 @@ + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Timimng + /// + #region Attribution + /* + Implementation based on the information contained here: + http://www.cpcwiki.eu/index.php/765_FDC + and here: + http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf + */ + #endregion + public partial class NECUPD765 + { + /// + /// The current Z80 cycle + /// + private long CurrentCPUCycle + { + get + { + if (_machine == null) + return 0; + else + return _machine.CPU.TotalExecutedCycles; + } + } + + /// + /// The last CPU cycle when the FDC accepted an IO read/write + /// + private long LastCPUCycle; + + /// + /// The current delay figure (in Z80 t-states) + /// This implementation only introduces delay upon main status register reads + /// All timing calculations should be done during the other read/write operations + /// + private long StatusDelay; + + /// + /// Defines the numbers of Z80 cycles per MS + /// + private long CPUCyclesPerMs; + + /// + /// The floppy drive emulated clock speed + /// + public const double DriveClock = 31250; + + /// + /// The number of floppy drive cycles per MS + /// + public long DriveCyclesPerMs; + + /// + /// The number of T-States in one floppy drive clock tick + /// + public long StatesPerDriveTick; + + /// + /// Responsible for measuring when the floppy drive is ready to run a cycle + /// + private long TickCounter; + + /// + /// Internal drive cycle counter + /// + private int DriveCycleCounter = 1; + + /// + /// Initializes the timing routines + /// + private void TimingInit() + { + // z80 timing + double frameSize = _machine.GateArray.FrameLength; + double rRate = _machine.GateArray.Z80ClockSpeed / frameSize; + long tPerSecond = (long)(frameSize * rRate); + CPUCyclesPerMs = tPerSecond / 1000; + + // drive timing + double dRate = DriveClock / frameSize; + long dPerSecond = (long)(frameSize * dRate); + DriveCyclesPerMs = dPerSecond / 1000; + + long TStatesPerDriveCycle = (long)((double)_machine.GateArray.Z80ClockSpeed / DriveClock); + StatesPerDriveTick = TStatesPerDriveCycle; + + } + + /// + /// Called by reads to the main status register + /// Returns true if there is no delay + /// Returns false if read is to be deferred + /// + /// + private bool CheckTiming() + { + // get delta + long delta = CurrentCPUCycle - LastCPUCycle; + + if (StatusDelay >= delta) + { + // there is still delay remaining + StatusDelay -= delta; + LastCPUCycle = CurrentCPUCycle; + return false; + } + else + { + // no delay remaining + StatusDelay = 0; + LastCPUCycle = CurrentCPUCycle; + return true; + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.cs new file mode 100644 index 0000000000..43f09fb829 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPD765.cs @@ -0,0 +1,246 @@ +using BizHawk.Common; +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// The NEC floppy disk controller (and floppy drive) found in the +3 + /// + #region Attribution + /* + Implementation based on the information contained here: + http://www.cpcwiki.eu/index.php/765_FDC + and here: + http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf + */ + #endregion + public partial class NECUPD765 + { + #region Devices + + /// + /// The emulated spectrum machine + /// + private CPCBase _machine; + + #endregion + + #region Construction & Initialization + + /// + /// Main constructor + /// + /// + public NECUPD765() + { + InitCommandList(); + } + + /// + /// Initialization routine + /// + public void Init(CPCBase machine) + { + _machine = machine; + FDD_Init(); + TimingInit(); + Reset(); + } + + /// + /// Resets the FDC + /// + public void Reset() + { + // setup main status + StatusMain = 0; + + Status0 = 0; + Status1 = 0; + Status2 = 0; + Status3 = 0; + + SetBit(MSR_RQM, ref StatusMain); + + SetPhase_Idle(); + + //FDC_FLAG_RQM = true; + //ActiveDirection = CommandDirection.IN; + SRT = 6; + HUT = 16; + HLT = 2; + HLT_Counter = 0; + HUT_Counter = 0; + IndexPulseCounter = 0; + CMD_FLAG_MF = false; + + foreach (var d in DriveStates) + { + //d.SeekingTrack = d.CurrentTrack; + ////d.SeekCounter = 0; + //d.FLAG_SEEK_INTERRUPT = false; + //d.IntStatus = 0; + //d.SeekState = SeekSubState.Idle; + //d.SeekIntState = SeekIntStatus.Normal; + + } + + } + + /// + /// Setup the command structure + /// Each command represents one of the internal UPD765 commands + /// + private void InitCommandList() + { + CommandList = new List + { + // read data + new Command { CommandDelegate = UPD_ReadData, CommandCode = 0x06, MT = true, MF = true, SK = true, IsRead = true, + Direction = CommandDirection.OUT, ParameterByteCount = 8, ResultByteCount = 7 }, + // read id + new Command { CommandDelegate = UPD_ReadID, CommandCode = 0x0a, MF = true, IsRead = true, + Direction = CommandDirection.OUT, ParameterByteCount = 1, ResultByteCount = 7 }, + // specify + new Command { CommandDelegate = UPD_Specify, CommandCode = 0x03, + Direction = CommandDirection.OUT, ParameterByteCount = 2, ResultByteCount = 0 }, + // read diagnostic + new Command { CommandDelegate = UPD_ReadDiagnostic, CommandCode = 0x02, MF = true, SK = true, IsRead = true, + Direction = CommandDirection.OUT, ParameterByteCount = 8, ResultByteCount = 7 }, + // scan equal + new Command { CommandDelegate = UPD_ScanEqual, CommandCode = 0x11, MT = true, MF = true, SK = true, IsRead = true, + Direction = CommandDirection.IN, ParameterByteCount = 8, ResultByteCount = 7 }, + // scan high or equal + new Command { CommandDelegate = UPD_ScanHighOrEqual, CommandCode = 0x1d, MT = true, MF = true, SK = true, IsRead = true, + Direction = CommandDirection.IN, ParameterByteCount = 8, ResultByteCount = 7 }, + // scan low or equal + new Command { CommandDelegate = UPD_ScanLowOrEqual, CommandCode = 0x19, MT = true, MF = true, SK = true, IsRead = true, + Direction = CommandDirection.IN, ParameterByteCount = 8, ResultByteCount = 7 }, + // read deleted data + new Command { CommandDelegate = UPD_ReadDeletedData, CommandCode = 0x0c, MT = true, MF = true, SK = true, IsRead = true, + Direction = CommandDirection.OUT, ParameterByteCount = 8, ResultByteCount = 7 }, + // write data + new Command { CommandDelegate = UPD_WriteData, CommandCode = 0x05, MT = true, MF = true, IsWrite = true, + Direction = CommandDirection.IN, ParameterByteCount = 8, ResultByteCount = 7 }, + // write id + new Command { CommandDelegate = UPD_WriteID, CommandCode = 0x0d, MF = true, IsWrite = true, + Direction = CommandDirection.IN, ParameterByteCount = 5, ResultByteCount = 7 }, + // write deleted data + new Command { CommandDelegate = UPD_WriteDeletedData, CommandCode = 0x09, MT = true, MF = true, IsWrite = true, + Direction = CommandDirection.IN, ParameterByteCount = 8, ResultByteCount = 7 }, + // seek + new Command { CommandDelegate = UPD_Seek, CommandCode = 0x0f, + Direction = CommandDirection.OUT, ParameterByteCount = 2, ResultByteCount = 0 }, + // recalibrate (seek track00) + new Command { CommandDelegate = UPD_Recalibrate, CommandCode = 0x07, + Direction = CommandDirection.OUT, ParameterByteCount = 1, ResultByteCount = 0 }, + // sense interrupt status + new Command { CommandDelegate = UPD_SenseInterruptStatus, CommandCode = 0x08, + Direction = CommandDirection.OUT, ParameterByteCount = 0, ResultByteCount = 2 }, + // sense drive status + new Command { CommandDelegate = UPD_SenseDriveStatus, CommandCode = 0x04, + Direction = CommandDirection.OUT, ParameterByteCount = 1, ResultByteCount = 1 }, + // version + new Command { CommandDelegate = UPD_Version, CommandCode = 0x10, + Direction = CommandDirection.OUT, ParameterByteCount = 0, ResultByteCount = 1 }, + // invalid + new Command { CommandDelegate = UPD_Invalid, CommandCode = 0x00, + Direction = CommandDirection.OUT, ParameterByteCount = 0, ResultByteCount = 1 }, + }; + } + + #endregion + + #region State Serialization + + public void SyncState(Serializer ser) + { + ser.BeginSection("NEC-UPD765"); + + #region FDD + + ser.Sync("FDD_FLAG_MOTOR", ref FDD_FLAG_MOTOR); + + for (int i = 0; i < 4; i++) + { + ser.BeginSection("HITDrive_" + i); + DriveStates[i].SyncState(ser); + ser.EndSection(); + } + + ser.Sync("DiskDriveIndex", ref _diskDriveIndex); + // set active drive + DiskDriveIndex = _diskDriveIndex; + + #endregion + + #region Registers + + ser.Sync("_RegMain", ref StatusMain); + ser.Sync("_Reg0", ref Status0); + ser.Sync("_Reg1", ref Status1); + ser.Sync("_Reg2", ref Status2); + ser.Sync("_Reg3", ref Status3); + + #endregion + + #region Controller state + + ser.Sync("DriveLight", ref DriveLight); + ser.SyncEnum("ActivePhase", ref ActivePhase); + //ser.SyncEnum("ActiveDirection", ref ActiveDirection); + ser.SyncEnum("ActiveInterrupt", ref ActiveInterrupt); + ser.Sync("CommBuffer", ref CommBuffer, false); + ser.Sync("CommCounter", ref CommCounter); + ser.Sync("ResBuffer", ref ResBuffer, false); + ser.Sync("ExecBuffer", ref ExecBuffer, false); + ser.Sync("ExecCounter", ref ExecCounter); + ser.Sync("ExecLength", ref ExecLength); + ser.Sync("InterruptResultBuffer", ref InterruptResultBuffer, false); + ser.Sync("ResCounter", ref ResCounter); + ser.Sync("ResLength", ref ResLength); + ser.Sync("LastSectorDataWriteByte", ref LastSectorDataWriteByte); + ser.Sync("LastSectorDataReadByte", ref LastSectorDataReadByte); + ser.Sync("LastByteReceived", ref LastByteReceived); + + ser.Sync("_cmdIndex", ref _cmdIndex); + // resync the ActiveCommand + CMDIndex = _cmdIndex; + + ActiveCommandParams.SyncState(ser); + + ser.Sync("IndexPulseCounter", ref IndexPulseCounter); + //ser.SyncEnum("_activeStatus", ref _activeStatus); + //ser.SyncEnum("_statusRaised", ref _statusRaised); + + ser.Sync("CMD_FLAG_MT", ref CMD_FLAG_MT); + ser.Sync("CMD_FLAG_MF", ref CMD_FLAG_MF); + ser.Sync("CMD_FLAG_SK", ref CMD_FLAG_SK); + ser.Sync("SRT", ref SRT); + ser.Sync("HUT", ref HUT); + ser.Sync("HLT", ref HLT); + ser.Sync("ND", ref ND); + ser.Sync("SRT_Counter", ref SRT_Counter); + ser.Sync("HUT_Counter", ref HUT_Counter); + ser.Sync("HLT_Counter", ref HLT_Counter); + + ser.Sync("SectorDelayCounter", ref SectorDelayCounter); + ser.Sync("SectorID", ref SectorID); + + #endregion + + #region Timing + + ser.Sync("LastCPUCycle", ref LastCPUCycle); + ser.Sync("StatusDelay", ref StatusDelay); + ser.Sync("TickCounter", ref TickCounter); + ser.Sync("DriveCycleCounter", ref DriveCycleCounter); + + #endregion + + ser.EndSection(); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPS765.Static.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPS765.Static.cs new file mode 100644 index 0000000000..24ad458a34 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Disk/NECUPS765.Static.cs @@ -0,0 +1,107 @@ +using System.Collections; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Static helper methods + /// + #region Attribution + /* + Implementation based on the information contained here: + http://www.cpcwiki.eu/index.php/765_FDC + and here: + http://www.cpcwiki.eu/imgs/f/f3/UPD765_Datasheet_OCRed.pdf + */ + #endregion + public partial class NECUPD765 + { + /// + /// Returns the specified bit value from supplied byte + /// + /// + /// + /// + public static bool GetBit(int bitNumber, byte dataByte) + { + if (bitNumber < 0 || bitNumber > 7) + return false; + + BitArray bi = new BitArray(new byte[] { dataByte }); + + return bi[bitNumber]; + } + + /// + /// Sets the specified bit of the supplied byte to 1 + /// + /// + /// + public static void SetBit(int bitNumber, ref byte dataByte) + { + if (bitNumber < 0 || bitNumber > 7) + return; + + int db = (int)dataByte; + + db |= 1 << bitNumber; + + dataByte = (byte)db; + } + + /// + /// Sets the specified bit of the supplied byte to 0 + /// + /// + /// + public static void UnSetBit(int bitNumber, ref byte dataByte) + { + if (bitNumber < 0 || bitNumber > 7) + return; + + int db = (int)dataByte; + + db &= ~(1 << bitNumber); + + dataByte = (byte)db; + } + + /// + /// Returns a drive number (0-3) based on the first two bits of the supplied byte + /// + /// + /// + public static int GetUnitSelect(byte dataByte) + { + int driveNumber = dataByte & 0x03; + return driveNumber; + } + + /// + /// Sets the first two bits of a byte based on the supplied drive number (0-3) + /// + /// + /// + public static void SetUnitSelect(int driveNumber, ref byte dataByte) + { + switch (driveNumber) + { + case 0: + UnSetBit(SR0_US0, ref dataByte); + UnSetBit(SR0_US1, ref dataByte); + break; + case 1: + SetBit(SR0_US0, ref dataByte); + UnSetBit(SR0_US1, ref dataByte); + break; + case 2: + SetBit(SR0_US1, ref dataByte); + UnSetBit(SR0_US0, ref dataByte); + break; + case 3: + SetBit(SR0_US0, ref dataByte); + SetBit(SR0_US1, ref dataByte); + break; + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/AmstradGateArray.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/AmstradGateArray.cs new file mode 100644 index 0000000000..659ca850c5 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/AmstradGateArray.cs @@ -0,0 +1,1407 @@ +using BizHawk.Common; +using BizHawk.Common.NumberExtensions; +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// * Amstrad Gate Array * + /// http://www.cpcwiki.eu/index.php/Gate_Array + /// https://web.archive.org/web/20170612081209/http://www.grimware.org/doku.php/documentations/devices/gatearray + /// + public class AmstradGateArray : IPortIODevice, IVideoProvider + { + #region Devices + + private CPCBase _machine; + private Z80A CPU => _machine.CPU; + private CRCT_6845 CRCT => _machine.CRCT; + //private CRTDevice CRT => _machine.CRT; + private IPSG PSG => _machine.AYDevice; + private NECUPD765 FDC => _machine.UPDDiskDevice; + private DatacorderDevice DATACORDER => _machine.TapeDevice; + private ushort BUSRQ => CPU.MEMRQ[CPU.bus_pntr]; + public const ushort PCh = 1; + + private GateArrayType ChipType; + + #endregion + + #region Palettes + + /// + /// The standard CPC Pallete (ordered by firmware #) + /// http://www.cpcwiki.eu/index.php/CPC_Palette + /// + public static readonly int[] CPCFirmwarePalette = + { + Colors.ARGB(0x00, 0x00, 0x00), // Black + Colors.ARGB(0x00, 0x00, 0x80), // Blue + Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue + Colors.ARGB(0x80, 0x00, 0x00), // Red + Colors.ARGB(0x80, 0x00, 0x80), // Magenta + Colors.ARGB(0x80, 0x00, 0xFF), // Mauve + Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red + Colors.ARGB(0xFF, 0x00, 0x80), // Purple + Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta + Colors.ARGB(0x00, 0x80, 0x00), // Green + Colors.ARGB(0x00, 0x80, 0x80), // Cyan + Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue + Colors.ARGB(0x80, 0x80, 0x00), // Yellow + Colors.ARGB(0x80, 0x80, 0x80), // White + Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue + Colors.ARGB(0xFF, 0x80, 0x00), // Orange + Colors.ARGB(0xFF, 0x80, 0x80), // Pink + Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta + Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green + Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green + Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan + Colors.ARGB(0x80, 0xFF, 0x00), // Lime + Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green + Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan + Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow + Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow + Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White + }; + + /// + /// The standard CPC Pallete (ordered by hardware #) + /// http://www.cpcwiki.eu/index.php/CPC_Palette + /// + public static readonly int[] CPCHardwarePalette = + { + Colors.ARGB(0x80, 0x80, 0x80), // White + Colors.ARGB(0x80, 0x80, 0x80), // White (duplicate) + Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green + Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow + Colors.ARGB(0x00, 0x00, 0x80), // Blue + Colors.ARGB(0xFF, 0x00, 0x80), // Purple + Colors.ARGB(0x00, 0x80, 0x80), // Cyan + Colors.ARGB(0xFF, 0x80, 0x80), // Pink + Colors.ARGB(0xFF, 0x00, 0x80), // Purple (duplicate) + Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow (duplicate) + Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow + Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White + Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red + Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta + Colors.ARGB(0xFF, 0x80, 0x00), // Orange + Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta + Colors.ARGB(0x00, 0x00, 0x80), // Blue (duplicate) + Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green (duplicate) + Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green + Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan + Colors.ARGB(0x00, 0x00, 0x00), // Black + Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue + Colors.ARGB(0x00, 0x80, 0x00), // Green + Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue + Colors.ARGB(0x80, 0x00, 0x80), // Magenta + Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green + Colors.ARGB(0x80, 0xFF, 0x00), // Lime + Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan + Colors.ARGB(0x80, 0x00, 0x00), // Red + Colors.ARGB(0x80, 0x00, 0xFF), // Mauve + Colors.ARGB(0x80, 0x80, 0x00), // Yellow + Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue + }; + + #endregion + + #region Clocks and Timing + + /// + /// The Gate Array Clock Speed + /// + public int GAClockSpeed = 16000000; + + /// + /// The CPU Clock Speed + /// + public int Z80ClockSpeed = 4000000; + + /// + /// CRCT Clock Speed + /// + public int CRCTClockSpeed = 1000000; + + /// + /// AY-3-8912 Clock Speed + /// + public int PSGClockSpeed = 1000000; + + /// + /// Number of CPU cycles in one frame + /// + public int FrameLength = 79872; + + /// + /// Number of Gate Array cycles within one frame + /// + public int GAFrameLength = 319488; + + #endregion + + #region Construction + + public AmstradGateArray(CPCBase machine, GateArrayType chipType) + { + _machine = machine; + ChipType = chipType; + //PenColours = new int[17]; + borderType = _machine.CPC.SyncSettings.BorderType; + SetupScreenSize(); + //Reset(); + + CRCT.AttachHSYNCCallback(OnHSYNC); + CRCT.AttachVSYNCCallback(OnVSYNC); + + CurrentLine = new CharacterLine(); + InitByteLookup(); + CalculateNextScreenMemory(); + } + + #endregion + + #region Registers and Internal State + + /// + /// PENR (register 0) - Pen Selection + /// This register can be used to select one of the 17 color-registers (pen 0 to 15 or the border). + /// It will remain selected until another PENR command is executed. + /// PENR Index + /// 7 6 5 4 3 2 1 0 color register selected + /// 0 0 0 0 n n n n pen n from 0 to 15 (4bits) + /// 0 0 0 1 x x x x border + /// + /// x can be 0 or 1, it doesn't matter + /// + private byte _PENR; + public byte PENR + { + get { return _PENR; } + set + { + _PENR = value; + if (_PENR.Bit(4)) + { + // border select + CurrentPen = 16; + } + else + { + // pen select + CurrentPen = _PENR & 0x0f; + } + } + } + + /// + /// 0-15: Pen Registers + /// 16: Border Colour + /// + public int[] ColourRegisters = new int[17]; + + /// + /// The currently selected Pen + /// + private int CurrentPen; + + /// + /// INKR (register 1) - Colour Selection + /// This register takes a 5bits parameter which is a color-code. This color-code range from 0 to 31 but there's only 27 differents colors + /// (because the Gate Array use a 3-states logic on the R,G and B signals, thus 3x3x3=27). + /// INKR Color + /// 7 6 5 4 3 2 1 0 + /// 0 1 0 n n n n n where n is a color code (5 bits) + /// + /// The PEN affected by the INKR command is updated (almost) immediatly + /// + private byte _INKR; + public byte INKR + { + get { return _INKR; } + set + { + _INKR = value; + ColourRegisters[CurrentPen] = _INKR & 0x1f; + } + } + + /// + /// RMR (register 2) - Select screen mode and ROM configuration + /// This register control the interrupt counter (reset), the upper and lower ROM paging and the video mode. + /// RMR Commands + /// 7 6 5 4 3 2 1 0 + /// 1 0 0 I UR LR VM--> + /// + /// I : if set (1), this will reset the interrupt counter + /// UR : Enable (0) or Disable (1) the upper ROM paging (&C000 to &FFFF). You can select which upper ROM with the I/O address &DF00 + /// LR : Enable (0) or Disable (1) the lower ROM paging + /// VM : Select the video mode 0,1,2 or 3 (it will take effect after the next HSync) + /// + private byte _RMR; + public byte RMR + { + get { return _RMR; } + set + { + _RMR = value; + //ScreenMode = _RMR & 0x03; + var sm = _RMR & 0x03; + if (sm != 1) + { + + } + + if ((_RMR & 0x08) != 0) + _machine.UpperROMPaged = false; + else + _machine.UpperROMPaged = true; + + if ((_RMR & 0x04) != 0) + _machine.LowerROMPaged = false; + else + _machine.LowerROMPaged = true; + + if (_RMR.Bit(4)) + { + // reset interrupt counter + InterruptCounter = 0; + } + } + } + + /// + /// RAMR (register 3) - RAM Banking + /// This register exists only in CPCs with 128K RAM (like the CPC 6128, or CPCs with Standard Memory Expansions) + /// Note: In the CPC 6128, the register is a separate PAL that assists the Gate Array chip + /// + /// Bit Value Function + /// 7 1 Gate Array function 3 + /// 6 1 + /// 5 b 64K bank number(0..7); always 0 on an unexpanded CPC6128, 0-7 on Standard Memory Expansions + /// 4 b + /// 3 b + /// 2 x RAM Config(0..7) + /// 1 x "" + /// 0 x "" + /// + /// The 3bit RAM Config value is used to access the second 64K of the total 128K RAM that is built into the CPC 6128 or the additional 64K-512K of standard memory expansions. + /// These contain up to eight 64K ram banks, which are selected with bit 3-5. A standard CPC 6128 only contains bank 0. Normally the register is set to 0, so that only the + /// first 64K RAM are used (identical to the CPC 464 and 664 models). The register can be used to select between the following eight predefined configurations only: + /// + /// -Address- 0 1 2 3 4 5 6 7 + /// 0000-3FFF RAM_0 RAM_0 RAM_4 RAM_0 RAM_0 RAM_0 RAM_0 RAM_0 + /// 4000-7FFF RAM_1 RAM_1 RAM_5 RAM_3 RAM_4 RAM_5 RAM_6 RAM_7 + /// 8000-BFFF RAM_2 RAM_2 RAM_6 RAM_2 RAM_2 RAM_2 RAM_2 RAM_2 + /// C000-FFFF RAM_3 RAM_7 RAM_7 RAM_7 RAM_3 RAM_3 RAM_3 RAM_3 + /// + /// The Video RAM is always located in the first 64K, VRAM is in no way affected by this register + /// + private byte _RAMR; + /// + /// This is actually implemented outside of here. These values do nothing. + /// + public byte RAMR + { + get { return _RAMR; } + set + { + _RAMR = value; + } + } + + /// + /// The selected screen mode (updated after every HSYNC) + /// + private int ScreenMode; + + /// + /// Simulates the internal 6bit INT counter + /// + private int _interruptCounter; + public int InterruptCounter + { + get { return _interruptCounter; } + set { _interruptCounter = value; } + } + + /// + /// Interrupts are delayed when a VSYNC occurs + /// + private int VSYNCDelay; + + /// + /// Signals that the frame end has been reached + /// + public bool FrameEnd; + + /// + /// Internal phase clock + /// + private int ClockCounter; + + /// + /// Master frame clock counter + /// + public int FrameClock; + + /// + /// Simulates the gate array memory /WAIT line + /// + private bool WaitLine; + + /// + /// 16-bit address - read from the CRCT + /// + private short _MA; + private short MA + { + get + { + _MA = CRCT.MA; + return _MA; + } + } + + /// + /// Set when the HSYNC signal is detected from the CRCT + /// + private bool HSYNC; + + /// + /// Is set when an initial HSYNC is seen from the CRCT + /// On real hardware interrupt generation is based on the falling edge of the HSYNC signal + /// So in this emulation, once the falling edge is detected, interrupt processing happens + /// + //private bool HSYNC_falling; + + /// + /// Used to count HSYNCs during a VSYNC + /// + private int HSYNC_counter; + + /// + /// Set when the VSYNC signal is detected from the CRCT + /// + private bool VSYNC; + + /// + /// TRUE when the /INT pin is held low + /// + private bool InterruptRaised; + + /// + /// Counts the GA cycles that the /INT pin should be held low + /// + private int InterruptHoldCounter; + + /// + /// Set at the start of a new frame + /// + public bool IsNewFrame; + + /// + /// Set when a new line is beginning + /// + public bool IsNewLine; + + /// + /// Horizontal Character Counter + /// + private int HCC; + + /// + /// Vertical Line Counter + /// + private int VLC; + + /// + /// The first video byte fetched + /// + private byte VideoByte1; + + /// + /// The second video byte fetched + /// + private byte VideoByte2; + + #endregion + + #region Clock Business + + /// + /// Called every CPU cycle + /// In reality the GA is clocked at 16Mhz (4 times the frequency of the CPU) + /// Therefore this method has to take care of: + /// 4 GA cycles + /// 1 CRCT cycle every 4 calls + /// 1 PSG cycle every 4 calls + /// 1 CPU cycle (uncontended) + /// + public void ClockCycle() + { + // gatearray uses 4-phase clock to supply clocks to other devices + switch (ClockCounter) + { + case 0: + CRCT.ClockCycle(); + WaitLine = false; + break; + case 1: + WaitLine = true; + // detect new scanline and upcoming new frame on next render cycle + //FrameDetector(); + break; + case 2: + // video fetch + WaitLine = true; + //FetchByte(1); + break; + case 3: + // video fetch and render + WaitLine = true; + //FetchByte(2); + GACharacterCycle(); + //PixelGenerator(); + break; + } + + if (!HSYNC && CRCT.HSYNC) + { + HSYNC = true; + } + + // run the interrupt generator routine + InterruptGenerator(); + + if (!CRCT.HSYNC) + { + HSYNC = false; + } + + // conditional CPU cycle + DoConditionalCPUCycle(); + + AdvanceClock(); + } + + /// + /// Increments the internal clock counters by one + /// + private void AdvanceClock() + { + FrameClock++; + ClockCounter++; + + if (ClockCounter == 4) + ClockCounter = 0; + + // check for frame end + if (FrameClock == FrameLength) + { + FrameEnd = true; + } + } + + /// + /// Runs a 4 Mhz CPU cycle if neccessary + /// /WAIT line status is a factor here + /// + private void DoConditionalCPUCycle() + { + if (!WaitLine) + { + // /WAIT line is NOT active + CPU.ExecuteOne(); + return; + } + + // /WAIT line is active + switch (ClockCounter) + { + case 2: + case 3: + // gate array video fetch is occuring + // check for memory access + if (BUSRQ > 0) + { + // memory action upcoming - CPU clock is halted + CPU.TotalExecutedCycles++; + } + break; + + case 1: + // CPU accesses RAM if it's performing a non-opcode read or write + // assume for now that an opcode fetch is always looking at PC + if (BUSRQ == PCh) + { + // opcode fetch memory action upcoming - CPU clock is halted + CPU.TotalExecutedCycles++; + } + else + { + // no fetch, or non-opcode fetch + CPU.ExecuteOne(); + } + break; + } + } + + #endregion + + #region Frame & Interrupt Handling + + /// + /// The CRCT builds the picture in a strange way, so that the top left of the display area is the first pixel from + /// video RAM. The borders come either side of the HSYNC and VSYNCs later on: + /// https://web.archive.org/web/20170501112330im_/http://www.grimware.org/lib/exe/fetch.php/documentations/devices/crtc.6845/crtc.standard.video.frame.png?w=800&h=500 + /// Therefore when the gate array initialises, we will attempt end the frame early in order to + /// sync up at the point where VSYNC is active and HSYNC just begins. This is roughly how a CRT monitor would display the picture. + /// The CRT would start a new line at the point where an HSYNC is detected. + /// + private void FrameDetector() + { + if (CRCT.HSYNC && !IsNewLine) + { + // start of a new line on the next render cycle + IsNewLine = true; + + // process scanline + //CRT.CurrentLine.CommitScanline(); + + // check for end of frame + if (CRCT.VSYNC && !IsNewFrame) + { + // start of a new frame on the next render cycle + IsNewFrame = true; + //FrameEnd = true; + VLC = 0; + } + else if (!CRCT.VSYNC) + { + // increment line counter + VLC++; + IsNewFrame = false; + } + + HCC = 0; + + // update screenmode + //ScreenMode = RMR & 0x03; + //CRT.CurrentLine.InitScanline(ScreenMode, VLC); + } + else if (!CRCT.HSYNC) + { + // reset the flags + IsNewLine = false; + IsNewFrame = false; + } + } + + /// + /// Handles interrupt generation + /// + private void InterruptGenerator() + { + if (HSYNC && !CRCT.HSYNC) + { + // falling edge of the HSYNC detected + InterruptCounter++; + + if (CRCT.VSYNC) + { + if (HSYNC_counter >= 2) + { + // x2 HSYNC have happened during VSYNC + if (InterruptCounter >= 32) + { + // no interrupt + InterruptCounter = 0; + } + else if (InterruptCounter < 32) + { + // interrupt + InterruptRaised = true; + InterruptCounter = 0; + } + + HSYNC_counter = 0; + } + else + { + HSYNC_counter++; + } + } + + if (InterruptCounter == 52) + { + // gatearray should raise an interrupt + InterruptRaised = true; + InterruptCounter = 0; + } + } + + if (InterruptRaised) + { + // interrupt should been raised + CPU.FlagI = true; + InterruptHoldCounter++; + + // the INT signal should be held low for 1.4us. + // in gatearray cycles, this equates to 22.4 + // we will round down to 22 for emulation purposes + if (InterruptHoldCounter >= 22) + { + CPU.FlagI = false; + InterruptRaised = false; + InterruptHoldCounter = 0; + } + } + } + + #endregion + + #region Rendering Business + + /// + /// Builds up current scanline character information + /// Ther GA modifies HSYNC and VSYNC signals before they are sent to the monitor + /// This is handled here + /// Runs at 1Mhz + /// + private void GACharacterCycle() + { + if (CRCT.VSYNC && CRCT.HSYNC) + { + // both hsync and vsync active + CurrentLine.AddCharacter(Phase.HSYNCandVSYNC); + } + else if (CRCT.VSYNC) + { + // vsync is active but hsync is not + CurrentLine.AddCharacter(Phase.VSYNC); + } + else if (CRCT.HSYNC) + { + // hsync is active but vsync is not + CurrentLine.AddCharacter(Phase.HSYNC); + } + else if (!CRCT.DISPTMG) + { + // border generation + CurrentLine.AddCharacter(Phase.BORDER); + } + else if (CRCT.DISPTMG) + { + // pixels generated from video RAM + CurrentLine.AddCharacter(Phase.DISPLAY); + } + } + + /// + /// Holds the upcoming video RAM addresses for the next scanline + /// Firmware default size is 80 (40 characters - 2 bytes per character) + /// + private ushort[] NextVidRamLine = new ushort[40 * 2]; + + /// + /// The current character line we are working from + /// + private CharacterLine CurrentLine; + + /// + /// List of screen lines as they are built up + /// + private List ScreenLines = new List(); + + /// + /// Pixel value lookups for every scanline byte value + /// Based on the lookup at https://github.com/gavinpugh/xnacpc + /// + private int[][] ByteLookup = new int[4][]; + private void InitByteLookup() + { + int pix; + for (int m = 0; m < 4; m++) + { + int pos = 0; + ByteLookup[m] = new int[256 * 8]; + for (int b = 0; b < 256; b++) + { + switch (m) + { + case 0: + pix = b & 0xaa; + pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2)); + for (int c = 0; c < 4; c++) + ByteLookup[m][pos++] = pix; + pix = b & 0x55; + pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3)); + for (int c = 0; c < 4; c++) + ByteLookup[m][pos++] = pix; + break; + case 1: + pix = (((b & 0x80) >> 7) | ((b & 0x08) >> 2)); + ByteLookup[m][pos++] = pix; + ByteLookup[m][pos++] = pix; + pix = (((b & 0x40) >> 6) | ((b & 0x04) >> 1)); + ByteLookup[m][pos++] = pix; + ByteLookup[m][pos++] = pix; + pix = (((b & 0x20) >> 5) | (b & 0x02)); + ByteLookup[m][pos++] = pix; + ByteLookup[m][pos++] = pix; + pix = (((b & 0x10) >> 4) | ((b & 0x01) << 1)); + ByteLookup[m][pos++] = pix; + ByteLookup[m][pos++] = pix; + break; + case 2: + for (int i = 7; i >= 0; i--) + ByteLookup[m][pos++] = ((b & (1 << i)) != 0) ? 1 : 0; + break; + case 3: + pix = b & 0xaa; + pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2)); + for (int c = 0; c < 4; c++) + ByteLookup[m][pos++] = pix; + pix = b & 0x55; + pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3)); + for (int c = 0; c < 4; c++) + ByteLookup[m][pos++] = pix; + break; + } + } + } + } + + /// + /// Runs at HSYNC *AFTER* the scanline has been commmitted + /// Sets up the upcoming memory addresses for the next scanline + /// + private void CalculateNextScreenMemory() + { + var verCharCount = CRCT.VCC; + var verRasCount = CRCT.VLC; + + var screenWidthByteCount = CRCT.DisplayWidth * 2; + NextVidRamLine = new ushort[screenWidthByteCount * 2]; + var screenHeightCharCount = CRCT.DisplayHeightInChars; + var screenAddress = CRCT.MA; + + int baseAddress = ((screenAddress << 2) & 0xf000); + int offset = (screenAddress * 2) & 0x7ff; + + int x = offset + ((verCharCount * screenWidthByteCount) & 0x7ff); + int y = baseAddress + (verRasCount * 0x800); + + for (int b = 0; b < screenWidthByteCount; b++) + { + NextVidRamLine[b] = (ushort)(y + x); + x++; + x &= 0x7ff; + } + } + + /// + /// Called at the start of HSYNC, this renders the currently built-up scanline + /// + private void RenderScanline() + { + // memory addresses + int cRow = CRCT.VCC; + int cRas = CRCT.VLC; + + int screenByteWidth = CRCT.DisplayWidth * 2; + var screenHeightCharCount = CRCT.DisplayHeightInChars; + //CalculateNextScreenMemory(); + var crctAddr = CRCT.DStartHigh << 8; + crctAddr |= CRCT.DStartLow; + var baseAddr = ((crctAddr << 2) & (0xF000)); //CRCT.VideoPageBase;// + var baseOffset = (crctAddr * 2) & 0x7FF; //CRCT.VideoRAMOffset * 2; // + var xA = baseOffset + ((cRow * screenByteWidth) & 0x7ff); + var yA = baseAddr + (cRas * 2048); + + // border and display + int pix = 0; + int scrByte = 0; + + for (int i = 0; i < CurrentLine.PhaseCount; i++) + { + // every character renders 8 pixels + switch (CurrentLine.Phases[i]) + { + case Phase.NONE: + break; + + case Phase.HSYNC: + break; + case Phase.HSYNCandVSYNC: + break; + case Phase.VSYNC: + break; + case Phase.BORDER: + // output current border colour + for (pix = 0; pix < 16; pix++) + { + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[16]]); + } + break; + case Phase.DISPLAY: + // each character references 2 bytes in video RAM + byte data; + + for (int by = 0; by < 2; by++) + { + ushort addr = (ushort)(yA + xA); + data = _machine.FetchScreenMemory(addr); + scrByte++; + + switch (CurrentLine.ScreenMode) + { + case 0: + pix = data & 0xaa; + pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2)); + for (int c = 0; c < 4; c++) + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + pix = data & 0x55; + pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3)); + for (int c = 0; c < 4; c++) + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + break; + case 1: + pix = (((data & 0x80) >> 7) | ((data & 0x08) >> 2)); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + pix = (((data & 0x40) >> 6) | ((data & 0x04) >> 1)); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + pix = (((data & 0x20) >> 5) | (data & 0x02)); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + pix = (((data & 0x10) >> 4) | ((data & 0x01) << 1)); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + break; + case 2: + pix = data; + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(7) ? 1 : 0]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(6) ? 1 : 0]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(5) ? 1 : 0]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(4) ? 1 : 0]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(3) ? 1 : 0]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(2) ? 1 : 0]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(1) ? 1 : 0]]); + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix.Bit(0) ? 1 : 0]]); + break; + case 3: + pix = data & 0xaa; + pix = (((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02) << 2)); + for (int c = 0; c < 4; c++) + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + pix = data & 0x55; + pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01) << 3)); + for (int c = 0; c < 4; c++) + CurrentLine.Pixels.Add(CPCHardwarePalette[ColourRegisters[pix]]); + break; + } + + xA++; + xA &= 0x7ff; + } + + break; + } + } + + // add to the list + ScreenLines.Add(new CharacterLine + { + ScreenMode = CurrentLine.ScreenMode, + Phases = CurrentLine.Phases.ToList(), + Pixels = CurrentLine.Pixels.ToList() + }); + } + + #endregion + + #region Public Methods + + /// + /// Called when the Z80 acknowledges an interrupt + /// + public void IORQA() + { + // bit 5 of the interrupt counter is reset + InterruptCounter &= ~(1 << 5); + } + + private int slCounter = 0; + private int slBackup = 0; + + /// + /// Fired when the CRCT flags HSYNC + /// + public void OnHSYNC() + { + HSYNC = true; + slCounter++; + + // commit the scanline + RenderScanline(); + + // setup vid memory for next scanline + CalculateNextScreenMemory(); + + if (CRCT.VLC == 0) + { + // update screenmode + ScreenMode = _RMR & 0x03; + } + + // setup scanline for next + CurrentLine.Clear(ScreenMode); + } + + /// + /// Fired when the CRCT flags VSYNC + /// + public void OnVSYNC() + { + FrameEnd = true; + slBackup = slCounter; + slCounter = 0; + } + + #endregion + + #region IVideoProvider + + public int[] ScreenBuffer; + + private int _virtualWidth; + private int _virtualHeight; + private int _bufferWidth; + private int _bufferHeight; + + public int BackgroundColor + { + get { return CPCHardwarePalette[0]; } + } + + public int VirtualWidth + { + get { return _virtualWidth; } + set { _virtualWidth = value; } + } + + public int VirtualHeight + { + get { return _virtualHeight; } + set { _virtualHeight = value; } + } + + public int BufferWidth + { + get { return _bufferWidth; } + set { _bufferWidth = value; } + } + + public int BufferHeight + { + get { return _bufferHeight; } + set { _bufferHeight = value; } + } + + public int SysBufferWidth; + public int SysBufferHeight; + + public int VsyncNumerator + { + get { return 200000000; } + set { } + } + + public int VsyncDenominator + { + get { return Z80ClockSpeed; } + } + + public int[] GetVideoBuffer() + { + // get only lines that have pixel data + var lines = ScreenLines.Where(a => a.Pixels.Count > 0).ToList(); + var height = lines.Count(); + + int pos = 0; + int lCount = 0; + foreach (var l in lines) + { + var lCop = l.Pixels.ToList(); + var len = l.Pixels.Count; + if (l.Phases.Contains(Phase.VSYNC) && l.Phases.Contains(Phase.BORDER)) + continue; + + if (len < 320) + continue; + + var pad = BufferWidth - len; + if (pad < 0) + { + // trim the left and right + var padPos = pad * -1; + var excessL = padPos / 2; + var excessR = excessL + (padPos % 2); + for (int i = 0; i < excessL; i++) + { + var lThing = lCop.First(); + + lCop.Remove(lThing); + } + for (int i = 0; i < excessL; i++) + { + var lThing = lCop.Last(); + + lCop.Remove(lThing); + } + } + + var lPad = pad / 2; + var rPad = lPad + (pad % 2); + + for (int i = 0; i < 2; i++) + { + lCount++; + + for (int pL = 0; pL < lPad; pL++) + { + ScreenBuffer[pos++] = 0; + } + + for (int pix = 0; pix < lCop.Count; pix++) + { + ScreenBuffer[pos++] = lCop[pix]; + } + + for (int pR = 0; pR < rPad; pR++) + { + ScreenBuffer[pos++] = 0; + } + } + + if (lCount >= BufferHeight - 2) + { + break; + } + } + + ScreenLines.Clear(); + + return ScreenBuffer; + + switch (borderType) + { + // crop to 768x272 (544) + // borders 64px - 64 scanlines + case AmstradCPC.BorderType.Uniform: + /* + var slSize = 64; + var dispLines = (24 * 8) * 2; + var origTopBorder = (7 * 8) * 2; + var origBotBorder = (5 * 8) * 2; + + var lR = 16; + var rR = 16; + + var trimTop = origTopBorder - slSize; + + var startP = SysBufferWidth * (origTopBorder - 64); + var index1 = 0; + + // line by line + int cnt = 0; + for (int line = startP; line < ScreenBuffer.Length; line += SysBufferWidth) + { + cnt++; + // pixels in line + for (int p = lR; p < SysBufferWidth - rR; p++) + { + if (index1 == croppedBuffer.Length) + break; + + croppedBuffer[index1++] = ScreenBuffer[line + p]; + } + } + return croppedBuffer; + */ + + var slWidth = BufferWidth; + return ScreenBuffer; + + break; + + } + + return ScreenBuffer; + } + + public void SetupScreenSize() + { + SysBufferWidth = 800; + SysBufferHeight = 600; + BufferHeight = SysBufferHeight; + BufferWidth = SysBufferWidth; + VirtualHeight = BufferHeight; + VirtualWidth = BufferWidth; + ScreenBuffer = new int[BufferWidth * BufferHeight]; + croppedBuffer = ScreenBuffer; + + switch (borderType) + { + case AmstradCPC.BorderType.Uncropped: + break; + + case AmstradCPC.BorderType.Uniform: + BufferWidth = 800; + BufferHeight = 600; + VirtualHeight = BufferHeight; + VirtualWidth = BufferWidth; + croppedBuffer = new int[BufferWidth * BufferHeight]; + break; + + case AmstradCPC.BorderType.Widescreen: + break; + } + } + + protected int[] croppedBuffer; + + private AmstradCPC.BorderType _borderType; + + public AmstradCPC.BorderType borderType + { + get { return _borderType; } + set { _borderType = value; } + } + + #endregion + + #region IPortIODevice + + /// + /// Device responds to an IN instruction + /// + /// + /// + /// + public bool ReadPort(ushort port, ref int result) + { + // gate array is OUT only + return false; + } + + /// + /// Device responds to an OUT instruction + /// + /// + /// + /// + public bool WritePort(ushort port, int result) + { + BitArray portBits = new BitArray(BitConverter.GetBytes(port)); + BitArray dataBits = new BitArray(BitConverter.GetBytes((byte)result)); + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + // The gate array is selected when bit 15 of the I/O port address is set to "0" and bit 14 of the I/O port address is set to "1" + bool accessed = false; + if (!portUpper.Bit(7) && portUpper.Bit(6)) + accessed = true; + + if (!accessed) + return accessed; + + // Bit 9 and 8 of the data byte define the function to access + if (!dataBits[6] && !dataBits[7]) + { + // select pen + PENR = (byte)result; + } + + if (dataBits[6] && !dataBits[7]) + { + // select colour for selected pen + INKR = (byte)result; + } + + if (!dataBits[6] && dataBits[7]) + { + // select screen mode, ROM configuration and interrupt control + RMR = (byte)result; + } + + if (dataBits[6] && dataBits[7]) + { + // RAM memory management + RAMR = (byte)result; + } + + return true; + } + + #endregion + + #region Serialization + + public void SyncState(Serializer ser) + { + ser.BeginSection("GateArray"); + ser.SyncEnum("ChipType", ref ChipType); + ser.Sync("_PENR", ref _PENR); + ser.Sync("_INKR", ref _INKR); + ser.Sync("_RMR", ref _RMR); + ser.Sync("_RAMR", ref _RAMR); + ser.Sync("ColourRegisters", ref ColourRegisters, false); + ser.Sync("CurrentPen", ref CurrentPen); + ser.Sync("ClockCounter", ref ClockCounter); + ser.Sync("FrameClock", ref FrameClock); + ser.Sync("FrameEnd", ref FrameEnd); + ser.Sync("WaitLine", ref WaitLine); + ser.Sync("_interruptCounter", ref _interruptCounter); + ser.Sync("VSYNCDelay", ref VSYNCDelay); + ser.Sync("ScreenMode", ref ScreenMode); + ser.Sync("HSYNC", ref HSYNC); + //ser.Sync("HSYNC_falling", ref HSYNC_falling); + ser.Sync("HSYNC_counter", ref HSYNC_counter); + ser.Sync("VSYNC", ref VSYNC); + ser.Sync("InterruptRaised", ref InterruptRaised); + ser.Sync("InterruptHoldCounter", ref InterruptHoldCounter); + ser.Sync("_MA", ref _MA); + ser.Sync("IsNewFrame", ref IsNewFrame); + ser.Sync("IsNewLine", ref IsNewLine); + ser.Sync("HCC", ref HCC); + ser.Sync("VLC", ref VLC); + ser.Sync("VideoByte1", ref VideoByte1); + ser.Sync("VideoByte2", ref VideoByte2); + ser.Sync("NextVidRamLine", ref NextVidRamLine, false); + ser.EndSection(); + } + + #endregion + + #region Enums, Classes & Lookups + + /// + /// Represents a single scanline (in characters) + /// + public class CharacterLine + { + /// + /// Screenmode is defined at each HSYNC (start of a new character line) + /// Therefore we pass the mode in via constructor + /// + /// + //public CharacterLine(int screenMode) + //{ + //ScreenMode = screenMode; + //} + + public int ScreenMode = 1; + public List Phases = new List(); + public List Pixels = new List(); + + /// + /// Adds a new horizontal character to the list + /// + /// + public void AddCharacter(Phase phase) + { + Phases.Add(phase); + } + + public int PhaseCount + { + get { return Phases.Count(); } + } + + public void Clear(int screenMode) + { + ScreenMode = screenMode; + Phases.Clear(); + Pixels.Clear(); + } + } + + [Flags] + public enum Phase : int + { + /// + /// Nothing + /// + NONE = 0, + /// + /// Border is being rendered + /// + BORDER = 1, + /// + /// Display rendered from video RAM + /// + DISPLAY = 2, + /// + /// HSYNC in progress + /// + HSYNC = 3, + /// + /// VSYNC in process + /// + VSYNC = 4, + /// + /// HSYNC occurs within a VSYNC + /// + HSYNCandVSYNC = 5 + } + + public enum GateArrayType + { + /// + /// CPC 464 + /// The first version of the Gate Array is the 40007 and was released with the CPC 464 + /// + Amstrad40007, + /// + /// CPC 664 + /// Later, the CPC 664 came out fitted with the 40008 version (and at the same time, the CPC 464 was also upgraded with this version). + /// This version is pinout incompatible with the 40007 (that's why the upgraded 464 of this period have two Gate Array slots on the motherboard, + /// one for a 40007 and one for a 40008) + /// + Amstrad40008, + /// + /// CPC 6128 + /// The CPC 6128 was released with the 40010 version (and the CPC 464 and 664 manufactured at that time were also upgraded to this version). + /// The 40010 is pinout compatible with the previous 40008 + /// + Amstrad40010, + /// + /// Costdown CPC + /// In the last serie of CPC 464 and 6128 produced by Amstrad in 1988, a small ASIC chip have been used to reduce the manufacturing costs. + /// This ASIC emulates the Gate Array, the PAL and the CRTC 6845. And no, there is no extra features like on the Amstrad Plus. + /// The only noticeable difference seems to be about the RGB output levels which are not exactly the same than those produced with a real Gate Array + /// + Amstrad40226, + /// + /// Plus & GX-4000 + /// All the Plus range is built upon a bigger ASIC chip which is integrating many features of the classic CPC (FDC, CRTC, PPI, Gate Array/PAL) and all + /// the new Plus specific features. The Gate Array on the Plus have a new register, named RMR2, to expand the ROM mapping functionnalities of the machine. + /// This register requires to be unlocked first to be available. And finally, the RGB levels produced by the ASIC on the Plus are noticeably differents + /// + Amstrad40489, + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/CRCTChip.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/CRCTChip.cs new file mode 100644 index 0000000000..2cda19e091 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/CRCTChip.cs @@ -0,0 +1,832 @@ +using BizHawk.Common; +using BizHawk.Common.NumberExtensions; +using System.Collections; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CRT CONTROLLER (CRTC) + /// http://archive.pcjs.org/pubs/pc/datasheets/MC6845-CRT.pdf + /// http://www.cpcwiki.eu/imgs/c/c0/Hd6845.hitachi.pdf + /// http://www.cpcwiki.eu/imgs/1/13/Um6845.umc.pdf + /// http://www.cpcwiki.eu/imgs/b/b5/Um6845r.umc.pdf + /// http://www.cpcwiki.eu/index.php/CRTC + /// + public class CRCTChip + { + #region Devices + + private CPCBase _machine { get; set; } + private CRCTType ChipType; + + #endregion + + #region Construction + + public CRCTChip(CRCTType chipType, CPCBase machine) + { + _machine = machine; + ChipType = chipType; + //Reset(); + } + + #endregion + + #region Output Lines + + // State output lines + /// + /// This TTL compatible output is an active high signal which drives the monitor directly or is fed to Video Processing Logic for composite generation. + /// This signal determines the vertical position of the displayed text. + /// + public bool VSYNC { get { return _VSYNC; } } + /// + /// This TTL compatible output is an active high signal which drives the monitor directly or is fed to Video Processing Logic for composite generation. + /// This signal determines the horizontal position of the displayed text. + /// + public bool HSYNC { get { return _HSYNC; } } + /// + /// This TTL compatible output is an active high signal which indicates the CRTC is providing addressing in the active Display Area. + /// + public bool DisplayEnable { get { return DisplayEnable; } } + /// + /// This TTL compatible output indicates Cursor Display to external Video Processing Logic.Active high signal. + /// + public bool Cursor { get { return _Cursor; } } + + private bool _VSYNC; + private bool _HSYNC; + private bool _DisplayEnable; + private bool _Cursor; + + // Refresh memory addresses + /* + Refresh Memory Addresses (MAO-MA13) -These 14 outputs are used to refresh the CRT screen with pages of + data located within a 16K block of refresh memory. These outputs drive a TTL load and 30pF. A high level on + MAO-MA 13 is a logical "1." + */ + public bool MA0 { get { return LinearAddress.Bit(0); } } + public bool MA1 { get { return LinearAddress.Bit(1); } } + public bool MA2 { get { return LinearAddress.Bit(2); } } + public bool MA3 { get { return LinearAddress.Bit(3); } } + public bool MA4 { get { return LinearAddress.Bit(4); } } + public bool MA5 { get { return LinearAddress.Bit(5); } } + public bool MA6 { get { return LinearAddress.Bit(6); } } + public bool MA7 { get { return LinearAddress.Bit(7); } } + public bool MA8 { get { return LinearAddress.Bit(8); } } + public bool MA9 { get { return LinearAddress.Bit(9); } } + public bool MA10 { get { return LinearAddress.Bit(10); } } // cpcwiki would suggest that this isnt connected in the CPC range + public bool MA11 { get { return LinearAddress.Bit(11); } } // cpcwiki would suggest that this isnt connected in the CPC range + public bool MA12 { get { return LinearAddress.Bit(12); } } // cpcwiki would suggest that this is connected in the CPC range but not used + public bool MA13 { get { return LinearAddress.Bit(13); } } // cpcwiki would suggest that this is connected in the CPC range but not used + + // Row addresses for character generators + /* + Raster Addresses (RAO-RA4) - These 5 outputs from the internal Raster Counter address the Character ROM + for the row of a character. These outputs drive a TTL load and 30pF. A high level (on RAO-RA4) is a logical "1." + */ + public bool RA0 { get { return ScanLineCTR.Bit(0); } } + public bool RA1 { get { return ScanLineCTR.Bit(1); } } + public bool RA2 { get { return ScanLineCTR.Bit(2); } } + public bool RA3 { get { return ScanLineCTR.Bit(3); } } // cpcwiki would suggest that this isnt connected in the CPC range + public bool RA4 { get { return ScanLineCTR.Bit(4); } } // cpcwiki would suggest that this isnt connected in the CPC range + + /// + /// Built from R12, R13 and CLK + /// This is a logical emulator output and is how the CPC gatearray would translate the lines + /* + Memory Address Signal Signal source Signal name + A15 6845 MA13 + A14 6845 MA12 + A13 6845 RA2 + A12 6845 RA1 + A11 6845 RA0 + A10 6845 MA9 + A9 6845 MA8 + A8 6845 MA7 + A7 6845 MA6 + A6 6845 MA5 + A5 6845 MA4 + A4 6845 MA3 + A3 6845 MA2 + A2 6845 MA1 + A1 6845 MA0 + A0 Gate-Array CLK + + */ + /// + public ushort AddressLine + { + get + { + BitArray MA = new BitArray(16); + MA[0] = _CLK; + MA[1] = MA0; + MA[2] = MA1; + MA[3] = MA2; + MA[4] = MA3; + MA[5] = MA4; + MA[6] = MA5; + MA[7] = MA6; + MA[8] = MA7; + MA[9] = MA8; + MA[10] = MA9; + MA[11] = RA0; + MA[12] = RA1; + MA[13] = RA2; + MA[14] = MA12; + MA[15] = MA13; + ushort[] array = new ushort[1]; + MA.CopyTo(array, 0); + return array[0]; + } + } + + #endregion + + #region Input Lines + + /// + /// This TTL compatible output indicates Cursor Display to external Video Processing Logic.Active high signal. + /// + public bool CLK { get { return _CLK; } } + /// + /// The RES input is used to Reset the CRTC. An input low level on RES forces CRTC into following status: + /// (A) All the counters in CRTC are cleared and the device stops the display operation. + /// (C) Control registers in CRTC are not affected and remain unchanged. + /// This signal is different from other M6800 family in the following functions: + /// (A) RES signal has capability of reset function only. when LPSTB is at low level. + /// (B) After RES has gone down to low level, output s ignals of MAO -MA13 and RAO - RA4, synchronizing with CLK low level, goes down to low level. + /// (At least 1 cycle CLK signal is necessary for reset.) + /// (C) The CRTC starts the Display operation immediately after the release of RES signal. + /// + public bool RESET { get { return _RESET; } } + /// + /// Light Pen Strobe (LPSTR) - This high impedance TTLIMOS compatible input latches the cu rrent Refresh Addresses in the Register File. + /// Latching is on the low to high edge and is synchronized internally to character clock. + /// + public bool LPSTB { get { return _LPSTB; } } + + private bool _CLK; + private bool _RESET; + private bool _LPSTB; + + #endregion + + #region Internal Registers + + /// + /// The currently selected register + /// + private byte AddressRegister; + + /// + /// The internal register + /// The Address Register is a 5 bit write-only register used as an "indirect" or "pointer" register. + /// Its contents are the address of one of the other 18 registers in the file.When RS and CS are low, + /// the Address Register itself is addressed.When RS is high, the Register File is accessed. + /// + private byte[] Register = new byte[18]; + + // Horizontal timing register constants + /// + /// This 8 bit write-only register determines the horizontal frequency of HS. + /// It is the total of displayed plus non-displayed character time units minus one. + /// + private const int H_TOTAL = 0; + /// + /// This 8 bit write-only register determines the number of displayed characters per horizontal line. + /// + private const int H_DISPLAYED = 1; + /// + /// This 8 bit write-only register determines the horizontal sync postiion on the horizontal line. + /// + private const int H_SYNC_POS = 2; + /// + /// This 4 bit write-only register determines the width of the HS pulse. It may not be apparent why this width needs to be programmed.However, + /// consider that all timing widths must be programmed as multiples of the character clock period which varies.If HS width were fixed as an integral + /// number of character times, it would vary with character rate and be out of tolerance for certain monitors. + /// The rate programmable feature allows compensating HS width. + /// NOTE: Dependent on chiptype this also may include VSYNC width - check the UpdateWidths() method + /// + private const int SYNC_WIDTHS = 3; + + // Vertical timing register constants + /// + /// The vertical frequency of VS is determined by both R4 and R5.The calculated number of character I ine times is usual I y an integer plus a fraction to + /// get exactly a 50 or 60Hz vertical refresh rate. The integer number of character line times minus one is programmed in the 7 bit write-only Vertical Total Register; + /// the fraction is programmed in the 5 bit write-only Vertical Scan Adjust Register as a number of scan line times. + /// + private const int V_TOTAL = 4; + private const int V_TOTAL_ADJUST = 5; + /// + /// This 7 bit write-only register determines the number of displayed character rows on the CRT screen, and is programmed in character row times. + /// + private const int V_DISPLAYED = 6; + /// + /// This 7 bit write-only register determines the vertical sync position with respect to the reference.It is programmed in character row times. + /// + private const int V_SYNC_POS = 7; + /// + /// This 2 bit write-only register controls the raster scan mode(see Figure 11 ). When bit 0 and bit 1 are reset, or bit 0 is reset and bit 1 set, + /// the non· interlace raster scan mode is selected.Two interlace modes are available.Both are interlaced 2 fields per frame.When bit 0 is set and bit 1 is reset, + /// the interlace sync raster scan mode is selected.Also when bit 0 and bit 1 are set, the interlace sync and video raster scan mode is selected. + /// + private const int INTERLACE_MODE = 8; + /// + /// This 5 bit write·only register determines the number of scan lines per character row including spacing. + /// The programmed value is a max address and is one less than the number of scan l1nes. + /// + private const int MAX_SL_ADDRESS = 9; + + // Other register constants + /// + /// This 7 bit write-only register controls the cursor format(see Figure 10). Bit 5 is the blink timing control.When bit 5 is low, the blink frequency is 1/16 of the + /// vertical field rate, and when bit 5 is high, the blink frequency is 1/32 of the vertical field rate.Bit 6 is used to enable a blink. + /// The cursor start scan line is set by the lower 5 bits. + /// + private const int CURSOR_START = 10; + /// + /// This 5 bit write-only register sets the cursor end scan line + /// + private const int CURSOR_END = 11; + /// + /// Start Address Register is a 14 bit write-only register which determines the first address put out as a refresh address after vertical blanking. + /// It consists of an 8 bit lower register, and a 6 bit higher register. + /// + private const int START_ADDR_H = 12; + private const int START_ADDR_L = 13; + /// + /// This 14 bit read/write register stores the cursor location.This register consists of an 8 bit lower and 6 bit higher register. + /// + private const int CURSOR_H = 14; + private const int CURSOR_L = 15; + /// + /// This 14 bit read -only register is used to store the contents of the Address Register(H & L) when the LPSTB input pulses high. + /// This register consists of an 8 bit lower and 6 bit higher register. + /// + private const int LIGHT_PEN_H = 16; + private const int LIGHT_PEN_L = 17; + + #endregion + + #region Internal Fields & Properties + + /// + /// Calculated when set based on R3 + /// + private int HSYNCWidth; + /// + /// Calculated when set based on R3 + /// + private int VSYNCWidth; + + /// + /// Character pos address (0 index). + /// Feeds the MA lines + /// + private int LinearAddress; + + /// + /// The currently selected Interlace Mode (based on R8) + /// + private InterlaceMode CurrentInterlaceMode + { + get + { + if (!Register[INTERLACE_MODE].Bit(0)) + { + return InterlaceMode.NormalSyncMode; + } + else if (Register[INTERLACE_MODE].Bit(0)) + { + if (Register[INTERLACE_MODE].Bit(1)) + { + return InterlaceMode.InterlaceSyncAndVideoMode; + } + else + { + return InterlaceMode.InterlaceSyncMode; + } + } + + return InterlaceMode.NormalSyncMode; + } + } + + /// + /// The current cursor display mode (based on R14 & R15) + /// + private CursorControl CurrentCursorMode + { + get + { + if (!Register[CURSOR_START].Bit(6) && !Register[CURSOR_START].Bit(5)) + { + return CursorControl.NonBlink; + } + else if (!Register[CURSOR_START].Bit(6) && Register[CURSOR_START].Bit(5)) + { + return CursorControl.CursorNonDisplay; + } + else if (Register[CURSOR_START].Bit(6) && !Register[CURSOR_START].Bit(5)) + { + return CursorControl.Blink1_16; + } + else + { + return CursorControl.Blink1_32; + } + } + } + + // Counter microchips + private int HorizontalCTR { get { return _HorizontalCTR; } + set + { + if (value > 255) + _HorizontalCTR = value - 255; + } + } + + private int HorizontalSyncWidthCTR + { + get { return _HorizontalSyncWidthCTR; } + set + { + if (value > 15) + _HorizontalSyncWidthCTR = value - 15; + } + } + + private int CharacterRowCTR + { + get { return CharacterRowCTR; } + set + { + if (value > 127) + _CharacterRowCTR = value - 127; + } + } + + private int ScanLineCTR + { + get { return ScanLineCTR; } + set + { + if (value > 31) + _ScanLineCTR = value - 31; + } + } + + private int _HorizontalCTR; + private int _HorizontalSyncWidthCTR; + private int _CharacterRowCTR; + private int _ScanLineCTR; + + #endregion + + #region Databus Interface + /* + RegIdx Register Name Type + 0 1 2 3 4 + 0 Horizontal Total Write Only Write Only Write Only (note 2) (note 3) + 1 Horizontal Displayed Write Only Write Only Write Only (note 2) (note 3) + 2 Horizontal Sync Position Write Only Write Only Write Only (note 2) (note 3) + 3 H and V Sync Widths Write Only Write Only Write Only (note 2) (note 3) + 4 Vertical Total Write Only Write Only Write Only (note 2) (note 3) + 5 Vertical Total Adjust Write Only Write Only Write Only (note 2) (note 3) + 6 Vertical Displayed Write Only Write Only Write Only (note 2) (note 3) + 7 Vertical Sync position Write Only Write Only Write Only (note 2) (note 3) + 8 Interlace and Skew Write Only Write Only Write Only (note 2) (note 3) + 9 Maximum Raster Address Write Only Write Only Write Only (note 2) (note 3) + 10 Cursor Start Raster Write Only Write Only Write Only (note 2) (note 3) + 11 Cursor End Raster Write Only Write Only Write Only (note 2) (note 3) + 12 Disp. Start Address (High) Read/Write Write Only Write Only Read/Write (note 2) (note 3) + 13 Disp. Start Address (Low) Read/Write Write Only Write Only Read/Write (note 2) (note 3) + 14 Cursor Address (High) Read/Write Read/Write Read/Write Read/Write (note 2) (note 3) + 15 Cursor Address (Low) Read/Write Read/Write Read/Write Read/Write (note 2) (note 3) + 16 Light Pen Address (High) Read Only Read Only Read Only Read Only (note 2) (note 3) + 17 Light Pen Address (Low) Read Only Read Only Read Only Read Only (note 2) (note 3) + + 1. On type 0 and 1, if a Write Only register is read from, "0" is returned. + 2. See the document "Extra CPC Plus Hardware Information" for more details. + 3. CRTC type 4 is the same as CRTC type 3. The registers also repeat as they do on the type 3. + */ + + /* CPC: + #BCXX %x0xxxx00 xxxxxxxx 6845 CRTC Index - Write + #BDXX %x0xxxx01 xxxxxxxx 6845 CRTC Data Out - Write + #BEXX %x0xxxx10 xxxxxxxx 6845 CRTC Status (as far as supported) Read - + #BFXX %x0xxxx11 xxxxxxxx 6845 CRTC Data In (as far as supported) Read - + */ + + /// + /// CPU (or other device) reads from the 8-bit databus + /// + /// + /// + public bool ReadPort(ushort port, ref int result) + { + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + bool accessed = false; + + // The 6845 is selected when bit 14 of the I/O port address is set to "0" + if (portUpper.Bit(6)) + return accessed; + + // Bit 9 and 8 of the I/O port address define the function to access + if (portUpper.Bit(1) && !portUpper.Bit(0)) + { + // read status register + accessed = ReadStatus(ref result); + } + else if ((portUpper & 3) == 3) + { + // read data register + accessed = ReadRegister(ref result); + } + else + { + result = 0; + } + + return accessed; + } + + /// + /// CPU (or other device) writes to the 8-bit databus + /// + /// + /// + public bool WritePort(ushort port, int value) + { + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + bool accessed = false; + + // The 6845 is selected when bit 14 of the I/O port address is set to "0" + if (portUpper.Bit(6)) + return accessed; + + var func = portUpper & 3; + + switch (func) + { + // reg select + case 0: + SelectRegister(value); + break; + + // data write + case 1: + WriteRegister(value); + break; + } + + return accessed; + } + + #endregion + + #region Internal IO Methods + + /// + /// Selects a specific register + /// + /// + private void SelectRegister(int value) + { + var v = (byte)((byte)value & 0x1F); + if (v > 0 && v < 18) + { + AddressRegister = v; + } + } + + /// + /// Writes to the currently latched address register + /// + /// + private void WriteRegister(int value) + { + byte val = (byte)value; + + // lightpen regs are readonly on all models + if (AddressRegister == 16 || AddressRegister == 17) + return; + + // all other models can be written to + switch (AddressRegister) + { + case H_TOTAL: + case H_DISPLAYED: + case H_SYNC_POS: + case START_ADDR_L: + Register[AddressRegister] = val; + break; + + case SYNC_WIDTHS: + Register[AddressRegister] = val; + UpdateWidths(); + break; + + case V_TOTAL_ADJUST: + case CURSOR_END: + case MAX_SL_ADDRESS: + Register[AddressRegister] = (byte)(val & 0x1F); + break; + + case START_ADDR_H: + case CURSOR_H: + Register[AddressRegister] = (byte)(val & 0x3F); + break; + + case V_TOTAL: + case V_DISPLAYED: + case V_SYNC_POS: + case CURSOR_START: + Register[AddressRegister] = (byte)(val & 0x7F); + break; + + case INTERLACE_MODE: + Register[AddressRegister] = (byte)(val & 0x3); + break; + } + } + + /// + /// Reads from the currently selected register + /// + /// + private bool ReadRegister(ref int data) + { + bool addressed = false; + switch (AddressRegister) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + case 10: + case 11: + if ((int)ChipType == 0 || (int)ChipType == 1) + { + addressed = true; + data = 0; + } + break; + case 12: + case 13: + addressed = true; + if ((int)ChipType == 0) + data = Register[AddressRegister]; + else if ((int)ChipType == 1) + data = 0; + break; + case 14: + case 15: + case 16: + case 17: + addressed = true; + data = Register[AddressRegister]; + break; + + default: + // registers 18-31 read as 0, on type 0 and 2. registers 18-30 read as 0 on type1, register 31 reads as 0x0ff. + if (AddressRegister >= 18 && AddressRegister <= 30) + { + switch ((int)ChipType) + { + case 0: + case 2: + case 1: + addressed = true; + data = 0; + break; + } + } + else if (AddressRegister == 31) + { + if ((int)ChipType == 1) + { + addressed = true; + data = 0x0ff; + } + else if ((int)ChipType == 0 || (int)ChipType == 2) + { + addressed = true; + data = 0; + } + } + break; + } + + return addressed; + } + + /// + /// Updates the V and H SYNC widths + /// + private void UpdateWidths() + { + switch (ChipType) + { + case CRCTType.HD6845S: + // Bits 7..4 define Vertical Sync Width. If 0 is programmed this gives 16 lines of VSYNC. Bits 3..0 define Horizontal Sync Width. + // If 0 is programmed no HSYNC is generated. + HSYNCWidth = (Register[SYNC_WIDTHS] >> 0) & 0x0F; + VSYNCWidth = (Register[SYNC_WIDTHS] >> 4) & 0x0F; + break; + case CRCTType.UM6845R: + // Bits 7..4 are ignored. Vertical Sync is fixed at 16 lines. Bits 3..0 define Horizontal Sync Width. If 0 is programmed no HSYNC is generated. + HSYNCWidth = (Register[SYNC_WIDTHS] >> 0) & 0x0F; + VSYNCWidth = 16; + break; + case CRCTType.MC6845: + // Bits 7..4 are ignored. Vertical Sync is fixed at 16 lines. Bits 3..0 define Horizontal Sync Width. If 0 is programmed this gives a HSYNC width of 16. + HSYNCWidth = (Register[SYNC_WIDTHS] >> 0) & 0x0F; + if (HSYNCWidth == 0) + HSYNCWidth = 16; + VSYNCWidth = 16; + break; + case CRCTType.AMS40489: + case CRCTType.AMS40226: + // Bits 7..4 define Vertical Sync Width. If 0 is programmed this gives 16 lines of VSYNC.Bits 3..0 define Horizontal Sync Width. + // If 0 is programmed this gives a HSYNC width of 16. + HSYNCWidth = (Register[SYNC_WIDTHS] >> 0) & 0x0F; + VSYNCWidth = (Register[SYNC_WIDTHS] >> 4) & 0x0F; + if (HSYNCWidth == 0) + HSYNCWidth = 16; + if (VSYNCWidth == 0) + VSYNCWidth = 16; + break; + } + } + + + + /// + /// Reads from the status register + /// + /// + /// + private bool ReadStatus(ref int data) + { + bool addressed = false; + switch ((int)ChipType) + { + case 1: + // read status + //todo!! + addressed = true; + break; + case 0: + case 2: + // status reg not available + break; + case 3: + case 4: + // read from internal register instead + addressed = ReadRegister(ref data); + break; + } + return addressed; + } + + #endregion + + #region Public Functions + + /// + /// Performs a CRCT clock cycle. + /// On CPC this is called at 1Mhz == 1 Character cycle (2 bytes) + /// + public void ClockCycle() + { + // H clock + HorizontalCTR++; + + if (HorizontalCTR == Register[H_TOTAL]) + { + // end of current scanline + HorizontalCTR = 0; + // CRCT starts its scalines at the display area + _DisplayEnable = true; + + ScanLineCTR++; + + if (ScanLineCTR > Register[MAX_SL_ADDRESS]) + { + // end of vertical character + ScanLineCTR = 0; + CharacterRowCTR++; + + if (CharacterRowCTR == Register[V_TOTAL]) + { + // check for vertical adjust + if (Register[V_TOTAL_ADJUST] > 0) + { + + } + else + { + // end of CRCT frame + CharacterRowCTR = 0; + } + } + } + } + else if (HorizontalCTR == Register[H_DISPLAYED] + 1) + { + // end of display area + _DisplayEnable = false; + } + } + + #endregion + + #region Internal Functions + + + + #endregion + + #region Serialization + + public void SyncState(Serializer ser) + { + ser.BeginSection("CRCT"); + ser.SyncEnum("ChipType", ref ChipType); + ser.Sync("_VSYNC", ref _VSYNC); + ser.Sync("_HSYNC", ref _HSYNC); + ser.Sync("_DisplayEnable", ref _DisplayEnable); + ser.Sync("_Cursor", ref _Cursor); + ser.Sync("_CLK", ref _CLK); + ser.Sync("_RESET", ref _RESET); + ser.Sync("_LPSTB", ref _LPSTB); + ser.Sync("AddressRegister", ref AddressRegister); + ser.Sync("Register", ref Register, false); + ser.Sync("HSYNCWidth", ref HSYNCWidth); + ser.Sync("VSYNCWidth", ref VSYNCWidth); + ser.Sync("_HorizontalCTR", ref _HorizontalCTR); + ser.Sync("_HorizontalSyncWidthCTR", ref _HorizontalSyncWidthCTR); + ser.Sync("_CharacterRowCTR", ref _CharacterRowCTR); + ser.Sync("_ScanLineCTR", ref _ScanLineCTR); + ser.EndSection(); + + /* + + * */ + } + + #endregion + + #region Enums + + /// + /// The types of CRCT chip found in the CPC range + /// + public enum CRCTType + { + HD6845S = 0, + UM6845 = 0, + UM6845R = 1, + MC6845 = 2, + AMS40489 = 3, + AMS40226 = 4 + } + + /// + /// The available interlace modes in the CRCT + /// + private enum InterlaceMode + { + NormalSyncMode, + InterlaceSyncMode, + InterlaceSyncAndVideoMode + } + + /// + /// Cursor display modes + /// + private enum CursorControl + { + NonBlink, + CursorNonDisplay, + Blink1_16, + Blink1_32 + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/CRCT_6845.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/CRCT_6845.cs new file mode 100644 index 0000000000..8176b7db14 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/CRCT_6845.cs @@ -0,0 +1,1225 @@ +using BizHawk.Common; +using BizHawk.Common.NumberExtensions; +using System; +using System.Collections; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Cathode Ray Tube Controller Chip - 6845 + /// http://www.cpcwiki.eu/index.php/CRTC + /// https://web.archive.org/web/20170501112330/http://www.grimware.org/doku.php/documentations/devices/crtc + /// + public class CRCT_6845 : IPortIODevice + { + #region Devices + + private CPCBase _machine { get; set; } + private CRCTType ChipType; + + #endregion + + #region CallBacks + + public delegate void CallBack(); + + private CallBack HSYNC_Callbacks; + private CallBack VSYNC_Callbacks; + + public void AttachVSYNCCallback(CallBack vCall) + { + VSYNC_Callbacks += vCall; + } + + public void AttachHSYNCCallback(CallBack hCall) + { + HSYNC_Callbacks += hCall; + } + + #endregion + + #region Construction + + public CRCT_6845(CRCTType chipType, CPCBase machine) + { + _machine = machine; + ChipType = chipType; + Reset(); + } + + private const int WRITE = 0; + private const int READ = 1; + + #endregion + + #region Public Lines + + /// + /// Denotes that HSYNC is active + /// + public bool HSYNC = false; + + /// + /// Denotes that VSYNC is active + /// + public bool VSYNC = false; + + /// + /// TRUE: bits outputted to screen from video RAM + /// FALSE: current border colour is outputted + /// + public bool DISPTMG = true; + + /// + /// 16-bit memory address lines + /// The gate array uses this to grab the correct bits from video RAM + /// + public short MA; + + /// + /// Vertical Character Count + /// + public int VCC; + + /// + /// Vertical Scanline Count (within the current vertical character) + /// + public int VLC; + + #endregion + + #region Public Lookups + + /* + * These are not accessible directlyon real hardware + * It just makes screen generation easier to have these accessbile from the gate array + */ + + /// + /// The total frame width (in characters) + /// + public int FrameWidth + { + get + { + return (int)Regs[HOR_TOTAL] + 1; + } + } + + /// + /// The total frame height (in scanlines) + /// + public int FrameHeight + { + get + { + return ((int)Regs[VER_TOTAL] + 1) * ((int)Regs[MAX_RASTER_ADDR] + 1); + } + } + + /// + /// The total frame height (in scanlines) + /// + public int FrameHeightInChars + { + get + { + return ((int)Regs[VER_TOTAL] + 1); + } + } + + /// + /// The width of the display area (in characters) + /// + public int DisplayWidth + { + get + { + return (int)Regs[HOR_DISPLAYED]; + } + } + + /// + /// The width of the display area (in scanlines) + /// + public int DisplayHeight + { + get + { + return (int)Regs[VER_DISPLAYED] * ((int)Regs[MAX_RASTER_ADDR] + 1); + } + } + + /// + /// The width of the display area (in scanlines) + /// + public int DisplayHeightInChars + { + get + { + return (int)Regs[VER_DISPLAYED]; + } + } + + /// + /// The character at which to start HSYNC + /// + public int HorizontalSyncPos + { + get + { + return (int)Regs[HOR_SYNC_POS]; + } + } + + /// + /// Width (in characters) of the HSYNC + /// + public int HorizontalSyncWidth + { + get + { + return HSYNCWidth; + } + } + + /// + /// The vertical scanline at which to start VSYNC + /// + public int VerticalSyncPos + { + get + { + return (int)Regs[VER_SYNC_POS] * ((int)Regs[MAX_RASTER_ADDR] + 1); + } + } + + /// + /// Height (in scanlines) of the VSYNC + /// + public int VerticalSyncHeight + { + get + { + return VSYNCWidth; // * ((int)Regs[MAX_RASTER_ADDR] + 1); + } + } + + /// + /// The number of scanlines in one character (MAXRASTER) + /// + public int ScanlinesPerCharacter + { + get + { + return (int)Regs[MAX_RASTER_ADDR] + 1; + } + } + + /// + /// Returns the starting video page address as specified within R12 + /// + public int VideoPageBase + { + get + { + if (!Regs[12].Bit(4) && Regs[12].Bit(5)) + return 0x8000; + + if (Regs[12].Bit(4) && !Regs[12].Bit(5)) + return 0x4000; + + if (!Regs[12].Bit(4) && !Regs[12].Bit(5)) + return 0x0000; + + return 0xC000; + } + } + + public int DStartHigh + { get { return Regs[DISP_START_ADDR_H]; } } + + public int DStartLow + { get { return Regs[DISP_START_ADDR_L]; } } + + /// + /// Returns the video buffer size as specified within R12 + /// + public int VideoBufferSize + { + get + { + if (Regs[12].Bit(3) && Regs[12].Bit(2)) + return 0x8000; + + return 0x4000; + } + } + + /// + /// The offset into vRAM + /// + public int VideoRAMOffset + { + get + { + ushort combined = (ushort)(Regs[12] << 8 | Regs[13]); + int offset = combined & 0x3ff; + return offset; + } + } + + + /* Easier memory functions */ + + /// + /// The current byte address + /// + public ushort CurrentByteAddress; + + /// + /// ByteCounter + /// + public int ByteCounter; + + /// + /// Set at every HSYNC + /// + public int LatchedRAMOffset; + + /// + /// set at every HSYNC + /// + public int LatchedRAMStartAddress; + + /// + /// set at every HSYNC + /// + public int LatchedScreenWidthBytes; + + #endregion + + #region Internal Registers and State + + /* + Index Register Name Range CPC Setting Notes + 0 Horizontal Total 00000000 63 Width of the screen, in characters. Should always be 63 (64 characters). 1 character == 1μs. + 1 Horizontal Displayed 00000000 40 Number of characters displayed. Once horizontal character count (HCC) matches this value, DISPTMG is set to 1. + 2 Horizontal Sync Position 00000000 46 When to start the HSync signal. + 3 Horizontal and Vertical Sync Widths VVVVHHHH 128+14 HSync pulse width in characters (0 means 16 on some CRTC), should always be more than 8; VSync width in scan-lines. (0 means 16 on some CRTC. Not present on all CRTCs, fixed to 16 lines on these) + 4 Vertical Total x0000000 38 Height of the screen, in characters. + 5 Vertical Total Adjust xxx00000 0 Measured in scanlines, can be used for smooth vertical scrolling on CPC. + 6 Vertical Displayed x0000000 25 Height of displayed screen in characters. Once vertical character count (VCC) matches this value, DISPTMG is set to 1. + 7 Vertical Sync position x0000000 30 When to start the VSync signal, in characters. + 8 Interlace and Skew xxxxxx00 0 00: No interlace; 01: Interlace Sync Raster Scan Mode; 10: No Interlace; 11: Interlace Sync and Video Raster Scan Mode + 9 Maximum Raster Address xxx00000 7 Maximum scan line address on CPC can hold between 0 and 7, higher values' upper bits are ignored + 10 Cursor Start Raster xBP00000 0 Cursor not used on CPC. B = Blink On/Off; P = Blink Period Control (Slow/Fast). Sets first raster row of character that cursor is on to invert. + 11 Cursor End Raster xxx00000 0 Sets last raster row of character that cursor is on to invert + 12 Display Start Address (High) xx000000 32 + 13 Display Start Address (Low) 00000000 0 Allows you to offset the start of screen memory for hardware scrolling, and if using memory from address &0000 with the firmware. + 14 Cursor Address (High) xx000000 0 + 15 Cursor Address (Low) 00000000 0 + 16 Light Pen Address (High) xx000000 Read Only + 17 Light Pen Address (Low) 00000000 Read Only + */ + /// + /// 6845 internal registers + /// + private byte[] Regs = new byte[18]; + + // CRTC Register constants + /// + /// R0: Horizontal total character number + /// Unit: Character + /// Notes: Defines the width of a scanline + /// + public const int HOR_TOTAL = 0; + /// + /// R1: Horizontal displayed character number + /// Unit: Character + /// Notes: Defines when DISPEN goes OFF on the scanline + /// + public const int HOR_DISPLAYED = 1; + /// + /// R2: Position of horizontal sync. pulse + /// Unit: Character + /// Notes: Defines when the HSync goes ON on the scanline + /// + public const int HOR_SYNC_POS = 2; + /// + /// R3: Width of horizontal/vertical sync. pulses + /// Unit: Character + /// Notes: VSync width can only be changed on type 3 and 4 + /// + public const int HOR_AND_VER_SYNC_WIDTHS = 3; + /// + /// R4: Vertical total Line character number + /// Unit: Character + /// Notes: Defines the height of a screen + /// + public const int VER_TOTAL = 4; + /// + /// R5: Vertical raster adjust + /// Unit: Scanline + /// Notes: Defines additionnal scanlines at the end of a screen + /// can be used for smooth vertical scrolling on CPC + /// + public const int VER_TOTAL_ADJUST = 5; + /// + /// R6: Vertical displayed character number + /// Unit: Character + /// Notes: Define when DISPEN remains OFF until a new screen starts + /// Height of displayed screen in characters (Once vertical character count (VCC) matches this value, DISPTMG is set to 1) + /// + public const int VER_DISPLAYED = 6; + /// + /// R7: Position of vertical sync. pulse + /// Unit: Character + /// Notes: Define when the VSync goes ON on a screen + /// + public const int VER_SYNC_POS = 7; + /// + /// R8: Interlaced mode + /// Unit: + /// Notes: 00: No interlace; 01: Interlace Sync Raster Scan Mode; 10: No Interlace; 11: Interlace Sync and Video Raster Scan Mode + /// (crct type specific) + /// + public const int INTERLACE_SKEW = 8; + /// + /// R9: Maximum raster + /// Unit: Scanline + /// Notes: Defines the height of a CRTC-Char in scanlines + /// + public const int MAX_RASTER_ADDR = 9; + /// + /// R10: Cursor start raster + /// Unit: + /// Notes: Cursor not used on CPC. + /// (xBP00000) + /// B = Blink On/Off; + /// P = Blink Period Control (Slow/Fast). + /// Sets first raster row of character that cursor is on to invert + /// + public const int CUR_START_RASTER = 10; + /// + /// R11: Cursor end + /// Unit: + /// Notes: Sets last raster row of character that cursor is on to invert + /// + public const int CUR_END_RASTER = 11; + /// + /// R12: Display Start Address (High) + /// Unit: + /// Notes: Define the MSB of MA when a CRTC-screen starts + /// + public const int DISP_START_ADDR_H = 12; + /// + /// R13: Display Start Address (Low) + /// Unit: + /// Notes: Define the LSB of MA when a CRTC-screen starts + /// Allows you to offset the start of screen memory for hardware scrolling, and if using memory from address &0000 with the firmware. + /// + public const int DISP_START_ADDR_L = 13; + /// + /// R14: Cursor Address (High) + /// Unit: + /// Notes: Useless on the Amstrad CPC/Plus (text-mode is not wired) + /// + public const int CUR_ADDR_H = 14; + /// + /// R15: Cursor Address (Low) + /// Unit: + /// Notes: Useless on the Amstrad CPC/Plus (text-mode is not wired) + /// + public const int CUR_ADDR_L = 15; + /// + /// R16: Light Pen Address (High) + /// Unit: + /// Notes: Hold the MSB of the cursor position when the lightpen was ON + /// + public const int LPEN_ADDR_H = 16; + /// + /// R17: Light Pen Address (Low) + /// Unit: + /// Notes: Hold the LSB of the cursor position when the lightpen was ON + /// + public const int LPEN_ADDR_L = 17; + + /// + /// The currently selected register + /// + private int SelectedRegister; + + /// + /// CPC register default values + /// Taken from https://web.archive.org/web/20170501112330/http://www.grimware.org/doku.php/documentations/devices/crtc + /// http://www.cantrell.org.uk/david/tech/cpc/cpc-firmware/firmware.pdf + /// (The defaults values given here are those programmed by the firmware ROM after a cold/warm boot of the CPC/Plus) + /// + private byte[] RegDefaults = new byte[] { 63, 40, 46, 112, 38, 0, 25, 30, 0, 7, 0, 0, 48, 0, 192, 7, 0, 0 }; + + /// + /// Register masks + /// 0 = WRITE + /// 1 = READ + /// + private byte[] CPCMask = new byte[] { 255, 255, 255, 255, 127, 31, 127, 126, 3, 31, 31, 31, 63, 255, 63, 255, 63, 255 }; + + /// + /// Horizontal Character Count + /// + private int HCC; + + /// + /// Internal cycle counter + /// + private int CycleCounter; + + /// + /// Signs that we have finished the last character row + /// + private bool EndOfScreen; + + /// + /// HSYNC pulse width (in characters) + /// + private int HSYNCWidth; + + /// + /// Internal HSYNC counter + /// + private int HSYNCCounter; + + /// + /// VSYNC pulse width (in characters) + /// + private int VSYNCWidth; + + /// + /// Internal VSYNC counter + /// + private int VSYNCCounter; + + #endregion + + #region Public Methods + + public void ClockCycle() + { + CheckHSYNCOff(); + + HCC++; + + if (HCC == Regs[HOR_TOTAL] + 1) + { + // end of scanline + HCC = 0; + + if (VSYNCCounter > 0) + { + VSYNCCounter--; + if (VSYNCCounter == 0) + { + VSYNC = false; + } + } + + VLC++; + + if (VLC == Regs[MAX_RASTER_ADDR] + 1) + { + // end of rasterline + VLC = 0; + VCC++; + + if (VCC == Regs[VER_TOTAL] + 1) + { + // end of screen + VCC = 0; + } + + if (VCC == Regs[VER_SYNC_POS] && !VSYNC) + { + VSYNC = true; + VSYNCCounter = VSYNCWidth; + VSYNC_Callbacks(); + } + } + } + else + { + // still on the current scanline + if (HCC == Regs[HOR_SYNC_POS] && !HSYNC) + { + HSYNC = true; + HSYNCCounter = HSYNCWidth; + HSYNC_Callbacks(); + ByteCounter = 0; + } + + if (HCC >= Regs[HOR_DISPLAYED] + 1 || VCC >= Regs[VER_DISPLAYED]) + { + DISPTMG = false; + } + else + { + DISPTMG = true; + + var line = VCC; + var row = VLC; + var addrX = (LatchedRAMOffset * 2) + ((VCC * LatchedScreenWidthBytes) & 0x7ff) + ByteCounter; + // remove artifacts caused by certain hardware scrolling addresses + addrX &= 0x7ff; + var addrY = LatchedRAMStartAddress + (2048 * VLC); + + //var addr = VideoPageBase + (line * (0x50)) + (row * 0x800) + (ByteCounter); + CurrentByteAddress = (ushort)(addrX + addrY); + + ByteCounter += 2; + } + } + } + + private void CheckHSYNCOff() + { + if (HSYNCCounter > 0) + { + HSYNCCounter--; + if (HSYNCCounter == 0) + { + HSYNC = false; + } + } + } + + /// + /// Runs a CRCT clock cycle + /// This should be called at 1Mhz / 1us / every 4 uncontended CPU t-states + /// + public void ClockCycle2() + { + if (HSYNC) + { + // HSYNC in progress + HSYNCCounter++; + + ByteCounter = 0; + + if (HSYNCCounter >= HSYNCWidth) + { + // end of HSYNC + HSYNCCounter = 0; + HSYNC = false; + } + } + + if (HSYNC && HSYNCCounter == 1) + { + + } + + // move one horizontal character + HCC++; + + // check for DISPTMG + if (HCC >= Regs[HOR_DISPLAYED] + 1) + { + DISPTMG = false; + } + else if (VCC >= Regs[VER_DISPLAYED]) + { + DISPTMG = false; + } + else + { + DISPTMG = true; + + var line = VCC; + var row = VLC; + var addrX = (LatchedRAMOffset * 2) + ((VCC * LatchedScreenWidthBytes) & 0x7ff) + ByteCounter; + // remove artifacts caused by certain hardware scrolling addresses + addrX &= 0x7ff; + var addrY = LatchedRAMStartAddress + (2048 * VLC); + + //var addr = VideoPageBase + (line * (0x50)) + (row * 0x800) + (ByteCounter); + CurrentByteAddress = (ushort)(addrX + addrY); + + ByteCounter += 2; + } + + // check for the end of the current scanline + if (HCC == Regs[HOR_TOTAL] + 1) + { + // end of the current scanline + HCC = 0; + + + if (ChipType == (CRCTType)1 && VLC <= Regs[MAX_RASTER_ADDR]) + { + // https://web.archive.org/web/20170501112330/http://www.grimware.org/doku.php/documentations/devices/crtc + // The MA is reloaded with the value from R12 and R13 when VCC=0 and VLC=0 (that's when a new CRTC screen begin). + // However, CRTC Type 1 keep updating the MA on every new scanline while VCC=0 (and VLC== Regs[VER_TOTAL] + 1) + { + VCC = 0; + EndOfScreen = true; + } + } + + // does VSYNC need to be raised? + if (!VSYNC) + { + if (VCC == Regs[VER_SYNC_POS]) + { + VSYNC = true; + VSYNCCounter = 0; + VSYNC_Callbacks(); + } + } + } + else + { + // still processing a scanline + // check whether HSYNC needs raising + if (!HSYNC) + { + if (HCC == Regs[HOR_SYNC_POS]) + { + HSYNC = true; + HSYNCCounter = 0; + HSYNC_Callbacks(); + lineCounter++; + + LatchedRAMStartAddress = VideoPageBase; + LatchedRAMOffset = VideoRAMOffset; + LatchedScreenWidthBytes = DisplayWidth * 2; + + } + } + } + } + + /// + /// Runs a CRCT clock cycle + /// This should be called at 1Mhz / 1us / every 4 uncontended CPU t-states + /// + public void ClockCycle1() + { + // HSYNC processing + if (HSYNCCounter > 0) + { + HSYNCCounter--; + if (HSYNCCounter == 0) + HSYNC = false; + } + + HCC++; + + if (HCC == FrameWidth) + { + // we have finished the current scanline + HCC = 0; + + if (VSYNCCounter > 0) + { + VSYNCCounter--; + if (VSYNCCounter == 0) + VSYNC = false; + } + + VLC++; + + if (VLC == ScanlinesPerCharacter) + { + // completed a vertical character + VLC = 0; + VCC++; + + if (VCC == FrameHeight) + { + // screen has completed + VCC = 0; + } + } + + // check whether VSYNC should be raised + if (VCC == VerticalSyncPos && !VSYNC) + { + VSYNC = true; + VSYNCCounter = VSYNCWidth; + VSYNC_Callbacks(); + } + } + else if (HCC == HorizontalSyncPos && !HSYNC) + { + // start of HSYNC period + HSYNC = true; + HSYNCCounter = HSYNCWidth; + HSYNC_Callbacks(); + } + + // DISPTMG + if (HCC >= Regs[HOR_DISPLAYED] || VCC >= Regs[VER_DISPLAYED]) + { + DISPTMG = false; + } + else + { + DISPTMG = true; + } + /* + // check for DISPTMG + if (HCC >= Regs[HOR_DISPLAYED] + 1) + { + DISPTMG = false; + } + else if (VCC >= Regs[VER_DISPLAYED]) + { + DISPTMG = false; + } + else + { + DISPTMG = true; + } + */ + } + + public int lineCounter = 0; + + + + /// + /// Resets the chip + /// + public void Reset() + { + // set regs to default + for (int i = 0; i < 18; i++) + Regs[i] = RegDefaults[i]; + + SelectedRegister = 0; + + // populate initial MA address + MA = (short)(((Regs[DISP_START_ADDR_H]) & 0xff) << 8 | (Regs[DISP_START_ADDR_L]) & 0xff); + + // updates widths + UpdateWidths(); + + HSYNC = false; + VSYNC = false; + + HSYNCCounter = 0; + VSYNCCounter = 0; + + HCC = 0; + VCC = 0; + VLC = 0; + } + + #endregion + + #region Internal Methods + + /// + /// Selects a register + /// + /// + private void RegisterSelect(int data) + { + SelectedRegister = data & 0x1F; + } + + /* + RegIdx Register Name Type + 0 1 2 3 4 + 0 Horizontal Total Write Only Write Only Write Only (note 2) (note 3) + 1 Horizontal Displayed Write Only Write Only Write Only (note 2) (note 3) + 2 Horizontal Sync Position Write Only Write Only Write Only (note 2) (note 3) + 3 H and V Sync Widths Write Only Write Only Write Only (note 2) (note 3) + 4 Vertical Total Write Only Write Only Write Only (note 2) (note 3) + 5 Vertical Total Adjust Write Only Write Only Write Only (note 2) (note 3) + 6 Vertical Displayed Write Only Write Only Write Only (note 2) (note 3) + 7 Vertical Sync position Write Only Write Only Write Only (note 2) (note 3) + 8 Interlace and Skew Write Only Write Only Write Only (note 2) (note 3) + 9 Maximum Raster Address Write Only Write Only Write Only (note 2) (note 3) + 10 Cursor Start Raster Write Only Write Only Write Only (note 2) (note 3) + 11 Cursor End Raster Write Only Write Only Write Only (note 2) (note 3) + 12 Disp. Start Address (High) Read/Write Write Only Write Only Read/Write (note 2) (note 3) + 13 Disp. Start Address (Low) Read/Write Write Only Write Only Read/Write (note 2) (note 3) + 14 Cursor Address (High) Read/Write Read/Write Read/Write Read/Write (note 2) (note 3) + 15 Cursor Address (Low) Read/Write Read/Write Read/Write Read/Write (note 2) (note 3) + 16 Light Pen Address (High) Read Only Read Only Read Only Read Only (note 2) (note 3) + 17 Light Pen Address (Low) Read Only Read Only Read Only Read Only (note 2) (note 3) + + 1. On type 0 and 1, if a Write Only register is read from, "0" is returned. + 2. See the document "Extra CPC Plus Hardware Information" for more details. + 3. CRTC type 4 is the same as CRTC type 3. The registers also repeat as they do on the type 3. + */ + + /// + /// Writes to the currently selected register + /// + /// + private void WriteRegister(int data) + { + // 16 and 17 are read only registers on all types + if (SelectedRegister == 16 || SelectedRegister == 17) + return; + + // non existing registers + if (SelectedRegister > 17) + return; + + if (SelectedRegister == DISP_START_ADDR_L) + { + + } + + if (SelectedRegister == DISP_START_ADDR_H) + { + + } + + if (SelectedRegister == HOR_TOTAL) + { + // always 63 + if (data != 63) + return; + } + + if (SelectedRegister == 1) + { + var d = data; + } + + Regs[SelectedRegister] = (byte)(data & CPCMask[SelectedRegister]); + + if (SelectedRegister == HOR_AND_VER_SYNC_WIDTHS) + { + UpdateWidths(); + } + } + + /// + /// Reads from the currently selected register + /// + /// + private bool ReadRegister(ref int data) + { + bool addressed = false; + switch (SelectedRegister) + { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + case 10: + case 11: + if ((int)ChipType == 0 || (int)ChipType == 1) + { + addressed = true; + data = 0; + } + break; + case 12: + case 13: + addressed = true; + if ((int)ChipType == 0) + data = Regs[SelectedRegister]; + else if ((int)ChipType == 1) + data = 0; + break; + case 14: + case 15: + case 16: + case 17: + addressed = true; + data = Regs[SelectedRegister]; + break; + + default: + // registers 18-31 read as 0, on type 0 and 2. registers 18-30 read as 0 on type1, register 31 reads as 0x0ff. + if (SelectedRegister >= 18 && SelectedRegister <= 30) + { + switch ((int)ChipType) + { + case 0: + case 2: + case 1: + addressed = true; + data = 0; + break; + } + } + else if (SelectedRegister == 31) + { + if ((int)ChipType == 1) + { + addressed = true; + data = 0x0ff; + } + else if ((int)ChipType == 0 || (int)ChipType == 2) + { + addressed = true; + data = 0; + } + } + break; + } + + return addressed; + } + + /// + /// Reads from the status register + /// + /// + /// + private bool ReadStatus(ref int data) + { + bool addressed = false; + switch ((int)ChipType) + { + case 1: + // read status + //todo!! + addressed = true; + break; + case 0: + case 2: + // status reg not available + break; + case 3: + case 4: + // read from internal register instead + addressed = ReadRegister(ref data); + break; + } + return addressed; + } + + /// + /// Updates the V and H SYNC widths + /// + private void UpdateWidths() + { + switch (ChipType) + { + case CRCTType.HD6845S: + // Bits 7..4 define Vertical Sync Width. If 0 is programmed this gives 16 lines of VSYNC. Bits 3..0 define Horizontal Sync Width. + // If 0 is programmed no HSYNC is generated. + HSYNCWidth = (Regs[HOR_AND_VER_SYNC_WIDTHS] >> 0) & 0x0F; + VSYNCWidth = (Regs[HOR_AND_VER_SYNC_WIDTHS] >> 4) & 0x0F; + break; + case CRCTType.UM6845R: + // Bits 7..4 are ignored. Vertical Sync is fixed at 16 lines. Bits 3..0 define Horizontal Sync Width. If 0 is programmed no HSYNC is generated. + HSYNCWidth = (Regs[HOR_AND_VER_SYNC_WIDTHS] >> 0) & 0x0F; + VSYNCWidth = 16; + break; + case CRCTType.MC6845: + // Bits 7..4 are ignored. Vertical Sync is fixed at 16 lines. Bits 3..0 define Horizontal Sync Width. If 0 is programmed this gives a HSYNC width of 16. + HSYNCWidth = (Regs[HOR_AND_VER_SYNC_WIDTHS] >> 0) & 0x0F; + if (HSYNCWidth == 0) + HSYNCWidth = 16; + VSYNCWidth = 16; + break; + case CRCTType.AMS40489: + case CRCTType.AMS40226: + // Bits 7..4 define Vertical Sync Width. If 0 is programmed this gives 16 lines of VSYNC.Bits 3..0 define Horizontal Sync Width. + // If 0 is programmed this gives a HSYNC width of 16. + HSYNCWidth = (Regs[HOR_AND_VER_SYNC_WIDTHS] >> 0) & 0x0F; + VSYNCWidth = (Regs[HOR_AND_VER_SYNC_WIDTHS] >> 4) & 0x0F; + if (HSYNCWidth == 0) + HSYNCWidth = 16; + if (VSYNCWidth == 0) + VSYNCWidth = 16; + break; + } + } + + #endregion + + #region PortIODevice + + /* + #BCXX %x0xxxx00 xxxxxxxx 6845 CRTC Index - Write + #BDXX %x0xxxx01 xxxxxxxx 6845 CRTC Data Out - Write + #BEXX %x0xxxx10 xxxxxxxx 6845 CRTC Status (as far as supported) Read - + #BFXX %x0xxxx11 xxxxxxxx 6845 CRTC Data In (as far as supported) Read - + */ + + /// + /// Device responds to an IN instruction + /// + /// + /// + /// + public bool ReadPort(ushort port, ref int result) + { + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + bool accessed = false; + + // The 6845 is selected when bit 14 of the I/O port address is set to "0" + if (portUpper.Bit(6)) + return accessed; + + // Bit 9 and 8 of the I/O port address define the function to access + if (portUpper.Bit(1) && !portUpper.Bit(0)) + { + // read status register + accessed = ReadStatus(ref result); + } + else if ((portUpper & 3) == 3) + { + // read data register + accessed = ReadRegister(ref result); + } + else + { + result = 0; + } + + return accessed; + } + + /// + /// Device responds to an OUT instruction + /// + /// + /// + /// + public bool WritePort(ushort port, int result) + { + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + bool accessed = false; + + // The 6845 is selected when bit 14 of the I/O port address is set to "0" + if (portUpper.Bit(6)) + return accessed; + + var func = portUpper & 3; + + switch (func) + { + // reg select + case 0: + RegisterSelect(result); + break; + + // data write + case 1: + WriteRegister(result); + break; + } + + return accessed; + } + + #endregion + + #region Serialization + + public void SyncState(Serializer ser) + { + ser.BeginSection("CRTC"); + ser.SyncEnum("ChipType", ref ChipType); + ser.Sync("HSYNC", ref HSYNC); + ser.Sync("VSYNC", ref VSYNC); + ser.Sync("DISPTMG", ref DISPTMG); + ser.Sync("MA", ref MA); + ser.Sync("CurrentByteAddress", ref CurrentByteAddress); + ser.Sync("ByteCounter", ref ByteCounter); + ser.Sync("Regs", ref Regs, false); + ser.Sync("SelectedRegister", ref SelectedRegister); + ser.Sync("HCC", ref HCC); + ser.Sync("VCC", ref VCC); + ser.Sync("VLC", ref VLC); + ser.Sync("CycleCounter", ref CycleCounter); + ser.Sync("EndOfScreen", ref EndOfScreen); + ser.Sync("HSYNCWidth", ref HSYNCWidth); + ser.Sync("HSYNCCounter", ref HSYNCCounter); + ser.Sync("VSYNCWidth", ref VSYNCWidth); + ser.Sync("VSYNCCounter", ref VSYNCCounter); + ser.EndSection(); + } + + #endregion + + #region Enums + + /// + /// The types of CRCT chip found in the CPC range + /// + public enum CRCTType + { + HD6845S = 0, + UM6845 = 0, + UM6845R = 1, + MC6845 = 2, + AMS40489 = 3, + AMS40226 = 4 + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/CRTDevice.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/CRTDevice.cs new file mode 100644 index 0000000000..5f318c47be --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Display/CRTDevice.cs @@ -0,0 +1,685 @@ +using BizHawk.Common; +using BizHawk.Common.NumberExtensions; +using BizHawk.Emulation.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Render pixels to the screen + /// + public class CRTDevice : IVideoProvider + { + #region Devices + + private CPCBase _machine; + private CRCT_6845 CRCT => _machine.CRCT; + private AmstradGateArray GateArray => _machine.GateArray; + + #endregion + + #region Construction + + public CRTDevice(CPCBase machine) + { + _machine = machine; + CurrentLine = new ScanLine(this); + + CRCT.AttachHSYNCCallback(OnHSYNC); + CRCT.AttachVSYNCCallback(OnVSYNC); + } + + #endregion + + #region Palettes + + /// + /// The standard CPC Pallete (ordered by firmware #) + /// http://www.cpcwiki.eu/index.php/CPC_Palette + /// + public static readonly int[] CPCFirmwarePalette = + { + Colors.ARGB(0x00, 0x00, 0x00), // Black + Colors.ARGB(0x00, 0x00, 0x80), // Blue + Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue + Colors.ARGB(0x80, 0x00, 0x00), // Red + Colors.ARGB(0x80, 0x00, 0x80), // Magenta + Colors.ARGB(0x80, 0x00, 0xFF), // Mauve + Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red + Colors.ARGB(0xFF, 0x00, 0x80), // Purple + Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta + Colors.ARGB(0x00, 0x80, 0x00), // Green + Colors.ARGB(0x00, 0x80, 0x80), // Cyan + Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue + Colors.ARGB(0x80, 0x80, 0x00), // Yellow + Colors.ARGB(0x80, 0x80, 0x80), // White + Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue + Colors.ARGB(0xFF, 0x80, 0x00), // Orange + Colors.ARGB(0xFF, 0x80, 0x80), // Pink + Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta + Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green + Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green + Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan + Colors.ARGB(0x80, 0xFF, 0x00), // Lime + Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green + Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan + Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow + Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow + Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White + }; + + /// + /// The standard CPC Pallete (ordered by hardware #) + /// http://www.cpcwiki.eu/index.php/CPC_Palette + /// + public static readonly int[] CPCHardwarePalette = + { + Colors.ARGB(0x80, 0x80, 0x80), // White + Colors.ARGB(0x80, 0x80, 0x80), // White (duplicate) + Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green + Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow + Colors.ARGB(0x00, 0x00, 0x80), // Blue + Colors.ARGB(0xFF, 0x00, 0x80), // Purple + Colors.ARGB(0x00, 0x80, 0x80), // Cyan + Colors.ARGB(0xFF, 0x80, 0x80), // Pink + Colors.ARGB(0xFF, 0x00, 0x80), // Purple (duplicate) + Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow (duplicate) + Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow + Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White + Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red + Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta + Colors.ARGB(0xFF, 0x80, 0x00), // Orange + Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta + Colors.ARGB(0x00, 0x00, 0x80), // Blue (duplicate) + Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green (duplicate) + Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green + Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan + Colors.ARGB(0x00, 0x00, 0x00), // Black + Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue + Colors.ARGB(0x00, 0x80, 0x00), // Green + Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue + Colors.ARGB(0x80, 0x00, 0x80), // Magenta + Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green + Colors.ARGB(0x80, 0xFF, 0x00), // Lime + Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan + Colors.ARGB(0x80, 0x00, 0x00), // Red + Colors.ARGB(0x80, 0x00, 0xFF), // Mauve + Colors.ARGB(0x80, 0x80, 0x00), // Yellow + Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue + }; + + + #endregion + + #region Public Stuff + + /// + /// The current scanline that is being added to + /// (will be processed and committed to the screen buffer every HSYNC) + /// + public ScanLine CurrentLine; + + /// + /// The number of top border scanlines to ommit when rendering + /// + public int TopLinesToTrim = 20; + + /// + /// Count of rendered scanlines this frame + /// + public int ScanlineCounter = 0; + + /// + /// Video buffer processing + /// + public int[] ProcessVideoBuffer() + { + return ScreenBuffer; + } + + /// + /// Sets up buffers and the like at the start of a frame + /// + public void SetupVideo() + { + if (BufferHeight == 576) + return; + + BufferWidth = 800; + BufferHeight = 576; + + VirtualWidth = BufferWidth / 2; + VirtualHeight = BufferHeight / 2; + + ScreenBuffer = new int[BufferWidth * BufferHeight]; + } + + /// + /// Fired when the CRCT flags HSYNC + /// + public void OnHSYNC() + { + + } + + /// + /// Fired when the CRCT flags VSYNC + /// + public void OnVSYNC() + { + + } + + #endregion + + #region IVideoProvider + + /// + /// Video output buffer + /// + public int[] ScreenBuffer; + + private int _virtualWidth; + private int _virtualHeight; + private int _bufferWidth; + private int _bufferHeight; + + public int BackgroundColor + { + get { return CPCHardwarePalette[0]; } + } + + public int VirtualWidth + { + get { return _virtualWidth; } + set { _virtualWidth = value; } + } + + public int VirtualHeight + { + get { return _virtualHeight; } + set { _virtualHeight = value; } + } + + public int BufferWidth + { + get { return _bufferWidth; } + set { _bufferWidth = value; } + } + + public int BufferHeight + { + get { return _bufferHeight; } + set { _bufferHeight = value; } + } + + public int VsyncNumerator + { + get { return GateArray.Z80ClockSpeed * 50; } + set { } + } + + public int VsyncDenominator + { + get { return GateArray.Z80ClockSpeed; } + } + + public int[] GetVideoBuffer() + { + return ProcessVideoBuffer(); + } + + public void SetupScreenSize() + { + BufferWidth = 1024; // 512; + BufferHeight = 768; + VirtualHeight = BufferHeight; + VirtualWidth = BufferWidth; + ScreenBuffer = new int[BufferWidth * BufferHeight]; + croppedBuffer = ScreenBuffer; + } + + protected int[] croppedBuffer; + + #endregion + + #region Serialization + + public void SyncState(Serializer ser) + { + ser.BeginSection("CRT"); + ser.Sync("BufferWidth", ref _bufferWidth); + ser.Sync("BufferHeight", ref _bufferHeight); + ser.Sync("VirtualHeight", ref _virtualHeight); + ser.Sync("VirtualWidth", ref _virtualWidth); + ser.Sync("ScreenBuffer", ref ScreenBuffer, false); + ser.Sync("ScanlineCounter", ref ScanlineCounter); + ser.EndSection(); + } + + #endregion + } + + /// + /// Represents a single scanline buffer + /// + public class ScanLine + { + /// + /// Array of character information + /// + public Character[] Characters; + + /// + /// The screenmode that was set at the start of this scanline + /// + public int ScreenMode = 1; + + /// + /// The scanline number (0 based) + /// + public int LineIndex; + + /// + /// The calling CRT device + /// + private CRTDevice CRT; + + public ScanLine(CRTDevice crt) + { + Reset(); + CRT = crt; + } + + // To be run after scanline has been fully processed + public void InitScanline(int screenMode, int lineIndex) + { + Reset(); + ScreenMode = screenMode; + LineIndex = lineIndex; + } + + /// + /// Adds a single scanline character into the matrix + /// + /// + /// + public void AddScanlineCharacter(int index, RenderPhase phase, byte vid1, byte vid2, int[] pens) + { + if (index >= 64) + { + return; + } + + switch (phase) + { + case RenderPhase.BORDER: + AddBorderValue(index, CRTDevice.CPCHardwarePalette[pens[16]]); + break; + case RenderPhase.DISPLAY: + AddDisplayValue(index, vid1, vid2, pens); + break; + default: + AddSyncValue(index, phase); + break; + } + } + + /// + /// Adds a HSYNC, VSYNC or HSYNC+VSYNC character into the scanline + /// + /// + /// + private void AddSyncValue(int charIndex, RenderPhase phase) + { + Characters[charIndex].Phase = phase; + Characters[charIndex].Pixels = new int[0]; + } + + /// + /// Adds a border character into the scanline + /// + /// + /// + private void AddBorderValue(int charIndex, int colourValue) + { + Characters[charIndex].Phase = RenderPhase.BORDER; + + switch (ScreenMode) + { + case 0: + Characters[charIndex].Pixels = new int[4]; + break; + case 1: + Characters[charIndex].Pixels = new int[8]; + break; + case 2: + Characters[charIndex].Pixels = new int[16]; + break; + case 3: + Characters[charIndex].Pixels = new int[8]; + break; + } + + + + for (int i = 0; i < Characters[charIndex].Pixels.Length; i++) + { + Characters[charIndex].Pixels[i] = colourValue; + } + } + + /// + /// Adds a display character into the scanline + /// Pixel matrix is calculated based on the current ScreenMode + /// + /// + /// + /// + public void AddDisplayValue(int charIndex, byte vid1, byte vid2, int[] pens) + { + Characters[charIndex].Phase = RenderPhase.DISPLAY; + + // generate pixels based on screen mode + switch (ScreenMode) + { + // 4 bits per pixel - 2 bytes - 4 pixels (8 CRT pixels) + // RECT + case 0: + Characters[charIndex].Pixels = new int[16]; + + int m0Count = 0; + + int pix = vid1 & 0xaa; + pix = ((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02 << 2)); + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + pix = vid1 & 0x55; + pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01 << 3))); + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + + pix = vid2 & 0xaa; + pix = ((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02 << 2)); + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + pix = vid2 & 0x55; + pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01 << 3))); + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]]; + /* + int m0B0P0i = vid1 & 0xaa; + int m0B0P0 = ((m0B0P0i & 0x80) >> 7) | ((m0B0P0i & 0x08) >> 2) | ((m0B0P0i & 0x20) >> 3) | ((m0B0P0i & 0x02 << 2)); + int m0B0P1i = vid1 & 85; + int m0B0P1 = ((m0B0P1i & 0x40) >> 6) | ((m0B0P1i & 0x04) >> 1) | ((m0B0P1i & 0x10) >> 2) | ((m0B0P1i & 0x01 << 3)); + + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B0P0]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B0P0]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B0P1]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B0P1]]; + + int m0B1P0i = vid2 & 170; + int m0B1P0 = ((m0B1P0i & 0x80) >> 7) | ((m0B1P0i & 0x08) >> 2) | ((m0B1P0i & 0x20) >> 3) | ((m0B1P0i & 0x02 << 2)); + int m0B1P1i = vid2 & 85; + int m0B1P1 = ((m0B1P1i & 0x40) >> 6) | ((m0B1P1i & 0x04) >> 1) | ((m0B1P1i & 0x10) >> 2) | ((m0B1P1i & 0x01 << 3)); + + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B1P0]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B1P0]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B1P1]]; + Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B1P1]]; + */ + break; + + // 2 bits per pixel - 2 bytes - 8 pixels (16 CRT pixels) + // SQUARE + case 1: + Characters[charIndex].Pixels = new int[8]; + + int m1Count = 0; + + int m1B0P0 = (((vid1 & 0x80) >> 7) | ((vid1 & 0x08) >> 2)); + int m1B0P1 = (((vid1 & 0x40) >> 6) | ((vid1 & 0x04) >> 1)); + int m1B0P2 = (((vid1 & 0x20) >> 5) | ((vid1 & 0x02))); + int m1B0P3 = (((vid1 & 0x10) >> 4) | ((vid1 & 0x01) << 1)); + + Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B0P0]]; + Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B0P1]]; + Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B0P2]]; + Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B0P3]]; + + int m1B1P0 = (((vid2 & 0x80) >> 7) | ((vid2 & 0x08) >> 2)); + int m1B1P1 = (((vid2 & 0x40) >> 6) | ((vid2 & 0x04) >> 1)); + int m1B1P2 = (((vid2 & 0x20) >> 5) | ((vid2 & 0x02))); + int m1B1P3 = (((vid2 & 0x10) >> 4) | ((vid2 & 0x01) << 1)); + + Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B1P0]]; + Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B1P1]]; + Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B1P2]]; + Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B1P3]]; + break; + + // 1 bit per pixel - 2 bytes - 16 pixels (16 CRT pixels) + // RECT + case 2: + Characters[charIndex].Pixels = new int[16]; + + int m2Count = 0; + + int[] pixBuff = new int[16]; + + for (int bit = 7; bit >= 0; bit--) + { + int val = vid1.Bit(bit) ? 1 : 0; + Characters[charIndex].Pixels[m2Count++] = CRTDevice.CPCHardwarePalette[pens[val]]; + + } + for (int bit = 7; bit >= 0; bit--) + { + int val = vid2.Bit(bit) ? 1 : 0; + Characters[charIndex].Pixels[m2Count++] = CRTDevice.CPCHardwarePalette[pens[val]]; + } + break; + + // 4 bits per pixel - 2 bytes - 4 pixels (8 CRT pixels) + // RECT + case 3: + Characters[charIndex].Pixels = new int[4]; + + int m3Count = 0; + + int m3B0P0i = vid1 & 170; + int m3B0P0 = ((m3B0P0i & 0x80) >> 7) | ((m3B0P0i & 0x08) >> 2) | ((m3B0P0i & 0x20) >> 3) | ((m3B0P0i & 0x02 << 2)); + int m3B0P1i = vid1 & 85; + int m3B0P1 = ((m3B0P1i & 0x40) >> 6) | ((m3B0P1i & 0x04) >> 1) | ((m3B0P1i & 0x10) >> 2) | ((m3B0P1i & 0x01 << 3)); + + Characters[charIndex].Pixels[m3Count++] = CRTDevice.CPCHardwarePalette[pens[m3B0P0]]; + Characters[charIndex].Pixels[m3Count++] = CRTDevice.CPCHardwarePalette[pens[m3B0P1]]; + + int m3B1P0i = vid1 & 170; + int m3B1P0 = ((m3B1P0i & 0x80) >> 7) | ((m3B1P0i & 0x08) >> 2) | ((m3B1P0i & 0x20) >> 3) | ((m3B1P0i & 0x02 << 2)); + int m3B1P1i = vid1 & 85; + int m3B1P1 = ((m3B1P1i & 0x40) >> 6) | ((m3B1P1i & 0x04) >> 1) | ((m3B1P1i & 0x10) >> 2) | ((m3B1P1i & 0x01 << 3)); + + Characters[charIndex].Pixels[m3Count++] = CRTDevice.CPCHardwarePalette[pens[m3B1P0]]; + Characters[charIndex].Pixels[m3Count++] = CRTDevice.CPCHardwarePalette[pens[m3B1P1]]; + break; + } + } + + /// + /// Returns the number of pixels decoded in this scanline (border and display) + /// + /// + private int GetPixelCount() + { + int cnt = 0; + + foreach (var c in Characters) + { + if (c.Pixels != null) + cnt += c.Pixels.Length; + } + + return cnt; + } + + /// + /// Called at the start of HSYNC + /// Processes and adds the scanline to the Screen Buffer + /// + public void CommitScanline() + { + int hScale = 1; + int vScale = 1; + + switch (ScreenMode) + { + case 0: + hScale = 1; + vScale = 2; + break; + case 1: + case 3: + hScale = 2; + vScale = 2; + break; + + case 2: + hScale = 1; + vScale = 2; + break; + } + + int hPix = GetPixelCount() * hScale; + //int hPix = GetPixelCount() * 2; + int leftOver = CRT.BufferWidth - hPix; + int lPad = leftOver / 2; + int rPad = lPad; + int rem = leftOver % 2; + if (rem != 0) + rPad += rem; + + if (LineIndex < CRT.TopLinesToTrim) + { + return; + } + + // render out the scanline + int pCount = (LineIndex - CRT.TopLinesToTrim) * vScale * CRT.BufferWidth; + + // vScale + for (int s = 0; s < vScale; s++) + { + // left padding + for (int lP = 0; lP < lPad; lP++) + { + CRT.ScreenBuffer[pCount++] = 0; + } + + // border and display + foreach (var c in Characters) + { + if (c.Pixels == null || c.Pixels.Length == 0) + continue; + + for (int p = 0; p < c.Pixels.Length; p++) + { + // hScale + for (int h = 0; h < hScale; h++) + { + CRT.ScreenBuffer[pCount++] = c.Pixels[p]; + } + + //CRT.ScreenBuffer[pCount++] = c.Pixels[p]; + } + } + + // right padding + for (int rP = 0; rP < rPad; rP++) + { + CRT.ScreenBuffer[pCount++] = 0; + } + + if (pCount != hPix) + { + + } + + CRT.ScanlineCounter++; + } + } + + public void Reset() + { + ScreenMode = 1; + Characters = new Character[64]; + + for (int i = 0; i < Characters.Length; i++) + { + Characters[i] = new Character(); + } + } + } + + /// + /// Contains data relating to one character written on one scanline + /// + public class Character + { + /// + /// Array of pixels generated for this character + /// + public int[] Pixels; + + /// + /// The type (NONE/BORDER/DISPLAY/HSYNC/VSYNC/HSYNC+VSYNC + /// + public RenderPhase Phase = RenderPhase.NONE; + + public Character() + { + Pixels = new int[0]; + } + } + + [Flags] + public enum RenderPhase : int + { + /// + /// Nothing + /// + NONE = 0, + /// + /// Border is being rendered + /// + BORDER = 1, + /// + /// Display rendered from video RAM + /// + DISPLAY = 2, + /// + /// HSYNC in progress + /// + HSYNC = 3, + /// + /// VSYNC in process + /// + VSYNC = 4, + /// + /// HSYNC occurs within a VSYNC + /// + HSYNCandVSYNC = 5 + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Input/StandardKeyboard.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Input/StandardKeyboard.cs new file mode 100644 index 0000000000..74e1b06d09 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/Input/StandardKeyboard.cs @@ -0,0 +1,160 @@ +using BizHawk.Common; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// The 48k keyboard device + /// + public class StandardKeyboard : IKeyboard + { + public CPCBase _machine { get; set; } + + private int _currentLine; + public int CurrentLine + { + get { return _currentLine; } + set + { + // bits 0-3 contain the line + var line = value & 0x0f; + + if (line > 0) + { + + } + + _currentLine = line; + } + } + + private bool[] _keyStatus; + public bool[] KeyStatus + { + get { return _keyStatus; } + set { _keyStatus = value; } + } + + private string[] _keyboardMatrix; + public string[] KeyboardMatrix + { + get { return _keyboardMatrix; } + set { _keyboardMatrix = value; } + } + + private string[] _nonMatrixKeys; + public string[] NonMatrixKeys + { + get { return _nonMatrixKeys; } + set { _nonMatrixKeys = value; } + } + + public StandardKeyboard(CPCBase machine) + { + _machine = machine; + //_machine.AYDevice.PortA_IN_CallBack = INCallback; + //_machine.AYDevice.PortA_OUT_CallBack = OUTCallback; + + // scancode rows, ascending (Bit0 - Bit7) + KeyboardMatrix = new string[] + { + // 0x40 + "Key CURUP", "Key CURRIGHT", "Key CURDOWN", "Key NUM9", "Key NUM6", "Key NUM3", "Key ENTER", "Key NUMPERIOD", + // 0x41 + "Key CURLEFT", "Key COPY", "Key NUM7", "Key NUM8", "Key NUM5", "Key NUM1", "Key NUM2", "Key NUM0", + // 0x42 + "Key CLR", "Key LeftBracket", "Key RETURN", "Key RightBracket", "Key NUM4", "Key SHIFT", "Key BackSlash", "Key CONTROL", + // 0x43 + "Key Hat", "Key Dash", "Key @", "Key P", "Key SemiColon", "Key Colon", "Key ForwardSlash", "Key Period", + // 0x44 + "Key 0", "Key 9", "Key O", "Key I", "Key L", "Key K", "Key M", "Key Comma", + // 0x45 + "Key 8", "Key 7", "Key U", "Key Y", "Key H", "Key J", "Key N", "Key SPACE", + // 0x46 + "Key 6", "Key 5", "Key R", "Key T", "Key G", "Key F", "Key B", "Key V", + // 0x47 + "Key 4", "Key 3", "Key E", "Key W", "Key S", "Key D", "Key C", "Key X", + // 0x48 + "Key 1", "Key 2", "Key ESC", "Key Q", "Key TAB", "Key A", "Key CAPSLOCK", "Key Z", + // 0x49 + "P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 Fire1", "P1 Fire2", "P1 Fire3", "Key DEL", + + }; + + // keystatus array to match the matrix + KeyStatus = new bool[8 * 10]; + + // nonmatrix keys (anything that hasnt already been taken) + var nonMatrix = new List(); + + foreach (var key in _machine.CPC.AmstradCPCControllerDefinition.BoolButtons) + { + if (!KeyboardMatrix.Any(s => s == key)) + nonMatrix.Add(key); + } + + NonMatrixKeys = nonMatrix.ToArray(); + } + + /// + /// Reads the currently selected line + /// + /// + public byte ReadCurrentLine() + { + var lin = _currentLine; // - 0x40; + var pos = lin * 8; + var l = KeyStatus.Skip(pos).Take(8).ToArray(); + BitArray bi = new BitArray(l); + byte[] bytes = new byte[1]; + bi.CopyTo(bytes, 0); + byte inv = (byte)(~bytes[0]); + return inv; + } + + /// + /// Returns the index of the key within the matrix + /// + /// + /// + public int GetKeyIndexFromMatrix(string key) + { + int index = Array.IndexOf(KeyboardMatrix, key); + return index; + } + + /// + /// Sets key status + /// + /// + /// + public void SetKeyStatus(string key, bool isPressed) + { + int index = GetKeyIndexFromMatrix(key); + KeyStatus[index] = isPressed; + } + + /// + /// Gets a key's status + /// + /// + /// + public bool GetKeyStatus(string key) + { + int index = GetKeyIndexFromMatrix(key); + return KeyStatus[index]; + } + + + public void SyncState(Serializer ser) + { + ser.BeginSection("Keyboard"); + ser.Sync("currentLine", ref _currentLine); + ser.Sync("keyStatus", ref _keyStatus, false); + ser.EndSection(); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/PPI/PPI_8255.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/PPI/PPI_8255.cs new file mode 100644 index 0000000000..921bf2a332 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/PPI/PPI_8255.cs @@ -0,0 +1,470 @@ +using BizHawk.Common; +using BizHawk.Common.NumberExtensions; +using System; +using System.Collections; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Emulates the PPI (8255) chip + /// http://www.cpcwiki.eu/imgs/d/df/PPI_M5L8255AP-5.pdf + /// http://www.cpcwiki.eu/index.php/8255 + /// + public class PPI_8255 : IPortIODevice + { + #region Devices + + private CPCBase _machine; + private CRCT_6845 CRTC => _machine.CRCT; + private AmstradGateArray GateArray => _machine.GateArray; + private IPSG PSG => _machine.AYDevice; + private DatacorderDevice Tape => _machine.TapeDevice; + private IKeyboard Keyboard => _machine.KeyboardDevice; + + #endregion + + #region Construction + + public PPI_8255(CPCBase machine) + { + _machine = machine; + Reset(); + } + + #endregion + + #region Implementation + + /// + /// BDIR Line connected to PSG + /// + public bool BDIR + { + get { return Regs[PORT_C].Bit(7); } + } + + /// + /// BC1 Line connected to PSG + /// + public bool BC1 + { + get { return Regs[PORT_C].Bit(6); } + } + + /* Port Constants */ + private const int PORT_A = 0; + private const int PORT_B = 1; + private const int PORT_C = 2; + private const int PORT_CONTROL = 3; + + /// + /// The i8255 internal data registers + /// + private byte[] Regs = new byte[4]; + + /// + /// Returns the currently latched port direction for Port A + /// + private PortDirection DirPortA + { + get { return Regs[PORT_CONTROL].Bit(4) ? PortDirection.Input : PortDirection.Output; } + } + + /// + /// Returns the currently latched port direction for Port B + /// + private PortDirection DirPortB + { + get { return Regs[PORT_CONTROL].Bit(1) ? PortDirection.Input : PortDirection.Output; } + } + + /// + /// Returns the currently latched port direction for Port C (lower half) + /// + private PortDirection DirPortCL + { + get { return Regs[PORT_CONTROL].Bit(0) ? PortDirection.Input : PortDirection.Output; } + } + + /// + /// Returns the currently latched port direction for Port C (upper half) + /// + private PortDirection DirPortCU + { + get { return Regs[PORT_CONTROL].Bit(3) ? PortDirection.Input : PortDirection.Output; } + } + + #region OUT Methods + + /// + /// Writes to Port A + /// + private void OUTPortA(int data) + { + // latch the data + Regs[PORT_A] = (byte)data; + + if (DirPortA == PortDirection.Output) + { + // PSG write + PSG.PortWrite(data); + } + } + + /// + /// Writes to Port B + /// + private void OUTPortB(int data) + { + // PortB is read only + // just latch the data + Regs[PORT_B] = (byte)data; + } + + /// + /// Writes to Port C + /// + private void OUTPortC(int data) + { + // latch the data + Regs[PORT_C] = (byte)data; + + if (DirPortCL == PortDirection.Output) + { + // lower Port C bits OUT + // keyboard line update + Keyboard.CurrentLine = Regs[PORT_C] & 0x0f; + } + + if (DirPortCU == PortDirection.Output) + { + // upper Port C bits OUT + // write to PSG using latched data + PSG.SetFunction(data); + PSG.PortWrite(Regs[PORT_A]); + + // cassete write data + //not implemeted + + // cas motor control + Tape.TapeMotor = Regs[PORT_C].Bit(4); + } + } + + /// + /// Writes to the control register + /// + /// + private void OUTControl(int data) + { + if (data.Bit(7)) + { + // update configuration + Regs[PORT_CONTROL] = (byte)data; + + // Writing to PIO Control Register (with Bit7 set), automatically resets PIO Ports A,B,C to 00h each + Regs[PORT_A] = 0; + Regs[PORT_B] = 0; + Regs[PORT_C] = 0; + } + else + { + // register is used to set/reset a single bit in Port C + bool isSet = data.Bit(0); + + // get the bit in PortC that we wish to change + var bit = (data >> 1) & 7; + + // modify this bit + if (isSet) + { + Regs[PORT_C] = (byte)(Regs[PORT_C] | (bit * bit)); + } + else + { + Regs[PORT_C] = (byte)(Regs[PORT_C] & ~(bit * bit)); + } + + // any other ouput business + if (DirPortCL == PortDirection.Output) + { + // update keyboard line + Keyboard.CurrentLine = Regs[PORT_C] & 0x0f; + } + + if (DirPortCU == PortDirection.Output) + { + // write to PSG using latched data + PSG.SetFunction(data); + PSG.PortWrite(Regs[PORT_A]); + + // cassete write data + //not implemeted + + // cas motor control + Tape.TapeMotor = Regs[PORT_C].Bit(4); + } + } + } + + #endregion + + #region IN Methods + + /// + /// Reads from Port A + /// + /// + private int INPortA() + { + if (DirPortA == PortDirection.Input) + { + // read from PSG + return PSG.PortRead(); + } + else + { + // Port A is set to output + // return latched value + return Regs[PORT_A]; + } + } + + /// + /// Reads from Port B + /// + /// + private int INPortB() + { + if (DirPortB == PortDirection.Input) + { + // build the PortB output + // start with every bit reset + BitArray rBits = new BitArray(8); + + // Bit0 - Vertical Sync ("1"=VSYNC active, "0"=VSYNC inactive) + if (CRTC.VSYNC) + rBits[0] = true; + + // Bits1-3 - Distributor ID. Usually set to 4=Awa, 5=Schneider, or 7=Amstrad + // force AMstrad + rBits[1] = true; + rBits[2] = true; + rBits[3] = true; + + // Bit4 - Screen Refresh Rate ("1"=50Hz, "0"=60Hz) + rBits[4] = true; + + // Bit5 - Expansion Port /EXP pin + rBits[5] = false; + + // Bit6 - Parallel/Printer port ready signal, "1" = not ready, "0" = Ready + rBits[6] = true; + + // Bit7 - Cassette data input + rBits[7] = Tape.GetEarBit(_machine.CPU.TotalExecutedCycles); + + // return the byte + byte[] bytes = new byte[1]; + rBits.CopyTo(bytes, 0); + return bytes[0]; + } + else + { + // return the latched value + return Regs[PORT_B]; + } + } + + /// + /// Reads from Port C + /// + /// + private int INPortC() + { + // get the PortC value + int val = Regs[PORT_C]; + + if (DirPortCU == PortDirection.Input) + { + // upper port C bits + // remove upper half + val &= 0x0f; + + // isolate control bits + var v = Regs[PORT_C] & 0xc0; + + if (v == 0xc0) + { + // set reg is present. change to write reg + v = 0x80; + } + + // cas wr is always set + val |= v | 0x20; + + if (Tape.TapeMotor) + { + val |= 0x10; + } + } + + if (DirPortCL == PortDirection.Input) + { + // lower port C bits + val |= 0x0f; + } + + return val; + } + + + #endregion + + #endregion + + #region Reset + + public void Reset() + { + for (int i = 0; i < 3; i++) + { + Regs[i] = 0xff; + } + + Regs[3] = 0xff; + } + + #endregion + + #region IPortIODevice + + /* + #F4XX %xxxx0x00 xxxxxxxx 8255 PIO Port A (PSG Data) Read Write + #F5XX %xxxx0x01 xxxxxxxx 8255 PIO Port B (Vsync,PrnBusy,Tape,etc.) Read - + #F6XX %xxxx0x10 xxxxxxxx 8255 PIO Port C (KeybRow,Tape,PSG Control) - Write + #F7XX %xxxx0x11 xxxxxxxx 8255 PIO Control-Register - Write + */ + + /// + /// Device responds to an IN instruction + /// + /// + /// + /// + public bool ReadPort(ushort port, ref int result) + { + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + // The 8255 responds to bit 11 reset with A10 and A12-A15 set + //if (portUpper.Bit(3)) + //return false; + + var PPIFunc = (port & 0x0300) >> 8; // portUpper & 3; + + switch (PPIFunc) + { + // Port A Read + case 0: + + // PSG (Sound/Keyboard/Joystick) + result = INPortA(); + + break; + + // Port B Read + case 1: + + // Vsync/Jumpers/PrinterBusy/CasIn/Exp + result = INPortB(); + + break; + + // Port C Read (docs define this as write-only but we do need to do some processing) + case 2: + + // KeybRow/CasOut/PSG + result = INPortC(); + + break; + } + + return true; + } + + /// + /// Device responds to an OUT instruction + /// + /// + /// + /// + public bool WritePort(ushort port, int result) + { + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + // The 8255 responds to bit 11 reset with A10 and A12-A15 set + if (portUpper.Bit(3)) + return false; + + var PPIFunc = portUpper & 3; + + switch (PPIFunc) + { + // Port A Write + case 0: + + // PSG (Sound/Keyboard/Joystick) + OUTPortA(result); + + break; + + // Port B Write + case 1: + + // Vsync/Jumpers/PrinterBusy/CasIn/Exp + OUTPortB(result); + + break; + + // Port C Write + case 2: + + // KeybRow/CasOut/PSG + OUTPortC(result); + + break; + + // Control Register Write + case 3: + + // Control + OUTControl((byte)result); + + break; + } + + return true; + } + + #endregion + + #region Serialization + + public void SyncState(Serializer ser) + { + ser.BeginSection("PPI"); + ser.Sync("Regs", ref Regs, false); + ser.EndSection(); + } + + #endregion + } + + public enum PortDirection + { + Input, + Output + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/SoundOutput/AY38912.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/SoundOutput/AY38912.cs new file mode 100644 index 0000000000..240acb9a2c --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/SoundOutput/AY38912.cs @@ -0,0 +1,888 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; +using System; +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Based heavily on the YM-2149F / AY-3-8910 emulator used in Unreal Speccy + /// (Originally created under Public Domain license by SMT jan.2006) /// + /// https://github.com/mkoloberdin/unrealspeccy/blob/master/sndrender/sndchip.cpp + /// https://github.com/mkoloberdin/unrealspeccy/blob/master/sndrender/sndchip.h + /// + public class AY38912 : IPSG + { + #region Device Fields + + /// + /// The emulated machine (passed in via constructor) + /// + private CPCBase _machine; + private IKeyboard _keyboard => _machine.KeyboardDevice; + + private int _tStatesPerFrame; + private int _sampleRate; + private int _samplesPerFrame; + private double _tStatesPerSample; + private short[] _audioBuffer; + private int _audioBufferIndex; + private int _lastStateRendered; + private int _clockCyclesPerFrame; + private int _cyclesPerSample; + + #endregion + + #region Construction & Initialization + + /// + /// Main constructor + /// + public AY38912(CPCBase machine) + { + _machine = machine; + + //_blipL.SetRates(1000000, 44100); + //_blipL.SetRates((_machine.GateArray.FrameLength * 50) / 4, 44100); + //_blipR.SetRates(1000000, 44100); + //_blipR.SetRates((_machine.GateArray.FrameLength * 50) / 4, 44100); + } + + /// + /// Initialises the AY chip + /// + public void Init(int sampleRate, int tStatesPerFrame) + { + InitTiming(sampleRate, tStatesPerFrame); + UpdateVolume(); + Reset(); + } + + #endregion + + #region AY Implementation + + #region Public Properties + + /// + /// AY mixer panning configuration + /// + [Flags] + public enum AYPanConfig + { + MONO = 0, + ABC = 1, + ACB = 2, + BAC = 3, + BCA = 4, + CAB = 5, + CBA = 6, + } + + /// + /// The AY panning configuration + /// + public AYPanConfig PanningConfiguration + { + get + { + return _currentPanTab; + } + set + { + if (value != _currentPanTab) + { + _currentPanTab = value; + UpdateVolume(); + } + } + } + + /// + /// The AY chip output volume + /// (0 - 100) + /// + public int Volume + { + get + { + return _volume; + } + set + { + //value = Math.Max(0, value); + //value = Math.Max(100, value); + if (_volume == value) + { + return; + } + _volume = value; + UpdateVolume(); + } + } + + /// + /// The currently selected register + /// + public int SelectedRegister + { + get { return _activeRegister; } + set + { + _activeRegister = (byte)value; + } + } + + #endregion + + #region Public Methods + + /// + /// Resets the PSG + /// + public void Reset() + { + /* + _noiseVal = 0x0FFFF; + _outABC = 0; + _outNoiseABC = 0; + _counterNoise = 0; + _counterA = 0; + _counterB = 0; + _counterC = 0; + _EnvelopeCounterBend = 0; + + // clear all the registers + for (int i = 0; i < 14; i++) + { + SelectedRegister = i; + PortWrite(0); + } + + randomSeed = 1; + + // number of frames to update + var fr = (_audioBufferIndex * _tStatesPerFrame) / _audioBuffer.Length; + + // update the audio buffer + BufferUpdate(fr); + */ + } + + /// + /// 0: Inactive + /// 1: Read Register + /// 2: Write Register + /// 3: Select Register + /// + public int ActiveFunction; + + public void SetFunction(int val) + { + int b = ((val & 0xc0) >> 6); + ActiveFunction = b; + } + + /// + /// Reads the value from the currently selected register + /// + /// + public int PortRead() + { + if (ActiveFunction == 1) + { + if (_activeRegister == 14) + { + if (PortAInput) + { + // exteral keyboard register + return _keyboard.ReadCurrentLine(); + } + else + { + return _keyboard.ReadCurrentLine() & _registers[_activeRegister]; + } + } + + if (_activeRegister < 16) + return _registers[_activeRegister]; + } + + + return 0; + } + + /// + /// Writes to the currently selected register + /// + /// + public void PortWrite(int value) + { + switch (ActiveFunction) + { + default: + break; + // select reg + case 3: + + int b = (value & 0x0f); + SelectedRegister = b; + + break; + + // write reg + case 2: + + if (_activeRegister == 14) + { + // external keyboard register + //return; + } + + if (_activeRegister >= 0x10) + return; + + byte val = (byte)value; + + if (((1 << _activeRegister) & ((1 << 1) | (1 << 3) | (1 << 5) | (1 << 13))) != 0) + val &= 0x0F; + + if (((1 << _activeRegister) & ((1 << 6) | (1 << 8) | (1 << 9) | (1 << 10))) != 0) + val &= 0x1F; + + if (_activeRegister != 13 && _registers[_activeRegister] == val) + return; + + _registers[_activeRegister] = val; + + switch (_activeRegister) + { + // Channel A (Combined Pitch) + // (not written to directly) + case 0: + case 1: + _dividerA = _registers[AY_A_FINE] | (_registers[AY_A_COARSE] << 8); + break; + // Channel B (Combined Pitch) + // (not written to directly) + case 2: + case 3: + _dividerB = _registers[AY_B_FINE] | (_registers[AY_B_COARSE] << 8); + break; + // Channel C (Combined Pitch) + // (not written to directly) + case 4: + case 5: + _dividerC = _registers[AY_C_FINE] | (_registers[AY_C_COARSE] << 8); + break; + // Noise Pitch + case 6: + _dividerN = val * 2; + break; + // Mixer + case 7: + _bit0 = 0 - ((val >> 0) & 1); + _bit1 = 0 - ((val >> 1) & 1); + _bit2 = 0 - ((val >> 2) & 1); + _bit3 = 0 - ((val >> 3) & 1); + _bit4 = 0 - ((val >> 4) & 1); + _bit5 = 0 - ((val >> 5) & 1); + + PortAInput = ((value & 0x40) == 0); + PortBInput = ((value & 0x80) == 0); + + break; + // Channel Volumes + case 8: + _eMaskA = (val & 0x10) != 0 ? -1 : 0; + _vA = ((val & 0x0F) * 2 + 1) & ~_eMaskA; + break; + case 9: + _eMaskB = (val & 0x10) != 0 ? -1 : 0; + _vB = ((val & 0x0F) * 2 + 1) & ~_eMaskB; + break; + case 10: + _eMaskC = (val & 0x10) != 0 ? -1 : 0; + _vC = ((val & 0x0F) * 2 + 1) & ~_eMaskC; + break; + // Envelope (Combined Duration) + // (not written to directly) + case 11: + case 12: + _dividerE = _registers[AY_E_FINE] | (_registers[AY_E_COARSE] << 8); + break; + // Envelope Shape + case 13: + // reset the envelope counter + _countE = 0; + + if ((_registers[AY_E_SHAPE] & 4) != 0) + { + // attack + _eState = 0; + _eDirection = 1; + } + else + { + // decay + _eState = 31; + _eDirection = -1; + } + break; + case 14: + // IO Port - not implemented + break; + } + + // do audio processing + BufferUpdate((int)_machine.CurrentFrameCycle); + + break; + } + + + } + + /// + /// Start of frame + /// + public void StartFrame() + { + _audioBufferIndex = 0; + BufferUpdate(0); + } + + /// + /// End of frame + /// + public void EndFrame() + { + BufferUpdate(_tStatesPerFrame); + } + + /// + /// Updates the audiobuffer based on the current frame t-state + /// + /// + public void UpdateSound(int frameCycle) + { + BufferUpdate(frameCycle); + } + + #endregion + + #region Private Fields + + /// + /// Register indicies + /// + private const int AY_A_FINE = 0; + private const int AY_A_COARSE = 1; + private const int AY_B_FINE = 2; + private const int AY_B_COARSE = 3; + private const int AY_C_FINE = 4; + private const int AY_C_COARSE = 5; + private const int AY_NOISEPITCH = 6; + private const int AY_MIXER = 7; + private const int AY_A_VOL = 8; + private const int AY_B_VOL = 9; + private const int AY_C_VOL = 10; + private const int AY_E_FINE = 11; + private const int AY_E_COARSE = 12; + private const int AY_E_SHAPE = 13; + private const int AY_PORT_A = 14; + private const int AY_PORT_B = 15; + + /// + /// The register array + /* + The AY-3-8910/8912 contains 16 internal registers as follows: + + Register Function Range + 0 Channel A fine pitch 8-bit (0-255) + 1 Channel A course pitch 4-bit (0-15) + 2 Channel B fine pitch 8-bit (0-255) + 3 Channel B course pitch 4-bit (0-15) + 4 Channel C fine pitch 8-bit (0-255) + 5 Channel C course pitch 4-bit (0-15) + 6 Noise pitch 5-bit (0-31) + 7 Mixer 8-bit (see below) + 8 Channel A volume 4-bit (0-15, see below) + 9 Channel B volume 4-bit (0-15, see below) + 10 Channel C volume 4-bit (0-15, see below) + 11 Envelope fine duration 8-bit (0-255) + 12 Envelope course duration 8-bit (0-255) + 13 Envelope shape 4-bit (0-15) + 14 I/O port A 8-bit (0-255) + 15 I/O port B 8-bit (0-255) (Not present on the AY-3-8912) + + * The volume registers (8, 9 and 10) contain a 4-bit setting but if bit 5 is set then that channel uses the + envelope defined by register 13 and ignores its volume setting. + * The mixer (register 7) is made up of the following bits (low=enabled): + + Bit: 7 6 5 4 3 2 1 0 + Register: I/O I/O Noise Noise Noise Tone Tone Tone + Channel: B A C B A C B A + + The AY-3-8912 ignores bit 7 of this register. + */ + /// + private int[] _registers = new int[16]; + + /// + /// The currently selected register + /// + private byte _activeRegister; + + + private bool PortAInput = true; + private bool PortBInput = true; + + /// + /// The frequency of the AY chip + /// + private static int _chipFrequency = 1000000; // 1773400; + + /// + /// The rendering resolution of the chip + /// + private double _resolution = 50D * 8D / _chipFrequency; + + /// + /// Channel generator state + /// + private int _bitA; + private int _bitB; + private int _bitC; + + /// + /// Envelope state + /// + private int _eState; + + /// + /// Envelope direction + /// + private int _eDirection; + + /// + /// Noise seed + /// + private int _noiseSeed; + + /// + /// Mixer state + /// + private int _bit0; + private int _bit1; + private int _bit2; + private int _bit3; + private int _bit4; + private int _bit5; + + /// + /// Noise generator state + /// + private int _bitN; + + /// + /// Envelope masks + /// + private int _eMaskA; + private int _eMaskB; + private int _eMaskC; + + /// + /// Amplitudes + /// + private int _vA; + private int _vB; + private int _vC; + + /// + /// Channel gen counters + /// + private int _countA; + private int _countB; + private int _countC; + + /// + /// Envelope gen counter + /// + private int _countE; + + /// + /// Noise gen counter + /// + private int _countN; + + /// + /// Channel gen dividers + /// + private int _dividerA; + private int _dividerB; + private int _dividerC; + + /// + /// Envelope gen divider + /// + private int _dividerE; + + /// + /// Noise gen divider + /// + private int _dividerN; + + /// + /// Panning table list + /// + private static List PanTabs = new List + { + // MONO + new uint[] { 50,50, 50,50, 50,50 }, + // ABC + new uint[] { 100,10, 66,66, 10,100 }, + // ACB + new uint[] { 100,10, 10,100, 66,66 }, + // BAC + new uint[] { 66,66, 100,10, 10,100 }, + // BCA + new uint[] { 10,100, 100,10, 66,66 }, + // CAB + new uint[] { 66,66, 10,100, 100,10 }, + // CBA + new uint[] { 10,100, 66,66, 100,10 } + }; + + /// + /// The currently selected panning configuration + /// + private AYPanConfig _currentPanTab = AYPanConfig.ABC; + + /// + /// The current volume + /// + private int _volume = 75; + + /// + /// Volume tables state + /// + private uint[][] _volumeTables; + + /// + /// Volume table to be used + /// + private static uint[] AYVolumes = new uint[] + { + 0x0000,0x0000,0x0340,0x0340,0x04C0,0x04C0,0x06F2,0x06F2, + 0x0A44,0x0A44,0x0F13,0x0F13,0x1510,0x1510,0x227E,0x227E, + 0x289F,0x289F,0x414E,0x414E,0x5B21,0x5B21,0x7258,0x7258, + 0x905E,0x905E,0xB550,0xB550,0xD7A0,0xD7A0,0xFFFF,0xFFFF, + }; + + #endregion + + #region Private Methods + + /// + /// Forces an update of the volume tables + /// + private void UpdateVolume() + { + int upperFloor = 40000; + var inc = (0xFFFF - upperFloor) / 100; + + var vol = inc * _volume; // ((ulong)0xFFFF * (ulong)_volume / 100UL) - 20000 ; + _volumeTables = new uint[6][]; + + // parent array + for (int j = 0; j < _volumeTables.Length; j++) + { + _volumeTables[j] = new uint[32]; + + // child array + for (int i = 0; i < _volumeTables[j].Length; i++) + { + _volumeTables[j][i] = (uint)( + (PanTabs[(int)_currentPanTab][j] * AYVolumes[i] * vol) / + (3 * 65535 * 100)); + } + } + } + + /// + /// Initializes timing information for the frame + /// + /// + /// + private void InitTiming(int sampleRate, int frameTactCount) + { + _sampleRate = sampleRate; + _tStatesPerFrame = frameTactCount; + _samplesPerFrame = sampleRate / 50; //882 + + _tStatesPerSample = (double)frameTactCount / (double)_samplesPerFrame; // 90; //(int)Math.Round(((double)_tStatesPerFrame * 50D) / + //(16D * (double)_sampleRate), + //MidpointRounding.AwayFromZero); + _audioBuffer = new short[_samplesPerFrame * 2]; + _audioBufferIndex = 0; + + ticksPerSample = ((double)_chipFrequency / sampleRate / 8); + } + private double ticksPerSample; + + private double tickCounter = 0; + + /// + /// Updates the audiobuffer based on the current frame t-state + /// + /// + private void BufferUpdate(int cycle) + { + if (cycle > _tStatesPerFrame) + { + // we are outside of the frame - just process the last value + cycle = _tStatesPerFrame; + } + + // get the current length of the audiobuffer + int bufferLength = _samplesPerFrame; // _audioBuffer.Length; + + double toEnd = ((double)(bufferLength * cycle) / (double)_tStatesPerFrame); + + // loop through the number of samples we need to render + while (_audioBufferIndex < toEnd) + { + // run the AY chip processing at the correct resolution + tickCounter += ticksPerSample; + + while (tickCounter > 0) + { + tickCounter--; + + if (++_countA >= _dividerA) + { + _countA = 0; + _bitA ^= -1; + } + + if (++_countB >= _dividerB) + { + _countB = 0; + _bitB ^= -1; + } + + if (++_countC >= _dividerC) + { + _countC = 0; + _bitC ^= -1; + } + + if (++_countN >= _dividerN) + { + _countN = 0; + _noiseSeed = (_noiseSeed * 2 + 1) ^ (((_noiseSeed >> 16) ^ (_noiseSeed >> 13)) & 1); + _bitN = 0 - ((_noiseSeed >> 16) & 1); + } + + if (++_countE >= _dividerE) + { + _countE = 0; + _eState += +_eDirection; + + if ((_eState & ~31) != 0) + { + var mask = (1 << _registers[AY_E_SHAPE]); + + if ((mask & ((1 << 0) | (1 << 1) | (1 << 2) | + (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6) | + (1 << 7) | (1 << 9) | (1 << 15))) != 0) + { + _eState = _eDirection = 0; + } + else if ((mask & ((1 << 8) | (1 << 12))) != 0) + { + _eState &= 31; + } + else if ((mask & ((1 << 10) | (1 << 14))) != 0) + { + _eDirection = -_eDirection; + _eState += _eDirection; + } + else + { + // 11,13 + _eState = 31; + _eDirection = 0; + } + } + } + } + + // mix the sample + var mixA = ((_eMaskA & _eState) | _vA) & ((_bitA | _bit0) & (_bitN | _bit3)); + var mixB = ((_eMaskB & _eState) | _vB) & ((_bitB | _bit1) & (_bitN | _bit4)); + var mixC = ((_eMaskC & _eState) | _vC) & ((_bitC | _bit2) & (_bitN | _bit5)); + + var l = _volumeTables[0][mixA]; + var r = _volumeTables[1][mixA]; + + l += _volumeTables[2][mixB]; + r += _volumeTables[3][mixB]; + l += _volumeTables[4][mixC]; + r += _volumeTables[5][mixC]; + + _audioBuffer[_audioBufferIndex * 2] = (short)l; + _audioBuffer[(_audioBufferIndex * 2) + 1] = (short)r; + + _audioBufferIndex++; + } + + _lastStateRendered = cycle; + } + + #endregion + + #endregion + + #region ISoundProvider + + public bool CanProvideAsync => false; + + public SyncSoundMode SyncMode => SyncSoundMode.Sync; + + public void SetSyncMode(SyncSoundMode mode) + { + if (mode != SyncSoundMode.Sync) + throw new InvalidOperationException("Only Sync mode is supported."); + } + + public void GetSamplesAsync(short[] samples) + { + throw new NotSupportedException("Async is not available"); + } + + public void DiscardSamples() + { + _audioBuffer = new short[_samplesPerFrame * 2]; + //_blipL.Clear(); + //_blipR.Clear(); + } + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + nsamp = _samplesPerFrame; + samples = _audioBuffer; + DiscardSamples(); + tickCounter = 0; + return; + /* + _blipL.EndFrame((uint)SampleClock); + _blipR.EndFrame((uint)SampleClock); + SampleClock = 0; + + int sampL = _blipL.SamplesAvailable(); + int sampR = _blipR.SamplesAvailable(); + + if (sampL > sampR) + nsamp = sampL; + else + nsamp = sampR; + + short[] buffL = new short[sampL]; + short[] buffR = new short[sampR]; + + _blipL.ReadSamples(buffL, sampL - 1, false); + _blipR.ReadSamples(buffR, sampR - 1, false); + + if (_audioBuffer.Length != nsamp * 2) + _audioBuffer = new short[nsamp * 2]; + + int p = 0; + for (int i = 0; i < nsamp; i++) + { + if (i < sampL) + _audioBuffer[p++] = buffL[i]; + if (i < sampR) + _audioBuffer[p++] = buffR[i]; + } + + //nsamp = _samplesPerFrame; + samples = _audioBuffer; + DiscardSamples(); + */ + } + + #endregion + + #region State Serialization + + public int nullDump = 0; + + /// + /// State serialization + /// + /// + public void SyncState(Serializer ser) + { + ser.BeginSection("PSG-AY"); + + ser.Sync("ActiveFunction", ref ActiveFunction); + + ser.Sync("_tStatesPerFrame", ref _tStatesPerFrame); + ser.Sync("_sampleRate", ref _sampleRate); + ser.Sync("_samplesPerFrame", ref _samplesPerFrame); + //ser.Sync("_tStatesPerSample", ref _tStatesPerSample); + ser.Sync("_audioBufferIndex", ref _audioBufferIndex); + ser.Sync("_audioBuffer", ref _audioBuffer, false); + ser.Sync("PortAInput", ref PortAInput); + ser.Sync("PortBInput", ref PortBInput); + + ser.Sync("_registers", ref _registers, false); + ser.Sync("_activeRegister", ref _activeRegister); + ser.Sync("_bitA", ref _bitA); + ser.Sync("_bitB", ref _bitB); + ser.Sync("_bitC", ref _bitC); + ser.Sync("_eState", ref _eState); + ser.Sync("_eDirection", ref _eDirection); + ser.Sync("_noiseSeed", ref _noiseSeed); + ser.Sync("_bit0", ref _bit0); + ser.Sync("_bit1", ref _bit1); + ser.Sync("_bit2", ref _bit2); + ser.Sync("_bit3", ref _bit3); + ser.Sync("_bit4", ref _bit4); + ser.Sync("_bit5", ref _bit5); + ser.Sync("_bitN", ref _bitN); + ser.Sync("_eMaskA", ref _eMaskA); + ser.Sync("_eMaskB", ref _eMaskB); + ser.Sync("_eMaskC", ref _eMaskC); + ser.Sync("_vA", ref _vA); + ser.Sync("_vB", ref _vB); + ser.Sync("_vC", ref _vC); + ser.Sync("_countA", ref _countA); + ser.Sync("_countB", ref _countB); + ser.Sync("_countC", ref _countC); + ser.Sync("_countE", ref _countE); + ser.Sync("_countN", ref _countN); + ser.Sync("_dividerA", ref _dividerA); + ser.Sync("_dividerB", ref _dividerB); + ser.Sync("_dividerC", ref _dividerC); + ser.Sync("_dividerE", ref _dividerE); + ser.Sync("_dividerN", ref _dividerN); + ser.SyncEnum("_currentPanTab", ref _currentPanTab); + ser.Sync("_volume", ref nullDump); + + for (int i = 0; i < 6; i++) + { + ser.Sync("volTable" + i, ref _volumeTables[i], false); + } + + if (ser.IsReader) + _volume = _machine.CPC.Settings.AYVolume; + + ser.EndSection(); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/SoundOutput/Beeper.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/SoundOutput/Beeper.cs new file mode 100644 index 0000000000..5f1f96c92e --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Hardware/SoundOutput/Beeper.cs @@ -0,0 +1,214 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; +using System; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Logical Beeper class + /// Used to emulate the sound generated by tape loading + /// This implementation uses BlipBuffer and should *always* output at 44100 with 882 samples per frame + /// (so that it can be mixed easily further down the line) + /// + public class Beeper : ISoundProvider, IBeeperDevice + { + #region Fields and Properties + + /// + /// Sample Rate + /// This usually has to be 44100 for ISoundProvider + /// + private int _sampleRate; + public int SampleRate + { + get { return _sampleRate; } + set { _sampleRate = value; } + } + + /// + /// Buzzer volume + /// Accepts an int 0-100 value + /// + private int _volume; + public int Volume + { + get + { + return VolumeConverterOut(_volume); + } + set + { + var newVol = VolumeConverterIn(value); + if (newVol != _volume) + blip.Clear(); + _volume = VolumeConverterIn(value); + } + } + + /// + /// The last used volume (used to modify blipbuffer delta values) + /// + private int lastVolume; + + /// + /// The number of cpu cycles per frame + /// + private long _tStatesPerFrame; + + /// + /// The parent emulated machine + /// + private CPCBase _machine; + + /// + /// The last pulse + /// + private bool LastPulse; + + /// + /// The last T-State (cpu cycle) that the last pulse was received + /// + private long LastPulseTState; + + /// + /// Device blipbuffer + /// + private readonly BlipBuffer blip = new BlipBuffer(883); + + #endregion + + #region Private Methods + + /// + /// Takes an int 0-100 and returns the relevant short volume to output + /// + /// + /// + private int VolumeConverterIn(int vol) + { + int maxLimit = short.MaxValue / 3; + int increment = maxLimit / 100; + + return vol * increment; + } + + /// + /// Takes an short volume and returns the relevant int value 0-100 + /// + /// + /// + private int VolumeConverterOut(int shortvol) + { + int maxLimit = short.MaxValue / 3; + int increment = maxLimit / 100; + + if (shortvol > maxLimit) + shortvol = maxLimit; + + return shortvol / increment; + } + + #endregion + + #region Construction & Initialisation + + public Beeper(CPCBase machine) + { + _machine = machine; + } + + /// + /// Initialises the beeper + /// + public void Init(int sampleRate, int tStatesPerFrame) + { + blip.SetRates((4000000), sampleRate); + _sampleRate = sampleRate; + _tStatesPerFrame = tStatesPerFrame; + } + + #endregion + + #region IBeeperDevice + + /// + /// Processes an incoming pulse value and adds it to the blipbuffer + /// + /// + public void ProcessPulseValue(bool pulse) + { + if (!_machine._renderSound) + return; + + if (LastPulse == pulse) + { + // no change + blip.AddDelta((uint)_machine.CurrentFrameCycle, 0); + } + + else + { + if (pulse) + blip.AddDelta((uint)_machine.CurrentFrameCycle, (short)(_volume)); + else + blip.AddDelta((uint)_machine.CurrentFrameCycle, -(short)(_volume)); + + lastVolume = _volume; + } + + LastPulse = pulse; + } + + #endregion + + #region ISoundProvider + + public bool CanProvideAsync => false; + + public SyncSoundMode SyncMode => SyncSoundMode.Sync; + + public void SetSyncMode(SyncSoundMode mode) + { + if (mode != SyncSoundMode.Sync) + throw new InvalidOperationException("Only Sync mode is supported."); + } + + public void GetSamplesAsync(short[] samples) + { + throw new NotSupportedException("Async is not available"); + } + + public void DiscardSamples() + { + blip.Clear(); + } + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + blip.EndFrame((uint)_tStatesPerFrame); + nsamp = blip.SamplesAvailable(); + samples = new short[nsamp * 2]; + blip.ReadSamples(samples, nsamp, true); + for (int i = 0; i < nsamp * 2; i += 2) + { + samples[i + 1] = samples[i]; + } + } + + #endregion + + #region State Serialization + + public void SyncState(Serializer ser) + { + ser.BeginSection("Buzzer"); + ser.Sync("_tStatesPerFrame", ref _tStatesPerFrame); + ser.Sync("_sampleRate", ref _sampleRate); + ser.Sync("LastPulse", ref LastPulse); + ser.Sync("LastPulseTState", ref LastPulseTState); + ser.EndSection(); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC464/CPC464.Memory.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC464/CPC464.Memory.cs new file mode 100644 index 0000000000..d2b069b5a0 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC464/CPC464.Memory.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPC464 + /// * Memory * + /// + public partial class CPC464 : CPCBase + { + /// + /// Simulates reading from the bus + /// ROM paging should be handled here + /// + /// + /// + public override byte ReadBus(ushort addr) + { + int divisor = addr / 0x4000; + byte result = 0xff; + + switch (divisor) + { + // 0x000 or LowerROM + case 0: + if (LowerROMPaged) + result = ROMLower[addr % 0x4000]; + else + result = RAM0[addr % 0x4000]; + break; + + // 0x4000 + case 1: + result = RAM1[addr % 0x4000]; + break; + + // 0x8000 + case 2: + result = RAM2[addr % 0x4000]; + break; + + // 0xc000 or UpperROM + case 3: + if (UpperROMPaged) + result = ROM0[addr % 0x4000]; + else + result = RAM3[addr % 0x4000]; + break; + default: + break; + } + + return result; + } + + /// + /// Simulates writing to the bus + /// Writes to the bus ALWAYS go to RAM, regardless of what upper and lower ROMs are paged in + /// + /// + /// + public override void WriteBus(ushort addr, byte value) + { + int divisor = addr / 0x4000; + + switch (divisor) + { + // RAM 0x000 + case 0: + RAM0[addr % 0x4000] = value; + break; + + // RAM 0x4000 + case 1: + RAM1[addr % 0x4000] = value; + break; + + // RAM 0x8000 + case 2: + RAM2[addr % 0x4000] = value; + break; + + // RAM 0xc000 + case 3: + RAM3[addr % 0x4000] = value; + break; + default: + break; + } + } + + /// + /// Reads a byte of data from a specified memory address + /// + /// + /// + public override byte ReadMemory(ushort addr) + { + var data = ReadBus(addr); + return data; + } + + /// + /// Writes a byte of data to a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public override void WriteMemory(ushort addr, byte value) + { + WriteBus(addr, value); + } + + + /// + /// Sets up the ROM + /// + /// + /// + public override void InitROM(RomData[] romData) + { + foreach (var r in romData) + { + if (r.ROMType == RomData.ROMChipType.Lower) + { + for (int i = 0; i < 0x4000; i++) + { + ROMLower[i] = r.RomBytes[i]; + + } + } + else + { + for (int i = 0; i < 0x4000; i++) + { + switch (r.ROMPosition) + { + case 0: + ROM0[i] = r.RomBytes[i]; + break; + case 7: + ROM7[i] = r.RomBytes[i]; + break; + } + } + } + } + + LowerROMPaged = true; + UpperROMPaged = true; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC464/CPC464.Port.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC464/CPC464.Port.cs new file mode 100644 index 0000000000..d51f167e5e --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC464/CPC464.Port.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPC464 + /// * Port * + /// + public partial class CPC464 : CPCBase + { + /// + /// Reads a byte of data from a specified port address + /// + /// + /// + public override byte ReadPort(ushort port) + { + BitArray portBits = new BitArray(BitConverter.GetBytes(port)); + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + int result = 0xff; + + if (DecodeINPort(port) == PortDevice.GateArray) + { + GateArray.ReadPort(port, ref result); + } + else if (DecodeINPort(port) == PortDevice.CRCT) + { + CRCT.ReadPort(port, ref result); + } + else if (DecodeINPort(port) == PortDevice.ROMSelect) + { + + } + else if (DecodeINPort(port) == PortDevice.Printer) + { + + } + else if (DecodeINPort(port) == PortDevice.PPI) + { + PPI.ReadPort(port, ref result); + } + else if (DecodeINPort(port) == PortDevice.Expansion) + { + + } + + return (byte)result; + } + + /// + /// Writes a byte of data to a specified port address + /// Because of the port decoding, multiple devices can be written to + /// + /// + /// + public override void WritePort(ushort port, byte value) + { + BitArray portBits = new BitArray(BitConverter.GetBytes(port)); + BitArray dataBits = new BitArray(BitConverter.GetBytes(value)); + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + var devs = DecodeOUTPort(port); + + foreach (var d in devs) + { + if (d == PortDevice.GateArray) + { + GateArray.WritePort(port, value); + } + else if (d == PortDevice.RAMManagement) + { + // not present in the unexpanded CPC464 + } + else if (d == PortDevice.CRCT) + { + CRCT.WritePort(port, value); + } + else if (d == PortDevice.ROMSelect) + { + + } + else if (d == PortDevice.Printer) + { + + } + else if (d == PortDevice.PPI) + { + PPI.WritePort(port, value); + } + else if (d == PortDevice.Expansion) + { + + } + } + + return; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC464/CPC464.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC464/CPC464.cs new file mode 100644 index 0000000000..930efa8fcd --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC464/CPC464.cs @@ -0,0 +1,48 @@ +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPC464 construction + /// + public partial class CPC464 : CPCBase + { + #region Construction + + /// + /// Main constructor + /// + /// + /// + public CPC464(AmstradCPC cpc, Z80A cpu, List files, bool autoTape, AmstradCPC.BorderType borderType) + { + CPC = cpc; + CPU = cpu; + + FrameLength = 79872; + + CRCT = new CRCT_6845(CRCT_6845.CRCTType.MC6845, this); + //CRT = new CRTDevice(this); + GateArray = new AmstradGateArray(this, AmstradGateArray.GateArrayType.Amstrad40007); + PPI = new PPI_8255(this); + + TapeBuzzer = new Beeper(this); + TapeBuzzer.Init(44100, FrameLength); + + //AYDevice = new PSG(this, PSG.ay38910_type_t.AY38910_TYPE_8912, GateArray.PSGClockSpeed, 882 * 50); + AYDevice = new AY38912(this); + AYDevice.Init(44100, FrameLength); + + KeyboardDevice = new StandardKeyboard(this); + + TapeDevice = new DatacorderDevice(autoTape); + TapeDevice.Init(this); + + InitializeMedia(files); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC6128/CPC6128.Memory.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC6128/CPC6128.Memory.cs new file mode 100644 index 0000000000..80e52351ac --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC6128/CPC6128.Memory.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPC6128 + /// * Memory * + /// + public partial class CPC6128 : CPCBase + { + /// + /// Simulates reading from the bus + /// ROM and RAM paging should be handled here + /// + /// + /// + public override byte ReadBus(ushort addr) + { + int divisor = addr / 0x4000; + byte result = 0xff; + + switch (divisor) + { + // RAM 0x000 + case 0: + if (LowerROMPaged) + { + result = ROMLower[addr % 0x4000]; + } + else + { + switch (RAMConfig) + { + case 2: + result = RAM4[addr % 0x4000]; + break; + default: + result = RAM0[addr % 0x4000]; + break; + } + } + break; + + // RAM 0x4000 + case 1: + switch (RAMConfig) + { + case 0: + case 1: + result = RAM1[addr % 0x4000]; + break; + case 2: + case 5: + result = RAM5[addr % 0x4000]; + break; + case 3: + result = RAM3[addr % 0x4000]; + break; + case 4: + result = RAM4[addr % 0x4000]; + break; + case 6: + result = RAM6[addr % 0x4000]; + break; + case 7: + result = RAM7[addr % 0x4000]; + break; + } + + break; + + // RAM 0x8000 + case 2: + switch (RAMConfig) + { + case 2: + result = RAM6[addr % 0x4000]; + break; + default: + result = RAM2[addr % 0x4000]; + break; + } + break; + + // RAM 0xc000 + case 3: + if (UpperROMPaged) + { + switch (UpperROMPosition) + { + case 7: + result = ROM7[addr % 0x4000]; + break; + case 0: + default: + result = ROM0[addr % 0x4000]; + break; + } + } + else + { + switch (RAMConfig) + { + case 1: + case 2: + case 3: + result = RAM7[addr % 0x4000]; + break; + default: + result = RAM3[addr % 0x4000]; + break; + } + } + break; + default: + break; + } + + return result; + } + + /// + /// Simulates writing to the bus + /// Writes to the bus ALWAYS go to RAM, regardless of what upper and lower ROMs are paged in + /// + /// + /// + public override void WriteBus(ushort addr, byte value) + { + int divisor = addr / 0x4000; + + switch (divisor) + { + // RAM 0x000 + case 0: + switch (RAMConfig) + { + case 2: + RAM4[addr % 0x4000] = value; + break; + default: + RAM0[addr % 0x4000] = value; + break; + } + break; + + // RAM 0x4000 + case 1: + switch (RAMConfig) + { + case 0: + case 1: + RAM1[addr % 0x4000] = value; + break; + case 2: + case 5: + RAM5[addr % 0x4000] = value; + break; + case 3: + RAM3[addr % 0x4000] = value; + break; + case 4: + RAM4[addr % 0x4000] = value; + break; + case 6: + RAM6[addr % 0x4000] = value; + break; + case 7: + RAM7[addr % 0x4000] = value; + break; + } + + break; + + // RAM 0x8000 + case 2: + switch (RAMConfig) + { + case 2: + RAM6[addr % 0x4000] = value; + break; + default: + RAM2[addr % 0x4000] = value; + break; + } + break; + + // RAM 0xc000 + case 3: + switch (RAMConfig) + { + case 1: + case 2: + case 3: + RAM7[addr % 0x4000] = value; + break; + default: + RAM3[addr % 0x4000] = value; + break; + } + break; + default: + break; + } + } + + /// + /// Reads a byte of data from a specified memory address + /// + /// + /// + public override byte ReadMemory(ushort addr) + { + var data = ReadBus(addr); + return data; + } + + /// + /// Writes a byte of data to a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public override void WriteMemory(ushort addr, byte value) + { + WriteBus(addr, value); + } + + + /// + /// Sets up the ROM + /// + /// + /// + public override void InitROM(RomData[] romData) + { + foreach (var r in romData) + { + if (r.ROMType == RomData.ROMChipType.Lower) + { + for (int i = 0; i < 0x4000; i++) + { + ROMLower[i] = r.RomBytes[i]; + + } + } + else + { + for (int i = 0; i < 0x4000; i++) + { + switch (r.ROMPosition) + { + case 0: + ROM0[i] = r.RomBytes[i]; + break; + case 7: + ROM7[i] = r.RomBytes[i]; + break; + } + } + } + } + + LowerROMPaged = true; + UpperROMPaged = true; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC6128/CPC6128.Port.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC6128/CPC6128.Port.cs new file mode 100644 index 0000000000..089bab38b8 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC6128/CPC6128.Port.cs @@ -0,0 +1,140 @@ +using BizHawk.Common.NumberExtensions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPC6128 + /// * Port * + /// + public partial class CPC6128 : CPCBase + { + /// + /// Reads a byte of data from a specified port address + /// + /// + /// + public override byte ReadPort(ushort port) + { + BitArray portBits = new BitArray(BitConverter.GetBytes(port)); + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + int result = 0xff; + + if (DecodeINPort(port) == PortDevice.GateArray) + { + GateArray.ReadPort(port, ref result); + } + else if (DecodeINPort(port) == PortDevice.CRCT) + { + CRCT.ReadPort(port, ref result); + } + else if (DecodeINPort(port) == PortDevice.ROMSelect) + { + + } + else if (DecodeINPort(port) == PortDevice.Printer) + { + + } + else if (DecodeINPort(port) == PortDevice.PPI) + { + PPI.ReadPort(port, ref result); + } + else if (DecodeINPort(port) == PortDevice.Expansion) + { + if (!port.Bit(7)) + { + // FDC + if (port.Bit(8) && !port.Bit(0)) + { + // FDC status register + UPDDiskDevice.ReadStatus(ref result); + } + if (port.Bit(8) && port.Bit(0)) + { + // FDC data register + UPDDiskDevice.ReadData(ref result); + } + } + } + + return (byte)result; + } + + /// + /// Writes a byte of data to a specified port address + /// Because of the port decoding, multiple devices can be written to + /// + /// + /// + public override void WritePort(ushort port, byte value) + { + BitArray portBits = new BitArray(BitConverter.GetBytes(port)); + BitArray dataBits = new BitArray(BitConverter.GetBytes(value)); + byte portUpper = (byte)(port >> 8); + byte portLower = (byte)(port & 0xff); + + var devs = DecodeOUTPort(port); + + foreach (var d in devs) + { + if (d == PortDevice.GateArray) + { + GateArray.WritePort(port, value); + } + else if (d == PortDevice.RAMManagement) + { + if (value.Bit(7) && value.Bit(6)) + { + RAMConfig = value & 0x07; + + // additional 64K bank index + var b64 = value & 0x38; + } + } + else if (d == PortDevice.CRCT) + { + CRCT.WritePort(port, value); + } + else if (d == PortDevice.ROMSelect) + { + UpperROMPosition = value; + } + else if (d == PortDevice.Printer) + { + + } + else if (d == PortDevice.PPI) + { + PPI.WritePort(port, value); + } + else if (d == PortDevice.Expansion) + { + if (!port.Bit(7)) + { + // FDC + if (port.Bit(8) && !port.Bit(0) || port.Bit(8) && port.Bit(0)) + { + // FDC data register + UPDDiskDevice.WriteData(value); + } + if ((!port.Bit(8) && !port.Bit(0)) || (!port.Bit(8) && port.Bit(0))) + { + // FDC motor + UPDDiskDevice.Motor(value); + } + } + } + } + + return; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC6128/CPC6128.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC6128/CPC6128.cs new file mode 100644 index 0000000000..c0aae0bf36 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPC6128/CPC6128.cs @@ -0,0 +1,51 @@ +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// CPC6128 construction + /// + public partial class CPC6128 : CPCBase + { + #region Construction + + /// + /// Main constructor + /// + /// + /// + public CPC6128(AmstradCPC cpc, Z80A cpu, List files, bool autoTape, AmstradCPC.BorderType borderType) + { + CPC = cpc; + CPU = cpu; + + FrameLength = 79872; + + CRCT = new CRCT_6845(CRCT_6845.CRCTType.MC6845, this); + //CRT = new CRTDevice(this); + GateArray = new AmstradGateArray(this, AmstradGateArray.GateArrayType.Amstrad40007); + PPI = new PPI_8255(this); + + TapeBuzzer = new Beeper(this); + TapeBuzzer.Init(44100, FrameLength); + + //AYDevice = new PSG(this, PSG.ay38910_type_t.AY38910_TYPE_8912, GateArray.PSGClockSpeed, 882 * 50); + AYDevice = new AY38912(this); + AYDevice.Init(44100, FrameLength); + + KeyboardDevice = new StandardKeyboard(this); + + TapeDevice = new DatacorderDevice(autoTape); + TapeDevice.Init(this); + + UPDDiskDevice = new NECUPD765(); + UPDDiskDevice.Init(this); + + InitializeMedia(files); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Input.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Input.cs new file mode 100644 index 0000000000..5fc7868bab --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Input.cs @@ -0,0 +1,295 @@ +using System.Collections.Generic; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Input * + /// + public abstract partial class CPCBase + { + string Play = "Play Tape"; + string Stop = "Stop Tape"; + string RTZ = "RTZ Tape"; + string Record = "Record Tape"; + string NextTape = "Insert Next Tape"; + string PrevTape = "Insert Previous Tape"; + string NextBlock = "Next Tape Block"; + string PrevBlock = "Prev Tape Block"; + string TapeStatus = "Get Tape Status"; + + string NextDisk = "Insert Next Disk"; + string PrevDisk = "Insert Previous Disk"; + string EjectDisk = "Eject Current Disk"; + string DiskStatus = "Get Disk Status"; + + string HardResetStr = "Power"; + string SoftResetStr = "Reset"; + + bool pressed_Play = false; + bool pressed_Stop = false; + bool pressed_RTZ = false; + bool pressed_NextTape = false; + bool pressed_PrevTape = false; + bool pressed_NextBlock = false; + bool pressed_PrevBlock = false; + bool pressed_TapeStatus = false; + bool pressed_NextDisk = false; + bool pressed_PrevDisk = false; + bool pressed_EjectDisk = false; + bool pressed_DiskStatus = false; + bool pressed_HardReset = false; + bool pressed_SoftReset = false; + + /// + /// Cycles through all the input callbacks + /// This should be done once per frame + /// + public void PollInput() + { + CPC.InputCallbacks.Call(); + + lock (this) + { + // parse single keyboard matrix keys. + // J1 and J2 are scanned as part of the keyboard + for (var i = 0; i < KeyboardDevice.KeyboardMatrix.Length; i++) + { + string key = KeyboardDevice.KeyboardMatrix[i]; + bool prevState = KeyboardDevice.GetKeyStatus(key); + bool currState = CPC._controller.IsPressed(key); + + if (currState != prevState) + KeyboardDevice.SetKeyStatus(key, currState); + } + + // non matrix keys (J2) + foreach (string k in KeyboardDevice.NonMatrixKeys) + { + if (!k.StartsWith("P2")) + continue; + + bool currState = CPC._controller.IsPressed(k); + + switch (k) + { + case "P2 Up": + if (currState) + KeyboardDevice.SetKeyStatus("Key 6", true); + else if (!KeyboardDevice.GetKeyStatus("Key 6")) + KeyboardDevice.SetKeyStatus("Key 6", false); + break; + case "P2 Down": + if (currState) + KeyboardDevice.SetKeyStatus("Key 5", true); + else if (!KeyboardDevice.GetKeyStatus("Key 5")) + KeyboardDevice.SetKeyStatus("Key 5", false); + break; + case "P2 Left": + if (currState) + KeyboardDevice.SetKeyStatus("Key R", true); + else if (!KeyboardDevice.GetKeyStatus("Key R")) + KeyboardDevice.SetKeyStatus("Key R", false); + break; + case "P2 Right": + if (currState) + KeyboardDevice.SetKeyStatus("Key T", true); + else if (!KeyboardDevice.GetKeyStatus("Key T")) + KeyboardDevice.SetKeyStatus("Key T", false); + break; + case "P2 Fire": + if (currState) + KeyboardDevice.SetKeyStatus("Key G", true); + else if (!KeyboardDevice.GetKeyStatus("Key G")) + KeyboardDevice.SetKeyStatus("Key G", false); + break; + } + } + } + + // Tape control + if (CPC._controller.IsPressed(Play)) + { + if (!pressed_Play) + { + CPC.OSD_FireInputMessage(Play); + TapeDevice.Play(); + pressed_Play = true; + } + } + else + pressed_Play = false; + + if (CPC._controller.IsPressed(Stop)) + { + if (!pressed_Stop) + { + CPC.OSD_FireInputMessage(Stop); + TapeDevice.Stop(); + pressed_Stop = true; + } + } + else + pressed_Stop = false; + + if (CPC._controller.IsPressed(RTZ)) + { + if (!pressed_RTZ) + { + CPC.OSD_FireInputMessage(RTZ); + TapeDevice.RTZ(); + pressed_RTZ = true; + } + } + else + pressed_RTZ = false; + + if (CPC._controller.IsPressed(Record)) + { + + } + if (CPC._controller.IsPressed(NextTape)) + { + if (!pressed_NextTape) + { + CPC.OSD_FireInputMessage(NextTape); + TapeMediaIndex++; + pressed_NextTape = true; + } + } + else + pressed_NextTape = false; + + if (CPC._controller.IsPressed(PrevTape)) + { + if (!pressed_PrevTape) + { + CPC.OSD_FireInputMessage(PrevTape); + TapeMediaIndex--; + pressed_PrevTape = true; + } + } + else + pressed_PrevTape = false; + + if (CPC._controller.IsPressed(NextBlock)) + { + if (!pressed_NextBlock) + { + CPC.OSD_FireInputMessage(NextBlock); + TapeDevice.SkipBlock(true); + pressed_NextBlock = true; + } + } + else + pressed_NextBlock = false; + + if (CPC._controller.IsPressed(PrevBlock)) + { + if (!pressed_PrevBlock) + { + CPC.OSD_FireInputMessage(PrevBlock); + TapeDevice.SkipBlock(false); + pressed_PrevBlock = true; + } + } + else + pressed_PrevBlock = false; + + if (CPC._controller.IsPressed(TapeStatus)) + { + if (!pressed_TapeStatus) + { + //Spectrum.OSD_FireInputMessage(TapeStatus); + CPC.OSD_ShowTapeStatus(); + pressed_TapeStatus = true; + } + } + else + pressed_TapeStatus = false; + + if (CPC._controller.IsPressed(HardResetStr)) + { + if (!pressed_HardReset) + { + HardReset(); + pressed_HardReset = true; + } + } + else + pressed_HardReset = false; + + if (CPC._controller.IsPressed(SoftResetStr)) + { + if (!pressed_SoftReset) + { + SoftReset(); + pressed_SoftReset = true; + } + } + else + pressed_SoftReset = false; + + // disk control + if (CPC._controller.IsPressed(NextDisk)) + { + if (!pressed_NextDisk) + { + CPC.OSD_FireInputMessage(NextDisk); + DiskMediaIndex++; + pressed_NextDisk = true; + } + } + else + pressed_NextDisk = false; + + if (CPC._controller.IsPressed(PrevDisk)) + { + if (!pressed_PrevDisk) + { + CPC.OSD_FireInputMessage(PrevDisk); + DiskMediaIndex--; + pressed_PrevDisk = true; + } + } + else + pressed_PrevDisk = false; + + if (CPC._controller.IsPressed(EjectDisk)) + { + if (!pressed_EjectDisk) + { + CPC.OSD_FireInputMessage(EjectDisk); + //if (UPDDiskDevice != null) + // UPDDiskDevice.FDD_EjectDisk(); + } + } + else + pressed_EjectDisk = false; + + if (CPC._controller.IsPressed(DiskStatus)) + { + if (!pressed_DiskStatus) + { + //Spectrum.OSD_FireInputMessage(TapeStatus); + CPC.OSD_ShowDiskStatus(); + pressed_DiskStatus = true; + } + } + else + pressed_DiskStatus = false; + } + + /// + /// Signs whether input read has been requested + /// This forms part of the IEmulator LagFrame implementation + /// + private bool inputRead; + public bool InputRead + { + get { return inputRead; } + set { inputRead = value; } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Media.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Media.cs new file mode 100644 index 0000000000..af001c4957 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Media.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Imported media * + /// + public abstract partial class CPCBase + { + /// + /// The tape or disk image(s) that are passed in from the main ZXSpectrum class + /// + protected List mediaImages { get; set; } + + /// + /// Tape images + /// + public List tapeImages { get; set; } + + /// + /// Disk images + /// + public List diskImages { get; set; } + + /// + /// The index of the currently 'loaded' tape image + /// + protected int tapeMediaIndex; + public int TapeMediaIndex + { + get { return tapeMediaIndex; } + set + { + int tmp = value; + int result = value; + + if (tapeImages == null || tapeImages.Count() == 0) + { + // no tape images found + return; + } + + if (value >= tapeImages.Count()) + { + // media at this index does not exist - loop back to 0 + result = 0; + } + else if (value < 0) + { + // negative index not allowed - move to last item in the collection + result = tapeImages.Count() - 1; + } + + // load the media into the tape device + tapeMediaIndex = result; + // fire osd message + //Spectrum.OSD_TapeInserted(); + LoadTapeMedia(); + } + } + + /// + /// The index of the currently 'loaded' disk image + /// + protected int diskMediaIndex; + public int DiskMediaIndex + { + get { return diskMediaIndex; } + set + { + int tmp = value; + int result = value; + + if (diskImages == null || diskImages.Count() == 0) + { + // no tape images found + return; + } + + if (value >= diskImages.Count()) + { + // media at this index does not exist - loop back to 0 + result = 0; + } + else if (value < 0) + { + // negative index not allowed - move to last item in the collection + result = diskImages.Count() - 1; + } + + // load the media into the disk device + diskMediaIndex = result; + + // fire osd message + CPC.OSD_DiskInserted(); + + LoadDiskMedia(); + } + } + + /// + /// Called on first instantiation (and subsequent core reboots) + /// + /// + protected void InitializeMedia(List files) + { + mediaImages = files; + LoadAllMedia(); + } + + /// + /// Attempts to load all media into the relevant structures + /// + protected void LoadAllMedia() + { + tapeImages = new List(); + diskImages = new List(); + + int cnt = 0; + foreach (var m in mediaImages) + { + switch (IdentifyMedia(m)) + { + case CPCMediaType.Tape: + tapeImages.Add(m); + CPC._tapeInfo.Add(CPC._gameInfo[cnt]); + break; + case CPCMediaType.Disk: + diskImages.Add(m); + CPC._diskInfo.Add(CPC._gameInfo[cnt]); + break; + case CPCMediaType.DiskDoubleSided: + // this is a bit tricky. we will attempt to parse the double sided disk image byte array, + // then output two separate image byte arrays + List working = new List(); + foreach (DiskType type in Enum.GetValues(typeof(DiskType))) + { + bool found = false; + + switch (type) + { + case DiskType.CPCExtended: + found = CPCExtendedFloppyDisk.SplitDoubleSided(m, working); + break; + case DiskType.CPC: + found = CPCFloppyDisk.SplitDoubleSided(m, working); + break; + } + + if (found) + { + // add side 1 + diskImages.Add(working[0]); + // add side 2 + diskImages.Add(working[1]); + + Common.GameInfo one = new Common.GameInfo(); + Common.GameInfo two = new Common.GameInfo(); + var gi = CPC._gameInfo[cnt]; + for (int i = 0; i < 2; i++) + { + Common.GameInfo work = new Common.GameInfo(); + if (i == 0) + { + work = one; + } + else if (i == 1) + { + work = two; + } + + work.FirmwareHash = gi.FirmwareHash; + work.Hash = gi.Hash; + work.Name = gi.Name + " (Parsed Side " + (i + 1) + ")"; + work.Region = gi.Region; + work.NotInDatabase = gi.NotInDatabase; + work.Status = gi.Status; + work.System = gi.System; + + CPC._diskInfo.Add(work); + } + } + else + { + + } + } + break; + } + + cnt++; + } + + if (tapeImages.Count > 0) + LoadTapeMedia(); + + if (diskImages.Count > 0) + LoadDiskMedia(); + } + + /// + /// Attempts to load a tape into the tape device based on tapeMediaIndex + /// + protected void LoadTapeMedia() + { + TapeDevice.LoadTape(tapeImages[tapeMediaIndex]); + } + + /// + /// Attempts to load a disk into the disk device based on diskMediaIndex + /// + protected void LoadDiskMedia() + { + if (this.GetType() == typeof(CPC464)) + { + CPC.CoreComm.ShowMessage("You are trying to load one of more disk images.\n\n Please select something other than CPC 464 emulation immediately and reboot the core"); + return; + } + + UPDDiskDevice.FDD_LoadDisk(diskImages[diskMediaIndex]); + } + + /// + /// Identifies and sorts the various media types + /// + /// + private CPCMediaType IdentifyMedia(byte[] data) + { + // get first 16 bytes as a string + string hdr = Encoding.ASCII.GetString(data.Take(16).ToArray()); + + // disk checking first + if (hdr.ToUpper().Contains("EXTENDED CPC DSK") || hdr.ToUpper().Contains("MV - CPC")) + { + // amstrad .dsk disk file + // check for number of sides + var sides = data[0x31]; + if (sides == 1) + return CPCMediaType.Disk; + else + return CPCMediaType.DiskDoubleSided; + } + + // tape checking + if (hdr.ToUpper().StartsWith("ZXTAPE!")) + { + // cdt tape file + return CPCMediaType.Tape; + } + + // not found + return CPCMediaType.None; + } + } + + public enum CPCMediaType + { + None, + Tape, + Disk, + DiskDoubleSided + } +} + diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Memory.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Memory.cs new file mode 100644 index 0000000000..fca9c18f31 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Memory.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Memory * + /// + public abstract partial class CPCBase + { + #region Memory Fields & Properties + + /* ROM Banks */ + /// + /// Lower: OS ROM + /// + public byte[] ROMLower = new byte[0x4000]; + /// + /// Upper: POS 0 (usually BASIC) + /// + public byte[] ROM0 = new byte[0x4000]; + /// + /// Upper: POS 7 (usually AMSDOS) + /// + public byte[] ROM7 = new byte[0x4000]; + + /* RAM Banks - Lower 64K */ + public byte[] RAM0 = new byte[0x4000]; + public byte[] RAM1 = new byte[0x4000]; + public byte[] RAM2 = new byte[0x4000]; + public byte[] RAM3 = new byte[0x4000]; + + /* RAM Banks - Upper 64K */ + public byte[] RAM4 = new byte[0x4000]; + public byte[] RAM5 = new byte[0x4000]; + public byte[] RAM6 = new byte[0x4000]; + public byte[] RAM7 = new byte[0x4000]; + + /// + /// Signs whether Upper ROM is paged in + /// + public bool UpperROMPaged; + + /// + /// The position of the currently paged upper ROM + /// + public int UpperROMPosition; + + /// + /// Signs whether Lower ROM is paged in + /// + public bool LowerROMPaged; + + /// + /// The currently selected RAM config + /// + public int RAMConfig; + + /// + /// Always 0 on a CPC6128 + /// On a machine with more than 128K RAM (standard memory expansion) this selects each additional 64K above the first upper 64K + /// + public int RAM64KBank; + + #endregion + + #region Memory Related Methods + + /// + /// Simulates reading from the bus + /// Paging should be handled here + /// + /// + /// + public abstract byte ReadBus(ushort addr); + + /// + /// Pushes a value onto the data bus that should be valid as long as the interrupt is true + /// + /// + /// + public virtual byte PushBus() + { + return 0xFF; + } + + /// + /// Simulates writing to the bus + /// Paging should be handled here + /// + /// + /// + public abstract void WriteBus(ushort addr, byte value); + + /// + /// Reads a byte of data from a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public abstract byte ReadMemory(ushort addr); + + /// + /// Writes a byte of data to a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public abstract void WriteMemory(ushort addr, byte value); + + /// + /// Sets up the ROM + /// + /// + public abstract void InitROM(RomData[] romData); + + /// + /// ULA reads the memory at the specified address + /// (No memory contention) + /// + /// + /// + public virtual byte FetchScreenMemory(ushort addr) + { + int divisor = addr / 0x4000; + byte result = 0xff; + + switch (divisor) + { + // 0x000 + case 0: + result = RAM0[addr % 0x4000]; + break; + + // 0x4000 + case 1: + result = RAM1[addr % 0x4000]; + break; + + // 0x8000 + case 2: + result = RAM2[addr % 0x4000]; + break; + + // 0xc000 or UpperROM + case 3: + result = RAM3[addr % 0x4000]; + break; + default: + break; + } + + return result; + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Port.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Port.cs new file mode 100644 index 0000000000..ccc61b9165 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.Port.cs @@ -0,0 +1,115 @@ + +using BizHawk.Common.NumberExtensions; +using System; +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Port Access * + /// + public abstract partial class CPCBase + { + /// + /// Reads a byte of data from a specified port address + /// + /// + /// + public abstract byte ReadPort(ushort port); + + /// + /// Writes a byte of data to a specified port address + /// + /// + /// + public abstract void WritePort(ushort port, byte value); + + /// + /// Returns a single port device enum based on the port address + /// (for IN operations) + /// https://web.archive.org/web/20090808085929/http://www.cepece.info/amstrad/docs/iopord.html + /// http://www.cpcwiki.eu/index.php/I/O_Port_Summary + /// + /// + /// + protected virtual PortDevice DecodeINPort(ushort port) + { + PortDevice dev = PortDevice.Unknown; + + if (!port.Bit(15) && port.Bit(14)) + dev = PortDevice.GateArray; + + else if (!port.Bit(15)) + dev = PortDevice.RAMManagement; + + else if (!port.Bit(14)) + dev = PortDevice.CRCT; + + else if (!port.Bit(13)) + dev = PortDevice.ROMSelect; + + else if (!port.Bit(12)) + dev = PortDevice.Printer; + + else if (!port.Bit(11)) + dev = PortDevice.PPI; + + else if (!port.Bit(10)) + dev = PortDevice.Expansion; + + return dev; + } + + /// + /// Returns a list of port device enums based on the port address + /// (for OUT operations) + /// https://web.archive.org/web/20090808085929/http://www.cepece.info/amstrad/docs/iopord.html + /// http://www.cpcwiki.eu/index.php/I/O_Port_Summary + /// + /// + /// + protected virtual List DecodeOUTPort(ushort port) + { + List devs = new List(); + + if (!port.Bit(15) && port.Bit(14)) + devs.Add(PortDevice.GateArray); + + if (!port.Bit(15)) + devs.Add(PortDevice.RAMManagement); + + if (!port.Bit(14)) + devs.Add(PortDevice.CRCT); + + if (!port.Bit(13)) + devs.Add(PortDevice.ROMSelect); + + if (!port.Bit(12)) + devs.Add(PortDevice.Printer); + + if (!port.Bit(11)) + devs.Add(PortDevice.PPI); + + if (!port.Bit(10)) + devs.Add(PortDevice.Expansion); + + return devs; + } + + /// + /// Potential port devices + /// + public enum PortDevice + { + Unknown, + GateArray, + RAMManagement, + CRCT, + ROMSelect, + Printer, + PPI, + Expansion + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.cs new file mode 100644 index 0000000000..0fb5f9a916 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/CPCBase.cs @@ -0,0 +1,374 @@ +using BizHawk.Common; +using BizHawk.Emulation.Cores.Components.Z80A; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Main properties / fields / contruction* + /// + public abstract partial class CPCBase + { + #region Devices + + /// + /// The calling ZXSpectrum class (piped in via constructor) + /// + public AmstradCPC CPC { get; set; } + + /// + /// Reference to the instantiated Z80 cpu (piped in via constructor) + /// + public Z80A CPU { get; set; } + + /// + /// ROM and extended info + /// + public RomData RomData { get; set; } + + /// + /// The Amstrad datacorder device + /// + public virtual DatacorderDevice TapeDevice { get; set; } + + /// + /// beeper output for the tape + /// + public IBeeperDevice TapeBuzzer { get; set; } + + /// + /// Device representing the AY-3-8912 chip found in the CPC + /// + public IPSG AYDevice { get; set; } + + /// + /// The keyboard device + /// Technically, this is controlled by the PSG, but has been abstracted due to the port over from ZXHawk + /// + public IKeyboard KeyboardDevice { get; set; } + + /// + /// The Amstrad disk drive + /// + public virtual NECUPD765 UPDDiskDevice { get; set; } + + /// + /// The Cathode Ray Tube Controller chip + /// + public CRCT_6845 CRCT { get; set; } + + /// + /// The Amstrad gate array + /// + public AmstradGateArray GateArray { get; set; } + + /// + /// Renders pixels to the screen + /// + //public CRTDevice CRT { get; set; } + + /// + /// The PPI contoller chip + /// + public PPI_8255 PPI { get; set; } + + /// + /// The length of a standard frame in CPU cycles + /// + public int FrameLength; + + #endregion + + #region Emulator State + + /// + /// Signs whether the frame has ended + /// + public bool FrameCompleted; + + /// + /// Overflow from the previous frame (in Z80 cycles) + /// + public int OverFlow; + + /// + /// The total number of frames rendered + /// + public int FrameCount; + + /// + /// The current cycle (T-State) that we are at in the frame + /// + public long _frameCycles; + + /// + /// Stores where we are in the frame after each CPU cycle + /// + public long LastFrameStartCPUTick; + + /// + /// Gets the current frame cycle according to the CPU tick count + /// + public virtual long CurrentFrameCycle => GateArray.FrameClock; // CPU.TotalExecutedCycles - LastFrameStartCPUTick; + + /// + /// Non-Deterministic bools + /// + public bool _render; + public bool _renderSound; + + #endregion + + #region Constants + + /// + /// Mask constants & misc + /// + protected const int BORDER_BIT = 0x07; + protected const int EAR_BIT = 0x10; + protected const int MIC_BIT = 0x08; + protected const int TAPE_BIT = 0x40; + protected const int AY_SAMPLE_RATE = 16; + + #endregion + + #region Emulation Loop + + /// + /// Executes a single frame + /// + public virtual void ExecuteFrame(bool render, bool renderSound) + { + GateArray.FrameEnd = false; + CRCT.lineCounter = 0; + + InputRead = false; + _render = render; + _renderSound = renderSound; + + FrameCompleted = false; + + if (UPDDiskDevice == null || !UPDDiskDevice.FDD_IsDiskLoaded) + TapeDevice.StartFrame(); + + if (_renderSound) + { + AYDevice.StartFrame(); + } + + PollInput(); + + //CRT.SetupVideo(); + //CRT.ScanlineCounter = 0; + + while (!GateArray.FrameEnd) + { + GateArray.ClockCycle(); + + // cycle the tape device + if (UPDDiskDevice == null || !UPDDiskDevice.FDD_IsDiskLoaded) + TapeDevice.TapeCycle(); + } + // we have reached the end of a frame + LastFrameStartCPUTick = CPU.TotalExecutedCycles; // - OverFlow; + + if (AYDevice != null) + AYDevice.EndFrame(); + + FrameCount++; + + if (UPDDiskDevice == null || !UPDDiskDevice.FDD_IsDiskLoaded) + TapeDevice.EndFrame(); + + FrameCompleted = true; + + // is this a lag frame? + CPC.IsLagFrame = !InputRead; + + // FDC debug + if (UPDDiskDevice != null && UPDDiskDevice.writeDebug) + { + // only write UPD log every second + if (FrameCount % 10 == 0) + { + System.IO.File.AppendAllLines(UPDDiskDevice.outputfile, UPDDiskDevice.dLog); + UPDDiskDevice.dLog = new System.Collections.Generic.List(); + //System.IO.File.WriteAllText(UPDDiskDevice.outputfile, UPDDiskDevice.outputString); + } + } + + GateArray.FrameClock = 0; + } + + #endregion + + #region Reset Functions + + /// + /// Hard reset of the emulated machine + /// + public virtual void HardReset() + { + /* + //ULADevice.ResetInterrupt(); + ROMPaged = 0; + SpecialPagingMode = false; + RAMPaged = 0; + CPU.RegPC = 0; + + Spectrum.SetCpuRegister("SP", 0xFFFF); + Spectrum.SetCpuRegister("IY", 0xFFFF); + Spectrum.SetCpuRegister("IX", 0xFFFF); + Spectrum.SetCpuRegister("AF", 0xFFFF); + Spectrum.SetCpuRegister("BC", 0xFFFF); + Spectrum.SetCpuRegister("DE", 0xFFFF); + Spectrum.SetCpuRegister("HL", 0xFFFF); + Spectrum.SetCpuRegister("SP", 0xFFFF); + Spectrum.SetCpuRegister("Shadow AF", 0xFFFF); + Spectrum.SetCpuRegister("Shadow BC", 0xFFFF); + Spectrum.SetCpuRegister("Shadow DE", 0xFFFF); + Spectrum.SetCpuRegister("Shadow HL", 0xFFFF); + + CPU.Regs[CPU.I] = 0; + CPU.Regs[CPU.R] = 0; + + TapeDevice.Reset(); + if (AYDevice != null) + AYDevice.Reset(); + + byte[][] rams = new byte[][] + { + RAM0, + RAM1, + RAM2, + RAM3, + RAM4, + RAM5, + RAM6, + RAM7 + }; + + foreach (var r in rams) + { + for (int i = 0; i < r.Length; i++) + { + r[i] = 0x00; + } + } + */ + } + + /// + /// Soft reset of the emulated machine + /// + public virtual void SoftReset() + { + /* + //ULADevice.ResetInterrupt(); + ROMPaged = 0; + SpecialPagingMode = false; + RAMPaged = 0; + CPU.RegPC = 0; + + Spectrum.SetCpuRegister("SP", 0xFFFF); + Spectrum.SetCpuRegister("IY", 0xFFFF); + Spectrum.SetCpuRegister("IX", 0xFFFF); + Spectrum.SetCpuRegister("AF", 0xFFFF); + Spectrum.SetCpuRegister("BC", 0xFFFF); + Spectrum.SetCpuRegister("DE", 0xFFFF); + Spectrum.SetCpuRegister("HL", 0xFFFF); + Spectrum.SetCpuRegister("SP", 0xFFFF); + Spectrum.SetCpuRegister("Shadow AF", 0xFFFF); + Spectrum.SetCpuRegister("Shadow BC", 0xFFFF); + Spectrum.SetCpuRegister("Shadow DE", 0xFFFF); + Spectrum.SetCpuRegister("Shadow HL", 0xFFFF); + + CPU.Regs[CPU.I] = 0; + CPU.Regs[CPU.R] = 0; + + TapeDevice.Reset(); + if (AYDevice != null) + AYDevice.Reset(); + + byte[][] rams = new byte[][] + { + RAM0, + RAM1, + RAM2, + RAM3, + RAM4, + RAM5, + RAM6, + RAM7 + }; + + foreach (var r in rams) + { + for (int i = 0; i < r.Length; i++) + { + r[i] = 0x00; + } + } + */ + } + + #endregion + + #region IStatable + + public void SyncState(Serializer ser) + { + ser.BeginSection("CPCMachine"); + ser.Sync("FrameCompleted", ref FrameCompleted); + ser.Sync("OverFlow", ref OverFlow); + ser.Sync("FrameCount", ref FrameCount); + ser.Sync("_frameCycles", ref _frameCycles); + ser.Sync("inputRead", ref inputRead); + ser.Sync("LastFrameStartCPUTick", ref LastFrameStartCPUTick); + ser.Sync("ROMLower", ref ROMLower, false); + ser.Sync("ROM0", ref ROM0, false); + ser.Sync("ROM7", ref ROM7, false); + ser.Sync("RAM0", ref RAM0, false); + ser.Sync("RAM1", ref RAM1, false); + ser.Sync("RAM2", ref RAM2, false); + ser.Sync("RAM3", ref RAM3, false); + ser.Sync("RAM4", ref RAM4, false); + ser.Sync("RAM5", ref RAM5, false); + ser.Sync("RAM6", ref RAM6, false); + ser.Sync("RAM7", ref RAM7, false); + + ser.Sync("UpperROMPosition", ref UpperROMPosition); + ser.Sync("UpperROMPaged", ref UpperROMPaged); + ser.Sync("LowerROMPaged", ref LowerROMPaged); + ser.Sync("RAMConfig", ref RAMConfig); + ser.Sync("RAM64KBank", ref RAM64KBank); + + CRCT.SyncState(ser); + //CRT.SyncState(ser); + GateArray.SyncState(ser); + KeyboardDevice.SyncState(ser); + TapeBuzzer.SyncState(ser); + AYDevice.SyncState(ser); + + ser.Sync("tapeMediaIndex", ref tapeMediaIndex); + if (ser.IsReader) + TapeMediaIndex = tapeMediaIndex; + + TapeDevice.SyncState(ser); + + ser.Sync("diskMediaIndex", ref diskMediaIndex); + if (ser.IsReader) + DiskMediaIndex = diskMediaIndex; + + if (UPDDiskDevice != null) + { + UPDDiskDevice.SyncState(ser); + } + + ser.EndSection(); + } + + #endregion + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/GateArrayBase.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/GateArrayBase.cs new file mode 100644 index 0000000000..768a377b67 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/GateArrayBase.cs @@ -0,0 +1,543 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Amstrad Gate Array * + /// https://web.archive.org/web/20170612081209/http://www.grimware.org/doku.php/documentations/devices/gatearray + /// + public abstract class GateArrayBase : IVideoProvider + { + public int Z80ClockSpeed = 4000000; + public int FrameLength = 79872; + + #region Devices + + private CPCBase _machine; + private Z80A CPU => _machine.CPU; + private CRCT_6845 CRCT => _machine.CRCT; + private IPSG PSG => _machine.AYDevice; + + #endregion + + #region Constants + + /// + /// CRTC Register constants + /// + public const int HOR_TOTAL = 0; + public const int HOR_DISPLAYED = 1; + public const int HOR_SYNC_POS = 2; + public const int HOR_AND_VER_SYNC_WIDTHS = 3; + public const int VER_TOTAL = 4; + public const int VER_TOTAL_ADJUST = 5; + public const int VER_DISPLAYED = 6; + public const int VER_SYNC_POS = 7; + public const int INTERLACE_SKEW = 8; + public const int MAX_RASTER_ADDR = 9; + public const int CUR_START_RASTER = 10; + public const int CUR_END_RASTER = 11; + public const int DISP_START_ADDR_H = 12; + public const int DISP_START_ADDR_L = 13; + public const int CUR_ADDR_H = 14; + public const int CUR_ADDR_L = 15; + public const int LPEN_ADDR_H = 16; + public const int LPEN_ADDR_L = 17; + + #endregion + + #region Palletes + + /// + /// The standard CPC Pallete (ordered by firmware #) + /// http://www.cpcwiki.eu/index.php/CPC_Palette + /// + private static readonly int[] CPCFirmwarePalette = + { + Colors.ARGB(0x00, 0x00, 0x00), // Black + Colors.ARGB(0x00, 0x00, 0x80), // Blue + Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue + Colors.ARGB(0x80, 0x00, 0x00), // Red + Colors.ARGB(0x80, 0x00, 0x80), // Magenta + Colors.ARGB(0x80, 0x00, 0xFF), // Mauve + Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red + Colors.ARGB(0xFF, 0x00, 0x80), // Purple + Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta + Colors.ARGB(0x00, 0x80, 0x00), // Green + Colors.ARGB(0x00, 0x80, 0x80), // Cyan + Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue + Colors.ARGB(0x80, 0x80, 0x00), // Yellow + Colors.ARGB(0x80, 0x80, 0x80), // White + Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue + Colors.ARGB(0xFF, 0x80, 0x00), // Orange + Colors.ARGB(0xFF, 0x80, 0x80), // Pink + Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta + Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green + Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green + Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan + Colors.ARGB(0x80, 0xFF, 0x00), // Lime + Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green + Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan + Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow + Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow + Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White + }; + + /// + /// The standard CPC Pallete (ordered by hardware #) + /// http://www.cpcwiki.eu/index.php/CPC_Palette + /// + private static readonly int[] CPCHardwarePalette = + { + Colors.ARGB(0x80, 0x80, 0x80), // White + Colors.ARGB(0x80, 0x80, 0x80), // White (duplicate) + Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green + Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow + Colors.ARGB(0x00, 0x00, 0x80), // Blue + Colors.ARGB(0xFF, 0x00, 0x80), // Purple + Colors.ARGB(0x00, 0x80, 0x80), // Cyan + Colors.ARGB(0xFF, 0x80, 0x80), // Pink + Colors.ARGB(0xFF, 0x00, 0x80), // Purple (duplicate) + Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow (duplicate) + Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow + Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White + Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red + Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta + Colors.ARGB(0xFF, 0x80, 0x00), // Orange + Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta + Colors.ARGB(0x00, 0x00, 0x80), // Blue (duplicate) + Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green (duplicate) + Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green + Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan + Colors.ARGB(0x00, 0x00, 0x00), // Black + Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue + Colors.ARGB(0x00, 0x80, 0x00), // Green + Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue + Colors.ARGB(0x80, 0x00, 0x80), // Magenta + Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green + Colors.ARGB(0x80, 0xFF, 0x00), // Lime + Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan + Colors.ARGB(0x80, 0x00, 0x00), // Red + Colors.ARGB(0x80, 0x00, 0xFF), // Mauve + Colors.ARGB(0x80, 0x80, 0x00), // Yellow + Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue + }; + + #endregion + + #region Construction + + public GateArrayBase(CPCBase machine) + { + _machine = machine; + PenColours = new int[17]; + SetupScreenSize(); + Reset(); + } + + /// + /// Inits the pen lookup table + /// + public void SetupScreenMapping() + { + for (int m = 0; m < 4; m++) + { + Lookup[m] = new int[256 * 8]; + int pos = 0; + + for (int b = 0; b < 256; b++) + { + switch (m) + { + case 1: + int pc = (((b & 0x80) >> 7) | ((b & 0x80) >> 2)); + Lookup[m][pos++] = pc; + Lookup[m][pos++] = pc; + pc = (((b & 0x40) >> 6) | ((b & 0x04) >> 1)); + Lookup[m][pos++] = pc; + Lookup[m][pos++] = pc; + pc = (((b & 0x20) >> 5) | (b & 0x02)); + Lookup[m][pos++] = pc; + Lookup[m][pos++] = pc; + pc = (((b & 0x10) >> 4) | ((b & 0x01) << 1)); + break; + + case 2: + for (int i = 7; i >= 0; i--) + { + bool pixel_on = ((b & (1 << i)) != 0); + Lookup[m][pos++] = pixel_on ? 1 : 0; + } + break; + + case 3: + case 0: + int pc2 = (b & 0xAA); + pc2 = ( + ((pc2 & 0x80) >> 7) | + ((pc2 & 0x08) >> 2) | + ((pc2 & 0x20) >> 3) | + ((pc2 & 0x02) << 2)); + Lookup[m][pos++] = pc2; + Lookup[m][pos++] = pc2; + Lookup[m][pos++] = pc2; + Lookup[m][pos++] = pc2; + pc2 = (b & 0x55); + pc2 = ( + ((pc2 & 0x40) >> 6) | + ((pc2 & 0x04) >> 1) | + ((pc2 & 0x10) >> 2) | + ((pc2 & 0x01) << 3)); + + Lookup[m][pos++] = pc2; + Lookup[m][pos++] = pc2; + Lookup[m][pos++] = pc2; + Lookup[m][pos++] = pc2; + break; + } + } + } + } + + #endregion + + #region State + + private int[] PenColours; + private int CurrentPen; + private int ScreenMode; + private int INTScanlineCnt; + private int VSYNCDelyCnt; + + private int[][] Lookup = new int[4][]; + + private bool DoModeUpdate; + + private int LatchedMode; + private int buffPos; + + public bool FrameEnd; + + public bool WaitLine; + + #endregion + + #region Clock Operations + + /// + /// The gatearray runs on a 16Mhz clock + /// (for the purposes of emulation, we will use a 4Mhz clock) + /// From this it generates: + /// 1Mhz clock for the CRTC chip + /// 1Mhz clock for the AY-3-8912 PSG + /// 4Mhz clock for the Z80 CPU + /// + public void ClockCycle() + { + // 4-phase clock + for (int i = 1; i < 5; i++) + { + switch (i) + { + // Phase 1 + case 1: + CRCT.ClockCycle(); + CPU.ExecuteOne(); + break; + // Phase 2 + case 2: + CPU.ExecuteOne(); + break; + // Phase 3 + case 3: + // video fetch + break; + // Phase 4 + case 4: + // video fetch + break; + } + } + } + + #endregion + + #region Internal Methods + + /// + /// Selects the pen + /// + /// + public virtual void SetPen(BitArray bi) + { + if (bi[4]) + { + // border select + CurrentPen = 16; + } + else + { + // pen select + byte[] b = new byte[1]; + bi.CopyTo(b, 0); + CurrentPen = b[0] & 0x0f; + } + } + + /// + /// Selects colour for the currently selected pen + /// + /// + public virtual void SetPenColour(BitArray bi) + { + byte[] b = new byte[1]; + bi.CopyTo(b, 0); + var colour = b[0] & 0x1f; + PenColours[CurrentPen] = colour; + } + + /// + /// Returns the actual ARGB pen colour value + /// + /// + /// + public virtual int GetPenColour(int idx) + { + return CPCHardwarePalette[PenColours[idx]]; + } + + /// + /// Screen mode and ROM config + /// + /// + public virtual void SetReg2(BitArray bi) + { + byte[] b = new byte[1]; + bi.CopyTo(b, 0); + + // screen mode + var mode = b[0] & 0x03; + ScreenMode = mode; + + // ROM + + // upper + if ((b[0] & 0x08) != 0) + { + _machine.UpperROMPaged = false; + } + else + { + _machine.UpperROMPaged = true; + } + + // lower + if ((b[0] & 0x04) != 0) + { + _machine.LowerROMPaged = false; + } + else + { + _machine.LowerROMPaged = true; + } + + // INT delay + if ((b[0] & 0x10) != 0) + { + INTScanlineCnt = 0; + } + } + + /// + /// Only available on machines with a 64KB memory expansion + /// Default assume we dont have this + /// + /// + public virtual void SetRAM(BitArray bi) + { + return; + } + + public void InterruptACK() + { + INTScanlineCnt &= 0x01f; + } + + + + + + #endregion + + #region Reset + + public void Reset() + { + CurrentPen = 0; + ScreenMode = 1; + for (int i = 0; i < 17; i++) + PenColours[i] = 0; + INTScanlineCnt = 0; + VSYNCDelyCnt = 0; + } + + #endregion + + #region IPortIODevice + + /// + /// Device responds to an IN instruction + /// + /// + /// + /// + public bool ReadPort(ushort port, ref int result) + { + // gate array is OUT only + return false; + } + + /// + /// Device responds to an OUT instruction + /// + /// + /// + /// + public bool WritePort(ushort port, int result) + { + BitArray portBits = new BitArray(BitConverter.GetBytes(port)); + BitArray dataBits = new BitArray(BitConverter.GetBytes((byte)result)); + + // The gate array responds to port 0x7F + bool accessed = !portBits[15]; + if (!accessed) + return false; + + // Bit 9 and 8 of the data byte define the function to access + if (!dataBits[6] && !dataBits[7]) + { + // select pen + SetPen(dataBits); + } + + if (dataBits[6] && !dataBits[7]) + { + // select colour for selected pen + SetPenColour(dataBits); + } + + if (!dataBits[6] && dataBits[7]) + { + // select screen mode, ROM configuration and interrupt control + SetReg2(dataBits); + } + + if (dataBits[6] && dataBits[7]) + { + // RAM memory management + SetRAM(dataBits); + } + + return true; + } + + #endregion + + #region IVideoProvider + + /// + /// Video output buffer + /// + public int[] ScreenBuffer; + + private int _virtualWidth; + private int _virtualHeight; + private int _bufferWidth; + private int _bufferHeight; + + public int BackgroundColor + { + get { return CPCHardwarePalette[16]; } + } + + public int VirtualWidth + { + get { return _virtualWidth; } + set { _virtualWidth = value; } + } + + public int VirtualHeight + { + get { return _virtualHeight; } + set { _virtualHeight = value; } + } + + public int BufferWidth + { + get { return _bufferWidth; } + set { _bufferWidth = value; } + } + + public int BufferHeight + { + get { return _bufferHeight; } + set { _bufferHeight = value; } + } + + public int VsyncNumerator + { + get { return Z80ClockSpeed * 50; } + set { } + } + + public int VsyncDenominator + { + get { return Z80ClockSpeed; } + } + + public int[] GetVideoBuffer() + { + return ScreenBuffer; + } + + protected void SetupScreenSize() + { + /* + * Rect Pixels: Mode 0: 160×200 pixels with 16 colors (4 bpp) + Sqaure Pixels: Mode 1: 320×200 pixels with 4 colors (2 bpp) + Rect Pixels: Mode 2: 640×200 pixels with 2 colors (1 bpp) + Rect Pixels: Mode 3: 160×200 pixels with 4 colors (2bpp) (this is not an official mode, but rather a side-effect of the hardware) + * + * */ + + // define maximum screen buffer size + // to fit all possible resolutions, 640x400 should do it + // therefore a scanline will take two buffer rows + // and buffer columns will be: + // Mode 1: 2 pixels + // Mode 2: 1 pixel + // Mode 0: 4 pixels + // Mode 3: 4 pixels + + BufferWidth = 640; + BufferHeight = 400; + VirtualHeight = BufferHeight; + VirtualWidth = BufferWidth; + ScreenBuffer = new int[BufferWidth * BufferHeight]; + croppedBuffer = ScreenBuffer; + } + + protected int[] croppedBuffer; + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/MachineType.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/MachineType.cs new file mode 100644 index 0000000000..f7f88a7a8b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Machine/MachineType.cs @@ -0,0 +1,18 @@ + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// The various CPC models CPCHawk emulates + /// + public enum MachineType + { + /// + /// Original Amstrad CPC model with builtin datacorder + /// + CPC464, + /// + /// 128K model with builtin 3" disk drive + /// + CPC6128, + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/CPCExtendedFloppyDisk.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/CPCExtendedFloppyDisk.cs new file mode 100644 index 0000000000..07919dce7a --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/CPCExtendedFloppyDisk.cs @@ -0,0 +1,258 @@ +using System.Text; +using BizHawk.Common; +using System; +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Logical object representing a standard +3 disk image + /// + public class CPCExtendedFloppyDisk : FloppyDisk + { + /// + /// The format type + /// + public override DiskType DiskFormatType => DiskType.CPCExtended; + + /// + /// Attempts to parse incoming disk data + /// + /// + /// + /// TRUE: disk parsed + /// FALSE: unable to parse disk + /// + public override bool ParseDisk(byte[] data) + { + // look for standard magic string + string ident = Encoding.ASCII.GetString(data, 0, 16); + + if (!ident.ToUpper().Contains("EXTENDED CPC DSK")) + { + // incorrect format + return false; + } + + // read the disk information block + DiskHeader.DiskIdent = ident; + DiskHeader.DiskCreatorString = Encoding.ASCII.GetString(data, 0x22, 14); + DiskHeader.NumberOfTracks = data[0x30]; + DiskHeader.NumberOfSides = data[0x31]; + DiskHeader.TrackSizes = new int[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides]; + DiskTracks = new Track[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides]; + DiskData = data; + int pos = 0x34; + + if (DiskHeader.NumberOfSides > 1) + { + StringBuilder sbm = new StringBuilder(); + sbm.AppendLine(); + sbm.AppendLine(); + sbm.AppendLine("The detected disk image contains multiple sides."); + sbm.AppendLine("This is NOT currently supported in CPCHawk."); + sbm.AppendLine("Please find an alternate image/dump where each side has been saved as a separate *.dsk image (and use the mutli-disk bundler tool to load into Bizhawk)."); + throw new System.NotImplementedException(sbm.ToString()); + } + + for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++) + { + DiskHeader.TrackSizes[i] = data[pos++] * 256; + } + + // move to first track information block + pos = 0x100; + + // parse each track + for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++) + { + // check for unformatted track + if (DiskHeader.TrackSizes[i] == 0) + { + DiskTracks[i] = new Track(); + DiskTracks[i].Sectors = new Sector[0]; + continue; + } + + int p = pos; + DiskTracks[i] = new Track(); + + // track info block + DiskTracks[i].TrackIdent = Encoding.ASCII.GetString(data, p, 12); + p += 16; + DiskTracks[i].TrackNumber = data[p++]; + DiskTracks[i].SideNumber = data[p++]; + DiskTracks[i].DataRate = data[p++]; + DiskTracks[i].RecordingMode = data[p++]; + DiskTracks[i].SectorSize = data[p++]; + DiskTracks[i].NumberOfSectors = data[p++]; + DiskTracks[i].GAP3Length = data[p++]; + DiskTracks[i].FillerByte = data[p++]; + + int dpos = pos + 0x100; + + // sector info list + DiskTracks[i].Sectors = new Sector[DiskTracks[i].NumberOfSectors]; + for (int s = 0; s < DiskTracks[i].NumberOfSectors; s++) + { + DiskTracks[i].Sectors[s] = new Sector(); + + DiskTracks[i].Sectors[s].TrackNumber = data[p++]; + DiskTracks[i].Sectors[s].SideNumber = data[p++]; + DiskTracks[i].Sectors[s].SectorID = data[p++]; + DiskTracks[i].Sectors[s].SectorSize = data[p++]; + DiskTracks[i].Sectors[s].Status1 = data[p++]; + DiskTracks[i].Sectors[s].Status2 = data[p++]; + DiskTracks[i].Sectors[s].ActualDataByteLength = MediaConverter.GetWordValue(data, p); + p += 2; + + // sector data - begins at 0x100 offset from the start of the track info block (in this case dpos) + DiskTracks[i].Sectors[s].SectorData = new byte[DiskTracks[i].Sectors[s].ActualDataByteLength]; + + // copy the data + for (int b = 0; b < DiskTracks[i].Sectors[s].ActualDataByteLength; b++) + { + DiskTracks[i].Sectors[s].SectorData[b] = data[dpos + b]; + } + + // check for multiple weak/random sectors stored + if (DiskTracks[i].Sectors[s].SectorSize <= 7) + { + // sectorsize n=8 is equivilent to n=0 - FDC will use DTL for length + int specifiedSize = 0x80 << DiskTracks[i].Sectors[s].SectorSize; + + if (specifiedSize < DiskTracks[i].Sectors[s].ActualDataByteLength) + { + // more data stored than sectorsize defines + // check for multiple weak/random copies + if (DiskTracks[i].Sectors[s].ActualDataByteLength % specifiedSize != 0) + { + DiskTracks[i].Sectors[s].ContainsMultipleWeakSectors = true; + } + } + } + + // move dpos to the next sector data postion + dpos += DiskTracks[i].Sectors[s].ActualDataByteLength; + } + + // move to the next track info block + pos += DiskHeader.TrackSizes[i]; + } + + // run protection scheme detector + ParseProtection(); + + return true; + } + + /// + /// Takes a double-sided disk byte array and converts into 2 single-sided arrays + /// + /// + /// + /// + public static bool SplitDoubleSided(byte[] data, List results) + { + // look for standard magic string + string ident = Encoding.ASCII.GetString(data, 0, 16); + if (!ident.ToUpper().Contains("EXTENDED CPC DSK")) + { + // incorrect format + return false; + } + + byte[] S0 = new byte[data.Length]; + byte[] S1 = new byte[data.Length]; + + // disk info block + Array.Copy(data, 0, S0, 0, 0x100); + Array.Copy(data, 0, S1, 0, 0x100); + // change side number + S0[0x31] = 1; + S1[0x31] = 1; + + // extended format has different track sizes + int[] trkSizes = new int[data[0x30] * data[0x31]]; + + int pos = 0x34; + for (int i = 0; i < data[0x30] * data[0x31]; i++) + { + trkSizes[i] = data[pos] * 256; + // clear destination trk sizes (will be added later) + S0[pos] = 0; + S1[pos] = 0; + pos++; + } + + // start at track info blocks + int mPos = 0x100; + int s0Pos = 0x100; + int s0tCount = 0; + int s1tCount = 0; + int s1Pos = 0x100; + int tCount = 0; + + while (tCount < data[0x30] * data[0x31]) + { + // which side is this? + var side = data[mPos + 0x11]; + if (side == 0) + { + // side 1 + Array.Copy(data, mPos, S0, s0Pos, trkSizes[tCount]); + s0Pos += trkSizes[tCount]; + // trk size table + S0[0x34 + s0tCount++] = (byte)(trkSizes[tCount] / 256); + } + else if (side == 1) + { + // side 2 + Array.Copy(data, mPos, S1, s1Pos, trkSizes[tCount]); + s1Pos += trkSizes[tCount]; + // trk size table + S1[0x34 + s1tCount++] = (byte)(trkSizes[tCount] / 256); + } + + mPos += trkSizes[tCount++]; + } + + byte[] s0final = new byte[s0Pos]; + byte[] s1final = new byte[s1Pos]; + Array.Copy(S0, 0, s0final, 0, s0Pos); + Array.Copy(S1, 0, s1final, 0, s1Pos); + + results.Add(s0final); + results.Add(s1final); + + return true; + } + + /// + /// State serlialization + /// + /// + public override void SyncState(Serializer ser) + { + ser.BeginSection("Plus3FloppyDisk"); + + ser.Sync("CylinderCount", ref CylinderCount); + ser.Sync("SideCount", ref SideCount); + ser.Sync("BytesPerTrack", ref BytesPerTrack); + ser.Sync("WriteProtected", ref WriteProtected); + ser.SyncEnum("Protection", ref Protection); + + ser.Sync("DirtyData", ref DirtyData); + if (DirtyData) + { + + } + + // sync deterministic track and sector counters + ser.Sync(" _randomCounter", ref _randomCounter); + RandomCounter = _randomCounter; + + ser.EndSection(); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/CPCFloppyDisk.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/CPCFloppyDisk.cs new file mode 100644 index 0000000000..c30415842e --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/CPCFloppyDisk.cs @@ -0,0 +1,241 @@ +using System.Text; +using BizHawk.Common; +using System.Collections.Generic; +using System; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Logical object representing a standard +3 disk image + /// + public class CPCFloppyDisk : FloppyDisk + { + /// + /// The format type + /// + public override DiskType DiskFormatType => DiskType.CPC; + + /// + /// Attempts to parse incoming disk data + /// + /// + /// + /// TRUE: disk parsed + /// FALSE: unable to parse disk + /// + public override bool ParseDisk(byte[] data) + { + // look for standard magic string + string ident = Encoding.ASCII.GetString(data, 0, 16); + + if (!ident.ToUpper().Contains("MV - CPC")) + { + // incorrect format + return false; + } + + // read the disk information block + DiskHeader.DiskIdent = ident; + DiskHeader.DiskCreatorString = Encoding.ASCII.GetString(data, 0x22, 14); + DiskHeader.NumberOfTracks = data[0x30]; + DiskHeader.NumberOfSides = data[0x31]; + DiskHeader.TrackSizes = new int[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides]; + DiskTracks = new Track[DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides]; + DiskData = data; + int pos = 0x32; + + if (DiskHeader.NumberOfSides > 1) + { + StringBuilder sbm = new StringBuilder(); + sbm.AppendLine(); + sbm.AppendLine(); + sbm.AppendLine("The detected disk image contains multiple sides."); + sbm.AppendLine("This is NOT currently supported in CPCHawk."); + sbm.AppendLine("Please find an alternate image/dump where each side has been saved as a separate *.dsk image (and use the mutli-disk bundler tool to load into Bizhawk)."); + throw new System.NotImplementedException(sbm.ToString()); + } + + // standard CPC format all track sizes are the same in the image + for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++) + { + DiskHeader.TrackSizes[i] = MediaConverter.GetWordValue(data, pos); + } + + // move to first track information block + pos = 0x100; + + // parse each track + for (int i = 0; i < DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; i++) + { + // check for unformatted track + if (DiskHeader.TrackSizes[i] == 0) + { + DiskTracks[i] = new Track(); + DiskTracks[i].Sectors = new Sector[0]; + continue; + } + + int p = pos; + DiskTracks[i] = new Track(); + + // track info block + DiskTracks[i].TrackIdent = Encoding.ASCII.GetString(data, p, 12); + p += 16; + DiskTracks[i].TrackNumber = data[p++]; + DiskTracks[i].SideNumber = data[p++]; + p += 2; + DiskTracks[i].SectorSize = data[p++]; + DiskTracks[i].NumberOfSectors = data[p++]; + DiskTracks[i].GAP3Length = data[p++]; + DiskTracks[i].FillerByte = data[p++]; + + int dpos = pos + 0x100; + + // sector info list + DiskTracks[i].Sectors = new Sector[DiskTracks[i].NumberOfSectors]; + for (int s = 0; s < DiskTracks[i].NumberOfSectors; s++) + { + DiskTracks[i].Sectors[s] = new Sector(); + + DiskTracks[i].Sectors[s].TrackNumber = data[p++]; + DiskTracks[i].Sectors[s].SideNumber = data[p++]; + DiskTracks[i].Sectors[s].SectorID = data[p++]; + DiskTracks[i].Sectors[s].SectorSize = data[p++]; + DiskTracks[i].Sectors[s].Status1 = data[p++]; + DiskTracks[i].Sectors[s].Status2 = data[p++]; + DiskTracks[i].Sectors[s].ActualDataByteLength = MediaConverter.GetWordValue(data, p); + p += 2; + + // actualdatabytelength value is calculated now + if (DiskTracks[i].Sectors[s].SectorSize == 0) + { + // no sectorsize specified - DTL will be used at runtime + DiskTracks[i].Sectors[s].ActualDataByteLength = DiskHeader.TrackSizes[i]; + } + else if (DiskTracks[i].Sectors[s].SectorSize > 6) + { + // invalid - wrap around to 0 + DiskTracks[i].Sectors[s].ActualDataByteLength = DiskHeader.TrackSizes[i]; + } + else if (DiskTracks[i].Sectors[s].SectorSize == 6) + { + // only 0x1800 bytes are stored + DiskTracks[i].Sectors[s].ActualDataByteLength = 0x1800; + } + else + { + // valid sector size for this format + DiskTracks[i].Sectors[s].ActualDataByteLength = 0x80 << DiskTracks[i].Sectors[s].SectorSize; + } + + // sector data - begins at 0x100 offset from the start of the track info block (in this case dpos) + DiskTracks[i].Sectors[s].SectorData = new byte[DiskTracks[i].Sectors[s].ActualDataByteLength]; + + // copy the data + for (int b = 0; b < DiskTracks[i].Sectors[s].ActualDataByteLength; b++) + { + DiskTracks[i].Sectors[s].SectorData[b] = data[dpos + b]; + } + + // move dpos to the next sector data postion + dpos += DiskTracks[i].Sectors[s].ActualDataByteLength; + } + + // move to the next track info block + pos += DiskHeader.TrackSizes[i]; + } + + // run protection scheme detector + ParseProtection(); + + return true; + } + + /// + /// Takes a double-sided disk byte array and converts into 2 single-sided arrays + /// + /// + /// + /// + public static bool SplitDoubleSided(byte[] data, List results) + { + // look for standard magic string + string ident = Encoding.ASCII.GetString(data, 0, 16); + if (!ident.ToUpper().Contains("MV - CPC")) + { + // incorrect format + return false; + } + + byte[] S0 = new byte[data.Length]; + byte[] S1 = new byte[data.Length]; + + // disk info block + Array.Copy(data, 0, S0, 0, 0x100); + Array.Copy(data, 0, S1, 0, 0x100); + // change side number + S0[0x31] = 1; + S1[0x31] = 1; + + int trkSize = MediaConverter.GetWordValue(data, 0x32); + + // start at track info blocks + int mPos = 0x100; + int s0Pos = 0x100; + int s1Pos = 0x100; + + while (mPos < trkSize * data[0x30] * data[0x31]) + { + // which side is this? + var side = data[mPos + 0x11]; + if (side == 0) + { + // side 1 + Array.Copy(data, mPos, S0, s0Pos, trkSize); + s0Pos += trkSize; + } + else if (side == 1) + { + // side 2 + Array.Copy(data, mPos, S1, s1Pos, trkSize); + s1Pos += trkSize; + } + + mPos += trkSize; + } + + byte[] s0final = new byte[s0Pos]; + byte[] s1final = new byte[s1Pos]; + Array.Copy(S0, 0, s0final, 0, s0Pos); + Array.Copy(S1, 0, s1final, 0, s1Pos); + + results.Add(s0final); + results.Add(s1final); + + return true; + } + + /// + /// State serlialization + /// + /// + public override void SyncState(Serializer ser) + { + ser.BeginSection("Plus3FloppyDisk"); + + ser.Sync("CylinderCount", ref CylinderCount); + ser.Sync("SideCount", ref SideCount); + ser.Sync("BytesPerTrack", ref BytesPerTrack); + ser.Sync("WriteProtected", ref WriteProtected); + ser.SyncEnum("Protection", ref Protection); + + ser.Sync("DirtyData", ref DirtyData); + if (DirtyData) + { + + } + + ser.EndSection(); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/DiskHandler.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/DiskHandler.cs new file mode 100644 index 0000000000..bc06c1094b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/DiskHandler.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// This is called first when importing disk images + /// Disk images can be single or double-sided, so we need to handle that + /// + public class DiskHandler + { + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/DiskType.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/DiskType.cs new file mode 100644 index 0000000000..75719b9df9 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/DiskType.cs @@ -0,0 +1,19 @@ + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// The different disk formats ZXHawk currently supports + /// + public enum DiskType + { + /// + /// Standard CPCEMU disk format (used in the built-in +3 disk drive) + /// + CPC, + + /// + /// Extended CPCEMU disk format (used in the built-in +3 disk drive) + /// + CPCExtended + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/FloppyDisk.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/FloppyDisk.cs new file mode 100644 index 0000000000..4524076517 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Disk/FloppyDisk.cs @@ -0,0 +1,750 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// This abstract class defines a logical floppy disk + /// + public abstract class FloppyDisk + { + /// + /// The disk format type + /// + public abstract DiskType DiskFormatType { get; } + + /// + /// Disk information header + /// + public Header DiskHeader = new Header(); + + /// + /// Track array + /// + public Track[] DiskTracks = null; + + /// + /// No. of tracks per side + /// + public int CylinderCount; + + /// + /// The number of physical sides + /// + public int SideCount; + + /// + /// The number of bytes per track + /// + public int BytesPerTrack; + + /// + /// The write-protect tab on the disk + /// + public bool WriteProtected; + + /// + /// The detected protection scheme (if any) + /// + public ProtectionType Protection; + + /// + /// The actual disk image data + /// + public byte[] DiskData; + + /// + /// If TRUE then data on the disk has changed (been written to) + /// This will be used to determine whether the disk data needs to be included + /// in any SyncState operations + /// + protected bool DirtyData = false; + + /// + /// Used to deterministically choose a 'random' sector when dealing with weak reads + /// + public int RandomCounter + { + get { return _randomCounter; } + set + { + _randomCounter = value; + + foreach (var trk in DiskTracks) + { + foreach (var sec in trk.Sectors) + { + sec.RandSecCounter = _randomCounter; + } + } + } + } + protected int _randomCounter; + + + /// + /// Attempts to parse incoming disk data + /// + /// + /// + /// TRUE: disk parsed + /// FALSE: unable to parse disk + /// + public virtual bool ParseDisk(byte[] diskData) + { + // default result + // override in inheriting class + return false; + } + + /// + /// Examines the floppydisk data to work out what protection (if any) is present + /// If possible it will also fix the disk data for this protection + /// This should be run at the end of the ParseDisk() method + /// + public virtual void ParseProtection() + { + int[] weakArr = new int[2]; + + // speedlock + if (DetectSpeedlock(ref weakArr)) + { + Protection = ProtectionType.Speedlock; + + Sector sec = DiskTracks[0].Sectors[1]; + if (!sec.ContainsMultipleWeakSectors) + { + byte[] origData = sec.SectorData.ToArray(); + List data = new List(); + for (int m = 0; m < 3; m++) + { + for (int i = 0; i < 512; i++) + { + // deterministic 'random' implementation + int n = origData[i] + m + 1; + if (n > 0xff) + n = n - 0xff; + else if (n < 0) + n = 0xff + n; + + byte nByte = (byte)n; + + if (m == 0) + { + data.Add(origData[i]); + continue; + } + + if (i < weakArr[0]) + { + data.Add(origData[i]); + } + + else if (weakArr[1] > 0) + { + data.Add(nByte); + weakArr[1]--; + } + + else + { + data.Add(origData[i]); + } + } + } + + sec.SectorData = data.ToArray(); + sec.ActualDataByteLength = data.Count(); + sec.ContainsMultipleWeakSectors = true; + } + } + else if (DetectAlkatraz(ref weakArr)) + { + Protection = ProtectionType.Alkatraz; + } + else if (DetectPaulOwens(ref weakArr)) + { + Protection = ProtectionType.PaulOwens; + } + else if (DetectHexagon(ref weakArr)) + { + Protection = ProtectionType.Hexagon; + } + else if (DetectShadowOfTheBeast()) + { + Protection = ProtectionType.ShadowOfTheBeast; + } + } + + /// + /// Detection routine for shadow of the beast game + /// Still cannot get this to work, but at least the game is detected + /// + /// + public bool DetectShadowOfTheBeast() + { + if (DiskTracks[0].Sectors.Length != 9) + return false; + + var zeroSecs = DiskTracks[0].Sectors; + if (zeroSecs[0].SectorID != 65 || + zeroSecs[1].SectorID != 66 || + zeroSecs[2].SectorID != 67 || + zeroSecs[3].SectorID != 68 || + zeroSecs[4].SectorID != 69 || + zeroSecs[5].SectorID != 70 || + zeroSecs[6].SectorID != 71 || + zeroSecs[7].SectorID != 72 || + zeroSecs[8].SectorID != 73) + return false; + + var oneSecs = DiskTracks[1].Sectors; + + if (oneSecs.Length != 8) + return false; + + if (oneSecs[0].SectorID != 17 || + oneSecs[1].SectorID != 18 || + oneSecs[2].SectorID != 19 || + oneSecs[3].SectorID != 20 || + oneSecs[4].SectorID != 21 || + oneSecs[5].SectorID != 22 || + oneSecs[6].SectorID != 23 || + oneSecs[7].SectorID != 24) + return false; + + return true; + } + + /// + /// Detect speedlock weak sector + /// + /// + /// + public bool DetectSpeedlock(ref int[] weak) + { + // SPEEDLOCK NOTES (-asni 2018-05-01) + // --------------------------------- + // Speedlock is one of the more common +3 disk protections and there are a few different versions + // Usually, track 0 sector 1 (ID 2) has data CRC errors that result in certain bytes returning a different value every time they are read + // Speedlock will generally read this track a number of times during the load process + // and if the correct bytes are not different between reads, the load fails + + // always must have track 0 containing 9 sectors + if (DiskTracks[0].Sectors.Length != 9) + return false; + + // check for SPEEDLOCK ident in sector 0 + string ident = Encoding.ASCII.GetString(DiskTracks[0].Sectors[0].SectorData, 0, DiskTracks[0].Sectors[0].SectorData.Length); + if (!ident.ToUpper().Contains("SPEEDLOCK")) + return false; + + // check for correct sector 0 lengths + if (DiskTracks[0].Sectors[0].SectorSize != 2 || + DiskTracks[0].Sectors[0].SectorData.Length < 0x200) + return false; + + // sector[1] (SectorID 2) contains the weak sectors + Sector sec = DiskTracks[0].Sectors[1]; + + // check for correct sector 1 lengths + if (sec.SectorSize != 2 || + sec.SectorData.Length < 0x200) + return false; + + // secID 2 needs a CRC error + //if (!(sec.Status1.Bit(5) || sec.Status2.Bit(5))) + //return false; + + // check for filler + bool startFillerFound = true; + for (int i = 0; i < 250; i++) + { + if (sec.SectorData[i] != sec.SectorData[i + 1]) + { + startFillerFound = false; + break; + } + } + + if (!startFillerFound) + { + weak[0] = 0; + weak[1] = 0x200; + } + else + { + weak[0] = 0x150; + weak[1] = 0x20; + } + + return true; + } + + /// + /// Detect Alkatraz + /// + /// + /// + public bool DetectAlkatraz(ref int[] weak) + { + try + { + var data1 = DiskTracks[0].Sectors[0].SectorData; + var data2 = DiskTracks[0].Sectors[0].SectorData.Length; + } + catch (Exception) + { + return false; + } + + // check for ALKATRAZ ident in sector 0 + string ident = Encoding.ASCII.GetString(DiskTracks[0].Sectors[0].SectorData, 0, DiskTracks[0].Sectors[0].SectorData.Length); + if (!ident.ToUpper().Contains("ALKATRAZ PROTECTION SYSTEM")) + return false; + + // ALKATRAZ NOTES (-asni 2018-05-01) + // --------------------------------- + // Alkatraz protection appears to revolve around a track on the disk with 18 sectors, + // (track position is not consistent) with the sector ID info being incorrect: + // TrackID is consistent between the sectors although is usually high (233, 237 etc) + // SideID is fairly random looking but with all IDs being even + // SectorID is also fairly random looking but contains both odd and even numbers + // + // There doesnt appear to be any CRC errors in this track, but the sector size is always 1 (256 bytes) + // Each sector contains different filler byte + // Once track 0 is loaded the CPU completely reads all the sectors in this track one-by-one. + // Data transferred during execution must be correct, also result ST0, ST1 and ST2 must be 64, 128 and 0 respectively + + // Immediately following this track are a number of tracks and sectors with a DAM set. + // These are all read in sector by sector + // Again, Alkatraz appears to require that ST0, ST1, and ST2 result bytes are set to 64, 128 and 0 respectively + // (so the CM in ST2 needs to be reset) + + return true; + } + + /// + /// Detect Paul Owens + /// + /// + /// + public bool DetectPaulOwens(ref int[] weak) + { + try + { + var data1 = DiskTracks[0].Sectors[2].SectorData; + var data2 = DiskTracks[0].Sectors[2].SectorData.Length; + } + catch (Exception) + { + return false; + } + + // check for PAUL OWENS ident in sector 2 + string ident = Encoding.ASCII.GetString(DiskTracks[0].Sectors[2].SectorData, 0, DiskTracks[0].Sectors[2].SectorData.Length); + if (!ident.ToUpper().Contains("PAUL OWENS")) + return false; + + // Paul Owens Disk Protection Notes (-asni 2018-05-01) + // --------------------------------------------------- + // + // This scheme looks a little similar to Alkatraz with incorrect sector ID info in many places + // and deleted address marks (although these do not seem to show the strict relience on removing the CM mark from ST2 result that Alkatraz does) + // There are also data CRC errors but these dont look to be read more than once/checked for changes during load + // Main identifiers: + // + // * There are more than 10 cylinders + // * Cylinder 1 has no sector data + // * The sector ID infomation in most cases contains incorrect track IDs + // * Tracks 0 (boot) and 5 appear to be pretty much the only tracks that do not have incorrect sector ID marks + + return true; + } + + /// + /// Detect Hexagon copy protection + /// + /// + /// + public bool DetectHexagon(ref int[] weak) + { + try + { + var data1 = DiskTracks[0].Sectors.Length; + var data2 = DiskTracks[0].Sectors[8].ActualDataByteLength; + var data3 = DiskTracks[0].Sectors[8].SectorData; + var data4 = DiskTracks[0].Sectors[8].SectorData.Length; + var data5 = DiskTracks[1].Sectors[0]; + } + catch (Exception) + { + return false; + } + + if (DiskTracks[0].Sectors.Length != 10 || DiskTracks[0].Sectors[8].ActualDataByteLength != 512) + return false; + + // check for Hexagon ident in sector 8 + string ident = Encoding.ASCII.GetString(DiskTracks[0].Sectors[8].SectorData, 0, DiskTracks[0].Sectors[8].SectorData.Length); + if (ident.ToUpper().Contains("GON DISK PROT")) + return true; + + // hexagon protection may not be labelled as such + var track = DiskTracks[1]; + var sector = track.Sectors[0]; + + if (sector.SectorSize == 6 && sector.Status1 == 0x20 && sector.Status2 == 0x60) + { + if (track.Sectors.Length == 1) + return true; + } + + + // Hexagon Copy Protection Notes (-asni 2018-05-01) + // --------------------------------------------------- + // + // + + return false; + } + + /* + /// + /// Should be run at the end of the ParseDisk process + /// If speedlock is detected the flag is set in the disk image + /// + /// + protected virtual void SpeedlockDetection() + { + + if (DiskTracks.Length == 0) + return; + + // check for speedlock copyright notice + string ident = Encoding.ASCII.GetString(DiskData, 0x100, 0x1400); + if (!ident.ToUpper().Contains("SPEEDLOCK")) + { + // speedlock not found + return; + } + + // get cylinder 0 + var cyl = DiskTracks[0]; + + // get sector with ID=2 + var sec = cyl.Sectors.Where(a => a.SectorID == 2).FirstOrDefault(); + + if (sec == null) + return; + + // check for already multiple weak copies + if (sec.ContainsMultipleWeakSectors || sec.SectorData.Length != 0x80 << sec.SectorSize) + return; + + // check for invalid crcs in sector 2 + if (sec.Status1.Bit(5) || sec.Status2.Bit(5)) + { + Protection = ProtectionType.Speedlock; + } + else + { + return; + } + + // we are going to create a total of 5 weak sector copies + // keeping the original copy + byte[] origData = sec.SectorData.ToArray(); + List data = new List(); + //Random rnd = new Random(); + + for (int i = 0; i < 6; i++) + { + for (int s = 0; s < origData.Length; s++) + { + if (i == 0) + { + data.Add(origData[s]); + continue; + } + + // deterministic 'random' implementation + int n = origData[s] + i + 1; + if (n > 0xff) + n = n - 0xff; + else if (n < 0) + n = 0xff + n; + + byte nByte = (byte)n; + + if (s < 336) + { + // non weak data + data.Add(origData[s]); + } + else if (s < 511) + { + // weak data + data.Add(nByte); + } + else if (s == 511) + { + // final sector byte + data.Add(nByte); + } + else + { + // speedlock sector should not be more than 512 bytes + // but in case it is just do non weak + data.Add(origData[i]); + } + } + } + + // commit the sector data + sec.SectorData = data.ToArray(); + sec.ContainsMultipleWeakSectors = true; + sec.ActualDataByteLength = data.Count(); + + } + */ + + /// + /// Returns the track count for the disk + /// + /// + public virtual int GetTrackCount() + { + return DiskHeader.NumberOfTracks * DiskHeader.NumberOfSides; + } + + /// + /// Reads the current sector ID info + /// + /// + /// + public virtual CHRN ReadID(byte trackIndex, byte side, int sectorIndex) + { + if (side != 0) + return null; + + if (DiskTracks.Length <= trackIndex || trackIndex < 0) + { + // invalid track - wrap around + trackIndex = 0; + } + + var track = DiskTracks[trackIndex]; + + if (track.NumberOfSectors <= sectorIndex) + { + // invalid sector - wrap around + sectorIndex = 0; + } + + var sector = track.Sectors[sectorIndex]; + + CHRN chrn = new CHRN(); + + chrn.C = sector.TrackNumber; + chrn.H = sector.SideNumber; + chrn.R = sector.SectorID; + + // wrap around for N > 7 + if (sector.SectorSize > 7) + { + chrn.N = (byte)(sector.SectorSize - 7); + } + else if (sector.SectorSize < 0) + { + chrn.N = 0; + } + else + { + chrn.N = sector.SectorSize; + } + + chrn.Flag1 = (byte)(sector.Status1 & 0x25); + chrn.Flag2 = (byte)(sector.Status2 & 0x61); + + chrn.DataBytes = sector.ActualData; + + return chrn; + } + + /// + /// State serialization routines + /// + /// + public abstract void SyncState(Serializer ser); + + + public class Header + { + public string DiskIdent { get; set; } + public string DiskCreatorString { get; set; } + public byte NumberOfTracks { get; set; } + public byte NumberOfSides { get; set; } + public int[] TrackSizes { get; set; } + } + + public class Track + { + public string TrackIdent { get; set; } + public byte TrackNumber { get; set; } + public byte SideNumber { get; set; } + public byte DataRate { get; set; } + public byte RecordingMode { get; set; } + public byte SectorSize { get; set; } + public byte NumberOfSectors { get; set; } + public byte GAP3Length { get; set; } + public byte FillerByte { get; set; } + public Sector[] Sectors { get; set; } + + /// + /// Presents a contiguous byte array of all sector data for this track + /// (including any multiple weak/random data) + /// + public byte[] TrackSectorData + { + get + { + List list = new List(); + + foreach (var sec in Sectors) + { + list.AddRange(sec.ActualData); + } + + return list.ToArray(); + } + } + } + + public class Sector + { + public byte TrackNumber { get; set; } + public byte SideNumber { get; set; } + public byte SectorID { get; set; } + public byte SectorSize { get; set; } + public byte Status1 { get; set; } + public byte Status2 { get; set; } + public int ActualDataByteLength { get; set; } + public byte[] SectorData { get; set; } + public bool ContainsMultipleWeakSectors { get; set; } + + public int WeakReadIndex = 0; + + public void SectorReadCompleted() + { + if (ContainsMultipleWeakSectors) + WeakReadIndex++; + } + + public int DataLen + { + get + { + if (!ContainsMultipleWeakSectors) + { + return ActualDataByteLength; + } + else + { + return ActualDataByteLength / (ActualDataByteLength / (0x80 << SectorSize)); + } + } + } + + + public int RandSecCounter = 0; + + public byte[] ActualData + { + get + { + if (!ContainsMultipleWeakSectors) + { + // check whether filler bytes are needed + int size = 0x80 << SectorSize; + if (size > ActualDataByteLength) + { + List l = new List(); + l.AddRange(SectorData); + for (int i = 0; i < size - ActualDataByteLength; i++) + { + //l.Add(SectorData[i]); + l.Add(SectorData.Last()); + } + + return l.ToArray(); + } + else + { + return SectorData; + } + } + else + { + // weak read neccessary + int copies = ActualDataByteLength / (0x80 << SectorSize); + + // handle index wrap-around + if (WeakReadIndex > copies - 1) + WeakReadIndex = copies - 1; + + // get the sector data based on the current weakreadindex + int step = WeakReadIndex * (0x80 << SectorSize); + byte[] res = new byte[(0x80 << SectorSize)]; + Array.Copy(SectorData, step, res, 0, 0x80 << SectorSize); + return res; + + /* + int copies = ActualDataByteLength / (0x80 << SectorSize); + Random rnd = new Random(); + int r = rnd.Next(0, copies - 1); + int step = r * (0x80 << SectorSize); + byte[] res = new byte[(0x80 << SectorSize)]; + Array.Copy(SectorData, step, res, 0, 0x80 << SectorSize); + return res; + */ + } + } + } + + public CHRN SectorIDInfo + { + get + { + return new CHRN + { + C = TrackNumber, + H = SideNumber, + R = SectorID, + N = SectorSize, + Flag1 = Status1, + Flag2 = Status2, + }; + } + } + } + } + + /// + /// Defines the type of speedlock detection found + /// + public enum ProtectionType + { + None, + Speedlock, + Alkatraz, + Hexagon, + Frontier, + PaulOwens, + ShadowOfTheBeast + } + +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/MediaConverter.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/MediaConverter.cs new file mode 100644 index 0000000000..3859f20479 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/MediaConverter.cs @@ -0,0 +1,153 @@ +using System; +using System.IO; +using System.IO.Compression; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Abtract class that represents all Media Converters + /// + public abstract class MediaConverter + { + /// + /// The type of serializer + /// + public abstract MediaConverterType FormatType { get; } + + /// + /// Signs whether this class can be used to read the data format + /// + public virtual bool IsReader + { + get + { + return false; + } + } + + /// + /// Signs whether this class can be used to write the data format + /// + public virtual bool IsWriter + { + get + { + return false; + } + } + + /// + /// Serialization method + /// + /// + public virtual void Read(byte[] data) + { + throw new NotImplementedException(this.GetType().ToString() + + "Read operation is not implemented for this converter"); + } + + /// + /// DeSerialization method + /// + /// + public virtual void Write(byte[] data) + { + throw new NotImplementedException(this.GetType().ToString() + + "Write operation is not implemented for this converter"); + } + + /// + /// Serializer does a quick check, returns TRUE if file is detected as this type + /// + /// + public virtual bool CheckType(byte[] data) + { + throw new NotImplementedException(this.GetType().ToString() + + "Check type operation is not implemented for this converter"); + } + + #region Static Tools + + /// + /// Converts an int32 value into a byte array + /// + /// + /// + public static byte[] GetBytes(int value) + { + byte[] buf = new byte[4]; + buf[0] = (byte)value; + buf[1] = (byte)(value >> 8); + buf[2] = (byte)(value >> 16); + buf[3] = (byte)(value >> 24); + return buf; + } + + /// + /// Returns an int32 from a byte array based on offset + /// + /// + /// + /// + public static int GetInt32(byte[] buf, int offsetIndex) + { + return buf[offsetIndex] | buf[offsetIndex + 1] << 8 | buf[offsetIndex + 2] << 16 | buf[offsetIndex + 3] << 24; + } + + /// + /// Returns an uint16 from a byte array based on offset + /// + /// + /// + /// + public static ushort GetWordValue(byte[] buf, int offsetIndex) + { + return (ushort)(buf[offsetIndex] | buf[offsetIndex + 1] << 8); + } + + /// + /// Updates a byte array with a uint16 value based on offset + /// + /// + /// + /// + public static void SetWordValue(byte[] buf, int offsetIndex, ushort value) + { + buf[offsetIndex] = (byte)value; + buf[offsetIndex + 1] = (byte)(value >> 8); + } + + /// + /// Takes a PauseInMilliseconds value and returns the value in T-States + /// + /// + /// + public static int TranslatePause(int pauseInMS) + { + // t-states per millisecond + var tspms = (69888 * 50) / 1000; + // get value + int res = pauseInMS * tspms; + + return res; + } + + /// + /// Decompresses a byte array that is Z-RLE compressed + /// + /// + /// + public static void DecompressZRLE(byte[] sourceBuffer, ref byte[] destBuffer) + { + MemoryStream stream = new MemoryStream(); + stream.Write(sourceBuffer, 0, sourceBuffer.Length); + stream.Position = 0; + stream.ReadByte(); + stream.ReadByte(); + DeflateStream ds = new DeflateStream(stream, CompressionMode.Decompress, false); + ds.Read(destBuffer, 0, destBuffer.Length); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/MediaConverterType.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/MediaConverterType.cs new file mode 100644 index 0000000000..08f5f73250 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/MediaConverterType.cs @@ -0,0 +1,13 @@ + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Represents the different types of media serializer avaiable + /// + public enum MediaConverterType + { + NONE, + CDT, + DSK + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Tape/CDT/CdtConverter.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Tape/CDT/CdtConverter.cs new file mode 100644 index 0000000000..304d53d0ab --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Tape/CDT/CdtConverter.cs @@ -0,0 +1,1999 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Reponsible for TZX format serializaton + /// + public class CdtConverter : MediaConverter + { + /// + /// The type of serializer + /// + private MediaConverterType _formatType = MediaConverterType.CDT; + public override MediaConverterType FormatType + { + get + { + return _formatType; + } + } + + /// + /// Signs whether this class can be used to read the data format + /// + public override bool IsReader { get { return true; } } + + /// + /// Signs whether this class can be used to write the data format + /// + public override bool IsWriter { get { return false; } } + + /// + /// Working list of generated tape data blocks + /// + private List _blocks = new List(); + + /// + /// Position counter + /// + private int _position = 0; + + /// + /// Object to keep track of loops - this assumes there is only one loop at a time + /// + private List> _loopCounter = new List>(); + + #region Construction + + private DatacorderDevice _datacorder; + + public CdtConverter(DatacorderDevice _tapeDevice) + { + _datacorder = _tapeDevice; + } + + #endregion + + /// + /// CDT format is essentially exactly the same as the TZX format + /// However all timings are based on spectrum timings (3.5Mhz) + /// so need to be adjusted for the CPC (4Mhz) + /// + /// + /// + private TapeDataBlock ConvertClock(TapeDataBlock db) + { + TapeDataBlock tb = new TapeDataBlock(); + tb.BlockDescription = db.BlockDescription; + tb.BlockID = db.BlockID; + tb.Command = db.Command; + tb.DataPeriods = new List(); + tb.InitialPulseLevel = db.InitialPulseLevel; + tb.MetaData = db.MetaData; + tb.PauseInMS = db.PauseInMS; + + double multiplier = (double)4 / (double)3.5; + double cycleScale = ((40 << 16) / 35); + double origPeriods = db.DataPeriods.Count(); + + for (int i = 0; i < origPeriods; i++) + { + int orig = db.DataPeriods[i]; + int np = (int)((double)orig * multiplier); + int nnp = ClockAdjust(orig); + tb.DataPeriods.Add(np); + } + + return tb; + } + + private int ClockAdjust(int val) + { + int cycleScale = ((40 << 16) / 35); + int res = (val * cycleScale) >> 16; + return res; + } + + private int Scale => ((40 << 16) / 35); + + private int Adjust(int val) + { + return (int)((val * CLOCK_MULTIPLIER)); + } + + private const double CLOCK_MULTIPLIER = 1.142857; + + /// + /// Returns TRUE if tzx header is detected + /// + /// + public override bool CheckType(byte[] data) + { + /* + // TZX Header + length: 10 bytes + Offset Value Type Description + 0x00 "ZXTape!" ASCII[7] TZX signature + 0x07 0x1A BYTE End of text file marker + 0x08 1 BYTE TZX major revision number + 0x09 20 BYTE TZX minor revision number + */ + + // check whether this is a valid tzx format file by looking at the identifier in the header + // (first 7 bytes of the file) + string ident = Encoding.ASCII.GetString(data, 0, 7); + // and 'end of text' marker + byte eotm = data[7]; + + // version info + int majorVer = data[8]; + int minorVer = data[9]; + + if (ident != "ZXTape!" || eotm != 0x1A) + { + // this is not a valid TZX format file + return false; + } + else + { + return true; + } + } + + /// + /// DeSerialization method + /// + /// + public override void Read(byte[] data) + { + // clear existing tape blocks + _datacorder.DataBlocks.Clear(); + +/* + // TZX Header + length: 10 bytes + Offset Value Type Description + 0x00 "ZXTape!" ASCII[7] TZX signature + 0x07 0x1A BYTE End of text file marker + 0x08 1 BYTE TZX major revision number + 0x09 20 BYTE TZX minor revision number +*/ + + // check whether this is a valid tzx format file by looking at the identifier in the header + // (first 7 bytes of the file) + string ident = Encoding.ASCII.GetString(data, 0, 7); + // and 'end of text' marker + byte eotm = data[7]; + + // version info + int majorVer = data[8]; + int minorVer = data[9]; + + if (ident != "ZXTape!" || eotm != 0x1A) + { + // this is not a valid TZX format file + throw new Exception(this.GetType().ToString() + + "This is not a valid TZX format file"); + } + + // iterate through each block + _position = 10; + while (_position < data.Length) + { + // block ID is the first byte in a new block + int ID = data[_position++]; + + // process the data + ProcessBlock(data, ID); + } + + /* + // convert for Amstrad CPC + List newBlocks = new List(); + for (int i = 0; i < _datacorder.DataBlocks.Count(); i++) + { + newBlocks.Add(ConvertClock(_datacorder.DataBlocks[i])); + } + + _datacorder.DataBlocks.Clear(); + _datacorder.DataBlocks.AddRange(newBlocks); + */ + } + + /// + /// Processes a TZX block + /// + /// + /// + private void ProcessBlock(byte[] data, int id) + { + // process based on detected block ID + switch (id) + { + // ID 10 - Standard Speed Data Block + case 0x10: + ProcessBlockID10(data); + break; + // ID 11 - Turbo Speed Data Block + case 0x11: + ProcessBlockID11(data); + break; + // ID 12 - Pure Tone + case 0x12: + ProcessBlockID12(data); + break; + // ID 13 - Pulse sequence + case 0x13: + ProcessBlockID13(data); + break; + // ID 14 - Pure Data Block + case 0x14: + ProcessBlockID14(data); + break; + // ID 15 - Direct Recording + case 0x15: + ProcessBlockID15(data); + break; + // ID 18 - CSW Recording + case 0x18: + ProcessBlockID18(data); + break; + // ID 19 - Generalized Data Block + case 0x19: + ProcessBlockID19(data); + break; + // ID 20 - Pause (silence) or 'Stop the Tape' command + case 0x20: + ProcessBlockID20(data); + break; + // ID 21 - Group start + case 0x21: + ProcessBlockID21(data); + break; + // ID 22 - Group end + case 0x22: + ProcessBlockID22(data); + break; + // ID 23 - Jump to block + case 0x23: + ProcessBlockID23(data); + break; + // ID 24 - Loop start + case 0x24: + ProcessBlockID24(data); + break; + // ID 25 - Loop end + case 0x25: + ProcessBlockID25(data); + break; + // ID 26 - Call sequence + case 0x26: + ProcessBlockID26(data); + break; + // ID 27 - Return from sequence + case 0x27: + ProcessBlockID27(data); + break; + // ID 28 - Select block + case 0x28: + ProcessBlockID28(data); + break; + // ID 2A - Stop the tape if in 48K mode + case 0x2A: + ProcessBlockID2A(data); + break; + // ID 2B - Set signal level + case 0x2B: + ProcessBlockID2B(data); + break; + // ID 30 - Text description + case 0x30: + ProcessBlockID30(data); + break; + // ID 31 - Message block + case 0x31: + ProcessBlockID31(data); + break; + // ID 32 - Archive info + case 0x32: + ProcessBlockID32(data); + break; + // ID 33 - Hardware type + case 0x33: + ProcessBlockID33(data); + break; + // ID 35 - Custom info block + case 0x35: + ProcessBlockID35(data); + break; + // ID 5A - "Glue" block + case 0x5A: + ProcessBlockID5A(data); + break; + + #region Depreciated Blocks + + // ID 16 - C64 ROM Type Data Block + case 0x16: + ProcessBlockID16(data); + break; + // ID 17 - C64 Turbo Tape Data Block + case 0x17: + ProcessBlockID17(data); + break; + // ID 34 - Emulation info + case 0x34: + ProcessBlockID34(data); + break; + // ID 40 - Snapshot block + case 0x40: + ProcessBlockID40(data); + break; + + #endregion + + default: + ProcessUnidentifiedBlock(data); + break; + } + } + + #region TZX Block Processors + + #region ID 10 - Standard Speed Data Block +/* length: [02,03]+04 + Offset Value Type Description + 0x00 - WORD Pause after this block (ms.) {1000} + 0x02 N WORD Length of data that follow + 0x04 - BYTE[N] Data as in .TAP files + + This block must be replayed with the standard Spectrum ROM timing values - see the values in + curly brackets in block ID 11. The pilot tone consists in 8063 pulses if the first data byte + (flag byte) is < 128, 3223 otherwise. This block can be used for the ROM loading routines AND + for custom loading routines that use the same timings as ROM ones do. */ + private void ProcessBlockID10(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x10; + t.BlockDescription = BlockType.Standard_Speed_Data_Block; + t.DataPeriods = new List(); + + int pauseLen = GetWordValue(data, _position); + if (pauseLen == 0) + pauseLen = 1000; + + t.PauseInMS = pauseLen; + + int blockLen = GetWordValue(data, _position + 2); + + _position += 4; + + byte[] tmp = new byte[blockLen]; + tmp = data.Skip(_position).Take(blockLen).ToArray(); + + var t2 = DecodeDataBlock(t, tmp, DataBlockType.Standard, pauseLen); + + // add the block + _datacorder.DataBlocks.Add(t2); + + // advance the position to the next block + _position += blockLen; + + // generate PAUSE block + CreatePauseBlock(_datacorder.DataBlocks.Last()); + } + #endregion + + #region ID 11 - Turbo Speed Data Block +/* length: [0F,10,11]+12 + Offset Value Type Description + 0x00 - WORD Length of PILOT pulse {2168} + 0x02 - WORD Length of SYNC first pulse {667} + 0x04 - WORD Length of SYNC second pulse {735} + 0x06 - WORD Length of ZERO bit pulse {855} + 0x08 - WORD Length of ONE bit pulse {1710} + 0x0A - WORD Length of PILOT tone (number of pulses) {8063 header (flag<128), 3223 data (flag>=128)} + 0x0C - BYTE Used bits in the last byte (other bits should be 0) {8} + (e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00, + where MSb is the leftmost bit, LSb is the rightmost bit) + 0x0D - WORD Pause after this block (ms.) {1000} + 0x0F N BYTE[3] Length of data that follow + 0x12 - BYTE[N] Data as in .TAP files + + This block is very similar to the normal TAP block but with some additional info on the timings and other important + differences. The same tape encoding is used as for the standard speed data block. If a block should use some non-standard + sync or pilot tones (i.e. all sorts of protection schemes) then use the next three blocks to describe it.*/ + private void ProcessBlockID11(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x11; + t.BlockDescription = BlockType.Turbo_Speed_Data_Block; + t.DataPeriods = new List(); + + int pilotPL = GetWordValue(data, _position); + int sync1P = GetWordValue(data, _position + 2); + int sync2P = GetWordValue(data, _position + 4); + int bit0P = GetWordValue(data, _position + 6); + int bit1P = GetWordValue(data, _position + 8); + int pilotTL = GetWordValue(data, _position + 10); + int bitinbyte = data[_position + 12]; + int pause = GetWordValue(data, _position + 13); + + + int blockLen = 0xFFFFFF & GetInt32(data, _position + 0x0F); + + byte[] bLenArr = data.Skip(_position + 0x0F).Take(3).ToArray(); + + _position += 0x12; + + byte[] tmp = new byte[blockLen]; + tmp = data.Skip(_position).Take(blockLen).ToArray(); + + var t2 = DecodeDataBlock(t, tmp, DataBlockType.Turbo, pause, pilotTL, pilotPL, sync1P, sync2P, bit0P, bit1P, bitinbyte); + + t.PauseInMS = pause; + + // add the block + _datacorder.DataBlocks.Add(t2); + + // advance the position to the next block + _position += blockLen; + + // generate PAUSE block + CreatePauseBlock(_datacorder.DataBlocks.Last()); + } + #endregion + + #region ID 12 - Pure Tone +/* length: 04 + Offset Value Type Description + 0x00 - WORD Length of one pulse in T-states + 0x02 - WORD Number of pulses + + This will produce a tone which is basically the same as the pilot tone in the ID 10, ID 11 blocks. You can define how + long the pulse is and how many pulses are in the tone. */ + private void ProcessBlockID12(byte[] data) + { + int blockLen = 4; + + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x12; + t.BlockDescription = BlockType.Pure_Tone; + t.DataPeriods = new List(); + t.PauseInMS = 0; + + // get values + int pulseLength = GetWordValue(data, _position); + int pulseCount = GetWordValue(data, _position + 2); + + t.AddMetaData(BlockDescriptorTitle.Pulse_Length, pulseLength.ToString() + " T-States"); + t.AddMetaData(BlockDescriptorTitle.Pulse_Count, pulseCount.ToString()); + + // build period information + for (int p = 0; p < pulseCount; p++) + { + t.DataPeriods.Add(pulseLength); + } + + // add the block + _datacorder.DataBlocks.Add(t); + + // advance the position to the next block + _position += blockLen; + } + #endregion + + #region ID 13 - Pulse sequence +/* length: [00]*02+01 + Offset Value Type Description + 0x00 N BYTE Number of pulses + 0x01 - WORD[N] Pulses' lengths + + This will produce N pulses, each having its own timing. Up to 255 pulses can be stored in this block; this is useful for non-standard + sync tones used by some protection schemes. */ + private void ProcessBlockID13(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x13; + t.BlockDescription = BlockType.Pulse_Sequence; + t.DataPeriods = new List(); + + t.PauseInMS = 0; + + // get pulse count + int pulseCount = data[_position]; + t.AddMetaData(BlockDescriptorTitle.Pulse_Count, pulseCount.ToString()); + _position++; + + // build period information + for (int p = 0; p < pulseCount; p++, _position += 2) + { + // get pulse length + int pulseLength = GetWordValue(data, _position); + t.AddMetaData(BlockDescriptorTitle.Needs_Parsing, "Pulse " + p + " Length\t" + pulseLength.ToString() + " T-States"); + t.DataPeriods.Add(pulseLength); + } + + // add the block + _datacorder.DataBlocks.Add(t); + } + #endregion + + #region ID 14 - Pure Data Block +/* length: [07,08,09]+0A + Offset Value Type Description + 0x00 - WORD Length of ZERO bit pulse + 0x02 - WORD Length of ONE bit pulse + 0x04 - BYTE Used bits in last byte (other bits should be 0) + (e.g. if this is 6, then the bits used (x) in the last byte are: xxxxxx00, + where MSb is the leftmost bit, LSb is the rightmost bit) + 0x05 - WORD Pause after this block (ms.) + 0x07 N BYTE[3] Length of data that follow + 0x0A - BYTE[N] Data as in .TAP files + + This is the same as in the turbo loading data block, except that it has no pilot or sync pulses. */ + private void ProcessBlockID14(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x14; + t.BlockDescription = BlockType.Pure_Data_Block; + t.DataPeriods = new List(); + + int pilotPL = 0; + int sync1P = 0; + int sync2P = 0; + int bit0P = GetWordValue(data, _position + 0); + int bit1P = GetWordValue(data, _position + 2); + int pilotTL = 0; + int bitinbyte = data[_position + 4]; + int pause = GetWordValue(data, _position + 5); + + int blockLen = 0xFFFFFF & GetInt32(data, _position + 0x07); + + _position += 0x0A; + + byte[] tmp = new byte[blockLen]; + tmp = data.Skip(_position).Take(blockLen).ToArray(); + + var t2 = DecodeDataBlock(t, tmp, DataBlockType.Pure, pause, pilotTL, pilotPL, sync1P, sync2P, bit0P, bit1P, bitinbyte); + + t.PauseInMS = pause; + + // add the block + _datacorder.DataBlocks.Add(t2); + + // advance the position to the next block + _position += blockLen; + + // generate PAUSE block + CreatePauseBlock(_datacorder.DataBlocks.Last()); + } + #endregion + + #region ID 15 - Direct Recording +/* length: [05,06,07]+08 + Offset Value Type Description + 0x00 - WORD Number of T-states per sample (bit of data) + 0x02 - WORD Pause after this block in milliseconds (ms.) + 0x04 - BYTE Used bits (samples) in last byte of data (1-8) + (e.g. if this is 2, only first two samples of the last byte will be played) + 0x05 N BYTE[3] Length of samples' data + 0x08 - BYTE[N] Samples data. Each bit represents a state on the EAR port (i.e. one sample). + MSb is played first. + + This block is used for tapes which have some parts in a format such that the turbo loader block cannot be used. + This is not like a VOC file, since the information is much more compact. Each sample value is represented by one bit only + (0 for low, 1 for high) which means that the block will be at most 1/8 the size of the equivalent VOC. + The preferred sampling frequencies are 22050 or 44100 Hz (158 or 79 T-states/sample). + Please, if you can, don't use other sampling frequencies. + Please use this block only if you cannot use any other block. */ + private void ProcessBlockID15(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x15; + t.BlockDescription = BlockType.Direct_Recording; + t.DataPeriods = new List(); + + // get values + int samLen = GetInt32(data, _position + 5); + int samSize = 0xFFFFFF & samLen; + + int tStatesPerSample = GetWordValue(data, _position); + int pauseAfterBlock = GetWordValue(data, _position + 2); + int usedBitsInLastByte = data[_position + 4]; + + // skip to samples data + _position += 8; + + int pulseLength = 0; + int pulseCount = 0; + + // ascertain the pulse count + for (int i = 0; i < samSize; i++) + { + for (int p = 0x80; p != 0; p >>= 1) + { + if (((data[_position + i] ^ pulseLength) & p) != 0) + { + pulseCount++; + pulseLength ^= -1; + } + } + } + + // get the pulses + t.DataPeriods = new List(pulseCount + 2); + int tStateCount = 0; + pulseLength = 0; + for (int i = 1; i < samSize; i++) + { + for (int p = 0x80; p != 0; p >>= 1) + { + tStateCount += tStatesPerSample; + if (((data[_position] ^ pulseLength) & p) != 0) + { + t.DataPeriods.Add(tStateCount); + pulseLength ^= -1; + tStateCount = 0; + } + } + + // incrememt position + _position++; + } + + // get the pulses in the last byte of data + for (int p = 0x80; p != (byte)(0x80 >> usedBitsInLastByte); p >>= 1) + { + tStateCount += tStatesPerSample; + if (((data[_position] ^ pulseLength) & p) != 0) + { + t.DataPeriods.Add(tStateCount); + pulseLength ^= -1; + tStateCount = 0; + } + } + + // add final pulse + t.DataPeriods.Add(tStateCount); + + // add end of block pause + if (pauseAfterBlock > 0) + { + //t.DataPeriods.Add(3500 * pauseAfterBlock); + } + + t.PauseInMS = pauseAfterBlock; + + // increment position + _position++; + + // add the block + _datacorder.DataBlocks.Add(t); + + // generate PAUSE block + CreatePauseBlock(_datacorder.DataBlocks.Last()); + } + #endregion + + #region ID 18 - CSW Recording +/* length: [00,01,02,03]+04 + Offset Value Type Description + 0x00 10+N DWORD Block length (without these four bytes) + 0x04 - WORD Pause after this block (in ms). + 0x06 - BYTE[3] Sampling rate + 0x09 - BYTE Compression type + 0x01: RLE + 0x02: Z-RLE + 0x0A - DWORD Number of stored pulses (after decompression, for validation purposes) + 0x0E - BYTE[N] CSW data, encoded according to the CSW file format specification. + + This block contains a sequence of raw pulses encoded in CSW format v2 (Compressed Square Wave). */ + private void ProcessBlockID18(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x18; + t.BlockDescription = BlockType.CSW_Recording; + t.DataPeriods = new List(); + + int blockLen = GetInt32(data, _position); + _position += 4; + + t.PauseInMS = GetWordValue(data, _position); + + _position += 2; + + int sampleRate = data[_position++] << 16 | data[_position++] << 8 | data[_position++]; + byte compType = data[_position++]; + int pulses = GetInt32(data, _position); + _position += 4; + + int dataLen = blockLen - 10; + + // build source array + byte[] src = new byte[dataLen]; + // build destination array + byte[] dest = new byte[pulses + 1]; + + // process the CSW data + BizHawk.Emulation.Cores.Computers.SinclairSpectrum.CswConverter.ProcessCSWV2(src, ref dest, compType, pulses); + + // create the periods + var rate = (69888 * 50) / sampleRate; + + for (int i = 0; i < dest.Length;) + { + int length = dest[i++] * rate; + if (length == 0) + { + length = GetInt32(dest, i) / rate; + i += 4; + } + + t.DataPeriods.Add(length); + } + + // add closing period + t.DataPeriods.Add((69888 * 50) / 10); + + _position += dataLen; + //_position += blockLen; + + // add the block + _datacorder.DataBlocks.Add(t); + + // generate PAUSE block + CreatePauseBlock(_datacorder.DataBlocks.Last()); + } + #endregion + + #region ID 19 - Generalized Data Block +/* length: [00,01,02,03]+04 + Offset Value Type Description + 0x00 - DWORD Block length (without these four bytes) + 0x04 - WORD Pause after this block (ms) + 0x06 TOTP DWORD Total number of symbols in pilot/sync block (can be 0) + 0x0A NPP BYTE Maximum number of pulses per pilot/sync symbol + 0x0B ASP BYTE Number of pilot/sync symbols in the alphabet table (0=256) + 0x0C TOTD DWORD Total number of symbols in data stream (can be 0) + 0x10 NPD BYTE Maximum number of pulses per data symbol + 0x11 ASD BYTE Number of data symbols in the alphabet table (0=256) + 0x12 - SYMDEF[ASP] Pilot and sync symbols definition table + This field is present only if TOTP>0 + 0x12+ + (2*NPP+1)*ASP - PRLE[TOTP] Pilot and sync data stream + This field is present only if TOTP>0 + 0x12+ + (TOTP>0)*((2*NPP+1)*ASP)+ + TOTP*3 - SYMDEF[ASD] Data symbols definition table + This field is present only if TOTD>0 + 0x12+ + (TOTP>0)*((2*NPP+1)*ASP)+ + TOTP*3+ + (2*NPD+1)*ASD - BYTE[DS] Data stream + This field is present only if TOTD>0 + + This block has been specifically developed to represent an extremely wide range of data encoding techniques. + The basic idea is that each loading component (pilot tone, sync pulses, data) is associated to a specific sequence + of pulses, where each sequence (wave) can contain a different number of pulses from the others. + In this way we can have a situation where bit 0 is represented with 4 pulses and bit 1 with 8 pulses. + + ---- + SYMDEF structure format + Offset Value Type Description + 0x00 - BYTE Symbol flags + b0-b1: starting symbol polarity + 00: opposite to the current level (make an edge, as usual) - default + 01: same as the current level (no edge - prolongs the previous pulse) + 10: force low level + 11: force high level + 0x01 - WORD[MAXP] Array of pulse lengths. + + The alphabet is stored using a table where each symbol is a row of pulses. The number of columns (i.e. pulses) of the table is the + length of the longest sequence amongst all (MAXP=NPP or NPD, for pilot/sync or data blocks respectively); shorter waves are terminated by a + zero-length pulse in the sequence. + Any number of data symbols is allowed, so we can have more than two distinct waves; for example, imagine a loader which writes two bits at a + time by encoding them with four distinct pulse lengths: this loader would have an alphabet of four symbols, each associated to a specific + sequence of pulses (wave). + ---- + ---- + PRLE structure format + Offset Value Type Description + 0x00 - BYTE Symbol to be represented + 0x01 - WORD Number of repetitions + + Most commonly, pilot and sync are repetitions of the same pulse, thus they are represented using a very simple RLE encoding structure which stores + the symbol and the number of times it must be repeated. + Each symbol in the data stream is represented by a string of NB bits of the block data, where NB = ceiling(Log2(ASD)). + Thus the length of the whole data stream in bits is NB*TOTD, or in bytes DS=ceil(NB*TOTD/8). + ---- */ + private void ProcessBlockID19(byte[] data) + { + // not currently implemented properly + + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x19; + t.BlockDescription = BlockType.Generalized_Data_Block; + t.DataPeriods = new List(); + + int blockLen = GetInt32(data, _position); + _position += 4; + + int pause = GetWordValue(data, _position); + _position += 2; + + int totp = GetInt32(data, _position); + _position += 4; + + int npp = data[_position++]; + + int asp = data[_position++]; + + int totd = GetInt32(data, _position); + _position += 4; + + int npd = data[_position++]; + + int asd = data[_position++]; + + // add the block + _datacorder.DataBlocks.Add(t); + + // advance the position to the next block + _position += blockLen; + } + #endregion + + #region ID 20 - Pause (silence) or 'Stop the Tape' command +/* length: 02 + Offset Value Type Description + 0x00 - WORD Pause duration (ms.) + + This will make a silence (low amplitude level (0)) for a given time in milliseconds. If the value is 0 then the + emulator or utility should (in effect) STOP THE TAPE, i.e. should not continue loading until the user or emulator requests it. */ + private void ProcessBlockID20(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x20; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Pause_or_Stop_the_Tape; + + int pauseDuration = GetWordValue(data, _position); + if (pauseDuration != 0) + { + //t.BlockDescription = "Pause: " + pauseDuration + " ms"; + } + else + { + //t.BlockDescription = "[STOP THE TAPE]"; + } + + t.PauseInMS = pauseDuration; + + if (pauseDuration == 0) + { + // issue stop the tape command + t.Command = TapeCommand.STOP_THE_TAPE; + // add 1ms period + //t.DataPeriods.Add(3500); + //pauseDuration = -1; + + } + else + { + // this is actually just a pause + //pauseDuration = 3500 * pauseDuration; + //t.DataPeriods.Add(pauseDuration); + } + + // add end of block pause + //t.DataPeriods.Add(pauseDuration); + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advanced position to next block + _position += 2; + + // generate PAUSE block + CreatePauseBlock(_datacorder.DataBlocks.Last()); + + } + #endregion + + #region ID 21 - Group start +/* length: [00]+01 + Offset Value Type Description + 0x00 L BYTE Length of the group name string + 0x01 - CHAR[L] Group name in ASCII format (please keep it under 30 characters long) + + This block marks the start of a group of blocks which are to be treated as one single (composite) block. + This is very handy for tapes that use lots of subblocks like Bleepload (which may well have over 160 custom loading blocks). + You can also give the group a name (example 'Bleepload Block 1'). + For each group start block, there must be a group end block. Nesting of groups is not allowed. */ + private void ProcessBlockID21(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x21; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Group_Start; + + int nameLength = data[_position]; + _position++; + + string name = Encoding.ASCII.GetString(data, _position, nameLength); + //t.BlockDescription = "[GROUP: " + name + "]"; + t.Command = TapeCommand.BEGIN_GROUP; + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += nameLength; + } + #endregion + + #region ID 22 - Group end +/* length: 00 + + This indicates the end of a group. This block has no body. */ + private void ProcessBlockID22(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x22; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Group_End; + t.Command = TapeCommand.END_GROUP; + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + } + #endregion + + #region ID 23 - Jump to block +/* length: 02 + Offset Value Type Description + 0x00 - WORD Relative jump value + + This block will enable you to jump from one block to another within the file. The value is a signed short word + (usually 'signed short' in C); Some examples: + Jump 0 = 'Loop Forever' - this should never happen + Jump 1 = 'Go to the next block' - it is like NOP in assembler ;) + Jump 2 = 'Skip one block' + Jump -1 = 'Go to the previous block' + All blocks are included in the block count!. */ + private void ProcessBlockID23(byte[] data) + { + // not implemented properly + + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x23; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Jump_to_Block; + + int relativeJumpValue = GetWordValue(data, _position); + string result = string.Empty; + + switch(relativeJumpValue) + { + case 0: + result = "Loop Forever"; + break; + case 1: + result = "To Next Block"; + break; + case 2: + result = "Skip One Block"; + break; + case -1: + result = "Go to Previous Block"; + break; + } + + //t.BlockDescription = "[JUMP BLOCK - " + result +"]"; + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += 2; + } + #endregion + + #region ID 24 - Loop start +/* length: 02 + Offset Value Type Description + 0x00 - WORD Number of repetitions (greater than 1) + + If you have a sequence of identical blocks, or of identical groups of blocks, you can use this block to tell how many times they should + be repeated. This block is the same as the FOR statement in BASIC. + For simplicity reasons don't nest loop blocks! */ + private void ProcessBlockID24(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x24; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Loop_Start; + + // loop should start from the next block + int loopStart = _datacorder.DataBlocks.Count() + 1; + + int numberOfRepetitions = GetWordValue(data, _position); + + // update loop counter + _loopCounter.Add( + new KeyValuePair( + loopStart, + numberOfRepetitions)); + + // update description + //t.BlockDescription = "[LOOP START - " + numberOfRepetitions + " times]"; + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += 2; + } + #endregion + + #region ID 25 - Loop end +/* length: 00 + + This is the same as BASIC's NEXT statement. It means that the utility should jump back to the start of the loop if it hasn't + been run for the specified number of times. + This block has no body. */ + private void ProcessBlockID25(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x25; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Loop_End; + + // get the most recent loop info + var loop = _loopCounter.LastOrDefault(); + + int loopStart = loop.Key; + int numberOfRepetitions = loop.Value; + + if (numberOfRepetitions == 0) + { + return; + } + + // get the number of blocks to loop + int blockCnt = _datacorder.DataBlocks.Count() - loopStart; + + // loop through each group to repeat + for (int b = 0; b < numberOfRepetitions; b++) + { + TapeDataBlock repeater = new TapeDataBlock(); + //repeater.BlockDescription = "[LOOP REPEAT - " + (b + 1) + "]"; + repeater.DataPeriods = new List(); + + // add the repeat block + _datacorder.DataBlocks.Add(repeater); + + // now iterate through and add the blocks to be repeated + for (int i = 0; i < blockCnt; i++) + { + var block = _datacorder.DataBlocks[loopStart + i]; + _datacorder.DataBlocks.Add(block); + } + } + } + #endregion + + #region ID 26 - Call sequence +/* length: [00,01]*02+02 + Offset Value Type Description + 0x00 N WORD Number of calls to be made + 0x02 - WORD[N] Array of call block numbers (relative-signed offsets) + + This block is an analogue of the CALL Subroutine statement. It basically executes a sequence of blocks that are somewhere else and + then goes back to the next block. Because more than one call can be normally used you can include a list of sequences to be called. + The 'nesting' of call blocks is also not allowed for the simplicity reasons. You can, of course, use the CALL blocks in the LOOP + sequences and vice versa. The value is relative for the obvious reasons - so that you can add some blocks in the beginning of the + file without disturbing the call values. Please take a look at 'Jump To Block' for reference on the values. */ + private void ProcessBlockID26(byte[] data) + { + // block processing not implemented for this - just gets added for informational purposes only + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x26; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Call_Sequence; + + int blockSize = 2 + 2 * GetWordValue(data, _position); + t.PauseInMS = 0; + + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += blockSize; + } + #endregion + + #region ID 27 - Return from sequence +/* length: 00 + + This block indicates the end of the Called Sequence. The next block played will be the block after the last CALL block (or the next Call, + if the Call block had multiple calls). + Again, this block has no body. */ + private void ProcessBlockID27(byte[] data) + { + // block processing not implemented for this - just gets added for informational purposes only + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x27; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Return_From_Sequence; + t.PauseInMS = 0; + + + // add to tape + _datacorder.DataBlocks.Add(t); + } + #endregion + + #region ID 28 - Select block +/* length: [00,01]+02 + Offset Value Type Description + 0x00 - WORD Length of the whole block (without these two bytes) + 0x02 N BYTE Number of selections + 0x03 - SELECT[N] List of selections + + ---- + SELECT structure format + Offset Value Type Description + 0x00 - WORD Relative Offset + 0x02 L BYTE Length of description text + 0x03 - CHAR[L] Description text (please use single line and max. 30 chars) + ---- + + This block is useful when the tape consists of two or more separately-loadable parts. With this block, you are able to select + one of the parts and the utility/emulator will start loading from that block. For example you can use it when the game has a + separate Trainer or when it is a multiload. Of course, to make some use of it the emulator/utility has to show a menu with the + selections when it encounters such a block. All offsets are relative signed words. */ + private void ProcessBlockID28(byte[] data) + { + // block processing not implemented for this - just gets added for informational purposes only + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x28; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Select_Block; + + int blockSize = 2 + GetWordValue(data, _position); + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += blockSize; + } + #endregion + + #region ID 2A - Stop the tape if in 48K mode +/* length: 04 + Offset Value Type Description + 0x00 0 DWORD Length of the block without these four bytes (0) + + When this block is encountered, the tape will stop ONLY if the machine is an 48K Spectrum. This block is to be used for + multiloading games that load one level at a time in 48K mode, but load the entire tape at once if in 128K mode. + This block has no body of its own, but follows the extension rule. */ + private void ProcessBlockID2A(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x2A; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Stop_the_Tape_48K; + t.Command = TapeCommand.STOP_THE_TAPE_48K; + + int blockSize = 4 + GetWordValue(data, _position); + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += blockSize; + } + #endregion + + #region ID 2B - Set signal level +/* length: 05 + Offset Value Type Description + 0x00 1 DWORD Block length (without these four bytes) + 0x04 - BYTE Signal level (0=low, 1=high) + + This block sets the current signal level to the specified value (high or low). It should be used whenever it is necessary to avoid any + ambiguities, e.g. with custom loaders which are level-sensitive. */ + private void ProcessBlockID2B(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x2B; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Set_Signal_Level; + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += 5; + } + #endregion + + #region ID 30 - Text description +/* length: [00]+01 + Offset Value Type Description + 0x00 N BYTE Length of the text description + 0x01 - CHAR[N] Text description in ASCII format + + This is meant to identify parts of the tape, so you know where level 1 starts, where to rewind to when the game ends, etc. + This description is not guaranteed to be shown while the tape is playing, but can be read while browsing the tape or changing + the tape pointer. + The description can be up to 255 characters long but please keep it down to about 30 so the programs can show it in one line + (where this is appropriate). + Please use 'Archive Info' block for title, authors, publisher, etc. */ + private void ProcessBlockID30(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x30; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Text_Description; + + int textLen = data[_position]; + _position++; + + string desc = Encoding.ASCII.GetString(data, _position, textLen); + + t.PauseInMS = 0; + + //t.BlockDescription = "[" + desc + "]"; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += textLen; + } + #endregion + + #region ID 31 - Message block +/* length: [01]+02 + Offset Value Type Description + 0x00 - BYTE Time (in seconds) for which the message should be displayed + 0x01 N BYTE Length of the text message + 0x02 - CHAR[N] Message that should be displayed in ASCII format + + This will enable the emulators to display a message for a given time. This should not stop the tape and it should not make silence. + If the time is 0 then the emulator should wait for the user to press a key. + The text message should: + stick to a maximum of 30 chars per line; + use single 0x0D (13 decimal) to separate lines; + stick to a maximum of 8 lines. + If you do not obey these rules, emulators may display your message in any way they like. */ + private void ProcessBlockID31(byte[] data) + { + // currently not implemented properly in ZXHawk + + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x31; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Message_Block; + + _position++; + + int msgLen = data[_position]; + _position++; + + string desc = Encoding.ASCII.GetString(data, _position, msgLen); + + t.Command = TapeCommand.SHOW_MESSAGE; + + //t.BlockDescription = "[MESSAGE: " + desc + "]"; + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += msgLen; + } + #endregion + + #region ID 32 - Archive info +/* length: [00,01]+02 + Offset Value Type Description + 0x00 - WORD Length of the whole block (without these two bytes) + 0x02 N BYTE Number of text strings + 0x03 - TEXT[N] List of text strings + + ---- + TEXT structure format + Offset Value Type Description + 0x00 - BYTE Text identification byte: + 00 - Full title + 01 - Software house/publisher + 02 - Author(s) + 03 - Year of publication + 04 - Language + 05 - Game/utility type + 06 - Price + 07 - Protection scheme/loader + 08 - Origin + FF - Comment(s) + 0x01 L BYTE Length of text string + 0x02 - CHAR[L] Text string in ASCII format + ---- + + Use this block at the beginning of the tape to identify the title of the game, author, publisher, year of publication, price (including + the currency), type of software (arcade adventure, puzzle, word processor, ...), protection scheme it uses (Speedlock 1, Alkatraz, ...) + and its origin (Original, Budget re-release, ...), etc. This block is built in a way that allows easy future expansion. + The block consists of a series of text strings. Each text has its identification number (which tells us what the text means) and then + the ASCII text. To make it possible to skip this block, if needed, the length of the whole block is at the beginning of it. + If all texts on the tape are in English language then you don't have to supply the 'Language' field + The information about what hardware the tape uses is in the 'Hardware Type' block, so no need for it here. */ + private void ProcessBlockID32(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x32; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Archive_Info; + + int blockLen = GetWordValue(data, _position); + _position += 2; + int stringCount = data[_position++]; + + // iterate through each string + for (int s = 0; s < stringCount; s++) + { + // identify the type of text + int type = data[_position++]; + + // get text length + int strLen = data[_position++]; + + string title = "Info: "; + + switch (type) + { + case 0x00: + title = "Full Title: "; + break; + case 0x01: + title = "Software House/Publisher: "; + break; + case 0x02: + title = "Author(s): "; + break; + case 0x03: + title = "Year of Publication: "; + break; + case 0x04: + title = "Language: "; + break; + case 0x05: + title = "Game/Utility Type: "; + break; + case 0x06: + title = "Price: "; + break; + case 0x07: + title = "Protection Scheme/Loader: "; + break; + case 0x08: + title = "Origin: "; + break; + case 0xFF: + title = "Comment(s): "; + break; + default: + break; + } + + // add title to description + //t.BlockDescription += title; + + // get string data + string val = Encoding.ASCII.GetString(data, _position, strLen); + //t.BlockDescription += val + " \n"; + + t.PauseInMS = 0; + + // advance to next string block + _position += strLen; + } + + // add to tape + _datacorder.DataBlocks.Add(t); + } + #endregion + + #region ID 33 - Hardware type +/* length: [00]*03+01 + Offset Value Type Description + 0x00 N BYTE Number of machines and hardware types for which info is supplied + 0x01 - HWINFO[N] List of machines and hardware + + ---- + HWINFO structure format + Offset Value Type Description + 0x00 - BYTE Hardware type + 0x01 - BYTE Hardware ID + 0x02 - BYTE Hardware information: + 00 - The tape RUNS on this machine or with this hardware, + but may or may not use the hardware or special features of the machine. + 01 - The tape USES the hardware or special features of the machine, + such as extra memory or a sound chip. + 02 - The tape RUNS but it DOESN'T use the hardware + or special features of the machine. + 03 - The tape DOESN'T RUN on this machine or with this hardware. + ---- + + This blocks contains information about the hardware that the programs on this tape use. Please include only machines and hardware for + which you are 100% sure that it either runs (or doesn't run) on or with, or you know it uses (or doesn't use) the hardware or special + features of that machine. + If the tape runs only on the ZX81 (and TS1000, etc.) then it clearly won't work on any Spectrum or Spectrum variant, so there's no + need to list this information. + If you are not sure or you haven't tested a tape on some particular machine/hardware combination then do not include it in the list. + The list of hardware types and IDs is somewhat large, and may be found at the end of the format description. */ + private void ProcessBlockID33(byte[] data) + { + // currently not implemented properly in ZXHawk + + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x33; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Hardware_Type; + + t.PauseInMS = 0; + + // first byte contains number of HWINFOs + int infos = data[_position]; + + _position += 1; + + // now starts the HW infos (each block 3 bytes) + for (int i = 0; i < infos; i++) + { + _position += 3; + } + + // add to tape + _datacorder.DataBlocks.Add(t); + } + #endregion + + #region ID 35 - Custom info block +/* length: [10,11,12,13]+14 + Offset Value Type Description + 0x00 - CHAR[10] Identification string (in ASCII) + 0x10 L DWORD Length of the custom info + 0x14 - BYTE[L] Custom info + + This block can be used to save any information you want. For example, it might contain some information written by a utility, + extra settings required by a particular emulator, or even poke data. */ + private void ProcessBlockID35(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x35; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Custom_Info_Block; + + t.PauseInMS = 0; + + string info = Encoding.ASCII.GetString(data, _position, 0x10); + //t.BlockDescription = "[CUSTOM INFO: " + info + "]"; + _position += 0x10; + + int blockLen = BitConverter.ToInt32(data, _position); + _position += 4; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += blockLen; + } + #endregion + + #region ID 5A - "Glue" block +/* length: 09 + Offset Value Type Description + 0x00 - BYTE[9] Value: { "XTape!",0x1A,MajR,MinR } + Just skip these 9 bytes and you will end up on the next ID. + + This block is generated when you merge two ZX Tape files together. It is here so that you can easily copy the files together and use + them. Of course, this means that resulting file would be 10 bytes longer than if this block was not used. All you have to do + if you encounter this block ID is to skip next 9 bytes. + If you can avoid using this block for this purpose, then do so; it is preferable to use a utility to join the two files and + ensure that they are both of the higher version number. */ + private void ProcessBlockID5A(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x5A; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Glue_Block; + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += 9; + } + #endregion + + #region UnDetected Blocks + + private void ProcessUnidentifiedBlock(byte[] data) + { + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = -2; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Unsupported; + //t.BlockDescription = "[UNSUPPORTED - 0x" + data[_position - 1] + "]"; + + _position += GetInt32(data, _position) & 0xFFFFFF; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += 4; + } + + #endregion + + #region Depreciated Blocks + + // These mostly should be ignored by ZXHawk - here for completeness + + #region ID 16 - C64 ROM Type Data Block + private void ProcessBlockID16(byte[] data) + { + // zxhawk will not implement this block. it will however handle it so subsequent blocks can be parsed + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x16; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.C64_ROM_Type_Data_Block; + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + int blockLen = GetInt32(data, _position); + _position += blockLen; + } + #endregion + + #region ID 17 - C64 Turbo Tape Data Block + private void ProcessBlockID17(byte[] data) + { + // zxhawk will not implement this block. it will however handle it so subsequent blocks can be parsed + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x17; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.C64_Turbo_Tape_Data_Block; + + t.PauseInMS = 0; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + int blockLen = GetInt32(data, _position); + _position += blockLen; + } + #endregion + + #region ID 34 - Emulation info + private void ProcessBlockID34(byte[] data) + { + // currently not implemented properly in ZXHawk + + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x34; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Emulation_Info; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += 8; + } + #endregion + + #region ID 40 - Snapshot block + /* length: [01,02,03]+04 + Offset Value Type Description + 0x00 - BYTE Snapshot type: + 00: .Z80 format + 01: .SNA format + 0x01 L BYTE[3] Snapshot length + 0x04 - BYTE[L] Snapshot itself + + This would enable one to snapshot the game at the start and still have all the tape blocks (level data, etc.) in the same file. + Only .Z80 and .SNA snapshots are supported for compatibility reasons! + The emulator should take care of that the snapshot is not taken while the actual Tape loading is taking place (which doesn't do much sense). + And when an emulator encounters the snapshot block it should load it and then continue with the next block. */ + private void ProcessBlockID40(byte[] data) + { + // currently not implemented properly in ZXHawk + + TapeDataBlock t = new TapeDataBlock(); + t.BlockID = 0x40; + t.DataPeriods = new List(); + t.BlockDescription = BlockType.Snapshot_Block; + + _position++; + + int blockLen = data[_position] | + data[_position + 1] << 8 | + data[_position + 2] << 16; + _position += 3; + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += blockLen; + } + #endregion + + #endregion + + #endregion + + #region DataBlockDecoder + + /// + /// Used to process either a standard or turbo data block + /// + /// + /// + /// + private TapeDataBlock DecodeDataBlock + ( + TapeDataBlock block, + byte[] blockdata, + DataBlockType dataBlockType, + int pauseAfterBlock, + int pilotCount, + + int pilotToneLength = 2168, + int sync1PulseLength = 667, + int sync2PulseLength = 735, + int bit0PulseLength = 855, + int bit1PulseLength = 1710, + int bitsInLastByte = 8 + ) + { + + // first get the block description + string description = string.Empty; + + // process the type byte + /* (The type is 0,1,2 or 3 for a Program, Number array, Character array or Code file. + A SCREEN$ file is regarded as a Code file with start address 16384 and length 6912 decimal. + If the file is a Program file, parameter 1 holds the autostart line number (or a number >=32768 if no LINE parameter was given) + and parameter 2 holds the start of the variable area relative to the start of the program. If it's a Code file, parameter 1 holds + the start of the code block when saved, and parameter 2 holds 32768. For data files finally, the byte at position 14 decimal holds the variable name.) + */ + + int blockSize = blockdata.Length; + + // dont get description info for Pure Data Blocks + if (dataBlockType != DataBlockType.Pure) + { + if (blockdata[0] == 0x00 && blockSize == 19) + { + string fileName = Encoding.ASCII.GetString(blockdata.Skip(2).Take(10).ToArray()).Trim(); + string type = "Unknown Type"; + StringBuilder sb = new StringBuilder(); + + var param1 = GetWordValue(blockdata, 12); + var param2 = GetWordValue(blockdata, 14); + + // header block - examine first byte of header + if (blockdata[1] == 0) + { + type = "Program"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + else if (blockdata[1] == 1) + { + type = "NumArray"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + else if (blockdata[1] == 2) + { + type = "CharArray"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + else if (blockdata[1] == 3) + { + type = "Code"; + sb.Append(type + ": "); + sb.Append(fileName + " "); + } + } + else if (blockdata[0] == 0xff) + { + // data block + description = "Data Block " + (blockSize - 2) + "bytes"; + block.AddMetaData(BlockDescriptorTitle.Data_Bytes, (blockSize - 2).ToString() + " Bytes"); + } + else + { + // some other type (turbo data etc..) + description = string.Format("#{0} block, {1} bytes", blockdata[0].ToString("X2"), blockSize); + //description += string.Format(", crc {0}", ((crc != 0) ? string.Format("bad (#{0:X2}!=#{1:X2})", crcFile, crcValue) : "ok")); + block.AddMetaData(BlockDescriptorTitle.Undefined, description); + } + /* + if (blockdata[0] == 0x00 && blockSize == 19 && (blockdata[1] == 0x00) || (blockdata[1] == 3 && blockdata.Length > 3)) + { + if (dataBlockType != DataBlockType.Turbo) + { + // This is the program header + string fileName = Encoding.ASCII.GetString(blockdata.Skip(2).Take(10).ToArray()).Trim(); + + string type = ""; + if (blockdata[0] == 0x00) + { + type = "Program"; + block.AddMetaData(BlockDescriptorTitle.Program, fileName); + } + else + { + type = "Bytes"; + block.AddMetaData(BlockDescriptorTitle.Bytes, fileName); + } + + // now build the description string + StringBuilder sb = new StringBuilder(); + sb.Append(type + ": "); + sb.Append(fileName + " "); + sb.Append(GetWordValue(blockdata, 14)); + sb.Append(":"); + sb.Append(GetWordValue(blockdata, 12)); + description = sb.ToString(); + } + } + else if (blockdata[0] == 0xFF) + { + // this is a data block + description = "Data Block " + (blockSize - 2) + "bytes"; + block.AddMetaData(BlockDescriptorTitle.Data_Bytes, (blockSize - 2).ToString() + " Bytes"); + } + else + { + // other type + description = string.Format("#{0} block, {1} bytes", blockdata[0].ToString("X2"), blockSize); + //description += string.Format(", crc {0}", ((crc != 0) ? string.Format("bad (#{0:X2}!=#{1:X2})", crcFile, crcValue) : "ok")); + block.AddMetaData(BlockDescriptorTitle.Undefined, description); + } + */ + } + + // update metadata + switch (dataBlockType) + { + case DataBlockType.Standard: + case DataBlockType.Turbo: + + if (dataBlockType == DataBlockType.Standard) + block.BlockDescription = BlockType.Standard_Speed_Data_Block; + if (dataBlockType == DataBlockType.Turbo) + block.BlockDescription = BlockType.Turbo_Speed_Data_Block; + + block.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Length, pilotToneLength.ToString() + " T-States"); + block.AddMetaData(BlockDescriptorTitle.Pilot_Pulse_Count, pilotCount.ToString() + " Pulses"); + block.AddMetaData(BlockDescriptorTitle.First_Sync_Length, sync1PulseLength.ToString() + " T-States"); + block.AddMetaData(BlockDescriptorTitle.Second_Sync_Length, sync2PulseLength.ToString() + " T-States"); + break; + + case DataBlockType.Pure: + block.BlockDescription = BlockType.Pure_Data_Block; + break; + } + + block.AddMetaData(BlockDescriptorTitle.Zero_Bit_Length, bit0PulseLength.ToString() + " T-States"); + block.AddMetaData(BlockDescriptorTitle.One_Bit_Length, bit1PulseLength.ToString() + " T-States"); + block.AddMetaData(BlockDescriptorTitle.Data_Length, blockSize.ToString() + " Bytes"); + block.AddMetaData(BlockDescriptorTitle.Bits_In_Last_Byte, bitsInLastByte.ToString() + " Bits"); + block.AddMetaData(BlockDescriptorTitle.Pause_After_Data, pauseAfterBlock.ToString() + " ms"); + + // calculate period information + List dataPeriods = new List(); + + // generate pilot pulses + + if (pilotCount > 0) + { + for (int i = 0; i < pilotCount; i++) + { + dataPeriods.Add(pilotToneLength); + } + + // add syncro pulses + dataPeriods.Add(sync1PulseLength); + dataPeriods.Add(sync2PulseLength); + } + + int pos = 0; + + // add bit0 and bit1 periods + for (int i = 0; i < blockSize - 1; i++, pos++) + { + for (byte b = 0x80; b != 0; b >>= 1) + { + if ((blockdata[i] & b) != 0) + dataPeriods.Add(bit1PulseLength); + else + dataPeriods.Add(bit0PulseLength); + if ((blockdata[i] & b) != 0) + dataPeriods.Add(bit1PulseLength); + else + dataPeriods.Add(bit0PulseLength); + } + } + + // add the last byte + for (byte c = 0x80; c != (byte)(0x80 >> bitsInLastByte); c >>= 1) + { + if ((blockdata[pos] & c) != 0) + dataPeriods.Add(bit1PulseLength); + else + dataPeriods.Add(bit0PulseLength); + if ((blockdata[pos] & c) != 0) + dataPeriods.Add(bit1PulseLength); + else + dataPeriods.Add(bit0PulseLength); + } + + // add block pause if pause is not 0 + if (pauseAfterBlock != 0) + { + block.PauseInMS = pauseAfterBlock; + //int actualPause = pauseAfterBlock * 3500; + //dataPeriods.Add(actualPause); + } + + // add to the tapedatablock object + block.DataPeriods = dataPeriods; + + // add the raw data + block.BlockData = blockdata; + + return block; + } + + /// + /// Used to process either a standard or turbo data block + /// + /// + /// + /// + private TapeDataBlock DecodeDataBlock + ( + TapeDataBlock block, + byte[] blockData, + DataBlockType dataBlockType, + int pauseAfterBlock, + + int pilotToneLength = 2168, + int sync1PulseLength = 667, + int sync2PulseLength = 735, + int bit0PulseLength = 855, + int bit1PulseLength = 1710, + int bitsInLastByte = 8 + ) + { + + + int pilotCount = 3220; + /* + // pilot count needs to be ascertained from flag byte + int pilotCount; + if (blockData[0] < 128) + pilotCount = 8063; + else + pilotCount = 3223; + */ + + // now we can decode + var nBlock = DecodeDataBlock + ( + block, + blockData, + dataBlockType, + pauseAfterBlock, + pilotCount, + pilotToneLength, + sync1PulseLength, + sync2PulseLength, + bit0PulseLength, + bit1PulseLength, + bitsInLastByte + ); + + + return nBlock; + } + + #endregion + + #region Pause Block Creator + + /// + /// If neccessary a seperate PAUSE block will be created + /// + /// + private void CreatePauseBlock(TapeDataBlock original) + { + if (original.PauseInMS > 0) + { + TapeDataBlock pBlock = new TapeDataBlock(); + pBlock.DataPeriods = new List(); + pBlock.BlockDescription = BlockType.PAUSE_BLOCK; + pBlock.PauseInMS = 0; + var pauseInTStates = TranslatePause(original.PauseInMS); + + pBlock.AddMetaData(BlockDescriptorTitle.Block_ID, pauseInTStates.ToString() + " cycles"); + + int by1000 = pauseInTStates / 70000; + int rem1000 = pauseInTStates % 70000; + + if (by1000 > 1) + { + pBlock.DataPeriods.Add(35000); + pBlock.DataPeriods.Add(pauseInTStates - 35000); + } + else + { + pBlock.DataPeriods.Add(pauseInTStates); + pBlock.DataPeriods.Add(0); + } + + _datacorder.DataBlocks.Add(pBlock); + } + } + + #endregion + } + + public enum DataBlockType + { + Standard, + Turbo, + Pure + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Tape/TapeCommand.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Tape/TapeCommand.cs new file mode 100644 index 0000000000..58aa8cdbee --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Tape/TapeCommand.cs @@ -0,0 +1,16 @@ + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Represents the possible commands that can be raised from each tape block + /// + public enum TapeCommand + { + NONE, + STOP_THE_TAPE, + STOP_THE_TAPE_48K, + BEGIN_GROUP, + END_GROUP, + SHOW_MESSAGE, + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Tape/TapeDataBlock.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Tape/TapeDataBlock.cs new file mode 100644 index 0000000000..40efa71d1e --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/Media/Tape/TapeDataBlock.cs @@ -0,0 +1,288 @@ +using BizHawk.Common; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Represents a tape block + /// + public class TapeDataBlock + { + /// + /// Either the TZX block ID, or -1 in the case of non-tzx blocks + /// + private int _blockID = -1; + public int BlockID + { + get { return _blockID; } + set { + _blockID = value; + + if (MetaData == null) + MetaData = new Dictionary(); + + AddMetaData(BlockDescriptorTitle.Block_ID, value.ToString()); + } + } + + /// + /// The block type + /// + private BlockType _blockType; + public BlockType BlockDescription + { + get { return _blockType; } + set { + _blockType = value; + if (MetaData == null) + MetaData = new Dictionary(); + } + } + + /// + /// Byte array containing the raw block data + /// + private byte[] _blockData; + public byte[] BlockData + { + get { return _blockData; } + set { _blockData = value; } + } + + /// + /// An array of bytearray encoded strings (stored in this format for easy Bizhawk serialization) + /// Its basically tape information + /// + private byte[][] _tapeDescriptionData; + + /// + /// Returns the Tape Description Data in a human readable format + /// + public List TapeDescriptionData + { + get + { + List data = new List(); + + foreach (byte[] b in _tapeDescriptionData) + { + data.Add(Encoding.ASCII.GetString(b)); + } + + return data; + } + } + + + #region Block Meta Data + + /// + /// Dictionary of block related data + /// + public Dictionary MetaData { get; set; } + + /// + /// Adds a single metadata item to the Dictionary + /// + /// + /// + public void AddMetaData(BlockDescriptorTitle descriptor, string data) + { + // check whether entry already exists + bool check = MetaData.ContainsKey(descriptor); + if (check) + { + // already exists - update + MetaData[descriptor] = data; + } + else + { + // create new + MetaData.Add(descriptor, data); + } + } + + #endregion + + + + /// + /// List containing the pulse timing values + /// + public List DataPeriods = new List(); + + public bool InitialPulseLevel; + + /// + /// Command that is raised by this data block + /// (that may or may not need to be acted on) + /// + private TapeCommand _command = TapeCommand.NONE; + public TapeCommand Command + { + get { return _command; } + set { _command = value; } + } + + /// + /// The defined post-block pause + /// + private int _pauseInMS; + public int PauseInMS + { + get { return _pauseInMS; } + set { _pauseInMS = value; } + } + + + /// + /// Returns the data periods as an array + /// (primarily to aid in bizhawk state serialization) + /// + /// + public int[] GetDataPeriodsArray() + { + return DataPeriods.ToArray(); + } + + /// + /// Accepts an array of data periods and updates the DataPeriods list accordingly + /// (primarily to aid in bizhawk state serialization) + /// + /// + public void SetDataPeriodsArray(int[] periodArray) + { + DataPeriods = new List(); + + if (periodArray == null) + return; + + DataPeriods = periodArray.ToList(); + } + + /// + /// Bizhawk state serialization + /// + /// + public void SyncState(Serializer ser, int blockPosition) + { + ser.BeginSection("DataBlock" + blockPosition); + + ser.Sync("_blockID", ref _blockID); + //ser.SyncFixedString("_blockDescription", ref _blockDescription, 200); + ser.SyncEnum("_blockType", ref _blockType); + ser.Sync("_blockData", ref _blockData, true); + ser.SyncEnum("_command", ref _command); + + int[] tempArray = null; + + if (ser.IsWriter) + { + tempArray = GetDataPeriodsArray(); + ser.Sync("_periods", ref tempArray, true); + } + else + { + ser.Sync("_periods", ref tempArray, true); + SetDataPeriodsArray(tempArray); + } + + ser.EndSection(); + } + } + + /// + /// The types of TZX blocks + /// + public enum BlockType + { + Standard_Speed_Data_Block = 0x10, + Turbo_Speed_Data_Block = 0x11, + Pure_Tone = 0x12, + Pulse_Sequence = 0x13, + Pure_Data_Block = 0x14, + Direct_Recording = 0x15, + CSW_Recording = 0x18, + Generalized_Data_Block = 0x19, + Pause_or_Stop_the_Tape = 0x20, + Group_Start = 0x21, + Group_End = 0x22, + Jump_to_Block = 0x23, + Loop_Start = 0x24, + Loop_End = 0x25, + Call_Sequence = 0x26, + Return_From_Sequence = 0x27, + Select_Block = 0x28, + Stop_the_Tape_48K = 0x2A, + Set_Signal_Level = 0x2B, + Text_Description = 0x30, + Message_Block = 0x31, + Archive_Info = 0x32, + Hardware_Type = 0x33, + Custom_Info_Block = 0x35, + Glue_Block = 0x5A, + + // depreciated blocks + C64_ROM_Type_Data_Block = 0x16, + C64_Turbo_Tape_Data_Block = 0x17, + Emulation_Info = 0x34, + Snapshot_Block = 0x40, + + // unsupported / undetected + Unsupported, + + // PZX blocks + PZXT, + PULS, + DATA, + BRWS, + PAUS, + + // zxhawk proprietry + PAUSE_BLOCK, + + WAV_Recording + } + + + /// + /// Different title possibilities + /// + public enum BlockDescriptorTitle + { + Undefined, + Block_ID, + Program, + Data_Bytes, + Bytes, + + Pilot_Pulse_Length, + Pilot_Pulse_Count, + First_Sync_Length, + Second_Sync_Length, + Zero_Bit_Length, + One_Bit_Length, + Data_Length, + Bits_In_Last_Byte, + Pause_After_Data, + + Pulse_Length, + Pulse_Count, + + Text_Description, + Title, + Publisher, + Author, + Year, + Language, + Type, + Price, + Protection, + Origin, + Comments, + + Needs_Parsing + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/ROM/RomData.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/ROM/RomData.cs new file mode 100644 index 0000000000..cb37bbb177 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/ROM/RomData.cs @@ -0,0 +1,69 @@ + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// Information about Amstrad ROM + /// + public class RomData + { + /// + /// ROM Contents + /// + public byte[] RomBytes + { + get { return _romBytes; } + set { _romBytes = value; } + } + private byte[] _romBytes; + + public enum ROMChipType + { + Lower, + Upper + } + + /// + /// Whether this is an Upper or Lower ROM + /// + public ROMChipType ROMType; + + /// + /// The designated ROM position for this ROM + /// + public int ROMPosition; + + /// + /// Initialise a RomData object + /// + /// + /// + /// + /// + /// + public static RomData InitROM(MachineType machineType, byte[] rom, ROMChipType type, int romPosition = 0) + { + RomData RD = new RomData(); + RD.RomBytes = new byte[rom.Length]; + RD.RomBytes = rom; + RD.ROMType = type; + + if (type == ROMChipType.Upper) + { + RD.ROMPosition = romPosition; + } + + for (int i = 0; i < rom.Length; i++) + RD.RomBytes[i] = rom[i]; + + switch (machineType) + { + case MachineType.CPC464: + break; + case MachineType.CPC6128: + break; + } + + return RD; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/SoundProviderMixer.cs b/BizHawk.Emulation.Cores/Computers/AmstradCPC/SoundProviderMixer.cs new file mode 100644 index 0000000000..77e30f5fca --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/SoundProviderMixer.cs @@ -0,0 +1,217 @@ +using BizHawk.Emulation.Common; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.AmstradCPC +{ + /// + /// My attempt at mixing multiple ISoundProvider sources together and outputting another ISoundProvider + /// Currently only supports SyncSoundMode.Sync + /// Attached ISoundProvider sources must already be stereo 44.1khz and ideally sound buffers should be the same length (882) + /// (if not, only 882 samples of their buffer will be used) + /// + internal sealed class SoundProviderMixer : ISoundProvider + { + private class Provider + { + public ISoundProvider SoundProvider { get; set; } + public string ProviderDescription { get; set; } + public int MaxVolume { get; set; } + public short[] Buffer { get; set; } + public int NSamp { get; set; } + } + + private bool _stereo = true; + public bool Stereo + { + get { return _stereo; } + set { _stereo = value; } + } + + private readonly List SoundProviders; + + public SoundProviderMixer(params ISoundProvider[] soundProviders) + { + SoundProviders = new List(); + + foreach (var s in soundProviders) + { + SoundProviders.Add(new Provider + { + SoundProvider = s, + MaxVolume = short.MaxValue, + }); + } + + EqualizeVolumes(); + } + + public SoundProviderMixer(short maxVolume, string description, params ISoundProvider[] soundProviders) + { + SoundProviders = new List(); + + foreach (var s in soundProviders) + { + SoundProviders.Add(new Provider + { + SoundProvider = s, + MaxVolume = maxVolume, + ProviderDescription = description + }); + } + + EqualizeVolumes(); + } + + public void AddSource(ISoundProvider source, string description) + { + SoundProviders.Add(new Provider + { + SoundProvider = source, + MaxVolume = short.MaxValue, + ProviderDescription = description + }); + + EqualizeVolumes(); + } + + public void AddSource(ISoundProvider source, short maxVolume, string description) + { + SoundProviders.Add(new Provider + { + SoundProvider = source, + MaxVolume = maxVolume, + ProviderDescription = description + }); + + EqualizeVolumes(); + } + + public void DisableSource(ISoundProvider source) + { + var sp = SoundProviders.Where(a => a.SoundProvider == source); + if (sp.Count() == 1) + SoundProviders.Remove(sp.First()); + else if (sp.Count() > 1) + foreach (var s in sp) + SoundProviders.Remove(s); + + EqualizeVolumes(); + } + + public void EqualizeVolumes() + { + if (SoundProviders.Count < 1) + return; + + int eachVolume = short.MaxValue / SoundProviders.Count; + foreach (var source in SoundProviders) + { + source.MaxVolume = eachVolume; + } + } + + #region ISoundProvider + + public bool CanProvideAsync => false; + public SyncSoundMode SyncMode => SyncSoundMode.Sync; + + public void SetSyncMode(SyncSoundMode mode) + { + if (mode != SyncSoundMode.Sync) + throw new InvalidOperationException("Only Sync mode is supported."); + } + + public void GetSamplesAsync(short[] samples) + { + throw new NotSupportedException("Async is not available"); + } + + public void DiscardSamples() + { + foreach (var soundSource in SoundProviders) + { + soundSource.SoundProvider.DiscardSamples(); + } + } + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + samples = null; + nsamp = 0; + + // get samples from all the providers + foreach (var sp in SoundProviders) + { + int sampCount; + short[] samp; + sp.SoundProvider.GetSamplesSync(out samp, out sampCount); + sp.NSamp = sampCount; + sp.Buffer = samp; + } + + // are all the sample lengths the same? + var firstEntry = SoundProviders.First(); + bool sameCount = SoundProviders.All(s => s.NSamp == firstEntry.NSamp); + + if (!sameCount) + { + // this is a bit hacky, really all ISoundProviders should be supplying 44100 with 882 samples per frame. + // we will make sure this happens (no matter how it sounds) + if (SoundProviders.Count > 1) + { + for (int i = 0; i < SoundProviders.Count; i++) + { + int ns = SoundProviders[i].NSamp; + short[] buff = new short[882 * 2]; + + for (int b = 0; b < 882 * 2; b++) + { + if (b == SoundProviders[i].Buffer.Length - 1) + { + // end of source buffer + break; + } + + buff[b] = SoundProviders[i].Buffer[b]; + } + + // save back to the soundprovider + SoundProviders[i].NSamp = 882; + SoundProviders[i].Buffer = buff; + } + } + else + { + // just process what we have as-is + } + } + + // mix the soundproviders together + nsamp = 882; + samples = new short[nsamp * 2]; + + for (int i = 0; i < samples.Length; i++) + { + short sectorVal = 0; + foreach (var sp in SoundProviders) + { + if (i < sp.Buffer.Length) + { + if (sp.Buffer[i] > sp.MaxVolume) + sectorVal += (short)sp.MaxVolume; + else + sectorVal += sp.Buffer[i]; + } + + } + + samples[i] = sectorVal; + } + } + + #endregion + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/AmstradCPC/readme.md b/BizHawk.Emulation.Cores/Computers/AmstradCPC/readme.md new file mode 100644 index 0000000000..4fccd97e3b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/AmstradCPC/readme.md @@ -0,0 +1,33 @@ +## CPCHawk + +This may or may not work out. But I figured I could at least start by building on the existing ZXHawk implementation (the machines do have CPU, tape format, PSG and disk drive/controller in common). + +We'll see how that goes... + +#### In Place (but probably requires more work) + +* CPC464 model template +* Non-paged memory +* Standard lower and upper ROM +* Port IO decoding +* CRCT (Cathode Ray Tube Controller) chip emulation +* Amstrad Gate Array chip emulation +* Video rendering (mode 1) +* i8255 Programmable Peripheral Interface (PPI) chip emulation +* AY-3-8912 PSG Port IO +* Keyboard/Joystick +* .CDT tape image file support + +#### Not Yet + +* CPC664, CPC6128, CPC464plus, CPC6128plus, GX4000 models +* RAM banking +* Upper ROM banking +* Video rendering (modes 0, 2 & 3) +* AY-3-8912 PSG sound output +* Datacorder (tape) device +* FDC and FDD devices +* .DSK image parsing and identification (to auto differenciate from ZX Spectrum disk bootloader) +* Expansion IO + +-Asnivor diff --git a/BizHawk.Emulation.Cores/FileID.cs b/BizHawk.Emulation.Cores/FileID.cs index 1314be8b6f..f026b96e5b 100644 --- a/BizHawk.Emulation.Cores/FileID.cs +++ b/BizHawk.Emulation.Cores/FileID.cs @@ -43,6 +43,7 @@ namespace BizHawk.Emulation.Cores C64, ZXSpectrum, + AmstradCPC, INT, A26, A52, A78, LNX, diff --git a/BizHawk.Emulation.Cores/Properties/Resources.Designer.cs b/BizHawk.Emulation.Cores/Properties/Resources.Designer.cs index 73259a9985..29547db751 100644 --- a/BizHawk.Emulation.Cores/Properties/Resources.Designer.cs +++ b/BizHawk.Emulation.Cores/Properties/Resources.Designer.cs @@ -60,6 +60,56 @@ namespace BizHawk.Emulation.Cores.Properties { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] CPC_AMSDOS_0_5_ROM { + get { + object obj = ResourceManager.GetObject("CPC_AMSDOS_0_5_ROM", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] CPC_BASIC_1_0_ROM { + get { + object obj = ResourceManager.GetObject("CPC_BASIC_1_0_ROM", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] CPC_BASIC_1_1_ROM { + get { + object obj = ResourceManager.GetObject("CPC_BASIC_1_1_ROM", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] CPC_OS_6128_ROM { + get { + object obj = ResourceManager.GetObject("CPC_OS_6128_ROM", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] OS_464_ROM { + get { + object obj = ResourceManager.GetObject("OS_464_ROM", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// diff --git a/BizHawk.Emulation.Cores/Properties/Resources.resx b/BizHawk.Emulation.Cores/Properties/Resources.resx index 783b8f5ede..36a0577f82 100644 --- a/BizHawk.Emulation.Cores/Properties/Resources.resx +++ b/BizHawk.Emulation.Cores/Properties/Resources.resx @@ -151,4 +151,19 @@ ..\Resources\plus2.rom.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + ..\Resources\CPC_AMSDOS_0.5.ROM.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\CPC_BASIC_1.0.ROM.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\CPC_BASIC_1.1.ROM.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\CPC_OS_6128.ROM.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\OS_464.ROM.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/BizHawk.Emulation.Cores/Resources/CPC_AMSDOS_0.5.ROM.gz b/BizHawk.Emulation.Cores/Resources/CPC_AMSDOS_0.5.ROM.gz new file mode 100644 index 0000000000..e276f88d0a Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/CPC_AMSDOS_0.5.ROM.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/CPC_BASIC_1.0.ROM.gz b/BizHawk.Emulation.Cores/Resources/CPC_BASIC_1.0.ROM.gz new file mode 100644 index 0000000000..24f3db6bd3 Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/CPC_BASIC_1.0.ROM.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/CPC_BASIC_1.1.ROM.gz b/BizHawk.Emulation.Cores/Resources/CPC_BASIC_1.1.ROM.gz new file mode 100644 index 0000000000..6188f6f970 Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/CPC_BASIC_1.1.ROM.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/CPC_OS_6128.ROM.gz b/BizHawk.Emulation.Cores/Resources/CPC_OS_6128.ROM.gz new file mode 100644 index 0000000000..9503b733ee Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/CPC_OS_6128.ROM.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/OS_464.ROM.gz b/BizHawk.Emulation.Cores/Resources/OS_464.ROM.gz new file mode 100644 index 0000000000..671aa06a93 Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/OS_464.ROM.gz differ diff --git a/Bizware/BizHawk.Bizware.BizwareGL/BizHawk.Bizware.BizwareGL.csproj b/Bizware/BizHawk.Bizware.BizwareGL/BizHawk.Bizware.BizwareGL.csproj index 6e52535337..091299ac9f 100644 --- a/Bizware/BizHawk.Bizware.BizwareGL/BizHawk.Bizware.BizwareGL.csproj +++ b/Bizware/BizHawk.Bizware.BizwareGL/BizHawk.Bizware.BizwareGL.csproj @@ -101,6 +101,9 @@ BizHawk.Common + + + - + \ No newline at end of file diff --git a/Bizware/BizHawk.Bizware.BizwareGL/ClassDiagram1.cd b/Bizware/BizHawk.Bizware.BizwareGL/ClassDiagram1.cd new file mode 100644 index 0000000000..7b894197b9 --- /dev/null +++ b/Bizware/BizHawk.Bizware.BizwareGL/ClassDiagram1.cd @@ -0,0 +1,2 @@ + + \ No newline at end of file