rosu_render/model/
render.rs

1use std::{
2    borrow::Cow,
3    fmt::{Display, Formatter, Result as FmtResult},
4};
5
6use hyper::{body::Bytes, StatusCode};
7use serde::{
8    de::{Error as DeError, IgnoredAny, MapAccess, Unexpected, Visitor},
9    Deserialize, Deserializer, Serialize,
10};
11use time::OffsetDateTime;
12
13use crate::{request::Requestable, util::datetime::deserialize_datetime, ClientError};
14
15/// A list of [`Render`].
16#[derive(Clone, Debug, Deserialize)]
17pub struct RenderList {
18    /// Array of renders returned by the api
19    pub renders: Vec<Render>,
20    /// The total number of renders on o!rdr,
21    /// but if search query the total numbers of renders corresponding to that query will be used.
22    #[serde(rename = "maxRenders")]
23    pub max_renders: u32,
24}
25
26impl Requestable for RenderList {
27    fn response_error(status: StatusCode, bytes: Bytes) -> ClientError {
28        ClientError::response_error(bytes, status.as_u16())
29    }
30}
31
32#[derive(Clone, Debug, Deserialize)]
33pub struct Render {
34    #[serde(rename = "renderID")]
35    pub id: u32,
36    #[serde(deserialize_with = "deserialize_datetime")]
37    pub date: OffsetDateTime,
38    pub username: Box<str>,
39    pub progress: Box<str>,
40    pub renderer: Box<str>,
41    pub description: Box<str>,
42    pub title: Box<str>,
43    #[serde(rename = "isBot")]
44    pub is_bot: bool,
45    #[serde(rename = "isVerified")]
46    pub is_verified: bool,
47    #[serde(rename = "videoUrl")]
48    pub video_url: Box<str>,
49    #[serde(rename = "mapLink")]
50    pub map_link: Box<str>,
51    #[serde(rename = "mapTitle")]
52    pub map_title: Box<str>,
53    #[serde(rename = "replayDifficulty")]
54    pub replay_difficulty: Box<str>,
55    #[serde(rename = "replayUsername")]
56    pub replay_username: Box<str>,
57    #[serde(rename = "mapID")]
58    pub map_id: u32,
59    #[serde(rename = "needToRedownload")]
60    pub need_to_redownload: bool,
61    #[serde(rename = "motionBlur960fps")]
62    pub motion_blur: bool,
63    #[serde(rename = "renderStartTime", deserialize_with = "deserialize_datetime")]
64    pub render_start_time: OffsetDateTime,
65    #[serde(rename = "renderEndTime", deserialize_with = "deserialize_datetime")]
66    pub render_end_time: OffsetDateTime,
67    #[serde(rename = "uploadEndTime", deserialize_with = "deserialize_datetime")]
68    pub upload_end_time: OffsetDateTime,
69    #[serde(rename = "renderTotalTime")]
70    pub render_total_time: u32,
71    #[serde(rename = "uploadTotalTime")]
72    pub upload_total_time: u32,
73    #[serde(rename = "mapLength")]
74    pub map_length: u32,
75    #[serde(rename = "replayMods")]
76    pub replay_mods: Box<str>,
77    pub removed: bool,
78    #[serde(flatten)]
79    pub options: RenderOptions,
80    #[serde(flatten)]
81    pub skin: RenderSkinOption<'static>,
82}
83
84#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
85pub enum RenderResolution {
86    /// 720x480 (30fps)
87    #[serde(rename = "720x480")]
88    SD480,
89    /// 960x540 (30fps)
90    #[serde(rename = "960x540")]
91    SD960,
92    /// 1280x720 (60fps)
93    #[serde(rename = "1280x720")]
94    HD720,
95    /// 1920x1080 (60fps)
96    #[serde(rename = "1920x1080")]
97    HD1080,
98}
99
100impl RenderResolution {
101    #[must_use]
102    pub fn as_str(self) -> &'static str {
103        match self {
104            Self::SD480 => "720x480",
105            Self::SD960 => "960x540",
106            Self::HD720 => "1280x720",
107            Self::HD1080 => "1920x1080",
108        }
109    }
110}
111
112impl Display for RenderResolution {
113    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
114        f.write_str(self.as_str())
115    }
116}
117
118/// Customize danser settings when rendering.
119#[derive(Clone, Debug, Deserialize, Serialize)]
120pub struct RenderOptions {
121    pub resolution: RenderResolution,
122    /// The global volume for the video, in percent, from 0 to 100.
123    #[serde(rename = "globalVolume")]
124    pub global_volume: u8,
125    /// The music volume for the video, in percent, from 0 to 100.
126    #[serde(rename = "musicVolume")]
127    pub music_volume: u8,
128    /// The hitsounds volume for the video, in percent, from 0 to 100.
129    #[serde(rename = "hitsoundVolume")]
130    pub hitsound_volume: u8,
131    /// Show the hit error meter.
132    #[serde(rename = "showHitErrorMeter")]
133    pub show_hit_error_meter: bool,
134    /// Show the unstable rate, only takes effect if `show_hit_error_meter` is set to true.
135    #[serde(rename = "showUnstableRate")]
136    pub show_unstable_rate: bool,
137    /// Show the score.
138    #[serde(rename = "showScore")]
139    pub show_score: bool,
140    /// Show the HP bar.
141    #[serde(rename = "showHPBar")]
142    pub show_hp_bar: bool,
143    /// Show the combo counter.
144    #[serde(rename = "showComboCounter")]
145    pub show_combo_counter: bool,
146    /// Show the PP Counter or not.
147    #[serde(rename = "showPPCounter")]
148    pub show_pp_counter: bool,
149    /// Show the scoreboard or not.
150    #[serde(rename = "showScoreboard")]
151    pub show_scoreboard: bool,
152    /// Show the playfield borders or not.
153    #[serde(rename = "showBorders")]
154    pub show_borders: bool,
155    /// Show the mods used during the game or not.
156    #[serde(rename = "showMods")]
157    pub show_mods: bool,
158    /// Show the result screen or not.
159    #[serde(rename = "showResultScreen")]
160    pub show_result_screen: bool,
161    /// Use the skin cursor or not. If not, danser cursor will be used.
162    #[serde(rename = "useSkinCursor")]
163    pub use_skin_cursor: bool,
164    /// Use the skin combo colors or not.
165    #[serde(rename = "useSkinColors")]
166    pub use_skin_colors: bool,
167    /// Use skin hitsounds, if false beatmap hitsounds will be used.
168    #[serde(rename = "useSkinHitsounds")]
169    pub use_skin_hitsounds: bool,
170    /// Use the beatmap combo colors or not, overrides useSkinColors if true.
171    #[serde(rename = "useBeatmapColors")]
172    pub use_beatmap_colors: bool,
173    /// Scale cursor to circle size. Does not do anything at the moment.
174    #[serde(rename = "cursorScaleToCS")]
175    pub cursor_scale_to_cs: bool,
176    /// Makes the cursor rainbow, only takes effect if `use_skin_cursor` is set to false.
177    #[serde(rename = "cursorRainbow")]
178    pub cursor_rainbow: bool,
179    /// Have a glow with the trail or not.
180    #[serde(rename = "cursorTrailGlow")]
181    pub cursor_trail_glow: bool,
182    /// Draw follow points between objects or not.
183    #[serde(rename = "drawFollowPoints")]
184    pub draw_follow_points: bool,
185    /// Scale objects to the beat.
186    #[serde(rename = "scaleToTheBeat")]
187    pub beat_scaling: bool,
188    /// Merge sliders or not.
189    #[serde(rename = "sliderMerge")]
190    pub slider_merge: bool,
191    /// Makes the objects rainbow, overrides `use_skin_colors` and `use_beatmap_colors`.
192    #[serde(rename = "objectsRainbow")]
193    pub objects_rainbow: bool,
194    /// Makes the objects flash to the beat.
195    #[serde(rename = "objectsFlashToTheBeat")]
196    pub flash_objects: bool,
197    /// Makes the slider body have the same color as the hit circles.
198    #[serde(rename = "useHitCircleColor")]
199    pub use_slider_hitcircle_color: bool,
200    /// Display a 5 second seizure warning before the video.
201    #[serde(rename = "seizureWarning")]
202    pub seizure_warning: bool,
203    /// Load the background storyboard.
204    #[serde(rename = "loadStoryboard")]
205    pub load_storyboard: bool,
206    /// Load the background video (`load_storyboard` has to be set to true).
207    #[serde(rename = "loadVideo")]
208    pub load_video: bool,
209    /// Background dim for the intro, in percent, from 0 to 100.
210    #[serde(rename = "introBGDim")]
211    pub intro_bg_dim: u8,
212    /// Background dim in game, in percent, from 0 to 100.
213    #[serde(rename = "inGameBGDim")]
214    pub ingame_bg_dim: u8,
215    /// Background dim in break, in percent, from 0 to 100.
216    #[serde(rename = "breakBGDim")]
217    pub break_bg_dim: u8,
218    /// Adds a parallax effect.
219    #[serde(rename = "BGParallax")]
220    pub bg_parallax: bool,
221    /// Show danser logo on the intro.
222    #[serde(rename = "showDanserLogo")]
223    pub show_danser_logo: bool,
224    /// Skip the intro or not.
225    #[serde(rename = "skip")]
226    pub skip_intro: bool,
227    /// Show cursor ripples when keypress.
228    #[serde(rename = "cursorRipples")]
229    pub cursor_ripples: bool,
230    /// Set the cursor size, multiplier from 0.5 to 2.
231    #[serde(rename = "cursorSize")]
232    pub cursor_size: f32,
233    /// Show the cursor trail or not.
234    #[serde(rename = "cursorTrail")]
235    pub cursor_trail: bool,
236    /// Show the combo numbers in objects.
237    #[serde(rename = "drawComboNumbers")]
238    pub draw_combo_numbers: bool,
239    /// Have slider snaking in.
240    #[serde(rename = "sliderSnakingIn")]
241    pub slider_snaking_in: bool,
242    /// Have slider snaking out.
243    #[serde(rename = "sliderSnakingOut")]
244    pub slider_snaking_out: bool,
245    /// Shows a hit counter (100, 50, miss) below the PP counter.
246    #[serde(rename = "showHitCounter")]
247    pub show_hit_counter: bool,
248    /// Show the key overlay or not.
249    #[serde(rename = "showKeyOverlay")]
250    pub show_key_overlay: bool,
251    /// Show avatars on the left of the username of a player on the scoreboard.
252    /// May break some skins because the width of the scoreboard increases.
253    #[serde(rename = "showAvatarsOnScoreboard")]
254    pub show_avatars_on_scoreboard: bool,
255    /// Show the Aim Error Meter or not.
256    #[serde(rename = "showAimErrorMeter")]
257    pub show_aim_error_meter: bool,
258    /// Play nightcore hitsounds or not.
259    #[serde(rename = "playNightcoreSamples")]
260    pub play_nightcore_samples: bool,
261    /// Show the strain graph or not.
262    #[serde(rename = "showStrainGraph")]
263    pub show_strain_graph: bool,
264    /// Show the slider breaks count in the hit counter.
265    #[serde(rename = "showSliderBreaks")]
266    pub show_slider_breaks: bool,
267    /// Ignores fail in the replay or not.
268    #[serde(rename = "ignoreFail")]
269    pub ignore_fail: bool,
270    /// For verified bots, supplying the requester's Discord ID allows the o!rdr
271    /// server to set their favorite preset if they have one saved and set to be
272    /// used with bots in their issou.best account.
273    #[serde(
274        rename = "discordUserId",
275        skip_serializing_if = "Option::is_none",
276        with = "maybe_u64_as_str"
277    )]
278    pub discord_user_id: Option<u64>,
279}
280
281mod maybe_u64_as_str {
282    use serde::{
283        de::{Deserialize, Deserializer, Error},
284        Serializer,
285    };
286
287    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u64>, D::Error> {
288        match Option::<&str>::deserialize(d)? {
289            Some(s) => s.parse().map(Some).map_err(Error::custom),
290            None => Ok(None),
291        }
292    }
293
294    #[expect(clippy::ref_option, reason = "required by serde")]
295    pub fn serialize<S: Serializer>(opt: &Option<u64>, s: S) -> Result<S::Ok, S::Error> {
296        // we skip serialization if the option is `None`
297        let Some(n) = opt else { unreachable!() };
298
299        s.serialize_str(&n.to_string())
300    }
301}
302
303impl RenderOptions {
304    pub const DEFAULT_RESOLUTION: RenderResolution = RenderResolution::HD720;
305}
306
307impl Default for RenderOptions {
308    fn default() -> Self {
309        Self {
310            resolution: Self::DEFAULT_RESOLUTION,
311            global_volume: 50,
312            music_volume: 50,
313            hitsound_volume: 50,
314            show_hit_error_meter: true,
315            show_unstable_rate: true,
316            show_score: true,
317            show_hp_bar: true,
318            show_combo_counter: true,
319            show_pp_counter: true,
320            show_key_overlay: true,
321            show_scoreboard: true,
322            show_borders: true,
323            show_mods: true,
324            show_result_screen: true,
325            use_skin_cursor: true,
326            use_skin_colors: false,
327            use_skin_hitsounds: true,
328            use_beatmap_colors: true,
329            cursor_scale_to_cs: false,
330            cursor_rainbow: false,
331            cursor_trail_glow: false,
332            draw_follow_points: true,
333            draw_combo_numbers: true,
334            cursor_size: 1.0,
335            cursor_trail: true,
336            beat_scaling: false,
337            slider_merge: false,
338            objects_rainbow: false,
339            flash_objects: false,
340            use_slider_hitcircle_color: false,
341            seizure_warning: false,
342            load_storyboard: false,
343            load_video: false,
344            intro_bg_dim: 0,
345            ingame_bg_dim: 80,
346            break_bg_dim: 30,
347            bg_parallax: false,
348            show_danser_logo: true,
349            skip_intro: true,
350            cursor_ripples: false,
351            slider_snaking_in: true,
352            slider_snaking_out: true,
353            show_hit_counter: true,
354            show_avatars_on_scoreboard: false,
355            show_aim_error_meter: false,
356            play_nightcore_samples: true,
357            show_strain_graph: false,
358            show_slider_breaks: false,
359            ignore_fail: false,
360            discord_user_id: None,
361        }
362    }
363}
364
365#[derive(Clone, Debug, PartialEq, Eq)]
366pub enum RenderSkinOption<'a> {
367    Official { name: Cow<'a, str> },
368    Custom { id: u32 },
369}
370
371impl Default for RenderSkinOption<'_> {
372    fn default() -> Self {
373        Self::Official {
374            name: "default".into(),
375        }
376    }
377}
378
379impl From<u32> for RenderSkinOption<'_> {
380    fn from(id: u32) -> Self {
381        Self::Custom { id }
382    }
383}
384
385macro_rules! impl_from_name {
386    ( $( $ty:ty ),* ) => {
387        $(
388            impl<'a> From<$ty> for RenderSkinOption<'a> {
389                fn from(name: $ty) -> Self {
390                    Self::Official { name: name.into() }
391                }
392            }
393        )*
394    };
395}
396
397impl_from_name!(&'a str, &'a String, String, Cow<'a, str>);
398
399impl<'de> Deserialize<'de> for RenderSkinOption<'static> {
400    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
401        struct SkinVisitor;
402
403        impl<'de> Visitor<'de> for SkinVisitor {
404            type Value = RenderSkinOption<'static>;
405
406            fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
407                f.write_str("`skin` and `customSkin` fields")
408            }
409
410            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
411                let mut custom_skin: Option<bool> = None;
412                let mut skin: Option<String> = None;
413
414                while let Some(key) = map.next_key()? {
415                    match key {
416                        "customSkin" => custom_skin = Some(map.next_value()?),
417                        "skin" => skin = Some(map.next_value()?),
418                        _ => {
419                            let _: IgnoredAny = map.next_value()?;
420                        }
421                    }
422                }
423
424                let custom_skin =
425                    custom_skin.ok_or_else(|| DeError::missing_field("customSkin"))?;
426                let skin = skin.ok_or_else(|| DeError::missing_field("skin"))?;
427
428                let skin = if custom_skin {
429                    let id = skin
430                        .parse()
431                        .map_err(|_| DeError::invalid_value(Unexpected::Str(&skin), &"a u32"))?;
432
433                    RenderSkinOption::Custom { id }
434                } else {
435                    RenderSkinOption::Official {
436                        name: Cow::Owned(skin),
437                    }
438                };
439
440                Ok(skin)
441            }
442        }
443
444        d.deserialize_map(SkinVisitor)
445    }
446}
447
448#[derive(Clone, Debug, Deserialize, PartialEq)]
449pub struct RenderServers {
450    pub servers: Vec<RenderServer>,
451}
452
453impl Requestable for RenderServers {
454    fn response_error(status: StatusCode, bytes: Bytes) -> ClientError {
455        ClientError::response_error(bytes, status.as_u16())
456    }
457}
458
459#[derive(Clone, Debug, Deserialize, PartialEq)]
460pub struct RenderServer {
461    pub enabled: bool,
462    #[serde(rename = "lastSeen", deserialize_with = "deserialize_datetime")]
463    pub last_seen: OffsetDateTime,
464    pub name: Box<str>,
465    pub priority: f32,
466    #[serde(rename = "oldScore")]
467    pub old_score: f32,
468    #[serde(rename = "avgFPS")]
469    pub avg_fps: u32,
470    pub power: Box<str>,
471    pub status: Box<str>,
472    #[serde(rename = "totalRendered")]
473    pub total_rendered: u32,
474    #[serde(rename = "renderingType")]
475    pub rendering_type: Box<str>,
476    pub cpu: Box<str>,
477    pub gpu: Box<str>,
478    #[serde(rename = "motionBlurCapable")]
479    pub motion_blur_capable: bool,
480    #[serde(rename = "usingOsuApi")]
481    pub using_osu_api: bool,
482    #[serde(rename = "uhdCapable")]
483    pub uhd_capable: bool,
484    #[serde(rename = "avgRenderTime")]
485    pub avg_render_time: f32,
486    #[serde(rename = "avgUploadTime")]
487    pub avg_upload_time: f32,
488    #[serde(rename = "totalAvgTime")]
489    pub total_avg_time: f32,
490    #[serde(rename = "totalUploadedVideosSize")]
491    pub total_uploaded_videos_size: u32,
492    #[serde(rename = "ownerUserId")]
493    pub owner_user_id: u32,
494    #[serde(rename = "ownerUsername")]
495    pub owner_username: Box<str>,
496    pub customization: RenderServerOptions,
497}
498
499#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
500pub struct RenderServerOptions {
501    #[serde(rename = "textColor")]
502    pub text_color: Box<str>,
503    #[serde(rename = "backgroundType")]
504    pub background_type: i32,
505}
506
507#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
508pub struct ServerOnlineCount(pub u32);
509
510impl Requestable for ServerOnlineCount {
511    fn response_error(status: StatusCode, bytes: Bytes) -> ClientError {
512        ClientError::response_error(bytes, status.as_u16())
513    }
514}