Numerous improvements related to ghost replays (#3244)
* Update GUI files for replay improvements * Updated replay capabilities * Improve timer formatting possibilities Mainly, the ability to choose to display or not minutes, and to choose how many digits are shown after seconds (from 0 : second as smallest timestep - to 3 : ms as smallest timestep) Also displays "mm:ss.ms" rather than "mm:ss:ms". * Some new list widget possibilities Like the ability to update the header while the list is displayed (useful to add or remove columns) * Update ghost kart for the new replay data * Also update the ghost controller * Live differences with ghost replays in linear worlds * Replay-related UI changes Big changes to the replay selection screen, and small change to the race UI (add the live timer in ghost races) and the race result UI (add the option to directly race against a newly saved ghost). * Improves the replay action modal dialog * Fix time-to-ticks regression * Several requested improvements * Improved ghost icon Also updates the license * GUI changes and improvements to accomodate multi-mode support * Additional recorded data * More functions to get the current race state * Update replay variables to match what is used * Updated replay-related config values * Add ghost kart support to easter egg hunt mode * Transparent attachments for ghost karts * Use new stored data (color, item type) Also : - Interpolate speed for smoother display in watch-only mode - Coding style improvements * Fix coding style issues and add UI support for modes * Fix coding style issues & support for easter egg mode * Remove leftover prints * Use getDifficultyName to remove hardcoded values * Fix attach_ticks and coding style fixes * Make the position of the timers fully relative Otherwise, they would tend to touch each other in some resolutions Also add a comment about the 59.9995f * Make the list filling code clearer Also use getDifficultyName Most of the line changes shown by git correspond to moving around some bits or adjusting indentation. * Remove a TODO as requested * Fix dialog being dismissed too soon * Remove a fixme * Small clean up * Fix logging * Partial #3249 fix
This commit is contained in:
parent
0b79d9c1d1
commit
7f84dd39a6
@ -38,6 +38,8 @@ power.png by Auria, based on https://openclipart.org/detail/193925/check-engine
|
||||
|
||||
crown.png by glitch, from https://openclipart.org/detail/210257/misc-game-crown, released under public domain
|
||||
|
||||
ghost_plus.png by Alayan, based on https://openclipart.org/detail/17847/cartoon-ghost by lemmling, released under CC-O
|
||||
|
||||
====
|
||||
|
||||
Glass Skin by Auria, under CC-BY-SA 3+
|
||||
|
BIN
data/gui/ghost_plus.png
Normal file
BIN
data/gui/ghost_plus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -1,15 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<stkgui>
|
||||
<div x="5%" y="5%" width="90%" height="90%" layout="vertical-row">
|
||||
<div x="5%" y="0%" width="90%" proportion="6" layout="horizontal-row">
|
||||
<div width="40%" height="100%" layout="vertical-row">
|
||||
<icon id="icon" align="center" width="100%" icon="gui/loading.png" />
|
||||
<div y="2%" width="100%" height="96%" layout="vertical-row">
|
||||
<div width="100%" height="50%" proportion="6" layout="horizontal-row">
|
||||
<div width="25%" height="100%" layout="vertical-row">
|
||||
<icon-button proportion="1" width="100%" height="100%" id="track_screenshot" custom_ratio="1.33333"/>
|
||||
</div>
|
||||
<div width="60%" height="50%" layout="vertical-row">
|
||||
<label id="name" width="100%" text_align="left"/>
|
||||
<div width="75%" height="100%" layout="vertical-row">
|
||||
<div width="100%" height="25%" layout="vertical-row" >
|
||||
<label id="name" width="100%" text_align="center"/>
|
||||
</div>
|
||||
<!-- This is filled in programmatically -->
|
||||
<box width="98%" height="75%" align="center" layout="vertical-row" padding="1">
|
||||
<list id="current_replay_info" x="0" y="0" width="100%" height="100%"/>
|
||||
</box>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div width="90%" align="center" layout="vertical-row" height="fit">
|
||||
<div width="100%" height="fit" layout="horizontal-row" >
|
||||
<checkbox width="fit" id="record-race" I18N="Ghost replay info action" text_align="left"/>
|
||||
@ -21,13 +29,21 @@
|
||||
<spacer width="10"/>
|
||||
<label proportion="1" id="watch-only-text" height="100%" text_align="left" I18N="Ghost replay info action" text="Watch replay only"/>
|
||||
</div>
|
||||
<div width="100%" height="fit" layout="horizontal-row" >
|
||||
<checkbox width="fit" id="compare-ghost" I18N="Ghost replay info action" text_align="left"/>
|
||||
<spacer width="10"/>
|
||||
<label proportion="1" id="compare-ghost-text" height="100%" text_align="left" I18N="Ghost replay info action" text="Compare to another ghost"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div width="80%" proportion="5" align="center">
|
||||
<div width="90%" proportion="5" align="center">
|
||||
<buttonbar id="actions" x="0" y="0" height="100%" width="100%" align="center">
|
||||
<icon-button id="start" width="128" height="128"
|
||||
icon="gui/green_check.png"
|
||||
I18N="Ghost replay info screen action" text="Start Race" />
|
||||
<icon-button id="add-ghost-to-compare" width="128" height="128"
|
||||
icon="gui/ghost_plus.png"
|
||||
I18N="Ghost replay info screen action" text="Compare ghost" />
|
||||
<icon-button id="remove" width="128" height="128"
|
||||
icon="gui/remove.png"
|
||||
I18N="Ghost replay info action" text="Remove" />
|
||||
|
@ -8,19 +8,50 @@
|
||||
<icon-button id="reload" height="90%" icon="gui/restart.png"/>
|
||||
</div>
|
||||
|
||||
<!-- This is filled in programmatically -->
|
||||
<box proportion="1" width="98%" align="center" layout="vertical-row" padding="6">
|
||||
<list id="replay_list" x="0" y="0" width="100%" height="100%"/>
|
||||
</box>
|
||||
|
||||
<div width="99%" align="center" layout="vertical-row" height="fit">
|
||||
<div width="100%" height="fit" layout="horizontal-row" >
|
||||
<checkbox width="fit" id="replay_difficulty_toggle" text_align="left"/>
|
||||
<spacer width="10"/>
|
||||
<label proportion="1" height="100%" text_align="left" I18N="In the ghost replay selection screen" text="Only show replays matching the current difficulty"/>
|
||||
<tabs id="race_mode" height="6%" max_height="110" x="2%" width="98%" align="center">
|
||||
<icon-button id="tab_time_trial" width="128" height="128" icon="gui/mode_tt.png"
|
||||
I18N="In the ghost replay selection screen" text="Time trial"/>
|
||||
<icon-button id="tab_egg_hunt" width="128" height="128" icon="gui/mode_easter.png"
|
||||
I18N="In the ghost replay selection screen" text="Egg hunt"/>
|
||||
</tabs>
|
||||
|
||||
<spacer width="100%" height="1.5%" />
|
||||
|
||||
<div width="99%" align="center" layout="horizontal-row" height="fit">
|
||||
<div proportion="1" height="fit" layout="horizontal-row" >
|
||||
<checkbox width="fit" id="best_times_toggle" text_align="left"/>
|
||||
<spacer width="2%" height="fit"/>
|
||||
<label height="100%" text_align="left" I18N="In the ghost replay selection screen" text="Only show the best times"/>
|
||||
</div>
|
||||
<div proportion="1" height="fit" layout="horizontal-row" >
|
||||
<checkbox width="fit" id="compare_toggle" text_align="left"/>
|
||||
<spacer width="2%" height="fit"/>
|
||||
<label height="100%" id="compare-toggle-text" text_align="left" I18N="In the ghost replay selection screen" text="Compare replay"/>
|
||||
</div>
|
||||
</div>
|
||||
<spacer width="100%" height="1%" />
|
||||
<button x="1%" id="record-ghost" I18N="In the ghost replay selection screen" text="Record ghost replay"/>
|
||||
|
||||
<div width="99%" align="center" layout="horizontal-row" height="fit">
|
||||
<div proportion="2" height="fit" layout="horizontal-row" >
|
||||
<checkbox width="fit" id="replay_difficulty_toggle" text_align="left"/>
|
||||
<spacer width="1%" height="fit"/>
|
||||
<label height="100%" text_align="left" I18N="In the ghost replay selection screen" text="Only show replays matching the current difficulty"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div width="99%" align="center" layout="horizontal-row" height="fit">
|
||||
<div proportion="1" height="fit" layout="horizontal-row" >
|
||||
<checkbox width="fit" id="replay_version_toggle" text_align="left"/>
|
||||
<spacer width="1%" height="fit" />
|
||||
<label height="100%" text_align="left" I18N="In the ghost replay selection screen" text="Only show replays matching the current version"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button x="1%" id="record-ghost" I18N="In the ghost replay selection screen" text="Record a ghost replay"/>
|
||||
|
||||
</div>
|
||||
</stkgui>
|
||||
|
@ -113,14 +113,18 @@
|
||||
|
||||
<!-- Replay related values, mostly concerned with saving less data
|
||||
and using interpolation instead.
|
||||
max-time: Maximum race time that can be saved in a replay/history file.
|
||||
delta-t Minumum time between saving consecutive transform events.
|
||||
delta-pos If the interpolated position is within this delta, a
|
||||
transform event is not generated.
|
||||
delta-angle If the interpolated angle is within this delta,
|
||||
a transform event is not generated. -->
|
||||
<replay max-time="600" delta-t="0.05" delta-pos="0.1"
|
||||
delta-angle="0.5" />
|
||||
max-frames: Maximum number of transform events that can be saved
|
||||
in a replay/history file. With normal play, 900 are
|
||||
enough to store at least one minute, usually more.
|
||||
delta-t Maximum time between saving consecutive transform events.
|
||||
The recording will do more transform events when some kart data
|
||||
changes significantly.
|
||||
delta-speed If the speed difference exceeds this delta, a
|
||||
new transform event is generated before maximum time.
|
||||
delta-steering If the steering angle difference exceeds this delta,
|
||||
new transform event is generated before maximum time. -->
|
||||
<replay max-frames="12000" delta-t="0.200" delta-speed="0.6"
|
||||
delta-steering="0.35" />
|
||||
|
||||
<!-- Determines the minimap related values.
|
||||
size: The size of the minimap (scaled afterwards) 480 = full screen height)
|
||||
|
@ -150,9 +150,9 @@ void STKConfig::load(const std::string &filename)
|
||||
CHECK_NEG(m_leader_time_per_kart, "leader time-per-kart" );
|
||||
CHECK_NEG(m_penalty_ticks, "penalty-time" );
|
||||
CHECK_NEG(m_max_display_news, "max-display-news" );
|
||||
CHECK_NEG(m_replay_max_time, "replay max-time" );
|
||||
CHECK_NEG(m_replay_delta_angle, "replay delta-angle" );
|
||||
CHECK_NEG(m_replay_delta_pos2, "replay delta-position" );
|
||||
CHECK_NEG(m_replay_max_frames, "replay max-frames" );
|
||||
CHECK_NEG(m_replay_delta_steering, "replay delta-steering" );
|
||||
CHECK_NEG(m_replay_delta_speed, "replay delta-speed " );
|
||||
CHECK_NEG(m_replay_dt, "replay delta-t" );
|
||||
CHECK_NEG(m_minimap_size, "minimap size" );
|
||||
CHECK_NEG(m_minimap_ai_icon, "minimap ai_icon" );
|
||||
@ -164,7 +164,6 @@ void STKConfig::load(const std::string &filename)
|
||||
CHECK_NEG(m_default_moveable_friction, "physics default-moveable-friction");
|
||||
|
||||
// Square distance to make distance checks cheaper (no sqrt)
|
||||
m_replay_delta_pos2 *= m_replay_delta_pos2;
|
||||
m_default_kart_properties->checkAllSet(filename);
|
||||
} // load
|
||||
|
||||
@ -195,9 +194,9 @@ void STKConfig::init_defaults()
|
||||
m_min_server_version = -100;
|
||||
m_max_server_version = -100;
|
||||
m_max_display_news = -100;
|
||||
m_replay_max_time = -100;
|
||||
m_replay_delta_angle = -100;
|
||||
m_replay_delta_pos2 = -100;
|
||||
m_replay_max_frames = -100;
|
||||
m_replay_delta_steering = -100;
|
||||
m_replay_delta_speed = -100;
|
||||
m_replay_dt = -100;
|
||||
m_minimap_size = -100;
|
||||
m_minimap_ai_icon = -100;
|
||||
@ -405,10 +404,10 @@ void STKConfig::getAllData(const XMLNode * root)
|
||||
|
||||
if(const XMLNode *replay_node = root->getNode("replay"))
|
||||
{
|
||||
replay_node->get("delta-angle", &m_replay_delta_angle);
|
||||
replay_node->get("delta-pos", &m_replay_delta_pos2 );
|
||||
replay_node->get("delta-t", &m_replay_dt );
|
||||
replay_node->get("max-time", &m_replay_max_time );
|
||||
replay_node->get("delta-steering", &m_replay_delta_steering);
|
||||
replay_node->get("delta-speed", &m_replay_delta_speed );
|
||||
replay_node->get("delta-t", &m_replay_dt );
|
||||
replay_node->get("max-frames", &m_replay_max_frames );
|
||||
|
||||
}
|
||||
|
||||
|
@ -149,19 +149,19 @@ public:
|
||||
/** Filename of the title music to play.*/
|
||||
MusicInformation *m_title_music;
|
||||
|
||||
/** Maximum time of a replay. */
|
||||
int m_replay_max_time;
|
||||
/** Maximum number of transform events of a replay. */
|
||||
int m_replay_max_frames;
|
||||
|
||||
/** Minimum time between consecutive saved tranform events. */
|
||||
/** Maximum time between consecutive saved tranform events. */
|
||||
float m_replay_dt;
|
||||
|
||||
/** Maximum difference between interpolated and actual position. If the
|
||||
* difference is larger than this, a new event is generated. */
|
||||
float m_replay_delta_pos2;
|
||||
/** If the speed difference with the last transform event
|
||||
* is larger than this, a new event is generated. */
|
||||
float m_replay_delta_speed;
|
||||
|
||||
/** A heading difference of more than that will trigger a new event to
|
||||
/** A steering difference of more than that will trigger a new event to
|
||||
* be generated. */
|
||||
float m_replay_delta_angle;
|
||||
float m_replay_delta_steering;
|
||||
|
||||
/** The minimap size */
|
||||
float m_minimap_size;
|
||||
|
@ -44,6 +44,7 @@ ListWidget::ListWidget() : Widget(WTYPE_LIST)
|
||||
m_sort_default = true;
|
||||
m_sort_col = 0;
|
||||
m_sortable = true;
|
||||
m_header_created = false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@ -121,6 +122,18 @@ void ListWidget::add()
|
||||
m_element = list_box;
|
||||
m_element->setTabOrder( list_box->getID() );
|
||||
|
||||
createHeader();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
void ListWidget::createHeader()
|
||||
{
|
||||
if (m_header_created)
|
||||
return;
|
||||
|
||||
const int header_height = GUIEngine::getFontHeight() + 15;
|
||||
|
||||
if (m_header.size() > 0)
|
||||
{
|
||||
//const int col_size = m_w / m_header.size();
|
||||
@ -166,7 +179,8 @@ void ListWidget::add()
|
||||
|
||||
m_check_inside_me = true;
|
||||
}
|
||||
}
|
||||
m_header_created = true;
|
||||
} // createHeader
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@ -179,7 +193,22 @@ void ListWidget::clear()
|
||||
assert(list != NULL);
|
||||
|
||||
list->clear();
|
||||
}
|
||||
} //clear
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
void ListWidget::clearColumns()
|
||||
{
|
||||
m_header.clear();
|
||||
for (unsigned int n=0; n<m_header_elements.size(); n++)
|
||||
{
|
||||
m_header_elements[n].elementRemoved();
|
||||
m_children.remove( m_header_elements.get(n) );
|
||||
}
|
||||
|
||||
m_header_elements.clearAndDeleteAll();
|
||||
m_header_created = false;
|
||||
} //clearColumns
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
|
@ -89,6 +89,8 @@ namespace GUIEngine
|
||||
|
||||
bool m_sortable;
|
||||
|
||||
bool m_header_created;
|
||||
|
||||
public:
|
||||
typedef irr::gui::CGUISTKListBox::ListItem ListItem;
|
||||
typedef ListItem::ListCell ListCell;
|
||||
@ -135,12 +137,24 @@ namespace GUIEngine
|
||||
|
||||
void addItem( const std::string& internal_name,
|
||||
const std::vector<ListCell>& contents);
|
||||
|
||||
/**
|
||||
* \brief create a header based on m_header
|
||||
* \pre may only be called after the widget has been added to the screen with add()
|
||||
*/
|
||||
void createHeader();
|
||||
|
||||
/**
|
||||
* \brief erases all items in the list
|
||||
* \brief erases all items in the list, don't clear header
|
||||
* \pre may only be called after the widget has been added to the screen with add()
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/**
|
||||
* \brief clear the header
|
||||
* \pre may only be called after the widget has been added to the screen with add()
|
||||
*/
|
||||
void clearColumns();
|
||||
|
||||
/**
|
||||
* \return the number of items in the list
|
||||
@ -239,12 +253,10 @@ namespace GUIEngine
|
||||
m_listener = listener;
|
||||
}
|
||||
|
||||
/** To be called before Widget::add(); columns are persistent across multiple add/remove cycles
|
||||
/** Columns are persistent across multiple "clear" add/remove cycles. clearColumns clear them immediately.
|
||||
* \param proportion A column with proportion 2 will be twice as large as a column with proportion 1
|
||||
*/
|
||||
void addColumn(irr::core::stringw col, int proportion=1) { m_header.push_back( Column(col, proportion) ); }
|
||||
|
||||
void clearColumns() { m_header.clear(); }
|
||||
|
||||
void setSortable(bool sortable) { m_sortable = sortable; }
|
||||
};
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include "config/user_config.hpp"
|
||||
#include "graphics/explosion.hpp"
|
||||
#include "graphics/irr_driver.hpp"
|
||||
#include "graphics/render_info.hpp"
|
||||
#include "items/attachment_manager.hpp"
|
||||
#include "items/item_manager.hpp"
|
||||
#include "items/projectile_manager.hpp"
|
||||
@ -60,8 +61,13 @@ Attachment::Attachment(AbstractKart* kart)
|
||||
|
||||
// If we attach a NULL mesh, we get a NULL scene node back. So we
|
||||
// have to attach some kind of mesh, but make it invisible.
|
||||
m_node = irr_driver->addAnimatedMesh(
|
||||
attachment_manager->getMesh(Attachment::ATTACH_BOMB), "bomb");
|
||||
if (kart->isGhostKart())
|
||||
m_node = irr_driver->addAnimatedMesh(
|
||||
attachment_manager->getMesh(Attachment::ATTACH_BOMB), "bomb",
|
||||
NULL, std::make_shared<RenderInfo>(0.0f, true));
|
||||
else
|
||||
m_node = irr_driver->addAnimatedMesh(
|
||||
attachment_manager->getMesh(Attachment::ATTACH_BOMB), "bomb");
|
||||
#ifdef DEBUG
|
||||
std::string debug_name = kart->getIdent()+" (attachment)";
|
||||
m_node->setName(debug_name.c_str());
|
||||
|
@ -152,3 +152,11 @@ void AbstractKart::kartIsInRestNow()
|
||||
// after all karts are reset
|
||||
setTrans(getBody()->getWorldTransform());
|
||||
} // kartIsInRest
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
/** Returns the time at which the kart was at a given distance.
|
||||
* Returns -1.0f if none */
|
||||
float AbstractKart::getTimeForDistance(float distance)
|
||||
{
|
||||
return -1.0f;
|
||||
} // getTimeForDistance
|
||||
|
@ -186,6 +186,11 @@ public:
|
||||
* overwriting this function is possible, but this implementation must
|
||||
* be called. */
|
||||
virtual void kartIsInRestNow();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns the time at which the kart was at a given distance.
|
||||
* Returns -1.0f if none */
|
||||
virtual float getTimeForDistance(float distance);
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns true if this kart has no wheels. */
|
||||
bool isWheeless() const;
|
||||
@ -323,8 +328,8 @@ public:
|
||||
* \param fade_out_time How long the maximum speed will fade out linearly.
|
||||
*/
|
||||
virtual void instantSpeedIncrease(unsigned int category, float add_max_speed,
|
||||
float speed_boost, float engine_force,
|
||||
int duration, int fade_out_time) = 0;
|
||||
float speed_boost, float engine_force,
|
||||
int duration, int fade_out_time) = 0;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** Defines a slowdown, which is in fraction of top speed.
|
||||
@ -411,6 +416,9 @@ public:
|
||||
/** Returns the last used powerup type. */
|
||||
virtual PowerupManager::PowerupType getLastUsedPowerup() = 0;
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns the number of powerups. */
|
||||
virtual int getNumPowerup() const = 0;
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns a points to this kart's graphical effects. */
|
||||
virtual KartGFX* getKartGFX() = 0;
|
||||
// ------------------------------------------------------------------------
|
||||
|
@ -87,5 +87,7 @@ bool GhostController::action(PlayerAction action, int value, bool dry_run)
|
||||
// Watching replay use only
|
||||
if (action == PA_LOOK_BACK)
|
||||
m_controls->setLookBack(value!=0);
|
||||
else if (action == PA_PAUSE_RACE)
|
||||
if (value != 0) StateManager::get()->escapePressed();
|
||||
return true;
|
||||
} // action
|
||||
|
@ -79,6 +79,14 @@ public:
|
||||
// ------------------------------------------------------------------------
|
||||
unsigned int getCurrentReplayIndex() const
|
||||
{ return m_current_index; }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
float getTimeAtIndex(unsigned int index) const
|
||||
{
|
||||
assert(index < m_all_times.size());
|
||||
return m_all_times[index];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** Return the display name; if not set, use default display name (kart name) */
|
||||
core::stringw getName() const OVERRIDE
|
||||
|
@ -16,21 +16,26 @@
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
|
||||
#include "items/attachment.hpp"
|
||||
#include "items/powerup.hpp"
|
||||
#include "karts/ghost_kart.hpp"
|
||||
#include "karts/controller/ghost_controller.hpp"
|
||||
#include "karts/kart_gfx.hpp"
|
||||
#include "karts/kart_model.hpp"
|
||||
#include "graphics/render_info.hpp"
|
||||
#include "modes/easter_egg_hunt.hpp"
|
||||
#include "modes/linear_world.hpp"
|
||||
#include "modes/world.hpp"
|
||||
#include "replay/replay_recorder.hpp"
|
||||
|
||||
#include "LinearMath/btQuaternion.h"
|
||||
|
||||
GhostKart::GhostKart(const std::string& ident, unsigned int world_kart_id,
|
||||
int position)
|
||||
int position, float color_hue)
|
||||
: Kart(ident, world_kart_id,
|
||||
position, btTransform(btQuaternion(0, 0, 0, 1)),
|
||||
PLAYER_DIFFICULTY_NORMAL,
|
||||
std::make_shared<RenderInfo>(0.0f, true/*transparent*/))
|
||||
std::make_shared<RenderInfo>(color_hue, true/*transparent*/))
|
||||
{
|
||||
} // GhostKart
|
||||
|
||||
@ -41,12 +46,14 @@ void GhostKart::reset()
|
||||
Kart::reset();
|
||||
// This will set the correct start position
|
||||
update(0);
|
||||
m_last_egg_idx = 0;
|
||||
} // reset
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
void GhostKart::addReplayEvent(float time,
|
||||
const btTransform &trans,
|
||||
const ReplayBase::PhysicInfo &pi,
|
||||
const ReplayBase::BonusInfo &bi,
|
||||
const ReplayBase::KartReplayEvent &kre)
|
||||
{
|
||||
GhostController* gc = dynamic_cast<GhostController*>(getController());
|
||||
@ -54,6 +61,7 @@ void GhostKart::addReplayEvent(float time,
|
||||
|
||||
m_all_transform.push_back(trans);
|
||||
m_all_physic_info.push_back(pi);
|
||||
m_all_bonus_info.push_back(bi);
|
||||
m_all_replay_events.push_back(kre);
|
||||
|
||||
// Use first frame of replay to calculate default suspension
|
||||
@ -130,10 +138,65 @@ void GhostKart::update(int ticks)
|
||||
m_all_physic_info[idx].m_steer, m_all_physic_info[idx].m_speed,
|
||||
/*lean*/0.0f, idx);
|
||||
|
||||
// Attachment management
|
||||
// Note that this doesn't get ticks value from replay file,
|
||||
// and can't get the exact ticks values when it depends from kart
|
||||
// properties. It also doesn't manage the case where a shield is
|
||||
// renewed. These inaccuracies are minor as it is used for
|
||||
// graphical effect only.
|
||||
|
||||
Attachment::AttachmentType attach_type =
|
||||
ReplayRecorder::codeToEnumAttach(m_all_bonus_info[idx].m_attachment);
|
||||
int attach_ticks = 0;
|
||||
if (attach_type == Attachment::ATTACH_BUBBLEGUM_SHIELD)
|
||||
attach_ticks = stk_config->time2Ticks(10);
|
||||
else if (attach_type == Attachment::ATTACH_BOMB)
|
||||
attach_ticks = stk_config->time2Ticks(30);
|
||||
// The replay history will take care of clearing,
|
||||
// just make sure it won't expire by itself
|
||||
else
|
||||
attach_ticks = stk_config->time2Ticks(300);
|
||||
|
||||
if ( attach_type == Attachment::ATTACH_NOTHING )
|
||||
m_attachment->clear();
|
||||
// Setting again reinitialize the graphical size of the attachment,
|
||||
// so do so only if the type change
|
||||
else if ( attach_type != m_attachment->getType())
|
||||
m_attachment->set(attach_type,attach_ticks);
|
||||
|
||||
// So that the attachment's model size is updated
|
||||
m_attachment->update(ticks);
|
||||
|
||||
// Nitro and zipper amount (shown in the GUI in watch-only mode)
|
||||
m_powerup->reset();
|
||||
|
||||
// Update item amount and type
|
||||
PowerupManager::PowerupType item_type =
|
||||
ReplayRecorder::codeToEnumItem(m_all_bonus_info[idx].m_item_type);
|
||||
m_powerup->set(item_type, m_all_bonus_info[idx].m_item_amount);
|
||||
|
||||
// Update special values in easter egg and battle modes
|
||||
if (race_manager->isEggHuntMode())
|
||||
{
|
||||
if (idx > m_last_egg_idx &&
|
||||
m_all_bonus_info[idx].m_special_value >
|
||||
m_all_bonus_info[m_last_egg_idx].m_special_value)
|
||||
{
|
||||
EasterEggHunt *world = dynamic_cast<EasterEggHunt*>(World::getWorld());
|
||||
assert(world);
|
||||
world->collectedEasterEggGhost(getWorldKartId());
|
||||
m_last_egg_idx = idx;
|
||||
}
|
||||
}
|
||||
|
||||
m_collected_energy = (1- rd)*m_all_bonus_info[idx ].m_nitro_amount
|
||||
+ rd *m_all_bonus_info[idx + 1].m_nitro_amount;
|
||||
|
||||
// Graphical effects for nitro, zipper and skidding
|
||||
getKartGFX()->setGFXFromReplay(m_all_replay_events[idx].m_nitro_usage,
|
||||
m_all_replay_events[idx].m_zipper_usage,
|
||||
m_all_replay_events[idx].m_skidding_state,
|
||||
m_all_replay_events[idx].m_red_skidding);
|
||||
m_all_replay_events[idx].m_zipper_usage,
|
||||
m_all_replay_events[idx].m_skidding_effect,
|
||||
m_all_replay_events[idx].m_red_skidding);
|
||||
getKartGFX()->update(dt);
|
||||
|
||||
Vec3 front(0, 0, getKartLength()*0.5f);
|
||||
@ -159,6 +222,92 @@ float GhostKart::getSpeed() const
|
||||
const GhostController* gc =
|
||||
dynamic_cast<const GhostController*>(getController());
|
||||
|
||||
unsigned int current_index = gc->getCurrentReplayIndex();
|
||||
const float rd = gc->getReplayDelta();
|
||||
|
||||
assert(gc->getCurrentReplayIndex() < m_all_physic_info.size());
|
||||
return m_all_physic_info[gc->getCurrentReplayIndex()].m_speed;
|
||||
|
||||
if (current_index == m_all_physic_info.size())
|
||||
return m_all_physic_info[current_index].m_speed;
|
||||
|
||||
return (1-rd)*m_all_physic_info[current_index ].m_speed
|
||||
+ rd *m_all_physic_info[current_index + 1].m_speed;
|
||||
} // getSpeed
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
/** Returns the time at which the kart was at a given distance.
|
||||
* Returns -1.0f if none */
|
||||
float GhostKart::getTimeForDistance(float distance)
|
||||
{
|
||||
const GhostController* gc =
|
||||
dynamic_cast<const GhostController*>(getController());
|
||||
|
||||
int current_index = gc->getCurrentReplayIndex();
|
||||
|
||||
// Second, get the current distance
|
||||
float current_distance = m_all_replay_events[current_index].m_distance;
|
||||
|
||||
// This determines in which direction we will search a matching frame
|
||||
bool search_forward = (current_distance < distance);
|
||||
|
||||
// These are used to compute the time
|
||||
// upper_frame is set to current index so that the timer still runs
|
||||
// after the ghost has finished the race
|
||||
int lower_frame_index = current_index-1;
|
||||
unsigned int upper_frame_index = current_index;
|
||||
float upper_ratio = 0.0f;
|
||||
|
||||
// Third, search frame by frame in the good direction
|
||||
// Binary search is not used because the kart might go backwards
|
||||
// at some point (accident, rescue, wrong interpolation later corrected)
|
||||
// The distances are stored in the replay file rather than being
|
||||
// dynamically computed. Version 3 replay files thus don't support
|
||||
// the live time difference, but this reduces tremendously the overhead
|
||||
while (1)
|
||||
{
|
||||
// If we have reached the end of the replay file without finding the
|
||||
// searched distance, break
|
||||
if (upper_frame_index >= m_all_replay_events.size() ||
|
||||
lower_frame_index < 0 )
|
||||
break;
|
||||
|
||||
// The target distance was reached between those two frames
|
||||
if (m_all_replay_events[lower_frame_index].m_distance <= distance &&
|
||||
m_all_replay_events[upper_frame_index].m_distance >= distance )
|
||||
{
|
||||
float lower_diff =
|
||||
distance - m_all_replay_events[lower_frame_index].m_distance;
|
||||
float upper_diff =
|
||||
m_all_replay_events[upper_frame_index].m_distance - distance;
|
||||
|
||||
if ((lower_diff + upper_diff) == 0)
|
||||
upper_ratio = 0.0f;
|
||||
else
|
||||
upper_ratio = lower_diff/(lower_diff+upper_diff);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (search_forward)
|
||||
{
|
||||
lower_frame_index++;
|
||||
upper_frame_index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
lower_frame_index--;
|
||||
upper_frame_index--;
|
||||
}
|
||||
}
|
||||
|
||||
float ghost_time;
|
||||
|
||||
if (upper_frame_index >= m_all_replay_events.size() ||
|
||||
lower_frame_index < 0 )
|
||||
ghost_time = -1.0f;
|
||||
else
|
||||
ghost_time = gc->getTimeAtIndex(lower_frame_index)*(1.0f-upper_ratio)
|
||||
+gc->getTimeAtIndex(upper_frame_index)*(upper_ratio);
|
||||
|
||||
return ghost_time;
|
||||
}
|
||||
|
@ -42,11 +42,15 @@ private:
|
||||
|
||||
std::vector<ReplayBase::PhysicInfo> m_all_physic_info;
|
||||
|
||||
std::vector<ReplayBase::BonusInfo> m_all_bonus_info;
|
||||
|
||||
std::vector<ReplayBase::KartReplayEvent> m_all_replay_events;
|
||||
|
||||
unsigned int m_last_egg_idx = 0;
|
||||
|
||||
public:
|
||||
GhostKart(const std::string& ident,
|
||||
unsigned int world_kart_id, int position);
|
||||
GhostKart(const std::string& ident, unsigned int world_kart_id,
|
||||
int position, float color_hue);
|
||||
virtual void update(int ticks) OVERRIDE;
|
||||
virtual void updateGraphics(float dt) OVERRIDE;
|
||||
virtual void reset();
|
||||
@ -66,6 +70,7 @@ public:
|
||||
void addReplayEvent(float time,
|
||||
const btTransform &trans,
|
||||
const ReplayBase::PhysicInfo &pi,
|
||||
const ReplayBase::BonusInfo &bi,
|
||||
const ReplayBase::KartReplayEvent &kre);
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns whether this kart is a ghost (replay) kart. */
|
||||
@ -76,6 +81,12 @@ public:
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns the speed of the kart in meters/second. */
|
||||
virtual float getSpeed() const;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns the time at which the kart was at a given distance.
|
||||
* Returns -1.0f if none */
|
||||
float getTimeForDistance(float distance);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
virtual void kartIsInRestNow() {};
|
||||
// ------------------------------------------------------------------------
|
||||
|
@ -19,6 +19,7 @@
|
||||
|
||||
#include "io/file_manager.hpp"
|
||||
#include "karts/abstract_kart.hpp"
|
||||
#include "replay/replay_play.hpp"
|
||||
#include "tracks/track.hpp"
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -29,6 +30,7 @@ EasterEggHunt::EasterEggHunt() : LinearWorld()
|
||||
WorldStatus::setClockMode(CLOCK_CHRONO);
|
||||
m_use_highscores = true;
|
||||
m_eggs_found = 0;
|
||||
m_only_ghosts = false;
|
||||
} // EasterEggHunt
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -39,13 +41,19 @@ void EasterEggHunt::init()
|
||||
LinearWorld::init();
|
||||
m_display_rank = false;
|
||||
|
||||
unsigned int gk = 0;
|
||||
if (race_manager->hasGhostKarts())
|
||||
gk = ReplayPlay::get()->getNumGhostKart();
|
||||
// check for possible problems if AI karts were incorrectly added
|
||||
if(getNumKarts() > race_manager->getNumPlayers())
|
||||
if((getNumKarts() - gk) > race_manager->getNumPlayers())
|
||||
{
|
||||
Log::error("EasterEggHunt]", "No AI exists for this game mode");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (getNumKarts() == gk)
|
||||
m_only_ghosts = true;
|
||||
|
||||
m_eggs_collected.resize(m_karts.size(), 0);
|
||||
|
||||
} // EasterEggHunt
|
||||
@ -154,6 +162,15 @@ void EasterEggHunt::collectedEasterEgg(const AbstractKart *kart)
|
||||
m_eggs_found++;
|
||||
} // collectedEasterEgg
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Called when a ghost kart has collected an egg.
|
||||
* \param world_id The world id of the ghost kart that collected an egg.
|
||||
*/
|
||||
void EasterEggHunt::collectedEasterEggGhost(int world_id)
|
||||
{
|
||||
m_eggs_collected[world_id]++;
|
||||
} // collectedEasterEgg
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Update the world and the track.
|
||||
* \param ticks Physics time step size - should be 1.
|
||||
@ -169,8 +186,16 @@ void EasterEggHunt::update(int ticks)
|
||||
*/
|
||||
bool EasterEggHunt::isRaceOver()
|
||||
{
|
||||
if(m_eggs_found == m_number_of_eggs)
|
||||
if(!m_only_ghosts && m_eggs_found == m_number_of_eggs)
|
||||
return true;
|
||||
else if (m_only_ghosts)
|
||||
{
|
||||
for (unsigned int i=0 ; i<m_eggs_collected.size();i++)
|
||||
{
|
||||
if (m_eggs_collected[i] == m_number_of_eggs)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if(m_time<0)
|
||||
return true;
|
||||
return false;
|
||||
|
@ -40,8 +40,10 @@ private:
|
||||
/** Overall number of easter eggs. */
|
||||
int m_number_of_eggs;
|
||||
|
||||
/** Number of eggs found so far. */
|
||||
/** Number of eggs found so far by players. */
|
||||
int m_eggs_found;
|
||||
|
||||
bool m_only_ghosts;
|
||||
public:
|
||||
EasterEggHunt();
|
||||
virtual ~EasterEggHunt();
|
||||
@ -61,8 +63,11 @@ public:
|
||||
virtual void getKartsDisplayInfo(
|
||||
std::vector<RaceGUIBase::KartIconDisplayInfo> *info) OVERRIDE;
|
||||
|
||||
const int numberOfEggsFound() { return m_eggs_found; }
|
||||
|
||||
void updateKartRanks();
|
||||
void collectedEasterEgg(const AbstractKart *kart);
|
||||
void collectedEasterEggGhost(int world_id);
|
||||
void readData(const std::string &filename);
|
||||
|
||||
virtual void checkForWrongDirection(unsigned int i, float dt) OVERRIDE;
|
||||
|
@ -52,6 +52,8 @@ LinearWorld::LinearWorld() : WorldWithRank()
|
||||
m_last_lap_sfx_played = false;
|
||||
m_last_lap_sfx_playing = false;
|
||||
m_fastest_lap_ticks = INT_MAX;
|
||||
m_valid_reference_time = false;
|
||||
m_live_time_difference = 0.0f;
|
||||
} // LinearWorld
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@ -204,6 +206,10 @@ void LinearWorld::update(int ticks)
|
||||
m_kart_info[i].m_estimated_finish =
|
||||
estimateFinishTimeForKart(m_karts[i]);
|
||||
}
|
||||
// If one player and a ghost, or two compared ghosts,
|
||||
// compute the live time difference
|
||||
if(race_manager->hasGhostKarts() && race_manager->getNumberOfKarts() == 2)
|
||||
updateLiveDifference();
|
||||
|
||||
#ifdef DEBUG
|
||||
// Debug output in case that the double position error occurs again.
|
||||
@ -262,6 +268,45 @@ void LinearWorld::updateGraphics(float dt)
|
||||
|
||||
} // updateGraphics
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
/** This calculate the time difference between the second kart in the
|
||||
* race and the first kart in the race (who must be a ghost)
|
||||
*/
|
||||
void LinearWorld::updateLiveDifference()
|
||||
{
|
||||
// First check that the call requirements are verified
|
||||
assert (race_manager->hasGhostKarts() && race_manager->getNumberOfKarts() >= 2);
|
||||
|
||||
AbstractKart* ghost_kart = getKart(0);
|
||||
|
||||
// Get the distance at which the second kart is
|
||||
float second_kart_distance = getOverallDistance(1);
|
||||
|
||||
// Check when the ghost what at this position
|
||||
float ghost_time;
|
||||
|
||||
// If there are two ghost karts, the view is set to kart 0,
|
||||
// so switch roles in the comparison. Note that
|
||||
// we can't simply multiply the time by -1, as they are assymetrical.
|
||||
// When one kart don't increase its distance (rescue, etc),
|
||||
// the difference increases linearly for one and jump for the other.
|
||||
if (getKart(1)->isGhostKart())
|
||||
{
|
||||
ghost_kart = getKart(1);
|
||||
second_kart_distance = getOverallDistance(0);
|
||||
}
|
||||
ghost_time = ghost_kart->getTimeForDistance(second_kart_distance);
|
||||
|
||||
if (ghost_time >= 0.0f)
|
||||
m_valid_reference_time = true;
|
||||
else
|
||||
m_valid_reference_time = false;
|
||||
|
||||
float current_time = World::getWorld()->getTime();
|
||||
|
||||
m_live_time_difference = current_time - ghost_time;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Is called by check structures if a kart starts a new lap.
|
||||
* \param kart_index Index of the kart.
|
||||
|
@ -55,6 +55,21 @@ private:
|
||||
* get valid finish times estimates. */
|
||||
float m_distance_increase;
|
||||
|
||||
/** This stores the live time difference between a ghost kart
|
||||
* and a second kart racing against it (normal or ghost).
|
||||
*/
|
||||
|
||||
float m_live_time_difference;
|
||||
|
||||
/** True if the live_time_difference is invalid */
|
||||
bool m_valid_reference_time;
|
||||
|
||||
/** This calculate the time difference between the second kart in the race
|
||||
* (there must be at least two) and the first kart in the race
|
||||
* (who must be a ghost).
|
||||
*/
|
||||
void updateLiveDifference();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** Some additional info that needs to be kept for each kart
|
||||
* in this kind of race.
|
||||
@ -120,13 +135,14 @@ public:
|
||||
|
||||
virtual void update(int ticks) OVERRIDE;
|
||||
virtual void updateGraphics(float dt) OVERRIDE;
|
||||
float getDistanceDownTrackForKart(const int kart_id) const;
|
||||
float getDistanceDownTrackForKart(const int kart_id,
|
||||
bool account_for_checklines) const;
|
||||
float getDistanceToCenterForKart(const int kart_id) const;
|
||||
float getEstimatedFinishTime(const int kart_id) const;
|
||||
int getLapForKart(const int kart_id) const;
|
||||
int getTicksAtLapForKart(const int kart_id) const;
|
||||
float getLiveTimeDifference() const { return m_live_time_difference; }
|
||||
bool hasValidTimeDifference() const { return m_valid_reference_time; }
|
||||
|
||||
virtual void getKartsDisplayInfo(
|
||||
std::vector<RaceGUIBase::KartIconDisplayInfo> *info) OVERRIDE;
|
||||
|
@ -511,6 +511,20 @@ public:
|
||||
// ------------------------------------------------------------------------
|
||||
MinorRaceModeType getMinorMode() const { return m_minor_mode; }
|
||||
// ------------------------------------------------------------------------
|
||||
std::string getMinorModeName() const
|
||||
{
|
||||
switch (m_minor_mode)
|
||||
{
|
||||
case MINOR_MODE_NORMAL_RACE: return "normal";
|
||||
case MINOR_MODE_TIME_TRIAL: return "time-trial";
|
||||
case MINOR_MODE_FOLLOW_LEADER: return "follow-the-leader";
|
||||
case MINOR_MODE_3_STRIKES: return "battle";
|
||||
case MINOR_MODE_EASTER_EGG: return "egg-hunt";
|
||||
case MINOR_MODE_SOCCER: return "soccer";
|
||||
default: assert(false); return "";
|
||||
}
|
||||
}
|
||||
// ------------------------------------------------------------------------
|
||||
unsigned int getNumPlayers() const
|
||||
{
|
||||
return (unsigned int) m_player_karts.size();
|
||||
@ -524,8 +538,10 @@ public:
|
||||
*/
|
||||
int getNumLaps() const
|
||||
{
|
||||
if(m_minor_mode==MINOR_MODE_3_STRIKES ||
|
||||
m_minor_mode==MINOR_MODE_FOLLOW_LEADER )
|
||||
if(m_minor_mode==MINOR_MODE_3_STRIKES ||
|
||||
m_minor_mode==MINOR_MODE_FOLLOW_LEADER ||
|
||||
m_minor_mode==MINOR_MODE_SOCCER ||
|
||||
m_minor_mode==MINOR_MODE_EASTER_EGG )
|
||||
return 9999;
|
||||
return m_num_laps[m_track_number];
|
||||
} // getNumLaps
|
||||
@ -635,7 +651,7 @@ public:
|
||||
return m_ai_kart_list;
|
||||
} // getAIKartList
|
||||
// ------------------------------------------------------------------------
|
||||
/** \brief get information about given mode (returns true if 'mode' is of
|
||||
/** \brief get information about current mode (returns true if 'mode' is of
|
||||
* linear races type) */
|
||||
bool isLinearRaceMode()
|
||||
{
|
||||
@ -645,6 +661,19 @@ public:
|
||||
if(id > 999 && id < 2000) return true;
|
||||
else return false;
|
||||
} // isLinearRaceMode
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** \brief get information about given mode (returns true if 'mode' is of
|
||||
* linear races type) */
|
||||
bool isLinearRaceMode(const MinorRaceModeType mode)
|
||||
{
|
||||
const int id = (int)mode;
|
||||
// info is stored in its ID for conveniance, see the macros LINEAR_RACE
|
||||
// and BATTLE_ARENA above for exact meaning.
|
||||
if(id > 999 && id < 2000) return true;
|
||||
else return false;
|
||||
} // isLinearRaceMode
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** \brief Returns true if the current mode is a battle mode. */
|
||||
bool isBattleMode()
|
||||
@ -672,6 +701,18 @@ public:
|
||||
{
|
||||
return m_minor_mode == MINOR_MODE_TUTORIAL;
|
||||
} // isTutorialMode
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
bool isEggHuntMode()
|
||||
{
|
||||
return m_minor_mode == MINOR_MODE_EASTER_EGG;
|
||||
} // isEggHuntMode
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
bool isTimeTrialMode()
|
||||
{
|
||||
return m_minor_mode == MINOR_MODE_TIME_TRIAL;
|
||||
} // isTimeTrialMode
|
||||
// ------------------------------------------------------------------------
|
||||
/** \brief Returns true if the current mode has laps. */
|
||||
bool modeHasLaps()
|
||||
|
@ -29,10 +29,10 @@ ReplayBase::ReplayBase()
|
||||
* \param full_path True if the file is full path.
|
||||
* \return A FILE *, or NULL if the file could not be opened.
|
||||
*/
|
||||
FILE* ReplayBase::openReplayFile(bool writeable, bool full_path)
|
||||
FILE* ReplayBase::openReplayFile(bool writeable, bool full_path, int replay_file_number)
|
||||
{
|
||||
FILE *fd = fopen(full_path ? getReplayFilename().c_str() :
|
||||
(file_manager->getReplayDir() + getReplayFilename()).c_str(),
|
||||
FILE *fd = fopen(full_path ? getReplayFilename(replay_file_number).c_str() :
|
||||
(file_manager->getReplayDir() + getReplayFilename(replay_file_number)).c_str(),
|
||||
writeable ? "w" : "r");
|
||||
if (!fd)
|
||||
{
|
||||
|
@ -54,18 +54,43 @@ protected:
|
||||
float m_steer;
|
||||
/** The suspension length of 4 wheels at a certain time. */
|
||||
float m_suspension_length[4];
|
||||
/** The skidding state */
|
||||
int m_skidding_state;
|
||||
}; // PhysicInfo
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
struct BonusInfo
|
||||
{
|
||||
/** The attachment. This is stored using a custom format
|
||||
0 = none ; 1 = parachute ; 2 = anvil ; 3 = bomb ;
|
||||
4 = swatter ; 5 = bubblegum
|
||||
This is necessary so replay files are not broken if the
|
||||
game internal attachment format/ordering is changed. */
|
||||
int m_attachment;
|
||||
/** The nitro amount at a certain time. */
|
||||
float m_nitro_amount;
|
||||
/** The number of items at a certain time. */
|
||||
int m_item_amount;
|
||||
/** The type of item at a certain time. */
|
||||
int m_item_type;
|
||||
/** Used to store mode-specific values : eggs in egg-hunt,
|
||||
number of lives in battle-mode. */
|
||||
int m_special_value;
|
||||
}; // StateInfo
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** Records all other events. */
|
||||
struct KartReplayEvent
|
||||
{
|
||||
/** distance on track for the kart recorded. */
|
||||
float m_distance;
|
||||
/** Nitro usage for the kart recorded. */
|
||||
int m_nitro_usage;
|
||||
/** Zipper usage for the kart recorded. */
|
||||
bool m_zipper_usage;
|
||||
/** Skidding state for the kart recorded. */
|
||||
int m_skidding_state;
|
||||
/** Skidding effect for the kart recorded. */
|
||||
int m_skidding_effect;
|
||||
/** Kart skidding showing red flame or not. */
|
||||
bool m_red_skidding;
|
||||
/** True if the kart recorded is jumping. */
|
||||
@ -73,15 +98,19 @@ protected:
|
||||
}; // KartReplayEvent
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
FILE *openReplayFile(bool writeable, bool full_path = false);
|
||||
FILE *openReplayFile(bool writeable, bool full_path = false, int replay_file_number=1);
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns the filename that was opened. */
|
||||
virtual const std::string& getReplayFilename() const = 0;
|
||||
virtual const std::string& getReplayFilename(int replay_file_number = 1) const = 0;
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns the version number of the replay file. This is used to check
|
||||
* that a loaded replay file can still be understood by this
|
||||
* executable. */
|
||||
unsigned int getReplayVersion() const { return 3; }
|
||||
/** Returns the version number of the replay file recorderd by this executable.
|
||||
* This is also used as a maximum supported version by this exexcutable. */
|
||||
unsigned int getCurrentReplayVersion() const { return 4; }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** This is used to check that a loaded replay file can still
|
||||
* be understood by this executable. */
|
||||
unsigned int getMinSupportedReplayVersion() const { return 3; }
|
||||
|
||||
public:
|
||||
ReplayBase();
|
||||
|
@ -30,6 +30,7 @@
|
||||
#include <irrlicht.h>
|
||||
#include <stdio.h>
|
||||
#include <string>
|
||||
#include <cinttypes>
|
||||
|
||||
ReplayPlay::SortOrder ReplayPlay::m_sort_order = ReplayPlay::SO_DEFAULT;
|
||||
ReplayPlay *ReplayPlay::m_replay_play = NULL;
|
||||
@ -39,7 +40,9 @@ ReplayPlay *ReplayPlay::m_replay_play = NULL;
|
||||
*/
|
||||
ReplayPlay::ReplayPlay()
|
||||
{
|
||||
m_current_replay_file = 0;
|
||||
m_current_replay_file = 0;
|
||||
m_second_replay_file = 0;
|
||||
m_second_replay_enabled = false;
|
||||
} // ReplayPlay
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -83,20 +86,23 @@ void ReplayPlay::loadAllReplayFile()
|
||||
file_manager->listFiles(files, file_manager->getReplayDir(),
|
||||
/*is_full_path*/ false);
|
||||
|
||||
int j=0;
|
||||
|
||||
for (std::set<std::string>::iterator i = files.begin();
|
||||
i != files.end(); ++i)
|
||||
{
|
||||
if (!addReplayFile(*i))
|
||||
if (!addReplayFile(*i, false, j))
|
||||
{
|
||||
// Skip invalid replay file
|
||||
continue;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
} // loadAllReplayFile
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
bool ReplayPlay::addReplayFile(const std::string& fn, bool custom_replay)
|
||||
bool ReplayPlay::addReplayFile(const std::string& fn, bool custom_replay, int call_index)
|
||||
{
|
||||
|
||||
char s[1024], s1[1024];
|
||||
@ -119,14 +125,31 @@ bool ReplayPlay::addReplayFile(const std::string& fn, bool custom_replay)
|
||||
fclose(fd);
|
||||
return false;
|
||||
}
|
||||
if (version != getReplayVersion())
|
||||
if (version > getCurrentReplayVersion() ||
|
||||
version < getMinSupportedReplayVersion() )
|
||||
{
|
||||
Log::warn("Replay", "Replay is version '%d'", version);
|
||||
Log::warn("Replay", "STK version is '%d'", getReplayVersion());
|
||||
Log::warn("Replay", "STK replay version is '%d'", getCurrentReplayVersion());
|
||||
Log::warn("Replay", "Minimum supported replay version is '%d'", getMinSupportedReplayVersion());
|
||||
Log::warn("Replay", "Skipped '%s'", fn.c_str());
|
||||
fclose(fd);
|
||||
return false;
|
||||
}
|
||||
rd.m_replay_version = version;
|
||||
|
||||
if (version >= 4)
|
||||
{
|
||||
fgets(s, 1023, fd);
|
||||
if(sscanf(s, "stk_version: %s", s1) != 1)
|
||||
{
|
||||
Log::warn("Replay", "No STK release version found in replay file.");
|
||||
fclose(fd);
|
||||
return false;
|
||||
}
|
||||
rd.m_stk_version = s1;
|
||||
}
|
||||
else
|
||||
rd.m_stk_version = "";
|
||||
|
||||
while(true)
|
||||
{
|
||||
@ -160,13 +183,29 @@ bool ReplayPlay::addReplayFile(const std::string& fn, bool custom_replay)
|
||||
// (see GhostController::getName)
|
||||
rd.m_name_list.push_back("");
|
||||
}
|
||||
|
||||
// Read kart color data
|
||||
if (version >= 4)
|
||||
{
|
||||
float f = 0;
|
||||
fgets(s, 1023, fd);
|
||||
if(sscanf(s, "kart_color: %f", &f) != 1)
|
||||
{
|
||||
Log::warn("Replay", "Kart color missing in replay file.");
|
||||
fclose(fd);
|
||||
return false;
|
||||
}
|
||||
rd.m_kart_color.push_back(f);
|
||||
}
|
||||
else
|
||||
rd.m_kart_color.push_back(0.0f); // Use default kart color
|
||||
}
|
||||
|
||||
int reverse = 0;
|
||||
fgets(s, 1023, fd);
|
||||
if(sscanf(s, "reverse: %d", &reverse) != 1)
|
||||
{
|
||||
Log::warn("Replay", "Reverse info found in replay file.");
|
||||
Log::warn("Replay", "No reverse info found in replay file.");
|
||||
fclose(fd);
|
||||
return false;
|
||||
}
|
||||
@ -180,6 +219,22 @@ bool ReplayPlay::addReplayFile(const std::string& fn, bool custom_replay)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version >= 4)
|
||||
{
|
||||
fgets(s, 1023, fd);
|
||||
if (sscanf(s, "mode: %s", s1) != 1)
|
||||
{
|
||||
Log::warn("Replay", "Replay mode not found in replay file.");
|
||||
fclose(fd);
|
||||
return false;
|
||||
}
|
||||
rd.m_minor_mode = s1;
|
||||
}
|
||||
// Assume time-trial mode for old replays
|
||||
else
|
||||
rd.m_minor_mode = "time-trial";
|
||||
|
||||
|
||||
fgets(s, 1023, fd);
|
||||
if (sscanf(s, "track: %s", s1) != 1)
|
||||
{
|
||||
@ -212,6 +267,21 @@ bool ReplayPlay::addReplayFile(const std::string& fn, bool custom_replay)
|
||||
fclose(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version >= 4)
|
||||
{
|
||||
fgets(s, 1023, fd);
|
||||
if (sscanf(s, "replay_uid: %" PRIu64, &rd.m_replay_uid) != 1)
|
||||
{
|
||||
Log::warn("Replay", "Replay UID not found in replay file.");
|
||||
fclose(fd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// No UID in old replay format
|
||||
else
|
||||
rd.m_replay_uid = call_index;
|
||||
|
||||
fclose(fd);
|
||||
m_replay_file_list.push_back(rd);
|
||||
|
||||
@ -228,22 +298,42 @@ bool ReplayPlay::addReplayFile(const std::string& fn, bool custom_replay)
|
||||
void ReplayPlay::load()
|
||||
{
|
||||
m_ghost_karts.clearAndDeleteAll();
|
||||
|
||||
if (m_second_replay_enabled)
|
||||
loadFile(/* second replay */ true);
|
||||
|
||||
// Always load the first replay
|
||||
loadFile(/* second replay */ false);
|
||||
|
||||
} // load
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
void ReplayPlay::loadFile(bool second_replay)
|
||||
{
|
||||
char s[1024];
|
||||
|
||||
int replay_index = second_replay ? m_second_replay_file : m_current_replay_file;
|
||||
int replay_file_number = second_replay ? 2 : 1;
|
||||
|
||||
FILE *fd = openReplayFile(/*writeable*/false,
|
||||
m_replay_file_list.at(m_current_replay_file).m_custom_replay_file);
|
||||
m_replay_file_list.at(replay_index).m_custom_replay_file, replay_file_number);
|
||||
|
||||
if(!fd)
|
||||
{
|
||||
Log::error("Replay", "Can't read '%s', ghost replay disabled.",
|
||||
getReplayFilename().c_str());
|
||||
getReplayFilename(replay_file_number).c_str());
|
||||
destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
Log::info("Replay", "Reading replay file '%s'.", getReplayFilename().c_str());
|
||||
Log::info("Replay", "Reading replay file '%s'.", getReplayFilename(replay_file_number).c_str());
|
||||
|
||||
const unsigned int line_skipped = getNumGhostKart() + 7;
|
||||
for (unsigned int i = 0; i < line_skipped; i++)
|
||||
ReplayData &rd = m_replay_file_list[replay_index];
|
||||
unsigned int num_kart = m_replay_file_list.at(replay_index).m_kart_list.size();
|
||||
unsigned int lines_to_skip = (rd.m_replay_version == 3) ? 7 : 10;
|
||||
lines_to_skip += (rd.m_replay_version == 3) ? num_kart : 2*num_kart;
|
||||
|
||||
for (unsigned int i = 0; i < lines_to_skip; i++)
|
||||
fgets(s, 1023, fd);
|
||||
|
||||
// eof actually doesn't trigger here, since it requires first to try
|
||||
@ -252,27 +342,35 @@ void ReplayPlay::load()
|
||||
{
|
||||
if(fgets(s, 1023, fd)==NULL) // eof reached
|
||||
break;
|
||||
readKartData(fd, s);
|
||||
readKartData(fd, s, second_replay);
|
||||
}
|
||||
|
||||
fprintf(fd, "Replay file end.\n");
|
||||
fclose(fd);
|
||||
} // load
|
||||
} // loadFile
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Reads all data from a replay file for a specific kart.
|
||||
* \param fd The file descriptor from which to read.
|
||||
*/
|
||||
void ReplayPlay::readKartData(FILE *fd, char *next_line)
|
||||
void ReplayPlay::readKartData(FILE *fd, char *next_line, bool second_replay)
|
||||
{
|
||||
char s[1024];
|
||||
|
||||
int replay_index = second_replay ? m_second_replay_file : m_current_replay_file;
|
||||
|
||||
const unsigned int kart_num = m_ghost_karts.size();
|
||||
ReplayData &rd = m_replay_file_list[m_current_replay_file];
|
||||
m_ghost_karts.push_back(new GhostKart(rd.m_kart_list.at(kart_num),
|
||||
kart_num, kart_num + 1));
|
||||
unsigned int first_loaded_f_num = 0;
|
||||
|
||||
if (!second_replay && m_second_replay_enabled)
|
||||
first_loaded_f_num = m_replay_file_list.at(m_second_replay_file).m_kart_list.size();
|
||||
|
||||
ReplayData &rd = m_replay_file_list[replay_index];
|
||||
m_ghost_karts.push_back(new GhostKart(rd.m_kart_list.at(kart_num-first_loaded_f_num),
|
||||
kart_num, kart_num + 1,
|
||||
rd.m_kart_color.at(kart_num-first_loaded_f_num)));
|
||||
m_ghost_karts[kart_num].init(RaceManager::KT_GHOST);
|
||||
Controller* controller = new GhostController(getGhostKart(kart_num),
|
||||
rd.m_name_list[kart_num]);
|
||||
rd.m_name_list[kart_num-first_loaded_f_num]);
|
||||
getGhostKart(kart_num)->setController(controller);
|
||||
|
||||
unsigned int size;
|
||||
@ -283,45 +381,137 @@ void ReplayPlay::readKartData(FILE *fd, char *next_line)
|
||||
for(unsigned int i=0; i<size; i++)
|
||||
{
|
||||
fgets(s, 1023, fd);
|
||||
float x, y, z, rx, ry, rz, rw, time, speed, steer, w1, w2, w3, w4;
|
||||
int nitro, zipper, skidding, red_skidding, jumping;
|
||||
float x, y, z, rx, ry, rz, rw, time, speed, steer, w1, w2, w3, w4, nitro_amount, distance;
|
||||
int skidding_state, attachment, item_amount, item_type, special_value,
|
||||
nitro, zipper, skidding, red_skidding, jumping;
|
||||
|
||||
// Check for EV_TRANSFORM event:
|
||||
// -----------------------------
|
||||
if(sscanf(s, "%f %f %f %f %f %f %f %f %f %f %f %f %f %f %d %d %d %d %d\n",
|
||||
&time,
|
||||
&x, &y, &z,
|
||||
&rx, &ry, &rz, &rw,
|
||||
&speed, &steer, &w1, &w2, &w3, &w4,
|
||||
&nitro, &zipper, &skidding, &red_skidding, &jumping
|
||||
)==19)
|
||||
|
||||
// Up to STK 0.9.3 replays
|
||||
if (rd.m_replay_version == 3)
|
||||
{
|
||||
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;
|
||||
kre.m_nitro_usage = nitro;
|
||||
kre.m_zipper_usage = zipper!=0;
|
||||
kre.m_skidding_state = skidding;
|
||||
kre.m_red_skidding = red_skidding!=0;
|
||||
kre.m_jumping = jumping != 0;
|
||||
m_ghost_karts[kart_num].addReplayEvent(time,
|
||||
btTransform(q, xyz), pi, kre);
|
||||
if(sscanf(s, "%f %f %f %f %f %f %f %f %f %f %f %f %f %f %d %d %d %d %d\n",
|
||||
&time,
|
||||
&x, &y, &z,
|
||||
&rx, &ry, &rz, &rw,
|
||||
&speed, &steer, &w1, &w2, &w3, &w4,
|
||||
&nitro, &zipper, &skidding, &red_skidding, &jumping
|
||||
)==19)
|
||||
{
|
||||
btQuaternion q(rx, ry, rz, rw);
|
||||
btVector3 xyz(x, y, z);
|
||||
PhysicInfo pi = {0};
|
||||
BonusInfo bi = {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;
|
||||
pi.m_skidding_state = 0; //not saved in version 3 replays
|
||||
bi.m_attachment = 0; //not saved in version 3 replays
|
||||
bi.m_nitro_amount = 0; //not saved in version 3 replays
|
||||
bi.m_item_amount = 0; //not saved in version 3 replays
|
||||
bi.m_item_type = 0; //not saved in version 3 replays
|
||||
bi.m_special_value = 0; //not saved in version 3 replays
|
||||
kre.m_distance = 0.0f; //not saved in version 3 replays
|
||||
kre.m_nitro_usage = nitro;
|
||||
kre.m_zipper_usage = zipper!=0;
|
||||
kre.m_skidding_effect = skidding;
|
||||
kre.m_red_skidding = red_skidding!=0;
|
||||
kre.m_jumping = jumping != 0;
|
||||
m_ghost_karts[kart_num].addReplayEvent(time,
|
||||
btTransform(q, xyz), pi, bi, kre);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid record found
|
||||
// ---------------------
|
||||
Log::warn("Replay", "Can't read replay data line %d:", i);
|
||||
Log::warn("Replay", "%s", s);
|
||||
Log::warn("Replay", "Ignored.");
|
||||
}
|
||||
}
|
||||
|
||||
//version 4 replays (STK 0.9.4 and higher)
|
||||
else
|
||||
{
|
||||
// Invalid record found
|
||||
// ---------------------
|
||||
Log::warn("Replay", "Can't read replay data line %d:", i);
|
||||
Log::warn("Replay", "%s", s);
|
||||
Log::warn("Replay", "Ignored.");
|
||||
if(sscanf(s, "%f %f %f %f %f %f %f %f %f %f %f %f %f %f %d %d %f %d %d %d %f %d %d %d %d %d\n",
|
||||
&time,
|
||||
&x, &y, &z,
|
||||
&rx, &ry, &rz, &rw,
|
||||
&speed, &steer, &w1, &w2, &w3, &w4, &skidding_state,
|
||||
&attachment, &nitro_amount, &item_amount, &item_type, &special_value,
|
||||
&distance, &nitro, &zipper, &skidding, &red_skidding, &jumping
|
||||
)==26)
|
||||
{
|
||||
btQuaternion q(rx, ry, rz, rw);
|
||||
btVector3 xyz(x, y, z);
|
||||
PhysicInfo pi = {0};
|
||||
BonusInfo bi = {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;
|
||||
pi.m_skidding_state = skidding_state;
|
||||
bi.m_attachment = attachment;
|
||||
bi.m_nitro_amount = nitro_amount;
|
||||
bi.m_item_amount = item_amount;
|
||||
bi.m_item_type = item_type;
|
||||
bi.m_special_value = special_value;
|
||||
kre.m_distance = distance;
|
||||
kre.m_nitro_usage = nitro;
|
||||
kre.m_zipper_usage = zipper!=0;
|
||||
kre.m_skidding_effect = skidding;
|
||||
kre.m_red_skidding = red_skidding!=0;
|
||||
kre.m_jumping = jumping != 0;
|
||||
m_ghost_karts[kart_num].addReplayEvent(time,
|
||||
btTransform(q, xyz), pi, bi, kre);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid record found
|
||||
// ---------------------
|
||||
Log::warn("Replay", "Can't read replay data line %d:", i);
|
||||
Log::warn("Replay", "%s", s);
|
||||
Log::warn("Replay", "Ignored.");
|
||||
}
|
||||
}
|
||||
} // for i
|
||||
|
||||
} // readKartData
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** call getReplayIdByUID and set the current replay file to the first one
|
||||
* with a matching UID.
|
||||
* \param uid The UID to match
|
||||
*/
|
||||
void ReplayPlay::setReplayFileByUID(uint64_t uid)
|
||||
{
|
||||
m_current_replay_file = getReplayIdByUID(uid);
|
||||
} //setReplayFileByUID
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Search among replay file and return the first index with a matching UID.
|
||||
* \param uid The UID to match
|
||||
*/
|
||||
unsigned int ReplayPlay::getReplayIdByUID(uint64_t uid)
|
||||
{
|
||||
for(unsigned int i = 0; i < m_replay_file_list.size(); i++)
|
||||
{
|
||||
if (m_replay_file_list[i].m_replay_uid == uid)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
Log::error("Replay", "Replay with UID of " PRIu64 " not found.", uid);
|
||||
return 0;
|
||||
} //setReplayFileByUID
|
||||
|
@ -46,7 +46,8 @@ public:
|
||||
SO_DIFF,
|
||||
SO_LAPS,
|
||||
SO_TIME,
|
||||
SO_USER
|
||||
SO_USER,
|
||||
SO_VERSION
|
||||
};
|
||||
|
||||
class ReplayData
|
||||
@ -54,13 +55,18 @@ public:
|
||||
public:
|
||||
std::string m_filename;
|
||||
std::string m_track_name;
|
||||
std::string m_minor_mode;
|
||||
core::stringw m_stk_version;
|
||||
core::stringw m_user_name;
|
||||
std::vector<std::string> m_kart_list;
|
||||
std::vector<core::stringw> m_name_list;
|
||||
std::vector<float> m_kart_color; //no sorting for this
|
||||
bool m_reverse;
|
||||
bool m_custom_replay_file;
|
||||
unsigned int m_difficulty;
|
||||
unsigned int m_laps;
|
||||
unsigned int m_replay_version; //no sorting for this
|
||||
uint64_t m_replay_uid; //no sorting for this
|
||||
float m_min_time;
|
||||
|
||||
bool operator < (const ReplayData& r) const
|
||||
@ -88,6 +94,9 @@ public:
|
||||
case SO_USER:
|
||||
return m_user_name < r.m_user_name;
|
||||
break;
|
||||
case SO_VERSION:
|
||||
return m_stk_version < r.m_stk_version;
|
||||
break;
|
||||
} // switch
|
||||
return true;
|
||||
} // operator <
|
||||
@ -100,6 +109,10 @@ private:
|
||||
|
||||
unsigned int m_current_replay_file;
|
||||
|
||||
unsigned int m_second_replay_file;
|
||||
|
||||
bool m_second_replay_enabled;
|
||||
|
||||
std::vector<ReplayData> m_replay_file_list;
|
||||
|
||||
/** All ghost karts. */
|
||||
@ -107,10 +120,11 @@ private:
|
||||
|
||||
ReplayPlay();
|
||||
~ReplayPlay();
|
||||
void readKartData(FILE *fd, char *next_line);
|
||||
void readKartData(FILE *fd, char *next_line, bool second_replay);
|
||||
public:
|
||||
void reset();
|
||||
void load();
|
||||
void loadFile(bool second_replay);
|
||||
void loadAllReplayFile();
|
||||
// ------------------------------------------------------------------------
|
||||
static void setSortOrder(SortOrder so) { m_sort_order = so; }
|
||||
@ -124,9 +138,21 @@ public:
|
||||
// ------------------------------------------------------------------------
|
||||
void setReplayFile(unsigned int n)
|
||||
{ m_current_replay_file = n; }
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
void setSecondReplayFile(unsigned int n, bool second_replay_enabled)
|
||||
{ m_second_replay_file = n;
|
||||
m_second_replay_enabled = second_replay_enabled;}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
void setReplayFileByUID(uint64_t uid);
|
||||
// ------------------------------------------------------------------------
|
||||
unsigned int getReplayIdByUID(uint64_t uid);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
bool addReplayFile(const std::string& fn,
|
||||
bool custom_replay = false);
|
||||
bool custom_replay = false,
|
||||
int call_index = 0);
|
||||
// ------------------------------------------------------------------------
|
||||
const ReplayData& getReplayData(unsigned int n) const
|
||||
{ return m_replay_file_list.at(n); }
|
||||
@ -139,14 +165,25 @@ public:
|
||||
const unsigned int getNumGhostKart() const
|
||||
{
|
||||
assert(m_replay_file_list.size() > 0);
|
||||
return (unsigned int)m_replay_file_list.at(m_current_replay_file)
|
||||
unsigned int num = m_replay_file_list.at(m_current_replay_file)
|
||||
.m_kart_list.size();
|
||||
unsigned int second_file_num = m_replay_file_list.at(m_second_replay_file)
|
||||
.m_kart_list.size();
|
||||
|
||||
num = (m_second_replay_enabled) ? num + second_file_num : num;
|
||||
|
||||
return num;
|
||||
} // getNumGhostKart
|
||||
// ------------------------------------------------------------------------
|
||||
const std::string& getGhostKartName(int n) const
|
||||
const std::string& getGhostKartName(unsigned int n) const
|
||||
{
|
||||
assert(m_replay_file_list.size() > 0);
|
||||
return m_replay_file_list.at(m_current_replay_file).m_kart_list.at(n);
|
||||
|
||||
unsigned int fkn = m_replay_file_list.at(m_current_replay_file).m_kart_list.size();
|
||||
if (n < fkn)
|
||||
return m_replay_file_list.at(m_current_replay_file).m_kart_list.at(n);
|
||||
else
|
||||
return m_replay_file_list.at(m_second_replay_file).m_kart_list.at(n-fkn);
|
||||
}
|
||||
// ------------------------------------------------------------------------
|
||||
/** Creates a new instance of the replay object. */
|
||||
@ -160,10 +197,13 @@ public:
|
||||
{ delete m_replay_play; m_replay_play = NULL; }
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns the filename that was opened. */
|
||||
virtual const std::string& getReplayFilename() const
|
||||
virtual const std::string& getReplayFilename(int replay_file_number = 1) const
|
||||
{
|
||||
assert(m_replay_file_list.size() > 0);
|
||||
return m_replay_file_list.at(m_current_replay_file).m_filename;
|
||||
if (replay_file_number == 2)
|
||||
return m_replay_file_list.at(m_second_replay_file).m_filename;
|
||||
else
|
||||
return m_replay_file_list.at(m_current_replay_file).m_filename;
|
||||
}
|
||||
// ------------------------------------------------------------------------
|
||||
}; // Replay
|
||||
|
@ -20,9 +20,16 @@
|
||||
|
||||
#include "config/stk_config.hpp"
|
||||
#include "io/file_manager.hpp"
|
||||
#include "items/attachment.hpp"
|
||||
#include "items/powerup.hpp"
|
||||
#include "items/powerup_manager.hpp"
|
||||
#include "guiengine/message_queue.hpp"
|
||||
#include "karts/controller/player_controller.hpp"
|
||||
#include "karts/ghost_kart.hpp"
|
||||
#include "karts/skidding.hpp"
|
||||
#include "karts/kart_gfx.hpp"
|
||||
#include "modes/easter_egg_hunt.hpp"
|
||||
#include "modes/linear_world.hpp"
|
||||
#include "modes/world.hpp"
|
||||
#include "physics/btKart.hpp"
|
||||
#include "race/race_manager.hpp"
|
||||
@ -31,7 +38,7 @@
|
||||
#include <algorithm>
|
||||
#include <stdio.h>
|
||||
#include <string>
|
||||
#include <karts/controller/player_controller.hpp>
|
||||
#include <cinttypes>
|
||||
|
||||
ReplayRecorder *ReplayRecorder::m_replay_recorder = NULL;
|
||||
|
||||
@ -42,6 +49,10 @@ ReplayRecorder::ReplayRecorder()
|
||||
{
|
||||
m_complete_replay = false;
|
||||
m_incorrect_replay = false;
|
||||
m_previous_steer = 0.0f;
|
||||
|
||||
assert(stk_config->m_replay_max_frames >= 0);
|
||||
m_max_frames = stk_config->m_replay_max_frames;
|
||||
} // ReplayRecorder
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
@ -58,6 +69,7 @@ void ReplayRecorder::reset()
|
||||
m_incorrect_replay = false;
|
||||
m_transform_events.clear();
|
||||
m_physic_info.clear();
|
||||
m_bonus_info.clear();
|
||||
m_kart_replay_event.clear();
|
||||
m_count_transforms.clear();
|
||||
m_last_saved_time.clear();
|
||||
@ -78,14 +90,15 @@ void ReplayRecorder::init()
|
||||
reset();
|
||||
m_transform_events.resize(race_manager->getNumberOfKarts());
|
||||
m_physic_info.resize(race_manager->getNumberOfKarts());
|
||||
m_bonus_info.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);
|
||||
|
||||
for(unsigned int i=0; i<race_manager->getNumberOfKarts(); i++)
|
||||
{
|
||||
m_transform_events[i].resize(max_frames);
|
||||
m_physic_info[i].resize(max_frames);
|
||||
m_kart_replay_event[i].resize(max_frames);
|
||||
m_transform_events[i].resize(m_max_frames);
|
||||
m_physic_info[i].resize(m_max_frames);
|
||||
m_bonus_info[i].resize(m_max_frames);
|
||||
m_kart_replay_event[i].resize(m_max_frames);
|
||||
}
|
||||
|
||||
m_count_transforms.resize(race_manager->getNumberOfKarts(), 0);
|
||||
@ -116,13 +129,122 @@ void ReplayRecorder::update(int ticks)
|
||||
#ifdef DEBUG
|
||||
m_count++;
|
||||
#endif
|
||||
if (time - m_last_saved_time[i] < stk_config->m_replay_dt)
|
||||
// If one of the tracked kart data has significantly changed
|
||||
// for the kart, update sooner than the usual dt
|
||||
bool force_update = false;
|
||||
|
||||
// Don't save directly the enum value, because any change
|
||||
// to it would break the reading of old replays
|
||||
int attachment = enumToCode(kart->getAttachment()->getType());
|
||||
int powerup_type = enumToCode(kart->getPowerup()->getType());
|
||||
int special_value = 0;
|
||||
|
||||
// In egg hunt mode, use store the number of eggs found so far
|
||||
// This assumes that egg hunt mode is only available in single-player
|
||||
if (race_manager->isEggHuntMode())
|
||||
{
|
||||
EasterEggHunt *easterworld = dynamic_cast<EasterEggHunt*>(World::getWorld());
|
||||
special_value = easterworld->numberOfEggsFound();
|
||||
}
|
||||
|
||||
if (attachment == -1)
|
||||
{
|
||||
Log::error("ReplayRecorder", "Unknown attachment type");
|
||||
return;
|
||||
}
|
||||
if (powerup_type == -1)
|
||||
{
|
||||
Log::error("ReplayRecorder", "Unknown powerup type");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_count_transforms[i] >= 2)
|
||||
{
|
||||
BonusInfo *b_prev = &(m_bonus_info[i][m_count_transforms[i]-1]);
|
||||
BonusInfo *b_prev2 = &(m_bonus_info[i][m_count_transforms[i]-2]);
|
||||
PhysicInfo *q_prev = &(m_physic_info[i][m_count_transforms[i]-1]);
|
||||
|
||||
// If the kart changes its steering
|
||||
if (fabsf(kart->getControls().getSteer() - m_previous_steer) >
|
||||
stk_config->m_replay_delta_steering)
|
||||
force_update = true;
|
||||
|
||||
// If the kart starts or stops skidding
|
||||
if (kart->getSkidding()->getSkidState() != q_prev->m_skidding_state)
|
||||
force_update = true;
|
||||
// If the kart changes speed significantly
|
||||
float speed_change = fabsf(kart->getSpeed() - q_prev->m_speed);
|
||||
if ( speed_change > stk_config->m_replay_delta_speed )
|
||||
{
|
||||
if (speed_change > 4*stk_config->m_replay_delta_speed)
|
||||
force_update = true;
|
||||
else if (speed_change > 2*stk_config->m_replay_delta_speed &&
|
||||
time - m_last_saved_time[i] > (stk_config->m_replay_dt/8.0f))
|
||||
force_update = true;
|
||||
else if (time - m_last_saved_time[i] > (stk_config->m_replay_dt/3.0f))
|
||||
force_update = true;
|
||||
}
|
||||
|
||||
// If the attachment has changed
|
||||
if (attachment != b_prev->m_attachment)
|
||||
force_update = true;
|
||||
|
||||
// If the item amount has changed
|
||||
if (kart->getNumPowerup() != b_prev->m_item_amount)
|
||||
force_update = true;
|
||||
|
||||
// If the item type has changed
|
||||
if (powerup_type != b_prev->m_item_type)
|
||||
force_update = true;
|
||||
|
||||
// In egg-hunt mode, if an egg has been collected
|
||||
// In battle mode, if a live has been lost/gained
|
||||
if (special_value != b_prev->m_special_value)
|
||||
force_update = true;
|
||||
|
||||
// If nitro starts being used or is collected
|
||||
if (kart->getEnergy() != b_prev->m_nitro_amount &&
|
||||
b_prev->m_nitro_amount == b_prev2->m_nitro_amount)
|
||||
force_update = true;
|
||||
|
||||
// If nitro stops being used
|
||||
// (also generates an extra transform on collection,
|
||||
// should be negligible and better than heavier checks)
|
||||
if (kart->getEnergy() == b_prev->m_nitro_amount &&
|
||||
b_prev->m_nitro_amount != b_prev2->m_nitro_amount)
|
||||
force_update = true;
|
||||
|
||||
// If close to the end of the race, reduce the time step
|
||||
// for extra precision
|
||||
// TODO : fast updates when close to the last egg in egg hunt
|
||||
if (race_manager->isLinearRaceMode())
|
||||
{
|
||||
float full_distance = race_manager->getNumLaps()
|
||||
* Track::getCurrentTrack()->getTrackLength();
|
||||
|
||||
const LinearWorld *linearworld = dynamic_cast<LinearWorld*>(World::getWorld());
|
||||
if (full_distance + DISTANCE_MAX_UPDATES >= linearworld->getOverallDistance(i) &&
|
||||
full_distance <= linearworld->getOverallDistance(i) + DISTANCE_FAST_UPDATES)
|
||||
{
|
||||
if (fabsf(full_distance - linearworld->getOverallDistance(i)) < DISTANCE_MAX_UPDATES)
|
||||
force_update = true;
|
||||
else if (time - m_last_saved_time[i] > (stk_config->m_replay_dt/5.0f))
|
||||
force_update = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ( time - m_last_saved_time[i] < (stk_config->m_replay_dt - stk_config->ticks2Time(1)) &&
|
||||
!force_update)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
m_count_skipped_time++;
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
|
||||
m_previous_steer = kart->getControls().getSteer();
|
||||
m_last_saved_time[i] = time;
|
||||
m_count_transforms[i]++;
|
||||
if (m_count_transforms[i] >= m_transform_events[i].size())
|
||||
@ -140,6 +262,7 @@ void ReplayRecorder::update(int ticks)
|
||||
}
|
||||
TransformEvent *p = &(m_transform_events[i][m_count_transforms[i]-1]);
|
||||
PhysicInfo *q = &(m_physic_info[i][m_count_transforms[i]-1]);
|
||||
BonusInfo *b = &(m_bonus_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();
|
||||
@ -159,9 +282,25 @@ void ReplayRecorder::update(int ticks)
|
||||
->getWheelInfo(j).m_raycastInfo.m_suspensionLength;
|
||||
}
|
||||
}
|
||||
q->m_skidding_state = kart->getSkidding()->getSkidState();
|
||||
|
||||
b->m_attachment = attachment;
|
||||
b->m_nitro_amount = kart->getEnergy();
|
||||
b->m_item_amount = kart->getNumPowerup();
|
||||
b->m_item_type = powerup_type;
|
||||
b->m_special_value = special_value;
|
||||
|
||||
//Only saves distance if recording a linear race
|
||||
if (race_manager->isLinearRaceMode())
|
||||
{
|
||||
const LinearWorld *linearworld = dynamic_cast<LinearWorld*>(World::getWorld());
|
||||
r->m_distance = linearworld->getOverallDistance(kart->getWorldKartId());
|
||||
}
|
||||
else
|
||||
r->m_distance = 0.0f;
|
||||
|
||||
kart->getKartGFX()->getGFXStatus(&(r->m_nitro_usage),
|
||||
&(r->m_zipper_usage), &(r->m_skidding_state), &(r->m_red_skidding));
|
||||
&(r->m_zipper_usage), &(r->m_skidding_effect), &(r->m_red_skidding));
|
||||
r->m_jumping = kart->isJumping();
|
||||
} // for i
|
||||
|
||||
@ -172,6 +311,42 @@ void ReplayRecorder::update(int ticks)
|
||||
}
|
||||
} // update
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Compute the replay's UID ; partly based on race data ; partly randomly
|
||||
*/
|
||||
uint64_t ReplayRecorder::computeUID(float min_time)
|
||||
{
|
||||
uint64_t unique_identifier = 0;
|
||||
|
||||
// First store some basic replay data
|
||||
int min_time_uid = (int) (min_time*1000);
|
||||
min_time_uid = min_time_uid%60000;
|
||||
|
||||
int day, month, year;
|
||||
StkTime::getDate(&day, &month, &year);
|
||||
uint64_t date_uid = year%10;
|
||||
date_uid = date_uid*12 + (month-1);;
|
||||
date_uid = date_uid*31 + (day-1);
|
||||
|
||||
int reverse = race_manager->getReverseTrack() ? 1 : 0;
|
||||
unique_identifier += reverse;
|
||||
unique_identifier += race_manager->getDifficulty()*2;
|
||||
unique_identifier += (race_manager->getNumLaps()-1)*8;
|
||||
unique_identifier += min_time_uid*160;
|
||||
unique_identifier += date_uid*9600000;
|
||||
|
||||
// Add a random value to make sure the identifier is unique
|
||||
// and use it to make the non-random part non-obvious
|
||||
// using magic and arbitrary constants
|
||||
|
||||
int random = rand()%9998 + 2; //avoid 0 and 1
|
||||
unique_identifier += random*47;
|
||||
unique_identifier *= random*10000;
|
||||
unique_identifier += (10000-random);
|
||||
|
||||
return unique_identifier;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Saves the replay data stored in the internal data structures.
|
||||
*/
|
||||
@ -220,7 +395,10 @@ void ReplayRecorder::save()
|
||||
(file_manager->getReplayDir() + getReplayFilename()).c_str());
|
||||
MessageQueue::add(MessageQueue::MT_GENERIC, msg);
|
||||
|
||||
fprintf(fd, "version: %d\n", getReplayVersion());
|
||||
fprintf(fd, "version: %d\n", getCurrentReplayVersion());
|
||||
fprintf(fd, "stk_version: %s\n", STK_VERSION);
|
||||
|
||||
unsigned int player_count = 0;
|
||||
for (unsigned int real_karts = 0; real_karts < num_karts; real_karts++)
|
||||
{
|
||||
const AbstractKart *kart = world->getKart(real_karts);
|
||||
@ -229,30 +407,44 @@ void ReplayRecorder::save()
|
||||
// XML encode the username to handle Unicode
|
||||
fprintf(fd, "kart: %s %s\n", kart->getIdent().c_str(),
|
||||
StringUtils::xmlEncode(kart->getController()->getName()).c_str());
|
||||
|
||||
if (kart->getController()->isPlayerController())
|
||||
{
|
||||
fprintf(fd, "kart_color: %f\n", StateManager::get()->getActivePlayer(player_count)->getConstProfile()->getDefaultKartColor());
|
||||
player_count++;
|
||||
}
|
||||
else
|
||||
fprintf(fd, "kart_color: 0\n");
|
||||
}
|
||||
|
||||
m_last_uid = computeUID(min_time);
|
||||
|
||||
int num_laps = race_manager->getNumLaps();
|
||||
if (num_laps == 9999) num_laps = 0; // no lap in that race mode
|
||||
|
||||
fprintf(fd, "kart_list_end\n");
|
||||
fprintf(fd, "reverse: %d\n", (int)race_manager->getReverseTrack());
|
||||
fprintf(fd, "difficulty: %d\n", race_manager->getDifficulty());
|
||||
fprintf(fd, "mode: %s\n", race_manager->getMinorModeName().c_str());
|
||||
fprintf(fd, "track: %s\n", Track::getCurrentTrack()->getIdent().c_str());
|
||||
fprintf(fd, "laps: %d\n", race_manager->getNumLaps());
|
||||
fprintf(fd, "laps: %d\n", num_laps);
|
||||
fprintf(fd, "min_time: %f\n", min_time);
|
||||
fprintf(fd, "replay_uid: %" PRIu64 "\n", m_last_uid);
|
||||
|
||||
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++)
|
||||
{
|
||||
if (world->getKart(k)->isGhostKart()) continue;
|
||||
fprintf(fd, "size: %d\n", m_count_transforms[k]);
|
||||
|
||||
unsigned int num_transforms = std::min(max_frames,
|
||||
unsigned int num_transforms = std::min(m_max_frames,
|
||||
m_count_transforms[k]);
|
||||
for (unsigned int i = 0; i < num_transforms; i++)
|
||||
{
|
||||
const TransformEvent *p = &(m_transform_events[k][i]);
|
||||
const PhysicInfo *q = &(m_physic_info[k][i]);
|
||||
const BonusInfo *b = &(m_bonus_info[k][i]);
|
||||
const KartReplayEvent *r = &(m_kart_replay_event[k][i]);
|
||||
fprintf(fd, "%f %f %f %f %f %f %f %f %f %f %f %f %f %f %d %d %d %d %d\n",
|
||||
fprintf(fd, "%f %f %f %f %f %f %f %f %f %f %f %f %f %f %d %d %f %d %d %d %f %d %d %d %d %d\n",
|
||||
p->m_time,
|
||||
p->m_transform.getOrigin().getX(),
|
||||
p->m_transform.getOrigin().getY(),
|
||||
@ -267,9 +459,16 @@ void ReplayRecorder::save()
|
||||
q->m_suspension_length[1],
|
||||
q->m_suspension_length[2],
|
||||
q->m_suspension_length[3],
|
||||
q->m_skidding_state,
|
||||
b->m_attachment,
|
||||
b->m_nitro_amount,
|
||||
b->m_item_amount,
|
||||
b->m_item_type,
|
||||
b->m_special_value,
|
||||
r->m_distance,
|
||||
r->m_nitro_usage,
|
||||
(int)r->m_zipper_usage,
|
||||
r->m_skidding_state,
|
||||
r->m_skidding_effect,
|
||||
(int)r->m_red_skidding,
|
||||
(int)r->m_jumping
|
||||
);
|
||||
@ -277,3 +476,79 @@ void ReplayRecorder::save()
|
||||
}
|
||||
fclose(fd);
|
||||
} // save
|
||||
|
||||
/* Returns an encoding value for a given attachment type.
|
||||
* The internal values of the enum for attachments may change if attachments
|
||||
* are introduced, removed or even reordered. To avoid compatibility issues
|
||||
* with previous replay files, we add a layer to encode an enum semantical
|
||||
* value the same way, independently of its internal value.
|
||||
* \param type : the type of attachment to encode */
|
||||
|
||||
int ReplayRecorder::enumToCode (Attachment::AttachmentType type)
|
||||
{
|
||||
int code =
|
||||
(type == Attachment::ATTACH_NOTHING) ? 0 :
|
||||
(type == Attachment::ATTACH_PARACHUTE) ? 1 :
|
||||
(type == Attachment::ATTACH_ANVIL) ? 2 :
|
||||
(type == Attachment::ATTACH_BOMB) ? 3 :
|
||||
(type == Attachment::ATTACH_SWATTER) ? 4 :
|
||||
(type == Attachment::ATTACH_BUBBLEGUM_SHIELD) ? 5 :
|
||||
-1 ;
|
||||
|
||||
return code;
|
||||
} // enumToCode
|
||||
|
||||
/* Returns an encoding value for a given item type
|
||||
* \param type : the type of item to encode */
|
||||
|
||||
int ReplayRecorder::enumToCode (PowerupManager::PowerupType type)
|
||||
{
|
||||
int code =
|
||||
(type == PowerupManager::POWERUP_NOTHING) ? 0 :
|
||||
(type == PowerupManager::POWERUP_BUBBLEGUM) ? 1 :
|
||||
(type == PowerupManager::POWERUP_CAKE) ? 2 :
|
||||
(type == PowerupManager::POWERUP_BOWLING) ? 3 :
|
||||
(type == PowerupManager::POWERUP_ZIPPER) ? 4 :
|
||||
(type == PowerupManager::POWERUP_PLUNGER) ? 5 :
|
||||
(type == PowerupManager::POWERUP_SWITCH) ? 6 :
|
||||
(type == PowerupManager::POWERUP_SWATTER) ? 7 :
|
||||
(type == PowerupManager::POWERUP_RUBBERBALL) ? 8 :
|
||||
(type == PowerupManager::POWERUP_PARACHUTE) ? 9 :
|
||||
-1 ;
|
||||
|
||||
return code;
|
||||
} // enumToCode
|
||||
|
||||
/* Returns the attachment enum value for a given replay code */
|
||||
Attachment::AttachmentType ReplayRecorder::codeToEnumAttach (int code)
|
||||
{
|
||||
Attachment::AttachmentType type =
|
||||
(code == 0) ? Attachment::ATTACH_NOTHING :
|
||||
(code == 1) ? Attachment::ATTACH_PARACHUTE :
|
||||
(code == 2) ? Attachment::ATTACH_ANVIL :
|
||||
(code == 3) ? Attachment::ATTACH_BOMB :
|
||||
(code == 4) ? Attachment::ATTACH_SWATTER :
|
||||
(code == 5) ? Attachment::ATTACH_BUBBLEGUM_SHIELD :
|
||||
Attachment::ATTACH_NOTHING ;
|
||||
|
||||
return type;
|
||||
} // codeToEnumAttach
|
||||
|
||||
/* Returns the item enum value for a given replay code */
|
||||
PowerupManager::PowerupType ReplayRecorder::codeToEnumItem (int code)
|
||||
{
|
||||
PowerupManager::PowerupType type =
|
||||
(code == 0) ? PowerupManager::POWERUP_NOTHING :
|
||||
(code == 1) ? PowerupManager::POWERUP_BUBBLEGUM :
|
||||
(code == 2) ? PowerupManager::POWERUP_CAKE :
|
||||
(code == 3) ? PowerupManager::POWERUP_BOWLING :
|
||||
(code == 4) ? PowerupManager::POWERUP_ZIPPER :
|
||||
(code == 5) ? PowerupManager::POWERUP_PLUNGER :
|
||||
(code == 6) ? PowerupManager::POWERUP_SWITCH :
|
||||
(code == 7) ? PowerupManager::POWERUP_SWATTER :
|
||||
(code == 8) ? PowerupManager::POWERUP_RUBBERBALL :
|
||||
(code == 9) ? PowerupManager::POWERUP_PARACHUTE :
|
||||
PowerupManager::POWERUP_NOTHING ;
|
||||
|
||||
return type;
|
||||
} // codeToEnumItem
|
||||
|
@ -19,6 +19,8 @@
|
||||
#ifndef HEADER_REPLAY_RECORDER_HPP
|
||||
#define HEADER_REPLAY_RECORDER_HPP
|
||||
|
||||
#include "items/attachment.hpp"
|
||||
#include "items/powerup_manager.hpp"
|
||||
#include "karts/controller/kart_control.hpp"
|
||||
#include "replay/replay_base.hpp"
|
||||
|
||||
@ -38,6 +40,9 @@ private:
|
||||
/** A separate vector of Replay Events for all physic info. */
|
||||
std::vector< std::vector<PhysicInfo> > m_physic_info;
|
||||
|
||||
/** A separate vector of Replay Events for all item/nitro info. */
|
||||
std::vector< std::vector<BonusInfo> > m_bonus_info;
|
||||
|
||||
/** A separate vector of Replay Events for all other events. */
|
||||
std::vector< std::vector<KartReplayEvent> > m_kart_replay_event;
|
||||
|
||||
@ -54,6 +59,18 @@ private:
|
||||
|
||||
bool m_incorrect_replay;
|
||||
|
||||
unsigned int m_max_frames;
|
||||
|
||||
// Stores the steering value at the previous transform.
|
||||
// Used to trigger the recording of new transforms.
|
||||
float m_previous_steer = 0.0f;
|
||||
|
||||
const float DISTANCE_FAST_UPDATES = 10.0f;
|
||||
|
||||
const float DISTANCE_MAX_UPDATES = 1.0f;
|
||||
|
||||
uint64_t m_last_uid;
|
||||
|
||||
#ifdef DEBUG
|
||||
/** Counts overall number of events stored. */
|
||||
unsigned int m_count;
|
||||
@ -65,6 +82,9 @@ private:
|
||||
unsigned int m_count_skipped_interpolation;
|
||||
#endif
|
||||
|
||||
/** Compute the replay's UID ; partly based on race data ; partly randomly */
|
||||
uint64_t computeUID(float min_time);
|
||||
|
||||
|
||||
ReplayRecorder();
|
||||
~ReplayRecorder();
|
||||
@ -74,6 +94,16 @@ public:
|
||||
void save();
|
||||
void update(int ticks);
|
||||
|
||||
const uint64_t getLastUID() { return m_last_uid; }
|
||||
|
||||
/** Functions to encode and decode attahcments and item types,
|
||||
so that the stored value is independent from internal
|
||||
representation and resilient to such changes. */
|
||||
static int enumToCode (Attachment::AttachmentType type);
|
||||
static int enumToCode (PowerupManager::PowerupType type);
|
||||
static Attachment::AttachmentType codeToEnumAttach (int code);
|
||||
static PowerupManager::PowerupType codeToEnumItem (int code);
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** Creates a new instance of the replay object. */
|
||||
static void create() {
|
||||
@ -89,8 +119,9 @@ public:
|
||||
static void destroy() { delete m_replay_recorder; m_replay_recorder=NULL; }
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns the filename that was opened. */
|
||||
virtual const std::string& getReplayFilename() const { return m_filename; }
|
||||
virtual const std::string& getReplayFilename(int replay_file_number = 1) const { return m_filename; }
|
||||
// ------------------------------------------------------------------------
|
||||
}; // ReplayRecorder
|
||||
|
||||
#endif
|
||||
|
||||
|
@ -19,24 +19,59 @@
|
||||
#include "states_screens/dialogs/ghost_replay_info_dialog.hpp"
|
||||
|
||||
#include "config/player_manager.hpp"
|
||||
#include "graphics/stk_tex_manager.hpp"
|
||||
#include "replay/replay_play.hpp"
|
||||
#include "states_screens/ghost_replay_selection.hpp"
|
||||
#include "states_screens/state_manager.hpp"
|
||||
#include "tracks/track.hpp"
|
||||
#include "tracks/track_manager.hpp"
|
||||
|
||||
using namespace GUIEngine;
|
||||
using namespace irr::core;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
GhostReplayInfoDialog::GhostReplayInfoDialog(unsigned int replay_id)
|
||||
: ModalDialog(0.8f,0.5f), m_replay_id(replay_id)
|
||||
GhostReplayInfoDialog::GhostReplayInfoDialog(unsigned int replay_id,
|
||||
uint64_t compare_replay_uid, bool compare_ghost)
|
||||
: ModalDialog(0.85f,0.65f), m_replay_id(replay_id)
|
||||
{
|
||||
m_self_destroy = false;
|
||||
m_record_race = false;
|
||||
m_watch_only = false;
|
||||
m_self_destroy = false;
|
||||
m_record_race = false;
|
||||
m_watch_only = false;
|
||||
|
||||
m_compare_ghost = compare_ghost;
|
||||
m_compare_replay_uid = compare_replay_uid;
|
||||
|
||||
m_rd = ReplayPlay::get()->getReplayData(m_replay_id);
|
||||
|
||||
loadFromFile("ghost_replay_info_dialog.stkgui");
|
||||
|
||||
Track* track = track_manager->getTrack(m_rd.m_track_name);
|
||||
|
||||
m_track_screenshot_widget = getWidget<IconButtonWidget>("track_screenshot");
|
||||
m_track_screenshot_widget->setFocusable(false);
|
||||
m_track_screenshot_widget->m_tab_stop = false;
|
||||
|
||||
// temporary icon, will replace it just after (but it will be shown if the given icon is not found)
|
||||
m_track_screenshot_widget->m_properties[PROP_ICON] = "gui/main_help.png";
|
||||
|
||||
irr::video::ITexture* image = STKTexManager::getInstance()
|
||||
->getTexture(track->getScreenshotFile(),
|
||||
"While loading screenshot for track '%s':", track->getFilename());
|
||||
if(!image)
|
||||
{
|
||||
image = STKTexManager::getInstance()->getTexture("main_help.png",
|
||||
"While loading screenshot for track '%s':", track->getFilename());
|
||||
}
|
||||
if (image != NULL)
|
||||
m_track_screenshot_widget->setImage(image);
|
||||
|
||||
// TODO : small refinement, add the possibility to tab stops for lists
|
||||
// to make this unselectable by keyboard/mouse
|
||||
m_replay_info_widget = getWidget<GUIEngine::ListWidget>("current_replay_info");
|
||||
assert(m_replay_info_widget != NULL);
|
||||
|
||||
updateReplayDisplayedInfo();
|
||||
|
||||
LabelWidget *name = getWidget<LabelWidget>("name");
|
||||
assert(name);
|
||||
name->setText(stringw((m_rd.m_custom_replay_file ? StringUtils::getBasename
|
||||
@ -50,6 +85,7 @@ GhostReplayInfoDialog::GhostReplayInfoDialog(unsigned int replay_id)
|
||||
m_action_widget = getWidget<RibbonWidget>("actions");
|
||||
m_record_widget = getWidget<CheckBoxWidget>("record-race");
|
||||
m_watch_widget = getWidget<CheckBoxWidget>("watch-only");
|
||||
m_compare_widget = getWidget<CheckBoxWidget>("compare-ghost");
|
||||
|
||||
if (race_manager->getNumLocalPlayers() > 1)
|
||||
{
|
||||
@ -59,17 +95,96 @@ GhostReplayInfoDialog::GhostReplayInfoDialog(unsigned int replay_id)
|
||||
}
|
||||
|
||||
m_record_widget->setState(false);
|
||||
m_watch_widget->setState(false);
|
||||
m_watch_widget->setState(m_compare_ghost);
|
||||
m_compare_widget->setState(m_compare_ghost);
|
||||
|
||||
if (m_compare_ghost)
|
||||
{
|
||||
m_watch_only = true;
|
||||
m_record_race = false;
|
||||
m_record_widget->setState(false);
|
||||
m_record_widget->setVisible(!m_watch_only);
|
||||
getWidget<LabelWidget>("record-race-text")->setVisible(!m_watch_only);
|
||||
}
|
||||
|
||||
// Display this checkbox only if there is another replay file to compare with
|
||||
getWidget<LabelWidget>("compare-ghost-text")->setVisible(m_compare_ghost);
|
||||
m_compare_widget->setVisible(m_compare_ghost);
|
||||
|
||||
|
||||
m_action_widget->setFocusForPlayer(PLAYER_ID_GAME_MASTER);
|
||||
m_action_widget->select("start", PLAYER_ID_GAME_MASTER);
|
||||
} // GhostReplayInfoDialog
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
GhostReplayInfoDialog::~GhostReplayInfoDialog()
|
||||
{
|
||||
} // ~GhostReplayInfoDialog
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
void GhostReplayInfoDialog::updateReplayDisplayedInfo()
|
||||
{
|
||||
m_replay_info_widget->clear();
|
||||
|
||||
bool is_linear = GhostReplaySelection::getInstance()->isActiveModeLinear();
|
||||
std::vector<GUIEngine::ListWidget::ListCell> row;
|
||||
|
||||
// Display the header in normal cells
|
||||
// as the header doesn't work with modal dialogs
|
||||
if (is_linear)
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(_("Reverse"), -1, 3, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(_("Difficulty"), -1, 4, true));
|
||||
if (is_linear)
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(_("Laps"), -1, 3, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(_("Time"), -1, 4, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(_("User"), -1, 5, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(_("Version"), -1, 3, true));
|
||||
|
||||
m_replay_info_widget->addItem(StringUtils::toString(0), row);
|
||||
|
||||
// Now display the data from the selected replay,
|
||||
// and if applicable from the replay to compare with
|
||||
int num_replays_to_list = (m_compare_ghost) ? 2 : 1;
|
||||
|
||||
for (int i=1; i<=num_replays_to_list; i++)
|
||||
{
|
||||
row.clear();
|
||||
|
||||
int id;
|
||||
|
||||
if (i==1)
|
||||
id = m_replay_id;
|
||||
else
|
||||
id = ReplayPlay::get()->getReplayIdByUID(m_compare_replay_uid);
|
||||
|
||||
const ReplayPlay::ReplayData& rd = ReplayPlay::get()->getReplayData(id);
|
||||
|
||||
if (is_linear)
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(rd.m_reverse ? _("Yes") : _("No"), -1, 3, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(race_manager->
|
||||
getDifficultyName((RaceManager::Difficulty) rd.m_difficulty),
|
||||
-1, 4, true));
|
||||
if (is_linear)
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(StringUtils::toWString(rd.m_laps), -1, 3, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(StringUtils::toWString(rd.m_min_time) + L"s", -1, 4, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(rd.m_user_name.empty() ? " " : rd.m_user_name, -1, 5, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(rd.m_stk_version.empty() ? " " : rd.m_stk_version, -1, 3, true));
|
||||
|
||||
m_replay_info_widget->addItem(StringUtils::toString(i), row);
|
||||
} // for num_replays_to_list
|
||||
} // updateReplayDisplayedInfo
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
GUIEngine::EventPropagation
|
||||
GhostReplayInfoDialog::processEvent(const std::string& event_source)
|
||||
@ -82,6 +197,9 @@ GUIEngine::EventPropagation
|
||||
|
||||
if(selection == "start")
|
||||
{
|
||||
// Make sure to enable the correct race mode
|
||||
race_manager->setMinorMode(GhostReplaySelection::getInstance()->getActiveMode());
|
||||
|
||||
bool reverse = m_rd.m_reverse;
|
||||
std::string track_name = m_rd.m_track_name;
|
||||
int laps = m_rd.m_laps;
|
||||
@ -89,11 +207,21 @@ GUIEngine::EventPropagation
|
||||
|
||||
race_manager->setRecordRace(m_record_race);
|
||||
race_manager->setWatchingReplay(m_watch_only);
|
||||
|
||||
ModalDialog::dismiss();
|
||||
|
||||
ReplayPlay::get()->setReplayFile(replay_id);
|
||||
if (m_compare_ghost)
|
||||
{
|
||||
int second_replay_id = ReplayPlay::get()->getReplayIdByUID(m_compare_replay_uid);
|
||||
ReplayPlay::get()->setSecondReplayFile(second_replay_id, /* use a second replay*/ true);
|
||||
m_compare_ghost = false;
|
||||
}
|
||||
else
|
||||
ReplayPlay::get()->setSecondReplayFile(0, /* use a second replay*/ false);
|
||||
|
||||
race_manager->setRaceGhostKarts(true);
|
||||
|
||||
// The race manager automatically adds karts for the ghosts
|
||||
// so only set it to the number of human players
|
||||
race_manager->setNumKarts(race_manager->getNumLocalPlayers());
|
||||
|
||||
// Disable accidentally unlocking of a challenge
|
||||
@ -101,6 +229,11 @@ GUIEngine::EventPropagation
|
||||
|
||||
race_manager->setReverseTrack(reverse);
|
||||
|
||||
//Reset comparison if active
|
||||
GhostReplaySelection::getInstance()->setCompare(false);
|
||||
|
||||
ModalDialog::dismiss();
|
||||
|
||||
if (race_manager->isWatchingReplay())
|
||||
race_manager->startWatchingReplay(track_name, laps);
|
||||
else
|
||||
@ -108,6 +241,19 @@ GUIEngine::EventPropagation
|
||||
|
||||
return GUIEngine::EVENT_BLOCK;
|
||||
}
|
||||
else if(selection == "add-ghost-to-compare")
|
||||
{
|
||||
// First set values for comparison
|
||||
m_compare_replay_uid = m_rd.m_replay_uid;
|
||||
|
||||
m_compare_ghost = true;
|
||||
|
||||
refreshMainScreen();
|
||||
|
||||
// Now quit the dialog
|
||||
m_self_destroy = true;
|
||||
return GUIEngine::EVENT_BLOCK;
|
||||
}
|
||||
else if(selection == "remove")
|
||||
{
|
||||
std::string fn = m_rd.m_filename;
|
||||
@ -135,6 +281,32 @@ GUIEngine::EventPropagation
|
||||
m_record_widget->setState(false);
|
||||
m_record_widget->setVisible(!m_watch_only);
|
||||
getWidget<LabelWidget>("record-race-text")->setVisible(!m_watch_only);
|
||||
if (!m_watch_only && m_compare_ghost)
|
||||
{
|
||||
m_compare_ghost = false;
|
||||
m_compare_widget->setState(false);
|
||||
refreshMainScreen();
|
||||
|
||||
m_replay_id = ReplayPlay::get()->getReplayIdByUID(m_rd.m_replay_uid);
|
||||
}
|
||||
}
|
||||
|
||||
else if (event_source == "compare-ghost")
|
||||
{
|
||||
m_compare_ghost = m_compare_widget->getState();
|
||||
m_record_race = false;
|
||||
m_record_widget->setState(false);
|
||||
if (m_compare_ghost)
|
||||
{
|
||||
m_watch_only = true;
|
||||
m_watch_widget->setState(true);
|
||||
}
|
||||
m_record_widget->setVisible(!m_watch_only);
|
||||
getWidget<LabelWidget>("record-race-text")->setVisible(!m_watch_only);
|
||||
|
||||
refreshMainScreen();
|
||||
|
||||
m_replay_id = ReplayPlay::get()->getReplayIdByUID(m_rd.m_replay_uid);
|
||||
}
|
||||
|
||||
return GUIEngine::EVENT_LET;
|
||||
@ -153,7 +325,18 @@ void GhostReplayInfoDialog::onUpdate(float dt)
|
||||
{
|
||||
if (m_self_destroy)
|
||||
{
|
||||
ModalDialog::clearWindow();
|
||||
ModalDialog::dismiss();
|
||||
return;
|
||||
}
|
||||
} // onUpdate
|
||||
|
||||
void GhostReplayInfoDialog::refreshMainScreen()
|
||||
{
|
||||
GhostReplaySelection::getInstance()->setCompare(m_compare_ghost);
|
||||
GhostReplaySelection::getInstance()->setCompareReplayUid(m_compare_replay_uid);
|
||||
|
||||
// Refresh the list to have only compatible replays
|
||||
dynamic_cast<GhostReplaySelection*>(GUIEngine::getCurrentScreen())
|
||||
->refresh();
|
||||
}
|
||||
|
@ -20,9 +20,7 @@
|
||||
#define HEADER_GHOST_REPLAY_INFO_DIALOG_HPP
|
||||
|
||||
#include "guiengine/modaldialog.hpp"
|
||||
#include "guiengine/widgets/check_box_widget.hpp"
|
||||
#include "guiengine/widgets/icon_button_widget.hpp"
|
||||
#include "guiengine/widgets/ribbon_widget.hpp"
|
||||
#include "guiengine/widgets.hpp"
|
||||
#include "replay/replay_play.hpp"
|
||||
|
||||
/** \brief Dialog that allows a user to do action with ghost replay file
|
||||
@ -33,21 +31,32 @@ class GhostReplayInfoDialog : public GUIEngine::ModalDialog
|
||||
|
||||
private:
|
||||
|
||||
bool m_self_destroy;
|
||||
bool m_self_destroy;
|
||||
|
||||
bool m_record_race;
|
||||
bool m_watch_only;
|
||||
bool m_record_race;
|
||||
bool m_watch_only;
|
||||
bool m_compare_ghost;
|
||||
|
||||
unsigned int m_replay_id; // May be updated on list refreshes
|
||||
|
||||
uint64_t m_compare_replay_uid;
|
||||
|
||||
const unsigned int m_replay_id;
|
||||
ReplayPlay::ReplayData m_rd;
|
||||
|
||||
GUIEngine::RibbonWidget* m_action_widget;
|
||||
GUIEngine::IconButtonWidget* m_back_widget;
|
||||
GUIEngine::CheckBoxWidget* m_record_widget;
|
||||
GUIEngine::CheckBoxWidget* m_watch_widget;
|
||||
GUIEngine::RibbonWidget* m_action_widget;
|
||||
GUIEngine::IconButtonWidget* m_back_widget;
|
||||
GUIEngine::CheckBoxWidget* m_record_widget;
|
||||
GUIEngine::CheckBoxWidget* m_watch_widget;
|
||||
GUIEngine::CheckBoxWidget* m_compare_widget;
|
||||
|
||||
GUIEngine::ListWidget* m_replay_info_widget;
|
||||
GUIEngine::IconButtonWidget* m_track_screenshot_widget;
|
||||
|
||||
void updateReplayDisplayedInfo();
|
||||
void refreshMainScreen();
|
||||
|
||||
public:
|
||||
GhostReplayInfoDialog(unsigned int replay_id);
|
||||
GhostReplayInfoDialog(unsigned int replay_id, uint64_t compare_replay_uid, bool compare_ghost);
|
||||
~GhostReplayInfoDialog();
|
||||
|
||||
GUIEngine::EventPropagation processEvent(const std::string& eventSource);
|
||||
|
@ -33,6 +33,8 @@ using namespace GUIEngine;
|
||||
GhostReplaySelection::GhostReplaySelection() : Screen("ghost_replay_selection.stkgui")
|
||||
{
|
||||
m_sort_desc = true;
|
||||
m_is_comparing = false;
|
||||
m_replay_to_compare_uid = 0;
|
||||
} // GhostReplaySelection
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@ -45,11 +47,22 @@ GhostReplaySelection::~GhostReplaySelection()
|
||||
// ----------------------------------------------------------------------------
|
||||
/** Triggers a refresh of the replay file list.
|
||||
*/
|
||||
void GhostReplaySelection::refresh(bool forced_update)
|
||||
void GhostReplaySelection::refresh(bool forced_update, bool update_columns)
|
||||
{
|
||||
if (ReplayPlay::get()->getNumReplayFile() == 0 || forced_update)
|
||||
ReplayPlay::get()->loadAllReplayFile();
|
||||
loadList();
|
||||
|
||||
// Allow to disable a comparison, but not to start one
|
||||
m_compare_toggle_widget->setVisible(m_is_comparing);
|
||||
m_compare_toggle_widget->setState(m_is_comparing);
|
||||
getWidget<LabelWidget>("compare-toggle-text")->setVisible(m_is_comparing);
|
||||
|
||||
if (update_columns)
|
||||
{
|
||||
m_replay_list_widget->clearColumns();
|
||||
beforeAddingWidget();//Reload the columns used
|
||||
}
|
||||
} // refresh
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@ -60,10 +73,32 @@ void GhostReplaySelection::loadedFromFile()
|
||||
m_replay_list_widget = getWidget<GUIEngine::ListWidget>("replay_list");
|
||||
assert(m_replay_list_widget != NULL);
|
||||
m_replay_list_widget->setColumnListener(this);
|
||||
|
||||
m_replay_difficulty_toggle_widget =
|
||||
getWidget<GUIEngine::CheckBoxWidget>("replay_difficulty_toggle");
|
||||
m_replay_difficulty_toggle_widget->setState(true);
|
||||
m_replay_difficulty_toggle_widget->setState(/* default value */ true);
|
||||
m_same_difficulty = m_replay_difficulty_toggle_widget->getState();
|
||||
|
||||
m_replay_version_toggle_widget =
|
||||
getWidget<GUIEngine::CheckBoxWidget>("replay_version_toggle");
|
||||
m_replay_version_toggle_widget->setState(/* default value */ true);
|
||||
m_same_version = m_replay_version_toggle_widget->getState();
|
||||
|
||||
m_best_times_toggle_widget =
|
||||
getWidget<GUIEngine::CheckBoxWidget>("best_times_toggle");
|
||||
m_best_times_toggle_widget->setState(/* default value */ false);
|
||||
m_best_times = m_best_times_toggle_widget->getState();
|
||||
|
||||
m_compare_toggle_widget =
|
||||
getWidget<GUIEngine::CheckBoxWidget>("compare_toggle");
|
||||
m_compare_toggle_widget->setState(/* default value */ false);
|
||||
m_is_comparing = false;
|
||||
m_compare_toggle_widget->setVisible(false);
|
||||
getWidget<LabelWidget>("compare-toggle-text")->setVisible(false);
|
||||
|
||||
m_mode_tabs = getWidget<GUIEngine::RibbonWidget>("race_mode");
|
||||
m_active_mode = RaceManager::MINOR_MODE_TIME_TRIAL;
|
||||
m_active_mode_is_linear = true;
|
||||
} // loadedFromFile
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@ -71,14 +106,20 @@ void GhostReplaySelection::loadedFromFile()
|
||||
*/
|
||||
void GhostReplaySelection::beforeAddingWidget()
|
||||
{
|
||||
m_replay_list_widget->clearColumns();
|
||||
m_replay_list_widget->addColumn( _("Track"), 3 );
|
||||
m_replay_list_widget->addColumn( _("Players"), 1);
|
||||
m_replay_list_widget->addColumn( _("Reverse"), 1);
|
||||
m_replay_list_widget->addColumn( _("Difficulty"), 1);
|
||||
m_replay_list_widget->addColumn( _("Laps"), 1);
|
||||
m_replay_list_widget->addColumn( _("Finish Time"), 1);
|
||||
m_replay_list_widget->addColumn( _("User"), 1);
|
||||
m_replay_list_widget->addColumn( _("Track"), 9 );
|
||||
m_replay_list_widget->addColumn( _("Players"), 3);
|
||||
if (m_active_mode_is_linear)
|
||||
m_replay_list_widget->addColumn( _("Reverse"), 3);
|
||||
if (!m_same_difficulty)
|
||||
m_replay_list_widget->addColumn( _("Difficulty"), 4);
|
||||
if (m_active_mode_is_linear)
|
||||
m_replay_list_widget->addColumn( _("Laps"), 3);
|
||||
m_replay_list_widget->addColumn( _("Time"), 4);
|
||||
m_replay_list_widget->addColumn( _("User"), 5);
|
||||
if (!m_same_version)
|
||||
m_replay_list_widget->addColumn( _("Version"), 3);
|
||||
|
||||
m_replay_list_widget->createHeader();
|
||||
} // beforeAddingWidget
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@ -86,7 +127,7 @@ void GhostReplaySelection::init()
|
||||
{
|
||||
Screen::init();
|
||||
m_cur_difficulty = race_manager->getDifficulty();
|
||||
refresh(/*forced_update*/false);
|
||||
refresh(/*reload replay files*/ false, /* update columns */ true);
|
||||
} // init
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@ -97,36 +138,193 @@ void GhostReplaySelection::loadList()
|
||||
{
|
||||
ReplayPlay::get()->sortReplay(m_sort_desc);
|
||||
m_replay_list_widget->clear();
|
||||
|
||||
if (ReplayPlay::get()->getNumReplayFile() == 0)
|
||||
return;
|
||||
|
||||
if (m_best_times)
|
||||
{
|
||||
//First of all, clear the best time index
|
||||
m_best_times_index.clear();
|
||||
|
||||
// This is in O(N*M) ; N being the number of replay files
|
||||
// and M the number of different configuration (which is
|
||||
// the final size of the best times list).
|
||||
// Each time has to be compared against the previous best times
|
||||
// up until all are checked or a replay with the same configuration
|
||||
// is found.
|
||||
for (unsigned int i = 0; i < ReplayPlay::get()->getNumReplayFile() ; i++)
|
||||
{
|
||||
const ReplayPlay::ReplayData& rd = ReplayPlay::get()->getReplayData(i);
|
||||
|
||||
if (m_same_difficulty && m_cur_difficulty !=
|
||||
(RaceManager::Difficulty)rd.m_difficulty)
|
||||
continue;
|
||||
|
||||
core::stringw current_version = STK_VERSION;
|
||||
if (m_same_version && current_version != rd.m_stk_version)
|
||||
continue;
|
||||
|
||||
Track* track = track_manager->getTrack(rd.m_track_name);
|
||||
|
||||
if (track == NULL)
|
||||
continue;
|
||||
|
||||
// If no other replay with the same configuration is found in the index
|
||||
// this is the best time
|
||||
bool is_best_time = true;
|
||||
bool replace_old_best = false;
|
||||
// This is the position inside the best_times_index itself,
|
||||
// not inside the full list of replay files
|
||||
unsigned int index_old_best = 0;
|
||||
|
||||
for (unsigned int j = 0; j < m_best_times_index.size() ; j++)
|
||||
{
|
||||
// The replay in the best times index conform to the version and difficulty settings,
|
||||
// no need to check this again.
|
||||
const ReplayPlay::ReplayData& bt = ReplayPlay::get()->getReplayData(m_best_times_index[j]);
|
||||
|
||||
// If it's not the same track, check further in the index
|
||||
if (rd.m_track_name != bt.m_track_name)
|
||||
continue;
|
||||
|
||||
// If it's not the same difficulty, check further in the index
|
||||
if (rd.m_difficulty != bt.m_difficulty)
|
||||
continue;
|
||||
|
||||
// If it's not the same direction, check further in the index
|
||||
if (rd.m_reverse != bt.m_reverse)
|
||||
continue;
|
||||
|
||||
// If it's not the same lap numbers, check further in the index
|
||||
if (rd.m_laps != bt.m_laps)
|
||||
continue;
|
||||
|
||||
// The replay data have the same properties, compare the times
|
||||
if (rd.m_min_time < bt.m_min_time)
|
||||
{
|
||||
replace_old_best = true;
|
||||
index_old_best = j;
|
||||
}
|
||||
else
|
||||
is_best_time = false;
|
||||
|
||||
//No need to compare against other best times
|
||||
break;
|
||||
}
|
||||
|
||||
if (is_best_time)
|
||||
{
|
||||
// Update the value
|
||||
if (replace_old_best)
|
||||
{
|
||||
m_best_times_index[index_old_best] = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_best_times_index.push_back(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the best times index for faster lookup
|
||||
if (m_best_times && m_best_times_index.size() > 1)
|
||||
{
|
||||
// std::sort sorts by default in ascending order (0,1,2...)
|
||||
std::sort (m_best_times_index.begin(), m_best_times_index.end());
|
||||
}
|
||||
|
||||
unsigned int best_index = 0;
|
||||
|
||||
// getReplayIdByUID will send 0 if the UID is incorrect,
|
||||
// and m_is_comparing will be false if it is incorrect,
|
||||
// so it always work
|
||||
unsigned int compare_index = ReplayPlay::get()->getReplayIdByUID(m_replay_to_compare_uid);
|
||||
const ReplayPlay::ReplayData& rd_compare = ReplayPlay::get()->getReplayData(compare_index);
|
||||
|
||||
for (unsigned int i = 0; i < ReplayPlay::get()->getNumReplayFile() ; i++)
|
||||
{
|
||||
if (m_best_times)
|
||||
{
|
||||
// All best times have already been added
|
||||
if (best_index + 1 > m_best_times_index.size())
|
||||
break;
|
||||
// This works because the best times index is already sorted
|
||||
else if (m_best_times_index[best_index] == i)
|
||||
best_index++;
|
||||
// There are still best times to display
|
||||
// The current i don't correspond to a best time
|
||||
else
|
||||
continue;
|
||||
}
|
||||
|
||||
const ReplayPlay::ReplayData& rd = ReplayPlay::get()->getReplayData(i);
|
||||
|
||||
if (m_same_difficulty && m_cur_difficulty !=
|
||||
(RaceManager::Difficulty)rd.m_difficulty)
|
||||
continue;
|
||||
|
||||
core::stringw current_version = STK_VERSION;
|
||||
if (m_same_version && current_version != rd.m_stk_version)
|
||||
continue;
|
||||
|
||||
// Only display replays comparable with the replay selected for comparison
|
||||
if (m_is_comparing)
|
||||
{
|
||||
// If it's not the same track, check further in the index
|
||||
if (rd.m_track_name != rd_compare.m_track_name)
|
||||
continue;
|
||||
|
||||
// If it's not the same direction, check further in the index
|
||||
if (rd.m_reverse != rd_compare.m_reverse)
|
||||
continue;
|
||||
|
||||
// If it's not the same lap numbers, check further in the index
|
||||
if (rd.m_laps != rd_compare.m_laps)
|
||||
continue;
|
||||
|
||||
// Don't compare a replay with itself
|
||||
if (compare_index == i)
|
||||
continue;
|
||||
}
|
||||
// Only display replays matching the current mode
|
||||
if (m_active_mode == RaceManager::MINOR_MODE_TIME_TRIAL &&
|
||||
rd.m_minor_mode != "time-trial")
|
||||
continue;
|
||||
else if (m_active_mode == RaceManager::MINOR_MODE_EASTER_EGG &&
|
||||
rd.m_minor_mode != "egg-hunt")
|
||||
continue;
|
||||
|
||||
Track* track = track_manager->getTrack(rd.m_track_name);
|
||||
|
||||
if (track == NULL)
|
||||
continue;
|
||||
|
||||
std::vector<GUIEngine::ListWidget::ListCell> row;
|
||||
//The third argument should match the numbers used in beforeAddingWidget
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(translations->fribidize(track->getName()) , -1, 3));
|
||||
(translations->fribidize(track->getName()) , -1, 9));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(StringUtils::toWString(rd.m_kart_list.size()), -1, 1, true));
|
||||
(StringUtils::toWString(rd.m_kart_list.size()), -1, 3, true));
|
||||
if (m_active_mode_is_linear)
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(rd.m_reverse ? _("Yes") : _("No"), -1, 3, true));
|
||||
if (!m_same_difficulty)
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(race_manager->
|
||||
getDifficultyName((RaceManager::Difficulty) rd.m_difficulty),
|
||||
-1, 4, true));
|
||||
if (m_active_mode_is_linear)
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(StringUtils::toWString(rd.m_laps), -1, 3, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(rd.m_reverse ? _("Yes") : _("No"), -1, 1, true));
|
||||
(StringUtils::toWString(rd.m_min_time) + L"s", -1, 4, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(rd.m_difficulty == 3 ? _("SuperTux") : rd.m_difficulty == 2 ?
|
||||
_("Expert") : rd.m_difficulty == 1 ?
|
||||
_("Intermediate") : _("Novice") , -1, 1, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(StringUtils::toWString(rd.m_laps), -1, 1, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(StringUtils::toWString(rd.m_min_time) + L"s", -1, 1, true));
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(rd.m_user_name.empty() ? " " : rd.m_user_name, -1, 1, true));
|
||||
(rd.m_user_name.empty() ? " " : rd.m_user_name, -1, 5, true));
|
||||
if (!m_same_version)
|
||||
row.push_back(GUIEngine::ListWidget::ListCell
|
||||
(rd.m_stk_version.empty() ? " " : rd.m_stk_version, -1, 3, true));
|
||||
m_replay_list_widget->addItem(StringUtils::toString(i), row);
|
||||
}
|
||||
} // loadList
|
||||
@ -156,17 +354,48 @@ void GhostReplaySelection::eventCallback(GUIEngine::Widget* widget,
|
||||
{
|
||||
return;
|
||||
}
|
||||
new GhostReplayInfoDialog(selected_index);
|
||||
|
||||
new GhostReplayInfoDialog(selected_index, m_replay_to_compare_uid, m_is_comparing);
|
||||
} // click on replay file
|
||||
else if (name == "race_mode")
|
||||
{
|
||||
std::string selection = ((RibbonWidget*)widget)->getSelectionIDString(PLAYER_ID_GAME_MASTER);
|
||||
|
||||
if (selection == "tab_time_trial")
|
||||
m_active_mode = RaceManager::MINOR_MODE_TIME_TRIAL;
|
||||
else if (selection == "tab_egg_hunt")
|
||||
m_active_mode = RaceManager::MINOR_MODE_EASTER_EGG;
|
||||
|
||||
m_active_mode_is_linear = race_manager->isLinearRaceMode(m_active_mode);
|
||||
m_is_comparing = false;
|
||||
m_compare_toggle_widget->setState(false);
|
||||
refresh(/*reload replay files*/ false, /* update columns */ true);
|
||||
}
|
||||
else if (name == "record-ghost")
|
||||
{
|
||||
race_manager->setRecordRace(true);
|
||||
race_manager->setMinorMode(m_active_mode);
|
||||
TracksScreen::getInstance()->push();
|
||||
}
|
||||
else if (name == "replay_difficulty_toggle")
|
||||
{
|
||||
m_same_difficulty = m_replay_difficulty_toggle_widget->getState();
|
||||
refresh(/*forced_update*/false);
|
||||
refresh(/*reload replay files*/ false, /* update columns */ true);
|
||||
}
|
||||
else if (name == "replay_version_toggle")
|
||||
{
|
||||
m_same_version = m_replay_version_toggle_widget->getState();
|
||||
refresh(/*reload replay files*/ false, /* update columns */ true);
|
||||
}
|
||||
else if (name == "best_times_toggle")
|
||||
{
|
||||
m_best_times = m_best_times_toggle_widget->getState();
|
||||
refresh(/*reload replay files*/ false);
|
||||
}
|
||||
else if (name == "compare_toggle")
|
||||
{
|
||||
m_is_comparing = m_compare_toggle_widget->getState();
|
||||
refresh(/*reload replay files*/ false);
|
||||
}
|
||||
|
||||
} // eventCallback
|
||||
@ -197,33 +426,39 @@ void GhostReplaySelection::onConfirm()
|
||||
*/
|
||||
void GhostReplaySelection::onColumnClicked(int column_id)
|
||||
{
|
||||
switch (column_id)
|
||||
{
|
||||
case 0:
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_TRACK);
|
||||
break;
|
||||
case 1:
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_KART_NUM);
|
||||
break;
|
||||
case 2:
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_REV);
|
||||
break;
|
||||
case 3:
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_DIFF);
|
||||
break;
|
||||
case 4:
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_LAPS);
|
||||
break;
|
||||
case 5:
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_TIME);
|
||||
break;
|
||||
case 6:
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_USER);
|
||||
break;
|
||||
default:
|
||||
assert(0);
|
||||
break;
|
||||
} // switch
|
||||
int diff_difficulty = m_same_difficulty ? 1 : 0;
|
||||
int diff_linear = m_active_mode_is_linear ? 0 : 1;
|
||||
|
||||
if (column_id >= 2)
|
||||
column_id += diff_linear;
|
||||
|
||||
if (column_id >= 3)
|
||||
column_id += diff_difficulty;
|
||||
|
||||
if (column_id >= 4)
|
||||
column_id += diff_linear;
|
||||
|
||||
if (column_id == 0)
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_TRACK);
|
||||
else if (column_id == 1)
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_KART_NUM);
|
||||
else if (column_id == 2)
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_REV);
|
||||
else if (column_id == 3)
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_DIFF);
|
||||
else if (column_id == 4)
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_LAPS);
|
||||
else if (column_id == 5)
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_TIME);
|
||||
else if (column_id == 6)
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_USER);
|
||||
else if (column_id == 7)
|
||||
ReplayPlay::setSortOrder(ReplayPlay::SO_VERSION);
|
||||
else
|
||||
assert(0);
|
||||
|
||||
printf("column_id is %d\n", column_id);
|
||||
|
||||
/** \brief Toggle the sort order after column click **/
|
||||
m_sort_desc = !m_sort_desc;
|
||||
loadList();
|
||||
|
@ -44,20 +44,41 @@ private:
|
||||
|
||||
GUIEngine::ListWidget* m_replay_list_widget;
|
||||
GUIEngine::CheckBoxWidget* m_replay_difficulty_toggle_widget;
|
||||
GUIEngine::CheckBoxWidget* m_replay_version_toggle_widget;
|
||||
GUIEngine::CheckBoxWidget* m_best_times_toggle_widget;
|
||||
GUIEngine::CheckBoxWidget* m_compare_toggle_widget;
|
||||
GUIEngine::RibbonWidget* m_mode_tabs;
|
||||
RaceManager::Difficulty m_cur_difficulty;
|
||||
std::string m_file_to_be_deleted;
|
||||
std::vector<unsigned int> m_best_times_index;
|
||||
bool m_same_difficulty;
|
||||
bool m_same_version;
|
||||
bool m_best_times;
|
||||
bool m_sort_desc;
|
||||
bool m_is_comparing;
|
||||
bool m_active_mode_is_linear;
|
||||
RaceManager::MinorRaceModeType m_active_mode;
|
||||
// The index id of a replay file can change with sorting, etc.
|
||||
// Using the UID guarantees exact matchess
|
||||
uint64_t m_replay_to_compare_uid;
|
||||
|
||||
|
||||
public:
|
||||
|
||||
void refresh(bool forced_update = true);
|
||||
void setCompareReplayUid(uint64_t uid) { m_replay_to_compare_uid = uid; }
|
||||
void setCompare(bool compare) { m_is_comparing = compare; }
|
||||
|
||||
void refresh(bool forced_update = true, bool update_columns = false);
|
||||
|
||||
/** Load the addons into the main list.*/
|
||||
void loadList();
|
||||
|
||||
void onDeleteReplay(std::string& filename);
|
||||
|
||||
const RaceManager::MinorRaceModeType getActiveMode() { return m_active_mode; }
|
||||
|
||||
const bool isActiveModeLinear() { return m_active_mode_is_linear; }
|
||||
|
||||
/** \brief implement callback from parent class GUIEngine::Screen */
|
||||
virtual void loadedFromFile() OVERRIDE;
|
||||
|
||||
|
@ -71,10 +71,19 @@ RaceGUI::RaceGUI()
|
||||
// Determine maximum length of the rank/lap text, in order to
|
||||
// align those texts properly on the right side of the viewport.
|
||||
gui::ScalableFont* font = GUIEngine::getHighresDigitFont();
|
||||
core::dimension2du area = font->getDimension(L"99:99:99");
|
||||
core::dimension2du area = font->getDimension(L"99:99.99");
|
||||
m_timer_width = area.Width;
|
||||
m_font_height = area.Height;
|
||||
|
||||
area = font->getDimension(L"99.999");
|
||||
m_small_precise_timer_width = area.Width;
|
||||
|
||||
area = font->getDimension(L"99:99.999");
|
||||
m_big_precise_timer_width = area.Width;
|
||||
|
||||
area = font->getDimension(L"-");
|
||||
m_negative_timer_additional_width = area.Width;
|
||||
|
||||
if (race_manager->getMinorMode()==RaceManager::MINOR_MODE_FOLLOW_LEADER ||
|
||||
race_manager->getMinorMode()==RaceManager::MINOR_MODE_3_STRIKES ||
|
||||
race_manager->getNumLaps() > 9)
|
||||
@ -243,6 +252,11 @@ void RaceGUI::renderGlobal(float dt)
|
||||
if (world->getPhase()<WorldStatus::DELAY_FINISH_PHASE)
|
||||
drawGlobalTimer();
|
||||
|
||||
if (race_manager->isLinearRaceMode() &&
|
||||
race_manager->hasGhostKarts() &&
|
||||
race_manager->getNumberOfKarts() >= 2 )
|
||||
drawLiveDifference();
|
||||
|
||||
if(world->getPhase() == WorldStatus::GO_PHASE ||
|
||||
world->getPhase() == WorldStatus::MUSIC_PHASE)
|
||||
{
|
||||
@ -349,7 +363,7 @@ void RaceGUI::drawScores()
|
||||
} // drawScores
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Displays the racing time on the screen.s
|
||||
/** Displays the racing time on the screen.
|
||||
*/
|
||||
void RaceGUI::drawGlobalTimer()
|
||||
{
|
||||
@ -392,8 +406,10 @@ void RaceGUI::drawGlobalTimer()
|
||||
}
|
||||
}
|
||||
|
||||
core::rect<s32> pos(irr_driver->getActualScreenSize().Width - dist_from_right, 30,
|
||||
irr_driver->getActualScreenSize().Width , 50);
|
||||
core::rect<s32> pos(irr_driver->getActualScreenSize().Width - dist_from_right,
|
||||
irr_driver->getActualScreenSize().Height*2/100,
|
||||
irr_driver->getActualScreenSize().Width,
|
||||
irr_driver->getActualScreenSize().Height*6/100);
|
||||
|
||||
// special case : when 3 players play, use available 4th space for such things
|
||||
if (race_manager->getIfEmptyScreenSpaceExists())
|
||||
@ -411,6 +427,70 @@ void RaceGUI::drawGlobalTimer()
|
||||
|
||||
} // drawGlobalTimer
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Displays the live difference with a ghost on screen.
|
||||
*/
|
||||
void RaceGUI::drawLiveDifference()
|
||||
{
|
||||
assert(World::getWorld() != NULL);
|
||||
|
||||
if (!World::getWorld()->shouldDrawTimer())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const LinearWorld *linearworld = dynamic_cast<LinearWorld*>(World::getWorld());
|
||||
assert(linearworld != NULL);
|
||||
|
||||
// Don't display the live difference timer if its time is wrong
|
||||
// (before crossing the start line at start or after crossing it at end)
|
||||
if (!linearworld->hasValidTimeDifference())
|
||||
return;
|
||||
|
||||
float live_difference = linearworld->getLiveTimeDifference();
|
||||
|
||||
int timer_width = m_small_precise_timer_width;
|
||||
|
||||
// 59.9995 is the smallest number of seconds that could get rounded to 1 minute
|
||||
// when rounding at the closest ms
|
||||
if (fabsf(live_difference) >= 59.9995f)
|
||||
timer_width = m_big_precise_timer_width;
|
||||
|
||||
if (live_difference < 0.0f)
|
||||
timer_width += m_negative_timer_additional_width;
|
||||
|
||||
core::stringw sw;
|
||||
video::SColor time_color;
|
||||
|
||||
// Change color depending on value
|
||||
if (live_difference > 1.0f)
|
||||
time_color = video::SColor(255, 255, 0, 0);
|
||||
else if (live_difference > 0.0f)
|
||||
time_color = video::SColor(255, 255, 160, 0);
|
||||
else if (live_difference > -1.0f)
|
||||
time_color = video::SColor(255, 160, 255, 0);
|
||||
else
|
||||
time_color = video::SColor(255, 0, 255, 0);
|
||||
|
||||
int dist_from_right = 10 + timer_width;
|
||||
|
||||
sw = core::stringw (StringUtils::timeToString(live_difference,3,
|
||||
/* display_minutes_if_zero */ false).c_str() );
|
||||
|
||||
core::rect<s32> pos(irr_driver->getActualScreenSize().Width - dist_from_right,
|
||||
irr_driver->getActualScreenSize().Height*7/100,
|
||||
irr_driver->getActualScreenSize().Width,
|
||||
irr_driver->getActualScreenSize().Height*11/100);
|
||||
|
||||
gui::ScalableFont* font = GUIEngine::getHighresDigitFont();
|
||||
font->setShadow(video::SColor(255, 128, 0, 0));
|
||||
font->setScale(1.0f);
|
||||
font->draw(sw.c_str(), pos, time_color, false, false, NULL,
|
||||
true /* ignore RTL */);
|
||||
|
||||
} // drawLiveDifference
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
/** Draws the mini map and the position of all karts on it.
|
||||
*/
|
||||
|
@ -78,6 +78,16 @@ private:
|
||||
/** Maximum string length for the timer */
|
||||
int m_timer_width;
|
||||
|
||||
/** Maximum string length for a small precise timer
|
||||
* (like the live difference timer under a minute) */
|
||||
int m_small_precise_timer_width;
|
||||
|
||||
/** Maximum string length for a big precise timer
|
||||
* (like the live difference timer over a minute) */
|
||||
int m_big_precise_timer_width;
|
||||
|
||||
int m_negative_timer_additional_width;
|
||||
|
||||
/** Height of the digit font. */
|
||||
int m_font_height;
|
||||
|
||||
@ -120,6 +130,7 @@ private:
|
||||
/** Display items that are shown once only (for all karts). */
|
||||
void drawGlobalMiniMap ();
|
||||
void drawGlobalTimer ();
|
||||
void drawLiveDifference ();
|
||||
void drawScores();
|
||||
|
||||
|
||||
|
@ -49,6 +49,8 @@
|
||||
#include "network/stk_host.hpp"
|
||||
#include "network/protocols/client_lobby.hpp"
|
||||
#include "race/highscores.hpp"
|
||||
#include "replay/replay_play.hpp"
|
||||
#include "replay/replay_recorder.hpp"
|
||||
#include "scriptengine/property_animator.hpp"
|
||||
#include "states_screens/feature_unlocked.hpp"
|
||||
#include "states_screens/main_menu_screen.hpp"
|
||||
@ -224,7 +226,10 @@ void RaceResultGUI::enableAllButtons()
|
||||
}
|
||||
else
|
||||
{
|
||||
top->setText(_("Setup New Race"));
|
||||
if (race_manager->isRecordingRace())
|
||||
top->setText(_("Race against the new ghost replay"));
|
||||
else
|
||||
top->setText(_("Setup New Race"));
|
||||
top->setVisible(true);
|
||||
bottom->setText(_("Back to the menu"));
|
||||
}
|
||||
@ -385,6 +390,12 @@ void RaceResultGUI::eventCallback(GUIEngine::Widget* widget,
|
||||
StateManager::get()->popMenu();
|
||||
if (name == "top") // Setup new race
|
||||
{
|
||||
// Save current race data for race against new ghost
|
||||
std::string track_name = race_manager->getTrackName();
|
||||
int laps = race_manager->getNumLaps();
|
||||
bool reverse = race_manager->getReverseTrack();
|
||||
bool new_ghost_race = race_manager->isRecordingRace();
|
||||
|
||||
race_manager->exitRace();
|
||||
race_manager->setAIKartOverride("");
|
||||
|
||||
@ -394,6 +405,24 @@ void RaceResultGUI::eventCallback(GUIEngine::Widget* widget,
|
||||
StateManager::get()->resetAndGoToScreen(MainMenuScreen::getInstance());
|
||||
OverWorld::enterOverWorld();
|
||||
}
|
||||
// Special case : race against a newly saved ghost
|
||||
else if (new_ghost_race)
|
||||
{
|
||||
ReplayPlay::get()->loadAllReplayFile();
|
||||
unsigned long long int last_uid = ReplayRecorder::get()->getLastUID();
|
||||
ReplayPlay::get()->setReplayFileByUID(last_uid);
|
||||
|
||||
race_manager->setRecordRace(true);
|
||||
race_manager->setRaceGhostKarts(true);
|
||||
|
||||
race_manager->setNumKarts(race_manager->getNumLocalPlayers());
|
||||
|
||||
// Disable accidentally unlocking of a challenge
|
||||
PlayerManager::getCurrentPlayer()->setCurrentChallenge("");
|
||||
|
||||
race_manager->setReverseTrack(reverse);
|
||||
race_manager->startSingleRace(track_name, laps, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Screen* newStack[] = { MainMenuScreen::getInstance(),
|
||||
|
@ -499,37 +499,117 @@ namespace StringUtils
|
||||
} // ticksTimeToString(ticks)
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
/** Converts a time in seconds into a string of the form mm:ss:hh (minutes,
|
||||
* seconds, 1/100 seconds.
|
||||
/** Converts a time in seconds into a string of the form mm:ss.hhh (minutes,
|
||||
* seconds, milliseconds)
|
||||
* \param time Time in seconds.
|
||||
* \param precision The number of seconds decimals - 3 to display ms, default 2
|
||||
*/
|
||||
std::string timeToString(float time)
|
||||
std::string timeToString(float time, unsigned int precision, bool display_minutes_if_zero)
|
||||
{
|
||||
int int_time = (int)(time*100.0f+0.5f);
|
||||
//Time more detailed than ms are mostly meaningless
|
||||
if (precision > 3)
|
||||
precision = 3;
|
||||
|
||||
int int_time;
|
||||
int precision_power = 1;
|
||||
|
||||
for (unsigned int i=0;i<precision;i++)
|
||||
{
|
||||
precision_power *=10;
|
||||
}
|
||||
|
||||
float fprecision_power = (float) precision_power;
|
||||
|
||||
bool negative_time = (time < 0.0f);
|
||||
|
||||
// If the time is negative, make it positve
|
||||
// And add a "-" later
|
||||
if (negative_time) time *= -1.0f;
|
||||
|
||||
// cast to int truncates the value,
|
||||
// so add 0.5f to get the closest int
|
||||
|
||||
int_time = (int)(time*fprecision_power+0.5f);
|
||||
|
||||
// Avoid problems if time is negative or way too large (which
|
||||
// should only happen if something is broken in a track elsewhere,
|
||||
// and an incorrect finishing time is estimated.
|
||||
if(int_time<0)
|
||||
return std::string("00:00:00");
|
||||
else if(int_time >= 10000*60) // up to 99:59.99
|
||||
return std::string("99:59:99");
|
||||
{
|
||||
std::string final_append;
|
||||
if (precision == 3)
|
||||
final_append = ".000";
|
||||
else if (precision == 2)
|
||||
final_append = ".00";
|
||||
else if (precision == 1)
|
||||
final_append = ".0";
|
||||
else
|
||||
final_append = "";
|
||||
// concatenate the strings with +
|
||||
if (display_minutes_if_zero)
|
||||
return (std::string("00:00") + final_append);
|
||||
else
|
||||
return (std::string("00") + final_append);
|
||||
}
|
||||
else if(int_time >= 100*60*precision_power) // up to 99:59.999
|
||||
{
|
||||
std::string final_append;
|
||||
if (precision == 3)
|
||||
final_append = ".999";
|
||||
else if (precision == 2)
|
||||
final_append = ".99";
|
||||
else if (precision == 1)
|
||||
final_append = ".9";
|
||||
else
|
||||
final_append = "";
|
||||
// concatenate the strings with +
|
||||
return (std::string("99:59") + final_append);
|
||||
}
|
||||
|
||||
int min = int_time / 6000;
|
||||
int sec = (int_time-min*6000)/100;
|
||||
int hundredths = (int_time - min*6000-sec*100);
|
||||
// The proper c++ way would be:
|
||||
// std::ostringstream s;
|
||||
// s<<std::setw(2)<<std::setfill(' ')<<min<<":"
|
||||
// <<std::setw(2)<<std::setfill('0')<<sec<<":"
|
||||
// <<std::setw(2)<<std::setfill(' ')<<hundredths;
|
||||
// return s.str();
|
||||
// but that appears to be awfully complicated and slow, compared to
|
||||
// which admittedly only works for min < 100000 - which is about 68
|
||||
// days - good enough.
|
||||
char s[12];
|
||||
sprintf(s, "%02d:%02d:%02d", min, sec, hundredths);
|
||||
return std::string(s);
|
||||
// Principle of the computation in pseudo-code
|
||||
// 1) Divide by (current_time_unit_duration/next_smaller_unit_duration)
|
||||
// (1 if no smaller)
|
||||
// 2) Apply modulo (next_bigger_time_unit_duration/current_time_unit_duration)
|
||||
// (no modulo if no bigger)
|
||||
int subseconds = int_time % precision_power;
|
||||
int_time = int_time/precision_power;
|
||||
int sec = int_time % 60;
|
||||
int_time = int_time/60;
|
||||
if (int_time >= 100) int_time = 99;
|
||||
int min = int_time;
|
||||
|
||||
// Convert the times to string and add the missing zeroes if any
|
||||
|
||||
std::string s_min = std::to_string(min);
|
||||
if (min < 10)
|
||||
s_min = std::string("0") + s_min;
|
||||
std::string s_sec = std::to_string(sec);
|
||||
if (sec < 10)
|
||||
s_sec = std::string("0") + s_sec;
|
||||
std::string s_subsec = std::to_string(subseconds);
|
||||
|
||||
// If subseconds is 0 ; it is already in the string,
|
||||
// so skip one step
|
||||
for (unsigned int i=1;i<precision;i++)
|
||||
{
|
||||
precision_power = precision_power/10;
|
||||
if (subseconds < precision_power)
|
||||
s_subsec = std::string("0") + s_subsec;
|
||||
}
|
||||
|
||||
std::string s_neg = "";
|
||||
|
||||
if(negative_time)
|
||||
s_neg = "-";
|
||||
|
||||
std::string s_min_and_sec = s_sec;
|
||||
if (display_minutes_if_zero || min > 0)
|
||||
s_min_and_sec = s_min + std::string(":") + s_sec;
|
||||
|
||||
if (precision == 0)
|
||||
return (s_neg + s_min_and_sec);
|
||||
else
|
||||
return (s_neg + s_min_and_sec + std::string(".") + s_subsec);
|
||||
} // timeToString
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
@ -46,7 +46,7 @@ namespace StringUtils
|
||||
|
||||
bool notEmpty(const irr::core::stringw& input);
|
||||
std::string ticksTimeToString(int time);
|
||||
std::string timeToString(float time);
|
||||
std::string timeToString(float time, unsigned int precision=2, bool display_minutes_if_zero = true);
|
||||
irr::core::stringw loadingDots(float interval = 0.5f, int max_dots = 3);
|
||||
irr::core::stringw loadingDots(const wchar_t *s);
|
||||
std::string toUpperCase(const std::string&);
|
||||
|
Loading…
x
Reference in New Issue
Block a user