From 4cbb31b8e5fda368f6c5c08abcd6581933869c1e Mon Sep 17 00:00:00 2001 From: Benau Date: Thu, 4 Feb 2016 08:17:02 +0800 Subject: [PATCH 001/324] Ghost kart replay fixes: 1. Allow saving steering, speed, suspension length 2. No more crashes when trying to replay --- src/karts/ghost_kart.cpp | 32 ++++++++++++++++++++++++-- src/karts/ghost_kart.hpp | 10 ++++++-- src/karts/kart.hpp | 7 +++--- src/karts/kart_model.cpp | 42 +++++++++++++++++++++++++--------- src/karts/kart_model.hpp | 4 ++-- src/replay/replay_base.hpp | 15 ++++++++++-- src/replay/replay_play.cpp | 18 +++++++++++---- src/replay/replay_recorder.cpp | 37 ++++++++++++++++++++++++++---- src/replay/replay_recorder.hpp | 3 +++ 9 files changed, 136 insertions(+), 32 deletions(-) diff --git a/src/karts/ghost_kart.cpp b/src/karts/ghost_kart.cpp index 885e3dd73..c3804dcc7 100644 --- a/src/karts/ghost_kart.cpp +++ b/src/karts/ghost_kart.cpp @@ -17,6 +17,7 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "karts/ghost_kart.hpp" +#include "karts/kart_model.hpp" #include "modes/world.hpp" #include "LinearMath/btQuaternion.h" @@ -28,6 +29,9 @@ GhostKart::GhostKart(const std::string& ident) { m_current_transform = 0; m_next_event = 0; + m_all_times.clear(); + m_all_transform.clear(); + m_all_physic_info.clear(); } // GhostKart // ---------------------------------------------------------------------------- @@ -46,7 +50,9 @@ void GhostKart::reset() * the previous time and transform. * \param */ -void GhostKart::addTransform(float time, const btTransform &trans) +void GhostKart::addTransform(float time, + const btTransform &trans, + const ReplayBase::PhysicInfo &pi) { // FIXME: for now avoid that transforms for the same time are set // twice (to avoid division by zero in update). This should be @@ -55,6 +61,19 @@ void GhostKart::addTransform(float time, const btTransform &trans) return; m_all_times.push_back(time); m_all_transform.push_back(trans); + m_all_physic_info.push_back(pi); + + // Use first frame of replay to calculate default suspension + if (m_all_physic_info.size() == 1) + { + float f = 0; + for (int i = 0; i < 4; i++) + f += m_all_physic_info[0].m_suspension_length[i]; + m_graphical_y_offset = -f / 4 + getKartModel()->getLowestPoint(); + m_kart_model + ->setDefaultSuspension(); + } + } // addTransform // ---------------------------------------------------------------------------- @@ -114,5 +133,14 @@ void GhostKart::updateTransform(float t, float dt) .getRotation(), f); setRotation(q); - Moveable::updateGraphics(dt, Vec3(0,0,0), btQuaternion(0, 0, 0, 1)); + + Vec3 center_shift(0, 0, 0); + center_shift.setY(m_graphical_y_offset); + center_shift = getTrans().getBasis() * center_shift; + + Moveable::updateGraphics(dt, center_shift, btQuaternion(0, 0, 0, 1)); + getKartModel()->update(dt, dt*(m_all_physic_info[m_current_transform].m_speed), + m_all_physic_info[m_current_transform].m_steer, + m_all_physic_info[m_current_transform].m_speed, + m_current_transform); } // update diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp index f08742a1a..8b5df4237 100644 --- a/src/karts/ghost_kart.hpp +++ b/src/karts/ghost_kart.hpp @@ -42,6 +42,8 @@ private: /** The transforms to assume at the corresponding time in m_all_times. */ std::vector m_all_transform; + std::vector m_all_physic_info; + std::vector m_replay_events; /** Pointer to the last index in m_all_times that is smaller than @@ -55,7 +57,9 @@ private: public: GhostKart(const std::string& ident); virtual void update (float dt); - virtual void addTransform(float time, const btTransform &trans); + virtual void addTransform(float time, + const btTransform &trans, + const ReplayBase::PhysicInfo &pi); virtual void addReplayEvent(const ReplayBase::KartReplayEvent &kre); virtual void reset(); // ------------------------------------------------------------------------ @@ -68,6 +72,8 @@ public: // Not needed to create any physics for a ghost kart. virtual void createPhysics() {} // ------------------------------------------------------------------------ - + const float& getSuspensionLength(int index, int wheel) const + { return m_all_physic_info[index].m_suspension_length[wheel]; } + // ------------------------------------------------------------------------ }; // GhostKart #endif diff --git a/src/karts/kart.hpp b/src/karts/kart.hpp index 955337108..5374e4e3a 100644 --- a/src/karts/kart.hpp +++ b/src/karts/kart.hpp @@ -64,6 +64,10 @@ class TerrainInfo; class Kart : public AbstractKart { friend class Skidding; +protected: + /** Offset of the graphical kart chassis from the physical chassis. */ + float m_graphical_y_offset; + private: /** Handles speed increase and capping due to powerup, terrain, ... */ MaxSpeed *m_max_speed; @@ -106,9 +110,6 @@ private: * new lap is triggered. */ Vec3 m_xyz_front; - /** Offset of the graphical kart chassis from the physical chassis. */ - float m_graphical_y_offset; - /** True if the kart wins, false otherwise. */ bool m_race_result; diff --git a/src/karts/kart_model.cpp b/src/karts/kart_model.cpp index 8f78041da..45ab18a0c 100644 --- a/src/karts/kart_model.cpp +++ b/src/karts/kart_model.cpp @@ -769,6 +769,14 @@ void KartModel::OnAnimationEnd(scene::IAnimatedMeshSceneNode *node) // ---------------------------------------------------------------------------- void KartModel::setDefaultSuspension() { + GhostKart* gk = dynamic_cast(m_kart); + if (gk) + { + for (int i = 0; i < 4; i++) + m_default_physics_suspension[i] = gk->getSuspensionLength(0, i); + return; + } + for(int i=0; igetVehicle()->getNumWheels(); i++) { const btWheelInfo &wi = m_kart->getVehicle()->getWheelInfo(i); @@ -786,30 +794,42 @@ void KartModel::setDefaultSuspension() * \param suspension Suspension height for all four wheels. * \param speed The speed of the kart in meters/sec, used for the * speed-weighted objects' animations + * \param gt_replay_index The index to get replay data, used by ghost kart */ -void KartModel::update(float dt, float distance, float steer, float speed) +void KartModel::update(float dt, float distance, float steer, float speed, + int gt_replay_index) { core::vector3df wheel_steer(0, steer*30.0f, 0); for(unsigned int i=0; i<4; i++) { - if (!m_kart || !m_wheel_node[i]) continue; + if (!m_kart || !m_wheel_node[i]) continue; #ifdef DEBUG - if (UserConfigParams::m_physics_debug && - !dynamic_cast(m_kart) ) - { - const btWheelInfo &wi = m_kart->getVehicle()->getWheelInfo(i); - // Make wheels that are not touching the ground invisible - m_wheel_node[i]->setVisible(wi.m_raycastInfo.m_isInContact); - } + if (UserConfigParams::m_physics_debug && + !dynamic_cast(m_kart) ) + { + const btWheelInfo &wi = m_kart->getVehicle()->getWheelInfo(i); + // Make wheels that are not touching the ground invisible + m_wheel_node[i]->setVisible(wi.m_raycastInfo.m_isInContact); + } #endif core::vector3df pos = m_wheel_graphics_position[i].toIrrVector(); - const btWheelInfo &wi = m_kart->getVehicle()->getWheelInfo(i); + float suspension_length = 0.0f; + GhostKart* gk = dynamic_cast(m_kart); + if (gk && gt_replay_index != -1) + { + suspension_length = gk->getSuspensionLength(gt_replay_index, i); + } + else if (!gk) + { + suspension_length = m_kart->getVehicle()->getWheelInfo(i). + m_raycastInfo.m_suspensionLength; + } // Check documentation of Kart::updateGraphics for the following line pos.Y += m_default_physics_suspension[i] - - wi.m_raycastInfo.m_suspensionLength + - suspension_length - m_kart_lowest_point; m_wheel_node[i]->setPosition(pos); diff --git a/src/karts/kart_model.hpp b/src/karts/kart_model.hpp index 6180cb89a..27be3190f 100644 --- a/src/karts/kart_model.hpp +++ b/src/karts/kart_model.hpp @@ -237,8 +237,8 @@ public: void loadInfo(const XMLNode &node); bool loadModels(const KartProperties &kart_properties); void setDefaultSuspension(); - void update(float dt, float distance, float steer, - float speed); + void update(float dt, float distance, float steer, float speed, + int gt_replay_index = -1); void finishedRace(); scene::ISceneNode* attachModel(bool animatedModels, bool always_animated); diff --git a/src/replay/replay_base.hpp b/src/replay/replay_base.hpp index 6b48bdc24..b922822d7 100644 --- a/src/replay/replay_base.hpp +++ b/src/replay/replay_base.hpp @@ -42,11 +42,22 @@ protected: struct TransformEvent { /** Time at which this event happens. */ - float m_time; + float m_time; /** The transform at a certain time. */ - btTransform m_transform; + btTransform m_transform; }; // TransformEvent + // ------------------------------------------------------------------------ + struct PhysicInfo + { + /** The speed at a certain time. */ + float m_speed; + /** The steering at a certain time. */ + float m_steer; + /** The suspension length of 4 wheels at a certain time. */ + float m_suspension_length[4]; + }; // PhysicInfo + // ------------------------------------------------------------------------ /** Records all other events - atm start and end skidding. */ struct KartReplayEvent diff --git a/src/replay/replay_play.cpp b/src/replay/replay_play.cpp index 3a9c5fc83..3ebbf85d9 100644 --- a/src/replay/replay_play.cpp +++ b/src/replay/replay_play.cpp @@ -172,20 +172,28 @@ void ReplayPlay::readKartData(FILE *fd, char *next_line) for(unsigned int i=0; igetNumberOfKarts()); + m_physic_info.resize(race_manager->getNumberOfKarts()); m_skid_control.resize(race_manager->getNumberOfKarts()); m_kart_replay_event.resize(race_manager->getNumberOfKarts()); unsigned int max_frames = (unsigned int)( stk_config->m_replay_max_time @@ -60,6 +64,7 @@ void ReplayRecorder::init() for(unsigned int i=0; igetNumberOfKarts(); i++) { m_transform_events[i].resize(max_frames); + m_physic_info[i].resize(max_frames); // Rather arbitraritly sized, it will be added with push_back m_kart_replay_event[i].reserve(500); } @@ -140,10 +145,25 @@ void ReplayRecorder::update(float dt) } continue; } - TransformEvent *p = &(m_transform_events[i][m_count_transforms[i]-1]); - p->m_time = World::getWorld()->getTime(); + TransformEvent *p = &(m_transform_events[i][m_count_transforms[i]-1]); + PhysicInfo *q = &(m_physic_info[i][m_count_transforms[i]-1]); + p->m_time = World::getWorld()->getTime(); p->m_transform.setOrigin(kart->getXYZ()); p->m_transform.setRotation(kart->getVisualRotation()); + + q->m_speed = kart->getSpeed(); + q->m_steer = kart->getSteerPercent(); + const int num_wheels = kart->getVehicle()->getNumWheels(); + for (int j = 0; j < 4; j++) + { + if (j > num_wheels || num_wheels == 0) + q->m_suspension_length[j] = 0.0f; + else + { + q->m_suspension_length[j] = kart->getVehicle() + ->getWheelInfo(j).m_raycastInfo.m_suspensionLength; + } + } } // for i } // update @@ -184,8 +204,9 @@ void ReplayRecorder::Save() m_count_transforms[k]); for(unsigned int i=0; im_time, p->m_transform.getOrigin().getX(), p->m_transform.getOrigin().getY(), @@ -193,7 +214,13 @@ void ReplayRecorder::Save() p->m_transform.getRotation().getX(), p->m_transform.getRotation().getY(), p->m_transform.getRotation().getZ(), - p->m_transform.getRotation().getW() + p->m_transform.getRotation().getW(), + q->m_speed, + q->m_steer, + q->m_suspension_length[0], + q->m_suspension_length[1], + q->m_suspension_length[2], + q->m_suspension_length[3] ); } // for i fprintf(fd, "events: %d\n", (int)m_kart_replay_event[k].size()); diff --git a/src/replay/replay_recorder.hpp b/src/replay/replay_recorder.hpp index c77c2015c..aa09178b1 100644 --- a/src/replay/replay_recorder.hpp +++ b/src/replay/replay_recorder.hpp @@ -34,6 +34,9 @@ private: /** A separate vector of Replay Events for all transforms. */ std::vector< std::vector > m_transform_events; + /** A separate vector of Replay Events for all transforms. */ + std::vector< std::vector > m_physic_info; + /** Time at which a transform was saved for the last time. */ std::vector m_last_saved_time; From 38eeddd4e8b41a27cb0be46871f958c8a4dc34bc Mon Sep 17 00:00:00 2001 From: Benau Date: Thu, 4 Feb 2016 09:51:59 +0800 Subject: [PATCH 002/324] No reference for int and float --- src/karts/ghost_kart.hpp | 2 +- src/modes/soccer_world.hpp | 8 ++++---- src/tracks/battle_graph.cpp | 2 +- src/tracks/battle_graph.hpp | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp index 8b5df4237..334348f94 100644 --- a/src/karts/ghost_kart.hpp +++ b/src/karts/ghost_kart.hpp @@ -72,7 +72,7 @@ public: // Not needed to create any physics for a ghost kart. virtual void createPhysics() {} // ------------------------------------------------------------------------ - const float& getSuspensionLength(int index, int wheel) const + const float getSuspensionLength(int index, int wheel) const { return m_all_physic_info[index].m_suspension_length[wheel]; } // ------------------------------------------------------------------------ }; // GhostKart diff --git a/src/modes/soccer_world.hpp b/src/modes/soccer_world.hpp index 368c7d8e4..d43a830d7 100644 --- a/src/modes/soccer_world.hpp +++ b/src/modes/soccer_world.hpp @@ -157,23 +157,23 @@ public: m_blue_score_times : m_red_score_times); } // ------------------------------------------------------------------------ - const int& getKartNode(unsigned int kart_id) const + const int getKartNode(unsigned int kart_id) const { return m_kart_on_node[kart_id]; } // ------------------------------------------------------------------------ - const int& getBallNode() const + const int getBallNode() const { return m_ball_on_node; } // ------------------------------------------------------------------------ const Vec3& getBallPosition() const { return m_ball_position; } // ------------------------------------------------------------------------ - const int& getGoalNode(SoccerTeam team) const + const int getGoalNode(SoccerTeam team) const { return (team == SOCCER_TEAM_BLUE ? m_blue_goal_node : m_red_goal_node); } // ------------------------------------------------------------------------ bool isCorrectGoal(unsigned int kart_id, bool first_goal) const; // ------------------------------------------------------------------------ - const int& getDefender(SoccerTeam team) const + const int getDefender(SoccerTeam team) const { return (team == SOCCER_TEAM_BLUE ? m_blue_defender : m_red_defender); } diff --git a/src/tracks/battle_graph.cpp b/src/tracks/battle_graph.cpp index 5ecca6832..1cddf3cc2 100644 --- a/src/tracks/battle_graph.cpp +++ b/src/tracks/battle_graph.cpp @@ -216,7 +216,7 @@ int BattleGraph::pointToNode(const int cur_node, // ----------------------------------------------------------------------------- -const int & BattleGraph::getNextShortestPathPoly(int i, int j) const +const int BattleGraph::getNextShortestPathPoly(int i, int j) const { if (i == BattleGraph::UNKNOWN_POLY || j == BattleGraph::UNKNOWN_POLY) return BattleGraph::UNKNOWN_POLY; diff --git a/src/tracks/battle_graph.hpp b/src/tracks/battle_graph.hpp index 6b872f1f2..fd32d7e94 100644 --- a/src/tracks/battle_graph.hpp +++ b/src/tracks/battle_graph.hpp @@ -120,7 +120,7 @@ public: /** Returns the next polygon on the shortest path from i to j. * Note: m_parent_poly[j][i] contains the parent of i on path from j to i, * which is the next node on the path from i to j (undirected graph) */ - const int & getNextShortestPathPoly(int i, int j) const; + const int getNextShortestPathPoly(int i, int j) const; const std::vector < std::pair >& getItemList() { return m_items_on_graph; } From 5810acb11470200ea98503e62f9f81c58dd8d0fa Mon Sep 17 00:00:00 2001 From: Benau Date: Thu, 4 Feb 2016 10:11:14 +0800 Subject: [PATCH 003/324] Fix memory leak --- src/tracks/track.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tracks/track.cpp b/src/tracks/track.cpp index 0f7cb37a6..b50579225 100644 --- a/src/tracks/track.cpp +++ b/src/tracks/track.cpp @@ -493,6 +493,7 @@ void Track::loadTrackInfo() if(!root || root->getName()!="track") { + delete root; std::ostringstream o; o<<"Can't load track '"< Date: Fri, 5 Feb 2016 09:30:40 +0800 Subject: [PATCH 004/324] Save nitro and zipper GFX in replay --- src/karts/abstract_kart.hpp | 4 ++ src/karts/ghost_kart.cpp | 75 +++++++++++++------------------ src/karts/ghost_kart.hpp | 17 +++---- src/karts/kart.hpp | 2 +- src/karts/kart_model.cpp | 7 ++- src/replay/replay_base.hpp | 11 ++--- src/replay/replay_play.cpp | 41 +++++------------ src/replay/replay_recorder.cpp | 82 ++++++++++++++++++---------------- src/replay/replay_recorder.hpp | 10 ++--- 9 files changed, 109 insertions(+), 140 deletions(-) diff --git a/src/karts/abstract_kart.hpp b/src/karts/abstract_kart.hpp index c0cba1cfe..cae8dcdd6 100644 --- a/src/karts/abstract_kart.hpp +++ b/src/karts/abstract_kart.hpp @@ -46,6 +46,7 @@ class Material; class Powerup; class Skidding; class SlipStream; +class TerrainInfo; /** An abstract interface for the actual karts. Some functions are actually * implemented here in order to allow inlining. @@ -421,6 +422,9 @@ public: /** Shows the star effect for a certain time. */ virtual void showStarEffect(float t) = 0; // ------------------------------------------------------------------------ + /** Returns the terrain info oject. */ + virtual const TerrainInfo *getTerrainInfo() const = 0; + // ------------------------------------------------------------------------ /** Called when the kart crashes against another kart. * \param k The kart that was hit. * \param update_attachments If true the attachment of this kart and the diff --git a/src/karts/ghost_kart.cpp b/src/karts/ghost_kart.cpp index c3804dcc7..ba0e2e066 100644 --- a/src/karts/ghost_kart.cpp +++ b/src/karts/ghost_kart.cpp @@ -17,6 +17,7 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "karts/ghost_kart.hpp" +#include "karts/kart_gfx.hpp" #include "karts/kart_model.hpp" #include "modes/world.hpp" @@ -27,11 +28,10 @@ GhostKart::GhostKart(const std::string& ident) : Kart(ident, /*world kart id*/99999, /*position*/-1, btTransform(), PLAYER_DIFFICULTY_NORMAL) { - m_current_transform = 0; - m_next_event = 0; m_all_times.clear(); m_all_transform.clear(); m_all_physic_info.clear(); + m_all_replay_events.clear(); } // GhostKart // ---------------------------------------------------------------------------- @@ -40,19 +40,15 @@ void GhostKart::reset() m_node->setVisible(true); Kart::reset(); m_current_transform = 0; - m_next_event = 0; // This will set the correct start position update(0); } // reset // ---------------------------------------------------------------------------- -/** Sets the next time and transform. The current time and transform becomes - * the previous time and transform. - * \param - */ -void GhostKart::addTransform(float time, - const btTransform &trans, - const ReplayBase::PhysicInfo &pi) +void GhostKart::addReplayEvent(float time, + const btTransform &trans, + const ReplayBase::PhysicInfo &pi, + const ReplayBase::KartReplayEvent &kre) { // FIXME: for now avoid that transforms for the same time are set // twice (to avoid division by zero in update). This should be @@ -62,6 +58,7 @@ void GhostKart::addTransform(float time, m_all_times.push_back(time); m_all_transform.push_back(trans); m_all_physic_info.push_back(pi); + m_all_replay_events.push_back(kre); // Use first frame of replay to calculate default suspension if (m_all_physic_info.size() == 1) @@ -74,55 +71,45 @@ void GhostKart::addTransform(float time, ->setDefaultSuspension(); } -} // addTransform - -// ---------------------------------------------------------------------------- -/** Adds a replay event for this kart. - */ -void GhostKart::addReplayEvent(const ReplayBase::KartReplayEvent &kre) -{ - m_replay_events.push_back(kre); } // addReplayEvent // ---------------------------------------------------------------------------- -/** Updates the ghost data each time step. It uses interpolation to get a new - * position and rotation. +/** Updates the current event of the ghost kart using interpolation * \param dt Time step size. */ void GhostKart::update(float dt) { float t = World::getWorld()->getTime(); - // Don't do anything at startup - if(t==0) return; - updateTransform(t, dt); - while(m_next_event < m_replay_events.size() && - m_replay_events[m_next_event].m_time <= t) - { - Log::debug("Ghost_Kart", "Handling event %d", m_next_event); - // Handle the next event now - m_next_event++; - } -} -// ---------------------------------------------------------------------------- -/** Updates the current transform of the ghost kart using interpolation - * \param t Current world time. - * \param dt Time step size. - */ -void GhostKart::updateTransform(float t, float dt) -{ - // Find (if necessary) the next index to use - while(m_current_transform+1 < m_all_times.size() && - t>=m_all_times[m_current_transform+1]) + if (t != 0.0f) { - m_current_transform ++; + while (m_current_transform + 1 < m_all_times.size() && + t >= m_all_times[m_current_transform+1]) + { + m_current_transform++; + } } - if(m_current_transform+1>=m_all_times.size()) + + if (m_current_transform + 1 >= m_all_times.size()) { m_node->setVisible(false); return; } + float nitro_frac = 0; + if (m_all_replay_events[m_current_transform].m_on_nitro) + { + nitro_frac = fabsf(m_all_physic_info[m_current_transform].m_speed) / + (m_kart_properties->getEngineMaxSpeed()); + + if (nitro_frac > 1.0f) + nitro_frac = 1.0f; + } + getKartGFX()->updateNitroGraphics(nitro_frac); + + if (m_all_replay_events[m_current_transform].m_on_zipper) + showZipperFire(); + float f =(t - m_all_times[m_current_transform]) / ( m_all_times[m_current_transform+1] - m_all_times[m_current_transform] ); @@ -143,4 +130,6 @@ void GhostKart::updateTransform(float t, float dt) m_all_physic_info[m_current_transform].m_steer, m_all_physic_info[m_current_transform].m_speed, m_current_transform); + + getKartGFX()->update(dt); } // update diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp index 334348f94..cef74b2c4 100644 --- a/src/karts/ghost_kart.hpp +++ b/src/karts/ghost_kart.hpp @@ -44,23 +44,15 @@ private: std::vector m_all_physic_info; - std::vector m_replay_events; + std::vector m_all_replay_events; /** Pointer to the last index in m_all_times that is smaller than * the current world time. */ unsigned int m_current_transform; - /** Index of the next kart replay event. */ - unsigned int m_next_event; - - void updateTransform(float t, float dt); public: GhostKart(const std::string& ident); virtual void update (float dt); - virtual void addTransform(float time, - const btTransform &trans, - const ReplayBase::PhysicInfo &pi); - virtual void addReplayEvent(const ReplayBase::KartReplayEvent &kre); virtual void reset(); // ------------------------------------------------------------------------ /** No physics body for ghost kart, so nothing to adjust. */ @@ -72,8 +64,13 @@ public: // Not needed to create any physics for a ghost kart. virtual void createPhysics() {} // ------------------------------------------------------------------------ - const float getSuspensionLength(int index, int wheel) const + const float getSuspensionLength(int index, int wheel) const { return m_all_physic_info[index].m_suspension_length[wheel]; } // ------------------------------------------------------------------------ + void addReplayEvent(float time, + const btTransform &trans, + const ReplayBase::PhysicInfo &pi, + const ReplayBase::KartReplayEvent &kre); + // ------------------------------------------------------------------------ }; // GhostKart #endif diff --git a/src/karts/kart.hpp b/src/karts/kart.hpp index 5374e4e3a..eea4ded04 100644 --- a/src/karts/kart.hpp +++ b/src/karts/kart.hpp @@ -431,7 +431,7 @@ public: virtual void showStarEffect(float t); // ------------------------------------------------------------------------ /** Returns the terrain info oject. */ - TerrainInfo *getTerrainInfo() { return m_terrain_info; } + virtual const TerrainInfo *getTerrainInfo() const { return m_terrain_info; } // ------------------------------------------------------------------------ virtual void setOnScreenText(const wchar_t *text); // ------------------------------------------------------------------------ diff --git a/src/karts/kart_model.cpp b/src/karts/kart_model.cpp index 45ab18a0c..16b13acf7 100644 --- a/src/karts/kart_model.cpp +++ b/src/karts/kart_model.cpp @@ -817,11 +817,14 @@ void KartModel::update(float dt, float distance, float steer, float speed, float suspension_length = 0.0f; GhostKart* gk = dynamic_cast(m_kart); - if (gk && gt_replay_index != -1) + // Prevent using m_default_physics_suspension uninitialized + if (gk && gt_replay_index == -1) break; + + if (gk) { suspension_length = gk->getSuspensionLength(gt_replay_index, i); } - else if (!gk) + else { suspension_length = m_kart->getVehicle()->getWheelInfo(i). m_raycastInfo.m_suspensionLength; diff --git a/src/replay/replay_base.hpp b/src/replay/replay_base.hpp index b922822d7..ed6b7a5e8 100644 --- a/src/replay/replay_base.hpp +++ b/src/replay/replay_base.hpp @@ -59,17 +59,12 @@ protected: }; // PhysicInfo // ------------------------------------------------------------------------ - /** Records all other events - atm start and end skidding. */ + /** Records all other events - atm nitro and zipper handling. */ struct KartReplayEvent { /** The type of event. */ - enum KartReplayEventType {KRE_NONE, - KRE_SKID_LEFT, - KRE_SKID_MIN = KRE_SKID_LEFT, - KRE_SKID_RIGHT, KRE_SKID_RELEASE} m_type; - - /** Time at which this event happens. */ - float m_time; + bool m_on_nitro; + bool m_on_zipper; }; // KartReplayEvent // ------------------------------------------------------------------------ diff --git a/src/replay/replay_play.cpp b/src/replay/replay_play.cpp index 3ebbf85d9..e32b4e21a 100644 --- a/src/replay/replay_play.cpp +++ b/src/replay/replay_play.cpp @@ -173,27 +173,32 @@ void ReplayPlay::readKartData(FILE *fd, char *next_line) { fgets(s, 1023, fd); float x, y, z, rx, ry, rz, rw, time, speed, steer, w1, w2, w3, w4; + int nitro, zipper; // Check for EV_TRANSFORM event: // ----------------------------- - if(sscanf(s, "%f %f %f %f %f %f %f %f %f %f %f %f %f %f\n", + if(sscanf(s, "%f %f %f %f %f %f %f %f %f %f %f %f %f %f %d %d\n", &time, &x, &y, &z, &rx, &ry, &rz, &rw, - &speed, &steer, &w1, &w2, &w3, &w4 - )==14) + &speed, &steer, &w1, &w2, &w3, &w4, + &nitro, &zipper + )==16) { btQuaternion q(rx, ry, rz, rw); btVector3 xyz(x, y, z); PhysicInfo pi = {0}; + KartReplayEvent kre = {0}; pi.m_speed = speed; pi.m_steer = steer; pi.m_suspension_length[0] = w1; pi.m_suspension_length[1] = w2; pi.m_suspension_length[2] = w3; pi.m_suspension_length[3] = w4; - m_ghost_karts[m_ghost_karts.size()-1].addTransform(time, - btTransform(q, xyz), pi); + kre.m_on_nitro = (bool)nitro; + kre.m_on_zipper = (bool)zipper; + m_ghost_karts[m_ghost_karts.size()-1].addReplayEvent(time, + btTransform(q, xyz), pi, kre); } else { @@ -204,31 +209,5 @@ void ReplayPlay::readKartData(FILE *fd, char *next_line) Log::warn("Replay", "Ignored."); } } // for i - fgets(s, 1023, fd); - unsigned int num_events; - if(sscanf(s,"events: %u",&num_events)!=1) - Log::warn("Replay", "Number of events not found in replay file " - "for kart %d.", m_ghost_karts.size()-1); - - for(unsigned int i=0; i @@ -45,6 +46,7 @@ ReplayRecorder::~ReplayRecorder() { m_transform_events.clear(); m_physic_info.clear(); + m_kart_replay_event.clear(); } // ~Replay //----------------------------------------------------------------------------- @@ -55,9 +57,9 @@ void ReplayRecorder::init() { m_transform_events.clear(); m_physic_info.clear(); + m_kart_replay_event.clear(); m_transform_events.resize(race_manager->getNumberOfKarts()); m_physic_info.resize(race_manager->getNumberOfKarts()); - m_skid_control.resize(race_manager->getNumberOfKarts()); m_kart_replay_event.resize(race_manager->getNumberOfKarts()); unsigned int max_frames = (unsigned int)( stk_config->m_replay_max_time / stk_config->m_replay_dt); @@ -65,8 +67,7 @@ void ReplayRecorder::init() { m_transform_events[i].resize(max_frames); m_physic_info[i].resize(max_frames); - // Rather arbitraritly sized, it will be added with push_back - m_kart_replay_event[i].reserve(500); + m_kart_replay_event[i].resize(max_frames); } m_count_transforms.clear(); m_count_transforms.resize(race_manager->getNumberOfKarts(), 0); @@ -103,40 +104,22 @@ void ReplayRecorder::update(float dt) for(unsigned int i=0; igetKart(i); - - // Check if skidding state has changed. If so, store this - if(kart->getControls().m_skid != m_skid_control[i]) - { - KartReplayEvent kre; - kre.m_time = World::getWorld()->getTime(); - if(kart->getControls().m_skid==KartControl::SC_LEFT) - kre.m_type = KartReplayEvent::KRE_SKID_LEFT; - else if(kart->getControls().m_skid==KartControl::SC_RIGHT) - kre.m_type = KartReplayEvent::KRE_SKID_RIGHT; - else - kre.m_type = KartReplayEvent::KRE_NONE; - m_kart_replay_event[i].push_back(kre); - if(m_skid_control[i]!=KartControl::SC_NONE) - m_skid_control[i] = KartControl::SC_NONE; - else - m_skid_control[i] = kart->getControls().m_skid; - } #ifdef DEBUG - m_count ++; + m_count++; #endif - if(time - m_last_saved_time[i]m_replay_dt) + if (time - m_last_saved_time[i] < stk_config->m_replay_dt) { #ifdef DEBUG - m_count_skipped_time ++; + m_count_skipped_time++; #endif continue; } m_last_saved_time[i] = time; m_count_transforms[i]++; - if(m_count_transforms[i]>=m_transform_events[i].size()) + if (m_count_transforms[i] >= m_transform_events[i].size()) { // Only print this message once. - if(m_count_transforms[i]==m_transform_events[i].size()) + if (m_count_transforms[i] == m_transform_events[i].size()) { char buffer[100]; sprintf(buffer, "Can't store more events for kart %s.", @@ -147,6 +130,8 @@ void ReplayRecorder::update(float dt) } TransformEvent *p = &(m_transform_events[i][m_count_transforms[i]-1]); PhysicInfo *q = &(m_physic_info[i][m_count_transforms[i]-1]); + KartReplayEvent *r = &(m_kart_replay_event[i][m_count_transforms[i]-1]); + p->m_time = World::getWorld()->getTime(); p->m_transform.setOrigin(kart->getXYZ()); p->m_transform.setRotation(kart->getVisualRotation()); @@ -164,6 +149,28 @@ void ReplayRecorder::update(float dt) ->getWheelInfo(j).m_raycastInfo.m_suspensionLength; } } + + bool nitro = false; + bool zipper = false; + const KartControl kc = kart->getControls(); + const Material* m = kart->getTerrainInfo()->getMaterial(); + if (kc.m_nitro && kart->isOnGround() && + kart->isOnMinNitroTime() > 0.0f && kart->getEnergy() > 0.0f) + { + nitro = true; + } + if (m) + { + if (m->isZipper() && kart->isOnGround()) + zipper = true; + } + if (kc.m_fire && + kart->getPowerup()->getType() == PowerupManager::POWERUP_ZIPPER) + { + zipper = true; + } + r->m_on_nitro = nitro; + r->m_on_zipper = zipper; } // for i } // update @@ -177,7 +184,7 @@ void ReplayRecorder::Save() m_count, m_count_skipped_time); #endif FILE *fd = openReplayFile(/*writeable*/true); - if(!fd) + if (!fd) { Log::error("ReplayRecorder", "Can't open '%s' for writing - can't save replay data.", getReplayFilename().c_str()); @@ -195,18 +202,19 @@ void ReplayRecorder::Save() unsigned int max_frames = (unsigned int)( stk_config->m_replay_max_time / stk_config->m_replay_dt ); - for(unsigned int k=0; kgetKart(k)->getIdent().c_str()); fprintf(fd, "size: %d\n", m_count_transforms[k]); unsigned int num_transforms = std::min(max_frames, m_count_transforms[k]); - for(unsigned int i=0; im_time, p->m_transform.getOrigin().getX(), p->m_transform.getOrigin().getY(), @@ -220,15 +228,11 @@ void ReplayRecorder::Save() q->m_suspension_length[0], q->m_suspension_length[1], q->m_suspension_length[2], - q->m_suspension_length[3] + q->m_suspension_length[3], + (int)r->m_on_nitro, + (int)r->m_on_zipper ); } // for i - fprintf(fd, "events: %d\n", (int)m_kart_replay_event[k].size()); - for(unsigned int i=0; im_time, p->m_type); - } } fclose(fd); } // Save diff --git a/src/replay/replay_recorder.hpp b/src/replay/replay_recorder.hpp index aa09178b1..2bcb344aa 100644 --- a/src/replay/replay_recorder.hpp +++ b/src/replay/replay_recorder.hpp @@ -34,20 +34,18 @@ private: /** A separate vector of Replay Events for all transforms. */ std::vector< std::vector > m_transform_events; - /** A separate vector of Replay Events for all transforms. */ + /** A separate vector of Replay Events for all physic info. */ std::vector< std::vector > m_physic_info; + /** A separate vector of Replay Events for all other events. */ + std::vector< std::vector > m_kart_replay_event; + /** Time at which a transform was saved for the last time. */ std::vector m_last_saved_time; /** Counts the number of transform events for each kart. */ std::vector m_count_transforms; - /** Stores the last skid state. */ - std::vector m_skid_control; - - std::vector< std::vector > m_kart_replay_event; - /** Static pointer to the one instance of the replay object. */ static ReplayRecorder *m_replay_recorder; From 647f42e98473efff4724f983bd3af775e10436a4 Mon Sep 17 00:00:00 2001 From: Benau Date: Fri, 5 Feb 2016 09:40:34 +0800 Subject: [PATCH 005/324] Update comment --- src/replay/replay_base.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/replay/replay_base.hpp b/src/replay/replay_base.hpp index ed6b7a5e8..92aa91c4b 100644 --- a/src/replay/replay_base.hpp +++ b/src/replay/replay_base.hpp @@ -62,7 +62,8 @@ protected: /** Records all other events - atm nitro and zipper handling. */ struct KartReplayEvent { - /** The type of event. */ + /** True if the kart recorded is using nitro/zipper. + * If true, a suitable GFX will be replayed. */ bool m_on_nitro; bool m_on_zipper; }; // KartReplayEvent From c3f589561c64b698d2d2efded18017b88df0e5eb Mon Sep 17 00:00:00 2001 From: Benau Date: Sat, 6 Feb 2016 14:52:50 +0800 Subject: [PATCH 006/324] Initial work on supporting real race with ghost karts To test, you need to pass --ghost to supertuxkart with choosing a track that has replay recorded, you can record one with artist debug mode. TODO: correct position handling --- src/items/flyable.cpp | 4 +- src/items/powerup.cpp | 4 +- src/items/swatter.cpp | 2 +- src/karts/abstract_kart.hpp | 3 + src/karts/controller/skidding_ai.cpp | 2 +- src/karts/ghost_kart.cpp | 10 +++- src/karts/ghost_kart.hpp | 6 +- src/karts/kart.cpp | 22 ++++--- src/karts/kart.hpp | 11 ++-- src/karts/kart_model.cpp | 2 +- src/karts/skidding.cpp | 3 +- src/modes/linear_world.cpp | 19 ++++-- src/modes/standard_race.cpp | 5 +- src/modes/world.cpp | 54 ++++++++++++----- src/race/race_manager.cpp | 35 +++++++++-- src/replay/replay_play.cpp | 82 ++++++++++++++++++-------- src/replay/replay_play.hpp | 23 ++++++-- src/replay/replay_recorder.cpp | 15 ++++- src/states_screens/race_gui.cpp | 6 +- src/states_screens/race_gui_base.cpp | 7 ++- src/states_screens/race_result_gui.cpp | 7 ++- 21 files changed, 228 insertions(+), 94 deletions(-) diff --git a/src/items/flyable.cpp b/src/items/flyable.cpp index 39a5161bf..7c9cca504 100644 --- a/src/items/flyable.cpp +++ b/src/items/flyable.cpp @@ -226,7 +226,8 @@ void Flyable::getClosestKart(const AbstractKart **minKart, // it is not considered a target anymore. if(kart->isEliminated() || kart == m_owner || kart->isInvulnerable() || - kart->getKartAnimation() ) continue; + kart->getKartAnimation() || + kart->isGhostKart() ) continue; const SoccerWorld* sw = dynamic_cast(World::getWorld()); if (sw) @@ -480,6 +481,7 @@ void Flyable::explode(AbstractKart *kart_hit, PhysicalObject *object, ->getKartTeam(m_owner->getWorldKartId())) continue; } + if (kart->isGhostKart()) continue; // If no secondary hits should be done, only hit the // direct hit kart. diff --git a/src/items/powerup.cpp b/src/items/powerup.cpp index fe617e412..351ec6aa0 100644 --- a/src/items/powerup.cpp +++ b/src/items/powerup.cpp @@ -295,7 +295,7 @@ void Powerup::use() for(unsigned int i = 0 ; i < world->getNumKarts(); ++i) { AbstractKart *kart=world->getKart(i); - if(kart->isEliminated()) continue; + if(kart->isEliminated() || kart->isGhostKart()) continue; if(kart == m_owner) continue; if(kart->getPosition() == 1) { @@ -329,7 +329,7 @@ void Powerup::use() for(unsigned int i = 0 ; i < world->getNumKarts(); ++i) { AbstractKart *kart=world->getKart(i); - if(kart->isEliminated() || kart== m_owner) continue; + if(kart->isEliminated() || kart== m_owner || kart->isGhostKart()) continue; if(kart->isShielded()) { kart->decreaseShieldTime(); diff --git a/src/items/swatter.cpp b/src/items/swatter.cpp index 6e07df39d..6f98345e9 100644 --- a/src/items/swatter.cpp +++ b/src/items/swatter.cpp @@ -243,7 +243,7 @@ void Swatter::chooseTarget() { AbstractKart *kart = world->getKart(i); // TODO: isSwatterReady(), isSquashable()? - if(kart->isEliminated() || kart==m_kart) + if(kart->isEliminated() || kart == m_kart || kart->isGhostKart()) continue; // don't squash an already hurt kart if (kart->isInvulnerable() || kart->isSquashed()) diff --git a/src/karts/abstract_kart.hpp b/src/karts/abstract_kart.hpp index cae8dcdd6..5fa49d5f4 100644 --- a/src/karts/abstract_kart.hpp +++ b/src/karts/abstract_kart.hpp @@ -454,6 +454,9 @@ public: // ------------------------------------------------------------------------ /** Returns whether this kart wins or loses. */ virtual bool getRaceResult() const = 0; + // ------------------------------------------------------------------------ + /** Returns whether this kart is a ghost (replay) kart. */ + virtual bool isGhostKart() const = 0; }; // AbstractKart diff --git a/src/karts/controller/skidding_ai.cpp b/src/karts/controller/skidding_ai.cpp index fa2ce5784..491e457aa 100644 --- a/src/karts/controller/skidding_ai.cpp +++ b/src/karts/controller/skidding_ai.cpp @@ -1757,7 +1757,7 @@ void SkiddingAI::checkCrashes(const Vec3& pos ) { const AbstractKart* kart = m_world->getKart(j); // Ignore eliminated karts - if(kart==m_kart||kart->isEliminated()) continue; + if(kart==m_kart||kart->isEliminated()||kart->isGhostKart()) continue; const AbstractKart *other_kart = m_world->getKart(j); // Ignore karts ahead that are faster than this kart. if(m_kart->getVelocityLC().getZ() < other_kart->getVelocityLC().getZ()) diff --git a/src/karts/ghost_kart.cpp b/src/karts/ghost_kart.cpp index ba0e2e066..4783bc2a8 100644 --- a/src/karts/ghost_kart.cpp +++ b/src/karts/ghost_kart.cpp @@ -24,9 +24,11 @@ #include "LinearMath/btQuaternion.h" #include "utils/log.hpp" -GhostKart::GhostKart(const std::string& ident) - : Kart(ident, /*world kart id*/99999, - /*position*/-1, btTransform(), PLAYER_DIFFICULTY_NORMAL) +GhostKart::GhostKart(const std::string& ident, unsigned int world_kart_id, + int position) + : Kart(ident, world_kart_id, + position, btTransform(btQuaternion(0, 0, 0, 1)), + PLAYER_DIFFICULTY_NORMAL) { m_all_times.clear(); m_all_transform.clear(); @@ -131,5 +133,7 @@ void GhostKart::update(float dt) m_all_physic_info[m_current_transform].m_speed, m_current_transform); + Vec3 front(0, 0, getKartLength()*0.5f); + m_xyz_front = getTrans()(front); getKartGFX()->update(dt); } // update diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp index cef74b2c4..8754704d2 100644 --- a/src/karts/ghost_kart.hpp +++ b/src/karts/ghost_kart.hpp @@ -51,7 +51,8 @@ private: unsigned int m_current_transform; public: - GhostKart(const std::string& ident); + GhostKart(const std::string& ident, + unsigned int world_kart_id, int position); virtual void update (float dt); virtual void reset(); // ------------------------------------------------------------------------ @@ -72,5 +73,8 @@ public: const ReplayBase::PhysicInfo &pi, const ReplayBase::KartReplayEvent &kre); // ------------------------------------------------------------------------ + /** Returns whether this kart is a ghost (replay) kart. */ + virtual bool isGhostKart() const { return true; } + }; // GhostKart #endif diff --git a/src/karts/kart.cpp b/src/karts/kart.cpp index 941b0f4e2..89470446a 100644 --- a/src/karts/kart.cpp +++ b/src/karts/kart.cpp @@ -512,7 +512,8 @@ void Kart::setController(Controller *controller) */ void Kart::setPosition(int p) { - m_controller->setPosition(p); + if (m_controller) + m_controller->setPosition(p); m_race_position = p; } // setPosition @@ -834,14 +835,15 @@ void Kart::finishedRace(float time) if(m_finished_race) return; m_finished_race = true; m_finish_time = time; - m_controller->finishedRace(time); + if(!isGhostKart()) + m_controller->finishedRace(time); m_kart_model->finishedRace(); race_manager->kartFinishedRace(this, time); if ((race_manager->getMinorMode() == RaceManager::MINOR_MODE_NORMAL_RACE || race_manager->getMinorMode() == RaceManager::MINOR_MODE_TIME_TRIAL || race_manager->getMinorMode() == RaceManager::MINOR_MODE_FOLLOW_LEADER) - && m_controller->isPlayerController()) + && (isGhostKart() ? false : m_controller->isPlayerController())) { RaceGUIBase* m = World::getWorld()->getRaceGUI(); if (m) @@ -869,10 +871,13 @@ void Kart::finishedRace(float time) { // Save for music handling in race result gui setRaceResult(); - setController(new EndController(this, m_controller->getPlayer(), - m_controller)); + if(!isGhostKart()) + { + setController(new EndController(this, m_controller->getPlayer(), + m_controller)); + } // Skip animation if this kart is eliminated - if (m_eliminated) return; + if (m_eliminated || isGhostKart()) return; m_kart_model->setAnimation(m_race_result ? KartModel::AF_WIN_START : KartModel::AF_LOSE_START); @@ -885,8 +890,9 @@ void Kart::setRaceResult() if (race_manager->getMinorMode() == RaceManager::MINOR_MODE_NORMAL_RACE || race_manager->getMinorMode() == RaceManager::MINOR_MODE_TIME_TRIAL) { - if (m_controller->isLocalPlayerController()) // if player is on this computer + if (isGhostKart() ? false : m_controller->isPlayerController()) { + // if player is on this computer PlayerProfile *player = PlayerManager::getCurrentPlayer(); const ChallengeStatus *challenge = player->getCurrentChallengeStatus(); // In case of a GP challenge don't make the end animation depend @@ -1487,7 +1493,7 @@ void Kart::showZipperFire() */ void Kart::setSquash(float time, float slowdown) { - if (isInvulnerable()) return; + if (isInvulnerable() || isGhostKart()) return; if (isShielded()) { diff --git a/src/karts/kart.hpp b/src/karts/kart.hpp index eea4ded04..5aba096af 100644 --- a/src/karts/kart.hpp +++ b/src/karts/kart.hpp @@ -68,6 +68,10 @@ protected: /** Offset of the graphical kart chassis from the physical chassis. */ float m_graphical_y_offset; + /** The coordinates of the front of the kart, used to determine when a + * new lap is triggered. */ + Vec3 m_xyz_front; + private: /** Handles speed increase and capping due to powerup, terrain, ... */ MaxSpeed *m_max_speed; @@ -106,10 +110,6 @@ private: /** Current race position (1-num_karts). */ int m_race_position; - /** The coordinates of the front of the kart, used to determine when a - * new lap is triggered. */ - Vec3 m_xyz_front; - /** True if the kart wins, false otherwise. */ bool m_race_result; @@ -448,6 +448,9 @@ public: // ------------------------------------------------------------------------ /** Set this kart race result. */ void setRaceResult(); + // ------------------------------------------------------------------------ + /** Returns whether this kart is a ghost (replay) kart. */ + virtual bool isGhostKart() const { return false; } }; // Kart diff --git a/src/karts/kart_model.cpp b/src/karts/kart_model.cpp index 16b13acf7..ce11b1a1f 100644 --- a/src/karts/kart_model.cpp +++ b/src/karts/kart_model.cpp @@ -806,7 +806,7 @@ void KartModel::update(float dt, float distance, float steer, float speed, if (!m_kart || !m_wheel_node[i]) continue; #ifdef DEBUG if (UserConfigParams::m_physics_debug && - !dynamic_cast(m_kart) ) + !m_kart->isGhostKart()) { const btWheelInfo &wi = m_kart->getVehicle()->getWheelInfo(i); // Make wheels that are not touching the ground invisible diff --git a/src/karts/skidding.cpp b/src/karts/skidding.cpp index 15aafaf25..4db1ed960 100644 --- a/src/karts/skidding.cpp +++ b/src/karts/skidding.cpp @@ -23,7 +23,6 @@ #endif #include "achievements/achievement_info.hpp" #include "config/player_manager.hpp" -#include "karts/ghost_kart.hpp" #include "karts/kart.hpp" #include "karts/kart_gfx.hpp" #include "karts/kart_properties.hpp" @@ -82,7 +81,7 @@ void Skidding::reset() btVector3 rot(0, 0, 0); // Only access the vehicle if the kart is not a ghost - if (dynamic_cast(m_kart)==NULL) + if (!m_kart->isGhostKart()) m_kart->getVehicle()->setTimedRotation(0, rot); } // reset diff --git a/src/modes/linear_world.cpp b/src/modes/linear_world.cpp index d6fc49185..9dbab415f 100644 --- a/src/modes/linear_world.cpp +++ b/src/modes/linear_world.cpp @@ -245,20 +245,24 @@ void LinearWorld::newLap(unsigned int kart_index) { KartInfo &kart_info = m_kart_info[kart_index]; AbstractKart *kart = m_karts[kart_index]; + const bool is_gk = kart->isGhostKart(); // Reset reset-after-lap achievements - StateManager::ActivePlayer *c = kart->getController()->getPlayer(); - PlayerProfile *p = PlayerManager::getCurrentPlayer(); - if (c && c->getConstProfile() == p) + if (!is_gk) { - p->getAchievementsStatus()->onLapEnd(); + StateManager::ActivePlayer *c = kart->getController()->getPlayer(); + PlayerProfile *p = PlayerManager::getCurrentPlayer(); + if (c && c->getConstProfile() == p) + { + p->getAchievementsStatus()->onLapEnd(); + } } // Only update the kart controller if a kart that has already finished // the race crosses the start line again. This avoids 'fastest lap' // messages if the end controller does a fastest lap, but especially // allows the end controller to switch end cameras - if(kart->hasFinishedRace()) + if(kart->hasFinishedRace() && !is_gk) { kart->getController()->newLap(kart_info.m_race_lap); return; @@ -374,7 +378,8 @@ void LinearWorld::newLap(unsigned int kart_index) } // end if new fastest lap kart_info.m_lap_start_time = getTime(); - kart->getController()->newLap(kart_info.m_race_lap); + if (!is_gk) + kart->getController()->newLap(kart_info.m_race_lap); } // newLap //----------------------------------------------------------------------------- @@ -840,6 +845,8 @@ void LinearWorld::updateRacePosition() */ void LinearWorld::checkForWrongDirection(unsigned int i, float dt) { + if (m_karts[i]->isGhostKart()) return; + if (!m_karts[i]->getController()->isLocalPlayerController()) return; diff --git a/src/modes/standard_race.cpp b/src/modes/standard_race.cpp index 1a8640edc..c028a079f 100644 --- a/src/modes/standard_race.cpp +++ b/src/modes/standard_race.cpp @@ -102,10 +102,11 @@ void StandardRace::endRaceEarly() continue; } - if (kart->getController()->isPlayerController()) + if (!kart->isGhostKart()) { // Keep active players apart for now - active_players.push_back(kartid); + if (kart->getController()->isPlayerController()) + active_players.push_back(kartid); } else { diff --git a/src/modes/world.cpp b/src/modes/world.cpp index e2984debc..622a11494 100644 --- a/src/modes/world.cpp +++ b/src/modes/world.cpp @@ -146,6 +146,9 @@ void World::init() m_eliminated_karts = 0; m_eliminated_players = 0; m_num_players = 0; + unsigned int gk = 0; + if (ReplayPlay::get()) + gk = ReplayPlay::get()->getNumGhostKart(); // Create the race gui before anything else is attached to the scene node // (which happens when the track is loaded). This allows the race gui to @@ -179,8 +182,16 @@ void World::init() // karts can be positioned properly on (and not in) the tracks. m_track->loadTrackModel(race_manager->getReverseTrack()); + if (gk > 0) + { + ReplayPlay::get()->load(); + for (unsigned int k = 0; k < gk; k++) + m_karts.push_back(ReplayPlay::get()->getGhostKart(k)); + } + for(unsigned int i=0; igetKartType(i) == RaceManager::KT_GHOST) continue; std::string kart_ident = history->replayHistory() ? history->getKartIdent(i) : race_manager->getKartIdent(i); @@ -201,11 +212,8 @@ void World::init() // Must be called after all karts are created m_race_gui->init(); - if(ReplayPlay::get()) - ReplayPlay::get()->Load(); - powerup_manager->updateWeightsForRace(num_karts); - + if (UserConfigParams::m_weather_effects) { m_weather = new Weather(m_track->getWeatherLightning(), @@ -382,16 +390,6 @@ World::~World() { irr_driver->onUnloadWorld(); - if(ReplayPlay::get()) - { - // Destroy the old replay object, which also stored the ghost - // karts, and create a new one (which means that in further - // races the usage of ghosts will still be enabled). - ReplayPlay::destroy(); - ReplayPlay::create(); - } - - // In case that a race is aborted (e.g. track not found) m_track is 0. if(m_track) m_track->cleanup(); @@ -416,9 +414,22 @@ World::~World() delete m_weather; for ( unsigned int i = 0 ; i < m_karts.size() ; i++ ) + { + // Let ReplayPlay destroy the ghost karts + if (m_karts[i]->isGhostKart()) continue; delete m_karts[i]; + } + if(ReplayPlay::get()) + { + // Destroy the old replay object, which also stored the ghost + // karts, and create a new one (which means that in further + // races the usage of ghosts will still be enabled). + ReplayPlay::destroy(); + ReplayPlay::create(); + } m_karts.clear(); + Camera::removeAllCameras(); projectile_manager->cleanup(); @@ -447,6 +458,7 @@ void World::onGo() // from sliding downhill) for(unsigned int i=0; iisGhostKart()) continue; m_karts[i]->getVehicle()->setAllBrakes(0); } } // onGo @@ -508,6 +520,7 @@ void World::terminateRace() } for(unsigned int i = 0; i < kart_amount; i++) { + if (m_karts[i]->isGhostKart()) continue; // Retrieve the current player StateManager::ActivePlayer* p = m_karts[i]->getController()->getPlayer(); if (p && p->getConstProfile() == PlayerManager::getCurrentPlayer()) @@ -533,6 +546,7 @@ void World::terminateRace() { for(unsigned int i = 0; i < kart_amount; i++) { + if (m_karts[i]->isGhostKart()) continue; // Retrieve the current player StateManager::ActivePlayer* p = m_karts[i]->getController()->getPlayer(); if (p && p->getConstProfile() == PlayerManager::getCurrentPlayer()) @@ -593,12 +607,13 @@ void World::resetAllKarts() getPhysics()->getPhysicsWorld()->resetLocalTime(); // If track checking is requested, check all rescue positions if - // they are heigh enough. + // they are high enough. if(UserConfigParams::m_track_debug) { // Loop over all karts, in case that some karts are dfferent for(unsigned int kart_id=0; kart_id<(unsigned int)m_karts.size(); kart_id++) { + if (m_karts[kart_id]->isGhostKart()) continue; for(unsigned int rescue_pos=0; rescue_posisGhostKart()) continue; Vec3 xyz = (*i)->getXYZ(); //start projection from top of kart Vec3 up_offset(0, 0.5f * ((*i)->getKartHeight()), 0); @@ -674,6 +690,7 @@ void World::resetAllKarts() all_finished=true; for ( KartList::iterator i=m_karts.begin(); i!=m_karts.end(); i++) { + if ((*i)->isGhostKart()) continue; if(!(*i)->isInRest()) { Vec3 normal; @@ -716,6 +733,7 @@ void World::resetAllKarts() for ( KartList::iterator i=m_karts.begin(); i!=m_karts.end(); i++) { + if ((*i)->isGhostKart()) continue; (*i)->kartIsInRestNow(); } @@ -1073,6 +1091,7 @@ void World::updateHighscores(int* best_highscore_rank, int* best_finish_time, for (unsigned int pos=0; posisGhostKart()) continue; if(index[pos] == 999) { // no kart claimed to be in this position, most likely means @@ -1137,11 +1156,14 @@ AbstractKart *World::getPlayerKart(unsigned int n) const unsigned int count=-1; for(unsigned int i=0; iisGhostKart()) continue; if(m_karts[i]->getController()->isPlayerController()) { count++; if(count==n) return m_karts[i]; } + } return NULL; } // getPlayerKart @@ -1162,6 +1184,7 @@ AbstractKart *World::getLocalPlayerKart(unsigned int n) const void World::eliminateKart(int kart_id, bool notify_of_elimination) { AbstractKart *kart = m_karts[kart_id]; + if (kart->isGhostKart()) return; // Display a message about the eliminated kart in the race guia if (notify_of_elimination) @@ -1238,6 +1261,7 @@ void World::unpause() for(unsigned int i=0; iisGhostKart()) continue; // Note that we can not test for isPlayerController here, since // an EndController will also return 'isPlayerController' if the // kart belonged to a player. diff --git a/src/race/race_manager.cpp b/src/race/race_manager.cpp index 40a55b47a..92bcccddc 100644 --- a/src/race/race_manager.cpp +++ b/src/race/race_manager.cpp @@ -47,6 +47,7 @@ #include "network/network_config.hpp" #include "network/network_world.hpp" #include "network/protocols/start_game_protocol.hpp" +#include "replay/replay_play.hpp" #include "scriptengine/property_animator.hpp" #include "states_screens/grand_prix_cutscene.hpp" #include "states_screens/grand_prix_lose.hpp" @@ -308,6 +309,13 @@ void RaceManager::computeRandomKartList() */ void RaceManager::startNew(bool from_overworld) { + unsigned int gk = 0; + if (ReplayPlay::get()) + { + ReplayPlay::get()->loadKartInfo(); + gk = ReplayPlay::get()->getNumGhostKart(); + } + m_started_from_overworld = from_overworld; m_saved_gp = NULL; // There will be checks for this being NULL done later @@ -359,16 +367,30 @@ void RaceManager::startNew(bool from_overworld) // Create the kart status data structure to keep track of scores, times, ... // ========================================================================== m_kart_status.clear(); - Log::verbose("RaceManager", "Nb of karts=%u, ai:%lu players:%lu\n", - (unsigned int) m_num_karts, m_ai_kart_list.size(), m_player_karts.size()); + if (gk > 0) + m_num_karts += gk; - assert((unsigned int)m_num_karts == m_ai_kart_list.size()+m_player_karts.size()); + Log::verbose("RaceManager", "Nb of karts=%u, ghost karts:%u ai:%lu players:%lu\n", + (unsigned int) m_num_karts, gk, m_ai_kart_list.size(), m_player_karts.size()); - // First add the AI karts (randomly chosen) + assert((unsigned int)m_num_karts == gk+m_ai_kart_list.size()+m_player_karts.size()); + + // First add the ghost karts (if any) // ---------------------------------------- - // GP ranks start with -1 for the leader. int init_gp_rank = getMinorMode()==MINOR_MODE_FOLLOW_LEADER ? -1 : 0; + if (gk > 0) + { + for(unsigned int i = 0; i < gk; i++) + { + m_kart_status.push_back(KartStatus(ReplayPlay::get()->getGhostKartName(i), + i, -1, -1, init_gp_rank, KT_GHOST, PLAYER_DIFFICULTY_NORMAL)); + init_gp_rank ++; + } + } + + // Then add the AI karts (randomly chosen) + // ---------------------------------------- const unsigned int ai_kart_count = m_ai_kart_list.size(); for(unsigned int i = 0; i < ai_kart_count; i++) { @@ -382,7 +404,7 @@ void RaceManager::startNew(bool from_overworld) } } - // Then add the players, which start behind the AI karts + // Finally add the players, which start behind the AI karts // ----------------------------------------------------- for(unsigned int i = 0; i < m_player_karts.size(); i++) { @@ -836,6 +858,7 @@ void RaceManager::kartFinishedRace(const AbstractKart *kart, float time) m_kart_status[id].m_overall_time += time; m_kart_status[id].m_last_time = time; m_num_finished_karts ++; + if(kart->isGhostKart()) return; if(kart->getController()->isPlayerController()) m_num_finished_players++; } // kartFinishedRace diff --git a/src/replay/replay_play.cpp b/src/replay/replay_play.cpp index e32b4e21a..f24c3cb66 100644 --- a/src/replay/replay_play.cpp +++ b/src/replay/replay_play.cpp @@ -35,7 +35,8 @@ ReplayPlay *ReplayPlay::m_replay_play = NULL; */ ReplayPlay::ReplayPlay() { - m_next = 0; + m_next = 0; + m_ghost_karts_list.clear(); } // ReplayPlay //----------------------------------------------------------------------------- @@ -44,21 +45,13 @@ ReplayPlay::~ReplayPlay() { } // ~Replay -//----------------------------------------------------------------------------- -/** Starts replay from the replay file in the current directory. - */ -void ReplayPlay::init() -{ - m_next = 0; - Load(); -} // init - //----------------------------------------------------------------------------- /** Resets all ghost karts back to start position. */ void ReplayPlay::reset() { m_next = 0; + m_ghost_karts_list.clear(); for(unsigned int i=0; i<(unsigned int)m_ghost_karts.size(); i++) { m_ghost_karts[i].reset(); @@ -77,10 +70,43 @@ void ReplayPlay::update(float dt) } // update +//----------------------------------------------------------------------------- +/** Loads the ghost karts info in the replay file, required by race manager. + */ +void ReplayPlay::loadKartInfo() +{ + char s[1024]; + + FILE *fd = openReplayFile(/*writeable*/false); + if(!fd) + { + Log::error("Replay", "Can't read '%s', ghost replay disabled.", + getReplayFilename().c_str()); + destroy(); + return; + } + + Log::info("Replay", "Reading ghost karts info"); + while(true) + { + if (fgets(s, 1023, fd) == NULL) + Log::fatal("Replay", "Could not read '%s'.", getReplayFilename().c_str()); + std::string is_end = std::string(s); + if (is_end == "kart_list_end\n" || is_end == "kart_list_end\r\n") break; + char s1[1024]; + + if (sscanf(s,"kart: %s", s1) != 1) + Log::fatal("Replay", "Could not read ghost karts info!"); + + m_ghost_karts_list.push_back(std::string(s1)); + } + fclose(fd); +} // loadKartInfo + //----------------------------------------------------------------------------- /** Loads a replay data from file called 'trackname'.replay. */ -void ReplayPlay::Load() +void ReplayPlay::load() { m_ghost_karts.clearAndDeleteAll(); char s[1024], s1[1024]; @@ -95,12 +121,21 @@ void ReplayPlay::Load() } Log::info("Replay", "Reading replay file '%s'.", getReplayFilename().c_str()); - if (fgets(s, 1023, fd) == NULL) Log::fatal("Replay", "Could not read '%s'.", getReplayFilename().c_str()); + for (unsigned int i = 0; i < m_ghost_karts_list.size(); i++) + { + if (fgets(s, 1023, fd) == NULL) + Log::fatal("Replay", "Could not read '%s'.", getReplayFilename().c_str()); + // Skip kart info which is already read. + } + if (fgets(s, 1023, fd) == NULL) + Log::fatal("Replay", "Could not read '%s'.", getReplayFilename().c_str()); + // Skip kart_list_end + unsigned int version; - if (sscanf(s,"Version: %u", &version) != 1) + if (sscanf(s,"version: %u", &version) != 1) Log::fatal("Replay", "No Version information found in replay file (bogus replay file)."); if (version != getReplayVersion()) @@ -113,7 +148,7 @@ void ReplayPlay::Load() if (fgets(s, 1023, fd) == NULL) Log::fatal("Replay", "Could not read '%s'.", getReplayFilename().c_str()); - int n; + int n; if(sscanf(s, "difficulty: %d", &n) != 1) Log::fatal("Replay", " No difficulty found in replay file."); @@ -130,7 +165,7 @@ void ReplayPlay::Load() unsigned int num_laps; fgets(s, 1023, fd); - if(sscanf(s, "Laps: %u", &num_laps) != 1) + if(sscanf(s, "laps: %u", &num_laps) != 1) Log::fatal("Replay", "No number of laps found in replay file."); race_manager->setNumLaps(num_laps); @@ -142,11 +177,11 @@ void ReplayPlay::Load() if(fgets(s, 1023, fd)==NULL) // eof reached break; readKartData(fd, s); - } // for k m_ghost_karts_list; + /** All ghost karts. */ PtrVector m_ghost_karts; @@ -45,20 +48,30 @@ private: ~ReplayPlay(); void readKartData(FILE *fd, char *next_line); public: - void init(); void update(float dt); void reset(); - void Load(); + void load(); + void loadKartInfo(); + // ------------------------------------------------------------------------ + GhostKart* getGhostKart(int n) { return m_ghost_karts.get(n); } + // ------------------------------------------------------------------------ + const unsigned int getNumGhostKart() const + { return m_ghost_karts_list.size(); } + // ------------------------------------------------------------------------ + const std::string& getGhostKartName(int n) const + { return m_ghost_karts_list.at(n); } // ------------------------------------------------------------------------ /** Creates a new instance of the replay object. */ - static void create() { m_replay_play = new ReplayPlay(); } + static void create() { m_replay_play = new ReplayPlay(); } // ------------------------------------------------------------------------ /** Returns the instance of the replay object. */ - static ReplayPlay *get() { return m_replay_play; } + static ReplayPlay *get() { return m_replay_play; } // ------------------------------------------------------------------------ /** Delete the instance of the replay object. */ - static void destroy() { delete m_replay_play; m_replay_play=NULL; } + static void destroy() + { delete m_replay_play; m_replay_play = NULL; } + // ------------------------------------------------------------------------ }; // Replay #endif diff --git a/src/replay/replay_recorder.cpp b/src/replay/replay_recorder.cpp index b12fde01c..40dc93661 100644 --- a/src/replay/replay_recorder.cpp +++ b/src/replay/replay_recorder.cpp @@ -104,6 +104,7 @@ void ReplayRecorder::update(float dt) for(unsigned int i=0; igetKart(i); + if (kart->isGhostKart()) continue; #ifdef DEBUG m_count++; #endif @@ -195,16 +196,24 @@ void ReplayRecorder::Save() World *world = World::getWorld(); unsigned int num_karts = world->getNumKarts(); - fprintf(fd, "Version: %d\n", getReplayVersion()); + for (unsigned int real_karts = 0; real_karts < num_karts; real_karts++) + { + if (world->getKart(real_karts)->isGhostKart()) continue; + fprintf(fd, "kart: %s\n", + world->getKart(real_karts)->getIdent().c_str()); + } + + fprintf(fd, "kart_list_end\n"); + fprintf(fd, "version: %d\n", getReplayVersion()); fprintf(fd, "difficulty: %d\n", race_manager->getDifficulty()); fprintf(fd, "track: %s\n", world->getTrack()->getIdent().c_str()); - fprintf(fd, "Laps: %d\n", race_manager->getNumLaps()); + fprintf(fd, "laps: %d\n", race_manager->getNumLaps()); unsigned int max_frames = (unsigned int)( stk_config->m_replay_max_time / stk_config->m_replay_dt ); for (unsigned int k = 0; k < num_karts; k++) { - fprintf(fd, "model: %s\n", world->getKart(k)->getIdent().c_str()); + if (world->getKart(k)->isGhostKart()) continue; fprintf(fd, "size: %d\n", m_count_transforms[k]); unsigned int num_transforms = std::min(max_frames, diff --git a/src/states_screens/race_gui.cpp b/src/states_screens/race_gui.cpp index fe7c0a7e0..5c4e2f2e5 100644 --- a/src/states_screens/race_gui.cpp +++ b/src/states_screens/race_gui.cpp @@ -378,9 +378,9 @@ void RaceGUI::drawGlobalMiniMap() // int marker_height = m_marker->getSize().Height; core::rect source(core::position2di(0, 0), icon->getSize()); - int marker_half_size = (kart->getController()->isLocalPlayerController() - ? m_minimap_player_size - : m_minimap_ai_size )>>1; + int marker_half_size = (kart->isGhostKart() ? m_minimap_ai_size : + (kart->getController()->isLocalPlayerController() ? m_minimap_player_size : + m_minimap_ai_size))>>1; core::rect position(m_map_left+(int)(draw_at.getX()-marker_half_size), lower_y -(int)(draw_at.getY()+marker_half_size), m_map_left+(int)(draw_at.getX()+marker_half_size), diff --git a/src/states_screens/race_gui_base.cpp b/src/states_screens/race_gui_base.cpp index 3639898c8..939b4b04b 100644 --- a/src/states_screens/race_gui_base.cpp +++ b/src/states_screens/race_gui_base.cpp @@ -802,13 +802,14 @@ void RaceGUIBase::drawGlobalPlayerIcons(int bottom_margin) // draw icon video::ITexture *icon = kart->getKartProperties()->getIconMaterial()->getTexture(); - int w = kart->getController() + int w = kart->isGhostKart() ? ICON_WIDTH : (kart->getController() ->isLocalPlayerController() ? ICON_PLAYER_WIDTH - : ICON_WIDTH; + : ICON_WIDTH); const core::rect pos(x, y, x+w, y+w); //to bring to light the player's icon: add a background - if (kart->getController()->isLocalPlayerController()) + if (kart->isGhostKart() ? + false : kart->getController()->isLocalPlayerController()) { video::SColor colors[4]; for (unsigned int i=0;i<4;i++) diff --git a/src/states_screens/race_result_gui.cpp b/src/states_screens/race_result_gui.cpp index 416030f0b..5033930b1 100644 --- a/src/states_screens/race_result_gui.cpp +++ b/src/states_screens/race_result_gui.cpp @@ -89,6 +89,7 @@ void RaceResultGUI::init() for (unsigned int kart_id = 0; kart_id < num_karts; kart_id++) { const AbstractKart *kart = World::getWorld()->getKart(kart_id); + if (kart->isGhostKart()) continue; if (kart->getController()->isPlayerController()) human_win = human_win && kart->getRaceResult(); } @@ -474,7 +475,8 @@ void RaceResultGUI::determineTableLayout() // Save a pointer to the current row_info entry RowInfo *ri = &(m_all_row_infos[position-first_position]); - ri->m_is_player_kart = kart->getController()->isLocalPlayerController(); + ri->m_is_player_kart = kart->isGhostKart() ? false : + kart->getController()->isLocalPlayerController(); // Identify Human player, if so display real name other than kart name const int rm_id = kart->getWorldKartId() - @@ -860,7 +862,8 @@ void RaceResultGUI::determineGPLayout() else ri->m_kart_name = translations->fribidize(kart->getName()); - ri->m_is_player_kart = kart->getController()->isLocalPlayerController(); + ri->m_is_player_kart = kart->isGhostKart() ? false : + kart->getController()->isLocalPlayerController(); ri->m_player = ri->m_is_player_kart ? kart->getController()->getPlayer() : NULL; From af5b23e5c6ab355603ed84299a1becdc7de455d2 Mon Sep 17 00:00:00 2001 From: Benau Date: Sun, 7 Feb 2016 00:56:14 +0800 Subject: [PATCH 007/324] Fix potential crash --- src/karts/controller/skidding_ai.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/karts/controller/skidding_ai.cpp b/src/karts/controller/skidding_ai.cpp index 491e457aa..0e3440cac 100644 --- a/src/karts/controller/skidding_ai.cpp +++ b/src/karts/controller/skidding_ai.cpp @@ -333,7 +333,8 @@ void SkiddingAI::update(float dt) // If we are faster, try to predict the point where we will hit // the other kart - if(m_kart_ahead->getSpeed() < m_kart->getSpeed()) + if((m_kart_ahead->getSpeed() < m_kart->getSpeed()) && + !m_kart_ahead->isGhostKart()) { float time_till_hit = m_distance_ahead / (m_kart->getSpeed()-m_kart_ahead->getSpeed()); From b6b644ecb571b8b09d548feff4a6f355b90d3a51 Mon Sep 17 00:00:00 2001 From: Benau Date: Sun, 7 Feb 2016 10:20:37 +0800 Subject: [PATCH 008/324] Clean up --- src/items/flyable.cpp | 3 +-- src/items/swatter.cpp | 2 +- src/karts/ghost_kart.hpp | 3 +++ src/karts/kart.cpp | 2 +- src/modes/standard_race.cpp | 22 +++++++++++++++------- src/replay/replay_play.cpp | 6 ++++-- 6 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/items/flyable.cpp b/src/items/flyable.cpp index 7c9cca504..82839730a 100644 --- a/src/items/flyable.cpp +++ b/src/items/flyable.cpp @@ -226,8 +226,7 @@ void Flyable::getClosestKart(const AbstractKart **minKart, // it is not considered a target anymore. if(kart->isEliminated() || kart == m_owner || kart->isInvulnerable() || - kart->getKartAnimation() || - kart->isGhostKart() ) continue; + kart->getKartAnimation() ) continue; const SoccerWorld* sw = dynamic_cast(World::getWorld()); if (sw) diff --git a/src/items/swatter.cpp b/src/items/swatter.cpp index 6f98345e9..6e07df39d 100644 --- a/src/items/swatter.cpp +++ b/src/items/swatter.cpp @@ -243,7 +243,7 @@ void Swatter::chooseTarget() { AbstractKart *kart = world->getKart(i); // TODO: isSwatterReady(), isSquashable()? - if(kart->isEliminated() || kart == m_kart || kart->isGhostKart()) + if(kart->isEliminated() || kart==m_kart) continue; // don't squash an already hurt kart if (kart->isInvulnerable() || kart->isSquashed()) diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp index 8754704d2..30333963f 100644 --- a/src/karts/ghost_kart.hpp +++ b/src/karts/ghost_kart.hpp @@ -75,6 +75,9 @@ public: // ------------------------------------------------------------------------ /** Returns whether this kart is a ghost (replay) kart. */ virtual bool isGhostKart() const { return true; } + // ------------------------------------------------------------------------ + /** Ghost can't be hunted. */ + virtual bool isInvulnerable() const { return true; } }; // GhostKart #endif diff --git a/src/karts/kart.cpp b/src/karts/kart.cpp index 89470446a..9ced75e24 100644 --- a/src/karts/kart.cpp +++ b/src/karts/kart.cpp @@ -1493,7 +1493,7 @@ void Kart::showZipperFire() */ void Kart::setSquash(float time, float slowdown) { - if (isInvulnerable() || isGhostKart()) return; + if (isInvulnerable()) return; if (isShielded()) { diff --git a/src/modes/standard_race.cpp b/src/modes/standard_race.cpp index c028a079f..600ea03c1 100644 --- a/src/modes/standard_race.cpp +++ b/src/modes/standard_race.cpp @@ -102,17 +102,25 @@ void StandardRace::endRaceEarly() continue; } - if (!kart->isGhostKart()) + if (kart->isGhostKart()) { - // Keep active players apart for now - if (kart->getController()->isPlayerController()) - active_players.push_back(kartid); + // Ghost karts finish + setKartPosition(kartid, i - (unsigned int) active_players.size()); + kart->finishedRace(estimateFinishTimeForKart(kart)); } else { - // AI karts finish - setKartPosition(kartid, i - (unsigned int) active_players.size()); - kart->finishedRace(estimateFinishTimeForKart(kart)); + if (kart->getController()->isPlayerController()) + { + // Keep active players apart for now + active_players.push_back(kartid); + } + else + { + // AI karts finish + setKartPosition(kartid, i - (unsigned int) active_players.size()); + kart->finishedRace(estimateFinishTimeForKart(kart)); + } } } // i <= kart_amount // Now make the active players finish diff --git a/src/replay/replay_play.cpp b/src/replay/replay_play.cpp index f24c3cb66..863d25320 100644 --- a/src/replay/replay_play.cpp +++ b/src/replay/replay_play.cpp @@ -25,6 +25,7 @@ #include "race/race_manager.hpp" #include "tracks/track.hpp" +#include #include #include @@ -91,8 +92,9 @@ void ReplayPlay::loadKartInfo() { if (fgets(s, 1023, fd) == NULL) Log::fatal("Replay", "Could not read '%s'.", getReplayFilename().c_str()); - std::string is_end = std::string(s); - if (is_end == "kart_list_end\n" || is_end == "kart_list_end\r\n") break; + core::stringc is_end(s); + is_end.trim(); + if (is_end == "kart_list_end") break; char s1[1024]; if (sscanf(s,"kart: %s", s1) != 1) From bf080421a73b8be2ac7364fa5da5edcef1877d7c Mon Sep 17 00:00:00 2001 From: Benau Date: Mon, 8 Feb 2016 12:28:40 +0800 Subject: [PATCH 009/324] Allow showing speed of ghost karts in replay Use when change camera target to ghost karts --- src/karts/ghost_kart.cpp | 7 ++----- src/karts/ghost_kart.hpp | 41 +++++++++++++++++++++----------------- src/modes/linear_world.cpp | 11 +++++++--- 3 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/karts/ghost_kart.cpp b/src/karts/ghost_kart.cpp index 4783bc2a8..0d6d713f5 100644 --- a/src/karts/ghost_kart.cpp +++ b/src/karts/ghost_kart.cpp @@ -69,8 +69,7 @@ void GhostKart::addReplayEvent(float time, for (int i = 0; i < 4; i++) f += m_all_physic_info[0].m_suspension_length[i]; m_graphical_y_offset = -f / 4 + getKartModel()->getLowestPoint(); - m_kart_model - ->setDefaultSuspension(); + m_kart_model->setDefaultSuspension(); } } // addReplayEvent @@ -118,9 +117,7 @@ void GhostKart::update(float dt) setXYZ((1-f)*m_all_transform[m_current_transform ].getOrigin() + f *m_all_transform[m_current_transform+1].getOrigin() ); const btQuaternion q = m_all_transform[m_current_transform].getRotation() - .slerp(m_all_transform[m_current_transform+1] - .getRotation(), - f); + .slerp(m_all_transform[m_current_transform+1].getRotation(), f); setRotation(q); Vec3 center_shift(0, 0, 0); diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp index 30333963f..dc8f6056c 100644 --- a/src/karts/ghost_kart.hpp +++ b/src/karts/ghost_kart.hpp @@ -37,47 +37,52 @@ class GhostKart : public Kart { private: /** The list of the times at which the transform were reached. */ - std::vector m_all_times; + std::vector m_all_times; /** The transforms to assume at the corresponding time in m_all_times. */ - std::vector m_all_transform; + std::vector m_all_transform; - std::vector m_all_physic_info; + std::vector m_all_physic_info; std::vector m_all_replay_events; /** Pointer to the last index in m_all_times that is smaller than * the current world time. */ - unsigned int m_current_transform; + unsigned int m_current_transform; public: - GhostKart(const std::string& ident, - unsigned int world_kart_id, int position); - virtual void update (float dt); - virtual void reset(); + GhostKart(const std::string& ident, + unsigned int world_kart_id, int position); + virtual void update (float dt); + virtual void reset(); // ------------------------------------------------------------------------ /** No physics body for ghost kart, so nothing to adjust. */ - virtual void updateWeight() {}; + virtual void updateWeight() {}; // ------------------------------------------------------------------------ /** No physics for ghost kart. */ - virtual void applyEngineForce (float force) {} + virtual void applyEngineForce (float force) {} // ------------------------------------------------------------------------ // Not needed to create any physics for a ghost kart. - virtual void createPhysics() {} + virtual void createPhysics() {} // ------------------------------------------------------------------------ - const float getSuspensionLength(int index, int wheel) const + const float getSuspensionLength(int index, int wheel) const { return m_all_physic_info[index].m_suspension_length[wheel]; } // ------------------------------------------------------------------------ - void addReplayEvent(float time, - const btTransform &trans, - const ReplayBase::PhysicInfo &pi, - const ReplayBase::KartReplayEvent &kre); + void addReplayEvent(float time, + const btTransform &trans, + const ReplayBase::PhysicInfo &pi, + const ReplayBase::KartReplayEvent &kre); // ------------------------------------------------------------------------ /** Returns whether this kart is a ghost (replay) kart. */ - virtual bool isGhostKart() const { return true; } + virtual bool isGhostKart() const { return true; } // ------------------------------------------------------------------------ /** Ghost can't be hunted. */ - virtual bool isInvulnerable() const { return true; } + virtual bool isInvulnerable() const { return true; } + // ------------------------------------------------------------------------ + /** Returns the speed of the kart in meters/second. */ + virtual float getSpeed() const + { return m_all_physic_info[m_current_transform].m_speed; } + // ------------------------------------------------------------------------ }; // GhostKart #endif diff --git a/src/modes/linear_world.cpp b/src/modes/linear_world.cpp index 9dbab415f..c3ae1e4fd 100644 --- a/src/modes/linear_world.cpp +++ b/src/modes/linear_world.cpp @@ -262,11 +262,16 @@ void LinearWorld::newLap(unsigned int kart_index) // the race crosses the start line again. This avoids 'fastest lap' // messages if the end controller does a fastest lap, but especially // allows the end controller to switch end cameras - if(kart->hasFinishedRace() && !is_gk) + if (!is_gk) { - kart->getController()->newLap(kart_info.m_race_lap); - return; + if (kart->hasFinishedRace()) + { + kart->getController()->newLap(kart_info.m_race_lap); + return; + } } + else if (kart->hasFinishedRace()) + return; const int lap_count = race_manager->getNumLaps(); From 0181ba0bc91ae2c6558dbee2b49b1a186f603e47 Mon Sep 17 00:00:00 2001 From: Benau Date: Wed, 10 Feb 2016 10:27:13 +0800 Subject: [PATCH 010/324] Add Ghost Controller --- patch | 368 ++++++++++++++++++++ sources.cmake | 2 +- src/karts/controller/ai_base_controller.hpp | 2 +- src/karts/controller/ghost_controller.cpp | 69 ++++ src/karts/controller/ghost_controller.hpp | 79 +++++ src/karts/ghost_kart.cpp | 65 ++-- src/karts/ghost_kart.hpp | 10 +- src/replay/replay_play.cpp | 3 + 8 files changed, 551 insertions(+), 47 deletions(-) create mode 100644 patch create mode 100644 src/karts/controller/ghost_controller.cpp create mode 100644 src/karts/controller/ghost_controller.hpp diff --git a/patch b/patch new file mode 100644 index 000000000..80818a7f2 --- /dev/null +++ b/patch @@ -0,0 +1,368 @@ +diff --git a/sources.cmake b/sources.cmake +index ddc029d..7c1db62 100644 +--- a/sources.cmake ++++ b/sources.cmake +@@ -1,5 +1,5 @@ + # Modify this file to change the last-modified date when you add/remove a file. +-# This will then trigger a new cmake run automatically. ++# This will then trigger a new cmake run automatically. + file(GLOB_RECURSE STK_HEADERS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "src/*.hpp") + file(GLOB_RECURSE STK_SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "src/*.cpp") + file(GLOB_RECURSE STK_SHADERS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "data/shaders/*") +diff --git a/src/karts/controller/ai_base_controller.hpp b/src/karts/controller/ai_base_controller.hpp +index d108ebe..fbea900 100644 +--- a/src/karts/controller/ai_base_controller.hpp ++++ b/src/karts/controller/ai_base_controller.hpp +@@ -64,7 +64,7 @@ protected: + void setControllerName(const std::string &name); + float steerToPoint(const Vec3 &point); + float normalizeAngle(float angle); +- virtual void update (float delta) ; ++ virtual void update (float delta); + virtual void setSteering (float angle, float dt); + virtual bool canSkid(float steer_fraction) = 0; + // ------------------------------------------------------------------------ +diff --git a/src/karts/controller/ghost_controller.cpp b/src/karts/controller/ghost_controller.cpp +new file mode 100644 +index 0000000..5f4000d +--- /dev/null ++++ b/src/karts/controller/ghost_controller.cpp +@@ -0,0 +1,70 @@ ++// ++// SuperTuxKart - a fun racing game with go-kart ++// Copyright (C) 2016 SuperTuxKart-Team ++// ++// This program 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. ++// ++// This program 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 this program; if not, write to the Free Software ++// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ++ ++#include "karts/controller/ghost_controller.hpp" ++ ++#include "config/user_config.hpp" ++#include "karts/abstract_kart.hpp" ++#include "karts/kart_properties.hpp" ++#include "karts/controller/ai_properties.hpp" ++#include "modes/world.hpp" ++#include "tracks/track.hpp" ++#include "utils/constants.hpp" ++ ++GhostController::GhostController(AbstractKart *kart, ++ StateManager::ActivePlayer *player) ++ : Controller(kart, player) ++{ ++ m_kart = kart; ++} // GhostController ++ ++//----------------------------------------------------------------------------- ++void GhostController::reset() ++{ ++ m_current_index = 0; ++ m_current_time = 0; ++ m_all_times.clear(); ++} // reset ++ ++//----------------------------------------------------------------------------- ++void GhostController::update(float dt) ++{ ++ m_current_time = World::getWorld()->getTime(); ++ // Find (if necessary) the next index to use ++ if (m_current_time != 0.0f) ++ { ++ while (m_current_index + 1 < m_all_times.size() && ++ m_current_time >= m_all_times[m_current_index + 1]) ++ { ++ m_current_index++; ++ } ++ } ++ ++} // update ++ ++//----------------------------------------------------------------------------- ++void GhostController::addReplayTime(float time) ++{ ++ // FIXME: for now avoid that transforms for the same time are set ++ // twice (to avoid division by zero in update). This should be ++ // done when saving in replay ++ if (m_all_times.size() > 0 && m_all_times.back() == time) ++ return; ++ m_all_times.push_back(time); ++ ++} // addReplayTime +diff --git a/src/karts/controller/ghost_controller.hpp b/src/karts/controller/ghost_controller.hpp +new file mode 100644 +index 0000000..64e4f22 +--- /dev/null ++++ b/src/karts/controller/ghost_controller.hpp +@@ -0,0 +1,79 @@ ++// ++// SuperTuxKart - a fun racing game with go-kart ++// Copyright (C) 2016 SuperTuxKart-Team ++// ++// This program 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. ++// ++// This program 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 this program; if not, write to the Free Software ++// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ++ ++#ifndef HEADER_GHOST_CONTROLLER_HPP ++#define HEADER_GHOST_CONTROLLER_HPP ++ ++#include "karts/controller/controller.hpp" ++#include "states_screens/state_manager.hpp" ++ ++#include ++ ++/** A class for Ghost controller. ++ * \ingroup controller ++ */ ++class GhostController : public Controller ++{ ++private: ++ /** Pointer to the last index in m_all_times that is smaller than ++ * the current world time. */ ++ unsigned int m_current_index; ++ ++ /** The current world time. */ ++ float m_current_time; ++ ++ /** The list of the times at which the events of kart were reached. */ ++ std::vector m_all_times; ++ ++public: ++ GhostController(AbstractKart *kart, ++ StateManager::ActivePlayer *player=NULL); ++ virtual ~GhostController() {}; ++ virtual void reset(); ++ virtual void update (float dt); ++ virtual bool disableSlipstreamBonus() const { return true; } ++ virtual void crashed(const Material *m) {}; ++ virtual void crashed(const AbstractKart *k) {}; ++ virtual void handleZipper(bool play_sound) {}; ++ virtual void finishedRace(float time) {}; ++ virtual void collectedItem(const Item &item, int add_info=-1, ++ float previous_energy=0) {}; ++ virtual void setPosition(int p) {}; ++ virtual bool isPlayerController() const { return false; } ++ virtual bool isLocalPlayerController() const { return false; } ++ virtual void action(PlayerAction action, int value) {}; ++ virtual void skidBonusTriggered() {}; ++ virtual void newLap(int lap) {}; ++ void addReplayTime(float time); ++ // ------------------------------------------------------------------------ ++ bool isReplayEnd() const ++ { return m_current_index + 1 >= m_all_times.size(); } ++ // ------------------------------------------------------------------------ ++ float getReplayDelta() const ++ { ++ return ((m_current_time - m_all_times[m_current_index]) ++ / (m_all_times[m_current_index + 1] ++ - m_all_times[m_current_index])); ++ } ++ // ------------------------------------------------------------------------ ++ unsigned int getCurrentReplayIndex() const ++ { return m_current_index; } ++ // ------------------------------------------------------------------------ ++}; // GhostController ++ ++#endif +diff --git a/src/karts/ghost_kart.cpp b/src/karts/ghost_kart.cpp +index 0d6d713..4e1c321 100644 +--- a/src/karts/ghost_kart.cpp ++++ b/src/karts/ghost_kart.cpp +@@ -17,6 +17,7 @@ + // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + #include "karts/ghost_kart.hpp" ++#include "karts/controller/ghost_controller.hpp" + #include "karts/kart_gfx.hpp" + #include "karts/kart_model.hpp" + #include "modes/world.hpp" +@@ -30,7 +31,6 @@ GhostKart::GhostKart(const std::string& ident, unsigned int world_kart_id, + position, btTransform(btQuaternion(0, 0, 0, 1)), + PLAYER_DIFFICULTY_NORMAL) + { +- m_all_times.clear(); + m_all_transform.clear(); + m_all_physic_info.clear(); + m_all_replay_events.clear(); +@@ -41,7 +41,6 @@ void GhostKart::reset() + { + m_node->setVisible(true); + Kart::reset(); +- m_current_transform = 0; + // This will set the correct start position + update(0); + } // reset +@@ -52,12 +51,9 @@ void GhostKart::addReplayEvent(float time, + const ReplayBase::PhysicInfo &pi, + const ReplayBase::KartReplayEvent &kre) + { +- // FIXME: for now avoid that transforms for the same time are set +- // twice (to avoid division by zero in update). This should be +- // done when saving in replay +- if(m_all_times.size()>0 && m_all_times.back()==time) +- return; +- m_all_times.push_back(time); ++ GhostController* gc = dynamic_cast(getController()); ++ gc->addReplayTime(time); ++ + m_all_transform.push_back(trans); + m_all_physic_info.push_back(pi); + m_all_replay_events.push_back(kre); +@@ -80,27 +76,23 @@ void GhostKart::addReplayEvent(float time, + */ + void GhostKart::update(float dt) + { +- float t = World::getWorld()->getTime(); +- // Find (if necessary) the next index to use +- if (t != 0.0f) +- { +- while (m_current_transform + 1 < m_all_times.size() && +- t >= m_all_times[m_current_transform+1]) +- { +- m_current_transform++; +- } +- } ++ GhostController* gc = dynamic_cast(getController()); ++ if (gc == NULL) return; + +- if (m_current_transform + 1 >= m_all_times.size()) ++ gc->update(dt); ++ if (gc->isReplayEnd()) + { + m_node->setVisible(false); + return; + } + ++ const unsigned int idx = gc->getCurrentReplayIndex(); ++ const float rd = gc->getReplayDelta(); ++ + float nitro_frac = 0; +- if (m_all_replay_events[m_current_transform].m_on_nitro) ++ if (m_all_replay_events[idx].m_on_nitro) + { +- nitro_frac = fabsf(m_all_physic_info[m_current_transform].m_speed) / ++ nitro_frac = fabsf(m_all_physic_info[idx].m_speed) / + (m_kart_properties->getEngineMaxSpeed()); + + if (nitro_frac > 1.0f) +@@ -108,16 +100,14 @@ void GhostKart::update(float dt) + } + getKartGFX()->updateNitroGraphics(nitro_frac); + +- if (m_all_replay_events[m_current_transform].m_on_zipper) ++ if (m_all_replay_events[idx].m_on_zipper) + showZipperFire(); + +- float f =(t - m_all_times[m_current_transform]) +- / ( m_all_times[m_current_transform+1] +- - m_all_times[m_current_transform] ); +- setXYZ((1-f)*m_all_transform[m_current_transform ].getOrigin() +- + f *m_all_transform[m_current_transform+1].getOrigin() ); +- const btQuaternion q = m_all_transform[m_current_transform].getRotation() +- .slerp(m_all_transform[m_current_transform+1].getRotation(), f); ++ setXYZ((1- rd)*m_all_transform[idx ].getOrigin() ++ + rd *m_all_transform[idx + 1].getOrigin() ); ++ ++ const btQuaternion q = m_all_transform[idx].getRotation() ++ .slerp(m_all_transform[idx + 1].getRotation(), rd); + setRotation(q); + + Vec3 center_shift(0, 0, 0); +@@ -125,12 +115,18 @@ void GhostKart::update(float dt) + center_shift = getTrans().getBasis() * center_shift; + + Moveable::updateGraphics(dt, center_shift, btQuaternion(0, 0, 0, 1)); +- getKartModel()->update(dt, dt*(m_all_physic_info[m_current_transform].m_speed), +- m_all_physic_info[m_current_transform].m_steer, +- m_all_physic_info[m_current_transform].m_speed, +- m_current_transform); ++ getKartModel()->update(dt, dt*(m_all_physic_info[idx].m_speed), ++ m_all_physic_info[idx].m_steer, m_all_physic_info[idx].m_speed, idx); + + Vec3 front(0, 0, getKartLength()*0.5f); + m_xyz_front = getTrans()(front); + getKartGFX()->update(dt); + } // update ++ ++// ---------------------------------------------------------------------------- ++float GhostKart::getSpeed() const ++{ ++ const GhostController* gc = ++ dynamic_cast(getController()); ++ return m_all_physic_info[gc->getCurrentReplayIndex()].m_speed; ++} +diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp +index dc8f605..d95ad8d 100644 +--- a/src/karts/ghost_kart.hpp ++++ b/src/karts/ghost_kart.hpp +@@ -36,9 +36,6 @@ + class GhostKart : public Kart + { + private: +- /** The list of the times at which the transform were reached. */ +- std::vector m_all_times; +- + /** The transforms to assume at the corresponding time in m_all_times. */ + std::vector m_all_transform; + +@@ -46,10 +43,6 @@ private: + + std::vector m_all_replay_events; + +- /** Pointer to the last index in m_all_times that is smaller than +- * the current world time. */ +- unsigned int m_current_transform; +- + public: + GhostKart(const std::string& ident, + unsigned int world_kart_id, int position); +@@ -80,8 +73,7 @@ public: + virtual bool isInvulnerable() const { return true; } + // ------------------------------------------------------------------------ + /** Returns the speed of the kart in meters/second. */ +- virtual float getSpeed() const +- { return m_all_physic_info[m_current_transform].m_speed; } ++ virtual float getSpeed() const; + // ------------------------------------------------------------------------ + + }; // GhostKart +diff --git a/src/replay/replay_play.cpp b/src/replay/replay_play.cpp +index 863d253..9babedc 100644 +--- a/src/replay/replay_play.cpp ++++ b/src/replay/replay_play.cpp +@@ -21,6 +21,7 @@ + #include "config/stk_config.hpp" + #include "io/file_manager.hpp" + #include "karts/ghost_kart.hpp" ++#include "karts/controller/ghost_controller.hpp" + #include "modes/world.hpp" + #include "race/race_manager.hpp" + #include "tracks/track.hpp" +@@ -196,6 +197,8 @@ void ReplayPlay::readKartData(FILE *fd, char *next_line) + m_ghost_karts.push_back(new GhostKart(m_ghost_karts_list.at(kart_num), + kart_num, kart_num + 1)); + m_ghost_karts[kart_num].init(RaceManager::KT_GHOST); ++ Controller* controller = new GhostController(getGhostKart(kart_num)); ++ getGhostKart(kart_num)->setController(controller); + + unsigned int size; + if(sscanf(next_line,"size: %u",&size)!=1) diff --git a/sources.cmake b/sources.cmake index ddc029d4f..7c1db620c 100644 --- a/sources.cmake +++ b/sources.cmake @@ -1,5 +1,5 @@ # Modify this file to change the last-modified date when you add/remove a file. -# This will then trigger a new cmake run automatically. +# This will then trigger a new cmake run automatically. file(GLOB_RECURSE STK_HEADERS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "src/*.hpp") file(GLOB_RECURSE STK_SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "src/*.cpp") file(GLOB_RECURSE STK_SHADERS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "data/shaders/*") diff --git a/src/karts/controller/ai_base_controller.hpp b/src/karts/controller/ai_base_controller.hpp index d108ebe0a..fbea9008c 100644 --- a/src/karts/controller/ai_base_controller.hpp +++ b/src/karts/controller/ai_base_controller.hpp @@ -64,7 +64,7 @@ protected: void setControllerName(const std::string &name); float steerToPoint(const Vec3 &point); float normalizeAngle(float angle); - virtual void update (float delta) ; + virtual void update (float delta); virtual void setSteering (float angle, float dt); virtual bool canSkid(float steer_fraction) = 0; // ------------------------------------------------------------------------ diff --git a/src/karts/controller/ghost_controller.cpp b/src/karts/controller/ghost_controller.cpp new file mode 100644 index 000000000..c7219a505 --- /dev/null +++ b/src/karts/controller/ghost_controller.cpp @@ -0,0 +1,69 @@ +// +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2016 SuperTuxKart-Team +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#include "karts/controller/ghost_controller.hpp" + +#include "config/user_config.hpp" +#include "karts/abstract_kart.hpp" +#include "karts/kart_properties.hpp" +#include "karts/controller/ai_properties.hpp" +#include "modes/world.hpp" +#include "tracks/track.hpp" +#include "utils/constants.hpp" + +GhostController::GhostController(AbstractKart *kart, + StateManager::ActivePlayer *player) + : Controller(kart, player) +{ + m_kart = kart; +} // GhostController + +//----------------------------------------------------------------------------- +void GhostController::reset() +{ + m_current_index = 0; + m_current_time = 0; +} // reset + +//----------------------------------------------------------------------------- +void GhostController::update(float dt) +{ + m_current_time = World::getWorld()->getTime(); + // Find (if necessary) the next index to use + if (m_current_time != 0.0f) + { + while (m_current_index + 1 < m_all_times.size() && + m_current_time >= m_all_times[m_current_index + 1]) + { + m_current_index++; + } + } + +} // update + +//----------------------------------------------------------------------------- +void GhostController::addReplayTime(float time) +{ + // FIXME: for now avoid that transforms for the same time are set + // twice (to avoid division by zero in update). This should be + // done when saving in replay + if (m_all_times.size() > 0 && m_all_times.back() == time) + return; + m_all_times.push_back(time); + +} // addReplayTime diff --git a/src/karts/controller/ghost_controller.hpp b/src/karts/controller/ghost_controller.hpp new file mode 100644 index 000000000..64e4f2255 --- /dev/null +++ b/src/karts/controller/ghost_controller.hpp @@ -0,0 +1,79 @@ +// +// SuperTuxKart - a fun racing game with go-kart +// Copyright (C) 2016 SuperTuxKart-Team +// +// This program 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. +// +// This program 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 this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +#ifndef HEADER_GHOST_CONTROLLER_HPP +#define HEADER_GHOST_CONTROLLER_HPP + +#include "karts/controller/controller.hpp" +#include "states_screens/state_manager.hpp" + +#include + +/** A class for Ghost controller. + * \ingroup controller + */ +class GhostController : public Controller +{ +private: + /** Pointer to the last index in m_all_times that is smaller than + * the current world time. */ + unsigned int m_current_index; + + /** The current world time. */ + float m_current_time; + + /** The list of the times at which the events of kart were reached. */ + std::vector m_all_times; + +public: + GhostController(AbstractKart *kart, + StateManager::ActivePlayer *player=NULL); + virtual ~GhostController() {}; + virtual void reset(); + virtual void update (float dt); + virtual bool disableSlipstreamBonus() const { return true; } + virtual void crashed(const Material *m) {}; + virtual void crashed(const AbstractKart *k) {}; + virtual void handleZipper(bool play_sound) {}; + virtual void finishedRace(float time) {}; + virtual void collectedItem(const Item &item, int add_info=-1, + float previous_energy=0) {}; + virtual void setPosition(int p) {}; + virtual bool isPlayerController() const { return false; } + virtual bool isLocalPlayerController() const { return false; } + virtual void action(PlayerAction action, int value) {}; + virtual void skidBonusTriggered() {}; + virtual void newLap(int lap) {}; + void addReplayTime(float time); + // ------------------------------------------------------------------------ + bool isReplayEnd() const + { return m_current_index + 1 >= m_all_times.size(); } + // ------------------------------------------------------------------------ + float getReplayDelta() const + { + return ((m_current_time - m_all_times[m_current_index]) + / (m_all_times[m_current_index + 1] + - m_all_times[m_current_index])); + } + // ------------------------------------------------------------------------ + unsigned int getCurrentReplayIndex() const + { return m_current_index; } + // ------------------------------------------------------------------------ +}; // GhostController + +#endif diff --git a/src/karts/ghost_kart.cpp b/src/karts/ghost_kart.cpp index 0d6d713f5..0ba9eb6b3 100644 --- a/src/karts/ghost_kart.cpp +++ b/src/karts/ghost_kart.cpp @@ -17,6 +17,7 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "karts/ghost_kart.hpp" +#include "karts/controller/ghost_controller.hpp" #include "karts/kart_gfx.hpp" #include "karts/kart_model.hpp" #include "modes/world.hpp" @@ -30,10 +31,6 @@ GhostKart::GhostKart(const std::string& ident, unsigned int world_kart_id, position, btTransform(btQuaternion(0, 0, 0, 1)), PLAYER_DIFFICULTY_NORMAL) { - m_all_times.clear(); - m_all_transform.clear(); - m_all_physic_info.clear(); - m_all_replay_events.clear(); } // GhostKart // ---------------------------------------------------------------------------- @@ -41,7 +38,6 @@ void GhostKart::reset() { m_node->setVisible(true); Kart::reset(); - m_current_transform = 0; // This will set the correct start position update(0); } // reset @@ -52,12 +48,9 @@ void GhostKart::addReplayEvent(float time, const ReplayBase::PhysicInfo &pi, const ReplayBase::KartReplayEvent &kre) { - // FIXME: for now avoid that transforms for the same time are set - // twice (to avoid division by zero in update). This should be - // done when saving in replay - if(m_all_times.size()>0 && m_all_times.back()==time) - return; - m_all_times.push_back(time); + GhostController* gc = dynamic_cast(getController()); + gc->addReplayTime(time); + m_all_transform.push_back(trans); m_all_physic_info.push_back(pi); m_all_replay_events.push_back(kre); @@ -80,27 +73,23 @@ void GhostKart::addReplayEvent(float time, */ void GhostKart::update(float dt) { - float t = World::getWorld()->getTime(); - // Find (if necessary) the next index to use - if (t != 0.0f) - { - while (m_current_transform + 1 < m_all_times.size() && - t >= m_all_times[m_current_transform+1]) - { - m_current_transform++; - } - } + GhostController* gc = dynamic_cast(getController()); + if (gc == NULL) return; - if (m_current_transform + 1 >= m_all_times.size()) + gc->update(dt); + if (gc->isReplayEnd()) { m_node->setVisible(false); return; } + const unsigned int idx = gc->getCurrentReplayIndex(); + const float rd = gc->getReplayDelta(); + float nitro_frac = 0; - if (m_all_replay_events[m_current_transform].m_on_nitro) + if (m_all_replay_events[idx].m_on_nitro) { - nitro_frac = fabsf(m_all_physic_info[m_current_transform].m_speed) / + nitro_frac = fabsf(m_all_physic_info[idx].m_speed) / (m_kart_properties->getEngineMaxSpeed()); if (nitro_frac > 1.0f) @@ -108,16 +97,14 @@ void GhostKart::update(float dt) } getKartGFX()->updateNitroGraphics(nitro_frac); - if (m_all_replay_events[m_current_transform].m_on_zipper) + if (m_all_replay_events[idx].m_on_zipper) showZipperFire(); - float f =(t - m_all_times[m_current_transform]) - / ( m_all_times[m_current_transform+1] - - m_all_times[m_current_transform] ); - setXYZ((1-f)*m_all_transform[m_current_transform ].getOrigin() - + f *m_all_transform[m_current_transform+1].getOrigin() ); - const btQuaternion q = m_all_transform[m_current_transform].getRotation() - .slerp(m_all_transform[m_current_transform+1].getRotation(), f); + setXYZ((1- rd)*m_all_transform[idx ].getOrigin() + + rd *m_all_transform[idx + 1].getOrigin() ); + + const btQuaternion q = m_all_transform[idx].getRotation() + .slerp(m_all_transform[idx + 1].getRotation(), rd); setRotation(q); Vec3 center_shift(0, 0, 0); @@ -125,12 +112,18 @@ void GhostKart::update(float dt) center_shift = getTrans().getBasis() * center_shift; Moveable::updateGraphics(dt, center_shift, btQuaternion(0, 0, 0, 1)); - getKartModel()->update(dt, dt*(m_all_physic_info[m_current_transform].m_speed), - m_all_physic_info[m_current_transform].m_steer, - m_all_physic_info[m_current_transform].m_speed, - m_current_transform); + getKartModel()->update(dt, dt*(m_all_physic_info[idx].m_speed), + m_all_physic_info[idx].m_steer, m_all_physic_info[idx].m_speed, idx); Vec3 front(0, 0, getKartLength()*0.5f); m_xyz_front = getTrans()(front); getKartGFX()->update(dt); } // update + +// ---------------------------------------------------------------------------- +float GhostKart::getSpeed() const +{ + const GhostController* gc = + dynamic_cast(getController()); + return m_all_physic_info[gc->getCurrentReplayIndex()].m_speed; +} diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp index dc8f6056c..d95ad8d85 100644 --- a/src/karts/ghost_kart.hpp +++ b/src/karts/ghost_kart.hpp @@ -36,9 +36,6 @@ class GhostKart : public Kart { private: - /** The list of the times at which the transform were reached. */ - std::vector m_all_times; - /** The transforms to assume at the corresponding time in m_all_times. */ std::vector m_all_transform; @@ -46,10 +43,6 @@ private: std::vector m_all_replay_events; - /** Pointer to the last index in m_all_times that is smaller than - * the current world time. */ - unsigned int m_current_transform; - public: GhostKart(const std::string& ident, unsigned int world_kart_id, int position); @@ -80,8 +73,7 @@ public: virtual bool isInvulnerable() const { return true; } // ------------------------------------------------------------------------ /** Returns the speed of the kart in meters/second. */ - virtual float getSpeed() const - { return m_all_physic_info[m_current_transform].m_speed; } + virtual float getSpeed() const; // ------------------------------------------------------------------------ }; // GhostKart diff --git a/src/replay/replay_play.cpp b/src/replay/replay_play.cpp index 863d25320..9babedc84 100644 --- a/src/replay/replay_play.cpp +++ b/src/replay/replay_play.cpp @@ -21,6 +21,7 @@ #include "config/stk_config.hpp" #include "io/file_manager.hpp" #include "karts/ghost_kart.hpp" +#include "karts/controller/ghost_controller.hpp" #include "modes/world.hpp" #include "race/race_manager.hpp" #include "tracks/track.hpp" @@ -196,6 +197,8 @@ void ReplayPlay::readKartData(FILE *fd, char *next_line) m_ghost_karts.push_back(new GhostKart(m_ghost_karts_list.at(kart_num), kart_num, kart_num + 1)); m_ghost_karts[kart_num].init(RaceManager::KT_GHOST); + Controller* controller = new GhostController(getGhostKart(kart_num)); + getGhostKart(kart_num)->setController(controller); unsigned int size; if(sscanf(next_line,"size: %u",&size)!=1) From 61d6e572c0204c6d715fa290c3620c34f80244bc Mon Sep 17 00:00:00 2001 From: Benau Date: Wed, 10 Feb 2016 10:38:27 +0800 Subject: [PATCH 011/324] Remove unnused file --- patch | 368 ---------------------------------------------------------- 1 file changed, 368 deletions(-) delete mode 100644 patch diff --git a/patch b/patch deleted file mode 100644 index 80818a7f2..000000000 --- a/patch +++ /dev/null @@ -1,368 +0,0 @@ -diff --git a/sources.cmake b/sources.cmake -index ddc029d..7c1db62 100644 ---- a/sources.cmake -+++ b/sources.cmake -@@ -1,5 +1,5 @@ - # Modify this file to change the last-modified date when you add/remove a file. --# This will then trigger a new cmake run automatically. -+# This will then trigger a new cmake run automatically. - file(GLOB_RECURSE STK_HEADERS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "src/*.hpp") - file(GLOB_RECURSE STK_SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "src/*.cpp") - file(GLOB_RECURSE STK_SHADERS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "data/shaders/*") -diff --git a/src/karts/controller/ai_base_controller.hpp b/src/karts/controller/ai_base_controller.hpp -index d108ebe..fbea900 100644 ---- a/src/karts/controller/ai_base_controller.hpp -+++ b/src/karts/controller/ai_base_controller.hpp -@@ -64,7 +64,7 @@ protected: - void setControllerName(const std::string &name); - float steerToPoint(const Vec3 &point); - float normalizeAngle(float angle); -- virtual void update (float delta) ; -+ virtual void update (float delta); - virtual void setSteering (float angle, float dt); - virtual bool canSkid(float steer_fraction) = 0; - // ------------------------------------------------------------------------ -diff --git a/src/karts/controller/ghost_controller.cpp b/src/karts/controller/ghost_controller.cpp -new file mode 100644 -index 0000000..5f4000d ---- /dev/null -+++ b/src/karts/controller/ghost_controller.cpp -@@ -0,0 +1,70 @@ -+// -+// SuperTuxKart - a fun racing game with go-kart -+// Copyright (C) 2016 SuperTuxKart-Team -+// -+// This program 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. -+// -+// This program 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 this program; if not, write to the Free Software -+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -+ -+#include "karts/controller/ghost_controller.hpp" -+ -+#include "config/user_config.hpp" -+#include "karts/abstract_kart.hpp" -+#include "karts/kart_properties.hpp" -+#include "karts/controller/ai_properties.hpp" -+#include "modes/world.hpp" -+#include "tracks/track.hpp" -+#include "utils/constants.hpp" -+ -+GhostController::GhostController(AbstractKart *kart, -+ StateManager::ActivePlayer *player) -+ : Controller(kart, player) -+{ -+ m_kart = kart; -+} // GhostController -+ -+//----------------------------------------------------------------------------- -+void GhostController::reset() -+{ -+ m_current_index = 0; -+ m_current_time = 0; -+ m_all_times.clear(); -+} // reset -+ -+//----------------------------------------------------------------------------- -+void GhostController::update(float dt) -+{ -+ m_current_time = World::getWorld()->getTime(); -+ // Find (if necessary) the next index to use -+ if (m_current_time != 0.0f) -+ { -+ while (m_current_index + 1 < m_all_times.size() && -+ m_current_time >= m_all_times[m_current_index + 1]) -+ { -+ m_current_index++; -+ } -+ } -+ -+} // update -+ -+//----------------------------------------------------------------------------- -+void GhostController::addReplayTime(float time) -+{ -+ // FIXME: for now avoid that transforms for the same time are set -+ // twice (to avoid division by zero in update). This should be -+ // done when saving in replay -+ if (m_all_times.size() > 0 && m_all_times.back() == time) -+ return; -+ m_all_times.push_back(time); -+ -+} // addReplayTime -diff --git a/src/karts/controller/ghost_controller.hpp b/src/karts/controller/ghost_controller.hpp -new file mode 100644 -index 0000000..64e4f22 ---- /dev/null -+++ b/src/karts/controller/ghost_controller.hpp -@@ -0,0 +1,79 @@ -+// -+// SuperTuxKart - a fun racing game with go-kart -+// Copyright (C) 2016 SuperTuxKart-Team -+// -+// This program 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. -+// -+// This program 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 this program; if not, write to the Free Software -+// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -+ -+#ifndef HEADER_GHOST_CONTROLLER_HPP -+#define HEADER_GHOST_CONTROLLER_HPP -+ -+#include "karts/controller/controller.hpp" -+#include "states_screens/state_manager.hpp" -+ -+#include -+ -+/** A class for Ghost controller. -+ * \ingroup controller -+ */ -+class GhostController : public Controller -+{ -+private: -+ /** Pointer to the last index in m_all_times that is smaller than -+ * the current world time. */ -+ unsigned int m_current_index; -+ -+ /** The current world time. */ -+ float m_current_time; -+ -+ /** The list of the times at which the events of kart were reached. */ -+ std::vector m_all_times; -+ -+public: -+ GhostController(AbstractKart *kart, -+ StateManager::ActivePlayer *player=NULL); -+ virtual ~GhostController() {}; -+ virtual void reset(); -+ virtual void update (float dt); -+ virtual bool disableSlipstreamBonus() const { return true; } -+ virtual void crashed(const Material *m) {}; -+ virtual void crashed(const AbstractKart *k) {}; -+ virtual void handleZipper(bool play_sound) {}; -+ virtual void finishedRace(float time) {}; -+ virtual void collectedItem(const Item &item, int add_info=-1, -+ float previous_energy=0) {}; -+ virtual void setPosition(int p) {}; -+ virtual bool isPlayerController() const { return false; } -+ virtual bool isLocalPlayerController() const { return false; } -+ virtual void action(PlayerAction action, int value) {}; -+ virtual void skidBonusTriggered() {}; -+ virtual void newLap(int lap) {}; -+ void addReplayTime(float time); -+ // ------------------------------------------------------------------------ -+ bool isReplayEnd() const -+ { return m_current_index + 1 >= m_all_times.size(); } -+ // ------------------------------------------------------------------------ -+ float getReplayDelta() const -+ { -+ return ((m_current_time - m_all_times[m_current_index]) -+ / (m_all_times[m_current_index + 1] -+ - m_all_times[m_current_index])); -+ } -+ // ------------------------------------------------------------------------ -+ unsigned int getCurrentReplayIndex() const -+ { return m_current_index; } -+ // ------------------------------------------------------------------------ -+}; // GhostController -+ -+#endif -diff --git a/src/karts/ghost_kart.cpp b/src/karts/ghost_kart.cpp -index 0d6d713..4e1c321 100644 ---- a/src/karts/ghost_kart.cpp -+++ b/src/karts/ghost_kart.cpp -@@ -17,6 +17,7 @@ - // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - - #include "karts/ghost_kart.hpp" -+#include "karts/controller/ghost_controller.hpp" - #include "karts/kart_gfx.hpp" - #include "karts/kart_model.hpp" - #include "modes/world.hpp" -@@ -30,7 +31,6 @@ GhostKart::GhostKart(const std::string& ident, unsigned int world_kart_id, - position, btTransform(btQuaternion(0, 0, 0, 1)), - PLAYER_DIFFICULTY_NORMAL) - { -- m_all_times.clear(); - m_all_transform.clear(); - m_all_physic_info.clear(); - m_all_replay_events.clear(); -@@ -41,7 +41,6 @@ void GhostKart::reset() - { - m_node->setVisible(true); - Kart::reset(); -- m_current_transform = 0; - // This will set the correct start position - update(0); - } // reset -@@ -52,12 +51,9 @@ void GhostKart::addReplayEvent(float time, - const ReplayBase::PhysicInfo &pi, - const ReplayBase::KartReplayEvent &kre) - { -- // FIXME: for now avoid that transforms for the same time are set -- // twice (to avoid division by zero in update). This should be -- // done when saving in replay -- if(m_all_times.size()>0 && m_all_times.back()==time) -- return; -- m_all_times.push_back(time); -+ GhostController* gc = dynamic_cast(getController()); -+ gc->addReplayTime(time); -+ - m_all_transform.push_back(trans); - m_all_physic_info.push_back(pi); - m_all_replay_events.push_back(kre); -@@ -80,27 +76,23 @@ void GhostKart::addReplayEvent(float time, - */ - void GhostKart::update(float dt) - { -- float t = World::getWorld()->getTime(); -- // Find (if necessary) the next index to use -- if (t != 0.0f) -- { -- while (m_current_transform + 1 < m_all_times.size() && -- t >= m_all_times[m_current_transform+1]) -- { -- m_current_transform++; -- } -- } -+ GhostController* gc = dynamic_cast(getController()); -+ if (gc == NULL) return; - -- if (m_current_transform + 1 >= m_all_times.size()) -+ gc->update(dt); -+ if (gc->isReplayEnd()) - { - m_node->setVisible(false); - return; - } - -+ const unsigned int idx = gc->getCurrentReplayIndex(); -+ const float rd = gc->getReplayDelta(); -+ - float nitro_frac = 0; -- if (m_all_replay_events[m_current_transform].m_on_nitro) -+ if (m_all_replay_events[idx].m_on_nitro) - { -- nitro_frac = fabsf(m_all_physic_info[m_current_transform].m_speed) / -+ nitro_frac = fabsf(m_all_physic_info[idx].m_speed) / - (m_kart_properties->getEngineMaxSpeed()); - - if (nitro_frac > 1.0f) -@@ -108,16 +100,14 @@ void GhostKart::update(float dt) - } - getKartGFX()->updateNitroGraphics(nitro_frac); - -- if (m_all_replay_events[m_current_transform].m_on_zipper) -+ if (m_all_replay_events[idx].m_on_zipper) - showZipperFire(); - -- float f =(t - m_all_times[m_current_transform]) -- / ( m_all_times[m_current_transform+1] -- - m_all_times[m_current_transform] ); -- setXYZ((1-f)*m_all_transform[m_current_transform ].getOrigin() -- + f *m_all_transform[m_current_transform+1].getOrigin() ); -- const btQuaternion q = m_all_transform[m_current_transform].getRotation() -- .slerp(m_all_transform[m_current_transform+1].getRotation(), f); -+ setXYZ((1- rd)*m_all_transform[idx ].getOrigin() -+ + rd *m_all_transform[idx + 1].getOrigin() ); -+ -+ const btQuaternion q = m_all_transform[idx].getRotation() -+ .slerp(m_all_transform[idx + 1].getRotation(), rd); - setRotation(q); - - Vec3 center_shift(0, 0, 0); -@@ -125,12 +115,18 @@ void GhostKart::update(float dt) - center_shift = getTrans().getBasis() * center_shift; - - Moveable::updateGraphics(dt, center_shift, btQuaternion(0, 0, 0, 1)); -- getKartModel()->update(dt, dt*(m_all_physic_info[m_current_transform].m_speed), -- m_all_physic_info[m_current_transform].m_steer, -- m_all_physic_info[m_current_transform].m_speed, -- m_current_transform); -+ getKartModel()->update(dt, dt*(m_all_physic_info[idx].m_speed), -+ m_all_physic_info[idx].m_steer, m_all_physic_info[idx].m_speed, idx); - - Vec3 front(0, 0, getKartLength()*0.5f); - m_xyz_front = getTrans()(front); - getKartGFX()->update(dt); - } // update -+ -+// ---------------------------------------------------------------------------- -+float GhostKart::getSpeed() const -+{ -+ const GhostController* gc = -+ dynamic_cast(getController()); -+ return m_all_physic_info[gc->getCurrentReplayIndex()].m_speed; -+} -diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp -index dc8f605..d95ad8d 100644 ---- a/src/karts/ghost_kart.hpp -+++ b/src/karts/ghost_kart.hpp -@@ -36,9 +36,6 @@ - class GhostKart : public Kart - { - private: -- /** The list of the times at which the transform were reached. */ -- std::vector m_all_times; -- - /** The transforms to assume at the corresponding time in m_all_times. */ - std::vector m_all_transform; - -@@ -46,10 +43,6 @@ private: - - std::vector m_all_replay_events; - -- /** Pointer to the last index in m_all_times that is smaller than -- * the current world time. */ -- unsigned int m_current_transform; -- - public: - GhostKart(const std::string& ident, - unsigned int world_kart_id, int position); -@@ -80,8 +73,7 @@ public: - virtual bool isInvulnerable() const { return true; } - // ------------------------------------------------------------------------ - /** Returns the speed of the kart in meters/second. */ -- virtual float getSpeed() const -- { return m_all_physic_info[m_current_transform].m_speed; } -+ virtual float getSpeed() const; - // ------------------------------------------------------------------------ - - }; // GhostKart -diff --git a/src/replay/replay_play.cpp b/src/replay/replay_play.cpp -index 863d253..9babedc 100644 ---- a/src/replay/replay_play.cpp -+++ b/src/replay/replay_play.cpp -@@ -21,6 +21,7 @@ - #include "config/stk_config.hpp" - #include "io/file_manager.hpp" - #include "karts/ghost_kart.hpp" -+#include "karts/controller/ghost_controller.hpp" - #include "modes/world.hpp" - #include "race/race_manager.hpp" - #include "tracks/track.hpp" -@@ -196,6 +197,8 @@ void ReplayPlay::readKartData(FILE *fd, char *next_line) - m_ghost_karts.push_back(new GhostKart(m_ghost_karts_list.at(kart_num), - kart_num, kart_num + 1)); - m_ghost_karts[kart_num].init(RaceManager::KT_GHOST); -+ Controller* controller = new GhostController(getGhostKart(kart_num)); -+ getGhostKart(kart_num)->setController(controller); - - unsigned int size; - if(sscanf(next_line,"size: %u",&size)!=1) From 5a9fdd7a8ddfc62213b75ef0a942b1ea63adda1a Mon Sep 17 00:00:00 2001 From: Benau Date: Wed, 10 Feb 2016 12:43:45 +0800 Subject: [PATCH 012/324] Allow replay reverse track --- src/karts/controller/ghost_controller.cpp | 9 +-------- src/karts/controller/ghost_controller.hpp | 6 +++--- src/karts/ghost_kart.cpp | 7 +++++-- src/race/race_manager.cpp | 2 +- src/replay/replay_play.cpp | 17 ++++++++++++++--- src/replay/replay_play.hpp | 2 +- src/replay/replay_recorder.cpp | 1 + 7 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/karts/controller/ghost_controller.cpp b/src/karts/controller/ghost_controller.cpp index c7219a505..2c89e62ce 100644 --- a/src/karts/controller/ghost_controller.cpp +++ b/src/karts/controller/ghost_controller.cpp @@ -17,14 +17,7 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "karts/controller/ghost_controller.hpp" - -#include "config/user_config.hpp" -#include "karts/abstract_kart.hpp" -#include "karts/kart_properties.hpp" -#include "karts/controller/ai_properties.hpp" #include "modes/world.hpp" -#include "tracks/track.hpp" -#include "utils/constants.hpp" GhostController::GhostController(AbstractKart *kart, StateManager::ActivePlayer *player) @@ -37,7 +30,7 @@ GhostController::GhostController(AbstractKart *kart, void GhostController::reset() { m_current_index = 0; - m_current_time = 0; + m_current_time = 0.0f; } // reset //----------------------------------------------------------------------------- diff --git a/src/karts/controller/ghost_controller.hpp b/src/karts/controller/ghost_controller.hpp index 64e4f2255..e1d76ac85 100644 --- a/src/karts/controller/ghost_controller.hpp +++ b/src/karts/controller/ghost_controller.hpp @@ -66,9 +66,9 @@ public: // ------------------------------------------------------------------------ float getReplayDelta() const { - return ((m_current_time - m_all_times[m_current_index]) - / (m_all_times[m_current_index + 1] - - m_all_times[m_current_index])); + assert(m_current_index < m_all_times.size()); + return ((m_current_time - m_all_times[m_current_index]) / + (m_all_times[m_current_index + 1] - m_all_times[m_current_index])); } // ------------------------------------------------------------------------ unsigned int getCurrentReplayIndex() const diff --git a/src/karts/ghost_kart.cpp b/src/karts/ghost_kart.cpp index 0ba9eb6b3..b903cdd56 100644 --- a/src/karts/ghost_kart.cpp +++ b/src/karts/ghost_kart.cpp @@ -23,7 +23,6 @@ #include "modes/world.hpp" #include "LinearMath/btQuaternion.h" -#include "utils/log.hpp" GhostKart::GhostKart(const std::string& ident, unsigned int world_kart_id, int position) @@ -85,6 +84,7 @@ void GhostKart::update(float dt) const unsigned int idx = gc->getCurrentReplayIndex(); const float rd = gc->getReplayDelta(); + assert(idx < m_all_transform.size()); float nitro_frac = 0; if (m_all_replay_events[idx].m_on_nitro) @@ -121,9 +121,12 @@ void GhostKart::update(float dt) } // update // ---------------------------------------------------------------------------- +/** Returns the speed of the kart in meters/second. */ float GhostKart::getSpeed() const { const GhostController* gc = dynamic_cast(getController()); + + assert(gc->getCurrentReplayIndex() < m_all_physic_info.size()); return m_all_physic_info[gc->getCurrentReplayIndex()].m_speed; -} +} // getSpeed diff --git a/src/race/race_manager.cpp b/src/race/race_manager.cpp index 92bcccddc..f12d824cf 100644 --- a/src/race/race_manager.cpp +++ b/src/race/race_manager.cpp @@ -312,7 +312,7 @@ void RaceManager::startNew(bool from_overworld) unsigned int gk = 0; if (ReplayPlay::get()) { - ReplayPlay::get()->loadKartInfo(); + ReplayPlay::get()->loadBasicInfo(); gk = ReplayPlay::get()->getNumGhostKart(); } diff --git a/src/replay/replay_play.cpp b/src/replay/replay_play.cpp index 9babedc84..7bba55442 100644 --- a/src/replay/replay_play.cpp +++ b/src/replay/replay_play.cpp @@ -73,9 +73,10 @@ void ReplayPlay::update(float dt) } // update //----------------------------------------------------------------------------- -/** Loads the ghost karts info in the replay file, required by race manager. +/** Loads the basic (ghost karts, reverse track) info in the replay file, + * required by race manager. */ -void ReplayPlay::loadKartInfo() +void ReplayPlay::loadBasicInfo() { char s[1024]; @@ -88,6 +89,12 @@ void ReplayPlay::loadKartInfo() return; } + int reverse; + fgets(s, 1023, fd); + if(sscanf(s, "reverse: %d", &reverse) != 1) + Log::fatal("Replay", "Reverse info found in replay file."); + race_manager->setReverseTrack((bool)reverse); + Log::info("Replay", "Reading ghost karts info"); while(true) { @@ -104,7 +111,7 @@ void ReplayPlay::loadKartInfo() m_ghost_karts_list.push_back(std::string(s1)); } fclose(fd); -} // loadKartInfo +} // loadBasicInfo //----------------------------------------------------------------------------- /** Loads a replay data from file called 'trackname'.replay. @@ -127,6 +134,10 @@ void ReplayPlay::load() if (fgets(s, 1023, fd) == NULL) Log::fatal("Replay", "Could not read '%s'.", getReplayFilename().c_str()); + if (fgets(s, 1023, fd) == NULL) + Log::fatal("Replay", "Could not read '%s'.", getReplayFilename().c_str()); + // Skip reverse info which is already read. + for (unsigned int i = 0; i < m_ghost_karts_list.size(); i++) { if (fgets(s, 1023, fd) == NULL) diff --git a/src/replay/replay_play.hpp b/src/replay/replay_play.hpp index 3c01599bd..ec2877d02 100644 --- a/src/replay/replay_play.hpp +++ b/src/replay/replay_play.hpp @@ -51,7 +51,7 @@ public: void update(float dt); void reset(); void load(); - void loadKartInfo(); + void loadBasicInfo(); // ------------------------------------------------------------------------ GhostKart* getGhostKart(int n) { return m_ghost_karts.get(n); } diff --git a/src/replay/replay_recorder.cpp b/src/replay/replay_recorder.cpp index 40dc93661..b05069971 100644 --- a/src/replay/replay_recorder.cpp +++ b/src/replay/replay_recorder.cpp @@ -208,6 +208,7 @@ void ReplayRecorder::Save() fprintf(fd, "difficulty: %d\n", race_manager->getDifficulty()); fprintf(fd, "track: %s\n", world->getTrack()->getIdent().c_str()); fprintf(fd, "laps: %d\n", race_manager->getNumLaps()); + fprintf(fd, "reverse: %d\n", (int)race_manager->getReverseTrack()); unsigned int max_frames = (unsigned int)( stk_config->m_replay_max_time / stk_config->m_replay_dt ); From bb88a0f0ec593823961175393e163c203fb74aa2 Mon Sep 17 00:00:00 2001 From: Benau Date: Thu, 11 Feb 2016 00:42:33 +0800 Subject: [PATCH 013/324] Allow auto-save replay when specified in time trial mode. It will disable AI when recording, also it will only save if the race is completed, ie no one gave up or all events fit in max frame recorded. --- data/gui/track_info.stkgui | 21 ++++++++++---- src/input/input_manager.cpp | 2 +- src/karts/controller/ghost_controller.cpp | 1 - src/karts/ghost_kart.hpp | 4 +-- src/modes/world.cpp | 5 ++-- src/race/race_manager.cpp | 1 + src/race/race_manager.hpp | 11 ++++++++ src/replay/replay_play.cpp | 12 -------- src/replay/replay_play.hpp | 1 - src/replay/replay_recorder.cpp | 34 +++++++++++++++-------- src/replay/replay_recorder.hpp | 7 +++-- src/states_screens/track_info_screen.cpp | 27 ++++++++++++++++++ src/states_screens/track_info_screen.hpp | 3 ++ src/utils/debug.cpp | 2 +- 14 files changed, 91 insertions(+), 40 deletions(-) diff --git a/data/gui/track_info.stkgui b/data/gui/track_info.stkgui index 957bcf7f8..8e37a4cbf 100644 --- a/data/gui/track_info.stkgui +++ b/data/gui/track_info.stkgui @@ -3,9 +3,9 @@
-
+
- + @@ -49,8 +49,8 @@
- - + +
- +
+
+
- + diff --git a/src/input/input_manager.cpp b/src/input/input_manager.cpp index 31b8793b3..de2833a75 100644 --- a/src/input/input_manager.cpp +++ b/src/input/input_manager.cpp @@ -368,7 +368,7 @@ void InputManager::handleStaticAction(int key, int value) if(world && value) { if(control_is_pressed && ReplayRecorder::get()) - ReplayRecorder::get()->Save(); + ReplayRecorder::get()->save(); else history->Save(); } diff --git a/src/karts/controller/ghost_controller.cpp b/src/karts/controller/ghost_controller.cpp index 2c89e62ce..10015bc2d 100644 --- a/src/karts/controller/ghost_controller.cpp +++ b/src/karts/controller/ghost_controller.cpp @@ -23,7 +23,6 @@ GhostController::GhostController(AbstractKart *kart, StateManager::ActivePlayer *player) : Controller(kart, player) { - m_kart = kart; } // GhostController //----------------------------------------------------------------------------- diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp index d95ad8d85..bbfe24a6c 100644 --- a/src/karts/ghost_kart.hpp +++ b/src/karts/ghost_kart.hpp @@ -53,10 +53,10 @@ public: virtual void updateWeight() {}; // ------------------------------------------------------------------------ /** No physics for ghost kart. */ - virtual void applyEngineForce (float force) {} + virtual void applyEngineForce (float force) {}; // ------------------------------------------------------------------------ // Not needed to create any physics for a ghost kart. - virtual void createPhysics() {} + virtual void createPhysics() {}; // ------------------------------------------------------------------------ const float getSuspensionLength(int index, int wheel) const { return m_all_physic_info[index].m_suspension_length[wheel]; } diff --git a/src/modes/world.cpp b/src/modes/world.cpp index 622a11494..49c72d86d 100644 --- a/src/modes/world.cpp +++ b/src/modes/world.cpp @@ -279,7 +279,7 @@ void World::reset() race_manager->reset(); // Make sure to overwrite the data from the previous race. if(!history->replayHistory()) history->initRecording(); - if(ReplayRecorder::get()) ReplayRecorder::get()->init(); + if(race_manager->willRecordRace()) ReplayRecorder::get()->init(); // Reset all data structures that depend on number of karts. irr_driver->reset(); @@ -970,8 +970,7 @@ void World::update(float dt) PROFILER_PUSH_CPU_MARKER("World::update (sub-updates)", 0x20, 0x7F, 0x00); history->update(dt); - if(ReplayRecorder::get()) ReplayRecorder::get()->update(dt); - if(ReplayPlay::get()) ReplayPlay::get()->update(dt); + if(race_manager->willRecordRace()) ReplayRecorder::get()->update(dt); if(history->replayHistory()) dt=history->getNextDelta(); WorldStatus::update(dt); if (m_script_engine) m_script_engine->update(dt); diff --git a/src/race/race_manager.cpp b/src/race/race_manager.cpp index f12d824cf..d0818f476 100644 --- a/src/race/race_manager.cpp +++ b/src/race/race_manager.cpp @@ -76,6 +76,7 @@ RaceManager::RaceManager() m_started_from_overworld = false; m_have_kart_last_position_on_overworld = false; setReverseTrack(false); + setRecordRace(false); setTrack("jungle"); m_default_ai_list.clear(); setNumPlayers(0); diff --git a/src/race/race_manager.hpp b/src/race/race_manager.hpp index 8ae4046be..edb9578d3 100644 --- a/src/race/race_manager.hpp +++ b/src/race/race_manager.hpp @@ -350,6 +350,7 @@ private: /** Determines if saved GP should be continued or not*/ bool m_continue_saved_gp; + bool m_will_record_race; public: RaceManager(); ~RaceManager(); @@ -697,6 +698,16 @@ public: { return m_kart_last_position_on_overworld; } // getKartLastPositionOnOverworld + // ------------------------------------------------------------------------ + void setRecordRace(bool record) + { + m_will_record_race = record; + } // setRecordRace + // ------------------------------------------------------------------------ + bool willRecordRace() const + { + return m_will_record_race; + } // willRecordRace }; // RaceManager diff --git a/src/replay/replay_play.cpp b/src/replay/replay_play.cpp index 7bba55442..b6c8f819c 100644 --- a/src/replay/replay_play.cpp +++ b/src/replay/replay_play.cpp @@ -60,18 +60,6 @@ void ReplayPlay::reset() } } // reset -//----------------------------------------------------------------------------- -/** Updates all ghost karts. - * \param dt Time step size. - */ -void ReplayPlay::update(float dt) -{ - // First update all ghost karts - for(unsigned int i=0; i<(unsigned int)m_ghost_karts.size(); i++) - m_ghost_karts[i].update(dt); - -} // update - //----------------------------------------------------------------------------- /** Loads the basic (ghost karts, reverse track) info in the replay file, * required by race manager. diff --git a/src/replay/replay_play.hpp b/src/replay/replay_play.hpp index ec2877d02..2ca4a1feb 100644 --- a/src/replay/replay_play.hpp +++ b/src/replay/replay_play.hpp @@ -48,7 +48,6 @@ private: ~ReplayPlay(); void readKartData(FILE *fd, char *next_line); public: - void update(float dt); void reset(); void load(); void loadBasicInfo(); diff --git a/src/replay/replay_recorder.cpp b/src/replay/replay_recorder.cpp index b05069971..67923a230 100644 --- a/src/replay/replay_recorder.cpp +++ b/src/replay/replay_recorder.cpp @@ -55,6 +55,8 @@ ReplayRecorder::~ReplayRecorder() */ void ReplayRecorder::init() { + m_complete_replay = false; + m_incorrect_replay = false; m_transform_events.clear(); m_physic_info.clear(); m_kart_replay_event.clear(); @@ -81,19 +83,14 @@ void ReplayRecorder::init() #endif } // init -//----------------------------------------------------------------------------- -/** Resets all ghost karts back to start position. - */ -void ReplayRecorder::reset() -{ -} // reset - //----------------------------------------------------------------------------- /** Saves the current replay data. * \param dt Time step size. */ void ReplayRecorder::update(float dt) { + if (m_incorrect_replay || m_complete_replay) return; + const World *world = World::getWorld(); unsigned int num_karts = world->getNumKarts(); @@ -104,6 +101,9 @@ void ReplayRecorder::update(float dt) for(unsigned int i=0; igetKart(i); + // Don't record give-up race + if (kart->isEliminated()) return; + if (kart->isGhostKart()) continue; #ifdef DEBUG m_count++; @@ -126,6 +126,7 @@ void ReplayRecorder::update(float dt) sprintf(buffer, "Can't store more events for kart %s.", kart->getIdent().c_str()); Log::warn("ReplayRecorder", buffer); + m_incorrect_replay = true; } continue; } @@ -173,13 +174,25 @@ void ReplayRecorder::update(float dt) r->m_on_nitro = nitro; r->m_on_zipper = zipper; } // for i + + if (world->getPhase() == World::RESULT_DISPLAY_PHASE && !m_complete_replay) + { + m_complete_replay = true; + save(); + } } // update //----------------------------------------------------------------------------- /** Saves the replay data stored in the internal data structures. */ -void ReplayRecorder::Save() +void ReplayRecorder::save() { + if (m_incorrect_replay || !m_complete_replay) + { + Log::warn("ReplayRecorder", "Incomplete replay file will not be saved."); + return; + } + #ifdef DEBUG printf("%d frames, %d removed because of frequency compression\n", m_count, m_count_skipped_time); @@ -194,6 +207,7 @@ void ReplayRecorder::Save() Log::info("ReplayRecorder", "Replay saved in '%s'.\n", getReplayFilename().c_str()); + fprintf(fd, "reverse: %d\n", (int)race_manager->getReverseTrack()); World *world = World::getWorld(); unsigned int num_karts = world->getNumKarts(); for (unsigned int real_karts = 0; real_karts < num_karts; real_karts++) @@ -208,7 +222,6 @@ void ReplayRecorder::Save() fprintf(fd, "difficulty: %d\n", race_manager->getDifficulty()); fprintf(fd, "track: %s\n", world->getTrack()->getIdent().c_str()); fprintf(fd, "laps: %d\n", race_manager->getNumLaps()); - fprintf(fd, "reverse: %d\n", (int)race_manager->getReverseTrack()); unsigned int max_frames = (unsigned int)( stk_config->m_replay_max_time / stk_config->m_replay_dt ); @@ -245,5 +258,4 @@ void ReplayRecorder::Save() } // for i } fclose(fd); -} // Save - +} // save diff --git a/src/replay/replay_recorder.hpp b/src/replay/replay_recorder.hpp index 2bcb344aa..c486c55a9 100644 --- a/src/replay/replay_recorder.hpp +++ b/src/replay/replay_recorder.hpp @@ -49,6 +49,10 @@ private: /** Static pointer to the one instance of the replay object. */ static ReplayRecorder *m_replay_recorder; + bool m_complete_replay; + + bool m_incorrect_replay; + #ifdef DEBUG /** Counts overall number of events stored. */ unsigned int m_count; @@ -66,8 +70,7 @@ private: public: void init(); void update(float dt); - void reset(); - void Save(); + void save(); // ------------------------------------------------------------------------ /** Creates a new instance of the replay object. */ diff --git a/src/states_screens/track_info_screen.cpp b/src/states_screens/track_info_screen.cpp index 34877139b..99f5c6c6d 100644 --- a/src/states_screens/track_info_screen.cpp +++ b/src/states_screens/track_info_screen.cpp @@ -68,7 +68,9 @@ void TrackInfoScreen::loadedFromFile() m_lap_spinner = getWidget("lap-spinner"); m_ai_kart_spinner = getWidget("ai-spinner"); m_reverse = getWidget("reverse"); + m_record_race = getWidget("record"); m_reverse->setState(false); + m_record_race->setState(false); m_highscore_label = getWidget("highscores"); @@ -209,6 +211,14 @@ void TrackInfoScreen::init() else m_reverse->setState(false); + // Record race or not + // ------------- + const bool record_available = race_manager->getMinorMode() == RaceManager::MINOR_MODE_TIME_TRIAL; + m_record_race->setVisible(record_available); + getWidget("record-race-text")->setVisible(record_available); + if (record_available) + m_record_race->setState(false); + // ---- High Scores m_highscore_label->setVisible(has_highscores); @@ -358,6 +368,23 @@ void TrackInfoScreen::eventCallback(Widget* widget, const std::string& name, // checkbox. updateHighScores(); } + else if (name == "record") + { + const bool record = m_record_race->getState(); + race_manager->setRecordRace(record); + m_ai_kart_spinner->setValue(0); + // Disable AI when recording ghost race + if (record) + { + m_ai_kart_spinner->setActive(false); + race_manager->setNumKarts(race_manager->getNumLocalPlayers()); + UserConfigParams::m_num_karts = race_manager->getNumLocalPlayers(); + } + else + { + m_ai_kart_spinner->setActive(true); + } + } else if (name == "lap-spinner") { assert(race_manager->modeHasLaps()); diff --git a/src/states_screens/track_info_screen.hpp b/src/states_screens/track_info_screen.hpp index b4583bdd9..b64157d2d 100644 --- a/src/states_screens/track_info_screen.hpp +++ b/src/states_screens/track_info_screen.hpp @@ -55,6 +55,9 @@ class TrackInfoScreen : public GUIEngine::Screen, /** Check box for reverse mode. */ GUIEngine::CheckBoxWidget* m_reverse; + /** Check box for record race. */ + GUIEngine::CheckBoxWidget* m_record_race; + /** The label of the highscore list. */ GUIEngine::LabelWidget* m_highscore_label; diff --git a/src/utils/debug.cpp b/src/utils/debug.cpp index 6468db322..d9ebf06d9 100644 --- a/src/utils/debug.cpp +++ b/src/utils/debug.cpp @@ -357,7 +357,7 @@ bool handleContextMenuAction(s32 cmdID) } else if (cmdID == DEBUG_SAVE_REPLAY) { - ReplayRecorder::get()->Save(); + ReplayRecorder::get()->save(); } else if (cmdID == DEBUG_SAVE_HISTORY) { From 3f89512b3467b848bd13cbf22e5e34cee7376cc8 Mon Sep 17 00:00:00 2001 From: Benau Date: Thu, 11 Feb 2016 09:01:09 +0800 Subject: [PATCH 014/324] Remove most isGhostKart() hack when avoidable --- src/karts/kart.cpp | 11 ++++------ src/modes/linear_world.cpp | 28 ++++++++------------------ src/modes/standard_race.cpp | 21 ++++++------------- src/modes/world.cpp | 7 ------- src/race/race_manager.cpp | 1 - src/states_screens/race_gui.cpp | 6 +++--- src/states_screens/race_gui_base.cpp | 7 +++---- src/states_screens/race_result_gui.cpp | 7 ++----- 8 files changed, 26 insertions(+), 62 deletions(-) diff --git a/src/karts/kart.cpp b/src/karts/kart.cpp index 9ced75e24..ef0dd8490 100644 --- a/src/karts/kart.cpp +++ b/src/karts/kart.cpp @@ -512,8 +512,7 @@ void Kart::setController(Controller *controller) */ void Kart::setPosition(int p) { - if (m_controller) - m_controller->setPosition(p); + m_controller->setPosition(p); m_race_position = p; } // setPosition @@ -835,15 +834,14 @@ void Kart::finishedRace(float time) if(m_finished_race) return; m_finished_race = true; m_finish_time = time; - if(!isGhostKart()) - m_controller->finishedRace(time); + m_controller->finishedRace(time); m_kart_model->finishedRace(); race_manager->kartFinishedRace(this, time); if ((race_manager->getMinorMode() == RaceManager::MINOR_MODE_NORMAL_RACE || race_manager->getMinorMode() == RaceManager::MINOR_MODE_TIME_TRIAL || race_manager->getMinorMode() == RaceManager::MINOR_MODE_FOLLOW_LEADER) - && (isGhostKart() ? false : m_controller->isPlayerController())) + && m_controller->isPlayerController()) { RaceGUIBase* m = World::getWorld()->getRaceGUI(); if (m) @@ -890,9 +888,8 @@ void Kart::setRaceResult() if (race_manager->getMinorMode() == RaceManager::MINOR_MODE_NORMAL_RACE || race_manager->getMinorMode() == RaceManager::MINOR_MODE_TIME_TRIAL) { - if (isGhostKart() ? false : m_controller->isPlayerController()) + if (m_controller->isLocalPlayerController()) // if player is on this computer { - // if player is on this computer PlayerProfile *player = PlayerManager::getCurrentPlayer(); const ChallengeStatus *challenge = player->getCurrentChallengeStatus(); // In case of a GP challenge don't make the end animation depend diff --git a/src/modes/linear_world.cpp b/src/modes/linear_world.cpp index c3ae1e4fd..d6fc49185 100644 --- a/src/modes/linear_world.cpp +++ b/src/modes/linear_world.cpp @@ -245,33 +245,24 @@ void LinearWorld::newLap(unsigned int kart_index) { KartInfo &kart_info = m_kart_info[kart_index]; AbstractKart *kart = m_karts[kart_index]; - const bool is_gk = kart->isGhostKart(); // Reset reset-after-lap achievements - if (!is_gk) + StateManager::ActivePlayer *c = kart->getController()->getPlayer(); + PlayerProfile *p = PlayerManager::getCurrentPlayer(); + if (c && c->getConstProfile() == p) { - StateManager::ActivePlayer *c = kart->getController()->getPlayer(); - PlayerProfile *p = PlayerManager::getCurrentPlayer(); - if (c && c->getConstProfile() == p) - { - p->getAchievementsStatus()->onLapEnd(); - } + p->getAchievementsStatus()->onLapEnd(); } // Only update the kart controller if a kart that has already finished // the race crosses the start line again. This avoids 'fastest lap' // messages if the end controller does a fastest lap, but especially // allows the end controller to switch end cameras - if (!is_gk) + if(kart->hasFinishedRace()) { - if (kart->hasFinishedRace()) - { - kart->getController()->newLap(kart_info.m_race_lap); - return; - } - } - else if (kart->hasFinishedRace()) + kart->getController()->newLap(kart_info.m_race_lap); return; + } const int lap_count = race_manager->getNumLaps(); @@ -383,8 +374,7 @@ void LinearWorld::newLap(unsigned int kart_index) } // end if new fastest lap kart_info.m_lap_start_time = getTime(); - if (!is_gk) - kart->getController()->newLap(kart_info.m_race_lap); + kart->getController()->newLap(kart_info.m_race_lap); } // newLap //----------------------------------------------------------------------------- @@ -850,8 +840,6 @@ void LinearWorld::updateRacePosition() */ void LinearWorld::checkForWrongDirection(unsigned int i, float dt) { - if (m_karts[i]->isGhostKart()) return; - if (!m_karts[i]->getController()->isLocalPlayerController()) return; diff --git a/src/modes/standard_race.cpp b/src/modes/standard_race.cpp index 600ea03c1..1a8640edc 100644 --- a/src/modes/standard_race.cpp +++ b/src/modes/standard_race.cpp @@ -102,25 +102,16 @@ void StandardRace::endRaceEarly() continue; } - if (kart->isGhostKart()) + if (kart->getController()->isPlayerController()) { - // Ghost karts finish - setKartPosition(kartid, i - (unsigned int) active_players.size()); - kart->finishedRace(estimateFinishTimeForKart(kart)); + // Keep active players apart for now + active_players.push_back(kartid); } else { - if (kart->getController()->isPlayerController()) - { - // Keep active players apart for now - active_players.push_back(kartid); - } - else - { - // AI karts finish - setKartPosition(kartid, i - (unsigned int) active_players.size()); - kart->finishedRace(estimateFinishTimeForKart(kart)); - } + // AI karts finish + setKartPosition(kartid, i - (unsigned int) active_players.size()); + kart->finishedRace(estimateFinishTimeForKart(kart)); } } // i <= kart_amount // Now make the active players finish diff --git a/src/modes/world.cpp b/src/modes/world.cpp index 49c72d86d..9bfdb9bdb 100644 --- a/src/modes/world.cpp +++ b/src/modes/world.cpp @@ -520,7 +520,6 @@ void World::terminateRace() } for(unsigned int i = 0; i < kart_amount; i++) { - if (m_karts[i]->isGhostKart()) continue; // Retrieve the current player StateManager::ActivePlayer* p = m_karts[i]->getController()->getPlayer(); if (p && p->getConstProfile() == PlayerManager::getCurrentPlayer()) @@ -546,7 +545,6 @@ void World::terminateRace() { for(unsigned int i = 0; i < kart_amount; i++) { - if (m_karts[i]->isGhostKart()) continue; // Retrieve the current player StateManager::ActivePlayer* p = m_karts[i]->getController()->getPlayer(); if (p && p->getConstProfile() == PlayerManager::getCurrentPlayer()) @@ -1090,7 +1088,6 @@ void World::updateHighscores(int* best_highscore_rank, int* best_finish_time, for (unsigned int pos=0; posisGhostKart()) continue; if(index[pos] == 999) { // no kart claimed to be in this position, most likely means @@ -1155,14 +1152,11 @@ AbstractKart *World::getPlayerKart(unsigned int n) const unsigned int count=-1; for(unsigned int i=0; iisGhostKart()) continue; if(m_karts[i]->getController()->isPlayerController()) { count++; if(count==n) return m_karts[i]; } - } return NULL; } // getPlayerKart @@ -1260,7 +1254,6 @@ void World::unpause() for(unsigned int i=0; iisGhostKart()) continue; // Note that we can not test for isPlayerController here, since // an EndController will also return 'isPlayerController' if the // kart belonged to a player. diff --git a/src/race/race_manager.cpp b/src/race/race_manager.cpp index d0818f476..c968889ec 100644 --- a/src/race/race_manager.cpp +++ b/src/race/race_manager.cpp @@ -859,7 +859,6 @@ void RaceManager::kartFinishedRace(const AbstractKart *kart, float time) m_kart_status[id].m_overall_time += time; m_kart_status[id].m_last_time = time; m_num_finished_karts ++; - if(kart->isGhostKart()) return; if(kart->getController()->isPlayerController()) m_num_finished_players++; } // kartFinishedRace diff --git a/src/states_screens/race_gui.cpp b/src/states_screens/race_gui.cpp index 5c4e2f2e5..fe7c0a7e0 100644 --- a/src/states_screens/race_gui.cpp +++ b/src/states_screens/race_gui.cpp @@ -378,9 +378,9 @@ void RaceGUI::drawGlobalMiniMap() // int marker_height = m_marker->getSize().Height; core::rect source(core::position2di(0, 0), icon->getSize()); - int marker_half_size = (kart->isGhostKart() ? m_minimap_ai_size : - (kart->getController()->isLocalPlayerController() ? m_minimap_player_size : - m_minimap_ai_size))>>1; + int marker_half_size = (kart->getController()->isLocalPlayerController() + ? m_minimap_player_size + : m_minimap_ai_size )>>1; core::rect position(m_map_left+(int)(draw_at.getX()-marker_half_size), lower_y -(int)(draw_at.getY()+marker_half_size), m_map_left+(int)(draw_at.getX()+marker_half_size), diff --git a/src/states_screens/race_gui_base.cpp b/src/states_screens/race_gui_base.cpp index 939b4b04b..3639898c8 100644 --- a/src/states_screens/race_gui_base.cpp +++ b/src/states_screens/race_gui_base.cpp @@ -802,14 +802,13 @@ void RaceGUIBase::drawGlobalPlayerIcons(int bottom_margin) // draw icon video::ITexture *icon = kart->getKartProperties()->getIconMaterial()->getTexture(); - int w = kart->isGhostKart() ? ICON_WIDTH : (kart->getController() + int w = kart->getController() ->isLocalPlayerController() ? ICON_PLAYER_WIDTH - : ICON_WIDTH); + : ICON_WIDTH; const core::rect pos(x, y, x+w, y+w); //to bring to light the player's icon: add a background - if (kart->isGhostKart() ? - false : kart->getController()->isLocalPlayerController()) + if (kart->getController()->isLocalPlayerController()) { video::SColor colors[4]; for (unsigned int i=0;i<4;i++) diff --git a/src/states_screens/race_result_gui.cpp b/src/states_screens/race_result_gui.cpp index 5033930b1..416030f0b 100644 --- a/src/states_screens/race_result_gui.cpp +++ b/src/states_screens/race_result_gui.cpp @@ -89,7 +89,6 @@ void RaceResultGUI::init() for (unsigned int kart_id = 0; kart_id < num_karts; kart_id++) { const AbstractKart *kart = World::getWorld()->getKart(kart_id); - if (kart->isGhostKart()) continue; if (kart->getController()->isPlayerController()) human_win = human_win && kart->getRaceResult(); } @@ -475,8 +474,7 @@ void RaceResultGUI::determineTableLayout() // Save a pointer to the current row_info entry RowInfo *ri = &(m_all_row_infos[position-first_position]); - ri->m_is_player_kart = kart->isGhostKart() ? false : - kart->getController()->isLocalPlayerController(); + ri->m_is_player_kart = kart->getController()->isLocalPlayerController(); // Identify Human player, if so display real name other than kart name const int rm_id = kart->getWorldKartId() - @@ -862,8 +860,7 @@ void RaceResultGUI::determineGPLayout() else ri->m_kart_name = translations->fribidize(kart->getName()); - ri->m_is_player_kart = kart->isGhostKart() ? false : - kart->getController()->isLocalPlayerController(); + ri->m_is_player_kart = kart->getController()->isLocalPlayerController(); ri->m_player = ri->m_is_player_kart ? kart->getController()->getPlayer() : NULL; From 4daa752bb6f501e899b51c8689b5b395c3b1f228 Mon Sep 17 00:00:00 2001 From: Benau Date: Thu, 11 Feb 2016 09:08:12 +0800 Subject: [PATCH 015/324] Don't hurt a kart when it's already hurt enough --- src/items/powerup.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/items/powerup.cpp b/src/items/powerup.cpp index 351ec6aa0..d69312b59 100644 --- a/src/items/powerup.cpp +++ b/src/items/powerup.cpp @@ -295,7 +295,7 @@ void Powerup::use() for(unsigned int i = 0 ; i < world->getNumKarts(); ++i) { AbstractKart *kart=world->getKart(i); - if(kart->isEliminated() || kart->isGhostKart()) continue; + if(kart->isEliminated() || kart->isInvulnerable()) continue; if(kart == m_owner) continue; if(kart->getPosition() == 1) { @@ -329,7 +329,7 @@ void Powerup::use() for(unsigned int i = 0 ; i < world->getNumKarts(); ++i) { AbstractKart *kart=world->getKart(i); - if(kart->isEliminated() || kart== m_owner || kart->isGhostKart()) continue; + if(kart->isEliminated() || kart== m_owner || kart->isInvulnerable()) continue; if(kart->isShielded()) { kart->decreaseShieldTime(); From 80152d29894e1e9153cd397c9a87dfb46db0630c Mon Sep 17 00:00:00 2001 From: Benau Date: Thu, 11 Feb 2016 10:05:40 +0800 Subject: [PATCH 016/324] Clean up --- src/karts/ghost_kart.hpp | 2 ++ src/modes/world.cpp | 1 - src/replay/replay_recorder.cpp | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/karts/ghost_kart.hpp b/src/karts/ghost_kart.hpp index bbfe24a6c..009baa00d 100644 --- a/src/karts/ghost_kart.hpp +++ b/src/karts/ghost_kart.hpp @@ -75,6 +75,8 @@ public: /** Returns the speed of the kart in meters/second. */ virtual float getSpeed() const; // ------------------------------------------------------------------------ + virtual void kartIsInRestNow() {}; + // ------------------------------------------------------------------------ }; // GhostKart #endif diff --git a/src/modes/world.cpp b/src/modes/world.cpp index 9bfdb9bdb..5174870bc 100644 --- a/src/modes/world.cpp +++ b/src/modes/world.cpp @@ -731,7 +731,6 @@ void World::resetAllKarts() for ( KartList::iterator i=m_karts.begin(); i!=m_karts.end(); i++) { - if ((*i)->isGhostKart()) continue; (*i)->kartIsInRestNow(); } diff --git a/src/replay/replay_recorder.cpp b/src/replay/replay_recorder.cpp index 67923a230..26f961a7e 100644 --- a/src/replay/replay_recorder.cpp +++ b/src/replay/replay_recorder.cpp @@ -92,6 +92,7 @@ void ReplayRecorder::update(float dt) if (m_incorrect_replay || m_complete_replay) return; const World *world = World::getWorld(); + const bool single_player = race_manager->getNumPlayers() == 1; unsigned int num_karts = world->getNumKarts(); float time = world->getTime(); @@ -101,8 +102,8 @@ void ReplayRecorder::update(float dt) for(unsigned int i=0; igetKart(i); - // Don't record give-up race - if (kart->isEliminated()) return; + // If a single player give up in game menu, stop recording + if (kart->isEliminated() && single_player) return; if (kart->isGhostKart()) continue; #ifdef DEBUG @@ -126,7 +127,7 @@ void ReplayRecorder::update(float dt) sprintf(buffer, "Can't store more events for kart %s.", kart->getIdent().c_str()); Log::warn("ReplayRecorder", buffer); - m_incorrect_replay = true; + m_incorrect_replay = true && single_player; } continue; } From 1278394740a5d4e2afd09cd002fac1394724c51a Mon Sep 17 00:00:00 2001 From: Benau Date: Thu, 11 Feb 2016 10:09:31 +0800 Subject: [PATCH 017/324] More clean --- src/replay/replay_recorder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/replay/replay_recorder.cpp b/src/replay/replay_recorder.cpp index 26f961a7e..4e993c179 100644 --- a/src/replay/replay_recorder.cpp +++ b/src/replay/replay_recorder.cpp @@ -127,7 +127,7 @@ void ReplayRecorder::update(float dt) sprintf(buffer, "Can't store more events for kart %s.", kart->getIdent().c_str()); Log::warn("ReplayRecorder", buffer); - m_incorrect_replay = true && single_player; + m_incorrect_replay = single_player; } continue; } From ece95cbb8139f9d11a788b043e14239a7b608a9b Mon Sep 17 00:00:00 2001 From: Benau Date: Thu, 11 Feb 2016 13:42:25 +0800 Subject: [PATCH 018/324] Use MessageQueue to show whether the replay file is saved successfully --- data/skins/Forest.stkskin | 4 ++++ data/skins/Ocean.stkskin | 4 ++++ data/skins/Peach.stkskin | 4 ++++ data/skins/Ruby.stkskin | 4 ++++ data/skins/forest/generic.png | Bin 0 -> 23440 bytes data/skins/ocean/generic.png | Bin 0 -> 22771 bytes data/skins/peach/generic.png | Bin 0 -> 25366 bytes data/skins/ruby/generic.png | Bin 0 -> 20300 bytes src/guiengine/message_queue.cpp | 2 ++ src/guiengine/message_queue.hpp | 2 +- src/replay/replay_recorder.cpp | 15 +++++++++------ 11 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 data/skins/forest/generic.png create mode 100644 data/skins/ocean/generic.png create mode 100644 data/skins/peach/generic.png create mode 100644 data/skins/ruby/generic.png diff --git a/data/skins/Forest.stkskin b/data/skins/Forest.stkskin index e21df8788..cd7514f44 100644 --- a/data/skins/Forest.stkskin +++ b/data/skins/Forest.stkskin @@ -81,6 +81,10 @@ when the border that intersect at this corner are enabled. left_border="128" right_border="13" top_border="13" bottom_border="13" preserve_h_aspect_ratios="true" hborder_out_portion="0" vborder_out_portion="0"/> + + diff --git a/data/skins/Ocean.stkskin b/data/skins/Ocean.stkskin index e8b75b13a..b251e5b37 100644 --- a/data/skins/Ocean.stkskin +++ b/data/skins/Ocean.stkskin @@ -80,6 +80,10 @@ when the border that intersect at this corner are enabled. left_border="128" right_border="13" top_border="13" bottom_border="13" preserve_h_aspect_ratios="true" hborder_out_portion="0" vborder_out_portion="0"/> + + diff --git a/data/skins/Peach.stkskin b/data/skins/Peach.stkskin index 0bed84a66..4b67f0f5b 100644 --- a/data/skins/Peach.stkskin +++ b/data/skins/Peach.stkskin @@ -80,6 +80,10 @@ when the border that intersect at this corner are enabled. left_border="128" right_border="13" top_border="13" bottom_border="13" preserve_h_aspect_ratios="true" hborder_out_portion="0" vborder_out_portion="0"/> + + diff --git a/data/skins/Ruby.stkskin b/data/skins/Ruby.stkskin index be8e6d327..76407f632 100644 --- a/data/skins/Ruby.stkskin +++ b/data/skins/Ruby.stkskin @@ -81,6 +81,10 @@ when the border that intersect at this corner are enabled. left_border="128" right_border="13" top_border="13" bottom_border="13" preserve_h_aspect_ratios="true" hborder_out_portion="0" vborder_out_portion="0"/> + + diff --git a/data/skins/forest/generic.png b/data/skins/forest/generic.png new file mode 100644 index 0000000000000000000000000000000000000000..5352e53f0bda545cddde5d7ca3a2dc08f48266dd GIT binary patch literal 23440 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 SV;kU!Atr_v295fz@&5xh_+C8# literal 0 HcmV?d00001 diff --git a/data/skins/peach/generic.png b/data/skins/peach/generic.png new file mode 100644 index 0000000000000000000000000000000000000000..57c34ec290813760139c3d5ee8a82cf763c6ae75 GIT binary patch literal 25366 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 literal 0 HcmV?d00001 diff --git a/data/skins/ruby/generic.png b/data/skins/ruby/generic.png new file mode 100644 index 0000000000000000000000000000000000000000..a4bcee9c6e955e7989c052ccae46d8495d2f82ce GIT binary patch literal 20300 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%0A3u#Nbj^t$^!e8}9%f+(@aT&IKR-&nx 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 U(4YIH3b}U(1DvrgLE9nh|1dY7tpET3 literal 0 HcmV?d00001 diff --git a/src/guiengine/message_queue.cpp b/src/guiengine/message_queue.cpp index ebe684fda..548c5a86c 100644 --- a/src/guiengine/message_queue.cpp +++ b/src/guiengine/message_queue.cpp @@ -54,6 +54,8 @@ public: m_render_type = "achievement-message::neutral"; else if (mt==MessageQueue::MT_ERROR) m_render_type = "error-message::neutral"; + else if (mt==MessageQueue::MT_GENERIC) + m_render_type = "generic-message::neutral"; else m_render_type = "friend-message::neutral"; } // Message diff --git a/src/guiengine/message_queue.hpp b/src/guiengine/message_queue.hpp index 3abeb5874..e6f027522 100644 --- a/src/guiengine/message_queue.hpp +++ b/src/guiengine/message_queue.hpp @@ -34,7 +34,7 @@ namespace MessageQueue * different look. This type is used to sort the messages, so it is * important that messages that need to be shown as early as possible * will be listed last (i.e. have highest priority). */ - enum MessageType { MT_FRIEND, MT_ACHIEVEMENT, MT_ERROR}; + enum MessageType { MT_FRIEND, MT_ACHIEVEMENT, MT_ERROR, MT_GENERIC}; void add(MessageType mt, const core::stringw &message); void updatePosition(); diff --git a/src/replay/replay_recorder.cpp b/src/replay/replay_recorder.cpp index 4e993c179..9718930cb 100644 --- a/src/replay/replay_recorder.cpp +++ b/src/replay/replay_recorder.cpp @@ -20,6 +20,7 @@ #include "config/stk_config.hpp" #include "io/file_manager.hpp" +#include "guiengine/message_queue.hpp" #include "karts/ghost_kart.hpp" #include "modes/world.hpp" #include "physics/btKart.hpp" @@ -190,23 +191,25 @@ void ReplayRecorder::save() { if (m_incorrect_replay || !m_complete_replay) { - Log::warn("ReplayRecorder", "Incomplete replay file will not be saved."); + MessageQueue::add(MessageQueue::MT_ERROR, + _("Incomplete replay file will not be saved.")); return; } #ifdef DEBUG - printf("%d frames, %d removed because of frequency compression\n", - m_count, m_count_skipped_time); + Log::debug("ReplayRecorder", "%d frames, %d removed because of" + "frequency compression", m_count, m_count_skipped_time); #endif FILE *fd = openReplayFile(/*writeable*/true); if (!fd) { - Log::error("ReplayRecorder", "Can't open '%s' for writing - can't save replay data.", - getReplayFilename().c_str()); + Log::error("ReplayRecorder", "Can't open '%s' for writing - " + "can't save replay data.", getReplayFilename().c_str()); return; } - Log::info("ReplayRecorder", "Replay saved in '%s'.\n", getReplayFilename().c_str()); + core::stringw msg = _("Replay saved in \"%s\".", getReplayFilename().c_str()); + MessageQueue::add(MessageQueue::MT_GENERIC, msg); fprintf(fd, "reverse: %d\n", (int)race_manager->getReverseTrack()); World *world = World::getWorld(); From a6c4a72e2c0479eb8436c827de085fb6a1ef917f Mon Sep 17 00:00:00 2001 From: Benau Date: Fri, 12 Feb 2016 01:18:26 +0800 Subject: [PATCH 019/324] Make lap counting works for ghost kart As no m_terrain_info->update in ghost kart update --- src/modes/linear_world.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modes/linear_world.cpp b/src/modes/linear_world.cpp index d6fc49185..db9045751 100644 --- a/src/modes/linear_world.cpp +++ b/src/modes/linear_world.cpp @@ -178,9 +178,10 @@ void LinearWorld::update(float dt) // in the position of the kart (e.g. while falling the kart // might get too close to another part of the track, shortly // jump to position one, then on reset fall back to last) - if (!kart_info.getTrackSector()->isOnRoad() && + if ((!kart_info.getTrackSector()->isOnRoad() && (!kart->getMaterial() || - kart->getMaterial()->isDriveReset()) ) + kart->getMaterial()->isDriveReset())) && + !kart->isGhostKart()) continue; kart_info.getTrackSector()->update(kart->getFrontXYZ()); kart_info.m_overall_distance = kart_info.m_race_lap From 5cd27f8f99e1762eecfa3b9296e48ae113f7ad73 Mon Sep 17 00:00:00 2001 From: Benau Date: Fri, 12 Feb 2016 10:01:54 +0800 Subject: [PATCH 020/324] Seperate directory for replay files It allows replay GUI to load them easier --- src/io/file_manager.cpp | 35 +++++++++++++++++++++++++++++++++++ src/io/file_manager.hpp | 5 +++++ src/replay/replay_base.cpp | 9 ++++----- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/io/file_manager.cpp b/src/io/file_manager.cpp index b173f94b2..748fd38d4 100644 --- a/src/io/file_manager.cpp +++ b/src/io/file_manager.cpp @@ -213,6 +213,7 @@ FileManager::FileManager() checkAndCreateConfigDir(); checkAndCreateAddonsDir(); checkAndCreateScreenshotDir(); + checkAndCreateReplayDir(); checkAndCreateCachedTexturesDir(); checkAndCreateGPDir(); @@ -641,6 +642,14 @@ std::string FileManager::getScreenshotDir() const return m_screenshot_dir; } // getScreenshotDir +//----------------------------------------------------------------------------- +/** Returns the directory in which replay file should be stored. + */ +std::string FileManager::getReplayDir() const +{ + return m_replay_dir; +} // getReplayDir + //----------------------------------------------------------------------------- /** Returns the directory in which resized textures should be cached. */ @@ -910,6 +919,32 @@ void FileManager::checkAndCreateScreenshotDir() } // checkAndCreateScreenshotDir +// ---------------------------------------------------------------------------- +/** Creates the directories for replay recorded. This will set m_replay_dir + * with the appropriate path. + */ +void FileManager::checkAndCreateReplayDir() +{ +#if defined(WIN32) || defined(__CYGWIN__) + m_replay_dir = m_user_config_dir + "replay/"; +#elif defined(__APPLE__) + m_replay_dir = getenv("HOME"); + m_replay_dir += "/Library/Application Support/SuperTuxKart/replay/"; +#else + m_replay_dir = checkAndCreateLinuxDir("XDG_DATA_HOME", "supertuxkart", + ".local/share", ".supertuxkart"); + m_replay_dir += "replay/"; +#endif + + if(!checkAndCreateDirectory(m_replay_dir)) + { + Log::error("FileManager", "Can not create replay directory '%s', " + "falling back to '.'.", m_replay_dir.c_str()); + m_replay_dir = "."; + } + +} // checkAndCreateReplayDir + // ---------------------------------------------------------------------------- /** Creates the directories for cached textures. This will set * m_cached_textures_dir with the appropriate path. diff --git a/src/io/file_manager.hpp b/src/io/file_manager.hpp index 003507f9c..41e9046f0 100644 --- a/src/io/file_manager.hpp +++ b/src/io/file_manager.hpp @@ -75,6 +75,9 @@ private: /** Directory to store screenshots in. */ std::string m_screenshot_dir; + /** Directory to store replays in. */ + std::string m_replay_dir; + /** Directory where resized textures are cached. */ std::string m_cached_textures_dir; @@ -97,6 +100,7 @@ private: bool isDirectory(const std::string &path) const; void checkAndCreateAddonsDir(); void checkAndCreateScreenshotDir(); + void checkAndCreateReplayDir(); void checkAndCreateCachedTexturesDir(); void checkAndCreateGPDir(); void discoverPaths(); @@ -118,6 +122,7 @@ public: XMLNode *createXMLTreeFromString(const std::string & content); std::string getScreenshotDir() const; + std::string getReplayDir() const; std::string getCachedTexturesDir() const; std::string getGPDir() const; std::string getTextureCacheLocation(const std::string& filename); diff --git a/src/replay/replay_base.cpp b/src/replay/replay_base.cpp index 6efda19c5..5695c48ea 100644 --- a/src/replay/replay_base.cpp +++ b/src/replay/replay_base.cpp @@ -33,13 +33,12 @@ ReplayBase::ReplayBase() */ FILE* ReplayBase::openReplayFile(bool writeable) { - m_filename = file_manager->getUserConfigFile( - race_manager->getTrackName()+".replay"); + m_filename = file_manager->getReplayDir() + + race_manager->getTrackName() + ".replay"; FILE *fd = fopen(m_filename.c_str(), writeable ? "w" : "r"); - if(!fd) + if (!fd) { - m_filename = race_manager->getTrackName()+".replay"; - fd = fopen(m_filename.c_str(), writeable ? "w" : "r"); + return NULL; } return fd; From 8a121ed32ba1b63045841221339b6f5f26b351ca Mon Sep 17 00:00:00 2001 From: Benau Date: Sat, 13 Feb 2016 01:34:00 +0800 Subject: [PATCH 021/324] Add Ghost replay GUI --- data/gui/ghost_replay_info_dialog.stkgui | 30 +++ data/gui/ghost_replay_selection.stkgui | 15 ++ data/gui/tracks.stkgui | 4 +- sources.cmake | 2 +- src/main.cpp | 7 +- src/modes/world.cpp | 7 +- src/race/race_manager.cpp | 6 +- src/race/race_manager.hpp | 12 ++ src/replay/replay_base.cpp | 9 +- src/replay/replay_base.hpp | 30 ++- src/replay/replay_play.cpp | 178 ++++++++---------- src/replay/replay_play.hpp | 38 +++- src/replay/replay_recorder.cpp | 4 +- src/replay/replay_recorder.hpp | 5 + .../dialogs/ghost_replay_info_dialog.cpp | 109 +++++++++++ .../dialogs/ghost_replay_info_dialog.hpp | 55 ++++++ src/states_screens/ghost_replay_selection.cpp | 130 +++++++++++++ src/states_screens/ghost_replay_selection.hpp | 72 +++++++ src/states_screens/tracks_screen.cpp | 12 +- 19 files changed, 583 insertions(+), 142 deletions(-) create mode 100644 data/gui/ghost_replay_info_dialog.stkgui create mode 100644 data/gui/ghost_replay_selection.stkgui create mode 100644 src/states_screens/dialogs/ghost_replay_info_dialog.cpp create mode 100644 src/states_screens/dialogs/ghost_replay_info_dialog.hpp create mode 100644 src/states_screens/ghost_replay_selection.cpp create mode 100644 src/states_screens/ghost_replay_selection.hpp diff --git a/data/gui/ghost_replay_info_dialog.stkgui b/data/gui/ghost_replay_info_dialog.stkgui new file mode 100644 index 000000000..b4d2de9b5 --- /dev/null +++ b/data/gui/ghost_replay_info_dialog.stkgui @@ -0,0 +1,30 @@ + + +
+
+
+ + +
+ +
+
+
+ +
+ + + + + +
+
+
diff --git a/data/gui/ghost_replay_selection.stkgui b/data/gui/ghost_replay_selection.stkgui new file mode 100644 index 000000000..771910e3e --- /dev/null +++ b/data/gui/ghost_replay_selection.stkgui @@ -0,0 +1,15 @@ + + +
+ +
+ +
+ +
+ + + + +
+
diff --git a/data/gui/tracks.stkgui b/data/gui/tracks.stkgui index aef3313d6..37720184e 100644 --- a/data/gui/tracks.stkgui +++ b/data/gui/tracks.stkgui @@ -25,6 +25,8 @@ - + + +