Skip to content
Draft
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions src/Cryogenic/Cryogenic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

<ItemGroup>
<PackageReference Include="Spice86" Version="11.1.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
Copy link

Copilot AI Nov 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CommunityToolkit.Mvvm package is added but not used anywhere in the codebase. The ViewModels implement INotifyPropertyChanged manually rather than using attributes from this toolkit. Consider removing this unused dependency to reduce the package footprint, or utilize the toolkit's features (e.g., [ObservableProperty], [RelayCommand]) to simplify the ViewModel code.

Suggested change
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />

Copilot uses AI. Check for mistakes.
</ItemGroup>

<ItemGroup>
Expand Down
59 changes: 59 additions & 0 deletions src/Cryogenic/GameEngineWindow/GameEngineWindowManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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(() => {
if (_window != null && _isWindowOpen) {
_window.Show();
_window.Activate();
return;
}

_viewModel?.Dispose();
_viewModel = new DuneGameStateViewModel(memory, segmentRegisters, pauseHandler);
_window = new DuneGameStateWindow {
DataContext = _viewModel
};

_window.Closed += (sender, args) => {
_isWindowOpen = false;
_viewModel?.Dispose();
_viewModel = null;
_window = null;
};

_isWindowOpen = true;
_window.Show();
});
}

public static void CloseWindow() {
if (!Dispatcher.UIThread.CheckAccess()) {
Dispatcher.UIThread.Post(CloseWindow);
return;
}

_window?.Close();
_viewModel?.Dispose();
_window = null;
_viewModel = null;
_isWindowOpen = false;
}

public static bool IsWindowOpen => _isWindowOpen;
}
164 changes: 164 additions & 0 deletions src/Cryogenic/GameEngineWindow/Models/DuneGameState.Locations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
namespace Cryogenic.GameEngineWindow.Models;

/// <summary>
/// Location/Sietch structure accessors for Dune game state.
/// </summary>
/// <remarks>
/// Location structure (28 bytes per entry, 70 max locations):
/// - 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 {
public byte GetLocationNameFirst(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize)];
}

public byte GetLocationNameSecond(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 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 UInt8[LocationBaseOffset + (index * LocationEntrySize) + 8];
}

public byte GetLocationHousedTroopId(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 9];
}

public byte GetLocationStatus(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 10];
}

public byte GetLocationSpiceFieldId(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 16];
}

public byte GetLocationSpiceAmount(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 17];
}

public byte GetLocationSpiceDensity(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 18];
}

public byte GetLocationHarvesters(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 20];
}

public byte GetLocationOrnithopters(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 21];
}

public byte GetLocationKrysKnives(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 22];
}

public byte GetLocationLaserGuns(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 23];
}

public byte GetLocationWeirdingModules(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 24];
}

public byte GetLocationAtomics(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 25];
}

public byte GetLocationBulbs(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 26];
}

public byte GetLocationWater(int index) {
if (index < 0 || index >= MaxLocations) return 0;
return UInt8[LocationBaseOffset + (index * LocationEntrySize) + 27];
}

public (ushort X, ushort Y) GetLocationCoordinates(int index) {
if (index < 0 || index >= MaxLocations) return (0, 0);
var baseOffset = LocationBaseOffset + (index * LocationEntrySize);
return (UInt16[baseOffset + 2], UInt16[baseOffset + 4]);
}
}
70 changes: 70 additions & 0 deletions src/Cryogenic/GameEngineWindow/Models/DuneGameState.Npcs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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):
/// - 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 {
public byte GetNpcSpriteId(int index) {
if (index < 0 || index >= MaxNpcs) return 0;
return UInt8[NpcBaseOffset + (index * NpcEntrySize)];
}

public byte GetNpcRoomLocation(int index) {
if (index < 0 || index >= MaxNpcs) return 0;
return UInt8[NpcBaseOffset + (index * NpcEntrySize) + 2];
}

public byte GetNpcPlaceType(int index) {
if (index < 0 || index >= MaxNpcs) return 0;
return UInt8[NpcBaseOffset + (index * NpcEntrySize) + 3];
}

public byte GetNpcExactPlace(int index) {
if (index < 0 || index >= MaxNpcs) return 0;
return UInt8[NpcBaseOffset + (index * NpcEntrySize) + 5];
}

public byte GetNpcDialogueFlag(int index) {
if (index < 0 || index >= MaxNpcs) return 0;
return UInt8[NpcBaseOffset + (index * NpcEntrySize) + 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}"
};
}
88 changes: 88 additions & 0 deletions src/Cryogenic/GameEngineWindow/Models/DuneGameState.Smugglers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace Cryogenic.GameEngineWindow.Models;

/// <summary>
/// Smuggler structure accessors for Dune game state.
/// </summary>
/// <remarks>
/// Smuggler structure (14 bytes per entry + 3 bytes padding = 17 bytes total, 6 smugglers max):
/// - Offset 0: Region/location byte
/// - Offset 1: Willingness to haggle
/// - Offset 2: Field C
/// - Offset 3: Field D
/// - Offset 4: Harvesters in stock
/// - Offset 5: Ornithopters in stock
/// - Offset 6: Krys knives in stock
/// - Offset 7: Laser guns in stock
/// - Offset 8: Weirding modules in stock
/// - Offset 9: Harvester price (x10)
/// - Offset 10: Ornithopter price (x10)
/// - Offset 11: Krys knife price (x10)
/// - Offset 12: Laser gun price (x10)
/// - Offset 13: Weirding module price (x10)
/// </remarks>
public partial class DuneGameState {
public byte GetSmugglerRegion(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize)];
}

public byte GetSmugglerWillingnessToHaggle(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 1];
}

public byte GetSmugglerHarvesters(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 4];
}

public byte GetSmugglerOrnithopters(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 5];
}

public byte GetSmugglerKrysKnives(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 6];
}

public byte GetSmugglerLaserGuns(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 7];
}

public byte GetSmugglerWeirdingModules(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 8];
}

public ushort GetSmugglerHarvesterPrice(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return (ushort)(UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 9] * 10);
}

public ushort GetSmugglerOrnithopterPrice(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return (ushort)(UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 10] * 10);
}

public ushort GetSmugglerKrysKnifePrice(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return (ushort)(UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 11] * 10);
}

public ushort GetSmugglerLaserGunPrice(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return (ushort)(UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 12] * 10);
}

public ushort GetSmugglerWeirdingModulePrice(int index) {
if (index < 0 || index >= MaxSmugglers) return 0;
return (ushort)(UInt8[SmugglerBaseOffset + (index * SmugglerEntrySize) + 13] * 10);
}

public string GetSmugglerLocationName(int index) {
byte region = GetSmugglerRegion(index);
return GetLocationNameStr(region, 0x0A);
}
}
Loading
Loading