From 956e1aaf584371d8a45f830f31c67ae553957490 Mon Sep 17 00:00:00 2001 From: codearranger Date: Mon, 15 Sep 2025 20:52:49 -0700 Subject: [PATCH 1/2] [macOS] Fix SDL threading issues and add full macOS support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move SDL operations to main thread to fix NSWindow threading errors - Add SDL_MAIN_HANDLED macro for macOS builds - Implement proper event processing in main game loop - Update documentation to reflect working macOS status - Add .gitattributes for consistent line endings across platforms - Add CLAUDE.md with project instructions for AI assistance - Ignore custom font file in .gitignore The SDL version now runs correctly on macOS with all threading issues resolved. Font fallback system automatically uses system fonts when custom font is unavailable. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .gitattributes | 4 + .gitignore | 35 +- CLAUDE.md | 102 + README.md | 4 +- Snipes.cpp | 5064 +++++++++++++++++++++++----------------------- console.h | 56 +- sdl/console.cpp | 1624 ++++++++------- sdl/keyboard.cpp | 345 ++-- sdl/sound.cpp | 167 +- sdl/timer.cpp | 49 +- 10 files changed, 3909 insertions(+), 3541 deletions(-) create mode 100644 .gitattributes create mode 100644 CLAUDE.md diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..39d8667 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf +*.sln text eol=crlf +*.vcxproj text eol=crlf +*.vcxproj.filters text eol=crlf diff --git a/.gitignore b/.gitignore index 022a1c7..bb29680 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,19 @@ -/config.h - -*.SnipesGame - -# Visual C++ -/*.vcxproj.user -/*.suo -/ipch -/Release -/Debug -/SDL Release -/SDL Debug - -# GNUmakefile -/snipes -/snipes.exe +/config.h + +*.SnipesGame + +# Visual C++ +/*.vcxproj.user +/*.suo +/ipch +/Release +/Debug +/SDL Release +/SDL Debug + +# GNUmakefile +/snipes +/snipes.exe + +# Font +SnipesConsole.ttf diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a5f231f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,102 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a modern C++ port of the classic 1982 text-mode game Snipes. The code has been reverse-engineered from the original DOS executable with 100% identical game logic. The project supports both SDL graphics and Windows console builds. + +## Build Commands + +### SDL Build (Default) +```bash +make +``` +This creates an SDL build requiring SDL2 and SDL2_ttf libraries. The build will automatically create `config.h` from `config-sample.h` if it doesn't exist. + +### Clean Build +```bash +make clean +``` + +### Visual Studio Build +1. Copy `config-sample.h` to `config.h` +2. Open `Snipes.sln` in Visual Studio +3. Build using the IDE (supports both SDL and Windows console configurations) + +## Configuration + +- **config.h**: Main configuration file (auto-created from `config-sample.h`) +- Key configuration options include: + - `USE_SCANCODES_FOR_LETTER_KEYS`: Useful for non-QWERTY keyboards + - `CHEAT_OMNISCIENCE`: Debug mode showing full maze + - Font and display settings for SDL builds + - Various bug fixes and compatibility options + +## Architecture + +### Core Structure +- **Snipes.cpp/h**: Main game logic and entry point +- **Platform abstraction**: Separate implementations for SDL (`sdl/`) and Windows (`windows/`) +- **Game components**: console, keyboard, sound, timer modules with platform-specific implementations + +### Key Constants +- Maze dimensions: 16x20 cells (128x120 pixels) +- Each cell: 8x6 pixels +- Viewport: 40x25 characters (except in omniscience mode) + +### Platform Abstraction +The game uses a clean platform abstraction layer: +- **SDL version**: Modern graphics with TTF fonts +- **Windows version**: Native console output +- Common interface through header files: `console.h`, `keyboard.h`, `sound.h`, `timer.h` + +## Dependencies + +### SDL Build +- SDL2 +- SDL2_ttf +- Custom font file: `SnipesConsole.ttf` (loaded at startup) + +### Development +- C++11 compatible compiler +- GNU Make or Visual Studio 2017+ + +## Replay System + +The game automatically records replay files with `.SnipesGame` extension. To play back: +```bash +./snipes "2016-07-08 09.10.11.SnipesGame" +``` + +## macOS Status + +✅ **Fully working**: The SDL version now runs correctly on macOS with all threading issues resolved. + +**Features**: +- ✅ Application builds and runs successfully on macOS +- ✅ SDL window creation and event handling run properly on the main thread +- ✅ Font fallback system automatically uses system fonts (Courier.ttc, Menlo.ttc, etc.) +- ✅ Core game logic is fully functional +- ✅ All rendering and input handling work correctly + +**Changes made for macOS compatibility**: +- SDL operations (window creation, event polling, rendering) now run on the main thread +- Added `SDL_MAIN_HANDLED` macro for macOS builds +- Implemented proper event processing in the main game loop +- Font loading includes automatic fallbacks to system fonts when custom font is unavailable + +### Getting the Custom Font + +The game expects `SnipesConsole.ttf` in the current directory. You can: +1. Download it from: http://kingbird.myphotos.cc/ee22d44076adb8a34d8e20df4be3730a/SnipesConsole.ttf +2. Or let it use system fallback fonts (Courier, Menlo, etc.) + +## Build Flags + +The GNUmakefile includes: +- `-std=c++11`: C++11 standard +- `-fstack-protector`: Security hardening +- Optional maintainer flags: `-Werror -Wall -Wextra` (enabled with `MAINT=1`) +- Optimized builds use `-O3` +- Debug builds can use `-Og -g -fsanitize=address -fsanitize=undefined` \ No newline at end of file diff --git a/README.md b/README.md index 0e6d85d..734772d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ For more information, see the [vogons.org forum thread](https://www.vogons.org/v SDL builds require the SDL2 and SDL2_ttf libraries. -On startup, the SDL build will attempt to load a custom font, which can be obtained separately from [here](http://kingbird.myphotos.cc/ee22d44076adb8a34d8e20df4be3730a/SnipesConsole.ttf). +On startup, the SDL build will attempt to load a custom font, which can be obtained separately from [here](http://kingbird.myphotos.cc/ee22d44076adb8a34d8e20df4be3730a/SnipesConsole.ttf). On macOS, if the custom font is not available, the game will automatically fall back to system fonts (Courier, Menlo, etc.). #### With Visual Studio @@ -23,6 +23,8 @@ On startup, the SDL build will attempt to load a custom font, which can be obtai 1. (Optional) Copy `config-sample.h` to `config.h`, and edit as desired 2. Run `make` to compile an SDL build. +The SDL build works on Linux, Windows, and macOS. + For Arch Linux, you can use the [snipes-git](https://aur.archlinux.org/packages/snipes-git/) AUR package. ### Replay recording diff --git a/Snipes.cpp b/Snipes.cpp index 0cc201b..3af3136 100644 --- a/Snipes.cpp +++ b/Snipes.cpp @@ -1,2522 +1,2542 @@ -#include -#include -#include -#include -#include -#include "config.h" -#include "Snipes.h" -#include "types.h" -#include "macros.h" -#include "console.h" -#include "timer.h" -#include "sound.h" -#include "keyboard.h" -#include "platform.h" - -bool got_ctrl_break = false; -bool forfeit_match = false; -bool instant_quit = false; -bool sound_enabled = true; -bool shooting_sound_enabled = false; -BYTE fast_forward = false; -BYTE spacebar_state = false; -static BYTE keyboard_state = 0; -#ifdef CHEAT -bool rerecordingMode; -int single_step = 0; -int step_backwards = 0; -int increment_initial_seed = 0; -WORD skip_to_frame = 0; -#endif - -#if defined(PLAYBACK_FOR_SCREEN_RECORDING) && !defined(CHEAT) - #define _PLAYBACK_FOR_SCREEN_RECORDING 1 -#else - #define _PLAYBACK_FOR_SCREEN_RECORDING 0 -#endif - -static WORD random_seed_lo = 33, random_seed_hi = 467; -WORD GetRandomMasked(WORD mask) -{ - random_seed_lo *= 2; - if (random_seed_lo > 941) - random_seed_lo -= 941; - random_seed_hi *= 2; - if (random_seed_hi > 947) - random_seed_hi -= 947; - return (random_seed_lo + random_seed_hi) & mask; -} -template -WORD GetRandomRanged() -{ - WORD mask = RANGE-1; - mask |= mask >> 1; - mask |= mask >> 2; - mask |= mask >> 4; - mask |= mask >> 8; - if (mask == RANGE-1) - return GetRandomMasked(mask); - for (;;) - { - WORD number = GetRandomMasked(mask); - if (number < RANGE) - return number; - } -} - -Uint skillLevelLetter = 0; -Uint skillLevelNumber = 1; - -void ParseSkillLevel(char *skillLevel, DWORD skillLevelLength) -{ - Uint skillLevelNumberTmp = 0; - for (; skillLevelLength; skillLevel++, skillLevelLength--) - { - if (skillLevelNumberTmp >= 0x80) - { - // strange behavior, but this is what the original game does - skillLevelNumber = 1; - return; - } - char chr = *skillLevel; - if (inrange(chr, 'a', 'z')) - skillLevelLetter = chr - 'a'; - else - if (inrange(chr, 'A', 'Z')) - skillLevelLetter = chr - 'A'; - else - if (inrange(chr, '0', '9')) - skillLevelNumberTmp = skillLevelNumberTmp * 10 + (chr - '0'); - } - if (inrange(skillLevelNumberTmp, 1, 9)) - skillLevelNumber = skillLevelNumberTmp; -} - -void ReadSkillLevel() -{ - char skillLevel[0x80] = {}; - DWORD skillLevelLength = ReadTextFromConsole(skillLevel, _countof(skillLevel)); - - if (skillLevelLength && skillLevel[skillLevelLength-1] == '\n') - skillLevelLength--; - if (skillLevelLength && skillLevel[skillLevelLength-1] == '\r') - skillLevelLength--; - - for (Uint i=0; i -OBJECT_TYPE &SetCurrentObject(ObjectIndex i) -{ - OBJECT_TYPE &object = ((OBJECT_TYPE*)objects)[i]; // cast the array before dereferencing, to avoid a bug that would otherwise happen with GCC's "-fstrict-aliasing" optimization - currentObject = (Object*)&object; - return object; -} - -Player &player = ((Player *)objects)[OBJECT_PLAYER ]; -Explosion &playerExplosion = ((Explosion*)objects)[OBJECT_PLAYEREXPLOSION]; - -static MazeTile maze[MAZE_WIDTH * MAZE_HEIGHT]; - -static const char statusLine[] = "\xDA\xBF\xB3\x01\x1A\xB3\x02\xB3""Skill""\xC0\xD9\xB3\x01\x1A\xB3\x02\xB3""Time Men Left Score 0 0000001 Man Left""e"; - -void InitializeHUD() -{ - char (&scratchBuffer)[WINDOW_WIDTH] = (char(&)[WINDOW_WIDTH])maze; - - char skillLetter = skillLevelLetter + 'A'; - memset(scratchBuffer, ' ', WINDOW_WIDTH); - outputText (0x17, WINDOW_WIDTH, 0, 0, scratchBuffer); - outputText (0x17, WINDOW_WIDTH, 1, 0, scratchBuffer); - outputText (0x17, WINDOW_WIDTH, 2, 0, scratchBuffer); - outputText (0x17, 2, 0, 0, statusLine); -#ifdef CHEAT_OMNISCIENCE - outputText (0x17, 1, 0, 40, "\xB3"); - outputText (0x17, 1, 1, 40, "\xB3"); - outputText (0x17, 1, 2, 40, "\xB3"); -#endif - outputNumber(0x13, 0, 2, 0, 3, 0); - outputText (0x17, 1, 0, 6, statusLine+2); - outputText (0x13, 2, 0, 8, statusLine+3); - outputNumber(0x13, 0, 4, 0, 11, 0); - outputText (0x17, 1, 0, 16, statusLine+5); - outputText (0x13, 1, 0, 18, statusLine+6); - outputNumber(0x13, 0, 4, 0, 20, 0); - outputText (0x17, 1, 0, 25, statusLine+7); - outputText (0x17, 5, 0, 27, statusLine+8); - outputNumber(0x17, 0, 1, 0, 38, skillLevelNumber); - outputText (0x17, 1, 0, 37, &skillLetter); - outputText (0x17, 2, 1, 0, statusLine+13); - outputText (0x17, 1, 1, 6, statusLine+15); - outputText (0x17, 2, 1, 8, statusLine+16); - outputText (0x17, 1, 1, 16, statusLine+18); - outputText (0x17, 1, 1, 18, statusLine+19); - outputText (0x17, 1, 1, 25, statusLine+20); - outputText (0x17, 4, 1, 27, statusLine+21); - memcpy(scratchBuffer, statusLine+25, 40); - scratchBuffer[0] = numLives + '0'; - scratchBuffer[1] = 0; - if (numLives == 1) - scratchBuffer[6] = 'a'; - outputText (0x17, 40, 2, 0, scratchBuffer); - - lastHUD_numSmallSnipes = 0xFF; - lastHUD_numSnipePortalsDestroyed = 0xFF; - lastHUD_numLargeSnipes = 0xFF; - lastHUD_numSmallSnipesDestroyed = 0xFFFF; - lastHUD_numLargeSnipesDestroyed = 0xFFFF; - lastHUD_numPlayerDeaths = 0xFF; - lastHUD_score = -1; -} - -#define MAZE_SCRATCH_BUFFER_SIZE (MAZE_WIDTH_IN_CELLS * MAZE_HEIGHT_IN_CELLS) - -void CreateMaze_helper(SHORT &data_1E0, BYTE data_2AF) -{ - switch (data_2AF) - { - case 0: - data_1E0 -= MAZE_WIDTH_IN_CELLS; - if (data_1E0 < 0) - data_1E0 += MAZE_SCRATCH_BUFFER_SIZE; - break; - case 1: - data_1E0 += MAZE_WIDTH_IN_CELLS; - if (data_1E0 >= MAZE_SCRATCH_BUFFER_SIZE) - data_1E0 -= MAZE_SCRATCH_BUFFER_SIZE; - break; - case 2: - if ((data_1E0 & 0xF) == 0) - data_1E0 += 0xF; - else - data_1E0--; - break; - case 3: - if ((data_1E0 & 0xF) == 0xF) - data_1E0 -= 0xF; - else - data_1E0++; - break; - default: - UNREACHABLE; - } -} - -void CreateMaze() -{ - BYTE (&mazeScratchBuffer)[MAZE_SCRATCH_BUFFER_SIZE] = (BYTE(&)[MAZE_SCRATCH_BUFFER_SIZE])objects; - - memset(mazeScratchBuffer, 0xF, MAZE_SCRATCH_BUFFER_SIZE); - mazeScratchBuffer[0] = 0xE; - mazeScratchBuffer[1] = 0xD; - Uint numMazeCellsUninitialized = MAZE_SCRATCH_BUFFER_SIZE - 2; - - static const BYTE data_EA3[] = {8, 4, 2, 1}; - static const BYTE data_EA7[] = {4, 8, 1, 2}; - - for (;;) - { - outer_loop: - if (!numMazeCellsUninitialized) - break; - WORD data_1DE = GetRandomRanged(); - if (mazeScratchBuffer[data_1DE] != 0xF) - continue; - WORD data_1E0; - WORD data_1E2 = GetRandomMasked(3); - WORD data_1E4 = 0; - BYTE data_2AF; - for (;;) - { - if (data_1E4 > 3) - goto outer_loop; - data_2AF = data_1E2 & 3; - data_1E0 = data_1DE; - CreateMaze_helper((SHORT&)data_1E0, data_2AF); - if (mazeScratchBuffer[data_1E0] != 0xF) - break; - data_1E2++; - data_1E4++; - } - numMazeCellsUninitialized--; - mazeScratchBuffer[data_1E0] ^= data_EA7[data_2AF]; - mazeScratchBuffer[data_1DE] ^= data_EA3[data_2AF]; - data_1E2 = data_1DE; - for (;;) - { - BYTE data_2AF = (BYTE)GetRandomMasked(3); - BYTE data_2B0 = (BYTE)GetRandomMasked(3) + 1; - data_1E0 = data_1E2; - for (;;) - { - CreateMaze_helper((SHORT&)data_1E0, data_2AF); - if (!data_2B0 || mazeScratchBuffer[data_1E0] != 0xF) - break; - mazeScratchBuffer[data_1E0] ^= data_EA7[data_2AF]; - mazeScratchBuffer[data_1E2] ^= data_EA3[data_2AF]; - numMazeCellsUninitialized--; - data_2B0--; - data_1E2 = data_1E0; - } - if (data_2B0) - goto outer_loop; - } - } - for (WORD data_1DE = 0; data_1DE < 0x40; data_1DE++) - { - WORD data_1E0 = GetRandomRanged(); - BYTE data_2AF = (BYTE)GetRandomMasked(3); - mazeScratchBuffer[data_1E0] &= ~data_EA3[data_2AF]; - CreateMaze_helper((SHORT&)data_1E0, data_2AF); - mazeScratchBuffer[data_1E0] &= ~data_EA7[data_2AF]; - } - for (Uint i=0; i<_countof(maze); i++) - maze[i] = MazeTile(0x9, ' '); - WORD data_1E4 = 0; - WORD data_1E2 = 0; - for (WORD data_1DE = 0; data_1DE < MAZE_HEIGHT_IN_CELLS; data_1DE++) - { - for (WORD data_1E0 = 0; data_1E0 < MAZE_WIDTH_IN_CELLS; data_1E0++) - { - if (mazeScratchBuffer[data_1E2] & 8) - for (Uint i=0; i= MAZE_WIDTH) - x = 0; - } - mazeTile += MAZE_WIDTH; - if (mazeTile >= &maze[_countof(maze)]) - mazeTile -= _countof(maze); - } - return false; -} - -void PlotObjectToMaze() // plots object currentObject with sprite currentSprite -{ - BYTE spriteHeight = ((BYTE*)currentSprite)[0]; - BYTE spriteWidth = ((BYTE*)currentSprite)[1]; - MazeTile *spriteTile = (MazeTile*)¤tSprite[1]; - MazeTile *mazeTile = &maze[currentObject->y * MAZE_WIDTH]; - for (BYTE row = 0; row < spriteHeight; row++) - { - BYTE x = currentObject->x; - for (BYTE column = 0; column < spriteWidth; column++) - { - mazeTile[x] = *spriteTile++; - if (++x >= MAZE_WIDTH) - x = 0; - } - mazeTile += MAZE_WIDTH; - if (mazeTile >= &maze[_countof(maze)]) - mazeTile -= _countof(maze); - } -} - -void PlaceObjectInRandomUnoccupiedMazeCell() -{ - do - { - currentObject->x = (BYTE)GetRandomRanged() * MAZE_CELL_WIDTH + MAZE_CELL_WIDTH /2; - currentObject->y = (BYTE)GetRandomRanged() * MAZE_CELL_HEIGHT + MAZE_CELL_HEIGHT/2; - } - while (IsObjectLocationOccupied(currentObject->y, currentObject->x)); -} - -void CreateSnipePortalsAndPlayer() -{ - for (WORD data_B58 = 1; data_B58 <= OBJECT_LASTFREE; data_B58++) - objects[data_B58].next = data_B58 + 1; - objects[OBJECT_LASTFREE].next = 0; - objectHead_free = 1; - objectHead_weapons = 0; - objectHead_explosions = 0; - objectHead_smallSnipes = 0; - objectHead_largeSnipes = 0; - objectHead_snipePortals = 0; - for (WORD data_B58 = 0; data_B58 < numSnipePortalsAtStart; data_B58++) - { - ObjectIndex newSnipePortal = CreateNewObject(); - objects[newSnipePortal].next = objectHead_snipePortals; - objectHead_snipePortals = newSnipePortal; - SnipePortal &snipePortal = SetCurrentObject(newSnipePortal); - snipePortal.sprite = FAKE_POINTER(1002); - currentSprite = data_1002; - PlaceObjectInRandomUnoccupiedMazeCell(); - snipePortal.unused = 0; - snipePortal.animFrame = (BYTE)GetRandomMasked(0xF); - snipePortal.spawnFrame = 1; - PlotObjectToMaze(); - } - numSnipePortals = numSnipePortalsAtStart; - numSmallSnipesDestroyed = 0; - numSmallSnipes = 0; - numWeapons = 0; - numLargeSnipesDestroyed = 0; - numPlayerDeaths = 0; - numLargeSnipes = 0; - score = 0; - playerAnimEyesNotWide = false; - playerEyeAnimFrame = 0; - isPlayerDying = false; - isPlayerExploding = false; - player.sprite = FAKE_POINTER(10E2); - currentSprite = data_10E2; - currentObject = (Object*)&player; - PlaceObjectInRandomUnoccupiedMazeCell(); - PlotObjectToMaze(); - viewportFocusX = player.x; - viewportFocusY = player.y; - player.inputFrame = 1; - player.firingFrame = 1; -} - -void SetSoundEffectState(BYTE frame, SoundEffect index) -{ - if (currentSoundEffect != SoundEffect_None && index < currentSoundEffect) - return; - if (!shooting_sound_enabled && !index) - return; - currentSoundEffectFrame = frame; - currentSoundEffect = index; -} - -void DrawViewport(); - -bool UpdateHUD(bool incrementFrame = true) // returns true if the match has been won -{ - frame += incrementFrame; - if (lastHUD_numLargeSnipesDestroyed != numLargeSnipesDestroyed) - outputNumber(0x13, 0, 4, 0, 11, lastHUD_numLargeSnipesDestroyed = numLargeSnipesDestroyed); - if (lastHUD_numSmallSnipesDestroyed != numSmallSnipesDestroyed) - outputNumber(0x13, 0, 4, 0, 20, lastHUD_numSmallSnipesDestroyed = numSmallSnipesDestroyed); - if (lastHUD_numSnipePortalsDestroyed != numSnipePortals) - { - outputNumber(0x17, 0, 2, 1, 3, lastHUD_numSnipePortalsDestroyed = numSnipePortals); - outputNumber(0x13, 0, 2, 0, 3, numSnipePortalsAtStart - numSnipePortals); - } - if (lastHUD_numLargeSnipes != numLargeSnipes) - outputNumber(0x17, 0, 3, 1, 12, lastHUD_numLargeSnipes = numLargeSnipes); - if (lastHUD_numSmallSnipes != numSmallSnipes) - outputNumber(0x17, 0, 3, 1, 21, lastHUD_numSmallSnipes = numSmallSnipes); - if (lastHUD_score != score) - { - lastHUD_score = score; - if (lastHUD_score > 0) - outputNumber(0x17, 1, 5, 2, 33, lastHUD_score); - else - outputText (0x17, 6, 2, 33, statusLine+65); - } -#ifdef CHEAT_OMNISCIENCE - { - char hex[strlength("RNG 941,947")+1]; - sprintf(hex, "RNG %03u,%03u", random_seed_lo, random_seed_hi); - outputText(0x17, strlength("RNG 941,947"), 2, 42, hex); - } -#endif - if (lastHUD_numPlayerDeaths != numPlayerDeaths) - { - BYTE livesRemaining = numLives - (lastHUD_numPlayerDeaths = numPlayerDeaths); - if (livesRemaining == 1) - outputText (0x1C, 10, 2, 0, statusLine+71); - else - { - {} outputNumber(0x17, 0, 1, 2, 0, livesRemaining); - if (!livesRemaining) - { - outputNumber(0x1C, 0, 1, 2, 0, 0); - outputText (0x1C, 1, 2, 3, statusLine+81); - } - } - } - outputNumber(0x17, 0, 5, 1, 34, frame); // Time - - if (numLargeSnipes || numSnipePortals || numSmallSnipes) - return false; - - DrawViewport(); - EraseBottomTwoLines(); - CloseDirectConsole(WINDOW_HEIGHT-2); - WriteTextToConsole("Congratulations --- YOU ACTUALLY WON!!!\r\n"); - return true; -} - -void ExplodeObject(ObjectIndex arg) -{ - if (arg == OBJECT_PLAYER) // explode the player (and don't overwrite the player object) - { - arg = OBJECT_PLAYEREXPLOSION; - playerExplosion.x = player.x; - playerExplosion.y = player.y; - playerExplosion.spriteSize = EXPLOSION_SIZE(2,2); - playerExplosion.sprite = FAKE_POINTER(12C2); - isPlayerExploding = true; - } - Explosion &explosion = SetCurrentObject(arg); - explosion.next = objectHead_explosions; - objectHead_explosions = arg; - const WORD *data_CC4 = FakePointerToPointer(explosion.sprite); - BYTE data_CC8 = ((BYTE*)data_CC4)[0]; - BYTE data_CC9 = ((BYTE*)data_CC4)[1]; - if (data_CC8 == 2 && data_CC9 == 2) - { - explosion.sprite = FAKE_POINTER(12C2); - currentSprite = data_12C2; - explosion.spriteSize = EXPLOSION_SIZE(2,2); - explosion.animFrame = 0; - SetSoundEffectState(0, SoundEffect_ExplodePlayer); - } - if (data_CC8 == 1 && data_CC9 == 2) - { - explosion.sprite = FAKE_POINTER(1316); - currentSprite = data_1316; - explosion.spriteSize = EXPLOSION_SIZE(2,1); - explosion.animFrame = 0; - SetSoundEffectState(0, SoundEffect_ExplodeLargeSnipe); - } - if (data_CC8 == 1 && data_CC9 == 1) - { - explosion.sprite = FAKE_POINTER(136A); - currentSprite = data_136A; - explosion.spriteSize = EXPLOSION_SIZE(1,1); - explosion.animFrame = 2; - SetSoundEffectState(2, SoundEffect_ExplodeSmallSnipe); - } - PlotObjectToMaze(); -} - -void FreeObjectInList_worker(ObjectIndex *objectHead, ObjectIndex object) -{ - ObjectIndex data_CCA = *objectHead; - if (object == data_CCA) - { - *objectHead = objects[object].next; - return; - } - for (;;) - { - ObjectIndex data_CCB = objects[data_CCA].next; - if (!data_CCB) - return; - if (object == data_CCB) - break; - data_CCA = data_CCB; - } - objects[data_CCA].next = objects[object].next; -} - -void FreeObjectInList(ObjectIndex *objectHead, ObjectIndex object) -{ - if (object == OBJECT_PLAYER) - { - ExplodeObject(object); - return; - } - FreeObjectInList_worker(objectHead, object); - if (objectHead != &objectHead_weapons) - { - ExplodeObject(object); - return; - } - FreeObject(object); -} - -bool MoveWeaponAndTestHit(OrthogonalDirection arg) -{ - switch (weaponCollisionDirection = arg) - { - case OrthogonalDirection_Up: - weaponTestPos -= MAZE_WIDTH; - if (--currentObject->y == 0xFF) - { - currentObject->y = MAZE_HEIGHT - 1; - weaponTestPos += _countof(maze); - } - break; - case OrthogonalDirection_Right: - weaponTestPos++; - if (++currentObject->x >= MAZE_WIDTH) - { - currentObject->x = 0; - weaponTestPos -= MAZE_WIDTH; - } - break; - case OrthogonalDirection_Down: - weaponTestPos += MAZE_WIDTH; - if (++currentObject->y >= MAZE_HEIGHT) - { - currentObject->y = 0; - weaponTestPos -= _countof(maze); - } - break; - case OrthogonalDirection_Left: - weaponTestPos--; - if (--currentObject->x == 0xFF) - { - currentObject->x = MAZE_WIDTH - 1; - weaponTestPos += MAZE_WIDTH; - } - break; - default: - UNREACHABLE; - } - return weaponTestPos->chr != ' '; -} - -void UpdateWeapons() -{ - ObjectIndex prevObject = 0; - for (ObjectIndex object = objectHead_weapons; object;) - { - Weapon &weapon = SetCurrentObject(object); - weaponTestPos = &maze[weapon.y * MAZE_WIDTH + weapon.x]; - if (weaponTestPos->chr == 0xB2) - { - ObjectIndex nextObject = weapon.next; - *weaponTestPos = MazeTile(0x9, ' '); - FreeObjectInList(&objectHead_weapons, object); - numWeapons--; - object = nextObject; - continue; - } - *weaponTestPos = MazeTile(0x9, ' '); - switch (weapon.moveDirection) - { - case MoveDirection_UpRight: - if (MoveWeaponAndTestHit(OrthogonalDirection_Right) || (IsDiagonalDoubledPhase(weapon.y) && MoveWeaponAndTestHit(OrthogonalDirection_Right))) - break; - goto case_MoveDirection_Up; - case MoveDirection_DownRight: - if (MoveWeaponAndTestHit(OrthogonalDirection_Down) || (IsDiagonalDoubledPhase(weapon.y) && MoveWeaponAndTestHit(OrthogonalDirection_Right))) - break; - // fall through - case MoveDirection_Right: - if (MoveWeaponAndTestHit(OrthogonalDirection_Right)) - break; - goto main_139A; - case MoveDirection_Down: - if (MoveWeaponAndTestHit(OrthogonalDirection_Down)) - break; - goto main_139A; - case MoveDirection_DownLeft: - if (MoveWeaponAndTestHit(OrthogonalDirection_Down) || (IsDiagonalDoubledPhase(weapon.y) && MoveWeaponAndTestHit(OrthogonalDirection_Left))) - break; - // fall through - case MoveDirection_Left: - if (MoveWeaponAndTestHit(OrthogonalDirection_Left)) - break; - goto main_139A; - case MoveDirection_UpLeft: - if (MoveWeaponAndTestHit(OrthogonalDirection_Left) || (IsDiagonalDoubledPhase(weapon.y) && MoveWeaponAndTestHit(OrthogonalDirection_Left))) - break; - // fall through - case MoveDirection_Up: - case_MoveDirection_Up: - if (MoveWeaponAndTestHit(OrthogonalDirection_Up)) - break; - main_139A: - if (weapon.weaponType!=WeaponType_Bullet) - currentSprite = FakePointerToPointer(weapon.sprite); - else - { - if (++weapon.animFrame > 3) - weapon.animFrame = 0; - currentSprite = data_11D4[weapon.animFrame]; - } - *weaponTestPos = (MazeTile&)currentSprite[1]; - prevObject = object; - object = weapon.next; - continue; - default: - UNREACHABLE; - } - BYTE find_this = weaponTestPos->chr; - if (!memchr(mazeWallCharacters, find_this, _countof(mazeWallCharacters))) - { - if (weapon.weaponType==WeaponType_Bullet) - { - if (memchr(enemyCharacters, find_this, _countof(enemyCharacters))) - score += 1; - else - if (IsSnipePortal(*weaponTestPos)) - score += 50; - } - else - if (snipePortalsResistSnipeSpears && IsSnipePortal(*weaponTestPos)) - goto main_150E; - *weaponTestPos = MazeTile(0xF, 0xB2); - } - else - if (enableBouncingBullets && weapon.weaponType==WeaponType_Bullet && weaponBouncesRemaining[object] && (weapon.moveDirection & MoveDirectionMask_Diagonal)) - { - weaponBouncesRemaining[object]--; - weapon.moveDirection = (MoveDirection)bulletBounceTable[weaponCollisionDirection][weapon.moveDirection]; - SetSoundEffectState(1, SoundEffect_Bullet); - MoveWeaponAndTestHit((weaponCollisionDirection + 2) & OrthogonalDirectionMask_All); - goto main_139A; - } - main_150E: - numWeapons--; - if (!prevObject) - objectHead_weapons = weapon.next; - else - objects[prevObject].next = weapon.next; - ObjectIndex nextObject = weapon.next; - FreeObject(object); - object = nextObject; - } -} - -struct OrthoDistanceInfo -{ - union - { - struct {BYTE x, y;}; - Coord xy; - }; - MoveDirection direction; -}; - -OrthoDistanceInfo GetOrthoDistanceAndDirection(Object &object) -// Calculates the orthogonal distance between object and the player -{ - OrthoDistanceInfo result; - BYTE bx = 1; - int ax = object.x - viewportFocusX; - if (ax <= 0) - { - bx = 0; - ax = -ax; - } - if (ax >= MAZE_WIDTH/2) - { - bx ^= 1; - ax = MAZE_WIDTH - ax; - } - result.x = ax; - ax = object.y - viewportFocusY; - if (ax < 0) - { - bx += 2; - ax = -ax; - } - if (ax >= MAZE_HEIGHT/2) - { - bx ^= 2; - ax = MAZE_HEIGHT - ax; - } - result.y = ax; - orthoDistance = result.x + result.y; - if (result.x == 0) - { - result.direction = (MoveDirection)(bx * 2); - return result; - } - if (result.y == 0) - { - result.direction = (MoveDirection)(bx * 4 + 2); - return result; - } - static const MoveDirection diagonalDirectionTable[] = {MoveDirection_UpRight, MoveDirection_UpLeft, MoveDirection_DownRight, MoveDirection_DownLeft}; - result.direction = diagonalDirectionTable[bx]; - return result; -} - -struct MoveObject_retval {bool hitObstruction; BYTE chrHit; Coord cx; MazeTile *bx_si;}; -MoveObject_retval MoveObject(MovingObject &object) -{ - Coord ax, cx, dx; - ax = cx = object.xy; - dx = 0; - int tmp; - switch (object.moveDirection) - { - case MoveDirection_UpRight: - tmp = cx.x + (dx.x = IsDiagonalDoubledPhase(cx.y) + 1); - if (tmp >= MAZE_WIDTH) - tmp -= MAZE_WIDTH; - cx.x = tmp; - // fall through - case MoveDirection_Up: - dx.y++; - tmp = cx.y - 1; - if (tmp < 0) - tmp = MAZE_HEIGHT - 1; - ax.y = cx.y = tmp; - break; - case MoveDirection_Right: - dx.x++; - tmp = cx.x + 1; - if (tmp >= MAZE_WIDTH) - tmp = 0; - cx.x = tmp; - break; - case MoveDirection_DownRight: - dx.y++; - tmp = cx.y + 1; - if (tmp >= MAZE_HEIGHT) - tmp = 0; - cx.y = tmp; - tmp = cx.x + (dx.x = IsDiagonalDoubledPhase(cx.y) + 1); - if (tmp >= MAZE_WIDTH) - tmp -= MAZE_WIDTH; - cx.x = tmp; - break; - case MoveDirection_Down: - dx.y++; - tmp = cx.y + 1; - if (tmp >= MAZE_HEIGHT) - tmp = 0; - cx.y = tmp; - break; - case MoveDirection_DownLeft: - dx.y++; - tmp = cx.y + 1; - if (tmp >= MAZE_HEIGHT) - tmp = 0; - cx.y = tmp; - tmp = cx.x - (dx.x = IsDiagonalDoubledPhase(cx.y) + 1); - if (tmp < 0) - tmp += MAZE_WIDTH; - ax.x = cx.x = tmp; - break; - case MoveDirection_Left: - dx.x++; - tmp = cx.x - 1; - if (tmp < 0) - tmp = MAZE_WIDTH - 1; - ax.x = cx.x = tmp; - break; - case MoveDirection_UpLeft: - tmp = cx.x - (dx.x = IsDiagonalDoubledPhase(cx.y) + 1); - if (tmp < 0) - tmp += MAZE_WIDTH; - cx.x = tmp; - dx.y++; - tmp = cx.y - 1; - if (tmp < 0) - tmp = MAZE_HEIGHT - 1; - cx.y = tmp; - ax = cx; - break; - default: - UNREACHABLE; - } - const WORD *ptr = FakePointerToPointer(object.sprite); - dx.x += ((BYTE*)ptr)[1]; - dx.y += ((BYTE*)ptr)[0]; - for (MazeTile *mazeRow = &maze[ax.y * MAZE_WIDTH];;) - { - BYTE countX = dx.x; - for (size_t x = ax.x;;) - { - if (mazeRow[x].chr != ' ') - { - MoveObject_retval retval; - retval.hitObstruction = true; - retval.chrHit = mazeRow[x].chr; - retval.cx = cx; - retval.bx_si = &mazeRow[x]; - return retval; - } - if (--countX == 0) - break; - if (++x >= MAZE_WIDTH) - x = 0; - } - if (--dx.y == 0) - break; - mazeRow += MAZE_WIDTH; - if (mazeRow >= &maze[_countof(maze)]) - mazeRow -= _countof(maze); - } - MoveObject_retval retval; - retval.hitObstruction = false; - retval.chrHit = 0; - retval.cx = cx; - return retval; -} - -void FireWeapon(BYTE weaponType) -{ - MovingObject &shooter = *(MovingObject*)currentObject; - MoveDirection fireDirection = shooter.moveDirection; - BYTE x = shooter.x; - BYTE y = shooter.y; - switch (fireDirection) - { - case MoveDirection_UpRight: - x += ((BYTE*)currentSprite)[1]; - goto case_MoveDirection_Up; - case MoveDirection_Right: - x += ((BYTE*)currentSprite)[1]; - break; - case MoveDirection_DownRight: - x += ((BYTE*)currentSprite)[1]; - goto main_168F; - case MoveDirection_Down: - y += ((BYTE*)currentSprite)[0]; - x += ((BYTE*)currentSprite)[1] - 1; - break; - case MoveDirection_DownLeft: - x--; - main_168F: - y += ((BYTE*)currentSprite)[0]; - break; - case MoveDirection_Left: - x--; - break; - case MoveDirection_UpLeft: - x--; - // fall through - case MoveDirection_Up: - case_MoveDirection_Up: - y--; - break; - default: - UNREACHABLE; - } - if (x >= MAZE_WIDTH) - { - if (x > 240) - x += MAZE_WIDTH; - else - x -= MAZE_WIDTH; - } - if (y >= MAZE_HEIGHT) - { - if (y > 240) - y += MAZE_HEIGHT; - else - y -= MAZE_HEIGHT; - } - const WORD *currentSprite_backup = currentSprite; - currentSprite = data_1150; - if (IsObjectLocationOccupied(y, x)) - { - MazeTile &weaponHitPos = maze[y * MAZE_WIDTH + x]; - if (!memchr(mazeWallCharacters, weaponHitPos.chr, _countof(mazeWallCharacters))) - { - if (weaponType==WeaponType_Bullet) - { - if (memchr(enemyCharacters, weaponHitPos.chr, _countof(enemyCharacters))) - score += 1; - else - if (IsSnipePortal(weaponHitPos)) - score += 50; - weaponHitPos = MazeTile(0xF, 0xB2); - } - else - if (!(snipePortalsResistSnipeSpears && IsSnipePortal(weaponHitPos))) - weaponHitPos = MazeTile(0xF, 0xB2); - } - } - else - { - ObjectIndex newWeapon = CreateNewObject(); - if (newWeapon && numWeapons <= 50) - { - numWeapons++; - Object *currentObject_backup = currentObject; - Weapon &weapon = SetCurrentObject(newWeapon); - if (!objectHead_weapons) - objectHead_weapons = newWeapon; - else - { - ObjectIndex objectTail_weapons = objectHead_weapons; - while (ObjectIndex nextObject = objects[objectTail_weapons].next) - objectTail_weapons = nextObject; - objects[objectTail_weapons].next = newWeapon; - } - weapon.next = 0; - if (weaponType==WeaponType_Bullet) - weapon.sprite = FAKE_POINTER(1150); - else - weapon.sprite = PointerToFakePointer(data_11B4[fireDirection]); - weapon.x = x; - weapon.y = y; - weapon.moveDirection = fireDirection; - weapon.animFrame = 0; - weapon.weaponType = weaponType; - weaponBouncesRemaining[newWeapon] = (BYTE)GetRandomMasked(7) + 1; - currentSprite = FakePointerToPointer(weapon.sprite); - PlotObjectToMaze(); - currentObject = currentObject_backup; - } - } - currentSprite = currentSprite_backup; -} - -bool FireSnipeSpear() -{ - BYTE shiftCount = orthoDistance >> snipeShootingAccuracy; - if (shiftCount > 10) - return false; - if (GetRandomMasked(0xFFFF >> (15 - shiftCount))) - return false; - FireWeapon(WeaponType_Spear); - SetSoundEffectState(0, SoundEffect_Spear); - return true; -} - -void UpdateLargeSnipes() -{ - for (ObjectIndex object = objectHead_largeSnipes; object;) - { - LargeSnipe &snipe = ((LargeSnipe*)objects)[object]; - MazeTile *snipeMazeRow = &maze[snipe.y * MAZE_WIDTH]; - MazeTile * leftPart = &snipeMazeRow[snipe.x]; - MazeTile *rightPart = snipe.x >= MAZE_WIDTH-1 ? &snipeMazeRow[0] : leftPart + 1; - MazeTile *smallSnipePart; - if (leftPart->chr == 0xB2) - { - *leftPart = MazeTile(0x9, ' '); - smallSnipePart = rightPart; - } - else - if (rightPart->chr == 0xB2) - { - *rightPart = MazeTile(0x9, ' '); - smallSnipePart = leftPart; - } - else - if (--snipe.moveFrame == 0) - { - * leftPart = MazeTile(0x9, ' '); - *rightPart = MazeTile(0x9, ' '); - if (GetRandomMasked(3) != 0) // 3/4 chance of moving in the same direction as before - { - MoveObject_retval result = MoveObject(snipe); - if (!result.hitObstruction) - { - snipe.xy = result.cx; - if (!(snipe.moveDirection & MoveDirectionMask_Diagonal)) - goto main_2021; - snipe.moveFrame = 8; - goto main_2025; - } - } - if (GetRandomMasked(3) == 0) // 1/4 * 1/4 chance of moving in a random direction - snipe.moveDirection = (MoveDirection)GetRandomMasked(MoveDirectionMask_All); - else // 1/4 * 3/4 chance of moving toward the player - snipe.moveDirection = GetOrthoDistanceAndDirection(snipe).direction; - for (Uint count=8; count; count--) - { - MoveObject_retval result = MoveObject(snipe); - if (!result.hitObstruction) - break; - if (snipe.movementFlags & EnemyMovementFlag_TurnDirection) - snipe.moveDirection = (snipe.moveDirection - 1) & MoveDirectionMask_All; - else - snipe.moveDirection = (snipe.moveDirection + 1) & MoveDirectionMask_All; - } - snipe.sprite = PointerToFakePointer(data_1130[snipe.moveDirection]); - main_2021: - snipe.moveFrame = 6; - main_2025: - const WORD *sprite = FakePointerToPointer(snipe.sprite); - maze[snipe.y * MAZE_WIDTH + snipe.x] = (MazeTile&)sprite[1 + 0]; - if (snipe.x < MAZE_WIDTH-1) - maze[snipe.y * MAZE_WIDTH + snipe.x+1] = (MazeTile&)sprite[1 + 1]; - else - maze[snipe.y * MAZE_WIDTH ] = (MazeTile&)sprite[1 + 1]; - OrthoDistanceInfo orthoDist = GetOrthoDistanceAndDirection(snipe); - MoveDirection al = orthoDist.direction; - MoveDirection ah = snipe.moveDirection; - if (al != ah) - goto main_209E; - if (orthoDist.x && orthoDist.y) - { - if (abs((int)orthoDist.x * MAZE_CELL_HEIGHT - (int)orthoDist.y * MAZE_CELL_WIDTH) >= MAZE_CELL_WIDTH) - goto next_snipe; - al = snipe.moveDirection; - } - for (;;) - { - { - MoveDirection moveDirection = snipe.moveDirection; - snipe.moveDirection = al; - currentObject = (Object*)&snipe; - currentSprite = FakePointerToPointer(snipe.sprite); - bool tmp = FireSnipeSpear(); - snipe.moveDirection = moveDirection; - if (!tmp) - break; - } - if (maze[snipe.y * MAZE_WIDTH + snipe.x].chr != 0x01) - maze[snipe.y * MAZE_WIDTH + snipe.x] = MazeTile(0x9, 0xFF); - else - { - if (snipe.x < MAZE_WIDTH-1) - maze[snipe.y * MAZE_WIDTH + snipe.x+1] = MazeTile(0x9, 0xFF); - else - maze[snipe.y * MAZE_WIDTH ] = MazeTile(0x9, 0xFF); - } - break; - main_209E: - if (orthoDist.y <= 2 && al != MoveDirection_Up && al != MoveDirection_Down) - { - if (al <= MoveDirection_Down) - { - if (inrange(ah, MoveDirection_UpRight, MoveDirection_DownRight)) - { - al = MoveDirection_Right; - continue; - } - } - else - if (ah >= MoveDirection_DownLeft) - { - al = MoveDirection_Left; - continue; - } - } - if (orthoDist.x > 2) - break; - al = (al + 1) & MoveDirectionMask_All; - if (al == MoveDirection_UpLeft || al == MoveDirection_DownRight) - break; - if (al < MoveDirection_Down) - { - ah = (ah + 1) & MoveDirectionMask_All; - if (ah > MoveDirection_Right) - break; - al = MoveDirection_Up; - continue; - } - ah = (ah + 2) & MoveDirectionMask_All; - if (ah <= MoveDirection_Down) - break; - al = MoveDirection_Down; - } - goto next_snipe; - } - else - { - next_snipe: - object = snipe.next; - continue; - } - if (enableSmallSnipes && smallSnipePart->chr == 0x01) - { - *smallSnipePart = MazeTile(0x5, 0x02); - SmallSnipe &smallSnipe = (SmallSnipe&)snipe; - smallSnipe.x = smallSnipePart - snipeMazeRow; - - // move this object out of the LargeSnipe linked-list and into the SmallSnipe linked-list - for (ObjectIndex *nextPtr = &objectHead_largeSnipes;; nextPtr = &objects[*nextPtr].next) - { - if (*nextPtr == object) - { - ObjectIndex nextObject = *nextPtr = smallSnipe.next; - smallSnipe.next = objectHead_smallSnipes; - objectHead_smallSnipes = object; - object = nextObject; - break; - } - } - smallSnipe.moveFrame = 2; - smallSnipe.sprite = FAKE_POINTER(10FE); - numSmallSnipes++; - } - else - { - ObjectIndex nextObject = snipe.next; - FreeObjectInList(&objectHead_largeSnipes, object); - object = nextObject; - } - numLargeSnipes--; - numLargeSnipesDestroyed++; - } -} - -void UpdateSmallSnipes() -{ - for (ObjectIndex object = objectHead_smallSnipes; object;) - { - SmallSnipe &smallSnipe = ((SmallSnipe*)objects)[object]; - MazeTile &smallSnipeInMaze = maze[smallSnipe.y * MAZE_WIDTH + smallSnipe.x]; - if (smallSnipeInMaze.chr != 0xB2) - { - if (--smallSnipe.moveFrame) - { - object = smallSnipe.next; - continue; - } - } - else - { - destroy_smallSnipe: - ObjectIndex nextObject = smallSnipe.next; - FreeObjectInList(&objectHead_smallSnipes, object); - object = nextObject; - numSmallSnipes--; - numSmallSnipesDestroyed++; - continue; - } - smallSnipeInMaze = MazeTile(0x9, ' '); - if (!(smallSnipe.movementFlags & EnemyMovementFlag_SmallSnipeMoveStraight)) - { - OrthoDistanceInfo orthoDist = GetOrthoDistanceAndDirection(smallSnipe); - if (orthoDistance <= 4) - { - smallSnipe.moveDirection = orthoDist.direction; - MoveObject_retval result = MoveObject(smallSnipe); - if (result.hitObstruction && (smallSnipe.moveDirection & MoveDirectionMask_Diagonal)) - if (IsPlayer(result.chrHit)) - { - if (GetRandomMasked(smallSnipeExplosionChance) == 0) - { - result.bx_si->chr = 0xB2; - goto destroy_smallSnipe; - } -#if !defined(FIX_BUGS) && !defined(FIX_SMALL_SNIPE_LOGIC_COMPILER_BUG) - // A PL/M-86 compiler bug manifesting in the original game caused the CX register to be overwritten by the call to GetRandomMasked(). - // When a small snipe has a chance to explode next to the player, but doesn't, this bug changes the way it will move. - orthoDist.xy = 947; -#endif - } - } - if (orthoDist.y == 1) - orthoDist.direction = orthoDist.direction >= MoveDirection_Down ? MoveDirection_Left : MoveDirection_Right; - else - if (orthoDist.y < 1) - { - smallSnipe.moveDirection = ++orthoDist.direction; - MoveObject_retval result = MoveObject(smallSnipe); - if (!result.hitObstruction) - { - smallSnipe.xy = result.cx; - goto plot_smallSnipe_and_continue; - } - orthoDist.direction -= 2; - } - else - if (orthoDist.x == 1) - orthoDist.direction = (orthoDist.direction + 1) & MoveDirectionMask_UpDown; - else - if (orthoDist.x < 1) - { - smallSnipe.moveDirection = orthoDist.direction += 2; - MoveObject_retval result = MoveObject(smallSnipe); - if (!result.hitObstruction) - { - smallSnipe.xy = result.cx; - goto plot_smallSnipe_and_continue; - } - orthoDist.direction = (orthoDist.direction - 4) & MoveDirectionMask_All; - } - smallSnipe.moveDirection = orthoDist.direction; - { - MoveObject_retval result = MoveObject(smallSnipe); - if (!result.hitObstruction) - { - smallSnipe.xy = result.cx; - goto plot_smallSnipe_and_continue; - } - } - if (orthoDistance >= 20) - smallSnipe.movementFlags |= EnemyMovementFlag_SmallSnipeMoveStraight; - smallSnipe.moveDirection = (MoveDirection)GetRandomMasked(MoveDirectionMask_All); - } - for (Uint count=8; count; count--) - { - MoveObject_retval result = MoveObject(smallSnipe); - if (!result.hitObstruction) - { - smallSnipe.xy = result.cx; - break; - } - smallSnipe.movementFlags &= ~EnemyMovementFlag_SmallSnipeMoveStraight; - if (smallSnipe.movementFlags & EnemyMovementFlag_TurnDirection) - smallSnipe.moveDirection = (smallSnipe.moveDirection - 1) & MoveDirectionMask_All; - else - smallSnipe.moveDirection = (smallSnipe.moveDirection + 1) & MoveDirectionMask_All; - } - plot_smallSnipe_and_continue: - maze[smallSnipe.y * MAZE_WIDTH + smallSnipe.x] = MazeTile(0x5, 0x02); - smallSnipe.moveFrame = 3; - object = smallSnipe.next; - } -} - -bool IsObjectTaggedToExplode() -{ - BYTE spriteHeight = ((BYTE*)currentSprite)[0]; - BYTE spriteWidth = ((BYTE*)currentSprite)[1]; - MazeTile *mazeTile = &maze[currentObject->y * MAZE_WIDTH]; - for (BYTE row = 0; row < spriteHeight; row++) - { - BYTE x = currentObject->x; - for (BYTE column = 0; column < spriteWidth; column++) - { - if (mazeTile[x].chr == 0xB2) - return true; - if (++x >= MAZE_WIDTH) - x = 0; - } - mazeTile += MAZE_WIDTH; - if (mazeTile >= &maze[_countof(maze)]) - mazeTile -= _countof(maze); - } - return false; -} - -void EraseObjectFromMaze() -{ - BYTE spriteHeight = ((BYTE*)currentSprite)[0]; - BYTE spriteWidth = ((BYTE*)currentSprite)[1]; - MazeTile *mazeTile = &maze[currentObject->y * MAZE_WIDTH]; - for (BYTE row = 0; row < spriteHeight; row++) - { - BYTE x = currentObject->x; - for (BYTE column = 0; column < spriteWidth; column++) - { - mazeTile[x] = MazeTile(0x9, ' '); - if (++x >= MAZE_WIDTH) - x = 0; - } - mazeTile += MAZE_WIDTH; - if (mazeTile >= &maze[_countof(maze)]) - mazeTile -= _countof(maze); - } -} - -//template TYPE &IncWrap(TYPE &n, - -void UpdateSnipePortals() -{ - for (ObjectIndex object = objectHead_snipePortals; object;) - { - SnipePortal &snipePortal = SetCurrentObject(object); - if (++snipePortal.animFrame >= _countof(data_10A2)) - snipePortal.animFrame = 0; - currentSprite = data_10A2[snipePortal.animFrame]; - snipePortal.sprite = PointerToFakePointer(currentSprite); - if (IsObjectTaggedToExplode()) - { - ObjectIndex nextObject = snipePortal.next; - FreeObjectInList(&objectHead_snipePortals, object); - object = nextObject; - numSnipePortals--; - continue; - } - EraseObjectFromMaze(); - PlotObjectToMaze(); - if (--snipePortal.spawnFrame == 0) - { - GetOrthoDistanceAndDirection(*currentObject); - if (frame >= 0xF00) - snipePortal.spawnFrame = 5; - else - snipePortal.spawnFrame = 5 + (orthoDistance >> (frame/0x100 + 1)); - if (GetRandomMasked(0xF >> (numSnipePortalsAtStart - numSnipePortals)) == 0) - { - currentSprite = data_1112; - BYTE x = snipePortal.x + 2; -#ifdef EMULATE_LATENT_BUGS - if (x > MAZE_WIDTH - 1) - x -= MAZE_WIDTH - 1; -#else - if (x >= MAZE_WIDTH) - x -= MAZE_WIDTH; -#endif - BYTE y = snipePortal.y; - if (!IsObjectLocationOccupied(y, x)) - { - if (numLargeSnipes + numSmallSnipes < maxSnipes) - { - ObjectIndex spawnedSnipeIndex = CreateNewObject(); - if (spawnedSnipeIndex) - { - numLargeSnipes++; - object = snipePortal.next; - LargeSnipe &spawnedSnipe = SetCurrentObject(spawnedSnipeIndex); - spawnedSnipe.next = objectHead_largeSnipes; - objectHead_largeSnipes = spawnedSnipeIndex; - spawnedSnipe.x = x; - spawnedSnipe.y = y; - spawnedSnipe.moveDirection = MoveDirection_Right; - spawnedSnipe.sprite = FAKE_POINTER(1112); - PlotObjectToMaze(); - spawnedSnipe.movementFlags = (BYTE)GetRandomMasked(1); // randomly set or clear EnemyMovementFlag_TurnDirection - spawnedSnipe.moveFrame = 4; - continue; - } - } - } - } - } - object = snipePortal.next; - } -} - -bool MovePlayer_helper(bool &hitObstruction, OrthogonalDirection arg) -{ - player.moveDirection = OrthoDirectionToMoveDirection(arg); - MoveObject_retval result = MoveObject(*(MovingObject*)currentObject); - if (result.hitObstruction) - return hitObstruction = true; - viewportFocusX = player.x = result.cx.x; - viewportFocusY = player.y = result.cx.y; - return false; -} - -bool MovePlayer() -{ - MoveDirection moveDirection = player.moveDirection; - bool hitObstruction = false; - switch (moveDirection) - { - case MoveDirection_UpRight: - MovePlayer_helper(hitObstruction, OrthogonalDirection_Right); - if (!IsDiagonalDoubledPhase(currentObject->y)) - goto case_MoveDirection_Up; - if (MovePlayer_helper(hitObstruction, OrthogonalDirection_Right)) - goto case_MoveDirection_Up; - if (!MovePlayer_helper(hitObstruction, OrthogonalDirection_Up)) - break; - goto case_MoveDirection_Left; - case MoveDirection_DownRight: - if (MovePlayer_helper(hitObstruction, OrthogonalDirection_Down)) - goto case_MoveDirection_Right; - if (IsDiagonalDoubledPhase(currentObject->y)) - MovePlayer_helper(hitObstruction, OrthogonalDirection_Right); - goto case_MoveDirection_Right; - case MoveDirection_Down: - MovePlayer_helper(hitObstruction, OrthogonalDirection_Down); - break; - case MoveDirection_DownLeft: - if (MovePlayer_helper(hitObstruction, OrthogonalDirection_Down)) - goto case_MoveDirection_Left; - if (!IsDiagonalDoubledPhase(currentObject->y)) - goto case_MoveDirection_Left; - MovePlayer_helper(hitObstruction, OrthogonalDirection_Left); - // fall through - case MoveDirection_Left: - case_MoveDirection_Left: - MovePlayer_helper(hitObstruction, OrthogonalDirection_Left); - break; - case MoveDirection_UpLeft: - MovePlayer_helper(hitObstruction, OrthogonalDirection_Left); - if (!IsDiagonalDoubledPhase(currentObject->y)) - goto case_MoveDirection_Up; - if (MovePlayer_helper(hitObstruction, OrthogonalDirection_Left)) - goto case_MoveDirection_Up; - if (!MovePlayer_helper(hitObstruction, OrthogonalDirection_Up)) - break; - // fall through - case MoveDirection_Right: - case_MoveDirection_Right: - MovePlayer_helper(hitObstruction, OrthogonalDirection_Right); - break; - case MoveDirection_Up: - case_MoveDirection_Up: - MovePlayer_helper(hitObstruction, OrthogonalDirection_Up); - break; - default: - UNREACHABLE; - } - if (!hitObstruction) - PlotObjectToMaze(); - player.moveDirection = moveDirection; - return !hitObstruction; -} - -bool UpdatePlayer(bool playbackMode, BYTE &replayIO) // returns true if the match has been lost -{ - if (!playbackMode) - replayIO = 0; - currentObject = (Object*)&player; - if (++playerEyeAnimFrame > 7) - { - playerEyeAnimFrame = 0; - playerAnimEyesNotWide ^= true; - } - if (playerAnimEyesNotWide) - currentSprite = data_10E2; - else - currentSprite = data_10EC; - if (isPlayerDying) - { - keyboard_state = PollKeyboard(); - if (numPlayerDeaths >= numLives) - { - DrawViewport(); - EraseBottomTwoLines(); - CloseDirectConsole(WINDOW_HEIGHT-2); - WriteTextToConsole("The SNIPES have triumphed!!!\r\n"); - return true; - } - if (!isPlayerExploding) - { - keyboard_state = PollKeyboard(); - isPlayerDying = false; - PlaceObjectInRandomUnoccupiedMazeCell(); - viewportFocusX = player.x; - viewportFocusY = player.y; - PlotObjectToMaze(); - return false; - } - return false; - } - else - { - if (--player.inputFrame) - { - if (IsObjectTaggedToExplode()) - goto explode_player; - return false; - } - keyboard_state = PollKeyboard(); - player.inputFrame = 2; - if (IsObjectTaggedToExplode()) - goto explode_player; - } - EraseObjectFromMaze(); - static const BYTE arrowKeyMaskToDirectionTable[] = - { - 0, - MoveDirection_Right, - MoveDirection_Left, - 0, - MoveDirection_Down, - MoveDirection_DownRight, - MoveDirection_DownLeft, - 0, - MoveDirection_Up, - MoveDirection_UpRight, - MoveDirection_UpLeft, - 0, - 0, - 0, - 0, - 0, - }; - BYTE moveDirection, keyboardMove; - if (playbackMode) - { - spacebar_state = replayIO >> 7; - moveDirection = (replayIO & 0x7F) % 9; - if (moveDirection) - { - moveDirection--; - goto playback_move; - } - } - else - if ((keyboardMove = keyboard_state & (KEYSTATE_MOVE_RIGHT | KEYSTATE_MOVE_LEFT | KEYSTATE_MOVE_DOWN | KEYSTATE_MOVE_UP))) - { - moveDirection = arrowKeyMaskToDirectionTable[keyboardMove]; -playback_move: - player.moveDirection = (MoveDirection)moveDirection; - if (!playbackMode) - replayIO = moveDirection + 1; - if (MovePlayer()) - { - if (!spacebar_state) - goto main_1B8F; - if (!playbackMode) - replayIO += 0x80; - if (player.inputFrame == 1) - { - EraseObjectFromMaze(); - if (!MovePlayer()) - { - if (enableElectricWalls) - goto explode_player; - PlotObjectToMaze(); - } - } - player.inputFrame = 1; - goto main_1B8F; - } - if (enableElectricWalls) - goto explode_player; - } - PlotObjectToMaze(); -main_1B8F: - BYTE fireDirection; - if (playbackMode) - { - fireDirection = (replayIO & 0x7F) / 9 % 9; - if (fireDirection) - { - fireDirection--; - goto playback_fire; - } - } - else - if (!spacebar_state && (keyboard_state & (KEYSTATE_FIRE_RIGHT | KEYSTATE_FIRE_LEFT | KEYSTATE_FIRE_DOWN | KEYSTATE_FIRE_UP))) - { - fireDirection = arrowKeyMaskToDirectionTable[keyboard_state >> 4]; -playback_fire: - if (!playbackMode) - replayIO += (fireDirection + 1) * 9; - if (--player.firingFrame) - return false; - MoveDirection moveDirection_backup = player.moveDirection; - player.moveDirection = (MoveDirection)fireDirection; - FireWeapon(WeaponType_Bullet); - SetSoundEffectState(0, SoundEffect_Bullet); - player.moveDirection = moveDirection_backup; - player.firingFrame = player.inputFrame == 1 ? playerFiringPeriod<<1 : playerFiringPeriod; - return false; - } - player.firingFrame = 1; - return false; -explode_player: - FreeObjectInList(&player.next, OBJECT_PLAYER); // explode the player - isPlayerDying = true; - numPlayerDeaths++; - return false; -} - -void UpdateExplosions() -{ - for (ObjectIndex object = objectHead_explosions; object;) - { - Explosion &explosion = SetCurrentObject(object); - currentSprite = FakePointerToPointer(explosion.sprite); - EraseObjectFromMaze(); - BYTE animFrame = (explosion.animFrame + 1) % 6; - ObjectIndex nextObject = explosion.next; - explosion.animFrame++; - if (explosion.animFrame > (object == OBJECT_PLAYEREXPLOSION ? 11 : 5)) - { - FreeObjectInList_worker(&objectHead_explosions, object); - if (object != OBJECT_PLAYEREXPLOSION) - FreeObject(object); - else - isPlayerExploding = false; - } - else - { - if (explosion.spriteSize == EXPLOSION_SIZE(2,2)) - { - explosion.sprite = PointerToFakePointer(data_12FE[animFrame]); - currentSprite = data_12FE[animFrame]; - if (object == OBJECT_PLAYEREXPLOSION && explosion.animFrame >= 6) - SetSoundEffectState(5 - (explosion.animFrame - 6), SoundEffect_ExplodePlayer); - else - SetSoundEffectState(animFrame, SoundEffect_ExplodePlayer); - } - else - if (explosion.spriteSize == EXPLOSION_SIZE(2,1)) - { - explosion.sprite = PointerToFakePointer(data_1352[animFrame]); - currentSprite = data_1352[animFrame]; - SetSoundEffectState(animFrame, SoundEffect_ExplodeLargeSnipe); - } - else - if (explosion.spriteSize == EXPLOSION_SIZE(1,1)) - { - explosion.sprite = PointerToFakePointer(data_1392[animFrame]); - currentSprite = data_1392[animFrame]; - SetSoundEffectState(animFrame, SoundEffect_ExplodeSmallSnipe); - } - PlotObjectToMaze(); - } - object = nextObject; - } -} - -static const WORD sound_ExplodeSmallSnipe[] = { 100, 100, 1400, 1800, 1600, 1200}; -static const WORD sound_ExplodeLargeSnipe[] = {2200, 6600, 1800, 4400, 8400, 1100}; -static const WORD sound_ExplodePlayer [] = {2000, 8000, 6500, 4000, 2500, 1000}; - -void UpdateSound() -{ - if (!sound_enabled) - { - ClearSound(); - return; - } - if (currentSoundEffect == SoundEffect_None) - { -#ifdef STOP_WAVE_OUT_DURING_SILENCE - PlayTone(-1); -#else - PlayTone(0); -#endif - return; - } - switch (currentSoundEffect) - { - case SoundEffect_Bullet: - if (!currentSoundEffectFrame) - PlayTone(1900); - else - PlayTone(1400); - break; - case SoundEffect_Spear: - PlayTone(1600); - break; - case SoundEffect_ExplodeSmallSnipe: - PlayTone(sound_ExplodeSmallSnipe[currentSoundEffectFrame]); - break; - case SoundEffect_ExplodeLargeSnipe: - PlayTone(sound_ExplodeLargeSnipe[currentSoundEffectFrame]); - break; - case SoundEffect_ExplodePlayer: - PlayTone(sound_ExplodePlayer[currentSoundEffectFrame]); - break; - default: - UNREACHABLE; - } - currentSoundEffect = SoundEffect_None; -} - -void DrawViewport() -{ - WORD outputRow = VIEWPORT_ROW; - SHORT data_298 = viewportFocusY - VIEWPORT_HEIGHT / 2; - if (data_298 < 0) - data_298 += MAZE_HEIGHT; - SHORT data_296 = viewportFocusX - WINDOW_WIDTH / 2; - if (data_296 < 0) - data_296 += MAZE_WIDTH; - SHORT wrappingColumn = 0; - if (data_296 + WINDOW_WIDTH >= MAZE_WIDTH) - wrappingColumn = MAZE_WIDTH - data_296; - else - wrappingColumn = WINDOW_WIDTH; - for (Uint row = 0; row < VIEWPORT_HEIGHT; row++) - { - WORD data_29C = data_298 * MAZE_WIDTH; - WriteTextMem(wrappingColumn, outputRow, 0, &maze[data_296 + data_29C]); - if (wrappingColumn != WINDOW_WIDTH) - WriteTextMem(WINDOW_WIDTH - wrappingColumn, outputRow, wrappingColumn, &maze[data_29C]); - outputRow++; - if (++data_298 == MAZE_HEIGHT) - data_298 = 0; - } -} - -void WaitForNextTick(WORD &tick_count) -{ - for (;;) - { - SleepTimeslice(); - WORD tick_count2 = GetTickCountWord(); - if (tick_count2 != tick_count) - { - tick_count = tick_count2; - break; - } - } -} - -#if !(defined(_WIN32) || defined(_WIN64)) || defined(_CONSOLE) -int __cdecl main(int argc, char* argv[]) -#else -extern "C" int __cdecl SDL_main(int argc, char* argv[]) -#endif -{ - if (argc > 2) - { - fprintf(stderr, "Usage: %s [filename of replay to play back]\n", argv[0]); - return -1; - } - bool playbackMode; - const char *replayPlaybackFilename = NULL; - if ((playbackMode = (argc == 2))) - replayPlaybackFilename = argv[1]; - - if (int result = OpenConsole()) - return result; - if (int result = OpenTimer()) - { - CloseConsole(); - return result; - } - if (int result = OpenSound()) - { - CloseConsole(); - CloseTimer(); - return result; - } - - WORD tick_count; - -#ifdef CHEAT - rerecordingMode = !playbackMode; - bool replayFileWriting = false; -#endif - - ClearConsole(); - if (!playbackMode) - { -#define _ " " -#define S "\x01" -#define i "\x18" -#define TITLE_SCREEN \ - " ported by David Ellsworth\r\n"\ - "\r\n"\ - _ _ _ _ i i i _ _ i _ _ _ i _ i i i _ i i i i _ _ i i i i i _ _ i i i "\r\n"\ - _ _ _ i S S S i _ S _ _ _ S _ S S S _ S S S S i _ S S S S S _ i S S S i "\r\n"\ - _ _ _ S _ _ _ S _ i i _ _ i _ _ i _ _ i _ _ _ S _ i _ _ _ _ _ S _ _ _ S "\r\n"\ - _ _ _ i _ _ _ _ _ S S _ _ S _ _ S _ _ S _ _ _ i _ S _ _ _ _ _ i "\r\n"\ - _ _ _ S i i i _ _ i _ i _ i _ _ i _ _ i i i i S _ i i i i _ _ S i i i "\r\n"\ - _ _ _ _ S S S i _ S _ S _ S _ _ S _ _ S S S S _ _ S S S S _ _ _ S S S i "\r\n"\ - _ _ _ _ _ _ _ S _ i _ _ i i _ _ i _ _ i _ _ _ _ _ i _ _ _ _ _ _ _ _ _ S "\r\n"\ - _ _ _ i _ _ _ i _ S _ _ S S _ _ S _ _ S _ _ _ _ _ S _ _ _ _ _ i _ _ _ i "\r\n"\ - _ _ _ S i i i S _ i _ _ _ i _ i i i _ i _ _ _ _ _ i i i i i _ S i i i S "\r\n"\ - _ _ _ _ S S S _ _ S _ _ _ S _ S S S _ S _ _ _ _ _ S S S S S _ _ S S S "\r\n"\ - "\r\n\r\n"\ - "(c)Copyright SuperSet Software Corp 1982\r\n"\ - "All Rights Reserved\r\n"\ - "\r\n\r\n" - WriteTextToConsole(TITLE_SCREEN "Enter skill level (A-Z)(1-9): "); -#undef _ -#undef S -#undef i - ReadSkillLevel(); - if (got_ctrl_break) - goto do_not_play_again; - } - - tick_count = GetTickCountWord(); - random_seed_lo = (BYTE)tick_count; - if (!random_seed_lo) - random_seed_lo = 444; - random_seed_hi = tick_count >> 8; - if (!random_seed_hi) - random_seed_hi = 555; - - for (;;) - { - FILE *replayFile = NULL; - if (playbackMode) - { - replayFile = fopen(replayPlaybackFilename, "rb"); - if (!replayFile) - { - fprintf(stderr, "Error opening replay file \"%s\" for playback\n", replayPlaybackFilename); - goto do_not_play_again; - } - - #define fread_all(ptr, size, count, stream) \ - do \ - { \ - size_t _recordsRead = fread(ptr, size, count, stream); \ - if (_recordsRead != count) \ - { \ - fprintf(stderr, "Error reading from replay file \"%s\" for playback\n", replayPlaybackFilename); \ - goto do_not_play_again; \ - } \ - } while (0) - - fread_all(&random_seed_lo, sizeof(random_seed_lo), 1, replayFile); - fread_all(&random_seed_hi, sizeof(random_seed_hi), 1, replayFile); - fread_all(&skillLevelLetter, 1, 1, replayFile); - fread_all(&skillLevelNumber, 1, 1, replayFile); - - #undef fread_all - } - else - { - time_t rectime = time(NULL); - struct tm *rectime_gmt; - rectime_gmt = gmtime(&rectime); - - char replayFilename[1024]; - sprintf(replayFilename, - "%04d-%02d-%02d %02d.%02d.%02d.SnipesGame", - 1900+rectime_gmt->tm_year, rectime_gmt->tm_mon+1, rectime_gmt->tm_mday, - rectime_gmt->tm_hour, rectime_gmt->tm_min, rectime_gmt->tm_sec); - -#ifdef CHEAT - replayFile = fopen(replayFilename, "w+b"); -#else - replayFile = fopen(replayFilename, "wb"); -#endif - if (replayFile) - { - setvbuf(replayFile, NULL, _IOFBF, 64); - fwrite(&random_seed_lo, sizeof(random_seed_lo), 1, replayFile); - fwrite(&random_seed_hi, sizeof(random_seed_hi), 1, replayFile); - fwrite(&skillLevelLetter, 1, 1, replayFile); - fwrite(&skillLevelNumber, 1, 1, replayFile); - } - } - - enableElectricWalls = skillLevelLetter >= 'M'-'A'; - snipePortalsResistSnipeSpears = skillLevelLetter >= 'W'-'A'; - enableBouncingBullets = bouncingBulletTable [skillLevelLetter]; - snipeShootingAccuracy = snipeShootingAccuracyTable [skillLevelLetter]; - enableSmallSnipes = enableSmallSnipesTable [skillLevelLetter]; - smallSnipeExplosionChance = smallSnipeExplosionChanceTable[skillLevelLetter]; - maxSnipes = maxSnipesTable [skillLevelNumber-1]; - numSnipePortalsAtStart = numSnipePortalsTable [skillLevelNumber-1]; - numLives = numLivesTable [skillLevelNumber-1]; - playerFiringPeriod = 2; - - OpenDirectConsole(); - if (int result = OpenKeyboard()) - { - CloseConsole(); - CloseTimer(); - CloseSound(); - return result; - } - -#ifdef CHEAT - WORD init_random_seed_lo = random_seed_lo; - WORD init_random_seed_hi = random_seed_hi; - - restart: -#endif - frame = 0; - InitializeHUD(); - CreateMaze(); - CreateSnipePortalsAndPlayer(); - SetSoundEffectState(0, SoundEffect_None); - - if (playbackMode && _PLAYBACK_FOR_SCREEN_RECORDING) - { - UpdateHUD(false); - WaitForKeyPress(); - WaitForNextTick(tick_count = GetTickCountWord()); - } - - for (;;) - { -#ifdef CHEAT - if (skip_to_frame && frame == skip_to_frame-1) - { - skip_to_frame = 0; - playbackMode = !rerecordingMode; - auto fileSize = ftell(replayFile); - if (replayFileWriting) - changesize(_fileno(replayFile), fileSize); - BYTE dummy; - size_t blah = fread(&dummy, 1, 1, replayFile); - fseek(replayFile, fileSize, SEEK_SET); - } - if (UpdateHUD()) - break; - if (!skip_to_frame) - { - DrawViewport(); - if (single_step > 0) - single_step--; - while (single_step == 0 && !step_backwards && !(increment_initial_seed && replayFileWriting)) - { - PollKeyboard(); - if (forfeit_match) - goto match_ended; - if (frame == 0) - step_backwards = 0; - if (rerecordingMode && !replayFileWriting && replayPlaybackFilename) - { - auto fileSize = ftell(replayFile); - fclose(replayFile); - replayFile = fopen(replayPlaybackFilename, "r+b"); - if (replayFile) - { - replayFileWriting = true; - playbackMode = false; - } - else - { - replayFile = fopen(replayPlaybackFilename, "rb"); - if (!replayFile) - { - fprintf(stderr, "Error opening replay file \"%s\" for playback\n", replayPlaybackFilename); - goto do_not_play_again; - } - rerecordingMode = false; - increment_initial_seed = 0; - } - fseek(replayFile, fileSize, SEEK_SET); - changesize(_fileno(replayFile), fileSize); - } - } - if (step_backwards) - { - skip_to_frame = frame - step_backwards; - step_backwards = 0; - single_step = 0; - if (skip_to_frame) - playbackMode = true; - fflush(replayFile); - fseek(replayFile, 6, SEEK_SET); - - random_seed_lo = init_random_seed_lo; - random_seed_hi = init_random_seed_hi; - currentSoundEffect = SoundEffect_None; - - goto restart; - } - if (frame==1 && step_backwards==0 && increment_initial_seed && replayFileWriting) - { - init_random_seed_lo += increment_initial_seed; - increment_initial_seed = 0; - if (char tmp = init_random_seed_lo >> 8) - { - init_random_seed_hi += tmp; - init_random_seed_lo &= 0xFF; - init_random_seed_hi &= 0xFF; - } - random_seed_lo = init_random_seed_lo; - random_seed_hi = init_random_seed_hi; - - fflush(replayFile); - fseek(replayFile, 0, SEEK_SET); - fwrite(&random_seed_lo, sizeof(random_seed_lo), 1, replayFile); - fwrite(&random_seed_hi, sizeof(random_seed_hi), 1, replayFile); - fwrite(&skillLevelLetter, 1, 1, replayFile); - fwrite(&skillLevelNumber, 1, 1, replayFile); - goto restart; - } - } -#else - if (UpdateHUD()) - break; - DrawViewport(); -#endif - - if (forfeit_match) - { - EraseBottomTwoLines(); - break; - } - -#ifdef CHEAT - if (!fast_forward && single_step<0 && !skip_to_frame) -#else - if (!playbackMode || !fast_forward) -#endif - WaitForNextTick(tick_count); - - UpdateWeapons(); - UpdateSmallSnipes(); - UpdateLargeSnipes(); - UpdateSnipePortals(); - - BYTE replayIO; - bool playbackFinished = false; - if (playbackMode && fread(&replayIO, 1, 1, replayFile) == 0) - playbackFinished = true; - if (UpdatePlayer(playbackMode, replayIO) || playbackFinished) - break; - if (!playbackMode && replayFile) - { - fwrite(&replayIO, 1, 1, replayFile); -#ifdef CHEAT - fflush(replayFile); -#endif - } - - UpdateExplosions(); - UpdateSound(); - } -#ifdef CHEAT - match_ended: -#endif - - if (playbackMode && _PLAYBACK_FOR_SCREEN_RECORDING) - WaitForNextTick(tick_count); - ClearSound(); - forfeit_match = false; - - if (replayFile) - fclose(replayFile); - - CloseDirectConsole(WINDOW_HEIGHT-1 - (playbackMode && !_PLAYBACK_FOR_SCREEN_RECORDING ? 1 : 0)); - if (instant_quit) - break; - if (playbackMode && !_PLAYBACK_FOR_SCREEN_RECORDING) - { -#ifndef _CONSOLE - // TODO: do this in the console build too, if it's possible to detect when Windows won't itself prompt the user to press any key - SetConsoleOutputTextColor(0x8); - WriteTextToConsole("\r\n""Press any key to continue..."); - WaitForKeyPress(); -#endif - break; - } - for (;;) - { - WriteTextToConsole("Play another game? (Y or N) "); - if (playbackMode) - { - WaitForKeyPress(); - goto do_not_play_again; - } - char playAgain; - auto numread = ReadTextFromConsole(&playAgain, 1); - if (got_ctrl_break) - goto do_not_play_again; - if (!numread) - continue; - if (playAgain == 'Y' || playAgain == 'y') - break; - if (playAgain == 'N' || playAgain == 'n') - goto do_not_play_again; - } - WriteTextToConsole("Enter new skill level (A-Z)(1-9): "); - ReadSkillLevel(); - if (got_ctrl_break) - break; - } -do_not_play_again: - - CloseSound(); - CloseTimer(); - CloseConsole(); - - return 0; -} +#include +#include +#include +#include +#include +#include "config.h" +#include "Snipes.h" +#include "types.h" +#include "macros.h" +#include "console.h" +#include "timer.h" +#include "sound.h" +#include "keyboard.h" +#include "platform.h" +#ifdef __APPLE__ +#include +#endif + +bool got_ctrl_break = false; +bool forfeit_match = false; +bool instant_quit = false; +bool sound_enabled = true; +bool shooting_sound_enabled = false; +BYTE fast_forward = false; +BYTE spacebar_state = false; +static BYTE keyboard_state = 0; +#ifdef CHEAT +bool rerecordingMode; +int single_step = 0; +int step_backwards = 0; +int increment_initial_seed = 0; +WORD skip_to_frame = 0; +#endif + +#if defined(PLAYBACK_FOR_SCREEN_RECORDING) && !defined(CHEAT) + #define _PLAYBACK_FOR_SCREEN_RECORDING 1 +#else + #define _PLAYBACK_FOR_SCREEN_RECORDING 0 +#endif + +static WORD random_seed_lo = 33, random_seed_hi = 467; +WORD GetRandomMasked(WORD mask) +{ + random_seed_lo *= 2; + if (random_seed_lo > 941) + random_seed_lo -= 941; + random_seed_hi *= 2; + if (random_seed_hi > 947) + random_seed_hi -= 947; + return (random_seed_lo + random_seed_hi) & mask; +} +template +WORD GetRandomRanged() +{ + WORD mask = RANGE-1; + mask |= mask >> 1; + mask |= mask >> 2; + mask |= mask >> 4; + mask |= mask >> 8; + if (mask == RANGE-1) + return GetRandomMasked(mask); + for (;;) + { + WORD number = GetRandomMasked(mask); + if (number < RANGE) + return number; + } +} + +Uint skillLevelLetter = 0; +Uint skillLevelNumber = 1; + +void ParseSkillLevel(char *skillLevel, DWORD skillLevelLength) +{ + Uint skillLevelNumberTmp = 0; + for (; skillLevelLength; skillLevel++, skillLevelLength--) + { + if (skillLevelNumberTmp >= 0x80) + { + // strange behavior, but this is what the original game does + skillLevelNumber = 1; + return; + } + char chr = *skillLevel; + if (inrange(chr, 'a', 'z')) + skillLevelLetter = chr - 'a'; + else + if (inrange(chr, 'A', 'Z')) + skillLevelLetter = chr - 'A'; + else + if (inrange(chr, '0', '9')) + skillLevelNumberTmp = skillLevelNumberTmp * 10 + (chr - '0'); + } + if (inrange(skillLevelNumberTmp, 1, 9)) + skillLevelNumber = skillLevelNumberTmp; +} + +void ReadSkillLevel() +{ + char skillLevel[0x80] = {}; + DWORD skillLevelLength = ReadTextFromConsole(skillLevel, _countof(skillLevel)); + + if (skillLevelLength && skillLevel[skillLevelLength-1] == '\n') + skillLevelLength--; + if (skillLevelLength && skillLevel[skillLevelLength-1] == '\r') + skillLevelLength--; + + for (Uint i=0; i +OBJECT_TYPE &SetCurrentObject(ObjectIndex i) +{ + OBJECT_TYPE &object = ((OBJECT_TYPE*)objects)[i]; // cast the array before dereferencing, to avoid a bug that would otherwise happen with GCC's "-fstrict-aliasing" optimization + currentObject = (Object*)&object; + return object; +} + +Player &player = ((Player *)objects)[OBJECT_PLAYER ]; +Explosion &playerExplosion = ((Explosion*)objects)[OBJECT_PLAYEREXPLOSION]; + +static MazeTile maze[MAZE_WIDTH * MAZE_HEIGHT]; + +static const char statusLine[] = "\xDA\xBF\xB3\x01\x1A\xB3\x02\xB3""Skill""\xC0\xD9\xB3\x01\x1A\xB3\x02\xB3""Time Men Left Score 0 0000001 Man Left""e"; + +void InitializeHUD() +{ + char (&scratchBuffer)[WINDOW_WIDTH] = (char(&)[WINDOW_WIDTH])maze; + + char skillLetter = skillLevelLetter + 'A'; + memset(scratchBuffer, ' ', WINDOW_WIDTH); + outputText (0x17, WINDOW_WIDTH, 0, 0, scratchBuffer); + outputText (0x17, WINDOW_WIDTH, 1, 0, scratchBuffer); + outputText (0x17, WINDOW_WIDTH, 2, 0, scratchBuffer); + outputText (0x17, 2, 0, 0, statusLine); +#ifdef CHEAT_OMNISCIENCE + outputText (0x17, 1, 0, 40, "\xB3"); + outputText (0x17, 1, 1, 40, "\xB3"); + outputText (0x17, 1, 2, 40, "\xB3"); +#endif + outputNumber(0x13, 0, 2, 0, 3, 0); + outputText (0x17, 1, 0, 6, statusLine+2); + outputText (0x13, 2, 0, 8, statusLine+3); + outputNumber(0x13, 0, 4, 0, 11, 0); + outputText (0x17, 1, 0, 16, statusLine+5); + outputText (0x13, 1, 0, 18, statusLine+6); + outputNumber(0x13, 0, 4, 0, 20, 0); + outputText (0x17, 1, 0, 25, statusLine+7); + outputText (0x17, 5, 0, 27, statusLine+8); + outputNumber(0x17, 0, 1, 0, 38, skillLevelNumber); + outputText (0x17, 1, 0, 37, &skillLetter); + outputText (0x17, 2, 1, 0, statusLine+13); + outputText (0x17, 1, 1, 6, statusLine+15); + outputText (0x17, 2, 1, 8, statusLine+16); + outputText (0x17, 1, 1, 16, statusLine+18); + outputText (0x17, 1, 1, 18, statusLine+19); + outputText (0x17, 1, 1, 25, statusLine+20); + outputText (0x17, 4, 1, 27, statusLine+21); + memcpy(scratchBuffer, statusLine+25, 40); + scratchBuffer[0] = numLives + '0'; + scratchBuffer[1] = 0; + if (numLives == 1) + scratchBuffer[6] = 'a'; + outputText (0x17, 40, 2, 0, scratchBuffer); + + lastHUD_numSmallSnipes = 0xFF; + lastHUD_numSnipePortalsDestroyed = 0xFF; + lastHUD_numLargeSnipes = 0xFF; + lastHUD_numSmallSnipesDestroyed = 0xFFFF; + lastHUD_numLargeSnipesDestroyed = 0xFFFF; + lastHUD_numPlayerDeaths = 0xFF; + lastHUD_score = -1; +} + +#define MAZE_SCRATCH_BUFFER_SIZE (MAZE_WIDTH_IN_CELLS * MAZE_HEIGHT_IN_CELLS) + +void CreateMaze_helper(SHORT &data_1E0, BYTE data_2AF) +{ + switch (data_2AF) + { + case 0: + data_1E0 -= MAZE_WIDTH_IN_CELLS; + if (data_1E0 < 0) + data_1E0 += MAZE_SCRATCH_BUFFER_SIZE; + break; + case 1: + data_1E0 += MAZE_WIDTH_IN_CELLS; + if (data_1E0 >= MAZE_SCRATCH_BUFFER_SIZE) + data_1E0 -= MAZE_SCRATCH_BUFFER_SIZE; + break; + case 2: + if ((data_1E0 & 0xF) == 0) + data_1E0 += 0xF; + else + data_1E0--; + break; + case 3: + if ((data_1E0 & 0xF) == 0xF) + data_1E0 -= 0xF; + else + data_1E0++; + break; + default: + UNREACHABLE; + } +} + +void CreateMaze() +{ + BYTE (&mazeScratchBuffer)[MAZE_SCRATCH_BUFFER_SIZE] = (BYTE(&)[MAZE_SCRATCH_BUFFER_SIZE])objects; + + memset(mazeScratchBuffer, 0xF, MAZE_SCRATCH_BUFFER_SIZE); + mazeScratchBuffer[0] = 0xE; + mazeScratchBuffer[1] = 0xD; + Uint numMazeCellsUninitialized = MAZE_SCRATCH_BUFFER_SIZE - 2; + + static const BYTE data_EA3[] = {8, 4, 2, 1}; + static const BYTE data_EA7[] = {4, 8, 1, 2}; + + for (;;) + { + outer_loop: + if (!numMazeCellsUninitialized) + break; + WORD data_1DE = GetRandomRanged(); + if (mazeScratchBuffer[data_1DE] != 0xF) + continue; + WORD data_1E0; + WORD data_1E2 = GetRandomMasked(3); + WORD data_1E4 = 0; + BYTE data_2AF; + for (;;) + { + if (data_1E4 > 3) + goto outer_loop; + data_2AF = data_1E2 & 3; + data_1E0 = data_1DE; + CreateMaze_helper((SHORT&)data_1E0, data_2AF); + if (mazeScratchBuffer[data_1E0] != 0xF) + break; + data_1E2++; + data_1E4++; + } + numMazeCellsUninitialized--; + mazeScratchBuffer[data_1E0] ^= data_EA7[data_2AF]; + mazeScratchBuffer[data_1DE] ^= data_EA3[data_2AF]; + data_1E2 = data_1DE; + for (;;) + { + BYTE data_2AF = (BYTE)GetRandomMasked(3); + BYTE data_2B0 = (BYTE)GetRandomMasked(3) + 1; + data_1E0 = data_1E2; + for (;;) + { + CreateMaze_helper((SHORT&)data_1E0, data_2AF); + if (!data_2B0 || mazeScratchBuffer[data_1E0] != 0xF) + break; + mazeScratchBuffer[data_1E0] ^= data_EA7[data_2AF]; + mazeScratchBuffer[data_1E2] ^= data_EA3[data_2AF]; + numMazeCellsUninitialized--; + data_2B0--; + data_1E2 = data_1E0; + } + if (data_2B0) + goto outer_loop; + } + } + for (WORD data_1DE = 0; data_1DE < 0x40; data_1DE++) + { + WORD data_1E0 = GetRandomRanged(); + BYTE data_2AF = (BYTE)GetRandomMasked(3); + mazeScratchBuffer[data_1E0] &= ~data_EA3[data_2AF]; + CreateMaze_helper((SHORT&)data_1E0, data_2AF); + mazeScratchBuffer[data_1E0] &= ~data_EA7[data_2AF]; + } + for (Uint i=0; i<_countof(maze); i++) + maze[i] = MazeTile(0x9, ' '); + WORD data_1E4 = 0; + WORD data_1E2 = 0; + for (WORD data_1DE = 0; data_1DE < MAZE_HEIGHT_IN_CELLS; data_1DE++) + { + for (WORD data_1E0 = 0; data_1E0 < MAZE_WIDTH_IN_CELLS; data_1E0++) + { + if (mazeScratchBuffer[data_1E2] & 8) + for (Uint i=0; i= MAZE_WIDTH) + x = 0; + } + mazeTile += MAZE_WIDTH; + if (mazeTile >= &maze[_countof(maze)]) + mazeTile -= _countof(maze); + } + return false; +} + +void PlotObjectToMaze() // plots object currentObject with sprite currentSprite +{ + BYTE spriteHeight = ((BYTE*)currentSprite)[0]; + BYTE spriteWidth = ((BYTE*)currentSprite)[1]; + MazeTile *spriteTile = (MazeTile*)¤tSprite[1]; + MazeTile *mazeTile = &maze[currentObject->y * MAZE_WIDTH]; + for (BYTE row = 0; row < spriteHeight; row++) + { + BYTE x = currentObject->x; + for (BYTE column = 0; column < spriteWidth; column++) + { + mazeTile[x] = *spriteTile++; + if (++x >= MAZE_WIDTH) + x = 0; + } + mazeTile += MAZE_WIDTH; + if (mazeTile >= &maze[_countof(maze)]) + mazeTile -= _countof(maze); + } +} + +void PlaceObjectInRandomUnoccupiedMazeCell() +{ + do + { + currentObject->x = (BYTE)GetRandomRanged() * MAZE_CELL_WIDTH + MAZE_CELL_WIDTH /2; + currentObject->y = (BYTE)GetRandomRanged() * MAZE_CELL_HEIGHT + MAZE_CELL_HEIGHT/2; + } + while (IsObjectLocationOccupied(currentObject->y, currentObject->x)); +} + +void CreateSnipePortalsAndPlayer() +{ + for (WORD data_B58 = 1; data_B58 <= OBJECT_LASTFREE; data_B58++) + objects[data_B58].next = data_B58 + 1; + objects[OBJECT_LASTFREE].next = 0; + objectHead_free = 1; + objectHead_weapons = 0; + objectHead_explosions = 0; + objectHead_smallSnipes = 0; + objectHead_largeSnipes = 0; + objectHead_snipePortals = 0; + for (WORD data_B58 = 0; data_B58 < numSnipePortalsAtStart; data_B58++) + { + ObjectIndex newSnipePortal = CreateNewObject(); + objects[newSnipePortal].next = objectHead_snipePortals; + objectHead_snipePortals = newSnipePortal; + SnipePortal &snipePortal = SetCurrentObject(newSnipePortal); + snipePortal.sprite = FAKE_POINTER(1002); + currentSprite = data_1002; + PlaceObjectInRandomUnoccupiedMazeCell(); + snipePortal.unused = 0; + snipePortal.animFrame = (BYTE)GetRandomMasked(0xF); + snipePortal.spawnFrame = 1; + PlotObjectToMaze(); + } + numSnipePortals = numSnipePortalsAtStart; + numSmallSnipesDestroyed = 0; + numSmallSnipes = 0; + numWeapons = 0; + numLargeSnipesDestroyed = 0; + numPlayerDeaths = 0; + numLargeSnipes = 0; + score = 0; + playerAnimEyesNotWide = false; + playerEyeAnimFrame = 0; + isPlayerDying = false; + isPlayerExploding = false; + player.sprite = FAKE_POINTER(10E2); + currentSprite = data_10E2; + currentObject = (Object*)&player; + PlaceObjectInRandomUnoccupiedMazeCell(); + PlotObjectToMaze(); + viewportFocusX = player.x; + viewportFocusY = player.y; + player.inputFrame = 1; + player.firingFrame = 1; +} + +void SetSoundEffectState(BYTE frame, SoundEffect index) +{ + if (currentSoundEffect != SoundEffect_None && index < currentSoundEffect) + return; + if (!shooting_sound_enabled && !index) + return; + currentSoundEffectFrame = frame; + currentSoundEffect = index; +} + +void DrawViewport(); + +bool UpdateHUD(bool incrementFrame = true) // returns true if the match has been won +{ + frame += incrementFrame; + if (lastHUD_numLargeSnipesDestroyed != numLargeSnipesDestroyed) + outputNumber(0x13, 0, 4, 0, 11, lastHUD_numLargeSnipesDestroyed = numLargeSnipesDestroyed); + if (lastHUD_numSmallSnipesDestroyed != numSmallSnipesDestroyed) + outputNumber(0x13, 0, 4, 0, 20, lastHUD_numSmallSnipesDestroyed = numSmallSnipesDestroyed); + if (lastHUD_numSnipePortalsDestroyed != numSnipePortals) + { + outputNumber(0x17, 0, 2, 1, 3, lastHUD_numSnipePortalsDestroyed = numSnipePortals); + outputNumber(0x13, 0, 2, 0, 3, numSnipePortalsAtStart - numSnipePortals); + } + if (lastHUD_numLargeSnipes != numLargeSnipes) + outputNumber(0x17, 0, 3, 1, 12, lastHUD_numLargeSnipes = numLargeSnipes); + if (lastHUD_numSmallSnipes != numSmallSnipes) + outputNumber(0x17, 0, 3, 1, 21, lastHUD_numSmallSnipes = numSmallSnipes); + if (lastHUD_score != score) + { + lastHUD_score = score; + if (lastHUD_score > 0) + outputNumber(0x17, 1, 5, 2, 33, lastHUD_score); + else + outputText (0x17, 6, 2, 33, statusLine+65); + } +#ifdef CHEAT_OMNISCIENCE + { + char hex[strlength("RNG 941,947")+1]; + sprintf(hex, "RNG %03u,%03u", random_seed_lo, random_seed_hi); + outputText(0x17, strlength("RNG 941,947"), 2, 42, hex); + } +#endif + if (lastHUD_numPlayerDeaths != numPlayerDeaths) + { + BYTE livesRemaining = numLives - (lastHUD_numPlayerDeaths = numPlayerDeaths); + if (livesRemaining == 1) + outputText (0x1C, 10, 2, 0, statusLine+71); + else + { + {} outputNumber(0x17, 0, 1, 2, 0, livesRemaining); + if (!livesRemaining) + { + outputNumber(0x1C, 0, 1, 2, 0, 0); + outputText (0x1C, 1, 2, 3, statusLine+81); + } + } + } + outputNumber(0x17, 0, 5, 1, 34, frame); // Time + + if (numLargeSnipes || numSnipePortals || numSmallSnipes) + return false; + + DrawViewport(); + EraseBottomTwoLines(); + CloseDirectConsole(WINDOW_HEIGHT-2); + WriteTextToConsole("Congratulations --- YOU ACTUALLY WON!!!\r\n"); + return true; +} + +void ExplodeObject(ObjectIndex arg) +{ + if (arg == OBJECT_PLAYER) // explode the player (and don't overwrite the player object) + { + arg = OBJECT_PLAYEREXPLOSION; + playerExplosion.x = player.x; + playerExplosion.y = player.y; + playerExplosion.spriteSize = EXPLOSION_SIZE(2,2); + playerExplosion.sprite = FAKE_POINTER(12C2); + isPlayerExploding = true; + } + Explosion &explosion = SetCurrentObject(arg); + explosion.next = objectHead_explosions; + objectHead_explosions = arg; + const WORD *data_CC4 = FakePointerToPointer(explosion.sprite); + BYTE data_CC8 = ((BYTE*)data_CC4)[0]; + BYTE data_CC9 = ((BYTE*)data_CC4)[1]; + if (data_CC8 == 2 && data_CC9 == 2) + { + explosion.sprite = FAKE_POINTER(12C2); + currentSprite = data_12C2; + explosion.spriteSize = EXPLOSION_SIZE(2,2); + explosion.animFrame = 0; + SetSoundEffectState(0, SoundEffect_ExplodePlayer); + } + if (data_CC8 == 1 && data_CC9 == 2) + { + explosion.sprite = FAKE_POINTER(1316); + currentSprite = data_1316; + explosion.spriteSize = EXPLOSION_SIZE(2,1); + explosion.animFrame = 0; + SetSoundEffectState(0, SoundEffect_ExplodeLargeSnipe); + } + if (data_CC8 == 1 && data_CC9 == 1) + { + explosion.sprite = FAKE_POINTER(136A); + currentSprite = data_136A; + explosion.spriteSize = EXPLOSION_SIZE(1,1); + explosion.animFrame = 2; + SetSoundEffectState(2, SoundEffect_ExplodeSmallSnipe); + } + PlotObjectToMaze(); +} + +void FreeObjectInList_worker(ObjectIndex *objectHead, ObjectIndex object) +{ + ObjectIndex data_CCA = *objectHead; + if (object == data_CCA) + { + *objectHead = objects[object].next; + return; + } + for (;;) + { + ObjectIndex data_CCB = objects[data_CCA].next; + if (!data_CCB) + return; + if (object == data_CCB) + break; + data_CCA = data_CCB; + } + objects[data_CCA].next = objects[object].next; +} + +void FreeObjectInList(ObjectIndex *objectHead, ObjectIndex object) +{ + if (object == OBJECT_PLAYER) + { + ExplodeObject(object); + return; + } + FreeObjectInList_worker(objectHead, object); + if (objectHead != &objectHead_weapons) + { + ExplodeObject(object); + return; + } + FreeObject(object); +} + +bool MoveWeaponAndTestHit(OrthogonalDirection arg) +{ + switch (weaponCollisionDirection = arg) + { + case OrthogonalDirection_Up: + weaponTestPos -= MAZE_WIDTH; + if (--currentObject->y == 0xFF) + { + currentObject->y = MAZE_HEIGHT - 1; + weaponTestPos += _countof(maze); + } + break; + case OrthogonalDirection_Right: + weaponTestPos++; + if (++currentObject->x >= MAZE_WIDTH) + { + currentObject->x = 0; + weaponTestPos -= MAZE_WIDTH; + } + break; + case OrthogonalDirection_Down: + weaponTestPos += MAZE_WIDTH; + if (++currentObject->y >= MAZE_HEIGHT) + { + currentObject->y = 0; + weaponTestPos -= _countof(maze); + } + break; + case OrthogonalDirection_Left: + weaponTestPos--; + if (--currentObject->x == 0xFF) + { + currentObject->x = MAZE_WIDTH - 1; + weaponTestPos += MAZE_WIDTH; + } + break; + default: + UNREACHABLE; + } + return weaponTestPos->chr != ' '; +} + +void UpdateWeapons() +{ + ObjectIndex prevObject = 0; + for (ObjectIndex object = objectHead_weapons; object;) + { + Weapon &weapon = SetCurrentObject(object); + weaponTestPos = &maze[weapon.y * MAZE_WIDTH + weapon.x]; + if (weaponTestPos->chr == 0xB2) + { + ObjectIndex nextObject = weapon.next; + *weaponTestPos = MazeTile(0x9, ' '); + FreeObjectInList(&objectHead_weapons, object); + numWeapons--; + object = nextObject; + continue; + } + *weaponTestPos = MazeTile(0x9, ' '); + switch (weapon.moveDirection) + { + case MoveDirection_UpRight: + if (MoveWeaponAndTestHit(OrthogonalDirection_Right) || (IsDiagonalDoubledPhase(weapon.y) && MoveWeaponAndTestHit(OrthogonalDirection_Right))) + break; + goto case_MoveDirection_Up; + case MoveDirection_DownRight: + if (MoveWeaponAndTestHit(OrthogonalDirection_Down) || (IsDiagonalDoubledPhase(weapon.y) && MoveWeaponAndTestHit(OrthogonalDirection_Right))) + break; + // fall through + case MoveDirection_Right: + if (MoveWeaponAndTestHit(OrthogonalDirection_Right)) + break; + goto main_139A; + case MoveDirection_Down: + if (MoveWeaponAndTestHit(OrthogonalDirection_Down)) + break; + goto main_139A; + case MoveDirection_DownLeft: + if (MoveWeaponAndTestHit(OrthogonalDirection_Down) || (IsDiagonalDoubledPhase(weapon.y) && MoveWeaponAndTestHit(OrthogonalDirection_Left))) + break; + // fall through + case MoveDirection_Left: + if (MoveWeaponAndTestHit(OrthogonalDirection_Left)) + break; + goto main_139A; + case MoveDirection_UpLeft: + if (MoveWeaponAndTestHit(OrthogonalDirection_Left) || (IsDiagonalDoubledPhase(weapon.y) && MoveWeaponAndTestHit(OrthogonalDirection_Left))) + break; + // fall through + case MoveDirection_Up: + case_MoveDirection_Up: + if (MoveWeaponAndTestHit(OrthogonalDirection_Up)) + break; + main_139A: + if (weapon.weaponType!=WeaponType_Bullet) + currentSprite = FakePointerToPointer(weapon.sprite); + else + { + if (++weapon.animFrame > 3) + weapon.animFrame = 0; + currentSprite = data_11D4[weapon.animFrame]; + } + *weaponTestPos = (MazeTile&)currentSprite[1]; + prevObject = object; + object = weapon.next; + continue; + default: + UNREACHABLE; + } + BYTE find_this = weaponTestPos->chr; + if (!memchr(mazeWallCharacters, find_this, _countof(mazeWallCharacters))) + { + if (weapon.weaponType==WeaponType_Bullet) + { + if (memchr(enemyCharacters, find_this, _countof(enemyCharacters))) + score += 1; + else + if (IsSnipePortal(*weaponTestPos)) + score += 50; + } + else + if (snipePortalsResistSnipeSpears && IsSnipePortal(*weaponTestPos)) + goto main_150E; + *weaponTestPos = MazeTile(0xF, 0xB2); + } + else + if (enableBouncingBullets && weapon.weaponType==WeaponType_Bullet && weaponBouncesRemaining[object] && (weapon.moveDirection & MoveDirectionMask_Diagonal)) + { + weaponBouncesRemaining[object]--; + weapon.moveDirection = (MoveDirection)bulletBounceTable[weaponCollisionDirection][weapon.moveDirection]; + SetSoundEffectState(1, SoundEffect_Bullet); + MoveWeaponAndTestHit((weaponCollisionDirection + 2) & OrthogonalDirectionMask_All); + goto main_139A; + } + main_150E: + numWeapons--; + if (!prevObject) + objectHead_weapons = weapon.next; + else + objects[prevObject].next = weapon.next; + ObjectIndex nextObject = weapon.next; + FreeObject(object); + object = nextObject; + } +} + +struct OrthoDistanceInfo +{ + union + { + struct {BYTE x, y;}; + Coord xy; + }; + MoveDirection direction; +}; + +OrthoDistanceInfo GetOrthoDistanceAndDirection(Object &object) +// Calculates the orthogonal distance between object and the player +{ + OrthoDistanceInfo result; + BYTE bx = 1; + int ax = object.x - viewportFocusX; + if (ax <= 0) + { + bx = 0; + ax = -ax; + } + if (ax >= MAZE_WIDTH/2) + { + bx ^= 1; + ax = MAZE_WIDTH - ax; + } + result.x = ax; + ax = object.y - viewportFocusY; + if (ax < 0) + { + bx += 2; + ax = -ax; + } + if (ax >= MAZE_HEIGHT/2) + { + bx ^= 2; + ax = MAZE_HEIGHT - ax; + } + result.y = ax; + orthoDistance = result.x + result.y; + if (result.x == 0) + { + result.direction = (MoveDirection)(bx * 2); + return result; + } + if (result.y == 0) + { + result.direction = (MoveDirection)(bx * 4 + 2); + return result; + } + static const MoveDirection diagonalDirectionTable[] = {MoveDirection_UpRight, MoveDirection_UpLeft, MoveDirection_DownRight, MoveDirection_DownLeft}; + result.direction = diagonalDirectionTable[bx]; + return result; +} + +struct MoveObject_retval {bool hitObstruction; BYTE chrHit; Coord cx; MazeTile *bx_si;}; +MoveObject_retval MoveObject(MovingObject &object) +{ + Coord ax, cx, dx; + ax = cx = object.xy; + dx = 0; + int tmp; + switch (object.moveDirection) + { + case MoveDirection_UpRight: + tmp = cx.x + (dx.x = IsDiagonalDoubledPhase(cx.y) + 1); + if (tmp >= MAZE_WIDTH) + tmp -= MAZE_WIDTH; + cx.x = tmp; + // fall through + case MoveDirection_Up: + dx.y++; + tmp = cx.y - 1; + if (tmp < 0) + tmp = MAZE_HEIGHT - 1; + ax.y = cx.y = tmp; + break; + case MoveDirection_Right: + dx.x++; + tmp = cx.x + 1; + if (tmp >= MAZE_WIDTH) + tmp = 0; + cx.x = tmp; + break; + case MoveDirection_DownRight: + dx.y++; + tmp = cx.y + 1; + if (tmp >= MAZE_HEIGHT) + tmp = 0; + cx.y = tmp; + tmp = cx.x + (dx.x = IsDiagonalDoubledPhase(cx.y) + 1); + if (tmp >= MAZE_WIDTH) + tmp -= MAZE_WIDTH; + cx.x = tmp; + break; + case MoveDirection_Down: + dx.y++; + tmp = cx.y + 1; + if (tmp >= MAZE_HEIGHT) + tmp = 0; + cx.y = tmp; + break; + case MoveDirection_DownLeft: + dx.y++; + tmp = cx.y + 1; + if (tmp >= MAZE_HEIGHT) + tmp = 0; + cx.y = tmp; + tmp = cx.x - (dx.x = IsDiagonalDoubledPhase(cx.y) + 1); + if (tmp < 0) + tmp += MAZE_WIDTH; + ax.x = cx.x = tmp; + break; + case MoveDirection_Left: + dx.x++; + tmp = cx.x - 1; + if (tmp < 0) + tmp = MAZE_WIDTH - 1; + ax.x = cx.x = tmp; + break; + case MoveDirection_UpLeft: + tmp = cx.x - (dx.x = IsDiagonalDoubledPhase(cx.y) + 1); + if (tmp < 0) + tmp += MAZE_WIDTH; + cx.x = tmp; + dx.y++; + tmp = cx.y - 1; + if (tmp < 0) + tmp = MAZE_HEIGHT - 1; + cx.y = tmp; + ax = cx; + break; + default: + UNREACHABLE; + } + const WORD *ptr = FakePointerToPointer(object.sprite); + dx.x += ((BYTE*)ptr)[1]; + dx.y += ((BYTE*)ptr)[0]; + for (MazeTile *mazeRow = &maze[ax.y * MAZE_WIDTH];;) + { + BYTE countX = dx.x; + for (size_t x = ax.x;;) + { + if (mazeRow[x].chr != ' ') + { + MoveObject_retval retval; + retval.hitObstruction = true; + retval.chrHit = mazeRow[x].chr; + retval.cx = cx; + retval.bx_si = &mazeRow[x]; + return retval; + } + if (--countX == 0) + break; + if (++x >= MAZE_WIDTH) + x = 0; + } + if (--dx.y == 0) + break; + mazeRow += MAZE_WIDTH; + if (mazeRow >= &maze[_countof(maze)]) + mazeRow -= _countof(maze); + } + MoveObject_retval retval; + retval.hitObstruction = false; + retval.chrHit = 0; + retval.cx = cx; + return retval; +} + +void FireWeapon(BYTE weaponType) +{ + MovingObject &shooter = *(MovingObject*)currentObject; + MoveDirection fireDirection = shooter.moveDirection; + BYTE x = shooter.x; + BYTE y = shooter.y; + switch (fireDirection) + { + case MoveDirection_UpRight: + x += ((BYTE*)currentSprite)[1]; + goto case_MoveDirection_Up; + case MoveDirection_Right: + x += ((BYTE*)currentSprite)[1]; + break; + case MoveDirection_DownRight: + x += ((BYTE*)currentSprite)[1]; + goto main_168F; + case MoveDirection_Down: + y += ((BYTE*)currentSprite)[0]; + x += ((BYTE*)currentSprite)[1] - 1; + break; + case MoveDirection_DownLeft: + x--; + main_168F: + y += ((BYTE*)currentSprite)[0]; + break; + case MoveDirection_Left: + x--; + break; + case MoveDirection_UpLeft: + x--; + // fall through + case MoveDirection_Up: + case_MoveDirection_Up: + y--; + break; + default: + UNREACHABLE; + } + if (x >= MAZE_WIDTH) + { + if (x > 240) + x += MAZE_WIDTH; + else + x -= MAZE_WIDTH; + } + if (y >= MAZE_HEIGHT) + { + if (y > 240) + y += MAZE_HEIGHT; + else + y -= MAZE_HEIGHT; + } + const WORD *currentSprite_backup = currentSprite; + currentSprite = data_1150; + if (IsObjectLocationOccupied(y, x)) + { + MazeTile &weaponHitPos = maze[y * MAZE_WIDTH + x]; + if (!memchr(mazeWallCharacters, weaponHitPos.chr, _countof(mazeWallCharacters))) + { + if (weaponType==WeaponType_Bullet) + { + if (memchr(enemyCharacters, weaponHitPos.chr, _countof(enemyCharacters))) + score += 1; + else + if (IsSnipePortal(weaponHitPos)) + score += 50; + weaponHitPos = MazeTile(0xF, 0xB2); + } + else + if (!(snipePortalsResistSnipeSpears && IsSnipePortal(weaponHitPos))) + weaponHitPos = MazeTile(0xF, 0xB2); + } + } + else + { + ObjectIndex newWeapon = CreateNewObject(); + if (newWeapon && numWeapons <= 50) + { + numWeapons++; + Object *currentObject_backup = currentObject; + Weapon &weapon = SetCurrentObject(newWeapon); + if (!objectHead_weapons) + objectHead_weapons = newWeapon; + else + { + ObjectIndex objectTail_weapons = objectHead_weapons; + while (ObjectIndex nextObject = objects[objectTail_weapons].next) + objectTail_weapons = nextObject; + objects[objectTail_weapons].next = newWeapon; + } + weapon.next = 0; + if (weaponType==WeaponType_Bullet) + weapon.sprite = FAKE_POINTER(1150); + else + weapon.sprite = PointerToFakePointer(data_11B4[fireDirection]); + weapon.x = x; + weapon.y = y; + weapon.moveDirection = fireDirection; + weapon.animFrame = 0; + weapon.weaponType = weaponType; + weaponBouncesRemaining[newWeapon] = (BYTE)GetRandomMasked(7) + 1; + currentSprite = FakePointerToPointer(weapon.sprite); + PlotObjectToMaze(); + currentObject = currentObject_backup; + } + } + currentSprite = currentSprite_backup; +} + +bool FireSnipeSpear() +{ + BYTE shiftCount = orthoDistance >> snipeShootingAccuracy; + if (shiftCount > 10) + return false; + if (GetRandomMasked(0xFFFF >> (15 - shiftCount))) + return false; + FireWeapon(WeaponType_Spear); + SetSoundEffectState(0, SoundEffect_Spear); + return true; +} + +void UpdateLargeSnipes() +{ + for (ObjectIndex object = objectHead_largeSnipes; object;) + { + LargeSnipe &snipe = ((LargeSnipe*)objects)[object]; + MazeTile *snipeMazeRow = &maze[snipe.y * MAZE_WIDTH]; + MazeTile * leftPart = &snipeMazeRow[snipe.x]; + MazeTile *rightPart = snipe.x >= MAZE_WIDTH-1 ? &snipeMazeRow[0] : leftPart + 1; + MazeTile *smallSnipePart; + if (leftPart->chr == 0xB2) + { + *leftPart = MazeTile(0x9, ' '); + smallSnipePart = rightPart; + } + else + if (rightPart->chr == 0xB2) + { + *rightPart = MazeTile(0x9, ' '); + smallSnipePart = leftPart; + } + else + if (--snipe.moveFrame == 0) + { + * leftPart = MazeTile(0x9, ' '); + *rightPart = MazeTile(0x9, ' '); + if (GetRandomMasked(3) != 0) // 3/4 chance of moving in the same direction as before + { + MoveObject_retval result = MoveObject(snipe); + if (!result.hitObstruction) + { + snipe.xy = result.cx; + if (!(snipe.moveDirection & MoveDirectionMask_Diagonal)) + goto main_2021; + snipe.moveFrame = 8; + goto main_2025; + } + } + if (GetRandomMasked(3) == 0) // 1/4 * 1/4 chance of moving in a random direction + snipe.moveDirection = (MoveDirection)GetRandomMasked(MoveDirectionMask_All); + else // 1/4 * 3/4 chance of moving toward the player + snipe.moveDirection = GetOrthoDistanceAndDirection(snipe).direction; + for (Uint count=8; count; count--) + { + MoveObject_retval result = MoveObject(snipe); + if (!result.hitObstruction) + break; + if (snipe.movementFlags & EnemyMovementFlag_TurnDirection) + snipe.moveDirection = (snipe.moveDirection - 1) & MoveDirectionMask_All; + else + snipe.moveDirection = (snipe.moveDirection + 1) & MoveDirectionMask_All; + } + snipe.sprite = PointerToFakePointer(data_1130[snipe.moveDirection]); + main_2021: + snipe.moveFrame = 6; + main_2025: + const WORD *sprite = FakePointerToPointer(snipe.sprite); + maze[snipe.y * MAZE_WIDTH + snipe.x] = (MazeTile&)sprite[1 + 0]; + if (snipe.x < MAZE_WIDTH-1) + maze[snipe.y * MAZE_WIDTH + snipe.x+1] = (MazeTile&)sprite[1 + 1]; + else + maze[snipe.y * MAZE_WIDTH ] = (MazeTile&)sprite[1 + 1]; + OrthoDistanceInfo orthoDist = GetOrthoDistanceAndDirection(snipe); + MoveDirection al = orthoDist.direction; + MoveDirection ah = snipe.moveDirection; + if (al != ah) + goto main_209E; + if (orthoDist.x && orthoDist.y) + { + if (abs((int)orthoDist.x * MAZE_CELL_HEIGHT - (int)orthoDist.y * MAZE_CELL_WIDTH) >= MAZE_CELL_WIDTH) + goto next_snipe; + al = snipe.moveDirection; + } + for (;;) + { + { + MoveDirection moveDirection = snipe.moveDirection; + snipe.moveDirection = al; + currentObject = (Object*)&snipe; + currentSprite = FakePointerToPointer(snipe.sprite); + bool tmp = FireSnipeSpear(); + snipe.moveDirection = moveDirection; + if (!tmp) + break; + } + if (maze[snipe.y * MAZE_WIDTH + snipe.x].chr != 0x01) + maze[snipe.y * MAZE_WIDTH + snipe.x] = MazeTile(0x9, 0xFF); + else + { + if (snipe.x < MAZE_WIDTH-1) + maze[snipe.y * MAZE_WIDTH + snipe.x+1] = MazeTile(0x9, 0xFF); + else + maze[snipe.y * MAZE_WIDTH ] = MazeTile(0x9, 0xFF); + } + break; + main_209E: + if (orthoDist.y <= 2 && al != MoveDirection_Up && al != MoveDirection_Down) + { + if (al <= MoveDirection_Down) + { + if (inrange(ah, MoveDirection_UpRight, MoveDirection_DownRight)) + { + al = MoveDirection_Right; + continue; + } + } + else + if (ah >= MoveDirection_DownLeft) + { + al = MoveDirection_Left; + continue; + } + } + if (orthoDist.x > 2) + break; + al = (al + 1) & MoveDirectionMask_All; + if (al == MoveDirection_UpLeft || al == MoveDirection_DownRight) + break; + if (al < MoveDirection_Down) + { + ah = (ah + 1) & MoveDirectionMask_All; + if (ah > MoveDirection_Right) + break; + al = MoveDirection_Up; + continue; + } + ah = (ah + 2) & MoveDirectionMask_All; + if (ah <= MoveDirection_Down) + break; + al = MoveDirection_Down; + } + goto next_snipe; + } + else + { + next_snipe: + object = snipe.next; + continue; + } + if (enableSmallSnipes && smallSnipePart->chr == 0x01) + { + *smallSnipePart = MazeTile(0x5, 0x02); + SmallSnipe &smallSnipe = (SmallSnipe&)snipe; + smallSnipe.x = smallSnipePart - snipeMazeRow; + + // move this object out of the LargeSnipe linked-list and into the SmallSnipe linked-list + for (ObjectIndex *nextPtr = &objectHead_largeSnipes;; nextPtr = &objects[*nextPtr].next) + { + if (*nextPtr == object) + { + ObjectIndex nextObject = *nextPtr = smallSnipe.next; + smallSnipe.next = objectHead_smallSnipes; + objectHead_smallSnipes = object; + object = nextObject; + break; + } + } + smallSnipe.moveFrame = 2; + smallSnipe.sprite = FAKE_POINTER(10FE); + numSmallSnipes++; + } + else + { + ObjectIndex nextObject = snipe.next; + FreeObjectInList(&objectHead_largeSnipes, object); + object = nextObject; + } + numLargeSnipes--; + numLargeSnipesDestroyed++; + } +} + +void UpdateSmallSnipes() +{ + for (ObjectIndex object = objectHead_smallSnipes; object;) + { + SmallSnipe &smallSnipe = ((SmallSnipe*)objects)[object]; + MazeTile &smallSnipeInMaze = maze[smallSnipe.y * MAZE_WIDTH + smallSnipe.x]; + if (smallSnipeInMaze.chr != 0xB2) + { + if (--smallSnipe.moveFrame) + { + object = smallSnipe.next; + continue; + } + } + else + { + destroy_smallSnipe: + ObjectIndex nextObject = smallSnipe.next; + FreeObjectInList(&objectHead_smallSnipes, object); + object = nextObject; + numSmallSnipes--; + numSmallSnipesDestroyed++; + continue; + } + smallSnipeInMaze = MazeTile(0x9, ' '); + if (!(smallSnipe.movementFlags & EnemyMovementFlag_SmallSnipeMoveStraight)) + { + OrthoDistanceInfo orthoDist = GetOrthoDistanceAndDirection(smallSnipe); + if (orthoDistance <= 4) + { + smallSnipe.moveDirection = orthoDist.direction; + MoveObject_retval result = MoveObject(smallSnipe); + if (result.hitObstruction && (smallSnipe.moveDirection & MoveDirectionMask_Diagonal)) + if (IsPlayer(result.chrHit)) + { + if (GetRandomMasked(smallSnipeExplosionChance) == 0) + { + result.bx_si->chr = 0xB2; + goto destroy_smallSnipe; + } +#if !defined(FIX_BUGS) && !defined(FIX_SMALL_SNIPE_LOGIC_COMPILER_BUG) + // A PL/M-86 compiler bug manifesting in the original game caused the CX register to be overwritten by the call to GetRandomMasked(). + // When a small snipe has a chance to explode next to the player, but doesn't, this bug changes the way it will move. + orthoDist.xy = 947; +#endif + } + } + if (orthoDist.y == 1) + orthoDist.direction = orthoDist.direction >= MoveDirection_Down ? MoveDirection_Left : MoveDirection_Right; + else + if (orthoDist.y < 1) + { + smallSnipe.moveDirection = ++orthoDist.direction; + MoveObject_retval result = MoveObject(smallSnipe); + if (!result.hitObstruction) + { + smallSnipe.xy = result.cx; + goto plot_smallSnipe_and_continue; + } + orthoDist.direction -= 2; + } + else + if (orthoDist.x == 1) + orthoDist.direction = (orthoDist.direction + 1) & MoveDirectionMask_UpDown; + else + if (orthoDist.x < 1) + { + smallSnipe.moveDirection = orthoDist.direction += 2; + MoveObject_retval result = MoveObject(smallSnipe); + if (!result.hitObstruction) + { + smallSnipe.xy = result.cx; + goto plot_smallSnipe_and_continue; + } + orthoDist.direction = (orthoDist.direction - 4) & MoveDirectionMask_All; + } + smallSnipe.moveDirection = orthoDist.direction; + { + MoveObject_retval result = MoveObject(smallSnipe); + if (!result.hitObstruction) + { + smallSnipe.xy = result.cx; + goto plot_smallSnipe_and_continue; + } + } + if (orthoDistance >= 20) + smallSnipe.movementFlags |= EnemyMovementFlag_SmallSnipeMoveStraight; + smallSnipe.moveDirection = (MoveDirection)GetRandomMasked(MoveDirectionMask_All); + } + for (Uint count=8; count; count--) + { + MoveObject_retval result = MoveObject(smallSnipe); + if (!result.hitObstruction) + { + smallSnipe.xy = result.cx; + break; + } + smallSnipe.movementFlags &= ~EnemyMovementFlag_SmallSnipeMoveStraight; + if (smallSnipe.movementFlags & EnemyMovementFlag_TurnDirection) + smallSnipe.moveDirection = (smallSnipe.moveDirection - 1) & MoveDirectionMask_All; + else + smallSnipe.moveDirection = (smallSnipe.moveDirection + 1) & MoveDirectionMask_All; + } + plot_smallSnipe_and_continue: + maze[smallSnipe.y * MAZE_WIDTH + smallSnipe.x] = MazeTile(0x5, 0x02); + smallSnipe.moveFrame = 3; + object = smallSnipe.next; + } +} + +bool IsObjectTaggedToExplode() +{ + BYTE spriteHeight = ((BYTE*)currentSprite)[0]; + BYTE spriteWidth = ((BYTE*)currentSprite)[1]; + MazeTile *mazeTile = &maze[currentObject->y * MAZE_WIDTH]; + for (BYTE row = 0; row < spriteHeight; row++) + { + BYTE x = currentObject->x; + for (BYTE column = 0; column < spriteWidth; column++) + { + if (mazeTile[x].chr == 0xB2) + return true; + if (++x >= MAZE_WIDTH) + x = 0; + } + mazeTile += MAZE_WIDTH; + if (mazeTile >= &maze[_countof(maze)]) + mazeTile -= _countof(maze); + } + return false; +} + +void EraseObjectFromMaze() +{ + BYTE spriteHeight = ((BYTE*)currentSprite)[0]; + BYTE spriteWidth = ((BYTE*)currentSprite)[1]; + MazeTile *mazeTile = &maze[currentObject->y * MAZE_WIDTH]; + for (BYTE row = 0; row < spriteHeight; row++) + { + BYTE x = currentObject->x; + for (BYTE column = 0; column < spriteWidth; column++) + { + mazeTile[x] = MazeTile(0x9, ' '); + if (++x >= MAZE_WIDTH) + x = 0; + } + mazeTile += MAZE_WIDTH; + if (mazeTile >= &maze[_countof(maze)]) + mazeTile -= _countof(maze); + } +} + +//template TYPE &IncWrap(TYPE &n, + +void UpdateSnipePortals() +{ + for (ObjectIndex object = objectHead_snipePortals; object;) + { + SnipePortal &snipePortal = SetCurrentObject(object); + if (++snipePortal.animFrame >= _countof(data_10A2)) + snipePortal.animFrame = 0; + currentSprite = data_10A2[snipePortal.animFrame]; + snipePortal.sprite = PointerToFakePointer(currentSprite); + if (IsObjectTaggedToExplode()) + { + ObjectIndex nextObject = snipePortal.next; + FreeObjectInList(&objectHead_snipePortals, object); + object = nextObject; + numSnipePortals--; + continue; + } + EraseObjectFromMaze(); + PlotObjectToMaze(); + if (--snipePortal.spawnFrame == 0) + { + GetOrthoDistanceAndDirection(*currentObject); + if (frame >= 0xF00) + snipePortal.spawnFrame = 5; + else + snipePortal.spawnFrame = 5 + (orthoDistance >> (frame/0x100 + 1)); + if (GetRandomMasked(0xF >> (numSnipePortalsAtStart - numSnipePortals)) == 0) + { + currentSprite = data_1112; + BYTE x = snipePortal.x + 2; +#ifdef EMULATE_LATENT_BUGS + if (x > MAZE_WIDTH - 1) + x -= MAZE_WIDTH - 1; +#else + if (x >= MAZE_WIDTH) + x -= MAZE_WIDTH; +#endif + BYTE y = snipePortal.y; + if (!IsObjectLocationOccupied(y, x)) + { + if (numLargeSnipes + numSmallSnipes < maxSnipes) + { + ObjectIndex spawnedSnipeIndex = CreateNewObject(); + if (spawnedSnipeIndex) + { + numLargeSnipes++; + object = snipePortal.next; + LargeSnipe &spawnedSnipe = SetCurrentObject(spawnedSnipeIndex); + spawnedSnipe.next = objectHead_largeSnipes; + objectHead_largeSnipes = spawnedSnipeIndex; + spawnedSnipe.x = x; + spawnedSnipe.y = y; + spawnedSnipe.moveDirection = MoveDirection_Right; + spawnedSnipe.sprite = FAKE_POINTER(1112); + PlotObjectToMaze(); + spawnedSnipe.movementFlags = (BYTE)GetRandomMasked(1); // randomly set or clear EnemyMovementFlag_TurnDirection + spawnedSnipe.moveFrame = 4; + continue; + } + } + } + } + } + object = snipePortal.next; + } +} + +bool MovePlayer_helper(bool &hitObstruction, OrthogonalDirection arg) +{ + player.moveDirection = OrthoDirectionToMoveDirection(arg); + MoveObject_retval result = MoveObject(*(MovingObject*)currentObject); + if (result.hitObstruction) + return hitObstruction = true; + viewportFocusX = player.x = result.cx.x; + viewportFocusY = player.y = result.cx.y; + return false; +} + +bool MovePlayer() +{ + MoveDirection moveDirection = player.moveDirection; + bool hitObstruction = false; + switch (moveDirection) + { + case MoveDirection_UpRight: + MovePlayer_helper(hitObstruction, OrthogonalDirection_Right); + if (!IsDiagonalDoubledPhase(currentObject->y)) + goto case_MoveDirection_Up; + if (MovePlayer_helper(hitObstruction, OrthogonalDirection_Right)) + goto case_MoveDirection_Up; + if (!MovePlayer_helper(hitObstruction, OrthogonalDirection_Up)) + break; + goto case_MoveDirection_Left; + case MoveDirection_DownRight: + if (MovePlayer_helper(hitObstruction, OrthogonalDirection_Down)) + goto case_MoveDirection_Right; + if (IsDiagonalDoubledPhase(currentObject->y)) + MovePlayer_helper(hitObstruction, OrthogonalDirection_Right); + goto case_MoveDirection_Right; + case MoveDirection_Down: + MovePlayer_helper(hitObstruction, OrthogonalDirection_Down); + break; + case MoveDirection_DownLeft: + if (MovePlayer_helper(hitObstruction, OrthogonalDirection_Down)) + goto case_MoveDirection_Left; + if (!IsDiagonalDoubledPhase(currentObject->y)) + goto case_MoveDirection_Left; + MovePlayer_helper(hitObstruction, OrthogonalDirection_Left); + // fall through + case MoveDirection_Left: + case_MoveDirection_Left: + MovePlayer_helper(hitObstruction, OrthogonalDirection_Left); + break; + case MoveDirection_UpLeft: + MovePlayer_helper(hitObstruction, OrthogonalDirection_Left); + if (!IsDiagonalDoubledPhase(currentObject->y)) + goto case_MoveDirection_Up; + if (MovePlayer_helper(hitObstruction, OrthogonalDirection_Left)) + goto case_MoveDirection_Up; + if (!MovePlayer_helper(hitObstruction, OrthogonalDirection_Up)) + break; + // fall through + case MoveDirection_Right: + case_MoveDirection_Right: + MovePlayer_helper(hitObstruction, OrthogonalDirection_Right); + break; + case MoveDirection_Up: + case_MoveDirection_Up: + MovePlayer_helper(hitObstruction, OrthogonalDirection_Up); + break; + default: + UNREACHABLE; + } + if (!hitObstruction) + PlotObjectToMaze(); + player.moveDirection = moveDirection; + return !hitObstruction; +} + +bool UpdatePlayer(bool playbackMode, BYTE &replayIO) // returns true if the match has been lost +{ + if (!playbackMode) + replayIO = 0; + currentObject = (Object*)&player; + if (++playerEyeAnimFrame > 7) + { + playerEyeAnimFrame = 0; + playerAnimEyesNotWide ^= true; + } + if (playerAnimEyesNotWide) + currentSprite = data_10E2; + else + currentSprite = data_10EC; + if (isPlayerDying) + { + keyboard_state = PollKeyboard(); + if (numPlayerDeaths >= numLives) + { + DrawViewport(); + EraseBottomTwoLines(); + CloseDirectConsole(WINDOW_HEIGHT-2); + WriteTextToConsole("The SNIPES have triumphed!!!\r\n"); + return true; + } + if (!isPlayerExploding) + { + keyboard_state = PollKeyboard(); + isPlayerDying = false; + PlaceObjectInRandomUnoccupiedMazeCell(); + viewportFocusX = player.x; + viewportFocusY = player.y; + PlotObjectToMaze(); + return false; + } + return false; + } + else + { + if (--player.inputFrame) + { + if (IsObjectTaggedToExplode()) + goto explode_player; + return false; + } + keyboard_state = PollKeyboard(); + player.inputFrame = 2; + if (IsObjectTaggedToExplode()) + goto explode_player; + } + EraseObjectFromMaze(); + static const BYTE arrowKeyMaskToDirectionTable[] = + { + 0, + MoveDirection_Right, + MoveDirection_Left, + 0, + MoveDirection_Down, + MoveDirection_DownRight, + MoveDirection_DownLeft, + 0, + MoveDirection_Up, + MoveDirection_UpRight, + MoveDirection_UpLeft, + 0, + 0, + 0, + 0, + 0, + }; + BYTE moveDirection, keyboardMove; + if (playbackMode) + { + spacebar_state = replayIO >> 7; + moveDirection = (replayIO & 0x7F) % 9; + if (moveDirection) + { + moveDirection--; + goto playback_move; + } + } + else + if ((keyboardMove = keyboard_state & (KEYSTATE_MOVE_RIGHT | KEYSTATE_MOVE_LEFT | KEYSTATE_MOVE_DOWN | KEYSTATE_MOVE_UP))) + { + moveDirection = arrowKeyMaskToDirectionTable[keyboardMove]; +playback_move: + player.moveDirection = (MoveDirection)moveDirection; + if (!playbackMode) + replayIO = moveDirection + 1; + if (MovePlayer()) + { + if (!spacebar_state) + goto main_1B8F; + if (!playbackMode) + replayIO += 0x80; + if (player.inputFrame == 1) + { + EraseObjectFromMaze(); + if (!MovePlayer()) + { + if (enableElectricWalls) + goto explode_player; + PlotObjectToMaze(); + } + } + player.inputFrame = 1; + goto main_1B8F; + } + if (enableElectricWalls) + goto explode_player; + } + PlotObjectToMaze(); +main_1B8F: + BYTE fireDirection; + if (playbackMode) + { + fireDirection = (replayIO & 0x7F) / 9 % 9; + if (fireDirection) + { + fireDirection--; + goto playback_fire; + } + } + else + if (!spacebar_state && (keyboard_state & (KEYSTATE_FIRE_RIGHT | KEYSTATE_FIRE_LEFT | KEYSTATE_FIRE_DOWN | KEYSTATE_FIRE_UP))) + { + fireDirection = arrowKeyMaskToDirectionTable[keyboard_state >> 4]; +playback_fire: + if (!playbackMode) + replayIO += (fireDirection + 1) * 9; + if (--player.firingFrame) + return false; + MoveDirection moveDirection_backup = player.moveDirection; + player.moveDirection = (MoveDirection)fireDirection; + FireWeapon(WeaponType_Bullet); + SetSoundEffectState(0, SoundEffect_Bullet); + player.moveDirection = moveDirection_backup; + player.firingFrame = player.inputFrame == 1 ? playerFiringPeriod<<1 : playerFiringPeriod; + return false; + } + player.firingFrame = 1; + return false; +explode_player: + FreeObjectInList(&player.next, OBJECT_PLAYER); // explode the player + isPlayerDying = true; + numPlayerDeaths++; + return false; +} + +void UpdateExplosions() +{ + for (ObjectIndex object = objectHead_explosions; object;) + { + Explosion &explosion = SetCurrentObject(object); + currentSprite = FakePointerToPointer(explosion.sprite); + EraseObjectFromMaze(); + BYTE animFrame = (explosion.animFrame + 1) % 6; + ObjectIndex nextObject = explosion.next; + explosion.animFrame++; + if (explosion.animFrame > (object == OBJECT_PLAYEREXPLOSION ? 11 : 5)) + { + FreeObjectInList_worker(&objectHead_explosions, object); + if (object != OBJECT_PLAYEREXPLOSION) + FreeObject(object); + else + isPlayerExploding = false; + } + else + { + if (explosion.spriteSize == EXPLOSION_SIZE(2,2)) + { + explosion.sprite = PointerToFakePointer(data_12FE[animFrame]); + currentSprite = data_12FE[animFrame]; + if (object == OBJECT_PLAYEREXPLOSION && explosion.animFrame >= 6) + SetSoundEffectState(5 - (explosion.animFrame - 6), SoundEffect_ExplodePlayer); + else + SetSoundEffectState(animFrame, SoundEffect_ExplodePlayer); + } + else + if (explosion.spriteSize == EXPLOSION_SIZE(2,1)) + { + explosion.sprite = PointerToFakePointer(data_1352[animFrame]); + currentSprite = data_1352[animFrame]; + SetSoundEffectState(animFrame, SoundEffect_ExplodeLargeSnipe); + } + else + if (explosion.spriteSize == EXPLOSION_SIZE(1,1)) + { + explosion.sprite = PointerToFakePointer(data_1392[animFrame]); + currentSprite = data_1392[animFrame]; + SetSoundEffectState(animFrame, SoundEffect_ExplodeSmallSnipe); + } + PlotObjectToMaze(); + } + object = nextObject; + } +} + +static const WORD sound_ExplodeSmallSnipe[] = { 100, 100, 1400, 1800, 1600, 1200}; +static const WORD sound_ExplodeLargeSnipe[] = {2200, 6600, 1800, 4400, 8400, 1100}; +static const WORD sound_ExplodePlayer [] = {2000, 8000, 6500, 4000, 2500, 1000}; + +void UpdateSound() +{ + if (!sound_enabled) + { + ClearSound(); + return; + } + if (currentSoundEffect == SoundEffect_None) + { +#ifdef STOP_WAVE_OUT_DURING_SILENCE + PlayTone(-1); +#else + PlayTone(0); +#endif + return; + } + switch (currentSoundEffect) + { + case SoundEffect_Bullet: + if (!currentSoundEffectFrame) + PlayTone(1900); + else + PlayTone(1400); + break; + case SoundEffect_Spear: + PlayTone(1600); + break; + case SoundEffect_ExplodeSmallSnipe: + PlayTone(sound_ExplodeSmallSnipe[currentSoundEffectFrame]); + break; + case SoundEffect_ExplodeLargeSnipe: + PlayTone(sound_ExplodeLargeSnipe[currentSoundEffectFrame]); + break; + case SoundEffect_ExplodePlayer: + PlayTone(sound_ExplodePlayer[currentSoundEffectFrame]); + break; + default: + UNREACHABLE; + } + currentSoundEffect = SoundEffect_None; +} + +void DrawViewport() +{ + WORD outputRow = VIEWPORT_ROW; + SHORT data_298 = viewportFocusY - VIEWPORT_HEIGHT / 2; + if (data_298 < 0) + data_298 += MAZE_HEIGHT; + SHORT data_296 = viewportFocusX - WINDOW_WIDTH / 2; + if (data_296 < 0) + data_296 += MAZE_WIDTH; + SHORT wrappingColumn = 0; + if (data_296 + WINDOW_WIDTH >= MAZE_WIDTH) + wrappingColumn = MAZE_WIDTH - data_296; + else + wrappingColumn = WINDOW_WIDTH; + for (Uint row = 0; row < VIEWPORT_HEIGHT; row++) + { + WORD data_29C = data_298 * MAZE_WIDTH; + WriteTextMem(wrappingColumn, outputRow, 0, &maze[data_296 + data_29C]); + if (wrappingColumn != WINDOW_WIDTH) + WriteTextMem(WINDOW_WIDTH - wrappingColumn, outputRow, wrappingColumn, &maze[data_29C]); + outputRow++; + if (++data_298 == MAZE_HEIGHT) + data_298 = 0; + } +} + +void WaitForNextTick(WORD &tick_count) +{ + for (;;) + { + SleepTimeslice(); + WORD tick_count2 = GetTickCountWord(); + if (tick_count2 != tick_count) + { + tick_count = tick_count2; + break; + } + } +} + +#if (defined(_WIN32) || defined(_WIN64)) && !defined(_CONSOLE) +extern "C" int __cdecl SDL_main(int argc, char* argv[]) +#else +int __cdecl main(int argc, char* argv[]) +#endif +{ + if (argc > 2) + { + fprintf(stderr, "Usage: %s [filename of replay to play back]\n", argv[0]); + return -1; + } + bool playbackMode; + const char *replayPlaybackFilename = NULL; + if ((playbackMode = (argc == 2))) + replayPlaybackFilename = argv[1]; + + if (int result = OpenConsole()) + return result; + + // Create SDL window on main thread for macOS + if (int result = CreateSDLWindow()) + { + CloseConsole(); + return result; + } + + if (int result = OpenTimer()) + { + CleanupSDL(); + CloseConsole(); + return result; + } + if (int result = OpenSound()) + { + CleanupSDL(); + CloseConsole(); + CloseTimer(); + return result; + } + + WORD tick_count; + +#ifdef CHEAT + rerecordingMode = !playbackMode; + bool replayFileWriting = false; +#endif + + ClearConsole(); + if (!playbackMode) + { +#define _ " " +#define S "\x01" +#define i "\x18" +#define TITLE_SCREEN \ + " ported by David Ellsworth\r\n"\ + "\r\n"\ + _ _ _ _ i i i _ _ i _ _ _ i _ i i i _ i i i i _ _ i i i i i _ _ i i i "\r\n"\ + _ _ _ i S S S i _ S _ _ _ S _ S S S _ S S S S i _ S S S S S _ i S S S i "\r\n"\ + _ _ _ S _ _ _ S _ i i _ _ i _ _ i _ _ i _ _ _ S _ i _ _ _ _ _ S _ _ _ S "\r\n"\ + _ _ _ i _ _ _ _ _ S S _ _ S _ _ S _ _ S _ _ _ i _ S _ _ _ _ _ i "\r\n"\ + _ _ _ S i i i _ _ i _ i _ i _ _ i _ _ i i i i S _ i i i i _ _ S i i i "\r\n"\ + _ _ _ _ S S S i _ S _ S _ S _ _ S _ _ S S S S _ _ S S S S _ _ _ S S S i "\r\n"\ + _ _ _ _ _ _ _ S _ i _ _ i i _ _ i _ _ i _ _ _ _ _ i _ _ _ _ _ _ _ _ _ S "\r\n"\ + _ _ _ i _ _ _ i _ S _ _ S S _ _ S _ _ S _ _ _ _ _ S _ _ _ _ _ i _ _ _ i "\r\n"\ + _ _ _ S i i i S _ i _ _ _ i _ i i i _ i _ _ _ _ _ i i i i i _ S i i i S "\r\n"\ + _ _ _ _ S S S _ _ S _ _ _ S _ S S S _ S _ _ _ _ _ S S S S S _ _ S S S "\r\n"\ + "\r\n\r\n"\ + "(c)Copyright SuperSet Software Corp 1982\r\n"\ + "All Rights Reserved\r\n"\ + "\r\n\r\n" + WriteTextToConsole(TITLE_SCREEN "Enter skill level (A-Z)(1-9): "); +#undef _ +#undef S +#undef i + // Render the title screen + RenderFrame(); + ReadSkillLevel(); + if (got_ctrl_break) + goto do_not_play_again; + } + + tick_count = GetTickCountWord(); + random_seed_lo = (BYTE)tick_count; + if (!random_seed_lo) + random_seed_lo = 444; + random_seed_hi = tick_count >> 8; + if (!random_seed_hi) + random_seed_hi = 555; + + for (;;) + { + FILE *replayFile = NULL; + if (playbackMode) + { + replayFile = fopen(replayPlaybackFilename, "rb"); + if (!replayFile) + { + fprintf(stderr, "Error opening replay file \"%s\" for playback\n", replayPlaybackFilename); + goto do_not_play_again; + } + + #define fread_all(ptr, size, count, stream) \ + do \ + { \ + size_t _recordsRead = fread(ptr, size, count, stream); \ + if (_recordsRead != count) \ + { \ + fprintf(stderr, "Error reading from replay file \"%s\" for playback\n", replayPlaybackFilename); \ + goto do_not_play_again; \ + } \ + } while (0) + + fread_all(&random_seed_lo, sizeof(random_seed_lo), 1, replayFile); + fread_all(&random_seed_hi, sizeof(random_seed_hi), 1, replayFile); + fread_all(&skillLevelLetter, 1, 1, replayFile); + fread_all(&skillLevelNumber, 1, 1, replayFile); + + #undef fread_all + } + else + { + time_t rectime = time(NULL); + struct tm *rectime_gmt; + rectime_gmt = gmtime(&rectime); + + char replayFilename[1024]; + sprintf(replayFilename, + "%04d-%02d-%02d %02d.%02d.%02d.SnipesGame", + 1900+rectime_gmt->tm_year, rectime_gmt->tm_mon+1, rectime_gmt->tm_mday, + rectime_gmt->tm_hour, rectime_gmt->tm_min, rectime_gmt->tm_sec); + +#ifdef CHEAT + replayFile = fopen(replayFilename, "w+b"); +#else + replayFile = fopen(replayFilename, "wb"); +#endif + if (replayFile) + { + setvbuf(replayFile, NULL, _IOFBF, 64); + fwrite(&random_seed_lo, sizeof(random_seed_lo), 1, replayFile); + fwrite(&random_seed_hi, sizeof(random_seed_hi), 1, replayFile); + fwrite(&skillLevelLetter, 1, 1, replayFile); + fwrite(&skillLevelNumber, 1, 1, replayFile); + } + } + + enableElectricWalls = skillLevelLetter >= 'M'-'A'; + snipePortalsResistSnipeSpears = skillLevelLetter >= 'W'-'A'; + enableBouncingBullets = bouncingBulletTable [skillLevelLetter]; + snipeShootingAccuracy = snipeShootingAccuracyTable [skillLevelLetter]; + enableSmallSnipes = enableSmallSnipesTable [skillLevelLetter]; + smallSnipeExplosionChance = smallSnipeExplosionChanceTable[skillLevelLetter]; + maxSnipes = maxSnipesTable [skillLevelNumber-1]; + numSnipePortalsAtStart = numSnipePortalsTable [skillLevelNumber-1]; + numLives = numLivesTable [skillLevelNumber-1]; + playerFiringPeriod = 2; + + OpenDirectConsole(); + if (int result = OpenKeyboard()) + { + CloseConsole(); + CloseTimer(); + CloseSound(); + return result; + } + +#ifdef CHEAT + WORD init_random_seed_lo = random_seed_lo; + WORD init_random_seed_hi = random_seed_hi; + + restart: +#endif + frame = 0; + InitializeHUD(); + CreateMaze(); + CreateSnipePortalsAndPlayer(); + SetSoundEffectState(0, SoundEffect_None); + + if (playbackMode && _PLAYBACK_FOR_SCREEN_RECORDING) + { + UpdateHUD(false); + WaitForKeyPress(); + WaitForNextTick(tick_count = GetTickCountWord()); + } + + for (;;) + { +#ifdef CHEAT + if (skip_to_frame && frame == skip_to_frame-1) + { + skip_to_frame = 0; + playbackMode = !rerecordingMode; + auto fileSize = ftell(replayFile); + if (replayFileWriting) + changesize(_fileno(replayFile), fileSize); + BYTE dummy; + size_t blah = fread(&dummy, 1, 1, replayFile); + fseek(replayFile, fileSize, SEEK_SET); + } + if (UpdateHUD()) + break; + if (!skip_to_frame) + { + DrawViewport(); + if (single_step > 0) + single_step--; + while (single_step == 0 && !step_backwards && !(increment_initial_seed && replayFileWriting)) + { + PollKeyboard(); + if (forfeit_match) + goto match_ended; + if (frame == 0) + step_backwards = 0; + if (rerecordingMode && !replayFileWriting && replayPlaybackFilename) + { + auto fileSize = ftell(replayFile); + fclose(replayFile); + replayFile = fopen(replayPlaybackFilename, "r+b"); + if (replayFile) + { + replayFileWriting = true; + playbackMode = false; + } + else + { + replayFile = fopen(replayPlaybackFilename, "rb"); + if (!replayFile) + { + fprintf(stderr, "Error opening replay file \"%s\" for playback\n", replayPlaybackFilename); + goto do_not_play_again; + } + rerecordingMode = false; + increment_initial_seed = 0; + } + fseek(replayFile, fileSize, SEEK_SET); + changesize(_fileno(replayFile), fileSize); + } + } + if (step_backwards) + { + skip_to_frame = frame - step_backwards; + step_backwards = 0; + single_step = 0; + if (skip_to_frame) + playbackMode = true; + fflush(replayFile); + fseek(replayFile, 6, SEEK_SET); + + random_seed_lo = init_random_seed_lo; + random_seed_hi = init_random_seed_hi; + currentSoundEffect = SoundEffect_None; + + goto restart; + } + if (frame==1 && step_backwards==0 && increment_initial_seed && replayFileWriting) + { + init_random_seed_lo += increment_initial_seed; + increment_initial_seed = 0; + if (char tmp = init_random_seed_lo >> 8) + { + init_random_seed_hi += tmp; + init_random_seed_lo &= 0xFF; + init_random_seed_hi &= 0xFF; + } + random_seed_lo = init_random_seed_lo; + random_seed_hi = init_random_seed_hi; + + fflush(replayFile); + fseek(replayFile, 0, SEEK_SET); + fwrite(&random_seed_lo, sizeof(random_seed_lo), 1, replayFile); + fwrite(&random_seed_hi, sizeof(random_seed_hi), 1, replayFile); + fwrite(&skillLevelLetter, 1, 1, replayFile); + fwrite(&skillLevelNumber, 1, 1, replayFile); + goto restart; + } + } +#else + if (UpdateHUD()) + break; + DrawViewport(); +#endif + + if (forfeit_match) + { + EraseBottomTwoLines(); + break; + } + +#ifdef CHEAT + if (!fast_forward && single_step<0 && !skip_to_frame) +#else + if (!playbackMode || !fast_forward) +#endif + WaitForNextTick(tick_count); + + UpdateWeapons(); + UpdateSmallSnipes(); + UpdateLargeSnipes(); + UpdateSnipePortals(); + + BYTE replayIO; + bool playbackFinished = false; + if (playbackMode && fread(&replayIO, 1, 1, replayFile) == 0) + playbackFinished = true; + if (UpdatePlayer(playbackMode, replayIO) || playbackFinished) + break; + if (!playbackMode && replayFile) + { + fwrite(&replayIO, 1, 1, replayFile); +#ifdef CHEAT + fflush(replayFile); +#endif + } + + UpdateExplosions(); + UpdateSound(); + + // Process SDL events and render frame for macOS + ProcessSDLEvents(); + RenderFrame(); + } +#ifdef CHEAT + match_ended: +#endif + + if (playbackMode && _PLAYBACK_FOR_SCREEN_RECORDING) + WaitForNextTick(tick_count); + ClearSound(); + forfeit_match = false; + + if (replayFile) + fclose(replayFile); + + CloseDirectConsole(WINDOW_HEIGHT-1 - (playbackMode && !_PLAYBACK_FOR_SCREEN_RECORDING ? 1 : 0)); + if (instant_quit) + break; + if (playbackMode && !_PLAYBACK_FOR_SCREEN_RECORDING) + { +#ifndef _CONSOLE + // TODO: do this in the console build too, if it's possible to detect when Windows won't itself prompt the user to press any key + SetConsoleOutputTextColor(0x8); + WriteTextToConsole("\r\n""Press any key to continue..."); + WaitForKeyPress(); +#endif + break; + } + for (;;) + { + WriteTextToConsole("Play another game? (Y or N) "); + if (playbackMode) + { + WaitForKeyPress(); + goto do_not_play_again; + } + char playAgain; + auto numread = ReadTextFromConsole(&playAgain, 1); + if (got_ctrl_break) + goto do_not_play_again; + if (!numread) + continue; + if (playAgain == 'Y' || playAgain == 'y') + break; + if (playAgain == 'N' || playAgain == 'n') + goto do_not_play_again; + } + WriteTextToConsole("Enter new skill level (A-Z)(1-9): "); + ReadSkillLevel(); + if (got_ctrl_break) + break; + } +do_not_play_again: + + CloseSound(); + CloseTimer(); + CleanupSDL(); + CloseConsole(); + + return 0; +} diff --git a/console.h b/console.h index f410568..681620f 100644 --- a/console.h +++ b/console.h @@ -1,25 +1,31 @@ -#pragma once - -#include "types.h" -#include "macros.h" - -void WriteTextMem(Uint count, WORD row, WORD column, MazeTile *src); - -void outputText(BYTE color, WORD count, WORD row, WORD column, const char *src); -void outputNumber(BYTE color, bool zeroPadding, WORD count, WORD row, WORD column, Uint number); -void EraseBottomTwoLines(); - -DWORD ReadTextFromConsole(char buffer[], DWORD bufsize); - -void SetConsoleOutputTextColor(WORD wAttributes); - -void WriteTextToConsole(char const *text, size_t length); -template void WriteTextToConsole(char const (&text)[LENGTH]) { WriteTextToConsole(text, strlength(text)); } - -void OpenDirectConsole(); -void CloseDirectConsole(Uint lineNum); - -void ClearConsole(); - -int OpenConsole(); -void CloseConsole(); +#pragma once + +#include "types.h" +#include "macros.h" + +void WriteTextMem(Uint count, WORD row, WORD column, MazeTile *src); + +void outputText(BYTE color, WORD count, WORD row, WORD column, const char *src); +void outputNumber(BYTE color, bool zeroPadding, WORD count, WORD row, WORD column, Uint number); +void EraseBottomTwoLines(); + +DWORD ReadTextFromConsole(char buffer[], DWORD bufsize); + +void SetConsoleOutputTextColor(WORD wAttributes); + +void WriteTextToConsole(char const *text, size_t length); +template void WriteTextToConsole(char const (&text)[LENGTH]) { WriteTextToConsole(text, strlength(text)); } + +void OpenDirectConsole(); +void CloseDirectConsole(Uint lineNum); + +void ClearConsole(); + +int OpenConsole(); +void CloseConsole(); + +// macOS-specific SDL functions +int CreateSDLWindow(); +void ProcessSDLEvents(); +void RenderFrame(); +void CleanupSDL(); diff --git a/sdl/console.cpp b/sdl/console.cpp index 7b05b73..a6aacb3 100644 --- a/sdl/console.cpp +++ b/sdl/console.cpp @@ -1,701 +1,923 @@ -#include -#include -#include -#include -#include "../config.h" -#include "../Snipes.h" -#include "../macros.h" -#include "../console.h" -#include "../platform.h" -#include "../timer.h" -#include "sdl.h" - -#define DEFAULT_TEXT_COLOR 0x7 // (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED) - -// TODO: Handle Ctrl+C / Ctrl+Break - -static MazeTile Screen[WINDOW_HEIGHT][WINDOW_WIDTH]; -SDL_mutex *ScreenMutex; - -int FontSize, TileWidth, TileHeight; - -static BYTE OutputTextColor = DEFAULT_TEXT_COLOR; -static Uint OutputCursorX = 0; -static Uint OutputCursorY = 0; -static bool OutputCursorVisible = true; -bool Paused = false; - -char InputBuffer[InputBufferSize]; -Uint InputBufferReadIndex = 0, InputBufferWriteIndex = 0; - -static bool Exiting = false; - -void WriteTextMem(Uint count, WORD row, WORD column, MazeTile *src) -{ - SDL_LockMutex(ScreenMutex); - MazeTile* dst = &Screen[row][column]; - for (Uint i=0; i= WINDOW_WIDTH*WINDOW_HEIGHT) - break; // Out of bounds - MazeTile tile = src[i]; -#ifdef CHEAT - if (single_step>=0 && tile.chr==0xFF) - tile.chr = 0xF0; -#endif -#if 0 // defined(CHEAT_OMNISCIENCE) && defined(CHEAT_OMNISCIENCE_SHOW_NORMAL_VIEWPORT) - if (!(inrangex(column+i, WINDOW_WIDTH/2 - 40/2, - WINDOW_WIDTH/2 + 40/2) && - inrangex(row, VIEWPORT_ROW + VIEWPORT_HEIGHT/2 - (25 - VIEWPORT_ROW)/2, - VIEWPORT_ROW + VIEWPORT_HEIGHT/2 + (25 - VIEWPORT_ROW)/2)) && row >= VIEWPORT_ROW) - tile.color += 0x40; -#endif - *dst++ = tile; - } - SDL_UnlockMutex(ScreenMutex); -} - -void outputText(BYTE color, WORD count, WORD row, WORD column, const char *src) -{ - SDL_LockMutex(ScreenMutex); - MazeTile* dst = &Screen[row][column]; - for (Uint i=0; i= WINDOW_WIDTH*WINDOW_HEIGHT) - break; // Out of bounds - *dst++ = MazeTile(color, src[i]); - } - SDL_UnlockMutex(ScreenMutex); -} - -void outputNumber(BYTE color, bool zeroPadding, WORD count, WORD row, WORD column, Uint number) -{ - SDL_LockMutex(ScreenMutex); - char textbuf[strlength("4294967295")+1]; - sprintf(textbuf, zeroPadding ? "%0*u" : "%*u", count, number); - outputText(color, count, row, column, textbuf); - SDL_UnlockMutex(ScreenMutex); -} - -void EraseBottomTwoLines() -{ - SDL_LockMutex(ScreenMutex); - for (Uint y = WINDOW_HEIGHT-2; y < WINDOW_HEIGHT; y++) - for (Uint x = 0; x < WINDOW_WIDTH; x++) - Screen[y][x] = MazeTile(7, ' '); - SDL_UnlockMutex(ScreenMutex); -} - -void CheckForBreak() -{ - // SleepTimeslice(); // allow ConsoleHandlerRoutine to be triggered - // if (forfeit_match) - // WriteConsole(output, "\r\n", 2, &numwritten, 0); - got_ctrl_break = forfeit_match; -} - -DWORD ReadTextFromConsole(char buffer[], DWORD bufsize) -{ - // TODO: Semaphores? - SDL_StartTextInput(); - - DWORD numread = 0; - while (true) - { - while (InputBufferReadIndex == InputBufferWriteIndex) - { - CheckForBreak(); - if (forfeit_match) - goto done; - SleepTimeslice(); - } - char c = InputBuffer[InputBufferReadIndex]; - InputBufferReadIndex = (InputBufferReadIndex+1) % InputBufferSize; - if (c == '\n') - { - WriteTextToConsole("\r\n", 2);; - break; - } - else - if (c == '\b') // Backspace - { - if (numread) - { - WriteTextToConsole(&c, 1);; - numread--; - } - } - else - { - if (numread < bufsize) - { - WriteTextToConsole(&c, 1);; - buffer[numread] = c; - numread++; - } - } - } - -done: - SDL_StopTextInput(); - return numread; -} - -void SetConsoleOutputTextColor(WORD wAttributes) -{ - OutputTextColor = (BYTE)wAttributes; -} - -void WriteTextToConsole(char const *text, size_t length) -{ - SDL_LockMutex(ScreenMutex); - for (Uint n=0; npixels + 3 + useRightEdge*(s->w - 1)*4; - for (int y0=0; y0h; y0++) - { - int y = y0 + d; - if (y >= s->h) - continue; - int alphaDiff = *alpha - SingleHorzLineEdge[y]; - diff[d] += (Uint)(alphaDiff * alphaDiff); - } - diff[1] = diff[1] * (s->h-1) / s->h; // compensate for there having been 1 extra sampling - } - if (diff[1] < diff[0]) // is the glitch active with this font size? - memmove((Uint8*)s->pixels + s->pitch, s->pixels, s->pitch * (s->h - 1)); // fix it by shifting the glyph down 1 pixel -} -static void FixUpGlyphVerticallyBottom(SDL_Surface *s) -{ - // Clone the bottommost-penultimate 1-pixel tall horizontal strip into the bottommost one - Uint8 *pixel = (Uint8*)s->pixels; - for (int x=0; xw; x++) - pixel[(TileHeight - 1)*s->pitch + x] = pixel[(TileHeight - 2)*s->pitch + x]; -} -static void FixUpGlyphVerticallyTop(SDL_Surface *s) -{ - // Clone the topmost-penultimate 1-pixel tall horizontal strip into the topmost one - Uint8 *pixel = (Uint8*)s->pixels; - for (int x=0; xw; x++) - pixel[0 * s->pitch + x] = pixel[1 * s->pitch + x]; -} - -static SDL_Texture*& GetGlyph(SDL_Renderer *ren, TTF_Font* font, BYTE chr) -{ - SDL_Texture*& t = Glyphs[chr]; - if (t) - return t; - static const wchar_t Chars[257] = - L" ☺☻♥♦♣♠•◘○◙♂♀♪♫☼" - L"►◄↕‼¶§▬↨↑↓→←∟↔▲▼" - L" !\"#$%&'()*+,-./" - L"0123456789:;<=>?" - L"@ABCDEFGHIJKLMNO" - L"PQRSTUVWXYZ[\\]^_" - L"`abcdefghijklmno" - L"pqrstuvwxyz{|}~⌂" - L"ÇüéâäàåçêëèïîìÄÅ" - L"ÉæÆôöòûùÿÖÜ¢£¥₧ƒ" - L"áíóúñѪº¿⌐¬½¼¡«»" - L"░▒▓│┤╡╢╖╕╣║╗╝╜╛┐" - L"└┴┬├─┼╞╟╚╔╩╦╠═╬╧" - L"╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀" - L"αßΓπΣσµτΦΘΩδ∞φε∩" - L"≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ "; - - wchar_t str[2]; - str[0] = Chars[chr]; - str[1] = 0; - SDL_Color white = {0xFF, 0xFF, 0xFF, 0xFF}; - SDL_Color black = {0x00, 0x00, 0x00, 0x00}; - SDL_Surface *s = TTF_RenderUNICODE_Shaded(font, (Uint16*)str, white, black); -#ifdef FONT_FIXUP_VERTICALLY - if (wcschr(L"│┤╡╢╖╕╣║╗┐┬├┼╞╟╔╦╠╬╤╥╒╓╫╪┌", str[0]) != NULL) FixUpGlyphVerticallyBottom(s); - if (wcschr(L"│┤╡╢╣║╝╜╛└┴├┼╞╟╚╩╠╬╧╨╙╘╫╪┘", str[0]) != NULL) FixUpGlyphVerticallyTop (s); -#endif -#ifdef FONT_FIXUP_HORIZONTALLY - if (wcschr(L"└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┌", str[0]) != NULL) - { - // Clone the rightmost-penultimate 1-pixel wide vertical strip into the rightmost one - Uint8 *pixel = (Uint8*)s->pixels; - for (int y=0; yh; y++, pixel += s->pitch) - pixel[TileWidth-1] = pixel[TileWidth-2]; - } - if (wcschr(L"└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┌", str[0]) != NULL) - { - // Clone the leftmost-penultimate 1-pixel wide vertical strip into the leftmost one - Uint8 *pixel = (Uint8*)s->pixels; - for (int y=0; yh; y++, pixel += s->pitch) - pixel[0] = pixel[1]; - } -#endif -#ifdef FONT_FIXUP_JOINING - if (chr == 0xC4 && SingleHorzLineEdge==NULL) // ─ - { - SingleHorzLineEdge = new Uint8 [TileHeight]; - Uint8 *alpha = (Uint8*)s->pixels; - for (int y=0; yh; y++, alpha += s->pitch) - SingleHorzLineEdge[y] = *alpha; - } - else - if (chr == 0xCD && DoubleHorzLineEdge==NULL) // ═ - { - DoubleHorzLineEdge = new Uint8 [TileHeight]; - Uint8 *alpha = (Uint8*)s->pixels; - for (int y=0; yh; y++, alpha += s->pitch) - DoubleHorzLineEdge[y] = *alpha; - } - else - if (wcschr(L"┤╢╖┐┬┼╥╫", str[0]) != NULL) - { - GetGlyph(ren, font, 0xC4); // ─ // make sure SingleHorzLineEdge is initialized - FixUpLineJoiningGlitch(s, SingleHorzLineEdge, false); - FixUpGlyphVerticallyTop(s); - } - if (wcschr(L"╡╕╣╗╦╬╤╪", str[0]) != NULL) - { - GetGlyph(ren, font, 0xCD); // ═ // make sure DoubleHorzLineEdge is initialized - FixUpLineJoiningGlitch(s, DoubleHorzLineEdge, false); - FixUpGlyphVerticallyTop(s); - } - else - if (wcschr(L"├╟╓┌", str[0]) != NULL) - { - GetGlyph(ren, font, 0xC4); // ─ // make sure SingleHorzLineEdge is initialized - FixUpLineJoiningGlitch(s, SingleHorzLineEdge, true); - FixUpGlyphVerticallyTop(s); - } - else - if (wcschr(L"╞╔╠╒", str[0]) != NULL) - { - GetGlyph(ren, font, 0xCD); // ═ // make sure DoubleHorzLineEdge is initialized - FixUpLineJoiningGlitch(s, DoubleHorzLineEdge, true); - FixUpGlyphVerticallyTop(s); - } -#endif - // Create an RGBA surface from the 8-bit surface created by TTF_RenderUNICODE_Shaded(), initializing the RGB values to maximum white and the alpha channel from the source surface. - // We assume that its palette maps 0-255 linearly to grayscale 0-255. - SDL_Surface *s_rgba = SDL_CreateRGBSurfaceWithFormat(0, s->w, s->h, 32, SDL_PIXELFORMAT_RGBA32); - SDL_FillRect(s_rgba, NULL, 0xFFFFFFFF); - Uint8 *srcPixel = (Uint8*)s ->pixels; - Uint8 *dstAlpha = (Uint8*)s_rgba->pixels + 3; - Uint total = 0; - for (int y=0; yh; y++, srcPixel += s->pitch, dstAlpha += s_rgba->pitch) - for (int x=0; xw; x++) - total += dstAlpha[x*4] = srcPixel[x]; - GlyphIsEmpty[chr] = total == 0; - - t = SDL_CreateTextureFromSurface(ren, s_rgba); - SDL_FreeSurface(s); - return t; -} - -static void RenderCharacterAt(SDL_Renderer *ren, TTF_Font* font, Uint x, Uint y) -{ - MazeTile tile = Screen[y][x]; - Uint fgScreen = tile.color & 0xF; - Uint bgScreen = tile.color >> 4; - SDL_Color fg = ScreenColors[fgScreen]; - SDL_Color bg = ScreenColors[bgScreen]; - SDL_Color black = {0, 0, 0, 0xFF}; - SDL_Rect rect = { (int)x * TileWidth, (int)y * TileHeight, TileWidth, TileHeight }; - SDL_SetRenderDrawColor(ren, bg.r, bg.g, bg.b, 0xFF); - if (bgScreen != 0) - SDL_RenderFillRect(ren, &rect); - if (GlyphIsEmpty[tile.chr]) // as opposed to tile.chr==0||tile.chr==' ', this leaves full flexibility up to the font - return; - - SDL_Texture*& t = GetGlyph(ren, font, tile.chr); - - int w, h; - SDL_QueryTexture(t, NULL, NULL, &w, &h); - SDL_Rect src = { 0, 0, w, h }; - SDL_Rect dst = { (int)x * TileWidth + (TileWidth - w) / 2, (int)y * TileHeight + (TileHeight - h) / 2, w, h }; - if (h > (int)TileHeight) // not sure if this will always happen, so make the fix conditional - { - src.y += TileHeight % 19 * 8 / 19 % 2; - src.h--; - dst.h--; - } - SDL_SetTextureColorMod(t, fg.r, fg.g, fg.b); - SDL_RenderCopy(ren, t, &src, &dst); -} - -static void ClearGlyphs() -{ - memset(Glyphs, 0, sizeof(Glyphs)); - memset(GlyphIsEmpty, 0, sizeof(GlyphIsEmpty)); - if (SingleHorzLineEdge) delete [] SingleHorzLineEdge; SingleHorzLineEdge = NULL; - if (DoubleHorzLineEdge) delete [] DoubleHorzLineEdge; DoubleHorzLineEdge = NULL; -} -static void DestroyGlyphs() -{ - for (Uint color=0; color<256; color++) - for (Uint chr=0; chr<256; chr++) - if (Glyphs[color]) - SDL_DestroyTexture(Glyphs[color]); -} - -static int SDLCALL ConsoleThreadFunc(void*) -{ - SDL_Rect usableRect; - if (SDL_GetDisplayUsableBounds(0, &usableRect) != 0) - { - fprintf(stderr, "SDL_GetDesktopDisplayMode: %s\n", SDL_GetError()); - SDL_Quit(); - return 1; - } - { -#ifdef TILE_HEIGHT - if (TILE_HEIGHT * WINDOW_HEIGHT > usableRect.h) - { - fprintf(stderr, "Error: Configured tile height (%d) exceeds usable height (%d)\n", TILE_HEIGHT, usableRect.h); - SDL_Quit(); - return 1; - } - TileHeight = TILE_HEIGHT; -#else - TileHeight = usableRect.h / WINDOW_HEIGHT; -#endif - -#ifndef TILE_WIDTH - TileWidth = ((TileHeight*3+2)/4); -#else - TileWidth = TILE_WIDTH; -#endif -#ifndef FONT_SIZE - FontSize = TileHeight; -#else - FontSize = FONT_SIZE; -#endif - } - SDL_Window *win; - int top, left, bottom, right, totalWidth, totalHeight; - for (Uint i=0; i<2; i++) - { - win = SDL_CreateWindow("Snipes", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH * TileWidth, WINDOW_HEIGHT * TileHeight, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); - if (!win) - { - fprintf(stderr, "SDL_CreateWindow: %s\n", SDL_GetError()); - return 1; - } - SDL_GetWindowBordersSize(win, &top, &left, &bottom, &right); - totalHeight = WINDOW_HEIGHT * TileHeight + (top + bottom); -#ifdef TILE_HEIGHT - break; -#else - if (i || totalHeight <= usableRect.h + ALLOWABLE_BORDER_EXCESS) - break; - TileHeight = (usableRect.h - (top + bottom)) / WINDOW_HEIGHT; - TileWidth = ((TileHeight*3+2)/4); - SDL_DestroyWindow(win); -#endif - } - totalWidth = WINDOW_WIDTH * TileWidth + (left + right); - SDL_SetWindowPosition(win, (usableRect.w - totalWidth) / 2 + left, (usableRect.h - totalHeight) / 2 + top); // center the window within the usable desktop area - - if (TTF_Init() != 0) - { - fprintf(stderr, "TTF_Init: %s\n", SDL_GetError()); - return 1; - } - - TTF_Font* font = TTF_OpenFont(FONT_FILENAME, FontSize); - if (!font) - { - fprintf(stderr, "TTF_OpenFont: %s\n", SDL_GetError()); - TTF_Quit(); - return 1; - } - TTF_SetFontHinting(font, TTF_HINTING_MONO); - - SDL_Renderer *ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); - if (!ren) - { - SDL_DestroyWindow(win); - fprintf(stderr, "SDL_CreateRenderer: %s\n", SDL_GetError()); - TTF_CloseFont(font); - TTF_Quit(); - return 1; - } - - ClearGlyphs(); - - while (!Exiting) - { - SDL_Event e; - while (SDL_PollEvent(&e)) - { - switch (e.type) - { - case SDL_QUIT: - instant_quit = forfeit_match = got_ctrl_break = true; - break; - case SDL_KEYDOWN: - case SDL_KEYUP: - HandleKey((SDL_KeyboardEvent*)&e); - break; - case SDL_TEXTINPUT: - /* Add new text onto the end of our text */ - for (const char* s = e.text.text; *s; s++) - { - InputBuffer[InputBufferWriteIndex] = *s; - InputBufferWriteIndex = (InputBufferWriteIndex+1) % InputBufferSize; - } - break; - case SDL_WINDOWEVENT: - switch (e.window.event) - { - case SDL_WINDOWEVENT_FOCUS_LOST: - Paused = true; - break; - case SDL_WINDOWEVENT_FOCUS_GAINED: - Paused = false; - break; - case SDL_WINDOWEVENT_RESIZED: - { - int newWidth = e.window.data1; - int newHeight = e.window.data2; - { - double widthRatio = (double)newWidth / (WINDOW_WIDTH * TileWidth); - double heightRatio = (double)newHeight / (WINDOW_HEIGHT * TileHeight); - if ( widthRatio < 1) widthRatio = 1. / widthRatio; - if (heightRatio < 1) heightRatio = 1. / heightRatio; - - if (heightRatio > widthRatio) - { - TileHeight = (newHeight + WINDOW_HEIGHT/2) / WINDOW_HEIGHT; - TileWidth = ((TileHeight*3+2)/4); - } - else - { - TileWidth = (newWidth + WINDOW_WIDTH/2) / WINDOW_WIDTH; - TileHeight = ((TileWidth*8+3)/6); - } - } - - FontSize = TileHeight; - DestroyGlyphs(); - ClearGlyphs(); - - TTF_Font* newFont = TTF_OpenFont(FONT_FILENAME, FontSize); - if (!newFont) - { - fprintf(stderr, "TTF_OpenFont: %s\n", SDL_GetError()); - instant_quit = forfeit_match = got_ctrl_break = true; - } - else - { - TTF_CloseFont(font); - font = newFont; - } - - SDL_SetWindowSize(win, WINDOW_WIDTH * TileWidth, WINDOW_HEIGHT * TileHeight); - } - break; - } - break; - } - } - - SDL_SetRenderDrawColor(ren, 0, 0, 0, 255); - - SDL_RenderClear(ren); - - SDL_LockMutex(ScreenMutex); - for (Uint y = 0; y < WINDOW_HEIGHT; y++) - for (Uint x = 0; x < WINDOW_WIDTH; x++) - RenderCharacterAt(ren, font, x, y); - SDL_UnlockMutex(ScreenMutex); -#if defined(CHEAT_OMNISCIENCE) && defined(CHEAT_OMNISCIENCE_SHOW_NORMAL_VIEWPORT) - { - SDL_Rect rect; - - // darken outside area - - SDL_SetRenderDrawColor(ren, CHEAT_OMNISCIENCE_RGBA_OUTSIDE_NORMAL_VIEWPORT); - SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND); - - rect.w = WINDOW_WIDTH * TileWidth; rect.x = 0; rect.y = VIEWPORT_ROW * TileHeight; rect.h = (VIEWPORT_HEIGHT/2 - (25 - VIEWPORT_ROW)/2) * TileHeight; - SDL_RenderFillRect(ren, &rect); - - rect.y = (VIEWPORT_ROW + VIEWPORT_HEIGHT/2 + (25 - VIEWPORT_ROW)/2) * TileHeight; - SDL_RenderFillRect(ren, &rect); - - rect.x = 0; rect.y = (VIEWPORT_ROW + VIEWPORT_HEIGHT/2 - (25 - VIEWPORT_ROW)/2) * TileHeight; rect.w = (WINDOW_WIDTH/2 - 40/2) * TileWidth; rect.h = ((25 - VIEWPORT_ROW)/2*2) * TileHeight; - SDL_RenderFillRect(ren, &rect); - - rect.x = (WINDOW_WIDTH/2 + 40/2) * TileWidth; - SDL_RenderFillRect(ren, &rect); - - #ifdef CHEAT_OMNISCIENCE_RGBA_AROUND_NORMAAL_VIEWPORT - // draw gray rectangle around inside area - rect.x = (WINDOW_WIDTH/2 - 40/2) * TileWidth - 1; rect.y = (VIEWPORT_ROW + VIEWPORT_HEIGHT/2 - (25 - VIEWPORT_ROW)/2 ) * TileHeight - 1; - rect.w = ( 80/2) * TileWidth + 2; rect.h = ( (25 - VIEWPORT_ROW)/2*2) * TileHeight + 2; - SDL_SetRenderDrawColor(ren, CHEAT_OMNISCIENCE_RGBA_AROUND_NORMAAL_VIEWPORT); - SDL_RenderDrawRect(ren, &rect); - #endif CHEAT_OMNISCIENCE_RGBA_AROUND_NORMAAL_VIEWPORT - - SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_NONE); - } -#endif - - if (OutputCursorVisible && SDL_GetTicks() % 500 < 250) - { - SDL_Color c = ScreenColors[OutputTextColor]; - SDL_SetRenderDrawColor(ren, c.r, c.g, c.b, c.a); - SDL_Rect rect = { (int)OutputCursorX * TileWidth, (int)OutputCursorY * TileHeight, TileWidth, TileHeight }; - SDL_RenderFillRect(ren, &rect); - } - -#if 0 - if (screenshot_filename) - { - SDL_Surface *infoSurface = SDL_GetWindowSurface(win); - unsigned char *pixels = new BYTE [infoSurface->w * infoSurface->h * infoSurface->format->BytesPerPixel]; - SDL_RenderReadPixels(ren, &infoSurface->clip_rect, infoSurface->format->format, pixels, infoSurface->w * infoSurface->format->BytesPerPixel); - SDL_Surface *saveSurface = SDL_CreateRGBSurfaceFrom(pixels, infoSurface->w, infoSurface->h, infoSurface->format->BitsPerPixel, infoSurface->w * infoSurface->format->BytesPerPixel, infoSurface->format->Rmask, infoSurface->format->Gmask, infoSurface->format->Bmask, infoSurface->format->Amask); - SDL_SaveBMP(saveSurface, screenshot_filename); - SDL_FreeSurface(saveSurface); - screenshot_filename = NULL; - } -#endif - SDL_RenderPresent(ren); - - SleepTimeslice(); - } - - DestroyGlyphs(); - - SDL_DestroyRenderer(ren); - SDL_DestroyWindow(win); - TTF_CloseFont(font); - TTF_Quit(); - - return 0; -} - -static SDL_Thread *ConsoleThread; - -int OpenConsole() -{ - Exiting = false; - - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) - { - fprintf(stderr, "SDL_Init: %s\n", SDL_GetError()); - return 1; - } - else - { - ConsoleThread = SDL_CreateThread(ConsoleThreadFunc, "ConsoleThread", NULL); - if (!ConsoleThread) - { - fprintf(stderr, "SDL_CreateWindow: %s\n", SDL_GetError()); - SDL_Quit(); - return 1; - } - - ScreenMutex = SDL_CreateMutex(); - - return 0; - } -} - -void CloseConsole() -{ - Exiting = true; - SDL_WaitThread(ConsoleThread, NULL); - SDL_DestroyMutex(ScreenMutex); - SDL_Quit(); -} +#include +#include +#ifdef __APPLE__ +#define SDL_MAIN_HANDLED +#endif +#include +#include +#include "../config.h" +#include "../Snipes.h" +#include "../macros.h" +#include "../console.h" +#include "../platform.h" +#include "../timer.h" +#include "sdl.h" + +#define DEFAULT_TEXT_COLOR 0x7 // (FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED) + +// TODO: Handle Ctrl+C / Ctrl+Break + +static MazeTile Screen[WINDOW_HEIGHT][WINDOW_WIDTH]; +SDL_mutex *ScreenMutex; + +int FontSize, TileWidth, TileHeight; + +static BYTE OutputTextColor = DEFAULT_TEXT_COLOR; +static Uint OutputCursorX = 0; +static Uint OutputCursorY = 0; +static bool OutputCursorVisible = true; +bool Paused = false; + +char InputBuffer[InputBufferSize]; +Uint InputBufferReadIndex = 0, InputBufferWriteIndex = 0; + +static bool Exiting = false; + +void WriteTextMem(Uint count, WORD row, WORD column, MazeTile *src) +{ + SDL_LockMutex(ScreenMutex); + MazeTile* dst = &Screen[row][column]; + for (Uint i=0; i= WINDOW_WIDTH*WINDOW_HEIGHT) + break; // Out of bounds + MazeTile tile = src[i]; +#ifdef CHEAT + if (single_step>=0 && tile.chr==0xFF) + tile.chr = 0xF0; +#endif +#if 0 // defined(CHEAT_OMNISCIENCE) && defined(CHEAT_OMNISCIENCE_SHOW_NORMAL_VIEWPORT) + if (!(inrangex(column+i, WINDOW_WIDTH/2 - 40/2, + WINDOW_WIDTH/2 + 40/2) && + inrangex(row, VIEWPORT_ROW + VIEWPORT_HEIGHT/2 - (25 - VIEWPORT_ROW)/2, + VIEWPORT_ROW + VIEWPORT_HEIGHT/2 + (25 - VIEWPORT_ROW)/2)) && row >= VIEWPORT_ROW) + tile.color += 0x40; +#endif + *dst++ = tile; + } + SDL_UnlockMutex(ScreenMutex); +} + +void outputText(BYTE color, WORD count, WORD row, WORD column, const char *src) +{ + SDL_LockMutex(ScreenMutex); + MazeTile* dst = &Screen[row][column]; + for (Uint i=0; i= WINDOW_WIDTH*WINDOW_HEIGHT) + break; // Out of bounds + *dst++ = MazeTile(color, src[i]); + } + SDL_UnlockMutex(ScreenMutex); +} + +void outputNumber(BYTE color, bool zeroPadding, WORD count, WORD row, WORD column, Uint number) +{ + SDL_LockMutex(ScreenMutex); + char textbuf[strlength("4294967295")+1]; + sprintf(textbuf, zeroPadding ? "%0*u" : "%*u", count, number); + outputText(color, count, row, column, textbuf); + SDL_UnlockMutex(ScreenMutex); +} + +void EraseBottomTwoLines() +{ + SDL_LockMutex(ScreenMutex); + for (Uint y = WINDOW_HEIGHT-2; y < WINDOW_HEIGHT; y++) + for (Uint x = 0; x < WINDOW_WIDTH; x++) + Screen[y][x] = MazeTile(7, ' '); + SDL_UnlockMutex(ScreenMutex); +} + +void CheckForBreak() +{ + // SleepTimeslice(); // allow ConsoleHandlerRoutine to be triggered + // if (forfeit_match) + // WriteConsole(output, "\r\n", 2, &numwritten, 0); + got_ctrl_break = forfeit_match; +} + +DWORD ReadTextFromConsole(char buffer[], DWORD bufsize) +{ + // TODO: Semaphores? + SDL_StartTextInput(); + + DWORD numread = 0; + while (true) + { + while (InputBufferReadIndex == InputBufferWriteIndex) + { + CheckForBreak(); + if (forfeit_match) + goto done; + + // Process SDL events and render while waiting for input + ProcessSDLEvents(); + RenderFrame(); + SleepTimeslice(); + } + char c = InputBuffer[InputBufferReadIndex]; + InputBufferReadIndex = (InputBufferReadIndex+1) % InputBufferSize; + if (c == '\n') + { + WriteTextToConsole("\r\n", 2);; + break; + } + else + if (c == '\b') // Backspace + { + if (numread) + { + WriteTextToConsole(&c, 1);; + numread--; + } + } + else + { + if (numread < bufsize) + { + WriteTextToConsole(&c, 1);; + buffer[numread] = c; + numread++; + } + } + } + +done: + SDL_StopTextInput(); + return numread; +} + +void SetConsoleOutputTextColor(WORD wAttributes) +{ + OutputTextColor = (BYTE)wAttributes; +} + +void WriteTextToConsole(char const *text, size_t length) +{ + SDL_LockMutex(ScreenMutex); + for (Uint n=0; npixels + 3 + useRightEdge*(s->w - 1)*4; + for (int y0=0; y0h; y0++) + { + int y = y0 + d; + if (y >= s->h) + continue; + int alphaDiff = *alpha - SingleHorzLineEdge[y]; + diff[d] += (Uint)(alphaDiff * alphaDiff); + } + diff[1] = diff[1] * (s->h-1) / s->h; // compensate for there having been 1 extra sampling + } + if (diff[1] < diff[0]) // is the glitch active with this font size? + memmove((Uint8*)s->pixels + s->pitch, s->pixels, s->pitch * (s->h - 1)); // fix it by shifting the glyph down 1 pixel +} +static void FixUpGlyphVerticallyBottom(SDL_Surface *s) +{ + // Clone the bottommost-penultimate 1-pixel tall horizontal strip into the bottommost one + Uint8 *pixel = (Uint8*)s->pixels; + for (int x=0; xw; x++) + pixel[(TileHeight - 1)*s->pitch + x] = pixel[(TileHeight - 2)*s->pitch + x]; +} +static void FixUpGlyphVerticallyTop(SDL_Surface *s) +{ + // Clone the topmost-penultimate 1-pixel tall horizontal strip into the topmost one + Uint8 *pixel = (Uint8*)s->pixels; + for (int x=0; xw; x++) + pixel[0 * s->pitch + x] = pixel[1 * s->pitch + x]; +} + +static SDL_Texture*& GetGlyph(SDL_Renderer *ren, TTF_Font* font, BYTE chr) +{ + SDL_Texture*& t = Glyphs[chr]; + if (t) + return t; + static const wchar_t Chars[257] = + L" ☺☻♥♦♣♠•◘○◙♂♀♪♫☼" + L"►◄↕‼¶§▬↨↑↓→←∟↔▲▼" + L" !\"#$%&'()*+,-./" + L"0123456789:;<=>?" + L"@ABCDEFGHIJKLMNO" + L"PQRSTUVWXYZ[\\]^_" + L"`abcdefghijklmno" + L"pqrstuvwxyz{|}~⌂" + L"ÇüéâäàåçêëèïîìÄÅ" + L"ÉæÆôöòûùÿÖÜ¢£¥₧ƒ" + L"áíóúñѪº¿⌐¬½¼¡«»" + L"░▒▓│┤╡╢╖╕╣║╗╝╜╛┐" + L"└┴┬├─┼╞╟╚╔╩╦╠═╬╧" + L"╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀" + L"αßΓπΣσµτΦΘΩδ∞φε∩" + L"≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ "; + + wchar_t str[2]; + str[0] = Chars[chr]; + str[1] = 0; + SDL_Color white = {0xFF, 0xFF, 0xFF, 0xFF}; + SDL_Color black = {0x00, 0x00, 0x00, 0x00}; + SDL_Surface *s = TTF_RenderUNICODE_Shaded(font, (Uint16*)str, white, black); +#ifdef FONT_FIXUP_VERTICALLY + if (wcschr(L"│┤╡╢╖╕╣║╗┐┬├┼╞╟╔╦╠╬╤╥╒╓╫╪┌", str[0]) != NULL) FixUpGlyphVerticallyBottom(s); + if (wcschr(L"│┤╡╢╣║╝╜╛└┴├┼╞╟╚╩╠╬╧╨╙╘╫╪┘", str[0]) != NULL) FixUpGlyphVerticallyTop (s); +#endif +#ifdef FONT_FIXUP_HORIZONTALLY + if (wcschr(L"└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┌", str[0]) != NULL) + { + // Clone the rightmost-penultimate 1-pixel wide vertical strip into the rightmost one + Uint8 *pixel = (Uint8*)s->pixels; + for (int y=0; yh; y++, pixel += s->pitch) + pixel[TileWidth-1] = pixel[TileWidth-2]; + } + if (wcschr(L"└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┌", str[0]) != NULL) + { + // Clone the leftmost-penultimate 1-pixel wide vertical strip into the leftmost one + Uint8 *pixel = (Uint8*)s->pixels; + for (int y=0; yh; y++, pixel += s->pitch) + pixel[0] = pixel[1]; + } +#endif +#ifdef FONT_FIXUP_JOINING + if (chr == 0xC4 && SingleHorzLineEdge==NULL) // ─ + { + SingleHorzLineEdge = new Uint8 [TileHeight]; + Uint8 *alpha = (Uint8*)s->pixels; + for (int y=0; yh; y++, alpha += s->pitch) + SingleHorzLineEdge[y] = *alpha; + } + else + if (chr == 0xCD && DoubleHorzLineEdge==NULL) // ═ + { + DoubleHorzLineEdge = new Uint8 [TileHeight]; + Uint8 *alpha = (Uint8*)s->pixels; + for (int y=0; yh; y++, alpha += s->pitch) + DoubleHorzLineEdge[y] = *alpha; + } + else + if (wcschr(L"┤╢╖┐┬┼╥╫", str[0]) != NULL) + { + GetGlyph(ren, font, 0xC4); // ─ // make sure SingleHorzLineEdge is initialized + FixUpLineJoiningGlitch(s, SingleHorzLineEdge, false); + FixUpGlyphVerticallyTop(s); + } + if (wcschr(L"╡╕╣╗╦╬╤╪", str[0]) != NULL) + { + GetGlyph(ren, font, 0xCD); // ═ // make sure DoubleHorzLineEdge is initialized + FixUpLineJoiningGlitch(s, DoubleHorzLineEdge, false); + FixUpGlyphVerticallyTop(s); + } + else + if (wcschr(L"├╟╓┌", str[0]) != NULL) + { + GetGlyph(ren, font, 0xC4); // ─ // make sure SingleHorzLineEdge is initialized + FixUpLineJoiningGlitch(s, SingleHorzLineEdge, true); + FixUpGlyphVerticallyTop(s); + } + else + if (wcschr(L"╞╔╠╒", str[0]) != NULL) + { + GetGlyph(ren, font, 0xCD); // ═ // make sure DoubleHorzLineEdge is initialized + FixUpLineJoiningGlitch(s, DoubleHorzLineEdge, true); + FixUpGlyphVerticallyTop(s); + } +#endif + // Create an RGBA surface from the 8-bit surface created by TTF_RenderUNICODE_Shaded(), initializing the RGB values to maximum white and the alpha channel from the source surface. + // We assume that its palette maps 0-255 linearly to grayscale 0-255. + SDL_Surface *s_rgba = SDL_CreateRGBSurfaceWithFormat(0, s->w, s->h, 32, SDL_PIXELFORMAT_RGBA32); + SDL_FillRect(s_rgba, NULL, 0xFFFFFFFF); + Uint8 *srcPixel = (Uint8*)s ->pixels; + Uint8 *dstAlpha = (Uint8*)s_rgba->pixels + 3; + Uint total = 0; + for (int y=0; yh; y++, srcPixel += s->pitch, dstAlpha += s_rgba->pitch) + for (int x=0; xw; x++) + total += dstAlpha[x*4] = srcPixel[x]; + GlyphIsEmpty[chr] = total == 0; + + t = SDL_CreateTextureFromSurface(ren, s_rgba); + SDL_FreeSurface(s); + return t; +} + +static void RenderCharacterAt(SDL_Renderer *ren, TTF_Font* font, Uint x, Uint y) +{ + MazeTile tile = Screen[y][x]; + Uint fgScreen = tile.color & 0xF; + Uint bgScreen = tile.color >> 4; + SDL_Color fg = ScreenColors[fgScreen]; + SDL_Color bg = ScreenColors[bgScreen]; + SDL_Color black = {0, 0, 0, 0xFF}; + SDL_Rect rect = { (int)x * TileWidth, (int)y * TileHeight, TileWidth, TileHeight }; + SDL_SetRenderDrawColor(ren, bg.r, bg.g, bg.b, 0xFF); + if (bgScreen != 0) + SDL_RenderFillRect(ren, &rect); + if (GlyphIsEmpty[tile.chr]) // as opposed to tile.chr==0||tile.chr==' ', this leaves full flexibility up to the font + return; + + SDL_Texture*& t = GetGlyph(ren, font, tile.chr); + + int w, h; + SDL_QueryTexture(t, NULL, NULL, &w, &h); + SDL_Rect src = { 0, 0, w, h }; + SDL_Rect dst = { (int)x * TileWidth + (TileWidth - w) / 2, (int)y * TileHeight + (TileHeight - h) / 2, w, h }; + if (h > (int)TileHeight) // not sure if this will always happen, so make the fix conditional + { + src.y += TileHeight % 19 * 8 / 19 % 2; + src.h--; + dst.h--; + } + SDL_SetTextureColorMod(t, fg.r, fg.g, fg.b); + SDL_RenderCopy(ren, t, &src, &dst); +} + +static void ClearGlyphs() +{ + memset(Glyphs, 0, sizeof(Glyphs)); + memset(GlyphIsEmpty, 0, sizeof(GlyphIsEmpty)); + if (SingleHorzLineEdge) delete [] SingleHorzLineEdge; SingleHorzLineEdge = NULL; + if (DoubleHorzLineEdge) delete [] DoubleHorzLineEdge; DoubleHorzLineEdge = NULL; +} +static void DestroyGlyphs() +{ + for (Uint color=0; color<256; color++) + for (Uint chr=0; chr<256; chr++) + if (Glyphs[color]) + SDL_DestroyTexture(Glyphs[color]); +} + +#ifdef __APPLE__ +static SDL_Window *main_window = NULL; +static bool window_created = false; +#endif + +// Helper function to load font with fallbacks +TTF_Font* LoadFontWithFallbacks(int size) +{ + TTF_Font* font = TTF_OpenFont(FONT_FILENAME, size); + if (!font) + { + fprintf(stderr, "Warning: Could not load %s, trying fallback fonts...\n", FONT_FILENAME); + // Try common system monospace fonts as fallbacks + const char* fallback_fonts[] = { + "/System/Library/Fonts/Courier.ttc", + "/System/Library/Fonts/SFNSMono.ttf", + "/System/Library/Fonts/Menlo.ttc", + "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", // Linux + NULL + }; + + for (int i = 0; fallback_fonts[i] != NULL; i++) + { + font = TTF_OpenFont(fallback_fonts[i], size); + if (font) + { + fprintf(stderr, "Using fallback font: %s\n", fallback_fonts[i]); + break; + } + } + } + return font; +} + +int ConsoleThreadFunc(void*) +{ + SDL_Rect usableRect; + if (SDL_GetDisplayUsableBounds(0, &usableRect) != 0) + { + fprintf(stderr, "SDL_GetDesktopDisplayMode: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + { +#ifdef TILE_HEIGHT + if (TILE_HEIGHT * WINDOW_HEIGHT > usableRect.h) + { + fprintf(stderr, "Error: Configured tile height (%d) exceeds usable height (%d)\n", TILE_HEIGHT, usableRect.h); + SDL_Quit(); + return 1; + } + TileHeight = TILE_HEIGHT; +#else + TileHeight = usableRect.h / WINDOW_HEIGHT; +#endif + +#ifndef TILE_WIDTH + TileWidth = ((TileHeight*3+2)/4); +#else + TileWidth = TILE_WIDTH; +#endif +#ifndef FONT_SIZE + FontSize = TileHeight; +#else + FontSize = FONT_SIZE; +#endif + } + SDL_Window *win; + int top, left, bottom, right, totalWidth, totalHeight; + for (Uint i=0; i<2; i++) + { + win = SDL_CreateWindow("Snipes", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH * TileWidth, WINDOW_HEIGHT * TileHeight, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); + if (!win) + { + fprintf(stderr, "SDL_CreateWindow: %s\n", SDL_GetError()); + return 1; + } + SDL_GetWindowBordersSize(win, &top, &left, &bottom, &right); + totalHeight = WINDOW_HEIGHT * TileHeight + (top + bottom); +#ifdef TILE_HEIGHT + break; +#else + if (i || totalHeight <= usableRect.h + ALLOWABLE_BORDER_EXCESS) + break; + TileHeight = (usableRect.h - (top + bottom)) / WINDOW_HEIGHT; + TileWidth = ((TileHeight*3+2)/4); + SDL_DestroyWindow(win); +#endif + } + totalWidth = WINDOW_WIDTH * TileWidth + (left + right); + SDL_SetWindowPosition(win, (usableRect.w - totalWidth) / 2 + left, (usableRect.h - totalHeight) / 2 + top); // center the window within the usable desktop area + + if (TTF_Init() != 0) + { + fprintf(stderr, "TTF_Init: %s\n", SDL_GetError()); + return 1; + } + + TTF_Font* font = LoadFontWithFallbacks(FontSize); + if (!font) + { + fprintf(stderr, "TTF_OpenFont: Could not load any fonts\n"); + TTF_Quit(); + return 1; + } + TTF_SetFontHinting(font, TTF_HINTING_MONO); + + SDL_Renderer *ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!ren) + { + SDL_DestroyWindow(win); + fprintf(stderr, "SDL_CreateRenderer: %s\n", SDL_GetError()); + TTF_CloseFont(font); + TTF_Quit(); + return 1; + } + + ClearGlyphs(); + + while (!Exiting) + { + SDL_Event e; + while (SDL_PollEvent(&e)) + { + switch (e.type) + { + case SDL_QUIT: + instant_quit = forfeit_match = got_ctrl_break = true; + break; + case SDL_KEYDOWN: + case SDL_KEYUP: + HandleKey((SDL_KeyboardEvent*)&e); + break; + case SDL_TEXTINPUT: + /* Add new text onto the end of our text */ + for (const char* s = e.text.text; *s; s++) + { + InputBuffer[InputBufferWriteIndex] = *s; + InputBufferWriteIndex = (InputBufferWriteIndex+1) % InputBufferSize; + } + break; + case SDL_WINDOWEVENT: + switch (e.window.event) + { + case SDL_WINDOWEVENT_FOCUS_LOST: + Paused = true; + break; + case SDL_WINDOWEVENT_FOCUS_GAINED: + Paused = false; + break; + case SDL_WINDOWEVENT_RESIZED: + { + int newWidth = e.window.data1; + int newHeight = e.window.data2; + { + double widthRatio = (double)newWidth / (WINDOW_WIDTH * TileWidth); + double heightRatio = (double)newHeight / (WINDOW_HEIGHT * TileHeight); + if ( widthRatio < 1) widthRatio = 1. / widthRatio; + if (heightRatio < 1) heightRatio = 1. / heightRatio; + + if (heightRatio > widthRatio) + { + TileHeight = (newHeight + WINDOW_HEIGHT/2) / WINDOW_HEIGHT; + TileWidth = ((TileHeight*3+2)/4); + } + else + { + TileWidth = (newWidth + WINDOW_WIDTH/2) / WINDOW_WIDTH; + TileHeight = ((TileWidth*8+3)/6); + } + } + + FontSize = TileHeight; + DestroyGlyphs(); + ClearGlyphs(); + + TTF_Font* newFont = LoadFontWithFallbacks(FontSize); + if (!newFont) + { + fprintf(stderr, "TTF_OpenFont: Could not load any fonts\n"); + instant_quit = forfeit_match = got_ctrl_break = true; + } + else + { + TTF_CloseFont(font); + font = newFont; + } + + SDL_SetWindowSize(win, WINDOW_WIDTH * TileWidth, WINDOW_HEIGHT * TileHeight); + } + break; + } + break; + } + } + + SDL_SetRenderDrawColor(ren, 0, 0, 0, 255); + + SDL_RenderClear(ren); + + SDL_LockMutex(ScreenMutex); + for (Uint y = 0; y < WINDOW_HEIGHT; y++) + for (Uint x = 0; x < WINDOW_WIDTH; x++) + RenderCharacterAt(ren, font, x, y); + SDL_UnlockMutex(ScreenMutex); +#if defined(CHEAT_OMNISCIENCE) && defined(CHEAT_OMNISCIENCE_SHOW_NORMAL_VIEWPORT) + { + SDL_Rect rect; + + // darken outside area + + SDL_SetRenderDrawColor(ren, CHEAT_OMNISCIENCE_RGBA_OUTSIDE_NORMAL_VIEWPORT); + SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND); + + rect.w = WINDOW_WIDTH * TileWidth; rect.x = 0; rect.y = VIEWPORT_ROW * TileHeight; rect.h = (VIEWPORT_HEIGHT/2 - (25 - VIEWPORT_ROW)/2) * TileHeight; + SDL_RenderFillRect(ren, &rect); + + rect.y = (VIEWPORT_ROW + VIEWPORT_HEIGHT/2 + (25 - VIEWPORT_ROW)/2) * TileHeight; + SDL_RenderFillRect(ren, &rect); + + rect.x = 0; rect.y = (VIEWPORT_ROW + VIEWPORT_HEIGHT/2 - (25 - VIEWPORT_ROW)/2) * TileHeight; rect.w = (WINDOW_WIDTH/2 - 40/2) * TileWidth; rect.h = ((25 - VIEWPORT_ROW)/2*2) * TileHeight; + SDL_RenderFillRect(ren, &rect); + + rect.x = (WINDOW_WIDTH/2 + 40/2) * TileWidth; + SDL_RenderFillRect(ren, &rect); + + #ifdef CHEAT_OMNISCIENCE_RGBA_AROUND_NORMAAL_VIEWPORT + // draw gray rectangle around inside area + rect.x = (WINDOW_WIDTH/2 - 40/2) * TileWidth - 1; rect.y = (VIEWPORT_ROW + VIEWPORT_HEIGHT/2 - (25 - VIEWPORT_ROW)/2 ) * TileHeight - 1; + rect.w = ( 80/2) * TileWidth + 2; rect.h = ( (25 - VIEWPORT_ROW)/2*2) * TileHeight + 2; + SDL_SetRenderDrawColor(ren, CHEAT_OMNISCIENCE_RGBA_AROUND_NORMAAL_VIEWPORT); + SDL_RenderDrawRect(ren, &rect); + #endif CHEAT_OMNISCIENCE_RGBA_AROUND_NORMAAL_VIEWPORT + + SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_NONE); + } +#endif + + if (OutputCursorVisible && SDL_GetTicks() % 500 < 250) + { + SDL_Color c = ScreenColors[OutputTextColor]; + SDL_SetRenderDrawColor(ren, c.r, c.g, c.b, c.a); + SDL_Rect rect = { (int)OutputCursorX * TileWidth, (int)OutputCursorY * TileHeight, TileWidth, TileHeight }; + SDL_RenderFillRect(ren, &rect); + } + +#if 0 + if (screenshot_filename) + { + SDL_Surface *infoSurface = SDL_GetWindowSurface(win); + unsigned char *pixels = new BYTE [infoSurface->w * infoSurface->h * infoSurface->format->BytesPerPixel]; + SDL_RenderReadPixels(ren, &infoSurface->clip_rect, infoSurface->format->format, pixels, infoSurface->w * infoSurface->format->BytesPerPixel); + SDL_Surface *saveSurface = SDL_CreateRGBSurfaceFrom(pixels, infoSurface->w, infoSurface->h, infoSurface->format->BitsPerPixel, infoSurface->w * infoSurface->format->BytesPerPixel, infoSurface->format->Rmask, infoSurface->format->Gmask, infoSurface->format->Bmask, infoSurface->format->Amask); + SDL_SaveBMP(saveSurface, screenshot_filename); + SDL_FreeSurface(saveSurface); + screenshot_filename = NULL; + } +#endif + SDL_RenderPresent(ren); + + SleepTimeslice(); + } + + DestroyGlyphs(); + + SDL_DestroyRenderer(ren); + SDL_DestroyWindow(win); + TTF_CloseFont(font); + TTF_Quit(); + + return 0; +} + +static SDL_Thread *ConsoleThread; + +int OpenConsole() +{ + Exiting = false; + + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) + { + fprintf(stderr, "SDL_Init: %s\n", SDL_GetError()); + return 1; + } + + // For macOS, we'll initialize everything on the main thread + ScreenMutex = SDL_CreateMutex(); + + // On macOS, the console window will be created when we run the main loop + return 0; +} + +void CloseConsole() +{ + Exiting = true; + SDL_DestroyMutex(ScreenMutex); + SDL_Quit(); +} + +// For macOS - run the SDL window creation and event loop on main thread +SDL_Window* global_window = NULL; +SDL_Renderer* global_renderer = NULL; +TTF_Font* global_font = NULL; + +int CreateSDLWindow() +{ + SDL_Rect usableRect; + if (SDL_GetDisplayUsableBounds(0, &usableRect) != 0) + { + fprintf(stderr, "SDL_GetDisplayUsableBounds: %s\n", SDL_GetError()); + return 1; + } + +#ifdef TILE_HEIGHT + if (TILE_HEIGHT * WINDOW_HEIGHT > usableRect.h) + { + fprintf(stderr, "Error: Configured tile height (%d) exceeds usable height (%d)\n", TILE_HEIGHT, usableRect.h); + return 1; + } + TileHeight = TILE_HEIGHT; +#else + TileHeight = usableRect.h / WINDOW_HEIGHT; +#endif + +#ifndef TILE_WIDTH + TileWidth = ((TileHeight*3+2)/4); +#else + TileWidth = TILE_WIDTH; +#endif +#ifndef FONT_SIZE + FontSize = TileHeight; +#else + FontSize = FONT_SIZE; +#endif + + int top, left, bottom, right, totalWidth, totalHeight; + for (Uint i=0; i<2; i++) + { + global_window = SDL_CreateWindow("Snipes", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + WINDOW_WIDTH * TileWidth, WINDOW_HEIGHT * TileHeight, + SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); + if (!global_window) + { + fprintf(stderr, "SDL_CreateWindow: %s\n", SDL_GetError()); + return 1; + } + SDL_GetWindowBordersSize(global_window, &top, &left, &bottom, &right); + totalHeight = WINDOW_HEIGHT * TileHeight + (top + bottom); +#ifdef TILE_HEIGHT + break; +#else + if (i || totalHeight <= usableRect.h + ALLOWABLE_BORDER_EXCESS) + break; + TileHeight = (usableRect.h - (top + bottom)) / WINDOW_HEIGHT; + TileWidth = ((TileHeight*3+2)/4); + FontSize = TileHeight; + SDL_DestroyWindow(global_window); +#endif + } + totalWidth = WINDOW_WIDTH * TileWidth + (left + right); + SDL_SetWindowPosition(global_window, (usableRect.w - totalWidth) / 2 + left, (usableRect.h - totalHeight) / 2 + top); + + if (TTF_Init() != 0) + { + fprintf(stderr, "TTF_Init: %s\n", SDL_GetError()); + SDL_DestroyWindow(global_window); + return 1; + } + + global_font = LoadFontWithFallbacks(FontSize); + if (!global_font) + { + fprintf(stderr, "TTF_OpenFont: Could not load any fonts\n"); + TTF_Quit(); + SDL_DestroyWindow(global_window); + return 1; + } + TTF_SetFontHinting(global_font, TTF_HINTING_MONO); + + global_renderer = SDL_CreateRenderer(global_window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!global_renderer) + { + fprintf(stderr, "SDL_CreateRenderer: %s\n", SDL_GetError()); + TTF_CloseFont(global_font); + TTF_Quit(); + SDL_DestroyWindow(global_window); + return 1; + } + + ClearGlyphs(); + return 0; +} + +void ProcessSDLEvents() +{ + SDL_Event e; + while (SDL_PollEvent(&e)) + { + switch (e.type) + { + case SDL_QUIT: + instant_quit = forfeit_match = got_ctrl_break = true; + break; + case SDL_KEYDOWN: + case SDL_KEYUP: + HandleKey((SDL_KeyboardEvent*)&e); + break; + case SDL_TEXTINPUT: + for (const char* s = e.text.text; *s; s++) + { + InputBuffer[InputBufferWriteIndex] = *s; + InputBufferWriteIndex = (InputBufferWriteIndex+1) % InputBufferSize; + } + break; + case SDL_WINDOWEVENT: + switch (e.window.event) + { + case SDL_WINDOWEVENT_SIZE_CHANGED: + { + TileWidth = e.window.data1 / WINDOW_WIDTH; + TileHeight = e.window.data2 / WINDOW_HEIGHT; + if (TileWidth > TileHeight) + { + TileWidth = TileHeight; + } + else + { + if (TileWidth != TileHeight) + { + TileHeight = ((TileWidth*8+3)/6); + } + } + + FontSize = TileHeight; + DestroyGlyphs(); + ClearGlyphs(); + + TTF_Font* newFont = LoadFontWithFallbacks(FontSize); + if (!newFont) + { + fprintf(stderr, "TTF_OpenFont: Could not load any fonts\n"); + instant_quit = forfeit_match = got_ctrl_break = true; + } + else + { + TTF_CloseFont(global_font); + global_font = newFont; + } + + SDL_SetWindowSize(global_window, WINDOW_WIDTH * TileWidth, WINDOW_HEIGHT * TileHeight); + } + break; + } + break; + } + } +} + +void RenderFrame() +{ + SDL_SetRenderDrawColor(global_renderer, 0, 0, 0, 255); + SDL_RenderClear(global_renderer); + + SDL_LockMutex(ScreenMutex); + for (Uint y = 0; y < WINDOW_HEIGHT; y++) + for (Uint x = 0; x < WINDOW_WIDTH; x++) + RenderCharacterAt(global_renderer, global_font, x, y); + SDL_UnlockMutex(ScreenMutex); + + if (OutputCursorVisible && SDL_GetTicks() % 500 < 250) + { + SDL_Color c = ScreenColors[OutputTextColor]; + SDL_SetRenderDrawColor(global_renderer, c.r, c.g, c.b, c.a); + SDL_Rect rect = { (int)OutputCursorX * TileWidth, (int)OutputCursorY * TileHeight, TileWidth, TileHeight }; + SDL_RenderFillRect(global_renderer, &rect); + } + + SDL_RenderPresent(global_renderer); +} + +void CleanupSDL() +{ + DestroyGlyphs(); + if (global_renderer) SDL_DestroyRenderer(global_renderer); + if (global_font) TTF_CloseFont(global_font); + TTF_Quit(); + if (global_window) SDL_DestroyWindow(global_window); +} diff --git a/sdl/keyboard.cpp b/sdl/keyboard.cpp index 1c140bb..cf834b5 100644 --- a/sdl/keyboard.cpp +++ b/sdl/keyboard.cpp @@ -1,171 +1,174 @@ -#include -#include -#include "../keyboard.h" -#include "../timer.h" -#include "../Snipes.h" -#include "sdl.h" - -std::map keyState; -#ifdef USE_SCANCODES_FOR_LETTER_KEYS -BYTE keyscanState[SDL_NUM_SCANCODES]; -#endif -Uint16 modifierState; -static bool anyKeyPressed = false; - -void ClearKeyboard() -{ - keyState.clear(); -#ifdef USE_SCANCODES_FOR_LETTER_KEYS - memset(keyscanState, 0, sizeof(keyscanState)); -#endif - modifierState = 0; - InputBufferReadIndex = InputBufferWriteIndex = 0; -} - -extern bool Paused; - -Uint PollKeyboard() -{ - while (Paused) - SleepTimeslice(); - - Uint state = 0; - if (keyState[SDLK_RIGHT]) state |= KEYSTATE_MOVE_RIGHT; - if (keyState[SDLK_LEFT ]) state |= KEYSTATE_MOVE_LEFT; - if (keyState[SDLK_DOWN ]) state |= KEYSTATE_MOVE_DOWN; - if (keyState[SDLK_UP ]) state |= KEYSTATE_MOVE_UP; - if (!(modifierState & KMOD_NUM)) - { - if (keyState[SDLK_KP_6 ]) state |= KEYSTATE_MOVE_RIGHT; - if (keyState[SDLK_KP_4 ]) state |= KEYSTATE_MOVE_LEFT; - if (keyState[SDLK_KP_5 ]) state |= KEYSTATE_MOVE_DOWN; // unfortunately, SDL does not differentiate between NumPad5, and the middle key on a non-inverted-T cursor pad, so this could be either one - if (keyState[SDLK_KP_2 ]) state |= KEYSTATE_MOVE_DOWN; - if (keyState[SDLK_KP_8 ]) state |= KEYSTATE_MOVE_UP; - } -#ifndef USE_SCANCODES_FOR_LETTER_KEYS - if (keyState[SDLK_d ]) state |= KEYSTATE_FIRE_RIGHT; - if (keyState[SDLK_a ]) state |= KEYSTATE_FIRE_LEFT; - if (keyState[SDLK_s ]) state |= KEYSTATE_FIRE_DOWN; - if (keyState[SDLK_x ]) state |= KEYSTATE_FIRE_DOWN; - if (keyState[SDLK_w ]) state |= KEYSTATE_FIRE_UP; -#else - if (keyscanState[SDL_SCANCODE_D ]) state |= KEYSTATE_FIRE_RIGHT; - if (keyscanState[SDL_SCANCODE_A ]) state |= KEYSTATE_FIRE_LEFT; - if (keyscanState[SDL_SCANCODE_S ]) state |= KEYSTATE_FIRE_DOWN; - if (keyscanState[SDL_SCANCODE_X ]) state |= KEYSTATE_FIRE_DOWN; - if (keyscanState[SDL_SCANCODE_W ]) state |= KEYSTATE_FIRE_UP; -#endif - spacebar_state = keyState[SDLK_SPACE]; - if ((state & (KEYSTATE_MOVE_RIGHT | KEYSTATE_MOVE_LEFT)) == (KEYSTATE_MOVE_RIGHT | KEYSTATE_MOVE_LEFT) || - (state & (KEYSTATE_MOVE_DOWN | KEYSTATE_MOVE_UP )) == (KEYSTATE_MOVE_DOWN | KEYSTATE_MOVE_UP )) - state &= ~(KEYSTATE_MOVE_RIGHT | KEYSTATE_MOVE_LEFT | KEYSTATE_MOVE_DOWN | KEYSTATE_MOVE_UP); - if ((state & (KEYSTATE_FIRE_RIGHT | KEYSTATE_FIRE_LEFT)) == (KEYSTATE_FIRE_RIGHT | KEYSTATE_FIRE_LEFT) || - (state & (KEYSTATE_FIRE_DOWN | KEYSTATE_FIRE_UP )) == (KEYSTATE_FIRE_DOWN | KEYSTATE_FIRE_UP )) - state &= ~(KEYSTATE_FIRE_RIGHT | KEYSTATE_FIRE_LEFT | KEYSTATE_FIRE_DOWN | KEYSTATE_FIRE_UP); - fast_forward = keyState[SDLK_f]; - return state; -} - -void WaitForKeyPress() -{ - anyKeyPressed = false; - while (!anyKeyPressed) - SleepTimeslice(); -} - -void HandleKey(SDL_KeyboardEvent* e) -{ - modifierState = e->keysym.mod; - if (e->type == SDL_KEYDOWN) - { - if (!keyState[e->keysym.sym]) - { - switch (e->keysym.sym) - { - case SDLK_LCTRL: - case SDLK_LSHIFT: - case SDLK_LALT: - case SDLK_LGUI: - case SDLK_RCTRL: - case SDLK_RSHIFT: - case SDLK_RALT: - case SDLK_RGUI: - break; - default: - anyKeyPressed = true; - break; - } - } - - keyState[e->keysym.sym] |= 1; -#ifdef USE_SCANCODES_FOR_LETTER_KEYS - keyscanState[e->keysym.scancode] |= 1; -#endif - - if (e->keysym.sym == SDLK_F1) - sound_enabled ^= true; - else - if (e->keysym.sym == SDLK_F2) - shooting_sound_enabled ^= true; - else - if (e->keysym.sym == SDLK_ESCAPE) - forfeit_match = true; - else - if (e->keysym.sym == SDLK_c || e->keysym.sym == SDLK_SCROLLLOCK) // SDL does not differentiate between Ctrl+ScrollLock and Ctrl+Pause - { - if ((e->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL)) && !(e->keysym.mod & (KMOD_LALT | KMOD_RALT))) - forfeit_match = true; - } -#ifdef CHEAT - else - if (e->keysym.sym == SDLK_t && (e->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL))) - rerecordingMode = true; - else - if (e->keysym.sym == SDLK_PERIOD) - single_step++; - else - if (e->keysym.sym == SDLK_COMMA) - step_backwards++; - else - if (e->keysym.sym == SDLK_KP_PLUS) - { - if (frame == 1 && rerecordingMode) - increment_initial_seed++; - } - else - if (e->keysym.sym == SDLK_KP_MINUS) - { - if (frame == 1 && rerecordingMode) - increment_initial_seed--; - } -#endif - else - if (e->keysym.sym == SDLK_RETURN) - { - InputBuffer[InputBufferWriteIndex] = '\n'; - InputBufferWriteIndex = (InputBufferWriteIndex+1) % InputBufferSize; - } - else - if (e->keysym.sym == SDLK_BACKSPACE) - { - InputBuffer[InputBufferWriteIndex] = '\b'; - InputBufferWriteIndex = (InputBufferWriteIndex+1) % InputBufferSize; - } - } - else - { - keyState[e->keysym.sym] &= ~1; -#ifdef USE_SCANCODES_FOR_LETTER_KEYS - keyscanState[e->keysym.scancode] &= ~1; -#endif - } -} - -int OpenKeyboard() -{ - ClearKeyboard(); - return 0; -} -void CloseKeyboard() -{ -} +#include +#ifdef __APPLE__ +#define SDL_MAIN_HANDLED +#endif +#include +#include "../keyboard.h" +#include "../timer.h" +#include "../Snipes.h" +#include "sdl.h" + +std::map keyState; +#ifdef USE_SCANCODES_FOR_LETTER_KEYS +BYTE keyscanState[SDL_NUM_SCANCODES]; +#endif +Uint16 modifierState; +static bool anyKeyPressed = false; + +void ClearKeyboard() +{ + keyState.clear(); +#ifdef USE_SCANCODES_FOR_LETTER_KEYS + memset(keyscanState, 0, sizeof(keyscanState)); +#endif + modifierState = 0; + InputBufferReadIndex = InputBufferWriteIndex = 0; +} + +extern bool Paused; + +Uint PollKeyboard() +{ + while (Paused) + SleepTimeslice(); + + Uint state = 0; + if (keyState[SDLK_RIGHT]) state |= KEYSTATE_MOVE_RIGHT; + if (keyState[SDLK_LEFT ]) state |= KEYSTATE_MOVE_LEFT; + if (keyState[SDLK_DOWN ]) state |= KEYSTATE_MOVE_DOWN; + if (keyState[SDLK_UP ]) state |= KEYSTATE_MOVE_UP; + if (!(modifierState & KMOD_NUM)) + { + if (keyState[SDLK_KP_6 ]) state |= KEYSTATE_MOVE_RIGHT; + if (keyState[SDLK_KP_4 ]) state |= KEYSTATE_MOVE_LEFT; + if (keyState[SDLK_KP_5 ]) state |= KEYSTATE_MOVE_DOWN; // unfortunately, SDL does not differentiate between NumPad5, and the middle key on a non-inverted-T cursor pad, so this could be either one + if (keyState[SDLK_KP_2 ]) state |= KEYSTATE_MOVE_DOWN; + if (keyState[SDLK_KP_8 ]) state |= KEYSTATE_MOVE_UP; + } +#ifndef USE_SCANCODES_FOR_LETTER_KEYS + if (keyState[SDLK_d ]) state |= KEYSTATE_FIRE_RIGHT; + if (keyState[SDLK_a ]) state |= KEYSTATE_FIRE_LEFT; + if (keyState[SDLK_s ]) state |= KEYSTATE_FIRE_DOWN; + if (keyState[SDLK_x ]) state |= KEYSTATE_FIRE_DOWN; + if (keyState[SDLK_w ]) state |= KEYSTATE_FIRE_UP; +#else + if (keyscanState[SDL_SCANCODE_D ]) state |= KEYSTATE_FIRE_RIGHT; + if (keyscanState[SDL_SCANCODE_A ]) state |= KEYSTATE_FIRE_LEFT; + if (keyscanState[SDL_SCANCODE_S ]) state |= KEYSTATE_FIRE_DOWN; + if (keyscanState[SDL_SCANCODE_X ]) state |= KEYSTATE_FIRE_DOWN; + if (keyscanState[SDL_SCANCODE_W ]) state |= KEYSTATE_FIRE_UP; +#endif + spacebar_state = keyState[SDLK_SPACE]; + if ((state & (KEYSTATE_MOVE_RIGHT | KEYSTATE_MOVE_LEFT)) == (KEYSTATE_MOVE_RIGHT | KEYSTATE_MOVE_LEFT) || + (state & (KEYSTATE_MOVE_DOWN | KEYSTATE_MOVE_UP )) == (KEYSTATE_MOVE_DOWN | KEYSTATE_MOVE_UP )) + state &= ~(KEYSTATE_MOVE_RIGHT | KEYSTATE_MOVE_LEFT | KEYSTATE_MOVE_DOWN | KEYSTATE_MOVE_UP); + if ((state & (KEYSTATE_FIRE_RIGHT | KEYSTATE_FIRE_LEFT)) == (KEYSTATE_FIRE_RIGHT | KEYSTATE_FIRE_LEFT) || + (state & (KEYSTATE_FIRE_DOWN | KEYSTATE_FIRE_UP )) == (KEYSTATE_FIRE_DOWN | KEYSTATE_FIRE_UP )) + state &= ~(KEYSTATE_FIRE_RIGHT | KEYSTATE_FIRE_LEFT | KEYSTATE_FIRE_DOWN | KEYSTATE_FIRE_UP); + fast_forward = keyState[SDLK_f]; + return state; +} + +void WaitForKeyPress() +{ + anyKeyPressed = false; + while (!anyKeyPressed) + SleepTimeslice(); +} + +void HandleKey(SDL_KeyboardEvent* e) +{ + modifierState = e->keysym.mod; + if (e->type == SDL_KEYDOWN) + { + if (!keyState[e->keysym.sym]) + { + switch (e->keysym.sym) + { + case SDLK_LCTRL: + case SDLK_LSHIFT: + case SDLK_LALT: + case SDLK_LGUI: + case SDLK_RCTRL: + case SDLK_RSHIFT: + case SDLK_RALT: + case SDLK_RGUI: + break; + default: + anyKeyPressed = true; + break; + } + } + + keyState[e->keysym.sym] |= 1; +#ifdef USE_SCANCODES_FOR_LETTER_KEYS + keyscanState[e->keysym.scancode] |= 1; +#endif + + if (e->keysym.sym == SDLK_F1) + sound_enabled ^= true; + else + if (e->keysym.sym == SDLK_F2) + shooting_sound_enabled ^= true; + else + if (e->keysym.sym == SDLK_ESCAPE) + forfeit_match = true; + else + if (e->keysym.sym == SDLK_c || e->keysym.sym == SDLK_SCROLLLOCK) // SDL does not differentiate between Ctrl+ScrollLock and Ctrl+Pause + { + if ((e->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL)) && !(e->keysym.mod & (KMOD_LALT | KMOD_RALT))) + forfeit_match = true; + } +#ifdef CHEAT + else + if (e->keysym.sym == SDLK_t && (e->keysym.mod & (KMOD_LCTRL | KMOD_RCTRL))) + rerecordingMode = true; + else + if (e->keysym.sym == SDLK_PERIOD) + single_step++; + else + if (e->keysym.sym == SDLK_COMMA) + step_backwards++; + else + if (e->keysym.sym == SDLK_KP_PLUS) + { + if (frame == 1 && rerecordingMode) + increment_initial_seed++; + } + else + if (e->keysym.sym == SDLK_KP_MINUS) + { + if (frame == 1 && rerecordingMode) + increment_initial_seed--; + } +#endif + else + if (e->keysym.sym == SDLK_RETURN) + { + InputBuffer[InputBufferWriteIndex] = '\n'; + InputBufferWriteIndex = (InputBufferWriteIndex+1) % InputBufferSize; + } + else + if (e->keysym.sym == SDLK_BACKSPACE) + { + InputBuffer[InputBufferWriteIndex] = '\b'; + InputBufferWriteIndex = (InputBufferWriteIndex+1) % InputBufferSize; + } + } + else + { + keyState[e->keysym.sym] &= ~1; +#ifdef USE_SCANCODES_FOR_LETTER_KEYS + keyscanState[e->keysym.scancode] &= ~1; +#endif + } +} + +int OpenKeyboard() +{ + ClearKeyboard(); + return 0; +} +void CloseKeyboard() +{ +} diff --git a/sdl/sound.cpp b/sdl/sound.cpp index 45d1c29..9068c75 100644 --- a/sdl/sound.cpp +++ b/sdl/sound.cpp @@ -1,82 +1,85 @@ -#include "../config.h" -#include "../sound.h" -#include "../platform.h" - -#include -#include -#include - -#define TONE_SAMPLE_RATE 48000 -#define WAVE_BUFFER_LENGTH 512 - -double toneFreq; -int currentFreqnum = 0; -Uint tonePhase; - -#ifdef STOP_WAVE_OUT_DURING_SILENCE -#error STOP_WAVE_OUT_DURING_SILENCE not supported in SDL build -#endif - -void SDLCALL SoundCallback(void *, Uint8 *_stream, int _length) -{ - Sint16 *stream = (Sint16*) _stream; - Uint length = _length / 2; - - if (currentFreqnum == 0) - for (Uint i=0; i +#include +#ifdef __APPLE__ +#define SDL_MAIN_HANDLED +#endif +#include + +#define TONE_SAMPLE_RATE 48000 +#define WAVE_BUFFER_LENGTH 512 + +double toneFreq; +int currentFreqnum = 0; +Uint tonePhase; + +#ifdef STOP_WAVE_OUT_DURING_SILENCE +#error STOP_WAVE_OUT_DURING_SILENCE not supported in SDL build +#endif + +void SDLCALL SoundCallback(void *, Uint8 *_stream, int _length) +{ + Sint16 *stream = (Sint16*) _stream; + Uint length = _length / 2; + + if (currentFreqnum == 0) + for (Uint i=0; i - -QWORD perf_freq; - -WORD GetTickCountWord() -{ - return (WORD)(SDL_GetTicks() * 13125uLL / (11*65535)); -} - -int OpenTimer() -{ - return 0; -} -void CloseTimer() -{ -} - -void SleepTimeslice() -{ - SDL_Delay(1); -} +#include "../timer.h" + +#ifdef __APPLE__ +#define SDL_MAIN_HANDLED +#endif +#include + +QWORD perf_freq; + +WORD GetTickCountWord() +{ + return (WORD)(SDL_GetTicks() * 13125uLL / (11*65535)); +} + +int OpenTimer() +{ + return 0; +} +void CloseTimer() +{ +} + +void SleepTimeslice() +{ + SDL_Delay(1); +} From 8c42e239225a191832bd917ce16ca68015095e96 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 04:02:27 +0000 Subject: [PATCH 2/2] Address review feedback - Replace CLAUDE.md with AGENTS.md to be agent-agnostic - Remove duplicated documentation from AGENTS.md - Make font fallback warnings more explicit - Clarify that custom font is required for authentic visuals Co-authored-by: Code Arranger --- AGENTS.md | 40 +++++++++++++++++++ CLAUDE.md | 102 ------------------------------------------------ README.md | 2 +- sdl/console.cpp | 11 ++++-- 4 files changed, 48 insertions(+), 107 deletions(-) create mode 100644 AGENTS.md delete mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0d9af1b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# AGENTS.md + +This file provides additional technical guidance for AI agents and automated tools when working with the Snipes codebase. + +## Quick Reference for Agents + +### Code Style Guidelines +- Follow existing code conventions in each file +- Use tabs for indentation in C++ files +- Maintain 80-character line width where practical +- Preserve original game logic exactly when making fixes + +### Platform-Specific Considerations + +#### macOS +- SDL operations must run on the main thread +- Use `SDL_MAIN_HANDLED` macro +- Font fallback system searches system fonts if custom font unavailable + +#### Windows +- Both SDL and console builds must remain functional +- Visual Studio solution supports multiple configurations +- Console build uses Windows-specific APIs in `windows/` directory + +#### Linux/POSIX +- SDL build is primary target +- Ensure POSIX compliance for portability +- Test with various distributions when possible + +### Testing Guidelines +- Preserve 100% game logic compatibility with original +- Test both windowed and fullscreen modes +- Verify replay file compatibility +- Check keyboard input with different layouts + +### Common Pitfalls to Avoid +- Don't modify core game logic in Snipes.cpp unless fixing documented bugs +- Maintain platform abstraction layer boundaries +- Keep SDL-specific code in `sdl/` directory +- Don't break Windows console build when adding SDL features \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index a5f231f..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,102 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -This is a modern C++ port of the classic 1982 text-mode game Snipes. The code has been reverse-engineered from the original DOS executable with 100% identical game logic. The project supports both SDL graphics and Windows console builds. - -## Build Commands - -### SDL Build (Default) -```bash -make -``` -This creates an SDL build requiring SDL2 and SDL2_ttf libraries. The build will automatically create `config.h` from `config-sample.h` if it doesn't exist. - -### Clean Build -```bash -make clean -``` - -### Visual Studio Build -1. Copy `config-sample.h` to `config.h` -2. Open `Snipes.sln` in Visual Studio -3. Build using the IDE (supports both SDL and Windows console configurations) - -## Configuration - -- **config.h**: Main configuration file (auto-created from `config-sample.h`) -- Key configuration options include: - - `USE_SCANCODES_FOR_LETTER_KEYS`: Useful for non-QWERTY keyboards - - `CHEAT_OMNISCIENCE`: Debug mode showing full maze - - Font and display settings for SDL builds - - Various bug fixes and compatibility options - -## Architecture - -### Core Structure -- **Snipes.cpp/h**: Main game logic and entry point -- **Platform abstraction**: Separate implementations for SDL (`sdl/`) and Windows (`windows/`) -- **Game components**: console, keyboard, sound, timer modules with platform-specific implementations - -### Key Constants -- Maze dimensions: 16x20 cells (128x120 pixels) -- Each cell: 8x6 pixels -- Viewport: 40x25 characters (except in omniscience mode) - -### Platform Abstraction -The game uses a clean platform abstraction layer: -- **SDL version**: Modern graphics with TTF fonts -- **Windows version**: Native console output -- Common interface through header files: `console.h`, `keyboard.h`, `sound.h`, `timer.h` - -## Dependencies - -### SDL Build -- SDL2 -- SDL2_ttf -- Custom font file: `SnipesConsole.ttf` (loaded at startup) - -### Development -- C++11 compatible compiler -- GNU Make or Visual Studio 2017+ - -## Replay System - -The game automatically records replay files with `.SnipesGame` extension. To play back: -```bash -./snipes "2016-07-08 09.10.11.SnipesGame" -``` - -## macOS Status - -✅ **Fully working**: The SDL version now runs correctly on macOS with all threading issues resolved. - -**Features**: -- ✅ Application builds and runs successfully on macOS -- ✅ SDL window creation and event handling run properly on the main thread -- ✅ Font fallback system automatically uses system fonts (Courier.ttc, Menlo.ttc, etc.) -- ✅ Core game logic is fully functional -- ✅ All rendering and input handling work correctly - -**Changes made for macOS compatibility**: -- SDL operations (window creation, event polling, rendering) now run on the main thread -- Added `SDL_MAIN_HANDLED` macro for macOS builds -- Implemented proper event processing in the main game loop -- Font loading includes automatic fallbacks to system fonts when custom font is unavailable - -### Getting the Custom Font - -The game expects `SnipesConsole.ttf` in the current directory. You can: -1. Download it from: http://kingbird.myphotos.cc/ee22d44076adb8a34d8e20df4be3730a/SnipesConsole.ttf -2. Or let it use system fallback fonts (Courier, Menlo, etc.) - -## Build Flags - -The GNUmakefile includes: -- `-std=c++11`: C++11 standard -- `-fstack-protector`: Security hardening -- Optional maintainer flags: `-Werror -Wall -Wextra` (enabled with `MAINT=1`) -- Optimized builds use `-O3` -- Debug builds can use `-Og -g -fsanitize=address -fsanitize=undefined` \ No newline at end of file diff --git a/README.md b/README.md index 734772d..5171888 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ For more information, see the [vogons.org forum thread](https://www.vogons.org/v SDL builds require the SDL2 and SDL2_ttf libraries. -On startup, the SDL build will attempt to load a custom font, which can be obtained separately from [here](http://kingbird.myphotos.cc/ee22d44076adb8a34d8e20df4be3730a/SnipesConsole.ttf). On macOS, if the custom font is not available, the game will automatically fall back to system fonts (Courier, Menlo, etc.). +On startup, the SDL build will attempt to load a custom font, which can be obtained separately from [here](http://kingbird.myphotos.cc/ee22d44076adb8a34d8e20df4be3730a/SnipesConsole.ttf). The custom font is required for authentic visuals. If unavailable, the game will fall back to system fonts with a warning that visuals will differ from the original. #### With Visual Studio diff --git a/sdl/console.cpp b/sdl/console.cpp index a6aacb3..eefd6b8 100644 --- a/sdl/console.cpp +++ b/sdl/console.cpp @@ -444,22 +444,25 @@ TTF_Font* LoadFontWithFallbacks(int size) TTF_Font* font = TTF_OpenFont(FONT_FILENAME, size); if (!font) { - fprintf(stderr, "Warning: Could not load %s, trying fallback fonts...\n", FONT_FILENAME); + fprintf(stderr, "WARNING: Could not load %s\n", FONT_FILENAME); + fprintf(stderr, "The custom font is required for authentic visuals.\n"); + fprintf(stderr, "Download from: http://kingbird.myphotos.cc/ee22d44076adb8a34d8e20df4be3730a/SnipesConsole.ttf\n"); + fprintf(stderr, "Attempting to use system fallback fonts (visuals may differ)...\n"); // Try common system monospace fonts as fallbacks const char* fallback_fonts[] = { "/System/Library/Fonts/Courier.ttc", - "/System/Library/Fonts/SFNSMono.ttf", + "/System/Library/Fonts/SFNSMono.ttf", "/System/Library/Fonts/Menlo.ttc", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", // Linux NULL }; - + for (int i = 0; fallback_fonts[i] != NULL; i++) { font = TTF_OpenFont(fallback_fonts[i], size); if (font) { - fprintf(stderr, "Using fallback font: %s\n", fallback_fonts[i]); + fprintf(stderr, "Using fallback font: %s (NOTICE: Game visuals will differ from original)\n", fallback_fonts[i]); break; } }