2021-04-23 13:31:43 +02:00
|
|
|
/*
|
|
|
|
* 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 <stdio.h>
|
|
|
|
#include <stdarg.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include <math.h>
|
|
|
|
|
|
|
|
#define OPPAI_VERSION_MAJOR 3
|
|
|
|
#define OPPAI_VERSION_MINOR 3
|
|
|
|
#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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
);
|
2021-04-28 22:44:01 +02:00
|
|
|
float miss_penality = (float)pow(0.97f, ez->nmiss + (ez->n50 * 0.35f));
|
2021-04-23 13:31:43 +02:00
|
|
|
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;
|
2021-04-28 22:44:01 +02:00
|
|
|
|
|
|
|
float aim_crosscheck;
|
2021-04-23 13:31:43 +02:00
|
|
|
|
|
|
|
/* 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 */
|
2021-04-28 22:44:01 +02:00
|
|
|
if (ez->ar > 10.7f) {
|
|
|
|
ar_bonus += 0.45f * (ez->ar - 10.7f);
|
2021-04-23 13:31:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/* low ar bonus */
|
2021-04-28 22:44:01 +02:00
|
|
|
else if (ez->ar < 8.5f) {
|
|
|
|
ar_bonus += 0.025f * (8.5f - ez->ar);
|
2021-04-23 13:31:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/* 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 = 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;
|
2021-04-28 22:44:01 +02:00
|
|
|
|
|
|
|
aim_crosscheck = al_max(0.75f, 0.6f + (float)pow(real_acc, 3.0f) / 2.0f);
|
2021-04-23 13:31:43 +02:00
|
|
|
|
|
|
|
ez->aim_pp *= acc_bonus;
|
|
|
|
ez->aim_pp *= od_bonus;
|
2021-04-28 22:44:01 +02:00
|
|
|
ez->aim_pp *= aim_crosscheck;
|
2021-04-23 13:31:43 +02:00
|
|
|
|
|
|
|
/* speed pp -------------------------------------------------------- */
|
|
|
|
ez->speed_pp = base_pp(ez->speed_stars);
|
|
|
|
|
|
|
|
/* acc pp ---------------------------------------------------------- */
|
|
|
|
/* arbitrary values tom crafted out of trial and error */
|
2021-04-28 22:44:01 +02:00
|
|
|
ez->acc_pp = (float)pow(1.52163f, 5.0f + ez->od / 2.0f) *
|
|
|
|
(float)pow(real_acc, 18.0f) * 2.83f;
|
2021-04-23 13:31:43 +02:00
|
|
|
|
|
|
|
/* 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;
|
|
|
|
|
|
|
|
// REWORK: ACC CHANGE
|
2021-04-28 14:59:33 +02:00
|
|
|
ez->acc_pp = (float) pow(ez->acc_pp, 0.94f);
|
2021-04-23 13:31:43 +02:00
|
|
|
// REWORK END
|
|
|
|
|
|
|
|
/* total pp -------------------------------------------------------- */
|
|
|
|
final_multiplier = 1.12f;
|
|
|
|
if (ez->mods & MODS_NF) final_multiplier *= 0.90f;
|
|
|
|
if (ez->mods & MODS_SO) final_multiplier *= 0.95f;
|
|
|
|
|
|
|
|
ez->pp = (float)(
|
|
|
|
pow(
|
2021-04-28 22:44:01 +02:00
|
|
|
pow(ez->aim_pp, 1.158f) +
|
|
|
|
pow(ez->acc_pp, 1.186f),
|
|
|
|
0.99f / 1.1f
|
2021-04-23 13:31:43 +02:00
|
|
|
) * 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 */
|