rosu_memory_lib/reader/beatmap/stable/
file.rs

1use std::path::PathBuf;
2
3use crate::common::GameMode;
4use crate::reader::beatmap::common::{
5    BeatmapInfo, BeatmapLocation, BeatmapMetadata, BeatmapStarRating, BeatmapStats, BeatmapStatus,
6    BeatmapTechnicalInfo,
7};
8use crate::reader::beatmap::stable::memory::{audio, filename, folder};
9use crate::reader::beatmap::stable::{beatmap_addr, offset::BEATMAP_OFFSET};
10use crate::reader::common::stable::memory::path_folder;
11use crate::reader::structs::State;
12use crate::Error;
13use rosu_map::section::hit_objects::HitObjectKind;
14use rosu_map::Beatmap as RmBeatmap;
15use rosu_mem::process::{Process, ProcessTraits};
16
17pub fn path(p: &Process, state: &mut State) -> Result<PathBuf, Error> {
18    let folder = folder(p, state)?;
19    let filename = filename(p, state)?;
20    let songs_path = path_folder(p, state)?;
21
22    Ok(songs_path.join(folder).join(filename))
23}
24
25pub fn audio_path(p: &Process, state: &mut State) -> Result<PathBuf, Error> {
26    let folder = folder(p, state)?;
27    let audio = audio(p, state)?;
28    let songs_path = path_folder(p, state)?;
29
30    Ok(songs_path.join(folder).join(audio))
31}
32
33// generate getters that use the default logic
34macro_rules! generate_beatmap_field_getter {
35    (
36        $( $fn_name:ident : $ret_ty:ty = $field:ident ; )*
37    ) => {
38        $(
39            pub fn $fn_name(p: &Process, state: &mut State) -> Result<$ret_ty, Error> {
40                let path = path(p, state)?;
41                let b = RmBeatmap::from_path(path)?;
42                Ok(b.$field)
43            }
44        )*
45    };
46}
47// generate getters that use custom logic
48macro_rules! generate_beatmap_custom_getter_safe {
49    (
50        $( $fn_name:ident : $ret_ty:ty = |$b:ident| $body:block )*
51    ) => {
52        $(
53            pub fn $fn_name(p: &Process, state: &mut State) -> Result<$ret_ty, Error> {
54                let path = path(p, state)?;
55                let $b = RmBeatmap::from_path(path)?;
56                $body
57            }
58        )*
59    };
60}
61
62generate_beatmap_field_getter! {
63    beatmap_id: i32 = beatmap_id;
64    beatmap_set_id: i32 = beatmap_set_id;
65    author: String = artist;
66    creator: String = creator;
67    title_romanized: String = title;
68    title: String = title_unicode;
69    difficulty: String = version;
70    tags: String = tags;
71    od: f32 = overall_difficulty;
72    ar: f32 = approach_rate;
73    cs: f32 = circle_size;
74    hp: f32 = hp_drain_rate;
75}
76
77generate_beatmap_custom_getter_safe! {
78    slider_count: i32 = |b| {
79        Ok(b.hit_objects.iter()
80            .filter(|h| matches!(h.kind, HitObjectKind::Slider(_)))
81            .count() as i32)
82    }
83    object_count: u32 = |b| {
84        Ok(b.hit_objects.len() as u32)
85    }
86    length: i32 = |b| {
87        let last = b.hit_objects.last().ok_or_else(|| Error::Other("Empty hitobject list".into()))?;
88
89        let duration = match &last.kind {
90            HitObjectKind::Hold(hold_data) => last.start_time + hold_data.duration,
91            _ => last.start_time,
92        };
93
94        Ok(duration as i32)
95    }
96    drain_time: i32 = |b| {
97        let first = b.hit_objects.first().ok_or_else(|| Error::Other("Empty hitobject list".into()))?;
98        let last = b.hit_objects.last().ok_or_else(|| Error::Other("Empty hitobject list".into()))?;
99
100        let duration = match &last.kind {
101            HitObjectKind::Hold(hold_data) => last.start_time + hold_data.duration,
102            _ => last.start_time,
103        };
104
105        Ok((duration - first.start_time) as i32)
106    }
107    mode: GameMode = |b| {
108        Ok(GameMode::from(b.mode as u32))
109    }
110
111}
112
113// cant do this in file mode
114pub fn status(p: &Process, state: &mut State) -> Result<BeatmapStatus, Error> {
115    // cant do this in file mode
116    crate::reader::beatmap::stable::memory::status(p, state)
117}
118
119pub fn star_rating(p: &Process, state: &mut State) -> Result<BeatmapStarRating, Error> {
120    let folder = folder(p, state)?;
121    let filename = filename(p, state)?;
122    let songs_path = path_folder(p, state)?;
123    let path = songs_path.join(folder).join(filename);
124    let b = rosu_pp::Beatmap::from_path(path)?;
125    let diff_attrs = rosu_pp::Difficulty::new().calculate(&b);
126    let diff_dt = rosu_pp::Difficulty::new().mods(64).calculate(&b);
127    let diff_ht = rosu_pp::Difficulty::new().mods(256).calculate(&b);
128    let no_mod = diff_attrs.stars();
129    let dt = diff_dt.stars();
130    let ht = diff_ht.stars();
131    Ok(BeatmapStarRating { no_mod, dt, ht })
132}
133
134pub fn md5(p: &Process, state: &mut State) -> Result<String, Error> {
135    // TODO: implement this for now will get from memory
136    crate::reader::beatmap::stable::memory::md5(p, state)
137}
138pub fn stats(p: &Process, state: &mut State) -> Result<BeatmapStats, Error> {
139    let beatmap_addr = path(p, state)?;
140    let b = RmBeatmap::from_path(beatmap_addr)?;
141    Ok(BeatmapStats {
142        ar: b.approach_rate,
143        od: b.overall_difficulty,
144        cs: b.circle_size,
145        hp: b.hp_drain_rate,
146        length: length(p, state)?,
147        star_rating: star_rating(p, state)?,
148        object_count: b.hit_objects.len() as i32,
149        slider_count: b
150            .hit_objects
151            .iter()
152            .filter(|h| matches!(h.kind, HitObjectKind::Slider(_)))
153            .count() as i32,
154    })
155}
156
157pub fn info(p: &Process, state: &mut State) -> Result<BeatmapInfo, Error> {
158    let beatmap_file = path(p, state)?;
159    let beatmap_addr = beatmap_addr(p, state)?;
160    let b = RmBeatmap::from_path(beatmap_file)?;
161    // done like that to be more efficient reading the string one by one would need to reload addr everytime which cost more
162    Ok(BeatmapInfo {
163        technical: BeatmapTechnicalInfo {
164            md5: crate::reader::beatmap::stable::file::md5(p, state)?,
165            id: b.beatmap_id,
166            set_id: b.beatmap_set_id,
167            mode: GameMode::Osu,
168            ranked_status: status(p, state)?,
169        },
170        metadata: BeatmapMetadata {
171            author: b.artist,
172            creator: b.creator,
173            title_romanized: b.title,
174            title_original: b.title_unicode,
175            difficulty: b.version,
176            tags: b.tags,
177        },
178        stats: BeatmapStats {
179            ar: b.approach_rate,
180            od: b.overall_difficulty,
181            cs: b.circle_size,
182            hp: b.hp_drain_rate,
183            length: length(p, state)?,
184            star_rating: star_rating(p, state)?,
185            object_count: b.hit_objects.len() as i32,
186            slider_count: b
187                .hit_objects
188                .iter()
189                .filter(|h| matches!(h.kind, HitObjectKind::Slider(_)))
190                .count() as i32,
191        },
192        location: BeatmapLocation {
193            folder: p.read_string(beatmap_addr + BEATMAP_OFFSET.location.folder)?,
194            filename: p.read_string(beatmap_addr + BEATMAP_OFFSET.location.filename)?,
195            audio: p.read_string(beatmap_addr + BEATMAP_OFFSET.location.audio)?,
196            cover: p.read_string(beatmap_addr + BEATMAP_OFFSET.location.cover)?,
197        },
198    })
199}