/* Copyright (C) 2007, 2010 - Bit-Blot This file is part of Aquaria. Aquaria is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "DSQ.h" #include "Game.h" #include "Avatar.h" #include "StatsAndAchievements.h" #ifndef ARRAYSIZE #define ARRAYSIZE(x) (sizeof (x) / sizeof ((x)[0])) #endif #define _ACH_ID( id, name ) { id, #id, name, "", 0, 0 } static Achievement g_rgAchievements[] = { _ACH_ID( ACH_DISCOVER_ALL_RECIPES, "Experienced Chef" ), // verified _ACH_ID( ACH_MASS_TRANSIT, "Mass Transit" ), // verified _ACH_ID( ACH_EXPLORER, "Explorer" ), // verified _ACH_ID( ACH_HEALTHY, "Healthy" ), // verified (really make sure) _ACH_ID( ACH_AQUIRE_ALL_SONGS, "Songstress" ), // verified _ACH_ID( ACH_DEFEAT_PRIESTS, "xxxxxxxxx" ), // verified _ACH_ID( ACH_DEFEAT_OCTOMUN, "xxxxxxxxx" ), // verified _ACH_ID( ACH_DEFEAT_ROCKCRAB, "xxxxxxxxx" ), // verified _ACH_ID( ACH_DEFEAT_MANTISSHRIMP, "xxxxxxxxx" ), // verified _ACH_ID( ACH_DEFEAT_KINGJELLY, "xxxxxxxxx" ), // verified _ACH_ID( ACH_REACHED_OPEN_WATERS, "xxxxxxxxx" ), // verified _ACH_ID( ACH_REACHED_SPRITE_CAVE, "xxxxxxxxx" ), // verified _ACH_ID( ACH_REACHED_THE_VEIL, "xxxxxxxxx" ), // verified _ACH_ID( ACH_ROMANCE, "xxxxxxxxx" ), // verified _ACH_ID( ACH_BELLY_OF_THE_WHALE, "xxxxxxxxx" ), // verified _ACH_ID( ACH_ALEK_AND_DAREC, "xxxxxxxxx" ), // verified _ACH_ID( ACH_THE_FROZEN_VEIL, "xxxxxxxxx" ), // verified _ACH_ID( ACH_MOMMY_AND_DADDY, "xxxxxxxxx" ), // verified _ACH_ID( ACH_RESCUED_ALL_SOULS, "xxxxxxxxx" ), // verified _ACH_ID( ACH_BUCKING_BRONCO, "xxxxxxxxx" ), // verified _ACH_ID( ACH_COMBO_EATER, "xxxxxxxxx" ), // verified (adjust # of devours?) _ACH_ID( ACH_ATE_A_PARROT, "xxxxxxxxx" ), // verified _ACH_ID( ACH_FLUNG_A_MONKEY, "xxxxxxxxx" ), // verified _ACH_ID( ACH_KILLED_THE_COWARD, "xxxxxxxxx" ), // verified _ACH_ID( ACH_HIGH_DIVE, "xxxxxxxxx" ), // verified _ACH_ID( ACH_SPEED_RACER, "xxxxxxxxx" ), // verified _ACH_ID( ACH_DEFEAT_MERGOG, "xxxxxxxxx" ), // verified }; // temp record bool killedParrotWithBite = false; bool flungMonkey = false; bool killedCoward = false; bool highDiveIsHigh = false; bool highDiveIsDone = false; float ridingEkkritTime = 0.0f; float ridingEkkritTimeMax = 60.0f; bool rodeEkkritToTheStars = false; int biteDeathComboNum = 0; float biteDeathComboTime = 0.5f; float biteDeathComboCounter = 0.0f; const int biteDeathComboMax = 6; const int seahorseRaceAchievementTimeMin = 59; const int SUNKENCITY_BOSSDONE = 16; const int FLAG_SUNKENCITY_PUZZLE = 113; const int FLAG_SPIRIT_ERULIAN = 124; const int FLAG_SPIRIT_KROTITE = 125; const int FLAG_SPIRIT_DRASK = 126; const int FLAG_SPIRIT_DRUNIAD = 127; const int FLAG_TRANSTURTLE_VEIL01 = 130; const int FLAG_TRANSTURTLE_OPENWATER06 = 131; const int FLAG_TRANSTURTLE_FOREST04 = 132; const int FLAG_TRANSTURTLE_OPENWATER03 = 133; const int FLAG_TRANSTURTLE_FOREST05 = 134; const int FLAG_TRANSTURTLE_MAINAREA = 135; const int FLAG_TRANSTURTLE_SEAHORSE = 136; const int FLAG_TRANSTURTLE_VEIL02 = 137; const int FLAG_TRANSTURTLE_ABYSS03 = 138; const int FLAG_TRANSTURTLE_FINALBOSS = 139; const int FLAG_SEAHORSEBESTTIME = 247; const int FLAG_MINIBOSS_START = 700; const int FLAG_MINIBOSS_NAUTILUSPRIME = 700; const int FLAG_MINIBOSS_KINGJELLY = 701; const int FLAG_MINIBOSS_MERGOG = 702; const int FLAG_MINIBOSS_CRAB = 703; const int FLAG_MINIBOSS_OCTOMUN = 704; const int FLAG_MINIBOSS_MANTISSHRIMP = 705; const int FLAG_MINIBOSS_PRIESTS = 706; const int FLAG_MINIBOSS_END = 720; //----------------------------------------------------------------------------- // Purpose: Constructor //----------------------------------------------------------------------------- #ifdef _MSC_VER #pragma warning( push ) // warning C4355: 'this' : used in base member initializer list // This is OK because it's warning on setting up the Steam callbacks, they won't use this until after construction is done #pragma warning( disable : 4355 ) #endif StatsAndAchievements::StatsAndAchievements() { /* std::ostringstream os; os << "app_id: " << SteamUtils()->GetAppID(); debugLog(os.str()); */ #ifdef BBGE_BUILD_ACHIEVEMENTS_INTERNAL unlockedDisplayTimestamp = -1.0f; #endif requestedStats = false; statsValid = false; storeStats = false; /* m_flGameFeetTraveled = 0; m_nTotalGamesPlayed = 0; m_nTotalNumWins = 0; m_nTotalNumLosses = 0; m_flTotalFeetTraveled = 0; m_flMaxFeetTraveled = 0; m_flAverageSpeed = 0; m_hDisplayFont = pGameEngine->HCreateFont( ACHDISP_FONT_HEIGHT, FW_MEDIUM, false, "Arial" ); if ( !m_hDisplayFont ) OutputDebugString( "Stats font was not created properly, text won't draw\n" ); */ } #ifdef _MSC_VER #pragma warning( pop ) #endif //----------------------------------------------------------------------------- // Purpose: Run a frame for the CStatsAndAchievements //----------------------------------------------------------------------------- void StatsAndAchievements::RunFrame() { #ifdef BBGE_BUILD_ACHIEVEMENTS_INTERNAL if ( !requestedStats ) { requestedStats = true; const size_t max_achievements = ARRAYSIZE(g_rgAchievements); FILE *io = NULL; // Get generic achievement data... io = fopen("data/achievements.txt", "r"); char line[1024]; for (size_t i = 0; i < max_achievements; i++) { if (!io || (fgets(line, sizeof (line), io) == NULL)) snprintf(line, sizeof (line), "Achievement #%d", (int) i); else { for (char *ptr = (line + strlen(line)) - 1; (ptr >= line) && ((*ptr == '\r') || (*ptr == '\n')); ptr--) *ptr = '\0'; } line[sizeof (g_rgAchievements[i].name) - 1] = '\0'; // just in case. strcpy(g_rgAchievements[i].name, line); if (!io || (fgets(line, sizeof (line), io) == NULL)) snprintf(line, sizeof (line), "[Description of Achievement #%d is missing!]", (int) i); else { for (char *ptr = (line + strlen(line)) - 1; (ptr >= line) && ((*ptr == '\r') || (*ptr == '\n')); ptr--) *ptr = '\0'; } line[sizeof (g_rgAchievements[i].desc) - 1] = '\0'; // just in case. strcpy(g_rgAchievements[i].desc, line); // unsupported at the moment. g_rgAchievements[i].iconImage = 0; } if (io != NULL) fclose(io); // See what this specific player has achieved... unsigned char *buf = new unsigned char[max_achievements]; size_t br = 0; const std::string fname(core->getUserDataFolder() + "/achievements.bin"); io = fopen(fname.c_str(), "rb"); if (io == NULL) statsValid = true; // nothing to report. else { br = fread(buf, sizeof (buf[0]), max_achievements, io); fclose(io); } if (br == max_achievements) { statsValid = true; // but we'll reset if there's a problem. for (size_t i = 0; statsValid && (i < max_achievements); i++) { const int val = ((int) (buf[i] ^ 0xFF)) - ((int)i); statsValid = ((val == 0) || (val == 1)); g_rgAchievements[i].achieved = (val == 1); } } delete[] buf; } #endif if ( !statsValid ) { debugLog("stats not valid"); return; } // Get info from sources // Evaluate achievements // but only if we're not in a mod if (!dsq->mod.isActive()) { for ( int iAch = 0; iAch < ARRAYSIZE( g_rgAchievements ); ++iAch ) { EvaluateAchievement( g_rgAchievements[iAch] ); } } // Store stats StoreStatsIfNecessary(); } void StatsAndAchievements::appendStringData(std::string &data) { if (!statsValid) { data += "(Sorry, achievement data is apparently invalid.)\n\n"; return; } int count; count = 0; data += "Unlocked:\n\n"; for ( int iAch = 0; iAch < ARRAYSIZE( g_rgAchievements ); ++iAch ) { const Achievement &ach = g_rgAchievements[iAch]; if (!ach.achieved) continue; count++; data += " "; data += ach.name; data += ": "; data += ach.desc; data += "\n"; } if (count == 0) data += " (none!)\n"; data += "\n"; count = 0; data += "Locked:\n\n"; for ( int iAch = 0; iAch < ARRAYSIZE( g_rgAchievements ); ++iAch ) { const Achievement &ach = g_rgAchievements[iAch]; if (ach.achieved) continue; count++; data += " "; data += ach.name; data += ": "; data += ach.desc; data += "\n"; } if (count == 0) data += " (none!)\n"; data += "\n"; } //----------------------------------------------------------------------------- // Purpose: Accumulate distance traveled //----------------------------------------------------------------------------- /* void StatsAndAchievements::AddDistanceTraveled( float flDistance ) { m_flGameFeetTraveled += SpaceWarClient()->PixelsToFeet( flDistance ); } */ //----------------------------------------------------------------------------- // Purpose: Game state has changed //----------------------------------------------------------------------------- /* void StatsAndAchievements::OnGameStateChange( EClientGameState eNewState ) { if ( !m_bStatsValid ) return; switch ( eNewState ) { case k_EClientStatsAchievements: case k_EClientGameStartServer: case k_EClientGameMenu: case k_EClientGameQuitMenu: case k_EClientGameExiting: case k_EClientGameInstructions: case k_EClientGameConnecting: case k_EClientGameConnectionFailure: default: break; case k_EClientGameActive: // Reset per-game stats m_flGameFeetTraveled = 0; m_ulTickCountGameStart = m_pGameEngine->GetGameTickCount(); break; case k_EClientFindInternetServers: break; case k_EClientGameWinner: if ( SpaceWarClient()->BLocalPlayerWonLastGame() ) m_nTotalNumWins++; else m_nTotalNumLosses++; // fall through case k_EClientGameDraw: // Tally games m_nTotalGamesPlayed++; // Accumulate distances m_flTotalFeetTraveled += m_flGameFeetTraveled; // New max? if ( m_flGameFeetTraveled > m_flMaxFeetTraveled ) m_flMaxFeetTraveled = m_flGameFeetTraveled; // Calc game duration m_flGameDurationSeconds = ( m_pGameEngine->GetGameTickCount() - m_ulTickCountGameStart ) / 1000.0; // We want to update stats the next frame. storeStats = true; break; } } */ //----------------------------------------------------------------------------- // Purpose: see if we should unlock this achievement //----------------------------------------------------------------------------- void StatsAndAchievements::EvaluateAchievement( Achievement &achievement ) { //debugLog("Eval..."); // Already have it? if ( achievement.achieved ) { //debugLog(std::string(achievement.chAchievementID) + " was already achieved"); return; } switch ( achievement.achievementID ) { // real evals: case ACH_DISCOVER_ALL_RECIPES: { bool knowAll=true; // this code is part of what avoids duplicate recipes being required // in the case of veggie soup there are two ways to make it // i figure that finding one way is enough for the achievement bool didLeafPoultice = false; bool didPoisonLoaf = false; bool didVeggieSoup = false; for (int i = 0; i < dsq->continuity.recipes.size(); i++ ) { if (dsq->continuity.recipes[i].isKnown()) { if (dsq->continuity.recipes[i].result == "LeafPoultice") { didLeafPoultice = true; } else if (dsq->continuity.recipes[i].result == "PoisonLoaf") { didPoisonLoaf = true; } else if (dsq->continuity.recipes[i].result == "VeggieSoup") { didVeggieSoup = true; } } } for (int i = 0; i < dsq->continuity.recipes.size(); i++ ) { if (!dsq->continuity.recipes[i].isKnown()) { if ((dsq->continuity.recipes[i].result == "LeafPoultice" && didLeafPoultice) || (dsq->continuity.recipes[i].result == "PoisonLoaf" && didPoisonLoaf) || (dsq->continuity.recipes[i].result == "VeggieSoup" && didVeggieSoup)) {} else { /* std::ostringstream os; os << "doesn't know recipe: " << dsq->continuity.recipes[i].result; debugLog(os.str()); */ knowAll = false; } } } if (knowAll) { UnlockAchievement(achievement); } } break; case ACH_MASS_TRANSIT: { /* debugLog("eval ACH_MASS_TRANSIT"); std::ostringstream os; os << "FLAG_TRANSTURTLE_VEIL01: " << dsq->continuity.getFlag(FLAG_TRANSTURTLE_VEIL01) << "\n"; os << "FLAG_TRANSTURTLE_VEIL02: " << dsq->continuity.getFlag(FLAG_TRANSTURTLE_VEIL02) << "\n"; os << "FLAG_TRANSTURTLE_OPENWATER03: " << dsq->continuity.getFlag(FLAG_TRANSTURTLE_OPENWATER03) << "\n"; os << "FLAG_TRANSTURTLE_FOREST04: " << dsq->continuity.getFlag(FLAG_TRANSTURTLE_FOREST04) << "\n"; os << "FLAG_TRANSTURTLE_FOREST05: " << dsq->continuity.getFlag(FLAG_TRANSTURTLE_FOREST05) << "\n"; os << "FLAG_TRANSTURTLE_MAINAREA: " << dsq->continuity.getFlag(FLAG_TRANSTURTLE_MAINAREA) << "\n"; os << "FLAG_TRANSTURTLE_SEAHORSE: " << dsq->continuity.getFlag(FLAG_TRANSTURTLE_SEAHORSE) << "\n"; os << "FLAG_TRANSTURTLE_ABYSS03: " << dsq->continuity.getFlag(FLAG_TRANSTURTLE_ABYSS03) << "\n"; os << "FLAG_TRANSTURTLE_FINALBOSS: " << dsq->continuity.getFlag(FLAG_TRANSTURTLE_FINALBOSS) << "\n"; debugLog(os.str()); */ if (dsq->continuity.getFlag(FLAG_TRANSTURTLE_VEIL01) > 0 && dsq->continuity.getFlag(FLAG_TRANSTURTLE_VEIL02) > 0 && dsq->continuity.getFlag(FLAG_TRANSTURTLE_OPENWATER03) > 0 && dsq->continuity.getFlag(FLAG_TRANSTURTLE_FOREST04) > 0 && dsq->continuity.getFlag(FLAG_TRANSTURTLE_FOREST05) > 0 && dsq->continuity.getFlag(FLAG_TRANSTURTLE_MAINAREA) > 0 && dsq->continuity.getFlag(FLAG_TRANSTURTLE_SEAHORSE) > 0 && dsq->continuity.getFlag(FLAG_TRANSTURTLE_ABYSS03) > 0 && dsq->continuity.getFlag(FLAG_TRANSTURTLE_FINALBOSS) > 0) { UnlockAchievement(achievement); } } break; case ACH_EXPLORER: { // check world map data somehow bool hasAllMap = true; for (int i = 0; i < dsq->continuity.worldMap.getNumWorldMapTiles(); i++) { WorldMapTile *tile = dsq->continuity.worldMap.getWorldMapTile(i); if (!tile->revealed && (nocasecmp(tile->name, "thirteenlair") != 0)) { /* std::ostringstream os; os << "does not have: " << tile->name; debugLog(os.str()); */ hasAllMap = false; break; } } if (hasAllMap) { UnlockAchievement(achievement); } } break; case ACH_HEALTHY: // is it really 10?? if ( dsq->continuity.maxHealth >= 10 ) { UnlockAchievement(achievement); } break; case ACH_AQUIRE_ALL_SONGS: //debugLog("eval ACH_AQUIRE_ALL_SONGS"); if (dsq->continuity.hasSong(SONG_BIND) && dsq->continuity.hasSong(SONG_SHIELDAURA) && dsq->continuity.hasSong(SONG_LI) && dsq->continuity.hasSong(SONG_ENERGYFORM) && dsq->continuity.hasSong(SONG_BEASTFORM) && dsq->continuity.hasSong(SONG_NATUREFORM) && dsq->continuity.hasSong(SONG_SUNFORM) && dsq->continuity.hasSong(SONG_DUALFORM) && dsq->continuity.hasSong(SONG_FISHFORM) && dsq->continuity.hasSong(SONG_SPIRITFORM)) { //errorLog("ACH_AQUIRE_ALL_SONGS!"); UnlockAchievement(achievement); } break; // gameplay case ACH_DEFEAT_PRIESTS: //if (dsq->continuity.getFlag(FLAG_MINIBOSS_PRIESTS) > 0) if (dsq->continuity.hasSong(SONG_SPIRITFORM)) { UnlockAchievement(achievement); } break; case ACH_DEFEAT_OCTOMUN: if (dsq->continuity.getFlag(FLAG_MINIBOSS_OCTOMUN) > 0) { UnlockAchievement(achievement); } break; case ACH_DEFEAT_ROCKCRAB: if (dsq->continuity.getFlag(FLAG_MINIBOSS_CRAB) > 0) { UnlockAchievement(achievement); } break; case ACH_DEFEAT_MERGOG: if (dsq->continuity.getFlag(FLAG_MINIBOSS_MERGOG) > 0) { UnlockAchievement(achievement); } break; case ACH_DEFEAT_MANTISSHRIMP: if (dsq->continuity.getFlag(FLAG_MINIBOSS_MANTISSHRIMP) > 0) { UnlockAchievement(achievement); } break; case ACH_DEFEAT_KINGJELLY: if (dsq->continuity.getFlag(FLAG_MINIBOSS_KINGJELLY) > 0) { UnlockAchievement(achievement); } break; case ACH_REACHED_OPEN_WATERS: if (dsq->game->sceneName == "openwater02") { UnlockAchievement(achievement); } break; case ACH_REACHED_SPRITE_CAVE: if (dsq->game->sceneName == "forestspritecave") { UnlockAchievement(achievement); } break; case ACH_REACHED_THE_VEIL: // when Naija jumps through the veil if (dsq->continuity.getFlag("leftWater")!=0) { UnlockAchievement(achievement); } break; case ACH_ROMANCE: if (dsq->continuity.getFlag(FLAG_LI) >= 100) { UnlockAchievement(achievement); } break; case ACH_BELLY_OF_THE_WHALE: if (dsq->game->sceneName == "whale") { UnlockAchievement(achievement); } break; case ACH_ALEK_AND_DAREC: if (dsq->game->sceneName == "weirdcave") { UnlockAchievement(achievement); } break; case ACH_THE_FROZEN_VEIL: if (dsq->game->sceneName == "frozenveil") { UnlockAchievement(achievement); } break; case ACH_MOMMY_AND_DADDY: //if (dsq->continuity.getFlag(SUNKENCITY_BOSSDONE) > 0) //setFlag(FLAG_SUNKENCITY_PUZZLE, SUNKENCITY_BOSSDONE) if (dsq->continuity.getFlag(FLAG_SUNKENCITY_PUZZLE) >= SUNKENCITY_BOSSDONE) { UnlockAchievement(achievement); } break; case ACH_RESCUED_ALL_SOULS: if (dsq->continuity.getFlag(FLAG_SPIRIT_ERULIAN) > 0 && dsq->continuity.getFlag(FLAG_SPIRIT_KROTITE) > 0 && dsq->continuity.getFlag(FLAG_SPIRIT_DRASK) > 0 && dsq->continuity.getFlag(FLAG_SPIRIT_DRUNIAD) > 0) { UnlockAchievement(achievement); } break; case ACH_BUCKING_BRONCO: // ride ekkrit for a minute if (rodeEkkritToTheStars) { UnlockAchievement(achievement); } break; case ACH_COMBO_EATER: // eat n=12 things in a row if (biteDeathComboNum >= biteDeathComboMax) { UnlockAchievement(achievement); } break; case ACH_ATE_A_PARROT: // eat one parrot if (killedParrotWithBite) { UnlockAchievement(achievement); } break; case ACH_FLUNG_A_MONKEY: // monkey gets hit with nature form if (flungMonkey) { UnlockAchievement(achievement); } break; case ACH_KILLED_THE_COWARD: // coward dies if (killedCoward) { UnlockAchievement(achievement); } break; case ACH_HIGH_DIVE: // fall from bird nest to water if (highDiveIsDone) { UnlockAchievement(achievement); } break; case ACH_SPEED_RACER: // get really low time in seahorse race int bestTime = dsq->continuity.getFlag(FLAG_SEAHORSEBESTTIME); if (bestTime > 0 && bestTime <= seahorseRaceAchievementTimeMin) { UnlockAchievement(achievement); } break; } } void StatsAndAchievements::flingMonkey(Entity *e) { flungMonkey = true; } void StatsAndAchievements::entityDied(Entity *eDead) { if (eDead->name == "parrot" && eDead->lastDamage.damageType == DT_AVATAR_BITE) { killedParrotWithBite = true; } if (eDead->name == "coward") { killedCoward = true; } if (eDead->lastDamage.damageType == DT_AVATAR_BITE) { // an enemy got bit to death // how much time from the last death? if (biteDeathComboCounter > biteDeathComboTime) { // then we missed out on a combo, suck a nut biteDeathComboNum = 0; } else { // we made it!! biteDeathComboNum ++; } biteDeathComboCounter = 0; } /* eDead->getState() == if (eDead->name == "monkey") { // we killed a monkey, but how? if (eDead->lastDamage.damageType == DT_AVATAR_VINE) { // with a nature form vine flungMonkey = true; } } */ } void StatsAndAchievements::update(float dt) { //debugLog("update stats and achievements"); Avatar *avatar = 0; if (dsq->game && dsq->game->avatar) { avatar = dsq->game->avatar; } if (avatar) { BoneLock *b = avatar->getBoneLock(); if (!rodeEkkritToTheStars) { if (!dsq->game->isPaused() && b->on) { //debugLog("boneLock->entity->name: " + b->entity->name); if (b->entity->name == "ekkrit") { ridingEkkritTime += dt; /* std::ostringstream os; os << "ridingEkkritTime: " << ridingEkkritTime; debugLog(os.str()); */ if (ridingEkkritTime >= ridingEkkritTimeMax) { rodeEkkritToTheStars = true; } } } } // TODO: verify sceneName is correct if (dsq->game->sceneName == "veil02") { if (avatar->position.x > 18056 && avatar->position.x < 21238 && avatar->position.y < 4185) { debugLog("highDiveIsHigh!"); highDiveIsHigh = true; } else if (highDiveIsHigh && avatar->isUnderWater()) { // fell into water from right place, must have been the right jump (lol, we hope) highDiveIsDone = true; debugLog("highDiveIsDone!"); } } else { highDiveIsHigh = false; highDiveIsDone = false; } } #ifdef BBGE_BUILD_ACHIEVEMENTS_INTERNAL // change no state if we're still fading in/out. if (!dsq->achievement_box->alpha.isInterpolating()) { const float maxUnlockDisplayTime = 5.0f; // still displaying an unlock notification? if (unlockedDisplayTimestamp > 0.0f) { unlockedDisplayTimestamp -= dt; if (unlockedDisplayTimestamp <= 0.0f) { unlockedDisplayTimestamp = -1.0f; dsq->achievement_text->alpha.interpolateTo(0, 1); dsq->achievement_box->alpha.interpolateTo(0, 1.2); } } // more achievements to display? else if (unlockedToBeDisplayed.size() > 0) { const std::string &name = unlockedToBeDisplayed.front(); unlockedDisplayTimestamp = maxUnlockDisplayTime; std::string text("Achievement Unlocked:\n"); text += name; unlockedToBeDisplayed.pop(); dsq->achievement_text->setText(text); dsq->achievement_text->alpha.interpolateTo(1, 1); dsq->achievement_box->alpha.interpolateTo(1, 0.1); } } #endif } //----------------------------------------------------------------------------- // Purpose: Unlock this achievement //----------------------------------------------------------------------------- void StatsAndAchievements::UnlockAchievement( Achievement &achievement ) { if ((achievement.achieved) || (!statsValid)) return; achievement.achieved = true; // the icon may change once it's unlocked achievement.iconImage = 0; #ifdef BBGE_BUILD_ACHIEVEMENTS_INTERNAL unlockedToBeDisplayed.push( std::string(achievement.name) ); #endif // Store stats end of frame storeStats = true; } //----------------------------------------------------------------------------- // Purpose: Store stats in the Steam database //----------------------------------------------------------------------------- void StatsAndAchievements::StoreStatsIfNecessary() { if ( storeStats ) { // already set any achievements in UnlockAchievement #ifdef BBGE_BUILD_ACHIEVEMENTS_INTERNAL storeStats = false; // only ever try once. // FIXME: We should use a temporary file to ensure that data // isn't lost if the filesystem gets full. The canonical // method is to write to a new file, then call // rename("new.file", "existing.file") after the new file has // been successfully written; POSIX specifies that such a call // atomically replaces "existing.file" with "new.file". // However, I've heard that Windows doesn't allow this sort of // file replacement. Will this work on Windows and MacOS? // Please advise. --achurch const std::string fname(core->getUserDataFolder() + "/achievements.bin"); FILE *io = fopen(fname.c_str(), "wb"); if (io == NULL) return; const size_t max_achievements = ARRAYSIZE(g_rgAchievements); unsigned char *buf = new unsigned char[max_achievements]; for (size_t i = 0; i < max_achievements; i++) { int val = g_rgAchievements[i].achieved ? 1 : 0; buf[i] = ((unsigned char) (val + ((int)i))) ^ 0xFF; } if (fwrite(buf, sizeof (buf[0]), max_achievements, io) != max_achievements) debugLog("Failed to write achievements 1"); delete[] buf; char cruft[101]; for (size_t i = 0; i < sizeof (cruft); i++) cruft[i] = (char) rand(); if (fwrite(cruft, sizeof (cruft[0]), ARRAYSIZE(cruft), io) != ARRAYSIZE(cruft)) debugLog("Failed to write achievements 2"); fclose(io); #endif } }