├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── build.yml ├── .gitignore ├── Goose-Logo-Web.png ├── LICENSE.md ├── README.md ├── Screenshots ├── Debugger.png ├── WS-Bootstrap-Logo.png ├── WS-Bootstrap-Menu.png ├── WS-DigiAnodeTamer.png ├── WS-FinalLap2000.png ├── WS-MedarotKabuto.png ├── WS-RockmanForte.png ├── WSC-Bootstrap-Logo.png ├── WSC-Bootstrap-Menu.png ├── WSC-DigiD1Tamers.png ├── WSC-FinalFantasy.png ├── WSC-MrDriller.png └── WSC-Riviera.png ├── StoicGoose.Common ├── Attributes │ ├── BitDescriptionAttribute.cs │ ├── FormatAttribute.cs │ └── PortAttribute.cs ├── Drawing │ └── RgbaFile.cs ├── Extensions │ ├── ObjectExtensionMethods.cs │ ├── SerializationExtensionMethods.cs │ └── StringExtensionMethods.cs ├── Localization │ └── Localizer.cs ├── OpenGL │ ├── Buffer.cs │ ├── ContextInfo.cs │ ├── Shaders │ │ ├── Bundles │ │ │ └── BundleManifest.cs │ │ ├── Program.cs │ │ └── ShaderFactory.cs │ ├── State.cs │ ├── Texture.cs │ ├── Uniforms │ │ ├── Color4Uniform.cs │ │ ├── FloatUniform.cs │ │ ├── GenericUniform.cs │ │ ├── IntUniform.cs │ │ ├── Matrix2Uniform.cs │ │ ├── Matrix3Uniform.cs │ │ ├── Matrix4Uniform.cs │ │ ├── UintUniform.cs │ │ ├── Vector2Uniform.cs │ │ ├── Vector3Uniform.cs │ │ └── Vector4Uniform.cs │ └── Vertices │ │ ├── IVertexStruct.cs │ │ ├── Vertex.cs │ │ ├── VertexArray.cs │ │ └── VertexAttribute.cs ├── StoicGoose.Common.csproj └── Utilities │ ├── Ansi.cs │ ├── Bcd.cs │ ├── BitHandling.cs │ ├── ConfigurationBase.cs │ ├── Crc32.cs │ ├── Log.cs │ └── Resources.cs ├── StoicGoose.Core ├── Bootstrap.cs ├── CPU │ ├── V30MZ.Addressing.cs │ ├── V30MZ.Flags.cs │ ├── V30MZ.Implementations.cs │ ├── V30MZ.Memory.cs │ ├── V30MZ.Miscellaneous.cs │ ├── V30MZ.ModRM.cs │ ├── V30MZ.OpcodeTable.cs │ ├── V30MZ.Prefixes.cs │ ├── V30MZ.Registers.cs │ ├── V30MZ.Segments.cs │ ├── V30MZ.Strings.cs │ └── V30MZ.cs ├── Cartridges │ ├── Cartridge.cs │ ├── Metadata.cs │ └── RTC.cs ├── DMA │ ├── SphinxGeneralDMAController.cs │ └── SphinxSoundDMAController.cs ├── Display │ ├── AswanDisplayController.cs │ ├── DisplayControllerCommon.cs │ ├── DisplayTimer.cs │ ├── DisplayUtilities.cs │ └── SphinxDisplayController.cs ├── EEPROMs │ └── EEPROM.cs ├── Interfaces │ ├── IComponent.cs │ ├── IMachine.cs │ ├── IMemoryAccessComponent.cs │ └── IPortAccessComponent.cs ├── Machines │ ├── MachineCommon.cs │ ├── WonderSwan.cs │ └── WonderSwanColor.cs ├── Serial │ └── SerialPort.cs ├── Sound │ ├── AswanSoundController.cs │ ├── SoundChannel1.cs │ ├── SoundChannel2.cs │ ├── SoundChannel3.cs │ ├── SoundChannel4.cs │ ├── SoundChannelHyperVoice.cs │ ├── SoundControllerCommon.cs │ └── SphinxSoundController.cs └── StoicGoose.Core.csproj ├── StoicGoose.GLWindow ├── Assets │ ├── Goose-Logo.rgba │ ├── JF-Dot-K14-2004.ttf │ ├── Localization.json │ └── WS-Icon.rgba ├── Configuration.cs ├── Debugging │ ├── Breakpoint.cs │ ├── BreakpointMemoryArray.cs │ ├── BreakpointVariables.cs │ ├── Disassembler.cs │ └── MemoryPatch.cs ├── GlobalVariables.cs ├── InputHandler.cs ├── Interface │ ├── Helpers.cs │ └── Windows │ │ ├── BreakpointWindow.cs │ │ ├── ComponentPortWindow.cs │ │ ├── DisassemblerWindow.cs │ │ ├── DisplayControllerStatusWindow.cs │ │ ├── DisplayWindow.cs │ │ ├── InputSettingsWindow.cs │ │ ├── MemoryEditorWindow.cs │ │ ├── MemoryPatchWindow.cs │ │ ├── SoundControllerStatusWindow.cs │ │ ├── SystemControllerStatusWindow.cs │ │ └── TilemapViewerWindow.cs ├── MainWindow.UI.cs ├── MainWindow.cs ├── Program.cs ├── SoundHandler.cs ├── StoicGoose.GLWindow.csproj └── WS-Icon.ico ├── StoicGoose.ImGuiCommon ├── Handlers │ ├── FileDialogHandler.cs │ ├── ImGuiHandler.cs │ ├── MenuHandler.cs │ ├── MessageBoxHandler.cs │ └── StatusBarHandler.cs ├── StoicGoose.ImGuiCommon.csproj ├── Widgets │ └── BackgroundLogo.cs └── Windows │ ├── LogWindow.cs │ └── WindowBase.cs ├── StoicGoose.WinForms ├── Assets │ ├── Icons │ │ ├── Aux1.rgba │ │ ├── Aux2.rgba │ │ ├── Aux3.rgba │ │ ├── Headphones.rgba │ │ ├── Horizontal.rgba │ │ ├── Initialized.rgba │ │ ├── LowBattery.rgba │ │ ├── Power.rgba │ │ ├── Sleep.rgba │ │ ├── Vertical.rgba │ │ ├── VolumeA0.rgba │ │ ├── VolumeA1.rgba │ │ ├── VolumeA2.rgba │ │ ├── VolumeB0.rgba │ │ ├── VolumeB1.rgba │ │ ├── VolumeB2.rgba │ │ └── VolumeB3.rgba │ ├── No-Intro │ │ ├── Bandai - WonderSwan Color.dat │ │ └── Bandai - WonderSwan.dat │ └── Shaders │ │ ├── Basic │ │ ├── Fragment.glsl │ │ └── Manifest.json │ │ ├── Dot-Matrix Color │ │ ├── Fragment.glsl │ │ └── Manifest.json │ │ ├── Dot-Matrix │ │ ├── Fragment.glsl │ │ └── Manifest.json │ │ ├── FragmentBase.glsl │ │ └── Vertex.glsl ├── Cheat.cs ├── CheatEditForm.Designer.cs ├── CheatEditForm.cs ├── CheatEditForm.resx ├── CheatsForm.Designer.cs ├── CheatsForm.cs ├── CheatsForm.resx ├── Configuration.cs ├── GlobalVariables.cs ├── Handlers │ ├── DatabaseHandler.cs │ ├── EmulatorHandler.cs │ ├── GraphicsHandler.cs │ ├── InputHandler.cs │ └── SoundHandler.cs ├── IO │ └── WaveFileWriter.cs ├── MainForm.Designer.cs ├── MainForm.cs ├── MainForm.resx ├── OpenGL │ └── RenderControl.cs ├── Program.cs ├── Properties │ ├── Resources.Designer.cs │ └── Resources.resx ├── SettingsForm.Designer.cs ├── SettingsForm.cs ├── SettingsForm.resx ├── SoundRecorderForm.Designer.cs ├── SoundRecorderForm.cs ├── SoundRecorderForm.resx ├── StoicGoose.WinForms.csproj ├── Utilities.cs ├── WS-Icon.ico ├── Windows │ ├── BindableToolStripMenuItem.cs │ ├── ControlHelpers.cs │ └── Controls │ │ ├── CheatsListView.cs │ │ ├── FileTextBox.Designer.cs │ │ ├── FileTextBox.cs │ │ ├── FileTextBox.resx │ │ ├── LabelEx.cs │ │ ├── ListViewEx.cs │ │ ├── TableLayoutPanelEx.cs │ │ ├── TextBoxEx.cs │ │ └── TreeViewEx.cs └── XInput │ ├── Controller.cs │ ├── ControllerManager.cs │ └── NativeMethods.cs └── StoicGoose.sln /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CA1822: Member als statisch markieren 4 | #dotnet_diagnostic.CA1822.severity = silent 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: xdanieldzd 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: ['main'] 5 | pull_request: 6 | branches: ['main'] 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: windows-latest 12 | 13 | env: 14 | DOTNET_CLI_TELEMETRY_OPTOUT: true 15 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 16 | DOTNET_NOLOGO: true 17 | 18 | strategy: 19 | matrix: 20 | project: ['StoicGoose.WinForms', 'StoicGoose.GLWindow'] 21 | include: 22 | - project: StoicGoose.WinForms 23 | csproj: StoicGoose.WinForms/StoicGoose.WinForms.csproj 24 | artifacts: StoicGoose.WinForms-artifacts 25 | - project: StoicGoose.GLWindow 26 | csproj: StoicGoose.GLWindow/StoicGoose.GLWindow.csproj 27 | artifacts: StoicGoose.GLWindow-artifacts 28 | 29 | steps: 30 | - uses: actions/checkout@v3 31 | 32 | - uses: actions/cache@v3 33 | with: 34 | path: ~/.nuget/packages 35 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} 36 | restore-keys: | 37 | ${{ runner.os }}-nuget 38 | 39 | - name: Setup .NET SDK 40 | uses: actions/setup-dotnet@v2 41 | with: 42 | dotnet-version: 6.0.x 43 | 44 | - name: Restore dependencies 45 | run: dotnet restore 46 | 47 | - name: Build 48 | run: dotnet build --no-restore 49 | 50 | - name: Publish 51 | run: dotnet publish ${{ matrix.csproj }} -c Release -o release --nologo 52 | 53 | - name: Upload build artifact 54 | uses: actions/upload-artifact@v3 55 | with: 56 | name: ${{ matrix.artifacts }}-${{ github.ref_name }}-${{ github.sha }} 57 | path: ${{ github.workspace }}/release/* 58 | -------------------------------------------------------------------------------- /Goose-Logo-Web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Goose-Logo-Web.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 xdaniel (Daniel R.) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Screenshots/Debugger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/Debugger.png -------------------------------------------------------------------------------- /Screenshots/WS-Bootstrap-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WS-Bootstrap-Logo.png -------------------------------------------------------------------------------- /Screenshots/WS-Bootstrap-Menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WS-Bootstrap-Menu.png -------------------------------------------------------------------------------- /Screenshots/WS-DigiAnodeTamer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WS-DigiAnodeTamer.png -------------------------------------------------------------------------------- /Screenshots/WS-FinalLap2000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WS-FinalLap2000.png -------------------------------------------------------------------------------- /Screenshots/WS-MedarotKabuto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WS-MedarotKabuto.png -------------------------------------------------------------------------------- /Screenshots/WS-RockmanForte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WS-RockmanForte.png -------------------------------------------------------------------------------- /Screenshots/WSC-Bootstrap-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WSC-Bootstrap-Logo.png -------------------------------------------------------------------------------- /Screenshots/WSC-Bootstrap-Menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WSC-Bootstrap-Menu.png -------------------------------------------------------------------------------- /Screenshots/WSC-DigiD1Tamers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WSC-DigiD1Tamers.png -------------------------------------------------------------------------------- /Screenshots/WSC-FinalFantasy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WSC-FinalFantasy.png -------------------------------------------------------------------------------- /Screenshots/WSC-MrDriller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WSC-MrDriller.png -------------------------------------------------------------------------------- /Screenshots/WSC-Riviera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/Screenshots/WSC-Riviera.png -------------------------------------------------------------------------------- /StoicGoose.Common/Attributes/BitDescriptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StoicGoose.Common.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] 6 | public class BitDescriptionAttribute : Attribute 7 | { 8 | public string Description { get; set; } = string.Empty; 9 | public int LowBit { get; set; } = -1; 10 | public int HighBit { get; set; } = -1; 11 | 12 | public string BitString => LowBit != -1 ? $"B{LowBit}{(HighBit > LowBit ? $"-{HighBit}" : string.Empty)}: " : string.Empty; 13 | 14 | public BitDescriptionAttribute(string desc, int low = -1, int high = -1) 15 | { 16 | Description = desc; 17 | LowBit = low; 18 | HighBit = high; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /StoicGoose.Common/Attributes/FormatAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StoicGoose.Common.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] 6 | public class FormatAttribute : Attribute 7 | { 8 | public string Format { get; set; } = string.Empty; 9 | public int Shift { get; set; } = 0; 10 | 11 | public FormatAttribute(string format, int shift = 0) 12 | { 13 | Format = format; 14 | Shift = shift; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /StoicGoose.Common/Attributes/PortAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace StoicGoose.Common.Attributes 5 | { 6 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] 7 | public class PortAttribute : Attribute 8 | { 9 | public string Name { get; set; } = string.Empty; 10 | public List Numbers { get; set; } = new(); 11 | 12 | public PortAttribute(string name, params ushort[] numbers) 13 | { 14 | Name = name; 15 | Numbers.AddRange(numbers); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /StoicGoose.Common/Drawing/RgbaFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace StoicGoose.Common.Drawing 6 | { 7 | /* RGBA bitmap file format -- https://github.com/bzotto/rgba_bitmap 8 | * ".rgba is the dumbest possible image interchange format, now available for your programming pleasure." 9 | */ 10 | 11 | public class RgbaFile 12 | { 13 | const string expectedMagic = "RGBA"; 14 | 15 | public string MagicNumber { get; protected set; } 16 | public uint Width { get; protected set; } 17 | public uint Height { get; protected set; } 18 | public byte[] PixelData { get; protected set; } 19 | 20 | public RgbaFile(string filename) : this(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { } 21 | 22 | public RgbaFile(Stream stream) 23 | { 24 | MagicNumber = ReadString(stream, 4); 25 | Width = ReadUInt32(stream); 26 | Height = ReadUInt32(stream); 27 | PixelData = new byte[Width * Height * 4]; 28 | stream.Read(PixelData); 29 | } 30 | 31 | public RgbaFile(uint width, uint height, byte[] pixelData) 32 | { 33 | MagicNumber = expectedMagic; 34 | Width = width; 35 | Height = height; 36 | PixelData = pixelData; 37 | } 38 | 39 | public void Save(string filename) => Save(new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite)); 40 | 41 | public void Save(Stream stream) 42 | { 43 | WriteString(stream, MagicNumber); 44 | WriteUInt32(stream, Width); 45 | WriteUInt32(stream, Height); 46 | stream.Write(PixelData); 47 | } 48 | 49 | private static string ReadString(Stream stream, int length) => new(Enumerable.Range(0, length).Select(_ => (char)stream.ReadByte()).ToArray()); 50 | private static uint ReadUInt32(Stream stream) => (uint)(((stream.ReadByte() & 0xFF) << 24) | ((stream.ReadByte() & 0xFF) << 16) | ((stream.ReadByte() & 0xFF) << 8) | ((stream.ReadByte() & 0xFF) << 0)); 51 | 52 | private static void WriteString(Stream stream, string str) => Array.ForEach(str.ToCharArray(), (x) => stream.WriteByte((byte)x)); 53 | private static void WriteUInt32(Stream stream, uint val) { stream.WriteByte((byte)((val >> 24) & 0xFF)); stream.WriteByte((byte)((val >> 16) & 0xFF)); stream.WriteByte((byte)((val >> 8) & 0xFF)); stream.WriteByte((byte)((val >> 0) & 0xFF)); } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /StoicGoose.Common/Extensions/ObjectExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace StoicGoose.Common.Extensions 4 | { 5 | public static class ObjectExtensionMethods 6 | { 7 | /* https://dotnetcoretutorials.com/2020/09/09/cloning-objects-in-c-and-net-core/ */ 8 | public static T Clone(this T source) 9 | { 10 | if (source is null) return default; 11 | return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source, new JsonSerializerSettings() 12 | { 13 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore 14 | }), new JsonSerializerSettings() 15 | { 16 | ObjectCreationHandling = ObjectCreationHandling.Replace 17 | }); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /StoicGoose.Common/Extensions/SerializationExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | using Newtonsoft.Json; 4 | 5 | namespace StoicGoose.Common.Extensions 6 | { 7 | public static class SerializationExtensionMethods 8 | { 9 | public static void SerializeToFile(this object obj, string jsonFileName) 10 | { 11 | SerializeToFile(obj, jsonFileName, new JsonSerializerSettings() { Formatting = Formatting.Indented }); 12 | } 13 | 14 | public static void SerializeToFile(this object obj, string jsonFileName, JsonSerializerSettings serializerSettings) 15 | { 16 | using var writer = new StreamWriter(jsonFileName); 17 | writer.Write(JsonConvert.SerializeObject(obj, serializerSettings)); 18 | } 19 | 20 | public static T DeserializeFromFile(this string jsonFileName) 21 | { 22 | using var reader = new StreamReader(jsonFileName); 23 | return (T)JsonConvert.DeserializeObject(reader.ReadToEnd(), typeof(T), new JsonSerializerSettings() { Formatting = Formatting.Indented }); 24 | } 25 | 26 | public static T DeserializeObject(this string jsonString) 27 | { 28 | return (T)JsonConvert.DeserializeObject(jsonString, typeof(T), new JsonSerializerSettings() { Formatting = Formatting.Indented }); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /StoicGoose.Common/Extensions/StringExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace StoicGoose.Common.Extensions 6 | { 7 | public static class StringExtensionMethods 8 | { 9 | /* Modified from https://stackoverflow.com/a/2641383 */ 10 | public static List IndexOfAll(this string str, string value) 11 | { 12 | if (string.IsNullOrEmpty(value)) 13 | throw new ArgumentException("Search string is null or empty", nameof(value)); 14 | 15 | var idxs = new List(); 16 | for (var i = 0; ; i += value.Length) 17 | { 18 | i = str.IndexOf(value, i); 19 | if (i == -1) return idxs; 20 | idxs.Add(i); 21 | } 22 | } 23 | 24 | public static string EnsureEndsWithPeriod(this string str) => str + (!str.EndsWith('.') ? "." : string.Empty); 25 | 26 | /* Regex via https://superuser.com/a/380778 */ 27 | public static string RemoveAnsi(this string str) => Regex.Replace(str, @"\x1b\[[0-9;]*[mGKHF]", ""); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /StoicGoose.Common/Localization/Localizer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | using Newtonsoft.Json; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace StoicGoose.Common.Localization 10 | { 11 | public static class Localizer 12 | { 13 | public static string FallbackCulture { get; set; } = "en"; 14 | 15 | static JObject source = default; 16 | 17 | public static void Initialize(string jsonData) => source = JsonConvert.DeserializeObject(jsonData) as JObject; 18 | 19 | public static CultureInfo[] GetSupportedLanguages() => source?.Children().Select(x => new CultureInfo((x as JProperty).Name)).ToArray() ?? Array.Empty(); 20 | 21 | private static JToken GetToken(string path) => source?.SelectToken($"{CultureInfo.CurrentUICulture.TwoLetterISOLanguageName}.{path}") ?? source?.SelectToken($"{FallbackCulture}.{path}"); 22 | public static string GetString(string path) => GetToken(path)?.Value() ?? path[(path.LastIndexOf('.') + 1)..]; 23 | public static string GetString(string path, object parameters) 24 | { 25 | var result = GetString(path); 26 | var properties = parameters.GetType().GetProperties(); 27 | foreach (Match match in Regex.Matches(result, @"{(?[^}:]*):*(?[^}]*)}").Where(x => x.Success)) 28 | { 29 | var property = properties.First(x => x.Name == match.Groups["param"].Value); 30 | var format = match.Groups["format"].Value; 31 | var formattedValue = string.IsNullOrEmpty(format) ? $"{property.GetValue(parameters)}" : string.Format($"{{0:{format}}}", property.GetValue(parameters)); 32 | result = result.Replace(match.Value, formattedValue); 33 | } 34 | return result; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Buffer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | using OpenTK.Graphics.OpenGL4; 5 | 6 | using StoicGoose.Common.OpenGL.Vertices; 7 | 8 | namespace StoicGoose.Common.OpenGL 9 | { 10 | public sealed class Buffer : IDisposable 11 | { 12 | internal readonly Type dataType = default; 13 | internal readonly BufferTarget bufferTarget = 0; 14 | internal readonly BufferUsageHint bufferUsageHint = 0; 15 | 16 | internal readonly int handle = GL.GenBuffer(); 17 | internal readonly int sizeInBytes = 0; 18 | internal int count = 0; 19 | 20 | public Buffer(Type type, BufferTarget target, BufferUsageHint usage) 21 | { 22 | dataType = type; 23 | bufferTarget = target; 24 | bufferUsageHint = usage; 25 | 26 | sizeInBytes = Marshal.SizeOf(dataType); 27 | } 28 | 29 | ~Buffer() 30 | { 31 | Dispose(); 32 | } 33 | 34 | public void Dispose() 35 | { 36 | if (GL.IsBuffer(handle)) 37 | GL.DeleteBuffer(handle); 38 | 39 | GC.SuppressFinalize(this); 40 | } 41 | 42 | public static Buffer CreateBuffer(BufferTarget target, BufferUsageHint usage) where T : struct => new(typeof(T), target, usage); 43 | public static Buffer CreateVertexBuffer(BufferUsageHint usage) where T : struct, IVertexStruct => CreateBuffer(BufferTarget.ArrayBuffer, usage); 44 | public static Buffer CreateIndexBuffer(BufferUsageHint usage) where T : struct, IConvertible => CreateBuffer(BufferTarget.ElementArrayBuffer, usage); 45 | 46 | public void Bind() 47 | { 48 | GL.BindBuffer(bufferTarget, handle); 49 | } 50 | 51 | public void Update(T[] data) where T : struct 52 | { 53 | if (dataType != typeof(T)) 54 | throw new Exception("Type mismatch on buffer update"); 55 | 56 | if (data != null) 57 | { 58 | Bind(); 59 | 60 | if (data.Length == count) 61 | GL.BufferSubData(bufferTarget, IntPtr.Zero, new IntPtr(count * sizeInBytes), data); 62 | else 63 | { 64 | count = data.Length; 65 | GL.BufferData(bufferTarget, new IntPtr(count * sizeInBytes), data, bufferUsageHint); 66 | } 67 | } 68 | } 69 | 70 | public void Update(IntPtr data, int size) where T : struct 71 | { 72 | if (dataType != typeof(T)) 73 | throw new Exception("Type mismatch on buffer update"); 74 | 75 | if (data != IntPtr.Zero) 76 | { 77 | Bind(); 78 | 79 | if (size == count) 80 | GL.BufferSubData(bufferTarget, IntPtr.Zero, new IntPtr(count * sizeInBytes), data); 81 | else 82 | { 83 | count = size; 84 | GL.BufferData(bufferTarget, new IntPtr(count * sizeInBytes), data, bufferUsageHint); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/ContextInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | using OpenTK.Graphics.OpenGL4; 4 | 5 | using StoicGoose.Common.Utilities; 6 | 7 | namespace StoicGoose.Common.OpenGL 8 | { 9 | public static class ContextInfo 10 | { 11 | public static string GLRenderer { get; } = GL.GetString(StringName.Renderer); 12 | public static string GLShadingLanguageVersion { get; } = GL.GetString(StringName.ShadingLanguageVersion); 13 | public static string GLVendor { get; } = GL.GetString(StringName.Vendor); 14 | public static string GLVersion { get; } = GL.GetString(StringName.Version); 15 | public static string[] GLExtensions { get; } = new string[GL.GetInteger(GetPName.NumExtensions)].Select((x, i) => x = GL.GetString(StringNameIndexed.Extensions, i)).ToArray(); 16 | 17 | public static void WriteToLog(object source, bool withExtensions = false) 18 | { 19 | Log.WriteEvent(LogSeverity.Debug, source, "OpenGL context:"); 20 | Log.WriteEvent(LogSeverity.Debug, source, $"- Renderer: {GLRenderer}"); 21 | Log.WriteEvent(LogSeverity.Debug, source, $"- Vendor: {GLVendor}"); 22 | Log.WriteEvent(LogSeverity.Debug, source, $"- Version: {GLVersion}"); 23 | Log.WriteEvent(LogSeverity.Debug, source, $"- GLSL version: {GLShadingLanguageVersion}"); 24 | Log.WriteEvent(LogSeverity.Debug, source, $"- {GLExtensions.Length} extension(s) supported."); 25 | if (withExtensions) 26 | foreach (var extension in GLExtensions) 27 | Log.WriteEvent(LogSeverity.Debug, source, $" {extension}"); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Shaders/Bundles/BundleManifest.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Converters; 3 | 4 | namespace StoicGoose.Common.OpenGL.Shaders.Bundles 5 | { 6 | public enum FilterMode { Linear, Nearest } 7 | 8 | public enum WrapMode { Repeat, Edge, Border, Mirror } 9 | 10 | public class BundleManifest 11 | { 12 | [JsonConverter(typeof(StringEnumConverter))] 13 | public FilterMode Filter { get; set; } = FilterMode.Linear; 14 | [JsonConverter(typeof(StringEnumConverter))] 15 | public WrapMode Wrap { get; set; } = WrapMode.Repeat; 16 | public int Samplers { get; set; } = 3; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Shaders/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using OpenTK.Graphics.OpenGL4; 5 | 6 | namespace StoicGoose.Common.OpenGL.Shaders 7 | { 8 | public sealed class Program : IDisposable 9 | { 10 | public int Handle { get; } = GL.CreateProgram(); 11 | 12 | readonly Dictionary uniformLocations = new(); 13 | 14 | bool disposed = false; 15 | 16 | public Program(params int[] shaders) 17 | { 18 | foreach (var shader in shaders) GL.AttachShader(Handle, shader); 19 | GL.LinkProgram(Handle); 20 | 21 | GL.GetProgram(Handle, GetProgramParameterName.LinkStatus, out int status); 22 | if (status != 1) 23 | { 24 | GL.GetProgramInfoLog(Handle, out string info); 25 | GL.DeleteProgram(Handle); 26 | throw new Exception($"Program link failed:\n{info}"); 27 | } 28 | 29 | foreach (var shader in shaders) 30 | { 31 | GL.DetachShader(Handle, shader); 32 | GL.DeleteShader(shader); 33 | } 34 | 35 | GL.UseProgram(0); 36 | } 37 | 38 | ~Program() 39 | { 40 | Dispose(false); 41 | } 42 | 43 | public void Dispose() 44 | { 45 | Dispose(true); 46 | GC.SuppressFinalize(this); 47 | } 48 | 49 | private void Dispose(bool disposing) 50 | { 51 | if (disposed) 52 | return; 53 | 54 | if (disposing) 55 | { 56 | if (GL.IsProgram(Handle)) 57 | GL.DeleteProgram(Handle); 58 | } 59 | 60 | disposed = true; 61 | } 62 | 63 | public void Bind() 64 | { 65 | GL.UseProgram(Handle); 66 | } 67 | 68 | public int GetUniformLocation(string name) 69 | { 70 | if (!uniformLocations.ContainsKey(name)) 71 | { 72 | var location = GL.GetUniformLocation(Handle, name); 73 | if (location != -1) uniformLocations[name] = location; 74 | return location; 75 | } 76 | else 77 | return uniformLocations[name]; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Shaders/ShaderFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using OpenTK.Graphics.OpenGL4; 5 | 6 | namespace StoicGoose.Common.OpenGL.Shaders 7 | { 8 | public static class ShaderFactory 9 | { 10 | public static int FromSource(ShaderType shaderType, params string[] shaderSource) 11 | { 12 | shaderSource = Sanitize(shaderSource); 13 | 14 | int handle = GL.CreateShader(shaderType); 15 | GL.ShaderSource(handle, shaderSource.Length, shaderSource, (int[])null); 16 | GL.CompileShader(handle); 17 | 18 | GL.GetShader(handle, ShaderParameter.CompileStatus, out int status); 19 | if (status != 1) 20 | { 21 | GL.GetShaderInfoLog(handle, out string info); 22 | GL.DeleteShader(handle); 23 | throw new Exception($"{shaderType} compile failed:\n{info}"); 24 | } 25 | 26 | return handle; 27 | } 28 | 29 | private static string[] Sanitize(string[] shaderSource) 30 | { 31 | return shaderSource.Where(x => !string.IsNullOrEmpty(x)).Select(z => string.Concat(z, Environment.NewLine)).ToArray(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/State.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Runtime.CompilerServices; 4 | 5 | using OpenTK.Graphics.OpenGL4; 6 | using OpenTK.Mathematics; 7 | 8 | namespace StoicGoose.Common.OpenGL 9 | { 10 | public class State 11 | { 12 | static State lastState = default; 13 | 14 | bool depthTestEnable = true, blendEnable = true, cullFaceEnable = true, scissorTestEnable = false; 15 | BlendingFactor blendSource = BlendingFactor.SrcAlpha, blendDest = BlendingFactor.OneMinusSrcAlpha; 16 | CullFaceMode cullFaceMode = CullFaceMode.Back; 17 | Vector4i scissorBox = Vector4i.Zero; 18 | Color clearColor = Color.Black; 19 | Vector4i viewport = Vector4i.Zero; 20 | 21 | public void Enable(EnableCap cap) => SetCap(cap, true); 22 | public void Disable(EnableCap cap) => SetCap(cap, false); 23 | 24 | public void SetBlending(BlendingFactor source, BlendingFactor dest) { blendSource = source; blendDest = dest; } 25 | public void SetCullFace(CullFaceMode mode) => cullFaceMode = mode; 26 | public void SetScissor(Vector4i box) => scissorBox = box; 27 | public void SetScissor(int x, int y, int width, int height) => scissorBox = new(x, y, width, height); 28 | public void SetClearColor(Color color) => clearColor = color; 29 | public void SetViewport(Vector4i vp) => viewport = vp; 30 | public void SetViewport(int x, int y, int width, int height) => viewport = new(x, y, width, height); 31 | 32 | private void SetCap(EnableCap cap, bool value) 33 | { 34 | switch (cap) 35 | { 36 | case EnableCap.DepthTest: depthTestEnable = value; break; 37 | case EnableCap.Blend: blendEnable = value; break; 38 | case EnableCap.CullFace: cullFaceEnable = value; break; 39 | case EnableCap.ScissorTest: scissorTestEnable = value; break; 40 | default: throw new StateException($"{cap} not implemented"); 41 | } 42 | } 43 | 44 | private bool GetCap(EnableCap cap) 45 | { 46 | return cap switch 47 | { 48 | EnableCap.DepthTest => depthTestEnable, 49 | EnableCap.Blend => blendEnable, 50 | EnableCap.CullFace => cullFaceEnable, 51 | EnableCap.ScissorTest => scissorTestEnable, 52 | _ => throw new StateException($"{cap} not implemented"), 53 | }; 54 | } 55 | 56 | public void Submit() 57 | { 58 | if (lastState?.clearColor != clearColor) 59 | GL.ClearColor(clearColor); 60 | 61 | SubmitState(EnableCap.DepthTest, depthTestEnable); 62 | SubmitState(EnableCap.Blend, blendEnable); 63 | SubmitState(EnableCap.CullFace, cullFaceEnable); 64 | SubmitState(EnableCap.ScissorTest, scissorTestEnable); 65 | 66 | if (lastState?.viewport != viewport) 67 | GL.Viewport(viewport.X, viewport.Y, viewport.Z, viewport.W); 68 | 69 | lastState = (State)MemberwiseClone(); 70 | } 71 | 72 | private void SubmitState(EnableCap cap, bool value) 73 | { 74 | var enableChanged = lastState?.GetCap(cap) != GetCap(cap); 75 | 76 | if (value) 77 | { 78 | if (enableChanged) GL.Enable(cap); 79 | 80 | switch (cap) 81 | { 82 | case EnableCap.Blend: 83 | if (lastState?.blendSource != blendSource || lastState?.blendDest != blendDest) 84 | GL.BlendFunc(blendSource, blendDest); 85 | break; 86 | 87 | case EnableCap.CullFace: 88 | if (lastState?.cullFaceMode != cullFaceMode) 89 | GL.CullFace(cullFaceMode); 90 | break; 91 | 92 | case EnableCap.ScissorTest: 93 | if (lastState?.scissorBox != scissorBox) 94 | GL.Scissor(scissorBox.X, scissorBox.Y, scissorBox.Z, scissorBox.W); 95 | break; 96 | } 97 | } 98 | else 99 | { 100 | if (enableChanged) GL.Disable(cap); 101 | } 102 | } 103 | } 104 | 105 | public class StateException : Exception 106 | { 107 | public StateException(string message, [CallerMemberName] string callerName = "") : base($"In {callerName}: {message}") { } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/Color4Uniform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | using OpenTK.Mathematics; 3 | 4 | namespace StoicGoose.Common.OpenGL.Uniforms 5 | { 6 | public sealed class Color4Uniform : GenericUniform 7 | { 8 | public Color4Uniform(string name) : this(name, Color4.White) { } 9 | public Color4Uniform(string name, Color4 value) : base(name, value) { } 10 | 11 | protected override void SubmitUniform(int location) 12 | { 13 | GL.Uniform4(location, value); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/FloatUniform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | 3 | namespace StoicGoose.Common.OpenGL.Uniforms 4 | { 5 | public sealed class FloatUniform : GenericUniform 6 | { 7 | public FloatUniform(string name) : this(name, 0.0f) { } 8 | public FloatUniform(string name, float value) : base(name, value) { } 9 | 10 | protected override void SubmitUniform(int location) 11 | { 12 | GL.Uniform1(location, value); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/GenericUniform.cs: -------------------------------------------------------------------------------- 1 | using ShaderProgram = StoicGoose.Common.OpenGL.Shaders.Program; 2 | 3 | namespace StoicGoose.Common.OpenGL.Uniforms 4 | { 5 | public abstract class GenericUniform 6 | { 7 | protected readonly string name; 8 | protected T value; 9 | 10 | public string Name => name; 11 | public T Value 12 | { 13 | get => value; 14 | set => this.value = value; 15 | } 16 | 17 | public GenericUniform(string name) : this(name, default) { } 18 | 19 | public GenericUniform(string name, T value) 20 | { 21 | this.name = name; 22 | this.value = value; 23 | } 24 | 25 | public void SubmitToProgram(ShaderProgram shaderProgram) 26 | { 27 | var location = shaderProgram.GetUniformLocation(name); 28 | if (location != -1) SubmitUniform(location); 29 | } 30 | 31 | protected abstract void SubmitUniform(int location); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/IntUniform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | 3 | namespace StoicGoose.Common.OpenGL.Uniforms 4 | { 5 | public sealed class IntUniform : GenericUniform 6 | { 7 | public IntUniform(string name) : this(name, 0) { } 8 | public IntUniform(string name, int value) : base(name, value) { } 9 | 10 | protected override void SubmitUniform(int location) 11 | { 12 | GL.Uniform1(location, value); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/Matrix2Uniform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | using OpenTK.Mathematics; 3 | 4 | namespace StoicGoose.Common.OpenGL.Uniforms 5 | { 6 | public sealed class Matrix2Uniform : GenericUniform 7 | { 8 | public Matrix2Uniform(string name) : this(name, Matrix2.Identity) { } 9 | public Matrix2Uniform(string name, Matrix2 value) : base(name, value) { } 10 | 11 | protected override void SubmitUniform(int location) 12 | { 13 | GL.UniformMatrix2(location, false, ref value); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/Matrix3Uniform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | using OpenTK.Mathematics; 3 | 4 | namespace StoicGoose.Common.OpenGL.Uniforms 5 | { 6 | public sealed class Matrix3Uniform : GenericUniform 7 | { 8 | public Matrix3Uniform(string name) : this(name, Matrix3.Identity) { } 9 | public Matrix3Uniform(string name, Matrix3 value) : base(name, value) { } 10 | 11 | protected override void SubmitUniform(int location) 12 | { 13 | GL.UniformMatrix3(location, false, ref value); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/Matrix4Uniform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | using OpenTK.Mathematics; 3 | 4 | namespace StoicGoose.Common.OpenGL.Uniforms 5 | { 6 | public sealed class Matrix4Uniform : GenericUniform 7 | { 8 | public Matrix4Uniform(string name) : this(name, Matrix4.Identity) { } 9 | public Matrix4Uniform(string name, Matrix4 value) : base(name, value) { } 10 | 11 | protected override void SubmitUniform(int location) 12 | { 13 | GL.UniformMatrix4(location, false, ref value); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/UintUniform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | 3 | namespace StoicGoose.Common.OpenGL.Uniforms 4 | { 5 | public sealed class UintUniform : GenericUniform 6 | { 7 | public UintUniform(string name) : this(name, 0) { } 8 | public UintUniform(string name, uint value) : base(name, value) { } 9 | 10 | protected override void SubmitUniform(int location) 11 | { 12 | GL.Uniform1(location, value); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/Vector2Uniform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | using OpenTK.Mathematics; 3 | 4 | namespace StoicGoose.Common.OpenGL.Uniforms 5 | { 6 | public sealed class Vector2Uniform : GenericUniform 7 | { 8 | public Vector2Uniform(string name) : this(name, Vector2.Zero) { } 9 | public Vector2Uniform(string name, Vector2 value) : base(name, value) { } 10 | 11 | protected override void SubmitUniform(int location) 12 | { 13 | GL.Uniform2(location, value); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/Vector3Uniform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | using OpenTK.Mathematics; 3 | 4 | namespace StoicGoose.Common.OpenGL.Uniforms 5 | { 6 | public sealed class Vector3Uniform : GenericUniform 7 | { 8 | public Vector3Uniform(string name) : this(name, Vector3.Zero) { } 9 | public Vector3Uniform(string name, Vector3 value) : base(name, value) { } 10 | 11 | protected override void SubmitUniform(int location) 12 | { 13 | GL.Uniform3(location, value); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Uniforms/Vector4Uniform.cs: -------------------------------------------------------------------------------- 1 | using OpenTK.Graphics.OpenGL4; 2 | using OpenTK.Mathematics; 3 | 4 | namespace StoicGoose.Common.OpenGL.Uniforms 5 | { 6 | public sealed class Vector4Uniform : GenericUniform 7 | { 8 | public Vector4Uniform(string name) : this(name, Vector4.Zero) { } 9 | public Vector4Uniform(string name, Vector4 value) : base(name, value) { } 10 | 11 | protected override void SubmitUniform(int location) 12 | { 13 | GL.Uniform4(location, value); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Vertices/IVertexStruct.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Common.OpenGL.Vertices 2 | { 3 | public interface IVertexStruct { } 4 | } 5 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Vertices/Vertex.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | using OpenTK.Mathematics; 4 | 5 | namespace StoicGoose.Common.OpenGL.Vertices 6 | { 7 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 8 | public struct Vertex : IVertexStruct 9 | { 10 | public Vector2 Position; 11 | public Vector2 TexCoord; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /StoicGoose.Common/OpenGL/Vertices/VertexAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StoicGoose.Common.OpenGL.Vertices 4 | { 5 | public sealed class VertexAttribute 6 | { 7 | public Type Type { get; internal set; } = default; 8 | public int Size { get; internal set; } = -1; 9 | public int Offset { get; internal set; } = -1; 10 | public string Name { get; internal set; } = string.Empty; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /StoicGoose.Common/StoicGoose.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 2.0.0 5 | xdaniel 6 | 7 | 16-bit handheld game system emulator common code 8 | Written 2021-2022 by xdaniel 9 | 10 | 11 | 12 | net6.0 13 | en 14 | disable 15 | disable 16 | true 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /StoicGoose.Common/Utilities/Bcd.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Common.Utilities 2 | { 3 | public static class Bcd 4 | { 5 | public static int DecimalToBcd(int value) => ((value / 10) << 4) + (value % 10); 6 | public static int BcdToDecimal(int value) => ((value >> 4) * 10) + value % 16; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /StoicGoose.Common/Utilities/BitHandling.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Common.Utilities 2 | { 3 | public static class BitHandling 4 | { 5 | public static void ChangeBit(ref byte value, int bit, bool state) 6 | { 7 | if (state) 8 | value |= (byte)(1 << bit); 9 | else 10 | value &= (byte)~(1 << bit); 11 | } 12 | 13 | public static bool IsBitSet(byte value, int bit) => (value & (1 << bit)) != 0; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /StoicGoose.Common/Utilities/ConfigurationBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace StoicGoose.Common.Utilities 6 | { 7 | public abstract class ConfigurationBase where T : class, new() 8 | { 9 | public static readonly Dictionary Defaults = default; 10 | 11 | static ConfigurationBase() 12 | { 13 | Defaults = GetDefaultValues(); 14 | } 15 | 16 | private static Dictionary GetDefaultValues() 17 | { 18 | var dict = new Dictionary(); 19 | var instance = new T(); 20 | 21 | foreach (var property in typeof(T).GetProperties().Where(x => x.CanWrite)) 22 | { 23 | var value = property.GetValue(instance); 24 | if (value == null || (property.PropertyType == typeof(string) && string.IsNullOrEmpty(value as string))) continue; 25 | dict.Add(property.Name, value); 26 | } 27 | 28 | return dict; 29 | } 30 | 31 | public void ResetToDefault(string name) 32 | { 33 | var property = GetType().GetProperty(name); 34 | if (property == null) throw new ArgumentException($"Setting '{name}' not found in {GetType().Name}", nameof(name)); 35 | property.SetValue(this, Defaults[name]); 36 | } 37 | } 38 | 39 | public static class ConfigurationBase 40 | { 41 | public static void CopyConfiguration(object source, object destination) 42 | { 43 | if (source == null) throw new ArgumentNullException(nameof(source), "Source cannot be null"); 44 | if (destination == null) throw new ArgumentNullException(nameof(destination), "Destination cannot be null"); 45 | 46 | var sourceType = source.GetType(); 47 | var destType = destination.GetType(); 48 | 49 | foreach (var sourceProperty in sourceType.GetProperties().Where(x => x.CanRead)) 50 | { 51 | var destProperty = destType.GetProperty(sourceProperty.Name); 52 | if (destProperty == null || !destProperty.CanWrite || destProperty.GetSetMethod(true) == null || destProperty.GetSetMethod(true).IsPrivate || 53 | destProperty.GetSetMethod(true).Attributes.HasFlag(System.Reflection.MethodAttributes.Static) || 54 | !destProperty.PropertyType.IsAssignableFrom(sourceProperty.PropertyType)) 55 | continue; 56 | 57 | var sourceValue = sourceProperty.GetValue(source, null); 58 | var destValue = destProperty.GetValue(destination, null); 59 | 60 | if ((sourceProperty.PropertyType.BaseType.IsGenericType ? sourceProperty.PropertyType.BaseType.GetGenericTypeDefinition() : sourceProperty.PropertyType.BaseType) == typeof(ConfigurationBase<>)) 61 | CopyConfiguration(sourceValue, destValue); 62 | else 63 | destProperty.SetValue(destination, sourceValue); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /StoicGoose.Common/Utilities/Crc32.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace StoicGoose.Common.Utilities 5 | { 6 | public static class Crc32 7 | { 8 | static readonly uint[] crcTable; 9 | static readonly uint crcPolynomial = 0xEDB88320; 10 | static readonly uint crcSeed = 0xFFFFFFFF; 11 | 12 | static Crc32() 13 | { 14 | crcTable = new uint[256]; 15 | 16 | for (var i = 0; i < 256; i++) 17 | { 18 | var entry = (uint)i; 19 | for (int j = 0; j < 8; j++) 20 | { 21 | if ((entry & 0x00000001) == 0x00000001) entry = (entry >> 1) ^ crcPolynomial; 22 | else entry >>= 1; 23 | } 24 | crcTable[i] = entry; 25 | } 26 | } 27 | 28 | private static void VerifyStartAndLength(int dataLength, int segmentStart, int segmentLength) 29 | { 30 | if (segmentStart >= dataLength) throw new Exception("Segment start offset is greater than total length"); 31 | if (segmentLength > dataLength) throw new Exception("Segment length is greater than total length"); 32 | if ((segmentStart + segmentLength) > dataLength) throw new Exception("Segment end offset is greater than total length"); 33 | } 34 | 35 | public static uint Calculate(FileInfo fileInfo) 36 | { 37 | return Calculate(fileInfo, 0, (int)fileInfo.Length); 38 | } 39 | 40 | public static uint Calculate(FileInfo fileInfo, int start, int length) 41 | { 42 | VerifyStartAndLength((int)fileInfo.Length, start, length); 43 | 44 | using FileStream file = fileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 45 | return Calculate(file, start, length); 46 | } 47 | 48 | public static uint Calculate(Stream stream) 49 | { 50 | return Calculate(stream, 0, (int)stream.Length); 51 | } 52 | 53 | public static uint Calculate(Stream stream, int start, int length) 54 | { 55 | VerifyStartAndLength((int)stream.Length, start, length); 56 | 57 | var lastStreamPosition = stream.Position; 58 | var data = new byte[length]; 59 | stream.Position = start; 60 | stream.Read(data, 0, length); 61 | var crc = Calculate(data, 0, data.Length); 62 | stream.Position = lastStreamPosition; 63 | 64 | return crc; 65 | } 66 | 67 | public static uint Calculate(byte[] data) 68 | { 69 | return Calculate(data, 0, data.Length); 70 | } 71 | 72 | public static uint Calculate(byte[] data, int start, int length) 73 | { 74 | VerifyStartAndLength(data.Length, start, length); 75 | 76 | uint crc = crcSeed; 77 | for (var i = start; i < (start + length); i++) 78 | crc = ((crc >> 8) ^ crcTable[data[i] ^ (crc & 0x000000FF)]); 79 | return ~crc; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /StoicGoose.Common/Utilities/Log.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | using Serilog; 6 | using Serilog.Core; 7 | using Serilog.Events; 8 | using Serilog.Formatting; 9 | using Serilog.Formatting.Display; 10 | 11 | using StoicGoose.Common.Extensions; 12 | 13 | namespace StoicGoose.Common.Utilities 14 | { 15 | public enum LogSeverity { Verbose, Debug, Information, Warning, Error, Fatal } 16 | 17 | public static class Log 18 | { 19 | const string defaultTemplate = "{Message}{NewLine}{Exception}"; 20 | 21 | readonly static Dictionary severityToEventLevelMapping = new() 22 | { 23 | { LogSeverity.Verbose, LogEventLevel.Verbose }, 24 | { LogSeverity.Debug, LogEventLevel.Debug }, 25 | { LogSeverity.Information, LogEventLevel.Information }, 26 | { LogSeverity.Warning, LogEventLevel.Warning }, 27 | { LogSeverity.Error, LogEventLevel.Error }, 28 | { LogSeverity.Fatal, LogEventLevel.Fatal } 29 | }; 30 | 31 | readonly static Dictionary logSeverityAnsiColors = new() 32 | { 33 | { LogSeverity.Verbose, Ansi.White }, 34 | { LogSeverity.Debug, Ansi.Cyan }, 35 | { LogSeverity.Information, Ansi.Green }, 36 | { LogSeverity.Warning, Ansi.Yellow }, 37 | { LogSeverity.Error, Ansi.Magenta }, 38 | { LogSeverity.Fatal, Ansi.Red } 39 | }; 40 | 41 | static Logger mainLogger = default; 42 | static Logger fileLogger = default; 43 | 44 | public static string LogPath { get; private set; } = string.Empty; 45 | 46 | public static void Initialize(string logPath) 47 | { 48 | if (File.Exists(logPath)) File.Delete(logPath); 49 | 50 | mainLogger = new LoggerConfiguration() 51 | .MinimumLevel.Verbose() 52 | .WriteTo.Console(outputTemplate: defaultTemplate) 53 | .CreateLogger(); 54 | 55 | fileLogger = new LoggerConfiguration() 56 | .MinimumLevel.Verbose() 57 | .WriteTo.File(LogPath = logPath, restrictedToMinimumLevel: LogEventLevel.Verbose) 58 | .CreateLogger(); 59 | } 60 | 61 | public static void AttachTextWriter(TextWriter writer) 62 | { 63 | mainLogger = new LoggerConfiguration() 64 | .MinimumLevel.Verbose() 65 | .WriteTo.Sink(mainLogger) 66 | .WriteTo.Sink(new TextWriterSink(writer, new MessageTemplateTextFormatter(defaultTemplate))) 67 | .CreateLogger(); 68 | } 69 | 70 | public static void WriteLine(string message) => Write(LogEventLevel.Information, message); 71 | public static void WriteFatal(string message) => Write(LogEventLevel.Fatal, message); 72 | 73 | private static void Write(LogEventLevel logEventLevel, string message) 74 | { 75 | mainLogger?.Write(logEventLevel, message); 76 | fileLogger?.Write(logEventLevel, message.RemoveAnsi()); 77 | } 78 | 79 | public static void WriteEvent(LogSeverity severity, object source, string message) 80 | { 81 | if (mainLogger == null && fileLogger == null) return; 82 | 83 | var eventLevel = severityToEventLevelMapping.ContainsKey(severity) ? severityToEventLevelMapping[severity] : LogEventLevel.Verbose; 84 | var logMessage = $"{logSeverityAnsiColors[severity]}[{source?.GetType().Name ?? string.Empty}]{Ansi.Reset}: {message}"; 85 | mainLogger?.Write(eventLevel, logMessage); 86 | fileLogger?.Write(eventLevel, logMessage.RemoveAnsi()); 87 | } 88 | } 89 | 90 | class TextWriterSink : ILogEventSink 91 | { 92 | readonly TextWriter textWriter = default; 93 | readonly ITextFormatter textFormatter = default; 94 | 95 | readonly object syncRoot = new(); 96 | 97 | public TextWriterSink(TextWriter writer, ITextFormatter formatter) 98 | { 99 | textWriter = writer; 100 | textFormatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); 101 | } 102 | 103 | public void Emit(LogEvent logEvent) 104 | { 105 | lock (syncRoot) 106 | { 107 | textFormatter.Format(logEvent ?? throw new ArgumentNullException(nameof(logEvent)), textWriter); 108 | textWriter.Flush(); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /StoicGoose.Common/Utilities/Resources.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | 4 | using StoicGoose.Common.Drawing; 5 | 6 | namespace StoicGoose.Common.Utilities 7 | { 8 | public static class Resources 9 | { 10 | private static Stream GetEmbeddedResourceStream(string name) 11 | { 12 | var assembly = Assembly.GetEntryAssembly(); 13 | name = $"{assembly.GetName().Name}.{name}"; 14 | return assembly.GetManifestResourceStream(name); 15 | } 16 | 17 | public static RgbaFile GetEmbeddedRgbaFile(string name) 18 | { 19 | using var stream = GetEmbeddedResourceStream(name); 20 | if (stream == null) return null; 21 | return new RgbaFile(stream); 22 | } 23 | 24 | public static string GetEmbeddedText(string name) 25 | { 26 | using var stream = GetEmbeddedResourceStream(name); 27 | if (stream == null) return string.Empty; 28 | using var reader = new StreamReader(stream); 29 | return reader.ReadToEnd(); 30 | } 31 | 32 | public static byte[] GetEmbeddedRawData(string name) 33 | { 34 | using var stream = GetEmbeddedResourceStream(name); 35 | if (stream == null) return null; 36 | var buffer = new byte[stream.Length]; 37 | stream.Read(buffer, 0, buffer.Length); 38 | return buffer; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /StoicGoose.Core/Bootstrap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using StoicGoose.Core.Interfaces; 4 | 5 | namespace StoicGoose.Core 6 | { 7 | public class Bootstrap : IComponent 8 | { 9 | readonly byte[] rom = Array.Empty(); 10 | readonly uint romMask = 0; 11 | 12 | public Bootstrap(int size) 13 | { 14 | rom = new byte[size]; 15 | romMask = (uint)(rom.Length - 1); 16 | } 17 | 18 | public void Reset() 19 | { 20 | // 21 | } 22 | 23 | public void Shutdown() 24 | { 25 | // 26 | } 27 | 28 | public void LoadRom(byte[] data) 29 | { 30 | if (data.Length != rom.Length) 31 | throw new Exception("Data size mismatch error"); 32 | 33 | Buffer.BlockCopy(data, 0, rom, 0, data.Length); 34 | } 35 | 36 | public byte ReadMemory(uint address) 37 | { 38 | return rom[address & romMask]; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /StoicGoose.Core/CPU/V30MZ.Addressing.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.CPU 2 | { 3 | public sealed partial class V30MZ 4 | { 5 | private byte ReadOpcodeEb() 6 | { 7 | ReadModRM(); 8 | if (modRm.Mod == ModRM.Modes.Register) 9 | return GetRegister8((RegisterNumber8)modRm.Mem); 10 | else 11 | return ReadMemory8(modRm.Segment, modRm.Offset); 12 | } 13 | 14 | private ushort ReadOpcodeEw() 15 | { 16 | ReadModRM(); 17 | if (modRm.Mod == ModRM.Modes.Register) 18 | return GetRegister16((RegisterNumber16)modRm.Mem); 19 | else 20 | return ReadMemory16(modRm.Segment, modRm.Offset); 21 | } 22 | 23 | private void WriteOpcodeEb(byte value) 24 | { 25 | ReadModRM(); 26 | if (modRm.Mod == ModRM.Modes.Register) 27 | SetRegister8((RegisterNumber8)modRm.Mem, value); 28 | else 29 | WriteMemory8(modRm.Segment, modRm.Offset, value); 30 | } 31 | 32 | private void WriteOpcodeEw(ushort value) 33 | { 34 | ReadModRM(); 35 | if (modRm.Mod == ModRM.Modes.Register) 36 | SetRegister16((RegisterNumber16)modRm.Mem, value); 37 | else 38 | WriteMemory16(modRm.Segment, modRm.Offset, value); 39 | } 40 | 41 | private byte ReadOpcodeGb() 42 | { 43 | ReadModRM(); 44 | return GetRegister8((RegisterNumber8)modRm.Reg); 45 | } 46 | 47 | private ushort ReadOpcodeGw() 48 | { 49 | ReadModRM(); 50 | return GetRegister16((RegisterNumber16)modRm.Reg); 51 | } 52 | 53 | private void WriteOpcodeGb(byte value) 54 | { 55 | ReadModRM(); 56 | SetRegister8((RegisterNumber8)modRm.Reg, value); 57 | } 58 | 59 | private void WriteOpcodeGw(ushort value) 60 | { 61 | ReadModRM(); 62 | SetRegister16((RegisterNumber16)modRm.Reg, value); 63 | } 64 | 65 | private ushort ReadOpcodeSw() 66 | { 67 | ReadModRM(); 68 | return GetSegment((SegmentNumber)modRm.Reg); 69 | } 70 | 71 | private void WriteOpcodeSw(ushort value) 72 | { 73 | ReadModRM(); 74 | SetSegment((SegmentNumber)modRm.Reg, value); 75 | } 76 | 77 | private byte ReadOpcodeIb() 78 | { 79 | return ReadMemory8(cs, ip++); 80 | } 81 | 82 | private ushort ReadOpcodeIw() 83 | { 84 | var value = ReadMemory16(cs, ip); 85 | ip += 2; 86 | return value; 87 | } 88 | 89 | private ushort ReadOpcodeJb() 90 | { 91 | var tmp1 = (ushort)(ip + 1); 92 | var tmp2 = (sbyte)ReadOpcodeIb(); 93 | return (ushort)(tmp1 + tmp2); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /StoicGoose.Core/CPU/V30MZ.Flags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StoicGoose.Core.CPU 4 | { 5 | public sealed partial class V30MZ 6 | { 7 | [Flags] 8 | public enum Flags : ushort 9 | { 10 | Carry = 1 << 0, /* CF */ 11 | ReservedB1 = 1 << 1, /* (reserved) */ 12 | Parity = 1 << 2, /* PF */ 13 | ReservedB3 = 1 << 3, /* (reserved) */ 14 | Auxiliary = 1 << 4, /* AF */ 15 | ReservedB5 = 1 << 5, /* (reserved) */ 16 | Zero = 1 << 6, /* ZF */ 17 | Sign = 1 << 7, /* SF */ 18 | Trap = 1 << 8, /* TF */ 19 | InterruptEnable = 1 << 9, /* IF */ 20 | Direction = 1 << 10, /* DF */ 21 | Overflow = 1 << 11, /* OF */ 22 | ReservedB12 = 1 << 12, /* (reserved) */ 23 | ReservedB13 = 1 << 13, /* (reserved) */ 24 | ReservedB14 = 1 << 14, /* (reserved) */ 25 | ReservedB15 = 1 << 15 /* (reserved) */ 26 | } 27 | 28 | private void SetFlags(Flags flags) 29 | { 30 | this.flags |= flags; 31 | } 32 | 33 | private void ClearFlags(Flags flags) 34 | { 35 | this.flags &= ~flags; 36 | } 37 | 38 | public bool IsFlagSet(Flags flags) 39 | { 40 | return (this.flags & flags) == flags; 41 | } 42 | 43 | private void SetClearFlagConditional(Flags flags, bool condition) 44 | { 45 | if (condition) this.flags |= flags; 46 | else this.flags &= ~flags; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /StoicGoose.Core/CPU/V30MZ.Memory.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.CPU 2 | { 3 | public sealed partial class V30MZ 4 | { 5 | private byte ReadMemory8(ushort segment, ushort offset) 6 | { 7 | return machine.ReadMemory((uint)((segment << 4) + offset)); 8 | } 9 | 10 | private ushort ReadMemory16(ushort segment, ushort offset) 11 | { 12 | return (ushort)((machine.ReadMemory((uint)((segment << 4) + offset + 1)) << 8) | machine.ReadMemory((uint)((segment << 4) + offset))); 13 | } 14 | 15 | private void WriteMemory8(ushort segment, ushort offset, byte value) 16 | { 17 | machine.WriteMemory((uint)((segment << 4) + offset), value); 18 | } 19 | 20 | private void WriteMemory16(ushort segment, ushort offset, ushort value) 21 | { 22 | machine.WriteMemory((uint)((segment << 4) + offset), (byte)(value & 0xFF)); 23 | machine.WriteMemory((uint)((segment << 4) + offset + 1), (byte)(value >> 8)); 24 | } 25 | 26 | private ushort ReadPort16(ushort port) 27 | { 28 | return (ushort)(machine.ReadPort((ushort)(port + 1)) << 8 | machine.ReadPort(port)); 29 | } 30 | 31 | private void WritePort16(ushort port, ushort value) 32 | { 33 | machine.WritePort(port, (byte)(value & 0xFF)); 34 | machine.WritePort((ushort)(port + 1), (byte)(value >> 8)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /StoicGoose.Core/CPU/V30MZ.Miscellaneous.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.CPU 2 | { 3 | public sealed partial class V30MZ 4 | { 5 | private void Push(ushort value) 6 | { 7 | sp -= 2; 8 | WriteMemory16(ss, sp, value); 9 | } 10 | 11 | private ushort Pop() 12 | { 13 | var value = ReadMemory16(ss, sp); 14 | sp += 2; 15 | return value; 16 | } 17 | 18 | private int Loop() 19 | { 20 | if (--cx.Word != 0) { ip = ReadOpcodeJb(); return 4; } 21 | else { ip++; return 1; } 22 | } 23 | 24 | private int LoopWhile(bool condition) 25 | { 26 | if (--cx.Word != 0 && condition) { ip = ReadOpcodeJb(); return 5; } 27 | else { ip++; return 2; } 28 | } 29 | 30 | private int JumpConditional(bool condition) 31 | { 32 | if (condition) { ip = ReadOpcodeJb(); return 4; } 33 | else { ip++; return 1; } 34 | } 35 | 36 | private static bool CalculateParity(int result) 37 | { 38 | int bitsSet = 0; 39 | while (result != 0) { bitsSet += result & 0x01; result >>= 1; } 40 | return bitsSet == 0 || (bitsSet % 2) == 0; 41 | } 42 | 43 | private static void Exchange(ref ushort a, ref ushort b) 44 | { 45 | (b, a) = (a, b); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /StoicGoose.Core/CPU/V30MZ.Prefixes.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.CPU 2 | { 3 | public sealed partial class V30MZ 4 | { 5 | SegmentNumber prefixSegOverride; 6 | bool prefixHasRepeat; 7 | bool prefixRepeatOnNotEqual; 8 | 9 | private void ResetPrefixes() 10 | { 11 | prefixSegOverride = SegmentNumber.Unset; 12 | prefixHasRepeat = false; 13 | prefixRepeatOnNotEqual = false; 14 | } 15 | 16 | private bool HandlePrefixes(byte op) 17 | { 18 | var isOpcode = true; 19 | 20 | switch (op) 21 | { 22 | /* Prefixes */ 23 | case 0x26: 24 | /* :ES */ 25 | prefixSegOverride = SegmentNumber.ES; 26 | isOpcode = false; 27 | break; 28 | 29 | case 0x2E: 30 | /* :CS */ 31 | prefixSegOverride = SegmentNumber.CS; 32 | isOpcode = false; 33 | break; 34 | 35 | case 0x36: 36 | /* :SS */ 37 | prefixSegOverride = SegmentNumber.SS; 38 | isOpcode = false; 39 | break; 40 | 41 | case 0x3E: 42 | /* :DS */ 43 | prefixSegOverride = SegmentNumber.DS; 44 | isOpcode = false; 45 | break; 46 | 47 | case 0xF0: 48 | /* LOCK */ 49 | //TODO: implement?? 50 | isOpcode = false; 51 | break; 52 | 53 | case 0xF2: 54 | /* REPNE */ 55 | prefixHasRepeat = true; 56 | prefixRepeatOnNotEqual = true; 57 | isOpcode = false; 58 | break; 59 | 60 | case 0xF3: 61 | /* REP/REPE */ 62 | prefixHasRepeat = true; 63 | prefixRepeatOnNotEqual = false; 64 | isOpcode = false; 65 | break; 66 | } 67 | 68 | return isOpcode; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /StoicGoose.Core/CPU/V30MZ.Registers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace StoicGoose.Core.CPU 5 | { 6 | public sealed partial class V30MZ 7 | { 8 | enum RegisterNumber8 : byte 9 | { 10 | AL = 0b000, 11 | CL = 0b001, 12 | DL = 0b010, 13 | BL = 0b011, 14 | AH = 0b100, 15 | CH = 0b101, 16 | DH = 0b110, 17 | BH = 0b111 18 | } 19 | 20 | enum RegisterNumber16 : byte 21 | { 22 | AX = 0b000, 23 | CX = 0b001, 24 | DX = 0b010, 25 | BX = 0b011, 26 | SP = 0b100, 27 | BP = 0b101, 28 | SI = 0b110, 29 | DI = 0b111 30 | } 31 | 32 | private byte GetRegister8(RegisterNumber8 reg) 33 | { 34 | return reg switch 35 | { 36 | RegisterNumber8.AL => ax.Low, 37 | RegisterNumber8.CL => cx.Low, 38 | RegisterNumber8.DL => dx.Low, 39 | RegisterNumber8.BL => bx.Low, 40 | RegisterNumber8.AH => ax.High, 41 | RegisterNumber8.CH => cx.High, 42 | RegisterNumber8.DH => dx.High, 43 | RegisterNumber8.BH => bx.High, 44 | _ => throw new ArgumentException("Invalid register", nameof(reg)), 45 | }; 46 | } 47 | 48 | private ushort GetRegister16(RegisterNumber16 reg) 49 | { 50 | return reg switch 51 | { 52 | RegisterNumber16.AX => ax.Word, 53 | RegisterNumber16.CX => cx.Word, 54 | RegisterNumber16.DX => dx.Word, 55 | RegisterNumber16.BX => bx.Word, 56 | RegisterNumber16.SP => sp, 57 | RegisterNumber16.BP => bp, 58 | RegisterNumber16.SI => si, 59 | RegisterNumber16.DI => di, 60 | _ => throw new ArgumentException("Invalid register", nameof(reg)), 61 | }; 62 | } 63 | 64 | private void SetRegister8(RegisterNumber8 reg, byte value) 65 | { 66 | switch (reg) 67 | { 68 | case RegisterNumber8.AL: ax.Low = value; break; 69 | case RegisterNumber8.CL: cx.Low = value; break; 70 | case RegisterNumber8.DL: dx.Low = value; break; 71 | case RegisterNumber8.BL: bx.Low = value; break; 72 | case RegisterNumber8.AH: ax.High = value; break; 73 | case RegisterNumber8.CH: cx.High = value; break; 74 | case RegisterNumber8.DH: dx.High = value; break; 75 | case RegisterNumber8.BH: bx.High = value; break; 76 | default: throw new ArgumentException("Invalid register", nameof(reg)); 77 | } 78 | } 79 | 80 | private void SetRegister16(RegisterNumber16 reg, ushort value) 81 | { 82 | switch (reg) 83 | { 84 | case RegisterNumber16.AX: ax.Word = value; break; 85 | case RegisterNumber16.CX: cx.Word = value; break; 86 | case RegisterNumber16.DX: dx.Word = value; break; 87 | case RegisterNumber16.BX: bx.Word = value; break; 88 | case RegisterNumber16.SP: sp = value; break; 89 | case RegisterNumber16.BP: bp = value; break; 90 | case RegisterNumber16.SI: si = value; break; 91 | case RegisterNumber16.DI: di = value; break; 92 | default: throw new ArgumentException("Invalid register", nameof(reg)); 93 | } 94 | } 95 | 96 | [StructLayout(LayoutKind.Explicit)] 97 | public struct Register16 98 | { 99 | [FieldOffset(0)] 100 | public byte Low; 101 | [FieldOffset(1)] 102 | public byte High; 103 | 104 | [FieldOffset(0)] 105 | public ushort Word; 106 | 107 | public static implicit operator Register16(ushort value) => new() { Word = value }; 108 | 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /StoicGoose.Core/CPU/V30MZ.Segments.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StoicGoose.Core.CPU 4 | { 5 | public sealed partial class V30MZ 6 | { 7 | enum SegmentNumber : byte 8 | { 9 | ES = 0b00, 10 | CS = 0b01, 11 | SS = 0b10, 12 | DS = 0b11, 13 | Unset = 0xFF 14 | } 15 | 16 | private ushort GetSegment(SegmentNumber seg) 17 | { 18 | return seg switch 19 | { 20 | SegmentNumber.ES => es, 21 | SegmentNumber.CS => cs, 22 | SegmentNumber.SS => ss, 23 | SegmentNumber.DS => ds, 24 | _ => throw new ArgumentException("Invalid segment", nameof(seg)), 25 | }; 26 | } 27 | 28 | private void SetSegment(SegmentNumber seg, ushort value) 29 | { 30 | switch (seg) 31 | { 32 | case SegmentNumber.ES: es = value; break; 33 | case SegmentNumber.CS: cs = value; break; 34 | case SegmentNumber.SS: ss = value; break; 35 | case SegmentNumber.DS: ds = value; break; 36 | default: throw new ArgumentException("Invalid segment", nameof(seg)); 37 | } 38 | } 39 | 40 | private ushort GetSegmentViaOverride(SegmentNumber seg) 41 | { 42 | return GetSegment(prefixSegOverride != SegmentNumber.Unset ? prefixSegOverride : seg); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /StoicGoose.Core/CPU/V30MZ.Strings.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.CPU 2 | { 3 | public sealed partial class V30MZ 4 | { 5 | private static int GetIncrement(bool is16Bit, bool isDirectionFlagSet) 6 | { 7 | return isDirectionFlagSet ? (is16Bit ? -2 : -1) : (is16Bit ? 2 : 1); 8 | } 9 | 10 | private void InString(bool is16Bit) 11 | { 12 | var increment = GetIncrement(is16Bit, IsFlagSet(Flags.Direction)); 13 | 14 | if (!is16Bit) 15 | WriteMemory8(es, di, machine.ReadPort(dx.Word)); 16 | else 17 | WriteMemory16(es, di, ReadPort16(dx.Word)); 18 | 19 | di = (ushort)(di + increment); 20 | } 21 | 22 | private void OutString(bool is16Bit) 23 | { 24 | var increment = GetIncrement(is16Bit, IsFlagSet(Flags.Direction)); 25 | 26 | var temp = GetSegmentViaOverride(SegmentNumber.DS); 27 | 28 | if (!is16Bit) 29 | machine.WritePort(dx.Word, ReadMemory8(temp, si)); 30 | else 31 | WritePort16(dx.Word, ReadMemory16(temp, si)); 32 | 33 | si = (ushort)(si + increment); 34 | } 35 | 36 | private void MoveString(bool is16Bit) 37 | { 38 | var increment = GetIncrement(is16Bit, IsFlagSet(Flags.Direction)); 39 | 40 | var temp = GetSegmentViaOverride(SegmentNumber.DS); 41 | 42 | if (!is16Bit) 43 | WriteMemory8(es, di, ReadMemory8(temp, si)); 44 | else 45 | WriteMemory16(es, di, ReadMemory16(temp, si)); 46 | 47 | di = (ushort)(di + increment); 48 | si = (ushort)(si + increment); 49 | } 50 | 51 | private void CompareString(bool is16Bit) 52 | { 53 | var increment = GetIncrement(is16Bit, IsFlagSet(Flags.Direction)); 54 | 55 | var temp = GetSegmentViaOverride(SegmentNumber.DS); 56 | 57 | if (!is16Bit) 58 | Sub8(false, ReadMemory8(temp, si), ReadMemory8(es, di)); 59 | else 60 | Sub16(false, ReadMemory16(temp, si), ReadMemory16(es, di)); 61 | 62 | di = (ushort)(di + increment); 63 | si = (ushort)(si + increment); 64 | } 65 | 66 | private void StoreString(bool is16Bit) 67 | { 68 | var increment = GetIncrement(is16Bit, IsFlagSet(Flags.Direction)); 69 | 70 | if (!is16Bit) 71 | WriteMemory8(es, di, ax.Low); 72 | else 73 | WriteMemory16(es, di, ax.Word); 74 | 75 | di = (ushort)(di + increment); 76 | } 77 | 78 | private void LoadString(bool is16Bit) 79 | { 80 | var increment = GetIncrement(is16Bit, IsFlagSet(Flags.Direction)); 81 | 82 | var temp = GetSegmentViaOverride(SegmentNumber.DS); 83 | 84 | if (!is16Bit) 85 | ax.Low = ReadMemory8(temp, si); 86 | else 87 | ax.Word = ReadMemory16(temp, si); 88 | 89 | si = (ushort)(si + increment); 90 | } 91 | 92 | private void ScanString(bool is16Bit) 93 | { 94 | var increment = GetIncrement(is16Bit, IsFlagSet(Flags.Direction)); 95 | 96 | if (!is16Bit) 97 | Sub8(false, ax.Low, ReadMemory8(es, di)); 98 | else 99 | Sub16(false, ax.Word, ReadMemory16(es, di)); 100 | 101 | di = (ushort)(di + increment); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /StoicGoose.Core/CPU/V30MZ.cs: -------------------------------------------------------------------------------- 1 | using StoicGoose.Core.Interfaces; 2 | 3 | namespace StoicGoose.Core.CPU 4 | { 5 | public sealed partial class V30MZ : IComponent 6 | { 7 | // TODO: attempt prefetch emulation (Meitantei Conan - Nishi no Meitantei Saidai no Kiki; cart changes banks on startup, can no longer execute jump, execs garbage) 8 | 9 | /* Parent machine instance */ 10 | readonly IMachine machine = default; 11 | 12 | /* General registers */ 13 | Register16 ax, bx, cx, dx; 14 | ushort sp, bp, si, di; 15 | /* Segment registers */ 16 | ushort cs, ds, ss, es; 17 | /* Status and instruction registers */ 18 | ushort ip; 19 | Flags flags; 20 | 21 | bool halted; 22 | int opCycles, intCycles; 23 | 24 | /* Public properties for registers */ 25 | public Register16 AX { get => ax; set => ax = value; } 26 | public Register16 BX { get => bx; set => bx = value; } 27 | public Register16 CX { get => cx; set => cx = value; } 28 | public Register16 DX { get => dx; set => dx = value; } 29 | public ushort SP { get => sp; set => sp = value; } 30 | public ushort BP { get => bp; set => bp = value; } 31 | public ushort SI { get => si; set => si = value; } 32 | public ushort DI { get => di; set => di = value; } 33 | public ushort CS { get => cs; set => cs = value; } 34 | public ushort DS { get => ds; set => ds = value; } 35 | public ushort SS { get => ss; set => ss = value; } 36 | public ushort ES { get => es; set => es = value; } 37 | public ushort IP { get => ip; set => ip = value; } 38 | 39 | public bool IsHalted { get => halted; set => halted = value; } 40 | 41 | public V30MZ(IMachine machine) 42 | { 43 | this.machine = machine; 44 | 45 | Reset(); 46 | } 47 | 48 | public void Reset() 49 | { 50 | /* CPU reset */ 51 | flags = Flags.ReservedB1 | Flags.ReservedB12 | Flags.ReservedB13 | Flags.ReservedB14 | Flags.ReservedB15; 52 | ip = 0x0000; 53 | cs = 0xFFFF; 54 | ds = 0x0000; 55 | ss = 0x0000; 56 | es = 0x0000; 57 | 58 | /* Initialized by WS bootstrap */ 59 | ax.Word = 0x0000; 60 | dx.Word = 0x0000; 61 | bp = 0x0000; 62 | ss = 0x0000; 63 | sp = 0x2000; 64 | ds = 0x0000; 65 | es = 0x0000; 66 | 67 | /* Misc variables */ 68 | halted = false; 69 | opCycles = intCycles = 0; 70 | 71 | ResetPrefixes(); 72 | modRm.Reset(); 73 | } 74 | 75 | public void Shutdown() 76 | { 77 | // 78 | } 79 | 80 | public void Interrupt(int vector) 81 | { 82 | /* Resume execution */ 83 | halted = false; 84 | 85 | /* Read interrupt handler's segment & offset */ 86 | var offset = ReadMemory16(0, (ushort)((vector * 4) + 0)); 87 | var segment = ReadMemory16(0, (ushort)((vector * 4) + 2)); 88 | 89 | /* Push state, clear flags, etc. */ 90 | Push((ushort)flags); 91 | Push(cs); 92 | Push(ip); 93 | 94 | ClearFlags(Flags.InterruptEnable); 95 | ClearFlags(Flags.Trap); 96 | 97 | ResetPrefixes(); 98 | modRm.Reset(); 99 | 100 | intCycles = 32; 101 | 102 | /* Continue with interrupt handler */ 103 | cs = segment; 104 | ip = offset; 105 | } 106 | 107 | public int Step() 108 | { 109 | var cycles = 0; 110 | 111 | if (halted) 112 | { 113 | /* CPU is halted */ 114 | cycles++; 115 | } 116 | else 117 | { 118 | /* Read any prefixes & opcode */ 119 | byte opcode; 120 | while (!HandlePrefixes(opcode = ReadMemory8(cs, ip++))) { } 121 | 122 | /* Execute instruction */ 123 | opCycles = instructions[opcode](this); 124 | 125 | cycles += opCycles; 126 | opCycles = 0; 127 | } 128 | 129 | cycles += intCycles; 130 | intCycles = 0; 131 | 132 | /* Reset state for next instruction */ 133 | ResetPrefixes(); 134 | modRm.Reset(); 135 | 136 | return cycles; 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /StoicGoose.Core/DMA/SphinxGeneralDMAController.cs: -------------------------------------------------------------------------------- 1 | using StoicGoose.Core.Interfaces; 2 | 3 | using static StoicGoose.Common.Utilities.BitHandling; 4 | 5 | namespace StoicGoose.Core.DMA 6 | { 7 | public class SphinxGeneralDMAController : IPortAccessComponent 8 | { 9 | // TODO: verify behavior! 10 | 11 | readonly IMachine machine = default; 12 | 13 | /* REG_DMA_SRC(_HI) */ 14 | uint dmaSource; 15 | /* REG_DMA_DST */ 16 | ushort dmaDestination; 17 | /* REG_DMA_LEN */ 18 | ushort dmaLength; 19 | /* REG_DMA_CTRL */ 20 | byte dmaControl; 21 | 22 | public bool IsActive => IsBitSet(dmaControl, 7); 23 | 24 | bool isDecrementMode => IsBitSet(dmaControl, 6); 25 | 26 | public SphinxGeneralDMAController(IMachine machine) 27 | { 28 | this.machine = machine; 29 | } 30 | 31 | public void Reset() 32 | { 33 | // 34 | 35 | ResetRegisters(); 36 | } 37 | 38 | private void ResetRegisters() 39 | { 40 | dmaSource = dmaDestination = dmaLength = dmaControl = 0; 41 | } 42 | 43 | public void Shutdown() 44 | { 45 | // 46 | } 47 | 48 | public int Step() 49 | { 50 | if (dmaLength == 0 || ((dmaSource >> 16) & 0x0F) == 0x01) 51 | { 52 | /* Disable DMA if length is zero OR source is SRAM */ 53 | ChangeBit(ref dmaControl, 7, false); 54 | return 5; 55 | } 56 | else 57 | { 58 | if (((dmaSource >> 16) & 0x0F) != 0x01) 59 | { 60 | /* Perform DMA if source is not SRAM */ 61 | machine.WriteMemory((uint)(dmaDestination + 0), machine.ReadMemory(dmaSource + 0)); 62 | machine.WriteMemory((uint)(dmaDestination + 1), machine.ReadMemory(dmaSource + 1)); 63 | } 64 | 65 | dmaSource += (uint)(isDecrementMode ? -2 : 2); 66 | dmaDestination += (ushort)(isDecrementMode ? -2 : 2); 67 | dmaLength -= 2; 68 | 69 | return 2; 70 | } 71 | } 72 | 73 | public byte ReadPort(ushort port) 74 | { 75 | var retVal = (byte)0; 76 | 77 | switch (port) 78 | { 79 | case 0x40: 80 | /* REG_DMA_SRC (low) */ 81 | retVal |= (byte)((dmaSource >> 0) & 0xFE); 82 | break; 83 | case 0x41: 84 | /* REG_DMA_SRC (mid) */ 85 | retVal |= (byte)((dmaSource >> 8) & 0xFF); 86 | break; 87 | case 0x42: 88 | /* REG_DMA_SRC_HI */ 89 | retVal |= (byte)((dmaSource >> 16) & 0x0F); 90 | break; 91 | 92 | case 0x44: 93 | /* REG_DMA_DST (low) */ 94 | retVal |= (byte)((dmaDestination >> 0) & 0xFE); 95 | break; 96 | case 0x45: 97 | /* REG_DMA_DST (high) */ 98 | retVal |= (byte)((dmaDestination >> 8) & 0xFF); 99 | break; 100 | 101 | case 0x46: 102 | /* REG_DMA_LEN */ 103 | retVal |= (byte)((dmaLength >> 0) & 0xFE); 104 | break; 105 | case 0x47: 106 | /* REG_DMA_LEN */ 107 | retVal |= (byte)((dmaLength >> 8) & 0xFF); 108 | break; 109 | 110 | case 0x48: 111 | /* REG_DMA_CTRL */ 112 | retVal |= (byte)(dmaControl & 0b11000000); 113 | break; 114 | } 115 | 116 | return retVal; 117 | } 118 | 119 | public void WritePort(ushort port, byte value) 120 | { 121 | switch (port) 122 | { 123 | case 0x40: 124 | /* REG_DMA_SRC (low) */ 125 | dmaSource &= 0xFFF00; 126 | dmaSource |= (uint)((value << 0) & 0x000FE); 127 | break; 128 | case 0x41: 129 | /* REG_DMA_SRC (high) */ 130 | dmaSource &= 0xF00FE; 131 | dmaSource |= (uint)((value << 8) & 0x0FF00); 132 | break; 133 | case 0x42: 134 | /* REG_DMA_SRC_HI */ 135 | dmaSource &= 0x0FFFE; 136 | dmaSource |= (uint)((value << 16) & 0xF0000); 137 | break; 138 | 139 | case 0x44: 140 | /* REG_DMA_DST (low) */ 141 | dmaDestination &= 0xFF00; 142 | dmaDestination |= (ushort)((value << 0) & 0x00FE); 143 | break; 144 | case 0x45: 145 | /* REG_DMA_DST (high) */ 146 | dmaDestination &= 0x00FE; 147 | dmaDestination |= (ushort)((value << 8) & 0xFF00); 148 | break; 149 | 150 | case 0x46: 151 | /* REG_DMA_LEN (low) */ 152 | dmaLength &= 0xFF00; 153 | dmaLength |= (ushort)((value << 0) & 0x00FE); 154 | break; 155 | case 0x47: 156 | /* REG_DMA_LEN (high) */ 157 | dmaLength &= 0x00FE; 158 | dmaLength |= (ushort)((value << 8) & 0xFF00); 159 | break; 160 | 161 | case 0x48: 162 | /* REG_DMA_CTRL */ 163 | dmaControl = (byte)(value & 0b11000000); 164 | break; 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /StoicGoose.Core/Display/DisplayTimer.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.Display 2 | { 3 | public class DisplayTimer 4 | { 5 | public bool Enable { get; set; } 6 | public bool Repeating { get; set; } 7 | public ushort Frequency { get; set; } 8 | 9 | public ushort Counter { get; set; } 10 | 11 | public DisplayTimer() 12 | { 13 | Reset(); 14 | } 15 | 16 | public void Reset() 17 | { 18 | Enable = Repeating = false; 19 | Frequency = Counter = 0; 20 | } 21 | 22 | public void Reload() 23 | { 24 | Counter = Frequency; 25 | } 26 | 27 | public bool Step() 28 | { 29 | var counterNew = (ushort)(Counter - 1); 30 | 31 | if (Enable && Counter != 0) 32 | { 33 | Counter = counterNew; 34 | if (Repeating && Counter == 0) 35 | Reload(); 36 | } 37 | return counterNew == 0; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /StoicGoose.Core/Display/DisplayUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using StoicGoose.Core.Interfaces; 3 | 4 | namespace StoicGoose.Core.Display 5 | { 6 | public static class DisplayUtilities 7 | { 8 | // TODO: WSC high contrast mode 9 | 10 | private static ushort ReadMemory16(IMachine machine, uint address) => (ushort)(machine.ReadMemory(address + 1) << 8 | machine.ReadMemory(address)); 11 | private static uint ReadMemory32(IMachine machine, uint address) => (uint)(machine.ReadMemory(address + 3) << 24 | machine.ReadMemory(address + 2) << 16 | machine.ReadMemory(address + 1) << 8 | machine.ReadMemory(address)); 12 | 13 | public static byte ReadPixel(IMachine machine, ushort tile, int y, int x, bool isPacked, bool is4bpp, bool isColor) 14 | { 15 | /* http://perfectkiosk.net/stsws.html#color_mode */ 16 | 17 | /* WonderSwan OR Color/Crystal in 2bpp mode */ 18 | if (!isColor || (isColor && !is4bpp)) 19 | { 20 | var data = ReadMemory16(machine, (uint)(0x2000 + (tile << 4) + ((y % 8) << 1))); 21 | return (byte)((((data >> 15 - (x % 8)) & 0b1) << 1 | ((data >> 7 - (x % 8)) & 0b1)) & 0b11); 22 | } 23 | 24 | /* WonderSwan Color/Crystal in 4bpp mode */ 25 | else if (isColor && is4bpp) 26 | { 27 | /* 4bpp planar mode */ 28 | if (!isPacked) 29 | { 30 | var data = ReadMemory32(machine, (uint)(0x4000 + ((tile & 0x03FF) << 5) + ((y % 8) << 2))); 31 | return (byte)((((data >> 31 - (x % 8)) & 0b1) << 3 | ((data >> 23 - (x % 8)) & 0b1) << 2 | ((data >> 15 - (x % 8)) & 0b1) << 1 | ((data >> 7 - (x % 8)) & 0b1)) & 0b1111); 32 | } 33 | 34 | /* 4bpp packed mode */ 35 | else if (isPacked) 36 | { 37 | var data = machine.ReadMemory((ushort)(0x4000 + ((tile & 0x03FF) << 5) + ((y % 8) << 2) + ((x % 8) >> 1))); 38 | return (byte)((data >> 4 - (((x % 8) & 0b1) << 2)) & 0b1111); 39 | } 40 | } 41 | 42 | throw new Exception("Invalid display controller configuration"); 43 | } 44 | 45 | public static ushort ReadColor(IMachine machine, byte paletteIdx, byte colorIdx) 46 | { 47 | var address = (uint)(0x0FE00 + (paletteIdx << 5) + (colorIdx << 1)); 48 | return (ushort)(machine.ReadMemory(address + 1) << 8 | machine.ReadMemory(address)); 49 | } 50 | 51 | private static byte DuplicateBits(int value) => (byte)((value & 0b1111) | (value & 0b1111) << 4); 52 | 53 | public static (byte r, byte g, byte b) GeneratePixel(byte data) => (DuplicateBits(data), DuplicateBits(data), DuplicateBits(data)); 54 | public static (byte r, byte g, byte b) GeneratePixel(ushort data) => (DuplicateBits(data >> 8), DuplicateBits(data >> 4), DuplicateBits(data >> 0)); 55 | 56 | public static void CopyPixel((byte r, byte g, byte b) pixel, byte[] data, int x, int y, int stride) => CopyPixel(pixel, data, ((y * stride) + x) * 4); 57 | public static void CopyPixel((byte r, byte g, byte b) pixel, byte[] data, long address) 58 | { 59 | data[address + 0] = pixel.r; 60 | data[address + 1] = pixel.g; 61 | data[address + 2] = pixel.b; 62 | data[address + 3] = 255; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /StoicGoose.Core/Interfaces/IComponent.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.Interfaces 2 | { 3 | public interface IComponent 4 | { 5 | void Reset(); 6 | void Shutdown(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /StoicGoose.Core/Interfaces/IMachine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | using StoicGoose.Core.Cartridges; 7 | using StoicGoose.Core.CPU; 8 | using StoicGoose.Core.Display; 9 | using StoicGoose.Core.EEPROMs; 10 | using StoicGoose.Core.Sound; 11 | 12 | namespace StoicGoose.Core.Interfaces 13 | { 14 | public interface IMachine 15 | { 16 | static IEnumerable GetMachineTypes() => Assembly.GetAssembly(typeof(IMachine)).GetTypes().Where(x => !x.IsInterface && !x.IsAbstract && x.IsAssignableTo(typeof(IMachine))); 17 | 18 | string Manufacturer { get; } 19 | string Model { get; } 20 | 21 | int ScreenWidth { get; } 22 | int ScreenHeight { get; } 23 | double RefreshRate { get; } 24 | string GameControls { get; } 25 | string HardwareControls { get; } 26 | string VerticalControlRemap { get; } 27 | 28 | string InternalEepromDefaultUsername { get; } 29 | Dictionary InternalEepromDefaultData { get; } 30 | 31 | Cartridge Cartridge { get; } 32 | V30MZ Cpu { get; } 33 | DisplayControllerCommon DisplayController { get; } 34 | SoundControllerCommon SoundController { get; } 35 | EEPROM InternalEeprom { get; } 36 | 37 | Func<(List buttonsPressed, List buttonsHeld)> ReceiveInput { get; set; } 38 | 39 | Func ReadMemoryCallback { get; set; } 40 | Action WriteMemoryCallback { get; set; } 41 | Func ReadPortCallback { get; set; } 42 | Action WritePortCallback { get; set; } 43 | Func RunStepCallback { get; set; } 44 | 45 | void Initialize(); 46 | void Reset(); 47 | void Shutdown(); 48 | 49 | void RunFrame(); 50 | void RunLine(); 51 | void RunStep(); 52 | 53 | void LoadBootstrap(byte[] data); 54 | bool IsBootstrapLoaded { get; } 55 | bool UseBootstrap { get; set; } 56 | void LoadInternalEeprom(byte[] data); 57 | void LoadRom(byte[] data); 58 | void LoadSaveData(byte[] data); 59 | 60 | byte[] GetInternalEeprom(); 61 | byte[] GetSaveData(); 62 | 63 | byte ReadMemory(uint address); 64 | void WriteMemory(uint address, byte value); 65 | byte ReadPort(ushort port); 66 | void WritePort(ushort port, byte value); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /StoicGoose.Core/Interfaces/IMemoryAccessComponent.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.Interfaces 2 | { 3 | interface IMemoryAccessComponent : IComponent 4 | { 5 | byte ReadMemory(uint address); 6 | void WriteMemory(uint address, byte value); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /StoicGoose.Core/Interfaces/IPortAccessComponent.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.Interfaces 2 | { 3 | interface IPortAccessComponent : IComponent 4 | { 5 | byte ReadPort(ushort port); 6 | void WritePort(ushort port, byte value); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /StoicGoose.Core/Serial/SerialPort.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using StoicGoose.Common.Attributes; 4 | using StoicGoose.Core.Interfaces; 5 | 6 | using static StoicGoose.Common.Utilities.BitHandling; 7 | 8 | namespace StoicGoose.Core.Serial 9 | { 10 | public class SerialPort : IPortAccessComponent 11 | { 12 | // https://github.com/ares-emulator/ares/tree/709afef820914e3399425bb77cbc1baf895028a1/ares/ws/serial 13 | 14 | [Flags] 15 | public enum SerialInterrupts 16 | { 17 | None = 0, 18 | SerialSend = 1 << 0, 19 | SerialRecieve = 1 << 1 20 | } 21 | 22 | int baudClock, txBitClock; 23 | 24 | /* REG_SER_DATA */ 25 | protected byte serialData; 26 | /* REG_SER_STATUS */ 27 | protected bool enable, baudRateSelect, txEmpty, rxOverrun, rxFull; 28 | 29 | public void Reset() 30 | { 31 | serialData = 0; 32 | enable = baudRateSelect = rxOverrun = rxFull = false; 33 | } 34 | 35 | public void Shutdown() 36 | { 37 | /* Nothing to do... */ 38 | } 39 | 40 | public SerialInterrupts Step() 41 | { 42 | if (!enable) return SerialInterrupts.None; 43 | 44 | if (!baudRateSelect && ++baudClock < 4) return SerialInterrupts.None; 45 | baudClock = 0; 46 | 47 | // STUB 48 | if (!txEmpty) 49 | { 50 | if (++txBitClock == 9) 51 | { 52 | txBitClock = 0; 53 | txEmpty = true; 54 | } 55 | } 56 | 57 | var interrupt = SerialInterrupts.None; 58 | if (txEmpty) interrupt |= SerialInterrupts.SerialSend; 59 | if (rxFull) interrupt |= SerialInterrupts.SerialRecieve; 60 | return interrupt; 61 | } 62 | 63 | public virtual byte ReadPort(ushort port) 64 | { 65 | var retVal = (byte)0; 66 | 67 | switch (port) 68 | { 69 | case 0xB1: 70 | /* REG_SER_DATA */ 71 | retVal = serialData; 72 | rxFull = false; 73 | break; 74 | 75 | case 0xB3: 76 | /* REG_SER_STATUS */ 77 | ChangeBit(ref retVal, 7, enable); 78 | ChangeBit(ref retVal, 6, baudRateSelect); 79 | ChangeBit(ref retVal, 2, txEmpty); 80 | ChangeBit(ref retVal, 1, rxOverrun); 81 | ChangeBit(ref retVal, 0, rxFull); 82 | break; 83 | } 84 | 85 | return retVal; 86 | } 87 | 88 | public virtual void WritePort(ushort port, byte value) 89 | { 90 | switch (port) 91 | { 92 | case 0xB1: 93 | /* REG_SER_DATA */ 94 | if (txEmpty) 95 | { 96 | serialData = value; 97 | txEmpty = false; 98 | } 99 | break; 100 | 101 | case 0xB3: 102 | /* REG_SER_STATUS */ 103 | enable = IsBitSet(value, 7); 104 | baudRateSelect = IsBitSet(value, 6); 105 | rxOverrun = !IsBitSet(value, 5); 106 | break; 107 | } 108 | } 109 | 110 | [Port("REG_SER_DATA", 0x0B1)] 111 | [BitDescription("Serial data")] 112 | [Format("X2")] 113 | public byte SerialData => serialData; 114 | [Port("REG_SER_STATUS", 0x0B3)] 115 | [BitDescription("Serial enabled?", 7)] 116 | public bool Enable => enable; 117 | [Port("REG_SER_STATUS", 0x0B3)] 118 | [BitDescription("Baud rate; is 38400 baud?", 6)] 119 | public bool BaudRateSelect => baudRateSelect; 120 | [Port("REG_SER_STATUS", 0x0B3)] 121 | [BitDescription("TX empty?", 2)] 122 | public bool TxEmpty => txEmpty; 123 | [Port("REG_SER_STATUS", 0x0B3)] 124 | [BitDescription("RX overrun?", 1)] 125 | public bool RxOverrun => rxOverrun; 126 | [Port("REG_SER_STATUS", 0x0B3)] 127 | [BitDescription("RX full?", 0)] 128 | public bool RxFull => rxFull; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /StoicGoose.Core/Sound/AswanSoundController.cs: -------------------------------------------------------------------------------- 1 | using StoicGoose.Common.Attributes; 2 | using StoicGoose.Core.Interfaces; 3 | 4 | namespace StoicGoose.Core.Sound 5 | { 6 | public class AswanSoundController : SoundControllerCommon 7 | { 8 | public override byte MaxMasterVolume => 2; 9 | public override int NumChannels => 4; 10 | 11 | /* REG_SND_9697 */ 12 | protected ushort unknown9697; 13 | /* REG_SND_9899 */ 14 | protected ushort unknown9899; 15 | 16 | public AswanSoundController(IMachine machine, int rate, int outChannels) : base(machine, rate, outChannels) { } 17 | 18 | public override void ResetRegisters() 19 | { 20 | base.ResetRegisters(); 21 | 22 | unknown9697 = 0; 23 | unknown9899 = 0; 24 | } 25 | 26 | public override int[] GenerateSample() 27 | { 28 | channelSampleBuffers[0].Add((short)(channel1.IsEnabled ? (channel1.OutputLeft & 0x07FF) << 5 : 0)); 29 | channelSampleBuffers[0].Add((short)(channel1.IsEnabled ? (channel1.OutputRight & 0x07FF) << 5 : 0)); 30 | channelSampleBuffers[1].Add((short)(channel2.IsEnabled ? (channel2.OutputLeft & 0x07FF) << 5 : 0)); 31 | channelSampleBuffers[1].Add((short)(channel2.IsEnabled ? (channel2.OutputRight & 0x07FF) << 5 : 0)); 32 | channelSampleBuffers[2].Add((short)(channel3.IsEnabled ? (channel3.OutputLeft & 0x07FF) << 5 : 0)); 33 | channelSampleBuffers[2].Add((short)(channel3.IsEnabled ? (channel3.OutputRight & 0x07FF) << 5 : 0)); 34 | channelSampleBuffers[3].Add((short)(channel4.IsEnabled ? (channel4.OutputLeft & 0x07FF) << 5 : 0)); 35 | channelSampleBuffers[3].Add((short)(channel4.IsEnabled ? (channel4.OutputRight & 0x07FF) << 5 : 0)); 36 | 37 | var mixedLeft = 0; 38 | if (channel1.IsEnabled) mixedLeft += channel1.OutputLeft; 39 | if (channel2.IsEnabled) mixedLeft += channel2.OutputLeft; 40 | if (channel3.IsEnabled) mixedLeft += channel3.OutputLeft; 41 | if (channel4.IsEnabled) mixedLeft += channel4.OutputLeft; 42 | mixedLeft = (mixedLeft & 0x07FF) << 5; 43 | 44 | var mixedRight = 0; 45 | if (channel1.IsEnabled) mixedRight += channel1.OutputRight; 46 | if (channel2.IsEnabled) mixedRight += channel2.OutputRight; 47 | if (channel3.IsEnabled) mixedRight += channel3.OutputRight; 48 | if (channel4.IsEnabled) mixedRight += channel4.OutputRight; 49 | mixedRight = (mixedRight & 0x07FF) << 5; 50 | 51 | return new[] { mixedLeft, mixedRight }; 52 | } 53 | 54 | public override byte ReadPort(ushort port) 55 | { 56 | return port switch 57 | { 58 | /* REG_SND_9697 (low) */ 59 | 0x96 => (byte)((unknown9697 >> 0) & 0b11111111), 60 | /* REG_SND_9697 (high) */ 61 | 0x97 => (byte)((unknown9697 >> 8) & 0b00000011), 62 | /* REG_SND_9899 (low) */ 63 | 0x98 => (byte)((unknown9899 >> 0) & 0b11111111), 64 | /* REG_SND_9899 (high) */ 65 | 0x99 => (byte)((unknown9899 >> 8) & 0b00000011), 66 | /* REG_SND_9A */ 67 | 0x9A => 0b111, 68 | /* REG_SND_9B */ 69 | 0x9B => 0b11111110, 70 | /* REG_SND_9C */ 71 | 0x9C => 0b11111111, 72 | /* REG_SND_9D */ 73 | 0x9D => 0b11111111, 74 | /* Fall through to common */ 75 | _ => base.ReadPort(port) 76 | }; 77 | } 78 | 79 | public override void WritePort(ushort port, byte value) 80 | { 81 | switch (port) 82 | { 83 | case 0x96: 84 | /* REG_SND_9697 (low) */ 85 | unknown9697 = (ushort)((unknown9697 & 0x0300) | (value << 0)); 86 | break; 87 | 88 | case 0x97: 89 | /* REG_SND_9697 (high) */ 90 | unknown9697 = (ushort)((unknown9697 & 0x00FF) | (value << 8)); 91 | break; 92 | 93 | case 0x98: 94 | /* REG_SND_9899 (low) */ 95 | unknown9899 = (ushort)((unknown9899 & 0x0300) | (value << 0)); 96 | break; 97 | 98 | case 0x99: 99 | /* REG_SND_9899 (high) */ 100 | unknown9899 = (ushort)((unknown9899 & 0x00FF) | (value << 8)); 101 | break; 102 | 103 | case 0x9A: 104 | case 0x9B: 105 | case 0x9C: 106 | case 0x9D: 107 | /* REG_SND_9x */ 108 | break; 109 | 110 | default: 111 | /* Fall through to common */ 112 | base.WritePort(port, value); 113 | break; 114 | } 115 | } 116 | 117 | [Port("REG_SND_9697", 0x096, 0x097)] 118 | [BitDescription("Unknown data", 0, 9)] 119 | [Format("X4")] 120 | public ushort Unknown9697 => unknown9697; 121 | [Port("REG_SND_9899", 0x098, 0x099)] 122 | [BitDescription("Unknown data", 0, 9)] 123 | [Format("X4")] 124 | public ushort Unknown9899 => unknown9899; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /StoicGoose.Core/Sound/SoundChannel1.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.Sound 2 | { 3 | /* Channel 1, no additional features */ 4 | public sealed class SoundChannel1 5 | { 6 | const int counterReload = 2048; 7 | 8 | ushort counter; 9 | byte pointer; 10 | 11 | public byte OutputLeft { get; set; } 12 | public byte OutputRight { get; set; } 13 | 14 | readonly WaveTableReadDelegate waveTableReadDelegate; 15 | 16 | /* REG_SND_CH1_PITCH */ 17 | public ushort Pitch { get; set; } 18 | /* REG_SND_CH1_VOL */ 19 | public byte VolumeLeft { get; set; } 20 | public byte VolumeRight { get; set; } 21 | /* REG_SND_CTRL */ 22 | public bool IsEnabled { get; set; } 23 | 24 | public SoundChannel1(WaveTableReadDelegate waveTableRead) => waveTableReadDelegate = waveTableRead; 25 | 26 | public void Reset() 27 | { 28 | counter = counterReload; 29 | pointer = 0; 30 | OutputLeft = OutputRight = 0; 31 | 32 | Pitch = 0; 33 | VolumeLeft = VolumeRight = 0; 34 | IsEnabled = false; 35 | } 36 | 37 | public void Step() 38 | { 39 | counter--; 40 | if (counter == Pitch) 41 | { 42 | var data = waveTableReadDelegate((ushort)(pointer >> 1)); 43 | if ((pointer & 0b1) == 0b1) data >>= 4; 44 | data &= 0x0F; 45 | 46 | OutputLeft = (byte)(data * VolumeLeft); 47 | OutputRight = (byte)(data * VolumeRight); 48 | 49 | pointer++; 50 | pointer &= 0b11111; 51 | counter = counterReload; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /StoicGoose.Core/Sound/SoundChannel2.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.Sound 2 | { 3 | /* Channel 2, supports PCM voice */ 4 | public sealed class SoundChannel2 5 | { 6 | const int counterReload = 2048; 7 | 8 | ushort counter; 9 | byte pointer; 10 | 11 | public byte OutputLeft { get; set; } 12 | public byte OutputRight { get; set; } 13 | 14 | readonly WaveTableReadDelegate waveTableReadDelegate; 15 | 16 | /* REG_SND_CH2_PITCH */ 17 | public ushort Pitch { get; set; } 18 | /* REG_SND_CH2_VOL */ 19 | public byte VolumeLeft { get; set; } 20 | public byte VolumeRight { get; set; } 21 | /* REG_SND_CTRL */ 22 | public bool IsEnabled { get; set; } 23 | public bool IsVoiceEnabled { get; set; } 24 | 25 | /* REG_SND_VOICE_CTRL */ 26 | public bool PcmRightFull { get; set; } 27 | public bool PcmRightHalf { get; set; } 28 | public bool PcmLeftFull { get; set; } 29 | public bool PcmLeftHalf { get; set; } 30 | 31 | public SoundChannel2(WaveTableReadDelegate waveTableRead) => waveTableReadDelegate = waveTableRead; 32 | 33 | public void Reset() 34 | { 35 | counter = counterReload; 36 | pointer = 0; 37 | OutputLeft = OutputRight = 0; 38 | 39 | Pitch = 0; 40 | VolumeLeft = VolumeRight = 0; 41 | IsEnabled = false; 42 | 43 | IsVoiceEnabled = false; 44 | 45 | PcmRightFull = PcmRightHalf = PcmLeftFull = PcmLeftHalf = false; 46 | } 47 | 48 | public void Step() 49 | { 50 | if (IsVoiceEnabled) 51 | { 52 | var pcm = (ushort)(VolumeLeft << 4 | VolumeRight); 53 | OutputLeft = (byte)(PcmLeftFull ? pcm : (PcmLeftHalf ? pcm >> 1 : 0)); 54 | OutputRight = (byte)(PcmRightFull ? pcm : (PcmRightHalf ? pcm >> 1 : 0)); 55 | } 56 | else 57 | { 58 | counter--; 59 | if (counter == Pitch) 60 | { 61 | var data = waveTableReadDelegate((ushort)(pointer >> 1)); 62 | if ((pointer & 0b1) == 0b1) data >>= 4; 63 | data &= 0x0F; 64 | 65 | OutputLeft = (byte)(data * VolumeLeft); 66 | OutputRight = (byte)(data * VolumeRight); 67 | 68 | pointer++; 69 | pointer &= 0b11111; 70 | counter = counterReload; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /StoicGoose.Core/Sound/SoundChannel3.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.Sound 2 | { 3 | /* Channel 3, has optional sweep */ 4 | public sealed class SoundChannel3 5 | { 6 | const int counterReload = 2048; 7 | const int sweepReload = 32 * 256; 8 | 9 | ushort counter; 10 | byte pointer; 11 | 12 | public byte OutputLeft { get; set; } 13 | public byte OutputRight { get; set; } 14 | 15 | int sweepCounter; 16 | int sweepCycles; 17 | 18 | readonly WaveTableReadDelegate waveTableReadDelegate; 19 | 20 | /* REG_SND_CH3_PITCH */ 21 | public ushort Pitch { get; set; } 22 | /* REG_SND_CH3_VOL */ 23 | public byte VolumeLeft { get; set; } 24 | public byte VolumeRight { get; set; } 25 | /* REG_SND_CTRL */ 26 | public bool IsEnabled { get; set; } 27 | public bool IsSweepEnabled { get; set; } 28 | 29 | /* REG_SND_SWEEP_VALUE */ 30 | public sbyte SweepValue { get; set; } 31 | /* REG_SND_SWEEP_TIME */ 32 | public byte SweepTime { get; set; } 33 | 34 | public SoundChannel3(WaveTableReadDelegate waveTableRead) => waveTableReadDelegate = waveTableRead; 35 | 36 | public void Reset() 37 | { 38 | counter = counterReload; 39 | pointer = 0; 40 | OutputLeft = OutputRight = 0; 41 | 42 | Pitch = 0; 43 | VolumeLeft = VolumeRight = 0; 44 | IsEnabled = false; 45 | 46 | IsSweepEnabled = false; 47 | 48 | sweepCounter = 0; 49 | sweepCycles = sweepReload; 50 | 51 | SweepValue = 0; 52 | SweepTime = 0; 53 | } 54 | 55 | public void Step() 56 | { 57 | if (IsSweepEnabled) 58 | { 59 | sweepCycles--; 60 | if (sweepCycles == 0) 61 | { 62 | sweepCounter--; 63 | if (sweepCounter <= 0) 64 | { 65 | sweepCounter = SweepTime; 66 | Pitch = (ushort)(Pitch + SweepValue); 67 | } 68 | } 69 | sweepCycles = sweepReload; 70 | } 71 | 72 | counter--; 73 | if (counter == Pitch) 74 | { 75 | var data = waveTableReadDelegate((ushort)(pointer >> 1)); 76 | if ((pointer & 0b1) == 0b1) data >>= 4; 77 | data &= 0x0F; 78 | 79 | OutputLeft = (byte)(data * VolumeLeft); 80 | OutputRight = (byte)(data * VolumeRight); 81 | 82 | pointer++; 83 | pointer &= 0b11111; 84 | counter = counterReload; 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /StoicGoose.Core/Sound/SoundChannel4.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.Sound 2 | { 3 | /* Channel 4, supports noise */ 4 | public sealed class SoundChannel4 5 | { 6 | const int counterReload = 2048; 7 | 8 | readonly static byte[] noiseLfsrTaps = { 14, 10, 13, 4, 8, 6, 9, 11 }; 9 | 10 | ushort counter; 11 | byte pointer; 12 | 13 | public byte OutputLeft { get; set; } 14 | public byte OutputRight { get; set; } 15 | 16 | readonly WaveTableReadDelegate waveTableReadDelegate; 17 | 18 | /* REG_SND_CH4_PITCH */ 19 | public ushort Pitch { get; set; } 20 | /* REG_SND_CH4_VOL */ 21 | public byte VolumeLeft { get; set; } 22 | public byte VolumeRight { get; set; } 23 | /* REG_SND_CTRL */ 24 | public bool IsEnabled { get; set; } 25 | public bool IsNoiseEnabled { get; set; } 26 | 27 | /* REG_SND_NOISE */ 28 | public byte NoiseMode { get; set; } 29 | public bool NoiseReset { get; set; } 30 | public bool NoiseEnable { get; set; } 31 | /* REG_SND_RANDOM */ 32 | public ushort NoiseLfsr { get; set; } 33 | 34 | public SoundChannel4(WaveTableReadDelegate waveTableRead) => waveTableReadDelegate = waveTableRead; 35 | 36 | public void Reset() 37 | { 38 | counter = counterReload; 39 | pointer = 0; 40 | OutputLeft = OutputRight = 0; 41 | 42 | Pitch = 0; 43 | VolumeLeft = VolumeRight = 0; 44 | IsEnabled = false; 45 | 46 | IsNoiseEnabled = false; 47 | 48 | NoiseMode = 0; 49 | NoiseReset = NoiseEnable = false; 50 | NoiseLfsr = 0; 51 | } 52 | 53 | public void Step() 54 | { 55 | counter--; 56 | if (counter == Pitch) 57 | { 58 | if (NoiseEnable) 59 | { 60 | var tap = noiseLfsrTaps[NoiseMode]; 61 | var noise = (1 ^ (NoiseLfsr >> 7) ^ (NoiseLfsr >> tap)) & 0b1; 62 | NoiseLfsr = (ushort)(((NoiseLfsr << 1) | noise) & 0x7FFF); 63 | } 64 | 65 | var data = IsNoiseEnabled ? ((NoiseLfsr & 0b1) * 0x0F) : waveTableReadDelegate((ushort)(pointer >> 1)); 66 | if (!IsNoiseEnabled) 67 | { 68 | if ((pointer & 0b1) == 0b1) data >>= 4; 69 | data &= 0x0F; 70 | } 71 | 72 | OutputLeft = (byte)(data * VolumeLeft); 73 | OutputRight = (byte)(data * VolumeRight); 74 | 75 | if (NoiseReset) 76 | { 77 | NoiseLfsr = 0; 78 | OutputLeft = OutputRight = 0; 79 | NoiseReset = false; 80 | } 81 | 82 | pointer++; 83 | pointer &= 0b11111; 84 | counter = counterReload; 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /StoicGoose.Core/Sound/SoundChannelHyperVoice.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.Core.Sound 2 | { 3 | /* HyperVoice channel */ 4 | public sealed class SoundChannelHyperVoice 5 | { 6 | public byte OutputLeft { get; set; } 7 | public byte OutputRight { get; set; } 8 | 9 | /* REG_HYPER_CTRL */ 10 | public bool IsEnabled { get; set; } 11 | public int ScalingMode { get; set; } 12 | public int Volume { get; set; } 13 | public byte CtrlUnknown { get; set; } 14 | 15 | /* REG_HYPER_CHAN_CTRL */ 16 | public bool RightEnable { get; set; } 17 | public bool LeftEnable { get; set; } 18 | public byte ChanCtrlUnknown { get; set; } 19 | 20 | /* REG_SND_HYPERVOICE */ 21 | public byte Data { get; set; } 22 | 23 | public SoundChannelHyperVoice() { } 24 | 25 | public void Reset() 26 | { 27 | OutputLeft = OutputRight = 0; 28 | 29 | IsEnabled = false; 30 | ScalingMode = Volume = 0; 31 | CtrlUnknown = 0; 32 | 33 | RightEnable = LeftEnable = false; 34 | ChanCtrlUnknown = 0; 35 | 36 | Data = 0; 37 | } 38 | 39 | public void Step() 40 | { 41 | var output = (byte)0; 42 | 43 | switch (ScalingMode) 44 | { 45 | case 0: output = (byte)(Data << 3 - Volume); break; 46 | case 1: output = (byte)((Data << 3 - Volume) | (-0x100 << 3 - Volume)); break; 47 | case 2: output = (byte)(Data << 3 - Volume); break; // ??? 48 | case 3: output = (byte)(Data << 3); break; 49 | } 50 | 51 | OutputLeft = LeftEnable ? output : (byte)0; 52 | OutputRight = RightEnable ? output : (byte)0; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /StoicGoose.Core/StoicGoose.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 2.0.0 5 | xdaniel 6 | 7 | 16-bit handheld game system emulation core 8 | Written 2021-2022 by xdaniel 9 | 10 | 11 | 12 | net6.0 13 | en 14 | disable 15 | disable 16 | true 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Assets/Goose-Logo.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.GLWindow/Assets/Goose-Logo.rgba -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Assets/JF-Dot-K14-2004.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.GLWindow/Assets/JF-Dot-K14-2004.ttf -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Assets/WS-Icon.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.GLWindow/Assets/WS-Icon.rgba -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Configuration.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel; 3 | 4 | using StoicGoose.Common.Localization; 5 | using StoicGoose.Common.Utilities; 6 | using StoicGoose.Core.Machines; 7 | 8 | namespace StoicGoose.GLWindow 9 | { 10 | public sealed class Configuration : ConfigurationBase 11 | { 12 | [DisplayName("UI Language")] 13 | [Description("Preferred user interface language.")] 14 | public string Language { get; set; } = Localizer.FallbackCulture; 15 | 16 | [DisplayName("Preferred System")] 17 | [Description("Preferred system to emulate.")] 18 | public string PreferredSystem { get; set; } = typeof(WonderSwan).FullName; 19 | 20 | [DisplayName("Use Bootstrap ROMs")] 21 | [Description("Toggle using WonderSwan bootstrap ROM images.")] 22 | public bool UseBootstrap { get; set; } = false; 23 | [DisplayName("Bootstrap ROM Paths")] 24 | [Description("Paths to the WonderSwan bootstrap ROM images to use.")] 25 | public Dictionary BootstrapFiles { get; set; } = new(); 26 | 27 | [DisplayName("Last ROM Loaded")] 28 | [Description("Most recently loaded ROM image.")] 29 | public string LastRomLoaded { get; set; } = string.Empty; 30 | 31 | [DisplayName("Enable Patch Callbacks")] 32 | [Description("Enable or disable callbacks functions for patches.")] 33 | public bool EnablePatchCallbacks { get; set; } = false; 34 | 35 | [DisplayName("Enable Breakpoints")] 36 | [Description("Enable or disable breakpoints.")] 37 | public bool EnableBreakpoints { get; set; } = false; 38 | 39 | [DisplayName("Limit FPS")] 40 | [Description("Toggle limiting the framerate to the system's native ~75.47 Hz.")] 41 | public bool LimitFps { get; set; } = true; 42 | 43 | [DisplayName("Display Size")] 44 | [Description("Size of the emulated screen, in times original display resolution.")] 45 | public int DisplaySize { get; set; } = 3; 46 | 47 | [DisplayName("Mute")] 48 | [Description("Toggles muting all sound output.")] 49 | public bool Mute { get; set; } = false; 50 | 51 | [DisplayName("Automatic Remapping")] 52 | [Description("Automatically remap X-/Y-pads with game orientation.")] 53 | public bool AutoRemap { get; set; } = true; 54 | [DisplayName("Game Controls")] 55 | [Description("Controls related to game input, i.e. X-/Y-pads, etc.")] 56 | public Dictionary GameControls { get; set; } = new(); 57 | [DisplayName("System Controls")] 58 | [Description("Controls related to hardware functions, i.e. volume button.")] 59 | public Dictionary SystemControls { get; set; } = new(); 60 | 61 | [DisplayName("Restored Windows")] 62 | [Description("Windows restored on program start.")] 63 | public List WindowsToRestore { get; set; } = new(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Debugging/Breakpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | using Newtonsoft.Json; 4 | 5 | using Microsoft.CodeAnalysis.CSharp.Scripting; 6 | using Microsoft.CodeAnalysis.Scripting; 7 | 8 | namespace StoicGoose.GLWindow.Debugging 9 | { 10 | public class Breakpoint 11 | { 12 | readonly static ScriptOptions scriptOptions = ScriptOptions.Default.AddReferences(Assembly.GetExecutingAssembly()); 13 | 14 | readonly static string codeDummy = $"default({typeof(bool).FullName})"; 15 | 16 | static Script lastScriptState = default; 17 | 18 | public string Expression = string.Empty; 19 | public bool Enabled = true; 20 | [JsonIgnore()] 21 | public ScriptRunner Runner = null; 22 | 23 | static Breakpoint() => lastScriptState = CSharpScript.Create(codeDummy, scriptOptions, typeof(BreakpointVariables)); 24 | 25 | public bool UpdateDelegate() 26 | { 27 | try 28 | { 29 | if (string.IsNullOrEmpty(Expression)) return false; 30 | var newScriptState = lastScriptState.ContinueWith($"return {Expression};", scriptOptions); 31 | Runner = newScriptState.CreateDelegate(); 32 | lastScriptState = newScriptState; 33 | } 34 | catch (CompilationErrorException) 35 | { 36 | return false; 37 | } 38 | catch 39 | { 40 | throw; 41 | } 42 | 43 | return true; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Debugging/BreakpointMemoryArray.cs: -------------------------------------------------------------------------------- 1 | using StoicGoose.Core.Interfaces; 2 | 3 | namespace StoicGoose.GLWindow.Debugging 4 | { 5 | public sealed class BreakpointMemoryArray 6 | { 7 | readonly IMachine machine = default; 8 | 9 | public byte this[uint addr] => machine.ReadMemory(addr); 10 | 11 | public BreakpointMemoryArray(IMachine machine) => this.machine = machine; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Debugging/BreakpointVariables.cs: -------------------------------------------------------------------------------- 1 | using StoicGoose.Core.CPU; 2 | using StoicGoose.Core.Interfaces; 3 | 4 | namespace StoicGoose.GLWindow.Debugging 5 | { 6 | public sealed class BreakpointVariables 7 | { 8 | readonly IMachine machine = default; 9 | 10 | public V30MZ.Register16 ax => machine.Cpu.AX; 11 | public V30MZ.Register16 bx => machine.Cpu.BX; 12 | public V30MZ.Register16 cx => machine.Cpu.CX; 13 | public V30MZ.Register16 dx => machine.Cpu.DX; 14 | public ushort sp => machine.Cpu.SP; 15 | public ushort bp => machine.Cpu.BP; 16 | public ushort si => machine.Cpu.SI; 17 | public ushort di => machine.Cpu.DI; 18 | public ushort cs => machine.Cpu.CS; 19 | public ushort ds => machine.Cpu.DS; 20 | public ushort ss => machine.Cpu.SS; 21 | public ushort es => machine.Cpu.ES; 22 | public ushort ip => machine.Cpu.IP; 23 | 24 | public BreakpointMemoryArray memoryMap { get; private set; } = default; 25 | 26 | public BreakpointVariables(IMachine machine) 27 | { 28 | this.machine = machine; 29 | memoryMap = new(this.machine); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Debugging/MemoryPatch.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.GLWindow.Debugging 2 | { 3 | public class MemoryPatch 4 | { 5 | // ... basically, a cheat, but MemoryPatch sounds more like something belonging in the Debugging namespace :P 6 | 7 | public bool IsEnabled = false; 8 | public string Description = string.Empty; 9 | public uint Address = 0; 10 | public MemoryPatchCondition Condition = MemoryPatchCondition.Always; 11 | public byte CompareValue = 0; 12 | public byte PatchedValue = 0; 13 | } 14 | 15 | public enum MemoryPatchCondition : int { Always = 0, LessThan, LessThanOrEqual, GreaterThanOrEqual, GreaterThan } 16 | } 17 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/GlobalVariables.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | 4 | namespace StoicGoose 5 | { 6 | public static class GlobalVariables 7 | { 8 | public static readonly bool IsAuthorsMachine = System.Environment.MachineName == "KAMIKO"; 9 | #if DEBUG 10 | public static readonly bool IsDebugBuild = true; 11 | #else 12 | public static readonly bool IsDebugBuild = false; 13 | #endif 14 | public static readonly bool EnableEasterEggs = true; 15 | 16 | public static string[] Dump() 17 | { 18 | var vars = new List(); 19 | foreach (var fieldInfo in typeof(GlobalVariables).GetFields(BindingFlags.Static | BindingFlags.Public)) 20 | vars.Add($"{fieldInfo.Name} == {fieldInfo.GetValue(null)}"); 21 | return vars.ToArray(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/InputHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using OpenTK.Windowing.Desktop; 6 | using OpenTK.Windowing.GraphicsLibraryFramework; 7 | 8 | namespace StoicGoose.GLWindow 9 | { 10 | public class InputHandler 11 | { 12 | readonly Dictionary keyboardMapping = new(); 13 | readonly Dictionary verticalRemapping = new(); 14 | readonly List lastPollHeld = new(); 15 | 16 | GameWindow gameWindow = default; 17 | 18 | bool isVerticalOrientation, enableRemapping; 19 | 20 | public void SetGameWindow(GameWindow window) => gameWindow = window; 21 | 22 | public void SetKeyMapping(params Dictionary[] keyConfigs) 23 | { 24 | keyboardMapping.Clear(); 25 | foreach (var keyConfig in keyConfigs) 26 | foreach (var (key, value) in keyConfig.Where(x => !string.IsNullOrEmpty(x.Value))) 27 | keyboardMapping.Add(key, (Keys)Enum.Parse(typeof(Keys), value)); 28 | } 29 | 30 | public void SetVerticalOrientation(bool vertical) 31 | { 32 | isVerticalOrientation = vertical; 33 | } 34 | 35 | public void SetEnableRemapping(bool enable) 36 | { 37 | enableRemapping = enable; 38 | } 39 | 40 | public void SetVerticalRemapping(Dictionary remapDict) 41 | { 42 | verticalRemapping.Clear(); 43 | foreach (var (key, value) in remapDict.Where(x => !string.IsNullOrEmpty(x.Value))) 44 | verticalRemapping.Add(key, value); 45 | } 46 | 47 | public List GetMappedKeyboardInputs() => GetMappedKeyboardInputs((_) => true); 48 | public List GetMappedKeyboardInputs(Func checkCondition) 49 | { 50 | var list = new List(); 51 | 52 | foreach (var (key, value) in keyboardMapping) 53 | { 54 | var keyName = isVerticalOrientation && enableRemapping && verticalRemapping.ContainsKey(key) ? verticalRemapping[key] : key; 55 | { 56 | if (checkCondition(keyName) && gameWindow.IsKeyDown(value)) 57 | list.Add(keyName); 58 | } 59 | } 60 | 61 | return list; 62 | } 63 | 64 | public void PollInput(ref List buttonsPressed, ref List buttonsHeld) 65 | { 66 | buttonsHeld.AddRange(GetMappedKeyboardInputs().Distinct()); 67 | buttonsPressed.AddRange(GetMappedKeyboardInputs((x) => !lastPollHeld.Contains(x)).Distinct()); 68 | 69 | lastPollHeld.Clear(); 70 | lastPollHeld.AddRange(buttonsHeld); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Interface/Helpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Reflection; 5 | 6 | using ImGuiNET; 7 | 8 | using NumericsVector2 = System.Numerics.Vector2; 9 | 10 | namespace StoicGoose.GLWindow.Interface 11 | { 12 | public static class Helpers 13 | { 14 | const string helpMarker = "(?)"; 15 | 16 | readonly static List typeList = new() 17 | { 18 | typeof(byte), 19 | typeof(sbyte), 20 | typeof(ushort), 21 | typeof(short), 22 | typeof(uint), 23 | typeof(int), 24 | typeof(ulong), 25 | typeof(long), 26 | }; 27 | 28 | readonly static Dictionary typeParseMethodDict = new(); 29 | 30 | static Helpers() 31 | { 32 | foreach (var type in typeList) 33 | { 34 | var method = type.GetMethod("Parse", new[] { typeof(string), typeof(NumberStyles) }); 35 | if (method != null) typeParseMethodDict.Add(type, method); 36 | } 37 | } 38 | 39 | public static bool InputHex(string label, ref T value, int digits, bool autoSize = true) where T : unmanaged, IComparable, IEquatable 40 | { 41 | var type = typeof(T); 42 | if (!typeParseMethodDict.ContainsKey(type)) throw new ArgumentException("Invalid type for input", nameof(T)); 43 | 44 | var textFlags = ImGuiInputTextFlags.CharsHexadecimal | ImGuiInputTextFlags.CharsUppercase | ImGuiInputTextFlags.EnterReturnsTrue; 45 | 46 | var stringValue = string.Format($"{{0:X{digits}}}", value); 47 | if (autoSize) ImGui.SetNextItemWidth(ImGui.CalcTextSize(stringValue).X + ImGui.GetStyle().ItemSpacing.X); 48 | 49 | var result = ImGui.InputText(label, ref stringValue, (uint)digits, textFlags); 50 | if (!string.IsNullOrEmpty(stringValue)) value = (T)typeParseMethodDict[type].Invoke(null, new object[] { stringValue, NumberStyles.HexNumber }); 51 | return result; 52 | } 53 | 54 | /* https://github.com/ocornut/imgui/blob/f5c5926fb91764c2ec0e995970818d79b5873d42/imgui_demo.cpp#L191 */ 55 | public static void HelpMarker(string desc) 56 | { 57 | ImGui.TextDisabled(helpMarker); 58 | if (ImGui.IsItemHovered()) 59 | { 60 | ImGui.BeginTooltip(); 61 | ImGui.PushTextWrapPos(ImGui.GetFontSize() * 35f); 62 | ImGui.TextUnformatted(desc); 63 | ImGui.PopTextWrapPos(); 64 | ImGui.EndTooltip(); 65 | } 66 | } 67 | 68 | public static void OpenMessageBox(string title) 69 | { 70 | ImGui.OpenPopup(title); 71 | } 72 | 73 | public static int ProcessMessageBox(string message, string title, params string[] buttons) 74 | { 75 | var buttonIdx = -1; 76 | 77 | var viewportCenter = ImGui.GetMainViewport().GetCenter(); 78 | ImGui.SetNextWindowPos(viewportCenter, ImGuiCond.Always, new NumericsVector2(0.5f, 0.5f)); 79 | 80 | var popupDummy = true; 81 | if (ImGui.BeginPopupModal($"{title}", ref popupDummy, ImGuiWindowFlags.AlwaysAutoResize)) 82 | { 83 | ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new NumericsVector2(5f)); 84 | ImGui.Text(message); 85 | 86 | ImGui.Dummy(new NumericsVector2(0f, 2f)); 87 | ImGui.Separator(); 88 | ImGui.Dummy(new NumericsVector2(0f, 2f)); 89 | 90 | var buttonWidth = (ImGui.GetContentRegionAvail().X - (ImGui.GetStyle().ItemSpacing.X * (buttons.Length - 1))) / buttons.Length; 91 | for (var i = 0; i < buttons.Length; i++) 92 | { 93 | if (ImGui.Button(buttons[i], new NumericsVector2(buttonWidth, 0f))) 94 | { 95 | ImGui.CloseCurrentPopup(); 96 | buttonIdx = i; 97 | break; 98 | } 99 | ImGui.SameLine(); 100 | } 101 | 102 | ImGui.PopStyleVar(); 103 | 104 | ImGui.EndPopup(); 105 | } 106 | 107 | return buttonIdx; 108 | } 109 | 110 | public static bool IsPointInsideRectangle(NumericsVector2 point, NumericsVector2 rectPos, NumericsVector2 rectSize) 111 | { 112 | return point.X >= rectPos.X && point.X < rectPos.X + rectSize.X && point.Y >= rectPos.Y && point.Y < rectPos.Y + rectSize.Y; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Interface/Windows/ComponentPortWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | using ImGuiNET; 7 | 8 | using StoicGoose.Common.Attributes; 9 | using StoicGoose.ImGuiCommon.Windows; 10 | 11 | using NumericsVector2 = System.Numerics.Vector2; 12 | 13 | namespace StoicGoose.GLWindow.Interface.Windows 14 | { 15 | public class ComponentPortWindow : WindowBase 16 | { 17 | const BindingFlags getPropBindingFlags = BindingFlags.Public | BindingFlags.Instance; 18 | 19 | readonly Dictionary numbers, List portInfos)> ports = new(); 20 | 21 | Type componentType = default; 22 | 23 | public ComponentPortWindow(string title) : base(title, new NumericsVector2(500f, 300f), ImGuiCond.FirstUseEver) { } 24 | 25 | public void SetComponentType(Type type) 26 | { 27 | if (componentType != type) 28 | { 29 | componentType = type; 30 | 31 | ports.Clear(); 32 | 33 | foreach (var propInfo in componentType.GetProperties(getPropBindingFlags).Where(x => !x.GetGetMethod().IsAbstract)) 34 | { 35 | if (propInfo.GetCustomAttribute() is PortAttribute regAttrib) 36 | { 37 | if (!ports.ContainsKey(regAttrib.Name)) 38 | ports.Add(regAttrib.Name, (regAttrib.Numbers, new())); 39 | } 40 | } 41 | 42 | foreach (var (name, (numbers, list)) in ports) 43 | { 44 | foreach (var propInfo in componentType.GetProperties(getPropBindingFlags) 45 | .Where(x => x.GetCustomAttribute()?.Numbers.SequenceEqual(numbers) == true && x.GetCustomAttribute()?.Name == name) 46 | .GroupBy(x => x.Name) 47 | .Select(x => x.First())) 48 | { 49 | var descAttrib = propInfo.GetCustomAttribute(); 50 | var formatAttrib = propInfo.GetCustomAttribute(); 51 | 52 | list.Add(new PortParameterInformation() 53 | { 54 | Index = descAttrib?.LowBit ?? 0, 55 | Description = descAttrib != null ? $"{descAttrib.BitString}{descAttrib.Description}" : "", 56 | FormatString = formatAttrib?.Format ?? string.Empty, 57 | BitShift = formatAttrib?.Shift ?? 0, 58 | PropInfo = propInfo 59 | }); 60 | } 61 | } 62 | } 63 | } 64 | 65 | protected override void DrawWindow(object userData) 66 | { 67 | if (userData.GetType() != componentType) return; 68 | 69 | if (ImGui.Begin(WindowTitle, ref isWindowOpen)) 70 | { 71 | foreach (var (name, (numbers, list)) in ports.OrderBy(x => x.Value.numbers.Min())) 72 | { 73 | if (ImGui.CollapsingHeader($"{string.Join(", ", numbers.Select(x => $"0x{x:X3}"))} -- {name}", ImGuiTreeNodeFlags.DefaultOpen)) 74 | { 75 | ImGui.BeginDisabled(true); 76 | { 77 | foreach (var entry in list.OrderBy(x => x.Index)) 78 | { 79 | var val = entry.PropInfo.GetValue(userData, null); 80 | if (val is bool valBool) 81 | ImGui.Checkbox(entry.Description, ref valBool); 82 | else if (!string.IsNullOrEmpty(entry.FormatString)) 83 | ImGui.LabelText(string.Format($"{{0:{entry.FormatString}}}", Convert.ToUInt64(val) << entry.BitShift), entry.Description); 84 | else 85 | ImGui.LabelText($"{val}", entry.Description); 86 | } 87 | 88 | ImGui.EndDisabled(); 89 | } 90 | } 91 | } 92 | 93 | ImGui.End(); 94 | } 95 | } 96 | } 97 | 98 | class PortParameterInformation 99 | { 100 | public int Index { get; set; } = -1; 101 | public string Description { get; set; } = string.Empty; 102 | public string FormatString { get; set; } = string.Empty; 103 | public int BitShift { get; set; } = 0; 104 | public PropertyInfo PropInfo { get; set; } = default; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Interface/Windows/DisplayControllerStatusWindow.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.GLWindow.Interface.Windows 2 | { 3 | public class DisplayControllerStatusWindow : ComponentPortWindow 4 | { 5 | public DisplayControllerStatusWindow() : base("Display Controller") { } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Interface/Windows/DisplayWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using ImGuiNET; 4 | 5 | using StoicGoose.Common.OpenGL; 6 | using StoicGoose.ImGuiCommon.Windows; 7 | 8 | using NumericsVector2 = System.Numerics.Vector2; 9 | 10 | namespace StoicGoose.GLWindow.Interface.Windows 11 | { 12 | public class DisplayWindow : WindowBase 13 | { 14 | public DisplayWindow() : base("Display") { } 15 | 16 | int windowScale = 1; 17 | 18 | public int WindowScale 19 | { 20 | get => windowScale; 21 | set => windowScale = value; 22 | } 23 | 24 | protected override void DrawWindow(object userData) 25 | { 26 | if (userData is not (Texture texture, bool vertical)) return; 27 | 28 | var textureSize = new NumericsVector2( 29 | !vertical ? texture.Size.X : texture.Size.Y, 30 | !vertical ? texture.Size.Y : texture.Size.X) 31 | * windowScale; 32 | 33 | var childBorderSize = new NumericsVector2(ImGui.GetStyle().ChildBorderSize); 34 | 35 | ImGui.SetNextWindowContentSize(textureSize + (childBorderSize * 2f)); 36 | 37 | ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, NumericsVector2.Zero); 38 | if (ImGui.Begin(WindowTitle, ref isWindowOpen)) 39 | { 40 | var drawList = ImGui.GetWindowDrawList(); 41 | var screenPos = ImGui.GetCursorScreenPos(); 42 | 43 | var pos = new NumericsVector2[4]; 44 | var uvs = new NumericsVector2[4]; 45 | 46 | if (vertical) 47 | { 48 | pos[0] = screenPos + new NumericsVector2(textureSize.X, 0f) + childBorderSize; 49 | pos[3] = screenPos + new NumericsVector2(textureSize.X, textureSize.Y) + childBorderSize; 50 | pos[2] = screenPos + new NumericsVector2(0f, textureSize.Y) + childBorderSize; 51 | pos[1] = screenPos + new NumericsVector2(0f, 0f) + childBorderSize; 52 | 53 | uvs[0] = new NumericsVector2(1f, 1f); 54 | uvs[1] = new NumericsVector2(1f, 0f); 55 | uvs[2] = new NumericsVector2(0f, 0f); 56 | uvs[3] = new NumericsVector2(0f, 1f); 57 | } 58 | else 59 | { 60 | pos[0] = screenPos + new NumericsVector2(0f, 0f) + childBorderSize; 61 | pos[1] = screenPos + new NumericsVector2(0f, textureSize.Y) + childBorderSize; 62 | pos[2] = screenPos + new NumericsVector2(textureSize.X, textureSize.Y) + childBorderSize; 63 | pos[3] = screenPos + new NumericsVector2(textureSize.X, 0f) + childBorderSize; 64 | 65 | uvs[0] = new NumericsVector2(0f, 0f); 66 | uvs[1] = new NumericsVector2(0f, 1f); 67 | uvs[2] = new NumericsVector2(1f, 1f); 68 | uvs[3] = new NumericsVector2(1f, 0f); 69 | } 70 | 71 | drawList.AddImageQuad( 72 | new IntPtr(texture.Handle), 73 | pos[0], pos[1], pos[2], pos[3], 74 | uvs[0], uvs[1], uvs[2], uvs[3]); 75 | 76 | if (ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows) && ImGui.IsMouseReleased(ImGuiMouseButton.Right)) 77 | ImGui.OpenPopup("context"); 78 | 79 | ImGui.PopStyleVar(); 80 | 81 | if (ImGui.BeginPopup("context")) 82 | { 83 | ImGui.SliderInt("##size", ref windowScale, 1, 5, "%dx"); 84 | ImGui.EndPopup(); 85 | } 86 | 87 | ImGui.End(); 88 | } 89 | else 90 | ImGui.PopStyleVar(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Interface/Windows/InputSettingsWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using ImGuiNET; 5 | 6 | using StoicGoose.ImGuiCommon.Windows; 7 | 8 | using NumericsVector2 = System.Numerics.Vector2; 9 | using OTKKeys = OpenTK.Windowing.GraphicsLibraryFramework.Keys; 10 | 11 | namespace StoicGoose.GLWindow.Interface.Windows 12 | { 13 | public class InputSettingsWindow : WindowBase 14 | { 15 | public InputSettingsWindow() : base("Input Settings", new(300f, 420f), ImGuiCond.Always) { } 16 | 17 | protected override void DrawWindow(object userData) 18 | { 19 | if (userData is not (Dictionary gameControls, Dictionary systemControls)) return; 20 | 21 | var gameControlChanges = new Dictionary(); 22 | var systemControlChanges = new Dictionary(); 23 | 24 | static void drawControls(Dictionary controls, ref Dictionary controlChanges) 25 | { 26 | foreach (var (input, key) in controls) 27 | { 28 | var popupId = $"Change Key##key-change-{input}"; 29 | 30 | var style = ImGui.GetStyle(); 31 | var cursorPos = ImGui.GetCursorPos(); 32 | 33 | ImGui.SetCursorPos(new(cursorPos.X, cursorPos.Y + style.FramePadding.Y)); 34 | ImGui.Text($"{input}:"); 35 | 36 | ImGui.SetCursorPos(new(cursorPos.X + 100f, cursorPos.Y)); 37 | if (ImGui.Button($"{key}##key-{input}", new NumericsVector2(ImGui.GetContentRegionAvail().X, 0f))) ImGui.OpenPopup(popupId); 38 | 39 | if (ImGui.IsPopupOpen(popupId)) 40 | { 41 | var labelPadding = 20f; 42 | 43 | var viewportCenter = ImGui.GetMainViewport().GetCenter(); 44 | ImGui.SetNextWindowPos(viewportCenter, ImGuiCond.Always, new NumericsVector2(0.5f, 0.5f)); 45 | 46 | var popupDummy = true; 47 | if (ImGui.BeginPopupModal(popupId, ref popupDummy, ImGuiWindowFlags.AlwaysAutoResize)) 48 | { 49 | ImGui.Dummy(new NumericsVector2(0f, labelPadding)); 50 | ImGui.Dummy(new NumericsVector2(labelPadding, 0f)); ImGui.SameLine(); 51 | ImGui.Text($"Please press the new key for '{input}'."); ImGui.SameLine(); 52 | ImGui.Dummy(new NumericsVector2(labelPadding, 0f)); 53 | ImGui.Dummy(new NumericsVector2(0f, labelPadding)); 54 | 55 | ImGui.Dummy(new NumericsVector2(0f, 2f)); 56 | ImGui.Separator(); 57 | ImGui.Dummy(new NumericsVector2(0f, 2f)); 58 | 59 | if (ImGui.Button("Cancel", new NumericsVector2(ImGui.GetContentRegionAvail().X, 0f))) 60 | ImGui.CloseCurrentPopup(); 61 | else 62 | { 63 | var io = ImGui.GetIO(); 64 | for (var i = 0; i < io.KeysData.Count; i++) 65 | { 66 | var keyData = io.KeysData[i]; 67 | if (keyData.Down != 0 && ((OTKKeys)io.KeyMap[i]) != OTKKeys.Unknown) 68 | { 69 | controlChanges.Add(input, ((OTKKeys)io.KeyMap[i]).ToString()); 70 | ImGui.CloseCurrentPopup(); 71 | break; 72 | } 73 | } 74 | } 75 | ImGui.EndPopup(); 76 | } 77 | } 78 | } 79 | } 80 | 81 | if (ImGui.Begin(WindowTitle, ref isWindowOpen, ImGuiWindowFlags.NoResize)) 82 | { 83 | ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new NumericsVector2(5f)); 84 | 85 | var style = ImGui.GetStyle(); 86 | 87 | drawControls(gameControls, ref gameControlChanges); 88 | drawControls(systemControls, ref systemControlChanges); 89 | 90 | foreach (var (input, newKey) in gameControlChanges) gameControls[input] = newKey; 91 | foreach (var (input, newKey) in systemControlChanges) systemControls[input] = newKey; 92 | 93 | ImGui.SetCursorPosY(ImGui.GetContentRegionMax().Y - ((style.FramePadding.Y * 3) + (ImGui.GetTextLineHeight() * 2))); 94 | 95 | ImGui.Dummy(new NumericsVector2(0f, 2f)); 96 | ImGui.Separator(); 97 | ImGui.Dummy(new NumericsVector2(0f, 2f)); 98 | 99 | if (ImGui.BeginChild("##controls-frame", NumericsVector2.Zero)) 100 | { 101 | if (ImGui.Button($"Close##close", new NumericsVector2(ImGui.GetContentRegionAvail().X, 0f))) 102 | isWindowOpen = false; 103 | 104 | ImGui.EndChild(); 105 | } 106 | 107 | ImGui.PopStyleVar(); 108 | 109 | ImGui.End(); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Interface/Windows/SoundControllerStatusWindow.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.GLWindow.Interface.Windows 2 | { 3 | public class SoundControllerStatusWindow : ComponentPortWindow 4 | { 5 | public SoundControllerStatusWindow() : base("Sound Controller") { } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/Interface/Windows/SystemControllerStatusWindow.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.GLWindow.Interface.Windows 2 | { 3 | public class SystemControllerStatusWindow : ComponentPortWindow 4 | { 5 | public SystemControllerStatusWindow() : base("System Controller") { } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/SoundHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | 6 | using OpenTK.Audio.OpenAL; 7 | 8 | using StoicGoose.Common.Utilities; 9 | 10 | namespace StoicGoose.GLWindow 11 | { 12 | public class SoundHandler : IDisposable 13 | { 14 | const int numBuffers = 4, maxQueueLength = 2; 15 | 16 | public bool IsAvailable { get; private set; } = false; 17 | 18 | public int SampleRate { get; private set; } = 0; 19 | public int NumChannels { get; private set; } = 0; 20 | 21 | readonly ALContext context = default; 22 | readonly int source = -1; 23 | readonly int[] buffers = new int[numBuffers]; 24 | 25 | readonly Queue sampleQueue = new(); 26 | short[] lastSamples = new short[512]; 27 | 28 | float volume = 1.0f; 29 | 30 | public SoundHandler(int sampleRate, int numChannels) 31 | { 32 | SampleRate = sampleRate; 33 | NumChannels = numChannels; 34 | 35 | try 36 | { 37 | var device = ALC.OpenDevice(null); 38 | IsAvailable = device.Handle != IntPtr.Zero; 39 | 40 | context = ALC.CreateContext(device, new ALContextAttributes()); 41 | ALC.MakeContextCurrent(context); 42 | 43 | source = AL.GenSource(); 44 | 45 | buffers = AL.GenBuffers(numBuffers); 46 | for (int i = 0; i < buffers.Length; i++) GenerateBuffer(buffers[i]); 47 | AL.SourcePlay(source); 48 | 49 | Log.WriteEvent(LogSeverity.Information, this, "Initialization successful."); 50 | } 51 | catch (DllNotFoundException e) 52 | { 53 | if (e.TargetSite.Module.Assembly == typeof(AL).Assembly) 54 | { 55 | var filename = Regex.Match(e.Message, "'(.*?)'").Groups[1].Value; 56 | var message = !string.IsNullOrEmpty(filename) ? $"Initialization failed; '{filename}' missing." : "Initialization failed; OpenAL implementation missing."; 57 | Log.WriteEvent(LogSeverity.Error, this, message); 58 | 59 | IsAvailable = false; 60 | } 61 | else 62 | throw; 63 | } 64 | } 65 | 66 | ~SoundHandler() 67 | { 68 | Dispose(); 69 | } 70 | 71 | public void Dispose() 72 | { 73 | if (IsAvailable) 74 | foreach (var buffer in buffers.Where(x => AL.IsBuffer(x))) 75 | AL.DeleteBuffer(buffer); 76 | 77 | GC.SuppressFinalize(this); 78 | } 79 | 80 | public void Update() 81 | { 82 | if (!IsAvailable) return; 83 | 84 | AL.GetSource(source, ALGetSourcei.BuffersProcessed, out int buffersProcessed); 85 | while (buffersProcessed-- > 0) 86 | { 87 | int buffer = AL.SourceUnqueueBuffer(source); 88 | if (buffer != 0) 89 | GenerateBuffer(buffer); 90 | } 91 | 92 | AL.GetSource(source, ALGetSourcei.SourceState, out int state); 93 | if ((ALSourceState)state != ALSourceState.Playing) 94 | AL.SourcePlay(source); 95 | } 96 | 97 | public void SetVolume(float value) 98 | { 99 | if (!IsAvailable) return; 100 | 101 | AL.Source(source, ALSourcef.Gain, volume = value); 102 | } 103 | 104 | public void SetMute(bool mute) 105 | { 106 | if (!IsAvailable) return; 107 | 108 | AL.Source(source, ALSourcef.Gain, mute ? 0.0f : volume); 109 | } 110 | 111 | public void EnqueueSamples(short[] samples) 112 | { 113 | if (sampleQueue.Count > maxQueueLength) 114 | { 115 | var samplesToDrop = sampleQueue.Count - maxQueueLength; 116 | for (int i = 0; i < samplesToDrop; i++) 117 | if (sampleQueue.Count != 0) 118 | sampleQueue.Dequeue(); 119 | } 120 | 121 | sampleQueue.Enqueue(samples.ToArray()); 122 | } 123 | 124 | public void ClearSampleBuffer() 125 | { 126 | sampleQueue.Clear(); 127 | 128 | if (lastSamples != null) 129 | { 130 | for (int i = 0; i < lastSamples.Length; i++) 131 | lastSamples[i] = 0; 132 | } 133 | } 134 | 135 | private void GenerateBuffer(int buffer) 136 | { 137 | if (sampleQueue.Count > 0) 138 | lastSamples = sampleQueue.Dequeue(); 139 | 140 | if (!IsAvailable) return; 141 | 142 | if (lastSamples != null) 143 | { 144 | AL.BufferData(buffer, ALFormat.Stereo16, lastSamples, SampleRate); 145 | AL.SourceQueueBuffer(source, buffer); 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/StoicGoose.GLWindow.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 2.0.0 5 | xdaniel 6 | 7 | 16-bit handheld game system emulator 8 | Written 2021-2022 by xdaniel 9 | 10 | 11 | 12 | net6.0 13 | en 14 | disable 15 | disable 16 | true 17 | WinExe 18 | WS-Icon.ico 19 | StoicGoose.GLWindow.Program 20 | true 21 | en 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /StoicGoose.GLWindow/WS-Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.GLWindow/WS-Icon.ico -------------------------------------------------------------------------------- /StoicGoose.ImGuiCommon/Handlers/MenuHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | using ImGuiNET; 7 | 8 | namespace StoicGoose.ImGuiCommon.Handlers 9 | { 10 | public class MenuHandler 11 | { 12 | readonly List menuItems = new(); 13 | 14 | public MenuHandler(params MenuItem[] menuItems) 15 | { 16 | this.menuItems.AddRange(menuItems); 17 | } 18 | 19 | public void AddMenu(MenuItem menuItem) 20 | { 21 | menuItems.Add(menuItem); 22 | } 23 | 24 | public void Draw() 25 | { 26 | if (ImGui.BeginMainMenuBar()) 27 | { 28 | foreach (var menuItem in menuItems) 29 | DrawMenu(menuItem); 30 | 31 | ImGui.EndMainMenuBar(); 32 | } 33 | } 34 | 35 | private void DrawMenu(MenuItem menuItem) 36 | { 37 | if (menuItem.Label == "-") 38 | ImGui.Separator(); 39 | else 40 | { 41 | if (menuItem.ClickAction == null || menuItem.SubItems.Length > 0) 42 | DrawSubMenus(menuItem); 43 | else 44 | { 45 | menuItem.UpdateAction?.Invoke(menuItem); 46 | if (ImGui.MenuItem(menuItem.Label, null, menuItem.IsChecked, menuItem.IsEnabled) && menuItem.ClickAction != null) 47 | menuItem.ClickAction(menuItem); 48 | } 49 | } 50 | } 51 | 52 | private void DrawSubMenus(MenuItem menuItem) 53 | { 54 | if (ImGui.BeginMenu(menuItem.Label ?? nameof(MenuItem))) 55 | { 56 | foreach (var subItem in menuItem.SubItems) 57 | DrawMenu(subItem); 58 | 59 | ImGui.EndMenu(); 60 | } 61 | } 62 | } 63 | 64 | public class MenuItem 65 | { 66 | public string Label { get; set; } = string.Empty; 67 | public string Localization { get; set; } = string.Empty; 68 | public Action ClickAction { get; set; } = default; 69 | public Action UpdateAction { get; set; } = default; 70 | public MenuItem[] SubItems { get; set; } = Array.Empty(); 71 | public bool IsEnabled { get; set; } = true; 72 | public bool IsChecked { get; set; } = false; 73 | 74 | public MenuItem(string label = "", string localization = "", Action clickAction = null, Action updateAction = null) 75 | { 76 | Label = label; 77 | Localization = localization; 78 | ClickAction = clickAction; 79 | UpdateAction = updateAction; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /StoicGoose.ImGuiCommon/Handlers/MessageBoxHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using ImGuiNET; 6 | 7 | using NumericsVector2 = System.Numerics.Vector2; 8 | 9 | namespace StoicGoose.ImGuiCommon.Handlers 10 | { 11 | public class MessageBoxHandler 12 | { 13 | readonly List messageBoxes = new(); 14 | 15 | public bool IsAnyMessageBoxOpen => messageBoxes.Any(x => x.IsOpen); 16 | 17 | public MessageBoxHandler(params MessageBox[] messageBoxes) 18 | { 19 | this.messageBoxes.AddRange(messageBoxes); 20 | } 21 | 22 | public void AddMessageBox(MessageBox messageBox) 23 | { 24 | messageBoxes.Add(messageBox); 25 | } 26 | 27 | public void Draw() 28 | { 29 | foreach (var messageBox in messageBoxes.Where(x => x.IsOpen)) 30 | ImGui.OpenPopup(messageBox.Title); 31 | 32 | for (var i = 0; i < messageBoxes.Count; i++) 33 | { 34 | if (!messageBoxes[i].IsOpen) continue; 35 | 36 | var viewportCenter = ImGui.GetMainViewport().GetCenter(); 37 | ImGui.SetNextWindowPos(viewportCenter, ImGuiCond.Always, new NumericsVector2(0.5f, 0.5f)); 38 | 39 | if (ImGui.BeginPopupModal(messageBoxes[i].Title, ref messageBoxes[i].IsOpen, ImGuiWindowFlags.AlwaysAutoResize)) 40 | { 41 | ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new NumericsVector2(5f)); 42 | ImGui.Text(messageBoxes[i].Message); 43 | 44 | ImGui.Dummy(new NumericsVector2(0f, 2f)); 45 | ImGui.Separator(); 46 | ImGui.Dummy(new NumericsVector2(0f, 2f)); 47 | 48 | var buttonWidth = (ImGui.GetContentRegionAvail().X - (ImGui.GetStyle().ItemSpacing.X * (messageBoxes[i].Buttons.Length - 1))) / messageBoxes[i].Buttons.Length; 49 | for (var j = 0; j < messageBoxes[i].Buttons.Length; j++) 50 | { 51 | if (ImGui.Button(messageBoxes[i].Buttons[j], new NumericsVector2(buttonWidth, 0f))) 52 | { 53 | ImGui.CloseCurrentPopup(); 54 | messageBoxes[i].ReturnValue = j; 55 | messageBoxes[i].IsOpen = false; 56 | break; 57 | } 58 | ImGui.SameLine(); 59 | } 60 | 61 | ImGui.PopStyleVar(); 62 | 63 | ImGui.EndPopup(); 64 | } 65 | } 66 | } 67 | } 68 | 69 | public class MessageBox 70 | { 71 | public string Title { get; set; } = string.Empty; 72 | public string Message { get; set; } = string.Empty; 73 | public string[] Buttons { get; set; } = Array.Empty(); 74 | public int ReturnValue { get; set; } = -1; 75 | 76 | public bool IsOpen = false; 77 | 78 | public MessageBox(string title, string message, params string[] buttons) 79 | { 80 | Title = title; 81 | Message = message; 82 | Buttons = buttons; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /StoicGoose.ImGuiCommon/Handlers/StatusBarHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using ImGuiNET; 4 | 5 | namespace StoicGoose.ImGuiCommon.Handlers 6 | { 7 | public class StatusBarHandler 8 | { 9 | public static void Draw(params StatusBarItem[] items) 10 | { 11 | var viewport = ImGui.GetMainViewport(); 12 | var frameHeight = ImGui.GetFrameHeight(); 13 | 14 | ImGui.SetNextWindowPos(new(viewport.Pos.X, viewport.Pos.Y + viewport.Size.Y - frameHeight)); 15 | ImGui.SetNextWindowSize(new(viewport.Size.X, frameHeight)); 16 | 17 | var flags = ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollWithMouse | 18 | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.MenuBar; 19 | 20 | var framePadding = ImGui.GetStyle().FramePadding.X; 21 | var itemPadding = framePadding * 4f; 22 | 23 | if (ImGui.Begin("##statusbar", flags)) 24 | { 25 | if (ImGui.BeginMenuBar()) 26 | { 27 | var drawList = ImGui.GetWindowDrawList(); 28 | var windowPos = ImGui.GetWindowPos(); 29 | 30 | var cursorFromLeft = framePadding * 2f; 31 | var cursorFromRight = ImGui.GetWindowWidth() - framePadding * 2f; 32 | 33 | foreach (var item in items) 34 | { 35 | var labelWidth = ImGui.CalcTextSize(item.Label).X; 36 | var itemWidth = Math.Max(labelWidth, item.Width); 37 | 38 | if (item.ItemAlignment == StatusBarItemAlign.Left) 39 | { 40 | ImGui.SetCursorPosX(cursorFromLeft); 41 | cursorFromLeft += itemWidth + itemPadding; 42 | if (item.ShowSeparator) 43 | drawList.AddLine( 44 | new(windowPos.X + cursorFromLeft - itemPadding / 2f, windowPos.Y), 45 | new(windowPos.X + cursorFromLeft - itemPadding / 2f, windowPos.Y + frameHeight), 46 | ImGui.GetColorU32(ImGuiCol.TextDisabled)); 47 | } 48 | else 49 | { 50 | ImGui.SetCursorPosX(cursorFromRight - itemWidth); 51 | cursorFromRight -= itemWidth + itemPadding; 52 | if (item.ShowSeparator) 53 | drawList.AddLine( 54 | new(windowPos.X + cursorFromRight + itemPadding / 2f, windowPos.Y), 55 | new(windowPos.X + cursorFromRight + itemPadding / 2f, windowPos.Y + frameHeight), 56 | ImGui.GetColorU32(ImGuiCol.TextDisabled)); 57 | } 58 | 59 | if (item.TextAlignment != StatusBarItemTextAlign.Left) 60 | { 61 | var cursorPos = ImGui.GetCursorPosX(); 62 | 63 | if (item.TextAlignment == StatusBarItemTextAlign.Right) 64 | ImGui.SetCursorPosX(cursorPos + itemWidth - labelWidth); 65 | else if (item.TextAlignment == StatusBarItemTextAlign.Center) 66 | ImGui.SetCursorPosX(cursorPos + (itemWidth / 2f - labelWidth / 2f)); 67 | 68 | if (item.IsEnabled) ImGui.Text(item.Label); 69 | else ImGui.TextDisabled(item.Label); 70 | 71 | ImGui.SetCursorPosX(cursorPos); 72 | } 73 | else 74 | { 75 | if (item.IsEnabled) ImGui.Text(item.Label); 76 | else ImGui.TextDisabled(item.Label); 77 | } 78 | } 79 | 80 | ImGui.EndMenuBar(); 81 | } 82 | } 83 | } 84 | } 85 | 86 | public enum StatusBarItemAlign { Left, Right } 87 | public enum StatusBarItemTextAlign { Left, Right, Center } 88 | 89 | public class StatusBarItem 90 | { 91 | public string Label { get; set; } = string.Empty; 92 | public float Width { get; set; } = 0f; 93 | public StatusBarItemAlign ItemAlignment { get; set; } = StatusBarItemAlign.Left; 94 | public StatusBarItemTextAlign TextAlignment { get; set; } = StatusBarItemTextAlign.Left; 95 | public bool ShowSeparator { get; set; } = true; 96 | public bool IsEnabled { get; set; } = true; 97 | 98 | public StatusBarItem(string label = "") => Label = label; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /StoicGoose.ImGuiCommon/StoicGoose.ImGuiCommon.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.0.0 5 | xdaniel 6 | 7 | 16-bit handheld game system emulator ImGui interface code 8 | Written 2021-2022 by xdaniel 9 | 10 | 11 | 12 | net6.0 13 | en 14 | disable 15 | disable 16 | true 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /StoicGoose.ImGuiCommon/Widgets/BackgroundLogo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using OpenTK.Graphics.OpenGL4; 4 | 5 | using ImGuiNET; 6 | 7 | using StoicGoose.Common.Drawing; 8 | using StoicGoose.Common.OpenGL; 9 | 10 | using NumericsVector2 = System.Numerics.Vector2; 11 | 12 | namespace StoicGoose.ImGuiCommon.Widgets 13 | { 14 | public class BackgroundLogo 15 | { 16 | public Texture Texture { get; set; } = default; 17 | public NumericsVector2 Size => new((float)Texture?.Size.X, (float)Texture?.Size.Y); 18 | 19 | public BackgroundLogoPositioning Positioning { get; set; } = BackgroundLogoPositioning.Center; 20 | public NumericsVector2 Offset { get; set; } = NumericsVector2.Zero; 21 | public NumericsVector2 Scale { get; set; } = NumericsVector2.One; 22 | public byte Alpha { get; set; } = 255; 23 | 24 | readonly bool debug = false; 25 | 26 | public void SetImage(RgbaFile background) 27 | { 28 | Texture = new(background); 29 | Texture.SetTextureFilter(TextureMinFilter.Nearest, TextureMagFilter.Nearest); 30 | Texture.SetTextureWrapMode(TextureWrapMode.Repeat, TextureWrapMode.Repeat); 31 | } 32 | 33 | public void Draw() 34 | { 35 | if (Texture == default) return; 36 | 37 | var scaledSize = Size * Scale; 38 | var position = Positioning switch 39 | { 40 | BackgroundLogoPositioning.Center => ImGui.GetMainViewport().Size / 2f - scaledSize / 2f + Offset, 41 | BackgroundLogoPositioning.TopLeft => Offset, 42 | BackgroundLogoPositioning.TopRight => new(ImGui.GetMainViewport().Size.X - scaledSize.X + Offset.X, Offset.Y), 43 | BackgroundLogoPositioning.BottomLeft => new(Offset.X, ImGui.GetMainViewport().Size.Y - scaledSize.Y + Offset.Y), 44 | BackgroundLogoPositioning.BottomRight => ImGui.GetMainViewport().Size - scaledSize + Offset, 45 | _ => throw new Exception("Invalid positioning mode"), 46 | }; 47 | 48 | var backgroundDrawList = ImGui.GetBackgroundDrawList(); 49 | backgroundDrawList.AddImage(new IntPtr(Texture.Handle), position, position + scaledSize, NumericsVector2.Zero, NumericsVector2.One, (uint)(Alpha << 24)); 50 | 51 | if (debug) backgroundDrawList.AddRect(position, position + scaledSize, 0xFF0000FF); 52 | } 53 | } 54 | 55 | public enum BackgroundLogoPositioning { Center, TopLeft, TopRight, BottomLeft, BottomRight } 56 | } 57 | -------------------------------------------------------------------------------- /StoicGoose.ImGuiCommon/Windows/WindowBase.cs: -------------------------------------------------------------------------------- 1 | using ImGuiNET; 2 | 3 | using NumericsVector2 = System.Numerics.Vector2; 4 | 5 | namespace StoicGoose.ImGuiCommon.Windows 6 | { 7 | public abstract class WindowBase 8 | { 9 | protected bool isWindowOpen = false; 10 | protected bool isFirstOpen = true; 11 | 12 | public bool IsWindowOpen { get => isWindowOpen; set => isWindowOpen = value; } 13 | 14 | public string WindowTitle { get; } = string.Empty; 15 | public NumericsVector2 InitialWindowSize { get; } = NumericsVector2.Zero; 16 | public ImGuiCond SizingCondition { get; } = ImGuiCond.None; 17 | 18 | public bool IsFocused { get; private set; } = default; 19 | 20 | public WindowBase(string title) 21 | { 22 | WindowTitle = title; 23 | } 24 | 25 | public WindowBase(string title, NumericsVector2 size, ImGuiCond condition) 26 | { 27 | WindowTitle = title; 28 | InitialWindowSize = size; 29 | SizingCondition = condition; 30 | } 31 | 32 | public virtual void Draw(object userData) 33 | { 34 | if (!isWindowOpen) return; 35 | 36 | if (isFirstOpen) 37 | { 38 | InitializeWindow(userData); 39 | isFirstOpen = false; 40 | } 41 | 42 | ImGui.SetNextWindowSize(InitialWindowSize, SizingCondition); 43 | 44 | DrawWindow(userData); 45 | 46 | if (ImGui.Begin(WindowTitle)) 47 | { 48 | IsFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows); 49 | ImGui.End(); 50 | } 51 | } 52 | 53 | protected virtual void InitializeWindow(object userData) { } 54 | 55 | protected abstract void DrawWindow(object userData); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/Aux1.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/Aux1.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/Aux2.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/Aux2.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/Aux3.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/Aux3.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/Headphones.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/Headphones.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/Horizontal.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/Horizontal.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/Initialized.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/Initialized.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/LowBattery.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/LowBattery.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/Power.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/Power.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/Sleep.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/Sleep.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/Vertical.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/Vertical.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/VolumeA0.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/VolumeA0.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/VolumeA1.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/VolumeA1.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/VolumeA2.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/VolumeA2.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/VolumeB0.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/VolumeB0.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/VolumeB1.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/VolumeB1.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/VolumeB2.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/VolumeB2.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Icons/VolumeB3.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/Assets/Icons/VolumeB3.rgba -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Shaders/Basic/Fragment.glsl: -------------------------------------------------------------------------------- 1 | vec4 renderDisplay() 2 | { 3 | return texture(textureSamplers[0], texCoord); 4 | } 5 | 6 | vec4 renderIcons() 7 | { 8 | return texture(textureSamplers[0], texCoord); 9 | } 10 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Shaders/Basic/Manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "Filter": "Nearest", 3 | "Wrap": "Border", 4 | "Samplers": "1" 5 | } 6 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Shaders/Dot-Matrix Color/Fragment.glsl: -------------------------------------------------------------------------------- 1 | vec3 accumulateTextures(); 2 | vec3 grid(vec3 color); 3 | vec3 gamma(vec3 color); 4 | 5 | // Main function for display 6 | vec4 renderDisplay() 7 | { 8 | vec4 outputColor = vec4(accumulateTextures(), 1.0); 9 | 10 | outputColor.rgb = grid(outputColor.rgb); 11 | outputColor.rgb = gamma(outputColor.rgb); 12 | 13 | return outputColor; 14 | } 15 | 16 | // Main function for system icons 17 | vec4 renderIcons() 18 | { 19 | vec4 outputColor = texture(textureSamplers[0], texCoord); 20 | 21 | outputColor.rgb = gamma(outputColor.rgb); 22 | 23 | return outputColor; 24 | } 25 | 26 | // Accumulate pixels from all texture samplers 27 | vec3 accumulateTextures() 28 | { 29 | vec3[numSamplers] outputColors; 30 | vec3 outputColor = vec3(0); 31 | 32 | for (int i = 0; i < numSamplers; i++) outputColors[i] = texture(textureSamplers[i], texCoord).rgb; 33 | for (int i = 0; i < numSamplers; i++) outputColor += outputColors[i] * (1.0 / float(numSamplers)); 34 | 35 | return outputColor; 36 | } 37 | 38 | // Apply basic grid pattern (horizontal, then vertical) 39 | vec3 grid(vec3 color) 40 | { 41 | vec3 outputColor = vec3(0); 42 | vec2 gridStep = vec2(outputViewport.z / inputViewport.z, outputViewport.w / inputViewport.w); 43 | 44 | if (gridStep.x > 1.0 && gridStep.y > 1.0) 45 | { 46 | outputColor = vec3(clamp(mod(gl_FragCoord.x - outputViewport.x, gridStep.x), 0.8, 1.0) * clamp(mod(gl_FragCoord.y - outputViewport.y, gridStep.y), 0.8, 1.0) * color); 47 | } 48 | 49 | return outputColor; 50 | } 51 | 52 | // Apply gamma correction 53 | vec3 gamma(vec3 color) 54 | { 55 | float gamma = 1.2; 56 | return pow(color, vec3(1.0 / gamma)); 57 | } 58 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Shaders/Dot-Matrix Color/Manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "Filter": "Nearest", 3 | "Wrap": "Border", 4 | "Samplers": "4" 5 | } 6 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Shaders/Dot-Matrix/Fragment.glsl: -------------------------------------------------------------------------------- 1 | vec3 accumulateTextures(); 2 | vec3 grid(vec3 color); 3 | vec3 gamma(vec3 color); 4 | vec3 reduceAndTint(vec3 color); 5 | 6 | // Main function for display 7 | vec4 renderDisplay() 8 | { 9 | vec4 outputColor = vec4(accumulateTextures(), 1.0); 10 | 11 | outputColor.rgb = grid(outputColor.rgb); 12 | outputColor.rgb = gamma(outputColor.rgb); 13 | outputColor.rgb = reduceAndTint(outputColor.rgb); 14 | 15 | return outputColor; 16 | } 17 | 18 | // Main function for system icons 19 | vec4 renderIcons() 20 | { 21 | vec4 outputColor = texture(textureSamplers[0], texCoord); 22 | 23 | outputColor.rgb = gamma(outputColor.rgb); 24 | outputColor.rgb = reduceAndTint(outputColor.rgb); 25 | 26 | return outputColor; 27 | } 28 | 29 | // Accumulate pixels from all texture samplers 30 | vec3 accumulateTextures() 31 | { 32 | vec3[numSamplers] outputColors; 33 | vec3 outputColor = vec3(0); 34 | 35 | for (int i = 0; i < numSamplers; i++) outputColors[i] = texture(textureSamplers[i], texCoord).rgb; 36 | for (int i = 0; i < numSamplers; i++) outputColor += outputColors[i] * (1.0 / float(numSamplers)); 37 | 38 | return outputColor; 39 | } 40 | 41 | // Apply basic grid pattern (horizontal, then vertical) 42 | vec3 grid(vec3 color) 43 | { 44 | vec3 outputColor = vec3(0); 45 | vec2 gridStep = vec2(outputViewport.z / inputViewport.z, outputViewport.w / inputViewport.w); 46 | 47 | if (gridStep.x > 1.0 && gridStep.y > 1.0) 48 | { 49 | outputColor = vec3(clamp(mod(gl_FragCoord.x - outputViewport.x, gridStep.x), 0.8, 1.0) * clamp(mod(gl_FragCoord.y - outputViewport.y, gridStep.y), 0.8, 1.0) * color); 50 | } 51 | 52 | return outputColor; 53 | } 54 | 55 | // Apply gamma correction 56 | vec3 gamma(vec3 color) 57 | { 58 | float gamma = 1.2; 59 | return pow(color, vec3(1.0 / gamma)); 60 | } 61 | 62 | // Reduce to grayscale & tint 63 | vec3 reduceAndTint(vec3 color) 64 | { 65 | return vec3(dot(color, vec3(0.299, 0.587, 0.114))) * vec3(0.875, 0.921, 0.886); 66 | } 67 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Shaders/Dot-Matrix/Manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "Filter": "Nearest", 3 | "Wrap": "Border", 4 | "Samplers": "4" 5 | } 6 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Shaders/FragmentBase.glsl: -------------------------------------------------------------------------------- 1 | const int renderModeDisplay = 0; 2 | const int renderModeIcons = 1; 3 | 4 | in vec2 texCoord; 5 | 6 | out vec4 fragColor; 7 | 8 | uniform int renderMode = 0; 9 | uniform int invertIcons = 0; 10 | 11 | uniform sampler2D textureSamplers[8]; 12 | 13 | uniform float displayBrightness; 14 | uniform float displayContrast; 15 | uniform float displaySaturation; 16 | 17 | uniform vec4 outputViewport; 18 | uniform vec4 inputViewport; 19 | 20 | vec4 renderDisplay(); 21 | vec4 renderIcons(); 22 | 23 | vec4 applyAdjustments(vec4 color); 24 | 25 | void main() 26 | { 27 | vec4 outputColor = vec4(0.0); 28 | 29 | switch (renderMode) 30 | { 31 | case renderModeDisplay: 32 | outputColor = renderDisplay(); 33 | break; 34 | 35 | case renderModeIcons: 36 | outputColor = renderIcons(); 37 | if (invertIcons != 0) outputColor = vec4(1.0 - outputColor.r, 1.0 - outputColor.g, 1.0 - outputColor.b, outputColor.a); 38 | break; 39 | } 40 | 41 | fragColor = applyAdjustments(outputColor); 42 | } 43 | 44 | // https://www.shadertoy.com/view/XdcXzn 45 | mat4 getBrightnessMatrix(float brightness) 46 | { 47 | return mat4( 48 | 1, 0, 0, 0, 49 | 0, 1, 0, 0, 50 | 0, 0, 1, 0, 51 | brightness, brightness, brightness, 1); 52 | } 53 | 54 | mat4 getContrastMatrix(float contrast) 55 | { 56 | float t = (1.0 - contrast) / 2.0; 57 | 58 | return mat4( 59 | contrast, 0, 0, 0, 60 | 0, contrast, 0, 0, 61 | 0, 0, contrast, 0, 62 | t, t, t, 1); 63 | } 64 | 65 | mat4 getSaturationMatrix(float saturation) 66 | { 67 | vec3 luminance = vec3(0.3086, 0.6094, 0.0820); 68 | 69 | float oneMinusSat = 1.0 - saturation; 70 | 71 | vec3 red = vec3(luminance.r * oneMinusSat); 72 | red += vec3(saturation, 0, 0); 73 | 74 | vec3 green = vec3(luminance.g * oneMinusSat); 75 | green += vec3(0, saturation, 0); 76 | 77 | vec3 blue = vec3(luminance.b * oneMinusSat); 78 | blue += vec3(0, 0, saturation); 79 | 80 | return mat4( 81 | red, 0, 82 | green, 0, 83 | blue, 0, 84 | 0, 0, 0, 1); 85 | } 86 | 87 | vec4 applyAdjustments(vec4 color) 88 | { 89 | return 90 | getBrightnessMatrix(displayBrightness) * 91 | getContrastMatrix(displayContrast) * 92 | getSaturationMatrix(displaySaturation) * 93 | color; 94 | } 95 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Assets/Shaders/Vertex.glsl: -------------------------------------------------------------------------------- 1 | layout (location = 0) in vec2 inPosition; 2 | layout (location = 1) in vec2 inTexCoord; 3 | 4 | out vec2 texCoord; 5 | 6 | uniform mat4 projectionMatrix; 7 | uniform mat4 modelviewMatrix; 8 | uniform mat4 textureMatrix; 9 | 10 | void main() 11 | { 12 | texCoord = (textureMatrix * vec4(inTexCoord, 0.0, 1.0)).xy; 13 | gl_Position = projectionMatrix * modelviewMatrix * vec4(inPosition, 0.0, 1.0); 14 | } 15 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Cheat.cs: -------------------------------------------------------------------------------- 1 | namespace StoicGoose.WinForms 2 | { 3 | public class Cheat 4 | { 5 | public bool IsEnabled { get; set; } = false; 6 | public string Description { get; set; } = string.Empty; 7 | public uint Address { get; set; } = 0; 8 | public CheatCondition Condition { get; set; } = CheatCondition.Always; 9 | public byte CompareValue { get; set; } = 0; 10 | public byte PatchedValue { get; set; } = 0; 11 | } 12 | 13 | public enum CheatCondition : int { Always = 0, LessThan, LessThanOrEqual, GreaterThanOrEqual, GreaterThan } 14 | } 15 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/CheatEditForm.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/CheatsForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Linq; 4 | using System.Windows.Forms; 5 | 6 | namespace StoicGoose.WinForms 7 | { 8 | public partial class CheatsForm : Form 9 | { 10 | readonly CheatEditForm cheatEditForm = new(); 11 | 12 | public Action Callback { get; set; } = default; 13 | 14 | public CheatsForm() 15 | { 16 | InitializeComponent(); 17 | 18 | UpdateControls(); 19 | } 20 | 21 | private void CheatsForm_FormClosing(object sender, FormClosingEventArgs e) 22 | { 23 | Callback?.Invoke(clvCheats.Items.Cast().Select(x => (Cheat)x.Tag).ToArray()); 24 | } 25 | 26 | private void btnClose_Click(object sender, EventArgs e) 27 | { 28 | Hide(); 29 | 30 | Callback?.Invoke(clvCheats.Items.Cast().Select(x => (Cheat)x.Tag).ToArray()); 31 | } 32 | 33 | private void clvCheats_SelectedIndexChanged(object sender, EventArgs e) 34 | { 35 | UpdateControls(); 36 | } 37 | 38 | private void btnAdd_Click(object sender, EventArgs e) 39 | { 40 | cheatEditForm.SetFormAddMode(true); 41 | cheatEditForm.SetCheat(new()); 42 | 43 | if (cheatEditForm.ShowDialog() == DialogResult.OK) 44 | clvCheats.Add(cheatEditForm.Cheat); 45 | 46 | UpdateControls(); 47 | } 48 | 49 | private void btnEdit_Click(object sender, EventArgs e) 50 | { 51 | var selectedCheat = clvCheats.GetSelectedItems().First(); 52 | 53 | cheatEditForm.SetFormAddMode(false); 54 | cheatEditForm.SetCheat(selectedCheat); 55 | 56 | if (cheatEditForm.ShowDialog() == DialogResult.OK) 57 | clvCheats.Replace(selectedCheat, cheatEditForm.Cheat); 58 | 59 | UpdateControls(); 60 | } 61 | 62 | private void btnDelete_Click(object sender, EventArgs e) 63 | { 64 | if (MessageBox.Show("Do you really want to delete this cheat?", "Delete Cheat", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) 65 | { 66 | var selected = clvCheats.GetSelectedItems().First(); 67 | clvCheats.Remove(selected); 68 | } 69 | 70 | UpdateControls(); 71 | } 72 | 73 | public void SetCheatList(Cheat[] cheats) 74 | { 75 | clvCheats.RemoveAll(); 76 | clvCheats.AddRange(cheats); 77 | 78 | UpdateControls(); 79 | } 80 | 81 | private void UpdateControls() 82 | { 83 | btnDelete.Enabled = clvCheats.Items.Count != 0 && clvCheats.SelectedItems.Count != 0; 84 | btnEdit.Enabled = clvCheats.SelectedItems.Count != 0; 85 | btnAdd.Enabled = true; 86 | } 87 | 88 | private void clvCheats_DoubleClick(object sender, EventArgs e) 89 | { 90 | if (clvCheats.SelectedIndices.Count != 0) 91 | btnEdit_Click(btnEdit, EventArgs.Empty); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Configuration.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Collections.Generic; 4 | 5 | using StoicGoose.Common.Utilities; 6 | 7 | namespace StoicGoose.WinForms 8 | { 9 | public sealed class Configuration : ConfigurationBase 10 | { 11 | [DisplayName("General")] 12 | [Description("General settings.")] 13 | public GeneralConfiguration General { get; set; } = new GeneralConfiguration(); 14 | [DisplayName("Video")] 15 | [Description("Settings related to video output.")] 16 | public VideoConfiguration Video { get; set; } = new VideoConfiguration(); 17 | [DisplayName("Sound")] 18 | [Description("Settings related to sound output.")] 19 | public SoundConfiguration Sound { get; set; } = new SoundConfiguration(); 20 | [DisplayName("Input")] 21 | [Description("Settings related to emulation input.")] 22 | public InputConfiguration Input { get; set; } = new InputConfiguration(); 23 | } 24 | 25 | public sealed class GeneralConfiguration : ConfigurationBase 26 | { 27 | [DisplayName("Prefer Original WS")] 28 | [Description("Prefer emulation of the original non-Color system.")] 29 | public bool PreferOriginalWS { get; set; } = false; 30 | [DisplayName("Use Bootstrap ROM")] 31 | [Description("Toggle using WonderSwan bootstrap ROM images.")] 32 | public bool UseBootstrap { get; set; } = false; 33 | [DisplayName("WS Bootstrap ROM Path")] 34 | [Description("Path to the WonderSwan bootstrap ROM image to use.")] 35 | public string BootstrapFile { get; set; } = string.Empty; 36 | [DisplayName("WSC Bootstrap ROM Path")] 37 | [Description("Path to the WonderSwan Color bootstrap ROM image to use.")] 38 | public string BootstrapFileWSC { get; set; } = string.Empty; 39 | [DisplayName("Limit FPS")] 40 | [Description("Toggle limiting the framerate to the system's native ~75.47 Hz.")] 41 | public bool LimitFps { get; set; } = true; 42 | [DisplayName("Enable Cheats")] 43 | [Description("Toggle using the cheat system.")] 44 | public bool EnableCheats { get; set; } = true; 45 | [DisplayName("Recent Files")] 46 | [Description("List of recently loaded files.")] 47 | public List RecentFiles { get; set; } = new List(15); 48 | } 49 | 50 | public sealed class VideoConfiguration : ConfigurationBase 51 | { 52 | [DisplayName("Screen Size")] 53 | [Description("Size of the emulated screen, in times original display resolution.")] 54 | public int ScreenSize { get; set; } = 3; 55 | [DisplayName("Shader")] 56 | [Description("Currently selected shader.")] 57 | public string Shader { get; set; } = string.Empty; 58 | [DisplayName("Brightness")] 59 | [Description("Adjust the brightness of the emulated screen, in percent.")] 60 | [Range(-100, 100)] 61 | public int Brightness { get; set; } = 0; 62 | [DisplayName("Contrast")] 63 | [Description("Adjust the contrast of the emulated screen, in percent.")] 64 | [Range(0, 200)] 65 | public int Contrast { get; set; } = 100; 66 | [DisplayName("Saturation")] 67 | [Description("Adjust the saturation of the emulated screen, in percent.")] 68 | [Range(0, 200)] 69 | public int Saturation { get; set; } = 100; 70 | } 71 | 72 | public sealed class SoundConfiguration : ConfigurationBase 73 | { 74 | [DisplayName("Mute")] 75 | [Description("Toggles muting all sound output.")] 76 | public bool Mute { get; set; } = false; 77 | [DisplayName("Low-Pass Filter")] 78 | [Description("Toggles low-pass filter for all sound output.")] 79 | public bool LowPassFilter { get; set; } = true; 80 | } 81 | 82 | public sealed class InputConfiguration : ConfigurationBase 83 | { 84 | [DisplayName("Automatic Remapping")] 85 | [Description("Automatically remap X-/Y-pads with game orientation.")] 86 | public bool AutoRemap { get; set; } = true; 87 | [DisplayName("Game Controls")] 88 | [Description("Controls related to game input, i.e. X-/Y-pads, etc.")] 89 | public Dictionary> GameControls { get; set; } = new Dictionary>(); 90 | [DisplayName("System Controls")] 91 | [Description("Controls related to hardware functions, i.e. volume button.")] 92 | public Dictionary> SystemControls { get; set; } = new Dictionary>(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/GlobalVariables.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | 4 | namespace StoicGoose.WinForms 5 | { 6 | public static class GlobalVariables 7 | { 8 | public static readonly bool IsAuthorsMachine = System.Environment.MachineName == "KAMIKO"; 9 | #if DEBUG 10 | public static readonly bool IsDebugBuild = true; 11 | #else 12 | public static readonly bool IsDebugBuild = false; 13 | #endif 14 | public static readonly bool EnableLocalDebugIO = IsAuthorsMachine; 15 | 16 | public static readonly bool EnableSuperVerbosity = false; 17 | public static readonly bool EnableOpenGLDebug = false; 18 | 19 | public static readonly bool EnableSkipBootstrapIfFound = false; 20 | 21 | public static string[] Dump() 22 | { 23 | var vars = new List(); 24 | foreach (var fieldInfo in typeof(GlobalVariables).GetFields(BindingFlags.Static | BindingFlags.Public)) 25 | vars.Add($"{fieldInfo.Name} == {fieldInfo.GetValue(null)}"); 26 | return vars.ToArray(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Handlers/EmulatorHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Windows.Forms; 5 | 6 | using StoicGoose.Core.Interfaces; 7 | 8 | namespace StoicGoose.WinForms.Handlers 9 | { 10 | public class EmulatorHandler 11 | { 12 | readonly static string threadName = $"{Application.ProductName}Emulation"; 13 | 14 | Thread thread = default; 15 | volatile bool threadRunning = false, threadPaused = false; 16 | 17 | volatile bool isResetRequested = false; 18 | volatile bool isPauseRequested = false, newPauseState = false; 19 | volatile bool isFpsLimiterChangeRequested = false, limitFps = true, newLimitFps = false; 20 | 21 | public bool IsRunning => threadRunning; 22 | public bool IsPaused => threadPaused; 23 | 24 | public IMachine Machine { get; } = default; 25 | 26 | public EmulatorHandler(Type machineType) 27 | { 28 | Machine = Activator.CreateInstance(machineType) as IMachine; 29 | Machine.Initialize(); 30 | } 31 | 32 | public void Startup() 33 | { 34 | Machine.Reset(); 35 | 36 | threadRunning = true; 37 | threadPaused = false; 38 | 39 | thread = new Thread(ThreadMainLoop) { Name = threadName, Priority = ThreadPriority.AboveNormal, IsBackground = false }; 40 | thread.Start(); 41 | } 42 | 43 | public void Reset() 44 | { 45 | isResetRequested = true; 46 | } 47 | 48 | public void Pause() 49 | { 50 | isPauseRequested = true; 51 | newPauseState = true; 52 | } 53 | 54 | public void Unpause() 55 | { 56 | isPauseRequested = true; 57 | newPauseState = false; 58 | } 59 | 60 | public void Shutdown() 61 | { 62 | threadRunning = false; 63 | threadPaused = false; 64 | 65 | thread?.Join(); 66 | 67 | Machine.Shutdown(); 68 | } 69 | 70 | public void SetFpsLimiter(bool value) 71 | { 72 | isFpsLimiterChangeRequested = true; 73 | newLimitFps = value; 74 | } 75 | 76 | private void ThreadMainLoop() 77 | { 78 | var stopWatch = Stopwatch.StartNew(); 79 | var interval = 1000.0 / Machine.RefreshRate; 80 | var lastTime = 0.0; 81 | 82 | while (true) 83 | { 84 | if (!threadRunning) break; 85 | 86 | if (isResetRequested) 87 | { 88 | Machine.Reset(); 89 | stopWatch.Restart(); 90 | lastTime = 0.0; 91 | 92 | isResetRequested = false; 93 | } 94 | 95 | if (isPauseRequested) 96 | { 97 | threadPaused = newPauseState; 98 | isPauseRequested = false; 99 | } 100 | 101 | if (isFpsLimiterChangeRequested) 102 | { 103 | limitFps = newLimitFps; 104 | isFpsLimiterChangeRequested = false; 105 | } 106 | 107 | if (!threadPaused) 108 | { 109 | if (limitFps) 110 | { 111 | while ((stopWatch.Elapsed.TotalMilliseconds - lastTime) < interval) 112 | Thread.Sleep(0); 113 | 114 | lastTime += interval; 115 | } 116 | else 117 | lastTime = stopWatch.Elapsed.TotalMilliseconds; 118 | 119 | Machine.RunFrame(); 120 | } 121 | else 122 | lastTime = stopWatch.Elapsed.TotalMilliseconds; 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/OpenGL/RenderControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | using System.Windows.Forms; 5 | 6 | using OpenTK.Graphics.OpenGL4; 7 | using OpenTK.Windowing.Common; 8 | using OpenTK.WinForms; 9 | 10 | namespace StoicGoose.WinForms.OpenGL 11 | { 12 | public class RenderControl : GLControl 13 | { 14 | readonly static DebugProc debugCallback = GLDebugCallback; 15 | static GCHandle debugCallbackHandle; 16 | 17 | bool wasShown = false; 18 | 19 | public RenderControl() : this(GLControlSettings.Default) { } 20 | 21 | public RenderControl(GLControlSettings settings) : base(settings) 22 | { 23 | Application.Idle += (s, e) => 24 | { 25 | if (HasValidContext) 26 | Invalidate(); 27 | }; 28 | } 29 | 30 | protected override bool IsInputKey(Keys keyData) 31 | { 32 | return keyData switch 33 | { 34 | Keys.Right or Keys.Left or Keys.Up or Keys.Down or Keys.Shift | Keys.Right or Keys.Shift | Keys.Left or Keys.Shift | Keys.Up or Keys.Shift | Keys.Down => true, 35 | _ => base.IsInputKey(keyData), 36 | }; 37 | } 38 | 39 | protected override void OnHandleDestroyed(EventArgs e) 40 | { 41 | if (debugCallbackHandle.IsAllocated) 42 | debugCallbackHandle.Free(); 43 | 44 | base.OnHandleDestroyed(e); 45 | } 46 | 47 | protected override void OnLoad(EventArgs e) 48 | { 49 | if ((Flags & ContextFlags.Debug) == ContextFlags.Debug) 50 | { 51 | debugCallbackHandle = GCHandle.Alloc(debugCallback); 52 | GL.DebugMessageCallback(debugCallback, IntPtr.Zero); 53 | GL.DebugMessageControl(DebugSourceControl.DontCare, DebugTypeControl.DontCare, DebugSeverityControl.DontCare, 0, (int[])null, true); 54 | 55 | GL.Enable(EnableCap.DebugOutput); 56 | GL.Enable(EnableCap.DebugOutputSynchronous); 57 | } 58 | 59 | base.OnLoad(e); 60 | } 61 | 62 | protected override void OnResize(EventArgs e) 63 | { 64 | base.OnResize(e); 65 | } 66 | 67 | protected override void OnPaint(PaintEventArgs e) 68 | { 69 | if (HasValidContext && !Context.IsCurrent) 70 | MakeCurrent(); 71 | 72 | if (!wasShown) 73 | { 74 | OnResize(EventArgs.Empty); 75 | wasShown = true; 76 | } 77 | 78 | SwapBuffers(); 79 | 80 | base.OnPaint(e); 81 | } 82 | 83 | private static void GLDebugCallback(DebugSource source, DebugType type, int id, DebugSeverity severity, int length, IntPtr message, IntPtr userParam) 84 | { 85 | var messageString = Marshal.PtrToStringAnsi(message, length); 86 | Debug.Print($"{(type == DebugType.DebugTypeError ? "GL ERROR" : "GL callback")}: source={source}, type={type}, severity={severity}, message={messageString}"); 87 | if (type == DebugType.DebugTypeError) throw new Exception(messageString); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Dieser Code wurde von einem Tool generiert. 4 | // Laufzeitversion:4.0.30319.42000 5 | // 6 | // Änderungen an dieser Datei können falsches Verhalten verursachen und gehen verloren, wenn 7 | // der Code erneut generiert wird. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace StoicGoose.WinForms.WinForms.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// Eine stark typisierte Ressourcenklasse zum Suchen von lokalisierten Zeichenfolgen usw. 17 | /// 18 | // Diese Klasse wurde von der StronglyTypedResourceBuilder automatisch generiert 19 | // -Klasse über ein Tool wie ResGen oder Visual Studio automatisch generiert. 20 | // Um einen Member hinzuzufügen oder zu entfernen, bearbeiten Sie die .ResX-Datei und führen dann ResGen 21 | // mit der /str-Option erneut aus, oder Sie erstellen Ihr VS-Projekt neu. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Gibt die zwischengespeicherte ResourceManager-Instanz zurück, die von dieser Klasse verwendet wird. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("StoicGoose.WinForms.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Überschreibt die CurrentUICulture-Eigenschaft des aktuellen Threads für alle 51 | /// Ressourcenzuordnungen, die diese stark typisierte Ressourcenklasse verwenden. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/SettingsForm.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Forms; 2 | 3 | using static StoicGoose.WinForms.Windows.ControlHelpers; 4 | 5 | namespace StoicGoose.WinForms 6 | { 7 | public partial class SettingsForm : Form 8 | { 9 | public Configuration Configuration { get; } = default; 10 | 11 | public SettingsForm(Configuration configuration) 12 | { 13 | Configuration = configuration; 14 | 15 | InitializeComponent(); 16 | 17 | InitializePages(); 18 | } 19 | 20 | private void InitializePages() 21 | { 22 | var pageGeneral = new SettingsPage(Configuration, nameof(Configuration.General)); 23 | pageGeneral.Append(CreateToggle(Configuration.General, nameof(Configuration.General.PreferOriginalWS))); 24 | pageGeneral.Append(CreateToggle(Configuration.General, nameof(Configuration.General.UseBootstrap))); 25 | pageGeneral.Append(CreatePathSelector(Configuration.General, nameof(Configuration.General.BootstrapFile))); 26 | pageGeneral.Append(CreatePathSelector(Configuration.General, nameof(Configuration.General.BootstrapFileWSC))); 27 | pageGeneral.Append(CreateToggle(Configuration.General, nameof(Configuration.General.LimitFps))); 28 | pageGeneral.Append(CreateToggle(Configuration.General, nameof(Configuration.General.EnableCheats))); 29 | pageGeneral.Attach(tvSettings); 30 | 31 | var pageVideo = new SettingsPage(Configuration, nameof(Configuration.Video)); 32 | pageVideo.Append(CreateSlider(Configuration.Video, nameof(Configuration.Video.Brightness))); 33 | pageVideo.Append(CreateSlider(Configuration.Video, nameof(Configuration.Video.Contrast))); 34 | pageVideo.Append(CreateSlider(Configuration.Video, nameof(Configuration.Video.Saturation))); 35 | pageVideo.Attach(tvSettings); 36 | 37 | var pageSound = new SettingsPage(Configuration, nameof(Configuration.Sound)); 38 | pageSound.Append(CreateToggle(Configuration.Sound, nameof(Configuration.Sound.Mute))); 39 | pageSound.Append(CreateToggle(Configuration.Sound, nameof(Configuration.Sound.LowPassFilter))); 40 | pageSound.Attach(tvSettings); 41 | 42 | var pageInput = new SettingsPage(Configuration, nameof(Configuration.Input)); 43 | pageInput.Append(CreateToggle(Configuration.Input, nameof(Configuration.Input.AutoRemap))); 44 | var pageInputGame = new SettingsPage(Configuration.Input, nameof(Configuration.Input.GameControls)); 45 | foreach (var (key, _) in Configuration.Input.GameControls) 46 | pageInputGame.Append(CreateKeyInput(Configuration.Input, nameof(Configuration.Input.GameControls), key)); 47 | pageInput.Append(pageInputGame); 48 | var pageInputSystem = new SettingsPage(Configuration.Input, nameof(Configuration.Input.SystemControls)); 49 | foreach (var (key, _) in Configuration.Input.SystemControls) 50 | pageInputSystem.Append(CreateKeyInput(Configuration.Input, nameof(Configuration.Input.SystemControls), key)); 51 | pageInput.Append(pageInputSystem); 52 | pageInput.Attach(tvSettings); 53 | } 54 | 55 | private void tvSettings_BeforeSelect(object sender, TreeViewCancelEventArgs e) 56 | { 57 | if (e.Node.Tag is Control[] controls) 58 | { 59 | tlpSettings.SuspendLayout(); 60 | tlpSettings.Controls.Clear(); 61 | foreach (var control in controls) 62 | { 63 | tlpSettings.Controls.Add(control); 64 | if (control.Tag is int span) { tlpSettings.SetColumnSpan(control, span); } 65 | } 66 | lblNothing.Text = string.Empty; 67 | lblNothing.Visible = false; 68 | 69 | tlpSettings.ResumeLayout(); 70 | tlpSettings.Visible = true; 71 | } 72 | else 73 | { 74 | tlpSettings.Visible = false; 75 | 76 | lblNothing.Text = "Honk."; 77 | lblNothing.Visible = true; 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/SettingsForm.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/SoundRecorderForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | 4 | using StoicGoose.WinForms.IO; 5 | 6 | namespace StoicGoose.WinForms 7 | { 8 | public partial class SoundRecorderForm : Form 9 | { 10 | WaveFileWriter waveFileWriter = default; 11 | 12 | readonly int sampleRate = 0, numChannels = 0; 13 | 14 | public bool IsRecording { get; private set; } = false; 15 | 16 | public SoundRecorderForm(int sampleRate, int numChannels) 17 | { 18 | InitializeComponent(); 19 | 20 | this.sampleRate = sampleRate; 21 | this.numChannels = numChannels; 22 | 23 | btnStart.Enabled = ftbWaveFile.IsFileSelected; 24 | btnStop.Enabled = false; 25 | } 26 | 27 | private void SoundRecorderForm_FormClosing(object sender, FormClosingEventArgs e) 28 | { 29 | if (IsRecording) 30 | waveFileWriter?.Save(); 31 | } 32 | 33 | private void ftbWaveFile_FileSelected(object sender, EventArgs e) 34 | { 35 | btnStart.Enabled = ftbWaveFile.IsFileSelected; 36 | } 37 | 38 | private void btnStart_Click(object sender, EventArgs e) 39 | { 40 | if (waveFileWriter != null) 41 | { 42 | waveFileWriter.Dispose(); 43 | waveFileWriter = null; 44 | } 45 | 46 | waveFileWriter = new(ftbWaveFile.FileName, sampleRate, numChannels); 47 | 48 | btnStart.Enabled = false; 49 | btnStop.Enabled = true; 50 | 51 | IsRecording = true; 52 | } 53 | 54 | private void btnStop_Click(object sender, EventArgs e) 55 | { 56 | if (IsRecording) 57 | waveFileWriter?.Save(); 58 | 59 | btnStart.Enabled = ftbWaveFile.IsFileSelected; 60 | btnStop.Enabled = false; 61 | 62 | IsRecording = false; 63 | } 64 | 65 | private void btnClose_Click(object sender, EventArgs e) 66 | { 67 | Hide(); 68 | } 69 | 70 | public void EnqueueSamples(short[] samples) 71 | { 72 | if (IsRecording) 73 | waveFileWriter?.Write(samples); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Utilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Runtime.Versioning; 5 | 6 | using Microsoft.Win32; 7 | 8 | using StoicGoose.Common.Drawing; 9 | using StoicGoose.Common.Utilities; 10 | 11 | namespace StoicGoose.WinForms 12 | { 13 | public static class Utilities 14 | { 15 | [SupportedOSPlatform("windows")] 16 | readonly static RegistryKey[] fontRegistryKeys = new RegistryKey[] 17 | { 18 | Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"), 19 | Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts") 20 | }; 21 | 22 | [SupportedOSPlatform("windows")] 23 | public static string GetSystemFontFilePath(string name) 24 | { 25 | foreach (var key in fontRegistryKeys.Where(x => x != null)) 26 | { 27 | var fullName = key.GetValueNames().FirstOrDefault(x => x.StartsWith(name)); 28 | if (key.GetValue(fullName) is string value) 29 | { 30 | var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), value); 31 | if (File.Exists(path)) return path; 32 | } 33 | } 34 | return null; 35 | } 36 | 37 | public static RgbaFile GetEmbeddedSystemIcon(string name) => Resources.GetEmbeddedRgbaFile($"Assets.Icons.{name}"); 38 | public static string GetEmbeddedShaderFile(string name) => Resources.GetEmbeddedText($"Assets.Shaders.{name}"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/WS-Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xdanieldzd/StoicGoose/71e53b24f466aabfe5c5e864ec2f9f6d6e87f9c6/StoicGoose.WinForms/WS-Icon.ico -------------------------------------------------------------------------------- /StoicGoose.WinForms/Windows/BindableToolStripMenuItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Windows.Forms; 4 | 5 | namespace StoicGoose.WinForms.Windows 6 | { 7 | public class BindableToolStripMenuItem : ToolStripMenuItem, IBindableComponent 8 | { 9 | public BindableToolStripMenuItem() : base() { } 10 | public BindableToolStripMenuItem(string text) : base(text) { } 11 | public BindableToolStripMenuItem(Image image) : base(image) { } 12 | public BindableToolStripMenuItem(string text, Image image) : base(text, image) { } 13 | public BindableToolStripMenuItem(string text, Image image, EventHandler onClick) : base(text, image, onClick) { } 14 | public BindableToolStripMenuItem(string text, Image image, params ToolStripMenuItem[] dropDownItems) : base(text, image, dropDownItems) { } 15 | public BindableToolStripMenuItem(string text, Image image, EventHandler onClick, Keys shortcutKeys) : base(text, image, onClick, shortcutKeys) { } 16 | public BindableToolStripMenuItem(string text, Image image, EventHandler onClick, string name) : base(text, image, onClick, name) { } 17 | 18 | BindingContext bindingContext; 19 | ControlBindingsCollection dataBindings; 20 | 21 | public BindingContext BindingContext 22 | { 23 | get 24 | { 25 | if (bindingContext == null) 26 | bindingContext = new BindingContext(); 27 | return bindingContext; 28 | } 29 | set 30 | { 31 | bindingContext = value; 32 | } 33 | } 34 | 35 | public ControlBindingsCollection DataBindings 36 | { 37 | get 38 | { 39 | if (dataBindings == null) 40 | dataBindings = new ControlBindingsCollection(this); 41 | return dataBindings; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Windows/Controls/CheatsListView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace StoicGoose.WinForms.Windows.Controls 5 | { 6 | public class CheatsListView : ListViewEx 7 | { 8 | readonly static string[] cheatConditionSymbols = new string[] { "^", "<", "<=", "=>", ">" }; 9 | 10 | readonly static List<(string ColumnName, Func ValueLookup, Func DisplayStringLookup)> columnMappingForCheat = new() 11 | { 12 | ("Address", c => c.Address, c => $"0x{c.Address:X6}"), 13 | ("Condition", c => c.Condition, c => cheatConditionSymbols[(int)c.Condition]), 14 | ("Compare", c => c.CompareValue, c => $"0x{c.CompareValue:X2}"), 15 | (string.Empty, c => 0, c => "="), 16 | ("Patch", c => c.PatchedValue, c => $"0x{c.PatchedValue:X2}"), 17 | ("Description", c => c.Description, c => c.Description) 18 | }; 19 | 20 | public CheatsListView() : base(columnMappingForCheat) { } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Windows/Controls/FileTextBox.Designer.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace StoicGoose.WinForms.Windows.Controls 3 | { 4 | partial class FileTextBox 5 | { 6 | /// 7 | /// Erforderliche Designervariable. 8 | /// 9 | private System.ComponentModel.IContainer components = null; 10 | 11 | /// 12 | /// Verwendete Ressourcen bereinigen. 13 | /// 14 | /// True, wenn verwaltete Ressourcen gelöscht werden sollen; andernfalls False. 15 | protected override void Dispose(bool disposing) 16 | { 17 | if (disposing && (components != null)) 18 | { 19 | components.Dispose(); 20 | } 21 | base.Dispose(disposing); 22 | } 23 | 24 | #region Vom Komponenten-Designer generierter Code 25 | 26 | /// 27 | /// Erforderliche Methode für die Designerunterstützung. 28 | /// Der Inhalt der Methode darf nicht mit dem Code-Editor geändert werden. 29 | /// 30 | private void InitializeComponent() 31 | { 32 | this.txtPath = new System.Windows.Forms.TextBox(); 33 | this.btnBrowse = new System.Windows.Forms.Button(); 34 | this.ofdOpen = new System.Windows.Forms.OpenFileDialog(); 35 | this.sfdSave = new System.Windows.Forms.SaveFileDialog(); 36 | this.SuspendLayout(); 37 | // 38 | // txtPath 39 | // 40 | this.txtPath.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 41 | | System.Windows.Forms.AnchorStyles.Left) 42 | | System.Windows.Forms.AnchorStyles.Right))); 43 | this.txtPath.BackColor = System.Drawing.SystemColors.Window; 44 | this.txtPath.Location = new System.Drawing.Point(0, 0); 45 | this.txtPath.Margin = new System.Windows.Forms.Padding(0, 0, 6, 0); 46 | this.txtPath.Name = "txtPath"; 47 | this.txtPath.ReadOnly = true; 48 | this.txtPath.Size = new System.Drawing.Size(106, 23); 49 | this.txtPath.TabIndex = 0; 50 | // 51 | // btnBrowse 52 | // 53 | this.btnBrowse.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 54 | | System.Windows.Forms.AnchorStyles.Right))); 55 | this.btnBrowse.Location = new System.Drawing.Point(112, 0); 56 | this.btnBrowse.Margin = new System.Windows.Forms.Padding(0); 57 | this.btnBrowse.Name = "btnBrowse"; 58 | this.btnBrowse.Size = new System.Drawing.Size(38, 24); 59 | this.btnBrowse.TabIndex = 1; 60 | this.btnBrowse.Text = "..."; 61 | this.btnBrowse.UseVisualStyleBackColor = true; 62 | this.btnBrowse.Click += new System.EventHandler(this.btnBrowse_Click); 63 | // 64 | // FileTextBox 65 | // 66 | this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); 67 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 68 | this.Controls.Add(this.btnBrowse); 69 | this.Controls.Add(this.txtPath); 70 | this.Name = "FileTextBox"; 71 | this.Size = new System.Drawing.Size(150, 24); 72 | this.ResumeLayout(false); 73 | this.PerformLayout(); 74 | 75 | } 76 | 77 | #endregion 78 | 79 | private System.Windows.Forms.TextBox txtPath; 80 | private System.Windows.Forms.Button btnBrowse; 81 | private System.Windows.Forms.OpenFileDialog ofdOpen; 82 | private System.Windows.Forms.SaveFileDialog sfdSave; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Windows/Controls/FileTextBox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | 4 | namespace StoicGoose.WinForms.Windows.Controls 5 | { 6 | public enum FileTextBoxDialogMode { Open, Save } 7 | 8 | public partial class FileTextBox : UserControl 9 | { 10 | public FileTextBoxDialogMode DialogMode { get; set; } = FileTextBoxDialogMode.Open; 11 | 12 | public string FileName { get; set; } = string.Empty; 13 | public string InitialDirectory { get; set; } = string.Empty; 14 | public string Filter { get; set; } = string.Empty; 15 | 16 | public bool IsFileSelected => !string.IsNullOrEmpty(FileName); 17 | 18 | public event EventHandler FileSelected; 19 | public void OnFileSelected(EventArgs e) => FileSelected?.Invoke(this, e); 20 | 21 | public FileTextBox() 22 | { 23 | InitializeComponent(); 24 | 25 | /* force to correct size */ 26 | txtPath.AutoSize = false; 27 | txtPath.Height = btnBrowse.Height; 28 | 29 | txtPath.DataBindings.Add(nameof(TextBox.Text), this, nameof(FileName), false, DataSourceUpdateMode.OnPropertyChanged); 30 | } 31 | 32 | private void btnBrowse_Click(object sender, EventArgs e) 33 | { 34 | FileDialog fileDialog = DialogMode switch 35 | { 36 | FileTextBoxDialogMode.Open => ofdOpen, 37 | FileTextBoxDialogMode.Save => sfdSave, 38 | _ => throw new Exception("Invalid dialog mode"), 39 | }; 40 | fileDialog.FileName = FileName; 41 | fileDialog.InitialDirectory = InitialDirectory; 42 | fileDialog.Filter = Filter; 43 | 44 | if (fileDialog.ShowDialog() == DialogResult.OK) 45 | { 46 | txtPath.Text = fileDialog.FileName; 47 | OnFileSelected(EventArgs.Empty); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Windows/Controls/FileTextBox.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | 61 | 17, 17 62 | 63 | 64 | 117, 17 65 | 66 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Windows/Controls/LabelEx.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Drawing.Text; 3 | using System.Windows.Forms; 4 | 5 | namespace StoicGoose.WinForms.Windows.Controls 6 | { 7 | public class LabelEx : Label 8 | { 9 | protected override void OnPaint(PaintEventArgs e) 10 | { 11 | e.Graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; 12 | 13 | var flags = TextFormatFlags.Default; 14 | switch (TextAlign) 15 | { 16 | case ContentAlignment.BottomCenter: flags |= TextFormatFlags.Bottom | TextFormatFlags.HorizontalCenter; break; 17 | case ContentAlignment.BottomLeft: flags |= TextFormatFlags.Bottom | TextFormatFlags.Left; break; 18 | case ContentAlignment.BottomRight: flags |= TextFormatFlags.Bottom | TextFormatFlags.Right; break; 19 | case ContentAlignment.MiddleCenter: flags |= TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter; break; 20 | case ContentAlignment.MiddleLeft: flags |= TextFormatFlags.VerticalCenter | TextFormatFlags.Left; break; 21 | case ContentAlignment.MiddleRight: flags |= TextFormatFlags.VerticalCenter | TextFormatFlags.Right; break; 22 | case ContentAlignment.TopCenter: flags |= TextFormatFlags.Top | TextFormatFlags.HorizontalCenter; break; 23 | case ContentAlignment.TopLeft: flags |= TextFormatFlags.Top | TextFormatFlags.Left; break; 24 | case ContentAlignment.TopRight: flags |= TextFormatFlags.Top | TextFormatFlags.Right; break; 25 | } 26 | var rect = e.ClipRectangle; 27 | rect.Offset(-2, -2); 28 | TextRenderer.DrawText(e.Graphics, Text, Font, rect, ForeColor, flags); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Windows/Controls/ListViewEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Windows.Forms; 6 | 7 | namespace StoicGoose.WinForms.Windows.Controls 8 | { 9 | // Based on https://stackoverflow.com/a/63778553 10 | 11 | public abstract class ListViewEx : ListView 12 | { 13 | IList<(string ColumnName, Func ValueLookup, Func DisplayStringLookup)> ColumnInfo { get; } 14 | 15 | public ListViewEx(IList<(string ColumnName, Func ValueLookup, Func DisplayStringLookup)> columnInfo) : base() 16 | { 17 | ColumnInfo = columnInfo; 18 | DoubleBuffered = true; 19 | 20 | columnInfo.Select(ci => ci.ColumnName).ToList().ForEach(columnName => 21 | { 22 | var col = Columns.Add(columnName); 23 | col.Width = -2; 24 | }); 25 | } 26 | 27 | public void Add(T item) 28 | { 29 | var lvi = Items.Add(""); 30 | lvi.Tag = item; 31 | 32 | RefreshContent(); 33 | } 34 | 35 | public void AddRange(IList items) 36 | { 37 | foreach (var item in items) 38 | Add(item); 39 | } 40 | 41 | public void RemoveAll() 42 | { 43 | foreach (var item in Items.Cast().Select(lvi => (T)lvi.Tag)) 44 | Remove(item); 45 | } 46 | 47 | public void Remove(T item) 48 | { 49 | if (item == null) return; 50 | 51 | var listviewItem = Items.Cast().Select(lvi => new { ListViewItem = lvi, Obj = (T)lvi.Tag }).FirstOrDefault(lvi => item.Equals(lvi.Obj)).ListViewItem; 52 | Items.Remove(listviewItem); 53 | 54 | RefreshContent(); 55 | } 56 | 57 | public void Replace(T oldItem, T newItem) 58 | { 59 | if (oldItem == null || newItem == null) return; 60 | 61 | var oldIndex = SelectedIndices.Cast().First(); 62 | if (oldIndex != -1) 63 | { 64 | Items.RemoveAt(oldIndex); 65 | var lvi = Items.Insert(oldIndex, ""); 66 | lvi.Tag = newItem; 67 | 68 | SelectedIndices.Clear(); 69 | SelectedIndices.Add(oldIndex); 70 | } 71 | 72 | RefreshContent(); 73 | } 74 | 75 | public List GetSelectedItems() 76 | { 77 | return SelectedItems.OfType().Select(lvi => (T)lvi.Tag).ToList(); 78 | } 79 | 80 | public void RefreshContent() 81 | { 82 | var columnsChanged = new List(); 83 | 84 | Items.Cast().Select(lvi => new { ListViewItem = lvi, Obj = (T)lvi.Tag }).ToList().ForEach(lvi => 85 | { 86 | ColumnInfo.Select((column, index) => new { Column = column, Index = index }).ToList().ForEach(col => 87 | { 88 | var newDisplayValue = col.Column.DisplayStringLookup(lvi.Obj); 89 | if (lvi.ListViewItem.SubItems.Count <= col.Index) 90 | lvi.ListViewItem.SubItems.Add(""); 91 | 92 | var subitem = lvi.ListViewItem.SubItems[col.Index]; 93 | var oldDisplayValue = subitem.Text ?? ""; 94 | 95 | if (!oldDisplayValue.Equals(newDisplayValue)) 96 | { 97 | subitem.Text = newDisplayValue; 98 | columnsChanged.Add(col.Index); 99 | } 100 | }); 101 | }); 102 | 103 | columnsChanged.ForEach(col => { Columns[col].Width = -2; }); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Windows/Controls/TableLayoutPanelEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Windows.Forms; 4 | 5 | namespace StoicGoose.WinForms.Windows.Controls 6 | { 7 | public class TableLayoutPanelEx : TableLayoutPanel 8 | { 9 | [DllImport("uxtheme.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] 10 | private static extern int SetWindowTheme(IntPtr hwnd, string pszSubAppName, string pszSubIdList); 11 | 12 | protected override void OnHandleCreated(EventArgs e) 13 | { 14 | if (!DesignMode && Environment.OSVersion?.Platform == PlatformID.Win32NT && Environment.OSVersion?.Version.Major >= 6) 15 | _ = SetWindowTheme(Handle, "explorer", null); 16 | 17 | base.OnHandleCreated(e); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Windows/Controls/TextBoxEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | 4 | namespace StoicGoose.WinForms.Windows.Controls 5 | { 6 | public class TextBoxEx : TextBox 7 | { 8 | protected Timer timer = new(); 9 | 10 | public event EventHandler TimerTick; 11 | protected void OnTimerTick(object s, EventArgs e) { TimerTick(s, e); } 12 | 13 | public int TimerInterval { get => timer.Interval; set => timer.Interval = value; } 14 | public bool TimerEnabled { get => timer.Enabled; set => timer.Enabled = value; } 15 | 16 | public void StartTimer() => timer.Start(); 17 | public void StopTimer() => timer.Stop(); 18 | 19 | public TextBoxEx() : base() 20 | { 21 | timer.Tick += (s, e) => { OnTimerTick(this, EventArgs.Empty); }; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/Windows/Controls/TreeViewEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Windows.Forms; 4 | 5 | namespace StoicGoose.WinForms.Windows.Controls 6 | { 7 | public class TreeViewEx : TreeView 8 | { 9 | [DllImport("uxtheme.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] 10 | private static extern int SetWindowTheme(IntPtr hwnd, string pszSubAppName, string pszSubIdList); 11 | 12 | protected override void OnHandleCreated(EventArgs e) 13 | { 14 | if (!DesignMode && Environment.OSVersion?.Platform == PlatformID.Win32NT && Environment.OSVersion?.Version.Major >= 6) 15 | _ = SetWindowTheme(Handle, "explorer", null); 16 | 17 | base.OnHandleCreated(e); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/XInput/ControllerManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace StoicGoose.WinForms.XInput 4 | { 5 | public static class ControllerManager 6 | { 7 | public const int MaxControllers = 4; 8 | 9 | readonly static Controller[] controllers; 10 | 11 | static ControllerManager() 12 | { 13 | controllers = new Controller[MaxControllers]; 14 | for (int i = 0; i < controllers.Length; i++) 15 | controllers[i] = new(i); 16 | } 17 | 18 | public static Controller GetController(int index) 19 | { 20 | if (index < 0 || index >= MaxControllers) throw new Exception("Controller index out of range"); 21 | return controllers[index]; 22 | } 23 | 24 | public static void Update() 25 | { 26 | for (int i = 0; i < controllers.Length; i++) 27 | controllers[i].Update(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /StoicGoose.WinForms/XInput/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace StoicGoose.WinForms.XInput 6 | { 7 | static class NativeMethods 8 | { 9 | const string dllName = "xinput9_1_0.dll"; 10 | 11 | public const int FlagGamepad = 0x00000001; 12 | 13 | [DllImport(dllName, EntryPoint = "XInputGetState")] 14 | public static extern int GetState(int dwUserIndex, ref XInputState pState); 15 | [DllImport(dllName, EntryPoint = "XInputSetState")] 16 | public static extern int SetState(int dwUserIndex, ref XInputVibration pVibration); 17 | [DllImport(dllName, EntryPoint = "XInputGetCapabilities")] 18 | public static extern int GetCapabilities(int dwUserIndex, int dwFlags, ref XInputCapabilities pCapabilities); 19 | } 20 | 21 | public enum Errors 22 | { 23 | Success = 0x00000000, 24 | BadArguments = 0x000000A0, 25 | DeviceNotConnected = 0x0000048F 26 | } 27 | 28 | /* https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.reference.xinput_gamepad%28v=vs.85%29.aspx */ 29 | [StructLayout(LayoutKind.Explicit)] 30 | public struct XInputGamepad 31 | { 32 | [FieldOffset(0)] 33 | readonly ushort wButtons; 34 | [FieldOffset(2)] 35 | public byte bLeftTrigger; 36 | [FieldOffset(3)] 37 | public byte bRightTrigger; 38 | [FieldOffset(4)] 39 | public short sThumbLX; 40 | [FieldOffset(6)] 41 | public short sThumbLY; 42 | [FieldOffset(8)] 43 | public short sThumbRX; 44 | [FieldOffset(10)] 45 | public short sThumbRY; 46 | 47 | public const int LeftThumbDeadzone = 7849; 48 | public const int RightThumbDeadzone = 8689; 49 | public const int TriggerThreshold = 30; 50 | 51 | public Buttons Buttons => (Buttons)wButtons; 52 | } 53 | 54 | [Flags] 55 | public enum Buttons 56 | { 57 | [Description("None")] 58 | None = 0x0000, 59 | [Description("D-Pad Up")] 60 | DPadUp = 0x0001, 61 | [Description("D-Pad Down")] 62 | DPadDown = 0x0002, 63 | [Description("D-Pad Left")] 64 | DPadLeft = 0x0004, 65 | [Description("D-Pad Right")] 66 | DPadRight = 0x0008, 67 | [Description("Start")] 68 | Start = 0x0010, 69 | [Description("Back")] 70 | Back = 0x0020, 71 | [Description("Left Thumbstick")] 72 | LeftThumb = 0x0040, 73 | [Description("Right Thumbstick")] 74 | RightThumb = 0x0080, 75 | [Description("Left Shoulder")] 76 | LeftShoulder = 0x0100, 77 | [Description("Right Shoulder")] 78 | RightShoulder = 0x0200, 79 | [Description("A")] 80 | A = 0x1000, 81 | [Description("B")] 82 | B = 0x2000, 83 | [Description("X")] 84 | X = 0x4000, 85 | [Description("Y")] 86 | Y = 0x8000 87 | } 88 | 89 | /* https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.reference.xinput_state%28v=vs.85%29.aspx */ 90 | [StructLayout(LayoutKind.Explicit)] 91 | public struct XInputState 92 | { 93 | [FieldOffset(0)] 94 | public uint dwPacketNumber; 95 | [FieldOffset(4)] 96 | public XInputGamepad Gamepad; 97 | } 98 | 99 | /* https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.reference.xinput_vibration%28v=vs.85%29.aspx */ 100 | [StructLayout(LayoutKind.Explicit)] 101 | public struct XInputVibration 102 | { 103 | [FieldOffset(0)] 104 | public ushort wLeftMotorSpeed; 105 | [FieldOffset(2)] 106 | public ushort wRightMotorSpeed; 107 | } 108 | 109 | /* https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.reference.xinput_capabilities%28v=vs.85%29.aspx */ 110 | [StructLayout(LayoutKind.Explicit)] 111 | public struct XInputCapabilities 112 | { 113 | [FieldOffset(0)] 114 | readonly byte type; 115 | [FieldOffset(1)] 116 | readonly byte subType; 117 | [FieldOffset(2)] 118 | readonly ushort flags; 119 | [FieldOffset(4)] 120 | public XInputGamepad Gamepad; 121 | [FieldOffset(16)] 122 | public XInputVibration Vibration; 123 | 124 | public DeviceType Type => (DeviceType)type; 125 | public DeviceSubType SubType => (DeviceSubType)subType; 126 | public DeviceFlags Flags => (DeviceFlags)flags; 127 | } 128 | 129 | public enum DeviceType 130 | { 131 | Gamepad = 0x01 132 | } 133 | 134 | public enum DeviceSubType 135 | { 136 | Gamepad = 0x01, 137 | Wheel = 0x02, 138 | ArcadeStick = 0x03, 139 | FlightStick = 0x04, 140 | DancePad = 0x05, 141 | Guitar = 0x06, 142 | DrumKit = 0x08 143 | } 144 | 145 | [Flags] 146 | public enum DeviceFlags 147 | { 148 | VoiceSupported = 0x0004 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /StoicGoose.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32516.85 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StoicGoose.WinForms", "StoicGoose.WinForms\StoicGoose.WinForms.csproj", "{74BF0AC4-AAC5-4173-9AAF-7ECB7C6D94A5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StoicGoose.Core", "StoicGoose.Core\StoicGoose.Core.csproj", "{1D3566F8-2643-4A91-82CB-CB90D66D33BD}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StoicGoose.Common", "StoicGoose.Common\StoicGoose.Common.csproj", "{0E284238-2E79-41B7-8F66-B61E6A1E3253}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StoicGoose.GLWindow", "StoicGoose.GLWindow\StoicGoose.GLWindow.csproj", "{4DEBA1D9-848E-4370-8D56-2EA14AC3A1C8}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StoicGoose.ImGuiCommon", "StoicGoose.ImGuiCommon\StoicGoose.ImGuiCommon.csproj", "{1F66A2A4-E9DF-4739-84F5-14A3A3401FA8}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {74BF0AC4-AAC5-4173-9AAF-7ECB7C6D94A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {74BF0AC4-AAC5-4173-9AAF-7ECB7C6D94A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {74BF0AC4-AAC5-4173-9AAF-7ECB7C6D94A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {74BF0AC4-AAC5-4173-9AAF-7ECB7C6D94A5}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {1D3566F8-2643-4A91-82CB-CB90D66D33BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {1D3566F8-2643-4A91-82CB-CB90D66D33BD}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {1D3566F8-2643-4A91-82CB-CB90D66D33BD}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {1D3566F8-2643-4A91-82CB-CB90D66D33BD}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {0E284238-2E79-41B7-8F66-B61E6A1E3253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {0E284238-2E79-41B7-8F66-B61E6A1E3253}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {0E284238-2E79-41B7-8F66-B61E6A1E3253}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {0E284238-2E79-41B7-8F66-B61E6A1E3253}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {4DEBA1D9-848E-4370-8D56-2EA14AC3A1C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {4DEBA1D9-848E-4370-8D56-2EA14AC3A1C8}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {4DEBA1D9-848E-4370-8D56-2EA14AC3A1C8}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {4DEBA1D9-848E-4370-8D56-2EA14AC3A1C8}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {1F66A2A4-E9DF-4739-84F5-14A3A3401FA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {1F66A2A4-E9DF-4739-84F5-14A3A3401FA8}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {1F66A2A4-E9DF-4739-84F5-14A3A3401FA8}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {1F66A2A4-E9DF-4739-84F5-14A3A3401FA8}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {E0BC41B7-C726-483F-ACDE-E40934F686DC} 48 | EndGlobalSection 49 | EndGlobal 50 | --------------------------------------------------------------------------------