diff --git a/Assets/defctrl.json b/Assets/defctrl.json index 42f8b4cee1..dace0123e0 100644 --- a/Assets/defctrl.json +++ b/Assets/defctrl.json @@ -461,6 +461,79 @@ "Key Cursor Up/Down": "DownArrow", "Key Cursor Left/Right": "RightArrow", "Key Space": "Space" + }, + "ZXSpectrum Controller": { + "P1 Up": "NumberPad8, J1 POV1U, X1 DpadUp, X1 LStickUp", + "P1 Down": "NumberPad2, J1 POV1D, X1 DpadDown, X1 LStickDown", + "P1 Left": "NumberPad4, J1 POV1L, X1 DpadLeft, X1 LStickLeft", + "P1 Right": "NumberPad6, J1 POV1R, X1 DpadRight, X1 LStickRight", + "P1 Button": "NumberPad1, J1 B1, X1 X", + "Key True Video": "", + "Key Inv Video": "", + "Key 1": "D1", + "Key 2": "D2", + "Key 3": "D3", + "Key 4": "D4", + "Key 5": "D5", + "Key 6": "D6", + "Key 7": "D7", + "Key 8": "D8", + "Key 9": "D9", + "Key 0": "D0", + "Key Break": "Delete", + "Key Delete": "Backspace", + "Key Graph": "", + "Key Q": "Q", + "Key W": "W", + "Key E": "E", + "Key R": "R", + "Key T": "T", + "Key Y": "Y", + "Key U": "U", + "Key I": "I", + "Key O": "O", + "Key P": "P", + "Key Extend Mode": "", + "Key Edit": "", + "Key A": "A", + "Key S": "S", + "Key D": "D", + "Key F": "F", + "Key G": "G", + "Key H": "H", + "Key J": "J", + "Key K": "K", + "Key L": "L", + "Key Return": "Return", + "Key Caps Shift": "LeftShift, RightShift", + "Key Caps Lock": "", + "Key Z": "Z", + "Key X": "X", + "Key C": "C", + "Key V": "V", + "Key B": "B", + "Key N": "N", + "Key M": "M", + "Key Period": "Period", + "Key Symbol Shift": "LeftControl, RightControl", + "Key Semi-Colon": "Semicolon", + "Key Inverted-Comma": "", + "Key Left Cursor": "LeftArrow", + "Key Right Cursor": "RightArrow", + "Key Space": "Space", + "Key Up Cursor": "UpArrow", + "Key Down Cursor": "DownArrow", + "Key Comma": "Comma", + "Play Tape": "F1", + "Stop Tape": "F2", + "RTZ Tape": "F3", + "Record Tape": "", + "Key Quote": "Shift+D2", + "Insert Next Tape": "F6", + "Insert Previous Tape": "F5", + "Next Tape Block": "F8", + "Prev Tape Block": "F7", + "Get Tape Status": "F10" }, "Intellivision Controller": { "P1 Up": "UpArrow, J1 POV1U, X1 DpadUp, X1 LStickUp", diff --git a/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs b/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs index 31b65c5df5..fcce1118cf 100644 --- a/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs +++ b/BizHawk.Client.ApiHawk/Classes/BizHawkSystemIdToCoreSystemEnumConverter.cs @@ -96,6 +96,9 @@ namespace BizHawk.Client.ApiHawk case "WSWAN": return CoreSystem.WonderSwan; + case "ZXSpectrum": + return CoreSystem.ZXSpectrum; + case "VB": case "NGP": case "DNGP": @@ -205,6 +208,9 @@ namespace BizHawk.Client.ApiHawk case CoreSystem.WonderSwan: return "WSWAN"; + case CoreSystem.ZXSpectrum: + return "ZXSpectrum"; + default: throw new IndexOutOfRangeException(string.Format("{0} is missing in convert list", value.ToString())); } diff --git a/BizHawk.Client.ApiHawk/Classes/ClientApi.cs b/BizHawk.Client.ApiHawk/Classes/ClientApi.cs index 12ee67277c..767d755105 100644 --- a/BizHawk.Client.ApiHawk/Classes/ClientApi.cs +++ b/BizHawk.Client.ApiHawk/Classes/ClientApi.cs @@ -427,11 +427,11 @@ namespace BizHawk.Client.ApiHawk } else { - return SystemInfo.DualGB; + return SystemInfo.DualGB; } default: - return SystemInfo.FindByCoreSystem(SystemIdConverter.Convert(Global.Emulator.SystemId)); + return SystemInfo.FindByCoreSystem(SystemIdConverter.Convert(Global.Emulator.SystemId)); } } } diff --git a/BizHawk.Client.Common/Api/CoreSystem.cs b/BizHawk.Client.Common/Api/CoreSystem.cs index 132a832b98..9bdc7d8232 100644 --- a/BizHawk.Client.Common/Api/CoreSystem.cs +++ b/BizHawk.Client.Common/Api/CoreSystem.cs @@ -29,6 +29,7 @@ WonderSwan, Libretro, VirtualBoy, - NeoGeoPocket + NeoGeoPocket, + ZXSpectrum } } diff --git a/BizHawk.Client.Common/Global.cs b/BizHawk.Client.Common/Global.cs index b592f1696b..edd287ef49 100644 --- a/BizHawk.Client.Common/Global.cs +++ b/BizHawk.Client.Common/Global.cs @@ -149,6 +149,8 @@ namespace BizHawk.Client.Common return SystemInfo.VirtualBoy; case "NGP": return SystemInfo.NeoGeoPocket; + case "ZXSpectrum": + return SystemInfo.ZXSpectrum; } } } diff --git a/BizHawk.Client.Common/RomLoader.cs b/BizHawk.Client.Common/RomLoader.cs index 6c7cac044f..9458897687 100644 --- a/BizHawk.Client.Common/RomLoader.cs +++ b/BizHawk.Client.Common/RomLoader.cs @@ -19,6 +19,7 @@ using BizHawk.Emulation.Cores.PCEngine; using BizHawk.Emulation.Cores.Sega.Saturn; using BizHawk.Emulation.Cores.Sony.PSP; using BizHawk.Emulation.Cores.Sony.PSX; +using BizHawk.Emulation.Cores.Computers.SinclairSpectrum; using BizHawk.Emulation.DiscSystem; using GPGX64 = BizHawk.Emulation.Cores.Consoles.Sega.gpgx; @@ -657,6 +658,21 @@ namespace BizHawk.Client.Common (C64.C64Settings)GetCoreSettings(), (C64.C64SyncSettings)GetCoreSyncSettings()); break; + case "ZXSpectrum": + + List zxGI = new List(); + foreach (var a in xmlGame.Assets) + { + zxGI.Add(new GameInfo { Name = Path.GetFileNameWithoutExtension(a.Key) }); + } + + nextEmulator = new ZXSpectrum( + nextComm, + xmlGame.Assets.Select(a => a.Value), //.First(), + zxGI, // GameInfo.NullInstance, + (ZXSpectrum.ZXSpectrumSettings)GetCoreSettings(), + (ZXSpectrum.ZXSpectrumSyncSettings)GetCoreSyncSettings()); + break; case "PSX": var entries = xmlGame.AssetFullPaths; var discs = new List(); @@ -990,6 +1006,10 @@ namespace BizHawk.Client.Common var c64 = new C64(nextComm, Enumerable.Repeat(rom.FileData, 1), rom.GameInfo, GetCoreSettings(), GetCoreSyncSettings()); nextEmulator = c64; break; + case "ZXSpectrum": + var zx = new ZXSpectrum(nextComm, Enumerable.Repeat(rom.RomData, 1), Enumerable.Repeat(rom.GameInfo, 1).ToList(), GetCoreSettings(), GetCoreSyncSettings()); + nextEmulator = zx; + break; case "GBA": if (Global.Config.GBA_UsemGBA) { diff --git a/BizHawk.Client.Common/SystemInfo.cs b/BizHawk.Client.Common/SystemInfo.cs index 82414adc50..2d65f1729c 100644 --- a/BizHawk.Client.Common/SystemInfo.cs +++ b/BizHawk.Client.Common/SystemInfo.cs @@ -188,6 +188,11 @@ namespace BizHawk.Client.Common /// public static SystemInfo NeoGeoPocket { get; } = new SystemInfo("Neo-Geo Pocket", CoreSystem.NeoGeoPocket, 1); + /// + /// Gets the instance for ZXSpectrum + /// + public static SystemInfo ZXSpectrum { get; } = new SystemInfo("ZX Spectrum", CoreSystem.ZXSpectrum, 2); + #endregion Get SystemInfo /// diff --git a/BizHawk.Client.Common/config/PathEntry.cs b/BizHawk.Client.Common/config/PathEntry.cs index ae05acc25e..d8e070f950 100644 --- a/BizHawk.Client.Common/config/PathEntry.cs +++ b/BizHawk.Client.Common/config/PathEntry.cs @@ -292,7 +292,13 @@ namespace BizHawk.Client.Common new PathEntry { System = "C64", SystemDisplayName = "Commodore 64", Type = "Screenshots", Path = Path.Combine(".", "Screenshots"), Ordinal = 4 }, new PathEntry { System = "C64", SystemDisplayName = "Commodore 64", 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 = "ZXSpectrum", SystemDisplayName = "Sinclair ZX Spectrum", Type = "Base", Path = Path.Combine(".", "ZXSpectrum"), Ordinal = 0 }, + new PathEntry { System = "ZXSpectrum", SystemDisplayName = "Sinclair ZX Spectrum", Type = "ROM", Path = ".", Ordinal = 1 }, + new PathEntry { System = "ZXSpectrum", SystemDisplayName = "Sinclair ZX Spectrum", Type = "Savestates", Path = Path.Combine(".", "State"), Ordinal = 2 }, + 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 = "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 }, new PathEntry { System = "PSX", SystemDisplayName = "Playstation", Type = "Save RAM", Path = Path.Combine(".", "SaveRAM"), Ordinal = 3 }, diff --git a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj index 0fa83153e4..7341cd207c 100644 --- a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj +++ b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj @@ -503,6 +503,24 @@ TI83PaletteConfig.cs + + Form + + + ZXSpectrumNonSyncSettings.cs + + + Form + + + ZXSpectrumCoreEmulationSettings.cs + + + Form + + + ZXSpectrumJoystickSettings.cs + Form @@ -1206,6 +1224,7 @@ + UserControl @@ -1403,6 +1422,15 @@ TI83PaletteConfig.cs + + ZXSpectrumNonSyncSettings.cs + + + ZXSpectrumCoreEmulationSettings.cs + + + ZXSpectrumJoystickSettings.cs + CoreFeatureAnalysis.cs @@ -1794,6 +1822,7 @@ + @@ -2118,6 +2147,7 @@ + diff --git a/BizHawk.Client.EmuHawk/FileLoader.cs b/BizHawk.Client.EmuHawk/FileLoader.cs index 0009646fb4..db3c00d76d 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" + ".EXE", ".PRG", ".D64", "*G64", ".CRT", ".TAP", ".32X", ".MDS", ".TZX" }; } diff --git a/BizHawk.Client.EmuHawk/MainForm.Designer.cs b/BizHawk.Client.EmuHawk/MainForm.Designer.cs index 5f47cb66d0..0077501f4c 100644 --- a/BizHawk.Client.EmuHawk/MainForm.Designer.cs +++ b/BizHawk.Client.EmuHawk/MainForm.Designer.cs @@ -73,7 +73,7 @@ this.LoadCurrentSlotMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SaveRAMSubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.FlushSaveRAMMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.toolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator(); + this.toolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator(); this.MovieSubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ReadonlyMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator15 = new System.Windows.Forms.ToolStripSeparator(); @@ -192,14 +192,13 @@ this.GbaCoreSubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.VbaNextCoreMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.MgbaCoreMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.Atari7800HawkCoreMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SGBCoreSubmenu = new System.Windows.Forms.ToolStripMenuItem(); - this.SgbBsnesMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.SgbBsnesMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SgbSameBoyMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.GBCoreSubmenu = new System.Windows.Forms.ToolStripMenuItem(); - this.GBGambatteMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.GBGBHawkMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.GBInSGBMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.GBCoreSubmenu = new System.Windows.Forms.ToolStripMenuItem(); + this.GBGambatteMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.GBGBHawkMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.GBInSGBMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripMenuItem16 = new System.Windows.Forms.ToolStripSeparator(); this.allowGameDBCoreOverridesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator8 = new System.Windows.Forms.ToolStripSeparator(); @@ -239,9 +238,8 @@ this.coreToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.quickNESToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.nesHawkToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.toolStripSeparator34 = new System.Windows.Forms.ToolStripSeparator(); - this.toolStripSeparator35 = new System.Windows.Forms.ToolStripSeparator(); - this.NESPPUViewerMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator34 = new System.Windows.Forms.ToolStripSeparator(); + this.NESPPUViewerMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.NESNametableViewerMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.NESGameGenieCodesMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.musicRipperToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -280,6 +278,12 @@ this.SMSdisplayNtscToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SMSdisplayPalToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SMSdisplayAutoToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.SMSControllerToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.SMSControllerStandardToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.SMSControllerPaddleToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.SMSControllerLightPhaserToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.SMSControllerSportsPadToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.SMSControllerKeyboardToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SMStoolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator(); this.SMSenableBIOSToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.SMSEnableFMChipMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -304,14 +308,14 @@ this.AtariSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.A7800SubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.A7800ControllerSettingsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.A7800FilterSettingsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.GBSubMenu = new System.Windows.Forms.ToolStripMenuItem(); + this.A7800FilterSettingsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.GBSubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.GBcoreSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.LoadGBInSGBMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator28 = new System.Windows.Forms.ToolStripSeparator(); this.GBGPUViewerMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.GBPrinterViewerMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.GBGameGenieMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.GBGameGenieMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.GBPrinterViewerMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.GBASubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.GBACoreSelectionSubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.GBAmGBAMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -333,6 +337,7 @@ this.SnesOptionsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.ColecoSubMenu = new System.Windows.Forms.ToolStripMenuItem(); this.ColecoControllerSettingsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.toolStripSeparator35 = new System.Windows.Forms.ToolStripSeparator(); this.ColecoSkipBiosMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.ColecoUseSGMMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.N64SubMenu = new System.Windows.Forms.ToolStripMenuItem(); @@ -376,6 +381,11 @@ this.ForumsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.FeaturesMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.AboutMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.zXSpectrumToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.ZXSpectrumCoreEmulationSettingsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.ZXSpectrumControllerConfigurationMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.ZXSpectrumNonSyncSettingsMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.Atari7800HawkCoreMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.MainStatusBar = new StatusStripEx(); this.DumpStatusButton = new System.Windows.Forms.ToolStripDropDownButton(); this.EmuStatus = new System.Windows.Forms.ToolStripStatusLabel(); @@ -447,13 +457,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.SMSControllerToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.SMSControllerStandardToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.SMSControllerPaddleToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.SMSControllerLightPhaserToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.SMSControllerSportsPadToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.SMSControllerKeyboardToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.MainformMenu.SuspendLayout(); + this.MainformMenu.SuspendLayout(); this.MainStatusBar.SuspendLayout(); this.MainFormContextMenu.SuspendLayout(); this.SuspendLayout(); @@ -490,7 +494,8 @@ this.pCFXToolStripMenuItem, this.virtualBoyToolStripMenuItem, this.neoGeoPocketToolStripMenuItem, - this.HelpSubMenu}); + this.HelpSubMenu, + this.zXSpectrumToolStripMenuItem}); this.MainformMenu.LayoutStyle = System.Windows.Forms.ToolStripLayoutStyle.Flow; this.MainformMenu.Location = new System.Drawing.Point(0, 0); this.MainformMenu.Name = "MainformMenu"; @@ -912,26 +917,26 @@ this.LoadCurrentSlotMenuItem.Size = new System.Drawing.Size(178, 22); this.LoadCurrentSlotMenuItem.Text = "Load Current Slot"; this.LoadCurrentSlotMenuItem.Click += new System.EventHandler(this.LoadCurrentSlotMenuItem_Click); - - // - // SaveRAMSubMenu - // - this.SaveRAMSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.FlushSaveRAMMenuItem}); - this.SaveRAMSubMenu.Name = "SaveRAMSubMenu"; - this.SaveRAMSubMenu.Size = new System.Drawing.Size(159, 22); - this.SaveRAMSubMenu.Text = "Save &RAM"; - this.SaveRAMSubMenu.DropDownOpened += new System.EventHandler(this.SaveRAMSubMenu_DropDownOpened); - // - // FlushSaveRAMMenuItem - // - this.FlushSaveRAMMenuItem.Name = "FlushSaveRAMMenuItem"; - this.FlushSaveRAMMenuItem.Size = new System.Drawing.Size(156, 22); - this.FlushSaveRAMMenuItem.Text = "&Flush Save Ram"; - this.FlushSaveRAMMenuItem.Click += new System.EventHandler(this.FlushSaveRAMMenuItem_Click); - // toolStripMenuItem2 - // - this.toolStripMenuItem2.Name = "toolStripMenuItem2"; + // + // SaveRAMSubMenu + // + this.SaveRAMSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.FlushSaveRAMMenuItem}); + this.SaveRAMSubMenu.Name = "SaveRAMSubMenu"; + this.SaveRAMSubMenu.Size = new System.Drawing.Size(159, 22); + this.SaveRAMSubMenu.Text = "Save &RAM"; + this.SaveRAMSubMenu.DropDownOpened += new System.EventHandler(this.SaveRAMSubMenu_DropDownOpened); + // + // FlushSaveRAMMenuItem + // + this.FlushSaveRAMMenuItem.Name = "FlushSaveRAMMenuItem"; + this.FlushSaveRAMMenuItem.Size = new System.Drawing.Size(156, 22); + this.FlushSaveRAMMenuItem.Text = "&Flush Save Ram"; + this.FlushSaveRAMMenuItem.Click += new System.EventHandler(this.FlushSaveRAMMenuItem_Click); + // + // toolStripMenuItem2 + // + this.toolStripMenuItem2.Name = "toolStripMenuItem2"; this.toolStripMenuItem2.Size = new System.Drawing.Size(156, 6); // // MovieSubMenu @@ -1126,7 +1131,7 @@ // this.RecordAVMenuItem.Image = global::BizHawk.Client.EmuHawk.Properties.Resources.RecordHS; this.RecordAVMenuItem.Name = "RecordAVMenuItem"; - this.RecordAVMenuItem.Size = new System.Drawing.Size(223, 22); + this.RecordAVMenuItem.Size = new System.Drawing.Size(225, 22); this.RecordAVMenuItem.Text = "&Record AVI/WAV"; this.RecordAVMenuItem.Click += new System.EventHandler(this.RecordAVMenuItem_Click); // @@ -1134,7 +1139,7 @@ // this.ConfigAndRecordAVMenuItem.Image = global::BizHawk.Client.EmuHawk.Properties.Resources.AVI; this.ConfigAndRecordAVMenuItem.Name = "ConfigAndRecordAVMenuItem"; - this.ConfigAndRecordAVMenuItem.Size = new System.Drawing.Size(223, 22); + this.ConfigAndRecordAVMenuItem.Size = new System.Drawing.Size(225, 22); this.ConfigAndRecordAVMenuItem.Text = "Config and Record AVI/WAV"; this.ConfigAndRecordAVMenuItem.Click += new System.EventHandler(this.ConfigAndRecordAVMenuItem_Click); // @@ -1142,26 +1147,26 @@ // this.StopAVIMenuItem.Image = global::BizHawk.Client.EmuHawk.Properties.Resources.Stop; this.StopAVIMenuItem.Name = "StopAVIMenuItem"; - this.StopAVIMenuItem.Size = new System.Drawing.Size(223, 22); + this.StopAVIMenuItem.Size = new System.Drawing.Size(225, 22); this.StopAVIMenuItem.Text = "&Stop AVI/WAV"; this.StopAVIMenuItem.Click += new System.EventHandler(this.StopAVMenuItem_Click); // // toolStripSeparator19 // this.toolStripSeparator19.Name = "toolStripSeparator19"; - this.toolStripSeparator19.Size = new System.Drawing.Size(220, 6); + this.toolStripSeparator19.Size = new System.Drawing.Size(222, 6); // // CaptureOSDMenuItem // this.CaptureOSDMenuItem.Name = "CaptureOSDMenuItem"; - this.CaptureOSDMenuItem.Size = new System.Drawing.Size(223, 22); + this.CaptureOSDMenuItem.Size = new System.Drawing.Size(225, 22); this.CaptureOSDMenuItem.Text = "Capture OSD"; this.CaptureOSDMenuItem.Click += new System.EventHandler(this.CaptureOSDMenuItem_Click); // // SynclessRecordingMenuItem // this.SynclessRecordingMenuItem.Name = "SynclessRecordingMenuItem"; - this.SynclessRecordingMenuItem.Size = new System.Drawing.Size(223, 22); + this.SynclessRecordingMenuItem.Size = new System.Drawing.Size(225, 22); this.SynclessRecordingMenuItem.Text = "S&yncless Recording Tools"; this.SynclessRecordingMenuItem.Click += new System.EventHandler(this.SynclessRecordingMenuItem_Click); // @@ -1816,8 +1821,8 @@ this.CoreSNESSubMenu, this.GbaCoreSubMenu, this.SGBCoreSubmenu, - this.GBCoreSubmenu, - this.GBInSGBMenuItem, + this.GBCoreSubmenu, + this.GBInSGBMenuItem, this.toolStripMenuItem16, this.allowGameDBCoreOverridesToolStripMenuItem, this.toolStripSeparator8, @@ -1900,12 +1905,6 @@ this.MgbaCoreMenuItem.Text = "mGBA"; this.MgbaCoreMenuItem.Click += new System.EventHandler(this.GbaCorePick_Click); // - // Atari7800HawkCoreMenuItem - // - this.Atari7800HawkCoreMenuItem.Name = "Atari7800HawkCoreMenuItem"; - this.Atari7800HawkCoreMenuItem.Size = new System.Drawing.Size(153, 22); - this.Atari7800HawkCoreMenuItem.Text = "Atari7800Hawk"; - // // SGBCoreSubmenu // this.SGBCoreSubmenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { @@ -1915,48 +1914,48 @@ this.SGBCoreSubmenu.Size = new System.Drawing.Size(239, 22); this.SGBCoreSubmenu.Text = "SGB"; this.SGBCoreSubmenu.DropDownOpened += new System.EventHandler(this.SGBCoreSubmenu_DropDownOpened); - // - // GBCoreSubmenu - // - this.GBCoreSubmenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.GBGambatteMenuItem, - this.GBGBHawkMenuItem}); - this.GBCoreSubmenu.Name = "GBCoreSubmenu"; - this.GBCoreSubmenu.Size = new System.Drawing.Size(239, 22); - this.GBCoreSubmenu.Text = "GB"; - this.GBCoreSubmenu.DropDownOpened += new System.EventHandler(this.GBCoreSubmenu_DropDownOpened); - // - // SgbBsnesMenuItem - // - this.SgbBsnesMenuItem.Name = "SgbBsnesMenuItem"; - this.SgbBsnesMenuItem.Size = new System.Drawing.Size(152, 22); + // + // SgbBsnesMenuItem + // + this.SgbBsnesMenuItem.Name = "SgbBsnesMenuItem"; + this.SgbBsnesMenuItem.Size = new System.Drawing.Size(123, 22); this.SgbBsnesMenuItem.Text = "BSNES"; this.SgbBsnesMenuItem.Click += new System.EventHandler(this.SgbCorePick_Click); // // SgbSameBoyMenuItem // this.SgbSameBoyMenuItem.Name = "SgbSameBoyMenuItem"; - this.SgbSameBoyMenuItem.Size = new System.Drawing.Size(152, 22); + this.SgbSameBoyMenuItem.Size = new System.Drawing.Size(123, 22); this.SgbSameBoyMenuItem.Text = "SameBoy"; this.SgbSameBoyMenuItem.Click += new System.EventHandler(this.SgbCorePick_Click); - // - // GBGambatteMenuItem - // - this.GBGambatteMenuItem.Name = "GBGambatteMenuItem"; - this.GBGambatteMenuItem.Size = new System.Drawing.Size(152, 22); - this.GBGambatteMenuItem.Text = "Gambatte"; - this.GBGambatteMenuItem.Click += new System.EventHandler(this.GBCorePick_Click); - // - // GBGBHawkMenuItem - // - this.GBGBHawkMenuItem.Name = "GBGBHawkMenuItem"; - this.GBGBHawkMenuItem.Size = new System.Drawing.Size(152, 22); - this.GBGBHawkMenuItem.Text = "GBHawk"; - this.GBGBHawkMenuItem.Click += new System.EventHandler(this.GBCorePick_Click); - // - // GBInSGBMenuItem - // - this.GBInSGBMenuItem.Name = "GBInSGBMenuItem"; + // + // GBCoreSubmenu + // + this.GBCoreSubmenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.GBGambatteMenuItem, + this.GBGBHawkMenuItem}); + this.GBCoreSubmenu.Name = "GBCoreSubmenu"; + this.GBCoreSubmenu.Size = new System.Drawing.Size(239, 22); + this.GBCoreSubmenu.Text = "GB"; + this.GBCoreSubmenu.DropDownOpened += new System.EventHandler(this.GBCoreSubmenu_DropDownOpened); + // + // GBGambatteMenuItem + // + this.GBGambatteMenuItem.Name = "GBGambatteMenuItem"; + this.GBGambatteMenuItem.Size = new System.Drawing.Size(126, 22); + this.GBGambatteMenuItem.Text = "Gambatte"; + this.GBGambatteMenuItem.Click += new System.EventHandler(this.GBCorePick_Click); + // + // GBGBHawkMenuItem + // + this.GBGBHawkMenuItem.Name = "GBGBHawkMenuItem"; + this.GBGBHawkMenuItem.Size = new System.Drawing.Size(126, 22); + this.GBGBHawkMenuItem.Text = "GBHawk"; + this.GBGBHawkMenuItem.Click += new System.EventHandler(this.GBCorePick_Click); + // + // GBInSGBMenuItem + // + this.GBInSGBMenuItem.Name = "GBInSGBMenuItem"; this.GBInSGBMenuItem.Size = new System.Drawing.Size(239, 22); this.GBInSGBMenuItem.Text = "GB in SGB"; this.GBInSGBMenuItem.Click += new System.EventHandler(this.GbInSgbMenuItem_Click); @@ -2053,7 +2052,7 @@ this.batchRunnerToolStripMenuItem, this.ExperimentalToolsSubMenu}); this.ToolsSubMenu.Name = "ToolsSubMenu"; - this.ToolsSubMenu.Size = new System.Drawing.Size(47, 19); + this.ToolsSubMenu.Size = new System.Drawing.Size(48, 19); this.ToolsSubMenu.Text = "&Tools"; this.ToolsSubMenu.DropDownOpened += new System.EventHandler(this.ToolsSubMenu_DropDownOpened); // @@ -2442,7 +2441,7 @@ // this.PceControllerSettingsMenuItem.Image = global::BizHawk.Client.EmuHawk.Properties.Resources.GameController; this.PceControllerSettingsMenuItem.Name = "PceControllerSettingsMenuItem"; - this.PceControllerSettingsMenuItem.Size = new System.Drawing.Size(258, 22); + this.PceControllerSettingsMenuItem.Size = new System.Drawing.Size(259, 22); this.PceControllerSettingsMenuItem.Text = "Controller Settings"; this.PceControllerSettingsMenuItem.Click += new System.EventHandler(this.PceControllerSettingsMenuItem_Click); // @@ -2450,59 +2449,59 @@ // this.PCEGraphicsSettingsMenuItem.Image = global::BizHawk.Client.EmuHawk.Properties.Resources.tvIcon; this.PCEGraphicsSettingsMenuItem.Name = "PCEGraphicsSettingsMenuItem"; - this.PCEGraphicsSettingsMenuItem.Size = new System.Drawing.Size(258, 22); + this.PCEGraphicsSettingsMenuItem.Size = new System.Drawing.Size(259, 22); this.PCEGraphicsSettingsMenuItem.Text = "Graphics Settings"; this.PCEGraphicsSettingsMenuItem.Click += new System.EventHandler(this.PceGraphicsSettingsMenuItem_Click); // // toolStripSeparator32 // this.toolStripSeparator32.Name = "toolStripSeparator32"; - this.toolStripSeparator32.Size = new System.Drawing.Size(255, 6); + this.toolStripSeparator32.Size = new System.Drawing.Size(256, 6); // // PCEBGViewerMenuItem // this.PCEBGViewerMenuItem.Name = "PCEBGViewerMenuItem"; - this.PCEBGViewerMenuItem.Size = new System.Drawing.Size(258, 22); + this.PCEBGViewerMenuItem.Size = new System.Drawing.Size(259, 22); this.PCEBGViewerMenuItem.Text = "&BG Viewer"; this.PCEBGViewerMenuItem.Click += new System.EventHandler(this.PceBgViewerMenuItem_Click); // // PCEtileViewerToolStripMenuItem // this.PCEtileViewerToolStripMenuItem.Name = "PCEtileViewerToolStripMenuItem"; - this.PCEtileViewerToolStripMenuItem.Size = new System.Drawing.Size(258, 22); + this.PCEtileViewerToolStripMenuItem.Size = new System.Drawing.Size(259, 22); this.PCEtileViewerToolStripMenuItem.Text = "&Tile Viewer"; this.PCEtileViewerToolStripMenuItem.Click += new System.EventHandler(this.PceTileViewerMenuItem_Click); // // PceSoundDebuggerToolStripMenuItem // this.PceSoundDebuggerToolStripMenuItem.Name = "PceSoundDebuggerToolStripMenuItem"; - this.PceSoundDebuggerToolStripMenuItem.Size = new System.Drawing.Size(258, 22); + this.PceSoundDebuggerToolStripMenuItem.Size = new System.Drawing.Size(259, 22); this.PceSoundDebuggerToolStripMenuItem.Text = "&Sound Debugger"; this.PceSoundDebuggerToolStripMenuItem.Click += new System.EventHandler(this.PceSoundDebuggerMenuItem_Click); // // toolStripSeparator25 // this.toolStripSeparator25.Name = "toolStripSeparator25"; - this.toolStripSeparator25.Size = new System.Drawing.Size(255, 6); + this.toolStripSeparator25.Size = new System.Drawing.Size(256, 6); // // PCEAlwaysPerformSpriteLimitMenuItem // this.PCEAlwaysPerformSpriteLimitMenuItem.Name = "PCEAlwaysPerformSpriteLimitMenuItem"; - this.PCEAlwaysPerformSpriteLimitMenuItem.Size = new System.Drawing.Size(258, 22); + this.PCEAlwaysPerformSpriteLimitMenuItem.Size = new System.Drawing.Size(259, 22); this.PCEAlwaysPerformSpriteLimitMenuItem.Text = "Always Perform Sprite Limit"; this.PCEAlwaysPerformSpriteLimitMenuItem.Click += new System.EventHandler(this.PCEAlwaysPerformSpriteLimitMenuItem_Click); // // PCEAlwaysEqualizeVolumesMenuItem // this.PCEAlwaysEqualizeVolumesMenuItem.Name = "PCEAlwaysEqualizeVolumesMenuItem"; - this.PCEAlwaysEqualizeVolumesMenuItem.Size = new System.Drawing.Size(258, 22); + this.PCEAlwaysEqualizeVolumesMenuItem.Size = new System.Drawing.Size(259, 22); this.PCEAlwaysEqualizeVolumesMenuItem.Text = "Always Equalize Volumes (PCE-CD)"; this.PCEAlwaysEqualizeVolumesMenuItem.Click += new System.EventHandler(this.PCEAlwaysEqualizeVolumesMenuItem_Click); // // PCEArcadeCardRewindEnableMenuItem // this.PCEArcadeCardRewindEnableMenuItem.Name = "PCEArcadeCardRewindEnableMenuItem"; - this.PCEArcadeCardRewindEnableMenuItem.Size = new System.Drawing.Size(258, 22); + this.PCEArcadeCardRewindEnableMenuItem.Size = new System.Drawing.Size(259, 22); this.PCEArcadeCardRewindEnableMenuItem.Text = "Arcade Card Rewind-Enable Hack"; this.PCEArcadeCardRewindEnableMenuItem.Click += new System.EventHandler(this.PCEArcadeCardRewindEnableMenuItem_Click); // @@ -2512,7 +2511,7 @@ this.SMSregionToolStripMenuItem, this.SMSdisplayToolStripMenuItem, this.SMSControllerToolStripMenuItem, - this.SMStoolStripMenuItem2, + this.SMStoolStripMenuItem2, this.SMSenableBIOSToolStripMenuItem, this.SMSEnableFMChipMenuItem, this.SMSOverclockMenuItem, @@ -2539,7 +2538,7 @@ this.SMSregionKoreaToolStripMenuItem, this.SMSregionAutoToolStripMenuItem}); this.SMSregionToolStripMenuItem.Name = "SMSregionToolStripMenuItem"; - this.SMSregionToolStripMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSregionToolStripMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSregionToolStripMenuItem.Text = "Region"; // // SMSregionExportToolStripMenuItem @@ -2577,7 +2576,7 @@ this.SMSdisplayPalToolStripMenuItem, this.SMSdisplayAutoToolStripMenuItem}); this.SMSdisplayToolStripMenuItem.Name = "SMSdisplayToolStripMenuItem"; - this.SMSdisplayToolStripMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSdisplayToolStripMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSdisplayToolStripMenuItem.Text = "Display Type"; // // SMSdisplayNtscToolStripMenuItem @@ -2587,51 +2586,9 @@ this.SMSdisplayNtscToolStripMenuItem.Text = "NTSC"; this.SMSdisplayNtscToolStripMenuItem.Click += new System.EventHandler(this.SMS_DisplayNTSC_Click); // - // SMSControllerToolStripMenuItem + // SMSdisplayPalToolStripMenuItem // - this.SMSControllerToolStripMenuItem.Name = "SMSControllerToolStripMenuItem"; - this.SMSControllerToolStripMenuItem.Text = "&Controller Type"; - this.SMSControllerToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.SMSControllerStandardToolStripMenuItem, - this.SMSControllerPaddleToolStripMenuItem, - this.SMSControllerLightPhaserToolStripMenuItem, - this.SMSControllerSportsPadToolStripMenuItem, - this.SMSControllerKeyboardToolStripMenuItem}); - // - // SMSControllerStandardToolStripMenuItem - // - this.SMSControllerStandardToolStripMenuItem.Name = "SMSControllerStandardToolStripMenuItem"; - this.SMSControllerStandardToolStripMenuItem.Text = "Standard"; - this.SMSControllerStandardToolStripMenuItem.Click += new System.EventHandler(this.SMSControllerStandardToolStripMenuItem_Click); - // - // SMSControllerPaddleToolStripMenuItem - // - this.SMSControllerPaddleToolStripMenuItem.Name = "SMSControllerPaddleToolStripMenuItem"; - this.SMSControllerPaddleToolStripMenuItem.Text = "Paddle"; - this.SMSControllerPaddleToolStripMenuItem.Click += new System.EventHandler(this.SMSControllerPaddleToolStripMenuItem_Click); - // - // SMSControllerLightPhaserToolStripMenuItem - // - this.SMSControllerLightPhaserToolStripMenuItem.Name = "SMSControllerLightPhaserToolStripMenuItem"; - this.SMSControllerLightPhaserToolStripMenuItem.Text = "Light Phaser"; - this.SMSControllerLightPhaserToolStripMenuItem.Click += new System.EventHandler(this.SMSControllerLightPhaserToolStripMenuItem_Click); - // - // SMSControllerSportsPadToolStripMenuItem - // - this.SMSControllerSportsPadToolStripMenuItem.Name = "SMSControllerSportsPadToolStripMenuItem"; - this.SMSControllerSportsPadToolStripMenuItem.Text = "Sports Pad"; - this.SMSControllerSportsPadToolStripMenuItem.Click += new System.EventHandler(this.SMSControllerSportsPadToolStripMenuItem_Click); - // - // SMSControllerKeyboardToolStripMenuItem - // - this.SMSControllerKeyboardToolStripMenuItem.Name = "SMSControllerKeyboardToolStripMenuItem"; - this.SMSControllerKeyboardToolStripMenuItem.Text = "Keyboard"; - this.SMSControllerKeyboardToolStripMenuItem.Click += new System.EventHandler(this.SMSControllerKeyboardToolStripMenuItem_Click); - - // - // SMSdisplayPalToolStripMenuItem - // - this.SMSdisplayPalToolStripMenuItem.Name = "SMSdisplayPalToolStripMenuItem"; + this.SMSdisplayPalToolStripMenuItem.Name = "SMSdisplayPalToolStripMenuItem"; this.SMSdisplayPalToolStripMenuItem.Size = new System.Drawing.Size(104, 22); this.SMSdisplayPalToolStripMenuItem.Text = "PAL"; this.SMSdisplayPalToolStripMenuItem.Click += new System.EventHandler(this.SMS_DisplayPAL_Click); @@ -2643,97 +2600,144 @@ this.SMSdisplayAutoToolStripMenuItem.Text = "Auto"; this.SMSdisplayAutoToolStripMenuItem.Click += new System.EventHandler(this.SMS_DisplayAuto_Click); // + // SMSControllerToolStripMenuItem + // + this.SMSControllerToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.SMSControllerStandardToolStripMenuItem, + this.SMSControllerPaddleToolStripMenuItem, + this.SMSControllerLightPhaserToolStripMenuItem, + this.SMSControllerSportsPadToolStripMenuItem, + this.SMSControllerKeyboardToolStripMenuItem}); + this.SMSControllerToolStripMenuItem.Name = "SMSControllerToolStripMenuItem"; + this.SMSControllerToolStripMenuItem.Size = new System.Drawing.Size(278, 22); + this.SMSControllerToolStripMenuItem.Text = "&Controller Type"; + // + // SMSControllerStandardToolStripMenuItem + // + this.SMSControllerStandardToolStripMenuItem.Name = "SMSControllerStandardToolStripMenuItem"; + this.SMSControllerStandardToolStripMenuItem.Size = new System.Drawing.Size(139, 22); + this.SMSControllerStandardToolStripMenuItem.Text = "Standard"; + this.SMSControllerStandardToolStripMenuItem.Click += new System.EventHandler(this.SMSControllerStandardToolStripMenuItem_Click); + // + // SMSControllerPaddleToolStripMenuItem + // + this.SMSControllerPaddleToolStripMenuItem.Name = "SMSControllerPaddleToolStripMenuItem"; + this.SMSControllerPaddleToolStripMenuItem.Size = new System.Drawing.Size(139, 22); + this.SMSControllerPaddleToolStripMenuItem.Text = "Paddle"; + this.SMSControllerPaddleToolStripMenuItem.Click += new System.EventHandler(this.SMSControllerPaddleToolStripMenuItem_Click); + // + // SMSControllerLightPhaserToolStripMenuItem + // + this.SMSControllerLightPhaserToolStripMenuItem.Name = "SMSControllerLightPhaserToolStripMenuItem"; + this.SMSControllerLightPhaserToolStripMenuItem.Size = new System.Drawing.Size(139, 22); + this.SMSControllerLightPhaserToolStripMenuItem.Text = "Light Phaser"; + this.SMSControllerLightPhaserToolStripMenuItem.Click += new System.EventHandler(this.SMSControllerLightPhaserToolStripMenuItem_Click); + // + // SMSControllerSportsPadToolStripMenuItem + // + this.SMSControllerSportsPadToolStripMenuItem.Name = "SMSControllerSportsPadToolStripMenuItem"; + this.SMSControllerSportsPadToolStripMenuItem.Size = new System.Drawing.Size(139, 22); + this.SMSControllerSportsPadToolStripMenuItem.Text = "Sports Pad"; + this.SMSControllerSportsPadToolStripMenuItem.Click += new System.EventHandler(this.SMSControllerSportsPadToolStripMenuItem_Click); + // + // SMSControllerKeyboardToolStripMenuItem + // + this.SMSControllerKeyboardToolStripMenuItem.Name = "SMSControllerKeyboardToolStripMenuItem"; + this.SMSControllerKeyboardToolStripMenuItem.Size = new System.Drawing.Size(139, 22); + this.SMSControllerKeyboardToolStripMenuItem.Text = "Keyboard"; + this.SMSControllerKeyboardToolStripMenuItem.Click += new System.EventHandler(this.SMSControllerKeyboardToolStripMenuItem_Click); + // // SMStoolStripMenuItem2 // this.SMStoolStripMenuItem2.Name = "SMStoolStripMenuItem2"; - this.SMStoolStripMenuItem2.Size = new System.Drawing.Size(274, 6); + this.SMStoolStripMenuItem2.Size = new System.Drawing.Size(275, 6); // // SMSenableBIOSToolStripMenuItem // this.SMSenableBIOSToolStripMenuItem.Name = "SMSenableBIOSToolStripMenuItem"; - this.SMSenableBIOSToolStripMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSenableBIOSToolStripMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSenableBIOSToolStripMenuItem.Text = "Enable BIOS (Must be Enabled for TAS)"; this.SMSenableBIOSToolStripMenuItem.Click += new System.EventHandler(this.SmsBiosMenuItem_Click); // // SMSEnableFMChipMenuItem // this.SMSEnableFMChipMenuItem.Name = "SMSEnableFMChipMenuItem"; - this.SMSEnableFMChipMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSEnableFMChipMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSEnableFMChipMenuItem.Text = "&Enable FM Chip"; this.SMSEnableFMChipMenuItem.Click += new System.EventHandler(this.SmsEnableFmChipMenuItem_Click); // // SMSOverclockMenuItem // this.SMSOverclockMenuItem.Name = "SMSOverclockMenuItem"; - this.SMSOverclockMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSOverclockMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSOverclockMenuItem.Text = "&Overclock when Known Safe"; this.SMSOverclockMenuItem.Click += new System.EventHandler(this.SMSOverclockMenuItem_Click); // // SMSForceStereoMenuItem // this.SMSForceStereoMenuItem.Name = "SMSForceStereoMenuItem"; - this.SMSForceStereoMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSForceStereoMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSForceStereoMenuItem.Text = "&Force Stereo Separation"; this.SMSForceStereoMenuItem.Click += new System.EventHandler(this.SMSForceStereoMenuItem_Click); // // SMSSpriteLimitMenuItem // this.SMSSpriteLimitMenuItem.Name = "SMSSpriteLimitMenuItem"; - this.SMSSpriteLimitMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSSpriteLimitMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSSpriteLimitMenuItem.Text = "Sprite &Limit"; this.SMSSpriteLimitMenuItem.Click += new System.EventHandler(this.SMSSpriteLimitMenuItem_Click); // // SMSDisplayOverscanMenuItem // this.SMSDisplayOverscanMenuItem.Name = "SMSDisplayOverscanMenuItem"; - this.SMSDisplayOverscanMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSDisplayOverscanMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSDisplayOverscanMenuItem.Text = "Display Overscan"; this.SMSDisplayOverscanMenuItem.Click += new System.EventHandler(this.SMSDisplayOverscanMenuItem_Click); // // SMSFix3DGameDisplayToolStripMenuItem // this.SMSFix3DGameDisplayToolStripMenuItem.Name = "SMSFix3DGameDisplayToolStripMenuItem"; - this.SMSFix3DGameDisplayToolStripMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSFix3DGameDisplayToolStripMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSFix3DGameDisplayToolStripMenuItem.Text = "Fix 3D Game Display"; this.SMSFix3DGameDisplayToolStripMenuItem.Click += new System.EventHandler(this.SMSFix3DDisplayMenuItem_Click); // // ShowClippedRegionsMenuItem // this.ShowClippedRegionsMenuItem.Name = "ShowClippedRegionsMenuItem"; - this.ShowClippedRegionsMenuItem.Size = new System.Drawing.Size(277, 22); + this.ShowClippedRegionsMenuItem.Size = new System.Drawing.Size(278, 22); this.ShowClippedRegionsMenuItem.Text = "&Show Clipped Regions"; this.ShowClippedRegionsMenuItem.Click += new System.EventHandler(this.ShowClippedRegionsMenuItem_Click); // // HighlightActiveDisplayRegionMenuItem // this.HighlightActiveDisplayRegionMenuItem.Name = "HighlightActiveDisplayRegionMenuItem"; - this.HighlightActiveDisplayRegionMenuItem.Size = new System.Drawing.Size(277, 22); + this.HighlightActiveDisplayRegionMenuItem.Size = new System.Drawing.Size(278, 22); this.HighlightActiveDisplayRegionMenuItem.Text = "&Highlight Active Display Region"; this.HighlightActiveDisplayRegionMenuItem.Click += new System.EventHandler(this.HighlightActiveDisplayRegionMenuItem_Click); // // SMSGraphicsSettingsMenuItem // this.SMSGraphicsSettingsMenuItem.Name = "SMSGraphicsSettingsMenuItem"; - this.SMSGraphicsSettingsMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSGraphicsSettingsMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSGraphicsSettingsMenuItem.Text = "&Graphics Settings..."; this.SMSGraphicsSettingsMenuItem.Click += new System.EventHandler(this.SMSGraphicsSettingsMenuItem_Click); // // toolStripSeparator24 // this.toolStripSeparator24.Name = "toolStripSeparator24"; - this.toolStripSeparator24.Size = new System.Drawing.Size(274, 6); + this.toolStripSeparator24.Size = new System.Drawing.Size(275, 6); // // SMSVDPViewerToolStripMenuItem // this.SMSVDPViewerToolStripMenuItem.Name = "SMSVDPViewerToolStripMenuItem"; - this.SMSVDPViewerToolStripMenuItem.Size = new System.Drawing.Size(277, 22); + this.SMSVDPViewerToolStripMenuItem.Size = new System.Drawing.Size(278, 22); this.SMSVDPViewerToolStripMenuItem.Text = "&VDP Viewer"; this.SMSVDPViewerToolStripMenuItem.Click += new System.EventHandler(this.SmsVdpViewerMenuItem_Click); // // GGGameGenieMenuItem // this.GGGameGenieMenuItem.Name = "GGGameGenieMenuItem"; - this.GGGameGenieMenuItem.Size = new System.Drawing.Size(277, 22); + this.GGGameGenieMenuItem.Size = new System.Drawing.Size(278, 22); this.GGGameGenieMenuItem.Text = "&Game Genie Encoder/Decoder"; this.GGGameGenieMenuItem.Click += new System.EventHandler(this.GGGameGenieMenuItem_Click); // @@ -2805,7 +2809,7 @@ // this.A7800SubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.A7800ControllerSettingsMenuItem, - this.A7800FilterSettingsMenuItem}); + this.A7800FilterSettingsMenuItem}); this.A7800SubMenu.Name = "A7800SubMenu"; this.A7800SubMenu.Size = new System.Drawing.Size(51, 19); this.A7800SubMenu.Text = "&A7800"; @@ -2814,26 +2818,26 @@ // A7800ControllerSettingsMenuItem // this.A7800ControllerSettingsMenuItem.Name = "A7800ControllerSettingsMenuItem"; - this.A7800ControllerSettingsMenuItem.Size = new System.Drawing.Size(125, 22); + this.A7800ControllerSettingsMenuItem.Size = new System.Drawing.Size(172, 22); this.A7800ControllerSettingsMenuItem.Text = "Controller Settings"; this.A7800ControllerSettingsMenuItem.Click += new System.EventHandler(this.A7800ControllerSettingsToolStripMenuItem_Click); - // - // A7800FilterSettingsMenuItem - // - this.A7800FilterSettingsMenuItem.Name = "A7800FilterSettingsMenuItem"; - this.A7800FilterSettingsMenuItem.Size = new System.Drawing.Size(125, 22); - this.A7800FilterSettingsMenuItem.Text = "Filter Settings"; - this.A7800FilterSettingsMenuItem.Click += new System.EventHandler(this.A7800FilterSettingsToolStripMenuItem_Click); - // - // GBSubMenu - // - this.GBSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + // + // A7800FilterSettingsMenuItem + // + this.A7800FilterSettingsMenuItem.Name = "A7800FilterSettingsMenuItem"; + this.A7800FilterSettingsMenuItem.Size = new System.Drawing.Size(172, 22); + this.A7800FilterSettingsMenuItem.Text = "Filter Settings"; + this.A7800FilterSettingsMenuItem.Click += new System.EventHandler(this.A7800FilterSettingsToolStripMenuItem_Click); + // + // GBSubMenu + // + this.GBSubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.GBcoreSettingsToolStripMenuItem, this.LoadGBInSGBMenuItem, this.toolStripSeparator28, this.GBGPUViewerMenuItem, this.GBGameGenieMenuItem, - this.GBPrinterViewerMenuItem}); + this.GBPrinterViewerMenuItem}); this.GBSubMenu.Name = "GBSubMenu"; this.GBSubMenu.Size = new System.Drawing.Size(34, 19); this.GBSubMenu.Text = "&GB"; @@ -2871,17 +2875,17 @@ this.GBGameGenieMenuItem.Size = new System.Drawing.Size(233, 22); this.GBGameGenieMenuItem.Text = "&Game Genie Encoder/Decoder"; this.GBGameGenieMenuItem.Click += new System.EventHandler(this.GBGameGenieMenuItem_Click); - // - // GBPrinterViewerMenuItem - // - this.GBPrinterViewerMenuItem.Name = "GBPrinterViewerMenuItem"; - this.GBPrinterViewerMenuItem.Size = new System.Drawing.Size(233, 22); - this.GBPrinterViewerMenuItem.Text = "&Printer Viewer"; - this.GBPrinterViewerMenuItem.Click += new System.EventHandler(this.GBPrinterViewerMenuItem_Click); - // - // GBASubMenu - // - this.GBASubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + // + // GBPrinterViewerMenuItem + // + this.GBPrinterViewerMenuItem.Name = "GBPrinterViewerMenuItem"; + this.GBPrinterViewerMenuItem.Size = new System.Drawing.Size(233, 22); + this.GBPrinterViewerMenuItem.Text = "&Printer Viewer"; + this.GBPrinterViewerMenuItem.Click += new System.EventHandler(this.GBPrinterViewerMenuItem_Click); + // + // GBASubMenu + // + this.GBASubMenu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.GBACoreSelectionSubMenu, this.GBAcoresettingsToolStripMenuItem1, this.toolStripSeparator33, @@ -3390,6 +3394,44 @@ this.AboutMenuItem.Text = "&About"; this.AboutMenuItem.Click += new System.EventHandler(this.AboutMenuItem_Click); // + // zXSpectrumToolStripMenuItem + // + this.zXSpectrumToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.ZXSpectrumCoreEmulationSettingsMenuItem, + this.ZXSpectrumControllerConfigurationMenuItem, + this.ZXSpectrumNonSyncSettingsMenuItem}); + this.zXSpectrumToolStripMenuItem.Name = "zXSpectrumToolStripMenuItem"; + this.zXSpectrumToolStripMenuItem.Size = new System.Drawing.Size(87, 19); + this.zXSpectrumToolStripMenuItem.Text = "ZX Spectrum"; + this.zXSpectrumToolStripMenuItem.DropDownOpened += new System.EventHandler(this.zXSpectrumToolStripMenuItem_DropDownOpened); + // + // ZXSpectrumCoreEmulationSettingsMenuItem + // + this.ZXSpectrumCoreEmulationSettingsMenuItem.Name = "ZXSpectrumCoreEmulationSettingsMenuItem"; + this.ZXSpectrumCoreEmulationSettingsMenuItem.Size = new System.Drawing.Size(201, 22); + this.ZXSpectrumCoreEmulationSettingsMenuItem.Text = "Core Emulation Settings"; + this.ZXSpectrumCoreEmulationSettingsMenuItem.Click += new System.EventHandler(this.ZXSpectrumCoreEmulationSettingsMenuItem_Click); + // + // ZXSpectrumControllerConfigurationMenuItem + // + this.ZXSpectrumControllerConfigurationMenuItem.Name = "ZXSpectrumControllerConfigurationMenuItem"; + this.ZXSpectrumControllerConfigurationMenuItem.Size = new System.Drawing.Size(201, 22); + this.ZXSpectrumControllerConfigurationMenuItem.Text = "Joystick Configuration"; + this.ZXSpectrumControllerConfigurationMenuItem.Click += new System.EventHandler(this.ZXSpectrumControllerConfigurationMenuItem_Click); + // + // ZXSpectrumNonSyncSettingsMenuItem + // + this.ZXSpectrumNonSyncSettingsMenuItem.Name = "ZXSpectrumNonSyncSettingsMenuItem"; + this.ZXSpectrumNonSyncSettingsMenuItem.Size = new System.Drawing.Size(201, 22); + this.ZXSpectrumNonSyncSettingsMenuItem.Text = "Non-Sync Settings"; + this.ZXSpectrumNonSyncSettingsMenuItem.Click += new System.EventHandler(this.ZXSpectrumNonSyncSettingsMenuItem_Click); + // + // Atari7800HawkCoreMenuItem + // + this.Atari7800HawkCoreMenuItem.Name = "Atari7800HawkCoreMenuItem"; + this.Atari7800HawkCoreMenuItem.Size = new System.Drawing.Size(153, 22); + this.Atari7800HawkCoreMenuItem.Text = "Atari7800Hawk"; + // // MainStatusBar // this.MainStatusBar.ClickThrough = true; @@ -4474,5 +4516,9 @@ 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; + } } diff --git a/BizHawk.Client.EmuHawk/MainForm.Events.cs b/BizHawk.Client.EmuHawk/MainForm.Events.cs index aafb6e0eaf..59450b68db 100644 --- a/BizHawk.Client.EmuHawk/MainForm.Events.cs +++ b/BizHawk.Client.EmuHawk/MainForm.Events.cs @@ -2451,11 +2451,42 @@ namespace BizHawk.Client.EmuHawk new IntvControllerSettings().ShowDialog(); } - #endregion + #endregion - #region Help + #region ZXSpectrum - private void HelpSubMenu_DropDownOpened(object sender, EventArgs e) + private void zXSpectrumToolStripMenuItem_DropDownOpened(object sender, EventArgs e) + { + + } + + + private void preferencesToolStripMenuItem4_Click(object sender, EventArgs e) + { + GenericCoreConfig.DoDialog(this, "ZXSpectrum Settings"); + } + + + private void ZXSpectrumControllerConfigurationMenuItem_Click(object sender, EventArgs e) + { + new ZXSpectrumJoystickSettings().ShowDialog(); + } + + private void ZXSpectrumCoreEmulationSettingsMenuItem_Click(object sender, EventArgs e) + { + new ZXSpectrumCoreEmulationSettings().ShowDialog(); + } + + private void ZXSpectrumNonSyncSettingsMenuItem_Click(object sender, EventArgs e) + { + new ZXSpectrumNonSyncSettings().ShowDialog(); + } + + #endregion + + #region Help + + private void HelpSubMenu_DropDownOpened(object sender, EventArgs e) { FeaturesMenuItem.Visible = VersionInfo.DeveloperBuild; } diff --git a/BizHawk.Client.EmuHawk/MainForm.cs b/BizHawk.Client.EmuHawk/MainForm.cs index d653e06691..3f7a9ea1df 100644 --- a/BizHawk.Client.EmuHawk/MainForm.cs +++ b/BizHawk.Client.EmuHawk/MainForm.cs @@ -1719,6 +1719,7 @@ namespace BizHawk.Client.EmuHawk sNESToolStripMenuItem.Visible = false; neoGeoPocketToolStripMenuItem.Visible = false; pCFXToolStripMenuItem.Visible = false; + zXSpectrumToolStripMenuItem.Visible = false; switch (system) { @@ -1816,6 +1817,9 @@ namespace BizHawk.Client.EmuHawk case "PCFX": pCFXToolStripMenuItem.Visible = true; break; + case "ZXSpectrum": + zXSpectrumToolStripMenuItem.Visible = true; + break; } } @@ -2077,7 +2081,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;%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;%ARCH%", "Music Files", "*.psf;*.minipsf;*.sid;*.nsf", "Disc Images", "*.cue;*.ccd;*.mds;*.m3u", "NES", "*.nes;*.fds;*.unf;*.nsf;%ARCH%", @@ -2105,6 +2109,7 @@ namespace BizHawk.Client.EmuHawk "Apple II", "*.dsk;*.do;*.po;%ARCH%", "Virtual Boy", "*.vb;%ARCH%", "Neo Geo Pocket", "*.ngp;*.ngc;%ARCH%", + "Sinclair ZX Spectrum", "*.tzx;*.tap;%ARCH%", "All Files", "*.*"); } @@ -4294,7 +4299,7 @@ namespace BizHawk.Client.EmuHawk GenericCoreConfig.DoDialog(this, "PC-FX Settings"); } - private bool Rewind(ref bool runFrame, long currentTimestamp, out bool returnToRecording) + private bool Rewind(ref bool runFrame, long currentTimestamp, out bool returnToRecording) { var isRewinding = false; diff --git a/BizHawk.Client.EmuHawk/Properties/Resources.Designer.cs b/BizHawk.Client.EmuHawk/Properties/Resources.Designer.cs index 72c965441f..7504818e28 100644 --- a/BizHawk.Client.EmuHawk/Properties/Resources.Designer.cs +++ b/BizHawk.Client.EmuHawk/Properties/Resources.Designer.cs @@ -489,7 +489,7 @@ namespace BizHawk.Client.EmuHawk.Properties { return ((System.Drawing.Bitmap)(obj)); } } - + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -3438,5 +3438,15 @@ namespace BizHawk.Client.EmuHawk.Properties { return ((System.Drawing.Bitmap)(obj)); } } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap ZXSpectrumKeyboards { + get { + object obj = ResourceManager.GetObject("ZXSpectrumKeyboards", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } } } diff --git a/BizHawk.Client.EmuHawk/Properties/Resources.resx b/BizHawk.Client.EmuHawk/Properties/Resources.resx index b77dae3d68..38184f2b35 100644 --- a/BizHawk.Client.EmuHawk/Properties/Resources.resx +++ b/BizHawk.Client.EmuHawk/Properties/Resources.resx @@ -1557,4 +1557,7 @@ ..\images\ControllerImages\NGPController.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - + + ..\config\controllerimages\zxspectrumkeyboards.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/Resources/ZXSpectrumKeyboard.bmp b/BizHawk.Client.EmuHawk/Resources/ZXSpectrumKeyboard.bmp new file mode 100644 index 0000000000..55516c7aaa Binary files /dev/null and b/BizHawk.Client.EmuHawk/Resources/ZXSpectrumKeyboard.bmp differ diff --git a/BizHawk.Client.EmuHawk/config/ControllerConfig.cs b/BizHawk.Client.EmuHawk/config/ControllerConfig.cs index ab488b1724..f17a7e1afe 100644 --- a/BizHawk.Client.EmuHawk/config/ControllerConfig.cs +++ b/BizHawk.Client.EmuHawk/config/ControllerConfig.cs @@ -172,15 +172,24 @@ namespace BizHawk.Client.EmuHawk string tabname = cat.Key; tt.TabPages.Add(tabname); tt.TabPages[pageidx].Controls.Add(createpanel(settings, cat.Value, tt.Size)); - } + + // zxhawk hack - it uses multiple categoryLabels + if (Global.Emulator.SystemId == "ZXSpectrum") + pageidx++; + + } if (buckets[0].Count > 0) { - string tabname = Global.Emulator.SystemId == "C64" ? "Keyboard" : "Console"; // hack - tt.TabPages.Add(tabname); - tt.TabPages[pageidx].Controls.Add(createpanel(settings, buckets[0], tt.Size)); - } - } + // ZXHawk needs to skip this bit + if (Global.Emulator.SystemId == "ZXSpectrum") + return; + + string tabname = (Global.Emulator.SystemId == "C64") ? "Keyboard" : "Console"; // hack + tt.TabPages.Add(tabname); + tt.TabPages[pageidx].Controls.Add(createpanel(settings, buckets[0], tt.Size)); + } + } } public ControllerConfig(ControllerDefinition def) @@ -256,6 +265,13 @@ namespace BizHawk.Client.EmuHawk pictureBox2.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom; } + + if (controlName == "ZXSpectrum 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 diff --git a/BizHawk.Client.EmuHawk/config/ControllerImages/ZXSpectrumKeyboards.png b/BizHawk.Client.EmuHawk/config/ControllerImages/ZXSpectrumKeyboards.png new file mode 100644 index 0000000000..1af85ebea9 Binary files /dev/null and b/BizHawk.Client.EmuHawk/config/ControllerImages/ZXSpectrumKeyboards.png differ diff --git a/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs b/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs index 5b2e5eca92..7b016a8cab 100644 --- a/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs +++ b/BizHawk.Client.EmuHawk/config/FirmwaresConfig.cs @@ -52,6 +52,7 @@ namespace BizHawk.Client.EmuHawk { "GBC", "Game Boy Color" }, { "PCFX", "PC-FX" }, { "32X", "32X" }, + { "ZXSpectrum", "ZX Spectrum" } }; public string TargetSystem = null; diff --git a/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.Designer.cs b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.Designer.cs new file mode 100644 index 0000000000..d7e1cf1e63 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.Designer.cs @@ -0,0 +1,187 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class ZXSpectrumCoreEmulationSettings + { + /// + /// 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(ZXSpectrumCoreEmulationSettings)); + 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.label2 = new System.Windows.Forms.Label(); + this.borderTypecomboBox1 = new System.Windows.Forms.ComboBox(); + this.lblBorderInfo = new System.Windows.Forms.Label(); + 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, 393); + 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, 393); + 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(361, 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 = "ZX Spectrum 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(15, 302); + 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; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(12, 335); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(118, 13); + this.label2.TabIndex = 23; + 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, 351); + this.borderTypecomboBox1.Name = "borderTypecomboBox1"; + this.borderTypecomboBox1.Size = new System.Drawing.Size(157, 21); + this.borderTypecomboBox1.TabIndex = 22; + this.borderTypecomboBox1.SelectedIndexChanged += new System.EventHandler(this.borderTypecomboBox1_SelectedIndexChanged); + // + // 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, 351); + this.lblBorderInfo.Name = "lblBorderInfo"; + this.lblBorderInfo.Size = new System.Drawing.Size(196, 21); + this.lblBorderInfo.TabIndex = 24; + this.lblBorderInfo.Text = "null"; + this.lblBorderInfo.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // ZXSpectrumCoreEmulationSettings + // + 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, 428); + this.Controls.Add(this.lblBorderInfo); + this.Controls.Add(this.label2); + this.Controls.Add(this.borderTypecomboBox1); + 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 = "ZXSpectrumCoreEmulationSettings"; + 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 label2; + private System.Windows.Forms.ComboBox borderTypecomboBox1; + private System.Windows.Forms.Label lblBorderInfo; + } +} \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.cs b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.cs new file mode 100644 index 0000000000..bb8ba92d50 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.cs @@ -0,0 +1,117 @@ +using System; +using System.Linq; +using System.Windows.Forms; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Cores.Computers.SinclairSpectrum; +using System.Text; + +namespace BizHawk.Client.EmuHawk +{ + public partial class ZXSpectrumCoreEmulationSettings : Form + { + private ZXSpectrum.ZXSpectrumSyncSettings _syncSettings; + + public ZXSpectrumCoreEmulationSettings() + { + InitializeComponent(); + } + + private void IntvControllerSettings_Load(object sender, EventArgs e) + { + _syncSettings = ((ZXSpectrum)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(ZXSpectrum.BorderType)); + foreach (var val in borderTypes) + { + borderTypecomboBox1.Items.Add(val); + } + borderTypecomboBox1.SelectedItem = _syncSettings.BorderType.ToString(); + UpdateBorderNotes((ZXSpectrum.BorderType)Enum.Parse(typeof(ZXSpectrum.BorderType), borderTypecomboBox1.SelectedItem.ToString())); + + // deterministic emulation + determEmucheckBox1.Checked = _syncSettings.DeterministicEmulation; + } + + 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; + + if (changed) + { + _syncSettings.MachineType = (MachineType)Enum.Parse(typeof(MachineType), MachineSelectionComboBox.SelectedItem.ToString()); + _syncSettings.BorderType = (ZXSpectrum.BorderType)Enum.Parse(typeof(ZXSpectrum.BorderType), borderTypecomboBox1.SelectedItem.ToString()); + _syncSettings.DeterministicEmulation = determEmucheckBox1.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 = ZXMachineMetaData.GetMetaString(type); + } + + private void borderTypecomboBox1_SelectedIndexChanged(object sender, EventArgs e) + { + ComboBox cb = sender as ComboBox; + UpdateBorderNotes((ZXSpectrum.BorderType)Enum.Parse(typeof(ZXSpectrum.BorderType), cb.SelectedItem.ToString())); + } + + private void UpdateBorderNotes(ZXSpectrum.BorderType type) + { + switch (type) + { + case ZXSpectrum.BorderType.Full: + lblBorderInfo.Text = "Original border sizes"; + break; + case ZXSpectrum.BorderType.Medium: + lblBorderInfo.Text = "All borders 24px"; + break; + case ZXSpectrum.BorderType.None: + lblBorderInfo.Text = "No border at all"; + break; + case ZXSpectrum.BorderType.Small: + lblBorderInfo.Text = "All borders 10px"; + break; + case ZXSpectrum.BorderType.Widescreen: + lblBorderInfo.Text = "No top and bottom border (almost 16:9)"; + break; + } + } + } +} diff --git a/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.resx b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.resx new file mode 100644 index 0000000000..ca821b54f8 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumCoreEmulationSettings.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/ZXSpectrum/ZXSpectrumJoystickSettings.Designer.cs b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumJoystickSettings.Designer.cs new file mode 100644 index 0000000000..342e833dc1 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumJoystickSettings.Designer.cs @@ -0,0 +1,184 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class ZXSpectrumJoystickSettings + { + /// + /// 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(ZXSpectrumJoystickSettings)); + this.OkBtn = new System.Windows.Forms.Button(); + this.CancelBtn = new System.Windows.Forms.Button(); + this.label5 = new System.Windows.Forms.Label(); + this.label4 = new System.Windows.Forms.Label(); + this.Port2ComboBox = new System.Windows.Forms.ComboBox(); + this.Port1ComboBox = new System.Windows.Forms.ComboBox(); + this.label1 = new System.Windows.Forms.Label(); + this.Port3ComboBox = new System.Windows.Forms.ComboBox(); + this.label2 = new System.Windows.Forms.Label(); + this.lblDoubleSize = new System.Windows.Forms.Label(); + 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(170, 312); + 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(236, 312); + 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); + // + // label5 + // + this.label5.AutoSize = true; + this.label5.Location = new System.Drawing.Point(9, 207); + this.label5.Name = "label5"; + this.label5.Size = new System.Drawing.Size(57, 13); + this.label5.TabIndex = 16; + this.label5.Text = "Joystick 2:"; + // + // label4 + // + this.label4.AutoSize = true; + this.label4.Location = new System.Drawing.Point(12, 157); + this.label4.Name = "label4"; + this.label4.Size = new System.Drawing.Size(57, 13); + this.label4.TabIndex = 15; + this.label4.Text = "Joystick 1:"; + // + // Port2ComboBox + // + this.Port2ComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.Port2ComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.Port2ComboBox.FormattingEnabled = true; + this.Port2ComboBox.Location = new System.Drawing.Point(12, 223); + this.Port2ComboBox.Name = "Port2ComboBox"; + this.Port2ComboBox.Size = new System.Drawing.Size(284, 21); + this.Port2ComboBox.TabIndex = 14; + // + // Port1ComboBox + // + this.Port1ComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.Port1ComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.Port1ComboBox.FormattingEnabled = true; + this.Port1ComboBox.Location = new System.Drawing.Point(12, 173); + this.Port1ComboBox.Name = "Port1ComboBox"; + this.Port1ComboBox.Size = new System.Drawing.Size(284, 21); + this.Port1ComboBox.TabIndex = 13; + // + // 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(151, 13); + this.label1.TabIndex = 17; + this.label1.Text = "ZX Spectrum Joystick Settings"; + // + // Port3ComboBox + // + this.Port3ComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.Port3ComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.Port3ComboBox.FormattingEnabled = true; + this.Port3ComboBox.Location = new System.Drawing.Point(12, 275); + this.Port3ComboBox.Name = "Port3ComboBox"; + this.Port3ComboBox.Size = new System.Drawing.Size(284, 21); + this.Port3ComboBox.TabIndex = 18; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(12, 259); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(57, 13); + this.label2.TabIndex = 19; + this.label2.Text = "Joystick 3:"; + // + // lblDoubleSize + // + this.lblDoubleSize.Location = new System.Drawing.Point(26, 40); + this.lblDoubleSize.Name = "lblDoubleSize"; + this.lblDoubleSize.Size = new System.Drawing.Size(254, 117); + this.lblDoubleSize.TabIndex = 20; + this.lblDoubleSize.Text = resources.GetString("lblDoubleSize.Text"); + // + // ZXSpectrumJoystickSettings + // + 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(308, 347); + this.Controls.Add(this.lblDoubleSize); + this.Controls.Add(this.label2); + this.Controls.Add(this.Port3ComboBox); + this.Controls.Add(this.label1); + this.Controls.Add(this.label5); + this.Controls.Add(this.label4); + this.Controls.Add(this.Port2ComboBox); + this.Controls.Add(this.Port1ComboBox); + this.Controls.Add(this.CancelBtn); + this.Controls.Add(this.OkBtn); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.Name = "ZXSpectrumJoystickSettings"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Joystick 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 label5; + private System.Windows.Forms.Label label4; + private System.Windows.Forms.ComboBox Port2ComboBox; + private System.Windows.Forms.ComboBox Port1ComboBox; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.ComboBox Port3ComboBox; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Label lblDoubleSize; + } +} \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumJoystickSettings.cs b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumJoystickSettings.cs new file mode 100644 index 0000000000..fc151c843c --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumJoystickSettings.cs @@ -0,0 +1,127 @@ +using System; +using System.Linq; +using System.Windows.Forms; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Cores.Computers.SinclairSpectrum; + +namespace BizHawk.Client.EmuHawk +{ + public partial class ZXSpectrumJoystickSettings : Form + { + private ZXSpectrum.ZXSpectrumSyncSettings _syncSettings; + + public ZXSpectrumJoystickSettings() + { + InitializeComponent(); + } + + private string[] possibleControllers; + + private void IntvControllerSettings_Load(object sender, EventArgs e) + { + _syncSettings = ((ZXSpectrum)Global.Emulator).GetSyncSettings().Clone(); + + possibleControllers = Enum.GetNames(typeof(JoystickType)); + + foreach (var val in possibleControllers) + { + Port1ComboBox.Items.Add(val); + Port2ComboBox.Items.Add(val); + Port3ComboBox.Items.Add(val); + } + + Port1ComboBox.SelectedItem = _syncSettings.JoystickType1.ToString(); + Port2ComboBox.SelectedItem = _syncSettings.JoystickType2.ToString(); + Port3ComboBox.SelectedItem = _syncSettings.JoystickType3.ToString(); + } + + private void OkBtn_Click(object sender, EventArgs e) + { + bool changed = + _syncSettings.JoystickType1.ToString() != Port1ComboBox.SelectedItem.ToString() + || _syncSettings.JoystickType2.ToString() != Port2ComboBox.SelectedItem.ToString() + || _syncSettings.JoystickType3.ToString() != Port3ComboBox.SelectedItem.ToString(); + + if (changed) + { + // enforce unique joystick selection + + bool selectionValid = true; + + var j1 = Port1ComboBox.SelectedItem.ToString(); + if (j1 != possibleControllers.First()) + { + if (j1 == Port2ComboBox.SelectedItem.ToString()) + { + Port2ComboBox.SelectedItem = possibleControllers.First(); + selectionValid = false; + } + if (j1 == Port3ComboBox.SelectedItem.ToString()) + { + Port3ComboBox.SelectedItem = possibleControllers.First(); + selectionValid = false; + } + } + + var j2 = Port2ComboBox.SelectedItem.ToString(); + if (j2 != possibleControllers.First()) + { + if (j2 == Port1ComboBox.SelectedItem.ToString()) + { + Port1ComboBox.SelectedItem = possibleControllers.First(); + selectionValid = false; + } + if (j2 == Port3ComboBox.SelectedItem.ToString()) + { + Port3ComboBox.SelectedItem = possibleControllers.First(); + selectionValid = false; + } + } + + var j3 = Port3ComboBox.SelectedItem.ToString(); + if (j3 != possibleControllers.First()) + { + if (j3 == Port1ComboBox.SelectedItem.ToString()) + { + Port1ComboBox.SelectedItem = possibleControllers.First(); + selectionValid = false; + } + if (j3 == Port2ComboBox.SelectedItem.ToString()) + { + Port2ComboBox.SelectedItem = possibleControllers.First(); + selectionValid = false; + } + } + + if (selectionValid) + { + _syncSettings.JoystickType1 = (JoystickType)Enum.Parse(typeof(JoystickType), Port1ComboBox.SelectedItem.ToString()); + _syncSettings.JoystickType2 = (JoystickType)Enum.Parse(typeof(JoystickType), Port2ComboBox.SelectedItem.ToString()); + _syncSettings.JoystickType3 = (JoystickType)Enum.Parse(typeof(JoystickType), Port3ComboBox.SelectedItem.ToString()); + + GlobalWin.MainForm.PutCoreSyncSettings(_syncSettings); + + DialogResult = DialogResult.OK; + Close(); + } + else + { + MessageBox.Show("Invalid joystick configuration. \nDuplicates have automatically been changed to NULL.\n\nPlease review the configuration"); + } + } + else + { + DialogResult = DialogResult.OK; + Close(); + } + } + + private void CancelBtn_Click(object sender, EventArgs e) + { + GlobalWin.OSD.AddMessage("Joystick settings aborted"); + DialogResult = DialogResult.Cancel; + Close(); + } + } +} diff --git a/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumJoystickSettings.resx b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumJoystickSettings.resx new file mode 100644 index 0000000000..c45473925d --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumJoystickSettings.resx @@ -0,0 +1,630 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + ZXHawk is set up to allow 3 different unique joysticks to be attached at one time. + +This is because the Kempston joystick had to be attached via a Kempton interface plugged into the single expansion port. The Sinclair and Cursor joysticks effectively mapped to different key presses on the keyboard. + + + + + + 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/ZXSpectrum/ZXSpectrumNonSyncSettings.Designer.cs b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumNonSyncSettings.Designer.cs new file mode 100644 index 0000000000..511f9eeea4 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumNonSyncSettings.Designer.cs @@ -0,0 +1,162 @@ +namespace BizHawk.Client.EmuHawk +{ + partial class ZXSpectrumNonSyncSettings + { + /// + /// 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.autoLoadcheckBox1 = new System.Windows.Forms.CheckBox(); + this.label2 = new System.Windows.Forms.Label(); + this.panTypecomboBox1 = new System.Windows.Forms.ComboBox(); + this.lblBorderInfo = new System.Windows.Forms.Label(); + this.lblAutoLoadText = new System.Windows.Forms.Label(); + 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, 158); + 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, 158); + 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"; + // + // autoLoadcheckBox1 + // + this.autoLoadcheckBox1.AutoSize = true; + this.autoLoadcheckBox1.Location = new System.Drawing.Point(15, 52); + this.autoLoadcheckBox1.Name = "autoLoadcheckBox1"; + this.autoLoadcheckBox1.Size = new System.Drawing.Size(103, 17); + this.autoLoadcheckBox1.TabIndex = 21; + this.autoLoadcheckBox1.Text = "Auto-Load Tape"; + this.autoLoadcheckBox1.UseVisualStyleBackColor = true; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(12, 97); + 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, 113); + 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, 106); + 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 (128K models only)"; + this.lblBorderInfo.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // 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(175, 46); + this.lblAutoLoadText.Name = "lblAutoLoadText"; + this.lblAutoLoadText.Size = new System.Drawing.Size(196, 30); + this.lblAutoLoadText.TabIndex = 25; + this.lblAutoLoadText.Text = "When enabled ZXHawk will attempt to control the tape device automatically when th" + + "e correct traps are detected"; + this.lblAutoLoadText.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // 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, 193); + this.Controls.Add(this.lblAutoLoadText); + this.Controls.Add(this.lblBorderInfo); + this.Controls.Add(this.label2); + this.Controls.Add(this.panTypecomboBox1); + this.Controls.Add(this.autoLoadcheckBox1); + 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.CheckBox autoLoadcheckBox1; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.ComboBox panTypecomboBox1; + private System.Windows.Forms.Label lblBorderInfo; + private System.Windows.Forms.Label lblAutoLoadText; + } +} \ No newline at end of file diff --git a/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumNonSyncSettings.cs b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumNonSyncSettings.cs new file mode 100644 index 0000000000..0b8bfcbc0c --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumNonSyncSettings.cs @@ -0,0 +1,66 @@ +using System; +using System.Linq; +using System.Windows.Forms; + +using BizHawk.Client.Common; +using BizHawk.Emulation.Cores.Computers.SinclairSpectrum; +using System.Text; + +namespace BizHawk.Client.EmuHawk +{ + public partial class ZXSpectrumNonSyncSettings : Form + { + private ZXSpectrum.ZXSpectrumSettings _settings; + + public ZXSpectrumNonSyncSettings() + { + InitializeComponent(); + } + + private void IntvControllerSettings_Load(object sender, EventArgs e) + { + _settings = ((ZXSpectrum)Global.Emulator).GetSettings().Clone(); + + // autoload tape + autoLoadcheckBox1.Checked = _settings.AutoLoadTape; + + // AY panning config + var panTypes = Enum.GetNames(typeof(AYChip.AYPanConfig)); + foreach (var val in panTypes) + { + panTypecomboBox1.Items.Add(val); + } + panTypecomboBox1.SelectedItem = _settings.AYPanConfig.ToString(); + } + + private void OkBtn_Click(object sender, EventArgs e) + { + bool changed = + _settings.AutoLoadTape != autoLoadcheckBox1.Checked + || _settings.AYPanConfig.ToString() != panTypecomboBox1.SelectedItem.ToString(); + + if (changed) + { + _settings.AutoLoadTape = autoLoadcheckBox1.Checked; + _settings.AYPanConfig = (AYChip.AYPanConfig)Enum.Parse(typeof(AYChip.AYPanConfig), panTypecomboBox1.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(); + } + } +} diff --git a/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumNonSyncSettings.resx b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumNonSyncSettings.resx new file mode 100644 index 0000000000..ca821b54f8 --- /dev/null +++ b/BizHawk.Client.EmuHawk/config/ZXSpectrum/ZXSpectrumNonSyncSettings.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/tools/MultiDiskBundler/MultiDiskBundler.Designer.cs b/BizHawk.Client.EmuHawk/tools/MultiDiskBundler/MultiDiskBundler.Designer.cs index 3c1e2f2e9e..5616d71e02 100644 --- a/BizHawk.Client.EmuHawk/tools/MultiDiskBundler/MultiDiskBundler.Designer.cs +++ b/BizHawk.Client.EmuHawk/tools/MultiDiskBundler/MultiDiskBundler.Designer.cs @@ -143,7 +143,8 @@ "GB", "PCFX", "PSX", - "SAT"}); + "SAT", + "ZXSpectrum"}); 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.Client.EmuHawk/tools/VirtualPads/schema/ZXSpectrumSchema.cs b/BizHawk.Client.EmuHawk/tools/VirtualPads/schema/ZXSpectrumSchema.cs new file mode 100644 index 0000000000..cdbf9228c0 --- /dev/null +++ b/BizHawk.Client.EmuHawk/tools/VirtualPads/schema/ZXSpectrumSchema.cs @@ -0,0 +1,260 @@ +using System.Collections.Generic; +using System.Drawing; + +using BizHawk.Emulation.Common; +using System.Linq; + +namespace BizHawk.Client.EmuHawk +{ + [Schema("ZXSpectrum")] + class ZXSpectrumSchema : IVirtualPadSchema + { + public IEnumerable GetPadSchemas(IEmulator core) + { + yield return Joystick(1); + yield return Joystick(2); + yield return Joystick(3); + yield return Keyboard(); + //yield return TapeDevice(); + } + + private static PadSchema Joystick(int controller) + { + return new PadSchema + { + DisplayName = "Joystick " + controller, + IsConsole = false, + DefaultSize = new Size(174, 74), + MaxSize = new Size(174, 74), + Buttons = new[] + { + new PadSchema.ButtonSchema + { + Name = "P" + controller + " Up", + DisplayName = "", + Icon = Properties.Resources.BlueUp, + Location = new Point(23, 15), + Type = PadSchema.PadInputType.Boolean + }, + new PadSchema.ButtonSchema + { + Name = "P" + controller + " Down", + DisplayName = "", + Icon = Properties.Resources.BlueDown, + Location = new Point(23, 36), + Type = PadSchema.PadInputType.Boolean + }, + new PadSchema.ButtonSchema + { + Name = "P" + controller + " Left", + DisplayName = "", + Icon = Properties.Resources.Back, + Location = new Point(2, 24), + Type = PadSchema.PadInputType.Boolean + }, + new PadSchema.ButtonSchema + { + Name = "P" + controller + " Right", + DisplayName = "", + Icon = Properties.Resources.Forward, + Location = new Point(44, 24), + Type = PadSchema.PadInputType.Boolean + }, + new PadSchema.ButtonSchema + { + Name = "P" + controller + " Button", + DisplayName = "B", + Location = new Point(124, 24), + Type = PadSchema.PadInputType.Boolean + } + } + }; + } + + private class ButtonLayout + { + public string Name { get; set; } + public string DisName { get; set; } + public double WidthFactor { get; set; } + public int Row { get; set; } + public bool IsActive = true; + } + + private static PadSchema Keyboard() + { + List bls = new List + { + new ButtonLayout { Name = "Key True Video", DisName = "TV", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Inv Video", DisName = "IV", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key 1", DisName = "1", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key 2", DisName = "2", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key 3", DisName = "3", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key 4", DisName = "4", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key 5", DisName = "5", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key 6", DisName = "6", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key 7", DisName = "7", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key 8", DisName = "8", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key 9", DisName = "9", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key 0", DisName = "0", Row = 0, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Break", DisName = "BREAK", Row = 0, WidthFactor = 1.5 }, + + new ButtonLayout { Name = "Key Delete", DisName = "DEL", Row = 1, WidthFactor = 1.5 }, + new ButtonLayout { Name = "Key Graph", DisName = "GR", Row = 1, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Q", DisName = "Q", Row = 1, WidthFactor = 1 }, + new ButtonLayout { Name = "Key W", DisName = "W", Row = 1, WidthFactor = 1 }, + new ButtonLayout { Name = "Key E", DisName = "E", Row = 1, WidthFactor = 1 }, + new ButtonLayout { Name = "Key R", DisName = "R", Row = 1, WidthFactor = 1 }, + new ButtonLayout { Name = "Key T", DisName = "T", Row = 1, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Y", DisName = "Y", Row = 1, WidthFactor = 1 }, + new ButtonLayout { Name = "Key U", DisName = "U", Row = 1, WidthFactor = 1 }, + new ButtonLayout { Name = "Key I", DisName = "I", Row = 1, WidthFactor = 1 }, + new ButtonLayout { Name = "Key O", DisName = "O", Row = 1, WidthFactor = 1 }, + new ButtonLayout { Name = "Key P", DisName = "P", Row = 1, WidthFactor = 1 }, + + new ButtonLayout { Name = "Key Extend Mode", DisName = "EM", Row = 2, WidthFactor = 1.5 }, + new ButtonLayout { Name = "Key Edit", DisName = "ED", Row = 2, WidthFactor = 1.25}, + new ButtonLayout { Name = "Key A", DisName = "A", Row = 2, WidthFactor = 1 }, + new ButtonLayout { Name = "Key S", DisName = "S", Row = 2, WidthFactor = 1 }, + new ButtonLayout { Name = "Key D", DisName = "D", Row = 2, WidthFactor = 1 }, + new ButtonLayout { Name = "Key F", DisName = "F", Row = 2, WidthFactor = 1 }, + new ButtonLayout { Name = "Key G", DisName = "G", Row = 2, WidthFactor = 1 }, + new ButtonLayout { Name = "Key H", DisName = "H", Row = 2, WidthFactor = 1 }, + new ButtonLayout { Name = "Key J", DisName = "J", Row = 2, WidthFactor = 1 }, + new ButtonLayout { Name = "Key K", DisName = "K", Row = 2, WidthFactor = 1 }, + new ButtonLayout { Name = "Key L", DisName = "L", Row = 2, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Return", DisName = "ENTER", Row = 2, WidthFactor = 1.75 }, + + new ButtonLayout { Name = "Key Caps Shift", DisName = "CAPS-S", Row = 3, WidthFactor = 2.25 }, + new ButtonLayout { Name = "Key Caps Lock", DisName = "CL", Row = 3, WidthFactor = 1}, + new ButtonLayout { Name = "Key Z", DisName = "Z", Row = 3, WidthFactor = 1 }, + new ButtonLayout { Name = "Key X", DisName = "X", Row = 3, WidthFactor = 1 }, + new ButtonLayout { Name = "Key C", DisName = "C", Row = 3, WidthFactor = 1 }, + new ButtonLayout { Name = "Key V", DisName = "V", Row = 3, WidthFactor = 1 }, + new ButtonLayout { Name = "Key B", DisName = "B", Row = 3, WidthFactor = 1 }, + new ButtonLayout { Name = "Key N", DisName = "N", Row = 3, WidthFactor = 1 }, + new ButtonLayout { Name = "Key M", DisName = "M", Row = 3, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Period", DisName = ".", Row = 3, WidthFactor = 1}, + new ButtonLayout { Name = "Key Caps Shift", DisName = "CAPS-S", Row = 3, WidthFactor = 2.25 }, + + new ButtonLayout { Name = "Key Symbol Shift", DisName = "SS", Row = 4, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Semi-Colon", DisName = ";", Row = 4, WidthFactor = 1}, + new ButtonLayout { Name = "Key Quote", DisName = "\"", Row = 4, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Left Cursor", DisName = "←", Row = 4, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Right Cursor", DisName = "→", Row = 4, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Space", DisName = "SPACE", Row = 4, WidthFactor = 4.5 }, + new ButtonLayout { Name = "Key Up Cursor", DisName = "↑", Row = 4, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Down Cursor", DisName = "↓", Row = 4, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Comma", DisName = ",", Row = 4, WidthFactor = 1 }, + new ButtonLayout { Name = "Key Symbol Shift", DisName = "SS", Row = 4, WidthFactor = 1 }, + }; + + PadSchema ps = new PadSchema + { + DisplayName = "Keyboard", + IsConsole = false, + DefaultSize = new Size(500, 170) + }; + + List btns = new List(); + + int rowHeight = 29; //24 + int stdButtonWidth = 29; //24 + int yPos = 18; + int xPos = 22; + int currRow = 0; + + foreach (var b in bls) + { + if (b.Row > currRow) + { + currRow++; + yPos += rowHeight; + xPos = 22; + } + + int txtLength = b.DisName.Length; + int btnSize = System.Convert.ToInt32((double)stdButtonWidth * b.WidthFactor); + + + string disp = b.DisName; + if (txtLength == 1) + disp = " " + disp; + + switch(b.DisName) + { + case "SPACE": disp = " " + disp + " "; break; + case "I": disp = " " + disp + " "; break; + case "W": disp = b.DisName; break; + } + + + if (b.IsActive) + { + PadSchema.ButtonSchema btn = new PadSchema.ButtonSchema(); + btn.Name = b.Name; + btn.DisplayName = disp; + btn.Location = new Point(xPos, yPos); + btn.Type = PadSchema.PadInputType.Boolean; + btns.Add(btn); + } + + xPos += btnSize; + } + + ps.Buttons = btns.ToArray(); + return ps; + } + + private static PadSchema TapeDevice() + { + return new PadSchema + { + DisplayName = "DATACORDER", + IsConsole = false, + DefaultSize = new Size(174, 74), + MaxSize = new Size(174, 74), + Buttons = new[] + { + new PadSchema.ButtonSchema + { + Name = "Play Tape", + Icon = Properties.Resources.Play, + Location = new Point(23, 22), + Type = PadSchema.PadInputType.Boolean + }, + new PadSchema.ButtonSchema + { + Name = "Stop Tape", + Icon = Properties.Resources.Stop, + Location = new Point(53, 22), + Type = PadSchema.PadInputType.Boolean + }, + new PadSchema.ButtonSchema + { + Name = "RTZ Tape", + Icon = Properties.Resources.BackMore, + Location = new Point(83, 22), + Type = PadSchema.PadInputType.Boolean + }, + new PadSchema.ButtonSchema + { + Name = "Insert Next Tape", + DisplayName = "NEXT TAPE", + //Icon = Properties.Resources.MoveRight, + Location = new Point(23, 52), + Type = PadSchema.PadInputType.Boolean + }, + new PadSchema.ButtonSchema + { + Name = "Insert Previous Tape", + DisplayName = "PREV TAPE", + //Icon = Properties.Resources.MoveLeft, + Location = new Point(100, 52), + Type = PadSchema.PadInputType.Boolean + }, + + } + }; + } + } +} diff --git a/BizHawk.Emulation.Common/Database/Database.cs b/BizHawk.Emulation.Common/Database/Database.cs index df1a2562c7..d106a5e91a 100644 --- a/BizHawk.Emulation.Common/Database/Database.cs +++ b/BizHawk.Emulation.Common/Database/Database.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading; using BizHawk.Common.BufferExtensions; +using System.Linq; namespace BizHawk.Emulation.Common { @@ -298,12 +299,24 @@ namespace BizHawk.Emulation.Common case ".D64": case ".T64": case ".G64": - case ".CRT": - case ".TAP": + case ".CRT": game.System = "C64"; break; - case ".Z64": + case ".TZX": + game.System = "ZXSpectrum"; + break; + + case ".TAP": + + byte[] head = romData.Take(8).ToArray(); + if (System.Text.Encoding.Default.GetString(head).Contains("C64-TAPE")) + game.System = "C64"; + else + game.System = "ZXSpectrum"; + break; + + case ".Z64": case ".V64": case ".N64": game.System = "N64"; diff --git a/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs b/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs index 45e30fc9cb..e279f00c5d 100644 --- a/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs +++ b/BizHawk.Emulation.Common/Database/FirmwareDatabase.cs @@ -50,9 +50,17 @@ namespace BizHawk.Emulation.Common FirmwareAndOption("AB16F56989B27D89BABE5F89C5A8CB3DA71A82F0", 16384, "C64", "Drive1541", "drive-1541.bin", "1541 Disk Drive Rom"); FirmwareAndOption("D3B78C3DBAC55F5199F33F3FE0036439811F7FB3", 16384, "C64", "Drive1541II", "drive-1541ii.bin", "1541-II Disk Drive Rom"); - // for saturn, we think any bios region can pretty much run any iso - // so, we're going to lay this out carefully so that we choose things in a sensible order, but prefer the correct region - var ss_100_j = File("2B8CB4F87580683EB4D760E4ED210813D667F0A2", 524288, "saturn-1.00-(J).bin", "Bios v1.00 (J)"); + // ZX Spectrum + /* These are now shipped with bizhawk + FirmwareAndOption("5EA7C2B824672E914525D1D5C419D71B84A426A2", 16384, "ZXSpectrum", "48ROM", "48.ROM", "Spectrum 48K ROM"); + FirmwareAndOption("16375D42EA109B47EDDED7A16028DE7FDB3013A1", 32768, "ZXSpectrum", "128ROM", "128.ROM", "Spectrum 128K ROM"); + FirmwareAndOption("8CAFB292AF58617907B9E6B9093D3588A75849B8", 32768, "ZXSpectrum", "PLUS2ROM", "PLUS2.ROM", "Spectrum 128K +2 ROM"); + FirmwareAndOption("929BF1A5E5687EBD8D7245F9B513A596C0EC21A4", 65536, "ZXSpectrum", "PLUS3ROM", "PLUS3.ROM", "Spectrum 128K +3 ROM"); + */ + + // for saturn, we think any bios region can pretty much run any iso + // so, we're going to lay this out carefully so that we choose things in a sensible order, but prefer the correct region + var ss_100_j = File("2B8CB4F87580683EB4D760E4ED210813D667F0A2", 524288, "saturn-1.00-(J).bin", "Bios v1.00 (J)"); var ss_100_ue = File("FAA8EA183A6D7BBE5D4E03BB1332519800D3FBC3", 524288, "saturn-1.00-(U+E).bin", "Bios v1.00 (U+E)"); var ss_100a_ue = File("3BB41FEB82838AB9A35601AC666DE5AACFD17A58", 524288, "saturn-1.00a-(U+E).bin", "Bios v1.00a (U+E)"); // ?? is this size correct? var ss_101_j = File("DF94C5B4D47EB3CC404D88B33A8FDA237EAF4720", 524288, "saturn-1.01-(J).bin", "Bios v1.01 (J)"); // ?? is this size correct? diff --git a/BizHawk.Emulation.Common/SystemLookup.cs b/BizHawk.Emulation.Common/SystemLookup.cs index f8e1a0de3c..4c6c692e46 100644 --- a/BizHawk.Emulation.Common/SystemLookup.cs +++ b/BizHawk.Emulation.Common/SystemLookup.cs @@ -32,7 +32,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 = "INTV", FullName = "Intellivision" }, + new SystemInfo { SystemId = "ZXSpectrum", FullName = "Sinclair ZX Spectrum" } }; public SystemInfo this[string systemId] diff --git a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj index 85124ebf54..0faaa3c4f6 100644 --- a/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj +++ b/BizHawk.Emulation.Cores/BizHawk.Emulation.Cores.csproj @@ -258,6 +258,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Atari2600.cs @@ -1338,16 +1393,29 @@ + + + + + + + + + + + + + "$(SolutionDir)subwcrev.bat" "$(ProjectDir)" diff --git a/BizHawk.Emulation.Cores/CPUs/Z80A/Execute.cs b/BizHawk.Emulation.Cores/CPUs/Z80A/Execute.cs index c70e4aa503..eddf74d0a4 100644 --- a/BizHawk.Emulation.Cores/CPUs/Z80A/Execute.cs +++ b/BizHawk.Emulation.Cores/CPUs/Z80A/Execute.cs @@ -4,8 +4,7 @@ namespace BizHawk.Emulation.Cores.Components.Z80A { public partial class Z80A { - private int totalExecutedCycles; - public int TotalExecutedCycles { get { return totalExecutedCycles; } set { totalExecutedCycles = value; } } + public long TotalExecutedCycles; private int EI_pending; diff --git a/BizHawk.Emulation.Cores/CPUs/Z80A/Interrupts.cs b/BizHawk.Emulation.Cores/CPUs/Z80A/Interrupts.cs index 8dc1cd24ce..2c4f5c5204 100644 --- a/BizHawk.Emulation.Cores/CPUs/Z80A/Interrupts.cs +++ b/BizHawk.Emulation.Cores/CPUs/Z80A/Interrupts.cs @@ -83,28 +83,27 @@ namespace BizHawk.Emulation.Cores.Components.Z80A } // Interrupt mode 2 uses the I vector combined with a byte on the data bus - // Again for now we assume only a 0 on the data bus and jump to (0xI00) - private void INTERRUPT_2(ushort src) + private void INTERRUPT_2() { cur_instr = new ushort[] {IDLE, IDLE, + FTCH_DB, + TR, Z, DB, + TR, W, I, + IDLE, DEC16, SPl, SPh, WR, SPl, SPh, PCh, IDLE, DEC16, SPl, SPh, - WR, SPl, SPh, PCl, - IDLE, - ASGN, PCl, 0, - TR, PCh, I, - IDLE, + WR, SPl, SPh, PCl, IDLE, - RD, Z, PCl, PCh, - INC16, PCl, PCh, + RD, PCl, Z, W, + INC16, Z, W, + IDLE, + RD, PCh, Z, W, IDLE, - RD, W, PCl, PCh, IDLE, - TR16, PCl, PCh, Z, W, OP }; } diff --git a/BizHawk.Emulation.Cores/CPUs/Z80A/NewDisassembler.cs b/BizHawk.Emulation.Cores/CPUs/Z80A/NewDisassembler.cs index a312975d78..8bc3938c7d 100644 --- a/BizHawk.Emulation.Cores/CPUs/Z80A/NewDisassembler.cs +++ b/BizHawk.Emulation.Cores/CPUs/Z80A/NewDisassembler.cs @@ -435,6 +435,12 @@ namespace BizHawk.Emulation.Cores.Components.Z80A addr += extra_inc; size = addr - start_addr; + // handle case of addr wrapping around at 16 bit boundary + if (addr < start_addr) + { + size = (0x10000 + addr) - start_addr; + } + return temp; } diff --git a/BizHawk.Emulation.Cores/CPUs/Z80A/Operations.cs b/BizHawk.Emulation.Cores/CPUs/Z80A/Operations.cs index cfaf517ec7..adf7452c29 100644 --- a/BizHawk.Emulation.Cores/CPUs/Z80A/Operations.cs +++ b/BizHawk.Emulation.Cores/CPUs/Z80A/Operations.cs @@ -8,31 +8,37 @@ namespace BizHawk.Emulation.Cores.Components.Z80A public void Read_Func(ushort dest, ushort src_l, ushort src_h) { Regs[dest] = ReadMemory((ushort)(Regs[src_l] | (Regs[src_h]) << 8)); + Regs[DB] = Regs[dest]; } public void I_Read_Func(ushort dest, ushort src_l, ushort src_h, ushort inc) { Regs[dest] = ReadMemory((ushort)((Regs[src_l] | (Regs[src_h] << 8)) + inc)); + Regs[DB] = Regs[dest]; } public void Write_Func(ushort dest_l, ushort dest_h, ushort src) { + Regs[DB] = Regs[src]; WriteMemory((ushort)(Regs[dest_l] | (Regs[dest_h] << 8)), (byte)Regs[src]); } public void I_Write_Func(ushort dest_l, ushort dest_h, ushort inc, ushort src) { + Regs[DB] = Regs[src]; WriteMemory((ushort)((Regs[dest_l] | (Regs[dest_h] << 8)) + inc), (byte)Regs[src]); } - public void OUT_Func(ushort dest, ushort src) + public void OUT_Func(ushort dest_l, ushort dest_h, ushort src) { - WriteHardware(Regs[dest], (byte)(Regs[src])); + Regs[DB] = Regs[src]; + WriteHardware((ushort)(Regs[dest_l] | (Regs[dest_h] << 8)), (byte)(Regs[src])); } - public void IN_Func(ushort dest, ushort src) + public void IN_Func(ushort dest, ushort src_l, ushort src_h) { - Regs[dest] = ReadHardware(Regs[src]); + Regs[dest] = ReadHardware((ushort)(Regs[src_l] | (Regs[src_h]) << 8)); + Regs[DB] = Regs[dest]; } public void TR_Func(ushort dest, ushort src) @@ -738,5 +744,10 @@ namespace BizHawk.Emulation.Cores.Components.Z80A Flag3 = (Regs[A] & 0x08) != 0; } } + + public void FTCH_DB_Func() + { + Regs[DB] = FetchDB(); + } } } diff --git a/BizHawk.Emulation.Cores/CPUs/Z80A/ReadMe.txt b/BizHawk.Emulation.Cores/CPUs/Z80A/ReadMe.txt index d11f79b637..10804fde94 100644 --- a/BizHawk.Emulation.Cores/CPUs/Z80A/ReadMe.txt +++ b/BizHawk.Emulation.Cores/CPUs/Z80A/ReadMe.txt @@ -1,8 +1,7 @@ TODO: -Mode 0 and 2 interrupts +Mode 0 Check T-cycle level memory access timing Check R register new tests for WZ Registers Memory refresh - IR is pushed onto the address bus at instruction start, does anything need this? -Data Bus - For mode zero and 2 interrupts, need a system that uses it to test diff --git a/BizHawk.Emulation.Cores/CPUs/Z80A/Registers.cs b/BizHawk.Emulation.Cores/CPUs/Z80A/Registers.cs index 628243c19e..14e447bc65 100644 --- a/BizHawk.Emulation.Cores/CPUs/Z80A/Registers.cs +++ b/BizHawk.Emulation.Cores/CPUs/Z80A/Registers.cs @@ -6,7 +6,6 @@ namespace BizHawk.Emulation.Cores.Components.Z80A public partial class Z80A { // registers - // note these are not constants. When shadows are used, they will be changed accordingly public ushort PCl = 0; public ushort PCh = 1; public ushort SPl = 2; @@ -40,6 +39,7 @@ namespace BizHawk.Emulation.Cores.Components.Z80A public ushort E_s = 29; public ushort H_s = 30; public ushort L_s = 31; + public ushort DB = 32; public ushort[] Regs = new ushort[36]; diff --git a/BizHawk.Emulation.Cores/CPUs/Z80A/Tables_Direct.cs b/BizHawk.Emulation.Cores/CPUs/Z80A/Tables_Direct.cs index 71b39d6018..e708b02bac 100644 --- a/BizHawk.Emulation.Cores/CPUs/Z80A/Tables_Direct.cs +++ b/BizHawk.Emulation.Cores/CPUs/Z80A/Tables_Direct.cs @@ -452,13 +452,13 @@ namespace BizHawk.Emulation.Cores.Components.Z80A { cur_instr = new ushort[] {IDLE, - RD, ALU, PCl, PCh, + RD, Z, PCl, PCh, IDLE, INC16, PCl, PCh, TR, W, A, - OUT, ALU, A, - TR, Z, ALU, - INC16, Z, ALU, + OUT, Z, W, A, + IDLE, + INC16, Z, W, IDLE, IDLE, OP}; @@ -469,9 +469,9 @@ namespace BizHawk.Emulation.Cores.Components.Z80A cur_instr = new ushort[] {IDLE, IDLE, - OUT, dest, src, - IDLE, TR16, Z, W, C, B, + OUT, Z, W, src, + IDLE, INC16, Z, W, IDLE, OP}; @@ -481,12 +481,12 @@ namespace BizHawk.Emulation.Cores.Components.Z80A { cur_instr = new ushort[] {IDLE, - RD, ALU, PCl, PCh, + RD, Z, PCl, PCh, IDLE, INC16, PCl, PCh, TR, W, A, - IN, A, ALU, - TR, Z, ALU, + IN, A, Z, W, + IDLE, INC16, Z, W, IDLE, IDLE, @@ -498,7 +498,7 @@ namespace BizHawk.Emulation.Cores.Components.Z80A cur_instr = new ushort[] {IDLE, IDLE, - IN, dest, src, + IN, dest, src, B, IDLE, TR16, Z, W, C, B, INC16, Z, W, diff --git a/BizHawk.Emulation.Cores/CPUs/Z80A/Tables_Indirect.cs b/BizHawk.Emulation.Cores/CPUs/Z80A/Tables_Indirect.cs index 4a1c46cc03..c53b013a86 100644 --- a/BizHawk.Emulation.Cores/CPUs/Z80A/Tables_Indirect.cs +++ b/BizHawk.Emulation.Cores/CPUs/Z80A/Tables_Indirect.cs @@ -419,7 +419,7 @@ private void IN_OP_R(ushort operation, ushort repeat_instr) { cur_instr = new ushort[] - {IN, ALU, C, + {IN, ALU, C, B, IDLE, WR, L, H, ALU, IDLE, @@ -438,7 +438,7 @@ cur_instr = new ushort[] {RD, ALU, L, H, IDLE, - OUT, C, ALU, + OUT, C, B, ALU, IDLE, IDLE, operation, L, H, diff --git a/BizHawk.Emulation.Cores/CPUs/Z80A/Z80A.cs b/BizHawk.Emulation.Cores/CPUs/Z80A/Z80A.cs index 46abee9809..f26582dd41 100644 --- a/BizHawk.Emulation.Cores/CPUs/Z80A/Z80A.cs +++ b/BizHawk.Emulation.Cores/CPUs/Z80A/Z80A.cs @@ -74,6 +74,7 @@ namespace BizHawk.Emulation.Cores.Components.Z80A public const ushort SET_FL_IR = 59; public const ushort I_BIT = 60; public const ushort HL_BIT = 61; + public const ushort FTCH_DB = 62; public byte temp_R; @@ -106,6 +107,11 @@ namespace BizHawk.Emulation.Cores.Components.Z80A public Func ReadHardware; public Action WriteHardware; + // Data Bus + // Interrupting Devices are responsible for putting a value onto the data bus + // for as long as the interrupt is valid + public Func FetchDB; + //this only calls when the first byte of an instruction is fetched. public Action OnExecFetch; @@ -190,9 +196,7 @@ namespace BizHawk.Emulation.Cores.Components.Z80A INTERRUPT_1(); break; case 2: - // Low byte of interrupt vector comes from data bus - // We'll assume it's zero for now - INTERRUPT_2(0); + INTERRUPT_2(); break; } IRQCallback(); @@ -315,9 +319,7 @@ namespace BizHawk.Emulation.Cores.Components.Z80A INTERRUPT_1(); break; case 2: - // Low byte of interrupt vector comes from data bus - // We'll assume it's zero for now - INTERRUPT_2(0); + INTERRUPT_2(); break; } IRQCallback(); @@ -339,6 +341,9 @@ namespace BizHawk.Emulation.Cores.Components.Z80A case HALT: halted = true; + // NOTE: Check how halt state effects the DB + Regs[DB] = 0xFF; + if (EI_pending > 0) { EI_pending--; @@ -382,9 +387,7 @@ namespace BizHawk.Emulation.Cores.Components.Z80A INTERRUPT_1(); break; case 2: - // Low byte of interrupt vector comes from data bus - // We'll assume it's zero for now - INTERRUPT_2(0); + INTERRUPT_2(); break; } IRQCallback(); @@ -569,10 +572,10 @@ namespace BizHawk.Emulation.Cores.Components.Z80A iff1 = iff2; break; case OUT: - OUT_Func(cur_instr[instr_pntr++], cur_instr[instr_pntr++]); + OUT_Func(cur_instr[instr_pntr++], cur_instr[instr_pntr++], cur_instr[instr_pntr++]); break; case IN: - IN_Func(cur_instr[instr_pntr++], cur_instr[instr_pntr++]); + IN_Func(cur_instr[instr_pntr++], cur_instr[instr_pntr++], cur_instr[instr_pntr++]); break; case NEG: NEG_8_Func(cur_instr[instr_pntr++]); @@ -595,8 +598,11 @@ namespace BizHawk.Emulation.Cores.Components.Z80A case SET_FL_IR: SET_FL_IR_Func(cur_instr[instr_pntr++]); break; + case FTCH_DB: + FTCH_DB_Func(); + break; } - totalExecutedCycles++; + TotalExecutedCycles++; } // tracer stuff @@ -651,8 +657,8 @@ namespace BizHawk.Emulation.Cores.Components.Z80A FlagI ? "E" : "e") }; } - // State Save/Load + // State Save/Load public void SyncState(Serializer ser) { ser.BeginSection("Z80A"); @@ -663,7 +669,7 @@ namespace BizHawk.Emulation.Cores.Components.Z80A ser.Sync("IFF1", ref iff1); ser.Sync("IFF2", ref iff2); ser.Sync("Halted", ref halted); - ser.Sync("ExecutedCycles", ref totalExecutedCycles); + ser.Sync("ExecutedCycles", ref TotalExecutedCycles); ser.Sync("EI_pending", ref EI_pending); ser.Sync("instruction_pointer", ref instr_pntr); diff --git a/BizHawk.Emulation.Cores/Calculator/TI83.IDebuggable.cs b/BizHawk.Emulation.Cores/Calculator/TI83.IDebuggable.cs index c5d795fcd5..d4e292ad18 100644 --- a/BizHawk.Emulation.Cores/Calculator/TI83.IDebuggable.cs +++ b/BizHawk.Emulation.Cores/Calculator/TI83.IDebuggable.cs @@ -12,35 +12,35 @@ namespace BizHawk.Emulation.Cores.Calculators { 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, + ["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 }; } @@ -49,85 +49,85 @@ namespace BizHawk.Emulation.Cores.Calculators { 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); + 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; } } @@ -145,6 +145,6 @@ namespace BizHawk.Emulation.Cores.Calculators return false; } - public int TotalExecutedCycles => _cpu.TotalExecutedCycles; + public int TotalExecutedCycles => (int)_cpu.TotalExecutedCycles; } } diff --git a/BizHawk.Emulation.Cores/Calculator/TI83.cs b/BizHawk.Emulation.Cores/Calculator/TI83.cs index cfe82f74eb..403f0fa7b8 100644 --- a/BizHawk.Emulation.Cores/Calculator/TI83.cs +++ b/BizHawk.Emulation.Cores/Calculator/TI83.cs @@ -137,6 +137,8 @@ namespace BizHawk.Emulation.Cores.Calculators private void WriteHardware(ushort addr, byte value) { + addr &= 0xFF; + switch (addr) { case 0: // PORT_LINK @@ -232,6 +234,8 @@ namespace BizHawk.Emulation.Cores.Calculators private byte ReadHardware(ushort addr) { + addr &= 0xFF; + switch (addr) { case 0: // PORT_LINK diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IBeeperDevice.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IBeeperDevice.cs new file mode 100644 index 0000000000..7367e3948f --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IBeeperDevice.cs @@ -0,0 +1,24 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public interface IBeeperDevice + { + void Init(int sampleRate, int tStatesPerFrame); + + void ProcessPulseValue(bool fromTape, bool earPulse); + + void StartFrame(); + + void EndFrame(); + + void SetTapeMode(bool tapeMode); + + void SyncState(Serializer ser); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IJoystick.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IJoystick.cs new file mode 100644 index 0000000000..6421e99af4 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IJoystick.cs @@ -0,0 +1,44 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// 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/SinclairSpectrum/Hardware/Abstraction/IKeyboard.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IKeyboard.cs new file mode 100644 index 0000000000..77282f1dfa --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IKeyboard.cs @@ -0,0 +1,84 @@ + + +using BizHawk.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Represents a spectrum keyboard + /// + public interface IKeyboard : IPortIODevice + { + /// + /// The calling spectrumbase class + /// + SpectrumBase _machine { get; } + + /// + /// The keyboard matrix for a particular spectrum 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 + /// + int[] KeyLine { get; set; } + + /// + /// Resets the line status + /// + void ResetLineStatus(); + + /// + /// There are some slight differences in how PortIN and PortOUT functions + /// between Issue2 and Issue3 keyboards (16k/48k spectrum only) + /// It is possible that some very old games require Issue2 emulation + /// + bool IsIssue2Keyboard { get; set; } + + /// + /// Sets the spectrum key status + /// + /// + /// + void SetKeyStatus(string key, bool isPressed); + + /// + /// Gets the status of a spectrum key + /// + /// + /// + bool GetKeyStatus(string key); + + /// + /// Returns the query byte + /// + /// + /// + byte GetLineStatus(byte lines); + + /// + /// Reads a keyboard byte + /// + /// + /// + byte ReadKeyboardByte(ushort addr); + + /// + /// Looks up a key in the keyboard matrix and returns the relevent byte value + /// + /// + /// + byte GetByteFromKeyMatrix(string key); + + + + void SyncState(Serializer ser); + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IPSG.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IPSG.cs new file mode 100644 index 0000000000..a6028aa2d6 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IPSG.cs @@ -0,0 +1,67 @@ +using BizHawk.Common; +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.SinclairSpectrum +{ + /// + /// Represents a PSG device (in this case an AY-3-891x) + /// + public interface IPSG : ISoundProvider, IPortIODevice + { + /// + /// Initlization routine + /// + /// + /// + void Init(int sampleRate, int tStatesPerFrame); + + /// + /// 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(); + + /// + /// 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/SinclairSpectrum/Hardware/Abstraction/IPortIODevice.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IPortIODevice.cs new file mode 100644 index 0000000000..2fbd89a0db --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Abstraction/IPortIODevice.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// 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/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs new file mode 100644 index 0000000000..cd7d7a2cde --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Datacorder/DatacorderDevice.cs @@ -0,0 +1,876 @@ +using BizHawk.Common; +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Represents the tape device (or build-in datacorder as it was called +2 and above) + /// + public class DatacorderDevice : IPortIODevice + { + #region Construction + + private SpectrumBase _machine { get; set; } + private Z80A _cpu { get; set; } + private IBeeperDevice _buzzer { get; set; } + + /// + /// Default constructor + /// + public DatacorderDevice() + { + + } + + /// + /// Initializes the datacorder device + /// + /// + public void Init(SpectrumBase machine) + { + _machine = machine; + _cpu = _machine.CPU; + _buzzer = machine.BuzzerDevice; + } + + #endregion + + #region State Information + + /// + /// 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; + public bool AutoPlay + { + get { return _machine.Spectrum.Settings.AutoLoadTape; } + set { _autoPlay = value; MonitorReset(); } + } + + + #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() + { + //if (TapeIsPlaying && AutoPlay) + //FlashLoad(); + } + + #endregion + + #region Tape Controls + + /// + /// Starts the tape playing from the beginning of the current block + /// + public void Play() + { + if (_tapeIsPlaying) + return; + + _machine.BuzzerDevice.SetTapeMode(true); + + _machine.Spectrum.OSD_TapePlaying(); + + // 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 + 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.BuzzerDevice.SetTapeMode(false); + + _machine.Spectrum.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.Spectrum.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.Spectrum.OSD_TapeNextBlock(sbd.ToString()); + else + _machine.Spectrum.OSD_TapePrevBlock(sbd.ToString()); + + CurrentDataBlockIndex = targetBlockId; + } + + /// + /// Inserts a new tape and sets up the tape device accordingly + /// + /// + public void LoadTape(byte[] tapeData) + { + // attempt TZX deserialization + TzxSerializer tzxSer = new TzxSerializer(this); + try + { + tzxSer.DeSerialize(tapeData); + return; + } + catch (Exception ex) + { + // TAP format not detected + var e = ex; + } + + // attempt TAP deserialization + TapSerializer tapSer = new TapSerializer(this); + try + { + tapSer.DeSerialize(tapeData); + return; + } + catch (Exception ex) + { + // TAP format not detected + var e = ex; + } + } + + /// + /// Resets the tape + /// + public void Reset() + { + RTZ(); + } + + #endregion + + #region Tape Device Methods + + /// + /// 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; + + bool is48k = _machine.IsIn48kMode(); + + // check whether tape is actually playing + if (_tapeIsPlaying == 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; + + // flip the current state + currentState = !currentState; + + if (_position == 0 && _tapeIsPlaying) + { + // start of block + + // 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.Spectrum.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.Spectrum.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) + { + // 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.Spectrum.OSD_TapeStoppedAuto(); + + if (_currentDataBlockIndex >= _dataBlocks.Count()) + RTZ(); + else + { + Stop(); + } + + _monitorTimeOut = 2000; + + break; + case TapeCommand.STOP_THE_TAPE_48K: + if (is48k) + { + _machine.Spectrum.OSD_TapeStoppedAuto(); + + if (_currentDataBlockIndex >= _dataBlocks.Count()) + RTZ(); + else + { + Stop(); + } + + _monitorTimeOut = 2000; + } + 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]; + + } + + // update lastCycle and return currentstate + _lastCycle = cpuCycle - (long)cycles; + + // play the buzzer + _buzzer.ProcessPulseValue(false, currentState); + + return currentState; + } + + /// + /// Flash loading implementation + /// (Deterministic Emulation must be FALSE) + /// + private bool FlashLoad() + { + // deterministic emulation must = false + //if (_machine.Spectrum.SyncSettings.DeterministicEmulation) + //return; + + var util = _machine.Spectrum; + + if (_currentDataBlockIndex < 0) + _currentDataBlockIndex = 0; + + if (_currentDataBlockIndex >= DataBlocks.Count) + return false; + + //var val = GetEarBit(_cpu.TotalExecutedCycles); + //_buzzer.ProcessPulseValue(true, val); + + ushort addr = _cpu.RegPC; + + if (_machine.Spectrum.SyncSettings.DeterministicEmulation) + { + + } + + var tb = DataBlocks[_currentDataBlockIndex]; + var tData = tb.BlockData; + + if (tData == null || tData.Length < 2) + { + // skip this + return false; + } + + var toRead = tData.Length - 1; + + if (toRead < _cpu.Regs[_cpu.E] + (_cpu.Regs[_cpu.D] << 8)) + { + + } + else + { + toRead = _cpu.Regs[_cpu.E] + (_cpu.Regs[_cpu.D] << 8); + } + + if (toRead <= 0) + return false; + + var parity = tData[0]; + + if (parity != _cpu.Regs[_cpu.F_s] + (_cpu.Regs[_cpu.A_s] << 8) >> 8) + return false; + + util.SetCpuRegister("Shadow AF", 0x0145); + + for (var i = 0; i < toRead; i++) + { + var v = tData[i + 1]; + _cpu.Regs[_cpu.L] = v; + parity ^= v; + var d = (ushort)(_cpu.Regs[_cpu.Ixl] + (_cpu.Regs[_cpu.Ixh] << 8) + 1); + _machine.WriteBus(d, v); + } + var pc = (ushort)0x05DF; + + if (_cpu.Regs[_cpu.E] + (_cpu.Regs[_cpu.D] << 8) == toRead && + toRead + 1 < tData.Length) + { + var v = tData[toRead + 1]; + _cpu.Regs[_cpu.L] = v; + parity ^= v; + _cpu.Regs[_cpu.B] = 0xB0; + } + else + { + _cpu.Regs[_cpu.L] = 1; + _cpu.Regs[_cpu.B] = 0; + _cpu.Regs[_cpu.F] = 0x50; + _cpu.Regs[_cpu.A] = parity; + pc = 0x05EE; + } + + _cpu.Regs[_cpu.H] = parity; + var de = _cpu.Regs[_cpu.E] + (_cpu.Regs[_cpu.D] << 8); + util.SetCpuRegister("DE", de - toRead); + var ix = _cpu.Regs[_cpu.Ixl] + (_cpu.Regs[_cpu.Ixh] << 8); + util.SetCpuRegister("IX", ix + toRead); + + util.SetCpuRegister("PC", pc); + + _currentDataBlockIndex++; + + return true; + + } + + #endregion + + #region TapeMonitor + + private long _lastINCycle = 0; + private int _monitorCount; + private int _monitorTimeOut; + private ushort _monitorLastPC; + private ushort[] _monitorLastRegs = new ushort[7]; + + /// + /// Resets the TapeMonitor + /// + private void MonitorReset() + { + _lastINCycle = 0; + _monitorCount = 0; + _monitorLastPC = 0; + _monitorLastRegs = null; + } + + /// + /// An iteration of the monitor process + /// + public void MonitorRead() + { + long cpuCycle = _cpu.TotalExecutedCycles; + int delta = (int)(cpuCycle - _lastINCycle); + _lastINCycle = cpuCycle; + + var nRegs = new ushort[] + { + _cpu.Regs[_cpu.A], + _cpu.Regs[_cpu.B], + _cpu.Regs[_cpu.C], + _cpu.Regs[_cpu.D], + _cpu.Regs[_cpu.E], + _cpu.Regs[_cpu.H], + _cpu.Regs[_cpu.L] + }; + + if (delta > 0 && + delta < 96 && + _cpu.RegPC == _monitorLastPC && + _monitorLastRegs != null) + { + int dCnt = 0; + int dVal = 0; + + for (int i = 0; i < nRegs.Length; i++) + { + if (_monitorLastRegs[i] != nRegs[i]) + { + dVal = _monitorLastRegs[i] - nRegs[i]; + dCnt++; + } + } + + if (dCnt == 1 && + (dVal == 1 || dVal == -1)) + { + _monitorCount++; + + if (_monitorCount >= 16 && _cpu.RegPC == 1523 && _machine.Spectrum.Settings.AutoLoadTape) + { + if (!_tapeIsPlaying) + { + Play(); + _machine.Spectrum.OSD_TapePlayingAuto(); + } + + _monitorTimeOut = 250; + } + } + else + { + _monitorCount = 0; + } + } + + _monitorLastRegs = nRegs; + _monitorLastPC = _cpu.RegPC; + } + + public void AutoStopTape() + { + if (_tapeIsPlaying) + return; + + if (!_machine.Spectrum.Settings.AutoLoadTape) + return; + + Stop(); + _machine.Spectrum.OSD_TapeStoppedAuto(); + } + + public void AutoStartTape() + { + if (_tapeIsPlaying) + return; + + if (!_machine.Spectrum.Settings.AutoLoadTape) + return; + + Play(); + _machine.Spectrum.OSD_TapePlayingAuto(); + } + + private void MonitorFrame() + { + if (_tapeIsPlaying && _machine.Spectrum.Settings.AutoLoadTape) + { + _monitorTimeOut--; + + if (_monitorTimeOut < 0) + { + // does not work properly - disabled for now (handled elsewhere) + + //Stop(); + //_machine.Spectrum.OSD_TapeStoppedAuto(); + } + } + } + + #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(ushort port, ref int result) + { + if (TapeIsPlaying) + { + GetEarBit(_cpu.TotalExecutedCycles); + } + if (currentState) + { + result |= TAPE_BIT; + } + else + { + result &= ~TAPE_BIT; + } + + MonitorRead(); + + /* + + if (TapeIsPlaying) + { + if (GetEarBit(_cpu.TotalExecutedCycles)) + { + result &= ~(TAPE_BIT); // reset is EAR ON + } + else + { + result |= (TAPE_BIT); // set is EAR Off + } + } + else + { + if (_machine.KeyboardDevice.IsIssue2Keyboard) + { + if ((_machine.LASTULAOutByte & (EAR_BIT + MIC_BIT)) == 0) + { + result &= ~(TAPE_BIT); + } + else + { + result |= (TAPE_BIT); + } + } + else + { + if ((_machine.LASTULAOutByte & EAR_BIT) == 0) + { + result &= ~(TAPE_BIT); + } + else + { + result |= TAPE_BIT; + } + } + } + + */ + + return true; + } + + /// + /// Device responds to an OUT instruction + /// + /// + /// + /// + public bool WritePort(ushort port, int result) + { + if (!TapeIsPlaying) + { + currentState = ((byte)result & 0x10) != 0; + } + + return true; + } + + #endregion + + #region State Serialization + + /// + /// Bizhawk state serialization + /// + /// + public void SyncState(Serializer ser) + { + ser.BeginSection("DatacorderDevice"); + 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("_lastINCycle", ref _lastINCycle); + ser.Sync("_monitorCount", ref _monitorCount); + ser.Sync("_monitorTimeOut", ref _monitorTimeOut); + ser.Sync("_monitorLastPC", ref _monitorLastPC); + ser.Sync("_monitorLastRegs", ref _monitorLastRegs, false); + ser.EndSection(); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/CursorJoystick.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/CursorJoystick.cs new file mode 100644 index 0000000000..edd54dd9b1 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/CursorJoystick.cs @@ -0,0 +1,116 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Cursor joystick + /// Maps to a combination of 0xf7fe and 0xeffe + /// + public class CursorJoystick : IJoystick + { + private int _joyLine; + private SpectrumBase _machine; + + #region Construction + + public CursorJoystick(SpectrumBase machine, int playerNumber) + { + _machine = machine; + _joyLine = 0; + _playerNumber = playerNumber; + + ButtonCollection = new List + { + "P" + _playerNumber + " Left", + "P" + _playerNumber + " Right", + "P" + _playerNumber + " Down", + "P" + _playerNumber + " Up", + "P" + _playerNumber + " Button", + }.ToArray(); + } + + private List btnLookups = new List + { + "Key 5", // left + "Key 8", // right + "Key 6", // down + "Key 7", // up + "Key 0", // fire + }; + + #endregion + + #region IJoystick + + public JoystickType JoyType => JoystickType.Cursor; + + public string[] ButtonCollection { get; set; } + + private int _playerNumber; + public int PlayerNumber + { + get { return _playerNumber; } + set { _playerNumber = value; } + } + + /// + /// Sets the joystick line based on key pressed + /// + /// + /// + public void SetJoyInput(string key, bool isPressed) + { + var pos = GetBitPos(key); + + if (isPressed) + { + _machine.KeyboardDevice.SetKeyStatus(btnLookups[pos], true); + } + else + { + if (_machine.KeyboardDevice.GetKeyStatus(btnLookups[pos])) + { + // key is already pressed elswhere - leave it as is + } + else + { + // key is safe to unpress + _machine.KeyboardDevice.SetKeyStatus(btnLookups[pos], false); + } + } + } + + /// + /// Gets the state of a particular joystick binding + /// + /// + /// + public bool GetJoyInput(string key) + { + var pos = GetBitPos(key); + if (_machine == null) + return false; + + var l = _machine.KeyboardDevice.GetKeyStatus(btnLookups[pos]); + return l; + } + + #endregion + + /// + /// Gets the bit position of a particular joystick binding from the matrix + /// + /// + /// + public int GetBitPos(string key) + { + int index = Array.IndexOf(ButtonCollection, key); + return index; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/KempstonJoystick.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/KempstonJoystick.cs new file mode 100644 index 0000000000..880e388f3a --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/KempstonJoystick.cs @@ -0,0 +1,108 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public class KempstonJoystick : IJoystick + { + private int _joyLine; + private SpectrumBase _machine; + + #region Construction + + public KempstonJoystick(SpectrumBase machine, int playerNumber) + { + _machine = machine; + _joyLine = 0; + _playerNumber = playerNumber; + + ButtonCollection = new List + { + "P" + _playerNumber + " Right", + "P" + _playerNumber + " Left", + "P" + _playerNumber + " Down", + "P" + _playerNumber + " Up", + "P" + _playerNumber + " Button", + }.ToArray(); + } + + #endregion + + #region IJoystick + + public JoystickType JoyType => JoystickType.Kempston; + + public string[] ButtonCollection { get; set; } + + private int _playerNumber; + public int PlayerNumber + { + get { return _playerNumber; } + set { _playerNumber = value; } + } + + /// + /// Sets the joystick line based on key pressed + /// + /// + /// + public void SetJoyInput(string key, bool isPressed) + { + var pos = GetBitPos(key); + if (isPressed) + _joyLine |= (1 << pos); + else + _joyLine &= ~(1 << pos); + } + + /// + /// Gets the state of a particular joystick binding + /// + /// + /// + public bool GetJoyInput(string key) + { + var pos = GetBitPos(key); + return (_joyLine & (1 << pos)) != 0; + } + + #endregion + + /// + /// Active bits high + /// 0 0 0 F U D L R + /// + public int JoyLine + { + get { return _joyLine; } + set { _joyLine = value; } + } + + /// + /// Gets the bit position of a particular joystick binding from the matrix + /// + /// + /// + public int GetBitPos(string key) + { + int index = Array.IndexOf(ButtonCollection, key); + return index; + } + + + /* + public readonly string[] _bitPos = new string[] + { + "P1 Right", + "P1 Left", + "P1 Down", + "P1 Up", + "P1 Button" + }; + */ + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/NullJoystick.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/NullJoystick.cs new file mode 100644 index 0000000000..ba2d3ea467 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/NullJoystick.cs @@ -0,0 +1,107 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// A null joystick object + /// + public class NullJoystick : IJoystick + { + private int _joyLine; + private SpectrumBase _machine; + + #region Construction + + public NullJoystick(SpectrumBase machine, int playerNumber) + { + _machine = machine; + _joyLine = 0; + _playerNumber = playerNumber; + + ButtonCollection = new List + { + + }.ToArray(); + } + + #endregion + + #region IJoystick + + public JoystickType JoyType => JoystickType.NULL; + + public string[] ButtonCollection { get; set; } + + private int _playerNumber; + public int PlayerNumber + { + get { return _playerNumber; } + set { _playerNumber = value; } + } + + /// + /// Sets the joystick line based on key pressed + /// + /// + /// + public void SetJoyInput(string key, bool isPressed) + { + var pos = GetBitPos(key); + if (isPressed) + _joyLine |= (1 << pos); + else + _joyLine &= ~(1 << pos); + } + + /// + /// Gets the state of a particular joystick binding + /// + /// + /// + public bool GetJoyInput(string key) + { + var pos = GetBitPos(key); + return (_joyLine & (1 << pos)) != 0; + } + + #endregion + + /// + /// Active bits high + /// 0 0 0 F U D L R + /// + public int JoyLine + { + get { return _joyLine; } + set { _joyLine = value; } + } + + /// + /// Gets the bit position of a particular joystick binding from the matrix + /// + /// + /// + public int GetBitPos(string key) + { + int index = Array.IndexOf(ButtonCollection, key); + return index; + } + + + /* + public readonly string[] _bitPos = new string[] + { + "P1 Right", + "P1 Left", + "P1 Down", + "P1 Up", + "P1 Button" + }; + */ + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/SinclairJoystick1.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/SinclairJoystick1.cs new file mode 100644 index 0000000000..a3789b9e5d --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/SinclairJoystick1.cs @@ -0,0 +1,115 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Sinclair Joystick LEFT + /// Just maps to the standard keyboard and is read the same (from port 0xf7fe) + /// + public class SinclairJoystick1 : IJoystick + { + private int _joyLine; + private SpectrumBase _machine; + + #region Construction + + public SinclairJoystick1(SpectrumBase machine, int playerNumber) + { + _machine = machine; + _joyLine = 0; + _playerNumber = playerNumber; + + ButtonCollection = new List + { + "P" + _playerNumber + " Left", + "P" + _playerNumber + " Right", + "P" + _playerNumber + " Down", + "P" + _playerNumber + " Up", + "P" + _playerNumber + " Button", + }.ToArray(); + } + + private List btnLookups = new List + { + "Key 1", // left + "Key 2", // right + "Key 3", // down + "Key 4", // up + "Key 5", // fire + }; + + #endregion + + #region IJoystick + + public JoystickType JoyType => JoystickType.SinclairLEFT; + + public string[] ButtonCollection { get; set; } + + private int _playerNumber; + public int PlayerNumber + { + get { return _playerNumber; } + set { _playerNumber = value; } + } + + /// + /// Sets the joystick line based on key pressed + /// + /// + /// + public void SetJoyInput(string key, bool isPressed) + { + var pos = GetBitPos(key); + + if (isPressed) + { + _machine.KeyboardDevice.SetKeyStatus(btnLookups[pos], true); + } + else + { + if (_machine.KeyboardDevice.GetKeyStatus(btnLookups[pos])) + { + // key is already pressed elswhere - leave it as is + } + else + { + // key is safe to unpress + _machine.KeyboardDevice.SetKeyStatus(btnLookups[pos], false); + } + } + } + + /// + /// Gets the state of a particular joystick binding + /// + /// + /// + public bool GetJoyInput(string key) + { + var pos = GetBitPos(key); + if (_machine == null) + return false; + + return _machine.KeyboardDevice.GetKeyStatus(btnLookups[pos]); + } + + #endregion + + /// + /// Gets the bit position of a particular joystick binding from the matrix + /// + /// + /// + public int GetBitPos(string key) + { + int index = Array.IndexOf(ButtonCollection, key); + return index; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/SinclairJoystick2.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/SinclairJoystick2.cs new file mode 100644 index 0000000000..82d9fd9857 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/SinclairJoystick2.cs @@ -0,0 +1,115 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Sinclair Joystick RIGHT + /// Just maps to the standard keyboard and is read the same (from port 0xeffe) + /// + public class SinclairJoystick2 : IJoystick + { + private int _joyLine; + private SpectrumBase _machine; + + #region Construction + + public SinclairJoystick2(SpectrumBase machine, int playerNumber) + { + _machine = machine; + _joyLine = 0; + _playerNumber = playerNumber; + + ButtonCollection = new List + { + "P" + _playerNumber + " Left", + "P" + _playerNumber + " Right", + "P" + _playerNumber + " Down", + "P" + _playerNumber + " Up", + "P" + _playerNumber + " Button", + }.ToArray(); + } + + private List btnLookups = new List + { + "Key 6", // left + "Key 7", // right + "Key 8", // down + "Key 9", // up + "Key 0", // fire + }; + + #endregion + + #region IJoystick + + public JoystickType JoyType => JoystickType.SinclairRIGHT; + + public string[] ButtonCollection { get; set; } + + private int _playerNumber; + public int PlayerNumber + { + get { return _playerNumber; } + set { _playerNumber = value; } + } + + /// + /// Sets the joystick line based on key pressed + /// + /// + /// + public void SetJoyInput(string key, bool isPressed) + { + var pos = GetBitPos(key); + + if (isPressed) + { + _machine.KeyboardDevice.SetKeyStatus(btnLookups[pos], true); + } + else + { + if (_machine.KeyboardDevice.GetKeyStatus(btnLookups[pos])) + { + // key is already pressed elswhere - leave it as is + } + else + { + // key is safe to unpress + _machine.KeyboardDevice.SetKeyStatus(btnLookups[pos], false); + } + } + } + + /// + /// Gets the state of a particular joystick binding + /// + /// + /// + public bool GetJoyInput(string key) + { + var pos = GetBitPos(key); + if (_machine == null) + return false; + + return _machine.KeyboardDevice.GetKeyStatus(btnLookups[pos]); + } + + #endregion + + /// + /// Gets the bit position of a particular joystick binding from the matrix + /// + /// + /// + public int GetBitPos(string key) + { + int index = Array.IndexOf(ButtonCollection, key); + return index; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/StandardKeyboard.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/StandardKeyboard.cs new file mode 100644 index 0000000000..0616003465 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Input/StandardKeyboard.cs @@ -0,0 +1,416 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The 48k keyboard device + /// + public class StandardKeyboard : IKeyboard + { + public SpectrumBase _machine { get; set; } + private byte[] LineStatus; + private string[] _keyboardMatrix; + private int[] _keyLine; + private bool _isIssue2Keyboard; + private string[] _nonMatrixKeys; + + public bool IsIssue2Keyboard + { + get { return _isIssue2Keyboard; } + set { _isIssue2Keyboard = value; } + } + + public int[] KeyLine + { + get { return _keyLine; } + set { _keyLine = value; } + } + + public string[] KeyboardMatrix + { + get { return _keyboardMatrix; } + set { _keyboardMatrix = value; } + } + + public string[] NonMatrixKeys + { + get { return _nonMatrixKeys; } + set { _nonMatrixKeys = value; } + } + + public StandardKeyboard(SpectrumBase machine) + { + _machine = machine; + + KeyboardMatrix = new string[] + { + // 0xfefe - 0 - 4 + "Key Caps Shift", "Key Z", "Key X", "Key C", "Key V", + // 0xfdfe - 5 - 9 + "Key A", "Key S", "Key D", "Key F", "Key G", + // 0xfbfe - 10 - 14 + "Key Q", "Key W", "Key E", "Key R", "Key T", + // 0xf7fe - 15 - 19 + "Key 1", "Key 2", "Key 3", "Key 4", "Key 5", + // 0xeffe - 20 - 24 + "Key 0", "Key 9", "Key 8", "Key 7", "Key 6", + // 0xdffe - 25 - 29 + "Key P", "Key O", "Key I", "Key U", "Key Y", + // 0xbffe - 30 - 34 + "Key Return", "Key L", "Key K", "Key J", "Key H", + // 0x7ffe - 35 - 39 + "Key Space", "Key Symbol Shift", "Key M", "Key N", "Key B" + }; + + var nonMatrix = new List(); + + foreach (var key in _machine.Spectrum.ZXSpectrumControllerDefinition.BoolButtons) + { + if (!KeyboardMatrix.Any(s => s == key)) + nonMatrix.Add(key); + } + + NonMatrixKeys = nonMatrix.ToArray(); + + LineStatus = new byte[8]; + _keyLine = new int[] { 255, 255, 255, 255, 255, 255, 255, 255 }; + IsIssue2Keyboard = true; + } + + public void SetKeyStatus(string key, bool isPressed) + { + int k = GetByteFromKeyMatrix(key); + + if (k != 255) + { + var lineIndex = k / 5; + var lineMask = 1 << k % 5; + + _keyLine[lineIndex] = isPressed + ? (byte)(_keyLine[lineIndex] & ~lineMask) + : (byte)(_keyLine[lineIndex] | lineMask); + } + + /* + if (isPressed) + { + switch (k) + { + // 0xfefe - 0 - 4 + case 0: _keyLine[0] = (_keyLine[0] & ~(0x1)); break; + case 1: _keyLine[0] = (_keyLine[0] & ~(0x02)); break; + case 2: _keyLine[0] = (_keyLine[0] & ~(0x04)); break; + case 3: _keyLine[0] = (_keyLine[0] & ~(0x08)); break; + case 4: _keyLine[0] = (_keyLine[0] & ~(0x10)); break; + // 0xfdfe - 5 - 9 + case 5: _keyLine[1] = (_keyLine[1] & ~(0x1)); break; + case 6: _keyLine[1] = (_keyLine[1] & ~(0x02)); break; + case 7: _keyLine[1] = (_keyLine[1] & ~(0x04)); break; + case 8: _keyLine[1] = (_keyLine[1] & ~(0x08)); break; + case 9: _keyLine[1] = (_keyLine[1] & ~(0x10)); break; + // 0xfbfe - 10 - 14 + case 10: _keyLine[2] = (_keyLine[2] & ~(0x1)); break; + case 11: _keyLine[2] = (_keyLine[2] & ~(0x02)); break; + case 12: _keyLine[2] = (_keyLine[2] & ~(0x04)); break; + case 13: _keyLine[2] = (_keyLine[2] & ~(0x08)); break; + case 14: _keyLine[2] = (_keyLine[2] & ~(0x10)); break; + // 0xf7fe - 15 - 19 + case 15: _keyLine[3] = (_keyLine[3] & ~(0x1)); break; + case 16: _keyLine[3] = (_keyLine[3] & ~(0x02)); break; + case 17: _keyLine[3] = (_keyLine[3] & ~(0x04)); break; + case 18: _keyLine[3] = (_keyLine[3] & ~(0x08)); break; + case 19: _keyLine[3] = (_keyLine[3] & ~(0x10)); break; + // 0xeffe - 20 - 24 + case 20: _keyLine[4] = (_keyLine[4] & ~(0x1)); break; + case 21: _keyLine[4] = (_keyLine[4] & ~(0x02)); break; + case 22: _keyLine[4] = (_keyLine[4] & ~(0x04)); break; + case 23: _keyLine[4] = (_keyLine[4] & ~(0x08)); break; + case 24: _keyLine[4] = (_keyLine[4] & ~(0x10)); break; + // 0xdffe - 25 - 29 + case 25: _keyLine[5] = (_keyLine[5] & ~(0x1)); break; + case 26: _keyLine[5] = (_keyLine[5] & ~(0x02)); break; + case 27: _keyLine[5] = (_keyLine[5] & ~(0x04)); break; + case 28: _keyLine[5] = (_keyLine[5] & ~(0x08)); break; + case 29: _keyLine[5] = (_keyLine[5] & ~(0x10)); break; + // 0xbffe - 30 - 34 + case 30: _keyLine[6] = (_keyLine[6] & ~(0x1)); break; + case 31: _keyLine[6] = (_keyLine[6] & ~(0x02)); break; + case 32: _keyLine[6] = (_keyLine[6] & ~(0x04)); break; + case 33: _keyLine[6] = (_keyLine[6] & ~(0x08)); break; + case 34: _keyLine[6] = (_keyLine[6] & ~(0x10)); break; + // 0x7ffe - 35 - 39 + case 35: _keyLine[7] = (_keyLine[7] & ~(0x1)); break; + case 36: _keyLine[7] = (_keyLine[7] & ~(0x02)); break; + case 37: _keyLine[7] = (_keyLine[7] & ~(0x04)); break; + case 38: _keyLine[7] = (_keyLine[7] & ~(0x08)); break; + case 39: _keyLine[7] = (_keyLine[7] & ~(0x10)); break; + } + } + else + { + switch (k) + { + // 0xfefe - 0 - 4 + case 0: _keyLine[0] = (_keyLine[0] | (0x1)); break; + case 1: _keyLine[0] = (_keyLine[0] | (0x02)); break; + case 2: _keyLine[0] = (_keyLine[0] | (0x04)); break; + case 3: _keyLine[0] = (_keyLine[0] | (0x08)); break; + case 4: _keyLine[0] = (_keyLine[0] | (0x10)); break; + // 0xfdfe - 5 - 9 + case 5: _keyLine[1] = (_keyLine[1] | (0x1)); break; + case 6: _keyLine[1] = (_keyLine[1] | (0x02)); break; + case 7: _keyLine[1] = (_keyLine[1] | (0x04)); break; + case 8: _keyLine[1] = (_keyLine[1] | (0x08)); break; + case 9: _keyLine[1] = (_keyLine[1] | (0x10)); break; + // 0xfbfe - 10 - 14 + case 10: _keyLine[2] = (_keyLine[2] | (0x1)); break; + case 11: _keyLine[2] = (_keyLine[2] | (0x02)); break; + case 12: _keyLine[2] = (_keyLine[2] | (0x04)); break; + case 13: _keyLine[2] = (_keyLine[2] | (0x08)); break; + case 14: _keyLine[2] = (_keyLine[2] | (0x10)); break; + // 0xf7fe - 15 - 19 + case 15: _keyLine[3] = (_keyLine[3] | (0x1)); break; + case 16: _keyLine[3] = (_keyLine[3] | (0x02)); break; + case 17: _keyLine[3] = (_keyLine[3] | (0x04)); break; + case 18: _keyLine[3] = (_keyLine[3] | (0x08)); break; + case 19: _keyLine[3] = (_keyLine[3] | (0x10)); break; + // 0xeffe - 20 - 24 + case 20: _keyLine[4] = (_keyLine[4] | (0x1)); break; + case 21: _keyLine[4] = (_keyLine[4] | (0x02)); break; + case 22: _keyLine[4] = (_keyLine[4] | (0x04)); break; + case 23: _keyLine[4] = (_keyLine[4] | (0x08)); break; + case 24: _keyLine[4] = (_keyLine[4] | (0x10)); break; + // 0xdffe - 25 - 29 + case 25: _keyLine[5] = (_keyLine[5] | (0x1)); break; + case 26: _keyLine[5] = (_keyLine[5] | (0x02)); break; + case 27: _keyLine[5] = (_keyLine[5] | (0x04)); break; + case 28: _keyLine[5] = (_keyLine[5] | (0x08)); break; + case 29: _keyLine[5] = (_keyLine[5] | (0x10)); break; + // 0xbffe - 30 - 34 + case 30: _keyLine[6] = (_keyLine[6] | (0x1)); break; + case 31: _keyLine[6] = (_keyLine[6] | (0x02)); break; + case 32: _keyLine[6] = (_keyLine[6] | (0x04)); break; + case 33: _keyLine[6] = (_keyLine[6] | (0x08)); break; + case 34: _keyLine[6] = (_keyLine[6] | (0x10)); break; + // 0x7ffe - 35 - 39 + case 35: _keyLine[7] = (_keyLine[7] | (0x1)); break; + case 36: _keyLine[7] = (_keyLine[7] | (0x02)); break; + case 37: _keyLine[7] = (_keyLine[7] | (0x04)); break; + case 38: _keyLine[7] = (_keyLine[7] | (0x08)); break; + case 39: _keyLine[7] = (_keyLine[7] | (0x10)); break; + } + } + + */ + + // Combination keys that are not in the keyboard matrix + // but are available on the Spectrum+, 128k +2 & +3 + // (GetByteFromKeyMatrix() should return 255) + // Processed after the matrix keys - only presses handled (unpressed get done above) + if (k == 255) + { + if (isPressed) + { + switch (key) + { + // Delete key (simulates Caps Shift + 0) + case "Key Delete": + _keyLine[0] = _keyLine[0] & ~(0x1); + _keyLine[4] = _keyLine[4] & ~(0x1); + break; + // Cursor left (simulates Caps Shift + 5) + case "Key Left Cursor": + _keyLine[0] = _keyLine[0] & ~(0x1); + _keyLine[3] = _keyLine[3] & ~(0x10); + break; + // Cursor right (simulates Caps Shift + 8) + case "Key Right Cursor": + _keyLine[0] = _keyLine[0] & ~(0x1); + _keyLine[4] = _keyLine[4] & ~(0x04); + break; + // Cursor up (simulates Caps Shift + 7) + case "Key Up Cursor": + _keyLine[0] = _keyLine[0] & ~(0x1); + _keyLine[4] = _keyLine[4] & ~(0x08); + break; + // Cursor down (simulates Caps Shift + 6) + case "Key Down Cursor": + _keyLine[0] = _keyLine[0] & ~(0x1); + _keyLine[4] = _keyLine[4] & ~(0x10); + break; + } + } + } + } + + public bool GetKeyStatus(string key) + { + byte keyByte = GetByteFromKeyMatrix(key); + var lineIndex = keyByte / 5; + var lineMask = 1 << keyByte % 5; + + return (_keyLine[lineIndex] & lineMask) == 0; + } + + public void ResetLineStatus() + { + lock (this) + { + for (int i = 0; i < KeyLine.Length; i++) + KeyLine[i] = 255; + } + } + + public byte GetLineStatus(byte lines) + { + lock(this) + { + byte status = 0; + lines = (byte)~lines; + var lineIndex = 0; + while (lines > 0) + { + if ((lines & 0x01) != 0) + status |= (byte)_keyLine[lineIndex]; + lineIndex++; + lines >>= 1; + } + var result = (byte)status; + + return result; + } + + /* + switch (lines) + { + case 0xfe: return (byte)KeyLine[0]; + case 0xfd: return (byte)KeyLine[1]; + case 0xfb: return (byte)KeyLine[2]; + case 0xf7: return (byte)KeyLine[3]; + case 0xef: return (byte)KeyLine[4]; + case 0xdf: return (byte)KeyLine[5]; + case 0xbf: return (byte)KeyLine[6]; + case 0x7f: return (byte)KeyLine[7]; + default: return 0; + } + */ + } + + public byte ReadKeyboardByte(ushort addr) + { + return GetLineStatus((byte)(addr >> 8)); + } + + public byte GetByteFromKeyMatrix(string key) + { + int index = Array.IndexOf(KeyboardMatrix, key); + return (byte)index; + } + + + #region IPortIODevice + + /// + /// Device responds to an IN instruction + /// + /// + /// + /// + public bool ReadPort(ushort port, ref int result) + { + /* + The high byte indicates which half-row of keys is being polled + A zero on one of these lines selects a particular half-row of five keys: + + IN: Reads keys (bit 0 to bit 4 inclusive) + 0xfefe SHIFT, Z, X, C, V 0xeffe 0, 9, 8, 7, 6 + 0xfdfe A, S, D, F, G 0xdffe P, O, I, U, Y + 0xfbfe Q, W, E, R, T 0xbffe ENTER, L, K, J, H + 0xf7fe 1, 2, 3, 4, 5 0x7ffe SPACE, SYM SHFT, M, N, B + + A zero in one of the five lowest bits means that the corresponding key is pressed. If more than one address line + is made low, the result is the logical AND of all single inputs, so a zero in a bit means that at least one of the + appropriate keys is pressed. For example, only if each of the five lowest bits of the result from reading from Port 00FE + (for instance by XOR A/IN A,(FE)) is one, no key is pressed + */ + + if ((port & 0x0001) != 0) + return false; + + if ((port & 0x8000) == 0) + { + result &= KeyLine[7]; + } + + if ((port & 0x4000) == 0) + { + result &= KeyLine[6]; + } + + if ((port & 0x2000) == 0) + { + result &= KeyLine[5]; + } + + if ((port & 0x1000) == 0) + { + result &= KeyLine[4]; + } + + if ((port & 0x800) == 0) + { + result &= KeyLine[3]; + } + + if ((port & 0x400) == 0) + { + result &= KeyLine[2]; + } + + if ((port & 0x200) == 0) + { + result &= KeyLine[1]; + } + + if ((port & 0x100) == 0) + { + result &= KeyLine[0]; + } + + // mask out lower 4 bits + result = result & 0x1f; + + // set bit 5 & 7 to 1 + result = result | 0xa0; + + return true; + } + + /// + /// Device responds to an OUT instruction + /// + /// + /// + /// + public bool WritePort(ushort port, int result) + { + // not implemented + return false; + } + + #endregion + + public void SyncState(Serializer ser) + { + ser.BeginSection("Keyboard"); + ser.Sync("LineStatus", ref LineStatus, false); + ser.Sync("_keyLine", ref _keyLine, false); + ser.EndSection(); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Rom/RomData.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Rom/RomData.cs new file mode 100644 index 0000000000..0f0f12110b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/Rom/RomData.cs @@ -0,0 +1,99 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + + public class RomData + { + /// + /// ROM Contents + /// + public byte[] RomBytes + { + get { return _romBytes; } + set { _romBytes = value; } + } + + /// + /// Useful ROM addresses that are needed during tape operations + /// + public ushort SaveBytesRoutineAddress + { + get { return _saveBytesRoutineAddress; } + set { _saveBytesRoutineAddress = value; } + } + public ushort LoadBytesRoutineAddress + { + get { return _loadBytesRoutineAddress; } + set { _loadBytesRoutineAddress = value; } + } + public ushort SaveBytesResumeAddress + { + get { return _saveBytesResumeAddress; } + set { _saveBytesResumeAddress = value; } + } + public ushort LoadBytesResumeAddress + { + get { return _loadBytesResumeAddress; } + set { _loadBytesResumeAddress = value; } + } + public ushort LoadBytesInvalidHeaderAddress + { + get { return _loadBytesInvalidHeaderAddress; } + set { _loadBytesInvalidHeaderAddress = value; } + } + + private byte[] _romBytes; + private ushort _saveBytesRoutineAddress; + private ushort _loadBytesRoutineAddress; + private ushort _saveBytesResumeAddress; + private ushort _loadBytesResumeAddress; + private ushort _loadBytesInvalidHeaderAddress; + + + public static RomData InitROM(MachineType machineType, byte[] rom) + { + RomData RD = new RomData(); + RD.RomBytes = new byte[rom.Length]; + RD.RomBytes = rom; + + switch (machineType) + { + case MachineType.ZXSpectrum48: + RD.SaveBytesRoutineAddress = 0x04C2; + RD.SaveBytesResumeAddress = 0x0000; + RD.LoadBytesRoutineAddress = 0x0808; //0x0556; //0x056C; + RD.LoadBytesResumeAddress = 0x05E2; + RD.LoadBytesInvalidHeaderAddress = 0x05B6; + break; + + case MachineType.ZXSpectrum128: + RD.SaveBytesRoutineAddress = 0x04C2; + RD.SaveBytesResumeAddress = 0x0000; + RD.LoadBytesRoutineAddress = 0x0808; //0x0556; //0x056C; + RD.LoadBytesResumeAddress = 0x05E2; + RD.LoadBytesInvalidHeaderAddress = 0x05B6; + break; + } + + return RD; + } + + public void SyncState(Serializer ser) + { + ser.BeginSection("RomData"); + ser.Sync("RomBytes", ref _romBytes, false); + ser.Sync("_saveBytesRoutineAddress", ref _saveBytesRoutineAddress); + ser.Sync("_loadBytesRoutineAddress", ref _loadBytesRoutineAddress); + ser.Sync("_saveBytesResumeAddress", ref _saveBytesResumeAddress); + ser.Sync("_loadBytesResumeAddress", ref _loadBytesResumeAddress); + ser.Sync("_loadBytesInvalidHeaderAddress", ref _loadBytesInvalidHeaderAddress); + ser.EndSection(); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/AYChip.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/AYChip.cs new file mode 100644 index 0000000000..34515cd5eb --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/AYChip.cs @@ -0,0 +1,802 @@ + +using BizHawk.Common; +using BizHawk.Emulation.Common; +using System; +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// AY-3-8912 Emulated Device + /// + /// 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 AYChip : IPSG + { + #region Device Fields + + /// + /// The emulated machine (passed in via constructor) + /// + private SpectrumBase _machine; + + private int _tStatesPerFrame; + private int _sampleRate; + private int _samplesPerFrame; + private int _tStatesPerSample; + private short[] _audioBuffer; + private int _audioBufferIndex; + private int _lastStateRendered; + + #endregion + + #region Construction & Initialization + + /// + /// Main constructor + /// + public AYChip(SpectrumBase machine) + { + _machine = machine; + } + + /// + /// Initialises the AY chip + /// + public void Init(int sampleRate, int tStatesPerFrame) + { + InitTiming(sampleRate, tStatesPerFrame); + UpdateVolume(); + Reset(); + } + + #endregion + + #region IPortIODevice + + public bool ReadPort(ushort port, ref int value) + { + if (port != 0xfffd) + { + // port read is not addressing this device + return false; + } + + value = PortRead(); + + return true; + } + + public bool WritePort(ushort port, int value) + { + if (port == 0xfffd) + { + // register select + SelectedRegister = value & 0x0f; + return true; + } + else if (port == 0xbffd) + { + // Update the audiobuffer based on the current CPU cycle + // (this process the previous data BEFORE writing to the currently selected register) + int d = (int)(_machine.CurrentFrameCycle); + BufferUpdate(d); + + // write to register + PortWrite(value); + return true; + } + return false; + } + + #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); + */ + } + + /// + /// Reads the value from the currently selected register + /// + /// + public int PortRead() + { + return _registers[_activeRegister]; + } + + /// + /// Writes to the currently selected register + /// + /// + public void PortWrite(int value) + { + 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); + 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; + } + } + + /// + /// 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; + + /// + /// The frequency of the AY chip + /// + private static int _chipFrequency = 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[] { 100,100, 100,100, 100,100 }, + // 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 = 50; + + /// + /// 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() + { + var vol = (ulong)0xFFFF * (ulong)_volume / 100UL; + _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)); + } + } + } + + private int mult_const; + + /// + /// Initializes timing information for the frame + /// + /// + /// + private void InitTiming(int sampleRate, int frameTactCount) + { + _sampleRate = sampleRate; + _tStatesPerFrame = frameTactCount; + + _tStatesPerSample = 79; //(int)Math.Round(((double)_tStatesPerFrame * 50D) / + //(16D * (double)_sampleRate), + //MidpointRounding.AwayFromZero); + + _samplesPerFrame = _tStatesPerFrame / _tStatesPerSample; + _audioBuffer = new short[_samplesPerFrame * 2]; //[_sampleRate / 50]; + _audioBufferIndex = 0; + + mult_const = ((_chipFrequency / 8) << 14) / _machine.ULADevice.ClockSpeed; + + var aytickspercputick = (double)_machine.ULADevice.ClockSpeed / (double)_chipFrequency; + int ayCyclesPerSample = (int)((double)_tStatesPerSample * (double)aytickspercputick); + } + + /// + /// 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; + + int toEnd = ((bufferLength * cycle) / _tStatesPerFrame); + + // loop through the number of samples we need to render + while(_audioBufferIndex < toEnd) + { + // run the AY chip processing at the correct resolution + for (int i = 0; i < _tStatesPerSample / 14; i++) + { + 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]; + } + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + 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("_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("_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); + } + + ser.EndSection(); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/Buzzer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/Buzzer.cs new file mode 100644 index 0000000000..7fc0a71715 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/Buzzer.cs @@ -0,0 +1,330 @@ + +using BizHawk.Common; +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Represents the piezoelectric buzzer used in the Spectrum to produce sound + /// The beeper is controlled by rapidly toggling bit 4 of port &FE + /// + /// For the purposes of emulation this devices is locked to a frame + /// a list of Pulses is built up over the course of the frame and outputted at the end of the frame + /// + public class Buzzer : ISoundProvider, IBeeperDevice + { + /// + /// Supplied values are right for 48K spectrum + /// These will deviate for 128k and up (as there are more T-States per frame) + /// + //public int SampleRate = 44100; //35000; + //public int SamplesPerFrame = 882; //699; + //public int TStatesPerSample = 79; //100; + + /// + /// Sample Rate + /// This usually has to be 44100 for ISoundProvider + /// + public int SampleRate + { + get { return _sampleRate; } + set { _sampleRate = value; } + } + + /// + /// Number of samples in one frame + /// + public int SamplesPerFrame + { + get { return _samplesPerFrame; } + set { _samplesPerFrame = value; } + } + + /// + /// Number of TStates in each sample + /// + public int TStatesPerSample + { + get { return _tStatesPerSample; } + set { _tStatesPerSample = value; } + } + + private SpectrumBase _machine; + + /// + /// State fields + /// + private long _frameStart; + private bool _tapeMode; + private long _tStatesPerFrame; + private int _sampleRate; + private int _samplesPerFrame; + private int _tStatesPerSample; + + /// + /// Pulses collected during the last frame + /// + public List Pulses { get; private set; } + + /// + /// The last pulse + /// + public bool LastPulse { get; set; } + + /// + /// The last T-State (cpu cycle) that the last pulse was received + /// + public long LastPulseTState { get; set; } + + #region Construction & Initialisation + + public Buzzer(SpectrumBase machine) + { + _machine = machine; + } + + /// + /// Initialises the buzzer + /// + public void Init(int sampleRate, int tStatesPerFrame) + { + _sampleRate = sampleRate; + _tStatesPerFrame = tStatesPerFrame; + _tStatesPerSample = 79; + _samplesPerFrame = (int)_tStatesPerFrame / _tStatesPerSample; + + /* + + // set the tstatesperframe + _tStatesPerFrame = tStatesPerFrame; + + // calculate actual refresh rate + double refresh = (double)_machine.ULADevice.ClockSpeed / (double)_tStatesPerFrame; + + // how many samples per frame are expected by ISoundProvider (at 44.1KHz) + _samplesPerFrame = 880;// (int)((double)sampleRate / (double)refresh); + + // set the sample rate + _sampleRate = sampleRate; + + // calculate samples per frame (what ISoundProvider will be expecting at 44100) + //_samplesPerFrame = (int)((double)_tStatesPerFrame / (double)refresh); + + // calculate tstates per sameple + _tStatesPerSample = 79;// _tStatesPerFrame / _samplesPerFrame; + + /* + + + + + + // get divisors + var divs = from a in Enumerable.Range(2, _tStatesPerFrame / 2) + where _tStatesPerFrame % a == 0 + select a; + + // get the highest int value under 120 (this will be TStatesPerSample) + _tStatesPerSample = divs.Where(a => a < 100).Last(); + + // get _samplesPerFrame + _samplesPerFrame = _tStatesPerFrame / _tStatesPerSample; + + */ + Pulses = new List(1000); + } + + #endregion + + /// + /// When the pulse value from the EAR output changes it is processed here + /// + /// + /// + public void ProcessPulseValue(bool fromTape, bool earPulse) + { + if (!_machine._renderSound) + return; + + if (!fromTape && _tapeMode) + { + // tape mode is active but the pulse value came from an OUT instruction + // do not process the value + //return; + } + + if (earPulse == LastPulse) + { + // no change detected + return; + } + + // set the lastpulse + LastPulse = earPulse; + + // get where we are in the frame + var currentULACycle = _machine.CurrentFrameCycle; + var currentBuzzerCycle = currentULACycle <= _tStatesPerFrame ? currentULACycle : _tStatesPerFrame; + var length = currentBuzzerCycle - LastPulseTState; + + if (length == 0) + { + // the first T-State has changed the pulse + // do not add it + } + else if (length > 0) + { + // add the pulse + Pulse p = new Pulse + { + State = !earPulse, + Length = length + }; + Pulses.Add(p); + } + + // set the last pulse tstate + LastPulseTState = currentBuzzerCycle; + } + + /// + /// New frame starts + /// + public void StartFrame() + { + //DiscardSamples(); + Pulses.Clear(); + LastPulseTState = 0; + } + + /// + /// Frame is completed + /// + public void EndFrame() + { + // store the last pulse information + if (LastPulseTState <= _tStatesPerFrame - 1) + { + Pulse p = new Pulse + { + State = LastPulse, + Length = _tStatesPerFrame - LastPulseTState + }; + Pulses.Add(p); + } + + // create the sample array + var firstSampleOffset = _frameStart % TStatesPerSample == 0 ? 0 : TStatesPerSample - (_frameStart + TStatesPerSample) % TStatesPerSample; + var samplesInFrame = (_tStatesPerFrame - firstSampleOffset - 1) / TStatesPerSample + 1; + var samples = new short[samplesInFrame]; + + // convert pulses to samples + var sampleIndex = 0; + var currentEnd = _frameStart; + + foreach (var pulse in Pulses) + { + var firstSample = currentEnd % TStatesPerSample == 0 + ? currentEnd : currentEnd + TStatesPerSample - currentEnd % TStatesPerSample; + + for (var i = firstSample; i < currentEnd + pulse.Length; i += TStatesPerSample) + { + if (_tapeMode) + samples[sampleIndex++] = pulse.State ? (short)(short.MaxValue / 6) : (short)0; + else + samples[sampleIndex++] = pulse.State ? (short)(short.MaxValue / 3) : (short)0; + } + + currentEnd += pulse.Length; + } + + // fill the _sampleBuffer for ISoundProvider + soundBufferContains = (int)samplesInFrame; + + if (soundBuffer.Length != soundBufferContains) + soundBuffer = new short[soundBufferContains]; + + samples.CopyTo(soundBuffer, 0); + + _frameStart += _tStatesPerFrame; + } + + /// + /// When the spectrum is set to receive tape input, the EAR output on the ULA is disabled + /// (so no buzzer sound is emitted) + /// + /// + public void SetTapeMode(bool tapeMode) + { + _tapeMode = tapeMode; + } + + + #region ISoundProvider + + private short[] soundBuffer = new short[882]; + private int soundBufferContains = 0; + + 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() + { + soundBufferContains = 0; + soundBuffer = new short[SamplesPerFrame]; + } + + public void GetSamplesSync(out short[] samples, out int nsamp) + { + // convert to stereo + short[] stereoBuffer = new short[soundBufferContains * 2]; + int index = 0; + for (int i = 0; i < soundBufferContains; i++) + { + stereoBuffer[index++] = soundBuffer[i]; + stereoBuffer[index++] = soundBuffer[i]; + } + + samples = stereoBuffer; + nsamp = _samplesPerFrame; // soundBufferContains; + } + + #endregion + + + public void SyncState(Serializer ser) + { + ser.BeginSection("Buzzer"); + ser.Sync("_frameStart", ref _frameStart); + ser.Sync("_tapeMode", ref _tapeMode); + ser.Sync("_tStatesPerFrame", ref _tStatesPerFrame); + ser.Sync("_sampleRate", ref _sampleRate); + ser.Sync("_samplesPerFrame", ref _samplesPerFrame); + ser.Sync("_tStatesPerSample", ref _tStatesPerSample); + + ser.Sync("soundBuffer", ref soundBuffer, false); + ser.Sync("soundBufferContains", ref soundBufferContains); + ser.EndSection(); + } + + + } + + +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/Pulse.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/Pulse.cs new file mode 100644 index 0000000000..140c1d136a --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Hardware/SoundOuput/Pulse.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The MIC and EAR pins in the spectrum deal in on/off pulses of varying lengths + /// This struct therefore represents 1 of these pulses + /// + public struct Pulse + { + /// + /// True: High State + /// False: Low State + /// + public bool State { get; set; } + + /// + /// Pulse length in Z80 T-States (cycles) + /// + public long Length { get; set; } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/MachineType.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/MachineType.cs new file mode 100644 index 0000000000..cee9f524da --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/MachineType.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public enum MachineType + { + /// + /// Original Sinclair Spectrum 16K model + /// + ZXSpectrum16, + + /// + /// Sinclair Spectrum 48K model + /// + ZXSpectrum48, + + /// + /// Sinclair Spectrum 128K model + /// + ZXSpectrum128, + + /// + /// Sinclair Spectrum 128 +2 model + /// + ZXSpectrum128Plus2, + + /// + /// Sinclair Spectrum 128 +2a model (same as the +3 just without disk drive) + /// + ZXSpectrum128Plus2a, + + /// + /// Sinclair Spectrum 128 +3 model + /// + ZXSpectrum128Plus3 + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Input.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Input.cs new file mode 100644 index 0000000000..978d815c52 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Input.cs @@ -0,0 +1,292 @@ + +using System.Collections.Generic; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Handles all ZX-level input + /// + public abstract partial class SpectrumBase + { + 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 HardResetStr = "Hard Reset"; + string SoftResetStr = "Soft 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_HardReset = false; + bool pressed_SoftReset = false; + + /// + /// Cycles through all the input callbacks + /// This should be done once per frame + /// + public void PollInput() + { + Spectrum.InputCallbacks.Call(); + + lock (this) + { + // parse single keyboard matrix keys + for (var i = 0; i < KeyboardDevice.KeyboardMatrix.Length; i++) + { + string key = KeyboardDevice.KeyboardMatrix[i]; + bool prevState = KeyboardDevice.GetKeyStatus(key); + bool currState = Spectrum._controller.IsPressed(key); + + if (currState != prevState) + KeyboardDevice.SetKeyStatus(key, currState); + } + + // non matrix keys + foreach (string k in KeyboardDevice.NonMatrixKeys) + { + if (!k.StartsWith("Key")) + continue; + + bool currState = Spectrum._controller.IsPressed(k); + + KeyboardDevice.SetKeyStatus(k, currState); + } + + // J1 + foreach (string j in JoystickCollection[0].ButtonCollection) + { + bool prevState = JoystickCollection[0].GetJoyInput(j); + bool currState = Spectrum._controller.IsPressed(j); + + if (currState != prevState) + JoystickCollection[0].SetJoyInput(j, currState); + } + + // J2 + foreach (string j in JoystickCollection[1].ButtonCollection) + { + bool prevState = JoystickCollection[1].GetJoyInput(j); + bool currState = Spectrum._controller.IsPressed(j); + + if (currState != prevState) + JoystickCollection[1].SetJoyInput(j, currState); + } + + // J3 + foreach (string j in JoystickCollection[2].ButtonCollection) + { + bool prevState = JoystickCollection[2].GetJoyInput(j); + bool currState = Spectrum._controller.IsPressed(j); + + if (currState != prevState) + JoystickCollection[2].SetJoyInput(j, currState); + } + } + + // Tape control + if (Spectrum._controller.IsPressed(Play)) + { + if (!pressed_Play) + { + Spectrum.OSD_FireInputMessage(Play); + TapeDevice.Play(); + pressed_Play = true; + } + } + else + pressed_Play = false; + + if (Spectrum._controller.IsPressed(Stop)) + { + if (!pressed_Stop) + { + Spectrum.OSD_FireInputMessage(Stop); + TapeDevice.Stop(); + pressed_Stop = true; + } + } + else + pressed_Stop = false; + + if (Spectrum._controller.IsPressed(RTZ)) + { + if (!pressed_RTZ) + { + Spectrum.OSD_FireInputMessage(RTZ); + TapeDevice.RTZ(); + pressed_RTZ = true; + } + } + else + pressed_RTZ = false; + + if (Spectrum._controller.IsPressed(Record)) + { + + } + if (Spectrum._controller.IsPressed(NextTape)) + { + if (!pressed_NextTape) + { + Spectrum.OSD_FireInputMessage(NextTape); + TapeMediaIndex++; + pressed_NextTape = true; + } + } + else + pressed_NextTape = false; + + if (Spectrum._controller.IsPressed(PrevTape)) + { + if (!pressed_PrevTape) + { + Spectrum.OSD_FireInputMessage(PrevTape); + TapeMediaIndex--; + pressed_PrevTape = true; + } + } + else + pressed_PrevTape = false; + + if (Spectrum._controller.IsPressed(NextBlock)) + { + if (!pressed_NextBlock) + { + Spectrum.OSD_FireInputMessage(NextBlock); + TapeDevice.SkipBlock(true); + pressed_NextBlock = true; + } + } + else + pressed_NextBlock = false; + + if (Spectrum._controller.IsPressed(PrevBlock)) + { + if (!pressed_PrevBlock) + { + Spectrum.OSD_FireInputMessage(PrevBlock); + TapeDevice.SkipBlock(false); + pressed_PrevBlock = true; + } + } + else + pressed_PrevBlock = false; + + if (Spectrum._controller.IsPressed(TapeStatus)) + { + if (!pressed_TapeStatus) + { + //Spectrum.OSD_FireInputMessage(TapeStatus); + Spectrum.OSD_ShowTapeStatus(); + pressed_TapeStatus = true; + } + } + else + pressed_TapeStatus = false; + + if (Spectrum._controller.IsPressed(HardResetStr)) + { + if (!pressed_HardReset) + { + HardReset(); + pressed_HardReset = true; + } + } + else + pressed_HardReset = false; + + if (Spectrum._controller.IsPressed(SoftResetStr)) + { + if (!pressed_SoftReset) + { + SoftReset(); + pressed_SoftReset = true; + } + } + else + pressed_SoftReset = false; + } + + /// + /// Instantiates the joysticks array + /// + /// + protected void InitJoysticks(List joys) + { + List jCollection = new List(); + + for (int i = 0; i < joys.Count(); i++) + { + jCollection.Add(InstantiateJoystick(joys[i], i + 1)); + } + + JoystickCollection = jCollection.ToArray(); + + for (int i = 0; i < JoystickCollection.Length; i++) + { + Spectrum.OSD_FireInputMessage("Joystick " + (i + 1) + ": " + JoystickCollection[i].JoyType.ToString()); + } + } + + /// + /// Instantiates a new IJoystick object + /// + /// + /// + /// + public IJoystick InstantiateJoystick(JoystickType type, int playerNumber) + { + switch (type) + { + case JoystickType.Kempston: + return new KempstonJoystick(this, playerNumber); + case JoystickType.Cursor: + return new CursorJoystick(this, playerNumber); + case JoystickType.SinclairLEFT: + return new SinclairJoystick1(this, playerNumber); + case JoystickType.SinclairRIGHT: + return new SinclairJoystick2(this, playerNumber); + case JoystickType.NULL: + return new NullJoystick(this, playerNumber); + } + + return null; + } + + /// + /// Returns a IJoystick object depending on the type (or null if not found) + /// + /// + /// + protected IJoystick LocateUniqueJoystick(JoystickType type) + { + return JoystickCollection.Where(a => a.JoyType == type).FirstOrDefault(); + } + + /// + /// 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/SinclairSpectrum/Machine/SpectrumBase.Media.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs new file mode 100644 index 0000000000..de2f5206d7 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Media.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public abstract partial class SpectrumBase + { + // until +3 disk drive is emulated, we assume that incoming files are tape images + + /// + /// The tape or disk image(s) that are passed in from the main ZXSpectrum class + /// + protected List mediaImages { get; set; } + + /// + /// Tape images + /// + protected List tapeImages { get; set; } + + /// + /// Disk images + /// + protected 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; + LoadDiskMedia(); + } + } + + /// + /// Called on first instantiation (and subsequent core reboots) + /// + /// + protected void InitializeMedia(List files) + { + mediaImages = files; + LoadAllMedia(); + Spectrum.OSD_TapeInit(); + } + + /// + /// Attempts to load all media into the relevant structures + /// + protected void LoadAllMedia() + { + tapeImages = new List(); + diskImages = new List(); + + foreach (var m in mediaImages) + { + switch (IdentifyMedia(m)) + { + case SpectrumMediaType.Tape: + tapeImages.Add(m); + break; + case SpectrumMediaType.Disk: + diskImages.Add(m); + break; + } + } + + 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() + { + throw new NotImplementedException("+3 disk drive device not yet implemented"); + } + + /// + /// Identifies and sorts the various media types + /// + /// + private SpectrumMediaType 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")) + { + // spectrum .dsk disk file + return SpectrumMediaType.Disk; + } + if (hdr.ToUpper().StartsWith("FDI")) + { + // spectrum .fdi disk file + return SpectrumMediaType.Disk; + } + + // tape checking + if (hdr.ToUpper().StartsWith("ZXTAPE!")) + { + // spectrum .tzx tape file + return SpectrumMediaType.Tape; + } + + // if we get this far, assume a .tap file + return SpectrumMediaType.Tape; + } + } + + public enum SpectrumMediaType + { + None, + Tape, + Disk + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Memory.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Memory.cs new file mode 100644 index 0000000000..a23c2796ff --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Memory.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Memory * + /// + public abstract partial class SpectrumBase + { + #region Memory Fields & Properties + + /// + /// ROM Banks + /// + public byte[] ROM0 = new byte[0x4000]; + public byte[] ROM1 = new byte[0x4000]; + public byte[] ROM2 = new byte[0x4000]; + public byte[] ROM3 = new byte[0x4000]; + + /// + /// RAM Banks + /// + public byte[] RAM0 = new byte[0x4000]; // Bank 0 + public byte[] RAM1 = new byte[0x4000]; // Bank 1 + public byte[] RAM2 = new byte[0x4000]; // Bank 2 + public byte[] RAM3 = new byte[0x4000]; // Bank 3 + public byte[] RAM4 = new byte[0x4000]; // Bank 4 + public byte[] RAM5 = new byte[0x4000]; // Bank 5 + public byte[] RAM6 = new byte[0x4000]; // Bank 6 + public byte[] RAM7 = new byte[0x4000]; // Bank 7 + + /// + /// Signs that the shadow screen is now displaying + /// Note: normal screen memory in RAM5 is not altered, the ULA just outputs Screen1 instead (RAM7) + /// + protected bool SHADOWPaged; + + /// + /// Index of the current RAM page + /// /// 128k, +2/2a and +3 only + /// + public int RAMPaged; + + /// + /// Signs that all paging is disabled + /// If this is TRUE, then 128k and above machines need a hard reset before paging is allowed again + /// + protected bool PagingDisabled; + + /// + /// Index of the currently paged ROM + /// 128k, +2/2a and +3 only + /// + protected int ROMPaged; + public virtual int _ROMpaged + { + get { return ROMPaged; } + set { ROMPaged = value; } + } + + /* + * +3/+2A only + */ + + /// + /// High bit of the ROM selection (in normal paging mode) + /// + protected bool ROMhigh = false; + + /// + /// Low bit of the ROM selection (in normal paging mode) + /// + protected bool ROMlow = false; + + /// + /// Signs that the +2a/+3 special paging mode is activated + /// + protected bool SpecialPagingMode; + + /// + /// Index of the current special paging mode (0-3) + /// + protected int PagingConfiguration; + + #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 virtual void WriteBus(ushort addr, byte value) + { + throw new NotImplementedException("Must be overriden"); + } + + /// + /// 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) + { + var value = ReadBus((ushort)((addr & 0x3FFF) + 0x4000)); + return value; + } + + #endregion + + #region Helper Methods + + /// + /// Detects whether this is a 48k machine (or a 128k in 48k mode) + /// + /// + public virtual bool IsIn48kMode() + { + if (this.GetType() == typeof(ZX48) || + this.GetType() == typeof(ZX16) || + PagingDisabled) + { + return true; + } + else + return false; + } + + /// + /// Monitors ROM access + /// Used to auto start/stop the tape device when appropriate + /// + /// + public virtual void TestForTapeTraps(int addr) + { + if (TapeDevice.TapeIsPlaying) + { + // THE 'ERROR' RESTART + if (addr == 8) + { + TapeDevice?.AutoStopTape(); + return; + } + + // THE 'ED-ERROR' SUBROUTINE + if (addr == 4223) + { + TapeDevice?.AutoStopTape(); + return; + } + + // THE 'ERROR-2' ROUTINE + if (addr == 83) + { + TapeDevice?.AutoStopTape(); + return; + } + + // THE 'MASKABLE INTERRUPT' ROUTINE + if (addr == 56) + { + //TapeDevice?.AutoStopTape(); + return; + } + } + else + { + // THE 'LD-BYTES' SUBROUTINE + if (addr == 1366) + { + TapeDevice?.AutoStartTape(); + return; + } + + // THE 'LD-EDGE-2' AND 'LD-EDGE-1' SUBROUTINES + if (addr == 1507) + { + TapeDevice?.AutoStartTape(); + return; + } + } + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Port.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Port.cs new file mode 100644 index 0000000000..3b53dd28e3 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.Port.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Port Access * + /// + public abstract partial class SpectrumBase + { + /// + /// The last OUT data that was sent to the ULA + /// + protected byte LastULAOutByte; + public byte LASTULAOutByte + { + get { return LastULAOutByte; } + set { LastULAOutByte = value; } + } + + /// + /// 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); + + /// + /// Increments the CPU totalCycles counter by the tStates value specified + /// + /// + public virtual void PortContention(int tStates) + { + CPU.TotalExecutedCycles += tStates; + } + + /// + /// Simulates IO port contention based on the supplied address + /// This method is for 48k and 128k/+2 machines only and should be overridden for other models + /// + /// + public virtual void ContendPortAddress(ushort addr) + { + /* + It takes four T states for the Z80 to read a value from an I/O port, or write a value to a port. As is the case with memory access, + this can be lengthened by the ULA. There are two effects which occur here: + + If the port address being accessed has its low bit reset, the ULA is required to supply the result, which leads to a delay if it is + currently busy handling the screen. + The address of the port being accessed is placed on the data bus. If this is in the range 0x4000 to 0x7fff, the ULA treats this as an + attempted access to contended memory and therefore introduces a delay. If the port being accessed is between 0xc000 and 0xffff, + this effect does not apply, even on a 128K machine if a contended memory bank is paged into the range 0xc000 to 0xffff. + + These two effects combine to lead to the following contention patterns: + + High byte | | + in 40 - 7F? | Low bit | Contention pattern + ------------+---------+------------------- + No | Reset | N:1, C:3 + No | Set | N:4 + Yes | Reset | C:1, C:3 + Yes | Set | C:1, C:1, C:1, C:1 + + The 'Contention pattern' column should be interpreted from left to right. An "N:n" entry means that no delay is applied at this cycle, and the Z80 continues uninterrupted for 'n' T states. A "C:n" entry means that the ULA halts the Z80; the delay is exactly the same as would occur for a contended memory access at this cycle (eg 6 T states at cycle 14335, 5 at 14336, etc on the 48K machine). After this delay, the Z80 then continues for 'n' cycles. + */ + + // is the low bit reset (i.e. is this addressing the ULA)? + bool lowBit = (addr & 0x0001) != 0; + + if ((addr & 0xc000) == 0x4000 || (addr & 0xc000) == 0xC000) + { + // high byte is in 40 - 7F + if (lowBit) + { + // lowbit is set + // C:1, C:1, C:1, C:1 + for (int i = 0; i < 4; i++) + { + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + CPU.TotalExecutedCycles++; + } + } + else + { + // low bit is reset + // C:1, C:3 + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + CPU.TotalExecutedCycles++; + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + CPU.TotalExecutedCycles += 3; + } + } + else + { + // high byte is NOT in 40 - 7F + if (lowBit) + { + // lowbit is set + // C:1, C:1, C:1, C:1 + CPU.TotalExecutedCycles += 4; + } + else + { + // lowbit is reset + // N:1, C:3 + CPU.TotalExecutedCycles++; + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + CPU.TotalExecutedCycles += 3; + } + } + } + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs new file mode 100644 index 0000000000..ac557b4c99 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/SpectrumBase.cs @@ -0,0 +1,325 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The abstract class that all emulated models will inherit from + /// * Main properties / fields / contruction* + /// + public abstract partial class SpectrumBase + { + #region Devices + + /// + /// The calling ZXSpectrum class (piped in via constructor) + /// + public ZXSpectrum Spectrum { 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 emulated ULA device + /// + public ULABase ULADevice { get; set; } + + /// + /// The spectrum buzzer/beeper + /// + public IBeeperDevice BuzzerDevice { get; set; } + + /// + /// Device representing the AY-3-8912 chip found in the 128k and up spectrums + /// + public IPSG AYDevice { get; set; } + + /// + /// The spectrum keyboard + /// + public virtual IKeyboard KeyboardDevice { get; set; } + + /// + /// The spectrum datacorder device + /// + public virtual DatacorderDevice TapeDevice { get; set; } + + /// + /// Holds the currently selected joysticks + /// + public virtual IJoystick[] JoystickCollection { get; set; } + + /// + /// Signs whether the disk motor is on or off + /// + protected bool DiskMotorState; + + /// + /// +3/2a printer port strobe + /// + protected bool PrinterPortStrobe; + + #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 => 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) + { + InputRead = false; + _render = render; + _renderSound = renderSound; + + FrameCompleted = false; + + TapeDevice.StartFrame(); + + if (_renderSound) + { + BuzzerDevice.StartFrame(); + if (AYDevice != null) + AYDevice.StartFrame(); + } + + PollInput(); + + while (CurrentFrameCycle < ULADevice.FrameLength) + { + // check for interrupt + ULADevice.CheckForInterrupt(CurrentFrameCycle); + + // run a single CPU instruction + CPU.ExecuteOne(); + + // update AY + int ayCnt = 0; + if (_renderSound) + { + if (AYDevice != null) + { + //AYDevice.UpdateSound(CurrentFrameCycle); + if (ayCnt > 10) + { + //AYDevice.UpdateSound(CurrentFrameCycle); + ayCnt = 0; + } + } + } + } + + // we have reached the end of a frame + LastFrameStartCPUTick = CPU.TotalExecutedCycles - OverFlow; + + // paint the buffer if needed + if (ULADevice.needsPaint && _render) + ULADevice.UpdateScreenBuffer(ULADevice.FrameLength); + + if (_renderSound) + BuzzerDevice.EndFrame(); + + if (AYDevice != null) + AYDevice.EndFrame(); + + FrameCount++; + + // setup for next frame + ULADevice.ResetInterrupt(); + + TapeDevice.EndFrame(); + + FrameCompleted = true; + + // is this a lag frame? + Spectrum.IsLagFrame = !InputRead; + } + + #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(); + } + + /// + /// 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(); + } + + #endregion + + #region IStatable + + public void SyncState(Serializer ser) + { + ser.BeginSection("ZXMachine"); + 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("LastULAOutByte", ref LastULAOutByte); + ser.Sync("ROM0", ref ROM0, false); + ser.Sync("ROM1", ref ROM1, false); + ser.Sync("ROM2", ref ROM2, false); + ser.Sync("ROM3", ref ROM3, 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("ROMPaged", ref ROMPaged); + ser.Sync("SHADOWPaged", ref SHADOWPaged); + ser.Sync("RAMPaged", ref RAMPaged); + ser.Sync("PagingDisabled", ref PagingDisabled); + ser.Sync("SpecialPagingMode", ref SpecialPagingMode); + ser.Sync("PagingConfiguration", ref PagingConfiguration); + ser.Sync("ROMhigh", ref ROMhigh); + ser.Sync("ROMlow", ref ROMlow); + + RomData.SyncState(ser); + KeyboardDevice.SyncState(ser); + BuzzerDevice.SyncState(ser); + ULADevice.SyncState(ser); + + if (AYDevice != null) + AYDevice.SyncState(ser); + + ser.Sync("tapeMediaIndex", ref tapeMediaIndex); + TapeMediaIndex = tapeMediaIndex; + + ser.Sync("diskMediaIndex", ref diskMediaIndex); + DiskMediaIndex = diskMediaIndex; + + TapeDevice.SyncState(ser); + + ser.EndSection(); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ULABase.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ULABase.cs new file mode 100644 index 0000000000..79479b4789 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ULABase.cs @@ -0,0 +1,760 @@ + +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// ULA (Uncommitted Logic Array) implementation + /// + public abstract class ULABase : IVideoProvider + { + #region General + + /// + /// Length of the frame in T-States + /// + public int FrameLength; + + /// + /// Emulated clock speed + /// + public int ClockSpeed; + + /// + /// Whether machine is late or early timing model + /// + public bool LateTiming; //currently not implemented + + /// + /// The current cycle within the current frame + /// + public int CurrentTStateInFrame; + + + protected SpectrumBase _machine; + + #endregion + + #region Palettes + + /// + /// The standard ULA palette + /// + private static readonly int[] ULAPalette = + { + Colors.ARGB(0x00, 0x00, 0x00), // Black + Colors.ARGB(0x00, 0x00, 0xD7), // Blue + Colors.ARGB(0xD7, 0x00, 0x00), // Red + Colors.ARGB(0xD7, 0x00, 0xD7), // Magenta + Colors.ARGB(0x00, 0xD7, 0x00), // Green + Colors.ARGB(0x00, 0xD7, 0xD7), // Cyan + Colors.ARGB(0xD7, 0xD7, 0x00), // Yellow + Colors.ARGB(0xD7, 0xD7, 0xD7), // White + Colors.ARGB(0x00, 0x00, 0x00), // Bright Black + Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue + Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red + Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta + Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green + Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan + Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow + Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White + }; + + #endregion + + #region Contention + + /// + /// T-State at which to start applying contention + /// + public int contentionStartPeriod; + /// + /// T-State at which to end applying contention + /// + public int contentionEndPeriod; + /// + /// T-State memory contention delay mapping + /// + public byte[] contentionTable; + + #endregion + + #region Screen Rendering + + /// + /// Video output buffer + /// + public int[] ScreenBuffer; + /// + /// Display memory + /// + protected byte[] screen; + /// + /// Attribute memory lookup (mapped 1:1 to screen for convenience) + /// + protected short[] attr; + /// + /// T-State display mapping + /// + protected short[] tstateToDisp; + /// + /// Table that stores T-State to screen/attribute address values + /// + public short[] floatingBusTable; + /// + /// Cycle at which the last render update took place + /// + protected long lastTState; + /// + /// T-States elapsed since last render update + /// + protected long elapsedTStates; + /// + /// T-State of top left raster pixel + /// + protected int actualULAStart; + /// + /// Offset into display memory based on current T-State + /// + protected int screenByteCtr; + /// + /// Offset into current pixel of rasterizer + /// + protected int ULAByteCtr; + /// + /// The current border colour + /// + public int borderColour; + /// + /// Signs whether the colour flash is ON or OFF + /// + protected bool flashOn = false; + + + protected int flashCounter; + public int FlashCounter + { + get { return flashCounter; } + set + { + flashCounter = value; + } + } + + + /// + /// Internal frame counter used for flasher operations + /// + protected int frameCounter = 0; + + /// + /// Last 8-bit bitmap read from display memory + /// (Floating bus implementation) + /// + protected int lastPixelValue; + /// + /// Last 8-bit attr val read from attribute memory + /// (Floating bus implementation) + /// + protected int lastAttrValue; + /// + /// Last 8-bit bitmap read from display memory+1 + /// (Floating bus implementation) + /// + protected int lastPixelValuePlusOne; + /// + /// Last 8-bit attr val read from attribute memory+1 + /// (Floating bus implementation) + /// + protected int lastAttrValuePlusOne; + + /// + /// Used to create the non-border display area + /// + protected int TtateAtLeft; + protected int TstateWidth; + protected int TstateAtTop; + protected int TstateHeight; + protected int TstateAtRight; + protected int TstateAtBottom; + + /// + /// Total T-States in one scanline + /// + protected int TstatesPerScanline; + /// + /// Total pixels in one scanline + /// + protected int ScanLineWidth; + /// + /// Total chars in one PRINT row + /// + protected int CharRows; + /// + /// Total chars in one PRINT column + /// + protected int CharCols; + /// + /// Total pixels in one display row + /// + protected int ScreenWidth; + /// + /// Total pixels in one display column + /// + protected int ScreenHeight; + /// + /// Total pixels in top border + /// + protected int BorderTopHeight; + /// + /// Total pixels in bottom border + /// + protected int BorderBottomHeight; + /// + /// Total pixels in left border width + /// + protected int BorderLeftWidth; + /// + /// Total pixels in right border width + /// + protected int BorderRightWidth; + /// + /// Memory address of display start + /// + protected int DisplayStart; + /// + /// Total number of bytes of display memory + /// + protected int DisplayLength; + /// + /// Memory address of attribute start + /// + protected int AttributeStart; + /// + /// Total number of bytes of attribute memory + /// + protected int AttributeLength; + + /// + /// Raised when ULA has finished painting the entire screen + /// + public bool needsPaint = false; + + #endregion + + #region Interrupt + + /// + /// The number of T-States that the INT pin is simulated to be held low + /// + public int InterruptPeriod; + + /// + /// The longest instruction cycle count + /// + protected int LongestOperationCycles = 23; + + /// + /// Signs that an interrupt has been raised in this frame. + /// + protected bool InterruptRaised; + + /// + /// Signs that the interrupt signal has been revoked + /// + protected bool InterruptRevoked; + + /// + /// Resets the interrupt - this should happen every frame in order to raise + /// the VBLANK interrupt in the proceding frame + /// + public virtual void ResetInterrupt() + { + InterruptRaised = false; + InterruptRevoked = false; + } + + /// + /// Generates an interrupt in the current phase if needed + /// + /// + public virtual void CheckForInterrupt(long currentCycle) + { + if (InterruptRevoked) + { + // interrupt has already been handled + return; + } + + if (currentCycle < LongestOperationCycles)// InterruptPeriod) + { + // interrupt does not need to be raised yet + return; + } + + if (currentCycle >= InterruptPeriod + LongestOperationCycles) + { + // interrupt should have already been raised and the cpu may or + // may not have caught it. The time has passed so revoke the signal + InterruptRevoked = true; + _machine.CPU.FlagI = false; + return; + } + + if (InterruptRaised) + { + // INT is raised but not yet revoked + // CPU has NOT handled it yet + return; + } + + // Raise the interrupt + InterruptRaised = true; + _machine.CPU.FlagI = true; + + // Signal the start of ULA processing + if (_machine._render) + ULAUpdateStart(); + + CalcFlashCounter(); + } + + #endregion + + #region Construction & Initialisation + + public ULABase(SpectrumBase machine) + { + _machine = machine; + borderType = _machine.Spectrum.SyncSettings.BorderType; + } + + #endregion + + #region Methods + + /// + /// Resets the ULA chip + /// + public abstract void Reset(); + + /// + /// Builds the contention table for the emulated model + /// + public abstract void BuildContentionTable(); + + /// + /// Returns true if the given memory address should be contended + /// + /// + /// + public abstract bool IsContended(int addr); + + /// + /// Contends the machine for a given address + /// + /// + public virtual void Contend(ushort addr) + { + if (IsContended(addr) && !(_machine is ZX128Plus3)) + { + _machine.CPU.TotalExecutedCycles += contentionTable[CurrentTStateInFrame]; + } + } + + public virtual void Contend(int addr, int time, int count) + { + if (IsContended(addr) && !(_machine is ZX128Plus3)) + { + for (int f = 0; f < count; f++) + { + _machine.CPU.TotalExecutedCycles += contentionTable[CurrentTStateInFrame] + time; + } + } + else + _machine.CPU.TotalExecutedCycles += count * time; + } + + /// + /// Resets render state once interrupt is generated + /// + public void ULAUpdateStart() + { + ULAByteCtr = 0; + screenByteCtr = DisplayStart; + lastTState = actualULAStart; + needsPaint = true; + } + + /// + /// Flash processing + /// + public void CalcFlashCounter() + { + flashCounter++; + + if (flashCounter > 15) + { + flashOn = !flashOn; + flashCounter = 0; + } + } + + /// + /// Builds the T-State to attribute map used with the floating bus + /// + public void BuildAttributeMap() + { + int start = DisplayStart; + + for (int f = 0; f < DisplayLength; f++, start++) + { + int addrH = start >> 8; //div by 256 + int addrL = start % 256; + + int pixelY = (addrH & 0x07); + pixelY |= (addrL & (0xE0)) >> 2; + pixelY |= (addrH & (0x18)) << 3; + + int attrIndex_Y = AttributeStart + ((pixelY >> 3) << 5);// pixel/8 * 32 + + addrL = start % 256; + int pixelX = addrL & (0x1F); + + attr[f] = (short)(attrIndex_Y + pixelX); + } + } + + /// + /// Updates the screen buffer based on the number of T-States supplied + /// + /// + public virtual void UpdateScreenBuffer(long _tstates) + { + if (_tstates < actualULAStart) + { + return; + } + else if (_tstates >= FrameLength) + { + _tstates = FrameLength - 1; + + needsPaint = true; + } + + //the additional 1 tstate is required to get correct number of bytes to output in ircontention.sna + elapsedTStates = (_tstates + 1 - lastTState) - 1; + + //It takes 4 tstates to write 1 byte. Or, 2 pixels per t-state. + + long numBytes = (elapsedTStates >> 2) + ((elapsedTStates % 4) > 0 ? 1 : 0); + + int pixelData; + int pixel2Data = 0xff; + int attrData; + int attr2Data; + int bright; + int ink; + int paper; + int flash; + + for (int i = 0; i < numBytes; i++) + { + if (tstateToDisp[lastTState] > 1) + { + screenByteCtr = tstateToDisp[lastTState] - 16384; //adjust for actual screen offset + + pixelData = _machine.FetchScreenMemory((ushort)screenByteCtr); //screen[screenByteCtr]; + attrData = _machine.FetchScreenMemory((ushort)(attr[screenByteCtr] - 16384)); //screen[attr[screenByteCtr] - 16384]; + + lastPixelValue = pixelData; + lastAttrValue = attrData; + + bright = (attrData & 0x40) >> 3; + flash = (attrData & 0x80) >> 7; + ink = (attrData & 0x07); + paper = ((attrData >> 3) & 0x7); + int paletteInk = ULAPalette[ink + bright]; + int palettePaper = ULAPalette[paper + bright]; + + if (flashOn && (flash != 0)) //swap paper and ink when flash is on + { + int temp = paletteInk; + paletteInk = palettePaper; + palettePaper = temp; + } + + for (int a = 0; a < 8; ++a) + { + if ((pixelData & 0x80) != 0) + { + ScreenBuffer[ULAByteCtr++] = paletteInk; + lastAttrValue = ink; + //pixelIsPaper = false; + } + else + { + ScreenBuffer[ULAByteCtr++] = palettePaper; + lastAttrValue = paper; + } + pixelData <<= 1; + } + } + else if (tstateToDisp[lastTState] == 1) + { + int bor = ULAPalette[borderColour]; + + for (int g = 0; g < 8; g++) + ScreenBuffer[ULAByteCtr++] = bor; + } + lastTState += 4; + } + } + + #endregion + + #region IVideoProvider + + private int _virtualWidth; + private int _virtualHeight; + private int _bufferWidth; + private int _bufferHeight; + + public int BackgroundColor + { + get { return ULAPalette[7]; } //ULAPalette[borderColour]; } + } + + 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 ClockSpeed; } + set { } + } + + public int VsyncDenominator + { + get { return FrameLength; } + } + + public int[] GetVideoBuffer() + { + switch (borderType) + { + // Full side borders, no top or bottom border (giving *almost* 16:9 output) + case ZXSpectrum.BorderType.Widescreen: + // we are cropping out the top and bottom borders + var startPixelsToCrop = ScanLineWidth * BorderTopHeight; + var endPixelsToCrop = ScanLineWidth * BorderBottomHeight; + int index = 0; + for (int i = startPixelsToCrop; i < ScreenBuffer.Length - endPixelsToCrop; i++) + { + croppedBuffer[index++] = ScreenBuffer[i]; + } + return croppedBuffer; + + // The full spectrum border + case ZXSpectrum.BorderType.Full: + return ScreenBuffer; + + case ZXSpectrum.BorderType.Medium: + // all border sizes now 24 + var lR = BorderLeftWidth - 24; + var rR = BorderRightWidth - 24; + var tR = BorderTopHeight - 24; + var bR = BorderBottomHeight - 24; + var startP = ScanLineWidth * tR; + var endP = ScanLineWidth * bR; + + int index2 = 0; + // line by line + for (int i = startP; i < ScreenBuffer.Length - endP; i += ScreenWidth + BorderLeftWidth + BorderRightWidth) + { + // each pixel in each line + for (int p = lR; p < ScreenWidth + BorderLeftWidth + BorderRightWidth - rR; p++) + { + if (index2 == croppedBuffer.Length) + break; + croppedBuffer[index2++] = ScreenBuffer[i + p]; + } + } + + return croppedBuffer; + + case ZXSpectrum.BorderType.Small: + // all border sizes now 24 + var lR_ = BorderLeftWidth - 10; + var rR_ = BorderRightWidth - 10; + var tR_ = BorderTopHeight - 10; + var bR_ = BorderBottomHeight - 10; + var startP_ = ScanLineWidth * tR_; + var endP_ = ScanLineWidth * bR_; + + int index2_ = 0; + // line by line + for (int i = startP_; i < ScreenBuffer.Length - endP_; i += ScreenWidth + BorderLeftWidth + BorderRightWidth) + { + // each pixel in each line + for (int p = lR_; p < ScreenWidth + BorderLeftWidth + BorderRightWidth - rR_; p++) + { + if (index2_ == croppedBuffer.Length) + break; + croppedBuffer[index2_++] = ScreenBuffer[i + p]; + } + } + + return croppedBuffer; + + case ZXSpectrum.BorderType.None: + // all border sizes now 24 + var lR__ = BorderLeftWidth; + var rR__ = BorderRightWidth; + var tR__ = BorderTopHeight; + var bR__ = BorderBottomHeight; + var startP__ = ScanLineWidth * tR__; + var endP__ = ScanLineWidth * bR__; + + int index2__ = 0; + // line by line + for (int i = startP__; i < ScreenBuffer.Length - endP__; i += ScreenWidth + BorderLeftWidth + BorderRightWidth) + { + // each pixel in each line + for (int p = lR__; p < ScreenWidth + BorderLeftWidth + BorderRightWidth - rR__; p++) + { + if (index2__ == croppedBuffer.Length) + break; + croppedBuffer[index2__++] = ScreenBuffer[i + p]; + } + } + + return croppedBuffer; + } + + return ScreenBuffer; + } + + protected void SetupScreenSize() + { + switch (borderType) + { + case ZXSpectrum.BorderType.Full: + BufferWidth = ScreenWidth + BorderLeftWidth + BorderRightWidth; + BufferHeight = ScreenHeight + BorderTopHeight + BorderBottomHeight; + VirtualHeight = BufferHeight; + VirtualWidth = BufferWidth; + ScreenBuffer = new int[BufferWidth * BufferHeight]; + break; + + case ZXSpectrum.BorderType.Widescreen: + BufferWidth = ScreenWidth + BorderLeftWidth + BorderRightWidth; + BufferHeight = ScreenHeight; + VirtualHeight = BufferHeight; + VirtualWidth = BufferWidth; + croppedBuffer = new int[BufferWidth * BufferHeight]; + break; + + case ZXSpectrum.BorderType.Medium: + BufferWidth = ScreenWidth + (24) + (24); + BufferHeight = ScreenHeight + (24) + (24); + VirtualHeight = BufferHeight; + VirtualWidth = BufferWidth; + croppedBuffer = new int[BufferWidth * BufferHeight]; + break; + + case ZXSpectrum.BorderType.Small: + BufferWidth = ScreenWidth + (10) + (10); + BufferHeight = ScreenHeight + (10) + (10); + VirtualHeight = BufferHeight; + VirtualWidth = BufferWidth; + croppedBuffer = new int[BufferWidth * BufferHeight]; + break; + + case ZXSpectrum.BorderType.None: + BufferWidth = ScreenWidth; + BufferHeight = ScreenHeight; + VirtualHeight = BufferHeight; + VirtualWidth = BufferWidth; + croppedBuffer = new int[BufferWidth * BufferHeight]; + break; + } + } + + protected int[] croppedBuffer; + + private ZXSpectrum.BorderType _borderType; + + public ZXSpectrum.BorderType borderType + { + get { return _borderType; } + set { _borderType = value; } + } + + + + #endregion + + #region IStatable + + public void SyncState(Serializer ser) + { + ser.BeginSection("ULA"); + ser.Sync("ScreenBuffer", ref ScreenBuffer, false); + ser.Sync("FrameLength", ref FrameLength); + ser.Sync("ClockSpeed", ref ClockSpeed); + ser.Sync("LateTiming", ref LateTiming); + ser.Sync("borderColour", ref borderColour); + ser.EndSection(); + } + + #endregion + + #region Attribution + + /* + * Based on code from ArjunNair's Zero emulator (MIT Licensed) + * https://github.com/ArjunNair/Zero-Emulator + + The MIT License (MIT) + + Copyright (c) 2009 Arjun Nair + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + */ + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Memory.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Memory.cs new file mode 100644 index 0000000000..e769fc46ef --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Memory.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX128 : SpectrumBase + { + /* 128k paging controlled by writes to port 0x7ffd + * + * + + #7FFD (32765) - decoded as A15=0, A1=0 and /IORQ=0. Bits 0..5 are latched. Bits 0..2 select RAM bank in secton D. Bit 3 selects RAM bank to dispay screen (0 - RAM5, 1 - RAM7). Bit 4 selects ROM bank (0 - ROM0, 1 - ROM1). Bit 5, when set locks future writing to #7FFD port until reset. Reading #7FFD port is the same as writing #FF into it. + #BFFD (49149) - write data byte into AY-3-8912 chip. + #FFFD (65533) - select AY-3-8912 addres (D4..D7 ignored) and reading data byte. + + * 0xffff +--------+--------+--------+--------+--------+--------+--------+--------+ + | Bank 0 | Bank 1 | Bank 2 | Bank 3 | Bank 4 | Bank 5 | Bank 6 | Bank 7 | + | | |(also at| | |(also at| | | + | | | 0x8000)| | | 0x4000)| | | + | | | | | | screen | | screen | + 0xc000 +--------+--------+--------+--------+--------+--------+--------+--------+ + | Bank 2 | Any one of these pages may be switched in. + | | + | | + | | + 0x8000 +--------+ + | Bank 5 | + | | + | | + | screen | + 0x4000 +--------+--------+ + | ROM 0 | ROM 1 | Either ROM may be switched in. + | | | + | | | + | | | + 0x0000 +--------+--------+ + */ + + /// + /// Simulates reading from the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override byte ReadBus(ushort addr) + { + int divisor = addr / 0x4000; + byte result = 0xff; + + switch (divisor) + { + // ROM 0x000 + case 0: + TestForTapeTraps(addr % 0x4000); + + if (ROMPaged == 0) + result = ROM0[addr % 0x4000]; + else + result = ROM1[addr % 0x4000]; + break; + + // RAM 0x4000 (RAM5 - Bank5) + case 1: + result = RAM5[addr % 0x4000]; + break; + + // RAM 0x8000 (RAM2 - Bank2) + case 2: + result = RAM2[addr % 0x4000]; + break; + + // RAM 0xc000 (any ram bank 0 - 7 may be paged in - default bank0) + case 3: + switch (RAMPaged) + { + case 0: + result = RAM0[addr % 0x4000]; + break; + case 1: + result = RAM1[addr % 0x4000]; + break; + case 2: + result = RAM2[addr % 0x4000]; + break; + case 3: + result = RAM3[addr % 0x4000]; + break; + case 4: + result = RAM4[addr % 0x4000]; + break; + case 5: + result = RAM5[addr % 0x4000]; + break; + case 6: + result = RAM6[addr % 0x4000]; + break; + case 7: + result = RAM7[addr % 0x4000]; + break; + } + break; + default: + break; + } + + return result; + } + + /// + /// Simulates writing to the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override void WriteBus(ushort addr, byte value) + { + int divisor = addr / 0x4000; + + switch (divisor) + { + // ROM 0x000 + case 0: + // cannot write to ROMs + /* + if (ROMPaged == 0) + ROM0[addr % 0x4000] = value; + else + ROM1[addr % 0x4000] = value; + */ + break; + + // RAM 0x4000 (RAM5 - Bank5 or shadow bank RAM7) + case 1: + RAM5[addr % 0x4000] = value; + break; + + // RAM 0x8000 (RAM2 - Bank2) + case 2: + RAM2[addr % 0x4000] = value; + break; + + // RAM 0xc000 (any ram bank 0 - 7 may be paged in - default bank0) + case 3: + switch (RAMPaged) + { + case 0: + RAM0[addr % 0x4000] = value; + break; + case 1: + RAM1[addr % 0x4000] = value; + break; + case 2: + RAM2[addr % 0x4000] = value; + break; + case 3: + RAM3[addr % 0x4000] = value; + break; + case 4: + RAM4[addr % 0x4000] = value; + break; + case 5: + RAM5[addr % 0x4000] = value; + break; + case 6: + RAM6[addr % 0x4000] = value; + break; + case 7: + RAM7[addr % 0x4000] = value; + break; + } + break; + default: + break; + } + + // update ULA screen buffer if necessary + if ((addr & 49152) == 16384 && _render) + ULADevice.UpdateScreenBuffer(CurrentFrameCycle); + } + + /// + /// Reads a byte of data from a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public override byte ReadMemory(ushort addr) + { + if (ULADevice.IsContended(addr)) + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + 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) + { + // apply contention if necessary + if (ULADevice.IsContended(addr)) + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + WriteBus(addr, value); + } + + /// + /// ULA reads the memory at the specified address + /// (No memory contention) + /// Will read RAM5 (screen0) by default, unless RAM7 (screen1) is selected as output + /// + /// + /// + public override byte FetchScreenMemory(ushort addr) + { + byte value = new byte(); + + if (SHADOWPaged && !PagingDisabled) + { + // shadow screen should be outputted + // this lives in RAM7 + value = RAM7[addr & 0x3FFF]; + } + else + { + // shadow screen is not set to display or paging is disabled (probably in 48k mode) + // (use screen0 at RAM5) + value = RAM5[addr & 0x3FFF]; + } + + return value; + } + + /// + /// Sets up the ROM + /// + /// + /// + public override void InitROM(RomData romData) + { + RomData = romData; + // 128k uses ROM0 and ROM1 + // 128k loader is in ROM0, and fallback 48k rom is in ROM1 + for (int i = 0; i < 0x4000; i++) + { + ROM0[i] = RomData.RomBytes[i]; + if (RomData.RomBytes.Length > 0x4000) + ROM1[i] = RomData.RomBytes[i + 0x4000]; + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Port.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Port.cs new file mode 100644 index 0000000000..e01525017a --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.Port.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX128 : SpectrumBase + { + /// + /// Reads a byte of data from a specified port address + /// + /// + /// + public override byte ReadPort(ushort port) + { + bool deviceAddressed = true; + + // process IO contention + ContendPortAddress(port); + + int result = 0xFF; + + // check AY + if (AYDevice.ReadPort(port, ref result)) + return (byte)result; + + // Kempston joystick input takes priority over all other input + // if this is detected just return the kempston byte + if ((port & 0xe0) == 0 || (port & 0x20) == 0) + { + if (LocateUniqueJoystick(JoystickType.Kempston) != null) + return (byte)((KempstonJoystick)LocateUniqueJoystick(JoystickType.Kempston) as KempstonJoystick).JoyLine; + + InputRead = true; + } + else + { + if (KeyboardDevice.ReadPort(port, ref result)) + { + // not a lagframe + InputRead = true; + + // process tape INs + TapeDevice.ReadPort(port, ref result); + } + else + deviceAddressed = false; + } + + if (!deviceAddressed) + { + // If this is an unused port the floating memory bus should be returned + // Floating bus is read on the previous cycle + long _tStates = CurrentFrameCycle - 1; + + // if we are on the top or bottom border return 0xff + if ((_tStates < ULADevice.contentionStartPeriod) || (_tStates > ULADevice.contentionEndPeriod)) + { + result = 0xff; + } + else + { + if (ULADevice.floatingBusTable[_tStates] < 0) + { + result = 0xff; + } + else + { + result = ReadBus((ushort)ULADevice.floatingBusTable[_tStates]); + } + } + } + + /* + + // Check whether the low bit is reset + // Technically the ULA should respond to every even I/O address + bool lowBitReset = (port & 0x0001) == 0; + + // Kempston joystick input takes priority over all other input + // if this is detected just return the kempston byte + if ((port & 0xe0) == 0 || (port & 0x20) == 0) + { + if (LocateUniqueJoystick(JoystickType.Kempston) != null) + return (byte)((KempstonJoystick)LocateUniqueJoystick(JoystickType.Kempston) as KempstonJoystick).JoyLine; + + InputRead = true; + } + else if (lowBitReset) + { + // Even I/O address so get input from keyboard + KeyboardDevice.ReadPort(port, ref result); + + // not a lagframe + InputRead = true; + + // tape loading monitor cycle + TapeDevice.MonitorRead(); + + // process tape INs + TapeDevice.ReadPort(port, ref result); + } + else if ((port & 0xc002) == 0xc000) + { + // AY sound chip + result = (int)AYDevice.PortRead(); + } + else + { + // devices other than the ULA will respond here + + // Kempston Mouse (not implemented yet) + + + // If this is an unused port the floating memory bus should be returned + // Floating bus is read on the previous cycle + int _tStates = CurrentFrameCycle - 1; + + // if we are on the top or bottom border return 0xff + if ((_tStates < ULADevice.contentionStartPeriod) || (_tStates > ULADevice.contentionEndPeriod)) + { + result = 0xff; + } + else + { + if (ULADevice.floatingBusTable[_tStates] < 0) + { + result = 0xff; + } + else + { + result = ReadBus((ushort)ULADevice.floatingBusTable[_tStates]); + } + } + } + + */ + + return (byte)result; + } + + /// + /// Writes a byte of data to a specified port address + /// + /// + /// + public override void WritePort(ushort port, byte value) + { + // process IO contention + ContendPortAddress(port); + + // get a BitArray of the port + BitArray portBits = new BitArray(BitConverter.GetBytes(port)); + // get a BitArray of the value byte + BitArray bits = new BitArray(new byte[] { value }); + + long currT = CPU.TotalExecutedCycles; + + AYDevice.WritePort(port, value); + + // paging + if (port == 0x7ffd) + { + //if (PagingDisabled) + //return; + + // Bits 0, 1, 2 select the RAM page + var rp = value & 0x07; + if (rp < 8) + RAMPaged = rp; + + // bit 3 controls shadow screen + SHADOWPaged = bits[3]; + + if (SHADOWPaged == false) + { + + } + else + { + + } + + // ROM page + if (bits[4]) + { + // 48k basic rom + ROMPaged = 1; + } + else + { + // 128k editor and menu system + ROMPaged = 0; + } + + // Bit 5 set signifies that paging is disabled until next reboot + PagingDisabled = bits[5]; + } + + // Check whether the low bit is reset + // Technically the ULA should respond to every even I/O address + bool lowBitReset = !portBits[0]; // (port & 0x01) == 0; + + // Only even addresses address the ULA + if (lowBitReset) + { + // store the last OUT byte + LastULAOutByte = value; + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + /* + Bit 7 6 5 4 3 2 1 0 + +-------------------------------+ + | | | | E | M | Border | + +-------------------------------+ + */ + + // Border - LSB 3 bits hold the border colour + if (ULADevice.borderColour != (value & BORDER_BIT)) + ULADevice.UpdateScreenBuffer(CurrentFrameCycle); + + ULADevice.borderColour = value & BORDER_BIT; + + // Buzzer + BuzzerDevice.ProcessPulseValue(false, (value & EAR_BIT) != 0); + TapeDevice.WritePort(port, value); + + // Tape + //TapeDevice.ProcessMicBit((value & MIC_BIT) != 0); + + } + /* + // Active AY Register + if ((port & 0xc002) == 0xc000) + { + var reg = value & 0x0f; + AYDevice.SelectedRegister = reg; + CPU.TotalExecutedCycles += 3; + } + + // AY Write + if ((port & 0xc002) == 0x8000) + { + AYDevice.PortWrite(value); + CPU.TotalExecutedCycles += 3; + } + */ + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.ULA.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.ULA.cs new file mode 100644 index 0000000000..59d9108290 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.ULA.cs @@ -0,0 +1,192 @@ + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + class ULA128 : ULABase + { + #region Construction + + public ULA128(SpectrumBase machine) + : base(machine) + { + InterruptPeriod = 36; + LongestOperationCycles = 64 + 2; + FrameLength = 70908; + ClockSpeed = 3546900; + + contentionTable = new byte[70930]; + floatingBusTable = new short[70930]; + for (int f = 0; f < 70930; f++) + floatingBusTable[f] = -1; + + CharRows = 24; + CharCols = 32; + ScreenWidth = 256; + ScreenHeight = 192; + BorderTopHeight = 48; + BorderBottomHeight = 56; + BorderLeftWidth = 48; + BorderRightWidth = 48; + DisplayStart = 16384; + DisplayLength = 6144; + AttributeStart = 22528; + AttributeLength = 768; + borderColour = 7; + ScanLineWidth = BorderLeftWidth + ScreenWidth + BorderRightWidth; + + TstatesPerScanline = 228; + TstateAtTop = BorderTopHeight * TstatesPerScanline; + TstateAtBottom = BorderBottomHeight * TstatesPerScanline; + tstateToDisp = new short[FrameLength]; + + ScreenBuffer = new int[ScanLineWidth * BorderTopHeight //48 lines of border + + ScanLineWidth * ScreenHeight //border + main + border of 192 lines + + ScanLineWidth * BorderBottomHeight]; //56 lines of border + + attr = new short[DisplayLength]; //6144 bytes of display memory will be mapped + + SetupScreenSize(); + + Reset(); + } + + #endregion + + #region Misc Operations + + public override void Reset() + { + contentionStartPeriod = 14361; // + LateTiming; + contentionEndPeriod = contentionStartPeriod + (ScreenHeight * TstatesPerScanline); + screen = _machine.RAM5; + screenByteCtr = DisplayStart; + ULAByteCtr = 0; + actualULAStart = 14366 - 24 - (TstatesPerScanline * BorderTopHeight);// + LateTiming; + lastTState = actualULAStart; + BuildAttributeMap(); + BuildContentionTable(); + } + + #endregion + + #region Contention Methods + + public override bool IsContended(int addr) + { + addr = addr & 0xc000; + + if (addr == 0x4000) + { + // low port contention + return true; + } + + if (addr == 0xc000) + { + // high port contention - check for contended bank paged in + switch (_machine.RAMPaged) + { + case 1: + case 3: + case 5: + case 7: + return true; + } + } + + return false; + } + + public override void BuildContentionTable() + { + int t = contentionStartPeriod; + while (t < contentionEndPeriod) + { + //for 128 t-states + for (int i = 0; i < 128; i += 8) + { + contentionTable[t++] = 6; + contentionTable[t++] = 5; + contentionTable[t++] = 4; + contentionTable[t++] = 3; + contentionTable[t++] = 2; + contentionTable[t++] = 1; + contentionTable[t++] = 0; + contentionTable[t++] = 0; + } + t += (TstatesPerScanline - 128); + } + + //build top half of tstateToDisp table + //vertical retrace period + for (t = 0; t < actualULAStart; t++) + tstateToDisp[t] = 0; + + //next 48 are actual border + while (t < actualULAStart + (TstateAtTop)) + { + for (int g = 0; g < 176; g++) + tstateToDisp[t++] = 1; + + for (int g = 176; g < TstatesPerScanline; g++) + tstateToDisp[t++] = 0; + } + + //build middle half + int _x = 0; + int _y = 0; + int scrval = 2; + while (t < actualULAStart + (TstateAtTop) + (ScreenHeight * TstatesPerScanline)) + { + for (int g = 0; g < 24; g++) + tstateToDisp[t++] = 1; + + for (int g = 24; g < 24 + 128; g++) + { + //Map screenaddr to tstate + if (g % 4 == 0) + { + scrval = (((((_y & 0xc0) >> 3) | (_y & 0x07) | (0x40)) << 8)) | (((_x >> 3) & 0x1f) | ((_y & 0x38) << 2)); + _x += 8; + } + tstateToDisp[t++] = (short)scrval; + } + _y++; + + for (int g = 24 + 128; g < 24 + 128 + 24; g++) + tstateToDisp[t++] = 1; + + for (int g = 24 + 128 + 24; g < 24 + 128 + 24 + 52; g++) + tstateToDisp[t++] = 0; + } + + int h = contentionStartPeriod + 3; + while (h < contentionEndPeriod + 3) + { + for (int j = 0; j < 128; j += 8) + { + floatingBusTable[h] = tstateToDisp[h + 2]; + floatingBusTable[h + 1] = attr[(tstateToDisp[h + 2] - 16384)]; + floatingBusTable[h + 2] = tstateToDisp[h + 2 + 4]; + floatingBusTable[h + 3] = attr[(tstateToDisp[h + 2 + 4] - 16384)]; + h += 8; + } + h += TstatesPerScanline - 128; + } + + //build bottom half + while (t < actualULAStart + (TstateAtTop) + (ScreenHeight * TstatesPerScanline) + (TstateAtBottom)) + { + for (int g = 0; g < 176; g++) + tstateToDisp[t++] = 1; + + for (int g = 176; g < TstatesPerScanline; g++) + tstateToDisp[t++] = 0; + } + } + + + #endregion + + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs new file mode 100644 index 0000000000..3ea955ddca --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128K/ZX128.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BizHawk.Emulation.Cores.Components.Z80A; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX128 : SpectrumBase + { + #region Construction + + /// + /// Main constructor + /// + /// + /// + public ZX128(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + { + Spectrum = spectrum; + CPU = cpu; + + ROMPaged = 0; + SHADOWPaged = false; + RAMPaged = 0; + PagingDisabled = false; + + ULADevice = new ULA128(this); + + BuzzerDevice = new Buzzer(this); + BuzzerDevice.Init(44100, ULADevice.FrameLength); + + AYDevice = new AYChip(this); + AYDevice.Init(44100, ULADevice.FrameLength); + + KeyboardDevice = new StandardKeyboard(this); + + InitJoysticks(joysticks); + + TapeDevice = new DatacorderDevice(); + TapeDevice.Init(this); + + InitializeMedia(files); + } + + #endregion + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2/ZX128Plus2.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2/ZX128Plus2.cs new file mode 100644 index 0000000000..72c546da51 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2/ZX128Plus2.cs @@ -0,0 +1,31 @@ +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// The +2 is almost identical to the 128k from an emulation point of view + /// There are just a few small changes in the ROMs + /// + public partial class ZX128Plus2 : ZX128 + { + #region Construction + + /// + /// Main constructor + /// + /// + /// + public ZX128Plus2(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + : base(spectrum, cpu, borderType, files, joysticks) + { + + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.Memory.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.Memory.cs new file mode 100644 index 0000000000..265a413002 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.Memory.cs @@ -0,0 +1,429 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX128Plus2a : SpectrumBase + { + /* http://www.worldofspectrum.org/faq/reference/128kreference.htm + * + * Port 0x7ffd behaves in the almost exactly the same way as on the 128K/+2, with two exceptions: + + Bit 4 is now the low bit of the ROM selection. + The partial decoding used is now slightly different: the hardware will respond only to those port addresses with bit 1 reset, bit 14 set and bit 15 reset (as opposed to just bits 1 and 15 reset on the 128K/+2). + The extra paging features of the +2A/+3 are controlled by port 0x1ffd (again, partial decoding applies here: the hardware will respond to all port addresses with bit 1 reset, bit 12 set and bits 13, 14 and 15 reset). This port is also write-only, and its last value should be saved at 0x5b67 (23399). + + Port 0x1ffd responds as follows: + + Bit 0: Paging mode. 0=normal, 1=special + Bit 1: In normal mode, ignored. + Bit 2: In normal mode, high bit of ROM selection. The four ROMs are: + ROM 0: 128k editor, menu system and self-test program + ROM 1: 128k syntax checker + ROM 2: +3DOS + ROM 3: 48 BASIC + Bit 3: Disk motor; 1=on, 0=off + Bit 4: Printer port strobe. + When special mode is selected, the memory map changes to one of four configurations specified in bits 1 and 2 of port 0x1ffd: + Bit 2 =0 Bit 2 =0 Bit 2 =1 Bit 2 =1 + Bit 1 =0 Bit 1 =1 Bit 1 =0 Bit 1 =1 + 0xffff +--------+ +--------+ +--------+ +--------+ + | Bank 3 | | Bank 7 | | Bank 3 | | Bank 3 | + | | | | | | | | + | | | | | | | | + | | | screen | | | | | + 0xc000 +--------+ +--------+ +--------+ +--------+ + | Bank 2 | | Bank 6 | | Bank 6 | | Bank 6 | + | | | | | | | | + | | | | | | | | + | | | | | | | | + 0x8000 +--------+ +--------+ +--------+ +--------+ + | Bank 1 | | Bank 5 | | Bank 5 | | Bank 7 | + | | | | | | | | + | | | | | | | | + | | | screen | | screen | | screen | + 0x4000 +--------+ +--------+ +--------+ +--------+ + | Bank 0 | | Bank 4 | | Bank 4 | | Bank 4 | + | | | | | | | | + | | | | | | | | + | | | | | | | | + 0x0000 +--------+ +--------+ +--------+ +--------+ + RAM banks 1,3,4 and 6 are used for the disc cache and RAMdisc, while Bank 7 contains editor scratchpads and +3DOS workspace. + */ + + /// + /// Simulates reading from the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override byte ReadBus(ushort addr) + { + int divisor = addr / 0x4000; + byte result = 0xff; + + // special paging + if (SpecialPagingMode) + { + switch (divisor) + { + case 0: + switch (PagingConfiguration) + { + case 0: + result = RAM0[addr % 0x4000]; + break; + case 1: + case 2: + case 3: + result = RAM4[addr % 0x4000]; + break; + } + break; + case 1: + switch (PagingConfiguration) + { + case 0: + result = RAM1[addr % 0x4000]; + break; + case 1: + case 2: + result = RAM5[addr % 0x4000]; + break; + case 3: + result = RAM7[addr % 0x4000]; + break; + } + break; + case 2: + switch (PagingConfiguration) + { + case 0: + result = RAM0[addr % 0x4000]; + break; + case 1: + case 2: + case 3: + result = RAM6[addr % 0x4000]; + break; + } + break; + case 3: + switch (PagingConfiguration) + { + case 0: + case 2: + case 3: + result = RAM3[addr % 0x4000]; + break; + case 1: + result = RAM7[addr % 0x4000]; + break; + } + break; + } + } + else + { + switch (divisor) + { + // ROM 0x000 + case 0: + switch (_ROMpaged) + { + case 0: + result = ROM0[addr % 0x4000]; + TestForTapeTraps(addr % 0x4000); + break; + case 1: + result = ROM1[addr % 0x4000]; + TestForTapeTraps(addr % 0x4000); + break; + case 2: + result = ROM2[addr % 0x4000]; + break; + case 3: + result = ROM3[addr % 0x4000]; + break; + } + break; + + // RAM 0x4000 (RAM5 - Bank5 always) + case 1: + result = RAM5[addr % 0x4000]; + break; + + // RAM 0x8000 (RAM2 - Bank2) + case 2: + result = RAM2[addr % 0x4000]; + break; + + // RAM 0xc000 (any ram bank 0 - 7 may be paged in - default bank0) + case 3: + switch (RAMPaged) + { + case 0: + result = RAM0[addr % 0x4000]; + break; + case 1: + result = RAM1[addr % 0x4000]; + break; + case 2: + result = RAM2[addr % 0x4000]; + break; + case 3: + result = RAM3[addr % 0x4000]; + break; + case 4: + result = RAM4[addr % 0x4000]; + break; + case 5: + result = RAM5[addr % 0x4000]; + break; + case 6: + result = RAM6[addr % 0x4000]; + break; + case 7: + result = RAM7[addr % 0x4000]; + break; + } + break; + default: + break; + } + } + + return result; + } + + /// + /// Simulates writing to the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override void WriteBus(ushort addr, byte value) + { + int divisor = addr / 0x4000; + + // special paging + if (SpecialPagingMode) + { + switch (divisor) + { + case 0: + switch (PagingConfiguration) + { + case 0: + RAM0[addr % 0x4000] = value; + break; + case 1: + case 2: + case 3: + RAM4[addr % 0x4000] = value; + break; + } + break; + case 1: + switch (PagingConfiguration) + { + case 0: + RAM1[addr % 0x4000] = value; + break; + case 1: + case 2: + RAM5[addr % 0x4000] = value; + break; + case 3: + RAM7[addr % 0x4000] = value; + break; + } + break; + case 2: + switch (PagingConfiguration) + { + case 0: + RAM2[addr % 0x4000] = value; + break; + case 1: + case 2: + case 3: + RAM6[addr % 0x4000] = value; + break; + } + break; + case 3: + switch (PagingConfiguration) + { + case 0: + case 2: + case 3: + RAM3[addr % 0x4000] = value; + break; + case 1: + RAM7[addr % 0x4000] = value; + break; + } + break; + } + } + else + { + switch (divisor) + { + // ROM 0x000 + case 0: + /* + switch (_ROMpaged) + { + // cannot write to ROMs + case 0: + ROM0[addr % 0x4000] = value; + break; + case 1: + ROM1[addr % 0x4000] = value; + break; + case 2: + ROM2[addr % 0x4000] = value; + break; + case 3: + ROM3[addr % 0x4000] = value; + break; + } + */ + break; + + // RAM 0x4000 (RAM5 - Bank5 only) + case 1: + RAM5[addr % 0x4000] = value; + break; + + // RAM 0x8000 (RAM2 - Bank2) + case 2: + RAM2[addr % 0x4000] = value; + break; + + // RAM 0xc000 (any ram bank 0 - 7 may be paged in - default bank0) + case 3: + switch (RAMPaged) + { + case 0: + RAM0[addr % 0x4000] = value; + break; + case 1: + RAM1[addr % 0x4000] = value; + break; + case 2: + RAM2[addr % 0x4000] = value; + break; + case 3: + RAM3[addr % 0x4000] = value; + break; + case 4: + RAM4[addr % 0x4000] = value; + break; + case 5: + RAM5[addr % 0x4000] = value; + break; + case 6: + RAM6[addr % 0x4000] = value; + break; + case 7: + RAM7[addr % 0x4000] = value; + break; + } + break; + default: + break; + } + } + + // update ULA screen buffer if necessary + if ((addr & 49152) == 16384 && _render) + ULADevice.UpdateScreenBuffer(CurrentFrameCycle); + } + + /// + /// Reads a byte of data from a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public override byte ReadMemory(ushort addr) + { + if (ULADevice.IsContended(addr)) + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + 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) + { + // apply contention if necessary + if (ULADevice.IsContended(addr)) + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + WriteBus(addr, value); + } + + /// + /// ULA reads the memory at the specified address + /// (No memory contention) + /// Will read RAM5 (screen0) by default, unless RAM7 (screen1) is selected as output + /// + /// + /// + public override byte FetchScreenMemory(ushort addr) + { + byte value = new byte(); + + if (SHADOWPaged && !PagingDisabled) + { + // shadow screen should be outputted + // this lives in RAM7 + value = RAM7[addr & 0x3FFF]; + } + else + { + // shadow screen is not set to display or paging is disabled (probably in 48k mode) + // (use screen0 at RAM5) + value = RAM5[addr & 0x3FFF]; + } + + return value; + } + + /// + /// Sets up the ROM + /// + /// + /// + public override void InitROM(RomData romData) + { + RomData = romData; + // +3 uses ROM0, ROM1, ROM2 & ROM3 + /* ROM 0: 128k editor, menu system and self-test program + ROM 1: 128k syntax checker + ROM 2: +3DOS + ROM 3: 48 BASIC + */ + Stream stream = new MemoryStream(RomData.RomBytes); + stream.Read(ROM0, 0, 16384); + stream.Read(ROM1, 0, 16384); + stream.Read(ROM2, 0, 16384); + stream.Read(ROM3, 0, 16384); + stream.Dispose(); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.Port.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.Port.cs new file mode 100644 index 0000000000..24e7a13760 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.Port.cs @@ -0,0 +1,296 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX128Plus2a : SpectrumBase + { + /// + /// Reads a byte of data from a specified port address + /// + /// + /// + public override byte ReadPort(ushort port) + { + bool deviceAddressed = true; + + // process IO contention + ContendPortAddress(port); + + int result = 0xFF; + + // check AY + if (AYDevice.ReadPort(port, ref result)) + return (byte)result; + + // Kempston joystick input takes priority over all other input + // if this is detected just return the kempston byte + if ((port & 0xe0) == 0 || (port & 0x20) == 0) + { + if (LocateUniqueJoystick(JoystickType.Kempston) != null) + return (byte)((KempstonJoystick)LocateUniqueJoystick(JoystickType.Kempston) as KempstonJoystick).JoyLine; + + InputRead = true; + } + else + { + if (KeyboardDevice.ReadPort(port, ref result)) + { + // not a lagframe + InputRead = true; + + // process tape INs + TapeDevice.ReadPort(port, ref result); + } + else + deviceAddressed = false; + } + + if (!deviceAddressed) + { + // If this is an unused port the floating memory bus should be returned + // Floating bus is read on the previous cycle + long _tStates = CurrentFrameCycle - 1; + + // if we are on the top or bottom border return 0xff + if ((_tStates < ULADevice.contentionStartPeriod) || (_tStates > ULADevice.contentionEndPeriod)) + { + result = 0xff; + } + else + { + if (ULADevice.floatingBusTable[_tStates] < 0) + { + result = 0xff; + } + else + { + result = ReadBus((ushort)ULADevice.floatingBusTable[_tStates]); + } + } + } + /* + // Check whether the low bit is reset + // Technically the ULA should respond to every even I/O address + bool lowBitReset = (port & 0x0001) == 0; + + // Kempston joystick input takes priority over all other input + // if this is detected just return the kempston byte + if ((port & 0xe0) == 0 || (port & 0x20) == 0) + { + if (LocateUniqueJoystick(JoystickType.Kempston) != null) + return (byte)((KempstonJoystick)LocateUniqueJoystick(JoystickType.Kempston) as KempstonJoystick).JoyLine; + + InputRead = true; + } + else if (lowBitReset) + { + // Even I/O address so get input from keyboard + KeyboardDevice.ReadPort(port, ref result); + + TapeDevice.MonitorRead(); + + // not a lagframe + InputRead = true; + + // tape loading monitor cycle + //TapeDevice.MonitorRead(); + + // process tape INs + TapeDevice.ReadPort(port, ref result); + } + else + { + // devices other than the ULA will respond here + // (e.g. the AY sound chip in a 128k spectrum + + // AY register activate - on +3/2a both FFFD and BFFD active AY + if ((port & 0xc002) == 0xc000) + { + result = (int)AYDevice.PortRead(); + } + else if ((port & 0xc002) == 0x8000) + { + result = (int)AYDevice.PortRead(); + } + + // Kempston Mouse + + /* + else if ((port & 0xF002) == 0x2000) //Is bit 12 set and bits 13,14,15 and 1 reset? + { + //result = udpDrive.DiskStatusRead(); + + // disk drive is not yet implemented - return a max status byte for the menu to load + result = 255; + } + else if ((port & 0xF002) == 0x3000) + { + //result = udpDrive.DiskReadByte(); + result = 0; + } + + else if ((port & 0xF002) == 0x0) + { + if (PagingDisabled) + result = 0x1; + else + result = 0xff; + } + *//* + + // if unused port the floating memory bus should be returned (still todo) + } + */ + + return (byte)result; + } + + /// + /// Writes a byte of data to a specified port address + /// + /// + /// + public override void WritePort(ushort port, byte value) + { + // process IO contention + ContendPortAddress(port); + + // get a BitArray of the port + BitArray portBits = new BitArray(BitConverter.GetBytes(port)); + // get a BitArray of the value byte + BitArray bits = new BitArray(new byte[] { value }); + + // Check whether the low bit is reset + bool lowBitReset = !portBits[0]; // (port & 0x01) == 0; + + AYDevice.WritePort(port, value); + + // port 0x7ffd - hardware should only respond when bits 1 & 15 are reset and bit 14 is set + if (port == 0x7ffd) + { + if (!PagingDisabled) + { + // bits 0, 1, 2 select the RAM page + var rp = value & 0x07; + if (rp < 8) + RAMPaged = rp; + + // bit 3 controls shadow screen + SHADOWPaged = bits[3]; + + // Bit 5 set signifies that paging is disabled until next reboot + PagingDisabled = bits[5]; + + // portbit 4 is the LOW BIT of the ROM selection + ROMlow = bits[4]; + } + } + // port 0x1ffd - hardware should only respond when bits 1, 13, 14 & 15 are reset and bit 12 is set + if (port == 0x1ffd) + { + if (!PagingDisabled) + { + if (!bits[0]) + { + // special paging is not enabled - get the ROMpage high byte + ROMhigh = bits[2]; + + // set the special paging mode flag + SpecialPagingMode = false; + } + else + { + // special paging is enabled + // this is decided based on combinations of bits 1 & 2 + // Config 0 = Bit1-0 Bit2-0 + // Config 1 = Bit1-1 Bit2-0 + // Config 2 = Bit1-0 Bit2-1 + // Config 3 = Bit1-1 Bit2-1 + BitArray confHalfNibble = new BitArray(2); + confHalfNibble[0] = bits[1]; + confHalfNibble[1] = bits[2]; + + // set special paging configuration + PagingConfiguration = ZXSpectrum.GetIntFromBitArray(confHalfNibble); + + // set the special paging mode flag + SpecialPagingMode = true; + } + } + + // bit 3 controls the disk motor (1=on, 0=off) + DiskMotorState = bits[3]; + + // bit 4 is the printer port strobe + PrinterPortStrobe = bits[4]; + } + + // Only even addresses address the ULA + if (lowBitReset) + { + // store the last OUT byte + LastULAOutByte = value; + + /* + Bit 7 6 5 4 3 2 1 0 + +-------------------------------+ + | | | | E | M | Border | + +-------------------------------+ + */ + + // Border - LSB 3 bits hold the border colour + if (ULADevice.borderColour != (value & BORDER_BIT)) + ULADevice.UpdateScreenBuffer(CurrentFrameCycle); + + ULADevice.borderColour = value & BORDER_BIT; + + // Buzzer + BuzzerDevice.ProcessPulseValue(false, (value & EAR_BIT) != 0); + + // Tape + TapeDevice.WritePort(port, value); + + // Tape + //TapeDevice.ProcessMicBit((value & MIC_BIT) != 0); + } + + + LastULAOutByte = value; + } + + /// + /// +3 and 2a overidden method + /// + public override int _ROMpaged + { + get + { + // calculate the ROMpage from the high and low bits + var rp = ZXSpectrum.GetIntFromBitArray(new BitArray(new bool[] { ROMlow, ROMhigh })); + + if (rp != 0) + { + + } + + return rp; + } + set { ROMPaged = value; } + } + + /// + /// Override port contention + /// +3/2a does not have the same ULA IO contention + /// + /// + public override void ContendPortAddress(ushort addr) + { + CPU.TotalExecutedCycles += 4; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.ULA.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.ULA.cs new file mode 100644 index 0000000000..c6547833ad --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.ULA.cs @@ -0,0 +1,196 @@ + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + class ULAPlus2a : ULABase + { + #region Construction + + public ULAPlus2a(SpectrumBase machine) + : base(machine) + { + InterruptPeriod = 36; + LongestOperationCycles = 64 + 2; + FrameLength = 70908; + ClockSpeed = 3546900; + + contentionTable = new byte[70930]; + floatingBusTable = new short[70930]; + for (int f = 0; f < 70930; f++) + floatingBusTable[f] = -1; + + CharRows = 24; + CharCols = 32; + ScreenWidth = 256; + ScreenHeight = 192; + BorderTopHeight = 48; + BorderBottomHeight = 56; + BorderLeftWidth = 48; + BorderRightWidth = 48; + DisplayStart = 16384; + DisplayLength = 6144; + AttributeStart = 22528; + AttributeLength = 768; + borderColour = 7; + ScanLineWidth = BorderLeftWidth + ScreenWidth + BorderRightWidth; + + TstatesPerScanline = 228; + TstateAtTop = BorderTopHeight * TstatesPerScanline; + TstateAtBottom = BorderBottomHeight * TstatesPerScanline; + tstateToDisp = new short[FrameLength]; + + ScreenBuffer = new int[ScanLineWidth * BorderTopHeight //48 lines of border + + ScanLineWidth * ScreenHeight //border + main + border of 192 lines + + ScanLineWidth * BorderBottomHeight]; //56 lines of border + + attr = new short[DisplayLength]; //6144 bytes of display memory will be mapped + + SetupScreenSize(); + + Reset(); + } + + #endregion + + #region Misc Operations + + public override void Reset() + { + contentionStartPeriod = 14361; // + LateTiming; + contentionEndPeriod = contentionStartPeriod + (ScreenHeight * TstatesPerScanline); + screen = _machine.RAM5; + screenByteCtr = DisplayStart; + ULAByteCtr = 0; + actualULAStart = 14365 - 24 - (TstatesPerScanline * BorderTopHeight);// + LateTiming; + lastTState = actualULAStart; + BuildAttributeMap(); + BuildContentionTable(); + } + + #endregion + + #region Contention Methods + + public override bool IsContended(int addr) + { + addr = addr & 0xc000; + + if (addr == 0x4000) + { + // low port contention + return true; + } + + if (addr == 0xc000) + { + // high port contention - check for contended bank paged in + switch (_machine.RAMPaged) + { + case 4: + case 5: + case 6: + case 7: + return true; + } + } + + return false; + } + + public override void BuildContentionTable() + { + int t = contentionStartPeriod; + while (t < contentionEndPeriod) + { + contentionTable[t++] = 1; + contentionTable[t++] = 0; + + //for 128 t-states + for (int i = 0; i < 128; i += 8) + { + contentionTable[t++] = 7; + contentionTable[t++] = 6; + contentionTable[t++] = 5; + contentionTable[t++] = 4; + contentionTable[t++] = 3; + contentionTable[t++] = 2; + contentionTable[t++] = 1; + contentionTable[t++] = 0; + } + t += (TstatesPerScanline - 128) - 2; + } + + //build top half of tstateToDisp table + //vertical retrace period + for (t = 0; t < actualULAStart; t++) + tstateToDisp[t] = 0; + + //next 48 are actual border + while (t < actualULAStart + (TstateAtTop)) + { + for (int g = 0; g < 176; g++) + tstateToDisp[t++] = 1; + + for (int g = 176; g < TstatesPerScanline; g++) + tstateToDisp[t++] = 0; + } + + //build middle half + int _x = 0; + int _y = 0; + int scrval = 2; + while (t < actualULAStart + (TstateAtTop) + (ScreenHeight * TstatesPerScanline)) + { + for (int g = 0; g < 24; g++) + tstateToDisp[t++] = 1; + + for (int g = 24; g < 24 + 128; g++) + { + //Map screenaddr to tstate + if (g % 4 == 0) + { + scrval = (((((_y & 0xc0) >> 3) | (_y & 0x07) | (0x40)) << 8)) | (((_x >> 3) & 0x1f) | ((_y & 0x38) << 2)); + _x += 8; + } + tstateToDisp[t++] = (short)scrval; + } + + _y++; + + for (int g = 24 + 128; g < 24 + 128 + 24; g++) + tstateToDisp[t++] = 1; + + for (int g = 24 + 128 + 24; g < 24 + 128 + 24 + 52; g++) + tstateToDisp[t++] = 0; + } + + int h = contentionStartPeriod + 3; + while (h < contentionEndPeriod + 3) + { + for (int j = 0; j < 128; j += 8) + { + floatingBusTable[h] = tstateToDisp[h + 2]; + floatingBusTable[h + 1] = attr[(tstateToDisp[h + 2] - 16384)]; + floatingBusTable[h + 2] = tstateToDisp[h + 2 + 4]; + floatingBusTable[h + 3] = attr[(tstateToDisp[h + 2 + 4] - 16384)]; + h += 8; + } + h += TstatesPerScanline - 128; + } + + //build bottom half + while (t < actualULAStart + (TstateAtTop) + (ScreenHeight * TstatesPerScanline) + (TstateAtBottom)) + { + for (int g = 0; g < 176; g++) + tstateToDisp[t++] = 1; + + for (int g = 176; g < TstatesPerScanline; g++) + tstateToDisp[t++] = 0; + } + } + + + #endregion + + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.cs new file mode 100644 index 0000000000..3cd1f695d5 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus2a/ZX128Plus2a.cs @@ -0,0 +1,49 @@ +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX128Plus2a : SpectrumBase + { + #region Construction + + /// + /// Main constructor + /// + /// + /// + public ZX128Plus2a(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + { + Spectrum = spectrum; + CPU = cpu; + + ROMPaged = 0; + SHADOWPaged = false; + RAMPaged = 0; + PagingDisabled = false; + + ULADevice = new ULAPlus2a(this); + + BuzzerDevice = new Buzzer(this); + BuzzerDevice.Init(44100, ULADevice.FrameLength); + + AYDevice = new AYChip(this); + AYDevice.Init(44100, ULADevice.FrameLength); + + KeyboardDevice = new StandardKeyboard(this); + + InitJoysticks(joysticks); + + TapeDevice = new DatacorderDevice(); + TapeDevice.Init(this); + + InitializeMedia(files); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Memory.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Memory.cs new file mode 100644 index 0000000000..bead71f975 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Memory.cs @@ -0,0 +1,429 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX128Plus3 : SpectrumBase + { + /* http://www.worldofspectrum.org/faq/reference/128kreference.htm + * + * Port 0x7ffd behaves in the almost exactly the same way as on the 128K/+2, with two exceptions: + + Bit 4 is now the low bit of the ROM selection. + The partial decoding used is now slightly different: the hardware will respond only to those port addresses with bit 1 reset, bit 14 set and bit 15 reset (as opposed to just bits 1 and 15 reset on the 128K/+2). + The extra paging features of the +2A/+3 are controlled by port 0x1ffd (again, partial decoding applies here: the hardware will respond to all port addresses with bit 1 reset, bit 12 set and bits 13, 14 and 15 reset). This port is also write-only, and its last value should be saved at 0x5b67 (23399). + + Port 0x1ffd responds as follows: + + Bit 0: Paging mode. 0=normal, 1=special + Bit 1: In normal mode, ignored. + Bit 2: In normal mode, high bit of ROM selection. The four ROMs are: + ROM 0: 128k editor, menu system and self-test program + ROM 1: 128k syntax checker + ROM 2: +3DOS + ROM 3: 48 BASIC + Bit 3: Disk motor; 1=on, 0=off + Bit 4: Printer port strobe. + When special mode is selected, the memory map changes to one of four configurations specified in bits 1 and 2 of port 0x1ffd: + Bit 2 =0 Bit 2 =0 Bit 2 =1 Bit 2 =1 + Bit 1 =0 Bit 1 =1 Bit 1 =0 Bit 1 =1 + 0xffff +--------+ +--------+ +--------+ +--------+ + | Bank 3 | | Bank 7 | | Bank 3 | | Bank 3 | + | | | | | | | | + | | | | | | | | + | | | screen | | | | | + 0xc000 +--------+ +--------+ +--------+ +--------+ + | Bank 2 | | Bank 6 | | Bank 6 | | Bank 6 | + | | | | | | | | + | | | | | | | | + | | | | | | | | + 0x8000 +--------+ +--------+ +--------+ +--------+ + | Bank 1 | | Bank 5 | | Bank 5 | | Bank 7 | + | | | | | | | | + | | | | | | | | + | | | screen | | screen | | screen | + 0x4000 +--------+ +--------+ +--------+ +--------+ + | Bank 0 | | Bank 4 | | Bank 4 | | Bank 4 | + | | | | | | | | + | | | | | | | | + | | | | | | | | + 0x0000 +--------+ +--------+ +--------+ +--------+ + RAM banks 1,3,4 and 6 are used for the disc cache and RAMdisc, while Bank 7 contains editor scratchpads and +3DOS workspace. + */ + + /// + /// Simulates reading from the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override byte ReadBus(ushort addr) + { + int divisor = addr / 0x4000; + byte result = 0xff; + + // special paging + if (SpecialPagingMode) + { + switch (divisor) + { + case 0: + switch (PagingConfiguration) + { + case 0: + result = RAM0[addr % 0x4000]; + break; + case 1: + case 2: + case 3: + result = RAM4[addr % 0x4000]; + break; + } + break; + case 1: + switch (PagingConfiguration) + { + case 0: + result = RAM1[addr % 0x4000]; + break; + case 1: + case 2: + result = RAM5[addr % 0x4000]; + break; + case 3: + result = RAM7[addr % 0x4000]; + break; + } + break; + case 2: + switch (PagingConfiguration) + { + case 0: + result = RAM0[addr % 0x4000]; + break; + case 1: + case 2: + case 3: + result = RAM6[addr % 0x4000]; + break; + } + break; + case 3: + switch (PagingConfiguration) + { + case 0: + case 2: + case 3: + result = RAM3[addr % 0x4000]; + break; + case 1: + result = RAM7[addr % 0x4000]; + break; + } + break; + } + } + else + { + switch (divisor) + { + // ROM 0x000 + case 0: + switch (_ROMpaged) + { + case 0: + result = ROM0[addr % 0x4000]; + TestForTapeTraps(addr % 0x4000); + break; + case 1: + result = ROM1[addr % 0x4000]; + TestForTapeTraps(addr % 0x4000); + break; + case 2: + result = ROM2[addr % 0x4000]; + break; + case 3: + result = ROM3[addr % 0x4000]; + break; + } + break; + + // RAM 0x4000 (RAM5 - Bank5 always) + case 1: + result = RAM5[addr % 0x4000]; + break; + + // RAM 0x8000 (RAM2 - Bank2) + case 2: + result = RAM2[addr % 0x4000]; + break; + + // RAM 0xc000 (any ram bank 0 - 7 may be paged in - default bank0) + case 3: + switch (RAMPaged) + { + case 0: + result = RAM0[addr % 0x4000]; + break; + case 1: + result = RAM1[addr % 0x4000]; + break; + case 2: + result = RAM2[addr % 0x4000]; + break; + case 3: + result = RAM3[addr % 0x4000]; + break; + case 4: + result = RAM4[addr % 0x4000]; + break; + case 5: + result = RAM5[addr % 0x4000]; + break; + case 6: + result = RAM6[addr % 0x4000]; + break; + case 7: + result = RAM7[addr % 0x4000]; + break; + } + break; + default: + break; + } + } + + return result; + } + + /// + /// Simulates writing to the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override void WriteBus(ushort addr, byte value) + { + int divisor = addr / 0x4000; + + // special paging + if (SpecialPagingMode) + { + switch (divisor) + { + case 0: + switch (PagingConfiguration) + { + case 0: + RAM0[addr % 0x4000] = value; + break; + case 1: + case 2: + case 3: + RAM4[addr % 0x4000] = value; + break; + } + break; + case 1: + switch (PagingConfiguration) + { + case 0: + RAM1[addr % 0x4000] = value; + break; + case 1: + case 2: + RAM5[addr % 0x4000] = value; + break; + case 3: + RAM7[addr % 0x4000] = value; + break; + } + break; + case 2: + switch (PagingConfiguration) + { + case 0: + RAM2[addr % 0x4000] = value; + break; + case 1: + case 2: + case 3: + RAM6[addr % 0x4000] = value; + break; + } + break; + case 3: + switch (PagingConfiguration) + { + case 0: + case 2: + case 3: + RAM3[addr % 0x4000] = value; + break; + case 1: + RAM7[addr % 0x4000] = value; + break; + } + break; + } + } + else + { + switch (divisor) + { + // ROM 0x000 + case 0: + /* + switch (_ROMpaged) + { + // cannot write to ROMs + case 0: + ROM0[addr % 0x4000] = value; + break; + case 1: + ROM1[addr % 0x4000] = value; + break; + case 2: + ROM2[addr % 0x4000] = value; + break; + case 3: + ROM3[addr % 0x4000] = value; + break; + } + */ + break; + + // RAM 0x4000 (RAM5 - Bank5 only) + case 1: + RAM5[addr % 0x4000] = value; + break; + + // RAM 0x8000 (RAM2 - Bank2) + case 2: + RAM2[addr % 0x4000] = value; + break; + + // RAM 0xc000 (any ram bank 0 - 7 may be paged in - default bank0) + case 3: + switch (RAMPaged) + { + case 0: + RAM0[addr % 0x4000] = value; + break; + case 1: + RAM1[addr % 0x4000] = value; + break; + case 2: + RAM2[addr % 0x4000] = value; + break; + case 3: + RAM3[addr % 0x4000] = value; + break; + case 4: + RAM4[addr % 0x4000] = value; + break; + case 5: + RAM5[addr % 0x4000] = value; + break; + case 6: + RAM6[addr % 0x4000] = value; + break; + case 7: + RAM7[addr % 0x4000] = value; + break; + } + break; + default: + break; + } + } + + // update ULA screen buffer if necessary + if ((addr & 49152) == 16384 && _render) + ULADevice.UpdateScreenBuffer(CurrentFrameCycle); + } + + /// + /// Reads a byte of data from a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public override byte ReadMemory(ushort addr) + { + if (ULADevice.IsContended(addr)) + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + 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) + { + // apply contention if necessary + if (ULADevice.IsContended(addr)) + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + WriteBus(addr, value); + } + + /// + /// ULA reads the memory at the specified address + /// (No memory contention) + /// Will read RAM5 (screen0) by default, unless RAM7 (screen1) is selected as output + /// + /// + /// + public override byte FetchScreenMemory(ushort addr) + { + byte value = new byte(); + + if (SHADOWPaged && !PagingDisabled) + { + // shadow screen should be outputted + // this lives in RAM7 + value = RAM7[addr & 0x3FFF]; + } + else + { + // shadow screen is not set to display or paging is disabled (probably in 48k mode) + // (use screen0 at RAM5) + value = RAM5[addr & 0x3FFF]; + } + + return value; + } + + /// + /// Sets up the ROM + /// + /// + /// + public override void InitROM(RomData romData) + { + RomData = romData; + // +3 uses ROM0, ROM1, ROM2 & ROM3 + /* ROM 0: 128k editor, menu system and self-test program + ROM 1: 128k syntax checker + ROM 2: +3DOS + ROM 3: 48 BASIC + */ + Stream stream = new MemoryStream(RomData.RomBytes); + stream.Read(ROM0, 0, 16384); + stream.Read(ROM1, 0, 16384); + stream.Read(ROM2, 0, 16384); + stream.Read(ROM3, 0, 16384); + stream.Dispose(); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Port.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Port.cs new file mode 100644 index 0000000000..e56c23440b --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.Port.cs @@ -0,0 +1,223 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX128Plus3 : SpectrumBase + { + /// + /// Reads a byte of data from a specified port address + /// + /// + /// + public override byte ReadPort(ushort port) + { + bool deviceAddressed = true; + + // process IO contention + ContendPortAddress(port); + + int result = 0xFF; + + // check AY + if (AYDevice.ReadPort(port, ref result)) + return (byte)result; + + // Kempston joystick input takes priority over all other input + // if this is detected just return the kempston byte + if ((port & 0xe0) == 0 || (port & 0x20) == 0) + { + if (LocateUniqueJoystick(JoystickType.Kempston) != null) + return (byte)((KempstonJoystick)LocateUniqueJoystick(JoystickType.Kempston) as KempstonJoystick).JoyLine; + + InputRead = true; + } + else + { + if (KeyboardDevice.ReadPort(port, ref result)) + { + // not a lagframe + InputRead = true; + + // process tape INs + TapeDevice.ReadPort(port, ref result); + } + else + deviceAddressed = false; + } + + if (!deviceAddressed) + { + // If this is an unused port the floating memory bus should be returned + // Floating bus is read on the previous cycle + long _tStates = CurrentFrameCycle - 1; + + // if we are on the top or bottom border return 0xff + if ((_tStates < ULADevice.contentionStartPeriod) || (_tStates > ULADevice.contentionEndPeriod)) + { + result = 0xff; + } + else + { + if (ULADevice.floatingBusTable[_tStates] < 0) + { + result = 0xff; + } + else + { + result = ReadBus((ushort)ULADevice.floatingBusTable[_tStates]); + } + } + } + + return (byte)result; + } + + /// + /// Writes a byte of data to a specified port address + /// + /// + /// + public override void WritePort(ushort port, byte value) + { + // process IO contention + ContendPortAddress(port); + + // get a BitArray of the port + BitArray portBits = new BitArray(BitConverter.GetBytes(port)); + // get a BitArray of the value byte + BitArray bits = new BitArray(new byte[] { value }); + + // Check whether the low bit is reset + bool lowBitReset = !portBits[0]; // (port & 0x01) == 0; + + AYDevice.WritePort(port, value); + + // port 0x7ffd - hardware should only respond when bits 1 & 15 are reset and bit 14 is set + if (port == 0x7ffd) + { + if (!PagingDisabled) + { + // bits 0, 1, 2 select the RAM page + var rp = value & 0x07; + if (rp < 8) + RAMPaged = rp; + + // bit 3 controls shadow screen + SHADOWPaged = bits[3]; + + // Bit 5 set signifies that paging is disabled until next reboot + PagingDisabled = bits[5]; + + // portbit 4 is the LOW BIT of the ROM selection + ROMlow = bits[4]; + } + } + // port 0x1ffd - hardware should only respond when bits 1, 13, 14 & 15 are reset and bit 12 is set + if (port == 0x1ffd) + { + if (!PagingDisabled) + { + if (!bits[0]) + { + // special paging is not enabled - get the ROMpage high byte + ROMhigh = bits[2]; + + // set the special paging mode flag + SpecialPagingMode = false; + } + else + { + // special paging is enabled + // this is decided based on combinations of bits 1 & 2 + // Config 0 = Bit1-0 Bit2-0 + // Config 1 = Bit1-1 Bit2-0 + // Config 2 = Bit1-0 Bit2-1 + // Config 3 = Bit1-1 Bit2-1 + BitArray confHalfNibble = new BitArray(2); + confHalfNibble[0] = bits[1]; + confHalfNibble[1] = bits[2]; + + // set special paging configuration + PagingConfiguration = ZXSpectrum.GetIntFromBitArray(confHalfNibble); + + // set the special paging mode flag + SpecialPagingMode = true; + } + } + + // bit 3 controls the disk motor (1=on, 0=off) + DiskMotorState = bits[3]; + + // bit 4 is the printer port strobe + PrinterPortStrobe = bits[4]; + } + + // Only even addresses address the ULA + if (lowBitReset) + { + // store the last OUT byte + LastULAOutByte = value; + + /* + Bit 7 6 5 4 3 2 1 0 + +-------------------------------+ + | | | | E | M | Border | + +-------------------------------+ + */ + + // Border - LSB 3 bits hold the border colour + if (ULADevice.borderColour != (value & BORDER_BIT)) + ULADevice.UpdateScreenBuffer(CurrentFrameCycle); + + ULADevice.borderColour = value & BORDER_BIT; + + // Buzzer + BuzzerDevice.ProcessPulseValue(false, (value & EAR_BIT) != 0); + + // Tape + TapeDevice.WritePort(port, value); + + // Tape + //TapeDevice.ProcessMicBit((value & MIC_BIT) != 0); + } + + + LastULAOutByte = value; + } + + /// + /// +3 and 2a overidden method + /// + public override int _ROMpaged + { + get + { + // calculate the ROMpage from the high and low bits + var rp = ZXSpectrum.GetIntFromBitArray(new BitArray(new bool[] { ROMlow, ROMhigh })); + + if (rp != 0) + { + + } + + return rp; + } + set { ROMPaged = value; } + } + + /// + /// Override port contention + /// +3/2a does not have the same ULA IO contention + /// + /// + public override void ContendPortAddress(ushort addr) + { + CPU.TotalExecutedCycles += 4; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.ULA.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.ULA.cs new file mode 100644 index 0000000000..3b2291a082 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.ULA.cs @@ -0,0 +1,196 @@ + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + class ULAPlus3 : ULABase + { + #region Construction + + public ULAPlus3(SpectrumBase machine) + : base(machine) + { + InterruptPeriod = 36; + LongestOperationCycles = 64 + 2; + FrameLength = 70908; + ClockSpeed = 3546900; + + contentionTable = new byte[70930]; + floatingBusTable = new short[70930]; + for (int f = 0; f < 70930; f++) + floatingBusTable[f] = -1; + + CharRows = 24; + CharCols = 32; + ScreenWidth = 256; + ScreenHeight = 192; + BorderTopHeight = 48; + BorderBottomHeight = 56; + BorderLeftWidth = 48; + BorderRightWidth = 48; + DisplayStart = 16384; + DisplayLength = 6144; + AttributeStart = 22528; + AttributeLength = 768; + borderColour = 7; + ScanLineWidth = BorderLeftWidth + ScreenWidth + BorderRightWidth; + + TstatesPerScanline = 228; + TstateAtTop = BorderTopHeight * TstatesPerScanline; + TstateAtBottom = BorderBottomHeight * TstatesPerScanline; + tstateToDisp = new short[FrameLength]; + + ScreenBuffer = new int[ScanLineWidth * BorderTopHeight //48 lines of border + + ScanLineWidth * ScreenHeight //border + main + border of 192 lines + + ScanLineWidth * BorderBottomHeight]; //56 lines of border + + attr = new short[DisplayLength]; //6144 bytes of display memory will be mapped + + SetupScreenSize(); + + Reset(); + } + + #endregion + + #region Misc Operations + + public override void Reset() + { + contentionStartPeriod = 14361; // + LateTiming; + contentionEndPeriod = contentionStartPeriod + (ScreenHeight * TstatesPerScanline); + screen = _machine.RAM5; + screenByteCtr = DisplayStart; + ULAByteCtr = 0; + actualULAStart = 14365 - 24 - (TstatesPerScanline * BorderTopHeight);// + LateTiming; + lastTState = actualULAStart; + BuildAttributeMap(); + BuildContentionTable(); + } + + #endregion + + #region Contention Methods + + public override bool IsContended(int addr) + { + addr = addr & 0xc000; + + if (addr == 0x4000) + { + // low port contention + return true; + } + + if (addr == 0xc000) + { + // high port contention - check for contended bank paged in + switch (_machine.RAMPaged) + { + case 4: + case 5: + case 6: + case 7: + return true; + } + } + + return false; + } + + public override void BuildContentionTable() + { + int t = contentionStartPeriod; + while (t < contentionEndPeriod) + { + contentionTable[t++] = 1; + contentionTable[t++] = 0; + + //for 128 t-states + for (int i = 0; i < 128; i += 8) + { + contentionTable[t++] = 7; + contentionTable[t++] = 6; + contentionTable[t++] = 5; + contentionTable[t++] = 4; + contentionTable[t++] = 3; + contentionTable[t++] = 2; + contentionTable[t++] = 1; + contentionTable[t++] = 0; + } + t += (TstatesPerScanline - 128) - 2; + } + + //build top half of tstateToDisp table + //vertical retrace period + for (t = 0; t < actualULAStart; t++) + tstateToDisp[t] = 0; + + //next 48 are actual border + while (t < actualULAStart + (TstateAtTop)) + { + for (int g = 0; g < 176; g++) + tstateToDisp[t++] = 1; + + for (int g = 176; g < TstatesPerScanline; g++) + tstateToDisp[t++] = 0; + } + + //build middle half + int _x = 0; + int _y = 0; + int scrval = 2; + while (t < actualULAStart + (TstateAtTop) + (ScreenHeight * TstatesPerScanline)) + { + for (int g = 0; g < 24; g++) + tstateToDisp[t++] = 1; + + for (int g = 24; g < 24 + 128; g++) + { + //Map screenaddr to tstate + if (g % 4 == 0) + { + scrval = (((((_y & 0xc0) >> 3) | (_y & 0x07) | (0x40)) << 8)) | (((_x >> 3) & 0x1f) | ((_y & 0x38) << 2)); + _x += 8; + } + tstateToDisp[t++] = (short)scrval; + } + + _y++; + + for (int g = 24 + 128; g < 24 + 128 + 24; g++) + tstateToDisp[t++] = 1; + + for (int g = 24 + 128 + 24; g < 24 + 128 + 24 + 52; g++) + tstateToDisp[t++] = 0; + } + + int h = contentionStartPeriod + 3; + while (h < contentionEndPeriod + 3) + { + for (int j = 0; j < 128; j += 8) + { + floatingBusTable[h] = tstateToDisp[h + 2]; + floatingBusTable[h + 1] = attr[(tstateToDisp[h + 2] - 16384)]; + floatingBusTable[h + 2] = tstateToDisp[h + 2 + 4]; + floatingBusTable[h + 3] = attr[(tstateToDisp[h + 2 + 4] - 16384)]; + h += 8; + } + h += TstatesPerScanline - 128; + } + + //build bottom half + while (t < actualULAStart + (TstateAtTop) + (ScreenHeight * TstatesPerScanline) + (TstateAtBottom)) + { + for (int g = 0; g < 176; g++) + tstateToDisp[t++] = 1; + + for (int g = 176; g < TstatesPerScanline; g++) + tstateToDisp[t++] = 0; + } + } + + + #endregion + + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.cs new file mode 100644 index 0000000000..916232bcb6 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum128KPlus3/ZX128Plus3.cs @@ -0,0 +1,49 @@ +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX128Plus3 : SpectrumBase + { + #region Construction + + /// + /// Main constructor + /// + /// + /// + public ZX128Plus3(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + { + Spectrum = spectrum; + CPU = cpu; + + ROMPaged = 0; + SHADOWPaged = false; + RAMPaged = 0; + PagingDisabled = false; + + ULADevice = new ULAPlus3(this); + + BuzzerDevice = new Buzzer(this); + BuzzerDevice.Init(44100, ULADevice.FrameLength); + + AYDevice = new AYChip(this); + AYDevice.Init(44100, ULADevice.FrameLength); + + KeyboardDevice = new StandardKeyboard(this); + + InitJoysticks(joysticks); + + TapeDevice = new DatacorderDevice(); + TapeDevice.Init(this); + + InitializeMedia(files); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum16K/ZX16.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum16K/ZX16.cs new file mode 100644 index 0000000000..3f6499e916 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum16K/ZX16.cs @@ -0,0 +1,142 @@ +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public class ZX16 : ZX48 + { + #region Construction + + /// + /// Main constructor + /// + /// + /// + public ZX16(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + : base(spectrum, cpu, borderType, files, joysticks) + { + + } + + #endregion + + + #region Memory + + /* 48K Spectrum has NO memory paging + * + * + | Bank 0 | + | | + | | + | screen | + 0x4000 +--------+ + | ROM 0 | + | | + | | + | | + 0x0000 +--------+ + */ + + /// + /// Simulates reading from the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override byte ReadBus(ushort addr) + { + int divisor = addr / 0x4000; + var index = addr % 0x4000; + + // paging logic goes here + + switch (divisor) + { + case 0: + TestForTapeTraps(addr % 0x4000); + return ROM0[index]; + case 1: return RAM0[index]; + default: + // memory does not exist + return 0xff; + } + } + + /// + /// Simulates writing to the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override void WriteBus(ushort addr, byte value) + { + int divisor = addr / 0x4000; + var index = addr % 0x4000; + + // paging logic goes here + + switch (divisor) + { + case 0: + // cannot write to ROM + break; + case 1: + RAM0[index] = value; + break; + } + + // update ULA screen buffer if necessary + if ((addr & 49152) == 16384 && _render) + ULADevice.UpdateScreenBuffer(CurrentFrameCycle); + } + + /// + /// Reads a byte of data from a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public override byte ReadMemory(ushort addr) + { + if (ULADevice.IsContended(addr)) + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + 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) + { + // apply contention if necessary + if (ULADevice.IsContended(addr)) + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + WriteBus(addr, value); + } + + /// + /// Sets up the ROM + /// + /// + /// + public override void InitROM(RomData romData) + { + RomData = romData; + // for 16/48k machines only ROM0 is used (no paging) + RomData.RomBytes?.CopyTo(ROM0, 0); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Memory.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Memory.cs new file mode 100644 index 0000000000..bef6b97317 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Memory.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX48 : SpectrumBase + { + /* 48K Spectrum has NO memory paging + * + * 0xffff +--------+ + | Bank 2 | + | | + | | + | | + 0xc000 +--------+ + | Bank 1 | + | | + | | + | | + 0x8000 +--------+ + | Bank 0 | + | | + | | + | screen | + 0x4000 +--------+ + | ROM 0 | + | | + | | + | | + 0x0000 +--------+ + */ + + /// + /// Simulates reading from the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override byte ReadBus(ushort addr) + { + int divisor = addr / 0x4000; + var index = addr % 0x4000; + + // paging logic goes here + + switch (divisor) + { + case 0: return ROM0[index]; + case 1: return RAM0[index]; + case 2: return RAM1[index]; + case 3: return RAM2[index]; + default: return 0; + } + } + + /// + /// Simulates writing to the bus (no contention) + /// Paging should be handled here + /// + /// + /// + public override void WriteBus(ushort addr, byte value) + { + int divisor = addr / 0x4000; + var index = addr % 0x4000; + + // paging logic goes here + + switch (divisor) + { + case 0: + // cannot write to ROM + break; + case 1: + RAM0[index] = value; + break; + case 2: + RAM1[index] = value; + break; + case 3: + RAM2[index] = value; + break; + } + + // update ULA screen buffer if necessary + if ((addr & 49152) == 16384 && _render) + ULADevice.UpdateScreenBuffer(CurrentFrameCycle); + } + + /// + /// Reads a byte of data from a specified memory address + /// (with memory contention if appropriate) + /// + /// + /// + public override byte ReadMemory(ushort addr) + { + if (ULADevice.IsContended(addr)) + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + 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) + { + // apply contention if necessary + if (ULADevice.IsContended(addr)) + CPU.TotalExecutedCycles += ULADevice.contentionTable[CurrentFrameCycle]; + + WriteBus(addr, value); + } + + /// + /// Sets up the ROM + /// + /// + /// + public override void InitROM(RomData romData) + { + RomData = romData; + // for 16/48k machines only ROM0 is used (no paging) + RomData.RomBytes?.CopyTo(ROM0, 0); + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Port.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Port.cs new file mode 100644 index 0000000000..0cb5b9c7c1 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.Port.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX48 : SpectrumBase + { + /// + /// Reads a byte of data from a specified port address + /// + /// + /// + public override byte ReadPort(ushort port) + { + // process IO contention + ContendPortAddress(port); + + int result = 0xFF; + + // Check whether the low bit is reset + // Technically the ULA should respond to every even I/O address + bool lowBitReset = (port & 0x0001) == 0; + + // Kempston joystick input takes priority over all other input + // if this is detected just return the kempston byte + if ((port & 0xe0) == 0 || (port & 0x20) == 0) + { + if (LocateUniqueJoystick(JoystickType.Kempston) != null) + return (byte)((KempstonJoystick)LocateUniqueJoystick(JoystickType.Kempston) as KempstonJoystick).JoyLine; + + // not a lag frame + InputRead = true; + } + else if (lowBitReset) + { + // Even I/O address so get input from keyboard + KeyboardDevice.ReadPort(port, ref result); + + // not a lagframe + InputRead = true; + + // process tape INs + TapeDevice.ReadPort(port, ref result); + } + else + { + // devices other than the ULA will respond here + // (e.g. the AY sound chip in a 128k spectrum + + // AY register activate - no AY chip in a 48k spectrum + + // Kemptson Mouse (not implemented yet) + + + // If this is an unused port the floating memory bus should be returned + // Floating bus is read on the previous cycle + long _tStates = CurrentFrameCycle - 1; + + // if we are on the top or bottom border return 0xff + if ((_tStates < ULADevice.contentionStartPeriod) || (_tStates > ULADevice.contentionEndPeriod)) + { + result = 0xff; + } + else + { + if (ULADevice.floatingBusTable[_tStates] < 0) + { + result = 0xff; + } + else + { + result = ReadBus((ushort)ULADevice.floatingBusTable[_tStates]); + } + } + } + + return (byte)result; + } + + /// + /// Writes a byte of data to a specified port address + /// + /// + /// + public override void WritePort(ushort port, byte value) + { + // process IO contention + ContendPortAddress(port); + + // Check whether the low bit is reset + // Technically the ULA should respond to every even I/O address + if ((port & 0x0001) != 0) + return; + + // store the last OUT byte + LastULAOutByte = value; + + /* + Bit 7 6 5 4 3 2 1 0 + +-------------------------------+ + | | | | E | M | Border | + +-------------------------------+ + */ + + // Border - LSB 3 bits hold the border colour + if (ULADevice.borderColour != (value & BORDER_BIT)) + { + // border value has changed - update the screen buffer + ULADevice.UpdateScreenBuffer(CurrentFrameCycle); + } + + ULADevice.borderColour = value & BORDER_BIT; + + // Buzzer + BuzzerDevice.ProcessPulseValue(false, (value & EAR_BIT) != 0); + + // Tape + TapeDevice.WritePort(port, value); + + // Tape mic processing (not implemented yet) + //TapeDevice.ProcessMicBit((value & MIC_BIT) != 0); + + } + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.ULA.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.ULA.cs new file mode 100644 index 0000000000..3b4eca3dc3 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.ULA.cs @@ -0,0 +1,180 @@ + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + class ULA48 : ULABase + { + #region Construction + + public ULA48(SpectrumBase machine) + : base(machine) + { + InterruptPeriod = 32; + LongestOperationCycles = 64; + FrameLength = 69888; + ClockSpeed = 3500000; + + contentionTable = new byte[70930]; + floatingBusTable = new short[70930]; + for (int f = 0; f < 70930; f++) + floatingBusTable[f] = -1; + + CharRows = 24; + CharCols = 32; + ScreenWidth = 256; + ScreenHeight = 192; + BorderTopHeight = 48; + BorderBottomHeight = 56; + BorderLeftWidth = 48; + BorderRightWidth = 48; + DisplayStart = 16384; + DisplayLength = 6144; + AttributeStart = 22528; + AttributeLength = 768; + borderColour = 7; + ScanLineWidth = BorderLeftWidth + ScreenWidth + BorderRightWidth; + + TstatesPerScanline = 224; + TstateAtTop = BorderTopHeight * TstatesPerScanline; + TstateAtBottom = BorderBottomHeight * TstatesPerScanline; + tstateToDisp = new short[FrameLength]; + + ScreenBuffer = new int[ScanLineWidth * BorderTopHeight //48 lines of border + + ScanLineWidth * ScreenHeight //border + main + border of 192 lines + + ScanLineWidth * BorderBottomHeight]; //56 lines of border + + attr = new short[DisplayLength]; //6144 bytes of display memory will be mapped + + SetupScreenSize(); + + Reset(); + } + + #endregion + + #region Misc Operations + + public override void Reset() + { + contentionStartPeriod = 14335; // + LateTiming; + contentionEndPeriod = contentionStartPeriod + (ScreenHeight * TstatesPerScanline); + screen = _machine.RAM0; + screenByteCtr = DisplayStart; + ULAByteCtr = 0; + actualULAStart = 14340 - 24 - (TstatesPerScanline * BorderTopHeight);// + LateTiming; + lastTState = actualULAStart; + BuildAttributeMap(); + BuildContentionTable(); + } + + #endregion + + #region Contention Methods + + public override bool IsContended(int addr) + { + if ((addr & 49152) == 16384) + return true; + return false; + } + + public override void BuildContentionTable() + { + int t = contentionStartPeriod; + while (t < contentionEndPeriod) + { + //for 128 t-states + for (int i = 0; i < 128; i += 8) + { + contentionTable[t++] = 6; + contentionTable[t++] = 5; + contentionTable[t++] = 4; + contentionTable[t++] = 3; + contentionTable[t++] = 2; + contentionTable[t++] = 1; + contentionTable[t++] = 0; + contentionTable[t++] = 0; + } + t += (TstatesPerScanline - 128); //24 tstates of right border + left border + 48 tstates of retrace + } + + //build top half of tstateToDisp table + //vertical retrace period + for (t = 0; t < actualULAStart; t++) + tstateToDisp[t] = 0; + + //next 48 are actual border + while (t < actualULAStart + (TstateAtTop)) + { + //border(24t) + screen (128t) + border(24t) = 176 tstates + for (int g = 0; g < 176; g++) + tstateToDisp[t++] = 1; + + //horizontal retrace + for (int g = 176; g < TstatesPerScanline; g++) + tstateToDisp[t++] = 0; + } + + //build middle half of display + int _x = 0; + int _y = 0; + int scrval = 2; + while (t < actualULAStart + (TstateAtTop) + (ScreenHeight * TstatesPerScanline)) + { + //left border + for (int g = 0; g < 24; g++) + tstateToDisp[t++] = 1; + + //screen + for (int g = 24; g < 24 + 128; g++) + { + //Map screenaddr to tstate + if (g % 4 == 0) + { + scrval = (((((_y & 0xc0) >> 3) | (_y & 0x07) | (0x40)) << 8)) | (((_x >> 3) & 0x1f) | ((_y & 0x38) << 2)); + _x += 8; + } + tstateToDisp[t++] = (short)scrval; + } + _y++; + + //right border + for (int g = 24 + 128; g < 24 + 128 + 24; g++) + tstateToDisp[t++] = 1; + + //horizontal retrace + for (int g = 24 + 128 + 24; g < 24 + 128 + 24 + 48; g++) + tstateToDisp[t++] = 0; + } + + int h = contentionStartPeriod + 3; + while (h < contentionEndPeriod + 3) + { + for (int j = 0; j < 128; j += 8) + { + floatingBusTable[h] = tstateToDisp[h + 2]; //screen address + floatingBusTable[h + 1] = attr[(tstateToDisp[h + 2] - 16384)]; //attr address + floatingBusTable[h + 2] = tstateToDisp[h + 2 + 4]; //screen address + 1 + floatingBusTable[h + 3] = attr[(tstateToDisp[h + 2 + 4] - 16384)]; //attr address + 1 + h += 8; + } + h += TstatesPerScanline - 128; + } + + //build bottom border + while (t < actualULAStart + (TstateAtTop) + (ScreenHeight * TstatesPerScanline) + (TstateAtBottom)) + { + //border(24t) + screen (128t) + border(24t) = 176 tstates + for (int g = 0; g < 176; g++) + tstateToDisp[t++] = 1; + + //horizontal retrace + for (int g = 176; g < TstatesPerScanline; g++) + tstateToDisp[t++] = 0; + } + } + + #endregion + + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.cs new file mode 100644 index 0000000000..d9737a40f1 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Machine/ZXSpectrum48K/ZX48.cs @@ -0,0 +1,41 @@ +using BizHawk.Emulation.Cores.Components.Z80A; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZX48 : SpectrumBase + { + #region Construction + + /// + /// Main constructor + /// + /// + /// + public ZX48(ZXSpectrum spectrum, Z80A cpu, ZXSpectrum.BorderType borderType, List files, List joysticks) + { + Spectrum = spectrum; + CPU = cpu; + + ULADevice = new ULA48(this); + + BuzzerDevice = new Buzzer(this); + BuzzerDevice.Init(44100, ULADevice.FrameLength); + + KeyboardDevice = new StandardKeyboard(this); + + InitJoysticks(joysticks); + + TapeDevice = new DatacorderDevice(); + TapeDevice.Init(this); + + InitializeMedia(files); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaSerializationType.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaSerializationType.cs new file mode 100644 index 0000000000..def321b245 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaSerializationType.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Represents the different types of media serializer avaiable + /// + public enum MediaSerializationType + { + NONE, + TZX, + TAP + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaSerializer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaSerializer.cs new file mode 100644 index 0000000000..44c99c28ff --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/MediaSerializer.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Abtract class that represents all Media Serializers + /// + public abstract class MediaSerializer + { + /// + /// The type of serializer + /// + public abstract MediaSerializationType FormatType { get; } + + /// + /// Signs whether this class can be used to serialize + /// + public virtual bool IsSerializer + { + get + { + return false; + } + } + + /// + /// Signs whether this class can be used to de-serialize + /// + public virtual bool IsDeSerializer + { + get + { + return false; + } + } + + /// + /// Serialization method + /// + /// + public virtual void Serialize(byte[] data) + { + throw new NotImplementedException(this.GetType().ToString() + + "Serialize operation is not implemented for this serializer"); + } + + /// + /// DeSerialization method + /// + /// + public virtual void DeSerialize(byte[] data) + { + throw new NotImplementedException(this.GetType().ToString() + + "DeSerialize operation is not implemented for this serializer"); + } + + #region Static Tools + + /// + /// Converts an int32 value into a byte array + /// + /// + /// + protected 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 + /// + /// + /// + /// + protected 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 + /// + /// + /// + /// + protected 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 + /// + /// + /// + /// + protected static void SetWordValue(byte[] buf, int offsetIndex, ushort value) + { + buf[offsetIndex] = (byte)value; + buf[offsetIndex + 1] = (byte)(value >> 8); + } + + #endregion + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapSerializer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapSerializer.cs new file mode 100644 index 0000000000..01e9bee86c --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapSerializer.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Reponsible for TAP format serializaton + /// + public class TapSerializer : MediaSerializer + { + /// + /// The type of serializer + /// + private MediaSerializationType _formatType = MediaSerializationType.TAP; + public override MediaSerializationType FormatType + { + get + { + return _formatType; + } + } + + /// + /// Signs whether this class can be used to serialize + /// + public override bool IsSerializer { get { return false; } } + + /// + /// Signs whether this class can be used to de-serialize + /// + public override bool IsDeSerializer { get { return true; } } + + #region Construction + + private DatacorderDevice _datacorder; + + public TapSerializer(DatacorderDevice _tapeDevice) + { + _datacorder = _tapeDevice; + } + + #endregion + + + #region TAP Format Constants + + /// + /// Pilot pulse length + /// + public const int PILOT_PL = 2168; + + /// + /// Pilot pulses in the ROM header block + /// + public const int HEADER_PILOT_COUNT = 8063; + + /// + /// Pilot pulses in the ROM data block + /// + public const int DATA_PILOT_COUNT = 3223; + + /// + /// Sync 1 pulse length + /// + public const int SYNC_1_PL = 667; + + /// + /// Sync 2 pulse lenth + /// + public const int SYNC_2_PL = 735; + + /// + /// Bit 0 pulse length + /// + public const int BIT_0_PL = 855; + + /// + /// Bit 1 pulse length + /// + public const int BIT_1_PL = 1710; + + /// + /// End sync pulse length + /// + public const int TERM_SYNC = 947; + + /// + /// 1 millisecond pause + /// + public const int PAUSE_MS = 3500; + + /// + /// Used bit count in last byte + /// + public const int BIT_COUNT_IN_LAST = 8; + + #endregion + + /// + /// DeSerialization method + /// + /// + public override void DeSerialize(byte[] data) + { + /* + The .TAP files contain blocks of tape-saved data. All blocks start with two bytes specifying how many bytes will follow (not counting the two length bytes). Then raw tape data follows, including the flag and checksum bytes. The checksum is the bitwise XOR of all bytes including the flag byte. For example, when you execute the line SAVE "ROM" CODE 0,2 this will result: + + |------ Spectrum-generated data -------| |---------| + + 13 00 00 03 52 4f 4d 7x20 02 00 00 00 00 80 f1 04 00 ff f3 af a3 + + ^^^^^...... first block is 19 bytes (17 bytes+flag+checksum) + ^^... flag byte (A reg, 00 for headers, ff for data blocks) + ^^ first byte of header, indicating a code block + + file name ..^^^^^^^^^^^^^ + header info ..............^^^^^^^^^^^^^^^^^ + checksum of header .........................^^ + length of second block ........................^^^^^ + flag byte ............................................^^ + first two bytes of rom .................................^^^^^ + checksum (checkbittoggle would be a better name!).............^^ + */ + + // clear existing tape blocks + _datacorder.DataBlocks.Clear(); + + // convert bytearray to memory stream + MemoryStream stream = new MemoryStream(data); + + // the first 2 bytes of the TAP file designate the length of the first data block + // this (I think) should always be 17 bytes (as this is the tape header) + byte[] blockLengthData = new byte[2]; + + // we are now going to stream through the entire file processing a block at a time + while (stream.Position < stream.Length) + { + // read and calculate the length of the block + stream.Read(blockLengthData, 0, 2); + int blockSize = BitConverter.ToUInt16(blockLengthData, 0); + if (blockSize == 0) + { + // block size is 0 - this is probably invalid (but I guess could be EoF in some situations) + break; + } + + // copy the entire block into a new bytearray + byte[] blockdata = new byte[blockSize]; + stream.Read(blockdata, 0, blockSize); + + // create and populate a new tapedatablock object + TapeDataBlock tdb = new TapeDataBlock(); + + // ascertain the block description + string description = string.Empty; + byte crc = 0; + byte crcValue = 0; + byte crcFile = 0; + byte[] programData = new byte[10]; + + // calculate block checksum value + for (int i = 0; i < blockSize; i++) + { + crc ^= blockdata[i]; + if (i < blockSize - 1) + { + crcValue = crc; + } + else + { + crcFile = blockdata[i]; + } + } + + // 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.) + */ + + if (blockdata[0] == 0x00 && blockSize == 19 && (blockdata[1] == 0x00) || blockdata[1] == 3) + { + // This is the PROGRAM header + // take the 10 filename bytes (that start at offset 2) + programData = blockdata.Skip(2).Take(10).ToArray(); + + // get the filename as a string (with padding removed) + string fileName = Encoding.ASCII.GetString(programData).Trim(); + + // get the type + string type = ""; + if (blockdata[0] == 0x00) + { + type = "Program"; + } + else + { + type = "Bytes"; + } + + // 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"; + } + else + { + // other type + description = string.Format("#{0} block, {1} bytes", blockdata[0].ToString("X2"), blockSize - 2); + description += string.Format(", crc {0}", ((crc != 0) ? string.Format("bad (#{0:X2}!=#{1:X2})", crcFile, crcValue) : "ok")); + } + + tdb.BlockDescription = BlockType.Standard_Speed_Data_Block; + + // calculate the data periods for this block + int pilotLength = 0; + + // work out pilot length + if (blockdata[0] < 4) + { + pilotLength = 8064; + } + else + { + pilotLength = 3220; + } + + // create a list to hold the data periods + List dataPeriods = new List(); + + // generate pilot pulses + for (int i = 0; i < pilotLength; i++) + { + dataPeriods.Add(PILOT_PL); + } + + // add syncro pulses + dataPeriods.Add(SYNC_1_PL); + dataPeriods.Add(SYNC_2_PL); + + 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(BIT_1_PL); + else + dataPeriods.Add(BIT_0_PL); + if ((blockdata[i] & b) != 0) + dataPeriods.Add(BIT_1_PL); + else + dataPeriods.Add(BIT_0_PL); + } + } + + // add the last byte + for (byte c = 0x80; c != (byte)(0x80 >> BIT_COUNT_IN_LAST); c >>= 1) + { + if ((blockdata[pos] & c) != 0) + dataPeriods.Add(BIT_1_PL); + else + dataPeriods.Add(BIT_0_PL); + if ((blockdata[pos] & c) != 0) + dataPeriods.Add(BIT_1_PL); + else + dataPeriods.Add(BIT_0_PL); + } + + // add block pause + int actualPause = PAUSE_MS * 1000; + dataPeriods.Add(actualPause); + + // add to the tapedatablock object + tdb.DataPeriods = dataPeriods; + + // add the raw data + tdb.BlockData = blockdata; + + // add block to the tape + _datacorder.DataBlocks.Add(tdb); + + } + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeCommand.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeCommand.cs new file mode 100644 index 0000000000..14fc54a460 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeCommand.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// 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/SinclairSpectrum/Media/Tape/TapeDataBlock.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs new file mode 100644 index 0000000000..f714d21385 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TapeDataBlock.cs @@ -0,0 +1,264 @@ +using BizHawk.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// 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(); + + /// + /// 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; } + } + + /// + /// 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 + } + + /// + /// 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/SinclairSpectrum/Media/Tape/TzxSerializer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TzxSerializer.cs new file mode 100644 index 0000000000..28a761cd3d --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/Media/Tape/TzxSerializer.cs @@ -0,0 +1,1632 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Reponsible for TZX format serializaton + /// + public class TzxSerializer : MediaSerializer + { + /// + /// The type of serializer + /// + private MediaSerializationType _formatType = MediaSerializationType.TZX; + public override MediaSerializationType FormatType + { + get + { + return _formatType; + } + } + + /// + /// Signs whether this class can be used to serialize + /// + public override bool IsSerializer { get { return false; } } + + /// + /// Signs whether this class can be used to de-serialize + /// + public override bool IsDeSerializer { get { return true; } } + + /// + /// 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 TzxSerializer(DatacorderDevice _tapeDevice) + { + _datacorder = _tapeDevice; + } + + #endregion + + /// + /// DeSerialization method + /// + /// + public override void DeSerialize(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); + } + + } + + /// + /// 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; + 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; + } + #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); + + _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); + + // add the block + _datacorder.DataBlocks.Add(t2); + + // advance the position to the next block + _position += blockLen; + } + #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(); + + // 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(); + + // 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); + + // add the block + _datacorder.DataBlocks.Add(t2); + + // advance the position to the next block + _position += blockLen; + } + #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); + } + + // increment position + _position++; + + // add the block + _datacorder.DataBlocks.Add(t); + } + #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(); + + // add the block + _datacorder.DataBlocks.Add(t); + } + #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) + { + string test = "dgfg"; + } + #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]"; + } + + 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; + } + + // add end of block pause + t.DataPeriods.Add(pauseDuration); + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advanced position to next block + _position += 2; + } + #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; + + // 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; + + // 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) + { + 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 +"]"; + + // 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]"; + + // 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); + + // 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; + + // 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); + + // 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); + + // 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; + + // 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.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 + "]"; + + // 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, 0); + _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"; + + // 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; + + _position += 2; + int blockLen = GetWordValue(data, 0); + + // add to tape + _datacorder.DataBlocks.Add(t); + + // advance to next block + _position += blockLen; + } + #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; + + 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; + + // 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) + { + + } + #endregion + + #region ID 17 - C64 Turbo Tape Data Block + private void ProcessBlockID17(byte[] data) + { + + } + #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 && (blockdata[1] == 0x00) || blockdata[1] == 3) + { + // 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 - 2); + //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) + { + 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 + ) + { + + // 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 + } + + public enum DataBlockType + { + Standard, + Turbo, + Pure + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs new file mode 100644 index 0000000000..303eda38b7 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/SoundProviderMixer.cs @@ -0,0 +1,288 @@ +using BizHawk.Emulation.Common; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// 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 + /// + internal sealed class SoundProviderMixer : ISoundProvider + { + private class Provider + { + public ISoundProvider SoundProvider { 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, params ISoundProvider[] soundProviders) + { + SoundProviders = new List(); + + foreach (var s in soundProviders) + { + SoundProviders.Add(new Provider + { + SoundProvider = s, + MaxVolume = maxVolume, + }); + } + + EqualizeVolumes(); + } + + public void AddSource(ISoundProvider source) + { + SoundProviders.Add(new Provider + { + SoundProvider = source, + MaxVolume = short.MaxValue + }); + + EqualizeVolumes(); + } + + public void AddSource(ISoundProvider source, short maxVolume) + { + SoundProviders.Add(new Provider + { + SoundProvider = source, + MaxVolume = maxVolume + }); + + 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) + { + nsamp = firstEntry.NSamp; + samples = new short[nsamp * 2]; + + if (_stereo) + { + for (int i = 0; i < samples.Length; i++) + { + short sectorVal = 0; + foreach (var sp in SoundProviders) + { + if (sp.Buffer[i] > sp.MaxVolume) + sectorVal += (short)sp.MaxVolume; + else + { + sectorVal += sp.Buffer[i]; + } + } + + samples[i] = sectorVal; + } + } + else + { + // convert to mono + for (int i = 0; i < samples.Length; i += 2) + { + short s = 0; + foreach (var sp in SoundProviders) + { + s += (short)((sp.Buffer[i] + sp.Buffer[i + 1]) / 2); + } + + samples[i] = s; + samples[i + 1] = s; + } + } + } + + else if (!sameCount) + { + // this is a pretty poor implementation that doesnt work very well + // ideally soundproviders should ensure that their number of samples is identical + int divisor = 1; + int highestCount = 0; + + // get the lowest divisor of all the soundprovider nsamps + for (int d = 2; d < 999; d++) + { + bool divFound = false; + foreach (var sp in SoundProviders) + { + if (sp.NSamp > highestCount) + highestCount = sp.NSamp; + + if (sp.NSamp % d == 0) + divFound = true; + else + divFound = false; + } + + if (divFound) + { + divisor = d; + break; + } + } + + // now we have the largest current number of samples among the providers + // along with a common divisor for all of them + nsamp = highestCount * divisor; + samples = new short[nsamp * 2]; + + // take a pass at populating the samples array for each provider + foreach (var sp in SoundProviders) + { + short sectorVal = 0; + int pos = 0; + for (int i = 0; i < sp.Buffer.Length; i++) + { + if (sp.Buffer[i] > sp.MaxVolume) + sectorVal = (short)sp.MaxVolume; + else + sectorVal = sp.Buffer[i]; + + for (int s = 0; s < divisor; s++) + { + samples[pos++] += sectorVal; + } + } + } + + /* + // get the highest number of samples + int max = SoundProviders.Aggregate((i, j) => i.Buffer.Length > j.Buffer.Length ? i : j).Buffer.Length; + + nsamp = max; + samples = new short[nsamp * 2]; + + // take a pass at populating the samples array for each provider + foreach (var sp in SoundProviders) + { + short sectorVal = 0; + int pos = 0; + for (int i = 0; i < sp.Buffer.Length; i++) + { + if (sp.Buffer[i] > sp.MaxVolume) + sectorVal = (short)sp.MaxVolume; + else + { + if (sp.SoundProvider is AY38912) + { + // boost audio + sectorVal += (short)(sp.Buffer[i] * 2); + } + else + { + sectorVal += sp.Buffer[i]; + } + } + + samples[pos++] += sectorVal; + } + } + */ + + } + } + + #endregion + + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Controllers.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Controllers.cs new file mode 100644 index 0000000000..38a281ecca --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Controllers.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using BizHawk.Common; +using BizHawk.Common.ReflectionExtensions; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum + { + /// + /// The one ZX Hawk ControllerDefinition + /// + public ControllerDefinition ZXSpectrumControllerDefinition + { + get + { + ControllerDefinition definition = new ControllerDefinition(); + definition.Name = "ZXSpectrum Controller"; + + // joysticks + List joys1 = new List + { + // P1 Joystick + "P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 Button", + }; + + foreach (var s in joys1) + { + definition.BoolButtons.Add(s); + definition.CategoryLabels[s] = "J1 (" + ((ZXSpectrumSyncSettings)SyncSettings as ZXSpectrumSyncSettings).JoystickType1.ToString() + ")"; + } + + List joys2 = new List + { + // P2 Joystick + "P2 Up", "P2 Down", "P2 Left", "P2 Right", "P2 Button", + }; + + foreach (var s in joys2) + { + definition.BoolButtons.Add(s); + definition.CategoryLabels[s] = "J2 (" + ((ZXSpectrumSyncSettings)SyncSettings as ZXSpectrumSyncSettings).JoystickType2.ToString() + ")"; + } + + List joys3 = new List + { + // P3 Joystick + "P3 Up", "P3 Down", "P3 Left", "P3 Right", "P3 Button", + }; + + foreach (var s in joys3) + { + definition.BoolButtons.Add(s); + definition.CategoryLabels[s] = "J3 (" + ((ZXSpectrumSyncSettings)SyncSettings as ZXSpectrumSyncSettings).JoystickType3.ToString() + ")"; + } + + // keyboard + List keys = new List + { + /// Controller mapping includes all keyboard keys from the following models: + /// https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/ZXSpectrum48k.jpg/1200px-ZXSpectrum48k.jpg + /// https://upload.wikimedia.org/wikipedia/commons/c/ca/ZX_Spectrum%2B.jpg + + // Keyboard - row 1 + "Key True Video", "Key Inv Video", "Key 1", "Key 2", "Key 3", "Key 4", "Key 5", "Key 6", "Key 7", "Key 8", "Key 9", "Key 0", "Key Break", + // Keyboard - row 2 + "Key Delete", "Key Graph", "Key Q", "Key W", "Key E", "Key R", "Key T", "Key Y", "Key U", "Key I", "Key O", "Key P", + // Keyboard - row 3 + "Key Extend Mode", "Key Edit", "Key A", "Key S", "Key D", "Key F", "Key G", "Key H", "Key J", "Key K", "Key L", "Key Return", + // Keyboard - row 4 + "Key Caps Shift", "Key Caps Lock", "Key Z", "Key X", "Key C", "Key V", "Key B", "Key N", "Key M", "Key Period", + // Keyboard - row 5 + "Key Symbol Shift", "Key Semi-Colon", "Key Quote", "Key Left Cursor", "Key Right Cursor", "Key Space", "Key Up Cursor", "Key Down Cursor", "Key Comma", + }; + + foreach (var s in keys) + { + definition.BoolButtons.Add(s); + definition.CategoryLabels[s] = "Keyboard"; + } + + // Datacorder (tape device) + List power = new List + { + // Tape functions + "Soft Reset", "Hard Reset" + }; + + 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"; + } + + return definition; + } + } + + + + /* + /// + /// Controller mapping includes all keyboard keys from the following models: + /// https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/ZXSpectrum48k.jpg/1200px-ZXSpectrum48k.jpg + /// https://upload.wikimedia.org/wikipedia/commons/c/ca/ZX_Spectrum%2B.jpg + /// + public static readonly ControllerDefinition ZXSpectrumControllerDefinition = new ControllerDefinition + { + Name = "ZXSpectrum Controller", + BoolButtons = + { + // Kempston Joystick (P1) + "P1 Up", "P1 Down", "P1 Left", "P1 Right", "P1 Button", + // Keyboard - row 1 + "Key True Video", "Key Inv Video", "Key 1", "Key 2", "Key 3", "Key 4", "Key 5", "Key 6", "Key 7", "Key 8", "Key 9", "Key 0", "Key Break", + // Keyboard - row 2 + "Key Delete", "Key Graph", "Key Q", "Key W", "Key E", "Key R", "Key T", "Key Y", "Key U", "Key I", "Key O", "Key P", + // Keyboard - row 3 + "Key Extend Mode", "Key Edit", "Key A", "Key S", "Key D", "Key F", "Key G", "Key H", "Key J", "Key K", "Key L", "Key Return", + // Keyboard - row 4 + "Key Caps Shift", "Key Caps Lock", "Key Z", "Key X", "Key C", "Key V", "Key B", "Key N", "Key M", "Key Period", + // Keyboard - row 5 + "Key Symbol Shift", "Key Semi-Colon", "Key Quote", "Key Left Cursor", "Key Right Cursor", "Key Space", "Key Up Cursor", "Key Down Cursor", "Key Comma", + // Tape functions + "Play Tape", "Stop Tape", "RTZ Tape", "Record Tape", "Insert Next Tape", "Insert Previous Tape", "Next Tape Block", "Prev Tape Block" + } + }; + + */ + } + + /// + /// The possible joystick types + /// + public enum JoystickType + { + NULL, + Kempston, + SinclairLEFT, + SinclairRIGHT, + Cursor + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IDebuggable.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IDebuggable.cs new file mode 100644 index 0000000000..e086ad09e3 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IDebuggable.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; + +using BizHawk.Common.NumberExtensions; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum : 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 int TotalExecutedCycles => (int)_cpu.TotalExecutedCycles; + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IEmulator.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IEmulator.cs new file mode 100644 index 0000000000..c87c9991d7 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IEmulator.cs @@ -0,0 +1,82 @@ +using BizHawk.Emulation.Common; +using BizHawk.Common.NumberExtensions; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum : 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 => "ZXSpectrum"; + + private bool deterministicEmulation; + public bool DeterministicEmulation + { + get { return deterministicEmulation; } + } + + //public bool DeterministicEmulation => true; + + 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/SinclairSpectrum/ZXSpectrum.IInputPollable.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IInputPollable.cs new file mode 100644 index 0000000000..da34d34c66 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IInputPollable.cs @@ -0,0 +1,26 @@ + +using System; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum : 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/SinclairSpectrum/ZXSpectrum.IMemoryDomains.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IMemoryDomains.cs new file mode 100644 index 0000000000..5245886f7c --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IMemoryDomains.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum //: IMemoryDomains + { + 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("ROM0", _machine.ROM0); + SyncByteArrayDomain("ROM1", _machine.ROM1); + SyncByteArrayDomain("ROM2", _machine.ROM2); + SyncByteArrayDomain("ROM3", _machine.ROM3); + 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/SinclairSpectrum/ZXSpectrum.ISettable.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISettable.cs new file mode 100644 index 0000000000..f1c5bdff80 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.ISettable.cs @@ -0,0 +1,349 @@ +using System; +using Newtonsoft.Json; + +using BizHawk.Common; +using BizHawk.Emulation.Common; +using System.ComponentModel; +using System.Text; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum : ISettable + { + internal ZXSpectrumSettings Settings = new ZXSpectrumSettings(); + internal ZXSpectrumSyncSettings SyncSettings = new ZXSpectrumSyncSettings(); + + public ZXSpectrumSettings GetSettings() + { + return Settings.Clone(); + } + + public ZXSpectrumSyncSettings GetSyncSettings() + { + return SyncSettings.Clone(); + } + + public bool PutSettings(ZXSpectrumSettings o) + { + //if (SoundMixer != null) + //SoundMixer.Stereo = o.StereoSound; + + if (_machine != null && _machine.AYDevice != null && _machine.AYDevice.GetType() == typeof(AYChip)) + { + ((AYChip)_machine.AYDevice as AYChip).PanningConfiguration = o.AYPanConfig; + } + + Settings = o; + + return false; + } + + public bool PutSyncSettings(ZXSpectrumSyncSettings o) + { + bool ret = ZXSpectrumSyncSettings.NeedsReboot(SyncSettings, o); + SyncSettings = o; + return ret; + } + + + + public class ZXSpectrumSettings + { + [DisplayName("Auto-load/stop tape")] + [Description("Auto or manual tape operation. Auto will attempt to detect CPU tape traps and automatically Stop/Start the tape")] + [DefaultValue(true)] + public bool AutoLoadTape { get; set; } + + /* + [DisplayName("Stereo Sound")] + [Description("Turn stereo sound on or off")] + [DefaultValue(true)] + public bool StereoSound { get; set; } + */ + + [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(AYChip.AYPanConfig.ABC)] + public AYChip.AYPanConfig AYPanConfig { 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; } + + + public ZXSpectrumSettings Clone() + { + return (ZXSpectrumSettings)MemberwiseClone(); + } + + public ZXSpectrumSettings() + { + BizHawk.Common.SettingsUtil.SetDefaultValues(this); + } + } + + public class ZXSpectrumSyncSettings + { + [DisplayName("Deterministic Emulation")] + [Description("If true, the core agrees to behave in a completely deterministic manner")] + [DefaultValue(true)] + public bool DeterministicEmulation { get; set; } + + [DisplayName("Spectrum model")] + [Description("The model of spectrum to be emulated")] + [DefaultValue(MachineType.ZXSpectrum48)] + public MachineType MachineType { get; set; } + + [DisplayName("Border type")] + [Description("Select how to show the border area")] + [DefaultValue(BorderType.Full)] + public BorderType BorderType { get; set; } + + [DisplayName("Tape Load Speed")] + [Description("Select how fast the spectrum loads the game from tape")] + [DefaultValue(TapeLoadSpeed.Accurate)] + public TapeLoadSpeed TapeLoadSpeed { get; set; } + + [DisplayName("Joystick 1")] + [Description("The emulated joystick assigned to P1 (SHOULD BE UNIQUE TYPE!)")] + [DefaultValue(JoystickType.Kempston)] + public JoystickType JoystickType1 { get; set; } + + [DisplayName("Joystick 2")] + [Description("The emulated joystick assigned to P2 (SHOULD BE UNIQUE TYPE!)")] + [DefaultValue(JoystickType.SinclairLEFT)] + public JoystickType JoystickType2 { get; set; } + + [DisplayName("Joystick 3")] + [Description("The emulated joystick assigned to P3 (SHOULD BE UNIQUE TYPE!)")] + [DefaultValue(JoystickType.SinclairRIGHT)] + public JoystickType JoystickType3 { get; set; } + + + public ZXSpectrumSyncSettings Clone() + { + return (ZXSpectrumSyncSettings)MemberwiseClone(); + } + + public ZXSpectrumSyncSettings() + { + SettingsUtil.SetDefaultValues(this); + } + + public static bool NeedsReboot(ZXSpectrumSyncSettings x, ZXSpectrumSyncSettings y) + { + return !DeepEquality.DeepEquals(x, y); + } + } + + public enum OSDVerbosity + { + /// + /// Show all OSD messages + /// + Full, + /// + /// Only show machine/device generated messages + /// + Medium, + /// + /// No core-driven OSD messages + /// + None + } + + /// + /// The size of the Spectrum border + /// + public enum BorderType + { + /// + /// How it was originally back in the day + /// + Full, + + /// + /// All borders 24px + /// + Medium, + + /// + /// All borders 10px + /// + Small, + + /// + /// No border at all + /// + None, + + /// + /// Top and bottom border removed so that the result is *almost* 16:9 + /// + Widescreen, + } + + /// + /// The speed at which the tape is loaded + /// NOT IN USE YET + /// + public enum TapeLoadSpeed + { + Accurate, + //Fast, + //Fastest + } + } + + /// + /// Provides information on each emulated machine + /// + public class ZXMachineMetaData + { + 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 ZXMachineMetaData GetMetaObject(MachineType type) + { + ZXMachineMetaData m = new ZXMachineMetaData(); + m.MachineType = type; + + switch (type) + { + case MachineType.ZXSpectrum16: + m.Name = "Sinclair ZX Spectrum 16K"; + m.Description = "The original ZX Spectrum 16K RAM version. Aside from available RAM this machine is technically identical to the 48K machine that was released at the same time. "; + m.Description += "Due to the small amount of RAM, very few games were actually made to run on this model."; + m.Released = "1982"; + m.CPU = "Zilog Z80A @ 3.5MHz"; + m.Memory = "16KB ROM / 16KB 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.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(); + + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IStatable.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IStatable.cs new file mode 100644 index 0000000000..01419fd093 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.IStatable.cs @@ -0,0 +1,97 @@ +using System.IO; + +using BizHawk.Common; +using BizHawk.Emulation.Common; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum : 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("ZXSpectrum"); + _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("ZXSpectrum"); + _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/SinclairSpectrum/ZXSpectrum.Messaging.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Messaging.cs new file mode 100644 index 0000000000..9ad4e3615a --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Messaging.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + /// + /// Handles all messaging (OSD) operations + /// + public partial class ZXSpectrum + { + /// + /// 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("ZXHAWK: "); + 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 TapeDevice Message Methods + + /// + /// Tape message that is fired on core init + /// + public void OSD_TapeInit() + { + StringBuilder sb = new StringBuilder(); + sb.Append("Tape Media Imported (count: " + _gameInfo.Count() + ")"); + sb.Append("\n"); + for (int i = 0; i < _gameInfo.Count(); i++) + sb.Append(i.ToString() + ": " + _gameInfo[i].Name + "\n"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Emulator); + } + + /// + /// Tape message that is fired when tape is playing + /// + public void OSD_TapePlaying() + { + StringBuilder sb = new StringBuilder(); + sb.Append("PLAYING (" + _machine.TapeMediaIndex + ": " + _gameInfo[_machine.TapeMediaIndex].Name + ")"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when tape is stopped + /// + public void OSD_TapeStopped() + { + StringBuilder sb = new StringBuilder(); + sb.Append("STOPPED (" + _machine.TapeMediaIndex + ": " + _gameInfo[_machine.TapeMediaIndex].Name + ")"); + + SendMessage(sb.ToString().TrimEnd('\n'), MessageCategory.Tape); + } + + /// + /// Tape message that is fired when tape is rewound + /// + public void OSD_TapeRTZ() + { + StringBuilder sb = new StringBuilder(); + sb.Append("REWOUND (" + _machine.TapeMediaIndex + ": " + _gameInfo[_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() + { + StringBuilder sb = new StringBuilder(); + sb.Append("TAPE INSERTED (" + _machine.TapeMediaIndex + ": " + _gameInfo[_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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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 + ": " + _gameInfo[_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(); + } + + #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/SinclairSpectrum/ZXSpectrum.Util.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Util.cs new file mode 100644 index 0000000000..4cb9656858 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.Util.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Emulation.Cores.Computers.SinclairSpectrum +{ + public partial class ZXSpectrum + { + /* + * CPU Helper Methods + */ + + public ushort RegPC + { + get { return (ushort)((_cpu.Regs[0] << 8 | _cpu.Regs[1])); } + set + { + _cpu.Regs[1] = (ushort)(value & 0xFF); + _cpu.Regs[0] = (ushort)((value >> 8) & 0xFF); + } + } + + public ushort RegIX + { + get { return (ushort)((_cpu.Regs[15] << 8 | _cpu.Regs[16] )); } + set + { + _cpu.Regs[16] = (ushort)(value & 0xFF); + _cpu.Regs[15] = (ushort)((value >> 8) & 0xFF); + } + } + + public ushort RegDE + { + get { return (ushort)((_cpu.Regs[8] << 8 | _cpu.Regs[9] )); } + set + { + _cpu.Regs[9] = (ushort)(value & 0xFF); + _cpu.Regs[8] = (ushort)((value >> 8) & 0xFF); + } + } + + public ushort RegAF + { + get { return (ushort)((_cpu.Regs[4] << 8 | _cpu.Regs[5])); } + set + { + _cpu.Regs[5] = (ushort)(value & 0xFF); + _cpu.Regs[4] = (ushort)((value >> 8) & 0xFF); + } + } + + + /// + /// Gets the IX word value + /// + /// + public ushort Get16BitIX() + { + return Convert.ToUInt16(_cpu.Regs[_cpu.Ixh] | _cpu.Regs[_cpu.Ixl] << 8); + } + + /// + /// Set the IX word value + /// + /// + /// + public void Set16BitIX(ushort IX) + { + _cpu.Regs[_cpu.Ixh] = (ushort)(IX & 0xFF); + _cpu.Regs[_cpu.Ixl] = (ushort)((IX >> 8) & 0xff); + } + + /// + /// Gets the AF word value + /// + /// + public ushort Get16BitAF() + { + return Convert.ToUInt16(_cpu.Regs[_cpu.A] | _cpu.Regs[_cpu.F] << 8); + } + + /// + /// Set the AF word value + /// + /// + /// + public void Set16BitAF(ushort AF) + { + _cpu.Regs[_cpu.A] = (ushort)(AF & 0xFF); + _cpu.Regs[_cpu.F] = (ushort)((AF >> 8) & 0xff); + } + + /// + /// Gets the AF shadow word value + /// + /// + public ushort Get16BitAF_() + { + return Convert.ToUInt16(_cpu.Regs[_cpu.A_s] | _cpu.Regs[_cpu.F_s] << 8); + } + + /// + /// Set the AF shadow word value + /// + /// + /// + public void Set16BitAF_(ushort AF_) + { + _cpu.Regs[_cpu.A_s] = (ushort)(AF_ & 0xFF); + _cpu.Regs[_cpu.F_s] = (ushort)((AF_ >> 8) & 0xff); + } + + /// + /// Gets the DE word value + /// + /// + public ushort Get16BitDE() + { + return Convert.ToUInt16(_cpu.Regs[_cpu.E] | _cpu.Regs[_cpu.D] << 8); + } + + /// + /// Set the DE word value + /// + /// + /// + public void Set16BitDE(ushort DE) + { + _cpu.Regs[_cpu.D] = (ushort)(DE & 0xFF); + _cpu.Regs[_cpu.E] = (ushort)((DE >> 8) & 0xff); + } + + + /// + /// Z80 Status Indicator Flag Reset masks + /// + /// + [Flags] + public enum FlagsResetMask : byte + { + /// Sign Flag + S = 0x7F, + + /// Zero Flag + Z = 0xBF, + + /// This flag is not used. + R5 = 0xDF, + + /// Half Carry Flag + H = 0xEF, + + /// This flag is not used. + R3 = 0xF7, + + /// Parity/Overflow Flag + PV = 0xFB, + + /// Add/Subtract Flag + N = 0xFD, + + /// Carry Flag + C = 0xFE, + } + + /// + /// Z80 Status Indicator Flag Set masks + /// + /// + [Flags] + public enum FlagsSetMask : byte + { + /// Sign Flag + /// + /// The Sign Flag (S) stores the state of the most-significant bit of + /// the Accumulator (bit 7). When the Z80 CPU performs arithmetic + /// operations on signed numbers, the binary twos complement notation + /// is used to represent and process numeric information. + /// + S = 0x80, + + /// + /// Zero Flag + /// + /// + /// The Zero Flag is set (1) or cleared (0) if the result generated by + /// the execution of certain instructions is 0. For 8-bit arithmetic and + /// logical operations, the Z flag is set to a 1 if the resulting byte in + /// the Accumulator is 0. If the byte is not 0, the Z flag is reset to 0. + /// + Z = 0x40, + + /// This flag is not used. + R5 = 0x20, + + /// Half Carry Flag + /// + /// The Half Carry Flag (H) is set (1) or cleared (0) depending on the + /// Carry and Borrow status between bits 3 and 4 of an 8-bit arithmetic + /// operation. This flag is used by the Decimal Adjust Accumulator (DAA) + /// instruction to correct the result of a packed BCD add or subtract operation. + /// + H = 0x10, + + /// This flag is not used. + R3 = 0x08, + + /// Parity/Overflow Flag + /// + /// The Parity/Overflow (P/V) Flag is set to a specific state depending on + /// the operation being performed. For arithmetic operations, this flag + /// indicates an overflow condition when the result in the Accumulator is + /// greater than the maximum possible number (+127) or is less than the + /// minimum possible number (–128). This overflow condition is determined by + /// examining the sign bits of the operands. + /// + PV = 0x04, + + /// Add/Subtract Flag + /// + /// The Add/Subtract Flag (N) is used by the Decimal Adjust Accumulator + /// instruction (DAA) to distinguish between the ADD and SUB instructions. + /// For ADD instructions, N is cleared to 0. For SUB instructions, N is set to 1. + /// + N = 0x02, + + /// Carry Flag + /// + /// The Carry Flag (C) is set or cleared depending on the operation being performed. + /// + C = 0x01, + + /// + /// Combination of S, Z, and PV + /// + SZPV = S | Z | PV, + + /// + /// Combination of N, and H + /// + NH = N | H, + + /// + /// Combination of R3, and R5 + /// + R3R5 = R3 | R5 + } + + /// + /// 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]; + } + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs new file mode 100644 index 0000000000..7f72bf4b4e --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/ZXSpectrum.cs @@ -0,0 +1,274 @@ +using BizHawk.Common; +using BizHawk.Emulation.Common; +using BizHawk.Emulation.Cores.Components; +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.SinclairSpectrum +{ + [Core( + "ZXHawk", + "Asnivor", + isPorted: false, + isReleased: false)] + public partial class ZXSpectrum : IRegionable, IDriveLight + { + public ZXSpectrum(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 }; + + //_file = file; + _files = files?.ToList() ?? new List(); + + if (settings == null) + settings = new ZXSpectrumSettings(); + if (syncSettings == null) + syncSettings = new ZXSpectrumSyncSettings(); + + PutSyncSettings((ZXSpectrumSyncSettings)syncSettings ?? new ZXSpectrumSyncSettings()); + PutSettings((ZXSpectrumSettings)settings ?? new ZXSpectrumSettings()); + + List joysticks = new List(); + joysticks.Add(((ZXSpectrumSyncSettings)syncSettings as ZXSpectrumSyncSettings).JoystickType1); + joysticks.Add(((ZXSpectrumSyncSettings)syncSettings as ZXSpectrumSyncSettings).JoystickType2); + joysticks.Add(((ZXSpectrumSyncSettings)syncSettings as ZXSpectrumSyncSettings).JoystickType3); + + deterministicEmulation = ((ZXSpectrumSyncSettings)syncSettings as ZXSpectrumSyncSettings).DeterministicEmulation; + + switch (SyncSettings.MachineType) + { + case MachineType.ZXSpectrum16: + ControllerDefinition = ZXSpectrumControllerDefinition; + Init(MachineType.ZXSpectrum16, SyncSettings.BorderType, SyncSettings.TapeLoadSpeed, _files, joysticks); + break; + case MachineType.ZXSpectrum48: + ControllerDefinition = ZXSpectrumControllerDefinition; + Init(MachineType.ZXSpectrum48, SyncSettings.BorderType, SyncSettings.TapeLoadSpeed, _files, joysticks); + break; + case MachineType.ZXSpectrum128: + ControllerDefinition = ZXSpectrumControllerDefinition; + Init(MachineType.ZXSpectrum128, SyncSettings.BorderType, SyncSettings.TapeLoadSpeed, _files, joysticks); + break; + case MachineType.ZXSpectrum128Plus2: + ControllerDefinition = ZXSpectrumControllerDefinition; + Init(MachineType.ZXSpectrum128Plus2, SyncSettings.BorderType, SyncSettings.TapeLoadSpeed, _files, joysticks); + break; + case MachineType.ZXSpectrum128Plus2a: + ControllerDefinition = ZXSpectrumControllerDefinition; + Init(MachineType.ZXSpectrum128Plus2a, SyncSettings.BorderType, SyncSettings.TapeLoadSpeed, _files, joysticks); + break; + case MachineType.ZXSpectrum128Plus3: + ControllerDefinition = ZXSpectrumControllerDefinition; + Init(MachineType.ZXSpectrum128Plus3, SyncSettings.BorderType, SyncSettings.TapeLoadSpeed, _files, joysticks); + 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; + + ser.Register(_tracer); + ser.Register(_cpu); + ser.Register(_machine.ULADevice); + + SoundMixer = new SoundProviderMixer((int)(32767 / 10), (ISoundProvider)_machine.BuzzerDevice); + if (_machine.AYDevice != null) + SoundMixer.AddSource(_machine.AYDevice); + //SoundMixer.Stereo = ((ZXSpectrumSettings)settings as ZXSpectrumSettings).StereoSound; + + if (_machine.AYDevice != null && _machine.AYDevice.GetType() == typeof(AYChip)) + { + ((AYChip)_machine.AYDevice as AYChip).PanningConfiguration = ((ZXSpectrumSettings)settings as ZXSpectrumSettings).AYPanConfig; + } + + + ser.Register(SoundMixer); + + + HardReset(); + + SetupMemoryDomains(); + } + + public Action HardReset; + public Action SoftReset; + + private readonly Z80A _cpu; + private readonly TraceBuffer _tracer; + public IController _controller; + public SpectrumBase _machine; + + private List _gameInfo; + + private SoundProviderMixer SoundMixer; + + private readonly List _files; + + public bool DiagRom = false; + + private List diagRoms = new List + { + @"\DiagROM.v28", + @"\zx-diagnostics\testrom.bin" + }; + private int diagIndex = 1; + + private byte[] GetFirmware(int length, params string[] names) + { + if (DiagRom & File.Exists(Directory.GetCurrentDirectory() + diagRoms[diagIndex])) + { + var rom = File.ReadAllBytes(Directory.GetCurrentDirectory() + diagRoms[diagIndex]); + return rom; + } + + // Amstrad licensed ROMs are free to distribute and shipped with BizHawk + byte[] embeddedRom = new byte[length]; + bool embeddedFound = true; + switch (names.FirstOrDefault()) + { + case "48ROM": + embeddedRom = Util.DecompressGzipFile(new MemoryStream(Resources.ZX_48_ROM)); + break; + case "128ROM": + embeddedRom = Util.DecompressGzipFile(new MemoryStream(Resources.ZX_128_ROM)); + break; + case "PLUS2ROM": + embeddedRom = Util.DecompressGzipFile(new MemoryStream(Resources.ZX_plus2_rom)); + break; + case "PLUS2AROM": + embeddedRom = Util.DecompressGzipFile(new MemoryStream(Resources.ZX_plus2a_rom)); + break; + case "PLUS3ROM": + byte[] r0 = Util.DecompressGzipFile(new MemoryStream(Resources.Spectrum3_V4_0_ROM0_bin)); + byte[] r1 = Util.DecompressGzipFile(new MemoryStream(Resources.Spectrum3_V4_0_ROM1_bin)); + byte[] r2 = Util.DecompressGzipFile(new MemoryStream(Resources.Spectrum3_V4_0_ROM2_bin)); + byte[] r3 = Util.DecompressGzipFile(new MemoryStream(Resources.Spectrum3_V4_0_ROM3_bin)); + embeddedRom = r0.Concat(r1).Concat(r2).Concat(r3).ToArray(); + 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("ZXSpectrum", 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, BorderType borderType, TapeLoadSpeed tapeLoadSpeed, List files, List joys) + { + _machineType = machineType; + + // setup the emulated model based on the MachineType + switch (machineType) + { + case MachineType.ZXSpectrum16: + _machine = new ZX16(this, _cpu, borderType, files, joys); + var _systemRom16 = GetFirmware(0x4000, "48ROM"); + var romData16 = RomData.InitROM(machineType, _systemRom16); + _machine.InitROM(romData16); + break; + case MachineType.ZXSpectrum48: + _machine = new ZX48(this, _cpu, borderType, files, joys); + var _systemRom = GetFirmware(0x4000, "48ROM"); + var romData = RomData.InitROM(machineType, _systemRom); + _machine.InitROM(romData); + break; + case MachineType.ZXSpectrum128: + _machine = new ZX128(this, _cpu, borderType, files, joys); + var _systemRom128 = GetFirmware(0x8000, "128ROM"); + var romData128 = RomData.InitROM(machineType, _systemRom128); + _machine.InitROM(romData128); + break; + case MachineType.ZXSpectrum128Plus2: + _machine = new ZX128Plus2(this, _cpu, borderType, files, joys); + var _systemRomP2 = GetFirmware(0x8000, "PLUS2ROM"); + var romDataP2 = RomData.InitROM(machineType, _systemRomP2); + _machine.InitROM(romDataP2); + break; + case MachineType.ZXSpectrum128Plus2a: + _machine = new ZX128Plus2a(this, _cpu, borderType, files, joys); + var _systemRomP4 = GetFirmware(0x10000, "PLUS2AROM"); + var romDataP4 = RomData.InitROM(machineType, _systemRomP4); + _machine.InitROM(romDataP4); + break; + case MachineType.ZXSpectrum128Plus3: + _machine = new ZX128Plus3(this, _cpu, borderType, files, joys); + var _systemRomP3 = GetFirmware(0x10000, "PLUS3ROM"); + var romDataP3 = RomData.InitROM(machineType, _systemRomP3); + _machine.InitROM(romDataP3); + //System.Windows.Forms.MessageBox.Show("+3 is not working at all yet :/"); + 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) + return true; + + return false; + } + } + + #endregion + + } +} diff --git a/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/readme.md b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/readme.md new file mode 100644 index 0000000000..36eb6a6f07 --- /dev/null +++ b/BizHawk.Emulation.Cores/Computers/SinclairSpectrum/readme.md @@ -0,0 +1,39 @@ +## ZXHawk + +At the moment this is experimental and is still being worked on. + +### Implemented and working (as far as I can tell) +* IEmulator +* ZX Spectrum 48k, 128k, +2 & +2A models +* ULA video output (implementing IVideoProvider) +* ULA Mode 1 VBLANK interrupt generation +* IM2 Interrupts and DataBus implementation (thanks Aloysha) +* Beeper/Buzzer output (implementing ISoundProvider) +* AY-3-8912 sound chip implementation (multiple stereo or mono panning options available as a setting) +* Keyboard input (implementing IInputPollable) +* Default keyboard keymappings +* Kempston, Cursor and Sinclair (Left & Right) joysticks emulated +* Tape device that will load spectrum games in realtime (*.tzx and *.tap) +* Most tape protection/loading schemes that I've tested are currently working +* IStatable +* ISettable core settings +* IDebuggable (for what it's worth) +* DeterministicEmulation as a SyncSetting, LagFrame detection and FrameAdvance render & renderSound bools respected (when DeterministicEmulation == false) +* Tape auto-loading routines (as a setting - default ON) +* Basic tape block navigation (NextBlock, PrevBlock) +* Tape-related OSD messages (verbosity level configured in settings) + +### Work in progress +* ZX Spectrum +3 emulation (partially working, see below) +* Exact emulator timings +* Floating memory bus emulation +* TASStudio (need to verify that this works as it should) + +### Not working +* +3 disk drive - no implementation yet +* Hard & Soft Reset menu options in the client (they are greyed out for some reason) + +### Help needed +* I'm not a TASer, i've never TASed before. It would be really useful if someone (anyone) can build this branch and test this core from a TAS-workflow / TAStudio perpective. There may still be some work to do an exact timings and memory contention, but otherwise this core is able to play the majority of speccy games out there. + +-Asnivor diff --git a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IDebuggable.cs b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IDebuggable.cs index c48bbabf8f..8739d2d616 100644 --- a/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IDebuggable.cs +++ b/BizHawk.Emulation.Cores/Consoles/Coleco/ColecoVision.IDebuggable.cs @@ -142,6 +142,6 @@ namespace BizHawk.Emulation.Cores.ColecoVision throw new NotImplementedException(); } - public int TotalExecutedCycles => _cpu.TotalExecutedCycles; + public int TotalExecutedCycles => (int)_cpu.TotalExecutedCycles; } } diff --git a/BizHawk.Emulation.Cores/Consoles/Sega/SMS/SMS.IDebuggable.cs b/BizHawk.Emulation.Cores/Consoles/Sega/SMS/SMS.IDebuggable.cs index d8b0d15402..3a9bf83be6 100644 --- a/BizHawk.Emulation.Cores/Consoles/Sega/SMS/SMS.IDebuggable.cs +++ b/BizHawk.Emulation.Cores/Consoles/Sega/SMS/SMS.IDebuggable.cs @@ -144,7 +144,7 @@ namespace BizHawk.Emulation.Cores.Sega.MasterSystem public int TotalExecutedCycles { - get { return Cpu.TotalExecutedCycles; } + get { return (int)Cpu.TotalExecutedCycles; } } } } diff --git a/BizHawk.Emulation.Cores/FileID.cs b/BizHawk.Emulation.Cores/FileID.cs index edc5252484..1314be8b6f 100644 --- a/BizHawk.Emulation.Cores/FileID.cs +++ b/BizHawk.Emulation.Cores/FileID.cs @@ -42,6 +42,7 @@ namespace BizHawk.Emulation.Cores WS, WSC, NGC, C64, + ZXSpectrum, INT, A26, A52, A78, LNX, diff --git a/BizHawk.Emulation.Cores/Properties/Resources.Designer.cs b/BizHawk.Emulation.Cores/Properties/Resources.Designer.cs index 5a3bbcce9a..73259a9985 100644 --- a/BizHawk.Emulation.Cores/Properties/Resources.Designer.cs +++ b/BizHawk.Emulation.Cores/Properties/Resources.Designer.cs @@ -89,5 +89,85 @@ namespace BizHawk.Emulation.Cores.Properties { return ((byte[])(obj)); } } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] Spectrum3_V4_0_ROM0_bin { + get { + object obj = ResourceManager.GetObject("Spectrum3_V4_0_ROM0_bin", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] Spectrum3_V4_0_ROM1_bin { + get { + object obj = ResourceManager.GetObject("Spectrum3_V4_0_ROM1_bin", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] Spectrum3_V4_0_ROM2_bin { + get { + object obj = ResourceManager.GetObject("Spectrum3_V4_0_ROM2_bin", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] Spectrum3_V4_0_ROM3_bin { + get { + object obj = ResourceManager.GetObject("Spectrum3_V4_0_ROM3_bin", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] ZX_128_ROM { + get { + object obj = ResourceManager.GetObject("ZX_128_ROM", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] ZX_48_ROM { + get { + object obj = ResourceManager.GetObject("ZX_48_ROM", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] ZX_plus2_rom { + get { + object obj = ResourceManager.GetObject("ZX_plus2_rom", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] ZX_plus2a_rom { + get { + object obj = ResourceManager.GetObject("ZX_plus2a_rom", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/BizHawk.Emulation.Cores/Properties/Resources.resx b/BizHawk.Emulation.Cores/Properties/Resources.resx index bdb07b9a9f..b5e12deeae 100644 --- a/BizHawk.Emulation.Cores/Properties/Resources.resx +++ b/BizHawk.Emulation.Cores/Properties/Resources.resx @@ -127,4 +127,28 @@ ..\Resources\sgb-cart-present.spc.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\resources\spectrum3_v4-0_rom0.bin.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\resources\spectrum3_v4-0_rom1.bin.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\resources\spectrum3_v4-0_rom2.bin.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\resources\spectrum3_v4-0_rom3.bin.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\128.ROM.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\48.ROM.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\resources\plus2a.rom.gz;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\plus2.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/128.ROM.gz b/BizHawk.Emulation.Cores/Resources/128.ROM.gz new file mode 100644 index 0000000000..697dc6e872 Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/128.ROM.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/48.ROM.gz b/BizHawk.Emulation.Cores/Resources/48.ROM.gz new file mode 100644 index 0000000000..76b8c9d536 Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/48.ROM.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM0.bin.gz b/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM0.bin.gz new file mode 100644 index 0000000000..d70d805d62 Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM0.bin.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM1.bin.gz b/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM1.bin.gz new file mode 100644 index 0000000000..f813586462 Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM1.bin.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM2.bin.gz b/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM2.bin.gz new file mode 100644 index 0000000000..df586562a5 Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM2.bin.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM3.bin.gz b/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM3.bin.gz new file mode 100644 index 0000000000..3062bc7fb1 Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/Spectrum3_V4-0_ROM3.bin.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/plus2.rom.gz b/BizHawk.Emulation.Cores/Resources/plus2.rom.gz new file mode 100644 index 0000000000..274507f352 Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/plus2.rom.gz differ diff --git a/BizHawk.Emulation.Cores/Resources/plus2a.rom.gz b/BizHawk.Emulation.Cores/Resources/plus2a.rom.gz new file mode 100644 index 0000000000..418ea1f1cf Binary files /dev/null and b/BizHawk.Emulation.Cores/Resources/plus2a.rom.gz differ diff --git a/BizHawk.Emulation.Cores/Sound/SN76489.cs b/BizHawk.Emulation.Cores/Sound/SN76489.cs index 1944edf267..c5e7e113c5 100644 --- a/BizHawk.Emulation.Cores/Sound/SN76489.cs +++ b/BizHawk.Emulation.Cores/Sound/SN76489.cs @@ -21,15 +21,15 @@ namespace BizHawk.Emulation.Cores.Components const int SampleRate = 44100; private static readonly byte[] LogScale = { 0, 10, 13, 16, 20, 26, 32, 40, 51, 64, 81, 102, 128, 161, 203, 255 }; - public void Mix(short[] samples, int start, int len, int maxVolume) + public void Mix(short[] samples, long start, long len, int maxVolume) { if (Volume == 0) return; float adjustedWaveLengthInSamples = SampleRate / (Noise ? (Frequency / (float)Wave.Length) : Frequency); float moveThroughWaveRate = Wave.Length / adjustedWaveLengthInSamples; - int end = start + len; - for (int i = start; i < end; ) + long end = start + len; + for (long i = start; i < end; ) { short value = Wave[(int)WaveOffset]; @@ -46,7 +46,7 @@ namespace BizHawk.Emulation.Cores.Components public byte PsgLatch; private readonly Queue commands = new Queue(256); - int frameStartTime, frameStopTime; + long frameStartTime, frameStopTime; const int PsgBase = 111861; @@ -84,7 +84,7 @@ namespace BizHawk.Emulation.Cores.Components } } - public void BeginFrame(int cycles) + public void BeginFrame(long cycles) { while (commands.Count > 0) { @@ -94,12 +94,12 @@ namespace BizHawk.Emulation.Cores.Components frameStartTime = cycles; } - public void EndFrame(int cycles) + public void EndFrame(long cycles) { frameStopTime = cycles; } - public void WritePsgData(byte value, int cycles) + public void WritePsgData(byte value, long cycles) { commands.Enqueue(new QueuedCommand { Value = value, Time = cycles - frameStartTime }); } @@ -227,15 +227,15 @@ namespace BizHawk.Emulation.Cores.Components public void DiscardSamples() { commands.Clear(); } public void GetSamples(short[] samples) { - int elapsedCycles = frameStopTime - frameStartTime; + long elapsedCycles = frameStopTime - frameStartTime; if (elapsedCycles == 0) elapsedCycles = 1; // hey it's better than diving by zero - int start = 0; + long start = 0; while (commands.Count > 0) { var cmd = commands.Dequeue(); - int pos = ((cmd.Time * samples.Length) / elapsedCycles) & ~1; + long pos = ((cmd.Time * samples.Length) / elapsedCycles) & ~1; GetSamplesImmediate(samples, start, pos - start); start = pos; WritePsgDataImmediate(cmd.Value); @@ -243,16 +243,16 @@ namespace BizHawk.Emulation.Cores.Components GetSamplesImmediate(samples, start, samples.Length - start); } - public void GetSamplesImmediate(short[] samples, int start, int len) + public void GetSamplesImmediate(short[] samples, long start, long len) { - for (int i = 0; i < 4; i++) + for (long i = 0; i < 4; i++) Channels[i].Mix(samples, start, len, MaxVolume); } class QueuedCommand { public byte Value; - public int Time; + public long Time; } } } \ No newline at end of file