X-Git-Url: http://4ch.mooo.com/gitweb/?p=16.git;a=blobdiff_plain;f=16%2Fkeen456%2FKEEN4-6%2FCK_PLAY.C;fp=16%2Fkeen456%2FKEEN4-6%2FCK_PLAY.C;h=0e0814056c3076c1fd918c546f9f38c8e909f906;hp=0000000000000000000000000000000000000000;hb=7d1948e210bb7b58af0a0412e71f2a0a0a2010af;hpb=ebc247a0a67daa69a027f31d9d7d9572db765e56 diff --git a/16/keen456/KEEN4-6/CK_PLAY.C b/16/keen456/KEEN4-6/CK_PLAY.C new file mode 100755 index 00000000..0e081405 --- /dev/null +++ b/16/keen456/KEEN4-6/CK_PLAY.C @@ -0,0 +1,2422 @@ +/* Reconstructed Commander Keen 4-6 Source Code + * Copyright (C) 2021 K1n9_Duk3 + * + * This file is loosely based on: + * Keen Dreams Source Code + * Copyright (C) 2014 Javier M. Chavez + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "CK_DEF.H" + +/* +============================================================================= + + GLOBAL VARIABLES + +============================================================================= +*/ + +ScanCode firescan = sc_Space; + +boolean singlestep, jumpcheat, godmode, keenkilled; + +exittype playstate; +gametype gamestate; + +objtype *new, *check, *player, *scoreobj; + +Uint16 originxtilemax; +Uint16 originytilemax; + +ControlInfo c; +boolean button2, button3; // never used + +objtype dummyobj; + +Sint16 invincible; + +boolean oldshooting, showscorebox, joypad; + +Sint16 groundslam; + +boolean debugok; +boolean jumpbutton, jumpheld, pogobutton, pogoheld, firebutton, fireheld, upheld; + + +/* +============================================================================= + + LOCAL VARIABLES + +============================================================================= +*/ + +objtype *obj; + +Uint16 centerlevel; + +Uint16 objectcount; +objtype objarray[MAXACTORS]; +objtype *lastobj; +objtype *objfreelist; + +Sint16 inactivateleft; +Sint16 inactivateright; +Sint16 inactivatetop; +Sint16 inactivatebottom; + +#ifdef KEEN6Ev15 +Uint16 __dummy__; // never used, but must be present to recreate the original EXE +#endif + +Uint16 extravbls; + +Uint16 windowofs; +Sint16 vislines; +boolean scrollup; + +Sint16 oldfirecount; + +//=========================================================================== + +/* +================== += += CountObjects += +================== +*/ + +void CountObjects(void) +{ + Uint16 activeobjects, inactiveobjects; + objtype *ob; + + activeobjects = inactiveobjects = 0; + for (ob=player; ob; ob=ob->next) + { + if (ob->active) + { + activeobjects++; + } + else + { + inactiveobjects++; + } + } + VW_FixRefreshBuffer(); + US_CenterWindow(18, 4); + PrintY += 7; + US_Print("Active Objects :"); + US_PrintUnsigned(activeobjects); + US_Print("\nInactive Objects:"); + US_PrintUnsigned(inactiveobjects); + VW_UpdateScreen(); + IN_Ack(); +} + +/* +================== += += DebugMemory += +================== +*/ + +void DebugMemory(void) +{ + VW_FixRefreshBuffer(); + US_CenterWindow(16, 7); + US_CPrint("Memory Usage"); + US_CPrint("------------"); + US_Print("Total :"); + US_PrintUnsigned((mminfo.mainmem+mminfo.EMSmem+mminfo.XMSmem)/1024); + US_Print("k\nFree :"); + US_PrintUnsigned(MM_UnusedMemory()/1024); + US_Print("k\nWith purge:"); + US_PrintUnsigned(MM_TotalFree()/1024); + US_Print("k\n"); + VW_UpdateScreen(); + IN_Ack(); +#if GRMODE != CGAGR + MM_ShowMemory(); +#endif +} + +/* +=================== += += TestSprites += +=================== +*/ + +void TestSprites(void) +{ + Uint16 infox, infoy; + Sint16 chunk, oldchunk; + Sint16 shift; + Uint16 infobottom, drawx; + spritetabletype far *info; + Uint8 _seg *block; + Uint16 size; + Uint16 scan; + Uint32 totalsize; + + VW_FixRefreshBuffer(); + US_CenterWindow(30, 17); + totalsize = 0; + US_CPrint("Sprite Test"); + US_CPrint("-----------"); + infoy = PrintY; + infox = (PrintX + 56) & ~7; + drawx = infox + 40; + US_Print("Chunk:\nWidth:\nHeight:\nOrgx:\nOrgy:\nXl:\nYl:\nXh:\nYh:\nShifts:\nMem:\n"); + infobottom = PrintY; + chunk = STARTSPRITES; + shift = 0; + while (1) + { + if (chunk >= STARTSPRITES+NUMSPRITES) + { + chunk = STARTSPRITES+NUMSPRITES-1; + } + else if (chunk < STARTSPRITES) + { + chunk = STARTSPRITES; + } + info = &spritetable[chunk-STARTSPRITES]; + block = grsegs[chunk]; + VWB_Bar(infox, infoy, 40, infobottom-infoy, WHITE); + PrintX = infox; + PrintY = infoy; + US_PrintUnsigned(chunk); + US_Print("\n"); + PrintX = infox; + US_PrintUnsigned(info->width); + US_Print("\n"); + PrintX = infox; + US_PrintUnsigned(info->height); + US_Print("\n"); + PrintX = infox; + US_PrintSigned(info->orgx); + US_Print("\n"); + PrintX = infox; + US_PrintSigned(info->orgy); + US_Print("\n"); + PrintX = infox; + US_PrintSigned(info->xl); + US_Print("\n"); + PrintX = infox; + US_PrintSigned(info->yl); + US_Print("\n"); + PrintX = infox; + US_PrintSigned(info->xh); + US_Print("\n"); + PrintX = infox; + US_PrintSigned(info->yh); + US_Print("\n"); + PrintX = infox; + US_PrintSigned(info->shifts); + US_Print("\n"); + PrintX = infox; + if (!block) + { + US_Print("-----"); + } + else + { + size = ((spritetype far *)block)->sourceoffset[3] + ((spritetype far *)block)->planesize[3]*5; + size = (size + 15) & ~15; //round up to multiples of 16 + totalsize += size; //useless: the value stored in 'totalsize' is never used + US_PrintUnsigned(size); + US_Print("="); + } + oldchunk = chunk; + do + { + VWB_Bar(drawx, infoy, 110, infobottom-infoy, WHITE); + if (block) + { + PrintX = drawx; + PrintY = infoy; + US_Print("Shift:"); + US_PrintUnsigned(shift); + US_Print("\n"); + VWB_DrawSprite(drawx + 2*shift + 16, PrintY, chunk); + } + VW_UpdateScreen(); + scan = IN_WaitForKey(); + switch (scan) + { + case sc_UpArrow: + chunk++; + break; + case sc_DownArrow: + chunk--; + break; + case sc_PgUp: + chunk += 10; + if (chunk >= STARTSPRITES+NUMSPRITES) + { + chunk = STARTSPRITES+NUMSPRITES-1; + } + break; + case sc_PgDn: + chunk -= 10; + if (chunk < STARTSPRITES) + { + chunk = STARTSPRITES; + } + break; + case sc_LeftArrow: + if (--shift == -1) + { + shift = 3; + } + break; + case sc_RightArrow: + if (++shift == 4) + { + shift = 0; + } + break; + case sc_Escape: + return; + } + + } while (chunk == oldchunk); + + } +} + +/* +=================== += += PicturePause += +=================== +*/ + +void PicturePause(void) +{ + Uint16 source; + Sint16 y; + +// +// wait for a key press, abort if it's not Enter +// + IN_ClearKeysDown(); + while (!LastScan); + if (LastScan != sc_Enter) + { + IN_ClearKeysDown(); + return; + } + + SD_PlaySound(SND_JUMP); + SD_WaitSoundDone(); + +// +// rearrange onscreen image into base EGA layout, so that it +// can be grabbed correctly by an external screenshot tool +// + source = displayofs + panadjust; + + VW_ColorBorder(15); // white (can't use WHITE as parameter, since that's defined as 3 for CGA and this must use 15) + VW_SetLineWidth(40); + VW_SetScreen(0, 0); + + if (source < 0x10000l-200*64) + { + // + // copy top line first + // + for (y=0; y<200; y++) + { + VW_ScreenToScreen(source+y*64, y*40, 40, 1); + } + } + else + { + // + // copy bottom line first + // + for (y=199; y>=0; y--) + { + VW_ScreenToScreen(source+y*64, y*40, 40, 1); + } + } + +// +// shut down input manager so that screenshot tool can see input again +// + IN_Shutdown(); + + SD_PlaySound(SND_EXTRAKEEN); + SD_WaitSoundDone(); + +// +// shut down the remaining ID managers, except VW (stay in graphics mode!) +// + US_Shutdown(); + SD_Shutdown(); + IN_Shutdown(); + RF_Shutdown(); + CA_Shutdown(); + MM_Shutdown(); + +// +// wait until user hits Escape +// + while (((bioskey(0) >> 8) & 0xFF) != sc_Escape); + +// +// back to text mode and exit to DOS +// + VW_Shutdown(); + exit(0); +} + +/* +=================== += += MaskOnTile += +=================== +*/ + +void MaskOnTile(Uint16 dest, Uint16 source) +{ + Sint16 i; + Uint16 _seg *sourceseg; + Uint16 _seg *destseg; + Uint16 sourceval, maskindex, sourcemask; + + sourceseg = (grsegs+STARTTILE16M)[source]; + destseg = (grsegs+STARTTILE16M)[dest]; + for (i=0; i<64; i++) + { + maskindex = i & 15; +#ifdef KEEN6Ev15 + sourceval = sourceseg[16+i]; +#else + sourceval = (sourceseg+16)[i]; +#endif + sourcemask = sourceseg[maskindex]; + destseg[maskindex] &= sourcemask; + destseg[16+i] &= sourcemask; + destseg[16+i] |= sourceval; + } +} + +/* +=================== += += WallDebug += +=================== +*/ + +void WallDebug(void) +{ + Sint16 i, val; + + VW_FixRefreshBuffer(); + US_CenterWindow(24, 3); + US_PrintCentered("WORKING"); + VW_UpdateScreen(); + for (i=STARTTILE16M+108; i 1) + { + strcpy(str, "WallDebug: East wall other than 1:"); + itoa(i, str2, 10); + strcat(str, str2); + Quit(str); + } + if (val) + { + MaskOnTile(i, val+114); //Note: val is always 1 here, so you could use 115 as 2nd arg + } + val = tinf[i+WESTWALL] & 7; + if (val > 1) + { + strcpy(str, "WallDebug: West wall other than 1:"); + itoa(i, str2, 10); + strcat(str, str2); + Quit(str); + } + if (val) + { + MaskOnTile(i, val+122); //Note: val is always 1 here, so you could use 123 as 2nd arg + } + } +} + + +//=========================================================================== + +/* +================ += += DebugKeys += +================ +*/ + +boolean DebugKeys(void) +{ + Sint16 level, i, esc; + + if (Keyboard[sc_B] && ingame) // B = border color + { + VW_FixRefreshBuffer(); + US_CenterWindow(24, 3); + PrintY += 6; + US_Print(" Border color (0-15):"); + VW_UpdateScreen(); + esc = !US_LineInput(px, py, str, NULL, true, 2, 0); + if (!esc) + { + level = atoi(str); + if (level >= 0 && level <= 15) + { + VW_ColorBorder(level); + } + } + return true; + } + + if (Keyboard[sc_C] && ingame) // C = count objects + { + CountObjects(); + return true; + } + + if (Keyboard[sc_D] && ingame) // D = start / end demo record + { + if (DemoMode == demo_Off) + { + StartDemoRecord(); + } + else if (DemoMode == demo_Record) + { + EndDemoRecord(); + playstate = ex_completed; + } + return true; + } + + if (Keyboard[sc_E] && ingame) // E = quit level + { + if (tedlevel) + { + TEDDeath(); + } + playstate = ex_completed; + //BUG? there is no return in this branch (should return false) + } + + if (Keyboard[sc_G] && ingame) // G = god mode + { + VW_FixRefreshBuffer(); + US_CenterWindow(12, 2); + if (godmode) + { + US_PrintCentered("God mode OFF"); + } + else + { + US_PrintCentered("God mode ON"); + } + VW_UpdateScreen(); + IN_Ack(); + godmode ^= true; + return true; + } + else if (Keyboard[sc_I]) // I = item cheat + { + VW_FixRefreshBuffer(); + US_CenterWindow(12, 3); + US_PrintCentered("Free items!"); + for (i=0; i<4; i++) + { + gamestate.keys[i] = 99; + } + gamestate.ammo = 99; +#if defined KEEN4 + gamestate.wetsuit = true; +#elif defined KEEN5 + gamestate.keycard = true; +#elif defined KEEN6 + gamestate.passcardstate=gamestate.hookstate=gamestate.sandwichstate = 1; +#endif + VW_UpdateScreen(); + IN_Ack(); + GivePoints(3000); + return true; + } + else if (Keyboard[sc_J]) // J = jump cheat + { + jumpcheat ^= true; + VW_FixRefreshBuffer(); + US_CenterWindow(18, 3); + if (jumpcheat) + { + US_PrintCentered("Jump cheat ON"); + } + else + { + US_PrintCentered("Jump cheat OFF"); + } + VW_UpdateScreen(); + IN_Ack(); + return true; + } + else if (Keyboard[sc_M]) // M = memory info + { + DebugMemory(); + return true; + } + else if (Keyboard[sc_N]) // N = no clip + { + VW_FixRefreshBuffer(); + US_CenterWindow(18, 3); + if (player->needtoclip) + { + US_PrintCentered("No clipping ON"); + player->needtoclip = cl_noclip; + } + else + { + US_PrintCentered("No clipping OFF"); + player->needtoclip = cl_midclip; + } + VW_UpdateScreen(); + IN_Ack(); + return true; + } + else if (Keyboard[sc_P]) // P = pause with no screen disruptioon + { + IN_ClearKeysDown(); + PicturePause(); + return true; + } + else if (Keyboard[sc_S] && ingame) // S = slow motion + { + singlestep ^= true; + VW_FixRefreshBuffer(); + US_CenterWindow(18, 3); + if (singlestep) + { + US_PrintCentered("Slow motion ON"); + } + else + { + US_PrintCentered("Slow motion OFF"); + } + VW_UpdateScreen(); + IN_Ack(); + return true; + } + else if (Keyboard[sc_T]) // T = sprite test + { + TestSprites(); + return true; + } + else if (Keyboard[sc_V]) // V = extra VBLs + { + VW_FixRefreshBuffer(); + US_CenterWindow(30, 3); + PrintY += 6; + US_Print(" Add how many extra VBLs(0-8):"); + VW_UpdateScreen(); + esc = !US_LineInput(px, py, str, NULL, true, 2, 0); + if (!esc) + { + level = atoi(str); + if (level >= 0 && level <= 8) + { + extravbls = level; + } + } + return true; + } + else if (Keyboard[sc_W] && ingame) // W = warp to level + { + VW_FixRefreshBuffer(); + US_CenterWindow(26, 3); + PrintY += 6; + US_Print(" Warp to which level(1-18):"); + VW_UpdateScreen(); + esc = !US_LineInput(px, py, str, NULL, true, 2, 0); + if (!esc) + { + level = atoi(str); + if (level > 0 && level <= 18) + { + gamestate.mapon = level; + playstate = ex_warped; + } + } + return true; + } + else if (Keyboard[sc_Y]) // Y = wall debug + { + WallDebug(); + return true; + } + else if (Keyboard[sc_Z]) // Z = game over + { + gamestate.lives = 0; + KillKeen(); + return false; + } + return false; +} + +//=========================================================================== + +/* +================ += += UserCheat += +================ +*/ + +void UserCheat(void) +{ + Sint16 i; + + for (i=sc_A; i<=sc_Z; i++) //Note: this does NOT check the keys in alphabetical order! + { + if (i != sc_B && i != sc_A && i != sc_T && Keyboard[i]) + { + return; + } + } + US_CenterWindow(20, 7); + PrintY += 2; + US_CPrint( + "Cheat Option!\n" + "\n" + "You just got all\n" + "the keys, 99 shots,\n" + "and an extra keen!"); + VW_UpdateScreen(); + IN_Ack(); + RF_ForceRefresh(); + gamestate.ammo = 99; + gamestate.lives++; +#ifdef KEEN5 + gamestate.keycard = true; +#endif + gamestate.keys[0] = gamestate.keys[1] = gamestate.keys[2] = gamestate.keys[3] = 1; +} + +//=========================================================================== + +/* +===================== += += CheckKeys += +===================== +*/ + +void CheckKeys(void) +{ + if (screenfaded) // don't do anything with a faded screen + { + return; + } + +// +// Enter for status screen +// + if (Keyboard[sc_Enter] || (GravisGamepad && GravisAction[ga_Status])) + { + StatusWindow(); + IN_ClearKeysDown(); + RF_ForceRefresh(); + lasttimecount = TimeCount; // BUG: should be the other way around + } + +// +// pause key wierdness can't be checked as a scan code +// + if (Paused) + { + SD_MusicOff(); + VW_FixRefreshBuffer(); + US_CenterWindow(8, 3); + US_PrintCentered("PAUSED"); + VW_UpdateScreen(); + IN_Ack(); + RF_ForceRefresh(); + Paused = false; + SD_MusicOn(); + } + +#ifndef KEEN6 +// +// F1 to enter help screens +// + if (LastScan == sc_F1) + { + StopMusic(); + HelpScreens(); + StartMusic(gamestate.mapon); + if (showscorebox) + { + scoreobj->temp2 = -1; + scoreobj->temp1 = -1; + scoreobj->temp3 = -1; + scoreobj->temp4 = -1; + } + RF_ForceRefresh(); + } +#endif + + if (!storedemo) + { +// +// F2-F7/ESC to enter control panel +// + if (LastScan >= sc_F2 && LastScan <= sc_F7 || LastScan == sc_Escape) + { + VW_FixRefreshBuffer(); + StopMusic(); + US_ControlPanel(); + RF_FixOfs(); + StartMusic(gamestate.mapon); + if (!showscorebox && scoreobj->sprite) + { + RF_RemoveSprite(&scoreobj->sprite); + } + if (showscorebox) + { + scoreobj->temp2 = -1; + scoreobj->temp1 = -1; + scoreobj->temp3 = -1; + scoreobj->temp4 = -1; + } + IN_ClearKeysDown(); + if (restartgame) + { + playstate = ex_resetgame; + } + else if (!loadedgame) + { + RF_ForceRefresh(); + } + if (abortgame) + { + abortgame = false; + playstate = ex_abortgame; + } + if (loadedgame) + { + playstate = ex_loadedgame; + } + lasttimecount = TimeCount; // BUG: should be the other way around + } + +// +// F9 boss key +// + if (LastScan == sc_F9) + { + VW_Shutdown(); + SD_MusicOff(); + cputs("C:>"); + IN_ClearKeysDown(); + while (LastScan != sc_Escape); + VW_SetScreenMode(GRMODE); + VW_ColorBorder(bordercolor); + RF_ForceRefresh(); + IN_ClearKeysDown(); + lasttimecount = TimeCount; // BUG: should be the other way around + SD_MusicOn(); + } + } + +// +// B-A-T cheat code +// + if (Keyboard[sc_B] && Keyboard[sc_A] && Keyboard[sc_T]) + { + UserCheat(); + } + +// +// F10-? debug keys +// + if (debugok && Keyboard[sc_F10]) + { + if (DebugKeys()) + { + RF_ForceRefresh(); + lasttimecount = TimeCount; // BUG: should be the other way around + } + } + +// +// Ctrl-S toggles sound (only in storedemo mode) +// + if (storedemo && Keyboard[sc_Control] && LastScan == sc_S) + { + if (SoundMode != sdm_Off) + { + SD_SetSoundMode(sdm_Off); + SD_SetMusicMode(smm_Off); + } + else + { + if (AdLibPresent) + { + SD_SetSoundMode(sdm_AdLib); + QuietFX = false; + SD_SetMusicMode(smm_AdLib); + } + else + { + SD_SetSoundMode(sdm_PC); + SD_SetMusicMode(smm_Off); + } + CA_LoadAllSounds(); + } + } + +// +// Ctrl-Q quick quit +// + if (Keyboard[sc_Control] && LastScan == sc_Q) + { + IN_ClearKeysDown(); + Quit(NULL); + } +} + +//=========================================================================== + +/* +================== += += PrintNumbers += +================== +*/ + +void PrintNumbers(Sint16 x, Sint16 y, Sint16 maxlen, Sint16 basetile, Sint32 number) +{ + register Sint16 i; + Sint16 len; + char buffer[20]; + + ltoa(number, buffer, 10); + len = strlen(buffer); + i = maxlen; + while (i>len) + { + VWB_DrawTile8(x, y, basetile); + i--; + x += 8; + } + while (i>0) + { + VWB_DrawTile8(x, y, basetile+buffer[len-i]+(1-'0')); + i--; + x += 8; + } +} + +/* +================== += += DrawStatusWindow += +================== +*/ + +#if GRMODE == CGAGR + +#define BACKCOLOR WHITE +#define TEXTBACK BLACK +#define NUMBERBACK BLACK + +#else + +#define BACKCOLOR LIGHTGRAY +#define TEXTBACK WHITE +#define NUMBERBACK BLACK + +#endif + +void DrawStatusWindow(void) +{ + Sint16 off, x, y, w, h, i; + Uint16 width, height; + + x = 64; + y = 16; + w = 184; + h = 144; + VWB_DrawTile8(x, y, 54); + VWB_DrawTile8(x, y+h, 60); + for (i=x+8; i<=x+w-8; i+=8) + { + VWB_DrawTile8(i, y, 55); + VWB_DrawTile8(i, y+h, 61); + } + VWB_DrawTile8(i, y, 56); + VWB_DrawTile8(i, y+h, 62); + for (i=y+8; i<=y+h-8; i+=8) + { + VWB_DrawTile8(x, i, 57); + VWB_DrawTile8(x+w, i, 59); + } + VWB_Bar(72, 24, 176, 136, BACKCOLOR); + + PrintY = 28; + WindowX = 80; + WindowW = 160; + US_CPrint("LOCATION"); + VWB_Bar(79, 38, 162, 20, TEXTBACK); +#ifdef KEEN5 + if (mapon == 0 && player->y > 100*TILEGLOBAL) + _fstrcpy(str, levelnames[13]); + else + _fstrcpy(str, levelnames[gamestate.mapon]); +#else + _fstrcpy(str, levelnames[gamestate.mapon]); +#endif + SizeText(str, &width, &height); + PrintY = (20-height)/2+40-2; + US_CPrint(str); + + PrintY = 61; + WindowX = 80; + WindowW = 64; + US_CPrint("SCORE"); + VWB_Bar(79, 71, 66, 10, NUMBERBACK); + PrintNumbers(80, 72, 8, 41, gamestate.score); + + PrintY = 61; + WindowX = 176; + WindowW = 64; + US_CPrint("EXTRA"); + VWB_Bar(175, 71, 66, 10, NUMBERBACK); + PrintNumbers(176, 72, 8, 41, gamestate.nextextra); + +#if defined KEEN4 + PrintY = 85; + WindowX = 80; + WindowW = 64; + US_CPrint("RESCUED"); + VWB_Bar(79, 95, 66, 10, NUMBERBACK); + for (i = 0; i < gamestate.rescued; i++, off+=8) + { + VWB_DrawTile8(i*8 + 80, 96, 40); + } +#elif defined KEEN5 + PrintY = 92; + PrintX = 80; + US_Print("KEYCARD"); + VWB_Bar(135, 91, 10, 10, NUMBERBACK); + if (gamestate.keycard) + { + VWB_DrawTile8(136, 92, 40); + } +#endif + + PrintY = 85; + WindowX = 176; + WindowW = 64; + US_CPrint("LEVEL"); + VWB_Bar(175, 95, 66, 10, TEXTBACK); + PrintY = 96; + WindowX = 176; + WindowW = 64; + switch (gamestate.difficulty) + { + case gd_Easy: + US_CPrint("Easy"); + break; + case gd_Normal: + US_CPrint("Normal"); + break; + case gd_Hard: + US_CPrint("Hard"); + break; + } + +#ifdef KEEN6 + PrintX = 80; + PrintY = 96; + US_Print("ITEMS"); + VWB_Bar(127, 95, 26, 10, NUMBERBACK); + if (gamestate.sandwichstate == 1) + { + VWB_DrawTile8(128, 96, 2); + } + else + { + VWB_DrawTile8(128, 96, 1); + } + if (gamestate.hookstate == 1) + { + VWB_DrawTile8(136, 96, 4); + } + else + { + VWB_DrawTile8(136, 96, 3); + } + if (gamestate.passcardstate == 1) + { + VWB_DrawTile8(144, 96, 6); + } + else + { + VWB_DrawTile8(144, 96, 5); + } +#endif + + PrintX = 80; + PrintY = 112; + US_Print("KEYS"); + VWB_Bar(119, 111, 34, 10, NUMBERBACK); + for (i = 0; i < 4; i++) + { + if (gamestate.keys[i]) + { + VWB_DrawTile8(i*8+120, 112, 36+i); + } + } + + PrintX = 176; + PrintY = 112; + US_Print("AMMO"); + VWB_Bar(215, 111, 26, 10, NUMBERBACK); + PrintNumbers(216, 112, 3, 41, gamestate.ammo); + + PrintX = 80; + PrintY = 128; + US_Print("KEENS"); + VWB_Bar(127, 127, 18, 10, NUMBERBACK); + PrintNumbers(128, 128, 2, 41, gamestate.lives); + + PrintX = 176; + PrintY = 128; + US_Print(DROPSNAME); + VWB_Bar(224, 127, 16, 10, NUMBERBACK); + PrintNumbers(224, 128, 2, 41, gamestate.drops); + +#ifdef KEEN4 + VWB_Bar(79, 143, 66, 10, TEXTBACK); + PrintY = 144; + WindowX = 80; + WindowW = 64; + if (gamestate.wetsuit) + { + US_CPrint("Wetsuit"); + } + else + { + US_CPrint("???"); + } +#endif + + // draw the tiles for "PRESS A KEY": + for (i = 0; i < 10; i++) + { + VWB_DrawTile8(i*8+STATUS_PRESSKEY_X, 140, i+72); + VWB_DrawTile8(i*8+STATUS_PRESSKEY_X, 148, i+82); + } +} + +/* +================== += += ScrollStatusWindow += +================== +*/ + +void ScrollStatusWindow(void) +{ + Uint16 source, dest; + Sint16 height; + + if (vislines > 152) + { + height = vislines - 152; + source = windowofs + panadjust + 8; + dest = bufferofs + panadjust + 8; + VW_ScreenToScreen(source, dest, 192/BYTEPIXELS, height); + VW_ClipDrawMPic((pansx+136)/BYTEPIXELS, -(16-height)+pansy, METALPOLEPICM); + source = windowofs + panadjust + 16*SCREENWIDTH + 8*CHARWIDTH; + dest = bufferofs + panadjust + height*SCREENWIDTH + 8; + height = 152; + } + else + { + source = windowofs + panadjust + (152-vislines)*SCREENWIDTH + 16*SCREENWIDTH + 8*CHARWIDTH; + dest = bufferofs + panadjust + 8; + height = vislines; + } + if (height > 0) + { + VW_ScreenToScreen(source, dest, 192/BYTEPIXELS, height); + } + if (scrollup) + { + height = 168-vislines; + source = masterofs + panadjust + vislines*SCREENWIDTH + 8; + dest = bufferofs + panadjust + vislines*SCREENWIDTH + 8; + VW_ScreenToScreen(source, dest, 192/BYTEPIXELS, height); + height = vislines; + source = windowofs + panadjust + 8 - 24/BYTEPIXELS; + dest = bufferofs + panadjust + 8 - 24/BYTEPIXELS; + if (height > 0) + VW_ScreenToScreen(source, dest, 24/BYTEPIXELS, height); + } + else + { + height = vislines + -72; + if (height > 0) + { + source = windowofs + panadjust + 8 - 24/BYTEPIXELS; + dest = bufferofs + panadjust + 8 - 24/BYTEPIXELS; + if (height > 0) + VW_ScreenToScreen(source, dest, 24/BYTEPIXELS, height); + } + } + if (vislines >= 72) + { + VW_ClipDrawMPic((pansx+40)/BYTEPIXELS, vislines-168+pansy, CORDPICM); + } + VW_UpdateScreen(); +} + +/* +================== += += StatusWindow += +================== +*/ + +void StatusWindow(void) +{ +#if GRMODE == CGAGR + + if (Keyboard[sc_A] && Keyboard[sc_2]) + { + US_CenterWindow(20, 2); + PrintY += 2; + US_Print("Debug keys active"); + VW_UpdateScreen(); + IN_Ack(); + debugok = true; + } + + WindowX = 0; + WindowW = 320; + WindowY = 0; + WindowH = 200; + DrawStatusWindow(); + VW_UpdateScreen(); + IN_ClearKeysDown(); + IN_Ack(); + +#else + + Uint16 oldbufferofs; + + WindowX = 0; + WindowW = 320; + WindowY = 0; + WindowH = 200; + + if (Keyboard[sc_A] && Keyboard[sc_2]) + { + US_CenterWindow(20, 2); + PrintY += 2; + US_Print("Debug keys active"); + VW_UpdateScreen(); + IN_Ack(); + debugok = true; + } + + RF_Refresh(); + RFL_InitAnimList(); + oldbufferofs = bufferofs; + bufferofs = windowofs = RF_FindFreeBuffer(); + VW_ScreenToScreen(displayofs, displayofs, 44, 224); // useless (source and dest offsets are identical) + VW_ScreenToScreen(displayofs, masterofs, 44, 224); + VW_ScreenToScreen(displayofs, bufferofs, 44, 168); + DrawStatusWindow(); + bufferofs = oldbufferofs; + RF_Refresh(); + + SD_PlaySound(SND_SHOWSTATUS); + vislines = 16; + scrollup = false; + RF_SetRefreshHook(ScrollStatusWindow); + + while (true) + { + RF_Refresh(); + if (vislines == 168) + break; + vislines = vislines + tics*8; + if (vislines > 168) + vislines = 168; + } + + RF_Refresh(); + RF_SetRefreshHook(NULL); + IN_ClearKeysDown(); + IN_Ack(); + + SD_PlaySound(SND_HIDESTATUS); + vislines -= 16; + scrollup = true; + RF_SetRefreshHook(ScrollStatusWindow); + + while (true) + { + RF_Refresh(); + if (vislines == 0) + break; + vislines = vislines - tics*8; + if (vislines < 0) + vislines = 0; + } + + RF_SetRefreshHook(NULL); + + scoreobj->x = 0; //force scorebox to redraw? + +#endif +} + +//=========================================================================== + +/* +================== += += CenterActor += +================== +*/ + +void CenterActor(objtype *ob) +{ + Uint16 orgx, orgy; + + centerlevel = 140; + if (ob->x < 152*PIXGLOBAL) + { + orgx = 0; + } + else + { + orgx = ob->x - 152*PIXGLOBAL; + } + if (mapon == 0) + { + if (ob->y < 80*PIXGLOBAL) + { + orgy = 0; + } + else + { + orgy = ob->y - 80*PIXGLOBAL; + } + } + else + { + if (ob->bottom < 140*PIXGLOBAL) + { + orgy = 0; + } + else + { + orgy = ob->bottom - 140*PIXGLOBAL; + } + } + if (!scorescreenkludge) + { + RF_NewPosition(orgx, orgy); + } + +// +// update limits for onscreen and inactivate checks +// + originxtilemax = (originxtile + PORTTILESWIDE) - 1; + originytilemax = (originytile + PORTTILESHIGH) - 1; + inactivateleft = originxtile - INACTIVATEDIST; + if (inactivateleft < 0) + { + inactivateleft = 0; + } + inactivateright = originxtilemax + INACTIVATEDIST; + if (inactivateright < 0) + { + inactivateright = 0; + } + inactivatetop = originytile - INACTIVATEDIST; + if (inactivatetop < 0) + { + inactivatetop = 0; + } + inactivatebottom = originytilemax + INACTIVATEDIST; + if (inactivatebottom < 0) + { + inactivatebottom = 0; + } +} + +//=========================================================================== + +/* +================== += += WorldScrollScreen += += Scroll if Keen is nearing an edge += +================== +*/ + +void WorldScrollScreen(objtype *ob) +{ + Sint16 xscroll, yscroll; + + if (keenkilled) + return; + + if (ob->left < originxglobal + 9*TILEGLOBAL) + { + xscroll = ob->left - (originxglobal + 9*TILEGLOBAL); + } + else if (ob->right > originxglobal + 12*TILEGLOBAL) + { + xscroll = ob->right + 16 - (originxglobal + 12*TILEGLOBAL); + } + else + { + xscroll = 0; + } + + if (ob->top < originyglobal + 5*TILEGLOBAL) + { + yscroll = ob->top - (originyglobal + 5*TILEGLOBAL); + } + else if (ob->bottom > originyglobal + 7*TILEGLOBAL) + { + yscroll = ob->bottom - (originyglobal + 7*TILEGLOBAL); + } + else + { + yscroll = 0; + } + + if (!xscroll && !yscroll) + return; + +// +// don't scroll more than one tile per frame +// + if (xscroll >= 0x100) + { + xscroll = 0xFF; + } + else if (xscroll <= -0x100) + { + xscroll = -0xFF; + } + if (yscroll >= 0x100) + { + yscroll = 0xFF; + } + else if (yscroll <= -0x100) + { + yscroll = -0xFF; + } + + RF_Scroll(xscroll, yscroll); + +// +// update limits for onscreen and inactivate checks +// + originxtilemax = (originxtile + PORTTILESWIDE) - 1; + originytilemax = (originytile + PORTTILESHIGH) - 1; + inactivateleft = originxtile - INACTIVATEDIST; + if (inactivateleft < 0) + { + inactivateleft = 0; + } + inactivateright = originxtilemax + INACTIVATEDIST; + if (inactivateright < 0) + { + inactivateright = 0; + } + inactivatetop = originytile - INACTIVATEDIST; + if (inactivatetop < 0) + { + inactivatetop = 0; + } + inactivatebottom = originytilemax + INACTIVATEDIST; + if (inactivatebottom < 0) + { + inactivatebottom = 0; + } +} + +//=========================================================================== + +/* +================== += += ScrollScreen += += Scroll if Keen is nearing an edge += Set playstate to ex_completes += +================== +*/ + +void ScrollScreen(objtype *ob) +{ + Sint16 xscroll, yscroll, pix, speed; + Uint16 bottom; + + if (keenkilled) + return; + +// +// walked off edge of map? +// + if (ob->left < originxmin || ob->right > originxmax + 320*PIXGLOBAL) + { + playstate = ex_completed; + return; + } + +// +// fallen off bottom of world? +// + if (ob->bottom > originymax + 13*TILEGLOBAL) + { + ob->y -= ob->bottom - (originymax + 13*TILEGLOBAL); + SD_PlaySound(SND_PLUMMET); + godmode = false; + KillKeen(); + return; + } + + xscroll=yscroll=0; + + if (ob->x < originxglobal + 9*TILEGLOBAL) + { + xscroll = ob->x - (originxglobal + 9*TILEGLOBAL); + } + else if (ob->x > originxglobal + 12*TILEGLOBAL) + { + xscroll = ob->x - (originxglobal + 12*TILEGLOBAL); + } + + if (ob->state == &s_keenlookup2) + { + if (centerlevel+tics > 167) + { + pix = 167-centerlevel; + } + else + { + pix = tics; + } + centerlevel += pix; + yscroll = CONVERT_PIXEL_TO_GLOBAL(-pix); + } + else if (ob->state == &s_keenlookdown3) + { + if (centerlevel-tics < 33) + { + pix = centerlevel + -33; + } + else + { + pix = tics; + } + centerlevel -= pix; + yscroll = CONVERT_PIXEL_TO_GLOBAL(pix); + } + +#ifdef KEEN6 + if (groundslam) + { + static Sint16 shaketable[] = {0, + -64, -64, -64, 64, 64, 64, + -200, -200, -200, 200, 200, 200, + -250, -250, -250, 250, 250, 250, + -250, -250, -250, 250, 250, 250 + }; + yscroll = yscroll + (bottom - (ob->bottom + shaketable[groundslam])); // BUG: 'bottom' has not been initialized yet! + } + else +#endif + if ( (ob->hitnorth || !ob->needtoclip || ob->state == &s_keenholdon)) + { + if ( ob->state != &s_keenclimbup + && ob->state != &s_keenclimbup2 + && ob->state != &s_keenclimbup3 + && ob->state != &s_keenclimbup4) + { + yscroll += ob->ymove; + bottom = originyglobal + yscroll + CONVERT_PIXEL_TO_GLOBAL(centerlevel); + if (ob->bottom == bottom) + { + // player didn't move, no additional scrolling + } + else + { + if (ob->bottom < bottom) + { + pix = bottom - ob->bottom; + } + else + { + pix = ob->bottom - bottom; + } + speed = CONVERT_PIXEL_TO_GLOBAL(pix) >> 7; + if (speed > 0x30) + { + speed = 0x30; + } + speed *= tics; + if (speed < 0x10) + { + if (pix < 0x10) + { + speed = pix; + } + else + { + speed = 0x10; + } + } + if (ob->bottom < bottom) + { + yscroll -= speed; + } + else + { + yscroll += speed; + } + } + } + } + else + { + centerlevel = 140; + } + + pix = (ob->bottom-32*PIXGLOBAL)-(originyglobal+yscroll); + if (pix < 0) + { + yscroll += pix; + } + pix = (ob->bottom+32*PIXGLOBAL)-(originyglobal+yscroll+200*PIXGLOBAL); + if (pix > 0) + { + yscroll += pix; + } + + if (xscroll == 0 && yscroll == 0) + return; + +// +// don't scroll more than one tile per frame +// + if (xscroll >= 0x100) + { + xscroll = 0xFF; + } + else if (xscroll <= -0x100) + { + xscroll = -0xFF; + } + if (yscroll >= 0x100) + { + yscroll = 0xFF; + } + else if (yscroll <= -0x100) + { + yscroll = -0xFF; + } + RF_Scroll(xscroll, yscroll); + +// +// update limits for onscreen and inactivate checks +// + originxtilemax = (originxtile + PORTTILESWIDE) - 1; + originytilemax = (originytile + PORTTILESHIGH) - 1; + inactivateleft = originxtile - INACTIVATEDIST; + if (inactivateleft < 0) + { + inactivateleft = 0; + } + inactivateright = originxtilemax + INACTIVATEDIST; + if (inactivateright < 0) + { + inactivateright = 0; + } + inactivatetop = originytile - INACTIVATEDIST; + if (inactivatetop < 0) + { + inactivatetop = 0; + } + inactivatebottom = originytilemax + INACTIVATEDIST; + if (inactivatebottom < 0) + { + inactivatebottom = 0; + } +} + +//=========================================================================== + + +/* +############################################################################# + + The objarray data structure + +############################################################################# + +Objarray contains structures for every actor currently playing. The structure +is accessed as a linked list starting at *player, ending when ob->next == +NULL. GetNewObj inserts a new object at the end of the list, meaning that +if an actor spawns another actor, the new one WILL get to think and react the +same frame. RemoveObj unlinks the given object and returns it to the free +list, but does not damage the objects ->next pointer, so if the current object +removes itself, a linked list following loop can still safely get to the +next element. + + + +############################################################################# +*/ + + +/* +========================= += += InitObjArray += += Call to clear out the entire object list, returning them all to the free += list. Allocates a special spot for the player. += +========================= +*/ + +void InitObjArray(void) +{ + Sint16 i; + + for (i=0; iprev; + memset(new, 0, sizeof(*new)); + if (lastobj) + { + lastobj->next = new; + } + new->prev = lastobj; // new->next is allready NULL from memset + + new->active = ac_yes; + new->needtoclip = cl_midclip; + lastobj = new; + + objectcount++; + return 0; +} + +//=========================================================================== + +/* +========================= += += RemoveObj += += Add the given object back into the free list, and unlink it from it's += neighbors += +========================= +*/ + +void RemoveObj(objtype *ob) +{ + if (ob == player) + Quit("RemoveObj: Tried to remove the player!"); + +// +// erase it from the refresh manager +// + RF_RemoveSprite(&ob->sprite); + if (ob->obclass == stunnedobj) + { + RF_RemoveSprite((void **)&ob->temp3); + } + +// +// fix the next object's back link +// + if (ob == lastobj) + { + lastobj = ob->prev; + } + else + { + ob->next->prev = ob->prev; + } + +// +// fix the previous object's forward link +// + ob->prev->next = ob->next; + +// +// add it back in to the free list +// + ob->prev = objfreelist; + objfreelist = ob; +} + +//========================================================================== + +/* +==================== += += GivePoints += += Grants extra men at 20k,40k,80k,160k,320k += +==================== +*/ + +void GivePoints(Uint16 points) +{ + gamestate.score += points; + if (!DemoMode && gamestate.score >= gamestate.nextextra) + { + SD_PlaySound(SND_EXTRAKEEN); + gamestate.lives++; + gamestate.nextextra *= 2; + } +} + +//========================================================================== + +/* +=================== += += PollControls += +=================== +*/ + +void PollControls(void) +{ + IN_ReadControl(0, &c); + if (c.yaxis != -1) + upheld = false; + + if (GravisGamepad && !DemoMode) + { + jumpbutton = GravisAction[ga_Jump]; + pogobutton = GravisAction[ga_Pogo]; + firebutton = GravisAction[ga_Fire]; + if (!jumpbutton) + jumpheld = false; + if (!pogobutton) + pogoheld = false; + if (!firebutton) + fireheld = false; + } + else if (oldshooting || DemoMode) + { + if (c.button0 && c.button1) + { + firebutton = true; + jumpbutton = pogobutton = jumpheld = pogoheld = false; + } + else + { + firebutton = fireheld = false; + if (c.button0) + { + jumpbutton = true; + } + else + { + jumpbutton = jumpheld = false; + } + if (c.button1) + { + if (oldfirecount <= 8) + { + oldfirecount = oldfirecount + tics; + } + else + { + pogobutton = true; + } + } + else + { + if (oldfirecount != 0) + { + pogobutton = true; + } + else + { + pogobutton = pogoheld = false; + } + oldfirecount = 0; + } + } + } + else + { + jumpbutton = c.button0; + pogobutton = c.button1; + firebutton = Keyboard[firescan]; + if (!jumpbutton) + jumpheld = false; + if (!pogobutton) + pogoheld = false; + if (!firebutton) + fireheld = false; + } +} + +//========================================================================== + + +/* +================= += += StopMusic += +================= +*/ + +void StopMusic(void) +{ + Sint16 i; + + SD_MusicOff(); + for (i=0; i= ARRAYLENGTH(songs) && num != 0xFFFF) + { + Quit("StartMusic() - bad level number"); + } + +#ifdef FIX_MUSIC_MEMORY_ISSUES + StopMusic(); +#else + SD_MusicOff(); +#endif + +#ifdef KEEN4 + if (num == 0xFFFF) + { + song = WONDER_MUS; + } + else + { + song = songs[num]; + } +#else + song = songs[num]; +#endif + + if (song == -1 || MusicMode != smm_AdLib) + { + return; + } + + MM_BombOnError(false); + CA_CacheAudioChunk(STARTMUSIC+song); + MM_BombOnError(true); + if (mmerror) + { + mmerror = false; + if (!DemoMode) + { + US_CenterWindow(20, 8); + PrintY += 20; + US_CPrint("Insufficient memory\nfor background music!"); + VW_UpdateScreen(); + wasfaded = screenfaded; + if (wasfaded) + { + VW_SetDefaultColors(); + } + IN_ClearKeysDown(); + IN_UserInput(3*TickBase, false); + if (wasfaded) + { + VW_FadeOut(); + } + } + } + else + { +#ifdef FIX_MUSIC_MEMORY_ISSUES + //The current music should be locked, so the memory manager will not + //mess with it when compressing memory blocks in MM_SortMem(). + MM_SetLock(&(memptr)audiosegs[STARTMUSIC+song], true); +#endif + SD_StartMusic((MusicGroup far *)audiosegs[STARTMUSIC+song]); + } +} + +//========================================================================== + + +/* +=================== += += PlayLoop += +=================== +*/ + +void PlayLoop(void) +{ + objtype *check; + + StartMusic(gamestate.mapon); + fireheld = pogoheld = upheld = jumpheld = false; + ingame = true; + playstate = ex_stillplaying; + invincible = keenkilled = oldfirecount = 0; + + CenterActor(player); + + if (DemoMode) + { + US_InitRndT(false); + } + else + { + US_InitRndT(true); + } + TimeCount = lasttimecount = tics = 3; + + do + { + PollControls(); + +// +// go through state changes and propose movements +// + for (obj=player; obj; obj=obj->next) + { + if (!obj->active && obj->tileright >= originxtile-1 + && obj->tileleft <= originxtilemax+1 && obj->tiletop <= originytilemax+1 + && obj->tilebottom >= originytile-1) + { + obj->needtoreact = true; + obj->active = ac_yes; + } + if (obj->active) + { + if (obj->tileright < inactivateleft + || obj->tileleft > inactivateright + || obj->tiletop > inactivatebottom + || obj->tilebottom < inactivatetop) + { + if (obj->active == ac_removable) + { + RemoveObj(obj); + continue; + } + else if (obj->active != ac_allways) + { + if (US_RndT() < tics*2 || screenfaded || loadedgame) + { + RF_RemoveSprite(&obj->sprite); + if (obj->obclass == stunnedobj) + RF_RemoveSprite((void **)&obj->temp3); + obj->active = ac_no; + continue; + } + } + } + StateMachine(obj); + } + } + + if (gamestate.riding) + { + HandleRiding(player); + } + +// +// check for and handle collisions between objects +// + for (obj=player; obj; obj=obj->next) + { + if (obj->active) + { + for (check=obj->next; check; check=check->next) + { + if (!check->active) + { + continue; + } + if (obj->right > check->left && obj->left < check->right + && obj->top < check->bottom && obj->bottom > check->top) + { + if (obj->state->contact) + { + obj->state->contact(obj, check); + } + if (check->state->contact) + { + check->state->contact(check, obj); + } + if (obj->obclass == nothing) //useless -- obclass is NOT set to nothing by RemoveObj + { + break; + } + } + } + } + } + +// +// check intiles +// + if (mapon != 0) + { + CheckInTiles(player); + } + else + { + CheckWorldInTiles(player); + } + +// +// react to whatever happened, and post sprites to the refresh manager +// + for (obj=player; obj; obj=obj->next) + { + if (!obj->active) + { + continue; + } + if (obj->tilebottom >= mapheight-1) + { + if (obj->obclass == keenobj) + { + playstate = ex_died; + } + else + { + RemoveObj(obj); + } + continue; + } + if (obj->needtoreact && obj->state->react) + { + obj->needtoreact = false; + obj->state->react(obj); + } + } + +// +// scroll the screen and update the score box +// +#ifdef KEEN4 + if (mapon != 0 && mapon != 17) +#else + if (mapon != 0) +#endif + { + ScrollScreen(player); + } + else + { + WorldScrollScreen(player); + } + UpdateScore(scoreobj); + if (loadedgame) + { + loadedgame = false; + } + +// +// update the screen and calculate the number of tics it took to execute +// this cycle of events (for adaptive timing of next cycle) +// + RF_Refresh(); + + if (invincible) + { + if ((invincible = invincible - tics) < 0) + invincible = 0; + } + +#ifdef KEEN6 + if (groundslam) + { + if ((groundslam = groundslam - tics) < 0) + groundslam = 0; + } +#endif +// +// single step debug mode +// + if (singlestep) + { + VW_WaitVBL(14); //reduces framerate to 5 fps on VGA or 4.3 fps on EGA cards + lasttimecount = TimeCount; + } +// +// extra VBLs debug mode +// + if (extravbls) + { + VW_WaitVBL(extravbls); + } + +// +// handle user inputs +// + if (DemoMode == demo_Playback) + { + if (!screenfaded && IN_IsUserInput()) + { + playstate = ex_completed; + if (LastScan != sc_F1) + { + LastScan = sc_Space; + } + } + } + else if (DemoMode == demo_PlayDone) + { + playstate = ex_completed; + } + else + { + CheckKeys(); + } + +// +// E-N-D cheat +// + if (Keyboard[sc_E] && Keyboard[sc_N] && Keyboard[sc_D]) + { +#if defined KEEN4 + gamestate.rescued = 7; + playstate = ex_rescued; +#elif defined KEEN5 + playstate = ex_qedbroke; +#elif defined KEEN6 + playstate = ex_molly; +#endif + } + + } while (playstate == ex_stillplaying); + + ingame = false; + StopMusic(); +} \ No newline at end of file