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/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/README.md b/README.md index 0e6d85d..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 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 @@ -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..eefd6b8 100644 --- a/sdl/console.cpp +++ b/sdl/console.cpp @@ -1,701 +1,926 @@ -#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\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/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 (NOTICE: Game visuals will differ from original)\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); +}