diff --git a/ManiaKV/ManiaKV.vcxproj b/ManiaKV/ManiaKV.vcxproj index cdb2b3e..1f1c5fa 100644 --- a/ManiaKV/ManiaKV.vcxproj +++ b/ManiaKV/ManiaKV.vcxproj @@ -221,6 +221,7 @@ + diff --git a/ManiaKV/ManiaKV.vcxproj.filters b/ManiaKV/ManiaKV.vcxproj.filters index 865d5c7..7497d82 100644 --- a/ManiaKV/ManiaKV.vcxproj.filters +++ b/ManiaKV/ManiaKV.vcxproj.filters @@ -36,6 +36,9 @@ lib\main + + lib + diff --git a/ManiaKV/lib/components/Stage.hpp b/ManiaKV/lib/components/Stage.hpp index e250fde..b8de737 100644 --- a/ManiaKV/lib/components/Stage.hpp +++ b/ManiaKV/lib/components/Stage.hpp @@ -91,6 +91,7 @@ class Stage { if (texture.find("Idle") != -1) { map keyMap; + // Temperary fix because I'm stupid and can't be bothered to properly implement it right now if (characterClass.keys == 2) { keyMap = { {availableKeys[texture.find("rightIdle") != -1 diff --git a/ManiaKV/lib/oppai.c b/ManiaKV/lib/oppai.c new file mode 100644 index 0000000..9b2c58d --- /dev/null +++ b/ManiaKV/lib/oppai.c @@ -0,0 +1,2943 @@ +/* + * this is free and unencumbered software released into the public domain. + * refer to the attached UNLICENSE or http://unlicense.org/ + * -- usage: --------------------------------------------------------------- + * #define OPPAI_IMPLEMENTATION + * #include "../oppai.c" + * + * int main() { + * ezpp_t ez = ezpp_new(); + * ezpp_set_mods(ez, MODS_HD | MODS_DT); + * ezpp(ez, "-"); + * printf("%gpp\n", ezpp_pp(ez)); + * return 0; + * } + * ------------------------------------------------------------------------ + * $ gcc test.c + * $ cat /path/to/file.osu | ./a.out + */ + +#if defined(_WIN32) && !defined(OPPAI_IMPLEMENTATION) +#ifdef OPPAI_EXPORT +#define OPPAIAPI __declspec(dllexport) +#elif defined(OPPAI_STATIC_HEADER) +#define OPPAIAPI +#else +#define OPPAIAPI __declspec(dllimport) +#endif +#else +#define OPPAIAPI +#endif + +typedef struct ezpp *ezpp_t; /* opaque handle */ + +OPPAIAPI ezpp_t ezpp_new(void); +OPPAIAPI void ezpp_free(ezpp_t ez); +OPPAIAPI int ezpp(ezpp_t ez, char *map); +OPPAIAPI float ezpp_pp(ezpp_t ez); +OPPAIAPI float ezpp_stars(ezpp_t ez); + +/* + * the above is all you need for basic usage. below are some advanced api's + * and usage examples + * + * - if map is "-" the map is read from standard input + * - you can use ezpp_data if you already have raw beatmap data in memory + * - if autocalc is set to 1, the results will be automatically refreshed + * when you change parameters. if reparsing is required, the last passed + * map or map data will be used + * - if map is 0 (NULL), difficulty calculation and map parsing are skipped + * and you must set at least mode, aim_stars, speed_stars, nobjects, + * base_ar, base_od, max_combo, nsliders, ncircles + * - if aim_stars or speed_stars are set difficulty calculation is also + * skipped but values are taken from map + * - setting mods or cs resets aim_stars and speed_stars, set those last + * = setting end resets accuracy_percent + * - if mode_override is set, std maps are converted to other modes + * - mode defaults to MODE_STD or the map's mode + * - mods default to MODS_NOMOD + * - combo defaults to full combo + * - nmiss defaults to 0 + * - score_version defaults to scorev1 + * - if accuracy_percent is set, n300/100/50 are automatically + * calculated and stored + * - if n300/100/50 are set, accuracy_percent is automatically + * calculated and stored + * - if none of the above are set, SS (100%) is assumed + * - if end is set, the map will be cut to this object index + * - if base_ar/od/cs are set, they will override the map's values + * - when you change map and you're reusing the handle, you should reset + * ar/od/cs/hp to -1 otherwise it will override them with the previous + * map's values + * - in autocalc mode, calling ezpp with a non-NULL map always resets + * ar/od/cs/hp overrides to -1 so you don't have to + */ + +OPPAIAPI void ezpp_set_autocalc(ezpp_t ez, int autocalc); +OPPAIAPI int ezpp_autocalc(ezpp_t ez); +OPPAIAPI int ezpp_data(ezpp_t ez, char *data, int data_size); +OPPAIAPI float ezpp_aim_stars(ezpp_t ez); +OPPAIAPI float ezpp_speed_stars(ezpp_t ez); +OPPAIAPI float ezpp_aim_pp(ezpp_t ez); +OPPAIAPI float ezpp_speed_pp(ezpp_t ez); +OPPAIAPI float ezpp_acc_pp(ezpp_t ez); +OPPAIAPI float ezpp_accuracy_percent(ezpp_t ez); +OPPAIAPI int ezpp_n300(ezpp_t ez); +OPPAIAPI int ezpp_n100(ezpp_t ez); +OPPAIAPI int ezpp_n50(ezpp_t ez); +OPPAIAPI int ezpp_nmiss(ezpp_t ez); +OPPAIAPI float ezpp_ar(ezpp_t ez); +OPPAIAPI float ezpp_cs(ezpp_t ez); +OPPAIAPI float ezpp_od(ezpp_t ez); +OPPAIAPI float ezpp_hp(ezpp_t ez); +OPPAIAPI char *ezpp_artist(ezpp_t ez); +OPPAIAPI char *ezpp_artist_unicode(ezpp_t ez); +OPPAIAPI char *ezpp_title(ezpp_t ez); +OPPAIAPI char *ezpp_title_unicode(ezpp_t ez); +OPPAIAPI char *ezpp_version(ezpp_t ez); +OPPAIAPI char *ezpp_creator(ezpp_t ez); +OPPAIAPI int ezpp_ncircles(ezpp_t ez); +OPPAIAPI int ezpp_nsliders(ezpp_t ez); +OPPAIAPI int ezpp_nspinners(ezpp_t ez); +OPPAIAPI int ezpp_nobjects(ezpp_t ez); +OPPAIAPI float ezpp_odms(ezpp_t ez); +OPPAIAPI int ezpp_mode(ezpp_t ez); +OPPAIAPI int ezpp_combo(ezpp_t ez); +OPPAIAPI int ezpp_max_combo(ezpp_t ez); +OPPAIAPI int ezpp_mods(ezpp_t ez); +OPPAIAPI int ezpp_score_version(ezpp_t ez); +OPPAIAPI float ezpp_time_at(ezpp_t ez, int i); /* milliseconds */ +OPPAIAPI float ezpp_strain_at(ezpp_t ez, int i, int difficulty_type); +OPPAIAPI int ezpp_ntiming_points(ezpp_t ez); +OPPAIAPI float ezpp_timing_time(ezpp_t ez, int i); /* milliseconds */ +OPPAIAPI float ezpp_timing_ms_per_beat(ezpp_t ez, int i); +OPPAIAPI int ezpp_timing_change(ezpp_t ez, int i); + +OPPAIAPI void ezpp_set_aim_stars(ezpp_t ez, float aim_stars); +OPPAIAPI void ezpp_set_speed_stars(ezpp_t ez, float speed_stars); +OPPAIAPI void ezpp_set_base_ar(ezpp_t ez, float ar); +OPPAIAPI void ezpp_set_base_od(ezpp_t ez, float od); +OPPAIAPI void ezpp_set_base_cs(ezpp_t ez, float cs); +OPPAIAPI void ezpp_set_base_hp(ezpp_t ez, float hp); +OPPAIAPI void ezpp_set_mode_override(ezpp_t ez, int mode_override); +OPPAIAPI void ezpp_set_mode(ezpp_t ez, int mode); +OPPAIAPI void ezpp_set_mods(ezpp_t ez, int mods); +OPPAIAPI void ezpp_set_combo(ezpp_t ez, int combo); +OPPAIAPI void ezpp_set_nmiss(ezpp_t ez, int nmiss); +OPPAIAPI void ezpp_set_score_version(ezpp_t ez, int score_version); +OPPAIAPI void ezpp_set_accuracy_percent(ezpp_t ez, float accuracy_percent); +OPPAIAPI void ezpp_set_accuracy(ezpp_t ez, int n100, int n50); +OPPAIAPI void ezpp_set_end(ezpp_t ez, int end); +OPPAIAPI void ezpp_set_end_time(ezpp_t ez, float end); + +/* + * these will make a copy of mapfile/data and free it automatically. this + * is slow but useful when working with bindings in other langs where + * pointers to strings aren't guaranteed to persist like python3 + */ +OPPAIAPI int ezpp_dup(ezpp_t ez, char *mapfile); +OPPAIAPI int ezpp_data_dup(ezpp_t ez, char *data, int data_size); + +/* errors -------------------------------------------------------------- */ + +/* + * all functions that return int can return errors in the form + * of a negative value. check if the return value is < 0 and call + * errstr to get the error message + */ + +#define ERR_MORE (-1) +#define ERR_SYNTAX (-2) +#define ERR_TRUNCATED (-3) +#define ERR_NOTIMPLEMENTED (-4) +#define ERR_IO (-5) +#define ERR_FORMAT (-6) +#define ERR_OOM (-7) + +OPPAIAPI char *errstr(int err); + +/* version info -------------------------------------------------------- */ + +OPPAIAPI void oppai_version(int *major, int *minor, int *patch); +OPPAIAPI char *oppai_version_str(void); + +/* --------------------------------------------------------------------- */ + +#define MODE_STD 0 +#define MODE_TAIKO 1 + +#define DIFF_SPEED 0 +#define DIFF_AIM 1 + +#define MODS_NOMOD 0 +#define MODS_NF (1 << 0) +#define MODS_EZ (1 << 1) +#define MODS_TD (1 << 2) +#define MODS_HD (1 << 3) +#define MODS_HR (1 << 4) +#define MODS_SD (1 << 5) +#define MODS_DT (1 << 6) +#define MODS_RX (1 << 7) +#define MODS_HT (1 << 8) +#define MODS_NC (1 << 9) +#define MODS_FL (1 << 10) +#define MODS_AT (1 << 11) +#define MODS_SO (1 << 12) +#define MODS_AP (1 << 13) +#define MODS_PF (1 << 14) +#define MODS_KEY4 (1 << 15) /* TODO: what are these abbreviated to? */ +#define MODS_KEY5 (1 << 16) +#define MODS_KEY6 (1 << 17) +#define MODS_KEY7 (1 << 18) +#define MODS_KEY8 (1 << 19) +#define MODS_FADEIN (1 << 20) +#define MODS_RANDOM (1 << 21) +#define MODS_CINEMA (1 << 22) +#define MODS_TARGET (1 << 23) +#define MODS_KEY9 (1 << 24) +#define MODS_KEYCOOP (1 << 25) +#define MODS_KEY1 (1 << 26) +#define MODS_KEY3 (1 << 27) +#define MODS_KEY2 (1 << 28) +#define MODS_SCOREV2 (1 << 29) +#define MODS_TOUCH_DEVICE MODS_TD +#define MODS_NOVIDEO MODS_TD /* never forget */ +#define MODS_SPEED_CHANGING (MODS_DT | MODS_HT | MODS_NC) +#define MODS_MAP_CHANGING (MODS_HR | MODS_EZ | MODS_SPEED_CHANGING) + +/* this is all you need to know for normal usage. internals below */ +/* ##################################################################### */ +/* ##################################################################### */ +/* ##################################################################### */ + +#ifdef OPPAI_EXPORT +#define OPPAI_IMPLEMENTATION +#endif + +#ifdef OPPAI_IMPLEMENTATION +#include +#include +#include +#include +#include + +#define OPPAI_VERSION_MAJOR 4 +#define OPPAI_VERSION_MINOR 1 +#define OPPAI_VERSION_PATCH 0 +#define STRINGIFY_(x) #x +#define STRINGIFY(x) STRINGIFY_(x) + +#define OPPAI_VERSION_STRING \ + STRINGIFY(OPPAI_VERSION_MAJOR) \ + "." STRINGIFY(OPPAI_VERSION_MINOR) "." STRINGIFY(OPPAI_VERSION_PATCH) + +OPPAIAPI +void oppai_version(int *major, int *minor, int *patch) +{ + *major = OPPAI_VERSION_MAJOR; + *minor = OPPAI_VERSION_MINOR; + *patch = OPPAI_VERSION_PATCH; +} + +OPPAIAPI +char *oppai_version_str() +{ + return OPPAI_VERSION_STRING; +} + +/* error utils --------------------------------------------------------- */ + +int info(char *fmt, ...) +{ + // int res; + // va_list va; + // va_start(va, fmt); + // res = vfprintf(stderr, fmt, va); + // va_end(va); + // return res; + return 0; +} + +OPPAIAPI +char *errstr(int err) +{ + switch (err) + { + case ERR_MORE: + return "call me again with more data"; + case ERR_SYNTAX: + return "syntax error"; + case ERR_TRUNCATED: + return "data was truncated, possibly because it was too big"; + case ERR_NOTIMPLEMENTED: + return "requested a feature that isn't implemented"; + case ERR_IO: + return "i/o error"; + case ERR_FORMAT: + return "invalid input format"; + case ERR_OOM: + return "out of memory"; + } + info("W: got unknown error %d\n", err); + return "unknown error"; +} + +/* math ---------------------------------------------------------------- */ + +#define log10f (float)log10 +#define al_round(x) (float)floor((x) + 0.5f) +#define al_min(a, b) ((a) < (b) ? (a) : (b)) +#define al_max(a, b) ((a) > (b) ? (a) : (b)) + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +float get_inf(void) +{ + static unsigned raw = 0x7F800000; + float *p = (float *)&raw; + return *p; +} + +float get_nan(void) +{ + static unsigned raw = 0x7FFFFFFF; + float *p = (float *)&raw; + return *p; +} + +/* dst = a - b */ +void v2f_sub(float *dst, float *a, float *b) +{ + dst[0] = a[0] - b[0]; + dst[1] = a[1] - b[1]; +} + +float v2f_len(float *v) +{ + return (float)sqrt(v[0] * v[0] + v[1] * v[1]); +} + +float v2f_dot(float *a, float *b) +{ + return a[0] * b[0] + a[1] * b[1]; +} + +/* https://www.doc.ic.ac.uk/%7Eeedwards/compsys/float/nan.html */ + +int is_nan(float b) +{ + unsigned *p = (void *)&b; + return ( + (*p > 0x7F800000 && *p < 0x80000000) || + (*p > 0x7FBFFFFF && *p <= 0xFFFFFFFF)); +} + +/* https://www.doc.ic.ac.uk/%7Eeedwards/compsys/float/nan.html */ + +int is_inf(float b) +{ + int *p = (int *)&b; + return *p == 0x7F800000 || *p == 0xFF800000; +} + +/* string utils -------------------------------------------------------- */ + +int whitespace(char c) +{ + switch (c) + { + case '\r': + case '\n': + case '\t': + case ' ': + return 1; + } + return 0; +} + +/* non-null terminated string, used internally for parsing */ +typedef struct slice +{ + char *start; + char *end; /* *(end - 1) is the last character */ +} slice_t; + +int slice_write(slice_t *s, FILE *f) +{ + return (int)fwrite(s->start, 1, s->end - s->start, f); +} + +int slice_whitespace(slice_t *s) +{ + char *p = s->start; + for (; p < s->end; ++p) + { + if (!whitespace(*p)) + { + return 0; + } + } + return 1; +} + +/* trims leading and trailing whitespace */ +void slice_trim(slice_t *s) +{ + for (; s->start < s->end && whitespace(*s->start); ++s->start) + ; + for (; s->end > s->start && whitespace(*(s->end - 1)); --s->end) + ; +} + +int slice_cmp(slice_t *s, char *str) +{ + int len = (int)strlen(str); + int s_len = (int)(s->end - s->start); + if (len < s_len) + { + return -1; + } + if (len > s_len) + { + return 1; + } + return strncmp(s->start, str, len); +} + +int slice_len(slice_t *s) +{ + return (int)(s->end - s->start); +} + +/* + * splits s at any of the separators in separator_list and stores + * pointers to the strings in arr. + * returns the number of elements written to arr. + * if more elements than nmax are found, err is set to + * ERR_TRUNCATED + */ +int slice_split(slice_t *s, char *separator_list, slice_t *arr, + int nmax, int *err) +{ + int res = 0; + char *p = s->start; + char *pprev = p; + if (!nmax) + { + return 0; + } + if (!*separator_list) + { + *arr = *s; + return 1; + } + for (; p <= s->end; ++p) + { + char *sep = separator_list; + for (; *sep; ++sep) + { + if (p >= s->end || *sep == *p) + { + if (res >= nmax) + { + *err = ERR_TRUNCATED; + goto exit; + } + arr[res].start = pprev; + arr[res].end = p; + pprev = p + 1; + ++res; + break; + } + } + } +exit: + return res; +} + +/* array --------------------------------------------------------------- */ + +#define array_t(type) \ + struct \ + { \ + int cap; \ + int len; \ + type *data; \ + } + +#define array_reserve(arr, n) \ + array_reserve_i(n, array_unpack(arr)) + +#define array_free(arr) \ + array_free_i(array_unpack(arr)) + +#define array_alloc(arr) \ + (array_reserve((arr), (arr)->len + 1) \ + ? &(arr)->data[(arr)->len++] \ + : 0) + +#define array_append(arr, x) \ + (array_reserve((arr), (arr)->len + 1) \ + ? ((arr)->data[(arr)->len++] = (x), 1) \ + : 0) + +/* internal helpers, not to be used directly */ +#define array_unpack(arr) \ + &(arr)->cap, \ + &(arr)->len, \ + (void **)&(arr)->data, \ + (int)sizeof((arr)->data[0]) + +int array_reserve_i(int n, int *cap, int *len, void **data, int esize) +{ + (void)len; + if (*cap <= n) + { + void *newdata; + int newcap = *cap ? *cap * 2 : 16; + newdata = realloc(*data, esize * newcap); + if (!newdata) + { + return 0; + } + *data = newdata; + *cap = newcap; + } + return 1; +} + +void array_free_i(int *cap, int *len, void **data, int esize) +{ + (void)esize; + free(*data); + *cap = 0; + *len = 0; + *data = 0; +} + +/* --------------------------------------------------------------------- */ + +#define OBJ_CIRCLE (1 << 0) +#define OBJ_SLIDER (1 << 1) +#define OBJ_SPINNER (1 << 3) + +#define SOUND_NONE 0 +#define SOUND_NORMAL (1 << 0) +#define SOUND_WHISTLE (1 << 1) +#define SOUND_FINISH (1 << 2) +#define SOUND_CLAP (1 << 3) + +typedef struct timing +{ + float time; /* milliseconds */ + float ms_per_beat; + int change; /* if 0, ms_per_beat is -100.0f * sv_multiplier */ + float px_per_beat; + + /* taiko stuff */ + float beat_len; + float velocity; +} timing_t; + +typedef struct object +{ + float time; /* milliseconds */ + int type; + + /* only for taiko maps */ + int nsound_types; + int *sound_types; + + /* only used by d_calc */ + float normpos[2]; + float angle; + float strains[2]; + int is_single; /* 1 if diff calc sees this as a singletap */ + float delta_time; + float d_distance; + int timing_point; + + float pos[2]; + float distance; /* only for sliders */ + int repetitions; + + /* taiko stuff */ + float duration; + float tick_spacing; + int slider_is_drum_roll; +} object_t; + +/* + * exposing the struct would cut down lines of code but makes it harder + * to use from langs that aren't c/c++ or don't have the same memory + * alignment etc + */ + +#define AUTOCALC_BIT (1 << 0) +#define OWNS_MAP_BIT (1 << 1) /* map/data freed on ezpp{,_data}, ezpp_free */ + +struct ezpp +{ + char *map; + char *data; + int data_size; + int flags; + int format_version; + int mode, mode_override, original_mode; + int score_version; + int mods, combo; + float accuracy_percent; + int n300, n100, n50, nmiss; + int end; + float end_time; + float base_ar, base_cs, base_od, base_hp; + int max_combo; + char *title; + char *title_unicode; + char *artist; + char *artist_unicode; + char *creator; + char *version; + int ncircles, nsliders, nspinners, nobjects; + float ar, od, cs, hp, odms, sv, tick_rate, speed_mul; + float stars; + float aim_stars, aim_difficulty, aim_length_bonus; + float speed_stars, speed_difficulty, speed_length_bonus; + float pp, aim_pp, speed_pp, acc_pp; + + /* parser */ + char section[64]; + char buf[0xFFFF]; + int p_flags; + array_t(object_t) objects; + array_t(timing_t) timing_points; + + /* diffcalc */ + float interval_end; + float max_strain; + array_t(float) highest_strains; + + /* allocator */ + char *block; + char *end_of_block; + array_t(char *) blocks; +}; + +/* memory arena (allocator) -------------------------------------------- */ + +#define M_ALIGN sizeof(void *) +#define M_BLOCK_SIZE 4096 + +/* aligns x down to a power-of-two value a */ +#define bit_align_down(x, a) \ + ((x) & ~((a)-1)) + +/* aligns x up to a power-of-two value a */ +#define bit_align_up(x, a) \ + bit_align_down((x) + (a)-1, a) + +int m_reserve(ezpp_t ez, int min_size) +{ + int size; + char *new_block; + if (ez->end_of_block - ez->block >= min_size) + { + return 1; + } + size = bit_align_up(al_max(min_size, M_BLOCK_SIZE), M_ALIGN); + new_block = malloc(size); + if (!new_block) + { + return 0; + } + ez->block = new_block; + ez->end_of_block = new_block + size; + array_append(&ez->blocks, ez->block); + return 1; +} + +void *m_alloc(ezpp_t ez, int size) +{ + void *res; + if (!m_reserve(ez, size)) + { + return 0; + } + size = bit_align_up(size, M_ALIGN); + res = ez->block; + ez->block += size; + return res; +} + +char *m_strndup(ezpp_t ez, char *s, int n) +{ + char *res = m_alloc(ez, n + 1); + if (res) + { + memcpy(res, s, n); + res[n] = 0; + } + return res; +} + +void m_free(ezpp_t ez) +{ + int i; + for (i = 0; i < ez->blocks.len; ++i) + { + free(ez->blocks.data[i]); + } + array_free(&ez->blocks); + ez->block = 0; + ez->end_of_block = 0; +} + +/* mods ---------------------------------------------------------------- */ + +float od10_ms[] = {20, 20}; /* std, taiko */ +float od0_ms[] = {80, 50}; +#define AR0_MS 1800.0f +#define AR5_MS 1200.0f +#define AR10_MS 450.0f + +float od_ms_step[] = {6.0f, 3.0f}; +#define AR_MS_STEP1 120.f /* ar0-5 */ +#define AR_MS_STEP2 150.f /* ar5-10 */ + +/* + * stats must be capped to 0-10 before HT/DT which brings them to a range + * of -4.42f to 11.08f for OD and -5 to 11 for AR + */ + +int mods_apply(ezpp_t ez) +{ + float od_ar_hp_multiplier, cs_multiplier, arms; + + switch (ez->mode) + { + case MODE_STD: + case MODE_TAIKO: + break; + default: + info("this gamemode is not yet supported for mods calc\n"); + return ERR_NOTIMPLEMENTED; + } + + ez->speed_mul = 1; + + if (!(ez->mods & MODS_MAP_CHANGING)) + { + ez->odms = od0_ms[ez->mode] - (float)ceil(od_ms_step[ez->mode] * ez->od); + return 0; + } + + if (ez->mods & (MODS_DT | MODS_NC)) + { + ez->speed_mul *= 1.5f; + } + if (ez->mods & MODS_HT) + { + ez->speed_mul *= 0.75f; + } + + /* global multipliers */ + od_ar_hp_multiplier = 1; + if (ez->mods & MODS_HR) + od_ar_hp_multiplier *= 1.4f; + if (ez->mods & MODS_EZ) + od_ar_hp_multiplier *= 0.5f; + + ez->od *= od_ar_hp_multiplier; + ez->odms = od0_ms[ez->mode] - (float)ceil(od_ms_step[ez->mode] * ez->od); + ez->odms = al_min(od0_ms[ez->mode], al_max(od10_ms[ez->mode], ez->odms)); + ez->odms /= ez->speed_mul; + ez->od = (od0_ms[ez->mode] - ez->odms) / od_ms_step[ez->mode]; + + ez->ar *= od_ar_hp_multiplier; + arms = ez->ar <= 5 + ? (AR0_MS - AR_MS_STEP1 * (ez->ar - 0)) + : (AR5_MS - AR_MS_STEP2 * (ez->ar - 5)); + arms = al_min(AR0_MS, al_max(AR10_MS, arms)); + arms /= ez->speed_mul; + ez->ar = arms > AR5_MS + ? (0 + (AR0_MS - arms) / AR_MS_STEP1) + : (5 + (AR5_MS - arms) / AR_MS_STEP2); + + cs_multiplier = 1; + if (ez->mods & MODS_HR) + cs_multiplier = 1.3f; + if (ez->mods & MODS_EZ) + cs_multiplier = 0.5f; + ez->cs *= cs_multiplier; + ez->cs = al_max(0.0f, al_min(10.0f, ez->cs)); + + ez->hp = al_min(ez->hp * od_ar_hp_multiplier, 10); + + return 0; +} + +/* beatmap parser ------------------------------------------------------ */ + +/* + * comments in beatmaps can only be an entire line because + * some properties such as author can contain // + * + * all p_* functions expect s to be a single line and trimmed + * on errors, p_* functions return < 0 error codes otherwise they + * return n bytes consumed + */ + +#define P_OVERRIDE_MODE (1 << 0) /* mode_override */ +#define P_FOUND_AR (1 << 1) + +#define CIRCLESIZE_BUFF_TRESHOLD 30.0f /* non-normalized diameter */ +#define PLAYFIELD_WIDTH 512.0f /* in osu!pixels */ +#define PLAYFIELD_HEIGHT 384.0f + +float playfield_center[] = { + PLAYFIELD_WIDTH / 2.0f, PLAYFIELD_HEIGHT / 2.0f}; + +void print_line(slice_t *line) +{ + info("in line: "); + slice_write(line, stderr); + info("\n"); +} + +int p_warn(char *e, slice_t *line) +{ + info(e); + info("\n"); + print_line(line); + return 0; +} + +/* consume until any of the characters in separators is found */ +int p_consume_til(slice_t *s, char *separators, slice_t *dst) +{ + char *p = s->start; + dst->start = s->start; + for (; p < s->end; ++p) + { + char *sep; + for (sep = separators; *sep; ++sep) + { + if (*p == *sep) + { + dst->start = s->start; + dst->end = p; + return (int)(p - s->start); + } + } + } + dst->end = p; + return ERR_MORE; +} + +float p_float(slice_t *value) +{ + float res; + char *p = value->start; + if (*p == '-') + { + res = -1; + ++p; + } + else + { + res = 1; + } + /* infinity symbol */ + if (!strncmp(p, "\xe2\x88\x9e", 3)) + { + res *= get_inf(); + } + else + { + if (sscanf(value->start, "%f", &res) != 1) + { + info("W: failed to parse float "); + slice_write(value, stderr); + info("\n"); + res = 0; + } + } + return res; +} + +/* [name] */ +int p_section_name(slice_t *s, slice_t *name) +{ + int n; + slice_t p = *s; + if (*p.start++ != '[') + { + return ERR_SYNTAX; + } + n = p_consume_til(&p, "]", name); + if (n < 0) + { + return n; + } + p.start += n; + if (p.start != p.end - 1) + { /* must end in ] */ + return ERR_SYNTAX; + } + return (int)(p.start - s->start); +} + +/* name: value (results are trimmed) */ +int p_property(slice_t *s, slice_t *name, slice_t *value) +{ + int n; + char *p = s->start; + n = p_consume_til(s, ":", name); + if (n < 0) + { + return n; + } + p += n; + ++p; /* skip : */ + value->start = p; + value->end = s->end; + slice_trim(name); + slice_trim(value); + return (int)(s->end - s->start); +} + +char *p_slicedup(ezpp_t ez, slice_t *s) +{ + return m_strndup(ez, s->start, slice_len(s)); +} + +int p_metadata(ezpp_t ez, slice_t *line) +{ + slice_t name, value; + int n = p_property(line, &name, &value); + if (n < 0) + { + return p_warn("W: malformed metadata line", line); + } + if (!slice_cmp(&name, "Title")) + { + ez->title = p_slicedup(ez, &value); + } + else if (!slice_cmp(&name, "TitleUnicode")) + { + ez->title_unicode = p_slicedup(ez, &value); + } + else if (!slice_cmp(&name, "Artist")) + { + ez->artist = p_slicedup(ez, &value); + } + else if (!slice_cmp(&name, "ArtistUnicode")) + { + ez->artist_unicode = p_slicedup(ez, &value); + } + else if (!slice_cmp(&name, "Creator")) + { + ez->creator = p_slicedup(ez, &value); + } + else if (!slice_cmp(&name, "Version")) + { + ez->version = p_slicedup(ez, &value); + } + return n; +} + +int p_general(ezpp_t ez, slice_t *line) +{ + slice_t name, value; + int n; + n = p_property(line, &name, &value); + if (n < 0) + { + return p_warn("W: malformed general line", line); + } + if (!slice_cmp(&name, "Mode")) + { + if (sscanf(value.start, "%d", &ez->original_mode) != 1) + { + return ERR_SYNTAX; + } + if (ez->p_flags & P_OVERRIDE_MODE) + { + ez->mode = ez->mode_override; + } + else + { + ez->mode = ez->original_mode; + } + switch (ez->mode) + { + case MODE_STD: + case MODE_TAIKO: + break; + default: + return ERR_NOTIMPLEMENTED; + } + } + return n; +} + +int p_difficulty(ezpp_t ez, slice_t *line) +{ + slice_t name, value; + int n = p_property(line, &name, &value); + if (n < 0) + { + return p_warn("W: malformed difficulty line", line); + } + if (!slice_cmp(&name, "CircleSize")) + { + ez->cs = p_float(&value); + } + else if (!slice_cmp(&name, "OverallDifficulty")) + { + ez->od = p_float(&value); + } + else if (!slice_cmp(&name, "ApproachRate")) + { + ez->ar = p_float(&value); + ez->p_flags |= P_FOUND_AR; + } + else if (!slice_cmp(&name, "HPDrainRate")) + { + ez->hp = p_float(&value); + } + else if (!slice_cmp(&name, "SliderMultiplier")) + { + ez->sv = p_float(&value); + } + else if (!slice_cmp(&name, "SliderTickRate")) + { + ez->tick_rate = p_float(&value); + } + return n; +} + +/* + * time, ms_per_beat, time_signature_id, sample_set_id, + * sample_bank_id, sample_volume, is_timing_change, effect_flags + * + * everything after ms_per_beat is optional + */ +int p_timing(ezpp_t ez, slice_t *line) +{ + int res = 0; + int n, i; + int err = 0; + slice_t split[8]; + timing_t *t = array_alloc(&ez->timing_points); + + if (!t) + { + return ERR_OOM; + } + + t->change = 1; + + n = slice_split(line, ",", split, 8, &err); + if (err < 0) + { + if (err == ERR_TRUNCATED) + { + info("W: timing point with trailing values"); + print_line(line); + } + else + { + return err; + } + } + + if (n < 2) + { + return p_warn("W: malformed timing point", line); + } + + res = (int)(split[n - 1].end - line->start); + for (i = 0; i < n; ++i) + { + slice_trim(&split[i]); + } + + t->time = p_float(&split[0]); + t->ms_per_beat = p_float(&split[1]); + + if (n >= 7) + { + if (slice_len(&split[6]) < 1) + { + t->change = 1; + } + else + { + t->change = *split[6].start != '0'; + } + } + + return res; +} + +int p_objects(ezpp_t ez, slice_t *line) +{ + object_t *o; + int err = 0; + int ne; + slice_t e[11]; + + if (ez->end > 0 && ez->objects.len >= ez->end) + { + return 0; + } + + o = array_alloc(&ez->objects); + + if (o) + { + memset(o, 0, sizeof(*o)); + } + else + { + return ERR_OOM; + } + + ne = slice_split(line, ",", e, 11, &err); + if (err < 0) + { + if (err == ERR_TRUNCATED) + { + info("W: object with trailing values\n"); + print_line(line); + } + else + { + return err; + } + } + + if (ne < 5) + { + return p_warn("W: malformed hitobject", line); + } + + o->time = p_float(&e[2]); + if (is_inf(o->time)) + { + o->time = 0.0f; + info("W: object with infinite time\n"); + print_line(line); + } + + if (ez->end_time > 0 && o->time >= ez->end_time) + { + --ez->objects.len; + return 0; + } + + if (sscanf(e[3].start, "%d", &o->type) != 1) + { + p_warn("W: malformed hitobject type", line); + o->type = OBJ_CIRCLE; + } + + if (ez->mode == MODE_TAIKO) + { + int *sound_type = m_alloc(ez, sizeof(int)); + if (!sound_type) + { + return ERR_OOM; + } + if (sscanf(e[4].start, "%d", sound_type) != 1) + { + p_warn("W: malformed hitobject sound type", line); + *sound_type = SOUND_NORMAL; + } + o->nsound_types = 1; + o->sound_types = sound_type; + /* wastes 4 bytes when you have per-node sounds but w/e */ + } + + if (o->type & OBJ_CIRCLE) + { + ++ez->ncircles; + o->pos[0] = p_float(&e[0]); + o->pos[1] = p_float(&e[1]); + } + + /* ?,?,?,?,?,end_time,custom_sample_banks */ + else if (o->type & OBJ_SPINNER) + { + ++ez->nspinners; + } + + /* + * x,y,time,type,sound_type,points,repetitions,distance, + * per_node_sounds,per_node_samples,custom_sample_banks + */ + else if (o->type & OBJ_SLIDER) + { + ++ez->nsliders; + if (ne < 7) + { + return p_warn("W: malformed slider", line); + } + + o->pos[0] = p_float(&e[0]); + o->pos[1] = p_float(&e[1]); + + if (sscanf(e[6].start, "%d", &o->repetitions) != 1) + { + o->repetitions = 1; + p_warn("W: malformed slider repetitions", line); + } + + if (ne > 7) + { + o->distance = p_float(&e[7]); + } + else + { + o->distance = 0; + } + + /* per-node sound types */ + if (ez->mode == MODE_TAIKO && ne > 8 && slice_len(&e[8]) > 0) + { + slice_t p = e[8]; + int i, nodes; + int sound_type = o->sound_types[0]; + + /* + * TODO: there's probably something subtly wrong with this. + * sometimes we get less sound types than nodes + * also I don't know if I'm supposed to include the previous + * sound type from the single sound_type field + */ + + /* repeats + head and tail. no repeats is 0 repetition */ + nodes = o->repetitions + 1; + if (nodes < 0 || nodes > 1000) + { + /* TODO: not sure if 1000 limit is enough */ + p_warn("W: malformed node count", line); + return ERR_SYNTAX; + } + o->sound_types = m_alloc(ez, sizeof(int) * nodes); + if (!o->sound_types) + { + return ERR_OOM; + } + + o->nsound_types = nodes; + for (i = 0; i < nodes; ++i) + { + o->sound_types[i] = sound_type; + } + + for (i = 0; i < nodes; ++i) + { + slice_t node; + int n; + int type; + node.start = node.end = 0; + n = p_consume_til(&p, "|", &node); + if (n < 0 && n != ERR_MORE) + { + return n; + } + if (node.start >= node.end || !node.start) + { + break; + } + p.start += n + 1; + if (sscanf(node.start, "%d", &type) != 1) + { + p_warn("W: malformed sound type", line); + break; + } + o->sound_types[i] = type; + if (p.start >= p.end) + { + break; + } + } + } + } + + return (int)(e[ne - 1].end - line->start); +} + +int p_line(ezpp_t ez, slice_t *line) +{ + int n = 0; + + if (line->start >= line->end) + { + /* empty line */ + return 0; + } + + if (slice_whitespace(line)) + { + return (int)(line->end - line->start); + } + + /* comments (according to lazer) */ + switch (*line->start) + { + case ' ': + case '_': + return (int)(line->end - line->start); + } + + /* from here on we don't care about leading or trailing whitespace */ + slice_trim(line); + + /* C++ style comments */ + if (!strncmp(line->start, "//", 2)) + { + return 0; + } + + /* new section */ + if (*line->start == '[') + { + slice_t section; + int len; + n = p_section_name(line, §ion); + if (n < 0) + { + return n; + } + if ((int)(section.end - section.start) >= (int)sizeof(ez->section)) + { + p_warn("W: truncated long section name", line); + } + len = al_min((int)sizeof(ez->section) - 1, + (int)(section.end - section.start)); + memcpy(ez->section, section.start, len); + ez->section[len] = 0; + return n; + } + + if (!strcmp(ez->section, "Metadata")) + { + n = p_metadata(ez, line); + } + else if (!strcmp(ez->section, "General")) + { + n = p_general(ez, line); + } + else if (!strcmp(ez->section, "Difficulty")) + { + n = p_difficulty(ez, line); + } + else if (!strcmp(ez->section, "TimingPoints")) + { + n = p_timing(ez, line); + } + else if (!strcmp(ez->section, "HitObjects")) + { + n = p_objects(ez, line); + } + else + { + char *p = line->start; + char *fmt_str = "file format v"; + for (; p < line->end && strncmp(p, fmt_str, 13); ++p) + ; + p += 13; + if (p < line->end) + { + if (sscanf(p, "%d", &ez->format_version) == 1) + { + return (int)(line->end - line->start); + } + } + } + + return n; +} + +void p_end(ezpp_t ez) +{ + int i; + float infinity = get_inf(); + float tnext = -infinity; + int tindex = -1; + float ms_per_beat = infinity; + float radius, scaling_factor; + float legacy_multiplier = 1; + + if (!(ez->p_flags & P_FOUND_AR)) + { + /* in old maps ar = od */ + ez->ar = ez->od; + } + + if (!ez->title_unicode) + { + ez->title_unicode = ez->title; + } + + if (!ez->artist_unicode) + { + ez->artist_unicode = ez->artist; + } + +#define s(x) ez->x = ez->x ? ez->x : "(null)" + s(title); + s(title_unicode); + s(artist); + s(artist_unicode); + s(creator); + s(version); +#undef s + + if (ez->base_ar < 0) + ez->base_ar = ez->ar; + else + ez->ar = ez->base_ar; + if (ez->base_cs < 0) + ez->base_cs = ez->cs; + else + ez->cs = ez->base_cs; + if (ez->base_od < 0) + ez->base_od = ez->od; + else + ez->od = ez->base_od; + if (ez->base_hp < 0) + ez->base_hp = ez->hp; + else + ez->hp = ez->base_hp; + mods_apply(ez); + + if (ez->mode == MODE_TAIKO && ez->mode != ez->original_mode) + { + legacy_multiplier = 1.4f; + ez->sv *= legacy_multiplier; + } + + for (i = 0; i < ez->timing_points.len; ++i) + { + timing_t *t = &ez->timing_points.data[i]; + float sv_multiplier = 1.0f; + if (t->change) + { + ms_per_beat = t->ms_per_beat; + } + if (!t->change && t->ms_per_beat < 0) + { + sv_multiplier = -100.0f / t->ms_per_beat; + } + t->beat_len = ms_per_beat / sv_multiplier; + t->px_per_beat = ez->sv * 100.0f; + t->velocity = 100.0f * ez->sv / t->beat_len; + if (ez->format_version >= 8) + { + t->beat_len *= sv_multiplier; + t->px_per_beat *= sv_multiplier; + } + } + + /* + * sliders get 2 + ticks combo (head, tail and ticks) each repetition adds + * an extra combo and an extra set of ticks + * + * calculate the number of slider ticks for one repetition + * --- + * example: a 3.75f beats slider at 1x tick rate will go: + * beat0 (head), beat1 (tick), beat2(tick), beat3(tick), + * beat3.75f(tail) + * so all we have to do is ceil the number of beats and subtract 1 to take + * out the tail + * --- + * the -0.1f is there to prevent ceil from ceiling whole values like 1.0f to + * 2.0f randomly + */ + + ez->nobjects = ez->objects.len; + ez->max_combo = ez->nobjects; + + /* spinners don't give combo in taiko */ + if (ez->mode == MODE_TAIKO) + { + ez->max_combo -= ez->nspinners + ez->nsliders; + } + + /* + * positions are normalized on circle radius so that we + * can calc as if everything was the same circlesize + * this should really be in diffcalc functions but putting it here + * makes it so that i only traverse the hitobjects twice in total + */ + radius = ((PLAYFIELD_WIDTH / 16.0f) * + (1.0f - 0.7f * ((float)ez->cs - 5.0f) / 5.0f)); + + scaling_factor = 52.0f / radius; + + /* cs buff (originally from osuElements) */ + if (radius < CIRCLESIZE_BUFF_TRESHOLD) + { + scaling_factor *= + 1.0f + al_min((CIRCLESIZE_BUFF_TRESHOLD - radius), 5.0f) / 50.0f; + } + + for (i = 0; i < ez->objects.len; ++i) + { + object_t *o = &ez->objects.data[i]; + timing_t *t; + int ticks; + float num_beats; + float *pos; + float dot, det; + + if (o->type & OBJ_SPINNER) + { + pos = playfield_center; + } + else + { + /* sliders also begin with pos so it's fine */ + pos = o->pos; + } + o->normpos[0] = pos[0] * scaling_factor; + o->normpos[1] = pos[1] * scaling_factor; + + /* angle data */ + if (i >= 2) + { + object_t *prev1 = &ez->objects.data[i - 1]; + object_t *prev2 = &ez->objects.data[i - 2]; + float v1[2], v2[2]; + v2f_sub(v1, prev2->normpos, prev1->normpos); + v2f_sub(v2, o->normpos, prev1->normpos); + dot = v2f_dot(v1, v2); + det = v1[0] * v2[1] - v1[1] * v2[0]; + o->angle = (float)fabs(atan2(det, dot)); + } + else + { + o->angle = get_nan(); + } + + /* keep track of the current timing point */ + while (o->time >= tnext) + { + ++tindex; + if (tindex + 1 < ez->timing_points.len) + { + tnext = ez->timing_points.data[tindex + 1].time; + } + else + { + tnext = infinity; + } + } + o->timing_point = tindex; + t = &ez->timing_points.data[tindex]; + + if (!(o->type & OBJ_SLIDER)) + { + continue; + } + + o->duration = o->distance * o->repetitions / t->velocity; + o->duration *= legacy_multiplier; + o->tick_spacing = al_min(t->beat_len / ez->tick_rate, + o->duration / o->repetitions); + o->slider_is_drum_roll = (o->tick_spacing > 0 && o->duration < 2 * t->beat_len); + + /* slider ticks for max_combo */ + switch (ez->mode) + { + case MODE_TAIKO: + { + if (o->slider_is_drum_roll && ez->mode != ez->original_mode) + { + ez->max_combo += (int)(ceil((o->duration + o->tick_spacing / 8) / o->tick_spacing)); + } + break; + } + case MODE_STD: + /* std slider ticks */ + num_beats = (o->distance * o->repetitions) / t->px_per_beat; + + ticks = (int)ceil( + (num_beats - 0.1f) / o->repetitions * ez->tick_rate); + --ticks; + + ticks *= o->repetitions; /* account for repetitions */ + ticks += o->repetitions + 1; /* add heads and tails */ + + /* + * actually doesn't include first head because we already + * added it by setting res = nobjects + */ + ez->max_combo += al_max(0, ticks - 1); + break; + } + } +} + +void p_reset(ezpp_t ez) +{ + ez->ncircles = ez->nsliders = ez->nspinners = ez->nobjects = 0; + ez->objects.len = 0; + ez->timing_points.len = 0; + m_free(ez); + memset(ez->section, 0, sizeof(ez->section)); +} + +int p_map(ezpp_t ez, FILE *f) +{ + char *p; + char *bufend; + int c, n; + slice_t line; + if (!f) + { + return ERR_IO; + } + + p_reset(ez); + + /* reading loop */ + bufend = ez->buf + sizeof(ez->buf) - 1; + do + { + p = ez->buf; + for (;;) + { + if (p >= bufend) + { + return ERR_TRUNCATED; + } + c = fgetc(f); + if (c == '\n' || c == EOF) + { + break; + } + *p++ = (char)c; + } + *p = 0; + line.start = ez->buf; + line.end = p; + n = p_line(ez, &line); + if (n < 0) + { + return n; + } + } while (c != EOF); + + p_end(ez); + ez->nobjects = ez->objects.len; + + return (int)(p - ez->buf); +} + +/* TODO: see if i can shrink this function */ +int p_map_mem(ezpp_t ez, char *data, int data_size) +{ + int res = 0; + int n; + int nlines = 0; /* complete lines in the current chunk */ + slice_t s; /* points to the remaining data in buf */ + + if (!data || data_size == 0) + { + return ERR_IO; + } + + p_reset(ez); + + s.start = data; + s.end = data + data_size; + + /* parsing loop */ + for (; s.start < s.end;) + { + slice_t line; + n = p_consume_til(&s, "\n", &line); + + if (n < 0) + { + if (n != ERR_MORE) + { + return n; + } + if (!nlines) + { + /* line doesn't fit the entire buffer */ + return ERR_TRUNCATED; + } + /* EOF, so we must process the remaining data as a line */ + line = s; + n = (int)(s.end - s.start); + } + else + { + ++n; /* also skip the \n */ + } + + res += n; + s.start += n; + ++nlines; + + n = p_line(ez, &line); + if (n < 0) + { + return n; + } + + res += n; + } + + p_end(ez); + + return res; +} + +/* diff calc ----------------------------------------------------------- */ + +/* based on tom94's osu!tp aimod and osuElements */ + +#define SINGLE_SPACING 125.0f +#define STAR_SCALING_FACTOR 0.0675f /* star rating multiplier */ +#define EXTREME_SCALING_FACTOR 0.5f /* used to mix aim/speed stars */ +#define STRAIN_STEP 400.0f /* diffcalc uses peak strains of 400ms chunks */ +#define DECAY_WEIGHT 0.9f /* peak strains are added in a weighed sum */ +#define MAX_SPEED_BONUS 45.0f /* ~330BPM 1/4 streams */ +#define MIN_SPEED_BONUS 75.0f /* ~200BPM 1/4 streams */ +#define ANGLE_BONUS_SCALE 90 +#define AIM_TIMING_THRESHOLD 107 +#define SPEED_ANGLE_BONUS_BEGIN (5 * M_PI / 6) +#define AIM_ANGLE_BONUS_BEGIN (M_PI / 3) +float decay_base[] = {0.3f, 0.15f}; /* strains decay per interval */ +float weight_scaling[] = {1400.0f, 26.25f}; /* balances aim/speed */ + +/* + * TODO: unbloat these params + * this function has become a mess with the latest changes, I should split + * it into separate funcs for speed and im + */ +float d_spacing_weight(float distance, float delta_time, + float prev_distance, float prev_delta_time, + float angle, int type, int *is_single) +{ + float angle_bonus; + float strain_time = al_max(delta_time, 50.0f); + switch (type) + { + case DIFF_SPEED: + { + float speed_bonus; + *is_single = distance > SINGLE_SPACING; + distance = al_min(distance, SINGLE_SPACING); + delta_time = al_max(delta_time, MAX_SPEED_BONUS); + speed_bonus = 1.0f; + if (delta_time < MIN_SPEED_BONUS) + { + speed_bonus += (float) + pow((MIN_SPEED_BONUS - delta_time) / 40.0f, 2); + } + angle_bonus = 1.0f; + if (!is_nan(angle) && angle < SPEED_ANGLE_BONUS_BEGIN) + { + float s = (float)sin(1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)); + angle_bonus += (float)pow(s, 2) / 3.57f; + if (angle < M_PI / 2) + { + angle_bonus = 1.28f; + if (distance < ANGLE_BONUS_SCALE && angle < M_PI / 4) + { + angle_bonus += (1 - angle_bonus) * al_min((ANGLE_BONUS_SCALE - distance) / 10, 1); + } + else if (distance < ANGLE_BONUS_SCALE) + { + angle_bonus += (1 - angle_bonus) * al_min((ANGLE_BONUS_SCALE - distance) / 10, 1) * (float)sin((M_PI / 2 - angle) * 4 / M_PI); + } + } + } + return ( + (1 + (speed_bonus - 1) * 0.75f) * + angle_bonus * + (0.95f + speed_bonus * (float)pow(distance / SINGLE_SPACING, 3.5))) / + strain_time; + } + case DIFF_AIM: + { + float result = 0; + float weighted_distance; + float prev_strain_time = al_max(prev_delta_time, 50.0f); + if (!is_nan(angle) && angle > AIM_ANGLE_BONUS_BEGIN) + { + angle_bonus = (float)sqrt( + al_max(prev_distance - ANGLE_BONUS_SCALE, 0) * pow(sin(angle - AIM_ANGLE_BONUS_BEGIN), 2) * al_max(distance - ANGLE_BONUS_SCALE, 0)); + result = 1.5f * (float)pow(al_max(0, angle_bonus), 0.99) / al_max(AIM_TIMING_THRESHOLD, prev_strain_time); + } + weighted_distance = (float)pow(distance, 0.99); + return al_max( + result + weighted_distance / + al_max(AIM_TIMING_THRESHOLD, strain_time), + weighted_distance / strain_time); + } + } + return 0.0f; +} + +void d_calc_strain(int type, object_t *o, object_t *prev, float speedmul) +{ + float res = 0; + float time_elapsed = (o->time - prev->time) / speedmul; + float decay = (float)pow(decay_base[type], time_elapsed / 1000.0f); + float scaling = weight_scaling[type]; + + o->delta_time = time_elapsed; + + /* this implementation doesn't account for sliders */ + if (o->type & (OBJ_SLIDER | OBJ_CIRCLE)) + { + float diff[2]; + v2f_sub(diff, o->normpos, prev->normpos); + o->d_distance = v2f_len(diff); + res = d_spacing_weight(o->d_distance, time_elapsed, prev->d_distance, + prev->delta_time, o->angle, type, &o->is_single); + res *= scaling; + } + + o->strains[type] = prev->strains[type] * decay + res; +} + +#if defined(_WIN32) +#define FORCECDECL __cdecl +#else +#define FORCECDECL +#endif + +int FORCECDECL dbl_desc(void const *a, void const *b) +{ + float x = *(float const *)a; + float y = *(float const *)b; + if (x < y) + return 1; + if (x == y) + return 0; + return -1; +} + +int d_update_max_strains(ezpp_t ez, float decay_factor, + float cur_time, float prev_time, float cur_strain, float prev_strain, + int first_obj) +{ + /* make previous peak strain decay until the current obj */ + while (cur_time > ez->interval_end) + { + if (!array_append(&ez->highest_strains, ez->max_strain)) + { + return ERR_OOM; + } + if (first_obj) + { + ez->max_strain = 0; + } + else + { + float decay; + decay = (float)pow(decay_factor, + (ez->interval_end - prev_time) / 1000.0f); + ez->max_strain = prev_strain * decay; + } + ez->interval_end += STRAIN_STEP * ez->speed_mul; + } + + ez->max_strain = al_max(ez->max_strain, cur_strain); + return 0; +} + +void d_weigh_strains(ezpp_t ez, float *pdiff, float *ptotal) +{ + int i; + int nstrains = 0; + float *strains; + float total = 0; + float difficulty = 0; + float weight = 1.0f; + + strains = (float *)ez->highest_strains.data; + nstrains = ez->highest_strains.len; + + /* sort strains from highest to lowest */ + qsort(strains, nstrains, sizeof(float), dbl_desc); + + for (i = 0; i < nstrains; ++i) + { + total += (float)pow(strains[i], 1.2); + difficulty += strains[i] * weight; + weight *= DECAY_WEIGHT; + } + + *pdiff = difficulty; + if (ptotal) + { + *ptotal = total; + } +} + +int d_calc_individual(ezpp_t ez, int type) +{ + int i; + + /* + * the first object doesn't generate a strain, + * so we begin with an incremented interval end + */ + ez->max_strain = 0.0f; + ez->interval_end = ((float)ceil(ez->objects.data[0].time / (STRAIN_STEP * ez->speed_mul)) * STRAIN_STEP * ez->speed_mul); + ez->highest_strains.len = 0; + + for (i = 0; i < ez->objects.len; ++i) + { + int err; + object_t *o = &ez->objects.data[i]; + object_t *prev = 0; + float prev_time = 0, prev_strain = 0; + if (i > 0) + { + prev = &ez->objects.data[i - 1]; + d_calc_strain(type, o, prev, ez->speed_mul); + prev_time = prev->time; + prev_strain = prev->strains[type]; + } + err = d_update_max_strains(ez, decay_base[type], o->time, prev_time, + o->strains[type], prev_strain, i == 0); + if (err < 0) + { + return err; + } + } + + /* + * the peak strain will not be saved for + * the last section in the above loop + */ + if (!array_append(&ez->highest_strains, ez->max_strain)) + { + return ERR_OOM; + } + + switch (type) + { + case DIFF_SPEED: + d_weigh_strains(ez, &ez->speed_stars, &ez->speed_difficulty); + break; + case DIFF_AIM: + d_weigh_strains(ez, &ez->aim_stars, &ez->aim_difficulty); + break; + } + return 0; +} + +float d_length_bonus(float stars, float difficulty) +{ + return 0.32f + 0.5f * (log10f(difficulty + stars) - log10f(stars)); +} + +int d_std(ezpp_t ez) +{ + int res; + + res = d_calc_individual(ez, DIFF_SPEED); + if (res < 0) + { + return res; + } + + res = d_calc_individual(ez, DIFF_AIM); + if (res < 0) + { + return res; + } + + ez->aim_length_bonus = d_length_bonus(ez->aim_stars, ez->aim_difficulty); + ez->speed_length_bonus = d_length_bonus(ez->speed_stars, ez->speed_difficulty); + ez->aim_stars = (float)sqrt(ez->aim_stars) * STAR_SCALING_FACTOR; + ez->speed_stars = (float)sqrt(ez->speed_stars) * STAR_SCALING_FACTOR; + + if (ez->mods & MODS_TOUCH_DEVICE) + { + ez->aim_stars = (float)pow(ez->aim_stars, 0.8f); + } + + /* calculate total star rating */ + ez->stars = ez->aim_stars + ez->speed_stars + + (float)fabs(ez->speed_stars - ez->aim_stars) * EXTREME_SCALING_FACTOR; + + return 0; +} + +/* taiko diff calc ----------------------------------------------------- */ + +#define TAIKO_STAR_SCALING_FACTOR 0.04125f +#define TAIKO_TYPE_CHANGE_BONUS 0.75f /* object type change bonus */ +#define TAIKO_RHYTHM_CHANGE_BONUS 1.0f +#define TAIKO_RHYTHM_CHANGE_BASE_THRESHOLD 0.2f +#define TAIKO_RHYTHM_CHANGE_BASE 2.0f + +typedef struct taiko_object +{ + int hit; + float strain; + float time; + float time_elapsed; + int rim; + int same_since; /* streak of hits of the same type (rim/center) */ + /* + * was the last hit type change at an even same_since count? + * -1 if there is no previous switch (for example if the + * previous object was not a hit + */ + int last_switch_even; +} taiko_object_t; + +/* object type change bonus */ +float taiko_change_bonus(taiko_object_t *cur, taiko_object_t *prev) +{ + if (prev->rim != cur->rim) + { + cur->last_switch_even = prev->same_since % 2 == 0; + if (prev->last_switch_even != cur->last_switch_even && + prev->last_switch_even != -1) + { + return TAIKO_TYPE_CHANGE_BONUS; + } + } + else + { + cur->last_switch_even = prev->last_switch_even; + cur->same_since = prev->same_since + 1; + } + return 0; +} + +/* rhythm change bonus */ +float taiko_rhythm_bonus(taiko_object_t *cur, taiko_object_t *prev) +{ + float ratio; + float diff; + + if (cur->time_elapsed == 0 || prev->time_elapsed == 0) + { + return 0; + } + + ratio = al_max(prev->time_elapsed / cur->time_elapsed, + cur->time_elapsed / prev->time_elapsed); + + if (ratio >= 8) + { + return 0; + } + + /* this is log base TAIKO_RHYTHM_CHANGE_BASE of ratio */ + diff = (float)fmod(log(ratio) / log(TAIKO_RHYTHM_CHANGE_BASE), 1.0f); + + /* + * threshold that determines whether the rhythm changed enough + * to be worthy of the bonus + */ + if (diff > TAIKO_RHYTHM_CHANGE_BASE_THRESHOLD && + diff < 1 - TAIKO_RHYTHM_CHANGE_BASE_THRESHOLD) + { + return TAIKO_RHYTHM_CHANGE_BONUS; + } + + return 0; +} + +void taiko_strain(taiko_object_t *cur, taiko_object_t *prev) +{ + float decay; + float addition = 1.0f; + float factor = 1.0f; + + decay = (float)pow(decay_base[0], cur->time_elapsed / 1000.0f); + + /* + * we only have strains for hits, also ignore objects that are + * more than 1 second apart + */ + if (prev->hit && cur->hit && cur->time - prev->time < 1000.0f) + { + addition += taiko_change_bonus(cur, prev); + addition += taiko_rhythm_bonus(cur, prev); + } + + /* 300+bpm streams nerf? */ + if (cur->time_elapsed < 50.0f) + { + factor = 0.4f + 0.6f * cur->time_elapsed / 50.0f; + } + + cur->strain = prev->strain * decay + addition * factor; +} + +void swap_ptrs(void **a, void **b) +{ + void *tmp; + tmp = *a; + *a = *b; + *b = tmp; +} + +int d_taiko(ezpp_t ez) +{ + int i, result; + + /* this way we can swap cur and prev without copying */ + taiko_object_t curprev[2]; + taiko_object_t *cur = &curprev[0]; + taiko_object_t *prev = &curprev[1]; + + ez->highest_strains.len = 0; + ez->max_strain = 0.0f; + ez->interval_end = STRAIN_STEP * ez->speed_mul; + + /* + * TODO: separate taiko conversion into its own function + * so that it can be reused? probably slower, but cleaner, + * more modular and more readable + */ + for (i = 0; i < ez->nobjects; ++i) + { + object_t *o = &ez->objects.data[i]; + + cur->hit = (o->type & OBJ_CIRCLE) != 0; + cur->time = o->time; + + if (i > 0) + { + cur->time_elapsed = (cur->time - prev->time) / ez->speed_mul; + } + else + { + cur->time_elapsed = 0; + } + + if (!o->sound_types) + { + return ERR_SYNTAX; + } + + cur->strain = 1; + cur->same_since = 1; + cur->last_switch_even = -1; + cur->rim = (o->sound_types[0] & (SOUND_CLAP | SOUND_WHISTLE)) != 0; + + if (ez->original_mode == MODE_TAIKO) + { + goto continue_loop; + } + + if (o->type & OBJ_SLIDER) + { + /* TODO: too much indentation, pull this out */ + int isound = 0; + float j; + + /* drum roll, ignore */ + if (!o->slider_is_drum_roll || i == 0) + { + goto continue_loop; + } + + /* + * sliders that meet the requirements will + * become streams of the slider's tick rate + */ + for (j = o->time; + j < o->time + o->duration + o->tick_spacing / 8; + j += o->tick_spacing) + { + int sound_type = o->sound_types[isound]; + cur->rim = (sound_type & (SOUND_CLAP | SOUND_WHISTLE)) != 0; + cur->hit = 1; + cur->time = j; + + cur->time_elapsed = (cur->time - prev->time) / ez->speed_mul; + cur->strain = 1; + cur->same_since = 1; + cur->last_switch_even = -1; + + /* update strains for this hit */ + if (i > 0 || j > o->time) + { + taiko_strain(cur, prev); + } + + result = d_update_max_strains(ez, decay_base[0], cur->time, + prev->time, cur->strain, prev->strain, i == 0 && j == o->time); + /* warning: j check might fail, floatcheck this */ + + if (result < 0) + { + return result; + } + + /* loop through the slider's sounds */ + ++isound; + isound %= o->nsound_types; + + swap_ptrs((void **)&prev, (void **)&cur); + } + + /* + * since we processed the slider as multiple hits, + * we must skip the prev/cur swap which we already did + * in the above loop + */ + continue; + } + + continue_loop: + /* update strains for hits and other object types */ + if (i > 0) + { + taiko_strain(cur, prev); + } + + result = d_update_max_strains(ez, decay_base[0], cur->time, prev->time, + cur->strain, prev->strain, i == 0); + + if (result < 0) + { + return result; + } + + swap_ptrs((void **)&prev, (void **)&cur); + } + + d_weigh_strains(ez, &ez->speed_stars, 0); + ez->speed_stars *= TAIKO_STAR_SCALING_FACTOR; + ez->stars = ez->speed_stars; + + return 0; +} + +int d_calc(ezpp_t ez) +{ + switch (ez->mode) + { + case MODE_STD: + return d_std(ez); + case MODE_TAIKO: + return d_taiko(ez); + } + info("this gamemode is not yet supported\n"); + return ERR_NOTIMPLEMENTED; +} + +/* acc calc ------------------------------------------------------------ */ + +float acc_calc(int n300, int n100, int n50, int misses) +{ + int total_hits = n300 + n100 + n50 + misses; + float acc = 0; + if (total_hits > 0) + { + acc = (n50 * 50.0f + n100 * 100.0f + n300 * 300.0f) / (total_hits * 300.0f); + } + return acc; +} + +void acc_round(float acc_percent, int nobjects, int misses, int *n300, + int *n100, int *n50) +{ + int max300; + float maxacc; + misses = al_min(nobjects, misses); + max300 = nobjects - misses; + maxacc = acc_calc(max300, 0, 0, misses) * 100.0f; + acc_percent = al_max(0.0f, al_min(maxacc, acc_percent)); + *n50 = 0; + + /* just some black magic maths from wolfram alpha */ + *n100 = (int)al_round( + -3.0f * ((acc_percent * 0.01f - 1.0f) * nobjects + misses) * 0.5f); + + if (*n100 > nobjects - misses) + { + /* acc lower than all 100s, use 50s */ + *n100 = 0; + *n50 = (int)al_round( + -6.0f * ((acc_percent * 0.01f - 1.0f) * nobjects + misses) * 0.2f); + *n50 = al_min(max300, *n50); + } + else + { + *n100 = al_min(max300, *n100); + } + + *n300 = nobjects - *n100 - *n50 - misses; +} + +float taiko_acc_calc(int n300, int n150, int nmiss) +{ + int total_hits = n300 + n150 + nmiss; + float acc = 0; + if (total_hits > 0) + { + acc = (n150 * 150.0f + n300 * 300.0f) / (total_hits * 300.0f); + } + return acc; +} + +void taiko_acc_round(float acc_percent, int nobjects, int nmisses, + int *n300, int *n150) +{ + int max300; + float maxacc; + nmisses = al_min(nobjects, nmisses); + max300 = nobjects - nmisses; + maxacc = acc_calc(max300, 0, 0, nmisses) * 100.0f; + acc_percent = al_max(0.0f, al_min(maxacc, acc_percent)); + /* just some black magic maths from wolfram alpha */ + *n150 = (int)al_round( + -2.0f * ((acc_percent * 0.01f - 1.0f) * nobjects + nmisses)); + *n150 = al_min(max300, *n150); + *n300 = nobjects - *n150 - nmisses; +} + +/* std pp calc --------------------------------------------------------- */ + +/* some kind of formula to get a base pp value from stars */ +float base_pp(float stars) +{ + return (float)pow(5.0f * al_max(1.0f, stars / 0.0675f) - 4.0f, 3.0f) / 100000.0f; +} + +int pp_std(ezpp_t ez) +{ + int ncircles = ez->ncircles; + float nobjects_over_2k = ez->nobjects / 2000.0f; + float length_bonus = (0.95f + + 0.4f * al_min(1.0f, nobjects_over_2k) + + (ez->nobjects > 2000 ? (float)log10(nobjects_over_2k) * 0.5f : 0.0f)); + + float miss_penality_aim = 0.97 * pow(1 - pow((double)ez->nmiss / ez->nobjects, 0.775), ez->nmiss); + float miss_penality_speed = 0.97 * pow(1 - pow((double)ez->nmiss / ez->nobjects, 0.775f), pow(ez->nmiss, 0.875f)); + + float combo_break = ((float)pow(ez->combo, 0.8f) / (float)pow(ez->max_combo, 0.8f)); + float ar_bonus; + float final_multiplier; + float acc_bonus, od_bonus; + float od_squared; + float hd_bonus; + + /* acc used for pp is different in scorev1 because it ignores sliders */ + float real_acc; + float accuracy; + + ez->nspinners = ez->nobjects - ez->nsliders - ez->ncircles; + + if (ez->max_combo <= 0) + { + info("W: max_combo <= 0, changing to 1\n"); + ez->max_combo = 1; + } + + accuracy = acc_calc(ez->n300, ez->n100, ez->n50, ez->nmiss); + + /* + * scorev1 ignores sliders and spinners since they are free 300s + * can go negative if we miss everything so we must clamp it + */ + + switch (ez->score_version) + { + case 1: + real_acc = acc_calc( + al_max(0, ez->n300 - ez->nsliders - ez->nspinners), + ez->n100, ez->n50, ez->nmiss); + break; + case 2: + real_acc = accuracy; + ncircles = ez->nobjects; + break; + default: + info("unsupported scorev%d\n", ez->score_version); + return ERR_NOTIMPLEMENTED; + } + + /* ar bonus -------------------------------------------------------- */ + ar_bonus = 0.0f; + + /* high ar bonus */ + if (ez->ar > 10.33f) + { + ar_bonus += 0.4f * (ez->ar - 10.33f); + } + + /* low ar bonus */ + else if (ez->ar < 8.0f) + { + ar_bonus += 0.01f * (8.0f - ez->ar); + } + + /* aim pp ---------------------------------------------------------- */ + ez->aim_pp = base_pp(ez->aim_stars); + ez->aim_pp *= length_bonus; + if (ez->nmiss > 0) + { + ez->aim_pp *= miss_penality_aim; + } + ez->aim_pp *= combo_break; + ez->aim_pp *= 1.0f + (float)al_min(ar_bonus, ar_bonus * (ez->nobjects / 1000.0f)); + + /* hidden */ + hd_bonus = 1.0f; + if (ez->mods & MODS_HD) + { + hd_bonus += 0.04f * (12.0f - ez->ar); + } + + ez->aim_pp *= hd_bonus; + + /* flashlight */ + if (ez->mods & MODS_FL) + { + float fl_bonus = 1.0f + 0.35f * al_min(1.0f, ez->nobjects / 200.0f); + if (ez->nobjects > 200) + { + fl_bonus += 0.3f * al_min(1, (ez->nobjects - 200) / 300.0f); + } + if (ez->nobjects > 500) + { + fl_bonus += (ez->nobjects - 500) / 1200.0f; + } + ez->aim_pp *= fl_bonus; + } + + /* acc bonus (bad aim can lead to bad acc) */ + acc_bonus = 0.5f + accuracy / 2.0f; + + /* od bonus (high od requires better aim timing to acc) */ + od_squared = (float)pow(ez->od, 2); + od_bonus = 0.98f + od_squared / 2500.0f; + + ez->aim_pp *= acc_bonus; + ez->aim_pp *= od_bonus; + + /* speed pp -------------------------------------------------------- */ + ez->speed_pp = base_pp(ez->speed_stars); + ez->speed_pp *= length_bonus; + if (ez->nmiss > 0) + { + ez->speed_pp *= miss_penality_speed; + } + ez->speed_pp *= combo_break; + if (ez->ar > 10.33f) + { + ez->speed_pp *= 1.0f + (float)al_min(ar_bonus, ar_bonus * (ez->nobjects / 1000.0f)); + ; + } + ez->speed_pp *= hd_bonus; + + /* scale the speed value with accuracy slightly */ + ez->speed_pp *= (0.95f + od_squared / 750) * (float)pow(accuracy, (14.5 - al_max(ez->od, 8)) / 2); + + /* it's important to also consider accuracy difficulty when doing that */ + ez->speed_pp *= (float)pow(0.98f, ez->n50 < ez->nobjects / 500.0f ? 0.00 : ez->n50 - ez->nobjects / 500.0f); + + /* acc pp ---------------------------------------------------------- */ + /* arbitrary values tom crafted out of trial and error */ + ez->acc_pp = (float)pow(1.52163f, ez->od) * + (float)pow(real_acc, 24.0f) * 2.83f; + + /* length bonus (not the same as speed/aim length bonus) */ + ez->acc_pp *= al_min(1.15f, (float)pow(ncircles / 1000.0f, 0.3f)); + + if (ez->mods & MODS_HD) + ez->acc_pp *= 1.08f; + if (ez->mods & MODS_FL) + ez->acc_pp *= 1.02f; + + /* total pp -------------------------------------------------------- */ + final_multiplier = 1.12f; + if (ez->mods & MODS_NF) + final_multiplier *= (float)al_max(0.9f, 1.0f - 0.2f * ez->nmiss); + if (ez->mods & MODS_SO) + final_multiplier *= 1.0 - pow((double)ez->nspinners / ez->nobjects, 0.85); + + ez->pp = (float)(pow( + pow(ez->aim_pp, 1.1f) + + pow(ez->speed_pp, 1.1f) + + pow(ez->acc_pp, 1.1f), + 1.0f / 1.1f) * + final_multiplier); + + ez->accuracy_percent = accuracy * 100.0f; + + return 0; +} + +/* taiko pp calc ------------------------------------------------------- */ + +int pp_taiko(ezpp_t ez) +{ + float length_bonus; + float final_multiplier; + float accuracy; + + ez->n300 = al_max(0, ez->max_combo - ez->n100 - ez->nmiss); + accuracy = taiko_acc_calc(ez->n300, ez->n100, ez->nmiss); + + /* base acc pp */ + ez->acc_pp = (float)pow(150.0f / ez->odms, 1.1f); + ez->acc_pp *= (float)pow(accuracy, 15.0f) * 22.0f; + + /* length bonus */ + ez->acc_pp *= al_min(1.15f, (float)pow(ez->max_combo / 1500.0f, 0.3f)); + + /* base speed pp */ + ez->speed_pp = ((float)pow(5.0f * al_max(1.0f, ez->stars / 0.0075f) - 4.0f, 2.0f)) / 100000.0f; + + /* length bonus (not the same as acc length bonus) */ + length_bonus = 1.0f + 0.1f * al_min(1.0f, ez->max_combo / 1500.0f); + ez->speed_pp *= length_bonus; + + /* miss penality */ + ez->speed_pp *= (float)pow(0.985f, ez->nmiss); + + if (ez->max_combo > 0) + { + ez->speed_pp *= (al_min((float)pow(ez->combo - ez->nmiss, 0.5f) / (float)pow(ez->max_combo, 0.5f), 1.0f)); + } + + /* speed mod bonuses */ + if (ez->mods & MODS_HD) + { + ez->speed_pp *= 1.025f; + } + + if (ez->mods & MODS_FL) + { + ez->speed_pp *= 1.05f * length_bonus; + } + + /* acc scaling */ + ez->speed_pp *= accuracy; + + /* overall multipliers */ + final_multiplier = 1.1f; + if (ez->mods & MODS_NF) + final_multiplier *= 0.90f; + if (ez->mods & MODS_HD) + final_multiplier *= 1.10f; + + ez->pp = ((float)pow( + pow(ez->speed_pp, 1.1f) + + pow(ez->acc_pp, 1.1f), + 1.0f / 1.1f) * + final_multiplier); + + ez->accuracy_percent = accuracy * 100.0f; + + return 0; +} + +/* main interface ------------------------------------------------------ */ + +int params_from_map(ezpp_t ez) +{ + int res; + + ez->ar = ez->cs = ez->hp = ez->od = 5.0f; + ez->sv = ez->tick_rate = 1.0f; + + ez->p_flags = 0; + if (ez->mode_override) + { + ez->p_flags |= P_OVERRIDE_MODE; + } + + if (ez->data) + { + res = p_map_mem(ez, ez->data, ez->data_size); + } + else if (!strcmp(ez->map, "-")) + { + res = p_map(ez, stdin); + } + else + { + FILE *f = fopen(ez->map, "rb"); + if (!f) + { + perror("fopen"); + res = ERR_IO; + } + else + { + res = p_map(ez, f); + fclose(f); + } + } + + if (res < 0) + { + goto cleanup; + } + + if (!ez->aim_stars && !ez->speed_stars) + { + res = d_calc(ez); + if (res < 0) + { + goto cleanup; + } + } + +cleanup: + return res; +} + +int calc(ezpp_t ez) +{ + int res; + + if (!ez->max_combo && (ez->map || ez->data)) + { + if (ez->flags & AUTOCALC_BIT) + { + ez->base_ar = ez->base_od = ez->base_cs = ez->base_hp = -1; + } + res = params_from_map(ez); + if (res < 0) + { + return res; + } + } + else + { + if (ez->base_ar >= 0) + ez->ar = ez->base_ar; + if (ez->base_od >= 0) + ez->od = ez->base_od; + if (ez->base_cs >= 0) + ez->cs = ez->base_cs; + if (ez->base_hp >= 0) + ez->hp = ez->base_hp; + mods_apply(ez); + } + + if (ez->mode == MODE_TAIKO) + { + ez->stars = ez->speed_stars; + } + + if (ez->accuracy_percent >= 0) + { + switch (ez->mode) + { + case MODE_STD: + acc_round(ez->accuracy_percent, ez->nobjects, ez->nmiss, + &ez->n300, &ez->n100, &ez->n50); + break; + case MODE_TAIKO: + taiko_acc_round(ez->accuracy_percent, ez->max_combo, + ez->nmiss, &ez->n300, &ez->n100); + break; + } + } + + if (ez->combo < 0) + { + ez->combo = ez->max_combo - ez->nmiss; + } + + ez->n300 = ez->nobjects - ez->n100 - ez->n50 - ez->nmiss; + + switch (ez->mode) + { + case MODE_STD: + res = pp_std(ez); + break; + case MODE_TAIKO: + res = pp_taiko(ez); + break; + default: + info("pp calc for this mode is not yet supported\n"); + return ERR_NOTIMPLEMENTED; + } + + if (res < 0) + { + return res; + } + + return 0; +} + +OPPAIAPI +ezpp_t ezpp_new(void) +{ + ezpp_t ez = calloc(sizeof(struct ezpp), 1); + if (ez) + { + ez->mode = MODE_STD; + ez->mods = MODS_NOMOD; + ez->combo = -1; + ez->score_version = 1; + ez->accuracy_percent = -1; + ez->base_ar = ez->base_od = ez->base_cs = ez->base_hp = -1; + array_reserve(&ez->objects, 600); + array_reserve(&ez->timing_points, 16); + array_reserve(&ez->highest_strains, 600); + } + return ez; +} + +void free_owned_map(ezpp_t ez) +{ + if (ez->flags & OWNS_MAP_BIT) + { + free(ez->map); + free(ez->data); + ez->flags &= ~OWNS_MAP_BIT; + } + ez->map = 0; + ez->data = 0; + ez->data_size = 0; + if (ez->flags & AUTOCALC_BIT) + { + ez->max_combo = 0; /* force re-parse */ + } +} + +OPPAIAPI +void ezpp_free(ezpp_t ez) +{ + free_owned_map(ez); + array_free(&ez->objects); + array_free(&ez->timing_points); + array_free(&ez->highest_strains); + m_free(ez); + free(ez); +} + +OPPAIAPI +int ezpp(ezpp_t ez, char *mapfile) +{ + free_owned_map(ez); + ez->map = mapfile; + return calc(ez); +} + +OPPAIAPI +int ezpp_data(ezpp_t ez, char *data, int data_size) +{ + free_owned_map(ez); + ez->data = data; + ez->data_size = data_size; + return calc(ez); +} + +void *memclone(void *p, int size) +{ + void *res = malloc(size); + if (res) + memcpy(res, p, size); + return res; +} + +char *strclone(char *s) +{ + int len = (int)strlen(s) + 1; + return memclone(s, len); +} + +OPPAIAPI +int ezpp_dup(ezpp_t ez, char *mapfile) +{ + free_owned_map(ez); + ez->flags |= OWNS_MAP_BIT; + ez->map = strclone(mapfile); + return calc(ez); +} + +OPPAIAPI +int ezpp_data_dup(ezpp_t ez, char *data, int data_size) +{ + free_owned_map(ez); + ez->flags |= OWNS_MAP_BIT; + ez->data = memclone(data, data_size); + ez->data_size = data_size; + return calc(ez); +} + +OPPAIAPI float ezpp_pp(ezpp_t ez) { return ez->pp; } +OPPAIAPI float ezpp_stars(ezpp_t ez) { return ez->stars; } +OPPAIAPI int ezpp_mode(ezpp_t ez) { return ez->mode; } +OPPAIAPI int ezpp_combo(ezpp_t ez) { return ez->combo; } +OPPAIAPI int ezpp_max_combo(ezpp_t ez) { return ez->max_combo; } +OPPAIAPI int ezpp_mods(ezpp_t ez) { return ez->mods; } +OPPAIAPI int ezpp_score_version(ezpp_t ez) { return ez->score_version; } +OPPAIAPI float ezpp_aim_stars(ezpp_t ez) { return ez->aim_stars; } +OPPAIAPI float ezpp_speed_stars(ezpp_t ez) { return ez->speed_stars; } +OPPAIAPI float ezpp_aim_pp(ezpp_t ez) { return ez->aim_pp; } +OPPAIAPI float ezpp_speed_pp(ezpp_t ez) { return ez->speed_pp; } +OPPAIAPI float ezpp_acc_pp(ezpp_t ez) { return ez->acc_pp; } +OPPAIAPI float ezpp_accuracy_percent(ezpp_t ez) { return ez->accuracy_percent; } +OPPAIAPI int ezpp_n300(ezpp_t ez) { return ez->n300; } +OPPAIAPI int ezpp_n100(ezpp_t ez) { return ez->n100; } +OPPAIAPI int ezpp_n50(ezpp_t ez) { return ez->n50; } +OPPAIAPI int ezpp_nmiss(ezpp_t ez) { return ez->nmiss; } +OPPAIAPI char *ezpp_title(ezpp_t ez) { return ez->title; } +OPPAIAPI char *ezpp_title_unicode(ezpp_t ez) { return ez->title_unicode; } +OPPAIAPI char *ezpp_artist(ezpp_t ez) { return ez->artist; } +OPPAIAPI char *ezpp_artist_unicode(ezpp_t ez) { return ez->artist_unicode; } +OPPAIAPI char *ezpp_creator(ezpp_t ez) { return ez->creator; } +OPPAIAPI char *ezpp_version(ezpp_t ez) { return ez->version; } +OPPAIAPI int ezpp_ncircles(ezpp_t ez) { return ez->ncircles; } +OPPAIAPI int ezpp_nsliders(ezpp_t ez) { return ez->nsliders; } +OPPAIAPI int ezpp_nspinners(ezpp_t ez) { return ez->nspinners; } +OPPAIAPI int ezpp_nobjects(ezpp_t ez) { return ez->nobjects; } +OPPAIAPI float ezpp_ar(ezpp_t ez) { return ez->ar; } +OPPAIAPI float ezpp_cs(ezpp_t ez) { return ez->cs; } +OPPAIAPI float ezpp_od(ezpp_t ez) { return ez->od; } +OPPAIAPI float ezpp_hp(ezpp_t ez) { return ez->hp; } +OPPAIAPI float ezpp_odms(ezpp_t ez) { return ez->odms; } +OPPAIAPI int ezpp_autocalc(ezpp_t ez) { return ez->flags & AUTOCALC_BIT; } + +OPPAIAPI float ezpp_time_at(ezpp_t ez, int i) +{ + return ez->objects.len ? ez->objects.data[i].time : 0; +} + +OPPAIAPI float ezpp_strain_at(ezpp_t ez, int i, int difficulty_type) +{ + return ez->objects.len ? ez->objects.data[i].strains[difficulty_type] : 0; +} + +OPPAIAPI int ezpp_ntiming_points(ezpp_t ez) +{ + return ez->timing_points.len; +} + +OPPAIAPI float ezpp_timing_time(ezpp_t ez, int i) +{ + return ez->timing_points.len ? ez->timing_points.data[i].time : 0; +} + +OPPAIAPI float ezpp_timing_ms_per_beat(ezpp_t ez, int i) +{ + return ez->timing_points.len ? ez->timing_points.data[i].ms_per_beat : 0; +} + +OPPAIAPI int ezpp_timing_change(ezpp_t ez, int i) +{ + return ez->timing_points.len ? ez->timing_points.data[i].change : 0; +} + +#define setter(t, x) \ + OPPAIAPI void ezpp_set_##x(ezpp_t ez, t x) \ + { \ + ez->x = x; \ + if (ez->flags & AUTOCALC_BIT) \ + { \ + calc(ez); \ + } \ + } +setter(float, aim_stars) + setter(float, speed_stars) + setter(float, base_ar) + setter(float, base_od) + setter(float, base_hp) + setter(int, mode) + setter(int, combo) + setter(int, score_version) + setter(float, accuracy_percent) +#undef setter + + OPPAIAPI + void ezpp_set_autocalc(ezpp_t ez, int autocalc) +{ + if (autocalc) + { + ez->flags |= AUTOCALC_BIT; + } + else + { + ez->flags &= ~AUTOCALC_BIT; + } +} + +OPPAIAPI +void ezpp_set_mods(ezpp_t ez, int mods) +{ + if ((mods ^ ez->mods) & (MODS_MAP_CHANGING | MODS_SPEED_CHANGING)) + { + /* force map reparse */ + ez->aim_stars = ez->speed_stars = ez->stars = 0; + ez->max_combo = 0; + } + ez->mods = mods; + if (ez->flags & AUTOCALC_BIT) + { + calc(ez); + } +} + +#define clobber_setter(t, x) \ + OPPAIAPI \ + void ezpp_set_##x(ezpp_t ez, t x) \ + { \ + ez->aim_stars = ez->speed_stars = ez->stars = 0; \ + ez->max_combo = 0; \ + ez->x = x; \ + if (ez->flags & AUTOCALC_BIT) \ + { \ + calc(ez); \ + } \ + } +clobber_setter(float, base_cs) + clobber_setter(int, mode_override) +#undef clobber_setter + +#define acc_clobber_setter(t, x) \ + OPPAIAPI \ + void ezpp_set_##x(ezpp_t ez, t x) \ + { \ + ez->accuracy_percent = -1; \ + ez->aim_stars = ez->speed_stars = ez->stars = 0; \ + ez->max_combo = 0; \ + ez->x = x; \ + if (ez->flags & AUTOCALC_BIT) \ + { \ + calc(ez); \ + } \ + } + + acc_clobber_setter(int, nmiss) + acc_clobber_setter(int, end) + acc_clobber_setter(float, end_time) + + OPPAIAPI + void ezpp_set_accuracy(ezpp_t ez, int n100, int n50) +{ + ez->accuracy_percent = -1; + ez->n100 = n100; + ez->n50 = n50; + if (ez->flags & AUTOCALC_BIT) + { + calc(ez); + } +} + +#endif /* OPPAI_IMPLEMENTATION */ \ No newline at end of file diff --git a/ManiaKV/main.cpp b/ManiaKV/main.cpp index 7e639b8..d42aab9 100644 --- a/ManiaKV/main.cpp +++ b/ManiaKV/main.cpp @@ -65,18 +65,18 @@ int main() { { // Update - if (!screenSwitchShortcutIsHeld && MKVIsPressed({ KEY_LEFT_CONTROL, KEY_LEFT_SHIFT, KEY_PERIOD })) { + /*if (!screenSwitchShortcutIsHeld && MKVIsPressed({KEY_LEFT_CONTROL, KEY_LEFT_SHIFT, KEY_PERIOD})) { if (currentScreen == KEYBOARD) { currentScreen = SETTINGS; } else currentScreen = KEYBOARD; screenSwitchShortcutIsHeld = true; - } else if (screenSwitchShortcutIsHeld && !MKVIsPressed({ KEY_LEFT_CONTROL, KEY_LEFT_SHIFT, KEY_PERIOD })) screenSwitchShortcutIsHeld = false; + } else if (screenSwitchShortcutIsHeld && !MKVIsPressed({ KEY_LEFT_CONTROL, KEY_LEFT_SHIFT, KEY_PERIOD })) screenSwitchShortcutIsHeld = false;*/ switch (currentScreen) { case KEYBOARD: { - if (!dataOverlayShortcutIsHeld && MKVIsPressed({ KEY_LEFT_CONTROL, KEY_LEFT_SHIFT, KEY_COMMA })) { + if (!dataOverlayShortcutIsHeld && MKVIsPressed({KEY_LEFT_CONTROL, KEY_LEFT_SHIFT, KEY_COMMA})) { if (showDataOverlay) { showDataOverlay = false; } else showDataOverlay = true;