Page MenuHomePhabricator (Chris)

No OneTemporary

Authored By
Unknown
Size
185 KB
Referenced Files
None
Subscribers
None
diff --git a/docs/ScriptAPI.md b/docs/ScriptAPI.md
index 238adc8..38d904f 100644
--- a/docs/ScriptAPI.md
+++ b/docs/ScriptAPI.md
@@ -1,549 +1,600 @@
Me and My Shadow Script API Reference
=====================================
(draft)
The script language is Lua 5.2 (later we may bump it to 5.3).
Always check `ScriptAPI.cpp` for the newest API changed unmentioned in this document.
How to edit script
==================
To edit the script of a block, right click the block and select "Scripting".
To edit the script of the level, right click the empty space of the level and select "Scripting".
Currently the scenery block doesn't support scripting.
Available event types of block:
Event type | Description
----------------------|--------------
"playerWalkOn" | Fired once when the player walks on. (For example this is used in fragile block.)
"playerIsOn" | Fired every frame when the player is on.
"playerLeave" | Fired once when the player leaves.
"onCreate" | Fired when object creates.
"onEnterFrame" | Fired every frame.
"onPlayerInteraction" | Fired when the player press DOWN key. Currently this event only fires when the block type is TYPE_SWITCH.
"onToggle" | Fired when the block receives "toggle" from a switch/button. NOTE: The switch/button itself will also receive this event. This is used in an old example found on my old computer.
"onSwitchOn" | Fired when the block receives "switch on" from a switch/button. NOTE: The switch/button itself will also receive this event. This is used in an old example found on my old computer.
"onSwitchOff" | Fired when the block receives "switch off" from a switch/button. NOTE: The switch/button itself will also receive this event. This is used in an old example found on my old computer.
NOTE: During the event execution the global variable `this` temporarily points to current block. (Ad-hoc workaround!)
When the event execution ends the global variable `this` is reset to its previous value.
The block event may return an integer value (default is 0) to alter the game logic:
Return value | Description
-------------|--------------
0 | Skip the default game logic for this event.
1 | Do the default game logic for this event.
Available event types of level:
Event type | Description
-----------|--------------
"onCreate" | Fired when the level is created or the game is reset. This happens after all the blocks are created and their `onCreate` is called.
"onSave" | Fired when the game is saved.
"onLoad" | Fired when the game is loaded.
For the newest lists of event types, see `init()` function in `Functions.cpp`.
NOTE: the following methods to specify scripts can be used:
* Specify scripts for each events in the block script editing dialog.
* Only specify `onCreate` script in the block script editing dialog,
and use `setEventHandler()` function in script to specify scripts for other events dynamically.
* Only specify `onCreate` script in the level script editing dialog,
and use `setEventHandler()` function in script to specify scripts for other events for level/blocks dynamically.
Script API reference
====================
The "block" library
-------------------
### Static functions:
* getBlockById(id)
Returns the first block with specified id. If not found, returns `nil`.
Example:
~~~lua
local b=block.getBlockById("1")
local x,y=b:getLocation()
print(x..","..y)
~~~
* getBlocksById(id)
Returns the list of all blocks with specified id.
Example:
~~~lua
local l=block.getBlocksById("1")
for i,b in ipairs(l) do
local x,y=b:getLocation()
print(x..","..y)
end
~~~
### Member functions:
* isValid() -- check the object is valid (i.e. not deleted, etc.)
* moveTo(x,y)
Move the block to the new position, update the velocity of block according to the position changed.
Example:
~~~lua
local b=block.getBlockById("1")
local x,y=b:getLocation()
b:moveTo(x+1,y)
~~~
* getLocation()
Returns the position of the block.
Example: see the example for moveTo().
* setLocation(x,y)
Move the block to the new position without updating the velocity of block.
Example: omitted since it's almost the same as moveTo().
* growTo(w,h)
Resize the block, update the velocity of block according to the size changed.
NOTE: I don't think the velocity need to be updated when resizing block, so don't use this function.
Example: omitted since it's almost the same as setSize().
* getSize()
Returns the size of the block.
Example:
~~~lua
local b=block.getBlockById("1")
local w,h=b:getSize()
print(w..","..h)
~~~
* setSize(w,h)
Resize the block without updating the velocity of block.
Example:
~~~lua
local b=block.getBlockById("1")
local w,h=b:getSize()
b:setSize(w+1,h)
~~~
* getType()
Returns the type of the block (which is a string).
Example:
~~~lua
local b=block.getBlockById("1")
local s=b:getType()
print(s)
~~~
* changeThemeState(new_state)
Change the state of the block to new_state (which is a string).
Example:
~~~lua
local b=block.getBlockById("1")
b:changeThemeState("activated")
~~~
* setVisible(b)
Set the visibility the block.
NOTE: The default value is `true`. If set to `false` the block is hidden completely,
the animation is stopped, can't receive any event, can't execute any scripts (except for `onCreate`),
can't be used as a portal destination,
doesn't participate in collision check and game logic, etc...
NOTE: This is a newly added feature.
If you find any bugs (e.g. if an invisible block still affects the game logic)
please report the bugs to GitHub issue tracker.
Example:
~~~lua
local b=block.getBlockById("1")
if b:isVisible() then
b:setVisible(false)
else
b:setVisible(true)
end
~~~
* isVisible()
Returns whether the block is visible.
Example: see the example for setVisible().
* getEventHandler(event_type)
Returns the event handler of event_type (which is a string).
Example:
~~~lua
local b=block.getBlockById("1")
local f=b:getEventHandler("onSwitchOn")
b:setEventHandler("onSwitchOff",f)
~~~
* setEventHandler(event_type,handler)
Set the handler of event_type (which is a string). The handler should be a function or `nil`.
Returns the previous event handler.
Example:
~~~lua
local b=block.getBlockById("1")
b:setEventHandler("onSwitchOff",function()
print("I am switched off.")
end)
~~~
* onEvent(eventType)
Fire an event to specified block.
NOTE: The event will be processed immediately.
Example:
~~~lua
local b=block.getBlockById("1")
b:onEvent("onToggle")
~~~
NOTE: Be careful not to write infinite recursive code! Bad example:
~~~lua
-- onToggle event of a moving block
this:onEvent("onToggle")
~~~
-* isActivated() / setActivated(bool) -- get/set a boolean indicates if the block is activated
- -- the block should be one of TYPE_MOVING_BLOCK, TYPE_MOVING_SHADOW_BLOCK, TYPE_MOVING_SPIKES,
- TYPE_CONVEYOR_BELT, TYPE_SHADOW_CONVEYOR_BELT.
+* isActivated() / setActivated(bool)
-* isAutomatic() / setAutomatic(bool) -- get/set a boolean indicates if the portal is automatic
- -- the block should be TYPE_PORTAL
+Get/set a boolean indicates if the block is activated.
-* getBehavior() / setBehavior(str) -- get/set a string (must be "on", "off" or "toggle")
- representing the behavior of the block -- the block should be TYPE_BUTTON, TYPE_SWITCH
+The block should be one of TYPE_MOVING_BLOCK, TYPE_MOVING_SHADOW_BLOCK, TYPE_MOVING_SPIKES,
+TYPE_CONVEYOR_BELT, TYPE_SHADOW_CONVEYOR_BELT.
-* getState() / setState(num) -- get/set a number (must be 0,1,2 or 3)
- representing the state of a fragile block -- the block should be TYPE_FRAGILE
+* isAutomatic() / setAutomatic(bool)
-* isPlayerOn() -- get a boolean indicates if the player is on -- only works for TYPE_BUTTON
+Get/set a boolean indicates if the portal is automatic.
-* getPathMaxTime() -- get the total time of the path of a moving block
+The block should be TYPE_PORTAL.
-* getPathTime() / setPathTime(num) -- get/set the current time of the path of a moving block
+* getBehavior() / setBehavior(str)
-* isLooping() / setLooping(bool) -- get/set the looping property of a moving block
+Get/set a string (must be "on", "off" or "toggle"),
+representing the behavior of the block.
-* getSpeed() / setSpeed(num) -- get/set the speed of a conveyor belt. NOTE: 1 Speed = 0.08 block/s = 0.1 pixel/frame
+The block should be TYPE_BUTTON, TYPE_SWITCH.
+
+* getState() / setState(num)
+
+Get/set a number (must be 0,1,2 or 3),
+representing the state of a fragile block.
+
+The block should be TYPE_FRAGILE.
+
+* isPlayerOn()
+
+Get a boolean indicates if the player is on.
+
+Currently only works for TYPE_BUTTON.
+
+* getPathMaxTime()
+
+Get the total time of the path of a moving block.
+
+* getPathTime() / setPathTime(num)
+
+Get/set the current time of the path of a moving block.
+
+* isLooping() / setLooping(bool)
+
+Get/set the looping property of a moving block.
+
+* getSpeed() / setSpeed(num)
+
+Get/set the speed of a conveyor belt.
+
+NOTE: 1 Speed = 0.08 block/s = 0.1 pixel/frame.
+
+* getAppearance() / setAppearance(str)
+
+Get/set the custom appearance of a block.
+
+The `str` is the name of the custom appearance, either `"<blockName>_Scenery"` or name of a scenery block.
+Empty string or nil means the default appearance.
+
+* getId() / setId(str)
+
+Get/set the id of a block.
+
+* getDestination() / setDestination(str)
+
+Get/set the destination of a portal.
+
+The block should be TYPE_PORTAL.
+
+* getMessage() / setMessage(str)
+
+Get/set the message of a notify block.
+
+The block should be TYPE_NOTIFICATION_BLOCK.
The "playershadow" library
--------------------------
### Global constants:
* player
The player object.
* shadow
The shadow object.
### Member functions:
* getLocation()
Returns the location of player/shadow.
Example:
~~~lua
local x,y=player:getLocation()
print("player: "..x..","..y)
x,y=shadow:getLocation()
print("shadow: "..x..","..y)
~~~
* setLocation(x,y)
Set the location of player/shadow.
Example:
~~~lua
local x,y=player:getLocation()
player:setLocation(x+1,y)
~~~
* jump([strength=13])
Let the player/shadow jump if it's allowed.
strength: Jump strength.
Example:
~~~lua
player:jump(20)
~~~
* isShadow()
Returns whether the current object is shadow.
Example:
~~~lua
print(player:isShadow())
print(shadow:isShadow())
~~~
* getCurrentStand()
Returns the block on which the player/shadow is standing on. Can be `nil`.
Example:
~~~lua
local b=player:getCurrentStand()
if b then
print(b:getType())
else
print("The player is not standing on any blocks")
end
~~~
* isInAir() -- returns a boolean indicating if the player is in air
* canMove() -- returns a boolean indicating if the player can move (i.e. not standing on shadow)
* isDead() -- returns a boolean indicating if the player is dead
* isHoldingOther() -- returns a boolean indicating if the player is holding other
The "level" library
-------------------
### Static functions:
* getSize() -- get the level size
* getWidth() -- get the level width
* getHeight() -- get the level height
* getName() -- get the level name
* getEventHandler(event_type) -- get the event handler
* setEventHandler(event_type,handler) -- set the event handler, return the old handler
* win() -- win the game
* getTime() -- get the game time (in frames)
* getRecordings() -- get the game recordings
* broadcastObjectEvent(eventType,[objectType=nil],[id=nil],[target=nil])
Broadcast the event to blocks satisfying the specified condition.
NOTE: The event will be processed in next frame.
Argument name | Description
--------------|-------------
eventType | string.
objectType | string or nil. If this is set then the event is only received by the block with specified type.
id | string or nil. If this is set then the event is only received by the block with specified id.
target | block or nil. If this is set then the event is only received by the specified block.
Example:
~~~lua
level.broadcastObjectEvent("onToggle",nil,"1")
~~~
The "delayExecution" library
----------------------------
### Static functions:
* schedule(func,time,[repeatCount=1],[repeatInterval],[enabled=true],[arguments...])
Schedule a delay execution of a given function after the given time.
Argument name | Description
---------------|-------------
func | A function to be executed.
time | Time, given in frames (NOTE: 40 frames = 1 second). NOTE: If <=0 it is the same as =1.
repeatCount | The number of times the function will be executed. After such number of times executed, the delay execution will be removed from the list and get deleted. If =0 the delay execution object will be deleted soon. If <0 the function will be executed indefinitely.
repeatInterval | The repeat interval. If it is `nil` then the `time` argument will be used instead. NOTE: If <=0 the repeat execution will be disabled at all and the repeatCount will be set to 1.
enabled | Enabled.
arguments | Optional arguments passed to the function.
Return value: the delayExecution object.
NOTE: If you want to update time/repeatCount during the function execution,
notice that the time/repeatCount is updated BEFORE the function execution.
NOTE: During the execution the global variable `this`
temporarily points to current delay execution object. (Ad-hoc workaround!)
When the execution ends the global variable `this` is reset to its previous value.
Example:
~~~lua
local f=function()
local a
a=0
return(function(b)
shadow:jump()
print('obj1 '..this:getExecutionTime()..' '..a..' '..tostring(b))
a=a+2
end)
end
local obj1=delayExecution.schedule(f(),40*2,5,nil,nil,100)
local obj2=delayExecution.schedule(
function(o)
print('obj2 '..tostring(o:isValid()))
if not o:isValid() then
this:setFunc(f())
end
end,40*1,-1,nil,nil,obj1)
local obj3=delayExecution.schedule(
function(o)
o:cancel()
end,40*30,1,nil,nil,obj2)
~~~
### Member functions:
* isValid() -- Check if it's valid, i.e. not removed from list.
* cancel() -- Cancels a delay execution. The canceled delay execution will be removed from the list and can not be restored.
* isEnabled()/setEnabled(bool) -- get/set enabled of a delay execution. A disabled one will not count down its timer.
* getTime()/setTime(integer) -- get/set the remaining time until the next execution. NOTE: If <=0 it is the same as =1.
* getRepeatCount()/setRepeatCount(integer) -- get/set the remaining repeat count. If =0 the object will get deleted soon. If <0 the function will be executed indefinitely.
* getRepeatInterval()/setRepeatInterval(integer) -- get/set the repeat interval. NOTE: If <=0 then nothing happens.
* getFunc()/setFunc(func) -- get/set the function to be executed. NOTE: The setFunc will return the original function.
* getArguments()/setArguments(args...) -- get/set the arguments
* getExecutionTime()/setExecutionTime(integer) -- get/set the number of times the function being executed. NOTE: this execution time doesn't affect the default logic.
The "camera" library
--------------------
### Static functions:
* setMode(mode) -- set the camera mode, which is "player" or "shadow"
* lookAt(x,y) -- set the camera mode to "custom" and set the new center of camera
The "audio" library
-------------------
NOTE: the following functions are not going to work if the sound/music volume is 0.
### Static functions:
* playSound(name[,concurrent=-1[,force=false[,fade=-1]]])
Play a sound effect.
Argument name | Description
--------------|-------------
name | The name of the sound effect. Currently available: "jump", "hit", "checkpoint", "swap", "toggle", "error", "collect", "achievement".
concurrent | The number of times the same sfx can be played at once, -1 is unlimited. NOTE: there are 64 channels.
force | If the sound must be played even if all channels are used. In this case the sound effect in the first channel will be stopped.
fade | A factor to temporarily turn the music volume down (0-128). -1 means don't use this feature.
Return value: The channel of the sfx. -1 means failed (channel is full, invalid sfx name, sfx volume is 0, etc.)
* playMusic(name[,fade=true])
Play a music.
Argument name | Description
--------------|-------------
name | The name of the song, e.g. "default/neverending" or "menu".
fade | Boolean if it should fade the current one out or not.
* pickMusic() - pick a song from the current music list.
* getMusicList()/setMusicList(name_of_the_music_list) - get/set the music list. Example: "default".
* currentMusic() - get the current music.
The "gettext" library
--------------------
This library is used for translation support.
NOTE: Currently this library only uses the dictionary for current level pack.
This means it doesn't work for individual level which doesn't contain in a level pack,
and it can't make use of the translations of the core game.
### Global functions:
* `_(msgid)` -- translate the string using default context
* `__(msgid)` -- does nothing, just outputs the original string. However, it will be scanned by `xgettext`.
Mainly used in block:setMessage() since the block message will always be passed to gettext().
Also used in construction of an array of strings, which will be translated dynamically.
### Static functions:
* gettext(msgid) -- translate the string using default context
* pgettext(msgctxt,msgid) -- translate the string using specified context
* ngettext(msgid,msgid_plural,n) -- translate the string using default context, taking plural form into consideration
* npgettext(msgctxt,msgid,msgid_plural,n) -- translate the string using specified context, taking plural form into consideration
diff --git a/src/Block.cpp b/src/Block.cpp
index 31ea7d1..c1418e9 100644
--- a/src/Block.cpp
+++ b/src/Block.cpp
@@ -1,1173 +1,1173 @@
/*
* 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 "Functions.h"
#include "LevelEditor.h"
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
using namespace std;
Block::Block(Game* parent,int x,int y,int w,int h,int type):
GameObject(parent),
animation(0),
flags(0),
temp(0),
dx(0),
dy(0),
movingPosTime(-1),
speed(0),
objCurrentStand(NULL),
xVel(0), yVel(0),
inAir(false), xVelBase(0), yVelBase(0),
isDelete(false)
{
//Make sure the type is set, if not init should be called somewhere else with this information.
if(type>=0 && type<TYPE_MAX)
init(x,y,w,h,type);
}
Block::Block(const Block& other)
: GameObject(other)
, ScriptProxyUserClass(other)
, animation(other.animation)
, flags(other.flags)
, temp(other.temp)
, dx(other.dx), dy(other.dy)
, movingPos(other.movingPos)
, movingPosTime(-1)
, speed(other.speed)
, objCurrentStand(other.objCurrentStand)
, customAppearanceName(other.customAppearanceName)
, appearance(other.appearance)
, xVel(other.xVel), yVel(other.yVel)
, inAir(other.inAir), xVelBase(other.xVelBase), yVelBase(other.yVelBase)
, id(other.id), destination(other.destination), message(other.message)
, isDelete(other.isDelete)
{
auto se = parent->getScriptExecutor();
if (se == NULL) return;
lua_State *state = se->getLuaState();
if (state == NULL) return;
//Copy the compiledScripts.
for (auto it = other.compiledScripts.begin(); it != other.compiledScripts.end(); ++it) {
lua_rawgeti(state, LUA_REGISTRYINDEX, it->second);
compiledScripts[it->first] = luaL_ref(state, LUA_REGISTRYINDEX);
}
}
Block::~Block() {
auto se = parent->getScriptExecutor();
if (se == NULL) return;
lua_State *state = se->getLuaState();
if (state == NULL) return;
//Delete the compiledScripts.
for (auto it = compiledScripts.begin(); it != compiledScripts.end(); ++it) {
luaL_unref(state, LUA_REGISTRYINDEX, it->second);
}
}
int Block::getPathMaxTime() {
if (movingPosTime < 0) {
movingPosTime = 0;
for (const SDL_Rect& p : movingPos) {
movingPosTime += p.w;
}
}
return movingPosTime;
}
void Block::init(int x,int y,int w,int h,int type){
//First set the location and size of the box.
//The default size is 50x50.
box.x=boxBase.x=x;
box.y=boxBase.y=y;
box.w=boxBase.w=w;
box.h=boxBase.h=h;
//Set the type.
this->type=type;
//Some types need type specific code.
if(type==TYPE_START_PLAYER){
//This is the player start so set the player here.
//We center the player, the player is 23px wide.
parent->player.setLocation(box.x+(box.w-23)/2,box.y);
parent->player.fx=box.x+(box.w-23)/2;
parent->player.fy=box.y;
}else if(type==TYPE_START_SHADOW){
//This is the shadow start so set the shadow here.
//We center the shadow, the shadow is 23px wide.
parent->shadow.setLocation(box.x+(box.w-23)/2,box.y);
parent->shadow.fx=box.x+(box.w-23)/2;
parent->shadow.fy=box.y;
}
objCurrentStand=NULL;
inAir=true;
xVel=yVel=xVelBase=yVelBase=0;
//And load the (default) appearance.
objThemes.getBlock(type)->createInstance(&appearance);
}
void Block::show(SDL_Renderer& renderer){
//Make sure we are visible.
if ((flags & 0x80000000) != 0 && (stateID != STATE_LEVEL_EDITOR || dynamic_cast<LevelEditor*>(parent)->isPlayMode()))
return;
//Check if the block is visible.
if(checkCollision(camera,box)==true || (stateID==STATE_LEVEL_EDITOR && checkCollision(camera,boxBase)==true)){
//Some type of block needs additional state check.
switch(type){
case TYPE_CHECKPOINT:
//Check if the checkpoint is last used.
if(parent!=NULL && parent->objLastCheckPoint.get()==this){
if(!temp) appearance.changeState("activated");
temp=1;
}else{
if(temp) appearance.changeState("default");
temp=0;
}
break;
}
//Always draw the base. (This is only supposed to show in editor.)
appearance.drawState("base", renderer, boxBase.x - camera.x, boxBase.y - camera.y, boxBase.w, boxBase.h);
//What we need to draw depends on the type of block.
switch (type) {
default:
//Draw normal.
appearance.draw(renderer, box.x - camera.x, box.y - camera.y, box.w, box.h);
break;
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
//Draw conveyor belt.
if (animation) {
// FIXME: ad-hoc code. Should add a new animation type in theme system.
const int a = animation / 10;
const SDL_Rect r = { box.x - camera.x, box.y - camera.y, box.w, box.h };
appearance.draw(renderer, box.x - camera.x - 50 + a, box.y - camera.y, box.w + 50, box.h, &r);
} else {
appearance.draw(renderer, box.x - camera.x, box.y - camera.y, box.w, box.h);
}
break;
}
//Some types need to draw something on top of the base/default.
switch(type){
case TYPE_BUTTON:
if(flags&4){
if(animation<5) animation++;
}else{
if(animation>0) animation--;
}
appearance.drawState("button", renderer, box.x - camera.x, box.y - camera.y - 5 + animation, box.w, box.h);
break;
}
//Draw some stupid icons during edit mode.
if (stateID == STATE_LEVEL_EDITOR) {
auto bmGUI = static_cast<LevelEditor*>(parent)->getGuiTexture();
if (!bmGUI) {
return;
}
int x = box.x - camera.x + 2;
//Scripted blocks
- if (!scripts.empty()){
+ if (!scripts.empty() || !compiledScripts.empty()) {
const SDL_Rect r = { 0, 32, 16, 16 };
const SDL_Rect dstRect = { x, box.y - camera.y + 2, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
x += 16;
}
//Invisible blocks
if (flags & 0x80000000) {
const SDL_Rect r = { 16, 48, 16, 16 };
const SDL_Rect dstRect = { x, box.y - camera.y + 2, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
x += 16;
}
//Block with custom appearance
if (!customAppearanceName.empty()) {
const SDL_Rect r = { 48, 16, 16, 16 };
const SDL_Rect dstRect = { x, box.y - camera.y + 2, 16, 16 };
SDL_RenderCopy(&renderer, bmGUI.get(), &r, &dstRect);
x += 16;
}
}
}
}
SDL_Rect Block::getBox(int boxType){
SDL_Rect r={0,0,0,0};
switch(boxType){
case BoxType_Base:
return boxBase;
case BoxType_Previous:
r.x=box.x-dx;
r.y=box.y-dy;
r.w=box.w;
r.h=box.h;
return r;
case BoxType_Delta:
r.x=dx;
r.y=dy;
return r;
case BoxType_Velocity:
r.x=xVel;
r.y=yVel;
//NOTE: In case of the pushable block we sometimes need to substract one from the vertical velocity.
//The yVel is set to one when it's resting, but should be handled as zero in collision.
if(type==TYPE_PUSHABLE && !inAir)
r.y=0;
return r;
case BoxType_Current:
return box;
}
return r;
}
void Block::moveTo(int x,int y){
//The block has moved so calculate the delta.
//NOTE: Every delta is summed since they all happened within one frame and for collision/movement we need the resulting delta.
int delta=(x-box.x);
dx+=delta;
xVel+=delta;
delta=(y-box.y);
dy+=delta;
yVel+=delta;
//And set the new location.
box.x=x;
box.y=y;
}
void Block::growTo(int w,int h){
//The block has changed size
//NOTE: Every delta is summed since they all happened within one frame and for collision/movement we need the resulting delta.
int delta=(w-box.w);
dx+=delta;
xVel+=delta;
delta=(h-box.h);
dy+=delta;
yVel+=delta;
//And set the new location.
box.w=w;
box.h=h;
}
void Block::playAnimation(){
switch(type){
case TYPE_SWAP:
appearance.changeState("activated");
break;
case TYPE_SWITCH:
temp^=1;
appearance.changeState(temp?"activated":"default");
break;
}
}
void Block::onEvent(int eventType){
//Make sure we are visible, otherwise no events should be handled except for 'OnCreate'.
if ((flags & 0x80000000) != 0 && eventType != GameObjectEvent_OnCreate)
return;
//Iterator used to check if the map contains certain entries.
map<int,int>::iterator it;
//Check if there's a script for the event.
it=compiledScripts.find(eventType);
if(it!=compiledScripts.end()){
//There is a script so execute it and check return value.
int ret=parent->getScriptExecutor()->executeScript(it->second,this);
//Return value 1 means do default event process.
//Other values are coming soon...
if(ret!=1) return;
}
//Event handling.
switch(eventType){
case GameObjectEvent_PlayerWalkOn:
switch(type){
case TYPE_FRAGILE:
if ((flags & 0x3) < 3) {
flags++;
const int f = flags & 0x3;
const char* s = (f <= 0) ? "default" : ((f == 1) ? "fragile1" : ((f == 2) ? "fragile2" : "fragile3"));
appearance.changeState(s);
}
break;
}
break;
case GameObjectEvent_PlayerIsOn:
switch(type){
case TYPE_BUTTON:
temp=1;
break;
}
break;
case GameObjectEvent_OnPlayerInteraction:
switch (type) {
case TYPE_SWITCH:
//Make sure that the id isn't emtpy.
if (!id.empty()) {
parent->broadcastObjectEvent(0x10000 | (flags & 3),
-1, id.c_str());
} else {
cerr << "WARNING: invalid switch id!" << endl;
}
break;
}
break;
case GameObjectEvent_OnToggle:
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
flags^=1;
break;
case TYPE_PORTAL:
appearance.changeState("activated");
break;
case TYPE_COLLECTABLE:
appearance.changeState("inactive");
flags|=1;
break;
}
break;
case GameObjectEvent_OnSwitchOn:
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
flags&=~1;
break;
case TYPE_EXIT:
appearance.changeState("default");
break;
}
break;
case GameObjectEvent_OnSwitchOff:
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
flags|=1;
break;
case TYPE_EXIT:
appearance.changeState("closed");
break;
}
break;
}
}
int Block::queryProperties(int propertyType,Player* obj){
switch(propertyType){
case GameObjectProperty_PlayerCanWalkOn:
switch(type){
case TYPE_BLOCK:
case TYPE_MOVING_BLOCK:
case TYPE_CONVEYOR_BELT:
case TYPE_BUTTON:
case TYPE_PUSHABLE:
return 1;
case TYPE_SHADOW_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_SHADOW_CONVEYOR_BELT:
if(obj!=NULL && obj->isShadow()) return 1;
break;
case TYPE_FRAGILE:
if ((flags & 0x3) < 3) return 1;
break;
}
break;
case GameObjectProperty_IsSpikes:
switch(type){
case TYPE_SPIKES:
case TYPE_MOVING_SPIKES:
return 1;
}
break;
case GameObjectProperty_Flags:
return flags;
break;
default:
break;
}
return 0;
}
void Block::getEditorData(std::vector<std::pair<std::string,std::string> >& obj){
//Every block has an id.
obj.push_back(pair<string,string>("id",id));
//And visibility.
obj.push_back(pair<string, string>("visible", (flags & 0x80000000) == 0 ? "1" : "0"));
//And custom appearance.
obj.push_back(pair<string, string>("appearance", customAppearanceName));
//Block specific properties.
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
{
char s[64],s0[64];
sprintf(s,"%d",(int)movingPos.size());
obj.push_back(pair<string,string>("MovingPosCount",s));
obj.push_back(pair<string,string>("activated",(flags&0x1)?"0":"1"));
obj.push_back(pair<string,string>("loop",(flags&0x2)?"0":"1"));
for(unsigned int i=0;i<movingPos.size();i++){
sprintf(s0+1,"%u",i);
sprintf(s,"%d",movingPos[i].x);
s0[0]='x';
obj.push_back(pair<string,string>(s0,s));
sprintf(s,"%d",movingPos[i].y);
s0[0]='y';
obj.push_back(pair<string,string>(s0,s));
sprintf(s,"%d",movingPos[i].w);
s0[0]='t';
obj.push_back(pair<string,string>(s0,s));
}
}
break;
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
{
char s[64];
obj.push_back(pair<string,string>("activated",(flags&0x1)?"0":"1"));
sprintf(s,"%d",speed);
obj.push_back(pair<string,string>("speed10",s));
}
break;
case TYPE_PORTAL:
obj.push_back(pair<string,string>("automatic",(flags&0x1)?"1":"0"));
obj.push_back(pair<string,string>("destination",destination));
break;
case TYPE_BUTTON:
case TYPE_SWITCH:
{
string s;
switch(flags&0x3){
case 1:
s="on";
break;
case 2:
s="off";
break;
default:
s="toggle";
break;
}
obj.push_back(pair<string,string>("behaviour",s));
}
break;
case TYPE_NOTIFICATION_BLOCK:
{
string value=message;
//Change \n with the characters '\n'.
while(value.find('\n',0)!=string::npos){
size_t pos=value.find('\n',0);
value=value.replace(pos,1,"\\n");
}
obj.push_back(pair<string,string>("message",value));
}
break;
case TYPE_FRAGILE:
{
char s[64];
sprintf(s,"%d",flags&0x3);
obj.push_back(pair<string,string>("state",s));
}
break;
}
}
void Block::setEditorData(std::map<std::string,std::string>& obj){
//Iterator used to check if the map contains certain entries.
map<string,string>::iterator it;
//Check if the data contains the appearance.
it = obj.find("appearance");
if (it != obj.end()) {
std::string newAppearanceName;
if (it->second.empty() || it->second == std::string(Game::blockName[type]) + "_Scenery") {
//Use the default appearance. (Do nothing since newAppearanceName is already empty)
} else {
//Use the custom appearance.
newAppearanceName = it->second;
}
if (newAppearanceName != customAppearanceName) {
//Try to find the custom appearance.
ThemeBlock *themeBlock = NULL;
if (!newAppearanceName.empty()) {
themeBlock = objThemes.getScenery(newAppearanceName);
if (themeBlock == NULL) {
std::cerr << "ERROR: failed to load custom appearance '" << newAppearanceName << "' for block " << Game::blockName[type] << std::endl;
}
} else {
themeBlock = objThemes.getBlock(type);
if (themeBlock == NULL) {
std::cerr << "ERROR: failed to load default appearance for block " << Game::blockName[type] << std::endl;
}
}
if (themeBlock) {
//Update the custom appearance name.
customAppearanceName = newAppearanceName;
//Recreate the theme block instance.
themeBlock->createInstance(&appearance);
//Do some block specific stuff,
//e.g. reset the state according to block type,
//or load some missing part of block states from default appearance.
switch (type) {
case TYPE_FRAGILE:
{
const int f = flags & 0x3;
const char* s = (f == 0) ? "default" : ((f == 1) ? "fragile1" : ((f == 2) ? "fragile2" : "fragile3"));
appearance.changeState(s);
}
break;
case TYPE_BUTTON:
if (appearance.blockStates.find("button") == appearance.blockStates.end()) {
//Try to load the "button" state from default appearance
ThemeBlockInstance defaultAppearance;
objThemes.getBlock(type)->createInstance(&defaultAppearance);
auto it = defaultAppearance.blockStates.find("button");
if (it != defaultAppearance.blockStates.end() && it->second >= 0 && it->second < (int)defaultAppearance.states.size()) {
appearance.states.push_back(defaultAppearance.states[it->second]);
appearance.blockStates[it->first] = appearance.states.size() - 1;
}
}
break;
}
}
}
}
//Check if the data contains the id block.
it=obj.find("id");
if(it!=obj.end()){
//Set the id of the block.
id=obj["id"];
}
//Check if the data contains the visibility
it = obj.find("visible");
if (it != obj.end()) {
//Set the visibility.
const string& s = it->second;
flags = (flags & ~0x80000000) | ((s == "true" || atoi(s.c_str())) ? 0 : 0x80000000);
}
//Block specific properties.
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
{
//Make sure that the editor data contains MovingPosCount.
it=obj.find("MovingPosCount");
if(it!=obj.end()){
char s0[64];
int m=atoi(obj["MovingPosCount"].c_str());
movingPos.clear();
for(int i=0;i<m;i++){
SDL_Rect r={0,0,0,0};
sprintf(s0+1,"%d",i);
s0[0]='x';
r.x=atoi(obj[s0].c_str());
s0[0]='y';
r.y=atoi(obj[s0].c_str());
s0[0]='t';
r.w=atoi(obj[s0].c_str());
movingPos.push_back(r);
}
}
//Check if the activated or disabled key is in the data.
//NOTE: 'disabled' is obsolete in V0.5.
it=obj.find("activated");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x1;
if(!(s=="true" || atoi(s.c_str()))) flags|=0x1;
}else{
it=obj.find("disabled");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x1;
if(s=="true" || atoi(s.c_str())) flags|=0x1;
}
}
//Check if the loop key is in the data.
it=obj.find("loop");
if(it!=obj.end()){
const string& s=it->second;
flags |= 0x2;
if (s == "true" || atoi(s.c_str())) flags &= ~0x2;
}
}
break;
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
{
//Check if there's a speed key in the editor data.
//NOTE: 'speed' is obsolete in V0.5.
it=obj.find("speed10");
if(it!=obj.end()){
speed=atoi(it->second.c_str());
}else{
it = obj.find("speed");
if (it != obj.end()){
speed = atoi(it->second.c_str()) * 10;
}
}
//Check if the activated or disabled key is in the data.
//NOTE: 'disabled' is obsolete in V0.5.
it=obj.find("activated");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x1;
if(!(s=="true" || atoi(s.c_str()))) flags|=0x1;
}else{
it=obj.find("disabled");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x1;
if(s=="true" || atoi(s.c_str())) flags|=0x1;
}
}
}
break;
case TYPE_PORTAL:
{
//Check if the automatic key is in the data.
it=obj.find("automatic");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x1;
if(s=="true" || atoi(s.c_str())) flags|=0x1;
}
//Check if the destination key is in the data.
it=obj.find("destination");
if(it!=obj.end()){
destination=it->second;
}
}
break;
case TYPE_BUTTON:
case TYPE_SWITCH:
{
//Check if the behaviour key is in the data.
it=obj.find("behaviour");
if(it!=obj.end()){
const string& s=it->second;
flags&=~0x3;
if(s=="on" || s==_("On")) flags|=1;
else if(s=="off" || s==_("Off")) flags|=2;
}
}
break;
case TYPE_NOTIFICATION_BLOCK:
{
//Check if the message key is in the data.
it=obj.find("message");
if(it!=obj.end()){
message=it->second;
//Change the characters '\n' to a real \n
while(message.find("\\n")!=string::npos){
message=message.replace(message.find("\\n"),2,"\n");
}
}
}
break;
case TYPE_FRAGILE:
{
//Check if the status is in the data.
it=obj.find("state");
if(it!=obj.end()){
flags=(flags&~0x3)|(atoi(it->second.c_str())&0x3);
const int f = flags & 0x3;
const char* s=(f==0)?"default":((f==1)?"fragile1":((f==2)?"fragile2":"fragile3"));
appearance.changeState(s);
}
}
break;
}
}
std::string Block::getEditorProperty(const std::string& property){
//First get the complete editor data.
vector<pair<string,string> > objMap;
vector<pair<string,string> >::iterator it;
getEditorData(objMap);
//Loop through the entries.
for(it=objMap.begin();it!=objMap.end();++it){
if(it->first==property)
return it->second;
}
//Nothing found.
return "";
}
void Block::setEditorProperty(const std::string& property, const std::string& value){
//Create a map to hold the property.
std::map<std::string,std::string> editorData;
editorData[property]=value;
//And call the setEditorData method.
setEditorData(editorData);
}
bool Block::loadFromNode(ImageManager&, SDL_Renderer&, TreeStorageNode* objNode){
//Make sure there are enough parameters.
if(objNode->value.size()<3)
return false;
//Load the type and location.
int type = 0;
{
auto it = Game::blockNameMap.find(objNode->value[0]);
if (it != Game::blockNameMap.end()) {
type = it->second;
} else {
cerr << "WARNING: Unknown block type '" << objNode->value[0] << "'!" << endl;
}
}
int x=atoi(objNode->value[1].c_str());
int y=atoi(objNode->value[2].c_str());
int w=50;
int h=50;
if(objNode->value.size()>3)
w=atoi(objNode->value[3].c_str());
if(objNode->value.size()>4)
h=atoi(objNode->value[4].c_str());
//Call the init method.
init(x,y,w,h,type);
//Loop through the attributes as editorProperties.
map<string,string> obj;
for(map<string,vector<string> >::iterator i=objNode->attributes.begin();i!=objNode->attributes.end();++i){
if(i->second.size()>0) obj[i->first]=i->second[0];
}
setEditorData(obj);
//Loop through the subNodes.
for(unsigned int i=0;i<objNode->subNodes.size();i++){
//FIXME: Ugly variable naming.
TreeStorageNode* obj=objNode->subNodes[i];
if(obj==NULL) continue;
//Check for a script block.
if(obj->name=="script" && !obj->value.empty()){
map<string,int>::iterator it=Game::gameObjectEventNameMap.find(obj->value[0]);
if(it!=Game::gameObjectEventNameMap.end()){
int eventType=it->second;
const std::string& script=obj->attributes["script"][0];
if(!script.empty()) scripts[eventType]=script;
}
}
}
return true;
}
void Block::move(){
bool isPlayMode = stateID != STATE_LEVEL_EDITOR || dynamic_cast<LevelEditor*>(parent)->isPlayMode();
//Make sure we are visible, if not return.
if ((flags & 0x80000000) != 0 && isPlayMode)
return;
//First update the animation of the appearance.
appearance.updateAnimation();
//Block specific move code.
switch(type){
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
//Only move block when we are in play mode.
if (isPlayMode) {
//Make sure the block is enabled, if so increase the time.
if(!(flags&0x1)) temp++;
int t=temp;
SDL_Rect r0={0,0,0,0},r1;
dx=0;
dy=0;
//Loop through the moving positions.
for(unsigned int i=0;i<movingPos.size();i++){
r1.x=movingPos[i].x;
r1.y=movingPos[i].y;
r1.w=movingPos[i].w;
if(t==0&&r1.w==0){ // time == 0 means the block deactivates at this point automatically
r1.w=1;
flags|=0x1;
}
if(t>=0 && t<(int)r1.w){
int newX=boxBase.x+(int)(float(r0.x)+(float(r1.x)-float(r0.x))*float(t)/float(r1.w));
int newY=boxBase.y+(int)(float(r0.y)+(float(r1.y)-float(r0.y))*float(t)/float(r1.w));
//Calculate the delta and velocity.
xVel=dx=newX-box.x;
yVel=dy=newY-box.y;
//Set the new location of the moving block.
box.x=newX;
box.y=newY;
return;
} else if (t == (int)r1.w && i == movingPos.size() - 1) {
//If the time is the time of the movingPosition then set it equal to the location.
//We do this to prevent a slight edge between normal blocks and moving blocks.
int newX=boxBase.x+r1.x;
int newY=boxBase.y+r1.y;
xVel=dx=newX-box.x;
yVel=dy=newY-box.y;
box.x=newX;
box.y=newY;
return;
}
t-=r1.w;
r0.x=r1.x;
r0.y=r1.y;
}
//Only reset the stuff when we're looping.
if((flags & 0x2) == 0){
//Set the time back to zero.
temp=0;
//Calculate the delta movement.
if(!movingPos.empty() && movingPos.back().x==0 && movingPos.back().y==0){
dx=boxBase.x-box.x;
dy=boxBase.y-box.y;
}
//Set the movingblock back to it's initial location.
box.x=boxBase.x;
box.y=boxBase.y;
} else {
//Reached the end, but not looping
xVel=yVel=dx=dy=0;
}
}
break;
case TYPE_BUTTON:
//Only move block when we are in play mode.
if (isPlayMode) {
//Check the third bit of flags to see if temp changed.
int new_flags=temp?4:0;
if((flags^new_flags)&4){
//The button has been pressed or unpressed so change the third bit on flags.
flags=(flags&~4)|new_flags;
if(parent && (new_flags || (flags&3)==0)){
//Make sure that id isn't empty.
if(!id.empty()){
parent->broadcastObjectEvent(0x10000|(flags&3),-1,id.c_str());
}else{
cerr<<"WARNING: invalid button id!"<<endl;
}
}
}
temp=0;
}
break;
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
//NOTE: we update conveyor belt animation even in edit mode.
//Increase the conveyor belt animation.
if((flags&1)==0){
//Since now 1 speed = 0.1 pixel/s we need some more sophisticated calculation.
int a = animation + speed, d = 0;
if (a < 0) {
//Add a delta value to make it positive
d = (((-a) / 500) + 1) * 500;
}
//Set the velocity NOTE This isn't the actual velocity of the block, but the speed of the player/shadow standing on it.
xVel = (a + d) / 10 - (animation + d) / 10;
//Update animation value
animation = (a + d) % 500;
assert(animation >= 0);
} else {
//Clear the velocity NOTE This isn't the actual velocity of the block, but the speed of the player/shadow standing on it.
xVel = 0;
}
break;
case TYPE_PUSHABLE:
//Only move block when we are in play mode.
if (isPlayMode) {
//Update the vertical velocity, horizontal is set by the player.
if(inAir==true){
yVel+=1;
//Cap fall speed to 13.
if(yVel>13)
yVel=13;
}
if (auto ocs = objCurrentStand.get()) {
//Now get the velocity and delta of the object the player is standing on.
SDL_Rect v=ocs->getBox(BoxType_Velocity);
SDL_Rect delta=ocs->getBox(BoxType_Delta);
switch(ocs->type){
//For conveyor belts the velocity is transfered.
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
{
xVelBase+=v.x;
}
break;
//In other cases, such as, player on shadow, player on crate... the change in x position must be considered.
default:
{
if(delta.x != 0)
xVelBase+=delta.x;
}
break;
}
//NOTE: Only copy the velocity of the block when moving down.
//Upwards is automatically resolved before the player is moved.
if(delta.y>0)
yVelBase=delta.y;
else
yVelBase=0;
}
//Set the object the player is currently standing to NULL.
objCurrentStand=NULL;
//Store the location of the player.
int lastX=box.x;
int lastY=box.y;
//An array that will hold all the GameObjects that are involved in the collision/movement.
vector<Block*> objects;
//All the blocks have moved so if there's collision with the player, the block moved into him.
for(auto o : parent->levelObjects){
//Make sure to only check visible blocks.
if(o->flags & 0x80000000)
continue;
//Make sure we aren't the block.
if(o==this)
continue;
//Make sure the object is spike or solid for the player.
if(o->type!=TYPE_SPIKES && o->type!=TYPE_MOVING_SPIKES && !o->queryProperties(GameObjectProperty_PlayerCanWalkOn,&parent->player))
continue;
//Check for collision.
if(checkCollision(box,o->getBox()))
objects.push_back(o);
}
//There was collision so try to resolve it.
if(!objects.empty()){
//FIXME: When multiple moving blocks are overlapping the pushable can be "bounced" off depending on the block order.
for(auto o : objects){
SDL_Rect r=o->getBox();
SDL_Rect delta=o->getBox(BoxType_Delta);
//Check on which side of the box the pushable is.
if(delta.x!=0){
if(delta.x>0){
//Move the pushable right if necessary.
if((r.x+r.w)-box.x<=delta.x && box.x<r.x+r.w)
box.x=r.x+r.w;
}else{
//Move the pushable left if necessary.
if((box.x+box.w)-r.x<=-delta.x && box.x>r.x-box.w)
box.x=r.x-box.w;
}
}
if(delta.y!=0){
if(delta.y>0){
//Move the pushable down if necessary.
if((r.y+r.h)-box.y<=delta.y && box.y<r.y+r.h)
box.y=r.y+r.h;
}else{
//Move the pushable up if necessary.
if((box.y+box.h)-r.y<=-delta.y && box.y>r.y-box.h)
box.y=r.y-box.h;
}
}
}
}
//Reuse the objects array, this time for blocks the block moves into.
objects.clear();
//Determine the collision frame.
SDL_Rect frame={box.x,box.y,box.w,box.h};
//Keep the horizontal movement of the block in mind.
if(xVel+xVelBase>=0){
frame.w+=(xVel+xVelBase);
}else{
frame.x+=(xVel+xVelBase);
frame.w-=(xVel+xVelBase);
}
//And the vertical movement.
if(yVel+yVelBase>=0){
frame.h+=(yVel+yVelBase);
}else{
frame.y+=(yVel+yVelBase);
frame.h-=(yVel+yVelBase);
}
//Loop through the game objects.
for(auto o : parent->levelObjects){
//Make sure the object is visible.
if(o->flags & 0x80000000)
continue;
//Make sure we aren't the block.
if(o==this)
continue;
//Check if the player can collide with this game object, or this object is spike.
if(o->type!=TYPE_SPIKES && o->type!=TYPE_MOVING_SPIKES && !o->queryProperties(GameObjectProperty_PlayerCanWalkOn,&parent->player))
continue;
//Check if the block is inside the frame.
if(checkCollision(frame,o->getBox()))
objects.push_back(o);
}
//Horizontal pass.
if(xVel+xVelBase!=0){
box.x+=xVel+xVelBase;
for(auto o : objects){
SDL_Rect r=o->getBox();
if(!checkCollision(box,r))
continue;
if(xVel+xVelBase>0){
//We came from the left so the right edge of the player must be less or equal than xVel+xVelBase.
if((box.x+box.w)-r.x<=xVel+xVelBase)
box.x=r.x-box.w;
}else{
//We came from the right so the left edge of the player must be greater or equal than xVel+xVelBase.
if(box.x-(r.x+r.w)>=xVel+xVelBase)
box.x=r.x+r.w;
}
}
}
//Some variables that are used in vertical movement.
Block* lastStand=NULL;
inAir=true;
//Vertical pass.
if(yVel+yVelBase!=0){
box.y+=yVel+yVelBase;
for(auto o : objects){
SDL_Rect r=o->getBox();
if(!checkCollision(box,r))
continue;
//Now check how we entered the block (vertically or horizontally).
if(yVel+yVelBase>0){
//We came from the top so the bottom edge of the player must be less or equal than yVel+yVelBase.
if((box.y+box.h)-r.y<=yVel+yVelBase){
//NOTE: lastStand is handled later since the player can stand on only one block at the time.
//Check if there's already a lastStand.
if(lastStand){
//There is one, so check 'how much' the player is on the blocks.
SDL_Rect r=o->getBox();
int w=0;
if(box.x+box.w>r.x+r.w)
w=(r.x+r.w)-box.x;
else
w=(box.x+box.w)-r.x;
//Do the same for the other box.
r=lastStand->getBox();
int w2=0;
if(box.x+box.w>r.x+r.w)
w2=(r.x+r.w)-box.x;
else
w2=(box.x+box.w)-r.x;
//NOTE: It doesn't matter which block the player is on if they are both stationary.
SDL_Rect v=o->getBox(BoxType_Velocity);
SDL_Rect v2=lastStand->getBox(BoxType_Velocity);
if(v.y==v2.y){
if(w>w2)
lastStand=o;
}else if(v.y<v2.y){
lastStand=o;
}
}else{
lastStand=o;
}
}
}else{
//We came from the bottom so the upper edge of the player must be greater or equal than yVel+yVelBase.
if(box.y-(r.y+r.h)>=yVel+yVelBase){
box.y=r.y+r.h;
yVel=0;
}
}
}
}
if(lastStand){
inAir=false;
yVel=1;
SDL_Rect r=lastStand->getBox();
box.y=r.y-box.h;
}
//Block will currently be standing on whatever it was last standing on.
objCurrentStand=lastStand;
dx=box.x-lastX;
dy=box.y-lastY;
xVel=0;
xVelBase=0;
}
break;
}
}
diff --git a/src/CachedTexture.h b/src/CachedTexture.h
index a8c7a9c..271e230 100644
--- a/src/CachedTexture.h
+++ b/src/CachedTexture.h
@@ -1,44 +1,48 @@
#ifndef CACHEDTEXTURE_H
#define CACHEDTEXTURE_H
#include "Render.h"
//A class to cache textures that display stuff that is updated regularly
template <typename T>
class CachedTexture
{
private:
T id;
TexturePtr tex;
public:
CachedTexture() {}
TexturePtr& getTexture(){
return tex;
}
SDL_Texture* get(){
return tex.get();
}
+ const T& getId() const {
+ return id;
+ }
+
// Check if the texture exists.
bool exists() {
return tex.get() != nullptr;
}
// Check if the contained texture has to be updated.
bool needsUpdate(const T& id){
if (this->id!=id || !tex) {
return true;
} else {
return false;
}
}
//Update the contained texture with a new id and texture.
void update(T id, TexturePtr&& texture) {
this->id = id;
this->tex = std::move(texture);
}
};
#endif // CACHEDTEXTURE_H
diff --git a/src/Game.cpp b/src/Game.cpp
index be1105c..535f3ed 100644
--- a/src/Game.cpp
+++ b/src/Game.cpp
@@ -1,1861 +1,1867 @@
/*
* 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 <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)
,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),totalCollectables(0),currentCollectablesSaved(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.
LEVEL_WIDTH=800;
LEVEL_HEIGHT=600;
currentCollectables=0;
totalCollectables=0;
currentCollectablesSaved=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.
LEVEL_WIDTH=atoi(i->second[0].c_str());
LEVEL_HEIGHT=atoi(i->second[1].c_str());
}
}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)
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);
//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;
string s=fileName;
//Parse the file.
if(!objSerializer.loadNodeFromFile(s.c_str(),&obj,true)){
cerr<<"ERROR: Can't load record file "<<s<<endl;
return;
}
}
//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],"???");
//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.
//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();
//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));
transform(keyCode.begin(),keyCode.end(),keyCode.begin(),::toupper);
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));
transform(keyCodeRestart.begin(),keyCodeRestart.end(),keyCodeRestart.begin(),::toupper);
//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));
transform(keyCodeLoad.begin(),keyCodeLoad.end(),keyCodeLoad.begin(),::toupper);
//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 && totalCollectables!=0 && !interlevel && time>0){
if(collectablesTexture.needsUpdate(currentCollectables)) {
//Temp stringstream just to addup all the text nicely
std::stringstream temp;
temp << currentCollectables << "/" << totalCollectables;
collectablesTexture.update(currentCollectables,
textureFromText(renderer,
*fontText,
temp.str().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.
if(stateID==STATE_LEVEL_EDITOR && 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());
applyTexture(0,y,*recordingsTexture.get(), renderer);
if(timeTexture.needsUpdate(time)) {
const size_t len = 32;
char c[len];
SDL_snprintf(c,len,"%-.2fs",time/40.0);
timeTexture.update(time,
textureFromTextShaded(
renderer,
*fontText,
c,
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);
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 camera mode and target.
cameraModeSaved=cameraMode;
cameraTargetSaved=cameraTarget;
//Save the current collectables
currentCollectablesSaved=currentCollectables;
//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 camera mode and target.
cameraMode=cameraModeSaved;
cameraTarget=cameraTargetSaved;
//Load the current collactbles
currentCollectables=currentCollectablesSaved;
//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;
}
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 camera.
cameraMode=CAMERA_PLAYER;
if(save) cameraModeSaved=CAMERA_PLAYER;
cameraTarget.x=cameraTarget.y=cameraTarget.w=cameraTarget.h=0;
if(save) cameraTargetSaved.x=cameraTargetSaved.y=cameraTargetSaved.w=cameraTargetSaved.h=0;
//Reset the number of collectables
currentCollectables=0;
if(save)
currentCollectablesSaved=0;
//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/Game.h b/src/Game.h
index 6b000f6..7417c84 100644
--- a/src/Game.h
+++ b/src/Game.h
@@ -1,306 +1,311 @@
/*
* 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/>.
*/
#ifndef GAME_H
#define GAME_H
#include <SDL.h>
#include <array>
#include <vector>
#include <map>
#include <string>
#include "CachedTexture.h"
#include "GameState.h"
#include "GUIObject.h"
#include "Block.h"
#include "Scenery.h"
#include "SceneryLayer.h"
#include "Player.h"
#include "Render.h"
#include "Shadow.h"
//This structure contains variables that make a GameObjectEvent.
struct typeGameObjectEvent{
//The type of event.
int eventType;
//The type of object that should react to the event.
int objectType;
//Flags, 0x1 means use the id.
int flags;
//Blocks with this id should react to the event.
std::string id;
//Optional pointer to the block the event should be called on.
GameObject* target;
};
class ThemeManager;
class ThemeBackground;
class TreeStorageNode;
class ScriptExecutor;
//The different level events.
enum LevelEventType{
//Event called when the level is created or the game is reset. This happens after all the blocks are created and their onCreate is called.
LevelEvent_OnCreate=1,
//Event called when the game is saved.
LevelEvent_OnSave,
//Event called when the game is loaded.
LevelEvent_OnLoad,
};
class Game : public GameState,public GUIEventCallback{
private:
//Boolean if the game should reset. This happens when player press 'R' button.
bool isReset;
//The script executor.
ScriptExecutor *scriptExecutor;
protected:
//contains currently played level.
TreeStorageNode* currentLevelNode;
//Array containing "tooltips" for certain block types.
//It will be shown in the topleft corner of the screen.
std::array<TexturePtr, TYPE_MAX> bmTips;
//Texture containing the action images (record, play, etc..)
SharedTexture action;
//Texture containing the medal image.
SharedTexture medals;
//ThemeBlockInstance containing the collectable image.
ThemeBlockInstance collectable;
//The name of the current level.
std::string levelName;
//The path + file of the current level.
std::string levelFile;
//Editor data containing information like name, size, etc...
std::map<std::string,std::string> editorData;
//Vector used to queue the gameObjectEvents.
std::vector<typeGameObjectEvent> eventQueue;
//The themeManager.
ThemeManager* customTheme;
//The themeBackground.
ThemeBackground* background;
CachedTexture<int> collectablesTexture;
CachedTexture<int> recordingsTexture;
CachedTexture<int> timeTexture;
CachedTexture<Block*> notificationTexture;
//Load a level from node.
//After calling this function the ownership of
//node will transfer to Game class. So don't delete
//the node after calling this function!
virtual void loadLevelFromNode(ImageManager& imageManager, SDL_Renderer& renderer, TreeStorageNode* obj, const std::string& fileName);
//(internal function) Reload the music according to level music and level pack music.
//This is mainly used in level editor.
void reloadMusic();
public:
//Array used to convert GameObject type->string.
static const char* blockName[TYPE_MAX];
//Map used to convert GameObject string->type.
static std::map<std::string,int> blockNameMap;
//Map used to convert GameObjectEventType type->string.
static std::map<int,std::string> gameObjectEventTypeMap;
//Map used to convert GameObjectEventType string->type.
static std::map<std::string,int> gameObjectEventNameMap;
//Map used to convert LevelEventType type->string.
static std::map<int,std::string> levelEventTypeMap;
//Map used to convert LevelEventType string->type.
static std::map<std::string,int> levelEventNameMap;
//Boolean that is set to true when a game is won.
bool won;
//Boolean that is set to true when we should save game on next logic update.
bool saveStateNextTime;
//Boolean that is set to true when we should load game on next logic update.
bool loadStateNextTime;
//Boolean if the replaying currently done is for the interlevel screen.
bool interlevel;
//Integer containing the current tip index.
int gameTipIndex;
//Integer containing the number of ticks passed since the start of the level.
int time;
//Integer containing the stored value of time.
int timeSaved;
//Integer containing the number of recordings it took to finish.
int recordings;
//Integer containing the stored value of recordings.
int recordingsSaved;
//Integer keeping track of currently obtained collectables
int currentCollectables;
//Integer keeping track of total colletables in the level
int totalCollectables;
//Integer containing the stored value of current collectables
int currentCollectablesSaved;
//Time of recent swap, for achievements. (in game-ticks)
int recentSwap,recentSwapSaved;
//Store time of recent save/load for achievements (in millisecond)
Uint32 recentLoad,recentSave;
//Enumeration with the different camera modes.
enum CameraMode{
CAMERA_PLAYER,
CAMERA_SHADOW,
CAMERA_CUSTOM
};
//The current camera mode.
CameraMode cameraMode;
//Rectangle containing the target for the camera.
SDL_Rect cameraTarget;
//The saved cameraMode.
CameraMode cameraModeSaved;
SDL_Rect cameraTargetSaved;
//Level scripts.
std::map<int,std::string> scripts;
//Compiled scripts. Use lua_rawgeti(L, LUA_REGISTRYINDEX, r) to get the function.
std::map<int, int> compiledScripts, savedCompiledScripts, initialCompiledScripts;
//Vector containing all the levelObjects in the current game.
std::vector<Block*> levelObjects, levelObjectsSave, levelObjectsInitial;
//The layers for the scenery.
// We utilize the fact that std::map is sorted, and we compare the layer name with "f",
// If name<"f" then it's background layer, if name>="f" then it's foreground layer.
// NOTE: the layer name is case sensitive, in particular if the name start with capital "F"
// then it is still background layer.
std::map<std::string,SceneryLayer*> sceneryLayers;
//The player...
Player player;
//... and his shadow.
Shadow shadow;
//warning: weak reference only, may point to invalid location
Block::ObservePointer objLastCheckPoint;
//Constructor.
Game(SDL_Renderer& renderer, ImageManager& imageManager);
//If this is not empty then when next Game class is created
//it will play this record file.
static std::string recordFile;
//Destructor.
//It will call destroy();
~Game();
//Method used to clean up the GameState.
void destroy();
//Inherited from GameState.
void handleEvents(ImageManager&imageManager, SDL_Renderer&renderer) override;
void logic(ImageManager& imageManager, SDL_Renderer& renderer) override;
void render(ImageManager&,SDL_Renderer& renderer) override;
void resize(ImageManager& imageManager, SDL_Renderer& renderer) override;
//This method will load a level.
//fileName: The fileName of the level.
virtual void loadLevel(ImageManager& imageManager, SDL_Renderer& renderer, std::string fileName);
//Method used to broadcast a GameObjectEvent.
//eventType: The type of event.
//objectType: The type of object that should react to the event.
//id: The id of the blocks that should react.
//target: Pointer to the object
void broadcastObjectEvent(int eventType,int objectType=-1,const char* id=NULL,GameObject* target=NULL);
//Compile all scripts and run onCreate script.
//NOTE: Call this function only after script state reset, or there will be some memory leaks.
void compileScript();
//Method that will check if a script for a given levelEvent is present.
//If that's the case the script will be executed.
//eventType: The level event type to execute.
void inline executeScript(int eventType);
//Returns if the player and shadow can save the current state.
bool canSaveState();
//Method used to store the current state.
//This is used for checkpoints.
//Returns: True if it succeeds without problems.
bool saveState();
//Method used to load the stored state.
//This is used for checkpoints.
//Returns: True if it succeeds without problems.
bool loadState();
//Method that will reset the GameState to it's initial state.
//save: Boolean if the saved state should also be deleted. This also means recreate the Lua context.
//noScript: Boolean if we should not compile the script at all. This is used by level editor when exiting test play.
void reset(bool save,bool noScript);
//Save current game record to the file.
//fileName: The filename of the destination file.
void saveRecord(const char* fileName);
//Load game record (and its level) from file and play it.
//fileName: The filename of the recording file.
void loadRecord(ImageManager& imageManager, SDL_Renderer& renderer, const char* fileName);
//Method called by the player (or shadow) when he finished.
void replayPlay(ImageManager& imageManager, SDL_Renderer &renderer);
//Method that gets called when the recording has ended.
void recordingEnded(ImageManager& imageManager, SDL_Renderer& renderer);
//get current level's auto-save record path,
//using current level's MD5, file name and other information.
void getCurrentLevelAutoSaveRecordPath(std::string &bestTimeFilePath,std::string &bestRecordingFilePath,bool createPath);
//Method that will prepare the gamestate for the next level and start it.
//If it's the last level it will show the congratulations text and return to the level select screen.
void gotoNextLevel(ImageManager& imageManager, SDL_Renderer& renderer);
//Get the name of the current level.
const std::string& getLevelName(){
return levelName;
}
//GUI event handling is done here.
void GUIEventCallback_OnEvent(ImageManager&imageManager, SDL_Renderer&renderer, std::string name, GUIObject* obj, int eventType) override;
//Get the script executor.
ScriptExecutor* getScriptExecutor() {
return scriptExecutor;
}
+
+ //Invalidates the notification texture.
+ //block: The block which is updated. The cached texture won't be invalidated if it's not for this block.
+ //NULL means invalidates the texture no matter which block is updated.
+ void invalidateNotificationTexture(Block *block = NULL);
};
#endif
diff --git a/src/ScriptAPI.cpp b/src/ScriptAPI.cpp
index df40171..3b4bca9 100644
--- a/src/ScriptAPI.cpp
+++ b/src/ScriptAPI.cpp
@@ -1,2014 +1,2186 @@
/*
* Copyright (C) 2012-2013 Me and My Shadow
*
* This file is part of Me and My Shadow.
*
* Me and My Shadow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Me and My Shadow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Me and My Shadow. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ScriptAPI.h"
#include "ScriptExecutor.h"
#include "SoundManager.h"
#include "Functions.h"
#include "Block.h"
#include "Game.h"
#include "MusicManager.h"
#include "ScriptDelayExecution.h"
#include <iostream>
#include <algorithm>
using namespace std;
/////////////////////////// HELPER MACRO ///////////////////////////
#define HELPER_GET_AND_CHECK_ARGS(ARGS) \
int args = lua_gettop(state); \
if(args != ARGS) { \
return luaL_error(state, "Incorrect number of arguments for %s, expected %d.", __FUNCTION__, ARGS); \
}
#define HELPER_GET_AND_CHECK_ARGS_RANGE(ARGS1, ARGS2) \
int args = lua_gettop(state); \
if(args < ARGS1 || args > ARGS2) { \
return luaL_error(state, "Incorrect number of arguments for %s, expected %d-%d.", __FUNCTION__, ARGS1, ARGS2); \
}
#define HELPER_GET_AND_CHECK_ARGS_2(ARGS1, ARGS2) \
int args = lua_gettop(state); \
if(args != ARGS1 && args != ARGS2) { \
return luaL_error(state, "Incorrect number of arguments for %s, expected %d or %d.", __FUNCTION__, ARGS1, ARGS2); \
}
#define HELPER_GET_AND_CHECK_ARGS_AT_LEAST(ARGS) \
int args = lua_gettop(state); \
if(args < ARGS) { \
return luaL_error(state, "Incorrect number of arguments for %s, expected at least %d.", __FUNCTION__, ARGS); \
}
#define HELPER_GET_AND_CHECK_ARGS_AT_MOST(ARGS) \
int args = lua_gettop(state); \
if(args > ARGS) { \
return luaL_error(state, "Incorrect number of arguments for %s, expected at most %d.", __FUNCTION__, ARGS); \
}
//================================================================
#define HELPER_CHECK_ARGS_TYPE(INDEX, TYPE) \
if(!lua_is##TYPE(state,INDEX)) { \
return luaL_error(state, "Invalid type for argument %d of %s, should be %s.", INDEX, __FUNCTION__, #TYPE); \
}
#define HELPER_CHECK_ARGS_TYPE_NO_HINT(INDEX, TYPE) \
if(!lua_is##TYPE(state,INDEX)) { \
return luaL_error(state, "Invalid type for argument %d of %s.", INDEX, __FUNCTION__); \
}
#define HELPER_CHECK_ARGS_TYPE_2(INDEX, TYPE1, TYPE2) \
if(!lua_is##TYPE1(state,INDEX) && !lua_is##TYPE2(state,INDEX)) { \
return luaL_error(state, "Invalid type for argument %d of %s, should be %s or %s.", INDEX, __FUNCTION__, #TYPE1, #TYPE2); \
}
#define HELPER_CHECK_ARGS_TYPE_2_NO_HINT(INDEX, TYPE1, TYPE2) \
if(!lua_is##TYPE1(state,INDEX) && !lua_is##TYPE2(state,INDEX)) { \
return luaL_error(state, "Invalid type for argument %d of %s.", INDEX, __FUNCTION__); \
}
#define HELPER_CHECK_ARGS_TYPE_OR_NIL(INDEX, TYPE) \
HELPER_CHECK_ARGS_TYPE_2(INDEX, TYPE, nil)
#define HELPER_CHECK_ARGS_TYPE_OR_NIL_NO_HINT(INDEX, TYPE) \
HELPER_CHECK_ARGS_TYPE_2_NO_HINT(INDEX, TYPE, nil)
//================================================================
#define HELPER_CHECK_OPTIONAL_ARGS_TYPE(INDEX, TYPE) \
if(args>=INDEX && !lua_is##TYPE(state,INDEX)) { \
return luaL_error(state, "Invalid type for argument %d of %s, should be %s.", INDEX, __FUNCTION__, #TYPE); \
}
#define HELPER_CHECK_OPTIONAL_ARGS_TYPE_NO_HINT(INDEX, TYPE) \
if(args>=INDEX && !lua_is##TYPE(state,INDEX)) { \
return luaL_error(state, "Invalid type for argument %d of %s.", INDEX, __FUNCTION__); \
}
#define HELPER_CHECK_OPTIONAL_ARGS_TYPE_2(INDEX, TYPE1, TYPE2) \
if(args>=INDEX && !lua_is##TYPE1(state,INDEX) && !lua_is##TYPE2(state,INDEX)) { \
return luaL_error(state, "Invalid type for argument %d of %s, should be %s or %s.", INDEX, __FUNCTION__, #TYPE1, #TYPE2); \
}
#define HELPER_CHECK_OPTIONAL_ARGS_TYPE_2_NO_HINT(INDEX, TYPE1, TYPE2) \
if(args>=INDEX && !lua_is##TYPE1(state,INDEX) && !lua_is##TYPE2(state,INDEX)) { \
return luaL_error(state, "Invalid type for argument %d of %s.", INDEX, __FUNCTION__); \
}
#define HELPER_CHECK_OPTIONAL_ARGS_TYPE_OR_NIL(INDEX, TYPE) \
HELPER_CHECK_OPTIONAL_ARGS_TYPE_2(INDEX, TYPE, nil)
#define HELPER_CHECK_OPTIONAL_ARGS_TYPE_OR_NIL_NO_HINT(INDEX, TYPE) \
HELPER_CHECK_OPTIONAL_ARGS_TYPE_2_NO_HINT(INDEX, TYPE, nil)
//================================================================
#define HELPER_REGISTER_IS_VALID_FUNCTION(CLASS) \
int isValid(lua_State* state){ \
HELPER_GET_AND_CHECK_ARGS(1); \
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata); \
CLASS* object = CLASS::getObjectFromUserData(state, 1); \
lua_pushboolean(state, object ? 1 : 0); \
return 1; \
}
//================================================================
#define _F(FUNC) { #FUNC, _L::FUNC }
#define _FG(FUNC) _F(get##FUNC)
#define _FI(FUNC) _F(is##FUNC)
#define _FS(FUNC) _F(set##FUNC)
#define _FGS(FUNC) _F(get##FUNC), _F(set##FUNC)
#define _FIS(FUNC) _F(is##FUNC), _F(set##FUNC)
///////////////////////////BLOCK SPECIFIC///////////////////////////
class BlockScriptAPI {
public:
static int getFlags(const Block* block) {
return block->flags;
}
static void setFlags(Block* block, int flags) {
block->flags = flags;
}
static void fragileUpdateState(Block* block, int state) {
state &= 0x3;
block->flags = (block->flags & ~0x3) | state;
const char* s = (state == 0) ? "default" : ((state == 1) ? "fragile1" : ((state == 2) ? "fragile2" : "fragile3"));
block->appearance.changeState(s);
}
static int getTemp(const Block* block) {
return block->temp;
}
static void setTemp(Block* block, int value) {
block->temp = value;
}
static int getSpeed(const Block* block) {
return block->speed;
}
static void setSpeed(Block* block, int value) {
block->speed = value;
}
};
namespace block {
HELPER_REGISTER_IS_VALID_FUNCTION(Block);
int getBlockById(lua_State* state){
//Get the number of args, this MUST be one.
HELPER_GET_AND_CHECK_ARGS(1);
//Make sure the given argument is an id (string).
HELPER_CHECK_ARGS_TYPE(1, string);
//Check if the currentState is the game state.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
//Get the actual game object.
string id = lua_tostring(state, 1);
std::vector<Block*>& levelObjects = game->levelObjects;
Block* object = NULL;
for (unsigned int i = 0; i < levelObjects.size(); i++){
if (levelObjects[i]->getEditorProperty("id") == id){
object = levelObjects[i];
break;
}
}
if (object == NULL){
//Unable to find the requested object.
//Return nothing, will result in a nil in the script.
return 0;
}
//Create the userdatum.
object->createUserData(state, "block");
//We return one object, the userdatum.
return 1;
}
int getBlocksById(lua_State* state){
//Get the number of args, this MUST be one.
HELPER_GET_AND_CHECK_ARGS(1);
//Make sure the given argument is an id (string).
HELPER_CHECK_ARGS_TYPE(1, string);
//Check if the currentState is the game state.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
//Get the actual game object.
string id = lua_tostring(state, 1);
std::vector<Block*>& levelObjects = game->levelObjects;
std::vector<Block*> result;
for (unsigned int i = 0; i < levelObjects.size(); i++){
if (levelObjects[i]->getEditorProperty("id") == id){
result.push_back(levelObjects[i]);
}
}
//Create the table that will hold the result.
lua_createtable(state, result.size(), 0);
//Loop through the results.
for (unsigned int i = 0; i < result.size(); i++){
//Create the userdatum.
result[i]->createUserData(state, "block");
//And set the table.
lua_rawseti(state, -2, i + 1);
}
//We return one object, the userdatum.
return 1;
}
int moveTo(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(3);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
HELPER_CHECK_ARGS_TYPE(3, integer);
//Now get the pointer to the object.
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
int x = lua_tointeger(state, 2);
int y = lua_tointeger(state, 3);
object->moveTo(x, y);
return 0;
}
int getLocation(lua_State* state){
//Make sure there's only one argument and that argument is an userdatum.
HELPER_GET_AND_CHECK_ARGS(1);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
//Get the object.
lua_pushinteger(state, object->getBox().x);
lua_pushinteger(state, object->getBox().y);
return 2;
}
int setLocation(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(3);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
HELPER_CHECK_ARGS_TYPE(3, integer);
//Now get the pointer to the object.
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
int x = lua_tointeger(state, 2);
int y = lua_tointeger(state, 3);
object->setLocation(x, y);
return 0;
}
int growTo(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(3);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
HELPER_CHECK_ARGS_TYPE(3, integer);
//Now get the pointer to the object.
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
int w = lua_tointeger(state, 2);
int h = lua_tointeger(state, 3);
object->growTo(w, h);
return 0;
}
int getSize(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
//Get the object.
lua_pushinteger(state, object->getBox().w);
lua_pushinteger(state, object->getBox().h);
return 2;
}
int setSize(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(3);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
HELPER_CHECK_ARGS_TYPE(3, integer);
//Now get the pointer to the object.
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
int w = lua_tointeger(state, 2);
int h = lua_tointeger(state, 3);
object->setSize(w, h);
return 0;
}
int getType(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL || object->type < 0 || object->type >= TYPE_MAX) return 0;
lua_pushstring(state, Game::blockName[object->type]);
return 1;
}
int changeThemeState(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, string);
Block* object = Block::getObjectFromUserData(state, 1);
object->appearance.changeState(lua_tostring(state, 2));
return 0;
}
int setVisible(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, boolean);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL)
return 0;
BlockScriptAPI::setFlags(object,
(BlockScriptAPI::getFlags(object) & ~0x80000000) | (lua_toboolean(state, 2) ? 0 : 0x80000000)
);
return 0;
}
int isVisible(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL)
return 0;
lua_pushboolean(state, (BlockScriptAPI::getFlags(object) & 0x80000000) ? 0 : 1);
return 1;
}
int getEventHandler(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, string);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
//Check event type
string eventType = lua_tostring(state, 2);
map<string, int>::iterator it = Game::gameObjectEventNameMap.find(eventType);
if (it == Game::gameObjectEventNameMap.end()) return 0;
//Check compiled script
map<int, int>::iterator script = object->compiledScripts.find(it->second);
if (script == object->compiledScripts.end()) return 0;
//Get event handler
lua_rawgeti(state, LUA_REGISTRYINDEX, script->second);
return 1;
}
//It will return old event handler.
int setEventHandler(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(3);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, string);
HELPER_CHECK_ARGS_TYPE_OR_NIL(3, function);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
//Check event type
string eventType = lua_tostring(state, 2);
map<string, int>::const_iterator it = Game::gameObjectEventNameMap.find(eventType);
if (it == Game::gameObjectEventNameMap.end()){
lua_pushfstring(state, "Unknown block event type: '%s'.", eventType.c_str());
return lua_error(state);
}
//Check compiled script
int scriptIndex = LUA_REFNIL;
{
map<int, int>::iterator script = object->compiledScripts.find(it->second);
if (script != object->compiledScripts.end()) scriptIndex = script->second;
}
//Set new event handler
object->compiledScripts[it->second] = luaL_ref(state, LUA_REGISTRYINDEX);
if (scriptIndex == LUA_REFNIL) return 0;
//Get old event handler and unreference it
lua_rawgeti(state, LUA_REGISTRYINDEX, scriptIndex);
luaL_unref(state, LUA_REGISTRYINDEX, scriptIndex);
return 1;
}
int onEvent(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, string);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
//Check event type
string eventType = lua_tostring(state, 2);
map<string, int>::const_iterator it = Game::gameObjectEventNameMap.find(eventType);
if (it == Game::gameObjectEventNameMap.end()){
lua_pushfstring(state, "Unknown block event type: '%s'.", eventType.c_str());
return lua_error(state);
}
object->onEvent(it->second);
return 0;
}
int isActivated(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
lua_pushboolean(state, (BlockScriptAPI::getFlags(object) & 0x1) ? 0 : 1);
return 1;
default:
return 0;
}
}
int setActivated(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, boolean);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
BlockScriptAPI::setFlags(object,
(BlockScriptAPI::getFlags(object) & ~1) | (lua_toboolean(state, 2) ? 0 : 1)
);
break;
}
return 0;
}
int isAutomatic(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_PORTAL:
lua_pushboolean(state, (BlockScriptAPI::getFlags(object) & 0x1) ? 1 : 0);
return 1;
default:
return 0;
}
}
int setAutomatic(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, boolean);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_PORTAL:
BlockScriptAPI::setFlags(object,
(BlockScriptAPI::getFlags(object) & ~1) | (lua_toboolean(state, 2) ? 1 : 0)
);
break;
}
return 0;
}
int getBehavior(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_BUTTON:
case TYPE_SWITCH:
switch (BlockScriptAPI::getFlags(object) & 0x3) {
default:
lua_pushstring(state, "toggle");
break;
case 1:
lua_pushstring(state, "on");
break;
case 2:
lua_pushstring(state, "off");
break;
}
return 1;
default:
return 0;
}
}
int setBehavior(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, string);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_BUTTON:
case TYPE_SWITCH:
{
int newFlags = BlockScriptAPI::getFlags(object) & ~3;
std::string s = lua_tostring(state, 2);
if (s == "on") newFlags |= 1;
else if (s == "off") newFlags |= 2;
BlockScriptAPI::setFlags(object, newFlags);
}
break;
}
return 0;
}
int getState(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_FRAGILE:
lua_pushinteger(state, BlockScriptAPI::getFlags(object) & 0x3);
return 1;
default:
return 0;
}
}
int setState(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_FRAGILE:
{
int oldState = BlockScriptAPI::getFlags(object) & 0x3;
int newState = (int)lua_tointeger(state, 2);
if (newState < 0) newState = 0;
else if (newState > 3) newState = 3;
if (newState != oldState) {
BlockScriptAPI::fragileUpdateState(object, newState);
}
}
break;
}
return 0;
}
int isPlayerOn(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_BUTTON:
lua_pushboolean(state, (BlockScriptAPI::getFlags(object) & 0x4) ? 1 : 0);
return 1;
default:
return 0;
}
}
int getPathMaxTime(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
lua_pushinteger(state, object->getPathMaxTime());
return 1;
default:
return 0;
}
}
int getPathTime(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
lua_pushinteger(state, BlockScriptAPI::getTemp(object));
return 1;
default:
return 0;
}
}
int setPathTime(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
BlockScriptAPI::setTemp(object, (int)lua_tointeger(state, 2));
break;
}
return 0;
}
int isLooping(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
lua_pushboolean(state, (BlockScriptAPI::getFlags(object) & 0x2) ? 0 : 1);
return 1;
default:
return 0;
}
}
int setLooping(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, boolean);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_MOVING_BLOCK:
case TYPE_MOVING_SHADOW_BLOCK:
case TYPE_MOVING_SPIKES:
BlockScriptAPI::setFlags(object,
(BlockScriptAPI::getFlags(object) & ~2) | (lua_toboolean(state, 2) ? 0 : 2)
);
break;
}
return 0;
}
int getSpeed(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
lua_pushinteger(state, BlockScriptAPI::getSpeed(object));
return 1;
default:
return 0;
}
}
int setSpeed(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
Block* object = Block::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
switch (object->type) {
case TYPE_CONVEYOR_BELT:
case TYPE_SHADOW_CONVEYOR_BELT:
BlockScriptAPI::setSpeed(object, (int)lua_tointeger(state, 2));
break;
}
return 0;
}
+ int getAppearance(lua_State* state) {
+ //Check the number of arguments.
+ HELPER_GET_AND_CHECK_ARGS(1);
+
+ //Check if the arguments are of the right type.
+ HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
+
+ Block* object = Block::getObjectFromUserData(state, 1);
+ if (object == NULL) return 0;
+
+ lua_pushstring(state, object->customAppearanceName.c_str());
+
+ return 1;
+ }
+
+ int setAppearance(lua_State* state) {
+ //Check the number of arguments.
+ HELPER_GET_AND_CHECK_ARGS(2);
+
+ //Check if the arguments are of the right type.
+ HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
+ HELPER_CHECK_ARGS_TYPE_OR_NIL(2, string);
+
+ Block* object = Block::getObjectFromUserData(state, 1);
+ if (object == NULL) return 0;
+
+ if (lua_isnil(state, 2)) {
+ object->setEditorProperty("appearance", "");
+ } else {
+ object->setEditorProperty("appearance", lua_tostring(state, 2));
+ }
+
+ return 0;
+ }
+
+ int getId(lua_State* state) {
+ //Check the number of arguments.
+ HELPER_GET_AND_CHECK_ARGS(1);
+
+ //Check if the arguments are of the right type.
+ HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
+
+ Block* object = Block::getObjectFromUserData(state, 1);
+ if (object == NULL) return 0;
+
+ lua_pushstring(state, object->id.c_str());
+
+ return 1;
+ }
+
+ int setId(lua_State* state) {
+ //Check the number of arguments.
+ HELPER_GET_AND_CHECK_ARGS(2);
+
+ //Check if the arguments are of the right type.
+ HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
+ HELPER_CHECK_ARGS_TYPE_OR_NIL(2, string);
+
+ Block* object = Block::getObjectFromUserData(state, 1);
+ if (object == NULL) return 0;
+
+ if (lua_isnil(state, 2)) {
+ object->id.clear();
+ } else {
+ object->id = lua_tostring(state, 2);
+ }
+
+ return 0;
+ }
+
+ int getDestination(lua_State* state) {
+ //Check the number of arguments.
+ HELPER_GET_AND_CHECK_ARGS(1);
+
+ //Check if the arguments are of the right type.
+ HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
+
+ Block* object = Block::getObjectFromUserData(state, 1);
+ if (object == NULL) return 0;
+
+ switch (object->type) {
+ case TYPE_PORTAL:
+ lua_pushstring(state, object->destination.c_str());
+ return 1;
+ default:
+ return 0;
+ }
+
+ return 1;
+ }
+
+ int setDestination(lua_State* state) {
+ //Check the number of arguments.
+ HELPER_GET_AND_CHECK_ARGS(2);
+
+ //Check if the arguments are of the right type.
+ HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
+ HELPER_CHECK_ARGS_TYPE_OR_NIL(2, string);
+
+ Block* object = Block::getObjectFromUserData(state, 1);
+ if (object == NULL) return 0;
+
+ switch (object->type) {
+ case TYPE_PORTAL:
+ if (lua_isnil(state, 2)) {
+ object->destination.clear();
+ } else {
+ object->destination = lua_tostring(state, 2);
+ }
+ break;
+ }
+
+ return 0;
+ }
+
+ int getMessage(lua_State* state) {
+ //Check the number of arguments.
+ HELPER_GET_AND_CHECK_ARGS(1);
+
+ //Check if the arguments are of the right type.
+ HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
+
+ Block* object = Block::getObjectFromUserData(state, 1);
+ if (object == NULL) return 0;
+
+ switch (object->type) {
+ case TYPE_NOTIFICATION_BLOCK:
+ lua_pushstring(state, object->message.c_str());
+ return 1;
+ default:
+ return 0;
+ }
+
+ return 1;
+ }
+
+ int setMessage(lua_State* state) {
+ //Check the number of arguments.
+ HELPER_GET_AND_CHECK_ARGS(2);
+
+ //Check if the arguments are of the right type.
+ HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
+ HELPER_CHECK_ARGS_TYPE_OR_NIL(2, string);
+
+ Block* object = Block::getObjectFromUserData(state, 1);
+ if (object == NULL) return 0;
+
+ std::string newMessage;
+
+ switch (object->type) {
+ case TYPE_NOTIFICATION_BLOCK:
+ if (!lua_isnil(state, 2)) {
+ newMessage = lua_tostring(state, 2);
+ }
+ if (newMessage != object->message) {
+ object->message = newMessage;
+
+ //Invalidate the notification texture
+ if (Game* game = dynamic_cast<Game*>(currentState)) {
+ game->invalidateNotificationTexture(object);
+ }
+ }
+ break;
+ }
+
+ return 0;
+ }
+
}
#define _L block
//Array with the methods for the block library.
-static const struct luaL_Reg blocklib_m[]={
+static const luaL_Reg blocklib_m[]={
_FI(Valid),
_FG(BlockById),
_FG(BlocksById),
_F(moveTo),
_FGS(Location),
_F(growTo),
_FGS(Size),
_FG(Type),
_F(changeThemeState),
_FIS(Visible),
_FGS(EventHandler),
_F(onEvent),
_FIS(Activated),
_FIS(Automatic),
_FGS(Behavior),
_FGS(State),
_FI(PlayerOn),
_FG(PathMaxTime),
_FGS(PathTime),
_FIS(Looping),
_FGS(Speed),
+ _FGS(Appearance),
+ _FGS(Id),
+ _FGS(Destination),
+ _FGS(Message),
{ NULL, NULL }
};
#undef _L
int luaopen_block(lua_State* state){
luaL_newlib(state,blocklib_m);
//Create the metatable for the block userdata.
luaL_newmetatable(state,"block");
lua_pushstring(state,"__index");
lua_pushvalue(state,-2);
lua_settable(state,-3);
Block::registerMetatableFunctions(state,-3);
//Register the functions and methods.
luaL_setfuncs(state,blocklib_m,0);
return 1;
}
//////////////////////////PLAYER SPECIFIC///////////////////////////
class PlayerScriptAPI {
public:
static bool isInAir(Player* player) {
return player->inAir;
}
static bool canMode(Player* player) {
return player->canMove;
}
static bool isDead(Player* player) {
return player->dead;
}
static bool isHoldingOther(Player* player) {
return player->holdingOther;
}
};
struct PlayerUserDatum{
char sig1,sig2,sig3,sig4;
};
Player* getPlayerFromUserData(lua_State* state,int idx){
PlayerUserDatum* ud=(PlayerUserDatum*)lua_touserdata(state,1);
//Make sure the user datum isn't null.
if(!ud) return NULL;
//Get the game state.
Game* game=dynamic_cast<Game*>(currentState);
if(game==NULL) return NULL;
Player* player=NULL;
//Check the signature to see if it's the player or the shadow.
if(ud->sig1=='P' && ud->sig2=='L' && ud->sig3=='Y' && ud->sig4=='R')
player=&game->player;
else if(ud->sig1=='S' && ud->sig2=='H' && ud->sig3=='D' && ud->sig4=='W')
player=&game->shadow;
return player;
}
namespace playershadow {
int getLocation(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Player* player = getPlayerFromUserData(state, 1);
if (player == NULL) return 0;
//Get the object.
lua_pushinteger(state, player->getBox().x);
lua_pushinteger(state, player->getBox().y);
return 2;
}
int setLocation(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(3);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
HELPER_CHECK_ARGS_TYPE(3, integer);
//Get the player.
Player* player = getPlayerFromUserData(state, 1);
if (player == NULL) return 0;
//Get the new location.
int x = lua_tointeger(state, 2);
int y = lua_tointeger(state, 3);
player->setLocation(x, y);
return 0;
}
int jump(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS_2(1, 2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_OPTIONAL_ARGS_TYPE(2, integer);
//Get the player.
Player* player = getPlayerFromUserData(state, 1);
if (player == NULL) return 0;
//Get the new location.
if (args == 2){
int yVel = lua_tointeger(state, 2);
player->jump(yVel);
} else{
//Use default jump strength.
player->jump();
}
return 0;
}
int isShadow(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Player* player = getPlayerFromUserData(state, 1);
if (player == NULL) return 0;
lua_pushboolean(state, player->isShadow());
return 1;
}
int getCurrentStand(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Player* player = getPlayerFromUserData(state, 1);
if (player == NULL) return 0;
//Get the actual game object.
Block* object = player->getObjCurrentStand();
if (object == NULL){
return 0;
}
//Create the userdatum.
object->createUserData(state, "block");
//We return one object, the userdatum.
return 1;
}
int isInAir(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Player* player = getPlayerFromUserData(state, 1);
if (player == NULL) return 0;
lua_pushboolean(state, PlayerScriptAPI::isInAir(player));
return 1;
}
int canMove(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Player* player = getPlayerFromUserData(state, 1);
if (player == NULL) return 0;
lua_pushboolean(state, PlayerScriptAPI::canMode(player));
return 1;
}
int isDead(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Player* player = getPlayerFromUserData(state, 1);
if (player == NULL) return 0;
lua_pushboolean(state, PlayerScriptAPI::isDead(player));
return 1;
}
int isHoldingOther(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
Player* player = getPlayerFromUserData(state, 1);
if (player == NULL) return 0;
lua_pushboolean(state, PlayerScriptAPI::isHoldingOther(player));
return 1;
}
}
#define _L playershadow
//Array with the methods for the player and shadow library.
-static const struct luaL_Reg playerlib_m[]={
+static const luaL_Reg playerlib_m[]={
_FGS(Location),
_F(jump),
_FI(Shadow),
_FG(CurrentStand),
_FI(InAir),
_F(canMove),
_FI(Dead),
_FI(HoldingOther),
{ NULL, NULL }
};
#undef _L
int luaopen_player(lua_State* state){
luaL_newlib(state,playerlib_m);
//Create the metatable for the player userdata.
luaL_newmetatable(state,"player");
lua_pushstring(state,"__index");
lua_pushvalue(state,-2);
lua_settable(state,-3);
//Now create two default player user data, one for the player and one for the shadow.
PlayerUserDatum* ud=(PlayerUserDatum*)lua_newuserdata(state,sizeof(PlayerUserDatum));
ud->sig1='P';ud->sig2='L';ud->sig3='Y';ud->sig4='R';
luaL_getmetatable(state,"player");
lua_setmetatable(state,-2);
lua_setglobal(state,"player");
ud=(PlayerUserDatum*)lua_newuserdata(state,sizeof(PlayerUserDatum));
ud->sig1='S';ud->sig2='H';ud->sig3='D';ud->sig4='W';
luaL_getmetatable(state,"player");
lua_setmetatable(state,-2);
lua_setglobal(state,"shadow");
//Register the functions and methods.
luaL_setfuncs(state,playerlib_m,0);
return 1;
}
//////////////////////////LEVEL SPECIFIC///////////////////////////
namespace level {
int getSize(lua_State* state){
//NOTE: this function accepts 0 arguments, but we ignore the argument count.
//Returns level size.
lua_pushinteger(state, LEVEL_WIDTH);
lua_pushinteger(state, LEVEL_HEIGHT);
return 2;
}
int getWidth(lua_State* state){
//NOTE: this function accepts 0 arguments, but we ignore the argument count.
//Returns level size.
lua_pushinteger(state, LEVEL_WIDTH);
return 1;
}
int getHeight(lua_State* state){
//NOTE: this function accepts 0 arguments, but we ignore the argument count.
//Returns level size.
lua_pushinteger(state, LEVEL_HEIGHT);
return 1;
}
int getName(lua_State* state){
//NOTE: this function accepts 0 arguments, but we ignore the argument count.
//Check if the currentState is the game state.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
//Returns level name.
lua_pushstring(state, game->getLevelName().c_str());
return 1;
}
int getEventHandler(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE(1, string);
//Check if the currentState is the game state.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
//Check event type
string eventType = lua_tostring(state, 1);
map<string, int>::iterator it = Game::levelEventNameMap.find(eventType);
if (it == Game::levelEventNameMap.end()) return 0;
//Check compiled script
map<int, int>::iterator script = game->compiledScripts.find(it->second);
if (script == game->compiledScripts.end()) return 0;
//Get event handler
lua_rawgeti(state, LUA_REGISTRYINDEX, script->second);
return 1;
}
//It will return old event handler.
int setEventHandler(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE(1, string);
HELPER_CHECK_ARGS_TYPE_OR_NIL(2, function);
//Check if the currentState is the game state.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
//Check event type
string eventType = lua_tostring(state, 1);
map<string, int>::const_iterator it = Game::levelEventNameMap.find(eventType);
if (it == Game::levelEventNameMap.end()){
lua_pushfstring(state, "Unknown level event type: '%s'.", eventType.c_str());
return lua_error(state);
}
//Check compiled script
int scriptIndex = LUA_REFNIL;
{
map<int, int>::iterator script = game->compiledScripts.find(it->second);
if (script != game->compiledScripts.end()) scriptIndex = script->second;
}
//Set new event handler
game->compiledScripts[it->second] = luaL_ref(state, LUA_REGISTRYINDEX);
if (scriptIndex == LUA_REFNIL) return 0;
//Get old event handler and unreference it
lua_rawgeti(state, LUA_REGISTRYINDEX, scriptIndex);
luaL_unref(state, LUA_REGISTRYINDEX, scriptIndex);
return 1;
}
int win(lua_State* state){
//NOTE: this function accepts 0 arguments, but we ignore the argument count.
//Check if the currentState is the game state.
if (stateID == STATE_LEVEL_EDITOR)
return 0;
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
game->won = true;
return 0;
}
int getTime(lua_State* state){
//NOTE: this function accepts 0 arguments, but we ignore the argument count.
//Check if the currentState is the game state.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
//Returns level size.
lua_pushinteger(state, game->time);
return 1;
}
int getRecordings(lua_State* state){
//NOTE: this function accepts 0 arguments, but we ignore the argument count.
//Check if the currentState is the game state.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
//Returns level size.
lua_pushinteger(state, game->recordings);
return 1;
}
int broadcastObjectEvent(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS_RANGE(1, 4);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE(1, string);
HELPER_CHECK_OPTIONAL_ARGS_TYPE_OR_NIL(2, string);
HELPER_CHECK_OPTIONAL_ARGS_TYPE_OR_NIL(3, string);
HELPER_CHECK_OPTIONAL_ARGS_TYPE_OR_NIL_NO_HINT(4, userdata);
//Check event type
int eventType = 0;
{
string s = lua_tostring(state, 1);
auto it = Game::gameObjectEventNameMap.find(s);
if (it == Game::gameObjectEventNameMap.end()){
lua_pushfstring(state, "Unknown block event type: '%s'.", s.c_str());
return lua_error(state);
} else {
eventType = it->second;
}
}
//Check object type
int objType = -1;
if (args >= 2 && lua_isstring(state, 2)) {
string s = lua_tostring(state, 2);
auto it = Game::blockNameMap.find(s);
if (it == Game::blockNameMap.end()){
lua_pushfstring(state, "Unknown object type: '%s'.", s.c_str());
return lua_error(state);
} else {
objType = it->second;
}
}
//Check id
const char* id = NULL;
if (args >= 3 && lua_isstring(state, 3)) {
id = lua_tostring(state, 3);
}
//Check target
Block *target = NULL;
if (args >= 4) {
target = Block::getObjectFromUserData(state, 4);
}
//Check if the currentState is the game state.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
game->broadcastObjectEvent(eventType, objType, id, target);
return 0;
}
}
#define _L level
//Array with the methods for the level library.
-static const struct luaL_Reg levellib_m[]={
+static const luaL_Reg levellib_m[]={
_FG(Size),
_FG(Width),
_FG(Height),
_FG(Name),
_FGS(EventHandler),
_F(win),
_FG(Time),
_FG(Recordings),
_F(broadcastObjectEvent),
{NULL,NULL}
};
#undef _L
int luaopen_level(lua_State* state){
luaL_newlib(state,levellib_m);
//Register the functions and methods.
luaL_setfuncs(state,levellib_m,0);
return 1;
}
/////////////////////////CAMERA SPECIFIC///////////////////////////
//FIXME: I can't define namespace camera since there is already a global variable named camera.
//Therefore I use struct camera for a workaround.
struct camera {
static int setMode(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE(1, string);
string mode = lua_tostring(state, 1);
//Get the game for setting the camera.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
//Check which mode.
if (mode == "player"){
game->cameraMode = Game::CAMERA_PLAYER;
} else if (mode == "shadow"){
game->cameraMode = Game::CAMERA_SHADOW;
} else{
//Unknown OR invalid camera mode.
return luaL_error(state, "Unknown or invalid camera mode for %s: '%s'.", __FUNCTION__, mode.c_str());
}
//Returns nothing.
return 0;
}
static int lookAt(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE(1, integer);
HELPER_CHECK_ARGS_TYPE(2, integer);
//Get the point.
int x = lua_tointeger(state, 1);
int y = lua_tointeger(state, 2);
//Get the game for setting the camera.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
game->cameraMode = Game::CAMERA_CUSTOM;
game->cameraTarget.x = x;
game->cameraTarget.y = y;
return 0;
}
};
#define _L camera
//Array with the methods for the camera library.
-static const struct luaL_Reg cameralib_m[]={
+static const luaL_Reg cameralib_m[]={
_FS(Mode),
_F(lookAt),
{NULL,NULL}
};
#undef _L
int luaopen_camera(lua_State* state){
luaL_newlib(state,cameralib_m);
//Register the functions and methods.
luaL_setfuncs(state,cameralib_m,0);
return 1;
}
/////////////////////////AUDIO SPECIFIC///////////////////////////
namespace audio {
int playSound(lua_State* state){
//Get the number of args, this can be anything from one to three.
HELPER_GET_AND_CHECK_ARGS_RANGE(1, 4);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE(1, string);
HELPER_CHECK_OPTIONAL_ARGS_TYPE(2, integer);
HELPER_CHECK_OPTIONAL_ARGS_TYPE(3, boolean);
HELPER_CHECK_OPTIONAL_ARGS_TYPE(4, integer);
//Default values for concurrent and force.
//See SoundManager.h
int concurrent = -1;
bool force = false;
int fadeMusic = -1;
//If there's a second one it should be an integer.
if (args > 1){
concurrent = lua_tointeger(state, 2);
}
//If there's a third one it should be a boolean.
if (args > 2){
force = lua_toboolean(state, 3);
}
if (args > 3){
fadeMusic = lua_tointeger(state, 4);
}
//Get the name of the sound.
string sound = lua_tostring(state, 1);
//Try to play the sound.
int channel = getSoundManager()->playSound(sound, concurrent, force, fadeMusic);
//Returns whether the operation is successful.
lua_pushboolean(state, channel >= 0 ? 1 : 0);
return 1;
}
int playMusic(lua_State* state){
//Get the number of args, this can be either one or two.
HELPER_GET_AND_CHECK_ARGS_2(1, 2);
//Make sure the first argument is a string.
HELPER_CHECK_ARGS_TYPE(1, string);
HELPER_CHECK_OPTIONAL_ARGS_TYPE(2, boolean);
//Default value of fade for playMusic.
//See MusicManager.h.
bool fade = true;
//If there's a second one it should be a boolean.
if (args > 1){
fade = lua_toboolean(state, 2);
}
//Get the name of the music.
string music = lua_tostring(state, 1);
//Try to switch to the new music.
getMusicManager()->playMusic(music, fade);
//Returns nothing.
return 0;
}
int pickMusic(lua_State* state){
//NOTE: this function accepts 0 arguments, but we ignore the argument count.
//Let the music manager pick a song from the current music list.
getMusicManager()->pickMusic();
return 0;
}
int setMusicList(lua_State* state){
//Get the number of args, this MUST be one.
HELPER_GET_AND_CHECK_ARGS(1);
//Make sure the given argument is a string.
HELPER_CHECK_ARGS_TYPE(1, string);
//And set the music list in the music manager.
string list = lua_tostring(state, 1);
getMusicManager()->setMusicList(list);
return 0;
}
int getMusicList(lua_State* state){
//NOTE: this function accepts 0 arguments, but we ignore the argument count.
//Return the name of the song (contains list prefix).
lua_pushstring(state, getMusicManager()->getCurrentMusicList().c_str());
return 1;
}
int currentMusic(lua_State* state){
//NOTE: this function accepts 0 arguments, but we ignore the argument count.
//Return the name of the song (contains list prefix).
lua_pushstring(state, getMusicManager()->getCurrentMusic().c_str());
return 1;
}
}
#define _L audio
//Array with the methods for the audio library.
-static const struct luaL_Reg audiolib_m[]={
+static const luaL_Reg audiolib_m[]={
_F(playSound),
_F(playMusic),
_F(pickMusic),
_FGS(MusicList),
_F(currentMusic),
{NULL,NULL}
};
#undef _L
int luaopen_audio(lua_State* state){
luaL_newlib(state,audiolib_m);
//Register the functions and methods.
luaL_setfuncs(state,audiolib_m,0);
return 1;
}
/////////////////////////DELAY EXECUTION SPECIFIC///////////////////////////
namespace delayExecution {
HELPER_REGISTER_IS_VALID_FUNCTION(ScriptDelayExecution);
int schedule(lua_State* state) {
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS_AT_LEAST(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE_OR_NIL(1, function);
HELPER_CHECK_ARGS_TYPE(2, integer);
HELPER_CHECK_OPTIONAL_ARGS_TYPE_OR_NIL(3, integer);
HELPER_CHECK_OPTIONAL_ARGS_TYPE_OR_NIL(4, integer);
HELPER_CHECK_OPTIONAL_ARGS_TYPE_OR_NIL(5, boolean);
//Check if the currentState is the game state.
Game* game = dynamic_cast<Game*>(currentState);
if (game == NULL) return 0;
//Create the delay execution object.
ScriptDelayExecution *obj = new ScriptDelayExecution(game->getScriptExecutor()->getDelayExecutionList());
obj->setActive();
obj->time = (int)lua_tointeger(state, 2);
obj->repeatCount = (args >= 3 && lua_isnumber(state, 3)) ? (int)lua_tointeger(state, 3) : 1;
obj->repeatInterval = (args >= 4 && lua_isnumber(state, 4)) ? (int)lua_tointeger(state, 4) : obj->time;
obj->enabled = ((args >= 5 && lua_isboolean(state, 5)) ? lua_toboolean(state, 5) : 1) != 0;
//Get arguments.
for (int i = 6; i <= args; i++) {
obj->arguments.push_back(luaL_ref(state, LUA_REGISTRYINDEX));
}
std::reverse(obj->arguments.begin(), obj->arguments.end());
//Get the function.
lua_settop(state, 1);
obj->func = luaL_ref(state, LUA_REGISTRYINDEX);
//Create the userdatum.
obj->createUserData(state, "delayExecution");
//We return one object, the userdatum.
return 1;
}
int cancel(lua_State* state){
HELPER_GET_AND_CHECK_ARGS(1);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
//Delete the object.
delete object;
return 0;
}
int isEnabled(lua_State* state){
HELPER_GET_AND_CHECK_ARGS(1);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
lua_pushboolean(state, object->enabled ? 1 : 0);
return 1;
}
int setEnabled(lua_State* state) {
HELPER_GET_AND_CHECK_ARGS(2);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, boolean);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
object->enabled = lua_toboolean(state, 2) != 0;
return 0;
}
int getTime(lua_State* state){
HELPER_GET_AND_CHECK_ARGS(1);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
lua_pushinteger(state, object->time);
return 1;
}
int setTime(lua_State* state) {
HELPER_GET_AND_CHECK_ARGS(2);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
object->time = (int)lua_tointeger(state, 2);
return 0;
}
int getRepeatCount(lua_State* state){
HELPER_GET_AND_CHECK_ARGS(1);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
lua_pushinteger(state, object->repeatCount);
return 1;
}
int setRepeatCount(lua_State* state) {
HELPER_GET_AND_CHECK_ARGS(2);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
object->repeatCount = (int)lua_tointeger(state, 2);
return 0;
}
int getRepeatInterval(lua_State* state){
HELPER_GET_AND_CHECK_ARGS(1);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
lua_pushinteger(state, object->repeatInterval);
return 1;
}
int setRepeatInterval(lua_State* state) {
HELPER_GET_AND_CHECK_ARGS(2);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
//Set the repeat interval (should >=1).
int i = (int)lua_tointeger(state, 2);
if (i > 0) object->repeatInterval = i;
return 0;
}
int getFunc(lua_State* state){
HELPER_GET_AND_CHECK_ARGS(1);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
if (object->func == LUA_REFNIL) return 0;
lua_rawgeti(state, LUA_REGISTRYINDEX, object->func);
return 1;
}
int setFunc(lua_State* state) {
HELPER_GET_AND_CHECK_ARGS(2);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE_OR_NIL(2, function);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
int oldFunc = object->func;
object->func = luaL_ref(state, LUA_REGISTRYINDEX);
if (oldFunc == LUA_REFNIL) return 0;
lua_rawgeti(state, LUA_REGISTRYINDEX, oldFunc);
luaL_unref(state, LUA_REGISTRYINDEX, oldFunc);
return 1;
}
int getArguments(lua_State* state){
HELPER_GET_AND_CHECK_ARGS(1);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
for (int a : object->arguments) {
lua_rawgeti(state, LUA_REGISTRYINDEX, a);
}
return object->arguments.size();
}
int setArguments(lua_State* state) {
HELPER_GET_AND_CHECK_ARGS_AT_LEAST(1);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
//Remove old arguments.
for (int a : object->arguments) {
luaL_unref(state, LUA_REGISTRYINDEX, a);
}
object->arguments.clear();
//Get arguments.
for (int i = 2; i <= args; i++) {
object->arguments.push_back(luaL_ref(state, LUA_REGISTRYINDEX));
}
std::reverse(object->arguments.begin(), object->arguments.end());
return 0;
}
int getExecutionTime(lua_State* state){
HELPER_GET_AND_CHECK_ARGS(1);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
lua_pushinteger(state, object->executionTime);
return 1;
}
int setExecutionTime(lua_State* state) {
HELPER_GET_AND_CHECK_ARGS(2);
HELPER_CHECK_ARGS_TYPE_NO_HINT(1, userdata);
HELPER_CHECK_ARGS_TYPE(2, integer);
auto object = ScriptDelayExecution::getObjectFromUserData(state, 1);
if (object == NULL) return 0;
object->executionTime = (int)lua_tointeger(state, 2);
return 0;
}
}
#define _L delayExecution
//Array with the methods for the block library.
-static const struct luaL_Reg delayExecutionLib_m[] = {
+static const luaL_Reg delayExecutionLib_m[] = {
_FI(Valid),
_F(schedule),
_F(cancel),
_FIS(Enabled),
_FGS(Time),
_FGS(RepeatCount),
_FGS(RepeatInterval),
_FGS(Func),
_FGS(Arguments),
_FGS(ExecutionTime),
{ NULL, NULL }
};
#undef _L
int luaopen_delayExecution(lua_State* state){
luaL_newlib(state, delayExecutionLib_m);
//Create the metatable for the delay execution userdata.
luaL_newmetatable(state, "delayExecution");
lua_pushstring(state, "__index");
lua_pushvalue(state, -2);
lua_settable(state, -3);
ScriptDelayExecution::registerMetatableFunctions(state, -3);
//Register the functions and methods.
luaL_setfuncs(state, delayExecutionLib_m, 0);
return 1;
}
/////////////////////////GETTEXT SPECIFIC///////////////////////////
namespace gettext {
int gettext(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(1);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE(1, string); //msgid
if (levels) {
auto dm = levels->getDictionaryManager();
if (dm) {
lua_pushstring(state, dm->get_dictionary().translate(lua_tostring(state, 1)).c_str());
return 1;
}
}
//If we failed to find dictionay manager, we just return the original string.
lua_pushvalue(state, 1);
return 1;
}
int pgettext(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(2);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE(1, string); //msgctxt
HELPER_CHECK_ARGS_TYPE(2, string); //msgid
if (levels) {
auto dm = levels->getDictionaryManager();
if (dm) {
lua_pushstring(state, dm->get_dictionary().translate_ctxt(lua_tostring(state, 1), lua_tostring(state, 2)).c_str());
return 1;
}
}
//If we failed to find dictionay manager, we just return the original string.
lua_pushvalue(state, 2);
return 1;
}
int ngettext(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(3);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE(1, string); //msgid
HELPER_CHECK_ARGS_TYPE(2, string); //msgid_plural
HELPER_CHECK_ARGS_TYPE(3, integer);
if (levels) {
auto dm = levels->getDictionaryManager();
if (dm) {
lua_pushstring(state, dm->get_dictionary().translate_plural(
lua_tostring(state, 1),
lua_tostring(state, 2),
lua_tointeger(state, 3)
).c_str());
return 1;
}
}
//If we failed to find dictionay manager, we just return the original string.
if (lua_tointeger(state, 3) == 1) {
lua_pushvalue(state, 1);
} else {
lua_pushvalue(state, 2);
}
return 1;
}
int npgettext(lua_State* state){
//Check the number of arguments.
HELPER_GET_AND_CHECK_ARGS(4);
//Check if the arguments are of the right type.
HELPER_CHECK_ARGS_TYPE(1, string); //msgctxt
HELPER_CHECK_ARGS_TYPE(2, string); //msgid
HELPER_CHECK_ARGS_TYPE(3, string); //msgid_plural
HELPER_CHECK_ARGS_TYPE(4, integer);
if (levels) {
auto dm = levels->getDictionaryManager();
if (dm) {
lua_pushstring(state, dm->get_dictionary().translate_ctxt_plural(
lua_tostring(state, 1),
lua_tostring(state, 2),
lua_tostring(state, 3),
lua_tointeger(state, 4)
).c_str());
return 1;
}
}
//If we failed to find dictionay manager, we just return the original string.
if (lua_tointeger(state, 4) == 1) {
lua_pushvalue(state, 2);
} else {
lua_pushvalue(state, 3);
}
return 1;
}
}
#define _L gettext
-static const struct luaL_Reg gettextlib_m[] = {
+static const luaL_Reg gettextlib_m[] = {
_F(gettext),
_F(pgettext),
_F(ngettext),
_F(npgettext),
{ NULL, NULL }
};
#undef _L
int luaopen_gettext(lua_State* state){
//Register the global shortcut function _() and __().
luaL_loadstring(state,
"function _(s)\n"
" return gettext.gettext(s)\n"
"end\n"
"function __(s)\n"
" return s\n"
"end\n"
);
lua_pcall(state, 0, 0, 0);
luaL_newlib(state, gettextlib_m);
//Register the functions and methods.
luaL_setfuncs(state, gettextlib_m, 0);
return 1;
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, May 16, 7:17 PM (1 d, 3 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
63206
Default Alt Text
(185 KB)

Event Timeline