Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e4dd54c
Initial plan
Copilot Nov 30, 2025
43a39b8
Changes before error encountered
Copilot Nov 30, 2025
6be0f98
Add Dune Game Engine Window with live memory view
Copilot Nov 30, 2025
beb901e
Address code review feedback
Copilot Nov 30, 2025
08fe88a
Add comprehensive game state tabs: Player Stats, NPCs, Sietches, Troops
Copilot Nov 30, 2025
11e2707
Refactor: Remove regions, add dynamic data grids for sietches/troops
Copilot Nov 30, 2025
6600569
Add IPauseHandler integration, NPCs, Smugglers, Locations tabs with p…
Copilot Nov 30, 2025
1d584c0
fix: window wasn't shown
maximilien-noal Nov 30, 2025
a32fc86
Fix charisma calculation and improve documentation
Copilot Nov 30, 2025
992574a
Address code review feedback
Copilot Nov 30, 2025
be47a22
Fix NPC and Smuggler entry size calculations
Copilot Nov 30, 2025
729388c
Add location status constants and use named constant
Copilot Nov 30, 2025
a1e2f6e
Use absolute memory addressing instead of DS-relative
Copilot Nov 30, 2025
39bb850
Improve documentation for absolute memory addressing
Copilot Nov 30, 2025
5fcef0f
Use multiple memory segments for correct data access
Copilot Nov 30, 2025
9390b1e
Restore RefreshSietches call per code review feedback
Copilot Nov 30, 2025
77cf22a
Address copilot AI review comments
Copilot Nov 30, 2025
fec175c
Fix memory addresses to use single DS segment 0x1138
Copilot Nov 30, 2025
dcb9caf
Fix location coordinates to read single bytes at correct offsets
Copilot Nov 30, 2025
37aa0f0
Fix memory segment addresses based on problem statement
Copilot Nov 30, 2025
d1e9a5e
WIP: Switch to DS-relative addressing with MemoryBasedDataStructureWi…
Copilot Dec 14, 2025
4c0b6f9
Complete DuneGameState API with all required properties and methods
Copilot Dec 14, 2025
dae09d3
Fix build errors - add missing method aliases and fix static/instance…
Copilot Dec 14, 2025
d65f628
fix: some build error (not all)
maximilien-noal Dec 14, 2025
255cdfa
Add all missing accessor methods and remove HNM Video tab
Copilot Dec 14, 2025
f54d579
Fix build errors: add GetSietchStatus and make IsTroopFremen static
Copilot Dec 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/Cryogenic/GameEngineWindow/GameEngineWindowManager.cs
Original file line number Diff line number Diff line change
@@ -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;
}
150 changes: 150 additions & 0 deletions src/Cryogenic/GameEngineWindow/Models/DuneGameState.Locations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
namespace Cryogenic.GameEngineWindow.Models;

/// <summary>
/// Partial class for Location/Sietch-related memory access.
/// </summary>
/// <remarks>
/// 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...
/// </remarks>
public partial class DuneGameState {

/// <summary>
/// Gets the base DS-relative offset for a location entry.
/// </summary>
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);
}

/// <summary>
/// Gets the first byte of the location name (DS:0x0100 + index*28 + 0).
/// </summary>
public byte GetLocationNameFirst(int index) => UInt8[GetLocationOffset(index)];

/// <summary>
/// Gets the second byte of the location name (DS:0x0100 + index*28 + 1).
/// Used to determine location type (palace, village, sietch).
/// </summary>
public byte GetLocationNameSecond(int index) => UInt8[GetLocationOffset(index) + 1];

/// <summary>
/// Gets the location status flags (DS:0x0100 + index*28 + 2).
/// Bit flags: 0x01=Vegetation, 0x02=InBattle, 0x04=Inventory,
/// 0x10=Windtrap, 0x40=Prospected, 0x80=Undiscovered
/// </summary>
public byte GetLocationStatus(int index) => UInt8[GetLocationOffset(index) + 2];

/// <summary>
/// Gets the location map X coordinate (DS:0x0100 + index*28 + 3).
/// </summary>
public byte GetLocationMapX(int index) => UInt8[GetLocationOffset(index) + 3];

/// <summary>
/// Gets the location map Y coordinate (DS:0x0100 + index*28 + 4).
/// </summary>
public byte GetLocationMapY(int index) => UInt8[GetLocationOffset(index) + 4];

/// <summary>
/// Gets the location coordinates as a tuple (X, Y).
/// </summary>
public (byte X, byte Y) GetLocationCoordinates(int index) {
int offset = GetLocationOffset(index);
return (UInt8[offset + 3], UInt8[offset + 4]);
}

/// <summary>
/// Gets the spice field value (DS:0x0100 + index*28 + 5).
/// </summary>
public byte GetLocationSpiceField(int index) => UInt8[GetLocationOffset(index) + 5];

/// <summary>
/// Gets the water value (DS:0x0100 + index*28 + 6).
/// </summary>
public byte GetLocationWater(int index) => UInt8[GetLocationOffset(index) + 6];

/// <summary>
/// Gets the equipment value (DS:0x0100 + index*28 + 7).
/// </summary>
public byte GetLocationEquipment(int index) => UInt8[GetLocationOffset(index) + 7];

/// <summary>
/// Determines the location type based on name_second byte.
/// </summary>
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})"
};
}

/// <summary>
/// Gets the location appearance (DS:0x0100 + index*28 + 8).
/// </summary>
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];

/// <summary>
/// Gets location name as a display string.
/// </summary>
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;
}
}
73 changes: 73 additions & 0 deletions src/Cryogenic/GameEngineWindow/Models/DuneGameState.Npcs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
namespace Cryogenic.GameEngineWindow.Models;

/// <summary>
/// Partial class for NPC-related memory access.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
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;

/// <summary>
/// Gets the base DS-relative offset for an NPC entry.
/// </summary>
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);
}

/// <summary>
/// Gets the NPC sprite index (DS:0xAC2E + index*16 + 0).
/// </summary>
public byte GetNpcSpriteIndex(int index) => UInt8[GetNpcOffset(index)];

/// <summary>
/// Gets the NPC room index (DS:0xAC2E + index*16 + 1).
/// </summary>
public byte GetNpcRoomIndex(int index) => UInt8[GetNpcOffset(index) + 1];

/// <summary>
/// Gets the NPC place type (DS:0xAC2E + index*16 + 2).
/// </summary>
public byte GetNpcPlaceType(int index) => UInt8[GetNpcOffset(index) + 2];

/// <summary>
/// Gets the NPC dialogue state (DS:0xAC2E + index*16 + 3).
/// </summary>
public byte GetNpcDialogueState(int index) => UInt8[GetNpcOffset(index) + 3];

/// <summary>
/// Gets a description of the NPC place type.
/// </summary>
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);
}
99 changes: 99 additions & 0 deletions src/Cryogenic/GameEngineWindow/Models/DuneGameState.Smugglers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
namespace Cryogenic.GameEngineWindow.Models;

/// <summary>
/// Partial class for Smuggler-related memory access.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
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;

/// <summary>
/// Gets the base DS-relative offset for a smuggler entry.
/// </summary>
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);
}

/// <summary>
/// Gets the smuggler location index (DS:0xAD2E + index*17 + 0).
/// </summary>
public byte GetSmugglerLocationIndex(int index) => UInt8[GetSmugglerOffset(index)];

/// <summary>
/// Gets the smuggler harvester inventory count (DS:0xAD2E + index*17 + 1).
/// </summary>
public byte GetSmugglerInventoryHarvesters(int index) => UInt8[GetSmugglerOffset(index) + 1];

/// <summary>
/// Gets the smuggler ornithopter inventory count (DS:0xAD2E + index*17 + 2).
/// </summary>
public byte GetSmugglerInventoryOrnithopters(int index) => UInt8[GetSmugglerOffset(index) + 2];

/// <summary>
/// Gets the smuggler weapon inventory count (DS:0xAD2E + index*17 + 3).
/// </summary>
public byte GetSmugglerInventoryWeapons(int index) => UInt8[GetSmugglerOffset(index) + 3];

/// <summary>
/// Gets the smuggler harvester price (DS:0xAD2E + index*17 + 4, u16).
/// </summary>
public ushort GetSmugglerPriceHarvesters(int index) => UInt16[GetSmugglerOffset(index) + 4];

/// <summary>
/// Gets the smuggler ornithopter price (DS:0xAD2E + index*17 + 6, u16).
/// </summary>
public ushort GetSmugglerPriceOrnithopters(int index) => UInt16[GetSmugglerOffset(index) + 6];

/// <summary>
/// Gets the smuggler weapon price (DS:0xAD2E + index*17 + 8, u16).
/// </summary>
public ushort GetSmugglerPriceWeapons(int index) => UInt16[GetSmugglerOffset(index) + 8];

/// <summary>
/// Gets the smuggler haggle willingness (DS:0xAD2E + index*17 + 10).
/// </summary>
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);
}
Loading
Loading