some fixes for new vccrun
[k8vacspelynky.git] / spelunky_main.vc
blob2658a3260d6d235fbd4ab011d8973a30b5981044
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2010, Moloch
4  * Copyright (c) 2018, Ketmar Dark
5  *
6  * This file is part of Spelunky.
7  *
8  * You can redistribute and/or modify Spelunky, including its source code, under
9  * the terms of the Spelunky User License.
10  *
11  * Spelunky is distributed in the hope that it will be entertaining and useful,
12  * but WITHOUT WARRANTY.  Please see the Spelunky User License for more details.
13  *
14  * The Spelunky User License should be available in "Game Information", which
15  * can be found in the Resource Explorer, or as an external file called COPYING.
16  * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17  *
18  **********************************************************************************/
19 import 'GLVideo';
20 import 'SoundSys';
21 import 'Geom';
22 import 'Game';
23 import 'Generator';
24 import 'Sprites';
27 #ifndef DISABLE_TIME_CHECK
28 # define DISABLE_TIME_CHECK
29 #endif
32 //#define MASK_TEST
34 //#define BIGGER_REPLAY_DATA
36 // ////////////////////////////////////////////////////////////////////////// //
37 #include "mapent/0all.vc"
38 #include "PlayerPawn.vc"
39 #include "PlayerPowerup.vc"
40 #include "GameLevel.vc"
43 // ////////////////////////////////////////////////////////////////////////// //
44 #include "uisimple.vc"
47 // ////////////////////////////////////////////////////////////////////////// //
48 class DebugSessionMovement : Object;
50 #ifdef BIGGER_REPLAY_DATA
51 array!(GameLevel::SavedKeyState) keypresses;
52 #else
53 array!ubyte keypresses; // on each frame
54 #endif
55 GameConfig playconfig;
57 transient int keypos;
58 transient int otherSeed, roomSeed;
61 override void Destroy () {
62   delete playconfig;
63   keypresses.length = 0;
64   ::Destroy();
68 final void resetReplay () {
69   keypos = 0;
73 #ifndef BIGGER_REPLAY_DATA
74 final void addKey (int kbidx, bool down) {
75   if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
76   keypresses[$] = kbidx|(down ? 0x80 : 0);
80 final void addEndOfFrame () {
81   keypresses[$] = 0xff;
85 enum {
86   NORMAL,
87   END_OF_FRAME,
88   END_OF_RECORD,
91 final int getKey (out int kbidx, out bool down) {
92   if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
93   if (keypos >= keypresses.length) return END_OF_RECORD;
94   ubyte b = keypresses[keypos++];
95   if (b == 0xff) return END_OF_FRAME;
96   kbidx = b&0x7f;
97   down = (b >= 0x80);
98   return NORMAL;
100 #endif
103 // ////////////////////////////////////////////////////////////////////////// //
104 class TempOptionsKeys : Object;
106 int[16*GameConfig::MaxActionBinds] keybinds;
107 int kbversion = 1;
110 // ////////////////////////////////////////////////////////////////////////// //
111 class Main : Object;
113 transient string dbgSessionStateFileName = "debug_game_session_state";
114 transient string dbgSessionMovementFileName = "debug_game_session_movement";
115 const float dbgSessionSaveIntervalInSeconds = 30;
117 GLTexture texTigerEye;
119 GameConfig config;
120 GameGlobal global;
121 SpriteStore sprStore;
122 BackTileStore bgtileStore;
123 GameLevel level;
125 int loserGPU;
127 int mouseX = int.min, mouseY = int.min;
128 int mouseLevelX = int.min, mouseLevelY = int.min;
129 bool renderMouseTile;
130 bool renderMouseRect;
132 enum StartMode {
133   Dead,
134   Alive,
135   Title,
136   Intro,
137   Stars,
138   Sun,
139   Moon,
142 StartMode startMode = StartMode.Intro;
143 bool pauseRequested;
144 bool helpRequested;
146 bool replayFastForward = false;
147 int replayFastForwardSpeed = 2;
148 bool saveGameSession = false;
149 bool replayGameSession = false;
150 enum Replay {
151   None,
152   Saving,
153   Replaying,
155 Replay doGameSavingPlaying = Replay.None;
156 float saveMovementLastTime = 0;
157 DebugSessionMovement debugMovement;
158 GameStats origStats; // for replaying
159 GameConfig origConfig; // for replaying
160 GameGlobal::SavedSeeds origSeeds;
162 int showHelp;
164 bool fullscreen;
165 transient bool allowRender = true;
168 #ifdef MASK_TEST
169 transient int maskSX, maskSY;
170 transient SpriteImage smask;
171 transient int maskFrame;
172 #endif
175 // ////////////////////////////////////////////////////////////////////////// //
176 final void saveKeyboardBindings () {
177   auto tok = SpawnObject(TempOptionsKeys);
178   foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
179   appSaveOptions(tok, "keybindings");
180   delete tok;
184 final void loadKeyboardBindings () {
185   auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
186   if (tok) {
187     if (tok.kbversion != TempOptionsKeys.default.kbversion) {
188       global.config.resetKeybindings();
189     } else {
190       foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
191     }
192     delete tok;
193   }
197 // ////////////////////////////////////////////////////////////////////////// //
198 void saveGameOptions () {
199   appSaveOptions(global.config, "config");
203 void loadGameOptions () {
204   auto cfg = appLoadOptions(GameConfig, "config");
205   if (cfg) {
206     auto oldHero = config.heroType;
207     auto tok = SpawnObject(TempOptionsKeys);
208     foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
209     delete global.config;
210     global.config = cfg;
211     config = cfg;
212     foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
213     delete tok;
214     writeln("config loaded");
215     global.restartMusic();
216     global.fixVolumes();
217     //config.heroType = GameConfig::Hero.Spelunker;
218     config.heroType = oldHero;
219   }
220   // fix my bug
221   if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
225 // ////////////////////////////////////////////////////////////////////////// //
226 void saveGameStats () {
227   if (level.stats) appSaveOptions(level.stats, "stats");
231 void loadGameStats () {
232   auto stats = appLoadOptions(GameStats, "stats");
233   if (stats) {
234     delete level.stats;
235     level.stats = stats;
236   }
237   if (!level.stats) level.stats = SpawnObject(GameStats);
238   level.stats.global = global;
242 // ////////////////////////////////////////////////////////////////////////// //
243 struct UIPaneSaveInfo {
244   name id;
245   UIPane::SaveInfo nfo;
248 transient UIPane optionsPane; // either options, or binding editor
250 transient GameLevel::IVec2D optionsPaneOfs;
251 transient void delegate () saveOptionsDG;
253 transient array!UIPaneSaveInfo optionsPaneState;
256 final void saveCurrentPane () {
257   if (!optionsPane || !optionsPane.id) return;
259   // summon ghost
260   if (optionsPane.id == 'CheatFlags') {
261     if (instantGhost && level.ghostTimeLeft > 0) {
262       level.ghostTimeLeft = 1;
263     }
264   }
266   foreach (ref auto psv; optionsPaneState) {
267     if (psv.id == optionsPane.id) {
268       optionsPane.saveState(psv.nfo);
269       return;
270     }
271   }
272   // append new
273   optionsPaneState.length += 1;
274   optionsPaneState[$-1].id = optionsPane.id;
275   optionsPane.saveState(optionsPaneState[$-1].nfo);
279 final void restoreCurrentPane () {
280   if (optionsPane) optionsPane.setupHotkeys(); // why not?
281   if (!optionsPane || !optionsPane.id) return;
282   foreach (ref auto psv; optionsPaneState) {
283     if (psv.id == optionsPane.id) {
284       optionsPane.restoreState(psv.nfo);
285       return;
286     }
287   }
291 // ////////////////////////////////////////////////////////////////////////// //
292 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
293   if (!it.tagClass) return;
294   if (class!MapObject(it.tagClass)) {
295     level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
296     it.owner.closeMe = true;
297   }
301 // ////////////////////////////////////////////////////////////////////////// //
302 transient array!(class!MapObject) cheatItemsList;
305 final void fillCheatItemsList () {
306   cheatItemsList.length = 0;
307   cheatItemsList[$] = ItemProjectileArrow;
308   cheatItemsList[$] = ItemWeaponShotgun;
309   cheatItemsList[$] = ItemWeaponAshShotgun;
310   cheatItemsList[$] = ItemWeaponPistol;
311   cheatItemsList[$] = ItemWeaponMattock;
312   cheatItemsList[$] = ItemWeaponMachete;
313   cheatItemsList[$] = ItemWeaponWebCannon;
314   cheatItemsList[$] = ItemWeaponSceptre;
315   cheatItemsList[$] = ItemWeaponBow;
316   cheatItemsList[$] = ItemBones;
317   cheatItemsList[$] = ItemFakeBones;
318   cheatItemsList[$] = ItemFishBone;
319   cheatItemsList[$] = ItemRock;
320   cheatItemsList[$] = ItemJar;
321   cheatItemsList[$] = ItemSkull;
322   cheatItemsList[$] = ItemGoldenKey;
323   cheatItemsList[$] = ItemGoldIdol;
324   cheatItemsList[$] = ItemCrystalSkull;
325   cheatItemsList[$] = ItemShellSingle;
326   cheatItemsList[$] = ItemChest;
327   cheatItemsList[$] = ItemCrate;
328   cheatItemsList[$] = ItemLockedChest;
329   cheatItemsList[$] = ItemDice;
330   cheatItemsList[$] = ItemBasketBall;
334 final UIPane createCheatItemsPane () {
335   if (!level.player) return none;
337   UIPane pane = SpawnObject(UIPane);
338   pane.id = 'Items';
339   pane.sprStore = sprStore;
341   pane.width = 320*3-64;
342   pane.height = 240*3-64;
344   foreach (auto ipk; cheatItemsList) {
345     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
346     it.tagClass = ipk;
347   }
349   //optionsPaneOfs.x = 100;
350   //optionsPaneOfs.y = 50;
352   return pane;
356 // ////////////////////////////////////////////////////////////////////////// //
357 transient array!(class!MapObject) cheatEnemiesList;
360 final void fillCheatEnemiesList () {
361   cheatEnemiesList.length = 0;
362   cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
363   cheatEnemiesList[$] = EnemyBat;
364   cheatEnemiesList[$] = EnemySpiderHang;
365   cheatEnemiesList[$] = EnemySpider;
366   cheatEnemiesList[$] = EnemySnake;
367   cheatEnemiesList[$] = EnemyCaveman;
368   cheatEnemiesList[$] = EnemySkeleton;
369   cheatEnemiesList[$] = MonsterShopkeeper;
370   cheatEnemiesList[$] = EnemyZombie;
371   cheatEnemiesList[$] = EnemyVampire;
372   cheatEnemiesList[$] = EnemyFrog;
373   cheatEnemiesList[$] = EnemyGreenFrog;
374   cheatEnemiesList[$] = EnemyFireFrog;
375   cheatEnemiesList[$] = EnemyMantrap;
376   cheatEnemiesList[$] = EnemyScarab;
377   cheatEnemiesList[$] = EnemyFloater;
378   cheatEnemiesList[$] = EnemyBlob;
379   cheatEnemiesList[$] = EnemyMonkey;
380   cheatEnemiesList[$] = EnemyGoldMonkey;
381   cheatEnemiesList[$] = EnemyAlien;
382   cheatEnemiesList[$] = EnemyYeti;
383   cheatEnemiesList[$] = EnemyHawkman;
384   cheatEnemiesList[$] = EnemyUFO;
385   cheatEnemiesList[$] = EnemyYetiKing;
389 final UIPane createCheatEnemiesPane () {
390   if (!level.player) return none;
392   UIPane pane = SpawnObject(UIPane);
393   pane.id = 'Enemies';
394   pane.sprStore = sprStore;
396   pane.width = 320*3-64;
397   pane.height = 240*3-64;
399   foreach (auto ipk; cheatEnemiesList) {
400     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
401     it.tagClass = ipk;
402   }
404   //optionsPaneOfs.x = 100;
405   //optionsPaneOfs.y = 50;
407   return pane;
411 // ////////////////////////////////////////////////////////////////////////// //
412 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
415 final void fillCheatPickupList () {
416   cheatPickupList.length = 0;
417   cheatPickupList[$] = ItemPickupBombBag;
418   cheatPickupList[$] = ItemPickupBombBox;
419   cheatPickupList[$] = ItemPickupPaste;
420   cheatPickupList[$] = ItemPickupRopePile;
421   cheatPickupList[$] = ItemPickupShellBox;
422   cheatPickupList[$] = ItemPickupAnkh;
423   cheatPickupList[$] = ItemPickupCape;
424   cheatPickupList[$] = ItemPickupJetpack;
425   cheatPickupList[$] = ItemPickupUdjatEye;
426   cheatPickupList[$] = ItemPickupCrown;
427   cheatPickupList[$] = ItemPickupKapala;
428   cheatPickupList[$] = ItemPickupParachute;
429   cheatPickupList[$] = ItemPickupCompass;
430   cheatPickupList[$] = ItemPickupSpectacles;
431   cheatPickupList[$] = ItemPickupGloves;
432   cheatPickupList[$] = ItemPickupMitt;
433   cheatPickupList[$] = ItemPickupJordans;
434   cheatPickupList[$] = ItemPickupSpringShoes;
435   cheatPickupList[$] = ItemPickupSpikeShoes;
436   cheatPickupList[$] = ItemPickupTeleporter;
440 final UIPane createCheatPickupsPane () {
441   if (!level.player) return none;
443   UIPane pane = SpawnObject(UIPane);
444   pane.id = 'Pickups';
445   pane.sprStore = sprStore;
447   pane.width = 320*3-64;
448   pane.height = 240*3-64;
450   foreach (auto ipk; cheatPickupList) {
451     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
452     it.tagClass = ipk;
453   }
455   //optionsPaneOfs.x = 100;
456   //optionsPaneOfs.y = 50;
458   return pane;
462 // ////////////////////////////////////////////////////////////////////////// //
463 transient int instantGhost;
465 final UIPane createCheatFlagsPane () {
466   UIPane pane = SpawnObject(UIPane);
467   pane.id = 'CheatFlags';
468   pane.sprStore = sprStore;
470   pane.width = 320*3-64;
471   pane.height = 240*3-64;
473   instantGhost = 0;
475   UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
476   UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
477   UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
478   UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
479   UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
480   //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
481   UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
482   UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
483   UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
484   UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
485   UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
486   UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
487   //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
488   UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
489   UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
490   UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
491   UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
492   UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
494   optionsPaneOfs.x = 100;
495   optionsPaneOfs.y = 50;
497   return pane;
501 final UIPane createOptionsPane () {
502   UIPane pane = SpawnObject(UIPane);
503   pane.id = 'Options';
504   pane.sprStore = sprStore;
506   pane.width = 320*3-64;
507   pane.height = 240*3-64;
510   // this is buggy
511   //!UICheckBox.Create(pane, &config.useFrozenRegion, "FROZEN REGION", "OFF-SCREEN ENTITIES ARE PAUSED TO IMPROVE PERFORMANCE. LEAVE THIS ENABLED IF YOU DON'T KNOW WHAT IT IS. DO A WEB SEARCH FOR 'SPELUNKY FROZEN REGION' FOR A FULL EXPLANATION. THE YASM README FILE ALSO HAS INFO.");
514   UILabel.Create(pane, "VISUAL OPTIONS");
515     UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
516     UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
517     UICheckBox.Create(pane, &config.alwaysCenterPlayer, "ALWAYS KEEP PLAYER IN CENTER", "ALWAYS KEEP PLAYER IN THE CENTER OF THE SCREEN. IF THIS OPTION IS UNSET, PLAYER WILL BE ALLOWED TO MOVE SLIGHTLY BEFORE THE VIEWPORT STARTS FOLLOWING HIM (THIS IS HOW IT WAS DONE IN THE ORIGINAL GAME).");
518     UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
519     auto startfs = UICheckBox.Create(pane, &config.startFullscreen, "START FULLSCREEN", "START THE GAME IN FULLSCREEN MODE?");
520     startfs.onValueChanged = delegate void (int newval) {
521       closeVideo();
522       fullscreen = newval;
523       initializeVideo();
524     };
525     auto fsmode = UIIntEnum.Create(pane, &config.fsmode, 1, 2, "FULLSCREEN MODE: ", "YOU CAN CHOOSE EITHER REAL FULLSCREEN MODE, OR SCALED. USUALLY, SCALED WORKS BETTER.");
526     fsmode.names[$] = "REAL";
527     fsmode.names[$] = "SCALED";
528     fsmode.onValueChanged = delegate void (int newval) {
529       if (fullscreen) {
530         closeVideo();
531         initializeVideo();
532       }
533     };
534     auto fsres = UIIntEnum.Create(pane, &config.realfsres, 0, GameConfig::RealFSModes.MAX, "FULLSCREEN RESOLUTION: ", "SELECT RESOLUTION FOR REAL FULLSCREEN MODE.");
535     fsres.names[$] = "1024x768";
536     fsres.names[$] = "1280x960";
537     fsres.names[$] = "1280x1024";
538     fsres.names[$] = "1600x1200";
539     fsres.names[$] = "1680x1050";
540     fsres.names[$] = "1920x1080";
541     fsres.names[$] = "1920x1200";
544   UILabel.Create(pane, "");
545   UILabel.Create(pane, "HUD OPTIONS");
546     UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
547     UICheckBox.Create(pane, &config.scumSmallHud, "SMALLER HUD", "THE INFORMATION AT THE TOP OF THE SCREEN SHOWING YOUR HEARTS, BOMBS, ROPES AND MONEY WILL BE REDUCED IN SIZE.");
548     auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
549     halpha.step = 10;
551     auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
552     ialpha.step = 10;
555   UILabel.Create(pane, "");
556   UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
557     //!UICheckBox.Create(pane, &config.optSeedComputer, "SHOW SEED COMPUTER", "SHOWS SEED COMPUTER IN TITLE ROOM. IT SHOULD PRODUCE REPEATEBLE ROOMS, BUT ACTUALLY IT IS OLD AND BROKEN, SO IT DOESN'T WORK AS EXPECTED.");
558     //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
559     UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
560     UICheckBox.Create(pane, &config.useDoorWithButton, "BUTTON TO USE DOOR", "WITH THIS OPTION ENABLED YOU WILL NEED TO PRESS THE 'PURCHASE' BUTTON INSTEAD OF 'UP' TO USE DOORS. RECOMMENDED FOR GAMEPAD USERS.");
561     UICheckBox.Create(pane, &config.toggleRunAnywhere, "EASY WALK/RUN SWITCH", "ALLOWS PLAYER TO CONTROL SPEED IN MID-AIR WITH THE RUN KEY LIKE SPELUNKY HD, INSTEAD OF KEEPING THE SAME AIR SPEED UNTIL TOUCHING THE GROUND AGAIN.");
562     UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
563     UICheckBox.Create(pane, &config.woodSpikes, "WOOD SPIKES", "REPLACES METAL SPIKES WITH WOODEN ONES THAT ALLOW YOU TO SAFELY DROP FROM ONE TILE ABOVE, AS IN AN EARLY VERSION OF THE GAME. DOES NOT AFFECT CUSTOM LEVELS.");
564     UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
567   UILabel.Create(pane, "");
568   UILabel.Create(pane, "GAMEPLAY OPTIONS");
569     UICheckBox.Create(pane, &config.scumFlipHold, "HOLD ITEM ON FLIP", "ALLOWS YOU TO FLIP DOWN TO HANG FROM A LEDGE WITHOUT BEING FORCED TO DROP ITEMS THAT COULD BE HELD WITH ONE HAND. HEAVY ITEMS WILL ALWAYS BE DROPPED.");
570     UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
571     UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
572     UICheckBox.Create(pane, &config.nudge, "MELEE ITEMS", "ALLOWS HITTING LOOSE ITEMS WITH MELEE WEAPONS TO MOVE THEM SLIGHTLY. WITH THE RIGHT TIMING YOU CAN HIT FLYING ARROWS TO DEFEND YOURSELF!");
573     UICheckBox.Create(pane, &config.scumSpringShoesReduceFallDamage, "SPRING SHOES EFFECT", "WITH THIS OPTION ENABLED, THE SPRING SHOES WILL ALLOW YOU TO FALL FARTHER THAN NORMAL BEFORE YOU TAKE DAMAGE.");
574     UICheckBox.Create(pane, &config.optSGAmmo, "SHOTGUN NEEDS AMMO", "SHOTGUNS WILL REQUIRE SHELLS TO SHOOT. NEW SHOTGUN HAS 7 SHELLS. YOU CAN ALSO FOUND SHELLS IN JARS, CRATES AND CHESTS.");
575     UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
576     UICheckBox.Create(pane, &config.enemyBreakWeb, "ENEMIES BREAK WEBS", "ALLOWS MOST ENEMIES TO BREAK FREE FROM SPIDER WEBS AFTER A PERIOD OF TIME. SNAKES AND BATS ARE TOO WEAK TO ESCAPE.");
577     UICheckBox.Create(pane, &config.ghostRandom, "RANDOM GHOST DELAY", "THIS OPTION WILL RANDOMIZE THE DELAY UNTIL THE GHOST APPEARS AFTER THE TIME LIMIT BELOW IS REACHED INSTEAD OF USING THE DEFAULT 30 SECONDS. CHANGES EACH LEVEL AND VARIES WITH THE TIME LIMIT YOU SET.");
578     UICheckBox.Create(pane, &config.ghostAtFirstLevel, "GHOST AT FIRST LEVEL", "TURN THIS OPTION ON IF YOU WANT THE GHOST TO BE SPAWNED ON THE FIRST LEVEL.");
579     UICheckBox.Create(pane, &config.optDoubleKiss, "UNHURT DAMSEL KISSES TWICE", "IF YOU WILL BRING UNHURT DAMSEL TO THE EXIT WITHOUT DROPPING HER, SHE WILL KISS YOU TWICE.");
580     UICheckBox.Create(pane, &config.optShopkeeperIdiots, "SHOPKEEPERS ARE IDIOTS", "DO YOU WANT SHOPKEEPERS TO BE A BUNCH OF MORONS, IGNORANT AND UNABLE TO NOTICE ARMED BOMBS?");
581     UIIntEnum.Create(pane, &config.scumClimbSpeed, 1, 3, "CLIMB SPEED:", "ADJUST THE SPEED THAT YOU CLIMB LADDERS, ROPES AND VINES. 1 IS DEFAULT SPEED, 2 IS FAST, AND 3 IS FASTER.");
582     UIIntEnum.Create(pane, &config.enemyMult, 1, 10, "ENEMIES:", "MULTIPLIES THE AMOUNT OF ENEMIES THAT SPAWN IN LEVELS. 1 IS NORMAL. THE SAME SETTING WILL AFFECT NORMAL AND BIZARRE MODES DIFFERENTLY.");
583     UIIntEnum.Create(pane, &config.trapMult, 1, 10,  "TRAPS  :", "MULTIPLIES THE AMOUNT OF TRAPS THAT SPAWN IN LEVELS. 1 IS NORMAL. THE SAME SETTING WILL AFFECT NORMAL AND BIZARRE MODES DIFFERENTLY.");
584     UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
585     UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
586     UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
587     auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
588     rstl.names[$] = "RANDOM";
589     rstl.names[$] = "NORMAL";
590     rstl.names[$] = "BIZARRE";
593   UILabel.Create(pane, "");
594   UILabel.Create(pane, "WHIP OPTIONS");
595     UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
596     auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
597     whiptype.names[$] = "NORMAL";
598     whiptype.names[$] = "LONG";
599     UICheckBox.Create(pane, &global.config.killEnemiesThruWalls, "PENETRATE WALLS", "WITH THIS OPTION ENABLED, YOU WILL BE ABLE TO WHIP ENEMIES THROUGH THE WALLS SOMETIMES. THIS IS HOW IT WORKED IN CLASSIC.");
602   UILabel.Create(pane, "");
603   UILabel.Create(pane, "PLAYER OPTIONS");
604     auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
605     herotype.names[$] = "SPELUNKY GUY";
606     herotype.names[$] = "DAMSEL";
607     herotype.names[$] = "TUNNEL MAN";
610   UILabel.Create(pane, "");
611   UILabel.Create(pane, "CHEAT OPTIONS");
612     UICheckBox.Create(pane, &config.scumUnlocked, "UNLOCK SHORTCUTS", "OPENS ALL DOORS IN THE SHORTCUT HOUSE AND HI-SCORES ROOM. DOES NOT AFFECT YOUR SCORES OR UNLOCK PROGRESS. DISABLE THIS AGAIN TO REVEAL WHAT YOU HAVE LEGITIMATELY UNLOCKED.");
613     auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
614     plrlit.names[$] = "NEVER";
615     plrlit.names[$] = "FORCED DARKNESS";
616     plrlit.names[$] = "ALWAYS";
617     UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
618     auto rdark = UIIntEnum.Create(pane, &config.scumDarkness, 0, 2, "DARK :", "THE CHANCE OF GETTING A DARK LEVEL. THE BLACK MARKET AND FINAL BOSS LEVELS WILL BE LIT EVEN IF THIS OPTION IS SET TO 'ALWAYS'.");
619     rdark.names[$] = "NEVER";
620     rdark.names[$] = "DEFAULT";
621     rdark.names[$] = "ALWAYS";
622     auto rghost = UIIntEnum.Create(pane, &config.scumGhost, -30, 960, "GHOST:", "HOW LONG UNTIL THE 'A CHILL RUNS DOWN YOUR SPINE!' WARNING APPEARS. 30 SECONDS AFTER THAT, THE GHOST APPEARS. DEFAULT TIME IS 2 MINUTES. 'INSTANT' WILL SUMMON THE GHOST AT LEVEL START WITHOUT THE 30 SECOND DELAY.");
623     rghost.step = 30;
624     rghost.getNameCB = delegate string (int val) {
625       if (val < 0) return "INSTANT";
626       if (val == 0) return "NEVER";
627       if (val < 120) return va("%d SEC", val);
628       if (val%60 == 0) return va("%d MIN", val/60);
629       if (val%60 == 30) return va("%d.5 MIN", val/60);
630       return va("%d MIN, %d SEC", val/60, val%60);
631     };
632     UIIntEnum.Create(pane, &config.scumFallDamage, 1, 10, "FALL DAMAGE: ", "ADJUST THE MULTIPLIER FOR THE AMOUNT OF DAMAGE YOU TAKE FROM LONG FALLS. 1 IS DEFAULT, 2 IS DOUBLE DAMAGE, ETC.");
634   UILabel.Create(pane, "");
635   UILabel.Create(pane, "CHEAT START OPTIONS");
636     UICheckBox.Create(pane, &config.scumBallAndChain, "BALL AND CHAIN", "PLAYER WILL ALWAYS BE WEARING THE BALL AND CHAIN. YOU CAN GAIN OR LOSE FAVOR WITH KALI AS NORMAL, BUT THE BALL AND CHAIN WILL REMAIN. FOR THOSE THAT WANT AN EXTRA CHALLENGE.");
637     UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
638     UIIntEnum.Create(pane, &config.scumStartLife,  1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
639     UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
640     UIIntEnum.Create(pane, &config.scumStartRope,  1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
643   UILabel.Create(pane, "");
644   UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
645     auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC  : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
646     mm.names[$] = "SILENCE";
647     mm.names[$] = "RESTART";
648     mm.names[$] = "DON'T TOUCH";
650     mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
651     //mm.names[$] = "SILENCE";
652     mm.names[$] = "RESTART";
653     mm.names[$] = "DON'T TOUCH";
656   //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
657   /*
658   swstereo.onValueChanged = delegate void (int newval) {
659     SoundSystem.SwapStereo = newval;
660   };
661   */
663   UILabel.Create(pane, "");
664   UILabel.Create(pane, "SOUND CONTROL CENTER");
665     auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
666     rmusonoff.onValueChanged = delegate void (int newval) {
667       global.restartMusic();
668     };
670     UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
672     auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
673     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
675     rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
676     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
679   saveOptionsDG = delegate void () {
680     writeln("saving options");
681     saveGameOptions();
682   };
683   optionsPaneOfs.x = 42;
684   optionsPaneOfs.y = 0;
686   return pane;
690 final void createBindingsControl (UIPane pane, int keyidx) {
691   string kname, khelp;
692   switch (keyidx) {
693     case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
694     case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
695     case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
696     case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
697     case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
698     case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
699     case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
700     case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
701     case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
702     case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
703     case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
704     default: return;
705   }
706   int arridx = GameConfig.getKeyIndex(keyidx);
707   UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
711 final UIPane createBindingsPane () {
712   UIPane pane = SpawnObject(UIPane);
713   pane.id = 'KeyBindings';
714   pane.sprStore = sprStore;
716   pane.width = 320*3-64;
717   pane.height = 240*3-64;
719   createBindingsControl(pane, GameConfig::Key.Left);
720   createBindingsControl(pane, GameConfig::Key.Right);
721   createBindingsControl(pane, GameConfig::Key.Up);
722   createBindingsControl(pane, GameConfig::Key.Down);
723   createBindingsControl(pane, GameConfig::Key.Jump);
724   createBindingsControl(pane, GameConfig::Key.Run);
725   createBindingsControl(pane, GameConfig::Key.Attack);
726   createBindingsControl(pane, GameConfig::Key.Switch);
727   createBindingsControl(pane, GameConfig::Key.Pay);
728   createBindingsControl(pane, GameConfig::Key.Bomb);
729   createBindingsControl(pane, GameConfig::Key.Rope);
731   saveOptionsDG = delegate void () {
732     writeln("saving keys");
733     saveKeyboardBindings();
734   };
735   optionsPaneOfs.x = 120;
736   optionsPaneOfs.y = 140;
738   return pane;
742 // ////////////////////////////////////////////////////////////////////////// //
743 void clearGameMovement () {
744   debugMovement = SpawnObject(DebugSessionMovement);
745   debugMovement.playconfig = SpawnObject(GameConfig);
746   debugMovement.playconfig.copyGameplayConfigFrom(config);
747   debugMovement.resetReplay();
751 void saveGameMovement (string fname, optional bool packit) {
752   if (debugMovement) appSaveOptions(debugMovement, fname, packit);
753   saveMovementLastTime = GetTickCount();
757 void loadGameMovement (string fname) {
758   delete debugMovement;
759   debugMovement = appLoadOptions(DebugSessionMovement, fname);
760   debugMovement.resetReplay();
761   if (debugMovement) {
762     delete origStats;
763     origStats = level.stats;
764     origStats.global = none;
765     level.stats = SpawnObject(GameStats);
766     level.stats.global = global;
767     delete origConfig;
768     origConfig = config;
769     config = debugMovement.playconfig;
770     global.config = config;
771     global.saveSeeds(origSeeds);
772   }
776 void stopReplaying () {
777   if (debugMovement) {
778     global.restoreSeeds(origSeeds);
779   }
780   delete debugMovement;
781   saveGameSession = false;
782   replayGameSession = false;
783   doGameSavingPlaying = Replay.None;
784   if (origStats) {
785     delete level.stats;
786     origStats.global = global;
787     level.stats = origStats;
788     origStats = none;
789   }
790   if (origConfig) {
791     delete config;
792     config = origConfig;
793     global.config = origConfig;
794     origConfig = none;
795   }
799 // ////////////////////////////////////////////////////////////////////////// //
800 final bool saveGame (string gmname) {
801   return appSaveOptions(level, gmname);
805 final bool loadGame (string gmname) {
806   auto olddel = GC_ImmediateDelete;
807   GC_ImmediateDelete = false;
808   bool res = false;
809   auto stats = level.stats;
810   level.stats = none;
812   auto lvl = appLoadOptions(GameLevel, gmname);
813   if (lvl) {
814     //lvl.global.config = config;
815     delete level;
816     delete global;
818     level = lvl;
819     level.loserGPU = loserGPU;
820     global = level.global;
821     global.config = config;
823     level.sprStore = sprStore;
824     level.bgtileStore = bgtileStore;
827     level.onBeforeFrame = &beforeNewFrame;
828     level.onAfterFrame = &afterNewFrame;
829     level.onInterFrame = &interFrame;
830     level.onLevelExitedCB = &levelExited;
831     level.onCameraTeleported = &cameraTeleportedCB;
833     //level.viewWidth = GLVideo.screenWidth;
834     //level.viewHeight = GLVideo.screenHeight;
835     level.viewWidth = 320*3;
836     level.viewHeight = 240*3;
838     level.onLoaded();
839     level.centerViewAtPlayer();
840     teleportCameraAt(level.viewStart);
842     recalcCameraCoords(0);
844     res = true;
845   }
846   level.stats = stats;
847   level.stats.global = level.global;
849   GC_ImmediateDelete = olddel;
850   GC_CollectGarbage(true); // destroy delayed objects too
851   return res;
855 // ////////////////////////////////////////////////////////////////////////// //
856 float lastThinkerTime;
857 int replaySkipFrame = 0;
860 final void onTimePasses () {
861   float curTime = GetTickCount();
862   if (lastThinkerTime > 0) {
863     if (curTime < lastThinkerTime) {
864       writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
865       lastThinkerTime = curTime;
866       return;
867     }
868     if (replayFastForward && replaySkipFrame) {
869       level.accumTime = 0;
870       lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
871       replaySkipFrame = 0;
872     }
873     level.processThinkers(curTime-lastThinkerTime);
874   }
875   lastThinkerTime = curTime;
879 final void resetFramesAndForceOne () {
880   float curTime = GetTickCount();
881   lastThinkerTime = curTime;
882   level.accumTime = 0;
883   auto wasPaused = level.gamePaused;
884   level.gamePaused = false;
885   if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
886   level.processThinkers(GameLevel::FrameTime);
887   level.gamePaused = wasPaused;
888   //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
892 // ////////////////////////////////////////////////////////////////////////// //
893 private float currFrameDelta; // so level renderer can properly interpolate the player
894 private GameLevel::IVec2D camPrev, camCurr;
895 private GameLevel::IVec2D camShake;
896 private GameLevel::IVec2D viewCameraPos;
899 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
900   camPrev.x = pos.x;
901   camPrev.y = pos.y;
902   camCurr.x = pos.x;
903   camCurr.y = pos.y;
904   viewCameraPos.x = pos.x;
905   viewCameraPos.y = pos.y;
906   camShake.x = 0;
907   camShake.y = 0;
911 // call `recalcCameraCoords()` to get real camera coords after this
912 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
913   // check if camera is moved too far, and teleport it
914   if (doTeleport ||
915       (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
916        abs(camCurr.y-pos.y)/global.scale >= 16*4))
917   {
918     teleportCameraAt(pos);
919   } else {
920     camPrev.x = camCurr.x;
921     camPrev.y = camCurr.y;
922     camCurr.x = pos.x;
923     camCurr.y = pos.y;
924   }
925   camShake.x = level.shakeDir.x*global.scale;
926   camShake.y = level.shakeDir.y*global.scale;
930 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
931   currFrameDelta = frameDelta;
932   viewCameraPos.x = roundi(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
933   viewCameraPos.y = roundi(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
935   viewCameraPos.x += camShake.x;
936   viewCameraPos.y += camShake.y;
940 GameLevel::SavedKeyState savedKeyState;
942 final void pauseGame () {
943   if (!level.gamePaused) {
944     if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
945     level.gamePaused = true;
946     global.pauseAllSounds();
947   }
951 final void unpauseGame () {
952   if (level.gamePaused) {
953     if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
954     level.gamePaused = false;
955     level.gameShowHelp = false;
956     level.gameHelpScreen = 0;
957     //lastThinkerTime = 0;
958     global.resumeAllSounds();
959   }
960   pauseRequested = false;
961   helpRequested = false;
962   showHelp = false;
966 final void beforeNewFrame (bool frameSkip) {
967   /*
968   if (freeRide) {
969     level.disablePlayerThink = true;
971     int delta = 2;
972     if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
973     if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
974     if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
976     if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
977     if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
978     if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
979     if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
980   } else {
981     level.disablePlayerThink = false;
982     level.fixCamera();
983   }
984   */
985   level.fixCamera();
987   if (!level.gamePaused) {
988     // save seeds for afterframe processing
989     /*
990     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
991       debugMovement.otherSeed = global.globalOtherSeed;
992       debugMovement.roomSeed = global.globalRoomSeed;
993     }
994     */
996     if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
998 #ifdef BIGGER_REPLAY_DATA
999     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
1000       debugMovement.keypresses.length += 1;
1001       level.keysSaveState(debugMovement.keypresses[$-1]);
1002       debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
1003       debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
1004     }
1005 #endif
1007     if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
1008 #ifdef BIGGER_REPLAY_DATA
1009       if (debugMovement.keypos < debugMovement.keypresses.length) {
1010         level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
1011         global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
1012         global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
1013         ++debugMovement.keypos;
1014       }
1015 #else
1016       for (;;) {
1017         int kbidx;
1018         bool down;
1019         auto code = debugMovement.getKey(out kbidx, out down);
1020         if (code == DebugSessionMovement::END_OF_RECORD) {
1021           // do this in main loop, so we can view totals
1022           //stopReplaying();
1023           break;
1024         }
1025         if (code == DebugSessionMovement::END_OF_FRAME) {
1026           break;
1027         }
1028         if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1029         level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1030       }
1031 #endif
1032     }
1033   }
1037 final void afterNewFrame (bool frameSkip) {
1038   if (!replayFastForward) replaySkipFrame = 0;
1040   if (level.gamePaused) return;
1042   if (!level.gamePaused) {
1043     if (doGameSavingPlaying != Replay.None) {
1044       if (doGameSavingPlaying == Replay.Saving) {
1045         replayFastForward = false; // just in case
1046 #ifndef BIGGER_REPLAY_DATA
1047         debugMovement.addEndOfFrame();
1048 #endif
1049         auto stt = GetTickCount();
1050         if (stt-saveMovementLastTime >= dbgSessionSaveIntervalInSeconds) saveGameMovement(dbgSessionMovementFileName);
1051       } else if (doGameSavingPlaying == Replay.Replaying) {
1052         if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1053           replaySkipFrame = 1;
1054         }
1055       }
1056     }
1057   }
1059   //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1060   //SoundSystem.UpdateSounds();
1062   //if (!freeRide) level.fixCamera();
1063   setNewCameraPos(level.viewStart);
1064   /*
1065   prevCameraX = currCameraX;
1066   prevCameraY = currCameraY;
1067   currCameraX = level.cameraX;
1068   currCameraY = level.cameraY;
1069   // disable camera interpolation if the screen is shaking
1070   if (level.shakeX|level.shakeY) {
1071     prevCameraX = currCameraX;
1072     prevCameraY = currCameraY;
1073     return;
1074   }
1075   // disable camera interpolation if it moves too far away
1076   if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1077   if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1078   */
1079   recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1081   if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1082     pauseRequested = false;
1083     pauseGame();
1084     if (helpRequested) {
1085       helpRequested = false;
1086       level.gameShowHelp = true;
1087       level.gameHelpScreen = 0;
1088       showHelp = 2;
1089     } else {
1090       if (!showHelp) showHelp = true;
1091     }
1092     writeln("active objects in level: ", level.activeItemsCount);
1093     return;
1094   }
1098 final void interFrame (float frameDelta) {
1099   if (!config.interpolateMovement) return;
1100   recalcCameraCoords(frameDelta);
1104 final void cameraTeleportedCB () {
1105   teleportCameraAt(level.viewStart);
1106   recalcCameraCoords(0);
1110 // ////////////////////////////////////////////////////////////////////////// //
1111 #ifdef MASK_TEST
1112 final void setColorByIdx (bool isset, int col) {
1113   if (col == -666) {
1114     // missed collision: red
1115     GLVideo.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1116   } else if (col == -999) {
1117     // superfluous collision: blue
1118     GLVideo.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1119   } else if (col <= 0) {
1120     // no collision: yellow
1121     GLVideo.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1122   } else if (col > 0) {
1123     // collision: green
1124     GLVideo.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1125   }
1129 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1130   if (!frm) return;
1131   CollisionMask cm = CollisionMask.Create(frm, false);
1132   if (!cm) return;
1133   int scale = global.config.scale;
1134   int bx0, by0, bx1, by1;
1135   frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1136   GLVideo.color = 0x7f_00_00_ff;
1137   GLVideo.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1138   if (!cm.isEmptyMask) {
1139     //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1140     foreach (int iy; 0..cm.height) {
1141       foreach (int ix; 0..cm.width) {
1142         int v = cm.mask[ix, iy];
1143         foreach (int dx; 0..32) {
1144           int xx = ix*32+dx;
1145           if (v < 0) {
1146             GLVideo.color = 0x3f_00_ff_00;
1147             GLVideo.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1148           }
1149           v <<= 1;
1150         }
1151       }
1152     }
1153   } else {
1154     // bounding box
1155     /+
1156     foreach (int iy; 0..frm.tex.height) {
1157       foreach (int ix; 0..(frm.tex.width+31)/31) {
1158         foreach (int dx; 0..32) {
1159           int xx = ix*32+dx;
1160           //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1161           if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1162             setColorByIdx(true, col);
1163             if (col <= 0) GLVideo.color = 0xaf_ff_ff_00;
1164           } else {
1165             GLVideo.color = 0xaf_00_ff_00;
1166           }
1167           GLVideo.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1168         }
1169       }
1170     }
1171     +/
1172     /*
1173     if (frm.bw > 0 && frm.bh > 0) {
1174       setColorByIdx(true, col);
1175       GLVideo.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1176       GLVideo.color = 0xff_00_00;
1177       GLVideo.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1178     }
1179     */
1180   }
1181   delete cm;
1183 #endif
1186 // ////////////////////////////////////////////////////////////////////////// //
1187 transient int drawStats;
1188 transient array!int statsTopItem;
1191 final int totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1192   auto sa = string(a.objName).toUpperCase;
1193   auto sb = string(b.objName).toUpperCase;
1194   if (sa < sb) return -1;
1195   if (sa > sb) return 1;
1196   return 0;
1200 final int getStatsTopItem () {
1201   return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1205 final void setStatsTopItem (int val) {
1206   if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1207   statsTopItem[drawStats] = val;
1211 final void resetStatsTopItem () {
1212   setStatsTopItem(0);
1216 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1217   sprStore.loadFont('sFontSmall');
1218   currX = 64;
1219   currY = 34;
1223 final int calcStatsVisItems () {
1224   int scale = 3;
1225   int currX, currY;
1226   statsDrawGetStartPosLoadFont(currX, currY);
1227   int endY = level.viewHeight-(currY*2);
1228   return max(1, endY/sprStore.getFontHeight(scale));
1232 int getStatsItemCount () {
1233   switch (drawStats) {
1234     case 2: return level.stats.totalKills.length;
1235     case 3: return level.stats.totalDeaths.length;
1236     case 4: return level.stats.totalCollected.length;
1237   }
1238   return -1;
1242 final void statsMoveUp () {
1243   int count = getStatsItemCount();
1244   if (count < 0) return;
1245   int visItems = calcStatsVisItems();
1246   if (count <= visItems) { resetStatsTopItem(); return; }
1247   int top = getStatsTopItem();
1248   if (!top) return;
1249   setStatsTopItem(top-1);
1253 final void statsMoveDown () {
1254   int count = getStatsItemCount();
1255   if (count < 0) return;
1256   int visItems = calcStatsVisItems();
1257   if (count <= visItems) { resetStatsTopItem(); return; }
1258   int top = getStatsTopItem();
1259   //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1260   top = clamp(top+1, 0, count-visItems);
1261   setStatsTopItem(top);
1265 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1266   arr.sort(&totalsNameCmpCB);
1267   int scale = 3;
1269   int currX, currY;
1270   statsDrawGetStartPosLoadFont(currX, currY);
1272   int endY = level.viewHeight-(currY*2);
1273   int visItems = calcStatsVisItems();
1275   if (arr.length <= visItems) resetStatsTopItem();
1277   int topItem = getStatsTopItem();
1279   // "upscroll" mark
1280   if (topItem > 0) {
1281     GLVideo.color = 0x3f_ff_ff_00;
1282     auto spr = sprStore['sPageUp'];
1283     spr.frames[0].blitAt(currX-28, currY, scale);
1284   }
1286   // "downscroll" mark
1287   if (topItem+visItems < arr.length) {
1288     GLVideo.color = 0x3f_ff_ff_00;
1289     auto spr = sprStore['sPageDown'];
1290     spr.frames[0].blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1291   }
1293   GLVideo.color = 0xff_ff_00;
1294   int hiColor = 0x00_ff_00;
1295   int hiColor1 = 0xf_ff_ff;
1297   int it = topItem;
1298   while (it < arr.length && visItems-- > 0) {
1299     sprStore.renderTextWithHighlight(currX, currY, va("%s |%s| ~%d~ TIME%s", pfx, string(arr[it].objName).toUpperCase, arr[it].count, (arr[it].count != 1 ? "S" : "")), scale, hiColor, hiColor1);
1300     currY += sprStore.getFontHeight(scale);
1301     ++it;
1302   }
1306 void drawStatsScreen () {
1307   int deathCount, killCount, collectCount;
1309   sprStore.loadFont('sFontSmall');
1311   GLVideo.color = 0xff_ff_ff;
1312   level.drawTextAtS3Centered(240-2-8, "ESC-RETURN  F10-QUIT  CTRL+DEL-SUICIDE");
1313   level.drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
1315   GLVideo.color = 0xff_ff_00;
1316   int hiColor = 0x00_ff_00;
1318   switch (drawStats) {
1319     case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1320     case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1321     case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1322   }
1324   if (drawStats > 1) {
1325     // turn off
1326     foreach (ref auto i; statsTopItem) i = 0;
1327     drawStats = 0;
1328     return;
1329   }
1331   foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1332   foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1333   foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1335   int currX = 64;
1336   int currY = 96;
1337   int scale = 3;
1339   sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1340   currY += sprStore.getFontHeight(scale);
1342   int gw = level.stats.gamesWon;
1343   sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1344   currY += sprStore.getFontHeight(scale);
1346   sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1347   currY += sprStore.getFontHeight(scale);
1349   sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1350   currY += sprStore.getFontHeight(scale);
1352   sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1353   currY += sprStore.getFontHeight(scale);
1355   sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1356   currY += sprStore.getFontHeight(scale);
1358   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1359   currY += sprStore.getFontHeight(scale);
1361   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1362   currY += sprStore.getFontHeight(scale);
1364   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1365   currY += sprStore.getFontHeight(scale);
1367   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1368   currY += sprStore.getFontHeight(scale);
1370   int gs = level.stats.totalGhostSummoned;
1371   sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1372   currY += sprStore.getFontHeight(scale);
1374   currY += sprStore.getFontHeight(scale);
1375   sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1376   currY += sprStore.getFontHeight(scale);
1380 bool frameRendered = true;
1382 void onSwapped () {
1383   if (level) level.keysNextFrame();
1384   frameRendered = false;
1388 void onDraw () {
1389   frameRendered = true;
1391   if (GLVideo.frameTime == 0) {
1392     onTimePasses();
1393     GLVideo.requestRefresh();
1394   }
1396   if (!level) return;
1398   if (level.framesProcessedFromLastClear < 1) return;
1399   calcMouseMapCoords();
1401   GLVideo.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1402   GLVideo.clearScreen();
1403   GLVideo.stencil = false;
1404   GLVideo.color = 0xff_ff_ff;
1405   GLVideo.textureFiltering = false;
1406   // don't touch framebuffer alpha
1407   GLVideo.colorMask = GLVideo::CMask.Colors;
1409   GLVideo::ScissorRect scsave;
1410   bool doRestoreGL = false;
1412   /*
1413   if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1414     doRestoreGL = true;
1415     GLVideo.getScissor(scsave);
1416     GLVideo.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1417     GLVideo.glPushMatrix();
1418     GLVideo.glTranslate(level.viewOffsetX, level.viewOffsetY);
1419   }
1420   */
1422   if (level.viewWidth != GLVideo.screenWidth || level.viewHeight != GLVideo.screenHeight) {
1423     doRestoreGL = true;
1424     float scx = float(GLVideo.screenWidth)/float(level.viewWidth);
1425     float scy = float(GLVideo.screenHeight)/float(level.viewHeight);
1426     float scale = fmin(scx, scy);
1427     int calcedW = trunci(level.viewWidth*scale);
1428     int calcedH = trunci(level.viewHeight*scale);
1429     GLVideo.getScissor(scsave);
1430     int ofsx = (GLVideo.screenWidth-calcedW)/2;
1431     int ofsy = (GLVideo.screenHeight-calcedH)/2;
1432     GLVideo.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1433     GLVideo.glPushMatrix();
1434     GLVideo.glTranslate(ofsx, ofsy);
1435     GLVideo.glScale(scale, scale);
1436   }
1438   //level.viewOffsetX = (GLVideo.screenWidth-320*3)/2;
1439   //level.viewOffsetY = (GLVideo.screenHeight-240*3)/2;
1441   if (fullscreen) {
1442     /*
1443     level.viewOffsetX = 0;
1444     level.viewOffsetY = 0;
1445     GLVideo.glScale(float(GLVideo.screenWidth)/float(level.viewWidth), float(GLVideo.screenHeight)/float(level.viewHeight));
1446     */
1447     /*
1448     float scx = float(GLVideo.screenWidth)/float(level.viewWidth);
1449     float scy = float(GLVideo.screenHeight)/float(level.viewHeight);
1450     GLVideo.glScale(float(GLVideo.screenWidth)/float(level.viewWidth), 1);
1451     */
1452   }
1455   if (allowRender) {
1456     level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1457   }
1459   if (level.gamePaused && showHelp != 2) {
1460     if (mouseLevelX != int.min) {
1461       int scale = level.global.scale;
1462       if (renderMouseRect) {
1463         GLVideo.color = 0xcf_ff_ff_00;
1464         GLVideo.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1465       }
1466       if (renderMouseTile) {
1467         GLVideo.color = 0xaf_ff_00_00;
1468         GLVideo.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1469       }
1470     }
1471   }
1473   switch (doGameSavingPlaying) {
1474     case Replay.Saving:
1475       GLVideo.color = 0x7f_00_ff_00;
1476       sprStore.loadFont('sFont');
1477       sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1478       break;
1479     case Replay.Replaying:
1480       if (level.player && !level.player.dead) {
1481         GLVideo.color = 0x7f_ff_00_00;
1482         sprStore.loadFont('sFont');
1483         sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1484         int th = sprStore.getFontHeight(2);
1485         if (replayFastForward) {
1486           sprStore.loadFont('sFontSmall');
1487           string sstr = va("x%d", replayFastForwardSpeed+1);
1488           sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1489         }
1490       }
1491       break;
1492     default:
1493       if (saveGameSession) {
1494         GLVideo.color = 0x7f_ff_7f_00;
1495         sprStore.loadFont('sFont');
1496         sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1497       }
1498       break;
1499   }
1502   if (level.player && level.player.dead && !showHelp) {
1503     // darken
1504     GLVideo.color = 0x8f_00_00_00;
1505     GLVideo.fillRect(0, 0, level.viewWidth, level.viewHeight);
1506     // draw text
1507     if (drawStats) {
1508       drawStatsScreen();
1509     } else {
1510       if (true /*level.inWinCutscene == 0*/) {
1511         GLVideo.color = 0xff_ff_ff;
1512         sprStore.loadFont('sFontSmall');
1513         string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1514                          "\n"~
1515                          "PRESS $PAY TO RESTART GAME\n"~
1516                          "\n"~
1517                          "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1518                          "\n"~
1519                          "TOTAL PLAYING TIME: |%s|"~
1520                          "",
1521                          (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1522                           level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1523                           level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1524                           level.stats.money),
1525                          GameLevel.time2str(level.stats.playingTime)
1526                         );
1527         kmsg = global.expandString(kmsg);
1528         sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1529       }
1530     }
1531   }
1533 #ifdef MASK_TEST
1534   {
1535     GLVideo.color = 0xff_7f_00;
1536     sprStore.loadFont('sFontSmall');
1537     sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1538     auto spf = smask.frames[maskFrame];
1539     sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1540       spf.xofs, spf.yofs,
1541       spf.bx, spf.by, spf.bw, spf.bh,
1542       (spf.maskEmpty ? "TAN" : "ONA"),
1543       (spf.precise ? "TAN" : "ONA")),
1544       2
1545     );
1546     //spf.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1547     //writeln("pos=(", maskSX, ",", maskSY, ")");
1548     int scale = global.config.scale;
1549     int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1550     int mapX = xofs/scale+maskSX;
1551     int mapY = yofs/scale+maskSY;
1552     mapX -= spf.xofs;
1553     mapY -= spf.yofs;
1554     writeln("==== tiles ====");
1555     /*
1556     level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1557       if (t.spectral || !t.isInstanceAlive) return false;
1558       GLVideo.color = 0x7f_ff_00_00;
1559       GLVideo.fillRect(t.x0*global.config.scale-viewCameraPos.x, t.y0*global.config.scale-viewCameraPos.y, t.width*global.config.scale, t.height*global.config.scale);
1560       auto tsf = t.getSpriteFrame();
1562       auto spf = smask.frames[maskFrame];
1563       int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1564       int mapX = xofs/global.config.scale+maskSX;
1565       int mapY = yofs/global.config.scale+maskSY;
1566       mapX -= spf.xofs;
1567       mapY -= spf.yofs;
1568       //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1569       bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1570       writeln("  tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1571       return false;
1572     });
1573     */
1574     level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1575       GLVideo.color = 0x7f_ff_00_00;
1576       GLVideo.fillRect(t.x0*global.config.scale-viewCameraPos.x, t.y0*global.config.scale-viewCameraPos.y, t.width*global.config.scale, t.height*global.config.scale);
1577       return false;
1578     });
1579     //
1580     drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1581     // mask
1582     GLVideo.color = 0xaf_ff_ff_ff;
1583     spf.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1584     GLVideo.color = 0xff_ff_00;
1585     GLVideo.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1586     // player colbox
1587     {
1588       bool doMirrorSelf;
1589       int fx0, fy0, fx1, fy1;
1590       auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1591       GLVideo.color = 0x7f_00_00_ff;
1592       GLVideo.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1593     }
1594   }
1595 #endif
1597   if (showHelp) {
1598     GLVideo.color = 0x8f_00_00_00;
1599     GLVideo.fillRect(0, 0, level.viewWidth, level.viewHeight);
1600     if (optionsPane) {
1601       optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1602     } else {
1603       if (drawStats) {
1604         drawStatsScreen();
1605       } else {
1606         GLVideo.color = 0xff_ff_00;
1607         //if (showHelp > 1) GLVideo.color = 0xaf_ff_ff_00;
1608         if (showHelp == 1) {
1609           int msx, msy, ww, wh;
1610           GLVideo.getMousePos(out msx, out msy);
1611           GLVideo.getRealWindowSize(out ww, out wh);
1612           if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1613             sprStore.loadFont('sFontSmall');
1614             GLVideo.color = 0xff_ff_00;
1615             sprStore.renderTextWrapped(16, 16, (320-16)*2,
1616               "F1: show this help\n"~
1617               "O : options\n"~
1618               "K : redefine keys\n"~
1619               "I : toggle interpolaion\n"~
1620               "N : create some blood\n"~
1621               "R : generate a new level\n"~
1622               "F : toggle \"Frozen Area\"\n"~
1623               "X : resurrect player\n"~
1624               "Q : teleport to exit\n"~
1625               "D : teleport to damel\n"~
1626               "--------------\n"~
1627               "C : cheat flags menu\n"~
1628               "P : cheat pickup menu\n"~
1629               "E : cheat enemy menu\n"~
1630               "Enter: cheat items menu\n"~
1631               "\n"~
1632               "TAB: toggle 'freeroam' mode\n"~
1633               "",
1634               2);
1635           }
1636         } else {
1637           if (level) level.renderPauseOverlay();
1638         }
1639       }
1640     }
1641     //SoundSystem.UpdateSounds();
1642   }
1643   //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1645   if (doRestoreGL) {
1646     GLVideo.setScissor(scsave);
1647     GLVideo.glPopMatrix();
1648   }
1651   if (TigerEye) {
1652     GLVideo.color = 0xaf_ff_ff_ff;
1653     texTigerEye.blitAt(GLVideo.screenWidth-texTigerEye.width-2, GLVideo.screenHeight-texTigerEye.height-2);
1654   }
1658 // ////////////////////////////////////////////////////////////////////////// //
1659 transient bool gameJustOver;
1660 transient bool waitingForPayRestart;
1663 final void calcMouseMapCoords () {
1664   if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1665     mouseLevelX = int.min;
1666     mouseLevelY = int.min;
1667     return;
1668   }
1669   mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1670   mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1671   //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1675 final void onEvent (ref event_t evt) {
1676   if (evt.type == ev_closequery) { GLVideo.requestQuit(); return; }
1678   if (evt.type == ev_winfocus) {
1679     if (level && !evt.focused) {
1680       level.clearKeys();
1681     }
1682     if (evt.focused) {
1683       //writeln("FOCUS!");
1684       GLVideo.getMousePos(out mouseX, out mouseY);
1685     }
1686     return;
1687   }
1689   if (evt.type == ev_uimouse) {
1690     mouseX = evt.x;
1691     mouseY = evt.y;
1692     calcMouseMapCoords();
1693   }
1695   if (evt.type == ev_keyup && evt.keycode == K_F12) {
1696     if (level) toggleFullscreen();
1697     return;
1698   }
1700   if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1701     writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1702     writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1703   }
1705   if (evt.type == ev_keydown) {
1706     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1707     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1708     renderMouseTile = evt.bCtrl;
1709     renderMouseRect = evt.bAlt;
1710   }
1712   if (evt.type == ev_keyup) {
1713     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1714     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1715     renderMouseTile = evt.bCtrl;
1716     renderMouseRect = evt.bAlt;
1717   }
1719   if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1720     int newScale = evt.keycode-48;
1721     if (global.config.scale != newScale) {
1722       global.config.scale = newScale;
1723       if (level) {
1724         level.fixCamera();
1725         cameraTeleportedCB();
1726       }
1727     }
1728     return;
1729   }
1731 #ifdef MASK_TEST
1732   if (evt.type == ev_uimouse) {
1733     maskSX = evt.x/global.config.scale;
1734     maskSY = evt.y/global.config.scale;
1735     return;
1736   }
1737   if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1738     maskFrame = max(0, maskFrame-1);
1739     return;
1740   }
1741   if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1742     maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1743     return;
1744   }
1745 #endif
1747   if (showHelp) {
1748     if (optionsPane) {
1749       if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1750         saveCurrentPane();
1751         if (saveOptionsDG) saveOptionsDG();
1752         saveOptionsDG = none;
1753         delete optionsPane;
1754         //SoundSystem.UpdateSounds(); // just in case
1755         if (global.hasSpectacles) level.pickedSpectacles();
1756         return;
1757       }
1758       optionsPane.onEvent(evt);
1759       return;
1760     }
1762     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1763     if (evt.type == ev_keydown) {
1764       if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1765       switch (evt.keycode) {
1766         case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1767         case K_F2: if (showHelp != 2) unpauseGame(); return;
1768         case K_F10: GLVideo.requestQuit(); return;
1769         case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1771         case K_BACKQUOTE:
1772           if (evt.bCtrl) {
1773             allowRender = !allowRender;
1774             unpauseGame();
1775             return;
1776           }
1777           break;
1779         case K_UPARROW: case K_PAD8:
1780           if (drawStats) statsMoveUp();
1781           return;
1783         case K_DOWNARROW: case K_PAD2:
1784           if (drawStats) statsMoveDown();
1785           return;
1787         case K_LEFTARROW: case K_PAD4:
1788           if (level && showHelp == 2 && level.gameShowHelp) {
1789             if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1790           }
1791           return;
1793         case K_RIGHTARROW: case K_PAD6:
1794           if (level && showHelp == 2 && level.gameShowHelp) {
1795             level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1796           }
1797           return;
1799         case K_F6: {
1800           // save level
1801           saveGame("level");
1802           unpauseGame();
1803           return;
1804         }
1806         case K_F9: {
1807           // load level
1808           loadGame("level");
1809           resetFramesAndForceOne();
1810           unpauseGame();
1811           return;
1812         }
1814         case K_F5:
1815           if (/*evt.bCtrl &&*/ showHelp != 2) {
1816             global.plife = 99;
1817             unpauseGame();
1818           }
1819           return;
1821         case K_s:
1822           ++drawStats;
1823           return;
1825         case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1826         case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1827         case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1828         case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1829         case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1830         case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1831         //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1832         //case K_j: global.hasJordans = !global.hasJordans; return;
1833         case K_x:
1834           if (/*evt.bCtrl &&*/ showHelp != 2) {
1835             level.resurrectPlayer();
1836             unpauseGame();
1837           }
1838           return;
1839         case K_r:
1840           //writeln("*** ROOM  SEED: ", global.globalRoomSeed);
1841           //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1842           if (evt.bAlt && level.player && level.player.dead) {
1843             saveGameSession = false;
1844             replayGameSession = true;
1845             unpauseGame();
1846             return;
1847           }
1848           if (/*evt.bCtrl &&*/ showHelp != 2) {
1849             if (evt.bShift) global.idol = false;
1850             level.generateLevel();
1851             level.centerViewAtPlayer();
1852             teleportCameraAt(level.viewStart);
1853             resetFramesAndForceOne();
1854           }
1855           return;
1856         case K_m:
1857           global.toggleMusic();
1858           return;
1859         case K_q:
1860           if (/*evt.bCtrl &&*/ showHelp != 2) {
1861             foreach (MapTile t; level.allExits) {
1862               if (!level.isSolidAtPoint(t.ix+8, t.iy+8)) {
1863                 level.teleportPlayerTo(t.ix+8, t.iy+8);
1864                 unpauseGame();
1865                 return;
1866               }
1867             }
1868           }
1869           return;
1870         case K_d:
1871           if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1872             auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1873             if (damsel) {
1874               level.teleportPlayerTo(damsel.ix, damsel.iy);
1875               unpauseGame();
1876             }
1877           }
1878           return;
1879         case K_h:
1880           if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1881             MapObject obj;
1882             if (evt.bAlt) {
1883               // locked chest
1884               obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1885             } else {
1886               // key
1887               obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1888             }
1889             if (obj) {
1890               level.teleportPlayerTo(obj.ix, obj.iy-4);
1891               unpauseGame();
1892             }
1893           }
1894           return;
1895         case K_g:
1896           if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1897             if (level && mouseLevelX != int.min) {
1898               //int scale = level.global.scale;
1899               int mapX = mouseLevelX;
1900               int mapY = mouseLevelY;
1901               level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1902             }
1903             return;
1904           }
1905           break;
1906         case K_w:
1907           if (evt.bCtrl && showHelp != 2) {
1908             if (level && mouseLevelX != int.min) {
1909               //int scale = level.global.scale;
1910               int mapX = mouseLevelX;
1911               int mapY = mouseLevelY;
1912               level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1913             }
1914             return;
1915           }
1916           break;
1917         case K_a:
1918           if (evt.bCtrl && showHelp != 2) {
1919             if (level && mouseLevelX != int.min) {
1920               //int scale = level.global.scale;
1921               int mapX = mouseLevelX;
1922               int mapY = mouseLevelY;
1923               level.RemoveMapTileFromGrid(mapX/16, mapY/16, "arrow trap");
1924               level.MakeMapTile(mapX/16, mapY/16, (level.player.dir == MapObject::Dir.Left ? 'oArrowTrapLeft' : 'oArrowTrapRight'));
1925             }
1926             return;
1927           }
1928           break;
1929         case K_b:
1930           if (evt.bCtrl && showHelp != 2) {
1931             if (level && mouseLevelX != int.min) {
1932               //int scale = level.global.scale;
1933               int mapX = mouseLevelX;
1934               int mapY = mouseLevelY;
1935               level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1936             }
1937             return;
1938           }
1939           if (evt.bAlt && showHelp != 2) {
1940             if (level && mouseLevelX != int.min) {
1941               //int scale = level.global.scale;
1942               int mapX = mouseLevelX;
1943               int mapY = mouseLevelY;
1944               level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1945             }
1946             return;
1947           }
1948           /*
1949           if (evt.bAlt) {
1950             if (level && mouseLevelX != int.min) {
1951               int scale = level.global.scale;
1952               int mapX = mouseLevelX;
1953               int mapY = mouseLevelY;
1954               int wdt = 12;
1955               int hgt = 14;
1956               writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1957               level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1958                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1959                 return false;
1960               });
1961               writeln(" ---");
1962               foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1963                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1964               }
1965             }
1966             return;
1967           }
1968           */
1969           if (evt.bShift && showHelp != 2 && level && mouseLevelX != int.min) {
1970             /*auto obj =*/ level.MakeMapTile(mouseLevelX/16, mouseLevelY/16, 'oBoulder');
1971           }
1972           return;
1974         case K_DELETE: // suicide
1975           if (doGameSavingPlaying == Replay.None) {
1976             if (level.player && !level.player.dead && evt.bCtrl) {
1977               global.hasAnkh = false;
1978               level.global.plife = 1;
1979               level.player.invincible = 0;
1980               auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1981               if (xplo) xplo.suicide = true;
1982               unpauseGame();
1983             }
1984           }
1985           return;
1987         case K_INSERT:
1988           if (level.player && !level.player.dead && evt.bAlt) {
1989             if (doGameSavingPlaying != Replay.None) {
1990               if (doGameSavingPlaying == Replay.Replaying) {
1991                 stopReplaying();
1992               } else if (doGameSavingPlaying == Replay.Saving) {
1993                 saveGameMovement(dbgSessionMovementFileName, packit:true);
1994               }
1995               doGameSavingPlaying = Replay.None;
1996               stopReplaying();
1997               saveGameSession = false;
1998               replayGameSession = false;
1999               unpauseGame();
2000             }
2001           }
2002           return;
2004         case K_SPACE:
2005           if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
2006             level.stats.setMoneyCheat();
2007             level.stats.addMoney(10000);
2008           }
2009           return;
2010       }
2011     }
2012   } else {
2013     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
2014       if (level.player && level.player.dead) {
2015         if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
2016       } else {
2017         showHelp = 2;
2018         pauseRequested = true;
2019       }
2020       return;
2021     }
2023     if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
2024     if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2025     if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2026   }
2028   //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
2030   if (level) {
2031     if (!level.player || !level.player.dead) {
2032       gameJustOver = false;
2033     } else if (level.player && level.player.dead) {
2034       if (!gameJustOver) {
2035         drawStats = 0;
2036         gameJustOver = true;
2037         waitingForPayRestart = true;
2038         level.clearKeysPressRelease();
2039         if (doGameSavingPlaying == Replay.None) {
2040           stopReplaying(); // just in case
2041           saveGameStats();
2042         }
2043       }
2044       replayFastForward = false;
2045       if (doGameSavingPlaying == Replay.Saving) {
2046         if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2047         doGameSavingPlaying = Replay.None;
2048         //clearGameMovement();
2049         saveGameSession = false;
2050         replayGameSession = false;
2051       }
2052     }
2053     if (evt.type == ev_keydown || evt.type == ev_keyup) {
2054       bool down = (evt.type == ev_keydown);
2055       if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2056         if (down && evt.keycode == K_f) {
2057           if (evt.bCtrl) {
2058             if (replayFastForwardSpeed != 4) {
2059               replayFastForwardSpeed = 4;
2060               replayFastForward = true;
2061             } else {
2062               replayFastForward = !replayFastForward;
2063             }
2064           } else {
2065             replayFastForwardSpeed = 2;
2066             replayFastForward = !replayFastForward;
2067           }
2068         }
2069       }
2070       if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2071         foreach (int kbidx, int kval; global.config.keybinds) {
2072           if (kval && kval == evt.keycode) {
2073 #ifndef BIGGER_REPLAY_DATA
2074             if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2075 #endif
2076             level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2077           }
2078         }
2079       }
2080       if (level.player && level.player.dead) {
2081         if (down && evt.keycode == K_r && evt.bAlt) {
2082           saveGameSession = false;
2083           replayGameSession = true;
2084           unpauseGame();
2085         }
2086         if (down && evt.keycode == K_s && evt.bAlt) {
2087           bool wasSaveReq = saveGameSession;
2088           stopReplaying(); // just in case
2089           saveGameSession = !wasSaveReq;
2090           replayGameSession = false;
2091           //unpauseGame();
2092         }
2093         if (replayGameSession) {
2094           stopReplaying(); // just in case
2095           saveGameSession = false;
2096           replayGameSession = false;
2097           loadGameMovement(dbgSessionMovementFileName);
2098           loadGame(dbgSessionStateFileName);
2099           doGameSavingPlaying = Replay.Replaying;
2100         } else {
2101           // stats
2102           if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2103           if (down && (evt.keycode == K_UPARROW || evt.keycode == K_PAD8) && !evt.bAlt && drawStats) statsMoveUp();
2104           if (down && (evt.keycode == K_DOWNARROW || evt.keycode == K_PAD2) && !evt.bAlt && drawStats) statsMoveDown();
2105           if (waitingForPayRestart) {
2106             level.isKeyReleased(GameConfig::Key.Pay);
2107             if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2108           } else {
2109             level.isKeyPressed(GameConfig::Key.Pay);
2110             if (level.isKeyReleased(GameConfig::Key.Pay)) {
2111               auto doSave = saveGameSession;
2112               stopReplaying(); // just in case
2113               level.clearKeysPressRelease();
2114               level.restartGame();
2115               level.generateNormalLevel();
2116               if (doSave) {
2117                 saveGameSession = false;
2118                 replayGameSession = false;
2119                 writeln("DBG: saving game session...");
2120                 clearGameMovement();
2121                 doGameSavingPlaying = Replay.Saving;
2122                 saveGame(dbgSessionStateFileName);
2123                 //saveGameMovement(dbgSessionMovementFileName);
2124               }
2125             }
2126           }
2127         }
2128       }
2129     }
2130   }
2134 void levelExited () {
2135   // just in case
2136   saveGameStats();
2140 void closeVideo () {
2141   if (fullscreen && GLVideo.isInitialized) GLVideo.showMouseCursor();
2142   GLVideo.closeScreen();
2146 void initializeVideo () {
2147   int wdt = 320*3;
2148   int hgt = 240*3;
2149   if (fullscreen && global.config.fsmode == 1) {
2150     switch (global.config.realfsres) {
2151       case GameConfig::RealFSModes.VM_1024x768: wdt = 1024; hgt = 768; break;
2152       case GameConfig::RealFSModes.VM_1280x960: wdt = 1280; hgt = 960; break;
2153       case GameConfig::RealFSModes.VM_1280x1024: wdt = 1280; hgt = 1024; break;
2154       case GameConfig::RealFSModes.VM_1600x1200: wdt = 1600; hgt = 1200; break;
2155       case GameConfig::RealFSModes.VM_1680x1050: wdt = 1680; hgt = 1050; break;
2156       case GameConfig::RealFSModes.VM_1920x1080: wdt = 1920; hgt = 1080; break;
2157       case GameConfig::RealFSModes.VM_1920x1200: wdt = 1920; hgt = 1200; break;
2158     }
2159   }
2160   GLVideo.openScreen("Spelunky/VaVoom C", wdt, hgt, (fullscreen ? global.config.fsmode : 0));
2161   if (GLVideo.realStencilBits < 8) {
2162     GLVideo.closeScreen();
2163     FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2164   }
2165   /*
2166   if (!loserGPU && !GLVideo.framebufferHasAlpha) {
2167     GLVideo.closeScreen();
2168     FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!\nRun the game with \"--loser-gpu\" arg if you still want to play.");
2169   }
2170   */
2171   if (!GLVideo.framebufferHasAlpha) {
2172     loserGPU = true;
2173     if (level) level.loserGPU = true;
2174   }
2175   /*
2176   if (!GLVideo.glHasNPOT) {
2177     GLVideo.closeScreen();
2178     FatalError("=== YOUR GPU SUX! ===\nno NPOT texture support!");
2179   }
2180   */
2181   if (fullscreen) GLVideo.hideMouseCursor();
2185 void toggleFullscreen () {
2186   closeVideo();
2187   fullscreen = !fullscreen;
2188   initializeVideo();
2192 final void runGameLoop () {
2193   GLVideo.frameTime = 0; // unlimited FPS
2194   lastThinkerTime = 0;
2196   sprStore = SpawnObject(SpriteStore);
2197   sprStore.bDumpLoaded = false;
2199   bgtileStore = SpawnObject(BackTileStore);
2200   bgtileStore.bDumpLoaded = false;
2202   level = SpawnObject(GameLevel);
2203   level.loserGPU = loserGPU;
2204   level.setup(global, sprStore, bgtileStore);
2206   level.BuildYear = BuildYear;
2207   level.BuildMonth = BuildMonth;
2208   level.BuildDay = BuildDay;
2209   level.BuildHour = BuildHour;
2210   level.BuildMin = BuildMin;
2212   level.global = global;
2213   level.sprStore = sprStore;
2214   level.bgtileStore = bgtileStore;
2216   loadGameStats();
2217   //level.stats.introViewed = 0;
2219   if (level.stats.introViewed == 0) {
2220     startMode = StartMode.Intro;
2221     writeln("FORCED INTRO");
2222   } else {
2223     //writeln("INTRO VIWED: ", level.stats.introViewed);
2224     if (level.global.config.skipIntro) startMode = StartMode.Title;
2225   }
2227   level.onBeforeFrame = &beforeNewFrame;
2228   level.onAfterFrame = &afterNewFrame;
2229   level.onInterFrame = &interFrame;
2230   level.onLevelExitedCB = &levelExited;
2231   level.onCameraTeleported = &cameraTeleportedCB;
2233 #ifdef MASK_TEST
2234   maskSX = -0x0ff_fff;
2235   maskSY = maskSX;
2236   smask = sprStore['sExplosionMask'];
2237   maskFrame = 3;
2238 #endif
2240   level.viewWidth = 320*3;
2241   level.viewHeight = 240*3;
2243   GLVideo.swapInterval = (global.config.optVSync ? 1 : 0);
2244   //GLVideo.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2245   fullscreen = global.config.startFullscreen;
2246   initializeVideo();
2248   sprStore.loadFont('sFontSmall');
2250   //SoundSystem.SwapStereo = config.swapStereo;
2251   SoundSystem.NumChannels = 32;
2252   SoundSystem.MaxHearingDistance = 12000;
2253   //SoundSystem.DopplerFactor = 1.0f;
2254   //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2255   SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2256   SoundSystem.ReferenceDistance = 16.0f*4;
2257   SoundSystem.MaxDistance = 16.0f*(5*10);
2259   SoundSystem.Initialize();
2260   if (!SoundSystem.IsInitialized) {
2261     writeln("WARNING: cannot initialize sound system, turning off sound and music");
2262     global.soundDisabled = true;
2263     global.musicDisabled = true;
2264   }
2265   global.fixVolumes();
2267   level.restartGame(); // this will NOT generate a new level
2268   setupCheats();
2269   setupSeeds();
2270   performTimeCheck();
2272   texTigerEye = GLTexture.Load("teye0.png");
2274   if (global.cheatEndGameSequence) {
2275     level.winTime = 12*60+42;
2276     level.stats.money = 6666;
2277     switch (global.cheatEndGameSequence) {
2278       case 1: default: level.startWinCutscene(); break;
2279       case 2: level.startWinCutsceneVolcano(); break;
2280       case 3: level.startWinCutsceneWinFall(); break;
2281     }
2282   } else {
2283     switch (startMode) {
2284       case StartMode.Title: level.restartTitle(); break;
2285       case StartMode.Intro: level.restartIntro(); break;
2286       case StartMode.Stars: level.restartStarsRoom(); break;
2287       case StartMode.Sun: level.restartSunRoom(); break;
2288       case StartMode.Moon: level.restartMoonRoom(); break;
2289       default:
2290         level.generateNormalLevel();
2291         if (startMode == StartMode.Dead) {
2292           level.player.dead = true;
2293           level.player.visible = false;
2294         }
2295         break;
2296     }
2297   }
2299   //global.rope = 666;
2300   //global.bombs = 666;
2302   //global.globalRoomSeed = 871520037;
2303   //global.globalOtherSeed = 1047036290;
2305   //level.createTitleRoom();
2306   //level.createTrans4Room();
2307   //level.createOlmecRoom();
2308   //level.generateLevel();
2310   //level.centerViewAtPlayer();
2311   teleportCameraAt(level.viewStart);
2312   //writeln(GLVideo.swapInterval);
2314   GLVideo.runEventLoop();
2315   closeVideo();
2316   SoundSystem.Shutdown();
2318   if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2319   stopReplaying();
2320   saveGameStats();
2322   delete level;
2326 // ////////////////////////////////////////////////////////////////////////// //
2327 // duplicates are not allowed!
2328 final void checkGameObjNames () {
2329   array!(class!Object) known;
2330   class!Object cc;
2331   int classCount = 0, namedCount = 0;
2332   foreach AllClasses(Object, out cc) {
2333     auto gn = GetClassGameObjName(cc);
2334     if (gn) {
2335       //writeln("'", gn, "' is `", GetClassName(cc), "`");
2336       auto nid = NameToIIndex(gn);
2337       if (nid < known.length && known[nid]) FatalError("duplicate game object name '%n' (defined for class is '%n', redefined in class '%n')", gn, GetClassName(known[nid]), GetClassName(cc));
2338       known[nid] = cc;
2339       ++namedCount;
2340     }
2341     ++classCount;
2342   }
2343   writeln(classCount, " classes, ", namedCount, " game object classes.");
2347 // ////////////////////////////////////////////////////////////////////////// //
2348 #include "timelimit.vc"
2349 //const int TimeLimitDate = 2018232;
2352 void performTimeCheck () {
2353 #ifdef DISABLE_TIME_CHECK
2354 #else
2355   if (TigerEye) return;
2357   TTimeVal tv;
2358   if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2360   TDateTime tm;
2361   if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2363   int tldate = tm.year*1000+tm.yday;
2365   if (tldate > TimeLimitDate) {
2366     level.maxPlayingTime = 24;
2367   } else {
2368     //writeln("*** days left: ", TimeLimitDate-tldate);
2369   }
2370 #endif
2374 void setupCheats () {
2375   return;
2377   //level.stats.resetTunnelPrices();
2378   startMode = StartMode.Alive;
2379   global.currLevel = 10;
2380   //global.scumGenAlienCraft = true;
2381   //global.scumGenYetiLair = true;
2382   return;
2384   startMode = StartMode.Alive;
2385   global.currLevel = 8;
2386   /*
2387   level.stats.tunnel1Left = level.stats.default.tunnel1Left;
2388   level.stats.tunnel2Left = level.stats.default.tunnel2Left;
2389   level.stats.tunnel1Active = false;
2390   level.stats.tunnel2Active = false;
2391   level.stats.tunnel3Active = false;
2392   */
2393   return;
2395   startMode = StartMode.Alive;
2396   global.currLevel = 2;
2397   global.scumGenShop = true;
2398   //global.scumGenShopType = GameGlobal::ShopType.Craps;
2399   //global.config.scale = 1;
2400   return;
2402   startMode = StartMode.Alive;
2403   global.currLevel = 13;
2404   global.config.scale = 2;
2405   return;
2407   startMode = StartMode.Alive;
2408   global.currLevel = 13;
2409   global.config.scale = 1;
2410   global.cityOfGold = true;
2411   return;
2413   startMode = StartMode.Alive;
2414   global.currLevel = 5;
2415   global.genBlackMarket = true;
2416   return;
2418   startMode = StartMode.Alive;
2419   global.currLevel = 2;
2420   global.scumGenShop = true;
2421   global.scumGenShopType = GameGlobal::ShopType.Weapon;
2422   //global.scumGenShopType = GameGlobal::ShopType.Craps;
2423   //global.config.scale = 1;
2424   return;
2426   //startMode = StartMode.Intro;
2427   //return;
2429   global.currLevel = 2;
2430   startMode = StartMode.Alive;
2431   return;
2433   global.currLevel = 5;
2434   startMode = StartMode.Alive;
2435   global.scumGenLake = true;
2436   global.config.scale = 1;
2437   return;
2439   startMode = StartMode.Alive;
2440   global.cheatCanSkipOlmec = true;
2441   global.currLevel = 16;
2442   //global.currLevel = 5;
2443   //global.currLevel = 13;
2444   //global.config.scale = 1;
2445   return;
2446   //startMode = StartMode.Dead;
2447   //startMode = StartMode.Title;
2448   //startMode = StartMode.Stars;
2449   //startMode = StartMode.Sun;
2450   startMode = StartMode.Moon;
2451   return;
2452   //global.scumGenSacrificePit = true;
2453   //global.scumAlwaysSacrificeAltar = true;
2455   // first lush jungle level
2456   //global.levelType = 1;
2457   /*
2458   global.scumGenCemetary = true;
2459   */
2460   //global.idol = false;
2461   //global.currLevel = 5;
2463   //global.isTunnelMan = true;
2464   //return;
2466   //global.currLevel = 5;
2467   //global.scumGenLake = true;
2469   //global.currLevel = 5;
2470   //global.currLevel = 9;
2471   //global.currLevel = 13;
2472   //global.currLevel = 14;
2473   //global.cheatEndGameSequence = 1;
2474   //return;
2476   //global.currLevel = 6;
2477   global.scumGenAlienCraft = true;
2478   global.currLevel = 9;
2479   //global.scumGenYetiLair = true;
2480   //global.genBlackMarket = true;
2481   //startDead = false;
2482   startMode = StartMode.Alive;
2483   return;
2485   global.cheatCanSkipOlmec = true;
2486   global.currLevel = 15;
2487   startMode = StartMode.Alive;
2488   return;
2490   global.scumGenShop = true;
2491   //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2492   global.scumGenShopType = GameGlobal::ShopType.Craps;
2493   //global.scumGenShopType = 6; // craps
2494   //global.scumGenShopType = 7; // kissing
2496   //global.scumAlwaysSacrificeAltar = true;
2500 void setupSeeds () {
2504 // ////////////////////////////////////////////////////////////////////////// //
2505 void main (ref array!string args) {
2506   foreach (string s; args) {
2507     if (s == "--loser-gpu") loserGPU = 1;
2508   }
2510   checkGameObjNames();
2512   appSetName("k8spelunky");
2513   config = SpawnObject(GameConfig);
2514   global = SpawnObject(GameGlobal);
2515   global.config = config;
2516   config.heroType = GameConfig::Hero.Spelunker;
2518   global.randomizeSeedAll();
2520   fillCheatPickupList();
2521   fillCheatItemsList();
2522   fillCheatEnemiesList();
2524   loadGameOptions();
2525   loadKeyboardBindings();
2527   // force "immediate delete" mode, it is faster
2528   GC_ImmediateDelete = false;
2529   GC_CollectGarbage(true); // destroy delayed objects too
2530   GC_ImmediateDelete = true;
2532   runGameLoop();