Page MenuHomePhabricator (Chris)

No OneTemporary

Authored By
Unknown
Size
102 KB
Referenced Files
None
Subscribers
None
diff --git a/data/levelpacks/tutorial/tut01.map b/data/levelpacks/tutorial/tut01.map
index 5e854e7..6631182 100755
--- a/data/levelpacks/tutorial/tut01.map
+++ b/data/levelpacks/tutorial/tut01.map
@@ -1,20 +1,20 @@
name="Walk in the park"
recordings=0
size=800,600
time=60
tile(Block,150,300)
tile(Block,200,300)
tile(Block,250,300)
tile(Block,300,300)
tile(Block,350,300)
tile(Block,400,300)
tile(Block,450,300)
tile(Block,500,300)
tile(Block,550,300)
tile(Block,600,300)
tile(ShadowStart,200,250)
tile(PlayerStart,250,250)
tile(NotificationBlock,300,250){
- message="Welcome to Me and My Shadow.\nYou can use the arrow keys to walk to the exit.\n\nGood luck!"
+ message="Welcome to Me and My Shadow.\nYou can use the {{{key_left}}} key\nand {{{key_right}}} key to walk to the exit.\n\nGood luck!"
}
tile(Exit,550,250)
diff --git a/data/levelpacks/tutorial/tut02.map b/data/levelpacks/tutorial/tut02.map
index ca81650..cfcf200 100755
--- a/data/levelpacks/tutorial/tut02.map
+++ b/data/levelpacks/tutorial/tut02.map
@@ -1,23 +1,23 @@
name="First jumps"
size=800,600
time=80
recordings=0
tile(Block,150,300)
tile(Block,200,300)
tile(Block,300,300)
tile(Block,350,300)
tile(Block,400,300)
tile(Block,450,300)
tile(Block,500,300)
tile(Block,600,300)
tile(Block,550,300)
tile(Block,250,300)
tile(ShadowStart,200,250)
tile(PlayerStart,250,250)
tile(NotificationBlock,300,250){
- message="You can jump using the up key.\nTry jumping over these blocks."
+ message="You can jump using the {{{key_jump}}} key.\nTry jumping over these blocks."
}
tile(Block,450,250)
tile(Block,400,250)
tile(Exit,550,250)
tile(Block,450,200)
diff --git a/data/levelpacks/tutorial/tut05.map b/data/levelpacks/tutorial/tut05.map
index eb5f4f5..c1d19bc 100755
--- a/data/levelpacks/tutorial/tut05.map
+++ b/data/levelpacks/tutorial/tut05.map
@@ -1,33 +1,33 @@
name=Shadow
size=800,600
time=120
recordings=1
tile(Block,150,300)
tile(Block,200,300)
tile(Block,250,300)
tile(Block,350,300)
tile(Block,400,300)
tile(Block,450,300)
tile(Block,500,300)
tile(Block,300,300)
tile(Block,550,300)
tile(Block,600,300)
tile(Exit,550,100)
tile(ShadowStart,200,100)
tile(PlayerStart,200,250)
tile(Block,150,150)
tile(Block,200,150)
tile(Block,250,150)
tile(Block,300,150)
tile(Block,350,150)
tile(Block,400,150)
tile(Block,450,150)
tile(Block,550,150)
tile(Block,500,150)
tile(Block,600,150)
tile(NotificationBlock,250,250){
- message="You can't reach the exit, but your shadow can.\nPress space to record your moves.\nPress space once again to let your shadow\nmimic your recording."
+ message="You can't reach the exit, but your shadow can.\nPress {{{key_space}}} to record your moves.\nPress {{{key_space}}} once again to let your shadow\nmimic your recording."
}
tile(NotificationBlock,550,250){
message="TIP:\nThink what moves your shadow has to make.\nThen let your character record those moves.\nYou can break it down into smaller recordings."
}
diff --git a/data/levelpacks/tutorial/tut07.map b/data/levelpacks/tutorial/tut07.map
index b9b3c4a..26c73a9 100755
--- a/data/levelpacks/tutorial/tut07.map
+++ b/data/levelpacks/tutorial/tut07.map
@@ -1,34 +1,34 @@
name="Shadow challenge"
size=800,600
time=120
recordings=1
tile(Block,150,300)
tile(Block,200,300)
tile(Block,250,300)
tile(Block,300,300)
tile(Block,350,300)
tile(Block,400,300)
tile(Block,450,300)
tile(Block,500,300)
tile(Block,550,300)
tile(Block,600,300)
tile(Exit,550,100)
tile(ShadowStart,200,100)
tile(PlayerStart,200,250)
tile(NotificationBlock,250,250){
message="Spikes are not only deadly for you,\nbut also for your shadow."
}
tile(Spikes,400,100)
tile(ShadowBlock,150,150)
tile(ShadowBlock,200,150)
tile(ShadowBlock,250,150)
tile(ShadowBlock,350,150)
tile(ShadowBlock,300,150)
tile(ShadowBlock,400,150)
tile(ShadowBlock,450,150)
tile(ShadowBlock,500,150)
tile(ShadowBlock,550,150)
tile(ShadowBlock,600,150)
tile(NotificationBlock,550,250){
- message="If your shadow dies you'll have to restart.\nRestart the game by pressing the 'R' key."
+ message="If your shadow dies you'll have to restart.\nRestart the game by pressing the {{{key_restart}}} key."
}
diff --git a/data/levelpacks/tutorial/tut10.map b/data/levelpacks/tutorial/tut10.map
index e81924d..0d7954f 100755
--- a/data/levelpacks/tutorial/tut10.map
+++ b/data/levelpacks/tutorial/tut10.map
@@ -1,71 +1,71 @@
name=Checkpoints
recordings=1
size=2900,600
time=260
tile(Block,150,300)
tile(Block,200,300)
tile(Block,250,300)
tile(Block,300,300)
tile(Block,350,300)
tile(Block,400,300)
tile(Block,450,350)
tile(Block,950,450)
tile(Block,1000,300)
tile(Block,1050,300)
tile(Exit,2650,150)
tile(ShadowStart,200,250)
tile(PlayerStart,250,250)
tile(NotificationBlock,300,250){
- message="You can save your progress in a level with\ncheckpoints. You can restore them at any\ntime using the F3 button."
+ message="You can save your progress in a level with\ncheckpoints. You can restore them at any\ntime using the {{{key_load}}} key."
}
tile(Block,500,350)
tile(Block,650,350)
tile(Block,750,350)
tile(Block,700,350)
tile(Block,850,450)
tile(Block,800,250)
tile(Block,850,250)
tile(Block,900,250)
tile(Block,900,450)
tile(Checkpoint,500,150)
tile(NotificationBlock,450,150){
message="Save your progress here."
}
tile(Block,450,200)
tile(Block,500,200)
tile(Block,550,200)
tile(Block,400,200)
tile(Block,1050,500)
tile(Block,1150,500)
tile(Block,1100,500)
tile(Block,1100,300)
tile(Block,1200,250)
tile(Block,1250,250)
tile(Block,1300,250)
tile(NotificationBlock,1250,200){
message="You've chosen the right way."
}
tile(Teleporter,1300,200){
automatic=0
destination=4
id=1
}
tile(NotificationBlock,1100,450){
- message="This is the wrong way.\nGo back to the previous checkpoint by pressing F3."
+ message="This is the wrong way.\nGo back to the previous checkpoint by pressing {{{key_load}}}."
}
tile(Teleporter,1150,450){
automatic=0
id=3
}
tile(Block,2600,250)
tile(Block,2650,250)
tile(Block,2700,250)
tile(Block,2550,250)
tile(Teleporter,2600,200){
automatic=0
destination=1
id=4
}
tile(Block,2650,200)
tile(Block,550,150)
tile(Block,550,100)
diff --git a/data/levelpacks/tutorial/tut18.map b/data/levelpacks/tutorial/tut18.map
index c1b253d..0f8e1d1 100755
--- a/data/levelpacks/tutorial/tut18.map
+++ b/data/levelpacks/tutorial/tut18.map
@@ -1,66 +1,66 @@
name="Stop trigger"
size=1150,600
time=410
recordings=0
tile(Block,150,300)
tile(Block,200,300)
tile(Block,250,300)
tile(Block,300,300)
tile(Block,350,300)
tile(Block,400,300)
tile(ShadowStart,200,250)
tile(PlayerStart,250,250)
tile(NotificationBlock,300,250){
- message="This trigger will deactivate the moving block.\nTry to stop it at the right moment.\nYou can only do this once, so if it fails you'll\nhave to reset the level with the 'R' key."
+ message="This trigger will deactivate the moving block.\nTry to stop it at the right moment.\nYou can only do this once, so if it fails you'll\nhave to reset the level with the {{{key_restart}}} key."
}
tile(ConveyorBelt,500,300){
disabled=0
speed=-5
}
tile(Block,850,300)
tile(Exit,900,250)
tile(ConveyorBelt,550,300){
disabled=0
speed=-5
}
tile(ConveyorBelt,600,300){
disabled=0
speed=-5
}
tile(ConveyorBelt,650,300){
disabled=0
speed=-5
}
tile(ConveyorBelt,700,300){
disabled=0
speed=-5
}
tile(MovingBlock,800,250){
MovingPosCount=2
disabled=0
id=8
loop=1
t0=120
t1=120
x0=-300
x1=0
y0=0
y1=0
}
tile(ConveyorBelt,750,300){
disabled=0
speed=-5
}
tile(Spikes,500,200)
tile(Spikes,550,200)
tile(Spikes,600,200)
tile(Spikes,650,200)
tile(Spikes,700,200)
tile(Switch,400,250){
behaviour=off
id=8
}
tile(Block,450,300)
tile(Block,800,300)
tile(Block,900,300)
tile(Block,950,300)
diff --git a/data/levelpacks/tutorial/tut19.map b/data/levelpacks/tutorial/tut19.map
index 056293b..8c09a8f 100755
--- a/data/levelpacks/tutorial/tut19.map
+++ b/data/levelpacks/tutorial/tut19.map
@@ -1,43 +1,43 @@
name="First portals"
recordings=0
size=800,600
time=100
tile(Block,150,300)
tile(Block,200,300)
tile(Block,250,300)
tile(Block,300,300)
tile(Block,350,300)
tile(Block,400,300)
tile(Block,450,300)
tile(Block,500,300)
tile(Block,550,300)
tile(Block,600,300)
tile(ShadowStart,200,250)
tile(PlayerStart,250,250)
tile(Teleporter,550,250){
automatic=0
destination=2
id=1
}
tile(Teleporter,200,100){
automatic=1
destination=1
id=2
}
tile(Block,150,150)
tile(Block,200,150)
tile(Block,250,150)
tile(Block,300,150)
tile(Block,350,150)
tile(Block,400,150)
tile(Block,450,150)
tile(Block,550,150)
tile(Block,500,150)
tile(Block,600,150)
tile(Exit,550,100)
tile(NotificationBlock,300,250){
- message="Now it's time to check out the portals.\nTo get to the exit you'll have to take the portal.\nWalk to it and press the down arrow to\nactivate."
+ message="Now it's time to check out the portals.\nTo get to the exit you'll have to take the portal.\nWalk to it and press the {{{key_action}}} key to\nactivate."
}
tile(NotificationBlock,300,100){
- message="NOTE:\nYou can go back by entering this portal.\nIt is however a bit different, you don't have to\npress the down key, it will activate when you walk in it."
+ message="NOTE:\nYou can go back by entering this portal.\nIt is however a bit different, you don't have to\npress the {{{key_action}}} key, it will activate when you walk in it."
}
diff --git a/data/levelpacks/tutorial/tut21.map b/data/levelpacks/tutorial/tut21.map
index 62d27df..47fbfca 100755
--- a/data/levelpacks/tutorial/tut21.map
+++ b/data/levelpacks/tutorial/tut21.map
@@ -1,50 +1,50 @@
name=Swappoints
recordings=1
size=1100,700
time=200
tile(ShadowStart,350,250)
tile(PlayerStart,350,450)
tile(NotificationBlock,400,450){
message="Now it's time for something completely\ndifferent: swappoints. When you or your\nshadow activate them you'll swap places."
}
tile(ShadowBlock,400,300)
tile(ShadowBlock,450,300)
tile(ShadowBlock,500,300)
tile(ShadowBlock,550,300)
tile(ShadowBlock,600,300)
tile(ShadowBlock,650,300)
tile(Block,700,300)
tile(Block,350,500)
tile(Block,400,500)
tile(Block,450,500)
tile(Block,500,500)
tile(Block,550,500)
tile(Block,600,500)
tile(Block,650,500)
tile(Block,700,500)
tile(Block,750,450)
tile(Block,750,350)
tile(Block,750,400)
tile(Block,750,300)
tile(Block,800,300)
tile(Block,850,300)
tile(Block,900,300)
tile(Swap,600,450)
tile(ShadowBlock,750,250)
tile(ShadowBlock,750,200)
tile(ShadowBlock,750,150)
tile(Exit,850,250)
tile(ShadowBlock,350,300)
tile(ShadowBlock,300,300)
tile(ShadowBlock,200,300)
tile(ShadowBlock,250,300)
tile(Block,300,500)
tile(Block,200,500)
tile(Block,250,500)
tile(Block,200,350)
tile(Block,200,450)
tile(Block,200,400)
tile(Block,750,500)
tile(NotificationBlock,700,450){
- message="If your shadow falls down here you will have to restart.\nPress 'R' to restart the level."
+ message="If your shadow falls down here you will have to restart.\nPress {{{key_restart}}} to restart the level."
}
diff --git a/data/levelpacks/tutorial/tut22.map b/data/levelpacks/tutorial/tut22.map
index ef7f0df..c5ae3df 100755
--- a/data/levelpacks/tutorial/tut22.map
+++ b/data/levelpacks/tutorial/tut22.map
@@ -1,42 +1,42 @@
name="Shadow swap"
size=950,650
time=210
recordings=1
tile(Block,150,350)
tile(Block,200,350)
tile(Block,250,350)
tile(Block,300,350)
tile(Block,350,350)
tile(Block,400,350)
tile(Block,450,350)
tile(Block,500,350)
tile(Block,550,350)
tile(Block,600,350)
tile(Block,150,50)
tile(Block,150,100)
tile(Block,250,100)
tile(Block,250,50)
tile(ShadowStart,200,50)
tile(PlayerStart,200,300)
tile(NotificationBlock,250,300){
message="You need your shadow to reach the exit.\nMake use of the swapper to get him down (or \nto get yourself down)."
}
tile(ShadowBlock,200,100)
tile(ShadowBlock,350,300)
tile(ShadowBlock,350,250)
tile(ShadowBlock,400,250)
tile(ShadowBlock,450,250)
tile(ShadowBlock,450,300)
tile(Swap,400,300)
tile(Block,650,350)
tile(NotificationBlock,550,300){
- message="TIP:\nWhen your shadow is trapped stand on the\nright side of the shadow blocks. Now record \nthe down key and let your shadow mimic."
+ message="TIP:\nWhen your shadow is trapped stand on the\nright side of the shadow blocks. Now record \nthe {{{key_action}}} key and let your shadow mimic."
}
tile(Block,650,300)
tile(Block,650,250)
tile(Exit,700,200)
tile(Block,700,250)
tile(Block,750,250)
tile(Block,150,0)
tile(Block,200,0)
tile(Block,250,0)
diff --git a/src/Game.cpp b/src/Game.cpp
index c437ff9..6d25d5d 100644
--- a/src/Game.cpp
+++ b/src/Game.cpp
@@ -1,1960 +1,1997 @@
/*
* 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 "GameObjects.h"
#include "ThemeManager.h"
#include "Game.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "InputManager.h"
#include "MusicManager.h"
#include "Render.h"
#include "StatisticsManager.h"
#include "ScriptExecutor.h"
#include "MD5.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>
#include <map>
#include <algorithm>
#include <assert.h>
#include <stdio.h>
#include <SDL_ttf.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 }), levelRectSaved(SDL_Rect{ 0, 0, 0, 0 }), levelRectInitial(SDL_Rect{ 0, 0, 0, 0 })
,won(false)
,interlevel(false)
,gameTipIndex(0)
,time(0),timeSaved(0)
,recordings(0),recordingsSaved(0)
,cameraMode(CAMERA_PLAYER),cameraModeSaved(CAMERA_PLAYER)
,player(this),shadow(this),objLastCheckPoint(NULL)
, currentCollectables(0), currentCollectablesSaved(0), currentCollectablesInitial(0)
, totalCollectables(0), totalCollectablesSaved(0), totalCollectablesInitial(0)
{
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);
}
void Game::destroy(){
delete scriptExecutor;
scriptExecutor = 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();
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){
//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 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){
//Check if the layer exists.
if (sceneryLayers[obj1->value[0]] == NULL) {
sceneryLayers[obj1->value[0]] = new SceneryLayer();
}
//Load contents from node.
sceneryLayers[obj1->value[0]]->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;
//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"]);
}
bmTips[0]=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);
}
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);
//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();
}
void Game::loadRecord(ImageManager& imageManager, SDL_Renderer& renderer, const char* fileName){
//Create a TreeStorageNode that will hold the loaded data.
TreeStorageNode obj;
{
POASerializer objSerializer;
//Parse the file.
if(!objSerializer.loadNodeFromFile(fileName,&obj,true)){
cerr<<"ERROR: Can't load record file "<<fileName<<endl;
return;
}
}
//Load the seed of psuedo-random number generator.
prngSeed.clear();
{
auto it = obj.attributes.find("seed");
if (it != obj.attributes.end() && !it->second.empty()) {
prngSeed = it->second[0];
}
}
//find the node named 'map'.
bool loaded=false;
for(unsigned int i=0;i<obj.subNodes.size();i++){
if(obj.subNodes[i]->name=="map"){
//load the level. (fileName=???)
loadLevelFromNode(imageManager,renderer,obj.subNodes[i],"?record?");
//remove this node to prevent delete it.
obj.subNodes[i]=NULL;
//over
loaded=true;
break;
}
}
if(!loaded){
cerr<<"ERROR: Can't find subnode named 'map' from record file"<<endl;
return;
}
//load the record.
{
vector<int> *record=player.getRecord();
record->clear();
vector<string> &v=obj.attributes["record"];
for(unsigned int i=0;i<v.size();i++){
string &s=v[i];
string::size_type pos=s.find_first_of('*');
if(pos==string::npos){
//1 item only.
int i=atoi(s.c_str());
record->push_back(i);
}else{
//contains many items.
int i=atoi(s.substr(0,pos).c_str());
int j=atoi(s.substr(pos+1).c_str());
for(;j>0;j--){
record->push_back(i);
}
}
}
}
#ifdef RECORD_FILE_DEBUG
//load the debug data
{
vector<string> &v=obj.attributes["recordPlayerPosition"];
vector<SDL_Rect> &playerPosition=player.playerPosition();
playerPosition.clear();
if(!v.empty()){
if(!v[0].empty()){
stringstream st(v[0]);
int m;
st>>m;
for(int i=0;i<m;i++){
SDL_Rect r;
st>>r.x>>r.y;
r.w=0;
r.h=0;
playerPosition.push_back(r);
}
}
}
}
#endif
//play the record.
//TODO: tell the level manager don't save the level progress.
player.playRecord();
shadow.playRecord(); //???
}
/////////////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.
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){
//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.
if(saveStateNextTime){
saveState();
}else if(loadStateNextTime){
loadState();
}
saveStateNextTime=false;
loadStateNextTime=false;
//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();
}
//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);
//Update the camera.
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;
}
//Check if we won.
if(won){
//Check if it's playing from record
if(player.isPlayFromRecord() && !interlevel){
recordingEnded(imageManager,renderer);
}else{
//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();
//Now check if we should update statistics
{
//Get previous and current medal
int oldMedal=level->won?1:0,newMedal=1;
int bestTime=level->time;
int targetTime=level->targetTime;
int bestRecordings=level->recordings;
int targetRecordings=level->targetRecordings;
if(oldMedal){
if(bestTime>=0 && (targetTime<0 || bestTime<=targetTime))
oldMedal++;
if(bestRecordings>=0 && (targetRecordings<0 || bestRecordings<=targetRecordings))
oldMedal++;
}else{
bestTime=time;
bestRecordings=recordings;
}
if(bestTime<0 || bestTime>time) bestTime=time;
if(bestRecordings<0 || bestRecordings>recordings) bestRecordings=recordings;
if(targetTime<0 || bestTime<=targetTime)
newMedal++;
if(targetRecordings<0 || bestRecordings<=targetRecordings)
newMedal++;
//Check if we need to update statistics
if(newMedal>oldMedal){
switch(oldMedal){
case 0:
statsMgr.completedLevels++;
break;
case 2:
statsMgr.silverLevels--;
break;
}
switch(newMedal){
case 2:
statsMgr.silverLevels++;
break;
case 3:
statsMgr.goldLevels++;
break;
}
}
}
//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==-1 || level->time>time){
level->time=time;
//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==-1 || level->recordings>recordings){
level->recordings=recordings;
//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
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;
}
}
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;
}
/////////////////RENDER//////////////////
void Game::render(ImageManager&,SDL_Renderer &renderer){
//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);
//And if it's the loaded background then also update the animation.
//FIXME: Updating the animation in the render method?
if(bg==background)
bg->updateAnimation();
}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 && bmTips[0]!=NULL && !interlevel){
withTexture(*bmTips[0], [&](SDL_Rect r){
drawGUIBox(-2,SCREEN_HEIGHT-r.h-4,r.w+8,r.h+6,renderer,0xFFFFFFFF);
applyTexture(2,SCREEN_HEIGHT-r.h,*bmTips[0],renderer,NULL);
});
}
//Check if there's a tooltip.
//NOTE: gameTipIndex 0 is used for the levelName, 1 for shadow death, 2 for restart text, 3 for restart+checkpoint.
if(gameTipIndex>3 && gameTipIndex<TYPE_MAX){
//Check if there's a tooltip for the type.
if(bmTips[gameTipIndex]==NULL){
//There isn't thus make it.
string s;
string keyCode = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_ACTION, false), inputMgr.getKeyCode(INPUTMGR_ACTION, true));
switch(gameTipIndex){
case TYPE_CHECKPOINT:
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
s=tfm::format(_("Press %s key to save the game."),keyCode);
break;
case TYPE_SWAP:
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
s=tfm::format(_("Press %s key to swap the position of player and shadow."),keyCode);
break;
case TYPE_SWITCH:
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
s=tfm::format(_("Press %s key to activate the switch."),keyCode);
break;
case TYPE_PORTAL:
/// TRANSLATORS: Please do not remove %s from your translation:
/// - %s will be replaced with current action key
s=tfm::format(_("Press %s key to teleport."),keyCode);
break;
}
//If we have a string then it's a supported GameObject type.
if(!s.empty()){
bmTips[gameTipIndex]=textureFromText(renderer, *fontText, s.c_str(), objThemes.getTextColor(true));
}
}
//We already have a gameTip for this type so draw it.
if(bmTips[gameTipIndex]!=NULL){
withTexture(*bmTips[gameTipIndex], [&](SDL_Rect r){
drawGUIBox(-2,-2,r.w+8,r.h+6,renderer,0xFFFFFFFF);
applyTexture(2,2,*bmTips[gameTipIndex],renderer);
});
}
}
//Set the gameTip to 0.
gameTipIndex=0;
// 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, meaning we draw a message.
if(player.dead){
//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(bmTips[3]==NULL){
//Get user defined key for loading checkpoint
string keyCodeLoad = InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_LOAD, false), inputMgr.getKeyCode(INPUTMGR_LOAD, true));
//Draw string
bmTips[3]=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=bmTips[3].get();
}else{
//Now check if the tip is already made, if not make it.
if(bmTips[2]==NULL){
bmTips[2]=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=bmTips[2].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(bmTips[1]==NULL){
bmTips[1]=textureFromText(renderer, *fontText,
_("Your shadow has died."),
objThemes.getTextColor(true));
}
bm=bmTips[1].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);
}
}
//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 && time>0){
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);
}
//show time and records used in level editor or during replay.
if((stateID==STATE_LEVEL_EDITOR || (!interlevel && player.isPlayFromRecord())) && time>0){
const SDL_Color fg=objThemes.getTextColor(true),bg={255,255,255,255};
const int alpha = 160;
if (recordingsTexture.needsUpdate(recordings)) {
recordingsTexture.update(recordings,
textureFromTextShaded(
renderer,
*fontText,
tfm::format(ngettext("%d recording","%d recordings",recordings).c_str(),recordings).c_str(),
fg,
bg
));
SDL_SetTextureAlphaMod(recordingsTexture.get(),alpha);
}
int y=SCREEN_HEIGHT - textureHeight(*recordingsTexture.get());
if (stateID != STATE_LEVEL_EDITOR && bmTips[0] != NULL && !interlevel) {
y -= textureHeight(bmTips[0]) + 4;
}
applyTexture(0,y,*recordingsTexture.get(), renderer);
if(timeTexture.needsUpdate(time)) {
const size_t len = 32;
timeTexture.update(time,
textureFromTextShaded(
renderer,
*fontText,
tfm::format("%-.2fs", time / 40.0).c_str(),
fg,
bg
));
}
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((time & 0x10)==0x10){
// FIXME: replace this ugly ad-hoc animation by a better one
const SDL_Rect r={50,0,50,50};
applyTexture(0,0,*action,renderer,&r);
//applyTexture(0,SCREEN_HEIGHT-50,*action,renderer,&r);
//applyTexture(SCREEN_WIDTH-50,SCREEN_HEIGHT-50,*action,renderer,&r);
}
}else if(auto blockId = player.objNotificationBlock.get()){
//If the player is in front of a notification block show the message.
//And it isn't a replay.
//Check if we need to update the notification message texture.
int maxWidth = 0;
int y = 20;
//We check against blockId rather than the full message, as blockId is most likely shorter.
if(notificationTexture.needsUpdate(blockId)) {
const std::string &untranslated_message=blockId->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);
+ }
+
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);
}
}
//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 x = 0;
TTF_SizeUTF8(fontText, string_data[i].c_str(), &x, NULL);
//Find out largest width
if (x>maxWidth)
maxWidth = x;
lines.emplace_back(TTF_RenderUTF8_Blended(fontText, string_data[i].c_str(), objThemes.getTextColor(true)));
//Increase y with 25, about the height of the text.
y += 25;
}
maxWidth+=SCREEN_WIDTH*0.15;
SurfacePtr surf = createSurface(maxWidth, y);
int y1 = y;
for(SurfacePtr &s : lines) {
if(s) {
applySurface((surf->w-s->w)/2,surf->h - y1,s.get(),surf.get(),NULL);
}
y1 -= 25;
}
notificationTexture.update(blockId, textureUniqueFromSurface(renderer,std::move(surf)));
} else {
auto texSize = rectFromTexture(*notificationTexture.get());
maxWidth=texSize.w;
y=texSize.h;
}
drawGUIBox((SCREEN_WIDTH-maxWidth)/2,SCREEN_HEIGHT-y-25,maxWidth,y+20,renderer,0xFFFFFFBF);
applyTexture((SCREEN_WIDTH-maxWidth)/2,SCREEN_HEIGHT-y,notificationTexture.getTexture(),renderer);
}
}
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;
//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=1;
if(bestTime>=0 && (targetTime<0 || bestTime<=targetTime))
medal++;
if(bestRecordings>=0 && (targetRecordings<0 || bestRecordings<=targetRecordings))
medal++;
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,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(_("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(_("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;
}
}
void Game::recordingEnded(ImageManager& imageManager, SDL_Renderer& renderer){
//Check if it's a normal replay, if so just stop.
if(!interlevel){
//Show the cursor so that the user can press the ok button.
SDL_ShowCursor(SDL_ENABLE);
//Now show the message box.
msgBox(imageManager,renderer,_("Game replay is done."),MsgBoxOKOnly,_("Game Replay"));
//Go to the level select menu.
setNextState(STATE_LEVEL_SELECT);
//And change the music back to the menu music.
getMusicManager()->playMusic("menu");
}else{
//Instead of directly replaying we set won true to let the Game handle the replaying at the end of the update cycle.
won=true;
}
}
bool Game::canSaveState(){
return (player.canSaveState() && shadow.canSaveState());
}
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 stats.
timeSaved=time;
recordingsSaved=recordings;
recentSwapSaved=recentSwap;
//Save the PRNG and seed.
prngSaved = prng;
prngSeedSaved = prngSeed;
//Save the level size.
levelRectSaved = levelRect;
//Save the camera mode and target.
cameraModeSaved=cameraMode;
cameraTargetSaved=cameraTarget;
//Save the current collectables
currentCollectablesSaved = currentCollectables;
totalCollectablesSaved = totalCollectables;
//Save scripts.
copyCompiledScripts(getScriptExecutor()->getLuaState(), compiledScripts, savedCompiledScripts);
//Save other state, for example moving blocks.
copyLevelObjects(levelObjects, levelObjectsSave, false);
//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 1000:
statsMgr.newAchievement("save1k");
break;
}
}
//Save the state for script executor.
getScriptExecutor()->saveState();
//Execute the onSave event.
executeScript(LevelEvent_OnSave);
//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()){
//Let the player and the shadow load their state.
player.loadState();
shadow.loadState();
//Load the stats.
time=timeSaved;
recordings=recordingsSaved;
recentSwap=recentSwapSaved;
//Load the PRNG and seed.
prng = prngSaved;
prngSeed = prngSeedSaved;
//Load the level size.
levelRect = levelRectSaved;
//Load the camera mode and target.
cameraMode=cameraModeSaved;
cameraTarget=cameraTargetSaved;
//Load the current collactbles
currentCollectables = currentCollectablesSaved;
totalCollectables = totalCollectablesSaved;
//Load scripts.
copyCompiledScripts(getScriptExecutor()->getLuaState(), savedCompiledScripts, compiledScripts);
//Load other state, for example moving blocks.
copyLevelObjects(levelObjectsSave, levelObjects, true);
//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 1000:
statsMgr.newAchievement("load1k");
break;
}
}
//Load the state for script executor.
getScriptExecutor()->loadState();
//Execute the onLoad event, if any.
executeScript(LevelEvent_OnLoad);
//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 if interlevel isn't true.
if(!interlevel){
time=0;
recordings=0;
}
recentSwap=-10000;
if(save) recentSwapSaved=-10000;
//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 create a new random seed!"
"This may breaks the behavior pseudo-random number generator in script!" << endl;
prngSeed = createNewSeed();
} else {
#ifdef _DEBUG
cout << "Use existing PRNG seed: " << prngSeed << endl;
#endif
}
} else {
prngSeed = createNewSeed();
#ifdef _DEBUG
cout << "Create new PRNG seed: " << prngSeed << endl;
#endif
}
prng.seed(std::seed_seq(prngSeed.begin(), prngSeed.end()));
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();
//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);
//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);
}
//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;
//Call the level's onCreate event.
executeScript(LevelEvent_OnCreate);
//Send GameObjectEvent_OnCreate event to the script
for (auto block : levelObjects) {
block->onEvent(GameObjectEvent_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/InputManager.cpp b/src/InputManager.cpp
index d5a2ae9..9de37bd 100644
--- a/src/InputManager.cpp
+++ b/src/InputManager.cpp
@@ -1,601 +1,610 @@
/*
* Copyright (C) 2011-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 "InputManager.h"
#include "Globals.h"
#include "Settings.h"
#include "Functions.h"
#include "GUIObject.h"
#include "GUIListBox.h"
#include <stdlib.h>
#include <stdio.h>
#include <string>
#include <iostream>
#include "libs/tinyformat/tinyformat.h"
using namespace std;
InputManager inputMgr;
//the order must be the same as InputManagerKeys
static const char* keySettingNames[INPUTMGR_MAX]={
"key_up","key_down","key_left","key_right","key_jump","key_action","key_space","key_cancelRecording",
"key_escape","key_restart","key_tab","key_save","key_load","key_swap",
"key_teleport","key_suicide","key_shift","key_next","key_previous","key_select"
};
//the order must be the same as InputManagerKeys
static const char* keySettingDescription[INPUTMGR_MAX]={
__("Up (in menu)"),__("Down (in menu)"),__("Left"),__("Right"),__("Jump"),__("Action"),__("Space (Record)"),__("Cancel recording"),
__("Escape"),__("Restart"),__("Tab (View shadow/Level prop.)"),__("Save game (in editor)"),__("Load game"),__("Swap (in editor)"),
__("Teleport (in editor)"),__("Suicide (in editor)"),__("Shift (in editor)"),__("Next block type (in Editor)"),
__("Previous block type (in editor)"), __("Select (in menu)")
};
InputManagerKeyCode::InputManagerKeyCode(int sym_, int mod_)
: type(KEYBOARD), sym(sym_), mod(mod_)
{
// normalize a bit
if (sym == 0) {
mod = 0;
} else {
mod = ((mod & KMOD_CTRL) ? KMOD_CTRL : 0)
| ((mod & KMOD_ALT) ? KMOD_ALT : 0)
| ((mod & KMOD_SHIFT) ? KMOD_SHIFT : 0);
if (sym == SDLK_LCTRL || sym == SDLK_RCTRL) mod &= ~KMOD_CTRL;
if (sym == SDLK_LALT || sym == SDLK_RALT) mod &= ~KMOD_ALT;
if (sym == SDLK_LSHIFT || sym == SDLK_RSHIFT) mod &= ~KMOD_SHIFT;
}
}
InputManagerKeyCode::InputManagerKeyCode(InputType type_, int buttonIndex_, int buttonValue_)
: type(type_), buttonIndex(buttonIndex_), buttonValue(buttonValue_)
{
}
InputManagerKeyCode InputManagerKeyCode::createFromString(const std::string& s) {
int i, j, lp;
if (s.find("JoystickAxis;") == 0) {
lp = s.find(';');
if (sscanf(s.c_str() + (lp + 1), "%d;%d", &i, &j) == 2) {
return InputManagerKeyCode(JOYSTICK_AXIS, i, j);
}
} else if (s.find("JoystickButton;") == 0) {
lp = s.find(';');
if (sscanf(s.c_str() + (lp + 1), "%d", &i) == 1) {
return InputManagerKeyCode(JOYSTICK_BUTTON, i, 0);
}
} else if (s.find("JoystickHat;") == 0) {
lp = s.find(';');
if (sscanf(s.c_str() + (lp + 1), "%d;%d", &i, &j) == 2) {
return InputManagerKeyCode(JOYSTICK_HAT, i, j);
}
} else {
i = atoi(s.c_str());
j = 0;
if (i) {
if (s.find(";Ctrl") != std::string::npos) j |= KMOD_CTRL;
if (s.find(";Alt") != std::string::npos) j |= KMOD_ALT;
if (s.find(";Shift") != std::string::npos) j |= KMOD_SHIFT;
}
return InputManagerKeyCode(i, j);
}
fprintf(stderr, "ERROR: Can't parse '%s' as InputManagerKeyCode\n", s.c_str());
return InputManagerKeyCode();
}
std::string InputManagerKeyCode::toString() const {
std::ostringstream str;
switch (type) {
default:
if (sym == 0) {
return std::string();
} else {
str << sym;
if (mod & KMOD_CTRL) str << ";Ctrl";
if (mod & KMOD_ALT) str << ";Alt";
if (mod & KMOD_SHIFT) str << ";Shift";
return str.str();
}
break;
case JOYSTICK_AXIS:
str << "JoystickAxis;" << buttonIndex << ";" << buttonValue;
return str.str();
break;
case JOYSTICK_BUTTON:
str << "JoystickButton;" << buttonIndex;
return str.str();
break;
case JOYSTICK_HAT:
str << "JoystickHat;" << buttonIndex << ";" << buttonValue;
return str.str();
break;
}
}
std::string InputManagerKeyCode::describe() const {
switch (type) {
default:
if (sym == 0) {
return std::string();
} else {
std::ostringstream str;
if (mod & KMOD_CTRL) str << "CTRL+";
if (mod & KMOD_ALT) str << "ALT+";
if (mod & KMOD_SHIFT) str << "SHIFT+";
const char* s = SDL_GetKeyName(sym);
if (s != NULL && s[0] != '\0'){
std::string keyCode = s;
std::transform(keyCode.begin(), keyCode.end(), keyCode.begin(), [](char c)->char {
if (c >= 'a' && c <= 'z') {
return c + ('A' - 'a');
} else {
return c;
}
});
str << (dictionaryManager != NULL ? dictionaryManager->get_dictionary().translate_ctxt("keys", keyCode) : keyCode);
} else{
/// TRANSLAOTRS: This is used when the name of the key code is not found.
str << tfm::format(_("(KEY %d)"), sym);
}
return str.str();
}
break;
case JOYSTICK_AXIS:
return tfm::format(_("JOYSTICK AXIS %d %s"), buttonIndex, buttonValue > 0 ? "+" : "-");
break;
case JOYSTICK_BUTTON:
return tfm::format(_("JOYSTICK BUTTON %d"), buttonIndex);
break;
case JOYSTICK_HAT:
switch (buttonValue){
case SDL_HAT_LEFT:
return tfm::format(_("JOYSTICK HAT %d LEFT"), buttonIndex);
break;
case SDL_HAT_RIGHT:
return tfm::format(_("JOYSTICK HAT %d RIGHT"), buttonIndex);
break;
case SDL_HAT_UP:
return tfm::format(_("JOYSTICK HAT %d UP"), buttonIndex);
break;
case SDL_HAT_DOWN:
return tfm::format(_("JOYSTICK HAT %d DOWN"), buttonIndex);
break;
default:
fprintf(stderr, "ERROR: Invalid joystick hat value %d\n", buttonValue);
/// TRANSLAOTRS: This is used when the JOYSTICK_HAT value is invalid.
return tfm::format(_("JOYSTICK HAT %d %d"), buttonIndex, buttonValue);
break;
}
break;
}
}
bool InputManagerKeyCode::empty() const {
return type == KEYBOARD && sym == 0;
}
std::string InputManagerKeyCode::describeTwo(const InputManagerKeyCode& keyCode, const InputManagerKeyCode& keyCodeAlt) {
std::string s = keyCode.describe();
if (!keyCodeAlt.empty()) {
if (!keyCode.empty()) {
s += " ";
s += _("OR");
s += " ";
}
s += keyCodeAlt.describe();
}
return s;
}
bool InputManagerKeyCode::operator == (const InputManagerKeyCode& rhs) const {
return type == rhs.type && sym == rhs.sym && ((type == KEYBOARD && sym == 0) || mod == rhs.mod);
}
bool InputManagerKeyCode::contains(const InputManagerKeyCode& rhs) const {
if (type == KEYBOARD && rhs.type == KEYBOARD) {
if ((mod & KMOD_CTRL) == 0 && (rhs.mod & KMOD_CTRL) != 0) return false;
if ((mod & KMOD_ALT) == 0 && (rhs.mod & KMOD_ALT) != 0) return false;
if ((mod & KMOD_SHIFT) == 0 && (rhs.mod & KMOD_SHIFT) != 0) return false;
if ((mod & KMOD_CTRL) != 0 && (rhs.mod & KMOD_CTRL) == 0) {
if (rhs.sym == SDLK_LCTRL || rhs.sym == SDLK_RCTRL) return true;
}
if ((mod & KMOD_ALT) != 0 && (rhs.mod & KMOD_ALT) == 0) {
if (rhs.sym == SDLK_LALT || rhs.sym == SDLK_RALT) return true;
}
if ((mod & KMOD_SHIFT) != 0 && (rhs.mod & KMOD_SHIFT) == 0) {
if (rhs.sym == SDLK_LSHIFT || rhs.sym == SDLK_RSHIFT) return true;
}
}
return false;
}
//A class that handles the gui events of the inputDialog.
class InputDialogHandler:public GUIEventCallback{
private:
//the list box which contains keys.
GUIListBox* listBox;
//the parent object.
InputManager* parent;
//update specified key config item
void updateConfigItem(SDL_Renderer& renderer,int index){
//Get the description of the key.
std::string s=_(keySettingDescription[index]);
s+=": ";
//Get the key code name.
s += InputManagerKeyCode::describeTwo(
parent->getKeyCode((InputManagerKeys)index, false),
parent->getKeyCode((InputManagerKeys)index, true)
);
//Update item.
listBox->updateItem(renderer, index, s);
}
public:
//Constructor.
InputDialogHandler(SDL_Renderer& renderer,GUIListBox* listBox,InputManager* parent):listBox(listBox),parent(parent){
//load the available keys to the list box.
for(int i=0;i<INPUTMGR_MAX;i++){
//Get the description of the key.
std::string s=_(keySettingDescription[i]);
s+=": ";
//Get key code name.
s += InputManagerKeyCode::describeTwo(
parent->getKeyCode((InputManagerKeys)i, false),
parent->getKeyCode((InputManagerKeys)i, true)
);
//Add item.
listBox->addItem(renderer, s);
}
}
virtual ~InputDialogHandler(){}
//When a key is pressed call this to set the key to currently-selected item.
void onKeyDown(SDL_Renderer& renderer,const InputManagerKeyCode& keyCode){
//Check if an item is selected.
int index=listBox->value;
if(index<0 || index>=INPUTMGR_MAX) return;
//Get the existing keys.
auto key = parent->getKeyCode((InputManagerKeys)index, false);
auto altKey = parent->getKeyCode((InputManagerKeys)index, true);
//SDLK_BACKSPACE will erase the last key if there are keys.
if (keyCode == InputManagerKeyCode(SDLK_BACKSPACE) && (!key.empty() || !altKey.empty())) {
if (!altKey.empty()) {
parent->setKeyCode((InputManagerKeys)index, InputManagerKeyCode(), true);
} else {
parent->setKeyCode((InputManagerKeys)index, InputManagerKeyCode(), false);
}
updateConfigItem(renderer, index);
} else if (keyCode == key || keyCode == altKey) {
//Do nothing if keyCode is equal to one of the existing keys.
} else if (key.empty() || (altKey.empty() && keyCode.contains(key))) {
//Update the main key if there isn't one.
parent->setKeyCode((InputManagerKeys)index, keyCode, false);
updateConfigItem(renderer, index);
} else if (altKey.empty() || keyCode.contains(altKey)) {
//Otherwise update the alternative key if there isn't one.
parent->setKeyCode((InputManagerKeys)index, keyCode, true);
updateConfigItem(renderer, index);
}
}
void GUIEventCallback_OnEvent(ImageManager& imageManager, SDL_Renderer& renderer, std::string name,GUIObject* obj,int eventType){
//Do nothing...
}
};
//Event handler.
static InputDialogHandler* handler;
//A GUIObject that is used to create events for key presses.
class GUIKeyListener:public GUIObject{
public:
GUIKeyListener(ImageManager& imageManager, SDL_Renderer& renderer)
:GUIObject(imageManager,renderer){}
private:
//Leave empty.
~GUIKeyListener(){}
bool handleEvents(SDL_Renderer& renderer,int x,int y,bool enabled,bool visible,bool processed){
if(enabled && handler){
if(event.type==SDL_KEYDOWN){
handler->onKeyDown(renderer, InputManagerKeyCode(event.key.keysym.sym, event.key.keysym.mod));
}
//Joystick
else if(event.type==SDL_JOYAXISMOTION){
if(event.jaxis.value>3200){
handler->onKeyDown(renderer, InputManagerKeyCode(InputManagerKeyCode::JOYSTICK_AXIS, event.jaxis.axis, 1));
}else if(event.jaxis.value<-3200){
handler->onKeyDown(renderer, InputManagerKeyCode(InputManagerKeyCode::JOYSTICK_AXIS, event.jaxis.axis, -1));
}
}
else if(event.type==SDL_JOYBUTTONDOWN){
handler->onKeyDown(renderer, InputManagerKeyCode(InputManagerKeyCode::JOYSTICK_BUTTON, event.jbutton.button, 0));
}
else if(event.type==SDL_JOYHATMOTION){
if (event.jhat.value & SDL_HAT_LEFT){
handler->onKeyDown(renderer, InputManagerKeyCode(InputManagerKeyCode::JOYSTICK_HAT, event.jhat.hat, SDL_HAT_LEFT));
} else if (event.jhat.value & SDL_HAT_RIGHT){
handler->onKeyDown(renderer, InputManagerKeyCode(InputManagerKeyCode::JOYSTICK_HAT, event.jhat.hat, SDL_HAT_RIGHT));
} else if (event.jhat.value & SDL_HAT_UP){
handler->onKeyDown(renderer, InputManagerKeyCode(InputManagerKeyCode::JOYSTICK_HAT, event.jhat.hat, SDL_HAT_UP));
} else if (event.jhat.value & SDL_HAT_DOWN){
handler->onKeyDown(renderer, InputManagerKeyCode(InputManagerKeyCode::JOYSTICK_HAT, event.jhat.hat, SDL_HAT_DOWN));
}
}
}
return false;
}
//Nothing to do.
void render(){}
};
+InputManagerKeys InputManager::getKeyFromName(const std::string& name) {
+ for (int i = 0; i < INPUTMGR_MAX; i++) {
+ if (name == keySettingNames[i]) {
+ return (InputManagerKeys)i;
+ }
+ }
+ return INPUTMGR_MAX;
+}
+
InputManagerKeyCode InputManager::getKeyCode(InputManagerKeys key, bool isAlternativeKey){
if(isAlternativeKey) return alternativeKeys[key];
else return keys[key];
}
void InputManager::setKeyCode(InputManagerKeys key, const InputManagerKeyCode& keyCode, bool isAlternativeKey){
if(isAlternativeKey) alternativeKeys[key]=keyCode;
else keys[key]=keyCode;
}
void InputManager::loadConfig(){
for(int i=0;i<INPUTMGR_MAX;i++){
string s=keySettingNames[i];
keys[i] = InputManagerKeyCode::createFromString(getSettings()->getValue(s));
s+="2";
alternativeKeys[i] = InputManagerKeyCode::createFromString(getSettings()->getValue(s));
//Move the alternative key to main key if the main key is empty.
if(keys[i].empty() && !alternativeKeys[i].empty()){
keys[i]=alternativeKeys[i];
alternativeKeys[i] = InputManagerKeyCode();
}
//Remove duplicate.
if(keys[i]==alternativeKeys[i])
alternativeKeys[i] = InputManagerKeyCode();
}
}
void InputManager::saveConfig(){
int i;
for(i=0;i<INPUTMGR_MAX;i++){
string s=keySettingNames[i];
//Remove duplicate.
if(keys[i]==alternativeKeys[i])
alternativeKeys[i] = InputManagerKeyCode();
getSettings()->setValue(s, keys[i].toString());
s+="2";
getSettings()->setValue(s, alternativeKeys[i].toString());
}
}
GUIObject* InputManager::showConfig(ImageManager& imageManager, SDL_Renderer& renderer,int height){
//Create the new GUI.
GUIObject* root=new GUIObject(imageManager,renderer,0,0,SCREEN_WIDTH,height);
//Instruction label.
GUIObject* obj=new GUILabel(imageManager,renderer,0,6,root->width,36,_("Select an item and press a key to change it."),0,true,true,GUIGravityCenter);
root->addChild(obj);
obj=new GUILabel(imageManager,renderer,0,30,root->width,36,_("Press backspace to clear the selected item."),0,true,true,GUIGravityCenter);
root->addChild(obj);
//The listbox for keys.
GUIListBox *listBox=new GUIListBox(imageManager,renderer,SCREEN_WIDTH*0.15,72,SCREEN_WIDTH*0.7,height-72-8);
root->addChild(listBox);
//Create the event handler.
if(handler)
delete handler;
handler=new InputDialogHandler(renderer,listBox,this);
obj=new GUIKeyListener(imageManager,renderer);
root->addChild(obj);
//Return final widget.
return root;
}
InputManager::InputManager(){
//Clear the arrays.
for(int i=0;i<INPUTMGR_MAX;i++){
keyFlags[i]=0;
}
}
InputManager::~InputManager(){
closeAllJoysticks();
}
int InputManagerKeyCode::getKeyState(int oldState, bool hasEvent, std::vector<SDL_Joystick*>& joysticks, int deadZone) const {
int state = 0;
switch (type) {
default: {
//Keyboard.
if (sym == 0) return 0;
const bool isPrintable = (mod & ~KMOD_SHIFT) == 0 && sym >= 32 && sym <= 126;
if (hasEvent && (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP)) {
const bool isModCorrect = (mod == 0) ? true :
(mod == InputManagerKeyCode(event.key.keysym.sym, event.key.keysym.mod).mod);
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == sym && isModCorrect) {
state |= 0x3;
if (!isPrintable) state |= 0x30;
} else if (event.type == SDL_KEYUP && event.key.keysym.sym == sym && isModCorrect) {
state |= 0x4;
if (!isPrintable) state |= 0x40;
}
}
{
//Get keyboard state for key code
//SDL_GetKeyboardState needs a scankey rather than keycode, so we convert
int numKeys = 0;
const Uint8* keyStates = SDL_GetKeyboardState(&numKeys);
SDL_Scancode scanCode = SDL_GetScancodeFromKey(sym);
if (scanCode < numKeys && keyStates[scanCode]) {
const int m = SDL_GetModState();
const bool isModCorrect = (mod == 0) ? true :
(mod == InputManagerKeyCode(sym, m).mod);
if (isModCorrect) {
state |= 0x1;
if (!isPrintable) state |= 0x10;
}
}
}
break;
}
case JOYSTICK_AXIS:
//Axis.
if (hasEvent && event.type == SDL_JOYAXISMOTION && event.jaxis.axis == buttonIndex) {
if ((buttonValue > 0 && event.jaxis.value > deadZone) || (buttonValue < 0 && event.jaxis.value < -deadZone)) {
if ((oldState & 0x1) == 0) state |= 0x3;
if ((oldState & 0x10) == 0) state |= 0x30;
} else {
if (oldState & 0x1) state |= 0x4;
if (oldState & 0x10) state |= 0x40;
}
}
for (auto j : joysticks) {
Sint16 v = SDL_JoystickGetAxis(j, buttonIndex);
if ((buttonValue > 0 && v > deadZone) || (buttonValue < 0 && v < -deadZone)){
state |= 0x11;
break;
}
}
break;
case JOYSTICK_BUTTON:
//Button.
if (hasEvent) {
if (event.type == SDL_JOYBUTTONDOWN && event.jbutton.button == buttonIndex){
state |= 0x33;
} else if (event.type == SDL_JOYBUTTONUP && event.jbutton.button == buttonIndex){
state |= 0x44;
}
}
for (auto j : joysticks) {
Uint8 v = SDL_JoystickGetButton(j, buttonIndex);
if (v) {
state |= 0x11;
break;
}
}
break;
case JOYSTICK_HAT:
//Hat.
if (hasEvent && event.type == SDL_JOYHATMOTION && event.jhat.hat == buttonIndex) {
if (event.jhat.value & buttonValue){
if ((oldState & 0x1) == 0) state |= 0x3;
if ((oldState & 0x10) == 0) state |= 0x30;
} else{
if (oldState & 0x1) state |= 0x4;
if (oldState & 0x10) state |= 0x40;
}
}
for (auto j : joysticks) {
Uint8 v = SDL_JoystickGetHat(j, buttonIndex);
if (v & buttonValue) {
state |= 0x11;
break;
}
}
break;
}
return state;
}
//Update the key state, according to current SDL event, etc.
void InputManager::updateState(bool hasEvent){
for(int i=0;i<INPUTMGR_MAX;i++){
keyFlags[i] = keys[i].getKeyState(keyFlags[i], hasEvent, joysticks) | alternativeKeys[i].getKeyState(keyFlags[i], hasEvent, joysticks);
}
}
//Check if there is KeyDown event.
bool InputManager::isKeyDownEvent(InputManagerKeys key, bool excludePrintable) {
return (keyFlags[key] & (excludePrintable ? 0x20 : 0x2)) != 0;
}
//Check if there is KeyUp event.
bool InputManager::isKeyUpEvent(InputManagerKeys key, bool excludePrintable) {
return (keyFlags[key] & (excludePrintable ? 0x40 : 0x4)) != 0;
}
//Check if specified key is down.
bool InputManager::isKeyDown(InputManagerKeys key, bool excludePrintable) {
return (keyFlags[key] & (excludePrintable ? 0x10 : 0x1)) != 0;
}
//Open all joysticks.
void InputManager::openAllJoysitcks(){
int i,m;
//First close previous joysticks.
closeAllJoysticks();
//Open all joysticks.
m=SDL_NumJoysticks();
for(i=0;i<m;i++){
SDL_Joystick *j=SDL_JoystickOpen(i);
if(j==NULL){
printf("ERROR: Couldn't open Joystick %d\n",i);
}else{
joysticks.push_back(j);
}
}
}
//Close all joysticks.
void InputManager::closeAllJoysticks(){
for(int i=0;i<(int)joysticks.size();i++){
SDL_JoystickClose(joysticks[i]);
}
joysticks.clear();
}
diff --git a/src/InputManager.h b/src/InputManager.h
index 049d7fb..9a2747b 100644
--- a/src/InputManager.h
+++ b/src/InputManager.h
@@ -1,171 +1,174 @@
/*
* Copyright (C) 2011-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 INPUTMANAGER_H
#define INPUTMANAGER_H
#include <SDL.h>
#include <vector>
#include <string>
#include "GUIObject.h"
enum InputManagerKeys{
INPUTMGR_UP,
INPUTMGR_DOWN,
INPUTMGR_LEFT,
INPUTMGR_RIGHT,
INPUTMGR_JUMP,
INPUTMGR_ACTION,
INPUTMGR_SPACE,
INPUTMGR_CANCELRECORDING,
INPUTMGR_ESCAPE,
INPUTMGR_RESTART,
INPUTMGR_TAB,
INPUTMGR_SAVE,
INPUTMGR_LOAD,
INPUTMGR_SWAP,
INPUTMGR_TELEPORT,
INPUTMGR_SUICIDE,
INPUTMGR_SHIFT,
INPUTMGR_NEXT,
INPUTMGR_PREVIOUS,
INPUTMGR_SELECT,
INPUTMGR_MAX
};
// Represents a key code or a joystick input code.
struct InputManagerKeyCode {
// The input type. Currently joystick ball is unsupported.
enum InputType {
KEYBOARD,
JOYSTICK_AXIS,
JOYSTICK_BUTTON,
JOYSTICK_HAT,
};
// The input type.
InputType type;
union {
struct {
// The keycode if the input type is KEYBOARD.
// NOTE: if type == KEYBOARD and sym == 0 then this key is disabled at all.
int sym;
// The modifier if the input type is KEYBOARD.
// NOTE: if modifier == 0 then we don't check the modifier at all.
int mod;
};
struct {
// Joystick button index.
int buttonIndex;
// Joystick button value.
// If type == JOYSTICK_AXIS then value should be 1 or -1.
// If type == JOYSTICK_BUTTON then it's unused.
// If type == JOYSTICK_HAT then it's SDL_HAT_LEFT, SDL_HAT_RIGHT, SDL_HAT_UP or SDL_HAT_DOWN.
int buttonValue;
};
};
// Constructor for a keyboard input.
explicit InputManagerKeyCode(int sym_ = 0, int mod_ = 0);
// Constructor for a joystick input.
explicit InputManagerKeyCode(InputType type_, int buttonIndex_, int buttonValue_);
// Create a key code from string.
static InputManagerKeyCode createFromString(const std::string& s);
// Convert a key code to a machine-readable string.
std::string toString() const;
// Convert a key code to a human-readable string.
std::string describe() const;
// Convert two key codes to a human-readable string.
static std::string describeTwo(const InputManagerKeyCode& keyCode, const InputManagerKeyCode& keyCodeAlt);
// Check if the key code is empty.
bool empty() const;
// Internal function to check if the key corresponding to this key code is pressed.
// oldState: The bit-field flag saves the key states. 0x1=key is down, 0x2=KeyDown event,0x4=KeyUp event.
// hasEvent: current SDL event is present.
// joysticks: List of joystick to detect.
// deadZone: Joystick dead zone for JOYSTICK_AXIS detection.
// Return value: The bit-field flag with the same meaning as oldState.
int getKeyState(int oldState, bool hasEvent, std::vector<SDL_Joystick*>& joysticks, int deadZone = 3200) const;
bool operator==(const InputManagerKeyCode& rhs) const;
bool operator!=(const InputManagerKeyCode& rhs) const { return !(*this == rhs); }
// Check if a keyboard input contains another keyboard input (for example, Ctrl+A contains Ctrl but doesn't contain A).
bool contains(const InputManagerKeyCode& rhs) const;
};
class InputManager{
public:
InputManager();
~InputManager();
+ //Get the key from its setting name. Returns INPUTMGR_MAX if the name is unknown.
+ static InputManagerKeys getKeyFromName(const std::string& name);
+
//Get and set key code of each key.
InputManagerKeyCode getKeyCode(InputManagerKeys key, bool isAlternativeKey);
void setKeyCode(InputManagerKeys key, const InputManagerKeyCode& keyCode, bool isAlternativeKey);
//Load and save key settings from config file.
void loadConfig();
void saveConfig();
//Show the config screen.
GUIObject* showConfig(ImageManager& imageManager, SDL_Renderer& renderer, int height);
//Update the key state, according to current SDL event, etc.
void updateState(bool hasEvent);
//Check if there is KeyDown event.
bool isKeyDownEvent(InputManagerKeys key, bool excludePrintable = false);
//Check if there is KeyUp event.
bool isKeyUpEvent(InputManagerKeys key, bool excludePrintable = false);
//Check if specified key is down.
bool isKeyDown(InputManagerKeys key, bool excludePrintable = false);
//Open all joysticks.
void openAllJoysitcks();
//Close all joysticks.
void closeAllJoysticks();
private:
//The key code of each key.
InputManagerKeyCode keys[INPUTMGR_MAX], alternativeKeys[INPUTMGR_MAX];
//The bit-field flag array saves the key states.
// 0x1 means the key is down.
// 0x2 means KeyDown event.
// 0x4 means KeyUp event.
int keyFlags[INPUTMGR_MAX];
//Contains all joysticks.
std::vector<SDL_Joystick*> joysticks;
};
extern InputManager inputMgr;
#endif //INPUTMANAGER_H

File Metadata

Mime Type
text/x-diff
Expires
Sat, May 16, 7:20 PM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
63212
Default Alt Text
(102 KB)

Event Timeline