Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 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
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
64 changes: 64 additions & 0 deletions src/Cryogenic/GameEngineWindow/GameEngineWindowManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace Cryogenic.GameEngineWindow;

using System;

using Avalonia.Threading;

using Cryogenic.GameEngineWindow.ViewModels;
using Cryogenic.GameEngineWindow.Views;

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, 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, 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;
}
170 changes: 170 additions & 0 deletions src/Cryogenic/GameEngineWindow/Models/DuneGameState.Locations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
namespace Cryogenic.GameEngineWindow.Models;

/// <summary>
/// Location/Sietch structure accessors for Dune game state.
/// </summary>
/// <remarks>
/// Location structure (28 bytes per entry, 70 max locations at 10FC:000F):
/// - Offset 0: Name first byte (region: 01-0C)
/// - Offset 1: Name second byte (type: 01-0B, 0A=village)
/// - Offset 2-7: Coordinates (6 bytes)
/// - Offset 8: Appearance type
/// - Offset 9: Housed troop ID
/// - Offset 10: Status flags
/// - Offset 11-15: Stage for discovery
/// - Offset 16: Spice field ID
/// - Offset 17: Spice amount
/// - Offset 18: Spice density
/// - Offset 19: Field J
/// - Offset 20: Harvesters count
/// - Offset 21: Ornithopters count
/// - Offset 22: Krys knives count
/// - Offset 23: Laser guns count
/// - Offset 24: Weirding modules count
/// - Offset 25: Atomics count
/// - Offset 26: Bulbs count
/// - Offset 27: Water amount
/// </remarks>
public partial class DuneGameState {
/// <summary>
/// Get the absolute address for a location entry.
/// </summary>
private uint GetLocationAddress(int index, int fieldOffset = 0) {
return LocationsBaseAddress + (uint)LocationArrayOffset + (uint)(index * LocationEntrySize) + (uint)fieldOffset;
}

public byte GetLocationNameFirst(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 0));
}

public byte GetLocationNameSecond(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 1));
}

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",
_ => $"Unknown({first:X2})"
};

str += 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;
}

public static string GetLocationTypeStr(byte second) => second switch {
0x01 => "Atreides Palace",
0x02 => "Harkonnen Palace",
0x03 => "Sietch (Tabr)",
0x04 => "Sietch (Timin)",
0x05 => "Sietch (Tuek)",
0x06 => "Sietch (Harg)",
0x07 => "Sietch (Clam)",
0x08 => "Sietch (Tsymyn)",
0x09 => "Sietch (Siet)",
0x0a => "Village (Pyons)",
0x0b => "Sietch (Pyort)",
_ => $"Unknown Type ({second:X2})"
};

public byte GetLocationAppearance(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 8));
}

public byte GetLocationHousedTroopId(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 9));
}

public byte GetLocationStatus(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 10));
}

public byte GetLocationSpiceFieldId(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 16));
}

public byte GetLocationSpiceAmount(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 17));
}

public byte GetLocationSpiceDensity(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 18));
}

public byte GetLocationHarvesters(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 20));
}

public byte GetLocationOrnithopters(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 21));
}

public byte GetLocationKrysKnives(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 22));
}

public byte GetLocationLaserGuns(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 23));
}

public byte GetLocationWeirdingModules(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 24));
}

public byte GetLocationAtomics(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 25));
}

public byte GetLocationBulbs(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 26));
}

public byte GetLocationWater(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return ReadByte(GetLocationAddress(index, 27));
}

public (ushort X, ushort Y) GetLocationCoordinates(int index) {
if (index < 0 || index >= MaxLocations) return (0, 0);
return (ReadWord(GetLocationAddress(index, 2)), ReadWord(GetLocationAddress(index, 4)));
}
}
80 changes: 80 additions & 0 deletions src/Cryogenic/GameEngineWindow/Models/DuneGameState.Npcs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace Cryogenic.GameEngineWindow.Models;

/// <summary>
/// NPC structure accessors for Dune game state.
/// </summary>
/// <remarks>
/// NPC structure (8 bytes per entry + 8 bytes padding = 16 bytes total, 16 NPCs max).
/// NPCs follow troops in memory at TroopsBaseAddress + troops size.
/// - Offset 0: Sprite identificator
/// - Offset 1: Field B
/// - Offset 2: Room location
/// - Offset 3: Type of place
/// - Offset 4: Field E
/// - Offset 5: Exact place
/// - Offset 6: For dialogue flag
/// - Offset 7: Field H
/// </remarks>
public partial class DuneGameState {
/// <summary>
/// Get the absolute address for an NPC entry.
/// NPCs follow troops in memory.
/// </summary>
private uint GetNpcAddress(int index, int fieldOffset = 0) {
uint npcsStart = TroopsBaseAddress + (uint)TroopArrayOffset + (uint)(MaxTroops * TroopEntrySize);
return npcsStart + (uint)(index * NpcTotalEntrySize) + (uint)fieldOffset;
}

public byte GetNpcSpriteId(int index) {
if (index < 0 || index >= MaxNpcs) return 0;
return ReadByte(GetNpcAddress(index, 0));
}

public byte GetNpcRoomLocation(int index) {
if (index < 0 || index >= MaxNpcs) return 0;
return ReadByte(GetNpcAddress(index, 2));
}

public byte GetNpcPlaceType(int index) {
if (index < 0 || index >= MaxNpcs) return 0;
return ReadByte(GetNpcAddress(index, 3));
}

public byte GetNpcExactPlace(int index) {
if (index < 0 || index >= MaxNpcs) return 0;
return ReadByte(GetNpcAddress(index, 5));
}

public byte GetNpcDialogueFlag(int index) {
if (index < 0 || index >= MaxNpcs) return 0;
return ReadByte(GetNpcAddress(index, 6));
}

public static string GetNpcName(byte npcId) => npcId switch {
0 => "None",
1 => "Duke Leto Atreides",
2 => "Jessica Atreides",
3 => "Thufir Hawat",
4 => "Duncan Idaho",
5 => "Gurney Halleck",
6 => "Stilgar",
7 => "Liet Kynes",
8 => "Chani",
9 => "Harah",
10 => "Baron Harkonnen",
11 => "Feyd-Rautha",
12 => "Emperor Shaddam IV",
13 => "Harkonnen Captains",
14 => "Smugglers",
15 => "The Fremen",
16 => "The Fremen",
_ => $"NPC #{npcId}"
};

public static string GetNpcPlaceTypeDescription(byte placeType) => placeType switch {
0 => "Not present",
1 => "Palace room",
2 => "Desert/Outside",
_ => $"Type {placeType:X2}"
};
}
Loading
Loading