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. ///