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;