commit
8da8390773
@ -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;
|
||||
|
@ -80,7 +80,7 @@ public:
|
||||
/** \brief Get the players that are in the game
|
||||
* \return A vector containing pointers on the players profiles. */
|
||||
std::vector<std::shared_ptr<NetworkPlayerProfile> >
|
||||
getConnectedPlayers() const
|
||||
getConnectedPlayers(bool same_index_for_disconnected = false) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_players_mutex);
|
||||
std::vector<std::shared_ptr<NetworkPlayerProfile> > players;
|
||||
@ -88,6 +88,8 @@ public:
|
||||
{
|
||||
if (auto player_connected = player_weak.lock())
|
||||
players.push_back(player_connected);
|
||||
else if (same_index_for_disconnected)
|
||||
players.push_back(nullptr);
|
||||
}
|
||||
return players;
|
||||
} // getConnectedPlayers
|
||||
|
@ -27,6 +27,8 @@
|
||||
#include "network/rewind_info.hpp"
|
||||
#include "physics/physics.hpp"
|
||||
#include "race/history.hpp"
|
||||
#include "tracks/track.hpp"
|
||||
#include "tracks/track_object_manager.hpp"
|
||||
#include "utils/log.hpp"
|
||||
#include "utils/profiler.hpp"
|
||||
|
||||
@ -318,6 +320,8 @@ void RewindManager::playEventsTill(int world_ticks, int *ticks)
|
||||
void RewindManager::rewindTo(int rewind_ticks, int now_ticks)
|
||||
{
|
||||
assert(!m_is_rewinding);
|
||||
// TODO Do it properly for track objects like soccer ball
|
||||
Track::getCurrentTrack()->getTrackObjectManager()->removeForRewind();
|
||||
bool is_history = history->replayHistory();
|
||||
history->setReplayHistory(false);
|
||||
|
||||
@ -384,5 +388,6 @@ void RewindManager::rewindTo(int rewind_ticks, int now_ticks)
|
||||
}
|
||||
|
||||
history->setReplayHistory(is_history);
|
||||
Track::getCurrentTrack()->getTrackObjectManager()->addForRewind();
|
||||
m_is_rewinding = false;
|
||||
} // rewindTo
|
||||
|
@ -206,6 +206,8 @@ public:
|
||||
const Material **material, btVector3 *normal,
|
||||
bool interpolate_normal) const;
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
bool isDynamic() const { return m_is_dynamic; }
|
||||
// ------------------------------------------------------------------------
|
||||
/** Returns the ID of this physical object. */
|
||||
std::string getID() { return m_id; }
|
||||
|
@ -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
|
||||
|
||||
|
@ -529,11 +529,12 @@ namespace Scripting
|
||||
m_time = time;
|
||||
m_callback_delegate = callback_delegate;
|
||||
|
||||
// This may be not needed in future angelscript versions
|
||||
#if ANGELSCRIPT_VERSION < 23300
|
||||
if (strstr(asGetLibraryOptions(), "AS_MAX_PORTABILITY"))
|
||||
{
|
||||
callback_delegate->AddRef();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
@ -63,7 +63,6 @@ void CustomVideoSettingsDialog::beforeAddingWidgets()
|
||||
particles_effects->addLabel(_("Important only"));
|
||||
particles_effects->addLabel(_("Enabled"));
|
||||
particles_effects->setValue(UserConfigParams::m_particles_effects);
|
||||
particles_effects->setMin(1);
|
||||
|
||||
SpinnerWidget* geometry_level = getWidget<SpinnerWidget>("geometry_detail");
|
||||
//I18N: Geometry level disabled : lowest level, no details
|
||||
|
@ -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(),
|
||||
|
@ -535,6 +535,7 @@ void TrackObject::updateGraphics(float dt)
|
||||
// have been converted to use separate updateGraphics() calls.
|
||||
|
||||
if (m_physical_object) m_physical_object->updateGraphics(dt);
|
||||
if (m_animator) m_animator->update(dt);
|
||||
|
||||
} // update
|
||||
|
||||
@ -546,10 +547,8 @@ void TrackObject::updateGraphics(float dt)
|
||||
void TrackObject::update(float dt)
|
||||
{
|
||||
if (m_presentation) m_presentation->update(dt);
|
||||
|
||||
if (m_physical_object) m_physical_object->update(dt);
|
||||
|
||||
if (m_animator) m_animator->update(dt);
|
||||
} // update
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -238,3 +238,25 @@ void TrackObjectManager::removeObject(TrackObject* obj)
|
||||
m_all_objects.remove(obj);
|
||||
delete obj;
|
||||
} // removeObject
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
void TrackObjectManager::removeForRewind()
|
||||
{
|
||||
for (TrackObject* curr : m_all_objects)
|
||||
{
|
||||
if (curr->getPhysicalObject() &&
|
||||
curr->getPhysicalObject()->isDynamic())
|
||||
curr->getPhysicalObject()->removeBody();
|
||||
}
|
||||
} // removeForRewind
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
void TrackObjectManager::addForRewind()
|
||||
{
|
||||
for (TrackObject* curr : m_all_objects)
|
||||
{
|
||||
if (curr->getPhysicalObject() &&
|
||||
curr->getPhysicalObject()->isDynamic())
|
||||
curr->getPhysicalObject()->addBody();
|
||||
}
|
||||
} // addForRewind
|
||||
|
@ -76,6 +76,8 @@ public:
|
||||
|
||||
PtrVector<TrackObject>& getObjects() { return m_all_objects; }
|
||||
const PtrVector<TrackObject>& getObjects() const { return m_all_objects; }
|
||||
void removeForRewind();
|
||||
void addForRewind();
|
||||
|
||||
}; // class TrackObjectManager
|
||||
|
||||
|
@ -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…
Reference in New Issue
Block a user