diff --git a/src/Cryogenic/GameEngineWindow/GameEngineWindowManager.cs b/src/Cryogenic/GameEngineWindow/GameEngineWindowManager.cs
new file mode 100644
index 0000000..b030977
--- /dev/null
+++ b/src/Cryogenic/GameEngineWindow/GameEngineWindowManager.cs
@@ -0,0 +1,65 @@
+namespace Cryogenic.GameEngineWindow;
+
+using System;
+
+using Avalonia.Threading;
+
+using Cryogenic.GameEngineWindow.ViewModels;
+using Cryogenic.GameEngineWindow.Views;
+
+using Spice86.Core.Emulator.CPU.Registers;
+using Spice86.Core.Emulator.Memory.ReaderWriter;
+using Spice86.Core.Emulator.VM;
+
+public static class GameEngineWindowManager {
+ private static DuneGameStateWindow? _window;
+ private static DuneGameStateViewModel? _viewModel;
+ private static bool _isWindowOpen;
+
+ public static void ShowWindow(IByteReaderWriter memory, SegmentRegisters segmentRegisters, IPauseHandler? pauseHandler = null) {
+ Dispatcher.UIThread.Post(() => {
+ // Check window state first using the flag which is the source of truth
+ if (_isWindowOpen) {
+ _window?.Show();
+ _window?.Activate();
+ return;
+ }
+
+ // Clean up any existing viewmodel
+ _viewModel?.Dispose();
+ _viewModel = new DuneGameStateViewModel(memory, segmentRegisters, pauseHandler);
+ _window = new DuneGameStateWindow {
+ DataContext = _viewModel
+ };
+
+ // Use a proper handler method that can be unsubscribed
+ _window.Closed += OnWindowClosed;
+
+ _isWindowOpen = true;
+ _window.Show();
+ });
+ }
+
+ private static void OnWindowClosed(object? sender, EventArgs args) {
+ _isWindowOpen = false;
+ _viewModel?.Dispose();
+ _viewModel = null;
+
+ // Unsubscribe to prevent memory leaks
+ if (_window != null) {
+ _window.Closed -= OnWindowClosed;
+ }
+ _window = null;
+ }
+
+ public static void CloseWindow() {
+ if (!Dispatcher.UIThread.CheckAccess()) {
+ Dispatcher.UIThread.Post(CloseWindow);
+ return;
+ }
+
+ _window?.Close();
+ }
+
+ public static bool IsWindowOpen => _isWindowOpen;
+}
diff --git a/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Locations.cs b/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Locations.cs
new file mode 100644
index 0000000..1b199f8
--- /dev/null
+++ b/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Locations.cs
@@ -0,0 +1,150 @@
+namespace Cryogenic.GameEngineWindow.Models;
+
+///
+/// Partial class for Location/Sietch-related memory access.
+///
+///
+/// Locations array is at DS:0x0100, 28 bytes per entry, 70 entries max.
+/// Structure from odrade and thomas.fach-pedersen.net:
+/// - Offset 0: name_first (u8)
+/// - Offset 1: name_second (u8) - used to determine location type
+/// - Offset 2: status (u8) - bit flags for vegetation, battle, discovered, etc.
+/// - Offset 3: map_x (u8)
+/// - Offset 4: map_y (u8)
+/// - Offset 5: spice_field (u8)
+/// - Offset 6: water (u8)
+/// - And more fields...
+///
+public partial class DuneGameState {
+
+ ///
+ /// Gets the base DS-relative offset for a location entry.
+ ///
+ private int GetLocationOffset(int index) {
+ if (index < 0 || index >= MaxLocations)
+ throw new ArgumentOutOfRangeException(nameof(index),
+ $"Location index must be between 0 and {MaxLocations - 1}");
+ return LocationArrayOffset + (index * LocationEntrySize);
+ }
+
+ ///
+ /// Gets the first byte of the location name (DS:0x0100 + index*28 + 0).
+ ///
+ public byte GetLocationNameFirst(int index) => UInt8[GetLocationOffset(index)];
+
+ ///
+ /// Gets the second byte of the location name (DS:0x0100 + index*28 + 1).
+ /// Used to determine location type (palace, village, sietch).
+ ///
+ public byte GetLocationNameSecond(int index) => UInt8[GetLocationOffset(index) + 1];
+
+ ///
+ /// Gets the location status flags (DS:0x0100 + index*28 + 2).
+ /// Bit flags: 0x01=Vegetation, 0x02=InBattle, 0x04=Inventory,
+ /// 0x10=Windtrap, 0x40=Prospected, 0x80=Undiscovered
+ ///
+ public byte GetLocationStatus(int index) => UInt8[GetLocationOffset(index) + 2];
+
+ ///
+ /// Gets the location map X coordinate (DS:0x0100 + index*28 + 3).
+ ///
+ public byte GetLocationMapX(int index) => UInt8[GetLocationOffset(index) + 3];
+
+ ///
+ /// Gets the location map Y coordinate (DS:0x0100 + index*28 + 4).
+ ///
+ public byte GetLocationMapY(int index) => UInt8[GetLocationOffset(index) + 4];
+
+ ///
+ /// Gets the location coordinates as a tuple (X, Y).
+ ///
+ public (byte X, byte Y) GetLocationCoordinates(int index) {
+ int offset = GetLocationOffset(index);
+ return (UInt8[offset + 3], UInt8[offset + 4]);
+ }
+
+ ///
+ /// Gets the spice field value (DS:0x0100 + index*28 + 5).
+ ///
+ public byte GetLocationSpiceField(int index) => UInt8[GetLocationOffset(index) + 5];
+
+ ///
+ /// Gets the water value (DS:0x0100 + index*28 + 6).
+ ///
+ public byte GetLocationWater(int index) => UInt8[GetLocationOffset(index) + 6];
+
+ ///
+ /// Gets the equipment value (DS:0x0100 + index*28 + 7).
+ ///
+ public byte GetLocationEquipment(int index) => UInt8[GetLocationOffset(index) + 7];
+
+ ///
+ /// Determines the location type based on name_second byte.
+ ///
+ public static string GetLocationTypeStr(byte nameSecond) {
+ return nameSecond switch {
+ 0x00 => "Atreides Palace",
+ 0x01 => "Harkonnen Palace",
+ 0x02 => "Village (Pyons)",
+ >= 0x03 and <= 0x09 => "Sietch",
+ 0x0B => "Sietch",
+ _ => $"Unknown (0x{nameSecond:X2})"
+ };
+ }
+
+ ///
+ /// Gets the location appearance (DS:0x0100 + index*28 + 8).
+ ///
+ public byte GetLocationAppearance(int index) => UInt8[GetLocationOffset(index) + 8];
+
+ // Aliases for ViewModel compatibility
+ public byte GetSietchSpiceField(int index) => GetLocationSpiceField(index);
+ public (byte X, byte Y) GetSietchCoordinates(int index) => GetLocationCoordinates(index);
+ public byte GetSietchStatus(int index) => GetLocationStatus(index);
+
+ // Additional location accessor methods for ViewModel
+ public byte GetLocationHousedTroopId(int index) => UInt8[GetLocationOffset(index) + 9];
+ public byte GetLocationSpiceFieldId(int index) => GetLocationSpiceField(index);
+ public byte GetLocationSpiceAmount(int index) => UInt8[GetLocationOffset(index) + 10];
+ public byte GetLocationSpiceDensity(int index) => UInt8[GetLocationOffset(index) + 11];
+ public byte GetLocationHarvesters(int index) => UInt8[GetLocationOffset(index) + 12];
+ public byte GetLocationOrnithopters(int index) => UInt8[GetLocationOffset(index) + 13];
+
+ ///
+ /// Gets location name as a display string.
+ ///
+ public static string GetLocationNameStr(byte first, byte second) {
+ string str = first switch {
+ 0x01 => "Arrakeen",
+ 0x02 => "Carthag",
+ 0x03 => "Tuono",
+ 0x04 => "Habbanya",
+ 0x05 => "Oxtyn",
+ 0x06 => "Tsympo",
+ 0x07 => "Bledan",
+ 0x08 => "Ergsun",
+ 0x09 => "Haga",
+ 0x0A => "Cielago",
+ 0x0B => "Sihaya",
+ 0x0C => "Celimyn",
+ _ => $"Location {first:X2}"
+ };
+
+ string suffix = second switch {
+ 0x01 => " (Atreides)",
+ 0x02 => " (Harkonnen)",
+ 0x03 => "-Tabr",
+ 0x04 => "-Timin",
+ 0x05 => "-Tuek",
+ 0x06 => "-Harg",
+ 0x07 => "-Clam",
+ 0x08 => "-Tsymyn",
+ 0x09 => "-Siet",
+ 0x0A => "-Pyons",
+ 0x0B => "-Pyort",
+ _ => ""
+ };
+
+ return str + suffix;
+ }
+}
diff --git a/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Npcs.cs b/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Npcs.cs
new file mode 100644
index 0000000..feab4fe
--- /dev/null
+++ b/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Npcs.cs
@@ -0,0 +1,73 @@
+namespace Cryogenic.GameEngineWindow.Models;
+
+///
+/// Partial class for NPC-related memory access.
+///
+///
+/// NPCs array follows troops in memory at DS:0xAC2E (troops end at 0xAA76 + 68*27 = 0xAC2E).
+/// 16 bytes per entry, 16 entries max.
+/// Structure from odrade:
+/// - Offset 0: sprite_index (u8)
+/// - Offset 1: room_index (u8)
+/// - Offset 2: place_type (u8)
+/// - Offset 3: dialogue_state (u8)
+/// - And more fields...
+/// Plus 8 bytes padding per entry = 16 bytes total
+///
+public partial class DuneGameState {
+
+ // NPCs array DS-relative
+ public const int NpcArrayOffset = 0xAC2E; // After troops: 0xAA76 + 68*27
+ public const int NpcEntrySize = 16; // 8 bytes data + 8 bytes padding
+ public const int MaxNpcs = 16;
+
+ ///
+ /// Gets the base DS-relative offset for an NPC entry.
+ ///
+ private int GetNpcOffset(int index) {
+ if (index < 0 || index >= MaxNpcs)
+ throw new ArgumentOutOfRangeException(nameof(index),
+ $"NPC index must be between 0 and {MaxNpcs - 1}");
+ return NpcArrayOffset + (index * NpcEntrySize);
+ }
+
+ ///
+ /// Gets the NPC sprite index (DS:0xAC2E + index*16 + 0).
+ ///
+ public byte GetNpcSpriteIndex(int index) => UInt8[GetNpcOffset(index)];
+
+ ///
+ /// Gets the NPC room index (DS:0xAC2E + index*16 + 1).
+ ///
+ public byte GetNpcRoomIndex(int index) => UInt8[GetNpcOffset(index) + 1];
+
+ ///
+ /// Gets the NPC place type (DS:0xAC2E + index*16 + 2).
+ ///
+ public byte GetNpcPlaceType(int index) => UInt8[GetNpcOffset(index) + 2];
+
+ ///
+ /// Gets the NPC dialogue state (DS:0xAC2E + index*16 + 3).
+ ///
+ public byte GetNpcDialogueState(int index) => UInt8[GetNpcOffset(index) + 3];
+
+ ///
+ /// Gets a description of the NPC place type.
+ ///
+ public static string GetNpcPlaceTypeDescription(byte placeType) {
+ return placeType switch {
+ 0x00 => "None",
+ 0x01 => "Arrakeen",
+ 0x02 => "Carthag",
+ 0x03 => "Sietch",
+ 0x04 => "Desert",
+ _ => $"Place 0x{placeType:X2}"
+ };
+ }
+
+ // Aliases for ViewModel compatibility
+ public byte GetNpcSpriteId(int index) => GetNpcSpriteIndex(index);
+ public byte GetNpcRoomLocation(int index) => GetNpcRoomIndex(index);
+ public byte GetNpcExactPlace(int index) => GetNpcPlaceType(index);
+ public byte GetNpcDialogueFlag(int index) => GetNpcDialogueState(index);
+}
diff --git a/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Smugglers.cs b/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Smugglers.cs
new file mode 100644
index 0000000..26836e4
--- /dev/null
+++ b/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Smugglers.cs
@@ -0,0 +1,99 @@
+namespace Cryogenic.GameEngineWindow.Models;
+
+///
+/// Partial class for Smuggler-related memory access.
+///
+///
+/// Smugglers array follows NPCs in memory at DS:0xAD2E (NPCs end at 0xAC2E + 16*16 = 0xAD2E).
+/// 17 bytes per entry, 6 entries max.
+/// Structure from odrade:
+/// - Offset 0: location_index (u8)
+/// - Offset 1: inventory_harvesters (u8)
+/// - Offset 2: inventory_ornithopters (u8)
+/// - Offset 3: inventory_weapons (u8)
+/// - Offset 4-5: price_harvesters (u16)
+/// - Offset 6-7: price_ornithopters (u16)
+/// - Offset 8-9: price_weapons (u16)
+/// - Offset 10: haggle_willingness (u8)
+/// - And more fields...
+/// Plus 3 bytes padding = 17 bytes total
+///
+public partial class DuneGameState {
+
+ // Smugglers array DS-relative
+ public const int SmugglerArrayOffset = 0xAD2E; // After NPCs: 0xAC2E + 16*16
+ public const int SmugglerEntrySize = 17; // 14 bytes data + 3 bytes padding
+ public const int MaxSmugglers = 6;
+
+ ///
+ /// Gets the base DS-relative offset for a smuggler entry.
+ ///
+ private int GetSmugglerOffset(int index) {
+ if (index < 0 || index >= MaxSmugglers)
+ throw new ArgumentOutOfRangeException(nameof(index),
+ $"Smuggler index must be between 0 and {MaxSmugglers - 1}");
+ return SmugglerArrayOffset + (index * SmugglerEntrySize);
+ }
+
+ ///
+ /// Gets the smuggler location index (DS:0xAD2E + index*17 + 0).
+ ///
+ public byte GetSmugglerLocationIndex(int index) => UInt8[GetSmugglerOffset(index)];
+
+ ///
+ /// Gets the smuggler harvester inventory count (DS:0xAD2E + index*17 + 1).
+ ///
+ public byte GetSmugglerInventoryHarvesters(int index) => UInt8[GetSmugglerOffset(index) + 1];
+
+ ///
+ /// Gets the smuggler ornithopter inventory count (DS:0xAD2E + index*17 + 2).
+ ///
+ public byte GetSmugglerInventoryOrnithopters(int index) => UInt8[GetSmugglerOffset(index) + 2];
+
+ ///
+ /// Gets the smuggler weapon inventory count (DS:0xAD2E + index*17 + 3).
+ ///
+ public byte GetSmugglerInventoryWeapons(int index) => UInt8[GetSmugglerOffset(index) + 3];
+
+ ///
+ /// Gets the smuggler harvester price (DS:0xAD2E + index*17 + 4, u16).
+ ///
+ public ushort GetSmugglerPriceHarvesters(int index) => UInt16[GetSmugglerOffset(index) + 4];
+
+ ///
+ /// Gets the smuggler ornithopter price (DS:0xAD2E + index*17 + 6, u16).
+ ///
+ public ushort GetSmugglerPriceOrnithopters(int index) => UInt16[GetSmugglerOffset(index) + 6];
+
+ ///
+ /// Gets the smuggler weapon price (DS:0xAD2E + index*17 + 8, u16).
+ ///
+ public ushort GetSmugglerPriceWeapons(int index) => UInt16[GetSmugglerOffset(index) + 8];
+
+ ///
+ /// Gets the smuggler haggle willingness (DS:0xAD2E + index*17 + 10).
+ ///
+ public byte GetSmugglerHaggleWillingness(int index) => UInt8[GetSmugglerOffset(index) + 10];
+
+ // Alias methods for ViewModel compatibility
+ public ushort GetSmugglerHarvesterPrice(int index) => GetSmugglerPriceHarvesters(index);
+ public ushort GetSmugglerOrnithopterPrice(int index) => GetSmugglerPriceOrnithopters(index);
+ public ushort GetSmugglerKrysKnifePrice(int index) => GetSmugglerPriceWeapons(index);
+ public ushort GetSmugglerLaserGunPrice(int index) => GetSmugglerPriceWeapons(index);
+ public ushort GetSmugglerWeirdingModulePrice(int index) => GetSmugglerPriceWeapons(index);
+
+ public byte GetSmugglerRegion(int index) => GetSmugglerLocationIndex(index);
+ public string GetSmugglerLocationName(int index) {
+ byte locIndex = GetSmugglerLocationIndex(index);
+ if (locIndex < MaxLocations) {
+ return GetLocationNameStr(GetLocationNameFirst(locIndex), GetLocationNameSecond(locIndex));
+ }
+ return $"Location {locIndex}";
+ }
+ public byte GetSmugglerWillingnessToHaggle(int index) => GetSmugglerHaggleWillingness(index);
+ public byte GetSmugglerHarvesters(int index) => GetSmugglerInventoryHarvesters(index);
+ public byte GetSmugglerOrnithopters(int index) => GetSmugglerInventoryOrnithopters(index);
+ public byte GetSmugglerKrysKnives(int index) => GetSmugglerInventoryWeapons(index);
+ public byte GetSmugglerLaserGuns(int index) => GetSmugglerInventoryWeapons(index);
+ public byte GetSmugglerWeirdingModules(int index) => GetSmugglerInventoryWeapons(index);
+}
diff --git a/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Troops.cs b/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Troops.cs
new file mode 100644
index 0000000..205aead
--- /dev/null
+++ b/src/Cryogenic/GameEngineWindow/Models/DuneGameState.Troops.cs
@@ -0,0 +1,133 @@
+namespace Cryogenic.GameEngineWindow.Models;
+
+///
+/// Partial class for Troop-related memory access.
+///
+///
+/// Troops array is at DS:0xAA76, 27 bytes per entry, 68 entries max.
+/// Structure from odrade:
+/// - Offset 0: occupation (u8) - troop type
+/// - Offset 1: position_in_sietch (u8)
+/// - Offset 2: position_in_location (u8)
+/// - Offset 3-4: population (u16)
+/// - And more fields for skills, equipment, motivation...
+///
+public partial class DuneGameState {
+
+ ///
+ /// Gets the base DS-relative offset for a troop entry.
+ ///
+ private int GetTroopOffset(int index) {
+ if (index < 0 || index >= MaxTroops)
+ throw new ArgumentOutOfRangeException(nameof(index),
+ $"Troop index must be between 0 and {MaxTroops - 1}");
+ return TroopArrayOffset + (index * TroopEntrySize);
+ }
+
+ ///
+ /// Gets the troop occupation/type (DS:0xAA76 + index*27 + 0).
+ ///
+ public byte GetTroopOccupation(int index) => UInt8[GetTroopOffset(index)];
+
+ ///
+ /// Gets the troop position in sietch (DS:0xAA76 + index*27 + 1).
+ ///
+ public byte GetTroopPositionInSietch(int index) => UInt8[GetTroopOffset(index) + 1];
+
+ ///
+ /// Gets the troop position in location (DS:0xAA76 + index*27 + 2).
+ ///
+ public byte GetTroopPosition(int index) => UInt8[GetTroopOffset(index) + 2];
+
+ ///
+ /// Gets the troop population (DS:0xAA76 + index*27 + 3, u16).
+ ///
+ public ushort GetTroopPopulation(int index) => UInt16[GetTroopOffset(index) + 3];
+
+ ///
+ /// Gets the troop spice mining skill (DS:0xAA76 + index*27 + 5).
+ ///
+ public byte GetTroopSpiceSkill(int index) => UInt8[GetTroopOffset(index) + 5];
+
+ ///
+ /// Gets the troop army skill (DS:0xAA76 + index*27 + 6).
+ ///
+ public byte GetTroopArmySkill(int index) => UInt8[GetTroopOffset(index) + 6];
+
+ ///
+ /// Gets the troop ecology skill (DS:0xAA76 + index*27 + 7).
+ ///
+ public byte GetTroopEcologySkill(int index) => UInt8[GetTroopOffset(index) + 7];
+
+ ///
+ /// Gets the troop equipment level (DS:0xAA76 + index*27 + 8).
+ ///
+ public byte GetTroopEquipment(int index) => UInt8[GetTroopOffset(index) + 8];
+
+ ///
+ /// Gets the troop motivation level (DS:0xAA76 + index*27 + 9).
+ ///
+ public byte GetTroopMotivation(int index) => UInt8[GetTroopOffset(index) + 9];
+
+ ///
+ /// Gets the troop dissatisfaction level (DS:0xAA76 + index*27 + 10).
+ ///
+ public byte GetTroopDissatisfaction(int index) => UInt8[GetTroopOffset(index) + 10];
+
+ ///
+ /// Checks if a troop is Fremen (vs Harkonnen).
+ /// Fremen occupations are 0x00 and 0x02 (without high bit flags).
+ ///
+ public static bool IsTroopFremen(byte occupation) {
+ byte baseOccupation = (byte)(occupation & 0x7F);
+ // Fremen occupations are 0x00 and 0x02, or slaved Fremen (occupation >= 0xA0)
+ return baseOccupation == 0x00 || baseOccupation == 0x02 || occupation >= 0xA0;
+ }
+
+ ///
+ /// Checks if a troop is active (population > 0).
+ ///
+ public bool IsTroopActive(int index) => GetTroopPopulation(index) > 0;
+
+ // Additional troop accessor methods for ViewModel
+ public byte GetTroopId(int index) => UInt8[GetTroopOffset(index) + 0];
+ public byte GetTroopSpecialSkills(int index) => UInt8[GetTroopOffset(index) + 11];
+
+ ///
+ /// Gets a description of the troop occupation.
+ ///
+ public static string GetTroopOccupationDescription(byte occupation) {
+ byte baseOcc = (byte)(occupation & 0x0F);
+ bool isSlaved = (occupation & 0x80) != 0;
+
+ string desc = baseOcc switch {
+ 0x00 => "Fremen Troop",
+ 0x02 => "Fremen Warriors",
+ 0x0C => "Harkonnen Patrol",
+ 0x0D => "Harkonnen Troopers",
+ 0x0E => "Harkonnen Troop",
+ 0x0F => "Harkonnen Army",
+ 0x1F => "Harkonnen Elite",
+ _ => $"Unknown (0x{occupation:X2})"
+ };
+
+ return isSlaved ? $"{desc} (Slaved)" : desc;
+ }
+
+ ///
+ /// Gets a description of the troop equipment level.
+ ///
+ public static string GetTroopEquipmentDescription(byte equipment) {
+ return equipment switch {
+ 0 => "None",
+ 1 => "Basic",
+ 2 => "Standard",
+ 3 => "Advanced",
+ 4 => "Elite",
+ _ => $"Level {equipment}"
+ };
+ }
+
+ // Aliases for ViewModel compatibility
+ public byte GetTroopId(int index) => GetTroopOccupation(index);
+}
diff --git a/src/Cryogenic/GameEngineWindow/Models/DuneGameState.cs b/src/Cryogenic/GameEngineWindow/Models/DuneGameState.cs
new file mode 100644
index 0000000..4469f6b
--- /dev/null
+++ b/src/Cryogenic/GameEngineWindow/Models/DuneGameState.cs
@@ -0,0 +1,353 @@
+namespace Cryogenic.GameEngineWindow.Models;
+
+using Spice86.Core.Emulator.CPU.Registers;
+using Spice86.Core.Emulator.Memory.ReaderWriter;
+using Spice86.Core.Emulator.ReverseEngineer.DataStructure;
+
+///
+/// Provides access to Dune game state values stored in emulated memory using DS-relative offsets.
+///
+///
+///
+/// This partial class uses MemoryBasedDataStructureWithDsBaseAddress which automatically
+/// resolves DS-relative offsets at runtime. At runtime, DS segment is typically 0x1138.
+///
+///
+/// DS-relative memory layout sources:
+/// - GlobalsOnDs.cs: Runtime-traced memory accesses
+/// - debrouxl/odrade: Data structure definitions
+/// - thomas.fach-pedersen.net: Memory map documentation
+///
+///
+/// Key DS-relative offsets:
+/// - Charisma (1 byte): DS:0x0029
+/// - Game Phase/Stage (1 byte): DS:0x002A
+/// - Spice (2 bytes): DS:0x009F
+/// - Locations/Sietches: DS:0x0100 (28 bytes × 70 entries)
+/// - Date & Time (2 bytes): DS:0x1174
+/// - Contact Distance (1 byte): DS:0x1176
+/// - Troops: DS:0xAA76 (27 bytes × 68 entries)
+/// - NPCs: DS:0xAC2E (16 bytes × 16 entries, follows troops)
+/// - Smugglers: DS:0xAD2E (17 bytes × 6 entries, follows NPCs)
+/// - HNM video state: DS:0xDBE7+
+/// - Framebuffers: DS:0xDBD6+
+/// - Mouse position: DS:0xDC36+
+///
+///
+/// Display formulas from DuneEdit2 and in-game observations:
+/// - Charisma: displayed = (raw * 2) + 1 (e.g., 0x0C raw → 25 displayed)
+/// - Date/Time: Packed format, details in GetDateTime()
+///
+///
+public partial class DuneGameState : MemoryBasedDataStructureWithDsBaseAddress {
+ public DuneGameState(IByteReaderWriter memory, SegmentRegisters segmentRegisters)
+ : base(memory, segmentRegisters) {
+ }
+
+ // Player data DS-relative offsets
+ // Verified against GlobalsOnDs.cs and odrade/thomas.fach-pedersen.net references
+ public const int GameTimeOffset = 0x0002; // game_time: u16 (internal counter)
+ public const int PersonsTravelingWithOffset = 0x0010; // persons_traveling_with: u16
+ public const int PersonsInRoomOffset = 0x0012; // persons_in_room: u16
+ public const int PersonsTalkingToOffset = 0x0014; // persons_talking_to: u16
+ public const int SietchesAvailableOffset = 0x0027; // sietches_available: u8
+ public const int CharismaOffset = 0x0029; // charisma: u8
+ public const int GamePhaseOffset = 0x002A; // game_phase: u8
+ public const int SpiceOffset = 0x009F; // spice: u16
+ public const int DaysLeftOffset = 0x00CF; // days_left_until_spice_shipment: u8
+ public const int UIHeadIndexOffset = 0x00E8; // ui_head_index: u8
+ public const int DateTimeOffset = 0x1174; // date_time: u16
+ public const int ContactDistanceOffset = 0x1176; // contact_distance: u8
+
+ // Locations/Sietches array DS-relative
+ public const int LocationArrayOffset = 0x0100; // sietches: [Sietch; 70]
+ public const int LocationEntrySize = 28; // sizeof(Sietch) = 28 bytes
+ public const int MaxLocations = 70;
+
+ // Troops array DS-relative
+ public const int TroopArrayOffset = 0xAA76; // troops: [Troop; 68]
+ public const int TroopEntrySize = 27; // sizeof(Troop) = 27 bytes
+ public const int MaxTroops = 68;
+
+ // Location status flags (bit flags in status byte)
+ public const byte LocationStatusVegetation = 0x01; // Bit 0: Vegetation present
+ public const byte LocationStatusInBattle = 0x02; // Bit 1: In battle
+ public const byte LocationStatusInventory = 0x04; // Bit 2: Has inventory
+ public const byte LocationStatusWindtrap = 0x10; // Bit 4: Windtrap present
+ public const byte LocationStatusProspected = 0x40; // Bit 6: Prospected for spice
+ public const byte LocationStatusUndiscovered = 0x80; // Bit 7: Not yet discovered
+
+ // Core player state accessors using DS-relative offsets
+
+ ///
+ /// Gets the raw charisma value from memory (DS:0x0029).
+ ///
+ public byte GetCharismaRaw() => UInt8[CharismaOffset];
+
+ ///
+ /// Gets the displayed charisma value.
+ /// Formula: (raw * 2) + 1
+ /// Example: 0x0C raw → 25 displayed (from screenshot)
+ ///
+ public int GetCharismaDisplayed() => (GetCharismaRaw() * 2) + 1;
+
+ ///
+ /// Gets the game phase/stage value from memory (DS:0x002A).
+ ///
+ public byte GetGamePhase() => UInt8[GamePhaseOffset];
+
+ ///
+ /// Gets the game elapsed time (internal counter).
+ ///
+ public ushort GameElapsedTime => UInt16[GameTimeOffset];
+
+ ///
+ /// Gets the raw charisma value.
+ ///
+ public byte CharismaRaw => GetCharismaRaw();
+
+ ///
+ /// Gets the displayed charisma value ((raw * 2) + 1).
+ ///
+ public int CharismaDisplayed => GetCharismaDisplayed();
+
+ ///
+ /// Gets the game phase/stage value.
+ ///
+ public byte GamePhase => GetGamePhase();
+
+ ///
+ /// Gets the total spice amount in kilograms.
+ ///
+ public ushort GetSpice() => UInt16[SpiceOffset];
+
+ ///
+ /// Gets the date and time value (DS:0x1174).
+ /// Packed format: contains day and time information.
+ ///
+ public ushort GetDateTime() => UInt16[DateTimeOffset];
+
+ ///
+ /// Gets the contact distance value (DS:0x1176).
+ ///
+ public byte GetContactDistance() => UInt8[ContactDistanceOffset];
+
+ ///
+ /// Gets the game elapsed time counter (DS:0x0002).
+ ///
+ public ushort GetGameElapsedTime() => UInt16[GameTimeOffset];
+
+ ///
+ /// Gets follower 1 ID from persons_traveling_with (DS:0x0010, low byte).
+ ///
+ public byte GetFollower1Id() => UInt8[PersonsTravelingWithOffset];
+
+ ///
+ /// Gets follower 2 ID from persons_traveling_with (DS:0x0010, high byte).
+ ///
+ public byte GetFollower2Id() => UInt8[PersonsTravelingWithOffset + 1];
+
+ ///
+ /// Gets the current room ID from persons_in_room (DS:0x0012, low byte).
+ ///
+ public byte GetCurrentRoomId() => UInt8[PersonsInRoomOffset];
+
+ ///
+ /// Gets the current speaker ID from persons_talking_to (DS:0x0014, low byte).
+ ///
+ public byte GetCurrentSpeakerId() => UInt8[PersonsTalkingToOffset];
+
+ ///
+ /// Gets the dialogue state from persons_talking_to (DS:0x0014, high byte).
+ ///
+ public byte GetDialogueState() => UInt8[PersonsTalkingToOffset + 1];
+
+ ///
+ /// Returns the count of discovered locations (not just sietches).
+ /// A location is discovered if the UNDISCOVERED flag (0x80) is NOT set.
+ ///
+ ///
+ /// Note: This counts ALL discovered locations including palaces and villages,
+ /// not just sietches. The method name is historical.
+ ///
+ public int GetDiscoveredSietchCount() {
+ int count = 0;
+ for (int i = 0; i < MaxLocations; i++) {
+ byte status = GetLocationStatus(i);
+ // Location is discovered if UNDISCOVERED flag is NOT set
+ if ((status & LocationStatusUndiscovered) == 0)
+ count++;
+ }
+ return count;
+ }
+
+ ///
+ /// Maps NPC ID to display name.
+ ///
+ public static string GetNpcName(byte npcId) {
+ return npcId switch {
+ 0x00 => "Paul Atreides",
+ 0x01 => "Jessica",
+ 0x02 => "Thufir Hawat",
+ 0x03 => "Gurney Halleck",
+ 0x04 => "Duncan Idaho",
+ 0x05 => "Dr. Yueh",
+ 0x06 => "Stilgar",
+ 0x07 => "Chani",
+ 0x08 => "Harah",
+ 0x09 => "Baron Harkonnen",
+ 0x0A => "Feyd-Rautha",
+ 0x0B => "Duke Leto",
+ 0x0C => "Liet Kynes",
+ 0x0D => "Smuggler",
+ 0x0E => "Fremen",
+ 0x0F => "Unknown",
+ _ => $"NPC #{npcId:X2}"
+ };
+ }
+
+ // Display subsystem accessors (high DS offsets)
+
+ ///
+ /// Gets the HNM video finished flag (DS:0xDBE7).
+ ///
+ public byte GetHnmFinishedFlag() => UInt8[0xDBE7];
+
+ ///
+ /// Gets the HNM frame counter (DS:0xDBE8).
+ ///
+ public ushort GetHnmFrameCounter() => UInt16[0xDBE8];
+
+ ///
+ /// Gets the HNM file offset (DS:0xDBEA).
+ ///
+ public uint GetHnmFileOffset() => UInt32[0xDBEA];
+
+ ///
+ /// Gets the HNM file remaining bytes (DS:0xDBEE).
+ ///
+ public uint GetHnmFileRemain() => UInt32[0xDBEE];
+
+ ///
+ /// Gets the front framebuffer address (DS:0xDBD6).
+ ///
+ public ushort GetFramebufferFront() => UInt16[0xDBD6];
+
+ ///
+ /// Gets the back framebuffer address (DS:0xDBD8).
+ ///
+ public ushort GetFramebufferBack() => UInt16[0xDBD8];
+
+ ///
+ /// Gets the active framebuffer address (DS:0xDBDA).
+ ///
+ public ushort GetFramebufferActive() => UInt16[0xDBDA];
+
+ ///
+ /// Gets the screen buffer address (DS:0xDBDC).
+ ///
+ public ushort GetScreenBuffer() => UInt16[0xDBDC];
+
+ ///
+ /// Gets the transition bitmask (DS:0xDBDE).
+ ///
+ public ushort GetTransitionBitmask() => UInt16[0xDBDE];
+
+ ///
+ /// Gets the mouse X position (DS:0xDC38).
+ ///
+ public ushort GetMousePosX() => UInt16[0xDC38];
+
+ ///
+ /// Gets the mouse Y position (DS:0xDC3A).
+ ///
+ public ushort GetMousePosY() => UInt16[0xDC3A];
+
+ ///
+ /// Gets the cursor type (DS:0xDC3C).
+ ///
+ public byte GetCursorType() => UInt8[0xDC3C];
+
+ // Property-style accessors for ViewModel compatibility
+ public byte GameStage => GetGamePhase();
+ public ushort Spice => GetSpice();
+ public ushort DateTimeRaw => GetDateTime();
+ public byte ContactDistance => GetContactDistance();
+ public byte Follower1Id => GetFollower1Id();
+ public byte Follower2Id => GetFollower2Id();
+ public byte CurrentRoomId => GetCurrentRoomId();
+ public byte CurrentSpeakerId => GetCurrentSpeakerId();
+ public ushort DialogueState => (ushort)GetDialogueState();
+
+ // Display/HNM properties
+ public byte HnmFinishedFlag => GetHnmFinishedFlag();
+ public ushort HnmFrameCounter => GetHnmFrameCounter();
+ public uint HnmFileOffset => GetHnmFileOffset();
+ public uint HnmFileRemain => GetHnmFileRemain();
+ public ushort FramebufferFront => GetFramebufferFront();
+ public ushort FramebufferBack => GetFramebufferBack();
+ public ushort FramebufferActive => GetFramebufferActive();
+ public ushort ScreenBuffer => GetScreenBuffer();
+ public ushort MousePosX => GetMousePosX();
+ public ushort MousePosY => GetMousePosY();
+ public byte CursorType => GetCursorType();
+ public byte TransitionBitmask => (byte)GetTransitionBitmask();
+
+ // Helper methods for formatting
+ public string GetFormattedSpice() => $"{Spice:N0} kg";
+
+ public string GetFormattedDateTime() {
+ ushort raw = DateTimeRaw;
+ // Simple approximation: day = high byte, hour = low byte / 10
+ int day = (raw >> 8) + 1;
+ int hour = (raw & 0xFF) / 10;
+ return $"Day {day}, {hour:D2}:00";
+ }
+
+ public string GetGameStageDescription() {
+ return GameStage switch {
+ 0x01 => "Start of game",
+ 0x02 => "Talked about stillsuit",
+ 0x03 => "Learning about stillsuit",
+ 0x04 => "Stillsuit briefing complete",
+ 0x05 => "Met spice prospectors",
+ 0x06 => "Got stillsuits",
+ _ => $"Stage 0x{GameStage:X2}"
+ };
+ }
+
+ public int GetActiveTroopCount() {
+ int count = 0;
+ for (int i = 0; i < MaxTroops; i++) {
+ if (IsTroopActive(i)) count++;
+ }
+ return count;
+ }
+
+ public int GetDiscoveredLocationCount() => GetDiscoveredSietchCount();
+
+ // Placeholder properties that may not exist in current memory layout
+ public ushort WorldPosX => 0; // TODO: Find actual offset
+ public ushort WorldPosY => 0; // TODO: Find actual offset
+ public ushort WaterReserve => 0; // TODO: Find actual offset
+ public ushort SpiceReserve => 0; // TODO: Find actual offset
+ public uint Money => 0; // TODO: Find actual offset
+ public byte MilitaryStrength => 0; // TODO: Find actual offset
+ public byte EcologyProgress => 0; // TODO: Find actual offset
+
+ // Additional HNM properties (placeholders)
+ public ushort HnmCounter2 => 0;
+ public byte CurrentHnmResourceFlag => 0;
+ public ushort HnmVideoId => 0;
+ public ushort HnmActiveVideoId => 0;
+
+ // Additional mouse properties
+ public ushort MouseDrawPosX => MousePosX;
+ public ushort MouseDrawPosY => MousePosY;
+ public byte CursorHideCounter => 0;
+ public ushort MapCursorType => 0;
+
+ // Sound property
+ public byte IsSoundPresent => 0;
+ public ushort MidiFunc5ReturnBx => 0;
+}
diff --git a/src/Cryogenic/GameEngineWindow/ViewModels/DuneGameStateViewModel.cs b/src/Cryogenic/GameEngineWindow/ViewModels/DuneGameStateViewModel.cs
new file mode 100644
index 0000000..e940fd6
--- /dev/null
+++ b/src/Cryogenic/GameEngineWindow/ViewModels/DuneGameStateViewModel.cs
@@ -0,0 +1,742 @@
+namespace Cryogenic.GameEngineWindow.ViewModels;
+
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+using Cryogenic.GameEngineWindow.Models;
+
+using Spice86.Core.Emulator.CPU.Registers;
+using Spice86.Core.Emulator.Memory.ReaderWriter;
+using Spice86.Core.Emulator.VM;
+
+public class DuneGameStateViewModel : INotifyPropertyChanged, IDisposable {
+ private readonly DuneGameState _gameState;
+ private readonly IPauseHandler? _pauseHandler;
+ private bool _disposed;
+ private bool _isPaused;
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public bool IsPaused {
+ get => _isPaused;
+ private set {
+ if (_isPaused != value) {
+ _isPaused = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public DuneGameStateViewModel(IByteReaderWriter memory, SegmentRegisters segmentRegisters, IPauseHandler? pauseHandler = null) {
+ _gameState = new DuneGameState(memory, segmentRegisters);
+ _pauseHandler = pauseHandler;
+
+ Locations = new ObservableCollection();
+ Troops = new ObservableCollection();
+ Npcs = new ObservableCollection();
+ Smugglers = new ObservableCollection();
+ Sietches = new ObservableCollection();
+
+ for (int i = 0; i < DuneGameState.MaxLocations; i++) {
+ Locations.Add(new LocationViewModel(i));
+ Sietches.Add(new SietchViewModel(i));
+ }
+ for (int i = 0; i < DuneGameState.MaxTroops; i++) {
+ Troops.Add(new TroopViewModel(i));
+ }
+ for (int i = 0; i < DuneGameState.MaxNpcs; i++) {
+ Npcs.Add(new NpcViewModel(i));
+ }
+ for (int i = 0; i < DuneGameState.MaxSmugglers; i++) {
+ Smugglers.Add(new SmugglerViewModel(i));
+ }
+
+ if (_pauseHandler != null) {
+ _pauseHandler.Paused += OnEmulatorPaused;
+ _pauseHandler.Resumed += OnEmulatorResumed;
+ IsPaused = _pauseHandler.IsPaused;
+ }
+ }
+
+ private void OnEmulatorPaused() {
+ IsPaused = true;
+ RefreshAllData();
+ }
+
+ private void OnEmulatorResumed() {
+ IsPaused = false;
+ }
+
+ public void RefreshAllData() {
+ RefreshLocations();
+ RefreshSietches();
+ RefreshTroops();
+ RefreshNpcs();
+ RefreshSmugglers();
+ NotifyGameStateProperties();
+ }
+
+ // Core game state
+ public ushort GameElapsedTime => _gameState.GameElapsedTime;
+ public string GameElapsedTimeHex => $"0x{GameElapsedTime:X4}";
+ public byte CharismaRaw => _gameState.CharismaRaw;
+ public int CharismaDisplayed => _gameState.CharismaDisplayed;
+ public string CharismaDisplay => $"{CharismaDisplayed} (raw: 0x{CharismaRaw:X2})";
+ public byte GameStage => _gameState.GameStage;
+ public string GameStageDisplay => _gameState.GetGameStageDescription();
+ public ushort Spice => _gameState.Spice;
+ public string SpiceDisplay => _gameState.GetFormattedSpice();
+ public ushort DateTimeRaw => _gameState.DateTimeRaw;
+ public string DateTimeDisplay => _gameState.GetFormattedDateTime();
+ public byte ContactDistance => _gameState.ContactDistance;
+ public string ContactDistanceDisplay => $"{ContactDistance} (0x{ContactDistance:X2})";
+
+ // HNM Video state
+ public byte HnmFinishedFlag => _gameState.HnmFinishedFlag;
+ public ushort HnmFrameCounter => _gameState.HnmFrameCounter;
+ public ushort HnmCounter2 => _gameState.HnmCounter2;
+ public byte CurrentHnmResourceFlag => _gameState.CurrentHnmResourceFlag;
+ public ushort HnmVideoId => _gameState.HnmVideoId;
+ public ushort HnmActiveVideoId => _gameState.HnmActiveVideoId;
+ public uint HnmFileOffset => _gameState.HnmFileOffset;
+ public uint HnmFileRemain => _gameState.HnmFileRemain;
+ public string HnmVideoIdDisplay => $"0x{HnmVideoId:X4}";
+ public string HnmFileOffsetDisplay => $"0x{HnmFileOffset:X8}";
+ public string HnmFileRemainDisplay => $"0x{HnmFileRemain:X8} ({HnmFileRemain} bytes)";
+
+ // Display and graphics
+ public ushort FramebufferFront => _gameState.FramebufferFront;
+ public ushort ScreenBuffer => _gameState.ScreenBuffer;
+ public ushort FramebufferActive => _gameState.FramebufferActive;
+ public ushort FramebufferBack => _gameState.FramebufferBack;
+ public string FramebufferFrontDisplay => $"0x{FramebufferFront:X4}";
+ public string ScreenBufferDisplay => $"0x{ScreenBuffer:X4}";
+ public string FramebufferActiveDisplay => $"0x{FramebufferActive:X4}";
+ public string FramebufferBackDisplay => $"0x{FramebufferBack:X4}";
+
+ // Mouse and cursor
+ public ushort MousePosY => _gameState.MousePosY;
+ public ushort MousePosX => _gameState.MousePosX;
+ public string MousePositionDisplay => $"({MousePosX}, {MousePosY})";
+ public ushort MouseDrawPosY => _gameState.MouseDrawPosY;
+ public ushort MouseDrawPosX => _gameState.MouseDrawPosX;
+ public string MouseDrawPositionDisplay => $"({MouseDrawPosX}, {MouseDrawPosY})";
+ public byte CursorHideCounter => _gameState.CursorHideCounter;
+ public ushort MapCursorType => _gameState.MapCursorType;
+ public string MapCursorTypeDisplay => $"0x{MapCursorType:X4}";
+
+ // Sound
+ public byte IsSoundPresent => _gameState.IsSoundPresent;
+ public string IsSoundPresentDisplay => IsSoundPresent != 0 ? "Yes" : "No";
+ public ushort MidiFunc5ReturnBx => _gameState.MidiFunc5ReturnBx;
+ public string MidiFunc5ReturnBxDisplay => $"0x{MidiFunc5ReturnBx:X4}";
+
+ // Effects
+ public byte TransitionBitmask => _gameState.TransitionBitmask;
+ public string TransitionBitmaskDisplay => $"0x{TransitionBitmask:X2} (0b{Convert.ToString(TransitionBitmask, 2).PadLeft(8, '0')})";
+
+ public ObservableCollection Sietches { get; }
+ public int DiscoveredSietchCount => _gameState.GetDiscoveredSietchCount();
+ public string DiscoveredSietchCountDisplay => $"{DiscoveredSietchCount} / {DuneGameState.MaxLocations}";
+
+ public ObservableCollection Locations { get; }
+ public int DiscoveredLocationCount => _gameState.GetDiscoveredLocationCount();
+ public string DiscoveredLocationCountDisplay => $"{DiscoveredLocationCount} / {DuneGameState.MaxLocations}";
+
+ public ObservableCollection Troops { get; }
+ public int ActiveTroopCount => _gameState.GetActiveTroopCount();
+ public string ActiveTroopCountDisplay => $"{ActiveTroopCount} / {DuneGameState.MaxTroops}";
+
+ public ObservableCollection Npcs { get; }
+ public ObservableCollection Smugglers { get; }
+
+ // NPCs/Characters
+ public byte Follower1Id => _gameState.Follower1Id;
+ public string Follower1Name => DuneGameState.GetNpcName(Follower1Id);
+ public byte Follower2Id => _gameState.Follower2Id;
+ public string Follower2Name => DuneGameState.GetNpcName(Follower2Id);
+ public byte CurrentRoomId => _gameState.CurrentRoomId;
+ public string CurrentRoomDisplay => $"Room #{CurrentRoomId} (0x{CurrentRoomId:X2})";
+ public ushort WorldPosX => _gameState.WorldPosX;
+ public ushort WorldPosY => _gameState.WorldPosY;
+ public string WorldPositionDisplay => $"({WorldPosX}, {WorldPosY})";
+ public byte CurrentSpeakerId => _gameState.CurrentSpeakerId;
+ public string CurrentSpeakerName => DuneGameState.GetNpcName(CurrentSpeakerId);
+ public ushort DialogueState => _gameState.DialogueState;
+ public string DialogueStateDisplay => $"0x{DialogueState:X4}";
+
+ // Player stats
+ public ushort WaterReserve => _gameState.WaterReserve;
+ public string WaterReserveDisplay => $"{WaterReserve} units";
+ public ushort SpiceReserve => _gameState.SpiceReserve;
+ public string SpiceReserveDisplay => $"{SpiceReserve} kg";
+ public uint Money => _gameState.Money;
+ public string MoneyDisplay => $"{Money:N0} solaris";
+ public byte MilitaryStrength => _gameState.MilitaryStrength;
+ public string MilitaryStrengthDisplay => $"{MilitaryStrength} (0x{MilitaryStrength:X2})";
+ public byte EcologyProgress => _gameState.EcologyProgress;
+ public string EcologyProgressDisplay => $"{EcologyProgress}% (0x{EcologyProgress:X2})";
+
+ private void RefreshLocations() {
+ for (int i = 0; i < DuneGameState.MaxLocations; i++) {
+ Locations[i].NameFirst = _gameState.GetLocationNameFirst(i);
+ Locations[i].NameSecond = _gameState.GetLocationNameSecond(i);
+ Locations[i].Status = _gameState.GetLocationStatus(i);
+ Locations[i].Appearance = _gameState.GetLocationAppearance(i);
+ Locations[i].HousedTroopId = _gameState.GetLocationHousedTroopId(i);
+ Locations[i].SpiceFieldId = _gameState.GetLocationSpiceFieldId(i);
+ Locations[i].SpiceAmount = _gameState.GetLocationSpiceAmount(i);
+ Locations[i].SpiceDensity = _gameState.GetLocationSpiceDensity(i);
+ Locations[i].Harvesters = _gameState.GetLocationHarvesters(i);
+ Locations[i].Ornithopters = _gameState.GetLocationOrnithopters(i);
+ Locations[i].Water = _gameState.GetLocationWater(i);
+ var coords = _gameState.GetLocationCoordinates(i);
+ Locations[i].X = coords.X;
+ Locations[i].Y = coords.Y;
+ }
+ }
+
+ private void RefreshSietches() {
+ for (int i = 0; i < DuneGameState.MaxLocations; i++) {
+ Sietches[i].Status = _gameState.GetSietchStatus(i);
+ Sietches[i].SpiceField = _gameState.GetSietchSpiceField(i);
+ var coords = _gameState.GetSietchCoordinates(i);
+ Sietches[i].X = coords.X;
+ Sietches[i].Y = coords.Y;
+ }
+ }
+
+ private void RefreshTroops() {
+ for (int i = 0; i < DuneGameState.MaxTroops; i++) {
+ Troops[i].TroopId = _gameState.GetTroopId(i);
+ Troops[i].Occupation = _gameState.GetTroopOccupation(i);
+ Troops[i].OccupationName = DuneGameState.GetTroopOccupationDescription(Troops[i].Occupation);
+ Troops[i].IsFremen = DuneGameState.IsTroopFremen(Troops[i].Occupation);
+ Troops[i].Position = _gameState.GetTroopPosition(i);
+ Troops[i].Motivation = _gameState.GetTroopMotivation(i);
+ Troops[i].Dissatisfaction = _gameState.GetTroopDissatisfaction(i);
+ Troops[i].SpiceSkill = _gameState.GetTroopSpiceSkill(i);
+ Troops[i].ArmySkill = _gameState.GetTroopArmySkill(i);
+ Troops[i].EcologySkill = _gameState.GetTroopEcologySkill(i);
+ Troops[i].Equipment = _gameState.GetTroopEquipment(i);
+ Troops[i].EquipmentDescription = DuneGameState.GetTroopEquipmentDescription(Troops[i].Equipment);
+ Troops[i].Population = _gameState.GetTroopPopulation(i);
+ }
+ }
+
+ private void RefreshNpcs() {
+ for (int i = 0; i < DuneGameState.MaxNpcs; i++) {
+ Npcs[i].SpriteId = _gameState.GetNpcSpriteId(i);
+ Npcs[i].RoomLocation = _gameState.GetNpcRoomLocation(i);
+ Npcs[i].PlaceType = _gameState.GetNpcPlaceType(i);
+ Npcs[i].ExactPlace = _gameState.GetNpcExactPlace(i);
+ Npcs[i].DialogueFlag = _gameState.GetNpcDialogueFlag(i);
+ }
+ }
+
+ private void RefreshSmugglers() {
+ for (int i = 0; i < DuneGameState.MaxSmugglers; i++) {
+ Smugglers[i].Region = _gameState.GetSmugglerRegion(i);
+ Smugglers[i].LocationName = _gameState.GetSmugglerLocationName(i);
+ Smugglers[i].WillingnessToHaggle = _gameState.GetSmugglerWillingnessToHaggle(i);
+ Smugglers[i].Harvesters = _gameState.GetSmugglerHarvesters(i);
+ Smugglers[i].Ornithopters = _gameState.GetSmugglerOrnithopters(i);
+ Smugglers[i].KrysKnives = _gameState.GetSmugglerKrysKnives(i);
+ Smugglers[i].LaserGuns = _gameState.GetSmugglerLaserGuns(i);
+ Smugglers[i].WeirdingModules = _gameState.GetSmugglerWeirdingModules(i);
+ Smugglers[i].HarvesterPrice = _gameState.GetSmugglerHarvesterPrice(i);
+ Smugglers[i].OrnithopterPrice = _gameState.GetSmugglerOrnithopterPrice(i);
+ Smugglers[i].KrysKnifePrice = _gameState.GetSmugglerKrysKnifePrice(i);
+ Smugglers[i].LaserGunPrice = _gameState.GetSmugglerLaserGunPrice(i);
+ Smugglers[i].WeirdingModulePrice = _gameState.GetSmugglerWeirdingModulePrice(i);
+ }
+ }
+
+ private void NotifyGameStateProperties() {
+ OnPropertyChanged(nameof(GameElapsedTime));
+ OnPropertyChanged(nameof(GameElapsedTimeHex));
+ OnPropertyChanged(nameof(DateTimeRaw));
+ OnPropertyChanged(nameof(DateTimeDisplay));
+ OnPropertyChanged(nameof(Spice));
+ OnPropertyChanged(nameof(SpiceDisplay));
+ OnPropertyChanged(nameof(CharismaRaw));
+ OnPropertyChanged(nameof(CharismaDisplayed));
+ OnPropertyChanged(nameof(CharismaDisplay));
+ OnPropertyChanged(nameof(ContactDistance));
+ OnPropertyChanged(nameof(ContactDistanceDisplay));
+ OnPropertyChanged(nameof(GameStage));
+ OnPropertyChanged(nameof(GameStageDisplay));
+ OnPropertyChanged(nameof(HnmFinishedFlag));
+ OnPropertyChanged(nameof(HnmFrameCounter));
+ OnPropertyChanged(nameof(HnmCounter2));
+ OnPropertyChanged(nameof(CurrentHnmResourceFlag));
+ OnPropertyChanged(nameof(HnmVideoId));
+ OnPropertyChanged(nameof(HnmVideoIdDisplay));
+ OnPropertyChanged(nameof(HnmActiveVideoId));
+ OnPropertyChanged(nameof(HnmFileOffset));
+ OnPropertyChanged(nameof(HnmFileOffsetDisplay));
+ OnPropertyChanged(nameof(HnmFileRemain));
+ OnPropertyChanged(nameof(HnmFileRemainDisplay));
+ OnPropertyChanged(nameof(FramebufferFront));
+ OnPropertyChanged(nameof(FramebufferFrontDisplay));
+ OnPropertyChanged(nameof(ScreenBuffer));
+ OnPropertyChanged(nameof(ScreenBufferDisplay));
+ OnPropertyChanged(nameof(FramebufferActive));
+ OnPropertyChanged(nameof(FramebufferActiveDisplay));
+ OnPropertyChanged(nameof(FramebufferBack));
+ OnPropertyChanged(nameof(FramebufferBackDisplay));
+ OnPropertyChanged(nameof(TransitionBitmask));
+ OnPropertyChanged(nameof(TransitionBitmaskDisplay));
+ OnPropertyChanged(nameof(MousePosX));
+ OnPropertyChanged(nameof(MousePosY));
+ OnPropertyChanged(nameof(MousePositionDisplay));
+ OnPropertyChanged(nameof(MouseDrawPosX));
+ OnPropertyChanged(nameof(MouseDrawPosY));
+ OnPropertyChanged(nameof(MouseDrawPositionDisplay));
+ OnPropertyChanged(nameof(CursorHideCounter));
+ OnPropertyChanged(nameof(MapCursorType));
+ OnPropertyChanged(nameof(MapCursorTypeDisplay));
+ OnPropertyChanged(nameof(IsSoundPresent));
+ OnPropertyChanged(nameof(IsSoundPresentDisplay));
+ OnPropertyChanged(nameof(MidiFunc5ReturnBx));
+ OnPropertyChanged(nameof(MidiFunc5ReturnBxDisplay));
+ OnPropertyChanged(nameof(DiscoveredSietchCount));
+ OnPropertyChanged(nameof(DiscoveredSietchCountDisplay));
+ OnPropertyChanged(nameof(ActiveTroopCount));
+ OnPropertyChanged(nameof(ActiveTroopCountDisplay));
+ OnPropertyChanged(nameof(Follower1Id));
+ OnPropertyChanged(nameof(Follower1Name));
+ OnPropertyChanged(nameof(Follower2Id));
+ OnPropertyChanged(nameof(Follower2Name));
+ OnPropertyChanged(nameof(CurrentRoomId));
+ OnPropertyChanged(nameof(CurrentRoomDisplay));
+ OnPropertyChanged(nameof(WorldPosX));
+ OnPropertyChanged(nameof(WorldPosY));
+ OnPropertyChanged(nameof(WorldPositionDisplay));
+ OnPropertyChanged(nameof(CurrentSpeakerId));
+ OnPropertyChanged(nameof(CurrentSpeakerName));
+ OnPropertyChanged(nameof(DialogueState));
+ OnPropertyChanged(nameof(DialogueStateDisplay));
+ OnPropertyChanged(nameof(WaterReserve));
+ OnPropertyChanged(nameof(WaterReserveDisplay));
+ OnPropertyChanged(nameof(SpiceReserve));
+ OnPropertyChanged(nameof(SpiceReserveDisplay));
+ OnPropertyChanged(nameof(Money));
+ OnPropertyChanged(nameof(MoneyDisplay));
+ OnPropertyChanged(nameof(MilitaryStrength));
+ OnPropertyChanged(nameof(MilitaryStrengthDisplay));
+ OnPropertyChanged(nameof(EcologyProgress));
+ OnPropertyChanged(nameof(EcologyProgressDisplay));
+ OnPropertyChanged(nameof(DiscoveredLocationCount));
+ OnPropertyChanged(nameof(DiscoveredLocationCountDisplay));
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ public void Dispose() {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing) {
+ if (!_disposed) {
+ if (disposing && _pauseHandler != null) {
+ _pauseHandler.Paused -= OnEmulatorPaused;
+ _pauseHandler.Resumed -= OnEmulatorResumed;
+ }
+ _disposed = true;
+ }
+ }
+}
+
+public class SietchViewModel : INotifyPropertyChanged {
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public SietchViewModel(int index) {
+ Index = index;
+ }
+
+ public int Index { get; }
+
+ private byte _status;
+ public byte Status {
+ get => _status;
+ set { if (_status != value) { _status = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsDiscovered)); } }
+ }
+
+ public bool IsDiscovered => Status != 0;
+
+ private ushort _spiceField;
+ public ushort SpiceField {
+ get => _spiceField;
+ set { if (_spiceField != value) { _spiceField = value; OnPropertyChanged(); } }
+ }
+
+ private ushort _x;
+ public ushort X {
+ get => _x;
+ set { if (_x != value) { _x = value; OnPropertyChanged(); } }
+ }
+
+ private ushort _y;
+ public ushort Y {
+ get => _y;
+ set { if (_y != value) { _y = value; OnPropertyChanged(); } }
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
+
+public class TroopViewModel : INotifyPropertyChanged {
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public TroopViewModel(int index) {
+ Index = index;
+ }
+
+ public int Index { get; }
+
+ private byte _troopId;
+ public byte TroopId {
+ get => _troopId;
+ set { if (_troopId != value) { _troopId = value; OnPropertyChanged(); } }
+ }
+
+ private byte _occupation;
+ public byte Occupation {
+ get => _occupation;
+ set { if (_occupation != value) { _occupation = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsActive)); } }
+ }
+
+ public bool IsActive => Occupation != 0;
+
+ private string _occupationName = "Idle";
+ public string OccupationName {
+ get => _occupationName;
+ set { if (_occupationName != value) { _occupationName = value; OnPropertyChanged(); } }
+ }
+
+ private bool _isFremen;
+ public bool IsFremen {
+ get => _isFremen;
+ set { if (_isFremen != value) { _isFremen = value; OnPropertyChanged(); } }
+ }
+
+ private byte _position;
+ public byte Position {
+ get => _position;
+ set { if (_position != value) { _position = value; OnPropertyChanged(); } }
+ }
+
+ private byte _location;
+ public byte Location {
+ get => _location;
+ set { if (_location != value) { _location = value; OnPropertyChanged(); } }
+ }
+
+ private byte _motivation;
+ public byte Motivation {
+ get => _motivation;
+ set { if (_motivation != value) { _motivation = value; OnPropertyChanged(); } }
+ }
+
+ private byte _dissatisfaction;
+ public byte Dissatisfaction {
+ get => _dissatisfaction;
+ set { if (_dissatisfaction != value) { _dissatisfaction = value; OnPropertyChanged(); } }
+ }
+
+ private byte _spiceSkill;
+ public byte SpiceSkill {
+ get => _spiceSkill;
+ set { if (_spiceSkill != value) { _spiceSkill = value; OnPropertyChanged(); } }
+ }
+
+ private byte _armySkill;
+ public byte ArmySkill {
+ get => _armySkill;
+ set { if (_armySkill != value) { _armySkill = value; OnPropertyChanged(); } }
+ }
+
+ private byte _ecologySkill;
+ public byte EcologySkill {
+ get => _ecologySkill;
+ set { if (_ecologySkill != value) { _ecologySkill = value; OnPropertyChanged(); } }
+ }
+
+ private byte _equipment;
+ public byte Equipment {
+ get => _equipment;
+ set { if (_equipment != value) { _equipment = value; OnPropertyChanged(); } }
+ }
+
+ private string _equipmentDescription = "None";
+ public string EquipmentDescription {
+ get => _equipmentDescription;
+ set { if (_equipmentDescription != value) { _equipmentDescription = value; OnPropertyChanged(); } }
+ }
+
+ private ushort _population;
+ public ushort Population {
+ get => _population;
+ set { if (_population != value) { _population = value; OnPropertyChanged(); } }
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
+
+public class LocationViewModel : INotifyPropertyChanged {
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public LocationViewModel(int index) {
+ Index = index;
+ }
+
+ public int Index { get; }
+
+ private byte _nameFirst;
+ public byte NameFirst {
+ get => _nameFirst;
+ set { if (_nameFirst != value) { _nameFirst = value; OnPropertyChanged(); OnPropertyChanged(nameof(Name)); OnPropertyChanged(nameof(LocationType)); } }
+ }
+
+ private byte _nameSecond;
+ public byte NameSecond {
+ get => _nameSecond;
+ set { if (_nameSecond != value) { _nameSecond = value; OnPropertyChanged(); OnPropertyChanged(nameof(Name)); OnPropertyChanged(nameof(LocationType)); } }
+ }
+
+ public string Name => DuneGameState.GetLocationNameStr(NameFirst, NameSecond);
+ public string LocationType {
+ get {
+ byte nameSecond = NameSecond;
+ return nameSecond switch {
+ 0x00 => "Atreides Palace",
+ 0x01 => "Harkonnen Palace",
+ 0x02 => "Village (Pyons)",
+ >= 0x03 and <= 0x09 => "Sietch",
+ 0x0B => "Sietch",
+ _ => $"Unknown (0x{nameSecond:X2})"
+ };
+ }
+ }
+
+ private byte _status;
+ public byte Status {
+ get => _status;
+ set { if (_status != value) { _status = value; OnPropertyChanged(); OnPropertyChanged(nameof(IsDiscovered)); } }
+ }
+
+ public bool IsDiscovered => (Status & DuneGameState.LocationStatusUndiscovered) == 0;
+
+ private byte _appearance;
+ public byte Appearance {
+ get => _appearance;
+ set { if (_appearance != value) { _appearance = value; OnPropertyChanged(); } }
+ }
+
+ private byte _housedTroopId;
+ public byte HousedTroopId {
+ get => _housedTroopId;
+ set { if (_housedTroopId != value) { _housedTroopId = value; OnPropertyChanged(); } }
+ }
+
+ private byte _spiceFieldId;
+ public byte SpiceFieldId {
+ get => _spiceFieldId;
+ set { if (_spiceFieldId != value) { _spiceFieldId = value; OnPropertyChanged(); } }
+ }
+
+ private byte _spiceAmount;
+ public byte SpiceAmount {
+ get => _spiceAmount;
+ set { if (_spiceAmount != value) { _spiceAmount = value; OnPropertyChanged(); } }
+ }
+
+ private byte _spiceDensity;
+ public byte SpiceDensity {
+ get => _spiceDensity;
+ set { if (_spiceDensity != value) { _spiceDensity = value; OnPropertyChanged(); } }
+ }
+
+ private byte _harvesters;
+ public byte Harvesters {
+ get => _harvesters;
+ set { if (_harvesters != value) { _harvesters = value; OnPropertyChanged(); } }
+ }
+
+ private byte _ornithopters;
+ public byte Ornithopters {
+ get => _ornithopters;
+ set { if (_ornithopters != value) { _ornithopters = value; OnPropertyChanged(); } }
+ }
+
+ private byte _water;
+ public byte Water {
+ get => _water;
+ set { if (_water != value) { _water = value; OnPropertyChanged(); } }
+ }
+
+ private ushort _x;
+ public ushort X {
+ get => _x;
+ set { if (_x != value) { _x = value; OnPropertyChanged(); } }
+ }
+
+ private ushort _y;
+ public ushort Y {
+ get => _y;
+ set { if (_y != value) { _y = value; OnPropertyChanged(); } }
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
+
+public class NpcViewModel : INotifyPropertyChanged {
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public NpcViewModel(int index) {
+ Index = index;
+ }
+
+ public int Index { get; }
+ public string Name => DuneGameState.GetNpcName((byte)(Index + 1));
+
+ private byte _spriteId;
+ public byte SpriteId {
+ get => _spriteId;
+ set { if (_spriteId != value) { _spriteId = value; OnPropertyChanged(); } }
+ }
+
+ private byte _roomLocation;
+ public byte RoomLocation {
+ get => _roomLocation;
+ set { if (_roomLocation != value) { _roomLocation = value; OnPropertyChanged(); } }
+ }
+
+ private byte _placeType;
+ public byte PlaceType {
+ get => _placeType;
+ set { if (_placeType != value) { _placeType = value; OnPropertyChanged(); OnPropertyChanged(nameof(PlaceTypeDescription)); } }
+ }
+
+ public string PlaceTypeDescription => DuneGameState.GetNpcPlaceTypeDescription(PlaceType);
+
+ private byte _exactPlace;
+ public byte ExactPlace {
+ get => _exactPlace;
+ set { if (_exactPlace != value) { _exactPlace = value; OnPropertyChanged(); } }
+ }
+
+ private byte _dialogueFlag;
+ public byte DialogueFlag {
+ get => _dialogueFlag;
+ set { if (_dialogueFlag != value) { _dialogueFlag = value; OnPropertyChanged(); } }
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
+
+public class SmugglerViewModel : INotifyPropertyChanged {
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ public SmugglerViewModel(int index) {
+ Index = index;
+ }
+
+ public int Index { get; }
+
+ private byte _region;
+ public byte Region {
+ get => _region;
+ set { if (_region != value) { _region = value; OnPropertyChanged(); } }
+ }
+
+ private string _locationName = "";
+ public string LocationName {
+ get => _locationName;
+ set { if (_locationName != value) { _locationName = value; OnPropertyChanged(); } }
+ }
+
+ private byte _willingnessToHaggle;
+ public byte WillingnessToHaggle {
+ get => _willingnessToHaggle;
+ set { if (_willingnessToHaggle != value) { _willingnessToHaggle = value; OnPropertyChanged(); } }
+ }
+
+ private byte _harvesters;
+ public byte Harvesters {
+ get => _harvesters;
+ set { if (_harvesters != value) { _harvesters = value; OnPropertyChanged(); } }
+ }
+
+ private byte _ornithopters;
+ public byte Ornithopters {
+ get => _ornithopters;
+ set { if (_ornithopters != value) { _ornithopters = value; OnPropertyChanged(); } }
+ }
+
+ private byte _krysKnives;
+ public byte KrysKnives {
+ get => _krysKnives;
+ set { if (_krysKnives != value) { _krysKnives = value; OnPropertyChanged(); } }
+ }
+
+ private byte _laserGuns;
+ public byte LaserGuns {
+ get => _laserGuns;
+ set { if (_laserGuns != value) { _laserGuns = value; OnPropertyChanged(); } }
+ }
+
+ private byte _weirdingModules;
+ public byte WeirdingModules {
+ get => _weirdingModules;
+ set { if (_weirdingModules != value) { _weirdingModules = value; OnPropertyChanged(); } }
+ }
+
+ private ushort _harvesterPrice;
+ public ushort HarvesterPrice {
+ get => _harvesterPrice;
+ set { if (_harvesterPrice != value) { _harvesterPrice = value; OnPropertyChanged(); } }
+ }
+
+ private ushort _ornithopterPrice;
+ public ushort OrnithopterPrice {
+ get => _ornithopterPrice;
+ set { if (_ornithopterPrice != value) { _ornithopterPrice = value; OnPropertyChanged(); } }
+ }
+
+ private ushort _krysKnifePrice;
+ public ushort KrysKnifePrice {
+ get => _krysKnifePrice;
+ set { if (_krysKnifePrice != value) { _krysKnifePrice = value; OnPropertyChanged(); } }
+ }
+
+ private ushort _laserGunPrice;
+ public ushort LaserGunPrice {
+ get => _laserGunPrice;
+ set { if (_laserGunPrice != value) { _laserGunPrice = value; OnPropertyChanged(); } }
+ }
+
+ private ushort _weirdingModulePrice;
+ public ushort WeirdingModulePrice {
+ get => _weirdingModulePrice;
+ set { if (_weirdingModulePrice != value) { _weirdingModulePrice = value; OnPropertyChanged(); } }
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
diff --git a/src/Cryogenic/GameEngineWindow/Views/DuneGameStateWindow.axaml b/src/Cryogenic/GameEngineWindow/Views/DuneGameStateWindow.axaml
new file mode 100644
index 0000000..8263b3b
--- /dev/null
+++ b/src/Cryogenic/GameEngineWindow/Views/DuneGameStateWindow.axaml
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Cryogenic/GameEngineWindow/Views/DuneGameStateWindow.axaml.cs b/src/Cryogenic/GameEngineWindow/Views/DuneGameStateWindow.axaml.cs
new file mode 100644
index 0000000..86c5919
--- /dev/null
+++ b/src/Cryogenic/GameEngineWindow/Views/DuneGameStateWindow.axaml.cs
@@ -0,0 +1,29 @@
+namespace Cryogenic.GameEngineWindow.Views;
+
+using Avalonia.Controls;
+
+///
+/// Window that displays live Dune game engine state from memory.
+///
+///
+/// This window provides a real-time view into the game's memory state,
+/// organized into tabs for different aspects of the game engine:
+/// - Player Stats: Core player-related values (spice, charisma, game stage)
+/// - NPCs: Non-player character data (sprite, room, dialogue state)
+/// - Followers: Current party members and position
+/// - Locations: All 70 locations with status, spice, water, equipment
+/// - Troops: All 68 troops with occupation, skills, equipment
+/// - Smugglers: All 6 smugglers with inventory and prices
+/// - HNM Video: Video playback state
+/// - Display: Graphics and framebuffer information
+/// - Input: Mouse and cursor state
+/// - Sound: Audio subsystem information
+///
+public partial class DuneGameStateWindow : Window {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DuneGameStateWindow() {
+ InitializeComponent();
+ }
+}
diff --git a/src/Cryogenic/Overrides/Overrides.cs b/src/Cryogenic/Overrides/Overrides.cs
index c188f72..3666d5a 100644
--- a/src/Cryogenic/Overrides/Overrides.cs
+++ b/src/Cryogenic/Overrides/Overrides.cs
@@ -1,5 +1,6 @@
namespace Cryogenic.Overrides;
+using Cryogenic.GameEngineWindow;
using Globals;
using Spice86.Core.CLI;
@@ -59,6 +60,9 @@ public partial class Overrides : CSharpOverrideHelper {
/// Accessor for game global variables stored in CS segment 0x2538.
private ExtraGlobalsOnCsSegment0x2538 globalsOnCsSegment0X2538;
+ /// Flag to track if the game engine window has been shown.
+ private bool _gameEngineWindowShown = false;
+
///
/// Initializes the override system and registers all function replacements and hooks.
///
@@ -130,10 +134,29 @@ public void DefineOverrides() {
DefineMemoryDumpsMapping();
DefineMT32DriverCodeOverrides();
+ // Show the Game Engine Window after drivers are loaded
+ DefineGameEngineWindowTrigger();
+
// Generated code, crashes for various reasons
//DefineGeneratedCodeOverrides();
}
+ ///
+ /// Registers a hook to show the Game Engine Window after drivers are loaded.
+ ///
+ ///
+ /// The window is shown after drivers are loaded (CS1:000C) to ensure
+ /// the memory layout is stable and all game structures are initialized.
+ ///
+ private void DefineGameEngineWindowTrigger() {
+ DoOnTopOfInstruction(cs1, 0x000C, () => {
+ if (!_gameEngineWindowShown) {
+ _gameEngineWindowShown = true;
+ GameEngineWindowManager.ShowWindow(Memory, State.SegmentRegisters, Machine.PauseHandler);
+ }
+ });
+ }
+
///
/// Registers memory dump triggers at strategic points during game initialization.
///