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

using System;

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;

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

using Spice86.Core.Emulator.CPU.Registers;
using Spice86.Core.Emulator.Memory.ReaderWriter;

/// <summary>
/// Helper class to create and show the Dune Game State Window.
/// </summary>
public static class GameEngineWindowManager {
private static DuneGameStateWindow? _window;
private static DuneGameStateViewModel? _viewModel;

/// <summary>
/// Creates and shows the Dune Game State Window.
/// </summary>
/// <param name="memory">The memory reader/writer interface for accessing emulated memory.</param>
/// <param name="segmentRegisters">The CPU segment registers.</param>
/// <remarks>
/// This method must be called from the UI thread or will dispatch to the UI thread.
/// The window is created as a non-modal window that stays open alongside the main window.
/// </remarks>
public static void ShowWindow(IByteReaderWriter memory, SegmentRegisters segmentRegisters) {
// Ensure we're on the UI thread
if (!Dispatcher.UIThread.CheckAccess()) {
Dispatcher.UIThread.Post(() => ShowWindow(memory, segmentRegisters));
return;
}

// If window already exists and is still valid, just show it
if (_window != null) {
try {
_window.Show();
_window.Activate();
return;
} catch {
// Window was closed, create a new one
_viewModel?.Dispose();
_window = null;
_viewModel = null;
}
}

// Create the ViewModel
_viewModel = new DuneGameStateViewModel(memory, segmentRegisters);

// Create the Window
_window = new DuneGameStateWindow {
DataContext = _viewModel
};

// Handle window closing
_window.Closing += (sender, args) => {
_viewModel?.Dispose();
_viewModel = null;
_window = null;
};

// Show the window
_window.Show();
}

/// <summary>
/// Closes the Dune Game State Window if it is open.
/// </summary>
public static void CloseWindow() {
if (!Dispatcher.UIThread.CheckAccess()) {
Dispatcher.UIThread.Post(CloseWindow);
return;
}

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

/// <summary>
/// Gets whether the window is currently open.
/// </summary>
public static bool IsWindowOpen => _window != null;
}
239 changes: 239 additions & 0 deletions src/Cryogenic/GameEngineWindow/Models/DuneGameState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
namespace Cryogenic.GameEngineWindow.Models;

using Spice86.Core.Emulator.CPU.Registers;
using Spice86.Core.Emulator.Memory.ReaderWriter;
using Spice86.Core.Emulator.ReverseEngineer.DataStructure;

/// <summary>
/// Provides access to Dune game state values stored in emulated memory.
/// </summary>
/// <remarks>
/// <para>
/// This class provides read access to live game state values from the Dune CD game memory.
/// The memory is read directly from the emulator at runtime, not from savegame files.
/// </para>
/// <para>
/// Memory regions per madmoose's analysis (from seg000:B427 sub_1B427_create_save_in_memory):
/// - From DS:[DCFE], 12671 bytes (2 bits for each byte of the next 50684 bytes)
/// - From CS:00AA, 162 bytes (includes some code)
/// - From DS:AA76, 4600 bytes
/// - From DS:0000, 4705 bytes
/// </para>
/// <para>
/// Memory layout reference (relative to DS segment):
/// </para>
/// </remarks>
public class DuneGameState : MemoryBasedDataStructureWithDsBaseAddress {
/// <summary>
/// Initializes a new instance of the <see cref="DuneGameState"/> class.
/// </summary>
/// <param name="memory">The memory reader/writer interface for accessing emulated memory.</param>
/// <param name="segmentRegisters">The CPU segment registers used to calculate absolute addresses.</param>
public DuneGameState(IByteReaderWriter memory, SegmentRegisters segmentRegisters)
: base(memory, segmentRegisters) {
}

#region Core Game State (DS:0000 region, 4705 bytes)

/// <summary>
/// Gets the game elapsed time from offset 0x2.
/// </summary>
public ushort GameElapsedTime => UInt16[0x0002];

/// <summary>
/// Gets the player's charisma level (offset 0x0029).
/// Value increases as player enlists more Fremen troops.
/// </summary>
public byte Charisma => UInt8[0x0029];

/// <summary>
/// Gets the current game stage/progression (offset 0x002A).
/// </summary>
public byte GameStage => UInt8[0x002A];

/// <summary>
/// Gets the total spice amount (offset 0x009F, 2 bytes).
/// </summary>
public ushort Spice => UInt16[0x009F];

/// <summary>
/// Gets the raw date and time value (offset 0x1174, 2 bytes).
/// </summary>
public ushort DateTimeRaw => UInt16[0x1174];

/// <summary>
/// Gets the contact distance for reaching Fremen sietches (offset 0x1176).
/// </summary>
public byte ContactDistance => UInt8[0x1176];

#endregion

#region HNM Video State

/// <summary>
/// HNM finished flag (offset 0xDBE7).
/// </summary>
public byte HnmFinishedFlag => UInt8[0xDBE7];

/// <summary>
/// HNM frame counter (offset 0xDBE8, 2 bytes).
/// </summary>
public ushort HnmFrameCounter => UInt16[0xDBE8];

/// <summary>
/// HNM counter 2 (offset 0xDBEA, 2 bytes).
/// </summary>
public ushort HnmCounter2 => UInt16[0xDBEA];

/// <summary>
/// Current HNM resource flag (offset 0xDBFE).
/// </summary>
public byte CurrentHnmResourceFlag => UInt8[0xDBFE];

/// <summary>
/// HNM video ID (offset 0xDC00, 2 bytes).
/// </summary>
public ushort HnmVideoId => UInt16[0xDC00];

/// <summary>
/// HNM active video ID (offset 0xDC02, 2 bytes).
/// </summary>
public ushort HnmActiveVideoId => UInt16[0xDC02];

/// <summary>
/// HNM file offset (offset 0xDC04, 4 bytes).
/// </summary>
public uint HnmFileOffset => UInt32[0xDC04];

/// <summary>
/// HNM file remaining bytes (offset 0xDC08, 4 bytes).
/// </summary>
public uint HnmFileRemain => UInt32[0xDC08];

#endregion

#region Display and Graphics State
Copy link
Member

Choose a reason for hiding this comment

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

@copilot don't use region directives...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in commit 6600569. Removed #region directives and split DuneGameState into partial class files: DuneGameState.Locations.cs, DuneGameState.Troops.cs, DuneGameState.Npcs.cs, and DuneGameState.Smugglers.cs.


/// <summary>
/// Front framebuffer segment (offset 0xDBD6).
/// </summary>
public ushort FramebufferFront => UInt16[0xDBD6];

/// <summary>
/// Screen buffer segment (offset 0xDBD8).
/// </summary>
public ushort ScreenBuffer => UInt16[0xDBD8];

/// <summary>
/// Active framebuffer segment (offset 0xDBDA).
/// </summary>
public ushort FramebufferActive => UInt16[0xDBDA];

/// <summary>
/// Back framebuffer segment (offset 0xDC32).
/// </summary>
public ushort FramebufferBack => UInt16[0xDC32];

#endregion

#region Mouse and Cursor State

/// <summary>
/// Mouse Y position (offset 0xDC36).
/// </summary>
public ushort MousePosY => UInt16[0xDC36];

/// <summary>
/// Mouse X position (offset 0xDC38).
/// </summary>
public ushort MousePosX => UInt16[0xDC38];

/// <summary>
/// Mouse draw Y position (offset 0xDC42).
/// </summary>
public ushort MouseDrawPosY => UInt16[0xDC42];

/// <summary>
/// Mouse draw X position (offset 0xDC44).
/// </summary>
public ushort MouseDrawPosX => UInt16[0xDC44];

/// <summary>
/// Cursor hide counter (offset 0xDC46).
/// </summary>
public byte CursorHideCounter => UInt8[0xDC46];

/// <summary>
/// Map cursor type (offset 0xDC58).
/// </summary>
public ushort MapCursorType => UInt16[0xDC58];

#endregion

#region Sound State

/// <summary>
/// Is sound present flag (offset 0xDBCD).
/// </summary>
public byte IsSoundPresent => UInt8[0xDBCD];

/// <summary>
/// MIDI func5 return Bx (offset 0xDBCE).
/// </summary>
public ushort MidiFunc5ReturnBx => UInt16[0xDBCE];

#endregion

#region Transition and Effects State

/// <summary>
/// Transition bitmask (offset 0xDCE6).
/// </summary>
public byte TransitionBitmask => UInt8[0xDCE6];

#endregion

#region Helper Methods

/// <summary>
/// Decodes the day from the raw date/time value.
/// </summary>
public int GetDay() => (DateTimeRaw >> 8) & 0xFF;

/// <summary>
/// Decodes the hour from the raw date/time value.
/// </summary>
public int GetHour() => (DateTimeRaw & 0xF0) >> 4;

/// <summary>
/// Decodes the minutes from the raw date/time value.
/// </summary>
public int GetMinutes() => (DateTimeRaw & 0x0F) * 30;

/// <summary>
/// Gets a formatted string representation of the game time.
/// </summary>
public string GetFormattedDateTime() => $"Day {GetDay()}, {GetHour():D2}:{GetMinutes():D2}";

/// <summary>
/// Gets the spice amount in a human-readable format.
/// </summary>
public string GetFormattedSpice() => $"{Spice * 10} kg";

/// <summary>
/// Gets a description of the current game stage.
/// </summary>
public string GetGameStageDescription() => GameStage switch {
0x01 => "Start of game",
0x02 => "Learning about stillsuits",
0x03 => "Stillsuit explanation",
0x04 => "Stillsuit mechanics",
0x05 => "Meeting spice prospectors",
0x06 => "Got stillsuits",
0x4F => "Can ride worms",
0x50 => "Have ridden a worm",
_ => $"Stage 0x{GameStage:X2}"
};

#endregion
}
Loading
Loading