text: /*
   * 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);
  OPPAIAPI float ezpp_strain_at(ezpp_t ez, int i, int difficulty_type);
  
  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 <stdio.h>
  #include <stdarg.h>
  #include <stdlib.h>
  #include <string.h>
  #include <math.h>
  
  #define OPPAI_VERSION_MAJOR 3
  #define OPPAI_VERSION_MINOR 2
  #define OPPAI_VERSION_PATCH 3
  #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;
  }
  
  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.3f;
    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.2f;
    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, &section);
      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.85f +
      0.32f * al_min(0.93f, nobjects_over_2k) +
      (ez->nobjects > 2000 ? (float)log10(nobjects_over_2k) * 0.45f : 0.0f)
    );
    float miss_penality = (float)pow(0.87f, ez->nmiss);
    float combo_break = (
      (float)pow(ez->combo, 0.727f) / (float)pow(ez->max_combo, 0.69f)
    );
    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 = 1.0f;
  
    /* high ar bonus */
    if (ez->ar > 10.33f) {
      ar_bonus += 0.23f * (ez->ar - 10.33f);
    }
  
    /* low ar bonus */
    else if (ez->ar < 8.0f) {
      ar_bonus += 0.012f * (8.0f - ez->ar);
    }
  
    /* aim pp ---------------------------------------------------------- */
    ez->aim_pp = base_pp(ez->aim_stars);
    ez->aim_pp *= length_bonus;
    ez->aim_pp *= miss_penality;
    ez->aim_pp *= combo_break;
    ez->aim_pp *= ar_bonus;
  
    /* hidden */
    hd_bonus = 0.84f;
    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 = 0.32f + 0.33f * al_min(1.0f, ez->nobjects / 200.0f);
      if (ez->nobjects > 200) {
        fl_bonus += 0.4f * al_min(1, (ez->nobjects - 200) / 300.0f);
      }
      if (ez->nobjects > 500) {
        fl_bonus += 0.6f * (ez->nobjects - 500) / 1200.0f;
      }
      ez->aim_pp *= fl_bonus;
    }
  
    /* acc bonus (bad aim can lead to bad acc) */
    acc_bonus = 0.32f + accuracy / 2.0f;
  
    /* od bonus (high od requires better aim timing to acc) */
    od_squared = (float)pow(ez->od, 2);
    od_bonus = 0.84f + 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;
    ez->speed_pp *= miss_penality;
    ez->speed_pp *= combo_break;
    if (ez->ar > 10.33f) {
      ez->speed_pp *= ar_bonus;
    }
    ez->speed_pp *= hd_bonus;
  
    /* scale the speed value with accuracy slightly */
    ez->speed_pp *= 0.02f + accuracy;
  
    /* it's important to also consider accuracy difficulty when doing that */
    ez->speed_pp *= 0.8f + (od_squared / 1600.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.06f;
    if (ez->mods & MODS_FL) ez->acc_pp *= 1.08f;
  
    /* total pp -------------------------------------------------------- */
    final_multiplier = 1.0f;
    /* if (ez->mods & MODS_NF) final_multiplier *= 0.90f; */
    if (ez->mods & MODS_SO) final_multiplier *= 1.0f;
  
    ez->pp = (float)(
      pow(
        pow(ez->aim_pp, 1.20f) +
        pow(ez->speed_pp, 0.7f) +
        pow(ez->acc_pp, 1.13f),
        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;
  }
  
  #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 */
  