diff --git a/src/analysis.cpp b/src/analysis.cpp index 8c2426c..97f5388 100644 --- a/src/analysis.cpp +++ b/src/analysis.cpp @@ -98,6 +98,7 @@ std::string Analysis::asJson() { ss << JUIN(1,"button_count", ap[p].button_count) << ",\n"; ss << JUIN(1,"cstick_count", ap[p].cstick_count) << ",\n"; ss << JUIN(1,"astick_count", ap[p].astick_count) << ",\n"; + ss << JUIN(1,"trigger_count", ap[p].trigger_count) << ",\n"; ss << JFLT(1,"actions_per_min", ap[p].apm) << ",\n"; ss << JUIN(1,"state_changes", ap[p].state_changes) << ",\n"; ss << JFLT(1,"states_per_min", ap[p].aspm) << ",\n"; diff --git a/src/analysis.h b/src/analysis.h index 3b506e5..00b2362 100644 --- a/src/analysis.h +++ b/src/analysis.h @@ -116,9 +116,10 @@ struct AnalysisPlayer { unsigned max_galint = 0; //Maximum GALINT frames after a ledgedash unsigned button_count = 0; //Button presses unsigned cstick_count = 0; //C stick movements - unsigned astick_count = 0; //Action state changes + unsigned astick_count = 0; //Analog stick movements + unsigned trigger_count = 0; //Analog trigger movements float apm = 0; //Actions per minute (combines buttons and csticks) - unsigned state_changes = 0; //Analog stick movements + unsigned state_changes = 0; //Action state changes float aspm = 0; //Action states per minute unsigned used_throws = 0; //Number of throws we threw out @@ -169,6 +170,7 @@ struct Analysis { std::string stage_name = ""; //Readable name of the stage int winner_port = 0; //Port index of the winning player (-1 == no winner) unsigned game_length = 0; //Length of the game in frames (0 == internal frame -123) + unsigned game_length_playable = 0; //Length of the game only counting playable frames unsigned timer = 0; //Game timer starting minutes AnalysisPlayer* ap; //Analysis of individual players in the game unsigned* dynamics; //Interaction dynamics on a per-frame basis diff --git a/src/analyzer.cpp b/src/analyzer.cpp index a9fba2c..d77f438 100644 --- a/src/analyzer.cpp +++ b/src/analyzer.cpp @@ -49,6 +49,7 @@ void Analyzer::getBasicGameInfo(const SlippiReplay &s, Analysis* a) const { a->parse_errors = s.errors; a->game_time = s.start_time; a->game_length = s.frame_count; + a->game_length_playable = s.frame_count + LOAD_FRAME - PLAYABLE_FRAME; a->timer = s.timer; a->end_type = s.end_type; a->lras_player = s.lras; @@ -140,6 +141,8 @@ void Analyzer::countButtons(const SlippiReplay &s, Analysis *a) const { float last_ay = 0; float last_cx = 0; float last_cy = 0; + float last_trigger_l = 0; + float last_trigger_r = 0; for(unsigned f = (-LOAD_FRAME); f < s.frame_count; ++f) { // Add buttons pressed this frame to button count uint16_t cur_buttons = p.frame[f].buttons; @@ -161,6 +164,14 @@ void Analyzer::countButtons(const SlippiReplay &s, Analysis *a) const { a->ap[pi].cstick_count += checkStickMovement(cur_cx,cur_cy,last_cx,last_cy); last_cx = cur_cx; last_cy = cur_cy; + + // Check analog trigger for movement + float cur_trigger_l = p.frame[f].phys_l; + float cur_trigger_r = p.frame[f].phys_r; + a->ap[pi].trigger_count += checkTriggerMovement(cur_trigger_l, last_trigger_l); + a->ap[pi].trigger_count += checkTriggerMovement(cur_trigger_r, last_trigger_r); + last_trigger_l = cur_trigger_l; + last_trigger_r = cur_trigger_r; } } } @@ -1065,12 +1076,12 @@ void Analyzer::computeTrivialInfo(const SlippiReplay &s, Analysis *a) const { } // Get actions per minute - unsigned total_actions = - a->ap[pi].button_count + a->ap[pi].cstick_count + a->ap[pi].astick_count; - a->ap[pi].apm = total_actions * (3600.0f / a->game_length); + unsigned total_actions = a->ap[pi].button_count + a->ap[pi].cstick_count + + a->ap[pi].astick_count + a->ap[pi].trigger_count; + a->ap[pi].apm = total_actions * (3600.0f / a->game_length_playable); // Get action states per minute - a->ap[pi].aspm = a->ap[pi].state_changes * (3600.0f / a->game_length); + a->ap[pi].aspm = a->ap[pi].state_changes * (3600.0f / a->game_length_playable); // Get total number of moves landed and move accuracy a->ap[pi].total_moves_landed = 0; diff --git a/src/analyzer.h b/src/analyzer.h index 53cfc0c..8e80b1d 100644 --- a/src/analyzer.h +++ b/src/analyzer.h @@ -1,12 +1,9 @@ #ifndef ANALYZER_H_ #define ANALYZER_H_ -#include -#include #include //sqrt #include "enums.h" -#include "util.h" #include "replay.h" #include "analysis.h" @@ -19,6 +16,18 @@ const unsigned MAX_WAIT = 15; //If we don't act out of wait or stun for namespace slip { +enum JoystickRegion { + DZ = 0, + NE = 1, + SE = 2, + SW = 3, + NW = 4, + N = 5, + E = 6, + S = 7, + W = 8, +}; + class Analyzer { private: int _debug; //Current debug level @@ -304,28 +313,48 @@ class Analyzer { static inline bool isDead(const SlippiFrame &f) { return (f.flags_5 & 0x10) || f.action_pre < Action::Sleep; } - static inline unsigned checkStickMovement(float x1, float y1, float x2, float y2, float neut=0.1f) { - // If a stick crossed an axis, that's a movement - if (x1 < 0 && x2 > 0) { return 1; } - if (x1 > 0 && x2 < 0) { return 1; } - if (y1 < 0 && y2 > 0) { return 1; } - if (y1 > 0 && y2 < 0) { return 1; } + static inline JoystickRegion getJoystickRegion(float x, float y, float neut) { + if (x >= neut && y >= neut) { + return JoystickRegion::NE; + } + else if (x >= neut && y <= -neut) { + return JoystickRegion::SE; + } + else if (x <= -neut && y <= -neut) { + return JoystickRegion::SW; + } + else if (x <= -neut && y >= neut) { + return JoystickRegion::NW; + } + else if (y >= neut) { + return JoystickRegion::N; + } + else if (x >= neut) { + return JoystickRegion::E; + } + else if (y <= -neut) { + return JoystickRegion::S; + } + else if (x <= -neut) { + return JoystickRegion::W; + } + + return JoystickRegion::DZ; + } + static inline unsigned checkStickMovement(float x1, float y1, float x2, float y2, float neut=0.2875) { + // logic copied from slippi-js + // https://github.com/project-slippi/slippi-js/blob/master/src/common/stats/inputs.ts#L97 - // If the higher absolute magnitude between x and y shifted, - // that's a movement - float ax1 = (x1 > 0) ? x1 : -x1; - float ax2 = (x2 > 0) ? x2 : -x2; - float ay1 = (y1 > 0) ? y1 : -y1; - float ay2 = (y2 > 0) ? y2 : -y2; - if (ax1 > ay1 && ax2 < ay2) { return 1; } - if (ax1 < ay1 && ax2 > ay2) { return 1; } + auto region1 = getJoystickRegion(x1, y1, neut); + auto region2 = getJoystickRegion(x2, y2, neut); - // If we moved from neutral to non neutral, that's a movement - if (ax1 < neut && ay1 < neut) { - if (ax2 > neut || ay2 > neut) { return 1; } - } + return region1 != JoystickRegion::DZ && region1 != region2 ? 1 : 0; + } + static inline unsigned checkTriggerMovement(float trigCurrent, float trigLast, float threshold = 0.3) { + // logic copied from slippi-js + // https://github.com/project-slippi/slippi-js/blob/master/src/common/stats/inputs.ts#L110 - return 0; + return trigLast < threshold && trigCurrent >= threshold ? 1 : 0; } static inline std::string frameAsTimer(unsigned fnum, unsigned startmins) { if (startmins == 0) { diff --git a/src/parser.cpp b/src/parser.cpp index 419b733..902d694 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -99,10 +99,11 @@ namespace slip { } if (_replay.errors == 0) { DOUT1(" Successfully parsed replay!"); + return true; } else { WARN(" Replay parsed with " << _replay.errors << " errors"); + return false; } - return true; } bool Parser::_parseHeader() {