// Hyperbolic Rogue -- starting and ending games // Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details /** \file system.cpp * \brief changing game modes, starting, closing, loading and saving games */ #include "hyper.h" namespace hr { #if HDR /** \brief This namespace has constants used as parameters in functions such as restart_game and wrongmode. */ namespace rg { static constexpr char nothing = 0; static constexpr char peace = 'P'; static constexpr char inv = 'i'; static constexpr char chaos = 'C'; static constexpr char tactic = 't'; static constexpr char tour = 'T'; static constexpr char yendor = 'y'; static constexpr char shmup = 's'; static constexpr char randpattern = 'r'; static constexpr char princess = 'p'; static constexpr char daily = 'd'; static constexpr char daily_off = 'D'; static constexpr char racing = 'R'; static constexpr char dualmode = 'U'; static constexpr char heptagons = '7'; /** \brief wrongmode only -- marks 'global' achievements not related to the current mode */ static constexpr char global = 'x'; /** \brief wrongmode only -- change vid.scfg.players then restart_game(rg::nothing) instead */ static constexpr char multi = 'm'; /** \brief wrongmode only -- mark achievements for special geometries/variations, this automatically marks the expected land_structure as lsSingle */ static constexpr char special_geometry = 'g'; /** \brief wrongmode only -- mark achievements for special geometries/variations */ static constexpr char special_geometry_nicewalls = 'G'; /** \brief wrongmode only -- specgeom returns 'fail' if the special geometry is not correct */ static constexpr char fail = 'X'; } #endif /** \brief is a game map currently loaded (it is false after hr::stop_game and true after hr::start_game) */ EX bool game_active; /** \brief God mode */ EX bool autocheat; /** \brief which wall should we fill the Canvas with */ EX eWall canvas_default_wall = waNone; /** \brief the number of Black Lotuses collected -- but updated only if we manage to escape */ EX int truelotus; EX int asteroids_generated, asteroid_orbs_generated; EX time_t timerstart, savetime; EX bool timerstopped; EX int savecount; EX int save_turns; EX bool doCross = false; EX bool gamegen_failure; EX eLand top_land; /** \brief a comparator for version number strings */ EX bool verless(string v, string cmp) { if(isdigit(v[0]) && isdigit(v[1])) v = "A" + v; if(isdigit(cmp[0]) && isdigit(cmp[1])) cmp = "A" + cmp; return v < cmp; } /** \brief Hooks for welcomeMessage. Return true to capture. */ EX hookset hooks_welcome_message; /** \brief Print the welcome message during the start of game. Depends on the current mode and other settings. */ EX void welcomeMessage() { if(callhandlers(false, hooks_welcome_message)) return; if(nohelp == 1) return; if(custom_welcome != "") addMessage(custom_welcome); else if(embedded_plane) return IPF(welcomeMessage()); #if CAP_TOUR else if(tour::on) return; // displayed by tour #endif else if(princess::challenge) { kills[moVizier] = 1; princess::forceMouse = true; if(yendor::everwon) items[itGreenStone] = 99; addMessage(XLAT("Welcome to %the1 Challenge!", moPrincess)); addMessage(XLAT("The more Hypersian Rugs you collect, the harder it is.", moPrincess)); } else if(intra::in) addMessage(XLAT("Welcome to a portal map! Press 'o' to configure.")); else if(randomPatternsMode) addMessage(XLAT("Welcome to the Random Pattern mode!")); else if(tactic::on) addMessage(XLAT("You are playing %the1 in the Pure Tactics mode.", firstland)); else if(yendor::on) addMessage(XLAT("Welcome to the Yendor Challenge %1!", its(yendor::challenge))); else if(peace::on) ; // no welcome message else if(shmup::on) ; // welcome message given elsewhere else if(euclid) addMessage(XLAT("Welcome to the Euclidean mode!")); else if(specialland == laHalloween && BITRUNCATED && among(geometry, gSphere, gElliptic)) addMessage(XLAT("Welcome to Halloween!")); else if(elliptic && WDIM == 2) addMessage(XLAT("Good luck in the elliptic plane!")); else if(elliptic) addMessage(XLAT("Good luck in the elliptic space!")); else if(sphere) addMessage(XLAT("Welcome to Spherogue!")); else if(in_s2xe()) addMessage(XLAT("Welcome to Spherindrogue!")); else if(in_h2xe()) addMessage(XLAT("Welcome to Hyper-X-R-Rogue!")); else if(sol) addMessage(XLAT("Welcome to SolvRogue!")); else if(nil) addMessage(XLAT("Welcome to NilRogue!")); else if(sl2) { if(hybrid::csteps == 0) addMessage(XLAT("Welcome to CoverRogue!")); else if(cgi.psl_steps % hybrid::csteps == 0) addMessage(XLAT("Welcome to PSL(2,R)-ogue!")); else addMessage(XLAT("Welcome to SL(2,R)-ogue!")); if(hybrid::underlying == gNormal && BITRUNCATED) addMessage(XLAT("Hint: this is more playable with pure {7,3} or pure {5,4}")); } else if(PURE && geometry == gNormal && !cheater) addMessage(XLAT("Welcome to the Heptagonal Mode!")); else if(cheater || autocheat) addMessage(XLAT("Welcome to HyperRogue! (cheat mode on)")); else if(hat::in()) addMessage(XLAT("Welcome to HatRogue!")); else if(bow::crossbow_mode()) addMessage(XLAT("Welcome to HyperRanger!")); else addMessage(XLAT("Welcome to HyperRogue!")); if(!safety && !daily::on) { auto lv = land_validity(specialland); if(lv.flags & lv::display_error_message) addMessage(XLAT(lv.msg)); } if(nohelp == 2) return; #if ISMAC addMessage(XLAT("Press F1 or right-shift-click things for help.")); #elif !ISMOBILE addMessage(XLAT("Press F1 or right-click things for help.")); #endif } /** \brief These hooks are called at the start of initgame. */ EX hookset hooks_initgame; /** \brief These hooks are called at the end of initgame. */ EX hookset hooks_post_initgame; EX bool ineligible_starting_land; EX int easy_specialland; /** \brief initialize the game */ EX void initgame() { DEBBI(DF_INIT, ("initGame")); callhooks(hooks_initgame); if(!safety) fix_land_structure_choice(); if(multi::players < 1 || multi::players > MAXPLAYER) multi::players = 1; multi::whereto[0].d = MD_UNDECIDED; multi::cpid = 0; yendor::init(1); if(safety && safetyseed) { shrand(safetyseed); firstland = safetyland; } if(!safety) { firstland = specialland; ineligible_starting_land = !landUnlockedIngame(specialland); } if(firstland == laNone || firstland == laBarrier) firstland = laCrossroads; easy_specialland = 0; if(firstland == laOceanWall) firstland = laOcean; if(firstland == laHauntedWall) firstland = laGraveyard; if(firstland == laHauntedBorder) firstland = laGraveyard; if(firstland == laHaunted && !tactic::on) firstland = laGraveyard; if(firstland == laMercuryRiver) firstland = laTerracotta; if(firstland == laMountain && !tactic::on && !ls::hv_structure()) firstland = laJungle; if(firstland == laPrincessQuest) firstland = laPalace; if(firstland == laMemory) firstland = laIce; if(!ls::hv_structure()) if((isGravityLand(firstland) && !isCyclic(firstland)) || (firstland == laOcean && !safety && !yendor::on)) { firstland = weirdhyperbolic ? laCrossroads4 : laCrossroads; easy_specialland = 3; } clear_euland(firstland); cwt.at = currentmap->gamestart(); cwt.spin = 0; cwt.mirrored = false; cwt.at->land = firstland; #if CAP_COMPLEX2 if(firstland == laBrownian) brownian::init(cwt.at); #endif chaosAchieved = false; clearing::direct = 0; clearing::imputed = 0; rosephase = 0; shmup::count_pauses = 0; splitrocks = 0; if(firstland == laElementalWall) cwt.at->land = randomElementalLand(); resetview(); createMov(cwt.at, 0); pregen(); setdist(cwt.at, BARLEV, NULL); if(!disable_bigstuff) if(isCyclic(specialland) || specialland == laCanvas) { #if CAP_COMPLEX2 camelot::anthraxBonus = items[itHolyGrail]; #endif cwt.at->move(0)->land = firstland; if(firstland == laWhirlpool) cwt.at->move(0)->wall = waSea; setdist(cwt.at->move(0), BARLEV-1, cwt.at); if(horo_ok()) { if(specialland == laCamelot) start_camelot(cwt.at); else { heptagon *h = create_altmap(cwt.at, 2, hsA); if(!h) printf("FAIL\n"); } } } if(tactic::on && firstland == laPower) { items[itOrbSpeed] = 30; items[itOrbWinter] = 30; items[itOrbFlash] = 30; } if(firstland == laCA) items[itOrbAether] = 2; if(tactic::on && firstland == laCaribbean) { if(hiitemsMax(itRedGem) >= 25) items[itRedGem] = min(hiitemsMax(itRedGem), 50); if(hiitemsMax(itFernFlower) >= 25) items[itFernFlower] = min(hiitemsMax(itFernFlower), 50); if(hiitemsMax(itWine) >= 25) items[itWine] = min(hiitemsMax(itWine), 50); } yendor::lastchallenge = yendor::challenge; if(shmup::on) shmup::init(); yendor::init(2); #if CAP_RACING if(racing::on) racing::generate_track(); #endif if(gamegen_failure) return; if(euclid && specialland == laPrincessQuest) { cell *c = euc::at(princess::coords()); princess::generating = true; c->land = laPalace; setdist(c, 7 - getDistLimit() - genrange_bonus, NULL); princess::generating = false; } if(cwt.at->land == laCrossroads2 && !ls::hv_structure()) { cell *c = cwt.at; if(mhybrid) { c = hybrid::get_where(c).first; PIU( c->cmove(0) ); } c->landparam = 12; c->cmove(0)->landparam = 44; c->cmove(0)->land = laCrossroads2; } sword::determine_sword_angles(); for(int i=0; iland = laHell; items[itHell] = 10; for(int i=BARLEV; i>=7 - getDistLimit() - genrange_bonus; i--) { setdist(cwt.at, i, NULL); currentmap->verify(); } if(doCross) { for(int i=0; iallcells()); i++) setdist(currentmap->allcells()[i], 8, NULL); } if(multi::players > 1 && !shmup::on) for(int i=0; itype; multi::player[i].at = cwt.at->move(idir); // special case -- otherwise they land on a wall if(firstland == laCrossroads2 && i == 1) multi::player[1].at = cwt.at; if(firstland == laCrossroads2 && i == 6) multi::player[6].at = createMov(createMov(cwt.at, 0), 3); setdist(cwt.at->move(idir), 7 - getDistLimit() - genrange_bonus, cwt.at); multi::player[i].spin = 0; multi::flipped[i] = true; multi::whereto[i].d = MD_UNDECIDED; } yendor::init(3); peace::simon::init(); multi::revive_queue.clear(); #if CAP_TOUR if(tour::on) tour::presentation(tour::pmRestart); #endif if(multi::players > 1 && !shmup::on) { for(cell *pc: player_positions()) makeEmpty(pc); } else { makeEmpty(cwt.at); } if(specialland == laMinefield && closed_or_bounded) { bfs(); generate_mines(); } if(specialland == laHalloween) halloween::generate(); if(in_lovasz()) { cwt.at->item = itOrbInvis; } princess::squeaked = false; clearing::current_root = NULL; if(!safety) { usedSafety = false; timerstart = time(NULL); turncount = 0; rosewave = 0; rosephase = 0; noiseuntil = 0; sagephase = 0; hardcoreAt = 0; timerstopped = false; savecount = 0; savetime = 0; tortoise::last21tort = 0; cheater = 0; if(autocheat) cheater = 1; if(!wfc::use_eclectic) cheater = 1; if(!autocheat && !cheater && geometry == gNormal) patterns::whichShape = 0; hauntedWarning = false; if(!autocheat) { timerghost = true; gen_wandering = true; } truelotus = 0; asteroids_generated = 0; asteroid_orbs_generated = 0; survivalist = true; #if CAP_CRYSTAL crystal::used_compass_inside = false; #endif got_achievements = {}; #if CAP_INV if(inv::on) inv::init(); #endif #if CAP_COMPLEX2 mine::auto_teleport_charges(); #endif welcomeMessage(); } else { usedSafety = true; safety = false; } havewhat = hadwhat = 0; rosemap.clear(); elec::lightningfast = 0; lastsafety = gold(); bfs(); checkmove(); playermoved = true; if(quotient || sphere) for(cell *c: currentmap->allcells()) setdist(c, 8, NULL); if(!allowChangeRange()) { gamerange_bonus = genrange_bonus = 0; if(vid.use_smart_range == 2) vid.use_smart_range = 1; } if(!allowIncreasedSight()) vid.use_smart_range = 0; callhooks(hooks_post_initgame); } bool havesave = true; #if CAP_SAVE /** \brief A namespace for loading and saving scores and saved games (system.cpp), and for displaying these scores (scores.cpp). * * Most ApplyBox functions are used both for saving savegames and scores to the logfile, loading savegames and scores from the logfile, * and loading highscore information from the logfile. The flags saving, loading, and loadingHi specify what is actually done. */ EX namespace scores { #if HDR /** \brief the amount of boxes reserved for each hr::score item */ #define MAXBOX 500 /** \brief currently used boxes in hr::score */ #define POSSCORE 418 /** \brief a struct to keep local score from an earlier game */ struct score { /** \brief version used */ string ver; /** \brief all the data of the saved score, see applyBoxes() */ int box[MAXBOX]; /** \brief yasc message */ string yasc_message; }; #endif /** \brief the current save */ EX score save; /** \brief the index of the next box */ EX int boxid; /** \brief see hr::applyBox */ EX bool saving, loading, loadingHi; /** \brief names of all the boxes */ EX string boxname[MAXBOX]; /** \brief 'fake' boxes should not appear when examining local scores */ EX bool fakebox[MAXBOX]; /** \brief does this box contain monster kills */ EX bool monsbox[MAXBOX]; /** \brief the next box should contain t */ void applyBox(int& t) { if(saving) save.box[boxid++] = t; else if(loading) t = save.box[boxid++]; else boxid++; } /** \brief the next box should contain tb */ void applyBoxBignum(bignum& tb, string name) { float tf; int ti; if(saving) tf = tb.approx_ld(); if(saving) memcpy(&ti, &tf, 4); applyBoxNum(ti, name); if(loading) memcpy(&tf, &ti, 4); if(loading) tb = bignum(tf); } /** \brief the next box should contain i, and possibly be named name */ EX void applyBoxNum(int& i, string name IS("")) { fakebox[boxid] = (name == "" || name[0] == '@'); boxname[boxid] = name; monsbox[boxid] = false; applyBox(i); } /** \brief the next box should contain b, and possibly be named name */ void applyBoxBool(bool& b, string name = "") { int i = b; applyBoxNum(i, name); monsbox[boxid] = false; b = i; } /** \brief Save i while saving, do nothing while loading. Use together with hr::scores::applyBoxLoad and boxid++ */ void applyBoxSave(int i, string name = "") { fakebox[boxid] = (name == ""); boxname[boxid] = name; applyBox(i); } /** \brief Load i while loading, do nothing while saving. Use together with hr::scores::applyBoxSave and boxid++ */ int applyBoxLoad(string name = "") { fakebox[boxid] = (name == ""); boxname[boxid] = name; int i=0; applyBox(i); return i; } /** \brief the next box is the number of collected items it */ void applyBoxI(eItem it, bool f = false) { boxname[boxid] = iinf[it].name; fakebox[boxid] = f; monsbox[boxid] = false; if(loadingHi) { updateHi_for_code(it, save.box[boxid++], saved_modecode); } else applyBox(items[it]); } vector invorb; void addinv(eItem it) { invorb.push_back(it); } /** \brief Handle the information about orb it. Need to call list_invorb later */ void applyBoxOrb(eItem it) { applyBoxI(it, true); invorb.push_back(it); } /** \brief Handle the OSM information for all orbs that applyBoxOrb has been called for so far */ void list_invorb() { for(eItem it: invorb) { #if CAP_INV if(true) { inv::applyBox(it); continue; } #endif int u = 0; applyBoxNum(u); } invorb.clear(); } /** \brief handle the number of monsters of type m killed */ void applyBoxM(eMonster m, bool f = false) { fakebox[boxid] = f; boxname[boxid] = minf[m].name; monsbox[boxid] = true; applyBox(kills[m]); } EX modecode_t saved_modecode; template void applyBoxEnum(T& i, string name IS("")) { int ii = (int) i; applyBoxNum(ii, name); i = (T) ii; } /** \brief Call applyBox for all the required values. This will save the values if hr::scores::saving==true, load if hr::scores::loading==true, load into highscores if hr::scores::loadingHi==true */ EX void applyBoxes() { invorb.clear(); eLand lostin = laNone; applyBoxSave((int) timerstart, "time elapsed"); time_t timer = time(NULL); applyBoxSave((int) timer, "date"); applyBoxSave(gold(), "treasure collected"); applyBoxSave(tkills(), "total kills"); applyBoxNum(turncount, "turn count"); applyBoxNum(cellcount, "cells generated"); if(loading) timerstart = time(NULL); for(int i=0; iland, "@safetyland"); else if(loading) firstland = safetyland = eLand(applyBoxLoad("@safetyland")); else lostin = eLand(save.box[boxid++]); for(int i=itOrbLightning; i<25; i++) applyBoxOrb(eItem(i)); applyBoxI(itRoyalJelly); applyBoxI(itWine); applyBoxI(itSilver); applyBoxI(itEmerald); applyBoxI(itPower); applyBoxOrb(itOrbFire); applyBoxOrb(itOrbInvis); applyBoxOrb(itOrbAether); applyBoxOrb(itOrbPsi); applyBoxM(moBug0); applyBoxM(moBug1); applyBoxM(moBug2); applyBoxM(moVineBeast); applyBoxM(moVineSpirit); applyBoxM(moLancer); applyBoxM(moFlailer); applyBoxM(moEarthElemental); applyBoxM(moDarkTroll); applyBoxM(moWitch); applyBoxM(moWitchFire); applyBoxM(moWitchFlash); applyBoxM(moWitchGhost); applyBoxM(moWitchSpeed); applyBoxM(moEvilGolem); applyBoxM(moWitchWinter); applyBoxI(itHolyGrail); applyBoxI(itGrimoire); applyBoxM(moKnight); applyBoxM(moCultistLeader); applyBoxM(moPirate); applyBoxM(moCShark); applyBoxM(moParrot); applyBoxI(itPirate); applyBoxOrb(itOrbTime); applyBoxM(moHexSnake); applyBoxM(moRedTroll); applyBoxI(itRedGem); applyBoxOrb(itOrbSpace); int geo = geometry; applyBoxNum(geo, "@geometry"); geometry = eGeometry(geo); applyBoxBool(hardcore, "hardcore"); applyBoxNum(hardcoreAt, "@hardcoreAt"); applyBoxBool(shmup::on, "shmup"); if(saving) applyBoxSave(specialland, "euclid land"); else if(loading) specialland = eLand(applyBoxLoad("euclid land")); else fakebox[boxid++] = true; applyBoxI(itCoast); applyBoxI(itWhirlpool); applyBoxI(itBombEgg); applyBoxM(moBomberbird); applyBoxM(moTameBomberbird); applyBoxM(moAlbatross); applyBoxOrb(itOrbFriend); applyBoxOrb(itOrbAir); applyBoxOrb(itOrbWater); applyBoxI(itPalace); applyBoxI(itFjord); applyBoxOrb(itOrbFrog); applyBoxOrb(itOrbDiscord); applyBoxM(moPalace); applyBoxM(moFatGuard); applyBoxM(moSkeleton); applyBoxM(moVizier); applyBoxM(moViking); applyBoxM(moFjordTroll); applyBoxM(moWaterElemental); applyBoxI(itSavedPrincess); applyBoxOrb(itOrbLove); applyBoxM(moPrincess); applyBoxM(moPrincessMoved, false); // live Princess for Safety applyBoxM(moPrincessArmedMoved, false); // live Princess for Safety applyBoxM(moMouse); applyBoxNum(princess::saveArmedHP, "@saveArmedHP"); applyBoxNum(princess::saveHP, "@saveHP"); applyBoxI(itIvory); applyBoxI(itElemental); applyBoxI(itZebra); applyBoxI(itFireShard); applyBoxI(itWaterShard); applyBoxI(itAirShard); applyBoxI(itEarthShard); applyBoxM(moAirElemental); applyBoxM(moFireElemental); applyBoxM(moFamiliar); applyBoxM(moGargoyle); applyBoxM(moOrangeDog); applyBoxOrb(itOrbSummon); applyBoxOrb(itOrbMatter); applyBoxM(moForestTroll); applyBoxM(moStormTroll); applyBoxM(moOutlaw); applyBoxM(moMutant); applyBoxM(moMetalBeast); applyBoxM(moMetalBeast2); applyBoxI(itMutant); applyBoxI(itFulgurite); applyBoxI(itBounty); applyBoxOrb(itOrbLuck); applyBoxOrb(itOrbStunning); applyBoxBool(tactic::on, "@tactic"); applyBoxNum(elec::lightningfast, "@lightningfast"); // if(save.box[boxid]) printf("lotus = %d (lost = %d)\n", save.box[boxid], isHaunted(lostin)); if(loadingHi && isHaunted(lostin)) boxid++; else applyBoxI(itLotus); applyBoxOrb(itOrbUndeath); applyBoxI(itWindstone); applyBoxOrb(itOrbEmpathy); applyBoxM(moWindCrow); applyBoxOrb(itMutant2); applyBoxOrb(itOrbFreedom); applyBoxM(moRedFox); applyBoxBool(survivalist, "@survivalist"); if(loadingHi) applyBoxI(itLotus); else applyBoxNum(truelotus, "lotus/escape"); applyBoxEnum(variation, "variation"); applyBoxI(itRose); applyBoxOrb(itOrbBeauty); applyBoxI(itCoral); applyBoxOrb(itOrb37); applyBoxOrb(itOrbEnergy); applyBoxM(moRatling); applyBoxM(moFalsePrincess); applyBoxM(moRoseLady); applyBoxM(moRoseBeauty); applyBoxEnum(land_structure, "land structure"); applyBoxNum(multi::players, "shmup players"); if(multi::players < 1 || multi::players > MAXPLAYER) multi::players = 1; applyBoxM(moRatlingAvenger); // printf("applybox %d\n", shmup::players); applyBoxI(itApple); applyBoxM(moSparrowhawk); applyBoxM(moResearcher); applyBoxI(itDragon); applyBoxM(moDragonHead); applyBoxOrb(itOrbDomination); applyBoxI(itBabyTortoise); applyBoxNum(tortoise::seekbits, "@seekbits"); applyBoxM(moTortoise); applyBoxOrb(itOrbShell); applyBoxNum(safetyseed, "@safetyseed"); // (+18) for(int i=0; i<6; i++) { applyBoxNum(multi::treasures[i], "@multi-treasures" + its(i)); applyBoxNum(multi::kills[i], "@multi-kills" + its(i)); applyBoxNum(multi::deaths[i], "@multi-deaths" + its(i)); } // (+8) applyBoxM(moDragonTail); applyBoxI(itKraken); applyBoxM(moKrakenH); applyBoxM(moKrakenT); applyBoxOrb(itOrbSword); applyBoxI(itBarrow); applyBoxM(moDraugr); applyBoxOrb(itOrbSword2); applyBoxI(itTrollEgg); applyBoxOrb(itOrbStone); bool sph; sph = false; applyBoxBool(sph, "sphere"); if(sph) geometry = gSphere; sph = false; applyBoxBool(sph, "elliptic"); if(sph) geometry = gElliptic; applyBoxNum(princess::reviveAt, "@reviveAt"); applyBoxI(itDodeca); applyBoxI(itAmethyst); applyBoxI(itSlime); applyBoxOrb(itOrbNature); applyBoxOrb(itOrbDash); addinv(itOrbRecall); applyBoxM(moBat); applyBoxM(moReptile); applyBoxM(moFriendlyIvy); applyBoxI(itGreenGrass); applyBoxI(itBull); applyBoxOrb(itOrbHorns); applyBoxOrb(itOrbBull); applyBoxM(moSleepBull); applyBoxM(moRagingBull); applyBoxM(moHerdBull); applyBoxM(moButterfly); applyBoxM(moGadfly); // 10.0: applyBoxNum(hinttoshow, "@hinttoshow"); // 258 addinv(itOrbMirror); addinv(itGreenStone); list_invorb(); #if CAP_INV applyBoxBool(inv::on, "inventory"); // 306 applyBoxNum(inv::rseed, "@inv-rseed"); #else { int u; applyBoxNum(u); applyBoxNum(u); } #endif // 10.1: applyBoxI(itLavaLily); applyBoxI(itHunting); applyBoxI(itBlizzard); applyBoxI(itTerra); applyBoxOrb(itOrbSide1); applyBoxOrb(itOrbSide2); applyBoxOrb(itOrbSide3); applyBoxOrb(itOrbLava); applyBoxOrb(itOrbMorph); applyBoxM(moHunterDog); applyBoxM(moIceGolem); applyBoxM(moVoidBeast); applyBoxM(moJiangshi); applyBoxM(moTerraWarrior); applyBoxM(moSalamander); applyBoxM(moLavaWolf); applyBoxOrb(itOrbSlaying); applyBoxOrb(itOrbMagnetism); applyBoxOrb(itOrbPhasing); applyBoxI(itDock); applyBoxI(itGlowCrystal); applyBoxI(itMagnet); applyBoxI(itRuins); applyBoxI(itSwitch); applyBoxM(moNorthPole); applyBoxM(moSouthPole); applyBoxM(moSwitch1); applyBoxM(moSwitch2); applyBoxM(moAltDemon); applyBoxM(moHexDemon); applyBoxM(moPair); applyBoxM(moCrusher); applyBoxM(moMonk); bool v2 = false; applyBoxBool(v2, "@variation"); if(loading && v2) variation = eVariation::goldberg; applyBoxNum(gp::param.first, "@gp-first"); applyBoxNum(gp::param.second, "@gp-second"); v2 = false; applyBoxBool(v2); if(loading && v2) variation = eVariation::irregular; applyBoxNum(irr::cellcount, "@irr-cellcount"); list_invorb(); applyBoxNum(irr::bitruncations_performed, "@irr-bitruncations"); applyBoxI(itVarTreasure); applyBoxI(itBrownian); applyBoxI(itWest); applyBoxM(moAcidBird); applyBoxM(moBrownBug); applyBoxM(moVariantWarrior); applyBoxM(moWestHawk); applyBoxM(moFallingDog); applyBoxOrb(itOrbIntensity); applyBoxOrb(itOrbChoice); applyBoxOrb(itOrbGravity); list_invorb(); applyBoxM(moNarciss); applyBoxM(moMirrorSpirit); applyBoxNum(clearing::direct, "@clearing-direct"); applyBoxBignum(clearing::imputed, "@clearing-imputed"); applyBoxOrb(itOrbImpact); applyBoxOrb(itOrbChaos); applyBoxOrb(itOrbPlague); applyBoxI(itEclectic); applyBoxI(itFrog); applyBoxI(itWet); applyBoxM(moFrog); applyBoxM(moPhaser); applyBoxM(moVaulter); applyBoxM(moPike); applyBoxM(moRusalka); list_invorb(); applyBoxNum(saved_modecode, "modecode"); applyBoxBool(ineligible_starting_land, "ineligible_starting_land"); applyBoxNum(yasc_code, "YASC code"); applyBoxBool(casual, "casual mode"); applyBoxI(itCursed); applyBoxI(itDice); applyBoxOrb(itOrbPurity); applyBoxOrb(itOrbWoods); applyBoxM(moHexer); applyBoxM(moAngryDie); applyBoxM(moAnimatedDie); applyBoxI(itCurseWeakness, true); applyBoxI(itCurseFatigue, true); applyBoxI(itCurseDraining, true); applyBoxI(itCurseRepulsion, true); applyBoxI(itCurseGluttony, true); applyBoxI(itCurseWater, true); list_invorb(); applyBoxEnum(bow::weapon, "weapon choice"); applyBoxEnum(bow::style, "crossbow style"); applyBoxOrb(itOrbFish); list_invorb(); applyBoxNum(items[itCrossbow]); applyBoxNum(items[itRevolver]); applyBoxNum(items[itAsteroid]); applyBoxM(moAsteroid); applyBoxNum(items[itTreat]); applyBoxM(moVampire); applyBoxNum(asteroids_generated); applyBoxNum(asteroid_orbs_generated); if(POSSCORE != boxid) printf("ERROR: %d boxes\n", boxid); if(isize(invorb)) { println(hlog, "ERROR: Orbs not taken into account"); exit(1); } } /** \brief save the current game values to save */ EX void saveBox() { boxid = 0; saving = true; applyBoxes(); saving = false; } /** \brief load the current game values from save */ void loadBox() { // have boxid boxid = 0; loading = true; applyBoxes(); loading = false; } const int MODECODE_BOX = 387; modecode_t fill_modecode() { dynamicval sp1(multi::players, save.box[197]); dynamicval sp2(geometry, (eGeometry) save.box[116]); if(among(geometry, gArchimedean, gProduct, gRotSpace, gArbitrary)) return 6; /* these would not be saved nor loaded correctly */ dynamicval sp3(shmup::on, save.box[119]); dynamicval sp4(land_structure, (eLandStructure) save.box[196]); dynamicval sp5(variation, (eVariation) save.box[186]); dynamicval sp7(gp::param.first, save.box[342]); dynamicval sp8(gp::param.second, save.box[343]); #if CAP_INV dynamicval spinv(inv::on, save.box[306]); #endif if(save.box[238]) geometry = gSphere; if(save.box[239]) geometry = gElliptic; if(save.box[341]) variation = eVariation::goldberg; if(save.box[344]) variation = eVariation::irregular; if(multi::players < 0 || multi::players > MAXPLAYER) return 6; if(multi::players == 0) multi::players = 1; if(shmup::on && multi::players == 1 && boxid <= 258) return 6; /* not sure why */ return modecode(2); } /** \brief load the current game values from save into the highscore tables */ void loadBoxHigh() { saved_modecode = save.box[MODECODE_BOX]; boxid = 0; loadingHi = true; applyBoxes(); loadingHi = false; } EX } // certify that saves and achievements were received // in an official version of HyperRogue EX namespace anticheat { EX int certify(const string& s, int a, int b, int c, int d IS(0)); EX } #if CAP_CERTIFY #include "private/certify.cpp" #endif #if !CAP_CERTIFY EX namespace anticheat { EX bool tampered; void save(FILE *f) {} bool load(FILE *f, scores::score& sc, const string& ver) {return true; } int certify(const string& s, int a, int b, int c, int d) { return d; } int check(int cv, const string& ver, const string& s, int a, int b, int c, int d=0) { return cv==d; } void nextid(int& tid, const string& ver, int cert) { tid++; } EX } #endif long long saveposition = -1; EX void remove_emergency_save() { if(scorefile == "") return; #if !ISWINDOWS if(saveposition >= 0) { if(truncate(scorefile.c_str(), saveposition)) {} saveposition = -1; } #endif } scores::score scorebox; EX bool save_cheats; EX void saveStats(bool emergency IS(false)) { DEBBI(DF_INIT, ("saveStats [", scorefile, "]")); if(autocheat && !save_cheats) return; if(scorefile == "") return; #if CAP_TOUR if(tour::on && !save_cheats) return; #endif if(randomPatternsMode && !save_cheats) return; if(daily::on) return; if(peace::on && !save_cheats) return; if(experimental) return; if(!gold() && !racing::on) return; remove_emergency_save(); auto& xcode = scores::saved_modecode; xcode = modecode(); FILE *f = fopen(scorefile.c_str(), "at"); if(!f) { // printf("Could not open the score file '%s'!\n", scorefile); addMessage(XLAT("Could not open the score file: ") + scorefile); return; } if(emergency) { saveposition = ftell(f); // if(!timerghost) addMessage(XLAT("Emergency save at ") + its(saveposition)); } time_t timer; timer = time(NULL); char sbuf[128]; strftime(sbuf, 128, "%c", localtime(&timerstart)); char buf[128]; strftime(buf, 128, "%c", localtime(&timer)); if((tactic::on || yendor::on) && !items[itOrbSafety] && !cheater) { int t = (int) (timer - timerstart); if(tactic::on) { int score = items[treasureType(specialland)]; if(score) { int c = anticheat::certify(dnameof(specialland), turncount, t, (int) timerstart, unsigned(xcode)*999 + tactic::id + 256 * score); fprintf(f, "TACTICS %s %d %d %d %d %d %d %d %d date: %s\n", VER, tactic::id, specialland, score, turncount, t, int(timerstart), c, int(xcode), buf); tactic::record(specialland, score); anticheat::nextid(tactic::id, VER, c); } } if(yendor::on) fprintf(f, "YENDOR %s %d %d %d %d %d %d %d %d date: %s\n", VER, yendor::lastchallenge, items[itOrbYendor], yendor::won, turncount, t, int(timerstart), anticheat::certify(yendor::won ? "WON" : "LOST", turncount, t, (int) timerstart, unsigned(xcode)*999 + yendor::lastchallenge + 256 * items[itOrbYendor]), int(xcode), buf); fclose(f); return; } #if CAP_RACING if(racing::on) { auto& bs = racing::best_scores_to_save; if(racing::official_race && !cheater && bs.count(specialland)) { fprintf(f, "RACING %s %d %d date: %s\n", VER, int(specialland), bs[specialland], buf); bs.erase(specialland); } fclose(f); return; } #endif fprintf(f, "HyperRogue: game statistics (version " VER ")\n"); if(cheater) fprintf(f, "CHEATER! (cheated %d times)\n", cheater); if(true) { fprintf(f, VER); if(!shmup::on) items[itOrbLife] = countMyGolems(moGolem); if(!shmup::on) items[itOrbFriend] = countMyGolems(moTameBomberbird); if(!shmup::on) kills[moPrincessMoved] = countMyGolems(moPrincess); if(!shmup::on) kills[moPrincessArmedMoved] = countMyGolems(moPrincessArmed); if(!shmup::on) princess::saveHP = countMyGolemsHP(moPrincess); if(!shmup::on) princess::saveArmedHP = countMyGolemsHP(moPrincessArmed); scores::saveBox(); for(int i=0; i 1) fprintf(f, "Multi-player (%d players)\n", multi::players); fprintf(f, "Number of cells explored, by distance from the player:\n"); {for(int i=0; i<10; i++) fprintf(f, " %d", explore[i]);} fprintf(f, "\n"); if(kills[0]) fprintf(f, "walls melted: %d\n", kills[0]); fprintf(f, "cells travelled: %d\n", celldist(cwt.at)); fprintf(f, "\n"); for(int i=0; i= 0 && save.box[258] < coh) { hints[save.box[258]].last = save.box[1]; } loadBoxHigh(); break; } } } if(buf[0] == 'A' && buf[1] == 'C' && buf[2] == 'H') { char buf1[80], buf2[80]; sscanf(buf, "%70s%70s", buf1, buf2); if(buf2 == string("PRINCESS1")) princess::everSaved = true; if(buf2 == string("YENDOR2")) yendor::everwon = true; if(buf2 == string("CR4")) chaosUnlocked = true; } if(buf[0] == 'T' && buf[1] == 'A' && buf[2] == 'C') { ok = false; char buf1[80], ver[10]; int tid, land, score, tc, t, ts, cert; int xc = -1; sscanf(buf, "%70s%9s%d%d%d%d%d%d%d%d", buf1, ver, &tid, &land, &score, &tc, &t, &ts, &cert, &xc); eLand l2 = eLand(land); if(land == laMirror && verless(ver, "10.0")) l2 = laMirrorOld; if(xc == -1) for(xc=0; xc<32768; xc++) if(anticheat::check(cert, ver, dnameof(l2), tc, t, ts, xc*999+unsigned(tid) + 256 * score)) break; if(tid == tactic::id && (anticheat::check(cert, ver, dnameof(l2), tc, t, ts, xc*unsigned(999)+ unsigned(tid) + 256 * score))) { if(score != 0 && !(land == laOcean && verless(ver, "8.0f")) && !(land == laTerracotta && verless(ver, "10.3e")) && !(land == laWildWest && verless(ver, "11.3b") && !verless(ver, "11.3"))) tactic::record(l2, score, xc); anticheat::nextid(tactic::id, ver, cert); } } if(buf[0] == 'Y' && buf[1] == 'E' && buf[2] == 'N') { char buf1[80], ver[10]; int cid, oy, won, tc, t, ts, cert=0, xc = -1; sscanf(buf, "%70s%9s%d%d%d%d%d%d%d%d", buf1, ver, &cid, &oy, &won, &tc, &t, &ts, &cert, &xc); if(xc == -1) for(xc=0; xc<32768; xc++) if(anticheat::check(cert, ver, won ? "WON" : "LOST", tc, t, ts, xc*999 + cid + 256 * oy)) break; if(won) if(anticheat::check(cert, ver, won ? "WON" : "LOST", tc, t, ts, xc*999 + cid + 256 * oy)) { if(xc == 19 && cid == 25) xc = 0; if(cid > 0 && cid < YENDORLEVELS) if(!(verless(ver, "8.0f") && oy > 1 && cid == 15)) if(!(verless(ver, "9.3b") && oy > 1 && (cid == 27 || cid == 28))) { yendor::bestscore[xc][cid] = max(yendor::bestscore[xc][cid], oy); } } } #if CAP_RACING if(buf[0] == 'R' && buf[1] == 'A' && buf[2] == 'C') { char buf1[80], ver[10]; int land, score; sscanf(buf, "%70s%9s%d%d", buf1, ver, &land, &score); /* score may equal 0 because of earlier bugs */ if(score) { auto& res = racing::best_scores[eLand(land)]; if(score < res || res == 0) res = score; } println(hlog, "loaded the score for ", dnameof(eLand(land)), " of ", score); } #endif } fclose(f); // this is the index of Orb of Safety if(ok && sc.box[65 + 4 + itOrbSafety - itOrbLightning]) load_last_save(); } EX void load_last_save() { auto& sc = scorebox; anticheat::tampered = tamper; // printf("box = %d (%d)\n", sc.box[65 + 4 + itOrbSafety - itOrbLightning], boxid); // printf("boxid = %d\n", boxid); using namespace scores; for(int i=0; i= OINF) variation = eVariation::pure; if(INVERSE && !mhybrid) variation = gp::variation_for(gp::param); if(ginf[target].default_variation == eVariation::pure && geometry != gArchimedean && !mhybrid) variation = eVariation::pure; geometry_settings(was_default); if(geometry == gArbitrary) { arb::convert::base_geometry = geometry; arb::convert::base_variation = variation; } if(rotspace) { check_cgi(); cgi.require_basics(); hybrid::csteps = cgi.psl_steps; } } } EX void set_variation(eVariation target) { if(variation != target) { stop_game(); if(is_subcube_based(target)) { if(!reg3::in()) geometry = hyperbolic ? gSpace435 : gCell8; variation = target; return; } if(is_reg3_variation(target)) { if(!reg3::in()) geometry = hyperbolic ? gSpace435 : gCell8; variation = target; return; } if(target != eVariation::pure) { if(bt::in() || sol || aperiodic || WDIM == 3) if(!mproduct) geometry = gNormal; } auto& cd = ginf[gCrystal]; if(target == eVariation::bitruncated && cryst && cd.sides == 8 && cd.vertex == 4) { cd.vertex = 3; cd.tiling_name = "{8,3}"; target = eVariation::pure; } variation = target; } } void stop_tour() { while(gamestack::pushed()) { gamestack::pop(); stop_game(); } } EX void switch_game_mode(char switchWhat) { DEBBI(DF_INIT, ("switch_game_mode ", switchWhat)); switch(switchWhat) { case rg::peace: peace::on = !peace::on; tactic::on = yendor::on = princess::challenge = randomPatternsMode = inv::on = false; racing::on = false; bow::weapon = bow::wBlade; break; case rg::dualmode: stop_tour(); tour::on = false; racing::on = false; yendor::on = tactic::on = princess::challenge = false; bow::weapon = bow::wBlade; if(!dual::state) dual::enable(); else dual::disable(); break; case rg::inv: inv::on = !inv::on; if(tactic::on) firstland = specialland = laIce; tactic::on = yendor::on = princess::challenge = peace::on = false; racing::on = false; break; case rg::chaos: if(tactic::on) firstland = laIce; yendor::on = tactic::on = princess::challenge = false; land_structure = ls::any_chaos() ? lsNiceWalls : lsChaos; if(closed_or_bounded) set_geometry(gNormal); racing::on = false; break; #if CAP_TOUR case rg::tour: if(tour::on) stop_tour(); geometry = gNormal; yendor::on = tactic::on = princess::challenge = peace::on = inv::on = false; dual::disable(); land_structure = lsNiceWalls; randomPatternsMode = false; variation = eVariation::bitruncated; #if CAP_GP gp::param = gp::loc(1, 1); #endif shmup::on = false; tour::on = !tour::on; racing::on = false; break; #endif case rg::yendor: yendor::on = !yendor::on; tactic::on = false; peace::on = false; inv::on = false; princess::challenge = false; randomPatternsMode = false; land_structure = lsNiceWalls; racing::on = false; if(!yendor::on) firstland = laIce; dual::disable(); break; #if CAP_RACING case rg::racing: racing::on = !racing::on; shmup::on = racing::on; peace::on = false; tour::on = false; inv::on = false; yendor::on = false; land_structure = racing::on ? lsSingle : lsNiceWalls; princess::challenge = false; dual::disable(); break; #endif case rg::tactic: tactic::on = !tactic::on; yendor::on = false; peace::on = false; inv::on = false; randomPatternsMode = false; princess::challenge = false; racing::on = false; land_structure = tactic::on ? lsSingle : lsNiceWalls; if(!tactic::on) firstland = laIce; dual::disable(); break; case rg::shmup: shmup::on = !shmup::on; princess::challenge = false; if(!shmup::on) racing::on = false; if(!shmup::on && multi::players > 1) bow::weapon = bow::wBlade; break; case rg::randpattern: randomPatternsMode = !randomPatternsMode; tactic::on = false; yendor::on = false; peace::on = false; princess::challenge = false; break; case rg::princess: princess::challenge = !princess::challenge; firstland = specialland = princess::challenge ? laPalace : laIce; shmup::on = false; tactic::on = false; yendor::on = false; land_structure = princess::challenge ? lsSingle : lsNiceWalls; inv::on = false; racing::on = false; dual::disable(); break; #if CAP_DAILY case rg::daily: daily::setup(); break; case rg::daily_off: daily::turnoff(); break; #endif } } EX void start_game() { if(game_active) return; DEBBI(DF_INIT, ("start_game")); if(dual::state == 1) dual::assign_landsides(); if(dual::split(start_game)) return; restart: game_active = true; gamegen_failure = false; ignored_memory_warning = false; check_cgi(); cgi.require_basics(); #if CAP_ARCM arcm::current_or_fake().compute_geometry(); #endif initcells(); get_expansion().reset(); init_disk_cells(); if(randomPatternsMode) { for(int i=0; i allowed_params = { "creature_scale", "global_boundary_ratio", "specialland" }; EX void save_mode_to_file(const string& fname) { shstream ss; save_mode_data(ss); string s = as_hexstring(ss.s); fhstream f(fname, "w"); if(!f.f) throw hstream_exception(); println(f, modheader); println(f, s); if(custom_welcome != "") println(f, "CMSG ", custom_welcome); for(auto& ap: allowed_params) { auto& s = params[ap]->saver; if(s->dosave()) println(f, ap, "=", s->save()); } } EX void load_mode_from_file(const string& fname) { fhstream f(fname, "r"); if(!f.f) throw hstream_exception(); string header = scanline(f); if(header[0] != '#') throw hstream_exception(); string hex = scanline(f); shstream ss; ss.s = from_hexstring(hex + "00"); custom_welcome = ""; while(true) { string s = scanline_noblank(f); if(s == "") break; else if(s.substr(0, 5) == "CMSG ") custom_welcome = s.substr(5); else { auto pos = s.find("="); if(pos != string::npos) { string name = s.substr(0, pos); string value = s.substr(pos+1); if(!params.count(name) || !allowed_params.count(name)) { println(hlog, "# parameter unknown: ", name); continue; } params[name]->load_as_animation(value); } } } stop_game(); load_mode_data_with_zero(ss); start_game(); } auto cgm = addHook(hooks_clearmemory, 40, [] () { pathq.clear(); dcal.clear(); clearshadow(); for(int i=0; istore(pathq); gd->store(dcal); gd->store(recallCell); gd->store(butterflies); gd->store(buggycells); gd->store(crush_now); gd->store(crush_next); gd->store(rosemap); gd->store(airmap); gd->store(adj_memo); gd->store(pd_from); gd->store(pd_range); gd->store(pathqm); gd->store(gravity_state); gd->store(last_gravity_state); gd->store(shpos); gd->store(cshpos); gd->store(gp::do_adjm); }) + addHook(hooks_removecells, 0, [] () { eliminate_if(crush_next, is_cell_removed); eliminate_if(crush_now, is_cell_removed); eliminate_if(buggycells, is_cell_removed); eliminate_if(butterflies, [] (pair& p) { return is_cell_removed(p.first); }); for(int i=0; i to_remove; for(auto p: rosemap) if(is_cell_removed(p.first)) to_remove.push_back(p.first); for(auto r: to_remove) rosemap.erase(r); }); }