diff --git a/.gitignore b/.gitignore index 78bb45e23..716232f88 100644 --- a/.gitignore +++ b/.gitignore @@ -281,4 +281,6 @@ __pycache__/ .vscode/settings.json BenchmarkDotNet.Artifacts/ + /src/apps/Highbyte.DotNet6502.App.SadConsole/appsettings.Development.json +/src/apps/Highbyte.DotNet6502.App.SilkNetNative/appsettings.Development.json diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs index 7d9095bf7..addc1a40f 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64ConfigUIConsole.cs @@ -45,8 +45,56 @@ public C64ConfigUIConsole(SadConsoleHostApp sadConsoleHostApp, IConfiguration co private void DrawUIItems() { + // Automatic download of C64 ROMs + var autoDownloadROMButton = new Button("Auto download ROM files") + { + Name = "autoDownloadROMButton", + Position = (1, 2), + }; + autoDownloadROMButton.Click += async (s, e) => + { + var autoDownloadROMInfoLabel = Controls["autoDownloadROMInfoLabel"] as Label; + try + { + await AutoDownloadROMs(); + autoDownloadROMInfoLabel.DisplayText = "ROMs downloaded OK"; + autoDownloadROMInfoLabel.TextColor = Color.Green; + } + catch (Exception ex) + { + autoDownloadROMInfoLabel.DisplayText = ex.Message; + autoDownloadROMInfoLabel.TextColor = Color.Red; + } + finally + { + IsDirty = true; // Mark as dirty to update the UI + } + }; + Controls.Add(autoDownloadROMButton); + + // Manual download link + var openROMDownloadURLButton = new Button("Manual ROM download link") + { + Name = "openROMDownloadURLButton", + Position = (31, autoDownloadROMButton.Position.Y), + }; + openROMDownloadURLButton.Click += (s, e) => OpenURL(new Uri(C64SystemConfig.ROMDownloadUrls[C64SystemConfig.KERNAL_ROM_NAME]).GetLeftPart(UriPartial.Authority)); + Controls.Add(openROMDownloadURLButton); + + // Auto ROM download status label + var autoDownloadROMInfoLabel = new Label(Width - 10) + { + Name = "autoDownloadROMInfoLabel", + Position = (1, autoDownloadROMButton.Bounds.MaxExtentY + 1), + IsEnabled = false, + DisplayText = "", + TextColor = Controls.GetThemeColors().Appearance_ControlDisabled.Foreground + }; + Controls.Add(autoDownloadROMInfoLabel); + + // ROM directory - var romDirectoryLabel = CreateLabel("ROM directory", 1, 1); + var romDirectoryLabel = CreateLabel("ROM directory", 1, autoDownloadROMInfoLabel.Position.Y + 2); var romDirectoryTextBox = new TextBox(Width - 10) { Name = "romDirectoryTextBox", @@ -118,27 +166,8 @@ private void DrawUIItems() Controls.Add(selectChargenROMButton); - // URL for downloading C64 ROMs - var romDownloadsLabel = CreateLabel("ROM download link", 1, chargenROMTextBox.Bounds.MaxExtentY + 2); - var romDownloadLinkTextBox = new TextBox(Width - 10) - { - Name = "romDownloadLinkTextBox", - Position = (1, romDownloadsLabel.Bounds.MaxExtentY + 1), - IsEnabled = false, - Text = "https://www.commodore.ca/manuals/funet/cbm/firmware/computers/c64/index-t.html", - }; - Controls.Add(romDownloadLinkTextBox); - - var openROMDownloadURLButton = new Button("...") - { - Name = "openROMDownloadURLButton", - Position = (romDownloadLinkTextBox.Bounds.MaxExtentX + 2, romDownloadLinkTextBox.Position.Y), - }; - openROMDownloadURLButton.Click += (s, e) => OpenURL(romDownloadLinkTextBox.Text); - Controls.Add(openROMDownloadURLButton); - // AI coding assistant selection - var codingAssistantLabel = CreateLabel("Basic AI assistant: ", 1, romDownloadLinkTextBox.Bounds.MaxExtentY + 3); + var codingAssistantLabel = CreateLabel("Basic AI assistant: ", 1, selectChargenROMButton.Bounds.MaxExtentY + 2); var codingAssistantValue = CreateLabel($"{C64HostConfig.CodeSuggestionBackendType}", codingAssistantLabel.Bounds.MaxExtentX + 1, codingAssistantLabel.Position.Y); codingAssistantValue.TextColor = Controls.GetThemeColors().White; @@ -289,6 +318,46 @@ private void ShowROMFolderPickerDialog() window.Show(true); } + private async Task AutoDownloadROMs() + { + var romFolder = PathHelper.ExpandOSEnvironmentVariables(C64SystemConfig.ROMDirectory); + if (!Directory.Exists(romFolder)) + { + Directory.CreateDirectory(romFolder); + } + + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + //httpClient.DefaultRequestHeaders.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); + httpClient.DefaultRequestHeaders.Accept.ParseAdd("*/*"); + + foreach (var romDownload in C64SystemConfig.ROMDownloadUrls) + { + var romName = romDownload.Key; + var romUrl = romDownload.Value; + var filename = Path.GetFileName(new Uri(romUrl).LocalPath); + var dest = Path.Combine(romFolder, filename); + try + { + using var response = await httpClient.GetAsync(romUrl); + if (!response.IsSuccessStatusCode) + throw new Exception($"Failed to get '{romUrl}' ({(int)response.StatusCode})"); + await using var fs = new FileStream(dest, FileMode.Create, FileAccess.Write, FileShare.None); + await response.Content.CopyToAsync(fs); + System.Console.WriteLine($"Downloaded {filename} to {dest}"); + + // Update the C64SystemConfig with the downloaded ROM file + C64SystemConfig.SetROM(romName, filename); + } + catch (Exception ex) + { + if (File.Exists(dest)) + File.Delete(dest); + throw new Exception($"Error downloading {romUrl}: {ex.Message}", ex); + } + } + } + protected override void OnIsDirtyChanged() { if (IsDirty) diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj b/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj index fc43ba5f6..01a6beae8 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj @@ -41,9 +41,7 @@ - - PreserveNewest - + PreserveNewest diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/InfoConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/InfoConsole.cs index 1112c1777..aeeab42ea 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/InfoConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/InfoConsole.cs @@ -1,9 +1,6 @@ -using System.Text; using Highbyte.DotNet6502.Systems.Commodore64; -using Highbyte.DotNet6502.Systems.Commodore64.Video; using Highbyte.DotNet6502.Systems.Generic; using Highbyte.DotNet6502.Systems.Logging.InMem; -using Highbyte.DotNet6502.Utils; using Microsoft.Extensions.Logging; using SadConsole.UI; using SadConsole.UI.Controls; diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ConfigUI/SilkNetImGuiC64Config.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ConfigUI/SilkNetImGuiC64Config.cs index 15faae863..80b645347 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ConfigUI/SilkNetImGuiC64Config.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/ConfigUI/SilkNetImGuiC64Config.cs @@ -4,6 +4,7 @@ using Highbyte.DotNet6502.Systems.Commodore64; using Highbyte.DotNet6502.Systems; using Highbyte.DotNet6502.Systems.Commodore64.Config; +using Highbyte.DotNet6502.Utils; namespace Highbyte.DotNet6502.App.SilkNetNative.ConfigUI; @@ -35,8 +36,14 @@ public class SilkNetImGuiC64Config private List _validationErrors = new(); + // ROM download state + private bool _isDownloadingRoms = false; + private string _downloadStatusMessage = ""; + private bool _downloadSuccess = false; + //private static Vector4 s_informationColor = new Vector4(1.0f, 1.0f, 1.0f, 1.0f); private static Vector4 s_errorColor = new Vector4(1.0f, 0.0f, 0.0f, 1.0f); + private static Vector4 s_successColor = new Vector4(0.0f, 1.0f, 0.0f, 1.0f); //private static Vector4 s_warningColor = new Vector4(0.5f, 0.8f, 0.8f, 1); private static Vector4 s_okButtonColor = new Vector4(0.0f, 0.6f, 0.0f, 1.0f); @@ -60,11 +67,16 @@ internal void Init(C64HostConfig c64HostConfig) _romDirectory = _systemConfig.ROMDirectory; _kernalRomFile = _systemConfig.HasROM(C64SystemConfig.KERNAL_ROM_NAME) ? _systemConfig.GetROM(C64SystemConfig.KERNAL_ROM_NAME).File! : ""; - _basicRomFile = _systemConfig.HasROM(C64SystemConfig.KERNAL_ROM_NAME) ? _systemConfig.GetROM(C64SystemConfig.BASIC_ROM_NAME).File! : ""; - _chargenRomFile = _systemConfig.HasROM(C64SystemConfig.KERNAL_ROM_NAME) ? _systemConfig.GetROM(C64SystemConfig.CHARGEN_ROM_NAME).File! : ""; + _basicRomFile = _systemConfig.HasROM(C64SystemConfig.BASIC_ROM_NAME) ? _systemConfig.GetROM(C64SystemConfig.BASIC_ROM_NAME).File! : ""; + _chargenRomFile = _systemConfig.HasROM(C64SystemConfig.CHARGEN_ROM_NAME) ? _systemConfig.GetROM(C64SystemConfig.CHARGEN_ROM_NAME).File! : ""; _selectedRenderer = _availableRenderers.ToList().IndexOf(_hostConfig.Renderer.ToString()); _openGLFineScrollPerRasterLineEnabled = _hostConfig.SilkNetOpenGlRendererConfig.UseFineScrollPerRasterLine; + + // Reset download status + _isDownloadingRoms = false; + _downloadStatusMessage = ""; + _downloadSuccess = false; } public void PostOnRender(string dialogLabel) @@ -77,6 +89,31 @@ public void PostOnRender(string dialogLabel) //ImGui.LabelText("VIC2 model", $"{_config!.Vic2Model}"); ImGui.Text("ROMs"); + + // ROM auto-download button + ImGui.BeginDisabled(_isDownloadingRoms); + if (ImGui.Button("Auto download ROM files")) + { + _ = Task.Run(async () => await AutoDownloadROMs()); + } + ImGui.EndDisabled(); + + ImGui.SameLine(); + if (ImGui.Button("Manual ROM download link")) + { + var url = new Uri(_systemConfig.ROMDownloadUrls[C64SystemConfig.KERNAL_ROM_NAME]).GetLeftPart(UriPartial.Authority); + OpenURL(url); + } + + // Download status message + if (!string.IsNullOrEmpty(_downloadStatusMessage)) + { + var color = _downloadSuccess ? s_successColor : s_errorColor; + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TextWrapped(_downloadStatusMessage); + ImGui.PopStyleColor(); + } + if (ImGui.InputText("Directory", ref _romDirectory, 255)) { _systemConfig!.ROMDirectory = _romDirectory; @@ -201,4 +238,89 @@ public void PostOnRender(string dialogLabel) ImGui.EndPopup(); } } + + private async Task AutoDownloadROMs() + { + _isDownloadingRoms = true; + _downloadStatusMessage = "Downloading ROMs..."; + _downloadSuccess = false; + + try + { + var romFolder = PathHelper.ExpandOSEnvironmentVariables(_systemConfig.ROMDirectory); + await DownloadC64RomsAsync(_systemConfig.ROMDownloadUrls, romFolder); + + // Update the system config with the downloaded ROM files + foreach (var romDownload in _systemConfig.ROMDownloadUrls) + { + var romName = romDownload.Key; + var romUrl = romDownload.Value; + var filename = Path.GetFileName(new Uri(romUrl).LocalPath); + _systemConfig.SetROM(romName, filename); + } + + // Update the UI variables + _romDirectory = _systemConfig.ROMDirectory; + _kernalRomFile = _systemConfig.HasROM(C64SystemConfig.KERNAL_ROM_NAME) ? _systemConfig.GetROM(C64SystemConfig.KERNAL_ROM_NAME).File! : ""; + _basicRomFile = _systemConfig.HasROM(C64SystemConfig.BASIC_ROM_NAME) ? _systemConfig.GetROM(C64SystemConfig.BASIC_ROM_NAME).File! : ""; + _chargenRomFile = _systemConfig.HasROM(C64SystemConfig.CHARGEN_ROM_NAME) ? _systemConfig.GetROM(C64SystemConfig.CHARGEN_ROM_NAME).File! : ""; + + _downloadStatusMessage = "ROMs downloaded successfully!"; + _downloadSuccess = true; + } + catch (Exception ex) + { + _downloadStatusMessage = $"Error downloading ROMs: {ex.Message}"; + _downloadSuccess = false; + } + finally + { + _isDownloadingRoms = false; + } + } + + private async Task DownloadC64RomsAsync(Dictionary romDownloadUrls, string destinationDirectory) + { + if (!Directory.Exists(destinationDirectory)) + { + Directory.CreateDirectory(destinationDirectory); + } + + using var httpClient = new HttpClient(); + httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + httpClient.DefaultRequestHeaders.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); + + foreach (var romDownload in romDownloadUrls) + { + var romName = romDownload.Key; + var romUrl = romDownload.Value; + var filename = Path.GetFileName(new Uri(romUrl).LocalPath); + var dest = Path.Combine(destinationDirectory, filename); + + try + { + using var response = await httpClient.GetAsync(romUrl); + if (!response.IsSuccessStatusCode) + throw new Exception($"Failed to get '{romUrl}' ({(int)response.StatusCode})"); + + await using var fs = new FileStream(dest, FileMode.Create, FileAccess.Write, FileShare.None); + await response.Content.CopyToAsync(fs); + Console.WriteLine($"Downloaded {filename} to {dest}"); + } + catch (Exception ex) + { + if (File.Exists(dest)) + File.Delete(dest); + throw new Exception($"Error downloading {romUrl}: {ex.Message}", ex); + } + } + } + + private void OpenURL(string url) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + throw new Exception($"Invalid URL: {url}"); + // Launch the URL in the default browser + Process.Start(new ProcessStartInfo(uri.AbsoluteUri) { UseShellExecute = true }); + } } diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj index b2ff293e4..ef8ffa037 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj @@ -38,6 +38,9 @@ + + PreserveNewest + PreserveNewest diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs index 7741930d1..c900c8e14 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Program.cs @@ -16,7 +16,9 @@ // ---------- var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json"); + .AddJsonFile("appsettings.json") + .AddJsonFile("appsettings.Development.json", optional: true); + IConfiguration Configuration = builder.Build(); // ---------- diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs index dcdb3b153..1f9c4b2cd 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Emulator/SystemSetup/C64HostConfig.cs @@ -18,6 +18,9 @@ public class C64HostConfig : IHostSystemConfig, ICloneable { public const string ConfigSectionName = "Highbyte.DotNet6502.C64.WASM"; + //public const string DefaultCorsProxyURL = "https://api.allorigins.win/raw?url="; + public const string DefaultCorsProxyURL = "https://thingproxy.freeboard.io/fetch/"; + private C64SystemConfig _systemConfig; ISystemConfig IHostSystemConfig.SystemConfig => _systemConfig; @@ -44,6 +47,8 @@ public void ClearDirty() public C64AspNetInputConfig InputConfig { get; set; } = new C64AspNetInputConfig(); + public string CorsProxyURL { get; set; } = DefaultCorsProxyURL; + private bool _basicAIAssistantDefaultEnabled; [JsonIgnore] public bool BasicAIAssistantDefaultEnabled diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor index 99337a8ba..82b6e0b67 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64ConfigUI.razor @@ -13,15 +13,17 @@
ROMs -

The C64 system requires the following types of ROM files: Kernal, Basic, and Character generator.

-

- Use existing C64 ROM files on your computer, or download them to your computer from the internet.

-

- Upload the ROM files from your computer to this emulator with the button below.

+

The C64 system requires the following ROM files: Kernal, Basic, and Character generator.

+

Note: You may need a license from Commodore/Cloanto (or own a C64) to use them.

+

How to download and use them:

+

- Automatically download ROM files from known URLs (see right).

+

- Or upload existing ROM files from your computer.

@@ -287,6 +293,8 @@ public ILocalStorageService LocalStorage { get; set; } = default!; [Inject] public IJSRuntime Js { get; set; } = default!; + [Inject] + public HttpClient HttpClient { get; set; } = default!; [CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; } = default!; @@ -359,6 +367,47 @@ private string _aiBackendValidationMessage = ""; + private async Task LoadROMsFromURL() + { + _isLoadingC64Roms = true; + _validationMessage = ""; + + HttpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); + HttpClient.DefaultRequestHeaders.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"); + + try + { + foreach (var romDownload in C64SystemConfig.ROMDownloadUrls) + { + var romName = romDownload.Key; + var romUrl = romDownload.Value; + var filename = Path.GetFileName(new Uri(romUrl).LocalPath); + + byte[] fileBuffer; + var fullROMUrl = !string.IsNullOrEmpty(C64HostConfig.CorsProxyURL) ? $"{C64HostConfig.CorsProxyURL}{Uri.EscapeDataString(romUrl)}" : romUrl; + try + { + fileBuffer = await HttpClient.GetByteArrayAsync(fullROMUrl); + } + catch (Exception ex) + { + throw new Exception($"Error downloading {fullROMUrl}: {ex.Message}", ex); + } + C64HostConfig.SystemConfig.SetROM(romName, data: fileBuffer); + } + } + catch (Exception ex) + { + _validationMessage = $"Error downloading ROMs: {ex.Message}"; + } + finally + { + _isLoadingC64Roms = false; + this.StateHasChanged(); + } + } + + private async Task OnC64RomFilePickerChange(InputFileChangeEventArgs e) { if (C64HostConfig == null) diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/global.json b/src/apps/Highbyte.DotNet6502.App.WASM/global.json index 31f23b687..4c686a526 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/global.json +++ b/src/apps/Highbyte.DotNet6502.App.WASM/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "9.0.201" + "version": "9.0.301" } } \ No newline at end of file diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Config/C64SystemConfig.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Config/C64SystemConfig.cs index ee6f5de45..d67b08e4f 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Config/C64SystemConfig.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Config/C64SystemConfig.cs @@ -14,7 +14,11 @@ public void ClearDirty() _isDirty = false; } - // TODO: Decide if DefaultKernalROMChecksums should exists here in C64SystemConfig, C64Config, or in C64 + public static string DEFAULT_KERNAL_ROM_DOWNLOAD_URL = "https://www.commodore.ca/manuals/funet/cbm/firmware/computers/c64/kernal.901227-03.bin"; + public static string DEFAULT_BASIC_ROM_DOWNLOAD_URL = "https://www.commodore.ca/manuals/funet/cbm/firmware/computers/c64/basic.901226-01.bin"; + public static string DEFAULT_CHARGEN_ROM_DOWNLOAD_URL = "https://www.commodore.ca/manuals/funet/cbm/firmware/computers/c64/characters.901225-01.bin"; + + // TODO: Decide if ROM checksums should exist in C64SystemConfig, C64Config, or in C64 // ROM version info from: https://www.commodore.ca/manuals/funet/cbm/firmware/computers/c64/ // Checksums calculated with SHA1 public const string KERNAL_ROM_NAME = "kernal"; @@ -85,6 +89,13 @@ public string ROMDirectory } } + public Dictionary ROMDownloadUrls { get; } = new() + { + { KERNAL_ROM_NAME, DEFAULT_KERNAL_ROM_DOWNLOAD_URL }, + { BASIC_ROM_NAME, DEFAULT_BASIC_ROM_DOWNLOAD_URL }, + { CHARGEN_ROM_NAME, DEFAULT_CHARGEN_ROM_DOWNLOAD_URL } + }; + private bool _audioEnabled; public bool AudioEnabled {