initial tests

This commit is contained in:
HorizonCode 2021-06-25 20:26:13 +02:00
commit d40ebbbff5
48 changed files with 21542 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
tags
*.log
*.tar.xz
*.tar.gz
*.zip
/oppai
*.json
*.obj
*.exe
*.swp
*.so
*.dll
*.lib
*.exp
/test/test_suite
/test/oppai_test
/swig/**/*.c
/swig/*/*.i
/swig/python/oppai.egg-info
/swig/python/build
/swig/python/oppai.py
*.whl
*.pyc

29
.travis.yml Normal file
View File

@ -0,0 +1,29 @@
language: c
matrix:
include:
- os: linux
dist: precise
- os: linux
dist: trusty
- os: osx
install: true
cache:
directories:
- test/test_suite
script:
- ./build
- ./libbuild
- cd test
- ./download_suite
- ./build
- DYLD_PRINT_LIBRARIES=1 DYLD_PRINT_LIBRARIES_POST_LAUNCH=1
DYLD_PRINT_RPATHS=1 LD_DEBUG=libs ./oppai_test
- wc -c ./oppai_test
- LDFLAGS="-loppai" ./build -L..
- LD_LIBRARY_PATH=.. DYLD_LIBRARY_PATH=..
DYLD_PRINT_LIBRARIES=1 DYLD_PRINT_LIBRARIES_POST_LAUNCH=1
DYLD_PRINT_RPATHS=1 LD_DEBUG=libs ./oppai_test
- wc -c ./oppai_test

6
Dockerfile Normal file
View File

@ -0,0 +1,6 @@
ARG PREFIX=
FROM ${PREFIX}ubuntu:bionic
RUN apt-get update && apt-get install -y \
gcc musl musl-tools musl-dev git-core file
WORKDIR /tmp
CMD setarch $arch ./release

367
README.md Normal file
View File

@ -0,0 +1,367 @@
[![Build Status](https://travis-ci.org/Francesco149/oppai-ng.svg?branch=master)](https://travis-ci.org/Francesco149/oppai-ng)
difficulty and pp calculator for osu!
this is a pure C89 rewrite of
[oppai](https://github.com/Francesco149/oppai) with much lower
memory usage, smaller and easier to read codebase
executable size and better performance.
experimental taiko support is now available and appears to give
correct values for actual taiko maps. converted maps are still
unreliable due to incorrect slider conversion and might be
completely off (use ```-m1``` or ```-taiko``` to convert a std map
to taiko).
- [installing (linux)](#installing-linux)
- [installing (windows)](#installing-windows)
- [installing (osx)](#installing-osx)
- [usage](#usage)
- [implementations for other programming languages](#implementations-for-other-programming-languages)
- [bindings for other programming languages](#bindings-for-other-programming-languages)
- [oppai-ng vs old oppai](#oppai-ng-vs-old-oppai)
- [compile from source (windows)](#compile-from-source-windows)
- [using oppai as a library or making bindings](#using-oppai-as-a-library-or-making-bindings)
- [other build parameters](#other-build-parameters)
# installing (linux)
```sh
wget https://github.com/Francesco149/oppai-ng/archive/HEAD.tar.gz
tar xf HEAD.tar.gz
cd oppai-*
./build
sudo install -Dm 755 oppai /usr/bin/oppai
oppai
```
you can also grab pre-compiled standalone binaries (statically
linked against musl libc) from
[here](https://github.com/Francesco149/oppai-ng/releases) if you
are somehow too scared to run those 5 commands.
# installing (windows)
download and unzip binaries from
[here](https://github.com/Francesco149/oppai-ng/releases) and
optionally add oppai's folder to your ```PATH``` environment
variable for easy access. you can find a guide
[here](https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/)
if you don't know how.
# installing (osx)
## via homebrew
```sh
brew install --HEAD pmrowla/homebrew-tap/oppai-ng
```
Note that installing with ```--HEAD``` is recommended but not required.
Installing from homebrew will place the ```oppai``` executable in your homebrew path.
## manually
Follow the same steps as for linux but substitute ```curl -O``` for ```wget``` since wget is not distributed by default in osx.
The same caveat applies if you want to run the test suite - you will need to edit the ```download_suite``` script to use curl.
# usage
you can run oppai with no arguments to check the documentation.
here's some example usages:
```sh
oppai path/to/map.osu +HDHR 98% 500x 1xmiss
oppai path/to/map.osu 3x100
oppai path/to/map.osu 3x100 OD10
oppai path/to/map.osu -ojson
```
you can also pipe maps from standard input by setting the filename
to ```-```.
for example on linux you can do:
```sh
curl https://osu.ppy.sh/osu/774965 | oppai - +HDDT
curl https://osu.ppy.sh/osu/774965 | oppai - +HDDT 1200x 1m
```
while on windows it's a bit more verbose (powershell):
```powershell
(New-Object System.Net.WebClient).DownloadString("https://osu.ppy.sh/osu/37658") | ./oppai -
(New-Object System.Net.WebClient).DownloadString("https://osu.ppy.sh/osu/37658") | ./oppai - +HDHR
(New-Object System.Net.WebClient).DownloadString("https://osu.ppy.sh/osu/37658") | ./oppai - +HDHR 99% 600x 1m
```
I got the .osu file url from "Grab latest .osu file" on the
beatmap's page.
# implementations for other programming languages
oppai has been implemented for many other programming languages.
If you feel like making your own implementation and want it listed
here, open an issue or pull request. the requirement is that it
should pass the same test suite that oppai-ng passes.
* [ojsama (javascript)](https://github.com/Francesco149/ojsama)
* [koohii (java)](https://github.com/Francesco149/koohii) . this
is currently being used in tillerino.
* [pyttanko (python)](https://github.com/Francesco149/pyttanko)
* [oppai5 (golang)](https://github.com/flesnuk/oppai5) (by flesnuk)
* [OppaiSharp (C#)](https://github.com/HoLLy-HaCKeR/OppaiSharp)
(by HoLLy)
* [osu-perf (rust)](https://gitlab.com/JackRedstonia/osu-perf/) (by JackRedstonia)
# bindings for other programming languages
thanks to swig it's trivial to generate native bindings for other
programming languages. bindings are an interface to the C code, meaning
that you get basically the same performance as C by sacrificing some
portability
* [python](https://github.com/Francesco149/oppai-ng/swig/python)
# oppai-ng vs old oppai
executable size is around 7 times smaller:
```sh
$ cd ~/src/oppai
$ ./build.sh -static
$ wc -c oppai
574648 oppai
$ cd ~/src/oppai-ng
$ ./build -static
$ wc -c oppai
75512 oppai
```
oppai-ng has proper error output in whatever format you select,
while legacy oppai either gives empty output or just dies with
a plaintext error.
oppai-ng has well-defined errno style error codes that you can
check for when using it as a library or reading its output.
the same test suite runs about 45% faster on oppai-ng compared
to old oppai, also the peak resident memory size is 4 to 6 times
smaller according to various ```time -v``` runs.
```sh
$ cd ~/src/oppai
$ ./build_test.sh
$ time -v ./oppai_test
...
Command being timed: "./oppai_test"
User time (seconds): 13.89
System time (seconds): 0.10
Percent of CPU this job got: 99%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0m 13.99s
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 45184
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 2143
Voluntary context switches: 1
Involuntary context switches: 41
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
$ cd ~/src/oppai-ng/test/
$ ./build
$ time -v ./oppai_test
...
Command being timed: "./oppai_test"
User time (seconds): 9.09
System time (seconds): 0.06
Percent of CPU this job got: 99%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0m 9.15s
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 11840
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 304
Voluntary context switches: 1
Involuntary context switches: 39
Swaps: 0
File system inputs: 0
File system outputs: 0
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0
```
note that when the test suite is compiled without libcurl, the
resident memory usage drops by a flat 4mb, so almost half of that
is curl.
you can expect oppai memory usage to be under 4 mb most of the time
with the raw parsed beatmap data not taking more than ~800k even
for a 15 minute marathon.
the codebase has ~3-4x less lines than legacy oppai, making it easy
to read and use as a single header library. not only it is smaller,
but it now also implements both taiko and osu, so more features
than legacy oppai.
the osu! pp and diff calc alone would be around ~3k LOC including
the cli, which would be 5x less lines than legacy oppai for the
same functionality.
```sh
$ cd ~/src/oppai
$ sloc *.cc
---------- Result ------------
Physical : 15310
Source : 14406
Comment : 301
Single-line comment : 289
Block comment : 12
Mixed : 23
Empty : 626
To Do : 11
Number of files read : 10
------------------------------
$ cd ~/src/oppai-ng
$ sloc *.c
---------- Result ------------
Physical : 4123
Source : 2906
Comment : 492
Single-line comment : 1
Block comment : 491
Mixed : 64
Empty : 811
To Do : 9
Number of files read : 2
------------------------------
```
not to mention it's C89, which will be compatible with many more
platforms and old compilers than c++98
```oppai.c``` alone is only ~2200 LOC (~1500 without comments), and
you can compile piece of it out when you don't need them.
of course, it's not as heavily tested as legacy oppai (which runs
24/7 on Tillerino's back-end), however the test suite is a very
good test that runs through ~12000 unique scores and I'm confident
this rewrite is already very stable.
# compile from source (windows)
oppai should compile even on old versions of msvc dating back to
2005, although it was only tested on msvc 2010 and higher.
have at least [microsoft c++ build tools](http://landinghub.visualstudio.com/visual-cpp-build-tools)
installed. visual studio with c/c++ support also works.
open a visual studio prompt:
```bat
cd path\to\oppai\source
build.bat
oppai
```
you can also probably set up mingw and cygwin and follow the linux
instructions instead, I'm not sure. I don't use windows.
# using oppai as a library or making bindings
the new codebase is much easier to isolate and include in your
projects.
just copy oppai.c into your project, it acts as a single-header
library.
```c
#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;
}
```
```sh
gcc test.c
cat /path/to/file.osu | ./a.out
```
read oppai.c, there's documentation for each function at the top.
see examples directory for detailed examples. you can also read
main.c to see how the CLI uses it.
if you don't feel comfortable writing bindings or using oppai
from c code, you can use the -o parameter to output in json or
other parsable formats. ```examples/binary.c``` shows how to parse
the binary output.
# shared library
you can also build oppai as a shared library with
```sh
./libbuild
```
this will generate a liboppai.so on linux/mac which you can copy to
```/usr/local/lib``` or anywhere in your library search paths
you can then use it by simply not defining ```OPPAI_IMPLEMENTATION``` .
this will exclude all the oppai code and just leave the header part
```c
#include "oppai.c"
int main() {
/* ... */
}
```
then you can compile and run with
```
gcc test.c -lm -loppai
cat /path/to/file.osu | ./a.out
```
for windows you can use ```libbuild.bat``` to build (for details see the
info on compiling on windows) which will generate a oppai.dll and .lib pair
and then compile your program with msvc like so
```
cl test.c oppai.lib
```
then you can simply place the dll in the same folder as your executable
and run
# build parameters
when you build the oppai cli, you can pass any of these parameters
to the build script to disable features:
* ```-DOPPAI_UTF8GRAPH``` use utf-8 characters for the strains graph

24
UNLICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

67
appveyor.yml Normal file
View File

@ -0,0 +1,67 @@
version: b{build}
image: Visual Studio 2017
cache: test/test_suite -> test/suite_url
build_script:
- ps: >-
function VcVars {
param ([string]$VcPath, [string]$BatName)
Push-Location $VcPath
cmd /c ($BatName + "&set") |
ForEach-Object {
if ($_ -match "=") {
$v = $_.split("="); set-item -force -path "ENV:\$($v[0])" -value "$($v[1])"
}
}
Pop-Location
}
$VcPath = "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build"
VcVars $VcPath vcvars32.bat
.\release.ps1; if (-not $?) { exit $LastExitCode }
VcVars $VcPath vcvars64.bat
.\release.ps1; if (-not $?) { exit $LastExitCode }
cd .\test
.\download_suite.ps1
.\build.bat; if (-not $?) { exit $LastExitCode }
.\oppai_test.exe; if (-not $?) { exit $LastExitCode }
Write-Host "bin size: " + (Get-Item '.\oppai_test.exe').length
dumpbin /dependents .\oppai_test.exe
.\build.bat ..\oppai.lib; if (-not $?) { exit $LastExitCode }
cp ..\oppai.dll .
.\oppai_test.exe; if (-not $?) { exit $LastExitCode }
Write-Host "bin size: " + (Get-Item '.\oppai_test.exe').length
dumpbin /dependents .\oppai_test.exe
test: off
artifacts:
- path: oppai-*-windows-*.zip
name: windows-binaries
deploy:
- provider: GitHub
tag: $(appveyor_repo_tag_name)
release: oppai $(appveyor_repo_tag_name)-$(appveyor_build_version)
description: linux binaries are manually uploaded shortly after the windows release and statically linked against musl libc\n\nwindows binaries should not require the c runtime\n\nx64 and x86_64 mean 64-bit i586 and x86 mean 32-bit\n\nthe binary packages include the source code inside the src directory
auth_token:
secure: k73tV2NZTFp4thujp/KiohNwRwIpWC12gU/qsnfCqlctcC+rqWiDWet3sSAz34gT
artifact: windows-binaries
force_update: true
on:
APPVEYOR_REPO_TAG: true

6
build Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
dir="$(dirname "$0")"
. "$dir"/cflags
$cc $cflags "$@" -DOPPAI_IMPLEMENTATION main.c oppai.c $ldflags -o oppai

12
build.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
del oppai.exe >nul 2>&1
del main.obj >nul 2>&1
cl -D_CRT_SECURE_NO_WARNINGS=1 ^
-DNOMINMAX=1 ^
-O2 -nologo -MT -Gm- -GR- -EHsc -W4 ^
-DOPPAI_IMPLEMENTATION ^
-DOPPAI_STATIC_HEADER ^
main.c oppai.c ^
-Feoppai.exe ^
|| EXIT /B 1

45
cflags Executable file
View File

@ -0,0 +1,45 @@
#!/bin/sh
cflags="-std=c89 -pedantic"
cflags="$cflags -Os"
cflags="$cflags -fno-strict-aliasing"
cflags="$cflags -Wall"
cflags="$cflags -ffunction-sections -fdata-sections"
if [ -z $DBGINFO ]; then
cflags="$cflags -g0 -fno-unwind-tables -s"
cflags="$cflags -fno-asynchronous-unwind-tables"
cflags="$cflags -fno-stack-protector"
else
cflags="$cflags -g -fsanitize=address -fsanitize=leak "
cflags="$cflags -fsanitize=signed-integer-overflow -fsanitize=undefined -static-libasan"
fi
if [ $(uname) = "Darwin" ]; then
cflags="$cflags -Wl,-dead_strip"
else
cflags="$cflags -Wl,--gc-sections,--build-id=none"
fi
ldflags="-lm"
cflags="$cflags $CFLAGS"
ldflags="$ldflags $LDFLAGS"
cc="$CC"
if [ $(uname) = "Darwin" ]; then
cc=${cc:-clang}
else
cc=${cc:-gcc}
fi
uname -a > flags.log
echo $cc >> flags.log
echo $cflags >> flags.log
echo $ldflags >> flags.log
$cc --version >> flags.log
$cc -dumpmachine >> flags.log
export cflags="$cflags"
export ldflags="$ldflags"
export cc="$cc"

26
examples/FFI.cs Normal file
View File

@ -0,0 +1,26 @@
// csc FFI.cs
// FFI.exe /path/to/file.osu
// make sure oppai.dll is in the same directory as FFI.exe
// see oppai.c for a full list of functions
using System;
using System.Runtime.InteropServices;
public class Program
{
[DllImport(@"oppai.dll")]
public static extern IntPtr ezpp_new();
[DllImport(@"oppai.dll")]
public static extern IntPtr ezpp(IntPtr ez, char[] map);
[DllImport(@"oppai.dll")]
public static extern float ezpp_pp(IntPtr ez);
static void Main(string[] args)
{
IntPtr ez = ezpp_new();
ezpp(ez, args[0].ToCharArray());
Console.WriteLine($"{ezpp_pp(ez)} pp");
}
}

133
examples/binary.c Normal file
View File

@ -0,0 +1,133 @@
/*
* example of parsing oppai's binary output
*
* gcc binary.c
* oppai /path/to/file.osu -obinary | ./a.out
*/
#include <stdio.h>
#include <string.h>
/*
* these are only necessary to ensure endian-ness, if you don't
* care about that you can read values like v = *(int*)p
*/
int read2(char** c) {
unsigned char* p = (unsigned char*)*c;
*c += 2;
return p[0] | (p[1] << 8);
}
int read4(char** c) {
unsigned char* p = (unsigned char*)*c;
*c += 4;
return p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24);
}
float read_flt(char** p) {
int v = read4(p);
float* pf = (float*)&v;
return *pf;
}
char* read_str(char** p, int* len) {
char* res;
*len = read2(p);
res = *p;
*p += *len + 1;
return res;
}
#define MODS_NF (1<<0)
#define MODS_EZ (1<<1)
#define MODS_HD (1<<3)
#define MODS_HR (1<<4)
#define MODS_DT (1<<6)
#define MODS_HT (1<<8)
#define MODS_NC (1<<9)
#define MODS_FL (1<<10)
#define MODS_SO (1<<12)
int main() {
char buf[8192];
char* p = buf;
int len;
int result;
int mods;
memset(buf, 0, sizeof(buf));
/* read stdin in binary mode */
if (!freopen(0, "rb", stdin)) {
perror("freopen");
return 1;
}
if (!fread(buf, 1, sizeof(buf), stdin)) {
perror("fread");
return 1;
}
if (strncmp((char const*)p, "binoppai", 8)) {
puts("invalid input");
return 1;
}
p += 8;
printf("oppai %d.%d.%d\n", p[0], p[1], p[2]);
p += 3;
puts("");
/* error code */
result = read4(&p);
if (result < 0) {
printf("error %d\n", result);
return 1;
}
printf("artist: %s\n", read_str(&p, &len));
printf("artist_unicode: %s\n", read_str(&p, &len));
printf("title: %s\n", read_str(&p, &len));
printf("title_unicode: %s\n", read_str(&p, &len));
printf("version: %s\n", read_str(&p, &len));
printf("creator: %s\n", read_str(&p, &len));
mods = read4(&p);
puts("");
printf("mods: ");
if (mods & MODS_NF) printf("NF");
if (mods & MODS_EZ) printf("EZ");
if (mods & MODS_HD) printf("HD");
if (mods & MODS_HR) printf("HR");
if (mods & MODS_DT) printf("DT");
if (mods & MODS_HT) printf("HT");
if (mods & MODS_NC) printf("NC");
if (mods & MODS_FL) printf("FL");
if (mods & MODS_SO) printf("SO");
puts("");
printf("OD%g ", read_flt(&p));
printf("AR%g ", read_flt(&p));
printf("CS%g ", read_flt(&p));
printf("HP%g\n", read_flt(&p));
printf("%d/%dx\n", read4(&p), read4(&p));
printf("%d circles ", read2(&p));
printf("%d sliders ", read2(&p));
printf("%d spinners\n", read2(&p));
printf("scorev%d\n", read4(&p));
puts("");
printf("%g stars ", read_flt(&p));
printf("(%g speed, ", read_flt(&p));
printf("%g aim)\n", read_flt(&p));
read2(&p); /* legacy */
read2(&p); /* legacy */
puts("");
printf("%g aim pp\n", read_flt(&p));
printf("%g speed pp\n", read_flt(&p));
printf("%g acc pp\n", read_flt(&p));
puts("");
printf("%g pp\n", read_flt(&p));
return 0;
}

16
examples/min.c Normal file
View File

@ -0,0 +1,16 @@
/*
* gcc min.c -lm
* cat /path/to/file.osu | ./a.out
*/
#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;
}

61
examples/reuse.c Normal file
View File

@ -0,0 +1,61 @@
/*
* gcc reuse.c -lm
* ./a.out /path/to/file.osu
*/
#define OPPAI_IMPLEMENTATION
#include "../oppai.c"
/*
* for better performance, the same instance can be reused
* settings are remembered and map is only reparsed if mods or cs change
*/
int main(int argc, char* argv[]) {
ezpp_t ez = ezpp_new();
ezpp_set_autocalc(ez, 1); /* autorecalc pp when changing any parameter */
ezpp(ez, argv[1]);
puts("---");
puts("nomod fc");
printf("%gpp\n", ezpp_pp(ez));
puts("---");
puts("nomod 95% fc");
ezpp_set_accuracy_percent(ez, 95);
printf("%gpp\n", ezpp_pp(ez));
puts("---");
puts("nomod 1x100 fc");
ezpp_set_accuracy(ez, 1, 0);
printf("%gpp\n", ezpp_pp(ez));
puts("---");
puts("HD 1x100 1miss 300x");
ezpp_set_mods(ez, MODS_HD);
ezpp_set_nmiss(ez, 1);
ezpp_set_combo(ez, 300);
printf("%gpp\n", ezpp_pp(ez));
puts("---");
puts("HDDT 1x100 1xmiss 300x");
ezpp_set_mods(ez, MODS_HD | MODS_DT);
printf("%gpp\n", ezpp_pp(ez));
puts("---");
puts("HDDT 1x100 1xmiss 300x ends at object 300");
ezpp_set_end(ez, 300);
printf("%gpp\n", ezpp_pp(ez));
puts("---");
puts("HDDT fc");
ezpp_set_end(ez, 0);
ezpp_set_combo(ez, -1);
ezpp_set_accuracy(ez, 0, 0);
ezpp_set_nmiss(ez, 0);
printf("%gpp\n", ezpp_pp(ez));
puts("---");
ezpp_free(ez);
return 0;
}

42
examples/reuse_mem.c Normal file
View File

@ -0,0 +1,42 @@
#define OPPAI_IMPLEMENTATION
#include "oppai.c"
char buf[1000000];
int mods[] = { 0, MODS_HR, MODS_HD | MODS_HR, MODS_DT, MODS_HD | MODS_DT };
#define N_MODS (sizeof(mods) / sizeof(mods[0]))
void print_mods(int mods) {
putchar('+');
if (!mods) puts("nomod");
else {
if (mods & MODS_HD) printf("hd");
if (mods & MODS_HR) printf("hr");
if (mods & MODS_DT) printf("dt");
puts("");
}
}
int main(int argc, char* argv[]) {
int i, j, n, acc;
ezpp_t ez = ezpp_new();
ezpp_set_autocalc(ez, 1);
for (i = 1; i < argc; ++i) {
FILE* f = fopen(argv[i], "r");
n = fread(buf, 1, sizeof(buf), f);
fclose(f);
ezpp_data(ez, buf, n);
printf("%s - %s [%s]\n", ezpp_artist(ez), ezpp_title(ez),
ezpp_version(ez));
for (j = 0; j < N_MODS; ++j) {
print_mods(mods[j]);
ezpp_set_mods(ez, mods[j]);
for (acc = 95; acc <= 100; ++acc) {
ezpp_set_accuracy_percent(ez, acc);
printf("%d%% -> %gpp\n", acc, ezpp_pp(ez));
}
}
puts("");
}
ezpp_free(ez);
return 0;
}

File diff suppressed because it is too large Load Diff

37
libbuild Executable file
View File

@ -0,0 +1,37 @@
#!/bin/sh
dir="$(dirname "$0")"
. "$dir"/cflags
tmp=$(mktemp -d)
hide_unnecessary_symbols() {
[ ! -d "$tmp" ] && echo "W: couldn't find tmp dir" && return
gcc_syms="$tmp/gcc_exports.sym"
clang_syms="$tmp/clang_exports.list"
code="$tmp/main.c"
echo "int main() { return 0; }" >> "$code"
exports='ezpp ezpp_ errstr oppai_'
( printf '{global:'
for e in $exports; do
printf '%s;' "$e"
done
printf 'local:*;};' ) | sed 's/_;/_*;/g' >"$gcc_syms"
echo "$exports" | tr ' ' '\n' | sed s/_$/_*/g > "$clang_syms"
for flags in "-Wl,--version-script=$gcc_syms" \
"-Wl,-exported_symbols_list,$clang_syms"
do
if "$cc" $flags "$code" -o /dev/null >/dev/null 2>&1; then
ldflags="$ldflags $flags"
return
fi
done
echo "W: can't figure out how to hide unnecessary symbols"
}
hide_unnecessary_symbols
$cc -shared $cflags -DOPPAI_EXPORT \
"$@" oppai.c $ldflags -fpic -o liboppai.so
[ -d "$tmp" ] && rm -rf "$tmp"

14
libbuild.bat Normal file
View File

@ -0,0 +1,14 @@
@echo off
del oppai.dll >nul 2>&1
del oppai.obj >nul 2>&1
del oppai.exp >nul 2>&1
echo compiling
cl ^
/c /O2 /nologo /Gm- /GR- /EHsc /W4 /Gz ^
/D_CRT_SECURE_NO_WARNINGS=1 /DNOMINMAX=1 ^
/DOPPAI_EXPORT /D_WINDLL /D_USRDLL ^
oppai.c ^
|| EXIT /B 1
echo making dll
link /OUT:oppai.dll /IMPLIB:oppai.lib /NOLOGO /DLL oppai.obj

966
main.c Normal file
View File

@ -0,0 +1,966 @@
/*
* this is free and unencumbered software released into the
* public domain.
*
* refer to the attached UNLICENSE or http://unlicense.org/
* ----------------------------------------------------------------
* command line interface for oppai
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <math.h>
#include <ctype.h>
#undef OPPAI_EXPORT
#undef OPPAI_IMPLEMENTATION
#include "oppai.c"
char* me = "oppai";
#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))
#define twodec(x) (al_round((x) * 100.0f) / 100.0f)
#define array_len(x) (sizeof(x) / sizeof((x)[0]))
static
float get_inf() {
static unsigned raw = 0x7F800000;
float* p = (float*)&raw;
return *p;
}
static
int is_nan(float b) {
int* p = (int*)&b;
return (
(*p > 0x7F800000 && *p < 0x80000000) ||
(*p > 0x7FBFFFFF && *p <= 0xFFFFFFFF)
);
}
static
int info(char* fmt, ...) {
int res;
va_list va;
va_start(va, fmt);
res = vfprintf(stderr, fmt, va);
va_end(va);
return res;
}
void usage() {
/* logo by flesnuk https://github.com/Francesco149/oppai-ng/issues/10 */
info(
" /\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb"
"\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb/ /\xe2\x8e\xbb\xe2"
"\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb"
"\xe2\x8e\xbb/ /\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2"
"\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb/ /\xe2\x8e"
"\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2"
"\x8e\xbb\xe2\x8e\xbb/ /\xe2\x8e\xbb/ /\xe2\x8e\xbb"
"\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\\ /\xe2\x8e\xbb"
"\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e"
"\xbb\xe2\x8e\xbb/\n / /\xe2\x8e\xbb\xe2\x8e\xbb\xe2"
"\x8e\xbb/ / / /\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb/ / "
);
info(
"/ /\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb/ / \xe2\x8e\xbb"
"\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb/ / / / "
"___ / /\xe2\x8e\xbb\xe2\x8e\xbb\\ \\ / /\xe2\x8e\xbb"
"\xe2\x8e\xbb\xe2\x8e\xbb/ /\n / / / / / / / / / /"
" / / /\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb\xe2\x8e\xbb"
"\xe2\x8e\xbb/ / / / /__/ / / / / / / / /\n / /___/ "
"/ / /___/ / / /___/ / / /___/ / / / / / / / / /__"
"_/ /\n /_______/ / ______/ / ______/ /_______/ /_/ "
"/_/ /_/ /_____ /\n / / / / "
" / / \n / / /"
" / /\xe2\x8e\xbb/___/"
" / \n /_/ /_/ "
" /_______/"
);
info("\n\n");
info("usage: %s /path/to/file.osu parameters\n\n", me);
info(
"set filename to '-' to read from standard input\n"
"all parameters are case insensitive\n"
"\n"
"-o[output_module]\n"
" output module. pass ? to list modules (oppai - -o?)\n"
" default: text\n"
" example: -ojson\n"
"\n"
"[accuracy]%%\n"
" accuracy percentage\n"
" default: 100%%\n"
" example: 95%%\n"
"\n"
"[n]x100\n"
" amount of 100s\n"
" default: 0\n"
" example: 2x100\n"
"\n"
);
info(
"[n]x50\n"
" amount of 50s\n"
" default: 0\n"
" example: 2x50\n"
"\n"
"[n]xm\n"
"[n]xmiss\n"
"[n]m\n"
" amount of misses\n"
" default: 0\n"
" example: 1m\n"
"\n"
"[combo]x\n"
" highest combo achieved\n"
" default: full combo (calculated from map data)\n"
" example: 500x\n"
"\n"
"scorev[n]\n"
" scoring system\n"
" default: 1\n"
" example: scorev2\n"
"\n"
);
info(
"ar[n]\n"
" base approach rate override\n"
" default: map's base approach rate\n"
" example: AR5\n"
"\n"
"od[n]\n"
" base overall difficulty override\n"
" default: map's base overall difficulty\n"
" example: OD10\n"
"\n"
"cs[n]\n"
" base circle size override\n"
" default: map's base circle size\n"
" example: CS6.5\n"
"\n"
);
info(
"-m[n]\n"
" gamemode id override for converted maps\n"
" default: uses the map's gamemode\n"
" example: -m1\n"
"\n"
"-taiko\n"
" forces gamemode to taiko for converted maps\n"
" default: disabled\n"
"\n"
"-touch\n"
" calculates pp for touchscreen / touch devices. can \n"
" also be specified as mod TD\n"
"\n"
"[n]speed\n"
" override speed stars. "
"useful for maps with incorrect star rating\n"
" default: uses computed speed stars\n"
" example: 3.5speed\n"
"\n"
);
info(
"[n]aim\n"
" override aim stars. "
"useful for maps with incorrect star rating\n"
" default: uses computed aim stars\n"
" example: 2.4aim\n"
"\n"
"-end[n]\n"
" cuts map to a certain number of objects\n"
);
}
#define output_sig(name) void name(int result, ezpp_t ez, char* mods_str)
typedef output_sig(fnoutput);
/* null output --------------------------------------------------------- */
/* stdout must be left alone, outputting to stderr is fine tho */
output_sig(output_null) { (void)result; (void)ez; (void)mods_str; }
/* text output --------------------------------------------------------- */
#define ASCIIPLT_W 51
void asciiplt(float (* getvalue)(void* data, int i), int n, void* data) {
static char* charset[] = {
#ifdef OPPAI_UTF8GRAPH
"\xe2\x96\x81",
"\xe2\x96\x82",
"\xe2\x96\x83",
"\xe2\x96\x84",
"\xe2\x96\x85",
"\xe2\x96\x86",
"\xe2\x96\x87",
"\xe2\x96\x88"
#else
" ", "_", ".", "-", "^"
#endif
};
static int charsetsize = array_len(charset);
float values[ASCIIPLT_W];
float minval = (float)get_inf();
float maxval = (float)-get_inf();
float range;
int i;
int chunksize;
int w = al_min(ASCIIPLT_W, n);
memset(values, 0, sizeof(values));
chunksize = (int)ceil((float)n / w);
for (i = 0; i < n; ++i) {
int chunki = i / chunksize;
values[chunki] = al_max(values[chunki], getvalue(data, i));
}
for (i = 0; i < n; ++i) {
int chunki = i / chunksize;
maxval = al_max(maxval, values[chunki]);
minval = al_min(minval, values[chunki]);
}
range = al_max(0.00001f, maxval - minval);
for (i = 0; i < w; ++i) {
int chari = (int)(((values[i] - minval) / range) * charsetsize);
chari = al_max(0, al_min(chari, charsetsize - 1));
printf("%s", charset[chari]);
}
puts("");
}
float getaim(void* data, int i) {
ezpp_t ez = data;
return ezpp_strain_at(ez, i, DIFF_AIM);
}
float getspeed(void* data, int i) {
ezpp_t ez = data;
return ezpp_strain_at(ez, i, DIFF_SPEED);
}
output_sig(output_text) {
float ar, od, cs, hp, stars, aim_stars, speed_stars, accuracy_percent;
float pp, aim_pp, speed_pp, acc_pp;
if (result < 0) {
puts(errstr(result));
return;
}
printf("%s - %s ", ezpp_artist(ez), ezpp_title(ez));
if (strcmp(ezpp_artist(ez), ezpp_artist_unicode(ez)) ||
strcmp(ezpp_title(ez), ezpp_title_unicode(ez)))
{
printf("(%s - %s) ", ezpp_artist_unicode(ez), ezpp_title_unicode(ez));
}
printf("[%s] mapped by %s ", ezpp_version(ez), ezpp_creator(ez));
puts("\n");
ar = twodec(ezpp_ar(ez));
od = twodec(ezpp_od(ez));
cs = twodec(ezpp_cs(ez));
hp = twodec(ezpp_hp(ez));
stars = twodec(ezpp_stars(ez));
aim_stars = twodec(ezpp_aim_stars(ez));
speed_stars = twodec(ezpp_speed_stars(ez));
accuracy_percent = twodec(ezpp_accuracy_percent(ez));
pp = twodec(ezpp_pp(ez));
aim_pp = twodec(ezpp_aim_pp(ez));
speed_pp = twodec(ezpp_speed_pp(ez));
acc_pp = twodec(ezpp_acc_pp(ez));
printf("AR%g OD%g ", ar, od);
if (ezpp_mode(ez) == MODE_STD) {
printf("CS%g ", cs);
}
printf("HP%g\n", hp);
printf("300 hitwindow: %g ms\n", ezpp_odms(ez));
printf("%d circles, %d sliders, %d spinners\n",
ezpp_ncircles(ez), ezpp_nsliders(ez), ezpp_nspinners(ez));
if (ezpp_mode(ez) == MODE_STD) {
printf("%g stars (%g aim, %g speed)\n", stars, aim_stars, speed_stars);
printf("\nspeed strain: ");
asciiplt(getspeed, ezpp_nobjects(ez), ez);
printf(" aim strain: ");
asciiplt(getaim, ezpp_nobjects(ez), ez);
} else {
printf("%g stars\n", ezpp_stars(ez));
}
printf("\n");
if (mods_str) {
printf("+%s ", mods_str);
}
printf("%d/%dx ", ezpp_combo(ez), ezpp_max_combo(ez));
printf("%g%%\n", accuracy_percent);
printf("%g pp (", pp);
if (ezpp_mode(ez) == MODE_STD) {
printf("%g aim, ", aim_pp);
}
printf("%g speed, ", speed_pp);
printf("%g acc)\n\n", acc_pp);
}
/* json output --------------------------------------------------------- */
void print_escaped_json_string_ex(char* str, int quotes) {
char* chars_to_escape = "\\\"";
char* p;
if (quotes) {
putchar('"');
}
for (; *str; ++str) {
/* escape all characters in chars_to_escape */
for (p = chars_to_escape; *p; ++p) {
if (*p == *str) {
putchar('\\');
}
}
putchar(*str);
}
if (quotes) {
putchar('"');
}
}
#define print_escaped_json_string(x) \
print_escaped_json_string_ex(x, 1)
/* https://www.doc.ic.ac.uk/%7Eeedwards/compsys/float/nan.html */
static int is_inf(float b) {
int* p = (int*)&b;
return *p == 0x7F800000 || *p == 0xFF800000;
}
/*
* json is mentally challenged and can't handle inf and nan so
* we're gonna be mathematically incorrect
*/
void fix_json_flt(float* v) {
if (is_inf(*v)) {
*v = -1;
} else if (is_nan(*v)) {
*v = 0;
}
}
output_sig(output_json) {
float pp, aim_pp, speed_pp, acc_pp, stars, aim_stars, speed_stars;
printf("{\"oppai_version\":\"%s\",", oppai_version_str());
if (result < 0) {
printf("\"code\":%d,", result);
printf("\"errstr\":");
print_escaped_json_string(errstr(result));
printf("}");
return;
}
pp = ezpp_pp(ez);
aim_pp = ezpp_aim_pp(ez);
speed_pp = ezpp_speed_pp(ez);
acc_pp = ezpp_acc_pp(ez);
stars = ezpp_stars(ez);
aim_stars = ezpp_aim_stars(ez);
speed_stars = ezpp_speed_stars(ez);
fix_json_flt(&pp);
fix_json_flt(&aim_pp);
fix_json_flt(&speed_pp);
fix_json_flt(&acc_pp);
fix_json_flt(&stars);
fix_json_flt(&aim_stars);
fix_json_flt(&speed_stars);
printf("\"code\":200,\"errstr\":\"no error\",");
printf("\"artist\":");
print_escaped_json_string(ezpp_artist(ez));
if (strcmp(ezpp_artist(ez), ezpp_artist_unicode(ez))) {
printf(",\"artist_unicode\":");
print_escaped_json_string(ezpp_artist_unicode(ez));
}
printf(",\"title\":");
print_escaped_json_string(ezpp_title(ez));
if (strcmp(ezpp_title(ez), ezpp_title_unicode(ez))) {
printf(",\"title_unicode\":");
print_escaped_json_string(ezpp_title_unicode(ez));
}
printf(",\"creator\":");
print_escaped_json_string(ezpp_creator(ez));
printf(",\"version\":");
print_escaped_json_string(ezpp_version(ez));
printf(",");
if (!mods_str) {
mods_str = "";
}
printf(
"\"mods_str\":\"%s\",\"mods\":%d,"
"\"od\":%g,\"ar\":%g,\"cs\":%g,\"hp\":%g,"
"\"combo\":%d,\"max_combo\":%d,"
"\"num_circles\":%d,\"num_sliders\":%d,"
"\"num_spinners\":%d,\"misses\":%d,"
"\"score_version\":%d,\"stars\":%.17g,"
"\"speed_stars\":%.17g,\"aim_stars\":%.17g,"
"\"aim_pp\":%.17g,\"speed_pp\":%.17g,\"acc_pp\":%.17g,"
"\"pp\":%.17g}",
mods_str, ezpp_mods(ez), ezpp_od(ez), ezpp_ar(ez),
ezpp_cs(ez), ezpp_hp(ez), ezpp_combo(ez),
ezpp_max_combo(ez), ezpp_ncircles(ez), ezpp_nsliders(ez),
ezpp_nspinners(ez), ezpp_nmiss(ez), ezpp_score_version(ez),
ezpp_stars(ez), ezpp_speed_stars(ez), ezpp_aim_stars(ez),
ezpp_aim_pp(ez), ezpp_speed_pp(ez), ezpp_acc_pp(ez), ezpp_pp(ez)
);
}
/* csv output ---------------------------------------------------------- */
void print_escaped_csv_string(char* str) {
char* chars_to_escape = "\\;";
char* p;
for (; *str; ++str) {
/* escape all characters in chars_to_escape */
for (p = chars_to_escape; *p; ++p) {
if (*p == *str) {
putchar('\\');
}
}
putchar(*str);
}
}
output_sig(output_csv) {
printf("oppai_version;%s\n", oppai_version_str());
if (result < 0) {
printf("code;%d\nerrstr;", result);
print_escaped_csv_string(errstr(result));
return;
}
printf("code;200\nerrstr;no error\n");
printf("artist;");
print_escaped_csv_string(ezpp_artist(ez));
puts("");
if (strcmp(ezpp_artist(ez), ezpp_artist_unicode(ez))) {
printf("artist_unicode;");
print_escaped_csv_string(ezpp_artist_unicode(ez));
puts("");
}
printf("title;");
print_escaped_csv_string(ezpp_title(ez));
puts("");
if (strcmp(ezpp_title(ez), ezpp_title_unicode(ez))) {
printf("title_unicode;");
print_escaped_csv_string(ezpp_title_unicode(ez));
puts("");
}
printf("version;");
print_escaped_csv_string(ezpp_version(ez));
puts("");
printf("creator;");
print_escaped_csv_string(ezpp_creator(ez));
puts("");
if (!mods_str) {
mods_str = "";
}
printf(
"mods_str;%s\nmods;%d\nod;%g\nar;%g\ncs;%g\nhp;%g\n"
"combo;%d\nmax_combo;%d\nnum_circles;%d\n"
"num_sliders;%d\nnum_spinners;%d\nmisses;%d\n"
"score_version;%d\nstars;%.17g\nspeed_stars;%.17g\n"
"aim_stars;%.17g\naim_pp;%.17g\nspeed_pp;%.17g\nacc_pp;%.17g\npp;%.17g",
mods_str, ezpp_mods(ez), ezpp_od(ez), ezpp_ar(ez),
ezpp_cs(ez), ezpp_hp(ez), ezpp_combo(ez),
ezpp_max_combo(ez), ezpp_ncircles(ez), ezpp_nsliders(ez),
ezpp_nspinners(ez), ezpp_nmiss(ez), ezpp_score_version(ez),
ezpp_stars(ez), ezpp_speed_stars(ez), ezpp_aim_stars(ez),
ezpp_aim_pp(ez), ezpp_speed_pp(ez), ezpp_acc_pp(ez), ezpp_pp(ez)
);
}
/* binary output ------------------------------------------------------- */
void write1(int v) {
char buf = (char)(v & 0xFF);
fwrite(&buf, 1, 1, stdout);
}
void write2(int v) {
char buf[2];
buf[0] = (char)(v & 0xFF);
buf[1] = (char)(v >> 8);
fwrite(buf, 1, 2, stdout);
}
void write4(int v) {
char buf[4];
buf[0] = (char)(v & 0xFF);
buf[1] = (char)((v >> 8) & 0xFF);
buf[2] = (char)((v >> 16) & 0xFF);
buf[3] = (char)((v >> 24) & 0xFF);
fwrite(buf, 1, 4, stdout);
}
void write_flt(float f) {
int* p = (int*)&f;
write4(*p);
}
void write_str(char* str) {
int len = al_min(0xFFFF, (int)strlen(str));
write2(len);
printf("%s", str);
write1(0);
}
output_sig(output_binary) {
int major, minor, patch;
(void)mods_str;
if (!freopen(0, "wb", stdout)) {
perror("freopen");
exit(1);
}
printf("binoppai");
oppai_version(&major, &minor, &patch);
write1(major);
write1(minor);
write1(patch);
write4(result);
if (result < 0) {
return;
}
/* TODO: use varargs to group calls of the same func */
write_str(ezpp_artist(ez));
write_str(ezpp_artist_unicode(ez));
write_str(ezpp_title(ez));
write_str(ezpp_title_unicode(ez));
write_str(ezpp_version(ez));
write_str(ezpp_creator(ez));
write4(ezpp_mods(ez));
write_flt(ezpp_od(ez));
write_flt(ezpp_ar(ez));
write_flt(ezpp_cs(ez));
write_flt(ezpp_hp(ez));
write4(ezpp_combo(ez));
write4(ezpp_max_combo(ez));
write2(ezpp_ncircles(ez));
write2(ezpp_nsliders(ez));
write2(ezpp_nspinners(ez));
write4(ezpp_score_version(ez));
write_flt(ezpp_stars(ez));
write_flt(ezpp_speed_stars(ez));
write_flt(ezpp_aim_stars(ez));
write2(0); /* legacy (nsingles) */
write2(0); /* legacy (nsigles_threshold) */
write_flt(ezpp_aim_pp(ez));
write_flt(ezpp_speed_pp(ez));
write_flt(ezpp_acc_pp(ez));
write_flt(ezpp_pp(ez));
}
/* gnuplot output ------------------------------------------------------ */
#define gnuplot_string(x) print_escaped_json_string_ex(x, 0)
void gnuplot_strains(ezpp_t ez, int type) {
int i;
for (i = 0; i < ezpp_nobjects(ez); ++i) {
printf("%.17g %.17g\n", ezpp_time_at(ez, i),
ezpp_strain_at(ez, i, type));
}
}
output_sig(output_gnuplot) {
if (result < 0 || ezpp_mode(ez) != MODE_STD) {
return;
}
puts("set encoding utf8;");
printf("set title \"");
gnuplot_string(ezpp_artist(ez));
printf(" - ");
gnuplot_string(ezpp_title(ez));
if (strcmp(ezpp_artist(ez), ezpp_artist_unicode(ez)) ||
strcmp(ezpp_title(ez), ezpp_title_unicode(ez)))
{
printf("(");
gnuplot_string(ezpp_artist_unicode(ez));
printf(" - ");
gnuplot_string(ezpp_title_unicode(ez));
printf(")");
}
printf(" [");
gnuplot_string(ezpp_version(ez));
printf("] mapped by ");
gnuplot_string(ezpp_creator(ez));
if (mods_str) printf(" +%s", mods_str);
puts("\";");
puts(
"set xlabel 'time (ms)';"
"set ylabel 'strain';"
"set multiplot layout 2,1 rowsfirst;"
"plot '-' with lines lc 1 title 'speed'"
);
gnuplot_strains(ez, DIFF_SPEED);
puts("e");
puts("unset title;");
puts("plot '-' with lines lc 2 title 'aim'");
gnuplot_strains(ez, DIFF_AIM);
}
/* ------------------------------------------------------------- */
#define CODE_DESC "the code and errstr fields " \
"should be checked for errors. a negative value for code " \
"indicates an error"
typedef struct output_module {
char* name;
fnoutput* func;
char* description[4];
/* null terminated array of strings because of c90 literal limits */
} output_module_t;
output_module_t modules[] = {
{ "null", output_null, { "no output", 0 } },
{ "text", output_text, { "plain text", 0 } },
{
"json", output_json,
{ "a single utf-8 json object.\n" CODE_DESC, 0 }
},
{
"csv", output_csv,
{ "fieldname;value\n"
"one value per line. ';' characters in strings will be "
"escaped to \"\\;\". utf-8.\n" CODE_DESC, 0 }
},
{
"binary",
output_binary,
{ "binary stream of values, encoded in little endian.\n"
"negative code values indicate an error, which matches "
"the error codes defined in oppai.c\n"
"for an example on how to read this in C, check out "
"examples/binary.c in oppai-ng's source\n"
"\n"
"floats and floats are represented using whatever "
"convention the host machine and compiler use. unless you "
"are on a really exotic machine it shouldn't matter\n"
"\n"
"strings (str) are encoded as a 2-byte integer indicating "
"the length in bytes, followed by the string bytes and ",
"a null (zero) terminating byte\n"
"\n"
"binoppai (8-byte magic), "
"int8 oppai_ver_major, int8 oppai_ver_minor, "
"int8 oppai_ver_patch, int error_code, "
"str artist, str artist_utf8, str title, str title_utf8, "
"str version, str creator, "
"int mods_bitmask, float od, float ar, float cs, "
"float hp, int combo, int max_combo, "
"int16 ncircles, int16 nsliders, int16 nspinner, "
"int score_version, float total_stars, ",
"float speed_stars, float aim_stars, int16 nsingles, "
"int16 nsingles_threshold, float aim_pp, "
"float speed_pp, float acc_pp, float pp",
0 }
},
{ "gnuplot", output_gnuplot, { "gnuplot .gp script", 0 } },
};
output_module_t* output_by_name(char* name) {
int i;
for (i = 0; i < array_len(modules); ++i) {
if (!strcmp(modules[i].name, name)) {
return &modules[i];
}
}
return 0;
}
int cmpsuffix(char* str, char* suffix) {
int sufflen = (int)al_min(strlen(str), strlen(suffix));
return strcmp(str + strlen(str) - sufflen, suffix);
}
char lowercase(char c) {
if (c >= 'A' && c <= 'Z') {
return c + ('a' - 'A');
}
return c;
}
char uppercase(char c) {
if (c >= 'a' && c <= 'z') {
return c - ('a' - 'A');
}
return c;
}
int strcmp_nc(char* a, char* b) {
for (;; ++a, ++b) {
char la = lowercase(*a);
char lb = lowercase(*b);
if (la > lb) {
return 1;
}
else if (la < lb) {
return -1;
}
if (!*a || *b) {
break;
}
}
return 0;
}
/* TODO: split main into smaller funcs for readability? */
int main(int argc, char* argv[]) {
int i;
int result;
ezpp_t ez = ezpp_new();
output_module_t* m;
char* output_name = "text";
char* mods_str = 0;
int mods = MODS_NOMOD;
float tmpf, speed_stars = 0, aim_stars = 0, accuracy_percent = 0;
int tmpi, n100 = 0, n50 = 0;
/* parse arguments ------------------------------------------------- */
me = argv[0];
if (argc < 2) {
usage();
return 1;
}
if (*argv[1] == '-' && strlen(argv[1]) > 1) {
char* a = argv[1] + 1;
if (!strcmp_nc(a, "version") || !strcmp_nc(a, "v")) {
puts(oppai_version_str());
return 0;
}
}
for (i = 2; i < argc; ++i) {
char* a = argv[i];
char* p;
int iswhite = 1;
for (p = a; *p; ++p) {
if (!isspace(*p)) {
iswhite = 0;
break;
}
}
if (iswhite) {
continue;
}
for (p = a; *p; ++p) {
*p = lowercase(*p);
}
if (*a == '-' && a[1] == 'o') {
output_name = a + 2;
if (!strcmp(output_name, "?")) {
int j;
int nmodules = sizeof(modules) / sizeof(modules[0]);
for (j = 0; j < nmodules; ++j) {
char** d = modules[j].description;
puts(modules[j].name);
for (; *d; ++d) {
printf("%s", *d);
}
puts("\n-");
}
return 0;
}
continue;
}
if (!cmpsuffix(a, "%") && sscanf(a, "%f", &accuracy_percent) == 1) {
continue;
}
if (!cmpsuffix(a, "x100") && sscanf(a, "%d", &n100) == 1) {
continue;
}
if (!cmpsuffix(a, "x50") && sscanf(a, "%d", &n50) == 1) {
continue;
}
if (!cmpsuffix(a, "speed") && sscanf(a, "%f", &speed_stars) == 1) {
continue;
}
if (!cmpsuffix(a, "aim") && sscanf(a, "%f", &aim_stars) == 1) {
continue;
}
if (!cmpsuffix(a, "xm") || !cmpsuffix(a, "xmiss") ||
!cmpsuffix(a, "m"))
{
if (sscanf(a, "%d", &tmpi) == 1) {
ezpp_set_nmiss(ez, tmpi);
continue;
}
}
if (!cmpsuffix(a, "x") && sscanf(a, "%d", &tmpi) == 1) {
ezpp_set_combo(ez, tmpi);
continue;
}
if (sscanf(a, "scorev%d", &tmpi)) {
ezpp_set_score_version(ez, tmpi);
continue;
}
if (sscanf(a, "ar%f", &tmpf)) {
ezpp_set_base_ar(ez, tmpf);
continue;
}
if (sscanf(a, "od%f", &tmpf)) {
ezpp_set_base_od(ez, tmpf);
continue;
}
if (sscanf(a, "cs%f", &tmpf)) {
ezpp_set_base_cs(ez, tmpf);
continue;
}
if (sscanf(a, "-m%d", &tmpi) == 1) {
ezpp_set_mode_override(ez, tmpi);
continue;
}
if (sscanf(a, "-end%d", &tmpi) == 1) {
ezpp_set_end(ez, tmpi);
continue;
}
if (!strcmp(a, "-taiko")) {
ezpp_set_mode_override(ez, MODE_TAIKO);
continue;
}
if (!strcmp(a, "-touch")) {
mods |= MODS_TOUCH_DEVICE;
continue;
}
/* this should be last because it uppercase's the string */
if (*a == '+') {
mods_str = a + 1;
for (p = mods_str; *p; ++p) {
*p = uppercase(*p);
}
#define m(mod) \
if (!strncmp(p, #mod, strlen(#mod))) { \
mods |= MODS_##mod; \
p += strlen(#mod); \
continue; \
}
for (p = mods_str; *p;) {
m(NF) m(EZ) m(TD) m(HD) m(HR) m(SD) m(DT) m(RX) m(HT) m(NC) m(FL)
m(AT) m(SO) m(AP) m(PF) m(NOMOD)
++p;
}
#undef m
continue;
}
info(">%s\n", a);
result = ERR_SYNTAX;
goto output;
}
if (accuracy_percent) {
ezpp_set_accuracy_percent(ez, accuracy_percent);
} else {
ezpp_set_accuracy(ez, n100, n50);
}
ezpp_set_mods(ez, mods);
ezpp_set_speed_stars(ez, speed_stars);
ezpp_set_aim_stars(ez, aim_stars);
result = ezpp(ez, argv[1]);
output:
m = output_by_name(output_name);
if (!m) {
info("output module '%s' does not exist. check 'oppai - -o?'\n",
output_name);
return 1;
}
m->func(result, ez, mods_str);
ezpp_free(ez); /* just so valgrind stops crying */
return result < 0;
}

2576
oppai.c Normal file

File diff suppressed because it is too large Load Diff

16
package Executable file
View File

@ -0,0 +1,16 @@
#!/bin/sh
dir=$(dirname $0)
dir=$(readlink -f $dir)
prevdir=$(pwd)
cd $dir
#git pull origin master || exit $?
for prefix in i386/ ""; do
container="${prefix}oppai-ng:ubuntu"
docker build -t "$container" \
--build-arg PREFIX="$prefix" . || exit $?
docker run --rm -v $dir:/tmp \
-e arch=$(echo ${prefix:-x86_64} | tr -d /) \
"$container" || exit $?
done
cd $prevdir

49
release Executable file
View File

@ -0,0 +1,49 @@
#!/bin/sh
dir=$(dirname $0)
olddir=$(pwd)
cd $dir
echo""
echo "compiling and stripping"
CC=musl-gcc ./build -static -no-pie || exit $?
CC=gcc ./libbuild || exit $?
echo""
echo "packaging"
folder="oppai-$(./oppai -version)-"
folder="${folder}$(gcc -dumpmachine)"
mkdir -p "$folder" || exit $?
mv ./oppai $folder/oppai || exit $?
mv ./liboppai.so $folder/liboppai.so || exit $?
git archive HEAD --prefix=src/ -o "$folder"/src.tar ||
exit $?
cd "$folder" || exit $?
tar xf src.tar || exit $?
cd .. || exit $?
rm "$folder".tar.xz
tar -cvJf "$folder".tar.xz \
"$folder"/oppai \
"$folder"/liboppai.so \
"$folder"/src \
|| exit $?
echo ""
file "$folder".tar.xz
tar tf "$folder".tar.xz
echo ""
file "$folder"/oppai
readelf --dynamic "$folder"/oppai
ldd "$folder"/oppai
echo ""
file "$folder"/liboppai.so
ldd "$folder"/liboppai.so
rm -rf "$folder"
cd $olddir

59
release.ps1 Normal file
View File

@ -0,0 +1,59 @@
# you must allow script execution by running
# 'Set-ExecutionPolicy RemoteSigned' in an admin powershell
# this requires vcvars to be already set (see vcvarsall17.ps1)
# 7zip is also required (choco install 7zip and add it to path)
$dir = Split-Path -Parent $MyInvocation.MyCommand.Definition
Push-Location "$dir"
git pull origin master -q
function Write-Header {
param ([string]$Text)
Write-Host $Text -Foreground Yellow -Background Black
}
function Header {
param ([string]$Title)
Write-Header ""
Write-Header "##########################################################"
Write-Header "> $Title"
}
cmd /c "build.bat"; if (-not $?) { exit $LastExitCode }
cmd /c "libbuild.bat"; if (-not $?) { exit $LastExitCode }
Header "Packaging"
$folder = "oppai-" + $(.\oppai.exe -version) + "-windows-"
$clout = & cl 2>&1 | %{ "$_" }
"$clout" -match "(Microsoft.*for )([a-z0-9\-_]+)" | Out-Null
if (-not $?) {
exit $LastExitCode
}
$folder = $folder + $Matches[2]
mkdir $folder; if (-not $?) { exit $LastExitCode }
Copy-Item oppai.exe $folder; if (-not $?) { exit $LastExitCode }
Copy-Item oppai.dll $folder; if (-not $?) { exit $LastExitCode }
Copy-Item oppai.lib $folder; if (-not $?) { exit $LastExitCode }
git archive HEAD --prefix=src\ -o $folder\src.zip
if (-not $?) {
exit $LastExitCode
}
Set-Location $folder; if (-not $?) { exit $LastExitCode }
&7z x src.zip; if (-not $?) { exit $LastExitCode }
Set-Location ..; if (-not $?) { exit $LastExitCode }
if (Test-Path "$folder.zip") {
Remove-Item "$folder.zip"
}
&7z a "$folder.zip" $folder\oppai.exe $folder\oppai.dll $folder\oppai.lib `
$folder\src
if (-not $?) {
exit $LastExitCode
}
Header "Result:"
&7z l "$folder.zip"
Remove-Item $folder -Force -Recurse
Pop-Location

17
swig/README.md Normal file
View File

@ -0,0 +1,17 @@
these are maintainer instructions, check the readme's in the binding
directories for user guides
# requirements
* swig
* docker
* twine (pip)
# building all bindings
```sh
./build.sh
```
# publishing all bindings
```sh
PUBLISH=1 ./build.sh
```

19
swig/build.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/sh
runall() {
for d in ./*/; do
[ "$d" = "." ] && continue
cd "$d"
./build.sh || return $?
[ ! -z $PUBLISH ] && ./publish.sh
cd ..
done
}
dir="$(dirname "$0")"
olddir="$(pwd)"
cd "$dir"
runall
res=$?
cd "$olddir"
exit $res

11
swig/oppai.i Normal file
View File

@ -0,0 +1,11 @@
%module oppai
%feature("autodoc", "3");
%apply int *OUTPUT {int*}
%begin{
#define SWIG_PYTHON_2_UNICODE
}
%{
#define OPPAI_IMPLEMENTATION
#include "oppai.c"
%}
#include "oppai.c"

42
swig/python/README.rst Normal file
View File

@ -0,0 +1,42 @@
osu! pp and difficulty calculator. automatically generated C bindings for
https://github.com/Francesco149/oppai-ng
usage
===========
.. code-block:: sh
pip install oppai
.. code-block:: python
#!/usr/bin/env python
import sys
from oppai import *
ez = ezpp_new()
ezpp(ez, sys.argv[1])
print("%g pp" % ezpp_pp(ez))
ezpp_free(ez)
.. code-block:: sh
./example.py /path/to/file.osu
.. code-block:: sh
python -c 'help("oppai")'
for a list of functions, or just read the top of oppai.c for better doc
limitations
===========
for some reason, python3 doesn't provide a persisting pointer to strings
you pass to c code even if you aren't doing anything with them, so if you
want to reuse the handle at all you have to use ezpp_dup and ezpp_data_dup,
which create a copy of the strings you pass in. this is inefficient so
it's recommended to use autocalc mode and only call ezpp_dup or
ezpp_data_dup when you're actually changing map

10
swig/python/build.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
rm -rf ./dist
cp ../../oppai.c .
cp ../oppai.i .
swig -python -includeall oppai.i || exit
for img in quay.io/pypa/manylinux2010_x86_64 quay.io/pypa/manylinux2010_i686; do
docker run --user 1000:1000 --rm -v $(pwd):/io -w /io $img \
./build_wheels.sh || exit
done

15
swig/python/build_wheels.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/sh
# this is meant to be used from docker
for pybin in /opt/python/*/bin
do
rm *.so
"$pybin/pip" wheel . -w dist/ || exit
done
"$pybin/python" ./setup.py sdist || exit
for w in dist/*linux_*.whl; do
auditwheel repair "$w" -w dist/ || exit
done
rm dist/*linux_*.whl

9
swig/python/examples/basic.py Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env python
import sys
from oppai import *
ez = ezpp_new()
ezpp(ez, sys.argv[1])
print("%g pp" % ezpp_pp(ez))
ezpp_free(ez)

16
swig/python/examples/reuse.py Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env python
import sys
from oppai import *
ez = ezpp_new()
ezpp_set_autocalc(ez, 1)
for osufile in sys.argv[1:]:
ezpp_dup(ez, osufile)
print("%s - %s [%s]" % (ezpp_artist(ez), ezpp_title(ez), ezpp_version(ez)))
print("%g stars" % ezpp_stars(ez))
for acc in range(95, 101):
ezpp_set_accuracy_percent(ez, acc)
print("%g%% -> %g pp" % (acc, ezpp_pp(ez)))
print("")
ezpp_free(ez)

View File

@ -0,0 +1,38 @@
#!/usr/bin/env python
import sys
from oppai import *
if sys.version_info[0] < 3:
# hack to force utf-8 on py < 3
reload(sys)
sys.setdefaultencoding("utf-8")
def mods_str(mods):
mods_str = "+"
if mods == 0:
mods_str += "nomod"
else:
if mods & MODS_HD: mods_str += "hd"
if mods & MODS_DT: mods_str += "dt"
if mods & MODS_HR: mods_str += "hr"
return mods_str
ez = ezpp_new()
ezpp_set_autocalc(ez, 1)
for osufile in sys.argv[1:]:
# by providing the map in memory we can speed up subsequent re-parses
f = open(osufile, 'r')
data = f.read()
f.close()
ezpp_data_dup(ez, data, len(data.encode('utf-8')))
print("%s - %s [%s]" % (ezpp_artist(ez), ezpp_title(ez), ezpp_version(ez)))
print("%g stars" % ezpp_stars(ez))
for mods in [ 0, MODS_HR, MODS_HD | MODS_HR, MODS_DT, MODS_HD | MODS_DT ]:
print(mods_str(mods))
ezpp_set_mods(ez, mods)
for acc in range(95, 101):
ezpp_set_accuracy_percent(ez, acc)
print("%g%% -> %g pp" % (acc, ezpp_pp(ez)))
print("")
ezpp_free(ez)

14
swig/python/examples/timing.py Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env python
import sys
from oppai import *
# prints timing points (just a test for this interface)
ez = ezpp_new()
ezpp(ez, sys.argv[1])
for i in range(ezpp_ntiming_points(ez)):
time = ezpp_timing_time(ez, i)
ms_per_beat = ezpp_timing_ms_per_beat(ez, i)
change = ezpp_timing_change(ez, i)
print("%f | %f beats per ms | change: %d" % (time, ms_per_beat, change))
ezpp_free(ez)

3
swig/python/publish.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
twine upload dist/*

25
swig/python/python.yml Normal file
View File

@ -0,0 +1,25 @@
environment:
TWINE_USERNAME: lolisamurai
TWINE_PASSWORD:
secure: DTyX4L2loxFxlsbPYAwuga0DyOlGiOnJyEwi/j08gba0NyNx21TvRFMHpITIqcfg
cache:
- C:\ProgramData\chocolatey\bin
- C:\ProgramData\chocolatey\lib
- C:\Users\appveyor\AppData\Local\pip\Cache
install:
- IF NOT EXIST C:\ProgramData\chocolatey\bin\swig.exe choco install swig --version 3.0.12 --yes --limit-output
- python -m pip install twine
- python -m pip install cibuildwheel==0.10.1
build_script:
- cd swig/python
- copy ..\..\oppai.c .
- copy ..\oppai.i .
- swig -python -includeall oppai.i
- cibuildwheel --output-dir wheelhouse
- ps: >-
if ($env:APPVEYOR_REPO_TAG -eq "true") {
Invoke-Expression "python -m twine upload --skip-existing wheelhouse/*.whl"
}
artifacts:
- path: "swig\\python\\wheelhouse\\*.whl"
name: wheels

2
swig/python/setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[build_ext]
swig-opts=-includeall

58
swig/python/setup.py Normal file
View File

@ -0,0 +1,58 @@
import os
import sys
def parse_version_str():
import re
version_re = re.compile('^#define OPPAI_VERSION_(MAJOR|MINOR|PATCH)')
version = []
with open('oppai.c', 'r') as f:
for line in f:
if version_re.match(line):
version.append(str(int(line.split(' ')[2])))
return '.'.join(version)
try:
from setuptools import setup, Extension
except ImportError:
from distutils.core import setup, Extension
try:
from oppai import oppai_version_str
except Exception:
def oppai_version_str():
try:
return parse_version_str()
except Exception:
return "INVALID"
oppai_classifiers = [
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Intended Audience :: Developers",
"License :: Public Domain",
"Topic :: Software Development :: Libraries",
"Topic :: Utilities",
]
f = open("README.rst", "r")
oppai_readme = f.read()
f.close()
oppai_sources=['oppai.i']
if os.system('swig') != 0:
oppai_sources=['oppai_wrap.c', 'oppai.c']
setup(
name="oppai",
version=oppai_version_str(),
author="Franc[e]sco",
author_email="lolisamurai@tfwno.gf",
url="https://github.com/Francesco149/oppai-ng",
ext_modules=[Extension('_oppai', oppai_sources)],
py_modules=["oppai"],
description="osu! pp and difficulty calculator, C bindings",
long_description=oppai_readme,
license="Unlicense",
classifiers=oppai_classifiers,
keywords="osu! osu"
)

8
test/build Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
dir="$(dirname "$0")"
. "$dir"/../cflags
$cc $cflags ${@:--DOPPAI_IMPLEMENTATION} \
test.c $ldflags -o oppai_test

16
test/build.bat Normal file
View File

@ -0,0 +1,16 @@
@echo off
set flags="%*"
IF "%1"=="" (
set flags=-DOPPAI_IMPLEMENTATION
)
del oppai_test.exe >nul 2>&1
del test.obj >nul 2>&1
cl -D_CRT_SECURE_NO_WARNINGS=1 ^
-DNOMINMAX=1 ^
-O2 -nologo -MT -Gm- -GR- -EHsc -W4 ^
%flags% ^
test.c ^
-Feoppai_test.exe ^
|| EXIT /B 1

21
test/download_suite Executable file
View File

@ -0,0 +1,21 @@
#!/bin/sh
dir="$(dirname "$0")"
if command -v realpath 2>&1 >/dev/null; then
wdir="$(realpath "$dir")"
else
wdir="$dir"
fi
olddir="$(pwd)"
cd "$wdir" || exit $?
url="$(cat ./suite_url)"
if [ $(find test_suite 2>/dev/null | tail -n +2 | wc -l) = "0" ]; then
curl -LO "$url" || exit $?
tar xf "$(basename $url)" || exit $?
else
echo "using existing test_suite"
fi
cd "$olddir"

19
test/download_suite.ps1 Normal file
View File

@ -0,0 +1,19 @@
# you must allow script execution by running
# 'Set-ExecutionPolicy RemoteSigned' in an admin powershell
# 7zip is also required (choco install 7zip and add it to path)
$url = Get-Content .\suite_url -Raw
$dir = Split-Path -Parent $MyInvocation.MyCommand.Definition
Push-Location "$dir"
if ((Test-Path .\test_suite) -and (Get-ChildItem .\test_suite | Measure-Object).Count -gt 0) {
Write-Host "using existing test_suite"
} else {
# my windows 7 install doesn't support Tls3
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
(New-Object System.Net.WebClient).DownloadFile($url, "$dir\test_suite.tar.gz")
&7z x .\test_suite.tar.gz
&7z x .\test_suite.tar
}
Pop-Location

79
test/download_suite.py Executable file
View File

@ -0,0 +1,79 @@
#!/usr/bin/env python
# very rough script that downloads unique maps from test_suite.json that
# gensuite.py generates
import sys
import json
try:
import httplib
except ImportError:
import http.client as httplib
try:
import urllib
except ImportError:
import urllib.parse as urllib
osu = httplib.HTTPSConnection('osu.ppy.sh')
def osu_get(path):
while True:
try:
osu.request('GET', path)
r = osu.getresponse()
raw = bytes()
while True:
try:
raw += r.read()
break
except httplib.IncompleteRead as e:
raw += e.partial
return raw
except (httplib.HTTPException, ValueError) as e:
sys.stderr.write('%s\n' % (traceback.format_exc()))
# prevents exceptions on next request if the
# response wasn't previously read due to errors
try:
osu.getresponse().read()
except httplib.HTTPException:
pass
time.sleep(5)
if len(sys.argv) != 2:
sys.stderr.write('usage: %s test_suite.json\n' % sys.argv[0])
sys.exit(1)
with open(sys.argv[1], 'r') as f:
scores = json.loads(f.read())
unique_maps = set([s['beatmap_id'] for m in [0, 1] for s in scores[m]])
i = 1
for b in unique_maps:
sys.stderr.write(
"[%.02f%% - %d/%d] %s" % (i / float(len(unique_maps)) * 100, i,
len(unique_maps), b)
)
i += 1
# TODO: tmp file and rename
try:
with open(b + '.osu', 'r') as f:
sys.stderr.write(' (already exists)\n')
continue
except FileNotFoundError:
pass
sys.stderr.write('\n')
with open(b + '.osu', 'wb') as f:
f.write(osu_get('/osu/' + b))

292
test/gentest.py Executable file
View File

@ -0,0 +1,292 @@
#!/usr/bin/env python
import sys
import os
import time
import json
import traceback
import argparse
import hashlib
if sys.version_info[0] < 3:
# hack to force utf-8
reload(sys)
sys.setdefaultencoding('utf-8')
try:
import httplib
except ImportError:
import http.client as httplib
try:
import urllib
except ImportError:
import urllib.parse as urllib
# -------------------------------------------------------------------------
parser = argparse.ArgumentParser(
description = (
'generates the oppai test suite. outputs c++ code to ' +
'stdout and the json dump to a file.'
)
)
parser.add_argument(
'-key',
default = None,
help = (
'osu! api key. required if -input-file is not present. ' +
'can also be specified through the OSU_API_KEY ' +
'environment variable'
)
)
parser.add_argument(
'-output-file',
default = 'test_suite.json',
help = 'dumps json to this file'
)
parser.add_argument(
'-input-file',
default = None,
help = (
'loads test suite from this json file instead of '
'fetching it from osu api. if set to "-", json will be '
'read from standard input'
)
)
args = parser.parse_args()
if args.key == None and 'OSU_API_KEY' in os.environ:
args.key = os.environ['OSU_API_KEY']
# -------------------------------------------------------------------------
osu_treset = time.time() + 60
osu_ncalls = 0
def osu_get(conn, endpoint, paramsdict=None):
# GETs /api/endpoint?paramsdict&k=args.key from conn
# return json object, exits process on api errors
global osu_treset, osu_ncalls, args
sys.stderr.write('%s %s\n' % (endpoint, str(paramsdict)))
paramsdict['k'] = args.key
path = '/api/%s?%s' % (endpoint, urllib.urlencode(paramsdict))
while True:
while True:
if time.time() >= osu_treset:
osu_ncalls = 0
osu_treset = time.time() + 60
sys.stderr.write('\napi ready\n')
if osu_ncalls < 60:
break
else:
sys.stderr.write('waiting for api cooldown...\r')
time.sleep(1)
try:
conn.request('GET', path)
osu_ncalls += 1
r = conn.getresponse()
raw = ''
while True:
try:
raw += r.read()
break
except httplib.IncompleteRead as e:
raw += e.partial
j = json.loads(raw)
if 'error' in j:
sys.stderr.write('%s\n' % j['error'])
sys.exit(1)
return j
except (httplib.HTTPException, ValueError) as e:
sys.stderr.write('%s\n' % (traceback.format_exc()))
try:
# prevents exceptions on next request if the
# response wasn't previously read due to errors
conn.getresponse().read()
except httplib.HTTPException:
pass
time.sleep(5)
def gen_modstr(bitmask):
# generates c++ code for a mod combination's bitmask
mods = []
allmods = {
(1<< 0, 'nf'), (1<< 1, 'ez'), (1<< 2, 'td'), (1<< 3, 'hd'),
(1<< 4, 'hr'), (1<< 6, 'dt'), (1<< 8, 'ht'),
(1<< 9, 'nc'), (1<<10, 'fl'), (1<<12, 'so')
}
for bit, string in allmods:
if bitmask & bit != 0:
mods.append(string)
if len(mods) == 0:
return 'nomod'
return ' | '.join(mods)
# -------------------------------------------------------------------------
if args.key == None:
sys.stderr.write(
'please set OSU_API_KEY or pass it as a parameter\n'
)
sys.exit(1)
scores = []
top_players = [
[ 124493, 4787150, 2558286, 1777162, 2831793, 50265 ],
[ 3174184, 8276884, 5991961, 2774767 ]
]
if args.input_file == None:
# fetch a fresh test suite from osu api
osu = httplib.HTTPSConnection('osu.ppy.sh')
for m in [0, 1]:
for u in top_players[m]:
params = { 'u': u, 'limit': 100, 'type': 'id', 'm': m }
batch = osu_get(osu, 'get_user_best', params)
for s in batch:
s['mode'] = m
scores += batch
# temporarily removed, not all std scores are recalc'd
#params = { 'm': 0, 'since': '2015-11-26' }
#maps = osu_get(osu, 'get_beatmaps', params)
# no taiko converts here because as explained below, tiny float
# errors can lead to completely broken conversions
for mode in [1]:
params = { 'm': mode, 'since': '2015-11-26' }
maps = osu_get(osu, 'get_beatmaps', params)
for m in maps:
params = { 'b': m['beatmap_id'], 'm': mode }
map_scores = osu_get(osu, 'get_scores', params)
if len(map_scores) == 0:
sys.stderr.write('W: map has no scores???\n')
continue
# note: api also returns qualified and loved, so ignore
# maps that don't have pp in rankings
if not 'pp' in map_scores[0] or map_scores[0]['pp'] is None:
sys.stderr.write('W: ignoring loved/qualified map\n')
continue
for s in map_scores:
s['beatmap_id'] = m['beatmap_id']
s['mode'] = mode
scores += map_scores
with open(args.output_file, 'w+') as f:
f.write(json.dumps(scores))
else:
# load existing test suite from json file
with open(args.input_file, 'r') as f:
scores = json.loads(f.read())
# sort by mode by map
scores.sort(key=lambda x: int(x['beatmap_id'])<<32|x['mode'],
reverse=True)
print('/* this code was automatically generated by gentest.py */')
print('')
# make code a little nicer by shortening mods
allmods = {
'nf', 'ez', 'td', 'hd', 'hr', 'dt', 'ht', 'nc', 'fl', 'so', 'nomod'
}
for mod in allmods:
print('#define %s MODS_%s' % (mod, mod.upper()))
print('''
typedef struct {
int mode;
int id;
int max_combo;
int n300, n100, n50, nmiss;
int mods;
double pp;
} score_t;
score_t suite[] = {''')
seen_hashes = []
for s in scores:
# due to tiny floating point errors, maps can even double
# in combo and not even lazer gets this right, taiko converts are hell
# so I'm just gonna exclude them
if s['mode'] == 1:
is_convert = False
with open('test_suite/'+s['beatmap_id']+'.osu') as f:
for line in f:
split = line.split(':')
if len(split) >= 2 and split[0] == 'Mode' and int(split[1]) == 0:
is_convert = True
break
if is_convert:
continue
# some taiko maps ignore combo scaling for no apparent reason
# so i will only include full combos for now
if int(s['countmiss']) != 0:
continue
if s['pp'] is None:
continue
# why is every value returned by osu api a string?
line = (
' { %d, %s, %s, %s, %s, %s, %s, %s, %s },' %
(
s['mode'], s['beatmap_id'], s['maxcombo'], s['count300'],
s['count100'], s['count50'], s['countmiss'],
gen_modstr(int(s['enabled_mods'])), s['pp']
)
)
# don't include identical scores by different people
s = hashlib.sha1(line).digest()
if s in seen_hashes:
continue
print(line)
seen_hashes.append(s)
print('};\n')
for mod in allmods:
print('#undef %s' % (mod))

11
test/pack_suite Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
dir="$(dirname "$0")"
wdir="$(realpath "$dir")"
olddir="$(pwd)"
cd "$wdir" || exit $?
tar -zcvf test_suite_$(date +%F).tar.gz test_suite/*.osu
res=$?
cd "$olddir"
exit $res

1
test/suite_url Normal file
View File

@ -0,0 +1 @@
https://github.com/Francesco149/oppai-ng/releases/download/2.3.2/test_suite_2019-02-19.tar.gz

119
test/test.c Normal file
View File

@ -0,0 +1,119 @@
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../oppai.c"
#include "test_suite.c" /* defines suite */
#define ERROR_MARGIN 0.02f /* pp can be off by +- 2% */
/*
* margin is actually
* - 3x for < 100pp
* - 2x for 100-200pp
* - 1.5x for 200-300pp
*/
void print_score(score_t* s) {
char mods_str_buf[20];
char* mods_str = mods_str_buf;
strcpy(mods_str, "nomod");
#define m(mod) \
if (s->mods & MODS_##mod) { \
mods_str += sprintf(mods_str, #mod); \
} \
m(HR) m(NC) m(HT) m(SO) m(NF) m(EZ) m(DT) m(FL) m(HD)
#undef m
printf("m=%d %u +%s %dx %dx300 %dx100 %dx50 %dxmiss %g pp\n",
s->mode, s->id, mods_str_buf, s->max_combo, s->n300, s->n100,
s->n50, s->nmiss, s->pp);
}
int main() {
char fname_buf[4096];
char* fname = fname_buf;
int i, n = (int)(sizeof(suite) / sizeof(suite[0]));
float max_err[2] = { 0, 0 };
int max_err_map[2] = { 0, 0 };
float avg_err[2] = { 0, 0 };
int count[2] = { 0, 0 };
float error = 0;
float error_percent = 0;
ezpp_t ez = ezpp_new();
int err;
int last_id = 0;
fname += sprintf(fname, "test_suite/");
for (i = 0; i < n; ++i) {
score_t* s = &suite[i];
float margin;
float pptotal;
print_score(s);
if (s->id != last_id) {
sprintf(fname, "%u.osu", s->id);
last_id = s->id;
/* force reparse and don't reuse prev map ar/cs/od/hp */
ezpp_set_base_cs(ez, -1);
ezpp_set_base_ar(ez, -1);
ezpp_set_base_od(ez, -1);
ezpp_set_base_hp(ez, -1);
}
ezpp_set_mods(ez, s->mods);
ezpp_set_accuracy(ez, s->n100, s->n50);
ezpp_set_nmiss(ez, s->nmiss);
ezpp_set_combo(ez, s->max_combo);
ezpp_set_mode_override(ez, s->mode);
err = ezpp(ez, fname_buf);
if (err < 0) {
printf("%s\n", errstr(err));
exit(1);
}
pptotal = ezpp_pp(ez);
++count[s->mode];
margin = (float)s->pp * ERROR_MARGIN;
if (s->pp < 100) {
margin *= 3;
} else if (s->pp < 200) {
margin *= 2;
} else if (s->pp < 300) {
margin *= 1.5f;
}
error = (float)fabs(pptotal - (float)s->pp);
error_percent = error / (float)s->pp;
avg_err[s->mode] += error_percent;
if (error_percent > max_err[s->mode]) {
max_err[s->mode] = error_percent;
max_err_map[s->mode] = s->id;
}
if (error >= margin) {
printf("failed test: got %g pp, expected %g\n", pptotal, s->pp);
exit(1);
}
}
for (i = 0; i < 2; ++i) {
switch (i) {
case MODE_STD: puts("osu"); break;
case MODE_TAIKO: puts("taiko"); break;
}
avg_err[i] /= count[i];
printf("%d scores\n", count[i]);
printf("avg err %f\n", avg_err[i]);
printf("max err %f on %d\n", max_err[i], max_err_map[i]);
}
ezpp_free(ez);
return 0;
}

14745
test/test_suite.c Normal file

File diff suppressed because it is too large Load Diff

3
valgrind Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
valgrind --leak-check=full --track-origins=yes --show-leak-kinds=all "$@"

15
vcvarsall17.ps1 Normal file
View File

@ -0,0 +1,15 @@
# https://stackoverflow.com/questions/2124753/how-can-i-use-powershell-with-the-visual-studio-command-prompt
param (
[string]$arch = "amd64"
)
Push-Location "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\Common7\Tools"
cmd /c "VsDevCmd.bat -arch=$arch&set" |
ForEach-Object {
if ($_ -match "=") {
$v = $_.split("="); set-item -force -path "ENV:\$($v[0])" -value "$($v[1])"
}
}
Pop-Location
Write-Host "`nVisual Studio 2017 Command Prompt variables set." -ForegroundColor Yellow