331 lines
11 KiB
C
331 lines
11 KiB
C
// doom.c — NT-Lite port of PureDOOM
|
||
// Win32 personality carrying all the weight, just like the real ntoskrnl does.
|
||
// Drop doom1.wad (or doom.wad) next to this binary on your FAT32 volume.
|
||
|
||
#include <windows.h>
|
||
#define DOOM_IMPLEMENTATION
|
||
#include "PureDOOM.h"
|
||
#include <stdint.h>
|
||
#include <stddef.h>
|
||
#include <string.h>
|
||
#include <intrin.h>
|
||
int _fltused;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Globals
|
||
// ---------------------------------------------------------------------------
|
||
static HANDLE g_stdout;
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// doom_print_fn
|
||
// ---------------------------------------------------------------------------
|
||
static void doom_print_wrapper(const char* msg)
|
||
{
|
||
UINT len = (UINT)strlen(msg);
|
||
WriteConsoleA(g_stdout, msg, len, 0, 0);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// doom_malloc_fn / doom_free_fn
|
||
// ---------------------------------------------------------------------------
|
||
static void* doom_malloc_wrapper(int size)
|
||
{
|
||
void* p = HeapAlloc(0, HEAP_ZERO_MEMORY, size);
|
||
if (!p) ExitProcess(GetLastError());
|
||
return p;
|
||
}
|
||
|
||
static void doom_free_wrapper(void* ptr)
|
||
{
|
||
HeapFree(0, 0, ptr);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// File I/O wrappers
|
||
// ---------------------------------------------------------------------------
|
||
static void* doom_open_wrapper(const char* path, const char* mode)
|
||
{
|
||
DWORD access = GENERIC_READ;
|
||
DWORD disp = OPEN_EXISTING;
|
||
|
||
if (mode[0] == 'w') {
|
||
access = GENERIC_WRITE;
|
||
disp = CREATE_ALWAYS;
|
||
} else if (mode[0] == 'r' && mode[1] == '+') {
|
||
access = GENERIC_READ | GENERIC_WRITE;
|
||
}
|
||
|
||
HANDLE h = CreateFileA(path, access, FILE_SHARE_READ, 0, disp, 0, 0);
|
||
return (h == INVALID_HANDLE_VALUE) ? 0 : h;
|
||
}
|
||
|
||
static void doom_close_wrapper(void* handle)
|
||
{
|
||
CloseHandle((HANDLE)handle);
|
||
}
|
||
|
||
static int doom_read_wrapper(void* handle, void* buf, int count)
|
||
{
|
||
DWORD read = 0;
|
||
ReadFile((HANDLE)handle, buf, (DWORD)count, &read, 0);
|
||
return (int)read;
|
||
}
|
||
|
||
static int doom_write_wrapper(void* handle, const void* buf, int count)
|
||
{
|
||
DWORD written = 0;
|
||
WriteFile((HANDLE)handle, buf, (DWORD)count, &written, 0);
|
||
return (int)written;
|
||
}
|
||
|
||
static int doom_seek_wrapper(void* handle, int offset, doom_seek_t origin)
|
||
{
|
||
DWORD method;
|
||
switch (origin) {
|
||
case DOOM_SEEK_SET: method = FILE_BEGIN; break;
|
||
case DOOM_SEEK_CUR: method = FILE_CURRENT; break;
|
||
case DOOM_SEEK_END: method = FILE_END; break;
|
||
default: method = FILE_BEGIN; break;
|
||
}
|
||
SetFilePointer((HANDLE)handle, (LONG)offset, 0, method);
|
||
return 0;
|
||
}
|
||
|
||
static int doom_tell_wrapper(void* handle)
|
||
{
|
||
return (int)SetFilePointer((HANDLE)handle, 0, 0, FILE_CURRENT);
|
||
}
|
||
|
||
static int doom_eof_wrapper(void* handle)
|
||
{
|
||
DWORD size = GetFileSize((HANDLE)handle, 0);
|
||
DWORD pos = (DWORD)doom_tell_wrapper(handle);
|
||
return (pos >= size) ? 1 : 0;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// doom_gettime_fn
|
||
// ---------------------------------------------------------------------------
|
||
static void doom_gettime_wrapper(int* sec, int* usec)
|
||
{
|
||
DWORD t = GetTickCount(); // milliseconds
|
||
*sec = (int)(t / 1000);
|
||
*usec = (int)((t % 1000) * 1000); // ms remainder → us
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// doom_exit_fn
|
||
// ---------------------------------------------------------------------------
|
||
static void doom_exit_wrapper(int code)
|
||
{
|
||
ExitProcess((UINT)code);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// doom_getenv_fn — we don't have environment variables
|
||
// ---------------------------------------------------------------------------
|
||
static char* doom_getenv_wrapper(const char* name)
|
||
{
|
||
if (strcmp(name, "HOME") == 0) {
|
||
static char home[] = "c:";
|
||
doom_print_wrapper("getenv: Returning HOME="); doom_print_wrapper(home); doom_print_wrapper("\n");
|
||
return home;
|
||
}
|
||
doom_print_wrapper("getenv: Unknown variable "); doom_print_wrapper(name); doom_print_wrapper("\n");
|
||
return NULL;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Input — poll GetAsyncKeyState every frame
|
||
// Generates key_down / key_up edge events, not held-key spam.
|
||
// ---------------------------------------------------------------------------
|
||
typedef struct { int vk; doom_key_t key; } KeyMap;
|
||
|
||
// NOTE: Win32 VK codes for letters are uppercase ASCII (e.g. 'W' = 0x57).
|
||
// DOOM_KEY_* for letters are lowercase ASCII (e.g. DOOM_KEY_W = 'w' = 0x77).
|
||
// These are different values — the VK column is what GetAsyncKeyState takes,
|
||
// the doom_key column is what PureDOOM wants. Don't collapse them.
|
||
static const KeyMap g_keymap[] = {
|
||
// Letters — VK is uppercase, DOOM key is lowercase
|
||
{ 'W', DOOM_KEY_W },
|
||
{ 'A', DOOM_KEY_A },
|
||
{ 'S', DOOM_KEY_S },
|
||
{ 'D', DOOM_KEY_D },
|
||
{ 'E', DOOM_KEY_E },
|
||
{ 'Q', DOOM_KEY_Q },
|
||
{ 'Y', DOOM_KEY_Y }, // yes/no menus
|
||
{ 'N', DOOM_KEY_N },
|
||
|
||
// Arrow keys
|
||
{ VK_UP, DOOM_KEY_UP_ARROW },
|
||
{ VK_DOWN, DOOM_KEY_DOWN_ARROW },
|
||
{ VK_LEFT, DOOM_KEY_LEFT_ARROW },
|
||
{ VK_RIGHT, DOOM_KEY_RIGHT_ARROW },
|
||
|
||
// Action keys — Ctrl=fire, Shift=run, Alt=strafe
|
||
{ VK_CONTROL, DOOM_KEY_CTRL },
|
||
{ VK_SPACE, DOOM_KEY_SPACE },
|
||
{ VK_SHIFT, DOOM_KEY_SHIFT },
|
||
{ VK_MENU, DOOM_KEY_ALT },
|
||
|
||
// Navigation
|
||
{ VK_ESCAPE, DOOM_KEY_ESCAPE },
|
||
{ VK_RETURN, DOOM_KEY_ENTER },
|
||
{ VK_TAB, DOOM_KEY_TAB },
|
||
{ VK_BACK, DOOM_KEY_BACKSPACE },
|
||
{ VK_PAUSE, DOOM_KEY_PAUSE },
|
||
|
||
// Weapon slots
|
||
{ '1', DOOM_KEY_1 },
|
||
{ '2', DOOM_KEY_2 },
|
||
{ '3', DOOM_KEY_3 },
|
||
{ '4', DOOM_KEY_4 },
|
||
{ '5', DOOM_KEY_5 },
|
||
{ '6', DOOM_KEY_6 },
|
||
{ '7', DOOM_KEY_7 },
|
||
|
||
// Misc
|
||
{ '-', DOOM_KEY_MINUS },
|
||
{ '=', DOOM_KEY_EQUALS },
|
||
{ ',', DOOM_KEY_COMMA }, // default strafe left
|
||
{ '.', DOOM_KEY_PERIOD }, // default strafe right
|
||
|
||
// Function keys — save/load/volume/etc
|
||
{ VK_F1, DOOM_KEY_F1 },
|
||
{ VK_F2, DOOM_KEY_F2 },
|
||
{ VK_F3, DOOM_KEY_F3 },
|
||
{ VK_F4, DOOM_KEY_F4 },
|
||
{ VK_F5, DOOM_KEY_F5 },
|
||
{ VK_F6, DOOM_KEY_F6 },
|
||
{ VK_F7, DOOM_KEY_F7 },
|
||
{ VK_F8, DOOM_KEY_F8 },
|
||
{ VK_F9, DOOM_KEY_F9 },
|
||
{ VK_F10, DOOM_KEY_F10 },
|
||
{ VK_F11, DOOM_KEY_F11 },
|
||
{ VK_F12, DOOM_KEY_F12 },
|
||
};
|
||
|
||
#define KEYMAP_COUNT (sizeof(g_keymap) / sizeof(g_keymap[0]))
|
||
|
||
static short g_prev_key[256];
|
||
|
||
static void poll_input(void)
|
||
{
|
||
for (int i = 0; i < (int)KEYMAP_COUNT; i++) {
|
||
int vk = g_keymap[i].vk;
|
||
short cur = GetAsyncKeyState(vk);
|
||
int cur_down = (cur & 0x8000) != 0;
|
||
int prev_down = (g_prev_key[vk] & 0x8000) != 0;
|
||
|
||
if (cur_down && !prev_down) doom_key_down(g_keymap[i].key);
|
||
if (!cur_down && prev_down) doom_key_up (g_keymap[i].key);
|
||
|
||
g_prev_key[vk] = cur;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Render — StretchDIBits to screen, same pipeline as video.c.
|
||
//
|
||
// NOTE: PureDOOM's framebuffer is RGBA; Win32 StretchDIBits with BI_RGB/32bpp
|
||
// expects BGRA. Channels will be swapped (red ↔ blue) until you either:
|
||
// a) swap in the blit loop below, or
|
||
// b) tell PureDOOM to give you BGRA if a future version supports it.
|
||
// For a first boot, blue-tinted demons are still demons.
|
||
// ---------------------------------------------------------------------------
|
||
#define DOOM_W 320
|
||
#define DOOM_H 200
|
||
#define SCALE 3
|
||
#define DISP_W (DOOM_W * SCALE)
|
||
#define DISP_H (DOOM_H * SCALE)
|
||
|
||
static BITMAPINFO g_bmi;
|
||
static uint32_t g_blit_buf[DOOM_W * DOOM_H]; // BGRA-swapped copy
|
||
|
||
static void render_frame(const uint8_t* rgba)
|
||
{
|
||
// Swap R <-> B so Win32 GDI sees correct colours
|
||
const uint32_t* src = (const uint32_t*)rgba;
|
||
for (int i = 0; i < DOOM_W * DOOM_H; i++) {
|
||
uint32_t p = src[i];
|
||
// RGBA -> BGRA: swap byte 0 (R) and byte 2 (B)
|
||
g_blit_buf[i] = (p & 0xFF00FF00u)
|
||
| ((p & 0x00FF0000u) >> 16)
|
||
| ((p & 0x000000FFu) << 16);
|
||
}
|
||
|
||
PAINTSTRUCT ps;
|
||
HDC hdc = BeginPaint((HWND)0, &ps);
|
||
StretchDIBits(
|
||
hdc,
|
||
0, 0, DISP_W, DISP_H, // destination — scaled up
|
||
0, 0, DOOM_W, DOOM_H, // source — native 320×200
|
||
g_blit_buf,
|
||
&g_bmi,
|
||
DIB_RGB_COLORS,
|
||
SRCCOPY
|
||
);
|
||
EndPaint((HWND)0, &ps);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// main
|
||
// ---------------------------------------------------------------------------
|
||
int main(const char* cmdline)
|
||
{
|
||
(void)cmdline;
|
||
|
||
g_stdout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||
WriteConsoleA(g_stdout, "doom: starting\n", 15, 0, 0);
|
||
|
||
// Prepare the BMI header once (top-down, like video.c)
|
||
memset(&g_bmi, 0, sizeof(g_bmi));
|
||
g_bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||
g_bmi.bmiHeader.biWidth = DOOM_W;
|
||
g_bmi.bmiHeader.biHeight = -DOOM_H; // negative = top-down
|
||
g_bmi.bmiHeader.biPlanes = 1;
|
||
g_bmi.bmiHeader.biBitCount = 32;
|
||
g_bmi.bmiHeader.biCompression = BI_RGB;
|
||
|
||
// Wire up PureDOOM
|
||
doom_set_print(doom_print_wrapper);
|
||
doom_set_malloc(doom_malloc_wrapper, doom_free_wrapper);
|
||
doom_set_file_io(
|
||
doom_open_wrapper,
|
||
doom_close_wrapper,
|
||
doom_read_wrapper,
|
||
doom_write_wrapper,
|
||
doom_seek_wrapper,
|
||
doom_tell_wrapper,
|
||
doom_eof_wrapper
|
||
);
|
||
doom_set_gettime(doom_gettime_wrapper);
|
||
doom_set_exit(doom_exit_wrapper);
|
||
doom_set_getenv(doom_getenv_wrapper);
|
||
|
||
// Resolution + modern WASD bindings
|
||
doom_set_resolution(DOOM_W, DOOM_H);
|
||
doom_set_default_int("key_up", DOOM_KEY_W);
|
||
doom_set_default_int("key_down", DOOM_KEY_S);
|
||
doom_set_default_int("key_strafeleft", DOOM_KEY_A);
|
||
doom_set_default_int("key_straferight", DOOM_KEY_D);
|
||
doom_set_default_int("key_use", DOOM_KEY_E);
|
||
doom_set_default_int("mouse_move", 0);
|
||
|
||
// No extra args — PureDOOM will scan for doom1.wad / doom.wad
|
||
const char* args[] = { "doom" };
|
||
doom_init(1, (char**)args, 0);
|
||
|
||
WriteConsoleA(g_stdout, "doom: entering game loop\n", 25, 0, 0);
|
||
|
||
// Game loop — identical shape to video.c's Bink loop
|
||
while (1) {
|
||
poll_input();
|
||
doom_update();
|
||
render_frame((const uint8_t*)doom_get_framebuffer(4 /* RGBA */));
|
||
}
|
||
|
||
// Unreachable: doom_exit() -> ExitProcess() handles clean shutdown
|
||
ExitProcess(0);
|
||
}
|