-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.cs
More file actions
434 lines (358 loc) · 17.5 KB
/
Program.cs
File metadata and controls
434 lines (358 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
using System;
using System.Windows.Forms;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Windows.Media;
using Windows.Media.Playback;
using Windows.Media.Core;
using Windows.Storage.Streams;
namespace DoomMediaController
{
class Program
{
[DllImport("user32.dll")]
static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll", SetLastError = true)]
static extern bool PrintWindow(IntPtr hwnd, IntPtr hDC, uint nFlags);
[DllImport("dwmapi.dll")]
static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out RECT pvAttribute, int cbAttribute);
const int DWMWA_EXTENDED_FRAME_BOUNDS = 9;
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
// Memory reading imports
[DllImport("kernel32.dll")]
static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32.dll")]
static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int dwSize, out int lpNumberOfBytesRead);
[DllImport("kernel32.dll")]
static extern bool CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll")]
static extern IntPtr GetModuleHandle(string lpModuleName);
const int PROCESS_VM_READ = 0x0010;
const int PROCESS_QUERY_INFORMATION = 0x0400;
// Chocolate Doom memory scanning
static IntPtr? cachedHealthAddress = null;
static IntPtr? cachedAmmoAddress = null;
static IntPtr processHandle = IntPtr.Zero;
static int lastKnownHealth = 100;
static int lastKnownAmmo = 50;
[DllImport("user32.dll")]
static extern bool SetProcessDPIAware();
[STAThread]
static void Main(string[] args)
{
// Fix DPI scaling issues (prevents cropping/zoomed screenshots)
SetProcessDPIAware();
// We need a message loop for COM/SMTC to work properly
var context = new ApplicationContext();
Task.Run(async () =>
{
try
{
await RunDoomMediaController();
}
catch (Exception ex)
{
Console.WriteLine($"Critical Error: {ex}");
}
finally
{
Application.Exit();
}
});
Application.Run(context);
}
static async Task RunDoomMediaController()
{
Console.WriteLine("=== DOOM MEDIA CONTROLLER v3 (Message Pump + SilentWaw + Manual) ===");
Console.WriteLine("Starting...");
// Initialize MediaPlayer to get SMTC
// Note: In an STA thread with a pump, this should bind correctly.
var mediaPlayer = new MediaPlayer();
// Disable CommandManager so we specifically control the metadata and thumbnail
// The message pump (Application.Run) handles the required window messages.
// Enable CommandManager intentionally.
// "The media type has not been initialized" error happens when CommandManager is disabled
// before the system acknowledges the media type.
mediaPlayer.CommandManager.IsEnabled = true;
var smtc = mediaPlayer.SystemMediaTransportControls;
// 1. Configure basic controls
smtc.IsPlayEnabled = true;
smtc.IsPauseEnabled = true;
smtc.IsNextEnabled = false;
smtc.IsPreviousEnabled = false;
// 2. Start Playback FIRST to initialize the Media Type in Windows' eyes
var wavStream = GenerateSilentWavStream(30);
mediaPlayer.Source = MediaSource.CreateFromStream(wavStream.AsRandomAccessStream(), "audio/wav");
mediaPlayer.IsLoopingEnabled = true;
mediaPlayer.Play();
// 3. Force status to Playing immediately
smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
// 4. NOW it is safe to set DisplayUpdater properties
// "Music" is generally safer than "Video" for robust thumbnail support
smtc.DisplayUpdater.Type = MediaPlaybackType.Music;
smtc.DisplayUpdater.MusicProperties.Title = "DOOM";
smtc.DisplayUpdater.MusicProperties.Artist = "Running in Media Controls";
smtc.DisplayUpdater.Update();
Console.WriteLine("Media Session Initialized.");
Console.WriteLine("Searching for 'chocolate-doom' process...");
IntPtr doomWindow = IntPtr.Zero;
while (true)
{
if (doomWindow == IntPtr.Zero || !IsWindowValid(doomWindow))
{
var processes = Process.GetProcessesByName("chocolate-doom");
if (processes.Length > 0)
{
var doomProcess = processes[0];
doomWindow = doomProcess.MainWindowHandle;
// Open process for memory reading
if (processHandle != IntPtr.Zero) CloseHandle(processHandle);
processHandle = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, doomProcess.Id);
// Reset cached addresses (need to re-scan)
cachedHealthAddress = null;
cachedAmmoAddress = null;
Console.WriteLine($"Found Doom! Window Handle: {doomWindow}, Process Handle: {processHandle}");
smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
smtc.DisplayUpdater.Update();
}
else
{
// Retry logic
await Task.Delay(1000);
continue;
}
}
// Try to read health and ammo from memory
var (health, ammo) = TryReadHealthAndAmmo(processHandle);
// Capture and Update
try
{
// Force state to playing to ensure controls stay visible
smtc.PlaybackStatus = MediaPlaybackStatus.Playing;
// Attempt to read Health (Need to open process)
// Standard Doom 1.9 shareware / Chocolate Doom static address for player health
// This is a guess for the offset based on standard doom.exe. Chocolate doom might vary.
// However, we can try to find the window title if Chocolate Doom puts health in title (it doesn't by default).
// Let's assume we just want to update the text for now with something we CAN get if memory is hard.
// But if user requested health, we try.
// For Chocolate Doom, we might not have the address easily.
// Let's settle for updating the title to "Doom" and Artist to "Playing" every frame first.
// If we really want health, we need ReadProcessMemory.
// Let's update the text to be dynamic to prove it works.
smtc.DisplayUpdater.MusicProperties.Title = $"HP: {health} | Ammo: {ammo}";
smtc.DisplayUpdater.MusicProperties.Artist = "DOOM";
using (var stream = CaptureWindow(doomWindow))
{
if (stream != null)
{
var inMemoryStream = new InMemoryRandomAccessStream();
using (var dataWriter = new DataWriter(inMemoryStream))
{
dataWriter.WriteBytes(stream.ToArray());
await dataWriter.StoreAsync();
await dataWriter.FlushAsync();
dataWriter.DetachStream();
}
inMemoryStream.Seek(0);
smtc.DisplayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromStream(inMemoryStream);
smtc.DisplayUpdater.Update();
// Console.WriteLine(".");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Capture loop error: {ex.Message}");
}
await Task.Delay(33); // ~30 FPS
}
}
static MemoryStream GenerateSilentWavStream(int seconds)
{
int sampleRate = 44100;
short numChannels = 1;
short bitsPerSample = 16;
int subChunk2Size = sampleRate * numChannels * (bitsPerSample / 8) * seconds;
int chunkSize = 36 + subChunk2Size;
var stream = new MemoryStream();
var writer = new BinaryWriter(stream);
// RIFF header
writer.Write("RIFF".ToCharArray());
writer.Write(chunkSize);
writer.Write("WAVE".ToCharArray());
// fmt chunk
writer.Write("fmt ".ToCharArray());
writer.Write(16); // SubChunk1Size for PCM
writer.Write((short)1); // AudioFormat (1 = PCM)
writer.Write(numChannels);
writer.Write(sampleRate);
writer.Write(sampleRate * numChannels * (bitsPerSample / 8)); // ByteRate
writer.Write((short)(numChannels * (bitsPerSample / 8))); // BlockAlign
writer.Write(bitsPerSample);
// data chunk
writer.Write("data".ToCharArray());
writer.Write(subChunk2Size);
// Silence data (0s)
for (int i = 0; i < subChunk2Size; i++)
{
writer.Write((byte)0);
}
writer.Flush();
stream.Position = 0;
return stream;
}
static bool IsWindowValid(IntPtr hWnd)
{
// Simple check if process is still alive logic or similar could go here.
// For now assume if capture fails we reset.
return hWnd != IntPtr.Zero;
}
[DllImport("user32.dll")]
static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
static MemoryStream? CaptureWindow(IntPtr handle)
{
if (handle == IntPtr.Zero) return null;
// Get window and client dimensions
GetWindowRect(handle, out RECT windowRect);
GetClientRect(handle, out RECT clientRect);
int windowWidth = windowRect.Right - windowRect.Left;
int windowHeight = windowRect.Bottom - windowRect.Top;
int clientWidth = clientRect.Right - clientRect.Left;
int clientHeight = clientRect.Bottom - clientRect.Top;
if (clientWidth <= 0 || clientHeight <= 0) return null;
// Calculate the exact offset of the game content within the window
// If the window is DPI-scaled, these will now be physical pixels due to SetProcessDPIAware()
POINT topLeft = new POINT { X = 0, Y = 0 };
ClientToScreen(handle, ref topLeft);
int offsetX = topLeft.X - windowRect.Left;
int offsetY = topLeft.Y - windowRect.Top;
try
{
// Capture the FULL window first (this works in background)
using (var fullBitmap = new Bitmap(windowWidth, windowHeight, PixelFormat.Format32bppArgb))
{
using (var g = Graphics.FromImage(fullBitmap))
{
IntPtr hdc = g.GetHdc();
try
{
// Flag 2 = PW_RENDERFULLCONTENT (captures hardware accelerated windows)
PrintWindow(handle, hdc, 2);
}
finally
{
g.ReleaseHdc(hdc);
}
}
// Ensure the crop doesn't go out of bounds (can happen if window is partially off-screen)
offsetX = Math.Max(0, Math.Min(offsetX, windowWidth - 1));
offsetY = Math.Max(0, Math.Min(offsetY, windowHeight - 1));
int finalWidth = Math.Min(clientWidth, windowWidth - offsetX);
int finalHeight = Math.Min(clientHeight, windowHeight - offsetY);
if (finalWidth <= 0 || finalHeight <= 0) return null;
using (var clientBitmap = fullBitmap.Clone(new Rectangle(offsetX, offsetY, finalWidth, finalHeight), fullBitmap.PixelFormat))
{
// Resize to target thumbnail size
int targetWidth = 640;
int targetHeight = (int)((float)finalHeight / finalWidth * targetWidth);
if (targetHeight <= 0) targetHeight = 1;
using (var resized = new Bitmap(clientBitmap, new Size(targetWidth, targetHeight)))
{
var ms = new MemoryStream();
resized.Save(ms, ImageFormat.Png);
ms.Position = 0;
return ms;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Capture Error: {ex.Message}");
return null;
}
}
static (int health, int ammo) TryReadHealthAndAmmo(IntPtr hProcess)
{
if (hProcess == IntPtr.Zero)
return (lastKnownHealth, lastKnownAmmo);
try
{
// Chocolate Doom is based on the original Doom source.
// The player structure is stored in a global `players` array.
// For Chocolate Doom, we need to scan for patterns or use known offsets.
//
// Common approach: Scan memory for the health value pattern.
// However, without ASLR bypass or known static addresses, we'll try some common offsets.
//
// Alternative: Parse the HUD from the captured image (OCR) - complex but reliable.
// For now, let's try scanning a reasonable memory range.
var processes = Process.GetProcessesByName("chocolate-doom");
if (processes.Length == 0) return (lastKnownHealth, lastKnownAmmo);
var process = processes[0];
var baseAddress = process.MainModule?.BaseAddress ?? IntPtr.Zero;
if (baseAddress == IntPtr.Zero) return (lastKnownHealth, lastKnownAmmo);
// Known offsets for Chocolate Doom (may vary by version)
// These are educated guesses based on Doom source structure.
// player_t.health is typically early in the structure.
//
// Common patterns in Doom source:
// - players[0].health at base + some offset
// - Typical offset range: 0x200000 - 0x400000 from base
// Let's try reading from a known relative offset pattern
// This is experimental - real implementation would need Cheat Engine analysis
int health = lastKnownHealth;
int ammo = lastKnownAmmo;
// Read health from confirmed offset: chocolate-doom.exe+75AC8
IntPtr healthAddress = baseAddress + 0x75AC8;
int healthValue = ReadInt32(hProcess, healthAddress);
if (healthValue >= 0 && healthValue <= 200) // Valid health range
{
health = healthValue;
}
// Read ammo from confirmed offset: chocolate-doom.exe+1496A8
IntPtr ammoAddress = baseAddress + 0x1496A8;
int ammoValue = ReadInt32(hProcess, ammoAddress);
if (ammoValue >= 0 && ammoValue <= 999) // Valid ammo range
{
ammo = ammoValue;
}
lastKnownHealth = health;
lastKnownAmmo = ammo;
return (health, ammo);
}
catch
{
return (lastKnownHealth, lastKnownAmmo);
}
}
static int ReadInt32(IntPtr hProcess, IntPtr address)
{
byte[] buffer = new byte[4];
if (ReadProcessMemory(hProcess, address, buffer, 4, out _))
{
return BitConverter.ToInt32(buffer, 0);
}
return -1;
}
}
}