/* Copyright (C) 1994-1998 Revolution Software Ltd. * Copyright (C) 2003-2005 The ScummVM project * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * $Header$ */ #include "common/stdafx.h" #include "common/file.h" #include "common/system.h" #include "sword2/sword2.h" #include "sword2/defs.h" #include "sword2/interpreter.h" #include "sword2/logic.h" #include "sword2/maketext.h" #include "sword2/memory.h" #include "sword2/resman.h" #include "sword2/sound.h" #include "sword2/driver/d_draw.h" #include "sword2/driver/d_sound.h" #include "sword2/driver/render.h" namespace Sword2 { int32 Logic::fnTestFunction(int32 *params) { // params: 0 address of a flag return IR_CONT; } int32 Logic::fnTestFlags(int32 *params) { // params: 0 value of flag return IR_CONT; } int32 Logic::fnGosub(int32 *params) { // params: 0 id of script // Hurray, script subroutines. Logic goes up - pc is saved for current // level. logicUp(params[0]); return IR_GOSUB; } int32 Logic::fnNewScript(int32 *params) { // change current script - must be followed by a TERMINATE script // directive // params: 0 id of script _scriptVars[PLAYER_ACTION] = 0; // must clear this logicReplace(params[0]); return IR_TERMINATE; } int32 Logic::fnInteract(int32 *params) { // Run targets action on a subroutine. Called by player on his base // level 0 idle, for example. // params: 0 id of target from which we derive action script // reference _scriptVars[PLAYER_ACTION] = 0; // must clear this logicUp((params[0] << 16) | 2); // 3rd script of clicked on id // Out, up and around again - pc is saved for current level to be // returned to. return IR_GOSUB; } int32 Logic::fnPreLoad(int32 *params) { // Forces a resource into memory before it's "officially" opened for // use. eg. if an anim needs to run on smoothly from another, // "preloading" gets it into memory in advance to avoid the cacheing // delay that normally occurs before the first frame. // params: 0 resource to preload _vm->_resman->openResource(params[0]); _vm->_resman->closeResource(params[0]); return IR_CONT; } int32 Logic::fnPreFetch(int32 *params) { // Go fetch resource in the background. // params: 0 resource to fetch [guess] return IR_CONT; } int32 Logic::fnFetchWait(int32 *params) { // Fetches a resource in the background but prevents the script from // continuing until the resource is in memory. // params: 0 resource to fetch [guess] return IR_CONT; } int32 Logic::fnRelease(int32 *params) { // Releases a resource from memory. Used for freeing memory for // sprites that have just been used and will not be used again. // Sometimes it is better to kick out a sprite straight away so that // the memory can be used for more frequent animations. // params: 0 resource to release [guess] return IR_CONT; } int32 Logic::fnRandom(int32 *params) { // params: 0 min // 1 max _scriptVars[RESULT] = _vm->_rnd.getRandomNumberRng(params[0], params[1]); return IR_CONT; } int32 Logic::fnPause(int32 *params) { // params: 0 pointer to object's logic structure // 1 number of game-cycles to pause // NB. Pause-value of 0 causes script to continue, 1 causes a 1-cycle // quit, 2 gives 2 cycles, etc. ObjectLogic *ob_logic = (ObjectLogic *) _vm->_memory->decodePtr(params[0]); if (ob_logic->looping == 0) { ob_logic->looping = 1; ob_logic->pause = params[1]; } if (ob_logic->pause) { ob_logic->pause--; return IR_REPEAT; } ob_logic->looping = 0; return IR_CONT; } int32 Logic::fnRandomPause(int32 *params) { // params: 0 pointer to object's logic structure // 1 minimum number of game-cycles to pause // 2 maximum number of game-cycles to pause ObjectLogic *ob_logic = (ObjectLogic *) _vm->_memory->decodePtr(params[0]); int32 pars[2]; if (ob_logic->looping == 0) { pars[0] = params[1]; pars[1] = params[2]; fnRandom(pars); pars[1] = _scriptVars[RESULT]; } pars[0] = params[0]; return fnPause(pars); } int32 Logic::fnPassGraph(int32 *params) { // makes an engine local copy of passed ObjectGraphic - run script 4 // of an object to request this used by fnTurnTo(id) etc // // remember, we cannot simply read a compact any longer but instead // must request it from the object itself // params: 0 pointer to an ObjectGraphic structure warning("fnPassGraph() is a no-op now"); return IR_CONT; } int32 Logic::fnPassMega(int32 *params) { // makes an engine local copy of passed graphic_structure and // mega_structure - run script 4 of an object to request this // used by fnTurnTo(id) etc // // remember, we cannot simply read a compact any longer but instead // must request it from the object itself // params: 0 pointer to a mega structure memcpy(&_engineMega, _vm->_memory->decodePtr(params[0]), sizeof(ObjectMega)); return IR_CONT; } int32 Logic::fnSetValue(int32 *params) { // temp. function! // used for setting far-referenced megaset resource field in mega // object, from start script // params: 0 pointer to object's mega structure // 1 value to set it to ObjectMega *ob_mega = (ObjectMega *) _vm->_memory->decodePtr(params[0]); ob_mega->megaset_res = params[1]; return IR_CONT; } #ifdef SWORD2_DEBUG #define BLACK 0 #define WHITE 1 #define RED 2 #define GREEN 3 #define BLUE 4 static uint8 black[4] = { 0, 0, 0, 0 }; static uint8 white[4] = { 255, 255, 255, 0 }; static uint8 red[4] = { 255, 0, 0, 0 }; static uint8 green[4] = { 0, 255, 0, 0 }; static uint8 blue[4] = { 0, 0, 255, 0 }; #endif int32 Logic::fnFlash(int32 *params) { // flash colour 0 (ie. border) - useful during script development // eg. fnFlash(BLUE) where a text line is missed; RED when some code // missing, etc // params: 0 colour to flash #ifdef SWORD2_DEBUG // what colour? switch (params[0]) { case WHITE: _vm->_graphics->setPalette(0, 1, white, RDPAL_INSTANT); break; case RED: _vm->_graphics->setPalette(0, 1, red, RDPAL_INSTANT); break; case GREEN: _vm->_graphics->setPalette(0, 1, green, RDPAL_INSTANT); break; case BLUE: _vm->_graphics->setPalette(0, 1, blue, RDPAL_INSTANT); break; } // There used to be a busy-wait loop here, so I don't know how long // the delay was meant to be. Probably doesn't matter much. _vm->_graphics->updateDisplay(); _vm->_system->delayMillis(250); _vm->_graphics->setPalette(0, 1, black, RDPAL_INSTANT); #endif return IR_CONT; } int32 Logic::fnColour(int32 *params) { // set border colour - useful during script development // eg. set to colour during a timer situation, then black when timed // out // params 0: colour (see defines above) #ifdef SWORD2_DEBUG // what colour? switch (params[0]) { case BLACK: _vm->_graphics->setPalette(0, 1, black, RDPAL_INSTANT); break; case WHITE: _vm->_graphics->setPalette(0, 1, white, RDPAL_INSTANT); break; case RED: _vm->_graphics->setPalette(0, 1, red, RDPAL_INSTANT); break; case GREEN: _vm->_graphics->setPalette(0, 1, green, RDPAL_INSTANT); break; case BLUE: _vm->_graphics->setPalette(0, 1, blue, RDPAL_INSTANT); break; } #endif return IR_CONT; } int32 Logic::fnDisplayMsg(int32 *params) { // Display a message to the user on the screen. // params: 0 Text number of message to be displayed. uint32 local_text = params[0] & 0xffff; uint32 text_res = params[0] / SIZE; // Display message for three seconds. // +2 to skip the encoded text number in the first 2 chars; 3 is // duration in seconds _vm->displayMsg(_vm->fetchTextLine(_vm->_resman->openResource(text_res), local_text) + 2, 3); _vm->_resman->closeResource(text_res); return IR_CONT; } int32 Logic::fnResetGlobals(int32 *params) { // fnResetGlobals is used by the demo - so it can loop back & restart // itself // params: none int32 size; uint32 *globals; size = _vm->_resman->fetchLen(1); size -= sizeof(StandardHeader); debug(5, "globals size: %d", size); globals = (uint32 *) ((byte *) _vm->_resman->openResource(1) + sizeof(StandardHeader)); // blank each global variable memset(globals, 0, size); _vm->_resman->closeResource(1); // all objects but george _vm->_resman->killAllObjects(false); // FOR THE DEMO - FORCE THE SCROLLING TO BE RESET! // - this is taken from fnInitBackground // switch on scrolling (2 means first time on screen) _vm->_thisScreen.scroll_flag = 2; return IR_CONT; } // FIXME: // // The original credits used a different font. I think it's stored in the // font.clu file, but I don't know how to interpret it. // // The original used the entire screen. This version cuts off the top and // bottom of the screen, because that's where the menus would usually be. // // The original had some sort of smoke effect at the bottom of the screen. enum { LINE_LEFT, LINE_CENTER, LINE_RIGHT }; struct CreditsLine { char *str; byte type; int top; int height; byte *sprite; }; #define CREDITS_FONT_HEIGHT 25 #define CREDITS_LINE_SPACING 20 int32 Logic::fnPlayCredits(int32 *params) { uint32 loopingMusicId = _vm->_loopingMusicId; // This function just quits the game if this is the playable demo, ie. // credits are NOT played in the demo any more! // params: none if (_scriptVars[DEMO]) { _vm->closeGame(); return IR_STOP; } // Prepare for the credits by fading down, stoping the music, etc. _vm->setMouse(0); _vm->_sound->muteFx(true); _vm->_sound->muteSpeech(true); _vm->_graphics->waitForFade(); _vm->_graphics->fadeDown(); _vm->_graphics->waitForFade(); _vm->_graphics->closeMenuImmediately(); // There are three files which I believe are involved in showing the // credits: // // credits.bmp - The "Smacker" logo, stored as follows: // // width 2 bytes, little endian // height 2 bytes, little endian // palette 3 * 256 bytes // data width * height bytes // // Note that the maximum colour component in the palette is 0x3F. // This is the same resolution as the _paletteMatch table. I doubt // that this is a coincidence, but let's use the image palette // directly anyway, just to be safe. // // credits.clu - The credits text // // This is simply a text file with CRLF line endings. // '^' is not shown, but used to mark the center of the line. // '@' is used as a placeholder for the "Smacker" logo. At least // when it appears alone. // Remaining lines are centered. // // fonts.clu - The credits font? // // FIXME: At this time I don't know how to interpret fonts.clu. For // now, let's just the standard speech font instead. SpriteInfo spriteInfo; File f; int i; // Read the "Smacker" logo uint16 logoWidth = 0; uint16 logoHeight = 0; byte *logoData = NULL; byte palette[256 * 4]; if (f.open("credits.bmp")) { logoWidth = f.readUint16LE(); logoHeight = f.readUint16LE(); for (i = 0; i < 256; i++) { palette[i * 4 + 0] = f.readByte() << 2; palette[i * 4 + 1] = f.readByte() << 2; palette[i * 4 + 2] = f.readByte() << 2; palette[i * 4 + 3] = 0; } logoData = (byte *) malloc(logoWidth * logoHeight); f.read(logoData, logoWidth * logoHeight); f.close(); } else { warning("Can't find credits.bmp"); memset(palette, 0, sizeof(palette)); palette[14 * 4 + 0] = 252; palette[14 * 4 + 1] = 252; palette[14 * 4 + 2] = 252; palette[14 * 4 + 3] = 0; } _vm->_graphics->setPalette(0, 256, palette, RDPAL_INSTANT); // Read the credits text // This should be plenty CreditsLine creditsLines[350]; for (i = 0; i < ARRAYSIZE(creditsLines); i++) { creditsLines[i].str = NULL; creditsLines[i].sprite = NULL; } if (!f.open("credits.clu")) { warning("Can't find credits.clu"); return IR_CONT; } int lineTop = 400; int lineCount = 0; int paragraphStart = 0; bool hasCenterMark = false; while (1) { if (lineCount >= ARRAYSIZE(creditsLines)) { warning("Too many credits lines"); break; } char buffer[80]; char *line = f.gets(buffer, sizeof(buffer)); if (!line || *line == 0) { if (!hasCenterMark) { for (i = paragraphStart; i < lineCount; i++) creditsLines[i].type = LINE_CENTER; } paragraphStart = lineCount; hasCenterMark = false; if (paragraphStart == lineCount) lineTop += CREDITS_LINE_SPACING; if (!line) break; continue; } char *center_mark = strchr(line, '^'); if (center_mark) { // The current paragraph has at least one center mark. hasCenterMark = true; if (center_mark != line) { // The center mark is somewhere inside the // line. Split it into left and right side. *center_mark = 0; creditsLines[lineCount].top = lineTop; creditsLines[lineCount].height = CREDITS_FONT_HEIGHT; creditsLines[lineCount].type = LINE_LEFT; creditsLines[lineCount].str = strdup(line); lineCount++; if (lineCount >= ARRAYSIZE(creditsLines)) { warning("Too many credits lines"); break; } *center_mark = '^'; } line = center_mark; } creditsLines[lineCount].top = lineTop; if (*line == '^') { creditsLines[lineCount].type = LINE_RIGHT; line++; } else creditsLines[lineCount].type = LINE_LEFT; if (strcmp(line, "@") == 0) { creditsLines[lineCount].height = logoHeight; lineTop += logoHeight; } else { creditsLines[lineCount].height = CREDITS_FONT_HEIGHT; lineTop += CREDITS_LINE_SPACING; } creditsLines[lineCount].str = strdup(line); lineCount++; } f.close(); // We could easily add some ScummVM stuff to the credits, if we wanted // to. On the other hand, anyone with the attention span to actually // read all the credits probably already knows. :-) // Start the music and roll the credits // The credits music (which can also be heard briefly in the "carib" // cutscene) is played once. int32 pars[2]; pars[0] = 309; pars[1] = FX_SPOT; fnPlayMusic(pars); _vm->_graphics->clearScene(); _vm->_graphics->fadeUp(0); spriteInfo.scale = 0; spriteInfo.scaledWidth = 0; spriteInfo.scaledHeight = 0; spriteInfo.type = RDSPR_DISPLAYALIGN | RDSPR_NOCOMPRESSION | RDSPR_TRANS; spriteInfo.blend = 0; int startLine = 0; int scrollPos = 0; bool abortCredits = false; int scrollSteps = lineTop + CREDITS_FONT_HEIGHT; uint32 musicStart = _vm->getMillis(); // Ideally the music should last just a tiny bit longer than the // credits. Note that musicTimeRemaining() will return 0 if the music // is muted, so we need a sensible fallback for that case. uint32 musicLength = MAX((int32) (1000 * (_vm->_sound->musicTimeRemaining() - 3)), 25 * (int32) scrollSteps); while (scrollPos < scrollSteps && !_vm->_quit) { bool foundStartLine = false; _vm->_graphics->clearScene(); for (i = startLine; i < lineCount; i++) { // Free any sprites that have scrolled off the screen if (creditsLines[i].top + creditsLines[i].height < scrollPos) { if (creditsLines[i].sprite) { free(creditsLines[i].sprite); creditsLines[i].sprite = NULL; debug(2, "Freeing sprite '%s'", creditsLines[i].str); } if (creditsLines[i].str) { free(creditsLines[i].str); creditsLines[i].str = NULL; } } else if (creditsLines[i].top < scrollPos + 400) { if (!foundStartLine) { startLine = i; foundStartLine = true; } if (!creditsLines[i].sprite) { debug(2, "Creating sprite '%s'", creditsLines[i].str); creditsLines[i].sprite = _vm->_fontRenderer->makeTextSprite((byte *) creditsLines[i].str, 600, 14, _vm->_speechFontId, 0); } FrameHeader *frame = (FrameHeader *) creditsLines[i].sprite; spriteInfo.y = creditsLines[i].top - scrollPos; spriteInfo.w = frame->width; spriteInfo.h = frame->height; spriteInfo.data = creditsLines[i].sprite + sizeof(FrameHeader); switch (creditsLines[i].type) { case LINE_LEFT: spriteInfo.x = RENDERWIDE / 2 - 5 - frame->width; break; case LINE_RIGHT: spriteInfo.x = RENDERWIDE / 2 + 5; break; case LINE_CENTER: if (strcmp(creditsLines[i].str, "@") == 0) { spriteInfo.data = logoData; spriteInfo.x = (RENDERWIDE - logoWidth) / 2; spriteInfo.w = logoWidth; spriteInfo.h = logoHeight; } else spriteInfo.x = (RENDERWIDE - frame->width) / 2; break; } if (spriteInfo.data) _vm->_graphics->drawSprite(&spriteInfo); } else break; } _vm->_graphics->updateDisplay(); KeyboardEvent *ke = _vm->keyboardEvent(); if (ke && ke->keycode == 27) { if (!abortCredits) { abortCredits = true; _vm->_graphics->fadeDown(); } } if (abortCredits && _vm->_graphics->getFadeStatus() == RDFADE_BLACK) break; _vm->sleepUntil(musicStart + (musicLength * scrollPos) / scrollSteps); scrollPos++; } // We're done. Clean up and try to put everything back where it was // before the credits. for (i = 0; i < lineCount; i++) { if (creditsLines[i].str) free(creditsLines[i].str); if (creditsLines[i].sprite) free(creditsLines[i].sprite); } if (logoData) free(logoData); if (!abortCredits) { // The music should either have stopped or be about to stop, so // wait for it to really happen. while (_vm->_sound->musicTimeRemaining() && !_vm->_quit) { _vm->_graphics->updateDisplay(false); _vm->_system->delayMillis(100); } } if (_vm->_quit) return IR_CONT; _vm->_sound->muteFx(false); _vm->_sound->muteSpeech(false); if (loopingMusicId) { pars[0] = loopingMusicId; pars[1] = FX_LOOP; fnPlayMusic(pars); } else fnStopMusic(NULL); _vm->_thisScreen.new_palette = 99; if (!_vm->_mouseStatus || _choosing) _vm->setMouse(NORMAL_MOUSE_ID); if (_scriptVars[DEAD]) _vm->buildSystemMenu(); return IR_CONT; } } // End of namespace Sword2