updated rosu-pp & whole rewrite

This commit is contained in:
Max
2022-10-29 16:17:33 +02:00
parent 2e98ffc5dc
commit 5c13a3a021
10 changed files with 985 additions and 903 deletions
Generated
+3 -3
View File
@@ -159,13 +159,13 @@ dependencies = [
[[package]]
name = "rosu-pp"
version = "0.8.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "238725936123d4c42c3a4492397d02614798de0881008e7377d78d5db7e0e042"
checksum = "70c4f12b757391bb233619b0bb267e424bc7395dcbfa1f85212f16b734784a07"
[[package]]
name = "rosu-pp-py"
version = "0.8.0"
version = "0.9.0"
dependencies = [
"pyo3",
"rosu-pp",
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "rosu-pp-py"
version = "0.8.0"
version = "0.9.0"
description = "osu! difficulty and pp calculation for all modes"
authors = ["Max Ohn <ohn.m@hotmail.de>"]
license = "MIT"
@@ -11,8 +11,8 @@ name = "rosu_pp_py"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.16", features = ["macros", "extension-module"] }
rosu-pp = { version = "0.8" }
pyo3 = { version = "0.16", features = ["extension-module", "macros"] }
rosu-pp = { version = "0.9" }
[profile.release]
lto = "fat"
+105
View File
@@ -0,0 +1,105 @@
use pyo3::{exceptions::PyTypeError, pyclass, pymethods, types::PyDict, PyResult};
use rosu_pp::Beatmap;
use crate::error::{ErrorExt, KwargsError, ParseError};
#[pyclass(name = "Beatmap")]
pub struct PyBeatmap {
pub(crate) inner: Beatmap,
}
#[pymethods]
impl PyBeatmap {
#[new]
#[args(kwargs = "**")]
fn new(kwargs: Option<&PyDict>) -> PyResult<Self> {
let kwargs = match kwargs {
Some(kwargs) => kwargs,
None => {
return Err(KwargsError::new_err(
"'Beatmap' constructor requires kwargs",
))
}
};
if let Some(arg) = kwargs.get_item("path") {
let path = arg
.extract::<&str>()
.map_err(|_| PyTypeError::new_err("kwarg 'path': must be a string"))?;
let map = Beatmap::from_path(path)
.map_err(|e| ParseError::new_err(e.unwind("Failed to parse beatmap")))?;
Self::new_with_attrs(map, kwargs)
} else if let Some(arg) = kwargs.get_item("content") {
if let Ok(content) = arg.extract::<&str>() {
Self::new_from_bytes(content.as_bytes(), kwargs)
} else if let Ok(bytes) = arg.extract::<&[u8]>() {
Self::new_from_bytes(bytes, kwargs)
} else {
Err(PyTypeError::new_err(
"kwarg 'content': must be a string or a bytearray",
))
}
} else if let Some(arg) = kwargs.get_item("bytes") {
let bytes = arg
.extract::<&[u8]>()
.map_err(|_| PyTypeError::new_err("kwarg 'bytes': must be a bytearray"))?;
Self::new_from_bytes(bytes, kwargs)
} else {
Err(KwargsError::new_err(
"kwargs must include 'path', 'content', or 'bytes'",
))
}
}
fn set_ar(&mut self, ar: f32) {
self.inner.ar = ar;
}
fn set_cs(&mut self, cs: f32) {
self.inner.cs = cs;
}
fn set_hp(&mut self, hp: f32) {
self.inner.hp = hp;
}
fn set_od(&mut self, od: f32) {
self.inner.od = od;
}
}
impl PyBeatmap {
fn new_from_bytes(bytes: &[u8], kwargs: &PyDict) -> PyResult<Self> {
let map = Beatmap::from_bytes(bytes)
.map_err(|e| ParseError::new_err(e.unwind("Failed to parse beatmap")))?;
Self::new_with_attrs(map, kwargs)
}
fn new_with_attrs(mut map: Beatmap, kwargs: &PyDict) -> PyResult<Self> {
macro_rules! parse_attr {
( $( $name:ident ),*) => {
$(
if let Some(arg) = kwargs.get_item(stringify!($name)) {
let value = arg.extract::<f32>().map_err(|_| {
PyTypeError::new_err(concat!(
"kwarg '",
stringify!($name),
"': must be a real number"
))
})?;
map.$name = value;
}
)*
};
}
parse_attr!(ar, cs, hp, od);
Ok(Self { inner: map })
}
}
+214
View File
@@ -0,0 +1,214 @@
use pyo3::{exceptions::PyValueError, pyclass, pymethods, types::PyDict, PyResult};
use rosu_pp::{AnyPP, AnyStars, DifficultyAttributes, GameMode};
use crate::{
beatmap::PyBeatmap, diff_attrs::PyDifficultyAttributes, error::KwargsError,
map_attrs::PyBeatmapAttributes, perf_attrs::PyPerformanceAttributes, strains::PyStrains,
};
#[pyclass]
#[derive(Default)]
pub struct Calculator {
attributes: Option<DifficultyAttributes>,
mode: Option<GameMode>,
mods: Option<u32>,
acc: Option<f64>,
n_geki: Option<usize>,
n_katu: Option<usize>,
n300: Option<usize>,
n100: Option<usize>,
n50: Option<usize>,
n_misses: Option<usize>,
combo: Option<usize>,
passed_objects: Option<usize>,
clock_rate: Option<f64>,
}
macro_rules! set_calc {
( $calc:ident, $this:ident: $( $field:ident ,)* ) => {
$(
if let Some(val) = $this.$field {
$calc = $calc.$field(val);
}
)*
};
}
#[pymethods]
impl Calculator {
#[new]
#[args(kwargs = "**")]
fn new(kwargs: Option<&PyDict>) -> PyResult<Self> {
let kwargs = match kwargs {
Some(kwargs) => kwargs,
None => return Ok(Self::default()),
};
let mut this = Self::default();
for (key, value) in kwargs.iter() {
match key.extract()? {
"mode" => {
this.mode = match value.extract::<u8>()? {
0 => Some(GameMode::Osu),
1 => Some(GameMode::Taiko),
2 => Some(GameMode::Catch),
3 => Some(GameMode::Mania),
_ => return Err(PyValueError::new_err("invalid mode integer")),
}
}
"mods" => this.mods = value.extract()?,
"n300" => this.n300 = value.extract()?,
"n100" => this.n100 = value.extract()?,
"n50" => this.n50 = value.extract()?,
"n_misses" => this.n_misses = value.extract()?,
"n_geki" => this.n_geki = value.extract()?,
"n_katu" => this.n_katu = value.extract()?,
"acc" | "accuracy" => this.acc = value.extract()?,
"combo" => this.combo = value.extract()?,
"passed_objects" => this.passed_objects = value.extract()?,
"clock_rate" => this.clock_rate = value.extract()?,
"difficulty" | "attributes" => {
let attrs = value.extract::<PyDifficultyAttributes>()?;
this.attributes = Some(attrs.inner);
}
kwarg => {
let err = format!(
"unexpected kwarg '{kwarg}': expected 'mode', 'mods', \n\
'n_geki', 'n_katu', 'n300', 'n100', 'n50', 'n_misses', \n\
'acc', 'combo', 'passed_objects', 'clock_rate', or 'difficulty'"
);
return Err(KwargsError::new_err(err));
}
}
}
Ok(this)
}
fn set_mods(&mut self, mods: u32) {
self.mods = Some(mods);
}
fn set_acc(&mut self, acc: f64) {
self.acc = Some(acc);
}
fn set_n_geki(&mut self, n_geki: usize) {
self.n_geki = Some(n_geki);
}
fn set_n_katu(&mut self, n_katu: usize) {
self.n_katu = Some(n_katu);
}
fn set_n300(&mut self, n300: usize) {
self.n300 = Some(n300);
}
fn set_n100(&mut self, n100: usize) {
self.n100 = Some(n100);
}
fn set_n50(&mut self, n50: usize) {
self.n50 = Some(n50);
}
fn set_n_misses(&mut self, n_misses: usize) {
self.n_misses = Some(n_misses);
}
fn set_combo(&mut self, combo: usize) {
self.combo = Some(combo);
}
fn set_passed_objects(&mut self, passed_objects: usize) {
self.passed_objects = Some(passed_objects);
}
fn set_clock_rate(&mut self, clock_rate: f64) {
self.clock_rate = Some(clock_rate);
}
fn set_difficulty(&mut self, difficulty: PyDifficultyAttributes) {
self.attributes = Some(difficulty.inner);
}
fn map_attributes(&self, map: &PyBeatmap) -> PyResult<PyBeatmapAttributes> {
let map = &map.inner;
let mut calc = map.attributes();
if let Some(mode) = self.mode {
calc.mode(mode);
if map.mode != mode && map.mode == GameMode::Osu {
calc.converted(true);
}
}
if let Some(mods) = self.mods {
calc.mods(mods);
}
if let Some(clock_rate) = self.clock_rate {
calc.clock_rate(clock_rate);
}
Ok(PyBeatmapAttributes::new(calc.build(), map))
}
fn difficulty(&self, map: &PyBeatmap) -> PyResult<PyDifficultyAttributes> {
let mut calc = AnyStars::new(&map.inner);
set_calc! { calc, self:
mode,
mods,
passed_objects,
clock_rate,
};
Ok(calc.calculate().into())
}
fn performance(&self, map: &PyBeatmap) -> PyResult<PyPerformanceAttributes> {
let mut calc = AnyPP::new(&map.inner);
set_calc! { calc, self:
mode,
mods,
n_geki,
n_katu,
n300,
n100,
n50,
n_misses,
combo,
passed_objects,
clock_rate,
};
if let Some(ref attrs) = self.attributes {
calc = calc.attributes(attrs.to_owned());
}
if let Some(acc) = self.acc {
calc = calc.accuracy(acc);
}
Ok(calc.calculate().into())
}
fn strains(&self, map: &PyBeatmap) -> PyResult<PyStrains> {
let mut calc = AnyStars::new(&map.inner);
set_calc! { calc, self:
mode,
mods,
passed_objects,
clock_rate,
};
Ok(calc.strains().into())
}
}
+233
View File
@@ -0,0 +1,233 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{pyclass, pymethods};
use rosu_pp::{
catch::CatchDifficultyAttributes, mania::ManiaDifficultyAttributes,
osu::OsuDifficultyAttributes, taiko::TaikoDifficultyAttributes, DifficultyAttributes,
};
#[pyclass(name = "DifficultyAttributes")]
#[derive(Clone, Debug)]
pub struct PyDifficultyAttributes {
pub(crate) inner: DifficultyAttributes,
}
impl From<DifficultyAttributes> for PyDifficultyAttributes {
#[inline]
fn from(attrs: DifficultyAttributes) -> Self {
Self { inner: attrs }
}
}
impl Display for PyDifficultyAttributes {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let borrowed = BorrowedDifficultyAttributes::from(&self.inner);
Debug::fmt(&borrowed, f)
}
}
pub(crate) enum BorrowedDifficultyAttributes<'a> {
Osu(&'a OsuDifficultyAttributes),
Taiko(&'a TaikoDifficultyAttributes),
Catch(&'a CatchDifficultyAttributes),
Mania(&'a ManiaDifficultyAttributes),
}
macro_rules! impl_from {
( $( $mode:ident: $attrs:ident, )* ) => {
$(
impl<'a> From<&'a $attrs> for BorrowedDifficultyAttributes<'a> {
#[inline]
fn from(attrs: &'a $attrs) -> Self {
Self::$mode(attrs)
}
}
)*
impl<'a> From<&'a DifficultyAttributes> for BorrowedDifficultyAttributes<'a> {
#[inline]
fn from(attrs: &'a DifficultyAttributes) -> Self {
match attrs {
$( DifficultyAttributes::$mode(attrs) => Self::$mode(attrs), )*
}
}
}
};
}
impl_from! {
Osu: OsuDifficultyAttributes,
Taiko: TaikoDifficultyAttributes,
Catch: CatchDifficultyAttributes,
Mania: ManiaDifficultyAttributes,
}
impl Debug for BorrowedDifficultyAttributes<'_> {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut debug = f.debug_struct("DifficultyAttributes");
macro_rules! debug {
( $( $field:ident $( , )? )* ) => {
debug $( .field(stringify!($field), $field) )*;
}
}
match self {
Self::Osu(attrs) => {
let OsuDifficultyAttributes {
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
ar,
od,
hp,
n_circles,
n_sliders,
n_spinners,
stars,
max_combo,
} = attrs;
debug.field("mode", &0_u8);
debug! {
aim,
speed,
flashlight,
slider_factor,
speed_note_count,
ar,
od,
hp,
n_circles,
n_sliders,
n_spinners,
stars,
max_combo,
}
}
Self::Taiko(attrs) => {
let TaikoDifficultyAttributes {
stamina,
rhythm,
colour,
peak,
hit_window,
stars,
max_combo,
} = attrs;
debug.field("mode", &1_u8).field("color", colour);
debug!(stamina, rhythm, peak, hit_window, stars, max_combo);
}
Self::Catch(attrs) => {
let max_combo = attrs.max_combo();
let CatchDifficultyAttributes {
stars,
ar,
n_fruits,
n_droplets,
n_tiny_droplets,
} = attrs;
debug.field("mode", &2_u8);
debug!(stars, ar, n_fruits, n_droplets, n_tiny_droplets);
debug.field("max_combo", &max_combo);
}
Self::Mania(attrs) => {
let ManiaDifficultyAttributes {
stars,
hit_window,
max_combo,
} = attrs;
debug.field("mode", &3_u8);
debug!(stars, hit_window, max_combo);
}
}
debug.finish()
}
}
macro_rules! getters {
(
$(
$field:ident as $ty:ty: ( $( $mode:ident ),* ),
)*
) => {
#[pymethods]
impl PyDifficultyAttributes {
#[getter]
fn mode(&self) -> u8 {
match self.inner {
DifficultyAttributes::Osu(_) => 0,
DifficultyAttributes::Taiko(_) => 1,
DifficultyAttributes::Catch(_) => 2,
DifficultyAttributes::Mania(_) => 3,
}
}
#[getter]
fn max_combo(&self) -> usize {
match &self.inner {
DifficultyAttributes::Osu(attrs) => attrs.max_combo,
DifficultyAttributes::Taiko(attrs) => attrs.max_combo,
DifficultyAttributes::Catch(attrs) => attrs.max_combo(),
DifficultyAttributes::Mania(attrs) => attrs.max_combo,
}
}
#[getter]
fn color(&self) -> Option<f64> {
if let DifficultyAttributes::Taiko(ref attrs) = self.inner {
Some(attrs.colour)
} else {
None
}
}
fn __repr__(&self) -> String {
self.to_string()
}
$(
#[getter]
fn $field(&self) -> Option<$ty> {
match &self.inner {
$( DifficultyAttributes::$mode(attrs) => Some(attrs.$field), )*
#[allow(unreachable_patterns)]
_ => None,
}
}
)*
}
};
}
getters! {
stars as f64: (Osu, Taiko, Catch, Mania),
aim as f64: (Osu),
speed as f64: (Osu),
flashlight as f64: (Osu),
slider_factor as f64: (Osu),
speed_note_count as f64: (Osu),
od as f64: (Osu),
n_circles as usize: (Osu),
n_sliders as usize: (Osu),
n_spinners as usize: (Osu),
stamina as f64: (Taiko),
rhythm as f64: (Taiko),
peak as f64: (Taiko),
n_fruits as usize: (Catch),
n_droplets as usize: (Catch),
n_tiny_droplets as usize: (Catch),
ar as f64: (Osu, Catch),
hit_window as f64: (Taiko, Mania),
}
+24
View File
@@ -0,0 +1,24 @@
use std::{error::Error, fmt::Write};
use pyo3::{create_exception, exceptions::PyException};
create_exception!(rosu_pp_py, KwargsError, PyException);
create_exception!(rosu_pp_py, ParseError, PyException);
pub trait ErrorExt {
fn unwind(&self, cause: &str) -> String;
}
impl<T: Error> ErrorExt for T {
fn unwind(&self, cause: &str) -> String {
let mut e = self as &dyn Error;
let mut content = format!("{cause}\n - caused by: {e}");
while let Some(src) = e.source() {
let _ = write!(content, "\n - caused by: {src}");
e = src;
}
content
}
}
+15 -897
View File
@@ -1,903 +1,21 @@
use std::{
collections::HashMap,
error::Error as StdError,
fmt::{Display, Formatter, Result as FmtResult, Write},
hash::{Hash, Hasher},
};
use pyo3::{
basic::CompareOp,
exceptions::{PyException, PyNotImplementedError, PyTypeError},
prelude::*,
types::{PyDict, PyIterator, PyTuple},
};
use rosu_pp::{
beatmap::BeatmapAttributes, catch::CatchPerformanceAttributes,
mania::ManiaPerformanceAttributes, osu::OsuPerformanceAttributes,
taiko::TaikoPerformanceAttributes, AnyPP, Beatmap, BeatmapExt, GameMode, PerformanceAttributes,
Strains as RosuStrains,
};
#[pyclass]
struct Calculator(Beatmap);
#[pymethods]
impl Calculator {
#[new]
#[args(kwds = "**")]
fn new(path: &str, kwds: Option<&PyDict>) -> PyResult<Self> {
let mut ar = None;
let mut cs = None;
let mut hp = None;
let mut od = None;
if let Some(dict) = kwds {
for (key, value) in dict.iter() {
if let Ok(key) = key.extract() {
match key {
"ar" => ar = Some(value.extract()?),
"cs" => cs = Some(value.extract()?),
"hp" => hp = Some(value.extract()?),
"od" => od = Some(value.extract()?),
_ => {
return Err(PyTypeError::new_err(format!(
"got an unexpected keyword argument '{}'; \
expected 'ar', 'cs', 'hp', 'od'",
key,
)))
}
}
}
}
}
Beatmap::from_path(path)
.map(|mut map| {
if let Some(ar) = ar {
map.ar = ar;
}
if let Some(cs) = cs {
map.cs = cs;
}
if let Some(hp) = hp {
map.hp = hp;
}
if let Some(od) = od {
map.od = od;
}
Self(map)
})
.map_err(|e| unwind_error("Failed to parse beatmap", &e))
.map_err(PyException::new_err)
}
fn set_ar(&mut self, ar: f32) {
self.0.ar = ar;
}
fn set_cs(&mut self, cs: f32) {
self.0.cs = cs;
}
fn set_hp(&mut self, hp: f32) {
self.0.hp = hp;
}
fn set_od(&mut self, od: f32) {
self.0.od = od;
}
fn calculate(&mut self, obj: &PyAny) -> PyResult<Vec<CalculateResult>> {
match obj.extract::<ScoreParams>() {
Ok(params) => {
let mods = params.mods;
let clock_rate = params.clock_rate;
let calculator = params.apply(AnyPP::new(&self.0));
let result =
CalculateResult::new(calculator.calculate(), &self.0, mods, clock_rate);
Ok(vec![result])
}
Err(_) => {
let mut mod_diffs = HashMap::new();
PyIterator::from_object(obj.py(), obj)
.map_err(|_| {
let py_type = obj.get_type().name().unwrap_or("<unknown type>");
format!(
"got '{}'; expected 'ScoreParams' or 'Iterable[ScoreParams]'",
py_type
)
})
.map_err(PyTypeError::new_err)?
.map(|elem| {
let params: ScoreParams = elem?.extract()?;
let mods = params.mods;
let clock_rate = params.clock_rate;
let attr_key = params.as_attr_key();
let difficulty = mod_diffs
.entry(attr_key)
.or_insert_with(|| {
let mut calculator = self.0.stars().mods(mods);
if let Some(passed_objects) = params.passed_objects {
calculator = calculator.passed_objects(passed_objects);
}
if let Some(clock_rate) = params.clock_rate {
calculator = calculator.clock_rate(clock_rate);
}
calculator.calculate()
})
.to_owned();
let attrs = params
.apply(AnyPP::new(&self.0).attributes(difficulty))
.calculate();
Ok(CalculateResult::new(attrs, &self.0, mods, clock_rate))
})
.collect::<Result<Vec<_>, PyErr>>()
}
}
}
#[args(args = "*")]
fn strains(&mut self, args: &PyTuple) -> PyResult<Strains> {
let mods = if let Ok(obj) = args.get_item(0) {
if let Ok(mods) = obj.extract::<u32>() {
mods
} else {
let py_type = obj.get_type().name().unwrap_or("<unknown type>");
let err = format!("got '{}'; expected 'int'", py_type);
return Err(PyTypeError::new_err(err));
}
} else {
0
};
Ok(self.0.strains(mods).into())
}
}
#[pyclass]
#[derive(Clone, Default, PartialEq)]
#[allow(non_snake_case)]
struct Strains {
#[pyo3(get, set)]
sectionLength: f64,
#[pyo3(get, set)]
color: Option<Vec<f64>>,
#[pyo3(get, set)]
rhythm: Option<Vec<f64>>,
#[pyo3(get, set)]
staminaLeft: Option<Vec<f64>>,
#[pyo3(get, set)]
staminaRight: Option<Vec<f64>>,
#[pyo3(get, set)]
aim: Option<Vec<f64>>,
#[pyo3(get, set)]
aimNoSliders: Option<Vec<f64>>,
#[pyo3(get, set)]
speed: Option<Vec<f64>>,
#[pyo3(get, set)]
flashlight: Option<Vec<f64>>,
#[pyo3(get, set)]
strains: Option<Vec<f64>>,
#[pyo3(get, set)]
movement: Option<Vec<f64>>,
}
#[pymethods]
impl Strains {
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
match (other.extract::<Self>(), op) {
(Ok(ref other), CompareOp::Eq) => Ok(self == other),
(Ok(ref other), CompareOp::Ne) => Ok(self != other),
_ => Err(PyNotImplementedError::new_err("")),
}
}
fn __repr__(&self) -> PyResult<String> {
Ok(self.to_string())
}
}
impl From<RosuStrains> for Strains {
#[inline]
fn from(strains: RosuStrains) -> Self {
match strains {
RosuStrains::Catch(strains) => Self {
sectionLength: strains.section_len,
movement: Some(strains.movement),
..Default::default()
},
RosuStrains::Mania(strains) => Self {
sectionLength: strains.section_len,
strains: Some(strains.strains),
..Default::default()
},
RosuStrains::Osu(strains) => Self {
sectionLength: strains.section_len,
aim: Some(strains.aim),
aimNoSliders: Some(strains.aim_no_sliders),
speed: Some(strains.speed),
flashlight: Some(strains.flashlight),
..Default::default()
},
RosuStrains::Taiko(strains) => Self {
sectionLength: strains.section_len,
color: Some(strains.color),
rhythm: Some(strains.rhythm),
staminaLeft: Some(strains.stamina_left),
staminaRight: Some(strains.stamina_right),
..Default::default()
},
}
}
}
#[pyclass]
#[derive(Clone, Default, PartialEq)]
struct ScoreParams {
#[pyo3(get, set)]
mode: Option<u8>,
#[pyo3(get, set)]
mods: u32,
#[pyo3(get, set)]
n300: Option<usize>,
#[pyo3(get, set)]
n100: Option<usize>,
#[pyo3(get, set)]
n50: Option<usize>,
n_misses: Option<usize>,
n_katu: Option<usize>,
#[pyo3(get, set)]
acc: Option<f64>,
#[pyo3(get, set)]
combo: Option<usize>,
#[pyo3(get, set)]
score: Option<u32>,
passed_objects: Option<usize>,
clock_rate: Option<f64>,
}
#[pyclass]
#[derive(Clone, Default, PartialEq)]
#[allow(non_snake_case)]
struct CalculateResult {
#[pyo3(get, set)]
mode: u8,
#[pyo3(get, set)]
stars: f64,
#[pyo3(get, set)]
pp: f64,
#[pyo3(get, set)]
ppAcc: Option<f64>,
#[pyo3(get, set)]
ppAim: Option<f64>,
#[pyo3(get, set)]
ppFlashlight: Option<f64>,
#[pyo3(get, set)]
ppSpeed: Option<f64>,
#[pyo3(get, set)]
ppStrain: Option<f64>,
#[pyo3(get, set)]
nFruits: Option<usize>,
#[pyo3(get, set)]
nDroplets: Option<usize>,
#[pyo3(get, set)]
nTinyDroplets: Option<usize>,
#[pyo3(get, set)]
aimStrain: Option<f64>,
#[pyo3(get, set)]
speedStrain: Option<f64>,
#[pyo3(get, set)]
flashlightRating: Option<f64>,
#[pyo3(get, set)]
sliderFactor: Option<f64>,
#[pyo3(get, set)]
ar: f64,
#[pyo3(get, set)]
cs: f64,
#[pyo3(get, set)]
hp: f64,
#[pyo3(get, set)]
od: f64,
#[pyo3(get, set)]
bpm: f64,
#[pyo3(get, set)]
clockRate: f64,
#[pyo3(get, set)]
timePreempt: Option<f64>,
#[pyo3(get, set)]
greatHitWindow: Option<f64>,
#[pyo3(get, set)]
nCircles: Option<usize>,
#[pyo3(get, set)]
nSliders: Option<usize>,
#[pyo3(get, set)]
nSpinners: Option<usize>,
#[pyo3(get, set)]
maxCombo: Option<usize>,
}
impl CalculateResult {
fn new(
attrs: PerformanceAttributes,
map: &Beatmap,
mods: u32,
clock_rate: Option<f64>,
) -> Self {
let mut attr_builder = map.attributes();
if let Some(clock_rate) = clock_rate {
attr_builder.clock_rate(clock_rate);
}
let mode = match &attrs {
PerformanceAttributes::Catch(_) => GameMode::Catch,
PerformanceAttributes::Mania(_) => GameMode::Mania,
PerformanceAttributes::Osu(_) => GameMode::Osu,
PerformanceAttributes::Taiko(_) => GameMode::Taiko,
};
attr_builder.converted(map.mode == GameMode::Osu && mode != GameMode::Osu);
let BeatmapAttributes {
ar,
cs,
hp,
od,
clock_rate,
hit_windows,
} = attr_builder.mods(mods).mode(mode).build();
let bpm = map.bpm() * clock_rate;
match attrs {
PerformanceAttributes::Catch(CatchPerformanceAttributes { pp, difficulty }) => Self {
mode: 2,
pp,
stars: difficulty.stars,
maxCombo: Some(difficulty.n_fruits + difficulty.n_droplets),
nFruits: Some(difficulty.n_fruits),
nDroplets: Some(difficulty.n_droplets),
nTinyDroplets: Some(difficulty.n_tiny_droplets),
nSpinners: Some(map.n_spinners as usize),
ar,
cs,
hp,
od,
bpm,
clockRate: clock_rate,
..Default::default()
},
PerformanceAttributes::Mania(ManiaPerformanceAttributes {
pp,
pp_acc,
pp_strain,
difficulty,
}) => Self {
mode: 3,
pp,
ppAcc: Some(pp_acc),
ppStrain: Some(pp_strain),
stars: difficulty.stars,
nCircles: Some(map.n_circles as usize),
nSliders: Some(map.n_sliders as usize),
ar,
cs,
hp,
od,
bpm,
clockRate: clock_rate,
greatHitWindow: Some(hit_windows.od),
..Default::default()
},
PerformanceAttributes::Osu(OsuPerformanceAttributes {
pp,
pp_acc,
pp_aim,
pp_flashlight,
pp_speed,
difficulty,
}) => Self {
mode: 0,
pp,
ppAcc: Some(pp_acc),
ppAim: Some(pp_aim),
ppFlashlight: Some(pp_flashlight),
ppSpeed: Some(pp_speed),
stars: difficulty.stars,
maxCombo: Some(difficulty.max_combo),
aimStrain: Some(difficulty.aim_strain),
speedStrain: Some(difficulty.speed_strain),
flashlightRating: Some(difficulty.flashlight_rating),
sliderFactor: Some(difficulty.slider_factor),
nCircles: Some(difficulty.n_circles),
nSliders: Some(difficulty.n_sliders),
nSpinners: Some(difficulty.n_spinners),
ar,
cs,
hp,
od,
bpm,
clockRate: clock_rate,
timePreempt: Some(hit_windows.ar),
greatHitWindow: Some(hit_windows.od),
..Default::default()
},
PerformanceAttributes::Taiko(TaikoPerformanceAttributes {
pp,
pp_acc,
pp_strain,
difficulty,
}) => Self {
mode: 1,
pp,
ppAcc: Some(pp_acc),
ppStrain: Some(pp_strain),
stars: difficulty.stars,
maxCombo: Some(difficulty.max_combo),
nCircles: Some(map.n_circles as usize),
nSliders: Some(map.n_sliders as usize),
nSpinners: Some(map.n_spinners as usize),
ar,
cs,
hp,
od,
bpm,
clockRate: clock_rate,
greatHitWindow: Some(hit_windows.od),
..Default::default()
},
}
}
}
#[pymethods]
impl CalculateResult {
#[new]
fn new_() -> Self {
Self::default()
}
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
match (other.extract::<Self>(), op) {
(Ok(ref other), CompareOp::Eq) => Ok(self == other),
(Ok(ref other), CompareOp::Ne) => Ok(self != other),
_ => Err(PyNotImplementedError::new_err("")),
}
}
fn __repr__(&self) -> PyResult<String> {
Ok(self.to_string())
}
}
fn unwind_error(cause: &str, mut e: &dyn StdError) -> String {
let mut content = format!("{}: {}\n", cause, e);
while let Some(src) = e.source() {
let _ = writeln!(content, " - caused by: {}", src);
e = src;
}
content
}
impl ScoreParams {
fn as_attr_key(&self) -> AttributeKey {
AttributeKey {
mods: self.mods,
passed_objects: self.passed_objects,
clock_rate: self.clock_rate,
}
}
fn apply(self, mut calculator: AnyPP) -> AnyPP {
let ScoreParams {
mode,
mods,
n300,
n100,
n50,
n_misses,
n_katu,
acc,
combo,
score,
passed_objects,
clock_rate,
} = self;
if let Some(mode @ 0..=3) = mode {
let mode = match mode {
0 => GameMode::Osu,
1 => GameMode::Taiko,
2 => GameMode::Catch,
3 => GameMode::Mania,
_ => unreachable!(),
};
calculator = calculator.mode(mode);
}
if let Some(n300) = n300 {
calculator = calculator.n300(n300);
}
if let Some(n100) = n100 {
calculator = calculator.n100(n100);
}
if let Some(n50) = n50 {
calculator = calculator.n50(n50);
}
if let Some(n_misses) = n_misses {
calculator = calculator.misses(n_misses);
}
if let Some(n_katu) = n_katu {
calculator = calculator.n_katu(n_katu);
}
if let Some(combo) = combo {
calculator = calculator.combo(combo);
}
if let Some(passed_objects) = passed_objects {
calculator = calculator.passed_objects(passed_objects);
}
if let Some(clock_rate) = clock_rate {
calculator = calculator.clock_rate(clock_rate);
}
calculator = calculator.mods(mods);
if let Some(acc) = acc {
calculator = calculator.accuracy(acc);
}
if let Some(score) = score {
calculator = calculator.score(score);
}
calculator
}
}
#[pymethods]
impl ScoreParams {
#[new]
#[args(kwds = "**")]
fn new(kwds: Option<&PyDict>) -> PyResult<Self> {
let mut params = Self::default();
if let Some(dict) = kwds {
for (key, value) in dict.iter() {
if let Ok(key) = key.extract() {
match key {
"mode" => params.mode = Some(value.extract()?),
"mods" => params.mods = value.extract()?,
"n300" => params.n300 = value.extract()?,
"n100" => params.n100 = value.extract()?,
"n50" => params.n50 = value.extract()?,
"nMisses" => params.n_misses = value.extract()?,
"nKatu" => params.n_katu = value.extract()?,
"acc" => params.acc = value.extract()?,
"combo" => params.combo = value.extract()?,
"score" => params.score = value.extract()?,
"passedObjects" => params.passed_objects = value.extract()?,
"clockRate" => params.clock_rate = value.extract()?,
_ => {
return Err(PyTypeError::new_err(format!(
"got an unexpected keyword argument '{}'; expected 'mode', 'mods', 'n300', 'n100', \
'n50', 'nMisses', 'nKatu', 'acc', 'combo', 'score', 'passedObjects', 'clockRate'",
key,
)))
}
}
}
}
}
Ok(params)
}
#[getter(nMisses)]
fn n_misses(&self) -> Option<usize> {
self.n_misses
}
#[setter(nMisses)]
fn set_n_misses(&mut self, n_misses: usize) {
self.n_misses = Some(n_misses);
}
#[getter(nKatu)]
fn n_katu(&self) -> Option<usize> {
self.n_katu
}
#[setter(nKatu)]
fn set_n_katu(&mut self, n_katu: usize) {
self.n_katu = Some(n_katu);
}
#[getter(passedObjects)]
fn passed_objects(&self) -> Option<usize> {
self.passed_objects
}
#[setter(passedObjects)]
fn set_passed_objects(&mut self, passed_objects: usize) {
self.passed_objects = Some(passed_objects);
}
#[getter(clockRate)]
fn clock_rate(&self) -> Option<f64> {
self.clock_rate
}
#[setter(clockRate)]
fn set_clock_rate(&mut self, clock_rate: f64) {
self.clock_rate = Some(clock_rate);
}
fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<bool> {
match (other.extract::<Self>(), op) {
(Ok(ref other), CompareOp::Eq) => Ok(self == other),
(Ok(ref other), CompareOp::Ne) => Ok(self != other),
_ => Err(PyNotImplementedError::new_err("")),
}
}
fn __repr__(&self) -> PyResult<String> {
Ok(self.to_string())
}
}
impl Display for Strains {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut s = f.debug_struct("Strains");
s.field("sectionLength", &self.sectionLength);
macro_rules! display_field {
($self:ident, $s:ident: $($field:ident,)*) => {
$(
if let Some(ref field) = $self.$field {
$s.field(stringify!($field), field);
}
)*
}
}
display_field! {
self, s:
color,
rhythm,
staminaLeft,
staminaRight,
aim,
aimNoSliders,
speed,
flashlight,
strains,
movement,
}
s.finish()
}
}
impl Display for CalculateResult {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut s = f.debug_struct("CalculateResult");
s.field("mode", &self.mode)
.field("stars", &self.stars)
.field("pp", &self.pp);
if let Some(ref pp_acc) = self.ppAcc {
s.field("ppAcc", pp_acc);
}
if let Some(ref pp_aim) = self.ppAim {
s.field("ppAim", pp_aim);
}
if let Some(ref pp_flashlight) = self.ppFlashlight {
s.field("ppFlashlight", pp_flashlight);
}
if let Some(ref pp_speed) = self.ppSpeed {
s.field("ppSpeed", pp_speed);
}
if let Some(ref pp_strain) = self.ppStrain {
s.field("ppStrain", pp_strain);
}
if let Some(ref n_fruits) = self.nFruits {
s.field("nFruits", n_fruits);
}
if let Some(ref n_droplets) = self.nDroplets {
s.field("nDroplets", n_droplets);
}
if let Some(ref n_tiny_droplets) = self.nTinyDroplets {
s.field("nTinyDroplets", n_tiny_droplets);
}
if let Some(ref aim_strain) = self.aimStrain {
s.field("aimStrain", aim_strain);
}
if let Some(ref speed_strain) = self.speedStrain {
s.field("speedStrain", speed_strain);
}
if let Some(ref flashlight_rating) = self.flashlightRating {
s.field("flashlightRating", flashlight_rating);
}
if let Some(ref slider_factor) = self.sliderFactor {
s.field("sliderFactor", slider_factor);
}
s.field("ar", &self.ar)
.field("cs", &self.cs)
.field("hp", &self.hp)
.field("od", &self.od)
.field("bpm", &self.bpm)
.field("clockRate", &self.clockRate);
if let Some(ref time_preempt) = self.timePreempt {
s.field("timePreempt", time_preempt);
}
if let Some(ref great_hit_window) = self.greatHitWindow {
s.field("greatHitWindow", great_hit_window);
}
if let Some(ref n_circles) = self.nCircles {
s.field("nCircles", n_circles);
}
if let Some(ref n_sliders) = self.nSliders {
s.field("nSliders", n_sliders);
}
if let Some(ref n_spinners) = self.nSpinners {
s.field("nSpinners", n_spinners);
}
if let Some(ref combo) = self.maxCombo {
s.field("maxCombo", combo);
}
s.finish()
}
}
impl Display for ScoreParams {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(
f,
"ScoreParams {{ \
mode: {}, \
mods: {}, \
n300: {}, \
n100: {}, \
n50: {}, \
nMisses: {}, \
nKatu: {}, \
acc: {}, \
combo: {}, \
score: {}, \
passedObjects: {}, \
clockRate: {} \
}}",
match self.mode {
Some(ref mode) => mode as &dyn Display,
None => &"None" as &dyn Display,
},
self.mods,
match self.n300 {
Some(ref n300) => n300 as &dyn Display,
None => &"None" as &dyn Display,
},
match self.n100 {
Some(ref n100) => n100 as &dyn Display,
None => &"None" as &dyn Display,
},
match self.n50 {
Some(ref n50) => n50 as &dyn Display,
None => &"None" as &dyn Display,
},
match self.n_misses {
Some(ref n_misses) => n_misses as &dyn Display,
None => &"None" as &dyn Display,
},
match self.n_katu {
Some(ref n_katu) => n_katu as &dyn Display,
None => &"None" as &dyn Display,
},
match self.acc {
Some(ref acc) => acc as &dyn Display,
None => &"None" as &dyn Display,
},
match self.combo {
Some(ref combo) => combo as &dyn Display,
None => &"None" as &dyn Display,
},
match self.score {
Some(ref score) => score as &dyn Display,
None => &"None" as &dyn Display,
},
match self.passed_objects {
Some(ref passed_objects) => passed_objects as &dyn Display,
None => &"None" as &dyn Display,
},
match self.clock_rate {
Some(ref clock_rate) => clock_rate as &dyn Display,
None => &"None" as &dyn Display,
},
)
}
}
#![deny(clippy::all, nonstandard_style, rust_2018_idioms, unused, warnings)]
use beatmap::PyBeatmap as Beatmap;
use calculator::Calculator;
use pyo3::{pymodule, types::PyModule, PyResult, Python};
mod beatmap;
mod calculator;
mod diff_attrs;
mod error;
mod map_attrs;
mod perf_attrs;
mod strains;
#[pymodule]
fn rosu_pp_py(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<ScoreParams>()?;
fn rosu_pp_py(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_class::<Beatmap>()?;
m.add_class::<Calculator>()?;
m.add_class::<CalculateResult>()?;
m.add_class::<Strains>()?;
Ok(())
}
struct AttributeKey {
mods: u32,
passed_objects: Option<usize>,
clock_rate: Option<f64>,
}
impl Hash for AttributeKey {
#[inline]
fn hash<H: Hasher>(&self, state: &mut H) {
self.mods.hash(state);
self.passed_objects.hash(state);
(&self.clock_rate as *const _ as *const Option<u64>).hash(state);
}
}
impl PartialEq for AttributeKey {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.mods == other.mods
&& self.passed_objects == other.passed_objects
&& self.clock_rate == other.clock_rate
}
}
impl Eq for AttributeKey {}
+69
View File
@@ -0,0 +1,69 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{pyclass, pymethods};
use rosu_pp::{beatmap::BeatmapAttributes, Beatmap};
#[pyclass(name = "BeatmapAttributes")]
#[derive(Debug)]
pub struct PyBeatmapAttributes {
#[pyo3(get)]
ar: f64,
#[pyo3(get)]
cs: f64,
#[pyo3(get)]
hp: f64,
#[pyo3(get)]
od: f64,
#[pyo3(get)]
ar_hit_window: f64,
#[pyo3(get)]
od_hit_window: f64,
#[pyo3(get)]
clock_rate: f64,
#[pyo3(get)]
bpm: f64,
#[pyo3(get)]
mode: u8,
#[pyo3(get)]
version: u8,
#[pyo3(get)]
n_circles: u32,
#[pyo3(get)]
n_sliders: u32,
#[pyo3(get)]
n_spinners: u32,
}
impl PyBeatmapAttributes {
pub fn new(attrs: BeatmapAttributes, map: &Beatmap) -> Self {
Self {
ar: attrs.ar,
cs: attrs.cs,
hp: attrs.hp,
od: attrs.od,
ar_hit_window: attrs.hit_windows.ar,
od_hit_window: attrs.hit_windows.od,
clock_rate: attrs.clock_rate,
bpm: map.bpm() * attrs.clock_rate,
mode: map.mode as u8,
version: map.version,
n_circles: map.n_circles,
n_sliders: map.n_sliders,
n_spinners: map.n_spinners,
}
}
}
impl Display for PyBeatmapAttributes {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
<Self as Debug>::fmt(self, f)
}
}
#[pymethods]
impl PyBeatmapAttributes {
fn __repr__(&self) -> String {
self.to_string()
}
}
+145
View File
@@ -0,0 +1,145 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{pyclass, pymethods};
use rosu_pp::{
catch::CatchPerformanceAttributes, mania::ManiaPerformanceAttributes,
osu::OsuPerformanceAttributes, taiko::TaikoPerformanceAttributes, PerformanceAttributes,
};
use crate::diff_attrs::{BorrowedDifficultyAttributes, PyDifficultyAttributes};
#[pyclass(name = "PerformanceAttributes")]
#[derive(Debug)]
pub struct PyPerformanceAttributes {
inner: PerformanceAttributes,
}
impl From<PerformanceAttributes> for PyPerformanceAttributes {
#[inline]
fn from(attrs: PerformanceAttributes) -> Self {
Self { inner: attrs }
}
}
impl Display for PyPerformanceAttributes {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut debug = f.debug_struct("PerformanceAttributes");
macro_rules! debug {
( $( $field:ident $( , )? )* ) => {
debug $( .field(stringify!($field), $field) )*;
}
}
match &self.inner {
PerformanceAttributes::Osu(attrs) => {
let OsuPerformanceAttributes {
difficulty,
pp,
pp_acc,
pp_aim,
pp_flashlight,
pp_speed,
effective_miss_count,
} = attrs;
let borrowed = BorrowedDifficultyAttributes::from(difficulty);
debug.field("mode", &0_u8).field("difficulty", &borrowed);
debug! {
pp,
pp_acc,
pp_aim,
pp_flashlight,
pp_speed,
effective_miss_count,
};
}
PerformanceAttributes::Taiko(attrs) => {
let TaikoPerformanceAttributes {
difficulty,
pp,
pp_acc,
pp_difficulty,
effective_miss_count,
} = attrs;
let borrowed = BorrowedDifficultyAttributes::from(difficulty);
debug.field("mode", &1_u8).field("difficulty", &borrowed);
debug!(pp, pp_acc, pp_difficulty, effective_miss_count);
}
PerformanceAttributes::Catch(attrs) => {
let CatchPerformanceAttributes { difficulty, pp } = attrs;
let borrowed = BorrowedDifficultyAttributes::from(difficulty);
debug.field("mode", &2_u8).field("difficulty", &borrowed);
debug!(pp);
}
PerformanceAttributes::Mania(attrs) => {
let ManiaPerformanceAttributes {
difficulty,
pp,
pp_difficulty,
} = attrs;
let borrowed = BorrowedDifficultyAttributes::from(difficulty);
debug.field("mode", &3_u8).field("difficulty", &borrowed);
debug!(pp, pp_difficulty);
}
}
debug.finish()
}
}
macro_rules! getters {
(
$(
$field:ident: ( $( $mode:ident ),* ),
)*
) => {
#[pymethods]
impl PyPerformanceAttributes {
#[getter]
fn mode(&self) -> u8 {
match self.inner {
PerformanceAttributes::Osu(_) => 0,
PerformanceAttributes::Taiko(_) => 1,
PerformanceAttributes::Catch(_) => 2,
PerformanceAttributes::Mania(_) => 3,
}
}
#[getter]
fn difficulty(&self) -> PyDifficultyAttributes {
self.inner.difficulty_attributes().into()
}
fn __repr__(&self) -> String {
self.to_string()
}
$(
#[getter]
fn $field(&self) -> Option<f64> {
match &self.inner {
$( PerformanceAttributes::$mode(attrs) => Some(attrs.$field), )*
#[allow(unreachable_patterns)]
_ => None,
}
}
)*
}
};
}
getters! {
pp: (Osu, Taiko, Catch, Mania),
pp_aim: (Osu),
pp_flashlight: (Osu),
pp_speed: (Osu),
pp_acc: (Osu, Taiko),
effective_miss_count: (Osu, Taiko),
pp_difficulty: (Taiko, Mania),
}
+174
View File
@@ -0,0 +1,174 @@
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use pyo3::{ffi, pyclass, pymethods, types::PyList, IntoPy, IntoPyPointer, Py, PyObject, Python};
use rosu_pp::{
catch::CatchStrains, mania::ManiaStrains, osu::OsuStrains, taiko::TaikoStrains, Strains,
};
#[pyclass(name = "Strains")]
#[derive(Debug)]
pub struct PyStrains {
inner: Strains,
}
impl From<Strains> for PyStrains {
#[inline]
fn from(strains: Strains) -> Self {
Self { inner: strains }
}
}
impl Display for PyStrains {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut debug = f.debug_struct("Strains");
macro_rules! debug {
( $( $field:ident $( , )? )* ) => {
debug $( .field(stringify!($field), $field) )*;
}
}
match &self.inner {
Strains::Osu(strains) => {
let OsuStrains {
section_len,
aim,
aim_no_sliders,
speed,
flashlight,
} = strains;
debug.field("mode", &0_u8);
debug!(section_len, aim, aim_no_sliders, speed, flashlight);
}
Strains::Taiko(strains) => {
let TaikoStrains {
section_len,
color,
rhythm,
stamina,
} = strains;
debug.field("mode", &1_u8);
debug!(section_len, color, rhythm, stamina);
}
Strains::Catch(strains) => {
let CatchStrains {
section_len,
movement,
} = strains;
debug.field("mode", &2_u8);
debug!(section_len, movement);
}
Strains::Mania(strains) => {
let ManiaStrains {
section_len,
strains,
} = strains;
debug.field("mode", &3_u8);
debug!(section_len, strains);
}
}
debug.finish()
}
}
macro_rules! getters {
(
$(
$mode:ident {
$( $field:ident ,)*
},
)*
) => {
#[pymethods]
impl PyStrains {
#[getter]
fn mode(&self) -> u8 {
match self.inner {
Strains::Osu(_) => 0,
Strains::Taiko(_) => 1,
Strains::Catch(_) => 2,
Strains::Mania(_) => 3,
}
}
#[getter]
fn section_len(&self) -> f64 {
match &self.inner {
Strains::Osu(strains) => strains.section_len,
Strains::Taiko(strains) => strains.section_len,
Strains::Catch(strains) => strains.section_len,
Strains::Mania(strains) => strains.section_len,
}
}
fn __repr__(&self) -> String {
self.to_string()
}
$(
$(
#[getter]
fn $field(&self) -> Option<SliceWrapper<'_>> {
if let Strains::$mode(ref attrs) = self.inner {
Some(SliceWrapper(&attrs.$field))
} else {
None
}
}
)*
)*
}
};
}
getters! {
Osu {
aim,
aim_no_sliders,
speed,
flashlight,
},
Taiko {
color,
stamina,
rhythm,
},
Catch {
movement,
},
Mania {
strains,
},
}
struct SliceWrapper<'i>(&'i [f64]);
impl IntoPy<PyObject> for SliceWrapper<'_> {
#[inline]
fn into_py(self, py: Python<'_>) -> PyObject {
let iter = self.0.iter().map(|e| e.into_py(py));
let len = self.0.len() as ffi::Py_ssize_t;
// SAFETY: analogous code to pyo3's `IntoPy` impl for `Vec<T>`
// https://github.com/PyO3/pyo3/blob/d7b05cbcf5785019a097e496454b924f3e11d94f/src/types/list.rs#L21-L54
unsafe {
let ptr = ffi::PyList_New(len);
let list: Py<PyList> = Py::from_owned_ptr(py, ptr);
for (item, i) in iter.zip(0..) {
#[cfg(not(Py_LIMITED_API))]
ffi::PyList_SET_ITEM(ptr, i, item.into_ptr());
#[cfg(Py_LIMITED_API)]
ffi::PyList_SetItem(ptr, i, obj.into_ptr());
}
list.into()
}
}
}