@@ -2,32 +2,36 @@ use std::fmt::Write;
22
33use bathbot_macros:: PaginationBuilder ;
44use bathbot_util:: {
5- AuthorBuilder , CowUtils , EmbedBuilder , FooterBuilder , MessageOrigin , attachment,
5+ AuthorBuilder , Authored , CowUtils , EmbedBuilder , FooterBuilder , MessageOrigin , attachment,
66 constants:: { AVATAR_URL , OSU_BASE } ,
77 datetime:: SecToMinSec ,
88 fields,
9+ modal:: { ModalBuilder , TextInputBuilder } ,
910 numbers:: { WithComma , round} ,
1011} ;
11- use eyre:: { Result , WrapErr } ;
12+ use eyre:: { ContextCompat , Result , WrapErr } ;
1213use rosu_pp:: { Difficulty , any:: HitResultPriority } ;
1314use rosu_v2:: prelude:: {
1415 BeatmapExtended , BeatmapsetExtended , GameMode , GameModsIntermode , Username ,
1516} ;
1617use twilight_model:: {
17- channel:: message:: Component ,
18+ channel:: message:: {
19+ Component ,
20+ component:: { ActionRow , Button , ButtonStyle } ,
21+ } ,
1822 id:: { Id , marker:: UserMarker } ,
1923} ;
2024
2125use crate :: {
2226 active:: {
2327 BuildPage , ComponentResult , IActiveMessage ,
24- pagination:: { Pages , handle_pagination_component, handle_pagination_modal } ,
28+ pagination:: { Pages , handle_pagination_component} ,
2529 } ,
2630 commands:: osu:: CustomAttrs ,
2731 core:: Context ,
2832 manager:: redis:: osu:: UserArgs ,
2933 util:: {
30- Emote ,
34+ Emote , ModalExt ,
3135 interaction:: { InteractionComponent , InteractionModal } ,
3236 } ,
3337} ;
@@ -80,7 +84,15 @@ impl IActiveMessage for MapPagination {
8084 let mut seconds_drain = map. seconds_drain ;
8185 let mut bpm = map. bpm as f64 ;
8286
83- let clock_rate = self . mods . legacy_clock_rate ( ) ;
87+ let clock_rate = match ( self . attrs . bpm , self . attrs . clock_rate ) {
88+ ( None , None ) => self . mods . legacy_clock_rate ( ) ,
89+ ( _, Some ( clock_rate) ) => clock_rate,
90+ ( Some ( new_bpm) , None ) => {
91+ let old_bpm = map. bpm ;
92+ ( new_bpm / old_bpm) as f64
93+ }
94+ } ;
95+
8496 seconds_total = ( seconds_total as f64 / clock_rate) as u32 ;
8597 seconds_drain = ( seconds_drain as f64 / clock_rate) as u32 ;
8698 bpm *= clock_rate;
@@ -321,15 +333,264 @@ impl IActiveMessage for MapPagination {
321333 }
322334
323335 fn build_components ( & self ) -> Vec < Component > {
324- self . pages . components ( )
336+ let mods = Button {
337+ custom_id : Some ( "map_mods" . to_owned ( ) ) ,
338+ disabled : false ,
339+ emoji : None ,
340+ label : Some ( "Mods" . to_owned ( ) ) ,
341+ style : ButtonStyle :: Primary ,
342+ url : None ,
343+ sku_id : None ,
344+ } ;
345+ let clock_rate = Button {
346+ custom_id : Some ( "map_clock_rate" . to_owned ( ) ) ,
347+ disabled : false ,
348+ emoji : None ,
349+ label : Some ( "Clock rate" . to_owned ( ) ) ,
350+ style : ButtonStyle :: Primary ,
351+ url : None ,
352+ sku_id : None ,
353+ } ;
354+ let attrs = Button {
355+ custom_id : Some ( "map_attrs" . to_owned ( ) ) ,
356+ disabled : false ,
357+ emoji : None ,
358+ label : Some ( "Attributes" . to_owned ( ) ) ,
359+ style : ButtonStyle :: Primary ,
360+ url : None ,
361+ sku_id : None ,
362+ } ;
363+ let top: Component = Component :: ActionRow ( ActionRow {
364+ components : vec ! [ mods, clock_rate, attrs]
365+ . into_iter ( )
366+ . map ( Component :: Button )
367+ . collect ( ) ,
368+ } ) ;
369+ let pages = if self . pages . last_index ( ) == 0 {
370+ None
371+ } else {
372+ let jump_start = Button {
373+ custom_id : Some ( "pagination_start" . to_owned ( ) ) ,
374+ disabled : self . pages . index ( ) == 0 ,
375+ emoji : Some ( Emote :: JumpStart . reaction_type ( ) ) ,
376+ label : None ,
377+ style : ButtonStyle :: Secondary ,
378+ url : None ,
379+ sku_id : None ,
380+ } ;
381+
382+ let single_step_back = Button {
383+ custom_id : Some ( "pagination_back" . to_owned ( ) ) ,
384+ disabled : self . pages . index ( ) == 0 ,
385+ emoji : Some ( Emote :: SingleStepBack . reaction_type ( ) ) ,
386+ label : None ,
387+ style : ButtonStyle :: Secondary ,
388+ url : None ,
389+ sku_id : None ,
390+ } ;
391+
392+ let jump_custom = Button {
393+ custom_id : Some ( "pagination_custom" . to_owned ( ) ) ,
394+ disabled : false ,
395+ emoji : Some ( Emote :: MyPosition . reaction_type ( ) ) ,
396+ label : None ,
397+ style : ButtonStyle :: Secondary ,
398+ url : None ,
399+ sku_id : None ,
400+ } ;
401+
402+ let single_step = Button {
403+ custom_id : Some ( "pagination_step" . to_owned ( ) ) ,
404+ disabled : self . pages . index ( ) == self . pages . last_index ( ) ,
405+ emoji : Some ( Emote :: SingleStep . reaction_type ( ) ) ,
406+ label : None ,
407+ style : ButtonStyle :: Secondary ,
408+ url : None ,
409+ sku_id : None ,
410+ } ;
411+
412+ let jump_end = Button {
413+ custom_id : Some ( "pagination_end" . to_owned ( ) ) ,
414+ disabled : self . pages . index ( ) == self . pages . last_index ( ) ,
415+ emoji : Some ( Emote :: JumpEnd . reaction_type ( ) ) ,
416+ label : None ,
417+ style : ButtonStyle :: Secondary ,
418+ url : None ,
419+ sku_id : None ,
420+ } ;
421+
422+ Some ( Component :: ActionRow ( ActionRow {
423+ components : vec ! [
424+ Component :: Button ( jump_start) ,
425+ Component :: Button ( single_step_back) ,
426+ Component :: Button ( jump_custom) ,
427+ Component :: Button ( single_step) ,
428+ Component :: Button ( jump_end) ,
429+ ] ,
430+ } ) )
431+ } ;
432+
433+ let mut components = vec ! [ top] ;
434+ if let Some ( pages) = pages {
435+ components. push ( pages) ;
436+ }
437+ components
325438 }
326439
327440 async fn handle_component ( & mut self , component : & mut InteractionComponent ) -> ComponentResult {
328- handle_pagination_component ( component, self . msg_owner , true , & mut self . pages ) . await
441+ let user_id = match component. user_id ( ) {
442+ Ok ( user_id) => user_id,
443+ Err ( err) => return ComponentResult :: Err ( err) ,
444+ } ;
445+
446+ if user_id != self . msg_owner {
447+ return ComponentResult :: Ignore ;
448+ }
449+
450+ if component. data . custom_id . starts_with ( "pagination" ) {
451+ return handle_pagination_component ( component, self . msg_owner , true , & mut self . pages )
452+ . await ;
453+ }
454+ let modal = match component. data . custom_id . as_str ( ) {
455+ "map_mods" => {
456+ let input = TextInputBuilder :: new ( "map_mods" , "Mods" )
457+ . placeholder ( "E.g. hd or HdHRdteZ" )
458+ . required ( false ) ;
459+
460+ ModalBuilder :: new ( "map_mods" , "Specify mods" ) . input ( input)
461+ }
462+ "map_clock_rate" => {
463+ let clock_rate = TextInputBuilder :: new ( "map_clock_rate" , "Clock rate" )
464+ . placeholder ( "Specify a clock rate" )
465+ . required ( false ) ;
466+
467+ let bpm = TextInputBuilder :: new (
468+ "map_bpm" ,
469+ "BPM (overwritten if clock rate is specified)" ,
470+ )
471+ . placeholder ( "Specify a BPM" )
472+ . required ( false ) ;
473+
474+ ModalBuilder :: new ( "map_speed_adjustments" , "Speed adjustments" )
475+ . input ( clock_rate)
476+ . input ( bpm)
477+ }
478+ "map_attrs" => {
479+ let ar = TextInputBuilder :: new ( "map_ar" , "AR" )
480+ . placeholder ( "Specify an approach rate" )
481+ . required ( false ) ;
482+
483+ let cs = TextInputBuilder :: new ( "map_cs" , "CS" )
484+ . placeholder ( "Specify a circle size" )
485+ . required ( false ) ;
486+
487+ let hp = TextInputBuilder :: new ( "map_hp" , "HP" )
488+ . placeholder ( "Specify a drain rate" )
489+ . required ( false ) ;
490+
491+ let od = TextInputBuilder :: new ( "map_od" , "OD" )
492+ . placeholder ( "Specify an overall difficulty" )
493+ . required ( false ) ;
494+
495+ ModalBuilder :: new ( "map_attrs" , "Attributes" )
496+ . input ( ar)
497+ . input ( cs)
498+ . input ( hp)
499+ . input ( od)
500+ }
501+ other => {
502+ warn ! ( name = %other, ?component, "Unknown map component" ) ;
503+
504+ return ComponentResult :: Ignore ;
505+ }
506+ } ;
507+ ComponentResult :: CreateModal ( modal)
329508 }
330509
331510 async fn handle_modal ( & mut self , modal : & mut InteractionModal ) -> Result < ( ) > {
332- handle_pagination_modal ( modal, self . msg_owner , true , & mut self . pages ) . await
511+ if modal. user_id ( ) ? != self . msg_owner {
512+ return Ok ( ( ) ) ;
513+ }
514+
515+ let input = modal
516+ . data
517+ . components
518+ . first ( )
519+ . and_then ( |row| row. components . first ( ) )
520+ . wrap_err ( "Missing map modal input" ) ?
521+ . value
522+ . as_deref ( )
523+ . filter ( |val| !val. is_empty ( ) ) ;
524+ let map = & self . maps [ self . pages . index ( ) ] ;
525+
526+ match modal. data . custom_id . as_str ( ) {
527+ "pagination_custom" => {
528+ let Some ( Ok ( page) ) = input. as_deref ( ) . map ( str:: parse) else {
529+ debug ! ( input = input, "Failed to parse page input as usize" ) ;
530+
531+ return Ok ( ( ) ) ;
532+ } ;
533+
534+ let max_page = self . pages . last_page ( ) ;
535+
536+ if !( 1 ..=max_page) . contains ( & page) {
537+ debug ! ( "Page {page} is not between 1 and {max_page}" ) ;
538+
539+ return Ok ( ( ) ) ;
540+ }
541+
542+ self . pages . set_index ( ( page - 1 ) * self . pages . per_page ( ) ) ;
543+ }
544+ "map_mods" => {
545+ let mods_res = input. map ( |s| {
546+ s. trim_start_matches ( '+' )
547+ . trim_end_matches ( '!' )
548+ . parse :: < GameModsIntermode > ( )
549+ } ) ;
550+
551+ let mods = match mods_res {
552+ Some ( Ok ( value) ) => Some ( value) ,
553+ Some ( Err ( _) ) => {
554+ debug ! ( input, "Failed to parse simulate mods" ) ;
555+
556+ return Ok ( ( ) ) ;
557+ }
558+ None => None ,
559+ } ;
560+ debug ! ( ?mods, "Matched mods" ) ;
561+
562+ match mods. map ( |mods| mods. try_with_mode ( map. mode ) ) {
563+ Some ( Some ( mods) ) if mods. is_valid ( ) => self . mods = mods. into ( ) ,
564+ None => self . mods = GameModsIntermode :: new ( ) ,
565+ Some ( Some ( mods) ) => {
566+ debug ! ( "Incompatible mods {mods}" ) ;
567+
568+ return Ok ( ( ) ) ;
569+ }
570+ Some ( None ) => {
571+ debug ! ( input, "Invalid mods for mode" ) ;
572+
573+ return Ok ( ( ) ) ;
574+ }
575+ }
576+ }
577+ "map_attrs" => {
578+ self . attrs . ar = parse_attr ( & * modal, "map_ar" ) ;
579+ self . attrs . cs = parse_attr ( & * modal, "map_cs" ) ;
580+ self . attrs . hp = parse_attr ( & * modal, "map_hp" ) ;
581+ self . attrs . od = parse_attr ( & * modal, "map_od" ) ;
582+ }
583+ "map_speed_adjustments" => {
584+ self . attrs . clock_rate = parse_attr ( & * modal, "map_clock_rate" ) ;
585+ self . attrs . bpm = parse_attr ( modal, "map_bpm" ) ;
586+ }
587+ other => warn ! ( name = %other, ?modal, "Unknown map modal" ) ,
588+ }
589+ if let Err ( err) = modal. defer ( ) . await {
590+ warn ! ( ?err, "Failed to defer modal" ) ;
591+ }
592+
593+ Ok ( ( ) )
333594 }
334595}
335596
@@ -361,3 +622,27 @@ async fn creator_name(map: &BeatmapExtended, mapset: &BeatmapsetExtended) -> Opt
361622 }
362623 }
363624}
625+
626+ fn parse_attr < T : std:: str:: FromStr + std:: fmt:: Debug > (
627+ modal : & InteractionModal ,
628+ component_id : & str ,
629+ ) -> Option < T > {
630+ let result = modal
631+ . data
632+ . components
633+ . iter ( )
634+ . find_map ( |row| {
635+ row. components . first ( ) . and_then ( |component| {
636+ ( component. custom_id == component_id) . then ( || {
637+ component
638+ . value
639+ . as_deref ( )
640+ . filter ( |value| !value. is_empty ( ) )
641+ . map ( str:: parse)
642+ . and_then ( Result :: ok)
643+ } )
644+ } )
645+ } )
646+ . flatten ( ) ;
647+ result
648+ }
0 commit comments