From 868883fd36d1049b86c6cae9d296988f34bb3917 Mon Sep 17 00:00:00 2001 From: Daniel Evans Date: Fri, 3 Jul 2015 02:52:43 +0100 Subject: [PATCH] Add Debugger functionality * Include Angluar JS for UI * Improve styling slightly * Add Interrupt/Continue buttons and api URLs * List of threads when game is paused --- rwengine/include/script/ScriptMachine.hpp | 6 + rwengine/src/engine/GameWorld.cpp | 7 ++ rwengine/src/script/ScriptMachine.cpp | 10 +- rwgame/RWGame.cpp | 19 +++- rwgame/debug/HttpServer.cpp | 130 +++++++++++++++++++++- rwgame/debug/HttpServer.hpp | 5 +- rwgame/ingamestate.cpp | 13 ++- rwgame/ingamestate.hpp | 10 +- rwgame/menustate.cpp | 2 +- 9 files changed, 186 insertions(+), 16 deletions(-) diff --git a/rwengine/include/script/ScriptMachine.hpp b/rwengine/include/script/ScriptMachine.hpp index a46c832a..1a68277a 100644 --- a/rwengine/include/script/ScriptMachine.hpp +++ b/rwengine/include/script/ScriptMachine.hpp @@ -205,6 +205,11 @@ public: */ void removeBreakpoint(SCMThread::pc_t pc); + /** + * Interupt VM execution at the start of the next instruction + */ + void interuptNext(); + /** * @brief executes threads until they are all in waiting state. */ @@ -214,6 +219,7 @@ private: SCMFile* _file; SCMOpcodes* _ops; GameState* state; + bool interupt; std::vector _activeThreads; diff --git a/rwengine/src/engine/GameWorld.cpp b/rwengine/src/engine/GameWorld.cpp index 32a15635..d723a9e2 100644 --- a/rwengine/src/engine/GameWorld.cpp +++ b/rwengine/src/engine/GameWorld.cpp @@ -191,6 +191,7 @@ InstanceObject *GameWorld::createInstance(const uint16_t id, const glm::vec3& po ); instancePool.insert(instance); + allObjects.push_back(instance); if( shouldBeOnGrid(instance) ) { @@ -304,6 +305,7 @@ CutsceneObject *GameWorld::createCutsceneObject(const uint16_t id, const glm::ve m); cutscenePool.insert( instance ); + allObjects.push_back(instance); return instance; @@ -369,6 +371,7 @@ VehicleObject *GameWorld::createVehicle(const uint16_t id, const glm::vec3& pos, vehicle->setGameObjectID(gid); vehiclePool.insert( vehicle ); + allObjects.push_back( vehicle ); return vehicle; } @@ -410,6 +413,7 @@ CharacterObject* GameWorld::createPedestrian(const uint16_t id, const glm::vec3& ped->setGameObjectID(gid); new DefaultAIController(ped); pedestrianPool.insert( ped ); + allObjects.push_back( ped ); return ped; } } @@ -437,6 +441,7 @@ CharacterObject* GameWorld::createPlayer(const glm::vec3& pos, const glm::quat& ped->setLifetime(GameObject::PlayerLifetime); players.push_back(new PlayerController(ped)); pedestrianPool.insert(ped); + allObjects.push_back( ped ); return ped; } } @@ -510,6 +515,8 @@ void GameWorld::destroyObject(GameObject* object) auto& pool = getTypeObjectPool(object); pool.remove(object); + + allObjects.erase(std::find(allObjects.begin(), allObjects.end(), object)); delete object; } diff --git a/rwengine/src/script/ScriptMachine.cpp b/rwengine/src/script/ScriptMachine.cpp index 30d05118..e48bbd4f 100644 --- a/rwengine/src/script/ScriptMachine.cpp +++ b/rwengine/src/script/ScriptMachine.cpp @@ -31,6 +31,11 @@ bool SCMOpcodes::findOpcode(ScriptFunctionID id, ScriptFunctionMeta** out) return false; } +void ScriptMachine::interuptNext() +{ + interupt = true; +} + #include void ScriptMachine::executeThread(SCMThread &t, int msPassed) { @@ -134,8 +139,9 @@ void ScriptMachine::executeThread(SCMThread &t, int msPassed) if( hasDebugging ) { - if( breakpoints.find(pc) != breakpoints.end() ) + if( breakpoints.find(pc) != breakpoints.end() || interupt ) { + interupt = false; SCMBreakpoint bp; bp.pc = pc; bp.thread = &t; @@ -189,7 +195,7 @@ void ScriptMachine::executeThread(SCMThread &t, int msPassed) } ScriptMachine::ScriptMachine(GameState* _state, SCMFile *file, SCMOpcodes *ops) - : _file(file), _ops(ops), state(_state) + : _file(file), _ops(ops), state(_state), interupt(false) { auto globals = _file->getGlobalsSize(); globalData.resize(globals); diff --git a/rwgame/RWGame.cpp b/rwgame/RWGame.cpp index 5e84788b..e9154ec3 100644 --- a/rwgame/RWGame.cpp +++ b/rwgame/RWGame.cpp @@ -37,6 +37,7 @@ RWGame::RWGame(const std::string& gamepath, int argc, char* argv[]) bool newgame = false; bool test = false; bool debugscript = false; + std::string startSave; for( int i = 1; i < argc; ++i ) { @@ -64,6 +65,10 @@ RWGame::RWGame(const std::string& gamepath, int argc, char* argv[]) { debugscript = true; } + if( strcmp( "--load", argv[i] ) == 0 && i+1 < argc ) + { + startSave = argv[i+1]; + } } @@ -129,8 +134,12 @@ RWGame::RWGame(const std::string& gamepath, int argc, char* argv[]) auto loading = new LoadingState(this); if( newgame ) { - loading->setNextState(new IngameState(this,true,test)); + loading->setNextState(new IngameState(this,true, "test")); } + else if( ! startSave.empty() ) + { + loading->setNextState(new IngameState(this,true, startSave)); + } else { loading->setNextState(new MenuState(this)); @@ -185,6 +194,8 @@ void RWGame::loadGame(const std::string& savename) delete state->script; state = nullptr; + log.info("Game", "Loading game " + savename); + newGame(); startScript("data/main.scm"); @@ -219,7 +230,7 @@ void RWGame::startScript(const std::string& name) script->setBreakpointHandler( [&](const SCMBreakpoint& bp) { - log.info("Script", "Breakpoint hit!"); + log.info("Script", "Breakpoint hit!"); std::stringstream ss; ss << " " << bp.function->description << "."; ss << " Args:"; @@ -234,8 +245,8 @@ void RWGame::startScript(const std::string& name) } log.info("Script", ss.str()); - }); - script->addBreakpoint(0); + httpserver->handleBreakpoint(bp); + }); state->script = script; } else { diff --git a/rwgame/debug/HttpServer.cpp b/rwgame/debug/HttpServer.cpp index c0efe6ba..b35b2691 100644 --- a/rwgame/debug/HttpServer.cpp +++ b/rwgame/debug/HttpServer.cpp @@ -1,12 +1,69 @@ #include "HttpServer.hpp" +#include #include #include #include +const char* src_debugger_js = R"( +var app = angular.module('debugApp', []); +app.controller('DebugCtrl', function($scope,$http) { + $scope.threads = []; + $scope.running = true; + $scope.refresh = function() { + var promise = $http.get('/state') + .success(function(data, status, headers, config) { + $scope.running = data.status == 'running'; + $scope.threads = data.threads; + }); + }; + $scope.interrupt = function() { + var promise = $http.get('/interrupt') + .success(function(data, status, headers, config) { + $scope.refresh(); + }); + } + $scope.continue = function() { + var promise = $http.get('/continue') + .success(function(data, status, headers, config) { + $scope.refresh(); + }); + } + $scope.refresh(); +}); + +)"; + +const char* src_page = R"( + + + + + + +

OpenRW Debugger

+ + Game is running +
+

Threads

+
    +
  • +

    {{thread.name}}

    + program counter: {{thread.program_counter}}
    + wake counter: {{thread.wake_counter}} ms
    +
  • +
+
+
+ + + )"; + HttpServer::HttpServer(RWGame* game, GameWorld* world) - : game(game), world(world) + : game(game), world(world), paused(false) {} void HttpServer::run() @@ -48,7 +105,76 @@ void HttpServer::run() listener.close(); } +void HttpServer::handleBreakpoint(const SCMBreakpoint &bp) +{ + paused = true; + + while( paused ) { + // Do nothing + } +} + +std::string HttpServer::getState() const +{ + if( !paused ) { + return R"({"status":"running"})"; + } + else { + std::stringstream ss; + ss << "{"; + ss << R"("status":"interrupted",)"; + ss << R"("threads": [)"; + for(unsigned int i = 0; i < game->getScript()->getThreads().size(); ++i) + { + SCMThread& th = game->getScript()->getThreads()[i]; + bool last = (game->getScript()->getThreads().size() == i+1); + ss << "{" + << "\"program_counter\": " << th.programCounter << "," + << "\"name\": \"" << th.name << "\"," + << "\"wake_counter\": " << th.wakeCounter << "" + << (last ? "}" : "},"); + } + ss << R"(])"; + ss << "}"; + return ss.str(); + } +} + std::string HttpServer::dispatch(std::string method, std::string path) { - return "HTTP/1.1 200 OK\n\n

HELLO FROM OPENRW

"; + std::stringstream ss; + std::string mime = "text/html"; + if(path == "/debugger.js") { + ss << src_debugger_js; + mime = "application/javascript"; + } + else if(path == "/state") { + ss << getState(); + mime = "application/json"; + } + else if(path == "/interrupt") { + game->getScript()->interuptNext(); + /* Block until paused is true */ + while( ! paused ) { std::this_thread::yield(); } + ss << getState(); + mime = "application/json"; + } + else if(path == "/continue") { + paused = false; + ss << getState(); + mime = "application/json"; + } + else if(path == "/") { + ss << src_page; + } + else { + ss << "HTTP/1.1 404 Not Found\n\n"; + return ss.str(); + } + std::stringstream outs; + outs << "HTTP/1.1 200 OK\n" + << "Content-Type: " << mime<< "\n" + << "Connection: Close\n" + << "\n" << ss.str(); + return outs.str(); } diff --git a/rwgame/debug/HttpServer.hpp b/rwgame/debug/HttpServer.hpp index a1e9fc95..dc880d47 100644 --- a/rwgame/debug/HttpServer.hpp +++ b/rwgame/debug/HttpServer.hpp @@ -25,10 +25,13 @@ public: HttpServer(RWGame* game, GameWorld* world); void run(); + void handleBreakpoint(const SCMBreakpoint& bp); private: ReuseableListener listener; RWGame* game; GameWorld* world; + bool paused; - std::string dispatch(std::string method, std::string path); + std::string dispatch(std::string method, std::string path); + std::string getState() const; }; diff --git a/rwgame/ingamestate.cpp b/rwgame/ingamestate.cpp index 1452570d..631c4fa3 100644 --- a/rwgame/ingamestate.cpp +++ b/rwgame/ingamestate.cpp @@ -17,8 +17,8 @@ #define AUTOLOOK_TIME 2.f -IngameState::IngameState(RWGame* game, bool newgame, bool test) - : State(game), started(false), newgame(newgame), test(test), +IngameState::IngameState(RWGame* game, bool newgame, const std::string& save) + : State(game), started(false), newgame(newgame), save(save), autolookTimer(0.f), camMode(IngameState::CAMERA_NORMAL) { } @@ -78,11 +78,16 @@ void IngameState::enter() { if( newgame ) { - if( test ) { + if( save.empty() ) + { + startGame(); + } + else if( save == "test" ) + { startTest(); } else { - startGame(); + game->loadGame( save ); } } started = true; diff --git a/rwgame/ingamestate.hpp b/rwgame/ingamestate.hpp index 8f81b76a..89f94d51 100644 --- a/rwgame/ingamestate.hpp +++ b/rwgame/ingamestate.hpp @@ -18,7 +18,7 @@ class IngameState : public State }; bool started; - bool test; + std::string save; bool newgame; ViewCamera _look; /** Player input */ @@ -28,7 +28,13 @@ class IngameState : public State float autolookTimer; CameraMode camMode; public: - IngameState(RWGame* game, bool newgame = true, bool test = false); + /** + * @brief IngameState + * @param game + * @param newgame + * @param game An empty string, a save game to load, or the string "test". + */ + IngameState(RWGame* game, bool newgame = true, const std::string& save = ""); void startTest(); void startGame(); diff --git a/rwgame/menustate.cpp b/rwgame/menustate.cpp index 95cfe3d8..d3dc0e65 100644 --- a/rwgame/menustate.cpp +++ b/rwgame/menustate.cpp @@ -21,7 +21,7 @@ void MenuState::enterMainMenu() })); m->addEntry(Menu::lambda("Start", [=] { StateManager::get().enter(new IngameState(game)); })); m->addEntry(Menu::lambda("Load Game", [=] { enterLoadMenu(); })); - m->addEntry(Menu::lambda("Test", [=] { StateManager::get().enter(new IngameState(game, true, true)); })); + m->addEntry(Menu::lambda("Test", [=] { StateManager::get().enter(new IngameState(game, true, "true")); })); m->addEntry(Menu::lambda("Options", [] { std::cout << "Options" << std::endl; })); m->addEntry(Menu::lambda("Exit", [=] { getWindow().close(); })); this->enterMenu(m);