Page Menu
Home
Phabricator (Chris)
Search
Configure Global Search
Log In
Files
F116770
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
28 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/RecordPlayback.cpp b/src/RecordPlayback.cpp
index a458dfc..482268e 100644
--- a/src/RecordPlayback.cpp
+++ b/src/RecordPlayback.cpp
@@ -1,632 +1,753 @@
/*
* Copyright (C) 2019 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 "RecordPlayback.h"
#include "TreeStorageNode.h"
#include "POASerializer.h"
#include "InputManager.h"
#include "Functions.h"
#include "Render.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>
#include <map>
#include <algorithm>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <SDL_ttf_fontfallback.h>
#include "libs/tinyformat/tinyformat.h"
const int MAX_MOUSE_IDLE_TIME = 1 * 40;
-const int MAX_RECENT_FRAMES = 64;
-const int MAX_FRAME_PER_TICK = 200;
+const int SAVE_REGULAR_FRAME_PER_TICK = 4;
using namespace std;
+class FrameQueue {
+private:
+ struct Node {
+ int frameNumber;
+ size_t t;
+ bool operator<(const Node& other) const {
+ return t > other.t;
+ }
+ };
+
+ size_t currentTime;
+
+ std::vector<Node> nodes;
+
+public:
+ size_t maxSize;
+
+public:
+ FrameQueue(size_t maxSize = 0) : currentTime(0), maxSize(maxSize) {}
+ bool empty() const { return nodes.empty(); }
+ size_t size() const { return nodes.size(); }
+ void clear() { nodes.clear(); }
+
+ void insertOrUpdate(int frameNumber, bool isUpdate, std::map<int, GameSaveState>& remove) {
+ // increate time
+ currentTime++;
+
+ // chcek if item is present
+ size_t m = nodes.size();
+ size_t i = m;
+ if (isUpdate) {
+ for (i = 0; i < m; i++) {
+ if (nodes[i].frameNumber == frameNumber) break;
+ }
+
+ // sanity check
+ assert(i < m);
+ }
+
+ // found it
+ if (i < m) {
+ nodes[i].t = currentTime;
+
+ // modify the heap
+ while (i * 2 + 1 < m) {
+ size_t j = (i * 2 + 2 < m && nodes[i * 2 + 2].t < nodes[i * 2 + 1].t) ?
+ (i * 2 + 2) : (i * 2 + 1);
+
+ std::swap(nodes[i], nodes[j]);
+ i = j;
+ }
+ } else if (!isUpdate) {
+ if (maxSize == 0 || nodes.size() < maxSize) {
+ // just insert it to the end
+ nodes.push_back(Node{ frameNumber, currentTime });
+ } else {
+ // pop heap
+ std::pop_heap(nodes.begin(), nodes.end());
+
+ // remove least used frame
+ auto it = remove.find(nodes.back().frameNumber);
+ assert(it != remove.end());
+ if (it != remove.end()) remove.erase(it);
+
+ nodes.back() = Node{ frameNumber, currentTime };
+ }
+ }
+
+ // sanity check
+ assert(std::is_heap(nodes.begin(), nodes.end()));
+ }
+};
+
+class FrameCache {
+private:
+ //The cached frames.
+ std::map<int, GameSaveState> cachedFrames;
+
+ //The (various) recent frames queue.
+ //Currently the frame is saved permanently if frame number is a multiple of 200,
+ //save to a key frame queue if frame number is a multiple of 40,
+ //otherwise save to a regular frame queue
+ FrameQueue queueRegular, queueKey;
+public:
+ FrameCache() : queueRegular(16), queueKey(16) {}
+
+ GameSaveState* getSaveState(int time, bool saveRegular) {
+ bool isKeyframe = (time % 40) == 0;
+
+ if (isKeyframe || saveRegular) {
+ bool isKeyframe200 = (time % 200) == 0;
+ bool isUpdate = cachedFrames.find(time) != cachedFrames.end();
+
+ if (isKeyframe200) {
+ } else if (isKeyframe) {
+ queueKey.insertOrUpdate(time, isUpdate, cachedFrames);
+ } else if (saveRegular) {
+ queueRegular.insertOrUpdate(time, isUpdate, cachedFrames);
+ }
+
+ if (!isUpdate) {
+ return &cachedFrames[time];
+ }
+ }
+
+ return NULL;
+ }
+
+ GameSaveState* getLoadState(int time) {
+ auto it = cachedFrames.find(time);
+ return it == cachedFrames.end() ? NULL : &it->second;
+ }
+
+ // find a cached frame with large time in the range (timeBegin, time].
+ std::pair<int, GameSaveState*> findLoadState(int timeBegin, int time) {
+ std::pair<int, GameSaveState*> ret{ -1, NULL };
+
+ for (auto it = cachedFrames.begin(); it != cachedFrames.end(); ++it) {
+ if (it->first > timeBegin && it->first <= time) {
+ ret.first = it->first;
+ ret.second = &it->second;
+ }
+ }
+
+ return ret;
+ }
+};
+
RecordPlayback::RecordPlayback(SDL_Renderer& renderer, ImageManager& imageManager)
: Game(renderer, imageManager)
, lastMouseX(-1), lastMouseY(-1), mouseIdleTime(-MAX_MOUSE_IDLE_TIME)
, replaySpeed(0x10), replayIdleTime(0), replayPaused(false)
+ , cachedFrames(new FrameCache())
, clickedTime(-1), loadThisNextTime(NULL)
+ , maxFramePerTick(16)
{
guiTexture = imageManager.loadTexture(getDataPath() + "gfx/gui.png", renderer);
objThemes.getCharacter(false)->createInstance(&walkingAnimation, "walkright");
// We always show the cursor since Game hides it first.
SDL_ShowCursor(SDL_ENABLE);
}
RecordPlayback::~RecordPlayback() {
+ delete cachedFrames;
}
enum RecordPlaybackButtons {
BTN_RESTART,
BTN_REWIND,
BTN_PREV_FRAME,
BTN_PLAY,
BTN_NEXT_FRAME,
BTN_FAST_FORWARD,
BTN_SLOWER,
BTN_FASTER,
};
void RecordPlayback::handleEvents(ImageManager& imageManager, SDL_Renderer& renderer) {
// First let Game process events.
Game::handleEvents(imageManager, renderer);
int clickedButton = -1, t = -1;
const InputManagerKeys keys[8] = {
INPUTMGR_RESTART,
INPUTMGR_LEFT,
INPUTMGR_PREVIOUS,
INPUTMGR_SPACE,
INPUTMGR_NEXT,
INPUTMGR_RIGHT,
INPUTMGR_UP,
INPUTMGR_DOWN,
};
for (int i = 0; i < 8; i++) {
if (inputMgr.isKeyDownEvent(keys[i])) {
clickedButton = i;
break;
}
}
if (event.type == SDL_MOUSEBUTTONDOWN) {
const SDL_Rect r = { 100, SCREEN_HEIGHT - 144, SCREEN_WIDTH - 200, 72 };
const SDL_Point p = { event.button.x, event.button.y };
if (mouseIdleTime < 0 && SDL_PointInRect(&p, &r)) {
if (p.y < r.y + 24) {
} else if (p.y < r.y + 40) {
// clicked the slider
if (p.x >= r.x + 8 && p.x < r.x + r.w - 8) {
int x = p.x - r.x - 12;
if (x < 0) x = 0; else if (x > r.w - 24) x = r.w - 24;
const int maxTime = player.getRecord()->size();
t = int(float(x) * float(maxTime) / float(r.w - 24) + 0.5f);
if (t < 0) t = 0;
else if (t > maxTime) t = maxTime;
mouseIdleTime = -MAX_MOUSE_IDLE_TIME;
}
} else if (p.y < r.y + 64) {
// clicked the button bar
if (p.x >= r.x + 8 && p.x < r.x + 152) {
clickedButton = (p.x - r.x - 8) / 24;
} else if (p.x < r.x + r.w - 104) {
} else if (p.x < r.x + r.w - 80) {
clickedButton = BTN_SLOWER;
} else if (p.x < r.x + r.w - 32) {
} else if (p.x < r.x + r.w - 8) {
clickedButton = BTN_FASTER;
}
}
} else {
clickedButton = BTN_PLAY;
}
}
if (clickedButton >= 0) {
switch (clickedButton) {
case BTN_RESTART:
t = 0;
break;
case BTN_REWIND:
t = time - ((time + 20) % 40) - 20;
if (t < 0) t = 0;
break;
case BTN_PREV_FRAME:
t = time - 1;
if (t < 0) t = 0;
break;
case BTN_PLAY:
replayPaused = !replayPaused;
break;
case BTN_NEXT_FRAME:
t = time + 1;
break;
case BTN_FAST_FORWARD:
t = time - ((time + 20) % 40) + 60;
break;
case BTN_SLOWER:
if (replaySpeed > 0x4) replaySpeed >>= 1;
break;
case BTN_FASTER:
if (replaySpeed < 0x40) replaySpeed <<= 1;
break;
}
mouseIdleTime = -MAX_MOUSE_IDLE_TIME;
}
if (t >= 0 && t != time) {
if (t > 0) clickedTime = t;
else restart();
}
}
void RecordPlayback::restart() {
- GameSaveState &state = cachedFrames[0];
- player.loadStateInternal(&state.playerSaved);
- shadow.loadStateInternal(&state.shadowSaved);
- loadGameOnlyStateInternal(&state.gameSaved);
+ GameSaveState *state = cachedFrames->getLoadState(0);
+ player.loadStateInternal(&state->playerSaved);
+ shadow.loadStateInternal(&state->shadowSaved);
+ loadGameOnlyStateInternal(&state->gameSaved);
player.playRecord();
shadow.playRecord(); //???
eventQueue.clear();
won = false;
}
void RecordPlayback::logic(ImageManager& imageManager, SDL_Renderer& renderer) {
// The number of ticks to advance.
int m = 0;
+ Uint64 t1 = SDL_GetPerformanceCounter();
+
if (clickedTime > 0 && clickedTime != time) {
if (clickedTime > time) { // fast forward
if (won) {
// the game replay is finished so we can't fast forward anymore
clickedTime = -1;
} else {
// find a nearest cached frame if available
- loadThisNextTime = NULL;
- int t = -1;
- for (auto it = cachedFrames.begin(); it != cachedFrames.end(); ++it) {
- if (it->first > time && it->first <= clickedTime) {
- t = it->first;
- loadThisNextTime = &it->second;
- }
- }
+ auto state = cachedFrames->findLoadState(time, clickedTime);
+ loadThisNextTime = state.second;
if (loadThisNextTime) {
// we found a suitable cached state, load it through logic().
Game::logic(imageManager, renderer);
// sanity check.
- assert(time == t);
+ assert(time == state.first);
}
m = clickedTime - time;
// set a maximal advance of frames per tick to prevent game lag.
- if (m > MAX_FRAME_PER_TICK) m = MAX_FRAME_PER_TICK;
+ if (m > maxFramePerTick) m = maxFramePerTick;
else clickedTime = -1;
}
} else { // rewind
// find a nearest cached frame if available
- loadThisNextTime = NULL;
- int t = -1;
- for (auto it = cachedFrames.begin(); it != cachedFrames.end(); ++it) {
- if (it->first > 0 && it->first <= clickedTime) {
- t = it->first;
- loadThisNextTime = &it->second;
- }
- }
+ auto state = cachedFrames->findLoadState(0, clickedTime);
+ loadThisNextTime = state.second;
if (loadThisNextTime) {
// we found a suitable cached state, load it through logic().
Game::logic(imageManager, renderer);
// sanity check.
- assert(time == t);
+ assert(time == state.first);
} else {
// otherwide we need to restart from the beginning.
restart();
}
m = clickedTime - time;
// set a maximal advance of frames per tick to prevent game lag.
- if (m > MAX_FRAME_PER_TICK) m = MAX_FRAME_PER_TICK;
+ if (m > maxFramePerTick) m = maxFramePerTick;
else clickedTime = -1;
}
} else {
clickedTime = -1;
}
if (m != 0) {
replayIdleTime = 0;
} else {
if (won || replayPaused) {
replayIdleTime = 0;
} else if (replaySpeed >= 0x10) {
m = replaySpeed >> 4;
replayIdleTime = 0;
} else {
replayIdleTime += replaySpeed;
if (replayIdleTime >= 0x10) {
m = 1;
replayIdleTime = 0;
}
}
}
// Let Game process logic.
for (int i = 0; i < m; i++) {
if (won) break;
- if (i == m - 1) saveStateNextTime = true;
+
+ // save the regular frame in the end of this run, under the condition that the game runs not too slow
+ if (i >= m - SAVE_REGULAR_FRAME_PER_TICK && maxFramePerTick >= 64) saveStateNextTime = true;
+
Game::logic(imageManager, renderer);
}
+
+ // Update max frame per tick.
+ if (m > 0 && !won) {
+ Uint64 t2 = SDL_GetPerformanceCounter(), f = SDL_GetPerformanceFrequency();
+
+ // second per game frame.
+ double t = std::max(double(t2 - t1) / double(f) / double(m), 1e-5);
+
+ // update max frame per tick smoothly.
+ maxFramePerTick = (maxFramePerTick * 7 + std::max(int(0.1 / t), 16)) / 8;
+ }
}
void RecordPlayback::checkSaveLoadState() {
assert(time > 0 && eventQueue.empty());
if (loadThisNextTime) {
player.loadStateInternal(&loadThisNextTime->playerSaved);
shadow.loadStateInternal(&loadThisNextTime->shadowSaved);
loadGameOnlyStateInternal(&loadThisNextTime->gameSaved);
player.playRecord(time - 1); //???
shadow.playRecord(); //???
won = false;
- } else {
- bool isKeyframe = (time % 40) == 0;
- if ((isKeyframe || saveStateNextTime) && cachedFrames.find(time) == cachedFrames.end()) {
- if (!isKeyframe) {
- if ((int)recentFrames.size() >= MAX_RECENT_FRAMES) {
- auto it = cachedFrames.find(recentFrames.front());
- if (it != cachedFrames.end()) cachedFrames.erase(it);
- recentFrames.pop();
- }
- recentFrames.push(time);
- }
-
- GameSaveState &state = cachedFrames[time];
- player.saveStateInternal(&state.playerSaved);
- shadow.saveStateInternal(&state.shadowSaved);
- saveGameOnlyStateInternal(&state.gameSaved);
- }
+ } else if (auto state = cachedFrames->getSaveState(time, saveStateNextTime && ((time % SAVE_REGULAR_FRAME_PER_TICK) == 0))) {
+ player.saveStateInternal(&state->playerSaved);
+ shadow.saveStateInternal(&state->shadowSaved);
+ saveGameOnlyStateInternal(&state->gameSaved);
}
loadThisNextTime = NULL;
saveStateNextTime = false;
}
void RecordPlayback::render(ImageManager& imageManager, SDL_Renderer& renderer) {
// First let Game render.
Game::render(imageManager, renderer);
const SDL_Rect r = { 100, SCREEN_HEIGHT - 144, SCREEN_WIDTH - 200, 72 };
SDL_Point p;
// Check if mouse moves (FIXME: logic in render function)
SDL_GetMouseState(&p.x, &p.y);
if (lastMouseX != p.x || lastMouseY != p.y || SDL_PointInRect(&p, &r)) {
lastMouseX = p.x;
lastMouseY = p.y;
mouseIdleTime = -MAX_MOUSE_IDLE_TIME;
}
// Now render the playback control.
if (mouseIdleTime < 0) {
std::string newToolTip;
SDL_Rect toolTipRect = {};
const SDL_Color fg = objThemes.getTextColor(true);
const int alpha = std::max(mouseIdleTime, -15) * (-17);
const int alpha2 = std::max(mouseIdleTime, -15) * (-10);
drawGUIBox(r.x, r.y, r.w, r.h, renderer, 0xFFFFFF00 | alpha2, true);
int y = r.y + 8;
const int maxTime = player.getRecord()->size();
const int tmp = time ^ (maxTime << 16) ^ 42;
// Draw time text. FIXME: here we screwed up Game::timeTexture.
if (timeTexture.needsUpdate(tmp)) {
timeTexture.update(tmp, textureUniqueFromSurface(renderer, SurfacePtr(TTF_RenderUTF8_Blended(fontMono,
tfm::format("%02d:%05.2f / %02d:%05.2f", time / 2400, (time % 2400) / 40.0, maxTime / 2400, (maxTime % 2400) / 40.0).c_str(),
fg))));
}
SDL_SetTextureAlphaMod(timeTexture.get(), (Uint8)alpha);
applyTexture(r.x + 8, y, *timeTexture.get(), renderer);
y += 16;
// Get the tool tip text of slider if mouse is over
if (p.y >= y && p.y < y + 24 && p.x >= r.x + 8 && p.x < r.x + r.w - 8) {
int x = p.x - r.x - 12;
if (x < 0) x = 0; else if (x > r.w - 24) x = r.w - 24;
toolTipRect = SDL_Rect{ r.x + x + 8, y, 8, -1 };
int t = int(float(x) * float(maxTime) / float(r.w - 24) + 0.5f);
if (t < 0) t = 0; else if (t > maxTime) t = maxTime;
newToolTip = tfm::format("%02d:%05.2f", t / 2400, (t % 2400) / 40.0);
}
// Draw slider.
int pos = maxTime > 0 ? int(float(r.w - 24) * float(std::min(time, maxTime)) / float(maxTime) + 0.5f) : 0;
drawGUIBox(r.x + 8, y + 6, pos + 2, 4, renderer, 0x80808000 | alpha, true);
drawGUIBox(r.x + pos + 14, y + 6, r.w - pos - 22, 4, renderer, 0xFFFFFF00 | alpha, true);
drawGUIBox(r.x + pos + 8, y, 8, 16, renderer, 0xFFFFFF00 | alpha, true);
y += 16;
// Get the tool tip text of buttons if mouse is over
if (p.y >= y && p.y < y + 24) {
if (p.x < r.x + 8){
} else if (p.x < r.x + 32) {
newToolTip = _("Restart");
newToolTip += " (" + InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_RESTART, false), inputMgr.getKeyCode(INPUTMGR_RESTART, true)) + ")";
toolTipRect = SDL_Rect{ r.x + 8, y, 24, 24 };
} else if (p.x < r.x + 56) {
newToolTip = _("Rewind");
newToolTip += " (" + InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_LEFT, false), inputMgr.getKeyCode(INPUTMGR_LEFT, true)) + ")";
toolTipRect = SDL_Rect{ r.x + 32, y, 24, 24 };
} else if (p.x < r.x + 80) {
newToolTip = _("Step back");
newToolTip += " (" + InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_PREVIOUS, false), inputMgr.getKeyCode(INPUTMGR_PREVIOUS, true)) + ")";
toolTipRect = SDL_Rect{ r.x + 56, y, 24, 24 };
} else if (p.x < r.x + 104) {
newToolTip = replayPaused ? _("Play") : _("Pause");
newToolTip += " (" + InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_SPACE, false), inputMgr.getKeyCode(INPUTMGR_SPACE, true)) + ")";
toolTipRect = SDL_Rect{ r.x + 80, y, 24, 24 };
} else if (p.x < r.x + 128) {
newToolTip = _("Step forward");
newToolTip += " (" + InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_NEXT, false), inputMgr.getKeyCode(INPUTMGR_NEXT, true)) + ")";
toolTipRect = SDL_Rect{ r.x + 104, y, 24, 24 };
} else if (p.x < r.x + 152) {
newToolTip = _("Fast forward");
newToolTip += " (" + InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_RIGHT, false), inputMgr.getKeyCode(INPUTMGR_RIGHT, true)) + ")";
toolTipRect = SDL_Rect{ r.x + 128, y, 24, 24 };
} else if (p.x < r.x + r.w - 104) {
} else if (p.x < r.x + r.w - 80) {
newToolTip = _("Slower");
newToolTip += " (" + InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_UP, false), inputMgr.getKeyCode(INPUTMGR_UP, true)) + ")";
toolTipRect = SDL_Rect{ r.x + r.w - 104, y, 24, 24 };
} else if (p.x < r.x + r.w - 32) {
} else if (p.x < r.x + r.w - 8) {
newToolTip = _("Faster");
newToolTip += " (" + InputManagerKeyCode::describeTwo(inputMgr.getKeyCode(INPUTMGR_DOWN, false), inputMgr.getKeyCode(INPUTMGR_DOWN, true)) + ")";
toolTipRect = SDL_Rect{ r.x + r.w - 32, y, 24, 24 };
}
if (toolTipRect.h > 0) {
drawGUIBox(toolTipRect.x, toolTipRect.y, toolTipRect.w, toolTipRect.h, renderer, 0xFFFFFF00 | alpha, true);
}
}
// Set the alpha of gui texture
SDL_SetTextureAlphaMod(guiTexture.get(), (Uint8)alpha);
// Draw buttons.
SDL_Rect srcrect = { 32, 80, 16, 16 };
SDL_Rect dstrect = { r.x + 12, y + 4, 16, 16 };
SDL_RenderCopy(&renderer, guiTexture.get(), &srcrect, &dstrect); // restart
srcrect.x += 16; dstrect.x += 24;
SDL_RenderCopy(&renderer, guiTexture.get(), &srcrect, &dstrect); // rewind
srcrect.x += 16; dstrect.x += 24;
SDL_RenderCopy(&renderer, guiTexture.get(), &srcrect, &dstrect); // previous frame
srcrect.x = replayPaused ? 96 : 112; srcrect.y = 64; dstrect.x += 24;
SDL_RenderCopy(&renderer, guiTexture.get(), &srcrect, &dstrect); // play/pause
srcrect.x = 80; srcrect.y = 80; dstrect.x += 24;
SDL_RenderCopy(&renderer, guiTexture.get(), &srcrect, &dstrect); // next frame
srcrect.x += 16; dstrect.x += 24;
SDL_RenderCopy(&renderer, guiTexture.get(), &srcrect, &dstrect); // fast forward
srcrect.x = 48; srcrect.y = 80; dstrect.x = r.x + r.w - 100;
SDL_RenderCopy(&renderer, guiTexture.get(), &srcrect, &dstrect); // slower
srcrect.x = 96; dstrect.x += 72;
SDL_RenderCopy(&renderer, guiTexture.get(), &srcrect, &dstrect); // faster
// Draw replay speed.
if (replaySpeedTexture.needsUpdate(replaySpeed)) {
replaySpeedTexture.update(replaySpeed, textureUniqueFromSurface(renderer, SurfacePtr(TTF_RenderUTF8_Blended(fontMono,
tfm::format("%gx", float(replaySpeed) / 16.0f).c_str(),
fg))));
}
SDL_Rect r1 = rectFromTexture(replaySpeedTexture);
SDL_SetTextureAlphaMod(replaySpeedTexture.get(), (Uint8)alpha);
applyTexture(r.x + r.w - 80 + (48 - r1.w) / 2, y + (24 - r1.h) / 2, *replaySpeedTexture.get(), renderer);
// Reset alpha of gui texture since it is global
SDL_SetTextureAlphaMod(guiTexture.get(), 255);
y += 24;
// Show tool tip if necessary.
if (!newToolTip.empty()) {
if (toolTipTexture.needsUpdate(newToolTip)) {
toolTipTexture.update(newToolTip, textureUniqueFromSurface(renderer, SurfacePtr(TTF_RenderUTF8_Blended(fontText,
newToolTip.c_str(), fg))));
}
SDL_Rect r1 = rectFromTexture(toolTipTexture);
toolTipRect.y = toolTipRect.y - r1.h - 6;
toolTipRect.w = r1.w + 4; toolTipRect.h = r1.h + 4;
if (toolTipRect.x + toolTipRect.w > SCREEN_WIDTH - 2) toolTipRect.x = SCREEN_WIDTH - 2 - toolTipRect.w;
drawGUIBox(toolTipRect.x, toolTipRect.y, toolTipRect.w, toolTipRect.h, renderer, 0xFFFFFF00 | alpha, true);
SDL_SetTextureAlphaMod(toolTipTexture.get(), (Uint8)alpha);
applyTexture(toolTipRect.x + 2, toolTipRect.y + 2, *toolTipTexture.get(), renderer);
}
}
// Advance the mouse idle time. (FIXME: logic in render function)
mouseIdleTime++;
// Render the busy animation.
if (clickedTime > 0) {
drawGUIBox(SCREEN_WIDTH / 2 - 25, SCREEN_HEIGHT / 2 - 25, 50, 50, renderer, 0xFFFFFF00 | 150, true);
walkingAnimation.draw(renderer, SCREEN_WIDTH / 2 - 11, SCREEN_HEIGHT / 2 - 20);
walkingAnimation.updateAnimation();
}
// Render the replay finished tooltip.
if (won) {
// FIXME: here we screwed up Game::bmTipsRestartCheckpoint.
// This shouldn't cause bugs since when playing from record, the save functionality is disabled.
if (bmTipsRestartCheckpoint == NULL){
//Draw string
bmTipsRestartCheckpoint = textureFromText(renderer, *fontText,
_("Game replay is done."),
objThemes.getTextColor(true));
}
SDL_Texture* bm = bmTipsRestartCheckpoint.get();
//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);
}
}
}
void RecordPlayback::loadRecord(ImageManager& imageManager, SDL_Renderer& renderer, const char* fileName, const char* levelFileName) {
//Create a TreeStorageNode that will hold the loaded data.
TreeStorageNode obj;
{
POASerializer objSerializer;
//Parse the file.
if(!objSerializer.loadNodeFromFile(fileName,&obj,true)){
cerr<<"ERROR: Can't load record file "<<fileName<<endl;
return;
}
}
//Load the seed of psuedo-random number generator.
prngSeed.clear();
{
auto it = obj.attributes.find("seed");
if (it != obj.attributes.end() && !it->second.empty()) {
prngSeed = it->second[0];
}
}
bool loaded=false;
//substitute the level if the level file name is specified.
if (levelFileName) {
TreeStorageNode *obj = new TreeStorageNode();
if (!POASerializer().loadNodeFromFile(levelFileName, obj, true)) {
cerr << "ERROR: Can't load level file " << levelFileName << endl;
delete obj;
} else {
//also replace the script by the external one.
std::string s = levelFileName;
size_t lp = s.find_last_of('.');
if (lp != std::string::npos) {
s = s.substr(0, lp);
}
s += ".lua";
loadLevelFromNode(imageManager, renderer, obj, "?record?", s);
loaded = true;
}
} else {
//find the node named 'map'.
for (unsigned int i = 0; i < obj.subNodes.size(); i++) {
if (obj.subNodes[i]->name == "map") {
//load the script from record file.
std::string s;
auto it = obj.attributes.find("script");
if (it != obj.attributes.end() && !it->second.empty()) {
s = "?" + it->second[0];
}
//load the level. (fileName=???)
loadLevelFromNode(imageManager, renderer, obj.subNodes[i], "?record?", s);
//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
//some sanity check
assert(eventQueue.empty());
//play the record.
player.playRecord();
shadow.playRecord(); //???
//Save the game state for time 0.
- GameSaveState &state = cachedFrames[0];
- player.saveStateInternal(&state.playerSaved);
- shadow.saveStateInternal(&state.shadowSaved);
- saveGameOnlyStateInternal(&state.gameSaved);
+ GameSaveState *state = cachedFrames->getSaveState(0, false);
+ player.saveStateInternal(&state->playerSaved);
+ shadow.saveStateInternal(&state->shadowSaved);
+ saveGameOnlyStateInternal(&state->gameSaved);
// We always show the cursor since Game hides it first.
SDL_ShowCursor(SDL_ENABLE);
}
diff --git a/src/RecordPlayback.h b/src/RecordPlayback.h
index 035dfde..d8fe95c 100644
--- a/src/RecordPlayback.h
+++ b/src/RecordPlayback.h
@@ -1,92 +1,91 @@
/*
* Copyright (C) 2019 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 RECORDPLAYBACK_H
#define RECORDPLAYBACK_H
#include "Game.h"
#include "ThemeManager.h"
-#include <map>
-#include <queue>
+class FrameCache;
class RecordPlayback : public Game {
protected:
int lastMouseX, lastMouseY, mouseIdleTime;
// The replay speed. 0x10 means 1.0x, 0x20 means 2.0x, 0x08 means 0.5x, etc.
int replaySpeed;
// The replay idle time. When replaySpeed < 0x10 then each time it will be added to replayIdleTime,
// and when replayIdleTime >= 0x10 the game tick increases.
int replayIdleTime;
// Boolean indicating if the replay is paused.
bool replayPaused;
//Texture containing the label of the replay speed.
CachedTexture<int> replaySpeedTexture;
//Texture containing the tool tip of buttons.
CachedTexture<std::string> toolTipTexture;
//Texture of gui.
SharedTexture guiTexture;
//The cached frames.
- std::map<int, GameSaveState> cachedFrames;
-
- //The list of recently used frames.
- std::queue<int> recentFrames;
+ FrameCache* cachedFrames;
//The time we are going to jump to. Should be >0, otherwise it will be ignored.
int clickedTime;
//The walking animation used when navigating.
ThemeBlockInstance walkingAnimation;
//State that is set when we should load it on next logic update.
GameSaveState *loadThisNextTime;
+ //Max frame per tick in fast-forward mode. Will be measured dynamically.
+ int maxFramePerTick;
+
//Restart the game from time 0.
void restart();
public:
//Constructor.
RecordPlayback(SDL_Renderer& renderer, ImageManager& imageManager);
//Destructor.
//It will call destroy();
~RecordPlayback();
//Inherited from GameState.
void handleEvents(ImageManager& imageManager, SDL_Renderer& renderer) override;
void logic(ImageManager& imageManager, SDL_Renderer& renderer) override;
void render(ImageManager& imageManager, SDL_Renderer& renderer) override;
//Check if we should save/load state, override it to provide seeking support.
void checkSaveLoadState() override;
//Load game record (and its level) from file and play it.
//fileName: The filename of the recording file.
//levelFileName: (Optional) The file name of the level file. It's only used when you want to test if the record works for another level.
void loadRecord(ImageManager& imageManager, SDL_Renderer& renderer, const char* fileName, const char* levelFileName = NULL);
};
#endif
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, May 8, 8:29 PM (1 w, 23 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
62762
Default Alt Text
(28 KB)
Attached To
Mode
R79 meandmyshadow
Attached
Detach File
Event Timeline