shithub: candycrisis

ref: 56f1e92bcab1dad60dad8791428b84a0a4c0453b
dir: /src/main.c/

View raw version
// main.c

//
//                    CANDY CRISIS 32-BIT - OS X and Windows
//
// FEATURES:
// • Runs natively on SDL 2.0, for both Intel OS X and Windows. Targeting
//   popular Windows installs in 2015 (i.e. Win7/8/10).
// • Now runs in 32-bit color.
//
// ETC:
// • Codebase should once again be almost entirely platform-neutral.
//

//
//                    CANDY CRISIS SDL 2.0
//
// FEATURES:
// • Runs natively on SDL 2.0 and Intel OS X. Targeting modern OS X,
//   circa 2015 (e.g. OS X 10.10).
//
// ETC:
// • Removed registration.
// • Resources moved into app bundle.
// • Migrated to SDL 2 and SDL_image 2 (for graphics--also removes
//   subdependencies on libjpeg/libpng)
//

//
//                      CANDY CRISIS SDL
//
// FEATURES:
// • Runs natively on any platform with support for SDL. Developed
//   primarily around the lame Mac Carbon SDL. Can compile and run
//   with gcc/mingw32 on Windows and gcc on Linux.
//
// ETC:
// • Game functionality unchanged.
// • Removed all Mac OS-specific concepts from the game, i.e.:
//   - GWorlds replaced with SDL_Surfaces.
//   - Rects and Points replaced with MRects and MPoints. (same contents)
// • All resources moved into external files in a folder called
//   "CandyCrisisResources." Graphics are all JPG and PNG, opened
//   with SDL_image. Sounds are WAV. Music is still MOD type.
// • Using the following Open Source projects:
//   SDL, SDL_image (for graphics)
//   libjpeg (used by SDL_image)
//   libpng (used by SDL_image)
//   zlib (used by libpng)
// • Using fmod for sound; replaces MikMod
// • Wrote a utility library, SDLU, to pick up slack in the SDL
//   implementation and to help SDL mesh with a Mac-centric universe.
// • New registration code algorithm based on a fast string hash.
//

//
//                      CANDY CRISIS X
//
// FEATURES:
// • Runs natively on Mac OS X. Developed around Mac OS X
// Public Beta 1H39 and 2E14.
// Updated 3/25/2001 for Mac OS X 4K78--OS X 10.0.
// Updated 8/25/2001 for Mac OS X 10.0.4.
//
// ETC:
// • Game functionality unchanged.
// • Zerius Sound System scrapped, replaced with LibMikMod. I'm
// very unhappy with performance relative to ZSS, but on G3s and up,
// performance should not be a major concern. If only I could get
// the source to ZSS so it could be Carbonized! 
// • Everything runs in one window now, instead of having
// one window per interface element. This was necessary 
// because OS X wanted to put drop shadows around everything
// and it looked pretty weird. This was also a personal pet
// peeve that I never had the motivation to fix until now.
// • Controls dialog is super Aqua savvy, using Theme Text 
// and Theme Buttons.
// • A couple of kludges added, to work around OS X bugs. Ugh.
// • Found bug which was causing blitter to draw larger dirty rects
// than necessary (top/left of dirty rect was always 0/0). Not sure 
// if it ever shipped like that or if this is something I changed 
// post-Candy Crisis 1.0.
// • Changed cursor management since OS X cursors don't know how to
// hide and show themselves properly.
//
// UNRESOLVED:
// • Stopped getting Out of Memory reports. I wonder if any of the
// Candy Crisis cleanups affected this...?
// • OS X displays a line of garbage when you try to put up a totally
// blank cursor. I'm not going to spend too long analyzing this; it's
// not my bug.
// 


//                    CANDY CRISIS 1.0 UPDATE
//
// FEATURES:
// • Rebranded "Candy Crisis" at the request of Mars Candy Co.
// Many, many graphical changes as a result. (New logo thanks 
// to Bob Frasure.)
// • "Controls" button in main menu per many user requests.
// • Slightly improved error reporting. 
// • Option-key at startup to turn on "allow background tasks" 
// or "don't change resolutions." (Don't change resolutions
// requires DrawSprocket 1.7.)
//
// NONCRITICAL:
// • Fixed bug in 2P mode where game would say "Player 1 got
//  best combo!" when Player 2 really got it, and vice versa. 
// • Changed Magic Skittle ratio to 1/19 instead of 1/17, after
// watching Brett get tons of Magic Skittles at work. Hmm.
// • Replaced RandomBefore with less hacked-up code, because
// Magic Skittles STILL seemed to be coming up more often than
// expected. That seemed to take care of it.
// • Fixed rare bug where, after losing, the game would sometimes
// get stuck until you explicitly chose "end game." (Would manifest
// more often on a slow computer and/or when Background Tasks were
// activated.)
//
// ETC:
// • Antipiracy measures.
// • Game fonts all loaded at startup time instead of dynamically,
// in an attempt to reduce the number of GWorlds which are created,
// then torn down, during the game (which could have been potentially
// fragmenting the heap, though I doubt this was a real problem).
//
// UNRESOLVED:
// • Still a handful of people who get Out of Memory when they 
// try to pause a game. Damn. Hopefully now I'll at least know
// where they're dying (though I highly suspect it's InitGWorld,
// which without a stack crawl is pretty much useless info...)
// One guy says this is fixed by deleting prefs. Huh??
//

//
//                     2.0.2 UPDATE
//
// FEATURES:
// • When you continue, your score is now rolled back to what
// it was when you first started the round. This prevents people
// from racking up high scores by continuing over and over again
// on the highest board.
// • You can now clear the high score tables to their default
// values by holding delete while clicking "high scores" on the
// main screen.
//
// ETC:
// • Added small picture to the controls dialog so people know
// which color is Player 1, and which is Player 2.
// • Holding option while warping causes a CPU/CPU match to
// occur.
// • Changed in-game registration URL to:
// http://emulation.net/s2.com/register.html
//
// CRITICAL:
// • Fixed minor memory corruption when a bomb hits floor or
// gray Skittle. Could potentially have corrupted 3 tiles of
// opponent's board.
// • Fixed bug where potential combo data would not be cleared
// when choosing "End Game" and then starting a new game, which
// led to really weird corruptions of potential combo data.
// • Fixed bug where holding down button after end-credits rolled
// would cause the pause dialog to pop up on a zero-gamma screen
// (whoops).
// 
// NONCRITICAL:
// • Fixed bug where dropping bomb would not display associated
// points.
// • Fixed bug where dropping bomb on floor/gray Skittle would
// reward the player for "killing" empty squares, making it
// score 100x(9-number of grays in 3x3 area) as opposed to
// the correct 100x(number of blobs in 3x3 area).
// • Occasionally, when Best Combo got corrupted, it would show
// the ending credits instead of displaying the Best Combo. Now
// there is code to ensure that the level # of the Best Combo 
// structure is in bounds. (If it isn't, the best combo is
// assumed to be corrupt, and it is deleted. Not the most 
// optimal solution, but what can I do?)
// • If you started the tutorial, ended it, then viewed the best
// combo, you'd see a speech balloon appear for one frame.
// • Fixed bug where bringing up InputSprocket dialog would 
// unload ics8's used to draw key caps (damn InputSprocket bugs).
// Does this only affect ISp < 1.7?
// • Fixed bug where bringing up InputSprocket dialog would not
// update game windows behind it after it got closed.
//
// UNRESOLVED:
// • Slow loading time issue seems to only be affecting an
// incredible minority of people. It's being caused by QuickTime
// decompressing JPEGs. I think it's not a Skittles 2 issue.
// • One guy says if he quits the game, reopens it, starts a game,
// then pauses it, he gets an Out of Memory condition. He's running
// 8.6-D clean. Hmm.
//

//
//                     2.0.1 UPDATE
//
// FEATURES:
// • Best Combo
// • New bg for level 8
// • New sfx for continue sound (requested by Nathan Lamont)
//
// ETC:
// • High score dialog enhanced to support Best Combo stuff
// • Tutorial suggests pressing esc to set up keys now
// • More aggressive AI for intellect>18 (does not percieve
// 1-level zap as advantageous)
//
// CRITICAL:
// • Fix for out-of-bounds array read (->crash) inside 
// ZapScoreDisplay.
// • Fixed bug where falling Skittles (in DropBlobs) would
// occasionally have their bottom half lopped off. Tough to
// see while in motion but totally obvious in screenshots.
// • Fixed InputSprocket icons in ISpConfigure dialog
// • Workaround for System 7 bug, where setting the 
// cursor while gamma is faded causes solid black cursor.
//
// NONCRITICAL:
// • Fixed bug where char fading on blobs that were
// in motion would cause crap to appear for one frame.
// Only apparent on slow Macs or in screenshots. Otherwise
// appeared as flicker and easily dismissed.
// • Cmd-tab inside High Scores or Game Over screen
// no longer leaves a white box (empty window) open
// and will not switch out with 0 gamma
// • Fixed DrawSprocket bug on Macs that can't do 640x480
// (i.e. PowerBook G3 is stuck at 1024x768). The window
// would be drawn in lower-right hand corner instead of
// centered.
// • Fixed bug in multipliers for getting multiple 
// colors at once. (Should have been *3/*6/*12/*24, was
// actually *3/*9/*21/*45! Ouch!)
// • Hitting cmd-Q at high score dialog would allow
// empty high score name to be added to high score table.
//
// UNRESOLVED:
// • A few users report very slow loading times between
// levels and during loading sequence. Problem tends to
// be alleviated by turning on VM. Can't reproduce here.
// 

//
//                 2.0.0 INITIAL RELEASE
//

#include "SDLU.h"
#include "version.h"

#include "main.h"

#include <limits.h>
#include <stdlib.h>

#include "hiscore.h"
#include "control.h"
#include "players.h"
#include "gworld.h"
#include "graphics.h"
#include "grays.h"
#include "soundfx.h"
#include "next.h"
#include "random.h"
#include "victory.h"
#include "score.h"
#include "graymonitor.h"
#include "music.h"
#include "gameticks.h"
#include "level.h"
#include "opponent.h"
#include "keyselect.h"
#include "blitter.h"
#include "prefs.h"
#include "tweak.h"
#include "zap.h"
#include "pause.h"
#include "tutorial.h"

#if __APPLE__
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <libproc.h>
#endif

SDL_Renderer* g_renderer;
SDL_Window*   g_window;
SDL_Texture*  g_windowTexture;
SDL_Surface*  g_frontSurface;
SDL_Rect      g_widescreenCrop = {0,60,640,360};
signed char  nextA[2], nextB[2], nextM[2], nextG[2], colorA[2], colorB[2],
	         blobX[2], blobY[2], blobR[2], blobSpin[2], speed[2], role[2], halfway[2],
	         control[2], dropping[2], magic[2], grenade[2], anim[2];
int          chain[2];
MTicks       blobTime[2], startTime, endTime;
MBoolean     finished = false, pauseKey = false, showStartMenu = true;
signed char  grid[2][kGridAcross][kGridDown], suction[2][kGridAcross][kGridDown], charred[2][kGridAcross][kGridDown], glow[2][kGridAcross][kGridDown];
MRect        playerWindowZRect, playerWindowRect[2];
MBoolean     playerWindowVisible[2] = { true, true };
KeyList      hitKey[2];
int          backgroundID = -1;
MPoint       blobWindow[8][2];
void         (*DoFullRepaint)() = NoPaint;
MBoolean     needsRefresh = false;

static char  candyCrisisResources[512];

MBoolean fullscreen = true;
MBoolean widescreen = true;
MBoolean crispUpscaling = false;

int main(int argc, char *argv[])
{
	(void) argc;
	(void) argv;

	Initialize( );

	LoadPrefs( );
	ReserveMonitor( );
	ShowTitle( );

	if ( GetCurrentMusic() != 13 )
		ChooseMusic( 13 );
	
	while (!finished)
	{
		if (showStartMenu)
		{
			GameStartMenu();
			showStartMenu = false;
		}
		
		if (finished)
            break;
        
        DoFullRepaint = NeedRefresh;
        CheckKeys( );
        HandlePlayers( );
        UpdateOpponent( );
        UpdateBalloon( );
        UpdateSound( );
        DoFullRepaint = NoPaint;
        
        if (needsRefresh)
        {
            RefreshAll();
            needsRefresh = false;
        }
        
        SDLU_Present();

        if (!showStartMenu && pauseKey)
        {
            FreezeGameTickCount( );
            PauseMusic( );
            MaskRect( &playerWindowRect[0] );
            MaskRect( &playerWindowRect[1] );
            WaitForRelease( );
            
            HandleDialog( kPauseDialog );
                            
            WaitForRelease( );
            RefreshPlayerWindow( 0 );
            RefreshPlayerWindow( 1 );
            ResumeMusic( );
            UnfreezeGameTickCount( );
		}
	}
	
	SavePrefs( );
	ReleaseMonitor( );

    ShutdownMusic( );
    ShutdownSound( );

	return 0;
}

void NoPaint( void )
{
}

void MaskRect( MRect *r )
{
	SDL_Rect sdlRect;
	SDLU_MRectToSDLRect( r, &sdlRect );
	SDLU_BlitFrontSurface( backdropSurface, &sdlRect, &sdlRect );
}

void RefreshPlayerWindow( short player )
{
	MRect fullUpdate = {0, 0, kGridDown * kBlobVertSize, kGridAcross * kBlobHorizSize };
	
	if( control[player] == kNobodyControl )
	{
		MaskRect( &playerWindowRect[player] );
	}
	else
	{
		SetUpdateRect( player, &fullUpdate );
		UpdatePlayerWindow( player );
	}
}

void NeedRefresh()
{
	needsRefresh = true;
}

void RefreshAll( void )
{	
	DrawBackdrop( );

	ShowGrayMonitor( 0 );
	ShowGrayMonitor( 1 );

	RefreshNext( 0 );
	RefreshNext( 1 );

	RefreshPlayerWindow( 0 );
	RefreshPlayerWindow( 1 );

	DrawFrozenOpponent( );
	DrawStage( );

	ShowScore( 0 );
	ShowScore( 1 );
}

void Error( const char* extra )
{
    char error[1024];

	SDL_snprintf(error, sizeof(error),
             "Sorry, a critical error has occurred. Please report the following error message:\n    %s", extra);

//    fprintf(stderr, "%s\n", error);

    SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Candy Crisis", error, NULL);

    abort();
}

void WaitForRelease( void )
{	
	do
	{
		SDLU_Yield();
	}
	while( AnyKeyIsPressed( ) || SDLU_Button() );
}

MBoolean AnyKeyIsPressed( void )
{
	int arraySize;
    const Uint8* pressedKeys;
    
    SDLU_PumpEvents();
    pressedKeys = SDL_GetKeyboardState( &arraySize );
    
	for (int index = 0; index < arraySize; index++)
	{
		if (pressedKeys[index] &&
		    index != SDL_SCANCODE_CAPSLOCK &&
            index != SDL_SCANCODE_NUMLOCKCLEAR &&
            index != SDL_SCANCODE_SCROLLLOCK)
        {
            return true;
        }
	}

    return false;
}

MBoolean ControlKeyIsPressed( void )
{
    int arraySize;
    const Uint8* pressedKeys;
    
    SDLU_PumpEvents();
    pressedKeys = SDL_GetKeyboardState( &arraySize );
    
	return pressedKeys[SDL_SCANCODE_LCTRL] || pressedKeys[SDL_SCANCODE_RCTRL];
}

MBoolean OptionKeyIsPressed( void )
{
    int arraySize;
    const Uint8* pressedKeys;
    
    SDLU_PumpEvents();
    pressedKeys = SDL_GetKeyboardState( &arraySize );
    
    return pressedKeys[SDL_SCANCODE_LALT] || pressedKeys[SDL_SCANCODE_RALT];
}

MBoolean DeleteKeyIsPressed( void )
{
    int arraySize;
    const Uint8* pressedKeys;
    
    SDLU_PumpEvents();
    pressedKeys = SDL_GetKeyboardState( &arraySize );
    
    return pressedKeys[SDL_SCANCODE_BACKSPACE];
}

void RetrieveResources( void )
{
	InitSound( );				

	InitBackdrop( );			

	GetBlobGraphics( );			
	
	InitNext( );				
	
	InitScore( );				

	InitGrayMonitors( );		
	
	InitOpponent( );

	InitStage( );   // must run after backdrop window is open
	InitGameTickCount( );

	InitPlayers( ); // must run after backdrop window is open
	InitFont( ); 
	InitZapStyle( );// must run after fonts are inited
	
	InitBlitter( ); // must run after player windows are open
	InitPlayerWorlds( );
	
	InitVictory( );	// must run after fonts are inited			
	InitTweak( );
}


void CenterRectOnScreen( MRect *rect, double locationX, double locationY )
{
	MPoint dest = {0,0};
	
	dest.h = (short)(locationX * (640 - (rect->right - rect->left)));
	dest.h &= ~3;
	dest.v = (short)(locationY * (480 - (rect->bottom - rect->top)));

	OffsetMRect( rect, -rect->left, -rect->top );
	OffsetMRect( rect, dest.h, dest.v );
}

void ReserveMonitor( void )
{
    SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
    
    int resW = 640;
    int resH = widescreen? 360: 480;

    SDL_Rect displayBounds = { .x = 0, .y = 0, .w = 640, .h = 480 };
    SDL_GetDisplayUsableBounds(0, &displayBounds);
    float scaleX = (displayBounds.w * 75/100) / (float)resW;	// allow covering at most 95% of the screen
    float scaleY = (displayBounds.h * 75/100) / (float)resH;
    float scale = scaleX < scaleY ? scaleX : scaleY;
    if (crispUpscaling)
        scale = (int) scale;
    scale = scale < 1 ? 1 : scale;

    SDL_CreateWindowAndRenderer(resW*scale, resH*scale, SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI, &g_window, &g_renderer);

    SDL_RenderSetLogicalSize(g_renderer, resW, resH);
    SDL_SetWindowTitle(g_window, "Candy Crisis (source port v" PROJECT_VERSION ")");

    SDL_SetRenderDrawColor(g_renderer, 0, 0, 0, 255);
    SDL_RenderClear(g_renderer);
    //SDL_RenderPresent(g_renderer);
    
    g_frontSurface = SDL_CreateRGBSurface(0, 640, 480, 32,
                                          RED_MASK, GREEN_MASK, BLUE_MASK, 0);
    
	SDLU_CreateRendererTexture();

    SetFullscreen(fullscreen);
}

void ReleaseMonitor( void )
{
}

void SetFullscreen( MBoolean fullscreenMode )
{
    SDL_SetWindowFullscreen(g_window, fullscreenMode? SDL_WINDOW_FULLSCREEN_DESKTOP: 0);
}

int Warp( void )
{
	return 8;
}

const char* QuickResourceName( const char* prefix, int id, const char* extension )
{
	static char name[1024];
	if (id)
	{
		SDL_snprintf( name, sizeof(name), "%s%s_%d%s", candyCrisisResources, prefix, id, extension );
	}
	else
	{
		SDL_snprintf( name, sizeof(name), "%s%s%s", candyCrisisResources, prefix, extension );
	}
	
	return name;
}

static MBoolean CheckDataPath(void)
{
	const char* path = QuickResourceName("snd", 128, ".wav");
	return FileExists(path);
}

void Initialize(void)
{
#if _WIN32
	SDL_snprintf(candyCrisisResources, sizeof(candyCrisisResources), "CandyCrisisResources\\");
#elif __APPLE__
	char pathbuf[PROC_PIDPATHINFO_MAXSIZE];

	pid_t pid = getpid();
	int ret = proc_pidpath(pid, pathbuf, sizeof(pathbuf));
	if (ret <= 0)
	{
		Error(strerror(errno));
	}

	// go up two levels
	for (int i = 0; i < 2; i++)
	{
		char* slashPos = SDL_strrchr(pathbuf, '/');
		if (!slashPos)
		{
			Error("can't find Resources folder");
		}
		*slashPos = '\0';
	}

	SDL_snprintf(candyCrisisResources, sizeof(candyCrisisResources), "%s/Resources/", pathbuf);
#else
	char* basePath = SDL_GetBasePath();

	SDL_snprintf(candyCrisisResources, sizeof(candyCrisisResources), "CandyCrisisResources/");
	if (CheckDataPath())
	{
		goto dataPathFound;
	}

	SDL_snprintf(candyCrisisResources, sizeof(candyCrisisResources), "%s../share/candycrisis/", basePath);
	if (CheckDataPath())
	{
		goto dataPathFound;
	}

	SDL_snprintf(candyCrisisResources, sizeof(candyCrisisResources), "%s../share/CandyCrisis/", basePath);
	if (CheckDataPath())
	{
		goto dataPathFound;
	}

	SDL_free(basePath);
	Error("Couldn't find the CandyCrisisResources or share/candycrisis folder.");
	return;

dataPathFound:
	SDL_free(basePath);
#endif

	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0)
	{
		Error("SDL_Init failed");
	}

	atexit(SDL_Quit);

    SDLU_Init();
}

void QuickFadeIn( MRGBColor *color )
{
	(void) color; // is unused

    for( float percent=0.0f; percent<1.0f; percent += 0.04f )
    {
        MTicks c = MTickCount( );
        SDLU_SetBrightness( percent );
        while( c == MTickCount( ) )
        {
            SDLU_Present();
            SDLU_Yield();
        }
    }

    SDLU_SetBrightness( 1.0 );

    SDL_Delay(200);
}

void QuickFadeOut( MRGBColor *color )
{
	(void) color; // is unused

    for( float percent=1.0f; percent>0.0f; percent -= 0.04f )
    {
        MTicks c = MTickCount( );
        SDLU_SetBrightness( percent );
        while( c == MTickCount( ) )
        {
            SDLU_Present();
            SDLU_Yield();
        }
    }

    SDLU_SetBrightness( 0.0 );
    SDLU_Present();
    SDLU_Present();

    SDL_Delay(200);
}

MBoolean FileExists( const char* name )
{
	SDL_RWops* f = SDL_RWFromFile( name, "rb" );
	if( f == NULL )
	{
		return false;
	}
	
	SDL_RWclose( f );
	return true;
}


void WaitForRegainFocus()
{
    do
    {  
        SDLU_PumpEvents();
        SDL_Delay(50);
    }
    while( !SDLU_IsForeground() );    

	DoFullRepaint();
}