Skip to content

Commit

Permalink
feat: use rosu-mods (#9)
Browse files Browse the repository at this point in the history
* use rosu-mods

* mention mods in readme

* use published rosu-pp

* remove test remnants
  • Loading branch information
MaxOhn authored Jul 12, 2024
1 parent 4d29dd7 commit bf6e57a
Show file tree
Hide file tree
Showing 10 changed files with 889 additions and 29 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/target
expanded.rs
output.rs

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
36 changes: 34 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ crate-type = ["cdylib"]

[dependencies]
pyo3 = { version = "0.22", features = ["extension-module", "macros"] }
rosu-mods = { version = "0.1.0", default-features = false, features = ["serde"] }
rosu-pp = { version = "1.0.0", features = ["sync"] }
serde = "1.0.203"

[profile.release]
lto = true
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,44 @@ while True:
i += 1
```

## Mods

Wherever mods are specified, their type should coincide with the following alias definition:
```py
GameMods = Union[int, str, GameMod, List[Union[GameMod, str, int]]]
GameMod = dict[str, Union[str, GameModSettings]]
GameModSettings = dict[str, Union[bool, float, str]]
```

That means, mods can be given either through their [(legacy) bitflags](https://github.com/ppy/osu-api/wiki#reference),
a string for acronyms, a "GameMod" `dict`, or a sequence whose items are either
a "GameMod" `dict`, a single acronym string, or bitflags for a single mod.

A "GameMod" `dict` **must** have the item `'acronym': str` and an optional item `'settings': GameModSettings`.

Some examples for valid mods look as follows:

```py
mods = 8 + 64 # Hidden, DoubleTime
mods = "hRNcWIez" # HardRock, Nightcore, Wiggle, Easy
mods = { 'acronym': "FI" } # FadeIn
mods = [
1024,
'nf',
{
'acronym': "AC",
'settings': {
'minimum_accuracy': 95,
'restart': True
}
}
] # Flashlight, NoFail, AccuracyChallenge

import json
mods_json = '[{"acronym": "TC"}, {"acronym": "HT", "settings": {"speed_change": 0.6}}]'
mods = json.loads(mods_json) # Traceable, HalfTime
```

## Installing rosu-pp-py

Installing rosu-pp-py requires a [supported version of Python and Rust](https://github.com/PyO3/PyO3#usage).
Expand Down
52 changes: 43 additions & 9 deletions rosu_pp_py.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ from enum import Enum
from typing import List, Optional, Union
from collections.abc import Iterator

GameMods = Union[int, str, GameMod, List[Union[GameMod, str, int]]]
GameMod = dict[str, Union[str, GameModSettings]]
"""
Must contain item `'acronym': str` and optionally `'settings': GameModSettings`
"""
GameModSettings = dict[str, Union[bool, float, str]]

class GameMode(Enum):
"""
Enum for a beatmap's gamemode
Expand Down Expand Up @@ -105,8 +112,17 @@ class Difficulty:
Builder for a difficulty calculation
The kwargs may include any of the following:
`'mods': int`
Specify mods through their bit values.
`'mods': GameMods`
Specify mods.
Relevant type aliases:
`GameMods = Union[int, str, GameMod, List[Union[GameMod, str, int]]]`
`GameMod = dict[str, Union[str, GameModSettings]]`
`GameMod` *must* have an item `'acronym': str` and an optional
item `'settings': GameModSettings`
`GameModSettings = dict[str, Union[bool, float, str]]`
See https://github.com/ppy/osu-api/wiki#mods
`'clock_rate': float`
Expand Down Expand Up @@ -194,7 +210,7 @@ class Difficulty:
Returns a gradual performance calculator for the current difficulty settings
"""

def set_mods(self, mods: Optional[int]) -> None: ...
def set_mods(self, mods: Optional[GameMods]) -> None: ...

def set_clock_rate(self, clock_rate: Optional[float]) -> None: ...

Expand Down Expand Up @@ -255,8 +271,17 @@ class Performance:
Builder for a performance calculation
The kwargs may include any of the following:
`'mods': int`
Specify mods through their bit values.
`'mods': GameMods`
Specify mods.
Relevant type aliases:
`GameMods = Union[int, str, GameMod, List[Union[GameMod, str, int]]]`
`GameMod = dict[str, Union[str, GameModSettings]]`
`GameMod` *must* have an item `'acronym': str` and an optional
item `'settings': GameModSettings`
`GameModSettings = dict[str, Union[bool, float, str]]`
See https://github.com/ppy/osu-api/wiki#mods
`'clock_rate': float`
Expand Down Expand Up @@ -364,7 +389,7 @@ class Performance:
Use the current difficulty settings to create a difficulty calculator
"""

def set_mods(self, mods: Optional[int]) -> None: ...
def set_mods(self, mods: Optional[GameMods]) -> None: ...

def set_clock_rate(self, clock_rate: Optional[float]) -> None: ...

Expand Down Expand Up @@ -501,8 +526,17 @@ class BeatmapAttributesBuilder:
Specify a gamemode
`'is_convert': bool`
Specify whether it's a converted map
`'mods': int`
Specify mods through their bit values.
`'mods': GameMods`
Specify mods.
Relevant type aliases:
`GameMods = Union[int, str, GameMod, List[Union[GameMod, str, int]]]`
`GameMod = dict[str, Union[str, GameModSettings]]`
`GameMod` *must* have an item `'acronym': str` and an optional
item `'settings': GameModSettings`
`GameModSettings = dict[str, Union[bool, float, str]]`
See https://github.com/ppy/osu-api/wiki#mods
`'clock_rate': float`
Expand Down Expand Up @@ -564,7 +598,7 @@ class BeatmapAttributesBuilder:

def set_mode(self, mode: Optional[GameMode], is_convert: bool) -> None: ...

def set_mods(self, mods: Optional[int]) -> None: ...
def set_mods(self, mods: Optional[GameMods]) -> None: ...

def set_clock_rate(self, clock_rate: Optional[float]) -> None: ...

Expand Down
18 changes: 12 additions & 6 deletions src/attributes/beatmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ use pyo3::{
};
use rosu_pp::model::beatmap::{BeatmapAttributes, BeatmapAttributesBuilder, HitWindows};

use crate::{beatmap::PyBeatmap, error::ArgsError, mode::PyGameMode};
use crate::{beatmap::PyBeatmap, error::ArgsError, mode::PyGameMode, mods::PyGameMods};

#[pyclass(name = "BeatmapAttributesBuilder")]
#[derive(Default)]
pub struct PyBeatmapAttributesBuilder {
mode: Option<PyGameMode>,
is_convert: bool,
mods: u32,
mods: PyGameMods,
clock_rate: Option<f64>,
ar: Option<f32>,
ar_with_mods: bool,
Expand Down Expand Up @@ -59,7 +59,7 @@ impl PyBeatmapAttributesBuilder {
"mods" => {
this.mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'mods': must be an int"))?
.map_err(|_| PyTypeError::new_err("kwarg 'mods': must be GameMods"))?
}
"clock_rate" => {
this.clock_rate =
Expand Down Expand Up @@ -132,7 +132,13 @@ impl PyBeatmapAttributesBuilder {
}

fn build(&self) -> PyBeatmapAttributes {
let mut builder = BeatmapAttributesBuilder::new().mods(self.mods);
let mut builder = BeatmapAttributesBuilder::new();

builder = match self.mods {
PyGameMods::Lazer(ref mods) => builder.mods(mods.clone()),
PyGameMods::Intermode(ref mods) => builder.mods(mods),
PyGameMods::Legacy(mods) => builder.mods(mods),
};

if let Some(mode) = self.mode {
builder = builder.mode(mode.into(), self.is_convert);
Expand Down Expand Up @@ -179,8 +185,8 @@ impl PyBeatmapAttributesBuilder {
}

#[pyo3(signature = (mods=None))]
fn set_mods(&mut self, mods: Option<u32>) {
self.mods = mods.unwrap_or(0);
fn set_mods(&mut self, mods: Option<PyGameMods>) {
self.mods = mods.unwrap_or_default();
}

#[pyo3(signature = (clock_rate=None))]
Expand Down
19 changes: 13 additions & 6 deletions src/difficulty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ use crate::{
beatmap::PyBeatmap,
error::ArgsError,
gradual::{difficulty::PyGradualDifficulty, performance::PyGradualPerformance},
mods::PyGameMods,
performance::PyPerformance,
strains::PyStrains,
};

#[pyclass(name = "Difficulty")]
#[derive(Default)]
pub struct PyDifficulty {
pub(crate) mods: u32,
pub(crate) mods: PyGameMods,
pub(crate) clock_rate: Option<f64>,
pub(crate) ar: Option<f32>,
pub(crate) ar_with_mods: bool,
Expand Down Expand Up @@ -48,7 +49,7 @@ impl PyDifficulty {
"mods" => {
this.mods = value
.extract()
.map_err(|_| PyTypeError::new_err("kwarg 'mods': must be an int"))?
.map_err(|_| PyTypeError::new_err("kwarg 'mods': must be GameMods"))?
}
"clock_rate" => {
this.clock_rate =
Expand Down Expand Up @@ -155,7 +156,7 @@ impl PyDifficulty {
} = self;

PyPerformance {
mods: *mods,
mods: mods.clone(),
clock_rate: *clock_rate,
ar: *ar,
ar_with_mods: *ar_with_mods,
Expand All @@ -180,8 +181,8 @@ impl PyDifficulty {
}

#[pyo3(signature = (mods=None))]
fn set_mods(&mut self, mods: Option<u32>) {
self.mods = mods.unwrap_or(0);
fn set_mods(&mut self, mods: Option<PyGameMods>) {
self.mods = mods.unwrap_or_default();
}

#[pyo3(signature = (clock_rate=None))]
Expand Down Expand Up @@ -226,7 +227,13 @@ impl PyDifficulty {

impl PyDifficulty {
pub fn as_difficulty(&self) -> Difficulty {
let mut difficulty = Difficulty::new().mods(self.mods);
let mut difficulty = Difficulty::new();

difficulty = match self.mods {
PyGameMods::Lazer(ref mods) => difficulty.mods(mods.clone()),
PyGameMods::Intermode(ref mods) => difficulty.mods(mods),
PyGameMods::Legacy(mods) => difficulty.mods(mods),
};

if let Some(passed_objects) = self.passed_objects {
difficulty = difficulty.passed_objects(passed_objects);
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ mod difficulty;
mod error;
mod gradual;
mod mode;
mod mods;
mod performance;
mod score_state;
mod strains;
Expand Down
Loading

0 comments on commit bf6e57a

Please sign in to comment.