Page Menu
Home
Phabricator (Chris)
Search
Configure Global Search
Log In
Files
F116734
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
161 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/Game.cpp b/src/Game.cpp
index 2ca9053..d5c47ad 100644
--- a/src/Game.cpp
+++ b/src/Game.cpp
@@ -1,2245 +1,2307 @@
/*
* Copyright (C) 2011-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Block.h"
#include "GameState.h"
#include "Functions.h"
#include "Settings.h"
#include "GameObjects.h"
#include "ThemeManager.h"
#include "Game.h"
#include "WordWrapper.h"
#include "LevelEditor.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "InputManager.h"
#include "MusicManager.h"
#include "Render.h"
#include "StatisticsManager.h"
#include "ScriptExecutor.h"
#include "ScriptDelayExecution.h"
#include "MD5.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>
#include <map>
#include <algorithm>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <SDL_ttf_fontfallback.h>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
const char* Game::blockName[TYPE_MAX]={"Block","PlayerStart","ShadowStart",
"Exit","ShadowBlock","Spikes",
"Checkpoint","Swap","Fragile",
"MovingBlock","MovingShadowBlock","MovingSpikes",
"Teleporter","Button","Switch",
"ConveyorBelt","ShadowConveyorBelt","NotificationBlock", "Collectable", "Pushable"
};
map<string,int> Game::blockNameMap;
map<int,string> Game::gameObjectEventTypeMap;
map<string,int> Game::gameObjectEventNameMap;
map<int,string> Game::levelEventTypeMap;
map<string,int> Game::levelEventNameMap;
string Game::recordFile;
//An internal function.
static void copyCompiledScripts(lua_State *state, const std::map<int, int>& src, std::map<int, int>& dest) {
//Clear the existing scripts.
for (auto it = dest.begin(); it != dest.end(); ++it) {
luaL_unref(state, LUA_REGISTRYINDEX, it->second);
}
dest.clear();
//Copy the source to the destination.
for (auto it = src.begin(); it != src.end(); ++it) {
lua_rawgeti(state, LUA_REGISTRYINDEX, it->second);
dest[it->first] = luaL_ref(state, LUA_REGISTRYINDEX);
}
}
//An internal function.
static void copyLevelObjects(const std::vector<Block*>& src, std::vector<Block*>& dest, bool setActive) {
//Clear the existing objects.
for (auto o : dest) delete o;
dest.clear();
//Copy the source to the destination.
for (auto o : src) {
if (o == NULL || o->isDelete) continue;
Block* o2 = new Block(*o);
dest.push_back(o2);
if (setActive) o2->setActive();
}
}
Game::Game(SDL_Renderer &renderer, ImageManager &imageManager):isReset(false)
, scriptExecutor(new ScriptExecutor())
,currentLevelNode(NULL)
,customTheme(NULL)
,background(NULL)
, levelRect(SDL_Rect{ 0, 0, 0, 0 }), levelRectInitial(SDL_Rect{ 0, 0, 0, 0 })
, arcade(false)
,won(false)
,interlevel(false)
,time(0)
,recordings(0)
,cameraMode(CAMERA_PLAYER)
,player(this),shadow(this),objLastCheckPoint(NULL)
, currentCollectables(0), currentCollectablesInitial(0)
, totalCollectables(0), totalCollectablesInitial(0)
{
levelRectSaved = SDL_Rect{ 0, 0, 0, 0 };
timeSaved = 0;
recordingsSaved = 0;
cameraModeSaved = (int)CAMERA_PLAYER;
currentCollectablesSaved = 0;
totalCollectablesSaved = 0;
scriptSaveState = LUA_REFNIL;
scriptExecutorSaved.savedDelayExecutionObjects = NULL;
saveStateNextTime=false;
loadStateNextTime=false;
recentSwap=recentSwapSaved=-10000;
recentLoad=recentSave=0;
action=imageManager.loadTexture(getDataPath()+"gfx/actions.png", renderer);
medals=imageManager.loadTexture(getDataPath()+"gfx/medals.png", renderer);
//Get the collectable image from the theme.
//NOTE: Isn't there a better way to retrieve the image?
objThemes.getBlock(TYPE_COLLECTABLE)->createInstance(&collectable);
//Hide the cursor if not in the leveleditor.
if(stateID!=STATE_LEVEL_EDITOR)
SDL_ShowCursor(SDL_DISABLE);
}
Game::~Game(){
//Simply call our destroy method.
destroy();
//Before we leave make sure the cursor is visible.
SDL_ShowCursor(SDL_ENABLE);
}
GameSaveState::GameSaveState() {
gameSaved.scriptSaveState = LUA_REFNIL;
gameSaved.scriptExecutorSaved.savedDelayExecutionObjects = NULL;
}
GameSaveState::~GameSaveState() {
//Simply call our destroy method.
destroy();
}
void GameSaveState::destroy() {
lua_State *state = NULL;
if (auto game = dynamic_cast<Game*>(currentState)) {
if (auto se = game->getScriptExecutor()) {
state = se->getLuaState();
}
}
//Delete script related stuff.
if (state) {
//Delete the compiledScripts.
for (auto it = gameSaved.savedCompiledScripts.begin(); it != gameSaved.savedCompiledScripts.end(); ++it) {
luaL_unref(state, LUA_REGISTRYINDEX, it->second);
}
//Delete the script save state.
luaL_unref(state, LUA_REGISTRYINDEX, gameSaved.scriptSaveState);
//Sanity check.
assert(gameSaved.scriptExecutorSaved.savedDelayExecutionObjects == NULL || gameSaved.scriptExecutorSaved.savedDelayExecutionObjects->state == state);
} else {
if (gameSaved.scriptExecutorSaved.savedDelayExecutionObjects) {
// Set the state to NULL since the state is already destroyed. This prevents the destructor to call luaL_unref.
gameSaved.scriptExecutorSaved.savedDelayExecutionObjects->state = NULL;
}
}
gameSaved.savedCompiledScripts.clear();
gameSaved.scriptSaveState = LUA_REFNIL;
delete gameSaved.scriptExecutorSaved.savedDelayExecutionObjects;
gameSaved.scriptExecutorSaved.savedDelayExecutionObjects = NULL;
for (auto o : gameSaved.levelObjectsSave) delete o;
gameSaved.levelObjectsSave.clear();
}
void Game::destroy(){
delete scriptExecutor;
scriptExecutor = NULL;
if (scriptExecutorSaved.savedDelayExecutionObjects) {
// Set the state to NULL since the state is already destroyed. This prevents the destructor to call luaL_unref.
scriptExecutorSaved.savedDelayExecutionObjects->state = NULL;
delete scriptExecutorSaved.savedDelayExecutionObjects;
scriptExecutorSaved.savedDelayExecutionObjects = NULL;
}
//Loop through the levelObjects, etc. and delete them.
for (auto o : levelObjects) delete o;
levelObjects.clear();
for (auto o : levelObjectsSave) delete o;
levelObjectsSave.clear();
for (auto o : levelObjectsInitial) delete o;
levelObjectsInitial.clear();
//Loop through the sceneryLayers and delete them.
for(auto it=sceneryLayers.begin();it!=sceneryLayers.end();++it){
delete it->second;
}
sceneryLayers.clear();
//Clear the name and the editor data.
levelName.clear();
levelFile.clear();
scriptFile.clear();
editorData.clear();
//Remove everything from the themeManager.
background=NULL;
if(customTheme)
objThemes.removeTheme();
customTheme=NULL;
//If there's a (partial) theme bundled with the levelpack remove that as well.
if(levels->customTheme)
objThemes.removeTheme();
//delete current level (if any)
if(currentLevelNode){
delete currentLevelNode;
currentLevelNode=NULL;
}
//Reset the time.
time=timeSaved=0;
recordings=recordingsSaved=0;
recentSwap=recentSwapSaved=-10000;
//Set the music list back to the configured list.
getMusicManager()->setMusicList(getSettings()->getValue("musiclist"));
}
void Game::reloadMusic() {
//NOTE: level music is always enabled.
//Check if the levelpack has a prefered music list.
if (levels && !levels->levelpackMusicList.empty())
getMusicManager()->setMusicList(levels->levelpackMusicList);
//Check for the music to use.
string &s = editorData["music"];
if (!s.empty()) {
getMusicManager()->playMusic(s);
} else {
getMusicManager()->pickMusic();
}
}
void Game::loadLevelFromNode(ImageManager& imageManager, SDL_Renderer& renderer, TreeStorageNode* obj, const string& fileName, const std::string& scriptFileName){
//Make sure there's nothing left from any previous levels.
assert(levelObjects.empty() && levelObjectsSave.empty() && levelObjectsInitial.empty());
//set current level to loaded one.
currentLevelNode=obj;
//Set the level dimensions to the default, it will probably be changed by the editorData,
//but 800x600 is a fallback.
levelRect = levelRectSaved = levelRectInitial = SDL_Rect{ 0, 0, 800, 600 };
currentCollectables = currentCollectablesSaved = currentCollectablesInitial = 0;
totalCollectables = totalCollectablesSaved = totalCollectablesInitial = 0;
//Load the additional data.
for(map<string,vector<string> >::iterator i=obj->attributes.begin();i!=obj->attributes.end();++i){
if(i->first=="size"){
//We found the size attribute.
if(i->second.size()>=2){
//Set the dimensions of the level.
int w = atoi(i->second[0].c_str()), h = atoi(i->second[1].c_str());
levelRect = levelRectSaved = levelRectInitial = SDL_Rect{ 0, 0, w, h };
}
}else if(i->second.size()>0){
//Any other data will be put into the editorData.
editorData[i->first]=i->second[0];
}
}
//Get the arcade property.
{
string &s = editorData["arcade"];
arcade = atoi(s.c_str()) != 0;
}
//Get the theme.
{
//NOTE: level themes are always enabled.
//Check for bundled (partial) themes for level pack.
if (levels->customTheme){
if (objThemes.appendThemeFromFile(levels->levelpackPath + "/theme/theme.mnmstheme", imageManager, renderer) == NULL){
//The theme failed to load so set the customTheme boolean to false.
levels->customTheme = false;
}
}
//Check for the theme to use for this level. This has higher priority.
//Examples: %DATA%/themes/classic or %USER%/themes/Orange
string &s = editorData["theme"];
if (!s.empty()){
customTheme = objThemes.appendThemeFromFile(processFileName(s) + "/theme.mnmstheme", imageManager, renderer);
}
//Set the Appearance of the player and the shadow.
objThemes.getCharacter(false)->createInstance(&player.appearance, "standright");
player.appearanceInitial = player.appearanceSave = player.appearance;
objThemes.getCharacter(true)->createInstance(&shadow.appearance, "standright");
shadow.appearanceInitial = shadow.appearanceSave = shadow.appearance;
}
//Get the music.
reloadMusic();
//Load the data from the level node.
for(unsigned int i=0;i<obj->subNodes.size();i++){
TreeStorageNode* obj1=obj->subNodes[i];
if(obj1==NULL) continue;
if(obj1->name=="tile"){
Block* block=new Block(this);
if(!block->loadFromNode(imageManager,renderer,obj1)){
delete block;
continue;
}
//If the type is collectable, increase the number of totalCollectables
if (block->type == TYPE_COLLECTABLE) {
totalCollectablesSaved = totalCollectablesInitial = ++totalCollectables;
}
//Add the block to the levelObjects vector.
levelObjects.push_back(block);
}else if(obj1->name=="scenerylayer" && obj1->value.size()==1){
std::string layerName = obj1->value[0];
//Upgrade the layer naming convention.
if (layerName >= "f") {
//Foreground layer.
if (layerName.size() < 3 || layerName[0] != 'f' || layerName[1] != 'g' || layerName[2] != '_') {
layerName = "fg_" + layerName;
}
} else {
//Background layer.
if (layerName.size() < 3 || layerName[0] != 'b' || layerName[1] != 'g' || layerName[2] != '_') {
layerName = "bg_" + layerName;
}
}
//Check if the layer exists.
if (sceneryLayers[layerName] == NULL) {
sceneryLayers[layerName] = new SceneryLayer();
}
//Load contents from node.
sceneryLayers[layerName]->loadFromNode(this, imageManager, renderer, obj1);
}else if(obj1->name=="script" && !obj1->value.empty()){
map<string,int>::iterator it=Game::levelEventNameMap.find(obj1->value[0]);
if(it!=Game::levelEventNameMap.end()){
int eventType=it->second;
const std::string& script=obj1->attributes["script"][0];
if(!script.empty()) scripts[eventType]=script;
}
}
}
//Set the levelName to the name of the current level.
levelName=editorData["name"];
levelFile=fileName;
scriptFile = scriptFileName;
//Some extra stuff only needed when not in the levelEditor.
if(stateID!=STATE_LEVEL_EDITOR){
//We create a text with the text "Level <levelno> <levelName>".
//It will be shown in the left bottom corner of the screen.
string s;
if(levels->getLevelCount()>1 && levels->type!=COLLECTION){
s=tfm::format(_("Level %d %s"),levels->getCurrentLevel()+1,_CC(levels->getDictionaryManager(),editorData["name"]));
} else {
s = _CC(levels->getDictionaryManager(), editorData["name"]);
}
bmTipsLevelName=textureFromText(renderer, *fontText,s.c_str(),objThemes.getTextColor(true));
}
//Get the background
background=objThemes.getBackground(false);
//Now the loading is finished, we reset all objects to their initial states.
//Before doing that we swap the levelObjects to levelObjectsInitial.
std::swap(levelObjects, levelObjectsInitial);
reset(true, stateID == STATE_LEVEL_EDITOR);
}
void Game::loadLevel(ImageManager& imageManager,SDL_Renderer& renderer,std::string fileName){
//Create a TreeStorageNode that will hold the loaded data.
TreeStorageNode *obj=new TreeStorageNode();
{
POASerializer objSerializer;
string s=fileName;
//Parse the file.
if(!objSerializer.loadNodeFromFile(s.c_str(),obj,true)){
cerr<<"ERROR: Can't load level file "<<s<<endl;
delete obj;
return;
}
}
//Now call another function.
loadLevelFromNode(imageManager, renderer, obj, fileName, std::string());
}
void Game::saveRecord(const char* fileName){
//check if current level is NULL (which should be impossible)
if(currentLevelNode==NULL) return;
TreeStorageNode obj;
POASerializer objSerializer;
//put current level to the node.
currentLevelNode->name="map";
obj.subNodes.push_back(currentLevelNode);
//put the random seed into the attributes.
obj.attributes["seed"].push_back(prngSeed);
//put the external script file into the attributes.
{
std::string s = scriptFile;
if (s.empty() && !levelFile.empty() && levelFile[0] != '?') {
s = levelFile;
size_t lp = s.find_last_of('.');
if (lp != std::string::npos) {
s = s.substr(0, lp);
}
s += ".lua";
}
//Now load the external script file.
if (!s.empty()) {
if (s[0] == '?') {
s = s.substr(1);
} else {
FILE *f = fopen(s.c_str(), "r");
if (f) {
fseek(f, 0, SEEK_END);
size_t m = ftell(f);
fseek(f, 0, SEEK_SET);
std::vector<char> tmp(m);
fread(tmp.data(), 1, m, f);
fclose(f);
tmp.push_back(0);
s = tmp.data();
} else {
s.clear();
}
}
}
if (!s.empty()) {
obj.attributes["script"].push_back(s);
}
}
//serialize the game record using RLE compression.
#define PUSH_BACK \
if(j>0){ \
if(j>1){ \
sprintf(c,"%d*%d",last,j); \
}else{ \
sprintf(c,"%d",last); \
} \
v.push_back(c); \
}
vector<string> &v=obj.attributes["record"];
vector<int> *record=player.getRecord();
char c[64];
int i,j=0,last;
for(i=0;i<(int)record->size();i++){
int currentKey=(*record)[i];
if(j==0 || currentKey!=last){
PUSH_BACK;
last=currentKey;
j=1;
}else{
j++;
}
}
PUSH_BACK;
#undef PUSH_BACK
#ifdef RECORD_FILE_DEBUG
//add record file debug data.
{
obj.attributes["recordKeyPressLog"].push_back(player.keyPressLog());
vector<SDL_Rect> &playerPosition=player.playerPosition();
string s;
char c[32];
sprintf(c,"%d\n",int(playerPosition.size()));
s=c;
for(unsigned int i=0;i<playerPosition.size();i++){
SDL_Rect& r=playerPosition[i];
sprintf(c,"%d %d\n",r.x,r.y);
s+=c;
}
obj.attributes["recordPlayerPosition"].push_back(s);
}
#endif
//save it
objSerializer.saveNodeToFile(fileName,&obj,true,true);
//remove current level from node to prevent delete it.
obj.subNodes.clear();
}
/////////////EVENT///////////////
void Game::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//First of all let the player handle input.
player.handleInput(&shadow);
//Check for an SDL_QUIT event.
if(event.type==SDL_QUIT){
//We need to quit so enter STATE_EXIT.
setNextState(STATE_EXIT);
}
//Check for the escape key.
if(stateID != STATE_LEVEL_EDITOR && inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
//Escape means we go one level up, to the level select state.
setNextState(STATE_LEVEL_SELECT);
//Save the progress if we are not watching a replay.
if (!player.isPlayFromRecord() || interlevel) {
levels->saveLevelProgress();
}
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}
//Check if 'R' is pressed.
if(inputMgr.isKeyDownEvent(INPUTMGR_RESTART)){
//Restart game only if we are not watching a replay.
if (!player.isPlayFromRecord() || interlevel) {
//Reset the game at next frame.
isReset = true;
//Also delete any gui (most likely the interlevel gui). Only in game mode.
if (GUIObjectRoot && stateID != STATE_LEVEL_EDITOR){
delete GUIObjectRoot;
GUIObjectRoot = NULL;
}
//And set interlevel to false.
interlevel = false;
}
}
//Check for the next level buttons when in the interlevel popup.
if (inputMgr.isKeyDownEvent(INPUTMGR_SPACE) || inputMgr.isKeyDownEvent(INPUTMGR_SELECT)){
if(interlevel){
//The interlevel popup is shown so we need to delete it.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//Now goto the next level.
gotoNextLevel(imageManager,renderer);
}
}
//Check if tab is pressed.
if(inputMgr.isKeyDownEvent(INPUTMGR_TAB)){
//Switch the camera mode.
switch(cameraMode){
case CAMERA_PLAYER:
cameraMode=CAMERA_SHADOW;
break;
case CAMERA_SHADOW:
case CAMERA_CUSTOM:
cameraMode=CAMERA_PLAYER;
break;
}
}
}
/////////////////LOGIC///////////////////
void Game::logic(ImageManager& imageManager, SDL_Renderer& renderer){
//Reset the gameTip.
gameTipText.clear();
//Add one tick to the time.
time++;
//NOTE: This code reverts some changes in commit 5f03ae5.
//This is part of old prepareFrame() code.
//This is needed since otherwise the script function block:setLocation() and block:moveTo() are completely broken.
//Later we should rewrite collision system completely which will remove this piece of code.
//NOTE: In new collision system the effect of dx/dy/xVel/yVel should only be restricted in one frame.
for (auto obj : levelObjects) {
switch (obj->type) {
default:
obj->dx = obj->dy = obj->xVel = obj->yVel = 0;
break;
case TYPE_PUSHABLE:
//NOTE: Currently the dx/dy/etc. of pushable blocks are still carry across frames, in order to make the collision system work correct.
break;
case TYPE_CONVEYOR_BELT: case TYPE_SHADOW_CONVEYOR_BELT:
//NOTE: We let the conveyor belt itself to reset its xVel/yVel.
obj->dx = obj->dy = 0;
break;
}
}
//NOTE2: The above code breaks pushable block with moving block in most cases,
//more precisely, if the pushable block is processed before the moving block then things may be broken.
//Therefore later we must process other blocks before moving pushable block.
//Process delay execution scripts.
getScriptExecutor()->processDelayExecution();
//Process any event in the queue.
for(unsigned int idx=0;idx<eventQueue.size();idx++){
//Get the event from the queue.
typeGameObjectEvent &e=eventQueue[idx];
//Check if the it has an id attached to it.
if(e.target){
//NOTE: Should we check if the target still exists???
e.target->onEvent(e.eventType);
}else if(e.flags&1){
//Loop through the levelObjects and give them the event if they have the right id.
for(unsigned int i=0;i<levelObjects.size();i++){
if(e.objectType<0 || levelObjects[i]->type==e.objectType){
if(levelObjects[i]->id==e.id){
levelObjects[i]->onEvent(e.eventType);
}
}
}
}else{
//Loop through the levelObjects and give them the event.
for(unsigned int i=0;i<levelObjects.size();i++){
if(e.objectType<0 || levelObjects[i]->type==e.objectType){
levelObjects[i]->onEvent(e.eventType);
}
}
}
}
//Done processing the events so clear the queue.
eventQueue.clear();
//Remove levelObjects whose isDelete is true.
{
int j = 0;
for (int i = 0; i < (int)levelObjects.size(); i++) {
if (levelObjects[i] == NULL) {
j++;
} else if (levelObjects[i]->isDelete) {
delete levelObjects[i];
levelObjects[i] = NULL;
j++;
} else if (j > 0) {
levelObjects[i - j] = levelObjects[i];
}
}
if (j > 0) levelObjects.resize(levelObjects.size() - j);
}
//Check if we should save/load state.
//NOTE: This happens after event handling so no eventQueue has to be saved/restored.
checkSaveLoadState();
//Loop through the gameobjects to update them.
for(unsigned int i=0;i<levelObjects.size();i++){
//Send GameObjectEvent_OnEnterFrame event to the script
levelObjects[i]->onEvent(GameObjectEvent_OnEnterFrame);
}
//Let the gameobject handle movement.
{
std::vector<Block*> pushableBlocks;
//First we process blocks which are not pushable blocks.
for (auto o : levelObjects) {
if (o->type == TYPE_PUSHABLE) {
pushableBlocks.push_back(o);
} else {
o->move();
}
}
//Sort pushable blocks by their position, which is an ad-hoc workaround for
//<https://forum.freegamedev.net/viewtopic.php?f=48&t=8047#p77692>.
std::stable_sort(pushableBlocks.begin(), pushableBlocks.end(),
[](const Block* obj1, const Block* obj2)->bool
{
SDL_Rect r1 = const_cast<Block*>(obj1)->getBox(), r2 = const_cast<Block*>(obj2)->getBox();
if (r1.y > r2.y) return true;
else if (r1.y < r2.y) return false;
else return r1.x < r2.x;
});
//Now we process pushable blocks.
for (auto o : pushableBlocks) {
o->move();
}
}
//Also update the scenery.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it){
it->second->updateAnimation();
}
//Also update the animation of the background.
{
//Get a pointer to the background.
ThemeBackground* bg = background;
//Check if the background is null, but there are themes.
if (bg == NULL && objThemes.themeCount() > 0){
//Get the background from the first theme in the stack.
bg = objThemes[0]->getBackground(false);
}
//Check if the background isn't null.
//And if it's the loaded background then also update the animation.
if (bg && bg == background) {
bg->updateAnimation();
}
}
//Let the player store his move, if recording.
player.shadowSetState();
//Let the player give his recording to the shadow, if configured.
player.shadowGiveState(&shadow);
//NOTE: to fix bugs regarding player/shadow swap, we should first process collision of player/shadow then move them
const SDL_Rect playerLastPosition = player.getBox();
const SDL_Rect shadowLastPosition = shadow.getBox();
//NOTE: The following is ad-hoc code to fix shadow on blocked player on conveyor belt bug
if (shadow.holdingOther) {
//We need to process shadow collision first if shadow is holding player.
//Let the shadow decide his move, if he's playing a recording.
shadow.moveLogic();
//Check collision for shadow.
shadow.collision(levelObjects, NULL);
//Get the new position of it.
const SDL_Rect r = shadow.getBox();
//Check collision for player. Transfer the velocity of shadow to it only if the shadow moves its position.
player.collision(levelObjects, (r.x != shadowLastPosition.x || r.y != shadowLastPosition.y) ? &shadow : NULL);
} else {
//Otherwise we process player first.
//Check collision for player.
player.collision(levelObjects, NULL);
//Get the new position of it.
const SDL_Rect r = player.getBox();
//Now let the shadow decide his move, if he's playing a recording.
shadow.moveLogic();
//Check collision for shadow. Transfer the velocity of player to it only if the player moves its position.
shadow.collision(levelObjects, (r.x != playerLastPosition.x || r.y != playerLastPosition.y) ? &player : NULL);
}
//Let the player move.
player.move(levelObjects, playerLastPosition.x, playerLastPosition.y);
//Let the shadow move.
shadow.move(levelObjects, shadowLastPosition.x, shadowLastPosition.y);
//Check collision and stuff for the shadow and player.
player.otherCheck(&shadow);
bool doNotResetWon = false;
//Check if we won.
if(won){
//Check if it's level editor test play
if (stateID == STATE_LEVEL_EDITOR) {
if (auto editor = dynamic_cast<LevelEditor*>(this)) {
editor->updateRecordInPlayMode(imageManager, renderer);
}
}
//Check if it's playing from record
else if (player.isPlayFromRecord() && !interlevel) {
//We don't reset the won to false, and let RecordPlayback class handle it.
doNotResetWon = true;
}else{
//Local copy of interlevel property since the replayPlay() will change it later.
const bool previousInterlevel = interlevel;
//We only update the level statistics when the previous state is not interlevel mode.
if (!previousInterlevel) {
//the string to store auto-save record path.
string bestTimeFilePath, bestRecordingFilePath;
//and if we can't get test path.
bool filePathError = false;
//Get current level
LevelPack::Level *level = levels->getLevel();
//Get previous medal
const int oldMedal = level->getMedal();
//Check if MD5 is changed
bool md5Changed = false;
for (int i = 0; i < 16; i++) {
if (level->md5Digest[i] != level->md5InLevelProgress[i]) {
md5Changed = true;
break;
}
}
//Erase existing record if MD5 is changed
if (md5Changed) {
//print some debug message
#if _DEBUG
cout << "MD5 is changed, old " << Md5::toString(level->md5InLevelProgress);
cout << ", new " << Md5::toString(level->md5Digest) << endl;
#endif
level->time = -1;
level->recordings = -1;
//Update the MD5 in record
memcpy(level->md5InLevelProgress, level->md5Digest, sizeof(level->md5Digest));
}
//Get better time and recordings
const int betterTime = level->getBetterTime(time);
const int betterRecordings = level->getBetterRecordings(level->arcade ? currentCollectables : recordings);
//Get new medal
const int newMedal = level->getMedal(betterTime, betterRecordings);
//Check if we need to update statistics
if (newMedal > oldMedal || md5Changed) {
+ //Get category.
+ int category = (int)levels->type;
+ if (category > CUSTOM) category = CUSTOM;
+
+ //Update pack medal.
+ if (levels->type != COLLECTION) {
+ int packMedal = 3;
+ for (int i = 0, j = levels->getCurrentLevel(), m = levels->getLevelCount(); i < m; i++) {
+ if (i != j) {
+ int medal = levels->getLevel(i)->getMedal();
+ if (packMedal > medal) packMedal = medal;
+ }
+ }
+ int oldPackMedal = std::min(packMedal, oldMedal), newPackMedal = std::min(packMedal, newMedal);
+
+ //Erase statictics for old medal
+ if (oldPackMedal > 0) {
+ statsMgr.completedLevelpacks--;
+ statsMgr.completedLevelpacksByCategory[category]--;
+ }
+ if (oldPackMedal == 2) {
+ statsMgr.silverLevelpacks--;
+ statsMgr.silverLevelpacksByCategory[category]--;
+ }
+ if (oldPackMedal == 3) {
+ statsMgr.goldLevelpacks--;
+ statsMgr.goldLevelpacksByCategory[category]--;
+ }
+
+ //Update statistics for new medal
+ if (newPackMedal > 0) {
+ statsMgr.completedLevelpacks++;
+ statsMgr.completedLevelpacksByCategory[category]++;
+ }
+ if (newPackMedal == 2) {
+ statsMgr.silverLevelpacks++;
+ statsMgr.silverLevelpacksByCategory[category]++;
+ }
+ if (newPackMedal == 3) {
+ statsMgr.goldLevelpacks++;
+ statsMgr.goldLevelpacksByCategory[category]++;
+ }
+ }
+
//Erase statictics for old medal
- if (oldMedal > 0) statsMgr.completedLevels--;
- if (oldMedal == 2) statsMgr.silverLevels--;
- if (oldMedal == 3) statsMgr.goldLevels--;
+ if (oldMedal > 0) {
+ statsMgr.completedLevels--;
+ statsMgr.completedLevelsByCategory[category]--;
+ }
+ if (oldMedal == 2) {
+ statsMgr.silverLevels--;
+ statsMgr.silverLevelsByCategory[category]--;
+ }
+ if (oldMedal == 3) {
+ statsMgr.goldLevels--;
+ statsMgr.goldLevelsByCategory[category]--;
+ }
//Update statistics for new medal
- if (newMedal > 0) statsMgr.completedLevels++;
- if (newMedal == 2) statsMgr.silverLevels++;
- if (newMedal == 3) statsMgr.goldLevels++;
+ if (newMedal > 0) {
+ statsMgr.completedLevels++;
+ statsMgr.completedLevelsByCategory[category]++;
+ }
+ if (newMedal == 2) {
+ statsMgr.silverLevels++;
+ statsMgr.silverLevelsByCategory[category]++;
+ }
+ if (newMedal == 3) {
+ statsMgr.goldLevels++;
+ statsMgr.goldLevelsByCategory[category]++;
+ }
}
//Check the achievement "Complete a level with checkpoint, but without saving"
if (objLastCheckPoint.get() == NULL) {
for (auto obj : levelObjects) {
if (obj->type == TYPE_CHECKPOINT) {
statsMgr.newAchievement("withoutsave");
break;
}
}
}
//Set the current level won.
level->won = true;
if (level->time != betterTime) {
level->time = betterTime;
//save the best-time game record.
if (bestTimeFilePath.empty()){
getCurrentLevelAutoSaveRecordPath(bestTimeFilePath, bestRecordingFilePath, true);
}
if (bestTimeFilePath.empty()){
cerr << "ERROR: Couldn't get auto-save record file path" << endl;
filePathError = true;
} else{
saveRecord(bestTimeFilePath.c_str());
}
}
if (level->recordings != betterRecordings) {
level->recordings = betterRecordings;
//save the best-recordings game record.
if (bestRecordingFilePath.empty() && !filePathError){
getCurrentLevelAutoSaveRecordPath(bestTimeFilePath, bestRecordingFilePath, true);
}
if (bestRecordingFilePath.empty()){
cerr << "ERROR: Couldn't get auto-save record file path" << endl;
filePathError = true;
} else{
saveRecord(bestRecordingFilePath.c_str());
}
}
//Set the next level unlocked if it exists.
if (levels->getCurrentLevel() + 1 < levels->getLevelCount()){
levels->setLocked(levels->getCurrentLevel() + 1);
}
//And save the progress.
levels->saveLevelProgress();
}
//Now go to the interlevel screen.
replayPlay(imageManager,renderer);
//Update achievements (only when the previous state is not interlevel mode)
if (!previousInterlevel) {
if (levels->levelpackName == "tutorial") statsMgr.updateTutorialAchievements();
statsMgr.updateLevelAchievements();
}
//NOTE: We set isReset false to prevent the user from getting a best time of 0.00s and 0 recordings.
isReset = false;
}
}
if (!doNotResetWon) won = false;
//Check if we should reset.
if (isReset) {
//NOTE: we don't need to reset save ??? it looks like that there are no bugs
reset(false, false);
}
isReset=false;
//Finally we update the animation of player and shadow (was previously in render() function).
shadow.updateAnimation();
player.updateAnimation();
}
void Game::checkSaveLoadState() {
//Check if we should save/load state.
//NOTE: This happens after event handling so no eventQueue has to be saved/restored.
if (saveStateNextTime){
saveState();
} else if (loadStateNextTime){
loadState();
}
saveStateNextTime = false;
loadStateNextTime = false;
}
/////////////////RENDER//////////////////
void Game::render(ImageManager&,SDL_Renderer &renderer){
//Update the camera (was previously in logic() function).
switch (cameraMode) {
case CAMERA_PLAYER:
player.setMyCamera();
break;
case CAMERA_SHADOW:
shadow.setMyCamera();
break;
case CAMERA_CUSTOM:
//NOTE: The target is (should be) screen size independent so calculate the real target x and y here.
int targetX = cameraTarget.x - (SCREEN_WIDTH / 2);
int targetY = cameraTarget.y - (SCREEN_HEIGHT / 2);
//Move the camera to the cameraTarget.
if (camera.x > targetX) {
camera.x -= (camera.x - targetX) >> 4;
//Make sure we don't go too far.
if (camera.x < targetX)
camera.x = targetX;
} else if (camera.x < targetX) {
camera.x += (targetX - camera.x) >> 4;
//Make sure we don't go too far.
if (camera.x > targetX)
camera.x = targetX;
}
if (camera.y > targetY) {
camera.y -= (camera.y - targetY) >> 4;
//Make sure we don't go too far.
if (camera.y < targetY)
camera.y = targetY;
} else if (camera.y < targetY) {
camera.y += (targetY - camera.y) >> 4;
//Make sure we don't go too far.
if (camera.y > targetY)
camera.y = targetY;
}
break;
}
//First of all render the background.
{
//Get a pointer to the background.
ThemeBackground* bg=background;
//Check if the background is null, but there are themes.
if(bg==NULL && objThemes.themeCount()>0){
//Get the background from the first theme in the stack.
bg=objThemes[0]->getBackground(false);
}
//Check if the background isn't null.
if(bg){
//It isn't so draw it.
bg->draw(renderer);
}else{
//There's no background so fill the screen with white.
SDL_SetRenderDrawColor(&renderer, 255,255,255,255);
SDL_RenderClear(&renderer);
}
}
//Now draw the blackground layers.
auto it = sceneryLayers.begin();
for (; it != sceneryLayers.end(); ++it){
if (it->first >= "f") break; // now we meet a foreground layer
it->second->show(renderer);
}
//Now we draw the levelObjects.
{
//NEW: always render the pushable blocks in front of other blocks
std::vector<Block*> pushableBlocks;
for (auto o : levelObjects) {
if (o->type == TYPE_PUSHABLE) {
pushableBlocks.push_back(o);
} else {
o->show(renderer);
}
}
for (auto o : pushableBlocks) {
o->show(renderer);
}
}
//Followed by the player and the shadow.
//NOTE: We draw the shadow first, because he needs to be behind the player.
shadow.show(renderer);
player.show(renderer);
//Now draw the foreground layers.
for (; it != sceneryLayers.end(); ++it){
it->second->show(renderer);
}
//Show the levelName if it isn't the level editor.
if(stateID!=STATE_LEVEL_EDITOR && bmTipsLevelName!=NULL && !interlevel){
SDL_Rect r(rectFromTexture(bmTipsLevelName));
drawGUIBox(-2, SCREEN_HEIGHT - r.h - 4, r.w + 8, r.h + 6, renderer, 0xFFFFFFFF);
applyTexture(2, SCREEN_HEIGHT - r.h, *bmTipsLevelName, renderer, NULL);
}
//Check if there's a tooltip.
if(!gameTipText.empty()){
std::string message = gameTipText;
if (gameTipTexture.needsUpdate(gameTipText)) {
int maxWidth = 0, y = 0;
std::vector<std::string> string_data;
//Trim the message.
{
size_t lps = message.find_first_not_of("\n\r \t");
if (lps == string::npos) {
message.clear(); // it's completely empty
} else {
message = message.substr(lps, message.find_last_not_of("\n\r \t") - lps + 1);
}
}
if (!message.empty()) {
message.push_back('\0');
//Split the message into lines.
for (int lps = 0;;) {
// determine the end of line
int lpe = lps;
for (; message[lpe] != '\n' && message[lpe] != '\r' && message[lpe] != '\0'; lpe++);
string_data.push_back(message.substr(lps, lpe - lps));
// break if the string ends
if (message[lpe] == '\0') break;
// skip "\r\n" for Windows line ending
if (message[lpe] == '\r' && message[lpe + 1] == '\n') lpe++;
// point to the start of next line
lps = lpe + 1;
}
vector<SurfacePtr> lines;
//Create the image for each lines
for (int i = 0; i < (int)string_data.size(); i++) {
//Integer used to center the sentence horizontally.
int w = 0, h = 0;
TTF_SizeUTF8(fontText, string_data[i].c_str(), &w, &h);
//Find out largest width
if (w > maxWidth)
maxWidth = w;
lines.emplace_back(TTF_RenderUTF8_Blended(fontText, string_data[i].c_str(), objThemes.getTextColor(true)));
//Increase y with the height of the text.
y += h;
}
SurfacePtr surf = createSurface(maxWidth, y);
y = 0;
for (auto &s : lines) {
if (s) {
applySurface(0, y, s.get(), surf.get(), NULL);
y += s->h;
}
}
gameTipTexture.update(gameTipText, textureUniqueFromSurface(renderer, std::move(surf)));
}
}
//We already have a gameTip for this type so draw it.
if (!message.empty() && gameTipTexture.get()){
SDL_Rect r(rectFromTexture(*gameTipTexture.get()));
drawGUIBox(-2, -2, r.w + 8, r.h + 6, renderer, 0xFFFFFFFF);
applyTexture(2, 2, *gameTipTexture.get(), renderer);
}
}
// Limit the scope of bm, as it's a borrowed pointer.
{
//Pointer to the sdl texture that will contain a message, if any.
SDL_Texture* bm=NULL;
//Check if the player is dead and the game is normal mode, meaning we draw a message.
if(player.dead && !arcade){
//Get user configured restart key
string keyCodeRestart = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_RESTART, false), inputMgr.getKeyCode(INPUTMGR_RESTART, true));
//The player is dead, check if there's a state that can be loaded.
if(player.canLoadState()){
//Now check if the tip is already made, if not make it.
if(bmTipsRestartCheckpoint==NULL){
//Get user defined key for loading checkpoint
string keyCodeLoad = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_LOAD, false), inputMgr.getKeyCode(INPUTMGR_LOAD, true));
//Draw string
bmTipsRestartCheckpoint = textureFromText(renderer, *fontText,//TTF_RenderUTF8_Blended(fontText,
/// TRANSLATORS: Please do not remove %s from your translation:
/// - first %s means currently configured key to restart game
/// - Second %s means configured key to load from last save
tfm::format(_("Press %s to restart current level or press %s to load the game."),
keyCodeRestart,keyCodeLoad).c_str(),
objThemes.getTextColor(true));
}
bm = bmTipsRestartCheckpoint.get();
}else{
//Now check if the tip is already made, if not make it.
if (bmTipsRestart == NULL){
bmTipsRestart = textureFromText(renderer, *fontText,
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with currently configured key to restart game
tfm::format(_("Press %s to restart current level."),keyCodeRestart).c_str(),
objThemes.getTextColor(true));
}
bm = bmTipsRestart.get();
}
}
//Check if the shadow has died (and there's no other notification).
//NOTE: We use the shadow's jumptime as countdown, this variable isn't used when the shadow is dead.
if(shadow.dead && bm==NULL && shadow.jumpTime>0){
//Now check if the tip is already made, if not make it.
if(bmTipsShadowDeath==NULL){
bmTipsShadowDeath = textureFromText(renderer, *fontText,
_("Your shadow has died."),
objThemes.getTextColor(true));
}
bm = bmTipsShadowDeath.get();
//NOTE: Logic in the render loop, we substract the shadow's jumptime by one.
shadow.jumpTime--;
//return view to player and keep it there
cameraMode=CAMERA_PLAYER;
}
//Draw the tip.
if(bm!=NULL){
const SDL_Rect textureSize = rectFromTexture(*bm);
int x=(SCREEN_WIDTH-textureSize.w)/2;
int y=32;
drawGUIBox(x-8,y-8,textureSize.w+16,textureSize.h+14,renderer,0xFFFFFFFF);
applyTexture(x,y,*bm,renderer);
}
}
{
int y = SCREEN_HEIGHT;
//Show the number of collectables the user has collected if there are collectables in the level.
//We hide this when interlevel.
if ((currentCollectables || totalCollectables) && !interlevel){
if (arcade) {
//Only show the current collectibles in arcade mode.
if (collectablesTexture.needsUpdate(currentCollectables)) {
collectablesTexture.update(currentCollectables,
textureFromText(renderer,
*fontText,
tfm::format("%d", currentCollectables).c_str(),
objThemes.getTextColor(true)));
}
} else {
if (collectablesTexture.needsUpdate(currentCollectables ^ (totalCollectables << 16))) {
collectablesTexture.update(currentCollectables ^ (totalCollectables << 16),
textureFromText(renderer,
*fontText,
tfm::format("%d/%d", currentCollectables, totalCollectables).c_str(),
objThemes.getTextColor(true)));
}
}
SDL_Rect bmSize = rectFromTexture(*collectablesTexture.get());
//Draw background
drawGUIBox(SCREEN_WIDTH - bmSize.w - 34, SCREEN_HEIGHT - bmSize.h - 4, bmSize.w + 34 + 2, bmSize.h + 4 + 2, renderer, 0xFFFFFFFF);
//Draw the collectable icon
collectable.draw(renderer, SCREEN_WIDTH - 50 + 12, SCREEN_HEIGHT - 50 + 10);
//Draw text
applyTexture(SCREEN_WIDTH - 50 - bmSize.w + 22, SCREEN_HEIGHT - bmSize.h, collectablesTexture.getTexture(), renderer);
y -= bmSize.h + 4;
}
//Show additional message in level editor.
if (stateID == STATE_LEVEL_EDITOR && additionalTexture) {
SDL_Rect r = rectFromTexture(*additionalTexture.get());
r.x = SCREEN_WIDTH - r.w;
r.y = y - r.h;
SDL_SetRenderDrawColor(&renderer, 255, 255, 255, 255);
SDL_RenderFillRect(&renderer, &r);
applyTexture(r.x, r.y, additionalTexture, renderer);
}
}
//show time and records used in level editor or during replay or in arcade mode.
if ((stateID == STATE_LEVEL_EDITOR || (!interlevel && (player.isPlayFromRecord() || arcade)))){
const SDL_Color fg=objThemes.getTextColor(true),bg={255,255,255,255};
const int alpha = 160;
//don't show number of records in arcade mode
if (!arcade && recordingsTexture.needsUpdate(recordings)) {
recordingsTexture.update(recordings,
textureFromTextShaded(
renderer,
*fontMono,
tfm::format(ngettext("%d recording","%d recordings",recordings).c_str(),recordings).c_str(),
fg,
bg
));
SDL_SetTextureAlphaMod(recordingsTexture.get(),alpha);
}
int y = SCREEN_HEIGHT - (arcade ? 0 : textureHeight(*recordingsTexture.get()));
if (stateID != STATE_LEVEL_EDITOR && bmTipsLevelName != NULL && !interlevel) {
y -= textureHeight(bmTipsLevelName) + 4;
}
if (!arcade) applyTexture(0,y,*recordingsTexture.get(), renderer);
if(timeTexture.needsUpdate(time)) {
timeTexture.update(time,
textureFromTextShaded(
renderer,
*fontMono,
tfm::format("%-.2fs", time / 40.0).c_str(),
fg,
bg
));
SDL_SetTextureAlphaMod(timeTexture.get(), alpha);
}
y -= textureHeight(*timeTexture.get());
applyTexture(0,y,*timeTexture.get(), renderer);
}
//Draw the current action in the upper right corner.
if(player.record){
const SDL_Rect r = { 0, 0, 50, 50 };
applyTexture(SCREEN_WIDTH - 50, 0, *action, renderer, &r);
} else if (shadow.state != 0){
const SDL_Rect r={50,0,50,50};
applyTexture(SCREEN_WIDTH-50,0,*action,renderer,&r);
}
//if the game is play from record then draw something indicates it
if(player.isPlayFromRecord()){
//Dim the screen if interlevel is true.
if(interlevel){
dimScreen(renderer,191);
}
}else if(auto blockId = player.objNotificationBlock.get()){
//If the player is in front of a notification block and it isn't a replay, show the message.
//Check if we need to update the notification message texture.
//We check against blockId rather than the full message, as blockId is most likely shorter.
if(notificationTexture.needsUpdate(blockId)) {
std::string message = translateAndExpandMessage(blockId->message);
std::vector<std::string> string_data;
//Trim the message.
{
size_t lps = message.find_first_not_of("\n\r \t");
if (lps == string::npos) {
message.clear(); // it's completely empty
} else {
message = message.substr(lps, message.find_last_not_of("\n\r \t") - lps + 1);
}
}
WordWrapper wrapper;
wrapper.font = fontText;
wrapper.maxWidth = SCREEN_WIDTH - 40;
wrapper.wordWrap = true;
wrapper.hyphen = "-";
//Split the message into lines.
wrapper.addString(string_data, message);
//Create the image.
notificationTexture.update(blockId,
textureFromMultilineText(renderer, *fontText, string_data, objThemes.getTextColor(true), GUIGravityCenter));
}
SDL_Rect texSize = rectFromTexture(*notificationTexture.get());
SDL_Rect boxSize = texSize;
const int verticalPadding = 15;
const int verticalPadding2 = 5; // additional padding from the bottom of GUI box to the bottom of screen
boxSize.w += int(SCREEN_WIDTH*0.15);
boxSize.h += verticalPadding * 2;
if (boxSize.w > SCREEN_WIDTH - 20) boxSize.w = SCREEN_WIDTH - 20;
drawGUIBox((SCREEN_WIDTH - boxSize.w) / 2,
SCREEN_HEIGHT - boxSize.h - verticalPadding2 - 3 /* ?? */,
boxSize.w,
boxSize.h + 3 /* ?? */,
renderer, 0xFFFFFFBF);
applyTexture((SCREEN_WIDTH - texSize.w) / 2,
SCREEN_HEIGHT - texSize.h - verticalPadding - verticalPadding2,
notificationTexture.getTexture(), renderer);
}
}
std::string Game::translateAndExpandMessage(const std::string &untranslated_message) {
std::string message = _CC(levels->getDictionaryManager(), untranslated_message);
//Expand the variables.
std::map<std::string, std::string> cachedVariables;
for (;;) {
size_t lps = message.find("{{{");
if (lps == std::string::npos) break;
size_t lpe = message.find("}}}", lps);
if (lpe == std::string::npos) break;
std::string varName = message.substr(lps + 3, lpe - lps - 3), varValue;
auto it = cachedVariables.find(varName);
if (it != cachedVariables.end()) {
varValue = it->second;
} else {
bool isUnknown = true;
if (varName.size() >= 4 && varName.substr(0, 4) == "key_") {
//Probably a key name.
InputManagerKeys key = InputManager::getKeyFromName(varName);
if (key != INPUTMGR_MAX) {
varValue = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(key, false), inputMgr.getKeyCode(key, true));
isUnknown = false;
}
}
if (isUnknown) {
//Unknown variable
cerr << "Warning: Unknown variable '{{{" << varName << "}}}' in notification block message!" << endl;
}
cachedVariables[varName] = varValue;
}
//Substitute.
message.replace(message.begin() + lps, message.begin() + (lpe + 3), varValue);
}
//Over.
return message;
}
void Game::resize(ImageManager&, SDL_Renderer& /*renderer*/){
//Check if the interlevel popup is shown.
if(interlevel && GUIObjectRoot){
GUIObjectRoot->left=(SCREEN_WIDTH-GUIObjectRoot->width)/2;
}
}
void Game::replayPlay(ImageManager& imageManager,SDL_Renderer& renderer){
//Set interlevel true.
interlevel=true;
//Make a copy of the playerButtons.
vector<int> recordCopy=player.recordButton;
//Backup of time and recordings, etc. before we reset the level.
//We choose the same variable names so that we don't need to modify the existing code.
const int time = this->time, recordings = this->recordings, currentCollectables = this->currentCollectables;
//Reset the game.
//NOTE: We don't reset the saves. I'll see that if it will introduce bugs.
reset(false, false);
//Make the cursor visible when the interlevel popup is up.
SDL_ShowCursor(SDL_ENABLE);
//Set the copy of playerButtons back.
player.recordButton=recordCopy;
//Now play the recording.
player.playRecord();
//Create the gui if it isn't already done.
if(!GUIObjectRoot){
//Create a new GUIObjectRoot the size of the screen.
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Make child widgets change color properly according to theme.
GUIObjectRoot->inDialog=true;
//Create a GUIFrame for the upper frame.
GUIFrame* upperFrame=new GUIFrame(imageManager,renderer,0,4,0,74);
GUIObjectRoot->addChild(upperFrame);
//Render the You've finished: text and add it to a GUIImage.
//NOTE: The texture is managed by the GUIImage so no need to free it ourselfs.
auto bm = SharedTexture(textureFromText(renderer, *fontGUI,_("You've finished:"),objThemes.getTextColor(true)));
const SDL_Rect textureSize = rectFromTexture(*bm);
GUIImage* title=new GUIImage(imageManager,renderer,0,4-GUI_FONT_RAISE,textureSize.w,textureSize.h,bm);
upperFrame->addChild(title);
//Create the sub title.
string s;
if (levels->getLevelCount()>0){
/// TRANSLATORS: Please do not remove %s or %d from your translation:
/// - %d means the level number in a levelpack
/// - %s means the name of current level
s=tfm::format(_("Level %d %s"),levels->getCurrentLevel()+1,_CC(levels->getDictionaryManager(),levelName));
}
GUIObject* obj=new GUILabel(imageManager,renderer,0,44,0,28,s.c_str(),0,true,true,GUIGravityCenter);
upperFrame->addChild(obj);
obj->render(renderer,0,0,false);
//Determine the width the upper frame should have.
int width;
if(textureSize.w>obj->width)
width=textureSize.w+32;
else
width=obj->width+32;
//Set the left of the title.
title->left=(width-title->width)/2;
//Set the width of the level label to the width of the frame for centering.
obj->width=width;
//Now set the position and width of the frame.
upperFrame->width=width;
upperFrame->left=(SCREEN_WIDTH-width)/2;
//Now create a GUIFrame for the lower frame.
GUIFrame* lowerFrame=new GUIFrame(imageManager,renderer,0,SCREEN_HEIGHT-140,570,135);
GUIObjectRoot->addChild(lowerFrame);
//The different values.
int bestTime=levels->getLevel()->time;
int targetTime=levels->getLevel()->targetTime;
int bestRecordings=levels->getLevel()->recordings;
int targetRecordings=levels->getLevel()->targetRecordings;
int medal = levels->getLevel()->getMedal();
assert(medal > 0);
int maxWidth=0;
int x=20;
//Is there a target time for this level?
int timeY=0;
bool isTargetTime=true;
if(targetTime<0){
isTargetTime=false;
timeY=12;
}
//Create the labels with the time and best time.
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
obj=new GUILabel(imageManager,renderer,x,10+timeY,-1,36,tfm::format(_("Time: %-.2fs"),time/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
obj=new GUILabel(imageManager,renderer,x,34+timeY,-1,36,tfm::format(_("Best time: %-.2fs"),bestTime/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %-.2f from your translation:
/// - %-.2f means time in seconds
/// - s is shortened form of a second. Try to keep it so.
if(isTargetTime){
obj=new GUILabel(imageManager,renderer,x,58,-1,36,tfm::format(_("Target time: %-.2fs"),targetTime/40.0).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
}
x+=maxWidth+20;
//Is there target recordings for this level?
int recsY=0;
bool isTargetRecs=true;
if(targetRecordings<0){
isTargetRecs=false;
recsY=12;
}
//Now the ones for the recordings.
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
obj = new GUILabel(imageManager, renderer, x, 10 + recsY, -1, 36,
arcade ? tfm::format(_("Collectibles: %d"), currentCollectables).c_str() : tfm::format(_("Recordings: %d"), recordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
obj = new GUILabel(imageManager, renderer, x, 34 + recsY, -1, 36,
tfm::format(_(arcade ? "Best collectibles: %d" : "Best recordings: %d"), bestRecordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
/// TRANSLATORS: Please do not remove %d from your translation:
/// - %d means the number of recordings user has made
if(isTargetRecs){
obj = new GUILabel(imageManager, renderer, x, 58, -1, 36,
tfm::format(_(arcade ? "Target collectibles: %d" : "Target recordings: %d"), targetRecordings).c_str());
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->width>maxWidth)
maxWidth=obj->width;
}
x+=maxWidth;
//The medal that is earned.
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with name of a prize medal (gold, silver or bronze)
string s1=tfm::format(_("You earned the %s medal"),(medal>1)?(medal==3)?_("GOLD"):_("SILVER"):_("BRONZE"));
obj=new GUILabel(imageManager,renderer,50,92,-1,36,s1.c_str(),0,true,true,GUIGravityCenter);
lowerFrame->addChild(obj);
obj->render(renderer,0,0,false);
if(obj->left+obj->width>x){
x=obj->left+obj->width+30;
}else{
obj->left=20+(x-20-obj->width)/2;
}
//Create the rectangle for the earned medal.
SDL_Rect r;
r.x=(medal-1)*30;
r.y=0;
r.w=30;
r.h=30;
//Create the medal on the left side.
obj=new GUIImage(imageManager,renderer,16,92,30,30,medals,r);
lowerFrame->addChild(obj);
//And the medal on the right side.
obj=new GUIImage(imageManager,renderer,x-24,92,30,30,medals,r);
lowerFrame->addChild(obj);
//Create the three buttons, Menu, Restart, Next.
/// TRANSLATORS: used as return to the level selector menu
GUIObject* b1=new GUIButton(imageManager,renderer,x,10,-1,36,_("Menu"),0,true,true,GUIGravityCenter);
b1->name="cmdMenu";
b1->eventCallback=this;
lowerFrame->addChild(b1);
b1->render(renderer,0,0,true);
/// TRANSLATORS: used as restart level
GUIObject* b2=new GUIButton(imageManager,renderer,x,50,-1,36,_("Restart"),0,true,true,GUIGravityCenter);
b2->name="cmdRestart";
b2->eventCallback=this;
lowerFrame->addChild(b2);
b2->render(renderer,0,0,true);
/// TRANSLATORS: used as next level
GUIObject* b3=new GUIButton(imageManager,renderer,x,90,-1,36,_("Next"),0,true,true,GUIGravityCenter);
b3->name="cmdNext";
b3->eventCallback=this;
lowerFrame->addChild(b3);
b3->render(renderer,0,0,true);
maxWidth=b1->width;
if(b2->width>maxWidth)
maxWidth=b2->width;
if(b3->width>maxWidth)
maxWidth=b3->width;
b1->left=b2->left=b3->left=x+maxWidth/2;
x+=maxWidth;
lowerFrame->width=x;
lowerFrame->left=(SCREEN_WIDTH-lowerFrame->width)/2;
}
}
bool Game::canSaveState(){
return (player.canSaveState() && shadow.canSaveState());
}
void Game::saveStateInternal(GameSaveState* o) {
//Let the player and the shadow save their state.
player.saveStateInternal(&o->playerSaved);
shadow.saveStateInternal(&o->shadowSaved);
//Save the game state.
saveGameOnlyStateInternal(&o->gameSaved);
//TODO: Save scenery layers, background animation,
}
void Game::loadStateInternal(GameSaveState* o) {
//Let the player and the shadow load their state.
player.loadStateInternal(&o->playerSaved);
shadow.loadStateInternal(&o->shadowSaved);
//Load the game state.
loadGameOnlyStateInternal(&o->gameSaved);
//TODO: load scenery layers, background animation,
}
void Game::saveGameOnlyStateInternal(GameOnlySaveState* o) {
if (o == NULL) o = static_cast<GameOnlySaveState*>(this);
auto state = getScriptExecutor()->getLuaState();
//Save the stats.
o->timeSaved = time;
o->recordingsSaved = recordings;
o->recentSwapSaved = recentSwap;
//Save the PRNG and seed.
o->prngSaved = prng;
o->prngSeedSaved = prngSeed;
//Save the level size.
o->levelRectSaved = levelRect;
//Save the camera mode and target.
o->cameraModeSaved = (int)cameraMode;
o->cameraTargetSaved = cameraTarget;
//Save the current collectables
o->currentCollectablesSaved = currentCollectables;
o->totalCollectablesSaved = totalCollectables;
//Save scripts.
copyCompiledScripts(state, compiledScripts, o->savedCompiledScripts);
//Save other state, for example moving blocks.
copyLevelObjects(levelObjects, o->levelObjectsSave, false);
//Save the state for script executor.
getScriptExecutor()->saveState(&o->scriptExecutorSaved);
//Check if we have onSave event.
if (compiledScripts.find(LevelEvent_OnSave) != compiledScripts.end()) {
//Backup old SAVESTATE.
lua_getglobal(state, "SAVESTATE");
int oldIndex = luaL_ref(state, LUA_REGISTRYINDEX);
//Prepare the SAVESTATE variable for onSave event.
lua_pushnil(state);
lua_setglobal(state, "SAVESTATE");
//Execute the onSave event.
executeScript(LevelEvent_OnSave);
//Retrieve the SAVESTATE and save it.
luaL_unref(state, LUA_REGISTRYINDEX, o->scriptSaveState); // remove old save state
lua_getglobal(state, "SAVESTATE");
o->scriptSaveState = luaL_ref(state, LUA_REGISTRYINDEX);
//Restore old SAVESTATE.
lua_rawgeti(state, LUA_REGISTRYINDEX, oldIndex);
luaL_unref(state, LUA_REGISTRYINDEX, oldIndex);
lua_setglobal(state, "SAVESTATE");
}
//TODO: Save scenery layers, background animation,
}
void Game::loadGameOnlyStateInternal(GameOnlySaveState* o) {
if (o == NULL) o = static_cast<GameOnlySaveState*>(this);
auto state = getScriptExecutor()->getLuaState();
//Load the stats.
time = o->timeSaved;
recordings = o->recordingsSaved;
recentSwap = o->recentSwapSaved;
//Load the PRNG and seed.
prng = o->prngSaved;
prngSeed = o->prngSeedSaved;
//Load the level size.
levelRect = o->levelRectSaved;
//Load the camera mode and target.
cameraMode = (CameraMode)o->cameraModeSaved;
cameraTarget = o->cameraTargetSaved;
//Load the current collactbles
currentCollectables = o->currentCollectablesSaved;
totalCollectables = o->totalCollectablesSaved;
//Load scripts.
copyCompiledScripts(state, o->savedCompiledScripts, compiledScripts);
//Load other state, for example moving blocks.
copyLevelObjects(o->levelObjectsSave, levelObjects, true);
//Load the state for script executor.
getScriptExecutor()->loadState(&o->scriptExecutorSaved);
//Check if we have onLoad event.
if (compiledScripts.find(LevelEvent_OnLoad) != compiledScripts.end()) {
//Backup old SAVESTATE.
lua_getglobal(state, "SAVESTATE");
int oldIndex = luaL_ref(state, LUA_REGISTRYINDEX);
//Prepare the SAVESTATE variable for onLoad event.
lua_rawgeti(state, LUA_REGISTRYINDEX, o->scriptSaveState);
lua_setglobal(state, "SAVESTATE");
//Execute the onLoad event.
executeScript(LevelEvent_OnLoad);
//Restore old SAVESTATE.
lua_rawgeti(state, LUA_REGISTRYINDEX, oldIndex);
luaL_unref(state, LUA_REGISTRYINDEX, oldIndex);
lua_setglobal(state, "SAVESTATE");
}
//TODO: load scenery layers, background animation,
}
bool Game::saveState(){
//Check if the player and shadow can save the current state.
if(canSaveState()){
//Let the player and the shadow save their state.
player.saveState();
shadow.saveState();
//Save the game state.
saveGameOnlyStateInternal();
//Also save states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->saveAnimation();
}
//Also save the background animation, if any.
if(background)
background->saveAnimation();
if(!player.isPlayFromRecord() && !interlevel){
//Update achievements
Uint32 t=SDL_GetTicks()+5000; //Add a bias to prevent bugs
if(recentSave+1000>t){
statsMgr.newAchievement("panicSave");
}
recentSave=t;
//Update statistics.
statsMgr.saveTimes++;
//Update achievements
switch(statsMgr.saveTimes){
case 100:
statsMgr.newAchievement("save100");
break;
}
}
//Return true.
return true;
}
//We can't save the state so return false.
return false;
}
bool Game::loadState(){
//Check if there's a state that can be loaded.
if(player.canLoadState() && shadow.canLoadState()){
//Reset the stats for level editor.
if (stateID == STATE_LEVEL_EDITOR) {
if (auto editor = dynamic_cast<LevelEditor*>(this)) {
editor->currentTime = editor->currentRecordings = -1;
editor->updateAdditionalTexture(getImageManager(), getRenderer());
}
}
//Let the player and the shadow load their state.
player.loadState();
shadow.loadState();
//Load the game state.
loadGameOnlyStateInternal();
//Also load states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->loadAnimation();
}
//Also load the background animation, if any.
if(background)
background->loadAnimation();
if(!player.isPlayFromRecord() && !interlevel){
//Update achievements.
Uint32 t=SDL_GetTicks()+5000; //Add a bias to prevent bugs
if(recentLoad+1000>t){
statsMgr.newAchievement("panicLoad");
}
recentLoad=t;
//Update statistics.
statsMgr.loadTimes++;
//Update achievements
switch(statsMgr.loadTimes){
case 100:
statsMgr.newAchievement("load100");
break;
}
}
//Return true.
return true;
}
//We can't load the state so return false.
return false;
}
static std::string createNewSeed() {
static int createSeedTime = 0;
struct Buffer {
time_t systemTime;
Uint32 sdlTicks;
int x;
int y;
int createSeedTime;
} buffer;
buffer.systemTime = time(NULL);
buffer.sdlTicks = SDL_GetTicks();
SDL_GetMouseState(&buffer.x, &buffer.y);
buffer.createSeedTime = ++createSeedTime;
return Md5::toString(Md5::calc(&buffer, sizeof(buffer), NULL));
}
void Game::reset(bool save,bool noScript){
//Some sanity check, i.e. if we switch from no-script mode to script mode, we should always reset the save
assert(noScript || getScriptExecutor() || save);
//We need to reset the game so we also reset the player and the shadow.
player.reset(save);
shadow.reset(save);
saveStateNextTime=false;
loadStateNextTime=false;
//Reset the stats.
time=0;
recordings=0;
recentSwap=-10000;
if(save) recentSwapSaved=-10000;
//Reset the stats for level editor.
if (stateID == STATE_LEVEL_EDITOR) {
if (auto editor = dynamic_cast<LevelEditor*>(this)) {
editor->currentTime = editor->currentRecordings = -1;
editor->updateAdditionalTexture(getImageManager(), getRenderer());
}
}
//Reset the pseudo-random number generator by creating a new seed, unless we are playing from record.
if (levelFile == "?record?" || interlevel) {
if (prngSeed.empty()) {
cout << "WARNING: The record file doesn't provide a random seed! Will use a default random seed! "
"This may breaks the behavior pseudo-random number generator in script!" << endl;
prngSeed = std::string(32, '0');
} else {
#ifdef _DEBUG
cout << "Use existing PRNG seed: " << prngSeed << endl;
#endif
}
} else {
prngSeed = createNewSeed();
#ifdef _DEBUG
cout << "Create new PRNG seed: " << prngSeed << endl;
#endif
}
{
std::seed_seq seq(prngSeed.begin(), prngSeed.end());
prng.seed(seq);
}
if (save) {
prngSaved = prng;
prngSeedSaved = prngSeed;
}
//Reset the level size.
levelRect = levelRectInitial;
if (save) levelRectSaved = levelRectInitial;
//Reset the camera.
cameraMode=CAMERA_PLAYER;
if(save) cameraModeSaved=CAMERA_PLAYER;
cameraTarget = SDL_Rect{ 0, 0, 0, 0 };
if (save) cameraTargetSaved = SDL_Rect{ 0, 0, 0, 0 };
//Reset the number of collectables
currentCollectables = currentCollectablesInitial;
totalCollectables = totalCollectablesInitial;
if (save) {
currentCollectablesSaved = currentCollectablesInitial;
totalCollectablesSaved = totalCollectablesInitial;
}
//Clear the event queue, since all the events are from before the reset.
eventQueue.clear();
//Reset states of scenery layers.
for (auto it = sceneryLayers.begin(); it != sceneryLayers.end(); ++it) {
it->second->resetAnimation(save);
}
//Also reset the background animation, if any.
if(background)
background->resetAnimation(save);
//Reset the cached notification block
notificationTexture.update(NULL, NULL);
//Reset the script environment if necessary.
if (noScript) {
//Destroys the script environment completely.
scriptExecutor->destroy();
//Reset some variables.
scriptSaveState = LUA_REFNIL;
if (scriptExecutorSaved.savedDelayExecutionObjects) {
// Set the state to NULL since the state is already destroyed. This prevents the destructor to call luaL_unref.
scriptExecutorSaved.savedDelayExecutionObjects->state = NULL;
delete scriptExecutorSaved.savedDelayExecutionObjects;
scriptExecutorSaved.savedDelayExecutionObjects = NULL;
}
//Clear the level script.
compiledScripts.clear();
savedCompiledScripts.clear();
initialCompiledScripts.clear();
//Clear the block script.
for (auto block : levelObjects){
block->compiledScripts.clear();
}
for (auto block : levelObjectsSave){
block->compiledScripts.clear();
}
for (auto block : levelObjectsInitial){
block->compiledScripts.clear();
}
} else if (save) {
//Create a new script environment.
getScriptExecutor()->reset(true);
//Reset some variables.
scriptSaveState = LUA_REFNIL;
if (scriptExecutorSaved.savedDelayExecutionObjects) {
// Set the state to NULL since the state is already destroyed. This prevents the destructor to call luaL_unref.
scriptExecutorSaved.savedDelayExecutionObjects->state = NULL;
delete scriptExecutorSaved.savedDelayExecutionObjects;
scriptExecutorSaved.savedDelayExecutionObjects = NULL;
}
//Recompile the level script.
compiledScripts.clear();
savedCompiledScripts.clear();
initialCompiledScripts.clear();
for (auto it = scripts.begin(); it != scripts.end(); ++it){
int index = getScriptExecutor()->compileScript(it->second);
compiledScripts[it->first] = index;
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
savedCompiledScripts[it->first] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
initialCompiledScripts[it->first] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
}
//Check if <levelname>.lua is present.
{
std::string s = scriptFile;
if (s.empty() && !levelFile.empty() && levelFile[0] != '?') {
s = levelFile;
size_t lp = s.find_last_of('.');
if (lp != std::string::npos) {
s = s.substr(0, lp);
}
s += ".lua";
}
int index = LUA_REFNIL;
//Now compile the file.
if (!s.empty()) {
if (s[0] == '?') {
index = getScriptExecutor()->compileScript(s.substr(1));
} else {
FILE *f = fopen(s.c_str(), "r");
if (f) {
fclose(f);
index = getScriptExecutor()->compileFile(s);
}
}
}
if (index != LUA_REFNIL) {
compiledScripts[LevelEvent_BeforeCreate] = index;
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
savedCompiledScripts[LevelEvent_BeforeCreate] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
lua_rawgeti(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX, index);
initialCompiledScripts[LevelEvent_BeforeCreate] = luaL_ref(getScriptExecutor()->getLuaState(), LUA_REGISTRYINDEX);
}
}
//Recompile the block script.
for (auto block : levelObjects){
block->compiledScripts.clear();
}
for (auto block : levelObjectsSave){
block->compiledScripts.clear();
}
for (auto block : levelObjectsInitial) {
block->compiledScripts.clear();
for (auto it = block->scripts.begin(); it != block->scripts.end(); ++it){
int index = getScriptExecutor()->compileScript(it->second);
block->compiledScripts[it->first] = index;
}
}
} else {
assert(getScriptExecutor());
//Do a soft reset.
getScriptExecutor()->reset(false);
//Restore the level script to initial state.
copyCompiledScripts(getScriptExecutor()->getLuaState(), initialCompiledScripts, compiledScripts);
//NOTE: We don't need to restore the block script since it will be restored automatically when the block array is copied.
}
//We reset levelObjects here since we need to wait the compiledScripts being initialized.
copyLevelObjects(levelObjectsInitial, levelObjects, true);
if (save) {
copyLevelObjects(levelObjectsInitial, levelObjectsSave, false);
}
//Also reset the last checkpoint so set it to NULL.
if (save)
objLastCheckPoint = NULL;
//Send GameObjectEvent_OnCreate event to the script
for (auto block : levelObjects) {
block->onEvent(GameObjectEvent_OnCreate);
}
//Call the level's onCreate event.
executeScript(LevelEvent_BeforeCreate);
executeScript(LevelEvent_OnCreate);
//Close exit(s) if there are any collectables
if (totalCollectables>0){
for (auto block : levelObjects){
if (block->type == TYPE_EXIT){
block->onEvent(GameObjectEvent_OnSwitchOff);
}
}
}
//Hide the cursor (if not the leveleditor).
if(stateID!=STATE_LEVEL_EDITOR)
SDL_ShowCursor(SDL_DISABLE);
}
void Game::executeScript(int eventType){
map<int,int>::iterator it;
//Check if there's a script for the given event.
it=compiledScripts.find(eventType);
if(it!=compiledScripts.end()){
//There is one so execute it.
getScriptExecutor()->executeScript(it->second);
}
}
void Game::broadcastObjectEvent(int eventType,int objectType,const char* id,GameObject* target){
//Create a typeGameObjectEvent that can be put into the queue.
typeGameObjectEvent e;
//Set the event type.
e.eventType=eventType;
//Set the object type.
e.objectType=objectType;
//By default flags=0.
e.flags=0;
//Unless there's an id.
if(id){
//Set flags to 0x1 and set the id.
e.flags|=1;
e.id=id;
}
//Or there's a target given.
if(target)
e.target=target;
else
e.target=NULL;
//Add the event to the queue.
eventQueue.push_back(e);
}
void Game::getCurrentLevelAutoSaveRecordPath(std::string &bestTimeFilePath,std::string &bestRecordingFilePath,bool createPath){
levels->getLevelAutoSaveRecordPath(-1,bestTimeFilePath,bestRecordingFilePath,createPath);
}
void Game::gotoNextLevel(ImageManager& imageManager, SDL_Renderer& renderer){
//Goto the next level.
levels->nextLevel();
//Check if the level exists.
if(levels->getCurrentLevel()<levels->getLevelCount()){
setNextState(STATE_GAME);
}else{
if(!levels->congratulationText.empty()){
msgBox(imageManager,renderer,_CC(levels->getDictionaryManager(),levels->congratulationText),MsgBoxOKOnly,_("Congratulations"));
}else{
msgBox(imageManager,renderer,_("You have finished the levelpack!"),MsgBoxOKOnly,_("Congratulations"));
}
//Now go back to the levelselect screen.
setNextState(STATE_LEVEL_SELECT);
//And set the music back to menu.
getMusicManager()->playMusic("menu");
}
}
void Game::GUIEventCallback_OnEvent(ImageManager& imageManager,SDL_Renderer& renderer, string name,GUIObject* obj,int eventType){
if(name=="cmdMenu"){
setNextState(STATE_LEVEL_SELECT);
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}else if(name=="cmdRestart"){
//Clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
interlevel=false;
//And reset the game.
//NOTE: We don't need to clear the save game because in level replay the game won't be saved (??)
//TODO: it seems work (??); I'll see if it introduce bugs
reset(false, false);
}else if(name=="cmdNext"){
//No matter what, clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
//And goto the next level.
gotoNextLevel(imageManager,renderer);
}
}
void Game::invalidateNotificationTexture(Block *block) {
if (block == NULL || block == notificationTexture.getId()) {
notificationTexture.update(NULL, NULL);
}
}
diff --git a/src/LevelEditSelect.cpp b/src/LevelEditSelect.cpp
index 542807f..b9e8e95 100644
--- a/src/LevelEditSelect.cpp
+++ b/src/LevelEditSelect.cpp
@@ -1,966 +1,972 @@
/*
* Copyright (C) 2012-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "LevelEditSelect.h"
#include "GameState.h"
#include "Functions.h"
#include "Settings.h"
#include "FileManager.h"
#include "Globals.h"
#include "GUIObject.h"
#include "GUIListBox.h"
#include "GUIScrollBar.h"
#include "GUISpinBox.h"
#include "InputManager.h"
#include "StatisticsManager.h"
#include "Game.h"
#include "WordWrapper.h"
#include "GUIOverlay.h"
#include "LevelPackPOTExporter.h"
#include <algorithm>
#include <string>
#include <iostream>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
LevelEditSelect::LevelEditSelect(ImageManager& imageManager, SDL_Renderer& renderer):LevelSelect(imageManager,renderer,_("Map Editor"),LevelPackManager::CUSTOM_PACKS){
//Create the gui.
createGUI(imageManager,renderer, true);
//show level list
changePack();
refresh(imageManager, renderer);
}
LevelEditSelect::~LevelEditSelect(){
selectedNumber=NULL;
}
void LevelEditSelect::createGUI(ImageManager& imageManager,SDL_Renderer &renderer, bool initial){
if(initial){
//Create the six buttons at the bottom of the screen.
newPack = new GUIButton(imageManager, renderer, 0, 0, -1, 32, _("New Levelpack"));
newPack->name = "cmdNewLvlpack";
newPack->eventCallback = this;
GUIObjectRoot->addChild(newPack);
propertiesPack = new GUIButton(imageManager, renderer, 0, 0, -1, 32, _("Pack Properties"), 0, true, true, GUIGravityCenter);
propertiesPack->name = "cmdLvlpackProp";
propertiesPack->eventCallback = this;
GUIObjectRoot->addChild(propertiesPack);
removePack = new GUIButton(imageManager, renderer, 0, 0, -1, 32, _("Remove Pack"), 0, true, true, GUIGravityRight);
removePack->name = "cmdRmLvlpack";
removePack->eventCallback = this;
GUIObjectRoot->addChild(removePack);
move = new GUIButton(imageManager, renderer, 0, 0, -1, 32, _("Move Map"));
move->name = "cmdMoveMap";
move->eventCallback = this;
//NOTE: Set enabled equal to the inverse of initial.
//When resizing the window initial will be false and therefor the move button can stay enabled.
move->enabled = false;
GUIObjectRoot->addChild(move);
remove = new GUIButton(imageManager, renderer, 0, 0, -1, 32, _("Remove Map"), 0, false, true, GUIGravityCenter);
remove->name = "cmdRmMap";
remove->eventCallback = this;
GUIObjectRoot->addChild(remove);
edit = new GUIButton(imageManager, renderer, 0, 0, -1, 32, _("Edit Map"), 0, false, true, GUIGravityRight);
edit->name = "cmdEdit";
edit->eventCallback = this;
GUIObjectRoot->addChild(edit);
}
//Move buttons to default position
const int x1 = int(SCREEN_WIDTH*0.02), x2 = int(SCREEN_WIDTH*0.5), x3 = int(SCREEN_WIDTH*0.98);
const int y1 = SCREEN_HEIGHT - 120, y2 = SCREEN_HEIGHT - 60;
newPack->left = x1; newPack->top = y1; newPack->gravity = GUIGravityLeft;
propertiesPack->left = x2; propertiesPack->top = y1; propertiesPack->gravity = GUIGravityCenter;
removePack->left = x3; removePack->top = y1; removePack->gravity = GUIGravityRight;
move->left = x1; move->top = y2; move->gravity = GUIGravityLeft;
remove->left = x2; remove->top = y2; remove->gravity = GUIGravityCenter;
edit->left = x3; edit->top = y2; edit->gravity = GUIGravityRight;
isVertical = false;
//Reset the font size
newPack->smallFont = false; newPack->width = -1;
propertiesPack->smallFont = false; propertiesPack->width = -1;
removePack->smallFont = false; removePack->width = -1;
move->smallFont = false; move->width = -1;
remove->smallFont = false; remove->width = -1;
edit->smallFont = false; edit->width = -1;
//Now update widgets and then check if they overlap
GUIObjectRoot->render(renderer, 0, 0, false);
if (propertiesPack->left - propertiesPack->gravityX < newPack->left + newPack->width ||
propertiesPack->left - propertiesPack->gravityX + propertiesPack->width > removePack->left - removePack->gravityX)
{
newPack->smallFont = true; newPack->width = -1;
propertiesPack->smallFont = true; propertiesPack->width = -1;
removePack->smallFont = true; removePack->width = -1;
move->smallFont = true; move->width = -1;
remove->smallFont = true; remove->width = -1;
edit->smallFont = true; edit->width = -1;
}
// NOTE: the following code is necessary (e.g. for Germany)
//Check again
GUIObjectRoot->render(renderer, 0, 0, false);
if (propertiesPack->left - propertiesPack->gravityX < newPack->left + newPack->width ||
propertiesPack->left - propertiesPack->gravityX + propertiesPack->width > removePack->left - removePack->gravityX)
{
newPack->left = SCREEN_WIDTH*0.02;
newPack->top = SCREEN_HEIGHT - 140;
newPack->smallFont = false;
newPack->width = -1;
newPack->gravity = GUIGravityLeft;
propertiesPack->left = SCREEN_WIDTH*0.02;
propertiesPack->top = SCREEN_HEIGHT - 100;
propertiesPack->smallFont = false;
propertiesPack->width = -1;
propertiesPack->gravity = GUIGravityLeft;
removePack->left = SCREEN_WIDTH*0.02;
removePack->top = SCREEN_HEIGHT - 60;
removePack->smallFont = false;
removePack->width = -1;
removePack->gravity = GUIGravityLeft;
move->left = SCREEN_WIDTH*0.98;
move->top = SCREEN_HEIGHT - 140;
move->smallFont = false;
move->width = -1;
move->gravity = GUIGravityRight;
remove->left = SCREEN_WIDTH*0.98;
remove->top = SCREEN_HEIGHT - 100;
remove->smallFont = false;
remove->width = -1;
remove->gravity = GUIGravityRight;
edit->left = SCREEN_WIDTH*0.98;
edit->top = SCREEN_HEIGHT - 60;
edit->smallFont = false;
edit->width = -1;
edit->gravity = GUIGravityRight;
isVertical = true;
}
}
void LevelEditSelect::changePack(){
packPath = levelpacks->getName();
if(packPath==CUSTOM_LEVELS_PATH){
//Disable some levelpack buttons.
propertiesPack->enabled=false;
removePack->enabled=false;
}else{
//Enable some levelpack buttons.
propertiesPack->enabled=true;
removePack->enabled=true;
}
//Set last levelpack.
getSettings()->setValue("lastlevelpack",packPath);
//Now let levels point to the right pack.
levels=getLevelPackManager()->getLevelPack(packPath);
//Get the untranslated pack name.
packName = levels->levelpackName;
//invalidate the tooltip
toolTip.number = -1;
}
void LevelEditSelect::packProperties(ImageManager& imageManager,SDL_Renderer& renderer, bool newPack){
packPropertiesFrames.clear();
//Open a message popup.
GUIObject *root = new GUIFrame(imageManager, renderer, (SCREEN_WIDTH - 600) / 2, (SCREEN_HEIGHT - 390) / 2, 600, 390,
newPack ? _("New Levelpack") : "");
GUIObject *obj;
if (newPack) {
packPropertiesFrames.resize(1);
} else {
packPropertiesFrames.resize(2);
GUISingleLineListBox *sllb = new GUISingleLineListBox(imageManager, renderer, 40, 12, 520, 36);
sllb->name = "sllb";
sllb->addItem("Properties", _("Properties"));
sllb->addItem("Tools", _("Tools"));
sllb->value = 0;
sllb->eventCallback = this;
root->addChild(sllb);
}
obj=new GUILabel(imageManager,renderer,40,50,240,36,_("Name:"));
root->addChild(obj);
packPropertiesFrames[0].push_back(obj);
obj=new GUITextBox(imageManager,renderer,60,80,480,36,packName.c_str());
if(newPack)
obj->caption="";
obj->name="LvlpackName";
root->addChild(obj);
packPropertiesFrames[0].push_back(obj);
obj=new GUILabel(imageManager,renderer,40,120,240,36,_("Description:"));
root->addChild(obj);
packPropertiesFrames[0].push_back(obj);
obj=new GUITextBox(imageManager,renderer,60,150,480,36,levels->levelpackDescription.c_str());
if(newPack)
obj->caption="";
obj->name="LvlpackDescription";
root->addChild(obj);
packPropertiesFrames[0].push_back(obj);
obj=new GUILabel(imageManager,renderer,40,190,240,36,_("Congratulation text:"));
root->addChild(obj);
packPropertiesFrames[0].push_back(obj);
obj=new GUITextBox(imageManager,renderer,60,220,480,36,levels->congratulationText.c_str());
if(newPack)
obj->caption="";
obj->name="LvlpackCongratulation";
root->addChild(obj);
packPropertiesFrames[0].push_back(obj);
obj = new GUILabel(imageManager, renderer, 40, 260, 240, 36, _("Music list:"));
root->addChild(obj);
packPropertiesFrames[0].push_back(obj);
obj = new GUITextBox(imageManager, renderer, 60, 290, 480, 36, levels->levelpackMusicList.c_str());
if (newPack)
obj->caption = "";
obj->name = "LvlpackMusic";
root->addChild(obj);
packPropertiesFrames[0].push_back(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.3,390-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->name="cfgOK";
obj->eventCallback=this;
root->addChild(obj);
packPropertiesFrames[0].push_back(obj);
GUIButton *cancelButton = new GUIButton(imageManager, renderer, root->width*0.7, 390 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
cancelButton->name = "cfgCancel";
cancelButton->eventCallback = this;
root->addChild(cancelButton);
packPropertiesFrames[0].push_back(cancelButton);
if (!newPack) {
GUIButton *btn = new GUIButton(imageManager, renderer, root->width*0.5, 80, -1, 36, _("Export translation template"), 0, true, false, GUIGravityCenter);
btn->name = "cfgExportPOT";
btn->smallFont = true;
btn->eventCallback = this;
root->addChild(btn);
packPropertiesFrames[1].push_back(btn);
obj = new GUIButton(imageManager, renderer, root->width*0.5, 390 - 44, -1, 36, _("Close"), 0, true, false, GUIGravityCenter);
obj->name = "cfgCancel";
obj->eventCallback = this;
root->addChild(obj);
packPropertiesFrames[1].push_back(obj);
}
//Create the gui overlay.
//NOTE: We don't need to store a pointer since it will auto cleanup itself.
new AddonOverlay(renderer, root, cancelButton, NULL, UpDownFocus | TabFocus | ReturnControls | LeftRightControls);
if(newPack){
packPath.clear();
packName.clear();
}
}
void LevelEditSelect::addLevel(ImageManager& imageManager,SDL_Renderer& renderer){
//Open a message popup.
GUIObject* root=new GUIFrame(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-200)/2,600,200,_("Add level"));
GUIObject* obj;
obj=new GUILabel(imageManager,renderer,40,80,240,36,_("File name:"));
root->addChild(obj);
char s[64];
SDL_snprintf(s,64,"map%02d.map",levels->getLevelCount()+1);
obj=new GUITextBox(imageManager,renderer,300,80,240,36,s);
obj->name="LvlFile";
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.3,200-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->name="cfgAddOK";
obj->eventCallback=this;
root->addChild(obj);
GUIButton *cancelButton = new GUIButton(imageManager, renderer, root->width*0.7, 200 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
cancelButton->name = "cfgAddCancel";
cancelButton->eventCallback = this;
root->addChild(cancelButton);
//Dim the screen using the tempSurface.
//NOTE: We don't need to store a pointer since it will auto cleanup itself.
new AddonOverlay(renderer, root, cancelButton, NULL, UpDownFocus | TabFocus | ReturnControls | LeftRightControls);
}
void LevelEditSelect::moveLevel(ImageManager& imageManager,SDL_Renderer& renderer){
//Open a message popup.
GUIObject* root=new GUIFrame(imageManager,renderer,(SCREEN_WIDTH-600)/2,(SCREEN_HEIGHT-200)/2,600,200,_("Move level"));
GUIObject* obj;
obj=new GUILabel(imageManager,renderer,40,60,240,36,_("Level: "));
root->addChild(obj);
GUISpinBox *spinBox = new GUISpinBox(imageManager, renderer, 300, 60, 240, 36);
spinBox->caption = tfm::format("%d", selectedNumber->getNumber() + 1);
spinBox->format = "%1.0f";
spinBox->limitMin = 1.0f;
spinBox->limitMax = float(levels->getLevelCount());
spinBox->name = "MoveLevel";
root->addChild(spinBox);
obj=new GUISingleLineListBox(imageManager,renderer,root->width*0.5,110,240,36,true,true,GUIGravityCenter);
obj->name="lstPlacement";
vector<string> v;
v.push_back(_("Before"));
v.push_back(_("After"));
v.push_back(_("Swap"));
(dynamic_cast<GUISingleLineListBox*>(obj))->addItems(v);
obj->value=0;
root->addChild(obj);
obj=new GUIButton(imageManager,renderer,root->width*0.3,200-44,-1,36,_("OK"),0,true,true,GUIGravityCenter);
obj->name="cfgMoveOK";
obj->eventCallback=this;
root->addChild(obj);
GUIButton *cancelButton = new GUIButton(imageManager, renderer, root->width*0.7, 200 - 44, -1, 36, _("Cancel"), 0, true, true, GUIGravityCenter);
cancelButton->name = "cfgMoveCancel";
cancelButton->eventCallback = this;
root->addChild(cancelButton);
//Create the gui overlay.
//NOTE: We don't need to store a pointer since it will auto cleanup itself.
new AddonOverlay(renderer, root, cancelButton, NULL, TabFocus | ReturnControls | LeftRightControls);
}
void LevelEditSelect::refresh(ImageManager& imageManager, SDL_Renderer& renderer, bool change){
int m=levels->getLevelCount();
if(change){
// only a temporary position
SDL_Rect box = { 0, 0, 50, 50 };
numbers.clear();
//clear the selected level
if(selectedNumber!=NULL){
selectedNumber=NULL;
}
//Disable the level specific buttons.
move->enabled=false;
remove->enabled=false;
edit->enabled=false;
for(int n=0;n<=m;n++){
numbers.emplace_back(imageManager, renderer);
if (n == m) numbers[n].init(renderer, "+", box, n);
else numbers[n].init(renderer, n, box);
}
if (levels->levelpackPath == LEVELS_PATH || levels->levelpackPath == CUSTOM_LEVELS_PATH)
levelpackDescription->caption = _("Individual levels which are not contained in any level packs");
else if (!levels->levelpackDescription.empty())
levelpackDescription->caption = _CC(levels->getDictionaryManager(), levels->levelpackDescription);
else
levelpackDescription->caption = "";
//invalidate the tooltip
toolTip.number = -1;
}
for(int n=0;n<=m;n++){
numbers[n].box = SDL_Rect{ (n%LEVELS_PER_ROW) * 64 + 80, (n / LEVELS_PER_ROW) * 64 + 184, 50, 50 };
}
m++; //including the "+" button
if(m>LEVELS_DISPLAYED_IN_SCREEN){
levelScrollBar->maxValue=(m-LEVELS_DISPLAYED_IN_SCREEN+LEVELS_PER_ROW-1)/LEVELS_PER_ROW;
levelScrollBar->visible=true;
}else{
levelScrollBar->maxValue=0;
levelScrollBar->visible=false;
}
}
void LevelEditSelect::selectNumber(ImageManager& imageManager, SDL_Renderer& renderer, unsigned int number, bool selected){
if (selected) {
if (number >= 0 && number < levels->getLevelCount()) {
levels->setCurrentLevel(number);
setNextState(STATE_LEVEL_EDITOR);
} else {
addLevel(imageManager, renderer);
}
}else{
move->enabled = false;
remove->enabled = false;
edit->enabled = false;
selectedNumber = NULL;
if (number == numbers.size() - 1){
if (isKeyboardOnly) {
selectedNumber = &numbers[number];
} else {
addLevel(imageManager, renderer);
}
} else if (number >= 0 && number < levels->getLevelCount()) {
selectedNumber=&numbers[number];
//Enable the level specific buttons.
//NOTE: We check if 'remove levelpack' is enabled, if not then it's the Levels levelpack.
if(removePack->enabled)
move->enabled=true;
remove->enabled=true;
edit->enabled=true;
}
}
}
void LevelEditSelect::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//Call handleEvents() of base class.
LevelSelect::handleEvents(imageManager, renderer);
if (section == 3) {
//Check focus movement
if (inputMgr.isKeyDownEvent(INPUTMGR_RIGHT)){
isKeyboardOnly = true;
section2 += isVertical ? 3 : 1;
} else if (inputMgr.isKeyDownEvent(INPUTMGR_LEFT)){
isKeyboardOnly = true;
section2 -= isVertical ? 3 : 1;
} else if (inputMgr.isKeyDownEvent(INPUTMGR_UP)){
isKeyboardOnly = true;
section2 -= isVertical ? 1 : 3;
} else if (inputMgr.isKeyDownEvent(INPUTMGR_DOWN)){
isKeyboardOnly = true;
section2 += isVertical ? 1 : 3;
}
if (section2 > 6) section2 -= 6;
else if (section2 < 1) section2 += 6;
//Check if enter is pressed
if (isKeyboardOnly && inputMgr.isKeyDownEvent(INPUTMGR_SELECT) && section2 >= 1 && section2 <= 6) {
GUIButton *buttons[6] = {
newPack, propertiesPack, removePack, move, remove, edit
};
GUIEventCallback_OnEvent(imageManager, renderer, buttons[section2 - 1]->name, buttons[section2 - 1], GUIEventClick);
}
}
}
void LevelEditSelect::render(ImageManager& imageManager,SDL_Renderer &renderer){
//Let the levelselect render.
LevelSelect::render(imageManager,renderer);
//Draw highlight in keyboard only mode.
if (isKeyboardOnly) {
GUIButton *buttons[6] = {
newPack, propertiesPack, removePack, move, remove, edit
};
for (int i = 0; i < 6; i++) {
buttons[i]->state = (section == 3 && section2 - 1 == i) ? 1 : 0;
}
}
}
void LevelEditSelect::resize(ImageManager& imageManager, SDL_Renderer &renderer){
//Let the levelselect resize.
LevelSelect::resize(imageManager, renderer);
//Create the GUI.
createGUI(imageManager,renderer, false);
//NOTE: This is a workaround for buttons failing when resizing.
if(packPath==CUSTOM_LEVELS_PATH){
removePack->enabled=false;
propertiesPack->enabled=false;
}
if(selectedNumber)
selectNumber(imageManager, renderer, selectedNumber->getNumber(),false);
}
void LevelEditSelect::renderTooltip(SDL_Renderer& renderer,unsigned int number,int dy){
if (!toolTip.name || toolTip.number != number) {
SDL_Color fg = objThemes.getTextColor(true);
toolTip.number = number;
if (number < (unsigned int)levels->getLevelCount()){
WordWrapper wrapper;
wrapper.font = fontText;
wrapper.maxWidth = int(SCREEN_WIDTH*0.5f);
wrapper.wordWrap = true;
wrapper.hyphen = "-";
std::vector<std::string> lines;
wrapper.addString(lines, _CC(levels->getDictionaryManager(), levels->getLevelName(number)));
//Render the name of the level.
toolTip.name = textureFromMultilineText(renderer, *fontText, lines, fg);
} else {
//Add level button
toolTip.name = textureFromText(renderer, *fontText, _("Add level"), fg);
}
}
//Check if name isn't null.
if(!toolTip.name)
return;
//Now draw a square the size of the three texts combined.
SDL_Rect r=numbers[number].box;
r.y-=dy*64;
const SDL_Rect nameSize = rectFromTexture(*toolTip.name);
r.w=nameSize.w;
r.h=nameSize.h;
//Make sure the tooltip doesn't go outside the window.
if(r.y>SCREEN_HEIGHT-200){
r.y-=nameSize.h+4;
}else{
r.y+=numbers[number].box.h+2;
}
if(r.x+r.w>SCREEN_WIDTH-50)
r.x=SCREEN_WIDTH-50-r.w;
//Draw a rectange
Uint32 color=0xFFFFFFFF;
drawGUIBox(r.x-5,r.y-5,r.w+10,r.h+10,renderer,color);
//Calc the position to draw.
SDL_Rect r2=r;
//Now we render the name if the surface isn't null.
if(toolTip.name){
//Draw the name.
applyTexture(r2.x, r2.y, toolTip.name, renderer);
}
}
//Escape invalid characters in a file name (mainly for Windows).
static std::string escapeFileName(const std::string& fileName) {
std::string ret;
for (int i = 0, m = fileName.size(); i < m; i++) {
bool escape = false;
char c = fileName[i];
switch (c) {
case '\"': case '*': case '/': case ':': case '<':
case '>': case '?': case '\\': case '|': case '%':
escape = true;
break;
}
if (c <= 0x1F || c >= 0x7F) escape = true;
if (i == 0 || i == m - 1) {
switch (c) {
case ' ': case '.':
escape = true;
break;
}
}
if (escape) {
ret += "%" + tfm::format("%02X", (int)(unsigned char)c);
} else {
ret.push_back(c);
}
}
return ret;
}
void LevelEditSelect::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//NOTE: We check for the levelpack change to enable/disable some levelpack buttons.
if(name=="cmdLvlPack"){
//We call changepack and return to prevent the LevelSelect to undo what we did.
changePack();
refresh(imageManager, renderer);
return;
}
//Let the level select handle his GUI events.
LevelSelect::GUIEventCallback_OnEvent(imageManager,renderer,name,obj,eventType);
//Check for the edit button.
if(name=="cmdNewLvlpack"){
//Create a new pack.
packProperties(imageManager,renderer, true);
}else if(name=="cmdLvlpackProp"){
//Show the pack properties.
packProperties(imageManager,renderer, false);
}else if(name=="cmdRmLvlpack"){
auto dm = getLevelPackManager()->getLevelPack(packPath)->getDictionaryManager();
//Show an "are you sure" message.
if (msgBox(imageManager, renderer, tfm::format(_("Are you sure remove the level pack '%s'?"),
dm ? dm->get_dictionary().translate(packName) : packName),
MsgBoxYesNo, _("Remove prompt")) == MsgBoxYes)
{
//Remove the directory.
if(!removeDirectory(levels->levelpackPath.c_str())){
cerr<<"ERROR: Unable to remove levelpack directory "<<levels->levelpackPath<<endl;
}
//Remove it from the vector (levelpack list).
for (auto it = levelpacks->item.begin(); it != levelpacks->item.end(); ++it){
if (it->first == levels->levelpackPath) {
levelpacks->item.erase(it);
break;
}
}
//Remove it from the levelpackManager.
getLevelPackManager()->removeLevelPack(levels->levelpackPath, true);
levels = NULL;
//And call changePack.
levelpacks->value=levelpacks->item.size()-1;
changePack();
refresh(imageManager, renderer);
}
}else if(name=="cmdMoveMap"){
if(selectedNumber!=NULL){
moveLevel(imageManager,renderer);
}
}else if(name=="cmdRmMap"){
if(selectedNumber!=NULL){
const std::string& levelName = levels->getLevel(selectedNumber->getNumber())->name;
auto dm = levels->getDictionaryManager();
//Show an "are you sure" message.
if (msgBox(imageManager, renderer, tfm::format(_("Are you sure remove the map '%s'?"),
dm ? dm->get_dictionary().translate(levelName) : levelName),
MsgBoxYesNo, _("Remove prompt")) != MsgBoxYes) {
return;
}
if(packPath!=CUSTOM_LEVELS_PATH){
if(!removeFile((levels->levelpackPath+"/"+levels->getLevel(selectedNumber->getNumber())->file).c_str())){
cerr<<"ERROR: Unable to remove level "<<(levels->levelpackPath+"/"+levels->getLevel(selectedNumber->getNumber())->file).c_str()<<endl;
}
levels->removeLevel(selectedNumber->getNumber());
levels->saveLevels(levels->levelpackPath+"/levels.lst");
}else{
//This is the levels levelpack so we just remove the file.
if(!removeFile(levels->getLevel(selectedNumber->getNumber())->file.c_str())){
cerr<<"ERROR: Unable to remove level "<<levels->getLevel(selectedNumber->getNumber())->file<<endl;
}
levels->removeLevel(selectedNumber->getNumber());
}
//And refresh the selection screen.
refresh(imageManager, renderer);
}
}else if(name=="cmdEdit"){
if(selectedNumber!=NULL){
levels->setCurrentLevel(selectedNumber->getNumber());
setNextState(STATE_LEVEL_EDITOR);
}
}
//Check for levelpack properties events.
else if(name=="cfgOK"){
GUIObject *lvlpackName = GUIObjectRoot->getChild("LvlpackName");
GUIObject *lvlpackDescription = GUIObjectRoot->getChild("LvlpackDescription");
GUIObject *lvlpackCongratulation = GUIObjectRoot->getChild("LvlpackCongratulation");
GUIObject *lvlpackMusic = GUIObjectRoot->getChild("LvlpackMusic");
assert(lvlpackName && lvlpackDescription && lvlpackCongratulation && lvlpackMusic);
if (lvlpackName->caption.empty()) {
msgBox(imageManager, renderer, _("Levelpack name cannot be empty."), MsgBoxOKOnly, _("Error"));
} else {
//Check if the name changed.
if (packName != lvlpackName->caption) {
std::string newPackPathMinusSlash = getUserPath(USER_DATA) + "custom/levelpacks/" + escapeFileName(lvlpackName->caption);
//Delete the old one.
if (!packName.empty()){
std::string oldPackPathMinusSlash = levels->levelpackPath;
if (!oldPackPathMinusSlash.empty()) {
if (oldPackPathMinusSlash[oldPackPathMinusSlash.size() - 1] == '/'
|| oldPackPathMinusSlash[oldPackPathMinusSlash.size() - 1] == '\\')
{
oldPackPathMinusSlash.pop_back();
}
}
if (!renameDirectory(oldPackPathMinusSlash.c_str(), newPackPathMinusSlash.c_str())) {
cerr << "ERROR: Unable to move levelpack directory '" << oldPackPathMinusSlash << "' to '"
<< newPackPathMinusSlash << "'! The levelpack directory will be kept unchanged." << endl;
//If we failed to rename the directory, we just keep the old directory name.
newPackPathMinusSlash = oldPackPathMinusSlash;
}
//Remove the old one from the levelpack manager.
getLevelPackManager()->removeLevelPack(levels->levelpackPath, false);
//And the levelpack list.
for (auto it = levelpacks->item.begin(); it != levelpacks->item.end(); ++it){
if (it->first == levels->levelpackPath) {
levelpacks->item.erase(it);
break;
}
}
} else {
//It's a new levelpack. First we try to create the dirs and the levels.lst.
if (dirExists(newPackPathMinusSlash.c_str())) {
cerr << "ERROR: The levelpack directory " << newPackPathMinusSlash << " already exists!" << endl;
msgBox(imageManager, renderer, tfm::format(_("The levelpack directory '%s' already exists!"), newPackPathMinusSlash), MsgBoxOKOnly, _("Error"));
return;
}
if (!createDirectory(newPackPathMinusSlash.c_str())) {
cerr << "ERROR: Unable to create levelpack directory " << newPackPathMinusSlash << endl;
msgBox(imageManager, renderer, tfm::format(_("Unable to create levelpack directory '%s'!"), newPackPathMinusSlash), MsgBoxOKOnly, _("Error"));
return;
}
if (fileExists((newPackPathMinusSlash + "/levels.lst").c_str())) {
cerr << "ERROR: The levelpack file " << (newPackPathMinusSlash + "/levels.lst") << " already exists!" << endl;
msgBox(imageManager, renderer, tfm::format(_("The levelpack file '%s' already exists!"), newPackPathMinusSlash + "/levels.lst"), MsgBoxOKOnly, _("Error"));
return;
}
if (!createFile((newPackPathMinusSlash + "/levels.lst").c_str())) {
cerr << "ERROR: Unable to create levelpack file " << (newPackPathMinusSlash + "/levels.lst") << endl;
msgBox(imageManager, renderer, tfm::format(_("Unable to create levelpack file '%s'!"), newPackPathMinusSlash + "/levels.lst"), MsgBoxOKOnly, _("Error"));
return;
}
//If it's successful we create a new levelpack.
levels = new LevelPack;
+
+ //Update statistics.
+ statsMgr.totalLevelpacks++;
+ statsMgr.totalLevelpacksByCategory[CUSTOM]++;
}
//And set the new name.
packName = levels->levelpackName = lvlpackName->caption;
packPath = levels->levelpackPath = newPackPathMinusSlash + "/";
//Also add the levelpack location
getLevelPackManager()->addLevelPack(levels);
auto dm = levels->getDictionaryManager();
levelpacks->addItem(packPath, dm ? dm->get_dictionary().translate(packName) : packName);
levelpacks->value = levelpacks->item.size() - 1;
//And call changePack.
changePack();
}
levels->levelpackDescription = lvlpackDescription->caption;
levels->congratulationText = lvlpackCongratulation->caption;
levels->levelpackMusicList = lvlpackMusic->caption;
//Refresh the leveleditselect to show the correct information.
refresh(imageManager, renderer);
//Save the configuration.
levels->saveLevels(levels->levelpackPath + "levels.lst");
getSettings()->setValue("lastlevelpack", levels->levelpackPath);
//Clear the gui.
if (GUIObjectRoot) {
delete GUIObjectRoot;
GUIObjectRoot = NULL;
}
}
}else if(name=="cfgCancel"){
//Check if packName is empty, if so it was a new levelpack and we need to revert to an existing one.
if(packName.empty()){
changePack();
}
//Clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
} else if (name == "sllb") {
GUIObject *sllb = GUIObjectRoot->getChild("sllb");
if (sllb) {
for (int i = 0, m = packPropertiesFrames.size(); i < m; i++) {
for (auto o : packPropertiesFrames[i]) {
o->visible = (i == sllb->value);
}
}
}
} else if (name == "cfgExportPOT") {
if (LevelPackPOTExporter::exportPOT(levels->levelpackPath)) {
msgBox(imageManager, renderer,
tfm::format(_("The translation template is exported at\n'%s'."), levels->levelpackPath + "locale/messages.pot"),
MsgBoxOKOnly,
_("Export translation template"));
} else {
msgBox(imageManager, renderer,
_("Failed to export translation template."),
MsgBoxOKOnly,
_("Error"));
}
}
//Check for add level events.
else if(name=="cfgAddOK"){
//Check if the file name isn't null.
//Now loop throught the children of the GUIObjectRoot in search of the fields.
for(unsigned int i=0;i<GUIObjectRoot->childControls.size();i++){
if(GUIObjectRoot->childControls[i]->name=="LvlFile"){
if(GUIObjectRoot->childControls[i]->caption.empty()){
msgBox(imageManager,renderer,_("No file name given for the new level."),MsgBoxOKOnly,_("Missing file name"));
return;
}else{
string tmp_caption = GUIObjectRoot->childControls[i]->caption;
//Replace all spaces with a underline.
size_t j;
for(;(j=tmp_caption.find(" "))!=string::npos;){
tmp_caption.replace(j,1,"_");
}
//If there isn't ".map" extension add it.
size_t found=tmp_caption.find_first_of(".");
if(found!=string::npos)
tmp_caption.replace(tmp_caption.begin()+found+1,tmp_caption.end(),"map");
else if (tmp_caption.substr(found+1)!="map")
tmp_caption.append(".map");
/* Create path and file in it */
string path=(levels->levelpackPath+"/"+tmp_caption);
if(packPath==CUSTOM_LEVELS_PATH){
path=(getUserPath(USER_DATA)+"/custom/levels/"+tmp_caption);
}
//First check if the file doesn't exist already.
FILE* f;
f=fopen(path.c_str(),"rb");
//Check if it exists.
if(f){
//Close the file.
fclose(f);
//Notify the user.
msgBox(imageManager, renderer, tfm::format(_("The file %s already exists."), tmp_caption), MsgBoxOKOnly, _("Error"));
return;
}
if(!createFile(path.c_str())){
cerr<<"ERROR: Unable to create level file "<<path<<endl;
}else{
//Update statistics.
statsMgr.newAchievement("create1");
if((++statsMgr.createdLevels)>=10) statsMgr.newAchievement("create10");
+ statsMgr.totalLevels++;
+ statsMgr.totalLevelsByCategory[CUSTOM]++;
}
levels->addLevel(path);
//NOTE: Also add the level to the levels levelpack in case of custom levels.
if(packPath==CUSTOM_LEVELS_PATH){
LevelPack* levelsPack=getLevelPackManager()->getLevelPack(LEVELS_PATH);
if(levelsPack){
levelsPack->addLevel(path);
levelsPack->setLocked(levelsPack->getLevelCount()-1);
}else{
cerr<<"ERROR: Unable to add level to Levels levelpack"<<endl;
}
}
if(packPath!=CUSTOM_LEVELS_PATH)
levels->saveLevels(levels->levelpackPath+"levels.lst");
refresh(imageManager, renderer);
//Clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
return;
}
}
}
}
}else if(name=="cfgAddCancel"){
//Clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}
//Check for move level events.
else if(name=="cfgMoveOK"){
//Check if the entered level number is valid.
//Now loop throught the children of the GUIObjectRoot in search of the fields.
int level=0;
int placement=0;
for(unsigned int i=0;i<GUIObjectRoot->childControls.size();i++){
if(GUIObjectRoot->childControls[i]->name=="MoveLevel"){
level=atoi(GUIObjectRoot->childControls[i]->caption.c_str());
if(level<=0 || level>levels->getLevelCount()){
msgBox(imageManager,renderer,_("The entered level number isn't valid!"),MsgBoxOKOnly,_("Illegal number"));
return;
}
}
if(GUIObjectRoot->childControls[i]->name=="lstPlacement"){
placement=GUIObjectRoot->childControls[i]->value;
}
}
//Now we execute the swap/move.
//Check for the place before.
if(placement==0){
//We place the selected level before the entered level.
levels->moveLevel(selectedNumber->getNumber(),level-1);
}else if(placement==1){
//We place the selected level after the entered level.
if(level<selectedNumber->getNumber())
levels->moveLevel(selectedNumber->getNumber(),level);
else
levels->moveLevel(selectedNumber->getNumber(),level+1);
}else if(placement==2){
//We swap the selected level with the entered level.
levels->swapLevel(selectedNumber->getNumber(),level-1);
}
//And save the change.
if(packPath!=CUSTOM_LEVELS_PATH)
levels->saveLevels(levels->levelpackPath+"/levels.lst");
refresh(imageManager, renderer);
//Clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}else if(name=="cfgMoveCancel"){
//Clear the gui.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}
}
diff --git a/src/StatisticsManager.cpp b/src/StatisticsManager.cpp
index 772f771..311511d 100644
--- a/src/StatisticsManager.cpp
+++ b/src/StatisticsManager.cpp
@@ -1,835 +1,872 @@
/*
* Copyright (C) 2012 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "StatisticsManager.h"
#include "FileManager.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "Functions.h"
#include "LevelPackManager.h"
#include "MusicManager.h"
#include "SoundManager.h"
#include "ThemeManager.h"
#include "WordWrapper.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <map>
#include "libs/tinyformat/tinyformat.h"
#include <SDL_ttf_fontfallback.h>
#if defined(WIN32)
#define PRINTF_LONGLONG "%I64d"
#else
#define PRINTF_LONGLONG "%lld"
#endif
using namespace std;
StatisticsManager statsMgr;
static const int achievementDisplayTime=(FPS*4500)/1000;
static const int achievementIntervalTime=achievementDisplayTime+(FPS*500)/1000;
static map<string,AchievementInfo*> avaliableAchievements;
//================================================================
StatisticsManager::StatisticsManager(){
bmDropShadow=NULL;
bmQuestionMark=NULL;
bmAchievement=NULL;
startTime=time(NULL);
tutorialLevels=0;
clear();
}
void StatisticsManager::clear(){
playerTravelingDistance=shadowTravelingDistance=0.0f;
playerJumps=shadowJumps
=playerDies=shadowDies
=playerSquashed=shadowSquashed
- =completedLevels=silverLevels=goldLevels
+ =completedLevels=silverLevels=goldLevels=totalLevels
+ =completedLevelpacks=silverLevelpacks=goldLevelpacks=totalLevelpacks
=recordTimes=switchTimes=swapTimes=saveTimes=loadTimes
=collectibleCollected
=playTime=levelEditTime
=createdLevels=tutorialCompleted=tutorialGold=0;
+ completedLevelsByCategory.fill(0);
+ silverLevelsByCategory.fill(0);
+ goldLevelsByCategory.fill(0);
+ totalLevelsByCategory.fill(0);
+ completedLevelpacksByCategory.fill(0);
+ silverLevelpacksByCategory.fill(0);
+ goldLevelpacksByCategory.fill(0);
+ totalLevelpacksByCategory.fill(0);
+
achievements.clear();
queuedAchievements.clear();
achievementTime=0;
currentAchievement=0;
if(bmAchievement){
bmAchievement.reset();
}
}
#define LOAD_STATS(var,func) { \
vector<string> &v=node.attributes[ #var ]; \
if(!v.empty() && !v[0].empty()) \
var=func(v[0].c_str()); \
}
void StatisticsManager::loadFile(const std::string& fileName){
clear();
ifstream file(fileName.c_str());
if(!file) return;
TreeStorageNode node;
POASerializer serializer;
if(!serializer.readNode(file,&node,true)) return;
//load statistics
LOAD_STATS(playerTravelingDistance,atof);
LOAD_STATS(shadowTravelingDistance,atof);
LOAD_STATS(playerJumps,atoi);
LOAD_STATS(shadowJumps,atoi);
LOAD_STATS(playerDies,atoi);
LOAD_STATS(shadowDies,atoi);
LOAD_STATS(playerSquashed,atoi);
LOAD_STATS(shadowSquashed,atoi);
LOAD_STATS(recordTimes,atoi);
LOAD_STATS(switchTimes,atoi);
LOAD_STATS(swapTimes,atoi);
LOAD_STATS(saveTimes,atoi);
LOAD_STATS(loadTimes,atoi);
LOAD_STATS(collectibleCollected,atoi);
LOAD_STATS(playTime,atoi);
LOAD_STATS(levelEditTime,atoi);
LOAD_STATS(createdLevels,atoi);
//load achievements.
//format is: name;time,name;time,...
{
vector<string> &v=node.attributes["achievements"];
for(unsigned int i=0;i<v.size();i++){
string s=v[i];
time_t t=0;
string::size_type lps=s.find(';');
if(lps!=string::npos){
string s1=s.substr(lps+1);
s=s.substr(0,lps);
long long n;
sscanf(s1.c_str(),PRINTF_LONGLONG,&n);
t=(time_t)n;
}
map<string,AchievementInfo*>::iterator it=avaliableAchievements.find(s);
if(it!=avaliableAchievements.end()){
OwnedAchievement ach={t,it->second};
achievements[it->first]=ach;
}
}
}
}
//Call when level edit is start
void StatisticsManager::startLevelEdit(){
levelEditStartTime=time(NULL);
}
//Call when level edit is end
void StatisticsManager::endLevelEdit(){
levelEditTime+=time(NULL)-levelEditStartTime;
}
//update in-game time
void StatisticsManager::updatePlayTime(){
time_t endTime=time(NULL);
playTime+=endTime-startTime;
startTime=endTime;
}
#define SAVE_STATS(var,pattern) { \
sprintf(s,pattern,var); \
node.attributes[ #var ].push_back(s); \
}
void StatisticsManager::saveFile(const std::string& fileName){
char s[64];
//update in-game time
updatePlayTime();
ofstream file(fileName.c_str());
if(!file) return;
TreeStorageNode node;
//save statistics
SAVE_STATS(playerTravelingDistance,"%.2f");
SAVE_STATS(shadowTravelingDistance,"%.2f");
SAVE_STATS(playerJumps,"%d");
SAVE_STATS(shadowJumps,"%d");
SAVE_STATS(playerDies,"%d");
SAVE_STATS(shadowDies,"%d");
SAVE_STATS(playerSquashed,"%d");
SAVE_STATS(shadowSquashed,"%d");
SAVE_STATS(recordTimes,"%d");
SAVE_STATS(switchTimes,"%d");
SAVE_STATS(swapTimes,"%d");
SAVE_STATS(saveTimes,"%d");
SAVE_STATS(loadTimes,"%d");
SAVE_STATS(playTime,"%d");
SAVE_STATS(levelEditTime,"%d");
SAVE_STATS(createdLevels,"%d");
//save achievements.
//format is: name;time,name;time,...
{
vector<string>& v=node.attributes["achievements"];
for(map<string,OwnedAchievement>::iterator it=achievements.begin();it!=achievements.end();++it){
stringstream strm;
char s[32];
long long n=it->second.achievedTime;
sprintf(s,PRINTF_LONGLONG,n);
strm<<it->first<<";"<<s;
v.push_back(strm.str());
}
}
POASerializer serializer;
serializer.writeNode(&node,file,true,true);
}
void StatisticsManager::loadPicture(SDL_Renderer& renderer, ImageManager& imageManager){
//Load drop shadow picture
bmDropShadow=imageManager.loadTexture(getDataPath()+"gfx/dropshadow.png", renderer);
bmQuestionMark=imageManager.loadImage(getDataPath()+"gfx/menu/questionmark.png");
}
int StatisticsManager::getTotalAchievements() const {
return avaliableAchievements.size();
}
int StatisticsManager::getCurrentNumberOfAchievements() const {
return achievements.size();
}
void StatisticsManager::registerAchievements(ImageManager& imageManager){
if(!avaliableAchievements.empty()) return;
for(int i=0;achievementList[i].id!=NULL;i++){
avaliableAchievements[achievementList[i].id]=&achievementList[i];
if(achievementList[i].imageFile!=NULL){
achievementList[i].imageSurface = imageManager.loadImage(getDataPath()+achievementList[i].imageFile);
}
}
}
void StatisticsManager::render(ImageManager&,SDL_Renderer &renderer){
if(achievementTime==0 && !bmAchievement && currentAchievement<(int)queuedAchievements.size()){
//create surface
bmAchievement=createAchievementSurface(renderer, queuedAchievements[currentAchievement++]);
//FIXME: Draw the box.
//drawGUIBox(0,0,bmAchievement->w,bmAchievement->h,bmAchievement,0xFFFFFF00);
//check if queue is empty
if(currentAchievement>=(int)queuedAchievements.size()){
queuedAchievements.clear();
currentAchievement=0;
}
//play a sound
getSoundManager()->playSound("achievement", 1, false, 32);
}
//check if we need to display achievements
if(bmAchievement){
achievementTime++;
if(achievementTime<=0){
return;
}else if(achievementTime<=5){
drawAchievement(renderer,achievementTime);
}else if(achievementTime<=achievementDisplayTime-5){
drawAchievement(renderer,5);
}else if(achievementTime<achievementDisplayTime){
drawAchievement(renderer,achievementDisplayTime-achievementTime);
}else if(achievementTime>=achievementIntervalTime){
if(bmAchievement){
bmAchievement.reset();
}
achievementTime=0;
}
}
}
void StatisticsManager::newAchievement(const std::string& id,bool save){
//check avaliable achievements
map<string,AchievementInfo*>::iterator it=avaliableAchievements.find(id);
if(it==avaliableAchievements.end()) return;
//check if already have this achievement
if(save){
map<string,OwnedAchievement>::iterator it2=achievements.find(id);
if(it2!=achievements.end()) return;
OwnedAchievement ach={time(NULL),it->second};
achievements[id]=ach;
}
//add it to queue
queuedAchievements.push_back(it->second);
}
time_t StatisticsManager::achievedTime(const std::string& id) {
auto it = achievements.find(id);
if (it == achievements.end()) return 0;
else return it->second.achievedTime;
}
float StatisticsManager::getAchievementProgress(AchievementInfo* info){
if(!strcmp(info->id,"experienced")){
return float(completedLevels)/50.0f*100.0f;
}
if(!strcmp(info->id,"expert")){
return float(goldLevels)/50.0f*100.0f;
}
if(!strcmp(info->id,"tutorial")){
if(tutorialLevels>0)
return float(tutorialCompleted)/float(tutorialLevels)*100.0f;
else
return 0.0f;
}
if(!strcmp(info->id,"tutorialGold")){
if(tutorialLevels>0)
return float(tutorialGold)/float(tutorialLevels)*100.0f;
else
return 0.0f;
}
if(!strcmp(info->id,"create10")){
return float(createdLevels)/10.0f*100.0f;
}
if (!strcmp(info->id, "jump100")){
return float(playerJumps + shadowJumps) / 100.0f*100.0f;
}
if (!strcmp(info->id, "jump1k")){
return float(playerJumps + shadowJumps) / 1000.0f*100.0f;
}
if(!strcmp(info->id,"die50")){
return float(playerDies+shadowDies)/50.0f*100.0f;
}
if(!strcmp(info->id,"die1000")){
return float(playerDies+shadowDies)/1000.0f*100.0f;
}
if(!strcmp(info->id,"suqash50")){
return float(playerSquashed+shadowSquashed)/50.0f*100.0f;
}
if(!strcmp(info->id,"travel100")){
return (playerTravelingDistance+shadowTravelingDistance)/100.0f*100.0f;
}
if(!strcmp(info->id,"travel1k")){
return (playerTravelingDistance+shadowTravelingDistance)/1000.0f*100.0f;
}
if(!strcmp(info->id,"travel10k")){
return (playerTravelingDistance+shadowTravelingDistance)/10000.0f*100.0f;
}
if(!strcmp(info->id,"travel42k")){
return (playerTravelingDistance+shadowTravelingDistance)/42195.0f*100.0f;
}
if(!strcmp(info->id,"record100")){
return float(recordTimes)/100.0f*100.0f;
}
if(!strcmp(info->id,"record1k")){
return float(recordTimes)/1000.0f*100.0f;
}
if(!strcmp(info->id,"switch100")){
return float(switchTimes)/100.0f*100.0f;
}
if(!strcmp(info->id,"switch1k")){
return float(switchTimes)/1000.0f*100.0f;
}
if(!strcmp(info->id,"swap100")){
return float(swapTimes)/100.0f*100.0f;
}
//not found
return 0.0f;
}
SharedTexture StatisticsManager::createAchievementSurface(SDL_Renderer& renderer, AchievementInfo* info,SDL_Rect* rect,bool showTip,const time_t *achievedTime){
if(info==NULL || info->id==NULL) return NULL;
//prepare text
SurfacePtr title0(nullptr);
SurfacePtr title1(nullptr);
vector<SDL_Surface*> descSurfaces;
SDL_Color fg = objThemes.getTextColor(true);
int fontHeight=TTF_FontLineSkip(fontText);
bool showDescription=false;
bool showImage=false;
float achievementProgress=0.0f;
if(showTip){
title0.reset(TTF_RenderUTF8_Blended(fontText,_("New achievement:"),fg));
title1.reset(TTF_RenderUTF8_Blended(fontGUISmall,_(info->name),fg));
showDescription=showImage=true;
}else if(achievedTime){
char s[256];
strftime(s,sizeof(s),"%c",localtime(achievedTime));
title0.reset(TTF_RenderUTF8_Blended(fontGUISmall,_(info->name),fg));
title1.reset(TTF_RenderUTF8_Blended(fontText, tfm::format(_("Achieved on %s"), (char*)s).c_str(), fg));
showDescription=showImage=true;
}else if(info->displayStyle==ACHIEVEMENT_HIDDEN){
title0.reset(TTF_RenderUTF8_Blended(fontGUISmall,_("Unknown achievement"),fg));
}else{
if(info->displayStyle==ACHIEVEMENT_PROGRESS){
achievementProgress=getAchievementProgress(info);
title1.reset(TTF_RenderUTF8_Blended(fontText, tfm::format(_("Achieved %1.0f%%"), achievementProgress).c_str(), fg));
}else{
title1.reset(TTF_RenderUTF8_Blended(fontText,_("Not achieved"),fg));
}
title0.reset(TTF_RenderUTF8_Blended(fontGUISmall,_(info->name),fg));
showDescription= info->displayStyle==ACHIEVEMENT_ALL || info->displayStyle==ACHIEVEMENT_PROGRESS;
showImage=true;
}
//calculate the size
int w=0,h=0,w1=8,h1=0;
if(title0!=NULL){
if(title0->w>w) w=title0->w;
h1+=title0->h;
}
if(title1!=NULL){
if(title1->w>w) w=title1->w;
h1+=title1->h;
/*//calc progress bar size
if(!showTip && !achievedTime && info->displayStyle==ACHIEVEMENT_PROGRESS){
h1+=4;
}*/
}
const int preferredImageWidth = 50;
const int preferredImageHeight = 50;
if(showImage){
if(info->imageSurface!=NULL){
// NEW: we have the preferred image size
const int width = std::max(info->r.w, preferredImageWidth);
const int height = std::max(info->r.h, preferredImageHeight);
w1+=width+8;
w+=width+8;
if(height>h1) h1=height;
}
}else{
w1+=bmQuestionMark->w+8;
w+=bmQuestionMark->w+8;
if(bmQuestionMark->h>h1) h1=bmQuestionMark->h;
}
//we render the description here since we need to know the width of title
if (info->description != NULL && showDescription){
string description = _(info->description);
WordWrapper wrapper;
wrapper.font = fontText;
wrapper.maxWidth = showTip ? std::max(int(SCREEN_WIDTH * 0.5f), w) : (rect ? (rect->w - 16) : -1);
wrapper.wordWrap = wrapper.maxWidth > 0;
wrapper.hyphen = "-";
vector<string> lines;
wrapper.addString(lines, description);
int start, end;
const int m = lines.size();
for (start = 0; start < m; start++) {
if (!lines[start].empty()) break;
}
for (end = m - 1; end >= start; end--) {
if (!lines[end].empty()) break;
}
for (int i = start; i <= end; i++) {
if (lines[i].empty()) {
descSurfaces.push_back(TTF_RenderUTF8_Blended(fontText, " ", fg));
} else {
descSurfaces.push_back(TTF_RenderUTF8_Blended(fontText, lines[i].c_str(), fg));
}
}
}
h=h1+8;
for(unsigned int i=0;i<descSurfaces.size();i++){
if(descSurfaces[i]!=NULL){
if(descSurfaces[i]->w>w) w=descSurfaces[i]->w;
}
}
h+=descSurfaces.size()*fontHeight;
w+=16;
h+=16;
//check if size is specified
int left=0,top=0;
if(rect!=NULL){
//NOTE: SDL2 port. This was never used.
/* if(surface!=NULL){
left=rect->x;
top=rect->y;
}*/
if(rect->w>0) w=rect->w;
else rect->w=w;
rect->h=h;
}
//create surface if necessary
SurfacePtr surface = createSurface(w, h);
std::unique_ptr<SDL_Renderer,decltype(&SDL_DestroyRenderer)> surfaceRenderer(
SDL_CreateSoftwareRenderer(surface.get()), &SDL_DestroyRenderer);
//draw background
const SDL_Rect r={left,top,w,h};
if(showTip || achievedTime){
SDL_FillRect(surface.get(),&r,SDL_MapRGB(surface->format,255,255,255));
}else{
SDL_FillRect(surface.get(),&r,SDL_MapRGB(surface->format,192,192,192));
}
//draw horizontal separator
//FIXME: this is moved from StatisticsScreen::createGUI
if (!showTip) {
const SDL_Rect r0 = { left, top, w, 1 };
const SDL_Rect r1 = { left, top + h - 2, w, 1 };
const SDL_Rect r2 = { left, top + h - 1, w, 1 };
Uint32 c0 = achievedTime ? SDL_MapRGB(surface->format, 224, 224, 224) : SDL_MapRGB(surface->format, 168, 168, 168);
Uint32 c2 = achievedTime ? SDL_MapRGB(surface->format, 128, 128, 128) : SDL_MapRGB(surface->format, 96, 96, 96);
SDL_FillRect(surface.get(), &r0, c0);
SDL_FillRect(surface.get(), &r1, c0);
SDL_FillRect(surface.get(), &r2, c2);
}
//draw picture
if(showImage){
if(info->imageSurface){
// NEW: we have the preferred image size
SDL_Rect r={left+8,top+8+(h1-info->r.h)/2,0,0};
if (info->r.w < preferredImageWidth) r.x += (preferredImageWidth - info->r.w) / 2;
SDL_BlitSurface(info->imageSurface,&info->r,surface.get(),&r);
}
}else{
SDL_Rect r={left+8,top+8+(h1-bmQuestionMark->h)/2,0,0};
SDL_BlitSurface(bmQuestionMark,NULL,surface.get(),&r);
}
//draw text
h=8;
if(title0){
SDL_Rect r={left+w1,top+h,0,0};
SDL_BlitSurface(title0.get(),NULL,surface.get(),&r);
h+=title0->h;
}
if(title1){
SDL_Rect r={left+w1,top+h,0,0};
//Draw progress bar.
if(!showTip && !achievedTime && info->displayStyle==ACHIEVEMENT_PROGRESS){
//Draw borders.
SDL_Rect r1={r.x,r.y,w-8-r.x,title1->h};
drawGUIBox(r1.x,r1.y,r1.w,r1.h,*surfaceRenderer,0x1D);
//Draw progress.
r1.x++;
r1.y++;
r1.w=int(achievementProgress/100.0f*float(r1.w-2)+0.5f);
r1.h-=2;
SDL_SetRenderDrawColor(surfaceRenderer.get(),0,0,0,100);
SDL_RenderFillRect(surfaceRenderer.get(),&r1);
//shift the text a little bit (???)
r.x+=2;
r.y+=2;
}
//Draw text.
SDL_BlitSurface(title1.get(),NULL,surface.get(),&r);
}
h=h1+16;
for(unsigned int i=0;i<descSurfaces.size();i++){
if(descSurfaces[i]!=NULL){
SDL_Rect r={left+8,top+h+static_cast<int>(i)*fontHeight,0,0};
SDL_BlitSurface(descSurfaces[i],NULL,surface.get(),&r);
}
}
//clean up
for(unsigned int i=0;i<descSurfaces.size();i++){
if(descSurfaces[i]!=NULL){
SDL_FreeSurface(descSurfaces[i]);
}
}
//FIXME: Should we clear the vector here?
//over
return textureFromSurface(renderer, std::move(surface));
}
void StatisticsManager::drawAchievement(SDL_Renderer& renderer,int alpha){
if(!bmAchievement || alpha<=0) {
return;
}
if(alpha>5) alpha=5;
SDL_Rect r = rectFromTexture(*bmAchievement);
int w=0,h=0;
SDL_GetRendererOutputSize(&renderer, &w, &h);
r.x = w-32-r.w;
r.y = 32;
SDL_SetTextureAlphaMod(bmAchievement.get(), alpha*40);
applyTexture(r.x, r.y,bmAchievement, renderer);
if(!bmDropShadow) {
return;
}
//draw drop shadow - corner
{
int w1=r.w/2,w2=r.w-w1,h1=r.h/2,h2=r.h-h1;
if(w1>16) w1=16;
if(w2>16) w2=16;
if(h1>16) h1=16;
if(h2>16) h2=16;
const int x=(5-alpha)*64;
//top-left
SDL_Rect r1={x,0,w1+16,h1+16};//),r2={r.x-16,r.y-16,0,0};
SDL_Rect r2 ={r.x-16, r.y-16, r1.w, r1.h};
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//top-right
r1.x=x+48-w2;r2.w=r1.w =w2+16;r2.x=r.x+r.w-w2;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom-right
r1.y=48-h2;r2.h=r1.h=h2+16;r2.y=r.y+r.h-h2;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom-left
r1.x=x;r2.w=r1.w=w1+16;r2.x=r.x-16;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
}
//draw drop shadow - border
int i=r.w-32;
while(i>0){
const int ii=i>128?128:i;
//top
SDL_Rect r1={0,256-alpha*16,ii,16};
SDL_Rect r2={r.x+r.w-16-i,r.y-16,r1.w,r1.h};
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom
r1.x=128;r2.y=r.y+r.h;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
i-=ii;
}
i=r.h-32;
while(i>0){
const int ii=i>128?128:i;
//top
SDL_Rect r1={512-alpha*16,0,16,ii};
SDL_Rect r2={r.x-16,r.y+r.h-16-i, r1.w, r1.h};
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
//bottom
r1.y=128;r2.x=r.x+r.w;
SDL_RenderCopy(&renderer, bmDropShadow.get(), &r1, &r2);
i-=ii;
}
}
void StatisticsManager::reloadCompletedLevelsAndAchievements(){
- completedLevels=silverLevels=goldLevels=0;
+ completedLevels=silverLevels=goldLevels=totalLevels
+ =completedLevelpacks=silverLevelpacks=goldLevelpacks=totalLevelpacks=0;
+
+ completedLevelsByCategory.fill(0);
+ silverLevelsByCategory.fill(0);
+ goldLevelsByCategory.fill(0);
+ totalLevelsByCategory.fill(0);
+ completedLevelpacksByCategory.fill(0);
+ silverLevelpacksByCategory.fill(0);
+ goldLevelpacksByCategory.fill(0);
+ totalLevelpacksByCategory.fill(0);
LevelPackManager *lpm=getLevelPackManager();
vector<pair<string,string> > v=lpm->enumLevelPacks();
- bool tutorial=false,tutorialIsGold=false;
+ bool tutorialFinished=false,tutorialIsGold=false;
for(unsigned int i=0;i<v.size();i++){
string& s=v[i].first;
LevelPack *levels=lpm->getLevelPack(s);
levels->loadProgress();
- bool b=false;
+ int category = (int)levels->type;
+ if (category > CUSTOM) category = CUSTOM;
+
+ int packMedal = 3;
+
+ bool isTutorial=false;
if(s==lpm->tutorialLevelPackPath){
tutorialLevels=levels->getLevelCount();
tutorialCompleted=tutorialGold=0;
- b=tutorial=tutorialIsGold=true;
+ isTutorial=true;
}
for(int n=0,m=levels->getLevelCount();n<m;n++){
- LevelPack::Level *lv=levels->getLevel(n);
- int medal=lv->won;
+ int medal = levels->getLevel(n)->getMedal();
+ if (packMedal > medal) packMedal = medal;
if(medal){
- if(lv->targetTime<0 || lv->time<=lv->targetTime)
- medal++;
- if(lv->targetRecordings<0 || lv->recordings<=lv->targetRecordings)
- medal++;
-
completedLevels++;
- if(b) tutorialCompleted++;
- if(medal==2) silverLevels++;
- if(medal==3){
+ completedLevelsByCategory[category]++;
+ if(isTutorial) tutorialCompleted++;
+ if (medal == 2) {
+ silverLevels++;
+ silverLevelsByCategory[category]++;
+ } else if (medal == 3) {
goldLevels++;
- if(b) tutorialGold++;
+ goldLevelsByCategory[category]++;
+ if (isTutorial) tutorialGold++;
}
+ }
- if(medal!=3 && b) tutorialIsGold=false;
- }else if(b){
- tutorial=tutorialIsGold=false;
+ totalLevels++;
+ totalLevelsByCategory[category]++;
+ }
+
+ if (isTutorial) {
+ tutorialFinished = packMedal > 0;
+ tutorialIsGold = packMedal == 3;
+ }
+
+ if (levels->type != COLLECTION) {
+ if (packMedal) {
+ completedLevelpacks++;
+ completedLevelpacksByCategory[category]++;
+ if (packMedal == 2) {
+ silverLevelpacks++;
+ silverLevelpacksByCategory[category]++;
+ } else if (packMedal == 3) {
+ goldLevelpacks++;
+ goldLevelpacksByCategory[category]++;
+ }
}
+ totalLevelpacks++;
+ totalLevelpacksByCategory[category]++;
}
}
//upadte achievements
updateLevelAchievements();
- updateTutorialAchievementsInternal((tutorial?1:0)|(tutorialIsGold?2:0));
+ updateTutorialAchievementsInternal((tutorialFinished?1:0)|(tutorialIsGold?2:0));
}
void StatisticsManager::reloadOtherAchievements(){
int i;
if(playTime>=7200) newAchievement("addicted");
if(playTime>=86400) newAchievement("loyalFan");
if(levelEditTime>=7200) newAchievement("constructor");
if(levelEditTime>=28800) newAchievement("constructor2");
if(createdLevels>=1) newAchievement("create1");
if(createdLevels>=10) newAchievement("create10");
i=playerJumps+shadowJumps;
if (i >= 100) newAchievement("jump100");
if (i >= 1000) newAchievement("jump1k");
i=playerDies+shadowDies;
if(i>=1) newAchievement("die1");
if(i>=50) newAchievement("die50");
if(i>=1000) newAchievement("die1000");
i=playerSquashed+shadowSquashed;
if(i>=1) newAchievement("squash1");
if(i>=50) newAchievement("squash50");
float d=playerTravelingDistance+shadowTravelingDistance;
if(d>=100.0f) newAchievement("travel100");
if(d>=1000.0f) newAchievement("travel1k");
if(d>=10000.0f) newAchievement("travel10k");
if(d>=42195.0f) newAchievement("travel42k");
if(recordTimes>=100) newAchievement("record100");
if(recordTimes>=1000) newAchievement("record1k");
if(switchTimes>=100) newAchievement("switch100");
if(switchTimes>=1000) newAchievement("switch1k");
if(swapTimes>=100) newAchievement("swap100");
if(saveTimes>=100) newAchievement("save100");
if(loadTimes>=100) newAchievement("load100");
if (collectibleCollected >= 100) newAchievement("collect100");
if (collectibleCollected >= 1000) newAchievement("collect1k");
if (version.find("Development") != string::npos
|| version.find("Alpha") != string::npos
|| version.find("Beta") != string::npos
|| version.find("RC") != string::npos
|| version.find("Candidate") != string::npos)
{
newAchievement("programmer");
}
}
//Update level specified achievements.
//Make sure the completed level count is correct.
void StatisticsManager::updateLevelAchievements(){
if(completedLevels>=1) newAchievement("newbie");
if(goldLevels>=1) newAchievement("goodjob");
if(completedLevels>=50) newAchievement("experienced");
if(goldLevels>=50) newAchievement("expert");
}
//Update tutorial specified achievements.
//Make sure the level progress of tutorial is correct.
void StatisticsManager::updateTutorialAchievements(){
//find tutorial level pack
LevelPackManager *lpm=getLevelPackManager();
LevelPack *levels=lpm->getTutorialLevelPack();
if(levels==NULL) return;
- bool tutorial=true,tutorialIsGold=true;
+ bool tutorialFinished=true,tutorialIsGold=true;
tutorialLevels=levels->getLevelCount();
tutorialCompleted=tutorialGold=0;
for(int n=0,m=levels->getLevelCount();n<m;n++){
- LevelPack::Level *lv=levels->getLevel(n);
- int medal=lv->won;
+ int medal = levels->getLevel(n)->getMedal();
if(medal){
- if(lv->targetTime<0 || lv->time<=lv->targetTime)
- medal++;
- if(lv->targetRecordings<0 || lv->recordings<=lv->targetRecordings)
- medal++;
-
tutorialCompleted++;
if(medal!=3) tutorialIsGold=false;
else tutorialGold++;
}else{
- tutorial=tutorialIsGold=false;
- break;
+ tutorialFinished=tutorialIsGold=false;
}
}
//upadte achievements
- updateTutorialAchievementsInternal((tutorial?1:0)|(tutorialIsGold?2:0));
+ updateTutorialAchievementsInternal((tutorialFinished?1:0)|(tutorialIsGold?2:0));
}
//internal function
//flags: a bit-field value indicates which achievements we have.
void StatisticsManager::updateTutorialAchievementsInternal(int flags){
if(flags&1) newAchievement("tutorial");
if(flags&2) newAchievement("tutorialGold");
}
diff --git a/src/StatisticsManager.h b/src/StatisticsManager.h
index f7684d2..465a642 100644
--- a/src/StatisticsManager.h
+++ b/src/StatisticsManager.h
@@ -1,166 +1,173 @@
/*
* Copyright (C) 2012 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef STATISTICSMANAGER_H
#define STATISTICSMANAGER_H
#include <SDL.h>
#include <string>
#include <vector>
+#include <array>
#include <map>
#include <time.h>
#include "Render.h"
#include "AchievementList.h"
struct OwnedAchievement{
time_t achievedTime;
AchievementInfo* info;
};
class StatisticsScreen;
class StatisticsManager{
friend class StatisticsScreen;
public:
//Player and shadow traveling distance (m), 1 block = 1 meter
float playerTravelingDistance,shadowTravelingDistance;
//Player and shadow jumps
int playerJumps,shadowJumps;
//Player and shadow dies
int playerDies,shadowDies;
//Player and shadow squashed
int playerSquashed,shadowSquashed;
//Completed levels. NOTE: this is dynamically calculated, and doesn't save to file.
- int completedLevels,silverLevels,goldLevels;
+ int completedLevels,silverLevels,goldLevels,totalLevels;
+ //Completed levels by category. NOTE: this is dynamically calculated, and doesn't save to file.
+ std::array<int, 3> completedLevelsByCategory, silverLevelsByCategory, goldLevelsByCategory, totalLevelsByCategory;
+ //Completed levelpacks (excluding individual levels). NOTE: this is dynamically calculated, and doesn't save to file.
+ int completedLevelpacks, silverLevelpacks, goldLevelpacks, totalLevelpacks;
+ //Completed levelpacks by category. NOTE: this is dynamically calculated, and doesn't save to file.
+ std::array<int, 3> completedLevelpacksByCategory, silverLevelpacksByCategory, goldLevelpacksByCategory, totalLevelpacksByCategory;
//Record times
int recordTimes;
//number of switched pulled
int switchTimes;
//swap times
int swapTimes;
//save and load times
int saveTimes,loadTimes;
//collectible collected
int collectibleCollected;
//play time (s)
int playTime;
//level edit time (s)
int levelEditTime;
//created levels
int createdLevels;
private:
//current achievement displayed time
int achievementTime;
//some picture
SharedTexture bmDropShadow;
SDL_Surface* bmQuestionMark;
//SDL_Surface for current achievement (excluding drop shadow)
//SDL_Surface *bmAchievement;
SharedTexture bmAchievement;
//currently owned achievements
std::map<std::string,OwnedAchievement> achievements;
//queued achievements for display
std::vector<AchievementInfo*> queuedAchievements;
//currently displayed achievement
int currentAchievement;
//starting time
time_t startTime;
//level edit starting time
time_t levelEditStartTime;
//statistics for tutorial level pack
int tutorialLevels,tutorialCompleted,tutorialGold;
public:
StatisticsManager();
int getTotalAchievements() const;
int getCurrentNumberOfAchievements() const;
//clear the statistics and achievements.
void clear();
//load needed picture
void loadPicture(SDL_Renderer &renderer, ImageManager &imageManager);
//register avaliable achievements
static void registerAchievements(ImageManager& imageManager);
//load statistics file.
void loadFile(const std::string& fileName);
//save statistics file.
void saveFile(const std::string& fileName);
//add or display a new achievement.
//name: the achievement id. if can't find it in avaliable achievement, nothing happens.
//save: if true then save to currently owned achievements. if it already exists in
//currently owned achievements, nothing happens.
//if false then just added it to queue, including duplicated achievements.
void newAchievement(const std::string& id,bool save=true);
//if there are new achievements, draw it on the screen,
//otherwise do nothing.
void render(ImageManager&,SDL_Renderer& renderer);
//get the achieved time of an achievement. 0 means not achieved yet.
time_t achievedTime(const std::string& id);
//Call this function to update completed levels.
//NOTE: Level progress files are reloaded, so it's slow.
void reloadCompletedLevelsAndAchievements();
//Call this function to update other achievements at game startup.
void reloadOtherAchievements();
//Update level specified achievements.
//Make sure the completed level count is correct.
void updateLevelAchievements();
//Update tutorial specified achievements.
//Make sure the level progress of tutorial is correct.
void updateTutorialAchievements();
//Call when level edit is start
void startLevelEdit();
//Call when level edit is end
void endLevelEdit();
//update in-game time
void updatePlayTime();
//create a SharedTexture contains specified achievements or draw to existing surface.
//renderer: renderer to create the texture on.
//info: achievement info.
//(surface: specifies SDL_Surface to draw on. if NULL then new surface will be created.)
//NOTE: Removed this arg for sdl2 port as it was not used anyway.
//rect [in, out, optional]: specifies position and optionally width to draw on. height will be returned.
// if NULL then will be drawn on top-left corner. if surface is NULL then rect->x and rect->y are ignored.
//showTip: shows "New achievement" tip
//achievedTime: if we should show achieved time (and progress bar if AchievementInfo specifies) and when is it.
// NOTE: if showTip=true then this argument does nothing.
//return value: A texture that contains the specified achievements or NULL if any error occured.
SharedTexture createAchievementSurface(SDL_Renderer& renderer, AchievementInfo* info,SDL_Rect* rect=NULL,bool showTip=true,const time_t *achievedTime=NULL);
private:
//internal function
//flags: a bit-field value indicates which achievements we have.
void updateTutorialAchievementsInternal(int flags);
//internal function. alpha should be 1-5, 5 means fully opaque (not really)
void drawAchievement(SDL_Renderer& renderer, int alpha);
//internal function for get progress (in percent, 0-100)
float getAchievementProgress(AchievementInfo* info);
};
extern StatisticsManager statsMgr;
#endif
diff --git a/src/StatisticsScreen.cpp b/src/StatisticsScreen.cpp
index 4bb5e15..740f7ac 100644
--- a/src/StatisticsScreen.cpp
+++ b/src/StatisticsScreen.cpp
@@ -1,429 +1,449 @@
/*
* Copyright (C) 2012 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow 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 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow 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 Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <string>
#include <vector>
#include <map>
#include "StatisticsManager.h"
#include "StatisticsScreen.h"
#include "Globals.h"
#include "Functions.h"
#include "ThemeManager.h"
#include "InputManager.h"
#include "GUIListBox.h"
#include "GUIScrollBar.h"
#include "EasterEggScreen.h"
#include <SDL_ttf_fontfallback.h>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
//GUI events are handled here.
//name: The name of the element that invoked the event.
//obj: Pointer to the object that invoked the event.
//eventType: Integer containing the type of event.
void StatisticsScreen::GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Check what type of event it was.
if(eventType==GUIEventClick){
if(name=="cmdBack"){
//Goto the main menu.
setNextState(STATE_MENU);
}
} else if (eventType == GUIEventChange && name == "lstAchievements") {
//NOTE: This code is only for testing purpose
int index = lists[0]->value;
if (index >= 0) statsMgr.newAchievement(achievementList[index].id, false);
lists[0]->value = -1;
}
}
//Constructor.
StatisticsScreen::StatisticsScreen(ImageManager& imageManager, SDL_Renderer& renderer){
//Update in-game time.
statsMgr.updatePlayTime();
//Render the title.
title = titleTextureFromText(renderer, _("Achievements and Statistics"), objThemes.getTextColor(false), SCREEN_WIDTH);
//Create GUI.
createGUI(imageManager, renderer);
}
//Destructor.
StatisticsScreen::~StatisticsScreen(){
//Delete the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
}
//we are so lazy that we just use height of the first text, ignore the others
#define DRAW_PLAYER_STATISTICS(name,var,fmt) { \
SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,name,objThemes.getTextColor(true))); \
SurfacePtr stats = createSurface(w,surface->h); \
SDL_FillRect(stats.get(),NULL,-1); \
applySurface(4,0,surface.get(),stats.get(),NULL); \
y=surface->h; \
surface.reset(TTF_RenderUTF8_Blended(fontText, \
tfm::format(fmt,statsMgr.player##var+statsMgr.shadow##var).c_str(), \
objThemes.getTextColor(true))); \
- applySurface(w-260-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
+ applySurface(w-260-surface->w,(y-surface->h),surface.get(),stats.get(),NULL); \
surface.reset(TTF_RenderUTF8_Blended(fontText, \
tfm::format(fmt,statsMgr.player##var).c_str(), \
objThemes.getTextColor(true))); \
- applySurface(w-140-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
+ applySurface(w-140-surface->w,(y-surface->h),surface.get(),stats.get(),NULL); \
surface.reset(TTF_RenderUTF8_Blended(fontText, \
tfm::format(fmt,statsMgr.shadow##var).c_str(), \
objThemes.getTextColor(true))); \
- applySurface(w-20-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL); \
+ applySurface(w-20-surface->w,(y-surface->h),surface.get(),stats.get(),NULL); \
list->addItem(renderer,"",textureFromSurface(renderer, std::move(stats))); /* add it to list box */ \
}
//Add an item to the listbox, that displays "name1", and "var1" formatted with "format"
//we are so lazy that we just use height of the first text, ignore the others
template <class T1>
static void drawMiscStatistics1(SDL_Renderer& renderer, int w,GUIListBox *list,const char* name1,const T1 var1,const char* format1){
//create new surface
SurfacePtr nameSurface(TTF_RenderUTF8_Blended(fontGUISmall,name1,objThemes.getTextColor(true)));
SurfacePtr stats=createSurface(w, nameSurface->h);
SDL_FillRect(stats.get(),NULL,-1);
applySurface(4,0,nameSurface.get(),stats.get(),NULL);
const int x=nameSurface->w+8;
const int y=nameSurface->h;
SurfacePtr formatSurface(TTF_RenderUTF8_Blended(fontText,
tfm::format(format1,var1).c_str(),
objThemes.getTextColor(true)));
//NOTE: SDL2 port. Not halving the y value here as this ends up looking better.
applySurface(x,y-formatSurface->h,formatSurface.get(),stats.get(),NULL);
//add it to list box
list->addItem(renderer, "",textureFromSurface(renderer, std::move(stats)));
//over
//return stats;
}
//NOTE: Disabled this for the SDL2 port for now. It looks a bit off anyhow.
//Might want to make a more general method that draws as many "cells" as there is space.
//Draws two stats on one line if there is space.
//we are so lazy that we just use height of the first text, ignore the others
/*template <class T1,class T2>
static void drawMiscStatistics2(int w,GUIListBox *list,const char* name1,const T1 var1,const char* format1,const char* name2,const T2 var2,const char* format2){
SDL_Surface* stats=drawMiscStatistics1(w,list,name1,var1,format1);
//Check if the width is enough
if(w>=800){
//draw name
SDL_Surface* surface=TTF_RenderUTF8_Blended(fontGUISmall,name2,objThemes.getTextColor(true));
applySurface(w/2-8,stats->h-surface->h,surface,stats,NULL);
int x=surface->w+w/2;
SDL_FreeSurface(surface);
//draw value
char s[1024];
//FIXME: Use tfm::format instead of sprintf to enable locale support
FIXME_sprintf(s,format2,var2);
surface=TTF_RenderUTF8_Blended(fontText,s,objThemes.getTextColor(true));
applySurface(x,(stats->h-surface->h)/2,surface,stats,NULL);
SDL_FreeSurface(surface);
}else{
//Split into two rows
drawMiscStatistics1(w,list,name2,var2,format2);
}
}*/
void StatisticsScreen::addAchievements(ImageManager& imageManager, SDL_Renderer &renderer, GUIListBox *list, bool revealUnknownAchievements) {
for (int idx = 0; achievementList[idx].id != NULL; ++idx) {
time_t *lpt = NULL;
map<string, OwnedAchievement>::iterator it = statsMgr.achievements.find(achievementList[idx].id);
if (it != statsMgr.achievements.end()) {
lpt = &it->second.achievedTime;
}
AchievementInfo info = achievementList[idx];
if (revealUnknownAchievements) {
if (info.displayStyle == ACHIEVEMENT_HIDDEN || info.displayStyle == ACHIEVEMENT_TITLE) {
info.displayStyle = ACHIEVEMENT_ALL;
}
}
SDL_Rect r;
r.x = r.y = 0;
r.w = list->width - 16;
auto surface = statsMgr.createAchievementSurface(renderer, &info, &r, false, lpt);
if (surface){
list->addItem(renderer, "", surface);
}
}
}
//Method that will create the GUI.
void StatisticsScreen::createGUI(ImageManager& imageManager, SDL_Renderer &renderer){
//Create the root element of the GUI.
if(GUIObjectRoot){
delete GUIObjectRoot;
GUIObjectRoot=NULL;
}
GUIObjectRoot=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
//Create back button.
GUIObject* obj=new GUIButton(imageManager,renderer,SCREEN_WIDTH*0.5,SCREEN_HEIGHT-60,-1,36,_("Back"),0,true,true,GUIGravityCenter);
obj->name="cmdBack";
obj->eventCallback=this;
GUIObjectRoot->addChild(obj);
//Create list box.
listBox=new GUISingleLineListBox(imageManager,renderer,(SCREEN_WIDTH-500)/2,104,500,32);
listBox->addItem(_("Achievements"));
listBox->addItem(_("Statistics"));
listBox->value=0;
GUIObjectRoot->addChild(listBox);
//Create list box for achievements.
GUIListBox *list=new GUIListBox(imageManager,renderer,64,150,SCREEN_WIDTH-128,SCREEN_HEIGHT-150-72);
list->name = "lstAchievements"; // debug only
list->eventCallback = this; // debug only
list->selectable=false;
GUIObjectRoot->addChild(list);
lists.clear();
lists.push_back(list);
addAchievements(imageManager, renderer, list);
//Now create list box for statistics.
list=new GUIListBox(imageManager,renderer,64,150,SCREEN_WIDTH-128,SCREEN_HEIGHT-150-72,true,false);
list->selectable=false;
GUIObjectRoot->addChild(list);
lists.push_back(list);
//Load needed pictures.
//FIXME: hard-coded image path
//TODO: Might want to consider not caching these as most other stuff use textures now.
SDL_Surface* bmPlayer=imageManager.loadImage(getDataPath()+"themes/Cloudscape/characters/player.png");
SDL_Surface* bmShadow=imageManager.loadImage(getDataPath()+"themes/Cloudscape/characters/shadow.png");
SDL_Surface* bmMedal=imageManager.loadImage(getDataPath()+"gfx/medals.png");
SDL_Rect r;
int x,y,w=SCREEN_WIDTH-128;
SharedTexture h_bar = [&](){
//The horizontal bar.
SurfacePtr h_bar(createSurface(w,2));
SDL_Color c = objThemes.getTextColor(true);
Uint32 clr=SDL_MapRGB(h_bar->format,c.r,c.g,c.b);
SDL_FillRect(h_bar.get(),NULL,clr);
return textureFromSurface(renderer, std::move(h_bar));
}();
//Player and shadow specific statistics
//The header.
{
SurfacePtr stats = createSurface(w, 44);
SDL_FillRect(stats.get(),NULL,-1);
SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,_("Total"),objThemes.getTextColor(true)));
applySurface(w-260-surface->w,stats->h-surface->h,surface.get(),stats.get(),NULL);
//FIXME: hard-coded player and shadow images
r.x=0;r.y=0;r.w=23;r.h=40;
applySurface(w-140-r.w,stats.get()->h-40,bmPlayer,stats.get(),&r);
applySurface(w-20-r.w,stats.get()->h-40,bmShadow,stats.get(),&r);
list->addItem(renderer, "",textureFromSurface(renderer, std::move(stats)));
}
//Each items.
{
DRAW_PLAYER_STATISTICS(_("Traveling distance (m)"),TravelingDistance,"%0.1f");
DRAW_PLAYER_STATISTICS(_("Jump times"),Jumps,"%d");
DRAW_PLAYER_STATISTICS(_("Die times"),Dies,"%d");
DRAW_PLAYER_STATISTICS(_("Squashed times"),Squashed,"%d");
}
//Game specific statistics.
list->addItem(renderer, "",h_bar);
auto drawMiscStats = [&](const char* name1,const int var1,const char* format1) {
drawMiscStatistics1(renderer, w, list, name1, var1, format1);
};
drawMiscStats(_("Recordings:"),statsMgr.recordTimes,"%d");
drawMiscStats(_("Switch pulled times:"),statsMgr.switchTimes,"%d");
drawMiscStats(_("Swap times:"),statsMgr.swapTimes,"%d");
drawMiscStats(_("Save times:"),statsMgr.saveTimes,"%d");
drawMiscStats(_("Load times:"),statsMgr.loadTimes,"%d");
drawMiscStats(_("Collectibles collected:"), statsMgr.collectibleCollected, "%d");
//Level specific statistics
list->addItem(renderer, "",h_bar);
- {
- SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,_("Completed levels:"),objThemes.getTextColor(true)));
+
+ auto drawLevelStats = [&](const char* name1, int completed, int total, int gold, int silver) {
+ SurfacePtr surface(TTF_RenderUTF8_Blended(fontGUISmall,name1,objThemes.getTextColor(true)));
SurfacePtr stats = createSurface(w, surface->h);
SDL_FillRect(stats.get(),NULL,-1);
applySurface(4,0,surface.get(),stats.get(),NULL);
x=surface->w+8;
y=surface->h;
surface.reset(TTF_RenderUTF8_Blended(fontText,
- tfm::format("%d", statsMgr.completedLevels).c_str(),
+ tfm::format("%d/%d", completed, total).c_str(),
objThemes.getTextColor(true)));
applySurface(x,(y-surface->h),surface.get(),stats.get(),NULL);
surface.reset(TTF_RenderUTF8_Blended(fontText,
- tfm::format("%d", statsMgr.completedLevels - statsMgr.goldLevels - statsMgr.silverLevels).c_str(),
+ tfm::format("%d", completed - gold - silver).c_str(),
objThemes.getTextColor(true)));
- applySurface(w-260-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
+ applySurface(w-260-surface->w,(y-surface->h),surface.get(),stats.get(),NULL);
r.x=0;r.y=0;r.w=30;r.h=30;
applySurface(w-260-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
surface.reset(TTF_RenderUTF8_Blended(fontText,
- tfm::format("%d", statsMgr.silverLevels).c_str(),
+ tfm::format("%d", silver).c_str(),
objThemes.getTextColor(true)));
- applySurface(w-140-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
+ applySurface(w-140-surface->w,(y-surface->h),surface.get(),stats.get(),NULL);
r.x+=30;
applySurface(w-140-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
surface.reset(TTF_RenderUTF8_Blended(fontText,
- tfm::format("%d", statsMgr.goldLevels).c_str(),
+ tfm::format("%d", gold).c_str(),
objThemes.getTextColor(true)));
- applySurface(w-20-surface->w,(y-surface->h)/2,surface.get(),stats.get(),NULL);
+ applySurface(w-20-surface->w,(y-surface->h),surface.get(),stats.get(),NULL);
r.x+=30;
applySurface(w-20-surface->w-30,(y-30)/2,bmMedal,stats.get(),&r);
list->addItem(renderer,"",textureFromSurface(renderer, std::move(stats)));
- }
+ };
+
+ drawLevelStats(_("Completed levels:"), statsMgr.completedLevels, statsMgr.totalLevels, statsMgr.goldLevels, statsMgr.silverLevels);
+ drawLevelStats(_("Official levels:"), statsMgr.completedLevelsByCategory[MAIN],
+ statsMgr.totalLevelsByCategory[MAIN], statsMgr.goldLevelsByCategory[MAIN], statsMgr.silverLevelsByCategory[MAIN]);
+ drawLevelStats(_("Addon levels:"), statsMgr.completedLevelsByCategory[ADDON],
+ statsMgr.totalLevelsByCategory[ADDON], statsMgr.goldLevelsByCategory[ADDON], statsMgr.silverLevelsByCategory[ADDON]);
+ drawLevelStats(_("Custom levels:"), statsMgr.completedLevelsByCategory[CUSTOM],
+ statsMgr.totalLevelsByCategory[CUSTOM], statsMgr.goldLevelsByCategory[CUSTOM], statsMgr.silverLevelsByCategory[CUSTOM]);
+
+ //Levelpack specific statistics
+ list->addItem(renderer, "", h_bar);
+
+ drawLevelStats(_("Completed levelpacks:"), statsMgr.completedLevelpacks, statsMgr.totalLevelpacks, statsMgr.goldLevelpacks, statsMgr.silverLevelpacks);
+ drawLevelStats(_("Official levelpacks:"), statsMgr.completedLevelpacksByCategory[MAIN],
+ statsMgr.totalLevelpacksByCategory[MAIN], statsMgr.goldLevelpacksByCategory[MAIN], statsMgr.silverLevelpacksByCategory[MAIN]);
+ drawLevelStats(_("Addon levelpacks:"), statsMgr.completedLevelpacksByCategory[ADDON],
+ statsMgr.totalLevelpacksByCategory[ADDON], statsMgr.goldLevelpacksByCategory[ADDON], statsMgr.silverLevelpacksByCategory[ADDON]);
+ drawLevelStats(_("Custom levelpacks:"), statsMgr.completedLevelpacksByCategory[CUSTOM],
+ statsMgr.totalLevelpacksByCategory[CUSTOM], statsMgr.goldLevelpacksByCategory[CUSTOM], statsMgr.silverLevelpacksByCategory[CUSTOM]);
//Other statistics.
list->addItem(renderer, "",h_bar);
drawMiscStatistics1(renderer,w,list,_("In-game time:"),
tfm::format("%02d:%02d:%02d", statsMgr.playTime / 3600, (statsMgr.playTime / 60) % 60, statsMgr.playTime % 60),
"%s");
drawMiscStatistics1(renderer,w,list,_("Level editing time:"),
tfm::format("%02d:%02d:%02d", statsMgr.levelEditTime / 3600, (statsMgr.levelEditTime / 60) % 60, statsMgr.levelEditTime % 60),
"%s");
drawMiscStats(_("Created levels:"),statsMgr.createdLevels,"%d");
drawMiscStatistics1(renderer, w, list, _("Achievement achieved:"),
tfm::format("%d/%d", statsMgr.getCurrentNumberOfAchievements(), statsMgr.getTotalAchievements()),
"%s");
}
//In this method all the key and mouse events should be handled.
//NOTE: The GUIEvents won't be handled here.
void StatisticsScreen::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer){
//Check if we need to quit, if so enter the exit state.
if(event.type==SDL_QUIT){
setNextState(STATE_EXIT);
}
//Check horizontal movement
int value = listBox->value;
if (inputMgr.isKeyDownEvent(INPUTMGR_RIGHT)){
isKeyboardOnly = true;
value++;
if (value >= (int)listBox->item.size()) value = 0;
} else if (inputMgr.isKeyDownEvent(INPUTMGR_LEFT)){
isKeyboardOnly = true;
value--;
if (value < 0) value = listBox->item.size() - 1;
}
listBox->value = value;
//Check vertical movement
if (value >= 0 && value < (int)lists.size()) {
if (inputMgr.isKeyDownEvent(INPUTMGR_UP)){
isKeyboardOnly = true;
lists[value]->scrollScrollbar(-1);
} else if (inputMgr.isKeyDownEvent(INPUTMGR_DOWN)){
isKeyboardOnly = true;
lists[value]->scrollScrollbar(1);
}
}
//Yet another cheat "ls -la" which reveals all unknown achievements
static char input[6];
static int inputLen = 0;
if (value == 0) {
if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym >= 32 && event.key.keysym.sym <= 126) {
if (inputLen < sizeof(input)) input[inputLen] = event.key.keysym.sym;
inputLen++;
} else {
if (event.key.keysym.sym == SDLK_RETURN && inputLen == 6 &&
input[0] == 'l' && input[1] == 's' && input[2] == ' ' && input[3] == '-' && input[4] == 'l' && input[5] == 'a')
{
if (easterEggScreen(imageManager, renderer)) {
//new achievement
statsMgr.newAchievement("cheat");
//reload achievement list with hidden achievements revealed
lists[0]->clearItems();
lists[0]->selectable = true; // debug only
addAchievements(imageManager, renderer, lists[0], true);
}
}
inputLen = 0;
}
}
} else {
inputLen = 0;
}
//Check if the escape button is pressed, if so go back to the main menu.
if(inputMgr.isKeyDownEvent(INPUTMGR_ESCAPE)){
setNextState(STATE_MENU);
}
}
//All the logic that needs to be done should go in this method.
void StatisticsScreen::logic(ImageManager&, SDL_Renderer&){
}
//This method handles all the rendering.
void StatisticsScreen::render(ImageManager&, SDL_Renderer& renderer){
//Draw background.
objThemes.getBackground(true)->draw(renderer);
objThemes.getBackground(true)->updateAnimation();
//Draw title.
drawTitleTexture(SCREEN_WIDTH, *title, renderer);
//Draw statistics.
int value=listBox->value;
for(unsigned int i=0;i<lists.size();i++){
lists[i]->visible=(i==value);
}
}
//Method that will be called when the screen size has been changed in runtime.
void StatisticsScreen::resize(ImageManager &imageManager, SDL_Renderer &renderer){
//Recreate the gui to fit the new resolution.
createGUI(imageManager, renderer);
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, May 8, 8:24 PM (1 w, 23 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
62737
Default Alt Text
(161 KB)
Attached To
Mode
R79 meandmyshadow
Attached
Detach File
Event Timeline