├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── analysis ├── __init__.py └── osu │ ├── __init__.py │ ├── catch │ ├── __init__.py │ └── map_data.py │ ├── mania │ ├── __init__.py │ ├── action_data.py │ ├── mania_layers.py │ ├── map_data.py │ ├── map_metrics.py │ └── score_data.py │ ├── std │ ├── __init__.py │ ├── map_data.py │ ├── map_metrics.py │ ├── map_patterns.py │ ├── replay_data.py │ ├── replay_metrics.py │ ├── score_data.py │ ├── score_metrics.py │ └── std_layers.py │ └── taiko │ └── map_data.py ├── cli ├── __init__.py ├── cmd_online.py ├── cmd_osu.py └── cmd_utils.py ├── core ├── gamemode_manager.py ├── graph_manager.py └── metric_manager.py ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── source │ ├── analysis │ │ ├── analysis.osu.catch.rst │ │ ├── analysis.osu.mania.rst │ │ ├── analysis.osu.rst │ │ ├── analysis.osu.std.rst │ │ ├── analysis.rst │ │ └── modules.rst │ ├── cli │ │ ├── cli.rst │ │ └── modules.rst │ ├── gui │ │ ├── gui.objects.graph.rst │ │ ├── gui.objects.layer.rst │ │ ├── gui.objects.rst │ │ ├── gui.rst │ │ └── modules.rst │ └── osu │ │ ├── modules.rst │ │ ├── online.rst │ │ ├── online.structs.rst │ │ ├── osu.online.rst │ │ ├── osu.online.structs.rst │ │ └── osu.rst └── tutorials │ ├── analyzing_beatmaps_and_replays.rst │ ├── exploring_online_data.rst │ ├── fetching_online_data.rst │ ├── gettings_started.rst │ └── the_dynamic_environment.rst ├── generic ├── switcher.py └── temporal.py ├── gui ├── __init__.py ├── frames │ ├── bottom_frame │ │ ├── bottom_frame.py │ │ └── children │ │ │ └── timeline.py │ ├── center_frame │ │ ├── center_frame.py │ │ └── children │ │ │ ├── left_frame.py │ │ │ ├── mid_frame.py │ │ │ └── right_frame.py │ └── main_frame.py ├── objects │ ├── __init__.py │ ├── display.py │ ├── graph │ │ ├── __init__.py │ │ ├── aim_plot.py │ │ ├── bar_plot.py │ │ ├── hitobject_plot.py │ │ ├── line_plot.py │ │ ├── scatter_plot.py │ │ ├── score_plot.py │ │ └── timing_plot.py │ ├── group.py │ ├── layer │ │ ├── __init__.py │ │ ├── layer.py │ │ ├── layer_manager.py │ │ └── layers │ │ │ ├── mania │ │ │ ├── __init__.py │ │ │ └── score_debug_layer.py │ │ │ ├── mania_data_2d_layer.py │ │ │ ├── std │ │ │ ├── __init__.py │ │ │ ├── aimpoint_angle_text_layer.py │ │ │ ├── aimpoint_paths_layer.py │ │ │ ├── aimpoint_vector_layer.py │ │ │ ├── aimpoint_velocity_text_layer.py │ │ │ ├── hitobject_aimpoint_layer.py │ │ │ ├── hitobject_outline_layer.py │ │ │ └── score_debug_layer.py │ │ │ └── std_data_2d_layer.py │ └── scene.py └── widgets │ ├── QContainer.py │ ├── collections_browser.py │ ├── data_2d_graph.py │ ├── dockable_graph.py │ ├── editable_value_field.py │ ├── file_browser.py │ ├── graph_manager.py │ ├── ipython_console.py │ ├── layer_manager.py │ ├── manager_switch.py │ ├── mania_settings.py │ ├── map_manager.py │ ├── online_browser.py │ ├── replay_manager.py │ ├── std_settings.py │ └── timeline.py ├── misc ├── __init__.py ├── bezier.py ├── callback.py ├── frozen_cls.py ├── geometry.py ├── json_obj.py ├── line.py ├── logger.py ├── math_utils.py ├── metrics.py ├── numpy_utils.py └── pos.py ├── osu ├── local │ ├── beatmap │ │ ├── __init__.py │ │ ├── beatmap.py │ │ └── beatmapIO.py │ ├── collection │ │ └── collectionIO.py │ ├── enums.py │ ├── hitobject │ │ ├── catch │ │ │ ├── __init__.py │ │ │ ├── catch.py │ │ │ ├── catch_holdnote_hitobject.py │ │ │ ├── catch_singlenote_hitobject.py │ │ │ └── catch_spinner_hitobject.py │ │ ├── hitobject.py │ │ ├── mania │ │ │ ├── __init__.py │ │ │ ├── mania.py │ │ │ ├── mania_holdnote_hitobject.py │ │ │ ├── mania_holdnote_io.py │ │ │ ├── mania_singlenote_hitobject.py │ │ │ └── mania_singlenote_io.py │ │ ├── std │ │ │ ├── __init__.py │ │ │ ├── std.py │ │ │ ├── std_holdnote_hitobject.py │ │ │ ├── std_holdnote_io.py │ │ │ ├── std_singlenote_hitobject.py │ │ │ ├── std_singlenote_io.py │ │ │ ├── std_spinner_hitobject.py │ │ │ └── std_spinner_io.py │ │ └── taiko │ │ │ ├── __init__.py │ │ │ ├── taiko.py │ │ │ ├── taiko_holdnote_hitobject.py │ │ │ ├── taiko_singlenote_hitobject.py │ │ │ └── taiko_spinner_hitobject.py │ ├── monitor.py │ └── replay │ │ ├── __init__.py │ │ ├── replay.py │ │ └── replayIO.py └── online │ ├── SessionMgr.py │ ├── __init__.py │ ├── login.py │ ├── login_sample.py │ ├── osu_api.py │ ├── osu_online.py │ ├── rate_limited.py │ └── structs │ ├── __init__.py │ └── web_structs.py ├── requirements.txt ├── run.py ├── scripts ├── aim_offsets.py ├── download_replays.py ├── mania_hit_offsets.py ├── map_press_rate.py ├── playback.py ├── save_hitoffsets.py ├── solve_hitoffsets.py ├── std_hit_offsets.py ├── std_timing_graph.py └── test_script.py ├── test.py └── unit_tests ├── collections └── collection.db ├── maps ├── mania │ ├── playable │ │ ├── Camellia - GHOST (qqqant) [Collab PHANTASM [MX]].osu │ │ └── DJ Genericname - Dear You (Taiwan-NAK) [S.Star's 4K HD+].osu │ ├── sr_testing │ │ ├── easy_to_acc │ │ │ ├── Endorfin. - Four Leaves ([ A v a l o n ]) [HEAVENLY].osu │ │ │ ├── Yamamoto Momiji - FantasticTo-ryanse! (short Ver.) (ALEFY) [EXH [NSV]].osu │ │ │ ├── Yooh - MariannE (-SoraGami-) [4K Goddess].osu │ │ │ ├── Yooh - MariannE (neonat) [Mania 4K Collab].osu │ │ │ ├── ginkiha - Anemoi ([ A v a l o n ]) [Daybreak].osu │ │ │ ├── ginkiha - Borealis ([ A v a l o n ]) [GRAVITY].osu │ │ │ ├── t+pazolite - Oshama Scramble! ([ A v a l o n ]) [MASTER].osu │ │ │ └── t+pazolite - Pumpin' Junkies (Draftnell) [Junkies !!!].osu │ │ ├── hard_to_acc │ │ │ ├── KillerBlood - -.-. .- .-. --- .-.. --- ..-. .-.. .. ..-. . (Dergo) [restoration].osu │ │ │ ├── Noyoungwoo - I made this while think you (Tidek) [To Oktawia].osu │ │ │ ├── gems - Gems Pack 13 - LN Master 5th (gemboyong) [8 - Silver Forest, Tsurupettan].osu │ │ │ └── uno(IOSYS) - #FairyJoke #SDVX_Edit (Hydria) [uno(IOSYS) - #FairyJoke #SDVX_Edit].osu │ │ ├── high_sr_low_diff_low_hp │ │ │ ├── Kaneko Chiharu - iLLness LiLin (Faing Fain) [Maware! Setsugetsuka].osu │ │ │ ├── Stereoman - Hydrangea (SReisen) [Star Burst 2!].osu │ │ │ ├── Stereoman - Hydrangea (SReisen) [Star Burst 3!!].osu │ │ │ ├── The Koxx - A FOOL MOON NIGHT (JumpTwiice) [4k].osu │ │ │ └── Tune Up! - Bounce (Nightcore Mix) (Yuichie) [EXCLSV-V01].osu │ │ ├── high_sr_low_diff_norm_hp │ │ │ ├── 3L - Endless Night x1.25 (Skorer) [Endless Longs Notes !!].osu │ │ │ ├── Giga-P - LUVORATORRRRRY! (Fullerene-) [Down Below].osu │ │ │ ├── Helblinde - Gateway to psycho (Flexo123) [PANIC!!!].osu │ │ │ ├── Imperial Circus Dead Decadence - Yomi Yori Kikoyu, Koukoku no Tou to Honoo no Shoujo. (Kall) [EXCLUSIVE].osu │ │ │ ├── Kurosaki Maon - Rakuen no Tsubasa (TimBergling) [Berguling uwu].osu │ │ │ ├── RADWIMPS - Zen Zen Zense (Niima) [Musubi].osu │ │ │ ├── Rib - Akaito (shuniki) [ShuChan!!].osu │ │ │ ├── The Quick Brown Fox - WANDERLUST (iJinjin) [Jinjin's 4K Extra].osu │ │ │ ├── Various Artists - Miracle of Mania Vol. 3 (Un-known) [Ice - Entrance].osu │ │ │ └── witch's slave - furioso melodia (Ranne Stuart) [4k Extra].osu │ │ ├── high_sr_low_diff_norm_hp_norm_od │ │ │ ├── Camellia - Shun no Shifudo o Ikashita Kare Fumi Paeria (XeoStyle) [Octodad Twerk Theme No SV].osu │ │ │ ├── Kaneko Chiharu - Kai Dan (Fresh Chicken) [Extra].osu │ │ │ ├── Kobaryo - Dotabata Animation [feat. t+pazolite] (YaHao) [Ultra].osu │ │ │ ├── Kobaryo - Kick To The Sky (KH_Supernova) [Extra].osu │ │ │ ├── Pegboard Nerds - Swamp Thing (Da Tweekaz Edit) (Hydria) [Extreme].osu │ │ │ ├── Various Artists - Charts for X Girlz (Chronocide) [Kobaryo - Transciever FX].osu │ │ │ ├── Various Artists - CommandoBlack's Status=n Mapset B (CommandoBlack) [Camellia - #Include].osu │ │ │ ├── Various Artists - arpia97's old maps package (leqek) [Chronomia].osu │ │ │ ├── Yooh - Road To The LegenD, (SpectorDG) [Thunder of Olympus].osu │ │ │ ├── t+pazolite vs C-Show - TRICKL4SH 220 (Hydria) [Extra].osu │ │ │ └── typeMARS - Triumph & Regret ([ A v a l o n ]) [Regret].osu │ │ ├── low_sr_high_diff │ │ │ ├── Akira Complex - Odyssey (Au5 Remix) (Shoegazer) [Dreamless].osu │ │ │ ├── Betwixt & Between - out of Blue (Shoegazer) [Abyss].osu │ │ │ ├── Blue Stahli - Shotgun Senorita (Zardonic Remix) (juankristal) [Machine Gun].osu │ │ │ ├── DJ Banzai - Disconnected Trance (Dingles) [speed].osu │ │ │ ├── Darude - Sandstorm (_underjoy) [0.9].osu │ │ │ ├── Don Huonot - Seireeni (Wh1teh) [!!].osu │ │ │ ├── Flashygoodness - Indignant Divinity (waffling) [Instant D BASIC].osu │ │ │ ├── FrenzyLi - Long Note Practice Pack (4K) (FrenzyLi) [ueotan - Mario Paint (Time Regression Mix for BMS) (LNHD)].osu │ │ │ ├── Hydria - Jackin' It To Anime OPs Pack 1 (Hydria) [Petit Rabbit's - Daydream cafe].osu │ │ │ ├── UNDEAD CORPORATION - The Empress scream off ver (TheZiemniax) [SC].osu │ │ │ ├── Various Artists - Dingles Speed Training Pack 1 (Dingles) [Lost One no Goukoku].osu │ │ │ ├── Various Artists - Dingles Speed Training Pack 1 (Dingles) [Oak Leaves].osu │ │ │ ├── Various Artists - I Like This Chart, LOL vol. 2 (Fullereneshift) [Eptic - Brainstorm (BG) (Marathon)].osu │ │ │ └── Various Artists - I Like This Chart, LOL vol. 2 (Fullereneshift) [Eptic - Brainstorm (IN) (Marathon)].osu │ │ └── sv_high_diff │ │ │ └── Tanchiky - Tenkai Rising (RiraN Psystyle Remix) (dionzz99) [Tachyon Rising].osu │ └── test │ │ ├── 14k_test.osu │ │ ├── 18k_test.osu │ │ ├── 1k_10x_0.25_chords.osu │ │ ├── 2k_10x_0.25_chords.osu │ │ ├── 3k_10x_0.25_chords.osu │ │ ├── 4k_10x_0.25_chords.osu │ │ ├── 5k_10x_0.25_chords.osu │ │ ├── 6k_10x_0.25_chords.osu │ │ ├── 7k_10x_0.25_chords.osu │ │ ├── 8k_holds.osu │ │ ├── 8k_mixed_timing_jacks.osu │ │ ├── altjacks_250ms.osu │ │ ├── chords_250ms.osu │ │ ├── fullalt_250ms.osu │ │ ├── jumptrill_250ms.osu │ │ ├── ladder_shift0_250ms.osu │ │ ├── ladder_shift1_250ms.osu │ │ ├── minijack_rampup.osu │ │ ├── misc_streams_250ms.osu │ │ ├── quadjacks_250ms.osu │ │ ├── shiftchords_250ms.osu │ │ └── trill_250ms.osu └── osu │ ├── playable │ ├── 07th Expansion - Aci-L (AngelHoney) [ignore's Insane].osu │ ├── Black Hole - Pluto (7odoa) [Difficult].osu │ ├── Dj Raaban - Anima Libera (ReMiX) (lthefuryl) [Champion].osu │ ├── Hatsuki Yura - Yoiyami Hanabi (Lan wings) [Lan].osu │ ├── Knife Party - Centipede (Sugoi-_-Desu) [This isn't a map, just a simple visualisation].osu │ ├── LeaF - I (Maddy) [Terror].osu │ ├── MiddleIsland - Roze (Lan wings) [Lan].osu │ ├── Mutsuhiko Izumi - Red Goose (nold_1702) [ERT Basic].osu │ ├── Nakamura Meiko - Aka no Ha (Lily Bread) [Extra].osu │ ├── Nico Nico Douga - Owens (AngelHoney) [Another].osu │ ├── O-Life Japan - Yakujinsama no Couple Dance (AngelHoney) [Lunatic].osu │ ├── Sharlo & yealina - Kakushigoto (Sharlo) [RLC's Extra].osu │ ├── Stefy Nrg - It's a Fable (Nightcore Mix) (osuplayer111) [Insane].osu │ ├── The Ghost Of 3.13 - Forgotten (Blue Dragon) [grumd].osu │ ├── The Yogscast - Diggy Diggy Hole (- N a n a k a -) [Diggy Gold].osu │ ├── Within Temptation - The Unforgiving (Armin) [Marathon].osu │ ├── capsule - JUMPER (Mafiamaster) [Insane].osu │ ├── goreshit - o'er the flood (grumd) [The Flood Beneath].osu │ └── goreshit - semantic compositions on death and its meaning (grumd) [Mortem Sensum].osu │ └── test │ ├── abraker - unknown (abraker) [250ms].1.osu │ ├── abraker - unknown (abraker) [250ms].osu │ ├── accel_change_impact_test.osu │ ├── accel_change_impact_test_2.osu │ ├── agility_test.osu │ ├── linear_agility.osu │ ├── linear_targeting_test.osu │ ├── rotation.osu │ ├── score_test.osu │ ├── silence (abraker) [Beat].osu │ ├── sqaure_jumps.osu │ ├── stack_jumps.osu │ ├── stream.osu │ ├── targeting_test.osu │ └── three_note_test.osu ├── replays ├── mania │ ├── abraker - DJ Genericname - Dear You [S.Star's 4K HD+] (2017-11-25) OsuMania.osr │ ├── abraker - DJ Genericname - Dear You [S.Star's 4K HD+] (2020-04-25) OsuMania.osr │ └── osu!topus! - DJ Genericname - Dear You [S.Star's 4K HD+] (2019-05-29) OsuMania.osr └── osu │ ├── LeaF - I (Maddy) [Terror] replay_0.osr │ ├── Toy - Within Temptation - The Unforgiving [Marathon] (2018-02-06) Osu.osr │ ├── abraker - Mutsuhiko Izumi - Red Goose [ERT Basic] (2019-08-24) Osu.osr │ ├── abraker - aim_miss [score_test] (2019-06-07) Osu.osr │ ├── abraker - both_keys_mouse_test [score_test] (2019-06-07) Osu.osr │ ├── abraker - both_keys_tap [score_test] (2019-06-07) Osu.osr │ ├── abraker - double_tap [score_test] (2019-06-07) Osu.osr │ ├── abraker - early_press [score_test] (2019-06-07) Osu.osr │ ├── abraker - first_note_miss [score_test] (2019-06-07) Osu.osr │ ├── abraker - last_note_miss [score_test] (2019-06-07) Osu.osr │ ├── abraker - mid_note_miss [score_test] (2019-06-07) Osu.osr │ ├── abraker - no_press [score_test] (2019-06-07) Osu.osr │ ├── abraker - random_miss [score_test] (2019-06-07) Osu.osr │ ├── abraker - rapid_press [score_test] (2019-06-07) Osu.osr │ ├── abraker - ss_test [score_test] (2019-06-07) Osu.osr │ ├── osu! - perfect_test [score_test] (2019-06-07) Osu.osr │ ├── score_test │ ├── aim_end_of_sliders.osr │ ├── autopilot.osr │ ├── autoplay.osr │ ├── best_play.osr │ ├── key_hold.osr │ ├── key_press_while_hold.osr │ ├── key_spam.osr │ ├── mis_aim_release.osr │ ├── miss_aim_every_other_note.osr │ ├── poor_aim.osr │ ├── press_out_of_order.osr │ ├── relax.osr │ ├── shaking_cursor_crazy.osr │ └── way_early_hit.osr │ ├── so bad - Nakamura Meiko - Aka no Ha [Extra] (2020-03-01) std Osu.osr │ └── so bad - Nakamura Meiko - Aka no Ha [Extra] (2020-03-01) std ripple.osr ├── test_beatmap.py ├── test_callback.py ├── test_collections.py ├── test_graph.py ├── test_graph_manager.py ├── test_manager_switch.py ├── test_mania_action_data.py ├── test_mania_layers.py ├── test_mania_metric_data.py ├── test_mania_score_data.py ├── test_mania_score_data_press.py ├── test_replay.py ├── test_std_map_data.py ├── test_std_map_metrics.py ├── test_std_map_patterns.py ├── test_std_replay_data.py ├── test_std_replay_visualization.py ├── test_std_score_data.py ├── test_std_score_data_free.py ├── test_std_score_data_hold.py ├── test_std_score_data_press.py ├── test_std_score_data_release.py ├── test_std_score_metrics.py └── test_timing_plot.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | tmp/ 4 | /docs/_build/* 5 | /download/* 6 | /venv/* 7 | /osu/online/login.py 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 abraker95 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | Python rewrite of my [old osu analyzer](https://github.com/abraker95/osu-Replay-Analyzer) that aims to be a lot more useful 4 | 5 | The Ultimate osu! Analyzer is a gui for mass analysis of data. It's been made with the intent to collect beatmaps and replays to analyze correlations between patterns and player performance. 6 | 7 | See the [tutorials](https://github.com/abraker95/ultimate_osu_analyzer/wiki/Tutorials) for info on how to use the analyzer and [documentation](https://ultimate-osu-analyzer.readthedocs.io/en/latest/source/analysis/analysis.osu.std.html) regarding various specifics. 8 | 9 | ## Run/Install 10 | 11 | Download the repo. You can do so by "Clone or download" button on the right of the main page: 12 | 13 | ![](https://i.imgur.com/XV6jqen.png) 14 | 15 | After downloading, extract the "ultimate_osu_analyzer-master" folder to your desired location. Next you will need to open cmd console. You can do so by typing in "cmd" in the address bar in the app folder and hitting enter: 16 | 17 | ![](https://i.imgur.com/QF3DYuC.png) 18 | 19 | If you have multiple Python versions installed, commands may differ slightly from min. But generally it will be either or similar variant of `pip`, `pip3`, or `pip37` and `python`, `python3`, or `python37`. If you still have issues running pip with multiple python versions installed, you can also do `python3 -m pip` as a replacement for `pip`. If that still doesn't work try `py -m pip`. 20 | 21 | First make sure you have all the needed libraries by running the command `pip install -r requirements.txt`. This will download a bunch of libraries needed to run the app. 22 | 23 | Run the app by running the command `python run.py` 24 | 25 | ![](https://i.imgur.com/dvlOPhX.png) 26 | 27 | ![](https://i.imgur.com/TyszCFI.png) 28 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-modernist -------------------------------------------------------------------------------- /analysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/analysis/__init__.py -------------------------------------------------------------------------------- /analysis/osu/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/analysis/osu/__init__.py -------------------------------------------------------------------------------- /analysis/osu/catch/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/analysis/osu/catch/__init__.py -------------------------------------------------------------------------------- /analysis/osu/catch/map_data.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from misc.numpy_utils import NumpyUtils 3 | 4 | 5 | class MapData(): 6 | 7 | TIME = 0 8 | POS = 1 9 | 10 | @staticmethod 11 | def get_data_before(hitobject_data, time): 12 | idx_time = hitobject_data.get_idx_start_time(time) 13 | 14 | if not idx_time: return None 15 | if idx_time < 1: return None 16 | 17 | return hitobject_data.hitobject_data[idx_time - 1][-1] 18 | 19 | 20 | @staticmethod 21 | def get_data_after(hitobject_data, time): 22 | idx_time = hitobject_data.get_idx_end_time(time) 23 | 24 | if not idx_time: return None 25 | if idx_time > len(hitobject_data) - 2: return None 26 | 27 | return hitobject_data.hitobject_data[idx_time + 1][0] 28 | 29 | 30 | @staticmethod 31 | def time_slice(hitobject_data, start_time, end_time): 32 | start_idx = hitobject_data.get_idx_start_time(start_time) 33 | end_idx = hitobject_data.get_idx_end_time(end_time) 34 | 35 | return hitobject_data.hitobject_data[start_idx:end_idx] 36 | 37 | 38 | ''' 39 | [ 40 | [ time, pos ], 41 | [ time, pos ], 42 | ... N fruits 43 | ] 44 | ''' 45 | def __init__(self): 46 | self.set_data_raw([]) 47 | 48 | 49 | def __len__(self): 50 | return len(self.hitobject_data) 51 | 52 | 53 | def set_data_hitobjects(self, hitobjects): 54 | self.hitobject_data = [ hitobject.raw_data() for hitobject in hitobjects ] 55 | return self 56 | 57 | 58 | def set_data_raw(self, raw_data): 59 | self.hitobject_data = raw_data 60 | return self 61 | 62 | 63 | MapData.full_hitobject_data = MapData() -------------------------------------------------------------------------------- /analysis/osu/mania/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/analysis/osu/mania/__init__.py -------------------------------------------------------------------------------- /analysis/osu/mania/map_data.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from osu.local.hitobject.mania.mania import Mania 4 | from misc.numpy_utils import NumpyUtils 5 | 6 | 7 | 8 | class ManiaMapData(): 9 | 10 | START_TIME = 0 11 | END_TIME = 1 12 | 13 | @staticmethod 14 | def get_hitobject_data(hitobjects): 15 | """ 16 | [ 17 | [ [ time_start, time_end ], [ time_start, time_end ], ... N notes in col ], 18 | [ [ time_start, time_end ], [ time_start, time_end ], ... N notes in col ], 19 | ... N col 20 | ] 21 | """ 22 | hitobject_data = list([ [] for _ in range(len(hitobjects)) ]) 23 | 24 | for column, column_hitobjects in zip(range(len(hitobjects)), hitobjects): 25 | for hitobject in column_hitobjects: 26 | hitobject_data[column].append([hitobject.time, hitobject.get_end_time()]) 27 | 28 | return hitobject_data 29 | 30 | 31 | @staticmethod 32 | def start_times(hitobject_data, column=None): 33 | if column == None: return np.sort(np.asarray([ hitobject[ManiaMapData.START_TIME] for column in hitobject_data for hitobject in column ])) 34 | else: return np.asarray([ hitobject[ManiaMapData.START_TIME] for hitobject in hitobject_data[column] ]) 35 | 36 | 37 | @staticmethod 38 | def end_times(hitobject_data, column=None): 39 | if column == None: return np.sort(np.asarray([ hitobject[ManiaMapData.END_TIME] for column in hitobject_data for hitobject in column ])) 40 | else: return np.asarray([ hitobject[ManiaMapData.END_TIME] for hitobject in hitobject_data[column] ]) 41 | 42 | 43 | @staticmethod 44 | def all_times(flat=True): 45 | # TODO 46 | if flat: return np.asarray([ ]) 47 | else: return [ ] 48 | 49 | 50 | @staticmethod 51 | def start_end_times(hitobject_data, column): 52 | return np.asarray(hitobject_data[column]) 53 | 54 | 55 | @staticmethod 56 | def get_idx_start_time(hitobject_data, column, time): 57 | if not time: return None 58 | 59 | times = ManiaMapData.start_times(hitobject_data, column) 60 | return min(max(0, np.searchsorted(times, [time], side='right')[0] - 1), len(times)) 61 | 62 | 63 | @staticmethod 64 | def get_idx_end_time(hitobject_data, column, time): 65 | if not time: return None 66 | 67 | times = ManiaMapData.end_times(hitobject_data, column) 68 | return min(max(0, np.searchsorted(times, [time], side='right')[0] - 1), len(times)) -------------------------------------------------------------------------------- /analysis/osu/std/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/analysis/osu/std/__init__.py -------------------------------------------------------------------------------- /cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/cli/__init__.py -------------------------------------------------------------------------------- /cli/cmd_online.py: -------------------------------------------------------------------------------- 1 | from osu.online.osu_online import OsuOnline 2 | from osu.online.osu_api import OsuApi 3 | from osu.online.structs.web_structs import * 4 | from osu.local.enums import MapStatus 5 | 6 | 7 | class CmdOnline(): 8 | 9 | @staticmethod 10 | def get_latest_beatmapsets(gamemode, status=MapStatus.Ranked): 11 | beatmapsets = OsuOnline.fetch_latest_beatmapsets(gamemode, status) 12 | return [ WebBeatmapset(beatmapset) for beatmapset in beatmapsets ] 13 | 14 | 15 | @staticmethod 16 | def get_beatmap(beatmap_id): 17 | return OsuOnline.fetch_beatmap_file(beatmap_id, ret_name=True) 18 | 19 | 20 | @staticmethod 21 | def get_scores(beatmap_id, mode, name=None): 22 | if name == None: name = OsuOnline.fetch_beatmap_name(beatmap_id) 23 | return [ WebScore(name, score) for score in OsuOnline.fetch_scores(beatmap_id, mode) ] 24 | 25 | 26 | @staticmethod 27 | def get_scores_api(beatmap_id, mode, mods, name=None): 28 | if name == None: name = OsuOnline.fetch_beatmap_name(beatmap_id) 29 | return [ APIv1Score(name, mode, score) for score in OsuApi.fetch_score_info(beatmap_id, gamemode=mode, mods=mods) ] 30 | 31 | 32 | @staticmethod 33 | def get_scores_from_beatmap(beatmap): 34 | return CmdOnline.get_scores(beatmap.metadata.beatmap_id, beatmap.gamemode, beatmap.metadata.name) -------------------------------------------------------------------------------- /cli/cmd_osu.py: -------------------------------------------------------------------------------- 1 | import time 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | from osu.local.beatmap.beatmapIO import BeatmapIO 6 | from osu.local.replay.replayIO import ReplayIO 7 | 8 | from misc.callback import callback 9 | 10 | 11 | class CmdOsu(): 12 | 13 | @staticmethod 14 | def open_replay_file(replay_filepath): 15 | return ReplayIO.open_replay(replay_filepath) 16 | 17 | 18 | @staticmethod 19 | def save_web_beatmaps(web_beatmaps): 20 | ''' 21 | In: [ WebBeatmap, ... ] 22 | ''' 23 | for web_beatmap in web_beatmaps: 24 | web_beatmap.download_beatmap('tmp/beatmaps/') 25 | time.sleep(0.1) 26 | 27 | 28 | @staticmethod 29 | def save_web_replays(web_replays): 30 | ''' 31 | In: [ WebReplay, ... ] 32 | ''' 33 | for web_replay in web_replays: 34 | web_replay.download_replay('tmp/replays/') 35 | time.sleep(10) 36 | 37 | 38 | @staticmethod 39 | def show_cursor_hit_offset_histogram(score_data, beatmap): 40 | xy = np.dstack(score_data[:,3]) 41 | dists = np.sqrt(xy[:,0][0]**2 + xy[:,1][0]**2) 42 | 43 | plt.hist(dists, 20) 44 | plt.xlabel('offset from center (osu!px)') 45 | plt.ylabel('number of hits') 46 | plt.title('Top 50 players cursor offsets\n' + str(beatmap.metadata.name)) -------------------------------------------------------------------------------- /cli/cmd_utils.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import numpy as np 3 | 4 | 5 | class CmdUtils(): 6 | 7 | ''' 8 | Use: 9 | result = threaded(func, (param,)) 10 | ''' 11 | @staticmethod 12 | def threaded(func, args): 13 | class ThreadedResult(): 14 | def __init__(self): 15 | self._has_result = False 16 | self._result = None 17 | 18 | def set(self, val): 19 | if self._has_result: 20 | raise ValueError('Cannot set result more than once') 21 | self._result = val 22 | self._has_result = True 23 | 24 | def get(self): 25 | if not self._has_result: 26 | raise ValueError('Threaded operation not finished') 27 | else: return self._result 28 | 29 | 30 | def wrap(func, args, result): 31 | result.set(func(*args)) 32 | 33 | print('-----------------------') 34 | print('DONE') 35 | 36 | 37 | result = ThreadedResult() 38 | threading.Thread(target=wrap, args=(func, args, result)).start() 39 | return result 40 | 41 | 42 | # TODO 43 | @staticmethod 44 | def console_help(): 45 | string = 'Available vars: \ 46 | timeline, get_beatmap(), ' 47 | 48 | #self.ipython_console.print_text('Available vars: ') 49 | 50 | 51 | @staticmethod 52 | def print_numbered_list(lst): 53 | for i in range(len(lst)): 54 | print(i, lst[i]) 55 | 56 | 57 | @staticmethod 58 | def run_script(filepath, globals=None, locals=None): 59 | if globals is None: globals = {} 60 | globals.update({ 61 | '__file__': filepath, 62 | '__name__': '__main__', 63 | }) 64 | 65 | with open(filepath, 'rb') as file: 66 | exec(compile(file.read(), filepath, 'exec'), globals, locals) 67 | 68 | 69 | @staticmethod 70 | def export_csv(filepath, data): 71 | np.savetxt(filepath, np.asarray(data).T, delimiter=',', newline='\n', fmt='%f') 72 | 73 | 74 | @staticmethod 75 | def import_csv(filepath): 76 | return np.loadtxt(open(filepath, 'rb'), delimiter=',', skiprows=0) -------------------------------------------------------------------------------- /core/gamemode_manager.py: -------------------------------------------------------------------------------- 1 | from generic.switcher import Switcher 2 | from core.metric_manager import MetricManager 3 | from osu.local.beatmap.beatmap import Beatmap 4 | 5 | 6 | class GamemodeManager(Switcher): 7 | 8 | def __init__(self): 9 | Switcher.__init__(self) 10 | 11 | self.add(Beatmap.GAMEMODE_OSU, MetricManager()) 12 | self.add(Beatmap.GAMEMODE_MANIA, MetricManager()) 13 | self.add(Beatmap.GAMEMODE_TAIKO, MetricManager()) 14 | self.add(Beatmap.GAMEMODE_CATCH, MetricManager()) 15 | 16 | 17 | def get_metric_lib(self, gamemode): 18 | return self.data[gamemode] 19 | 20 | 21 | def set_gamemode(self, gamemode): 22 | self.switch(gamemode) 23 | 24 | 25 | gamemode_manager = GamemodeManager() -------------------------------------------------------------------------------- /core/graph_manager.py: -------------------------------------------------------------------------------- 1 | from misc.callback import callback 2 | from gui.objects.group import Group 3 | 4 | 5 | class GraphManager(Group): 6 | 7 | def __init__(self): 8 | Group.__init__(self, 'root', self) 9 | 10 | 11 | @callback 12 | def add_graph(self, graph_name, graph): 13 | self.add_elem(graph_name, graph) 14 | 15 | 16 | @callback 17 | def rmv_graph(self, graph_name): 18 | self.rmv_elem(graph_name) -------------------------------------------------------------------------------- /core/metric_manager.py: -------------------------------------------------------------------------------- 1 | from misc.callback import callback 2 | from gui.objects.group import Group 3 | 4 | 5 | class MetricManager(Group): 6 | 7 | def __init__(self): 8 | Group.__init__(self, 'root', self) 9 | 10 | 11 | @callback 12 | def add_metric(self, metric_name, metric, path=None): 13 | if not path: group = self 14 | else: 15 | names = path.split('.') 16 | group = self.child(names[0]) 17 | 18 | for name in names[1:]: 19 | group = self.child(name) 20 | 21 | group.add_elem(metric_name, metric) 22 | 23 | 24 | @callback 25 | def rmv_metric(self, metric_name, path=None): 26 | if not path: group = self 27 | else: 28 | names = path.split('.') 29 | group = self.child(names[0]) 30 | 31 | for name in names[1:]: 32 | group = group.child(name) 33 | 34 | group.rmv_elem(metric_name) -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import sphinx_rtd_theme 14 | import os 15 | import sys 16 | 17 | sys.path.insert(0, os.path.abspath('..')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'ultimate_osu_analyzer' 23 | copyright = '2019, abraker95' 24 | author = 'abraker95' 25 | 26 | 27 | # -- General configuration --------------------------------------------------- 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.napoleon', 35 | 'sphinx_rtd_theme', 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # List of patterns, relative to source directory, that match files and 42 | # directories to ignore when looking for source files. 43 | # This pattern also affects html_static_path and html_extra_path. 44 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 45 | 46 | master_doc = 'index' 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = 'sphinx_rtd_theme' 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | html_static_path = ['_static'] -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. ultimate_osu_analyzer documentation master file, created by 2 | sphinx-quickstart on Sat Nov 23 13:06:58 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to ultimate_osu_analyzer's documentation! 7 | ================================================= 8 | 9 | .. toctree:: 10 | :glob: 11 | :caption: Tutorials: 12 | 13 | tutorials/* 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | sphinx-apidoc -o source/analysis ../analysis 4 | sphinx-apidoc -o source/cli ../cli 5 | sphinx-apidoc -o source/gui ../gui 6 | sphinx-apidoc -o source/osu ../osu 7 | 8 | pushd %~dp0 9 | 10 | REM Command file for Sphinx documentation 11 | 12 | if "%SPHINXBUILD%" == "" ( 13 | set SPHINXBUILD=sphinx-build 14 | ) 15 | set SOURCEDIR=. 16 | set BUILDDIR=_build 17 | 18 | if "%1" == "" goto help 19 | 20 | %SPHINXBUILD% >NUL 2>NUL 21 | if errorlevel 9009 ( 22 | echo. 23 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 24 | echo.installed, then set the SPHINXBUILD environment variable to point 25 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 26 | echo.may add the Sphinx directory to PATH. 27 | echo. 28 | echo.If you don't have Sphinx installed, grab it from 29 | echo.http://sphinx-doc.org/ 30 | exit /b 1 31 | ) 32 | 33 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 34 | goto end 35 | 36 | :help 37 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 38 | 39 | :end 40 | popd 41 | -------------------------------------------------------------------------------- /docs/source/analysis/analysis.osu.catch.rst: -------------------------------------------------------------------------------- 1 | analysis.osu.catch package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | analysis.osu.catch.map\_data module 8 | ----------------------------------- 9 | 10 | .. automodule:: analysis.osu.catch.map_data 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: analysis.osu.catch 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/analysis/analysis.osu.mania.rst: -------------------------------------------------------------------------------- 1 | analysis.osu.mania package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | analysis.osu.mania.map\_data module 8 | ----------------------------------- 9 | 10 | .. automodule:: analysis.osu.mania.map_data 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | analysis.osu.mania.map\_metrics module 16 | -------------------------------------- 17 | 18 | .. automodule:: analysis.osu.mania.map_metrics 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | analysis.osu.mania.replay\_data module 24 | -------------------------------------- 25 | 26 | .. automodule:: analysis.osu.mania.replay_data 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | analysis.osu.mania.score\_data module 32 | ------------------------------------- 33 | 34 | .. automodule:: analysis.osu.mania.score_data 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | Module contents 41 | --------------- 42 | 43 | .. automodule:: analysis.osu.mania 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /docs/source/analysis/analysis.osu.rst: -------------------------------------------------------------------------------- 1 | analysis.osu package 2 | ==================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | analysis.osu.catch 10 | analysis.osu.mania 11 | analysis.osu.std 12 | 13 | Module contents 14 | --------------- 15 | 16 | .. automodule:: analysis.osu 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | -------------------------------------------------------------------------------- /docs/source/analysis/analysis.osu.std.rst: -------------------------------------------------------------------------------- 1 | analysis.osu.std package 2 | ======================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | analysis.osu.std.map\_data module 8 | --------------------------------- 9 | 10 | .. automodule:: analysis.osu.std.map_data 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | analysis.osu.std.map\_metrics module 16 | ------------------------------------ 17 | 18 | .. automodule:: analysis.osu.std.map_metrics 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | analysis.osu.std.replay\_data module 24 | ------------------------------------ 25 | 26 | .. automodule:: analysis.osu.std.replay_data 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | analysis.osu.std.replay\_metrics module 32 | --------------------------------------- 33 | 34 | .. automodule:: analysis.osu.std.replay_metrics 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | analysis.osu.std.score\_data module 40 | ----------------------------------- 41 | 42 | .. automodule:: analysis.osu.std.score_data 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | analysis.osu.std.score\_metrics module 48 | -------------------------------------- 49 | 50 | .. automodule:: analysis.osu.std.score_metrics 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | analysis.osu.std.std\_layers module 56 | ----------------------------------- 57 | 58 | .. automodule:: analysis.osu.std.std_layers 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | 64 | Module contents 65 | --------------- 66 | 67 | .. automodule:: analysis.osu.std 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | -------------------------------------------------------------------------------- /docs/source/analysis/analysis.rst: -------------------------------------------------------------------------------- 1 | analysis package 2 | ================ 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | analysis.osu 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: analysis 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/source/analysis/modules.rst: -------------------------------------------------------------------------------- 1 | analysis 2 | ======== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | analysis 8 | -------------------------------------------------------------------------------- /docs/source/cli/cli.rst: -------------------------------------------------------------------------------- 1 | cli package 2 | =========== 3 | 4 | Submodules 5 | ---------- 6 | 7 | cli.cmd\_online module 8 | ---------------------- 9 | 10 | .. automodule:: cli.cmd_online 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | cli.cmd\_osu module 16 | ------------------- 17 | 18 | .. automodule:: cli.cmd_osu 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | cli.cmd\_utils module 24 | --------------------- 25 | 26 | .. automodule:: cli.cmd_utils 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: cli 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/source/cli/modules.rst: -------------------------------------------------------------------------------- 1 | cli 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | cli 8 | -------------------------------------------------------------------------------- /docs/source/gui/gui.objects.graph.rst: -------------------------------------------------------------------------------- 1 | gui.objects.graph package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | gui.objects.graph.hitobject\_plot module 8 | ---------------------------------------- 9 | 10 | .. automodule:: gui.objects.graph.hitobject_plot 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | gui.objects.graph.line\_plot module 16 | ----------------------------------- 17 | 18 | .. automodule:: gui.objects.graph.line_plot 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | gui.objects.graph.scatter\_plot module 24 | -------------------------------------- 25 | 26 | .. automodule:: gui.objects.graph.scatter_plot 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: gui.objects.graph 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/source/gui/gui.objects.layer.rst: -------------------------------------------------------------------------------- 1 | gui.objects.layer package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | gui.objects.layer.layer module 8 | ------------------------------ 9 | 10 | .. automodule:: gui.objects.layer.layer 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | gui.objects.layer.layer\_manager module 16 | --------------------------------------- 17 | 18 | .. automodule:: gui.objects.layer.layer_manager 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: gui.objects.layer 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/gui/gui.objects.rst: -------------------------------------------------------------------------------- 1 | gui.objects package 2 | =================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | gui.objects.graph 10 | gui.objects.layer 11 | 12 | Submodules 13 | ---------- 14 | 15 | gui.objects.display module 16 | -------------------------- 17 | 18 | .. automodule:: gui.objects.display 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | gui.objects.group module 24 | ------------------------ 25 | 26 | .. automodule:: gui.objects.group 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | gui.objects.scene module 32 | ------------------------ 33 | 34 | .. automodule:: gui.objects.scene 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | 40 | Module contents 41 | --------------- 42 | 43 | .. automodule:: gui.objects 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | -------------------------------------------------------------------------------- /docs/source/gui/gui.rst: -------------------------------------------------------------------------------- 1 | gui package 2 | =========== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | gui.objects 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: gui 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/source/gui/modules.rst: -------------------------------------------------------------------------------- 1 | gui 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | gui 8 | -------------------------------------------------------------------------------- /docs/source/osu/modules.rst: -------------------------------------------------------------------------------- 1 | osu 2 | === 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | online 8 | -------------------------------------------------------------------------------- /docs/source/osu/online.rst: -------------------------------------------------------------------------------- 1 | online package 2 | ============== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | online.structs 10 | 11 | Submodules 12 | ---------- 13 | 14 | online.login module 15 | ------------------- 16 | 17 | .. automodule:: online.login 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | online.osu\_api module 23 | ---------------------- 24 | 25 | .. automodule:: online.osu_api 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | online.osu\_api\_v2 module 31 | -------------------------- 32 | 33 | .. automodule:: online.osu_api_v2 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | online.osu\_online module 39 | ------------------------- 40 | 41 | .. automodule:: online.osu_online 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | online.rate\_limited module 47 | --------------------------- 48 | 49 | .. automodule:: online.rate_limited 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | online.session\_manager module 55 | ------------------------------ 56 | 57 | .. automodule:: online.session_manager 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | 63 | Module contents 64 | --------------- 65 | 66 | .. automodule:: online 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | -------------------------------------------------------------------------------- /docs/source/osu/online.structs.rst: -------------------------------------------------------------------------------- 1 | online.structs package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | online.structs.web\_structs module 8 | ---------------------------------- 9 | 10 | .. automodule:: online.structs.web_structs 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: online.structs 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/osu/osu.online.rst: -------------------------------------------------------------------------------- 1 | osu.online package 2 | ================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | osu.online.structs 10 | 11 | Submodules 12 | ---------- 13 | 14 | osu.online.login module 15 | ----------------------- 16 | 17 | .. automodule:: osu.online.login 18 | :members: 19 | :undoc-members: 20 | :show-inheritance: 21 | 22 | osu.online.osu\_api module 23 | -------------------------- 24 | 25 | .. automodule:: osu.online.osu_api 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | osu.online.osu\_api\_v2 module 31 | ------------------------------ 32 | 33 | .. automodule:: osu.online.osu_api_v2 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | osu.online.osu\_online module 39 | ----------------------------- 40 | 41 | .. automodule:: osu.online.osu_online 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | osu.online.rate\_limited module 47 | ------------------------------- 48 | 49 | .. automodule:: osu.online.rate_limited 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | osu.online.session\_manager module 55 | ---------------------------------- 56 | 57 | .. automodule:: osu.online.session_manager 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | 63 | Module contents 64 | --------------- 65 | 66 | .. automodule:: osu.online 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | -------------------------------------------------------------------------------- /docs/source/osu/osu.online.structs.rst: -------------------------------------------------------------------------------- 1 | osu.online.structs package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | osu.online.structs.web\_structs module 8 | -------------------------------------- 9 | 10 | .. automodule:: osu.online.structs.web_structs 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: osu.online.structs 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/osu/osu.rst: -------------------------------------------------------------------------------- 1 | osu package 2 | =========== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | 9 | osu.online 10 | 11 | Module contents 12 | --------------- 13 | 14 | .. automodule:: osu 15 | :members: 16 | :undoc-members: 17 | :show-inheritance: 18 | -------------------------------------------------------------------------------- /docs/tutorials/analyzing_beatmaps_and_replays.rst: -------------------------------------------------------------------------------- 1 | .. analyzing_beatmaps_and_replays: 2 | 3 | Analyzing Beatmaps and Replays 4 | ============================== -------------------------------------------------------------------------------- /docs/tutorials/exploring_online_data.rst: -------------------------------------------------------------------------------- 1 | .. exploring_online_data: 2 | 3 | Exploring Online Data 4 | ===================== -------------------------------------------------------------------------------- /docs/tutorials/fetching_online_data.rst: -------------------------------------------------------------------------------- 1 | .. fetching_online_data: 2 | 3 | Fetching Online Data 4 | ==================== -------------------------------------------------------------------------------- /docs/tutorials/gettings_started.rst: -------------------------------------------------------------------------------- 1 | .. gettings_started: 2 | 3 | Getting Started 4 | =============== -------------------------------------------------------------------------------- /docs/tutorials/the_dynamic_environment.rst: -------------------------------------------------------------------------------- 1 | .. the_dynamic_environment: 2 | 3 | The Dynamic Environment 4 | ======================= -------------------------------------------------------------------------------- /generic/switcher.py: -------------------------------------------------------------------------------- 1 | from misc.callback import callback 2 | 3 | 4 | class Switcher(): 5 | 6 | def __init__(self): 7 | self.data = { None : None } 8 | self.active = None 9 | 10 | 11 | @callback 12 | def switch(self, key): 13 | if not key in self.data: 14 | return False 15 | 16 | self.switch.emit(self.data[self.active], self.data[key], inst=self) 17 | self.active = key 18 | return True 19 | 20 | 21 | def __len__(self): 22 | return len(self.data) 23 | 24 | 25 | def get(self): 26 | return self.data[self.active] 27 | 28 | 29 | def add(self, key, data, overwrite=True): 30 | if key not in self.data or overwrite: 31 | self.data[key] = data 32 | else: 33 | raise Exception('Data with key "' + str(key) + '" already exists') 34 | 35 | 36 | # TODO: Figure out what is the best way to switch to another key 37 | def rmv(self, key): 38 | if key not in self.data: return 39 | if self.active == key: 40 | # Switch to the first key if possible 41 | if len(self.data) > 1: self.switch(list(self.data.keys())[0]) 42 | else: self.switch(None) 43 | 44 | del self.data[key] -------------------------------------------------------------------------------- /generic/temporal.py: -------------------------------------------------------------------------------- 1 | from misc.callback import callback 2 | 3 | 4 | class Temporal(): 5 | 6 | def __init__(self): 7 | self.time = None 8 | 9 | 10 | @callback 11 | def time_changed(self, time): 12 | self.time = time 13 | self.time_changed.emit(time) -------------------------------------------------------------------------------- /gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/gui/__init__.py -------------------------------------------------------------------------------- /gui/frames/bottom_frame/bottom_frame.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from gui.widgets.timeline import Timeline 6 | 7 | 8 | class BottomFrame(QFrame): 9 | 10 | def __init__(self): 11 | super().__init__() 12 | 13 | self.init_gui_elements() 14 | self.construct_gui() 15 | self.update_gui() 16 | 17 | 18 | def init_gui_elements(self): 19 | self.layout = QHBoxLayout() 20 | self.label = QLabel("BottomFrame", self) 21 | self.timeline = Timeline() 22 | 23 | # TODO: Horizontal layer 24 | # TODO: label frame 25 | # TODO: graph frame 26 | # TODO: ctrl frame 27 | 28 | def construct_gui(self): 29 | self.setLayout(self.layout) 30 | self.layout.addWidget(self.timeline) 31 | 32 | 33 | def update_gui(self): 34 | self.setFrameShape(QFrame.StyledPanel) 35 | self.label.setAlignment(Qt.AlignCenter) -------------------------------------------------------------------------------- /gui/frames/bottom_frame/children/timeline.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Timeline widget 3 | 4 | Design: 5 | - The timeline contains time dependent data and allows scrolling between temporal views 6 | - The time dependent data is graphed and displayed as a background layer 7 | - Any type of time dependent graph should be able to be displayed 8 | - line graph, density, etc 9 | - Multiple graphs can be displayed at a time 10 | - On the left side is displayed the upper value and lower value for the data displayed by graph 11 | - Additional two values are displayed for upper and lower intensity when relevant 12 | 13 | Functionality: 14 | 15 | 16 | User interaction: 17 | Temporal interface: 18 | - user is able to drag a timeline-cursor across the timeline 19 | - mouse left button dragging 20 | - ctrl left/right keys for small step 21 | - left/right keys for normal step 22 | - shift left/right keys for large step 23 | - user is able to zoom in and out 24 | - mouse wheel 25 | - ctrl +, ctrl - 26 | - user is able to pan view via mouse left button dragging 27 | - user is able to double click to make the timeline-cursor jump to that point 28 | - user is able to select a range via mouse right button dragging 29 | 30 | Graph interface: 31 | - user is able to mouse over a point on graph to see value 32 | - user is able to resize y-axis 33 | - mouse dragging 34 | - mouse wheel 35 | - user is able to click on a point on graph to highlight a point 36 | - user is able to double click a point on graph to make the timeline-cursor jump to that point 37 | - user is able to select a range of data via mouse right button dragging 38 | - holding ctrl allows user to perform inclusive selection 39 | - holding shift allows user to perform exclusive selection 40 | - displayed graph transparency can be adjusted via spinbox 41 | 42 | Graph cycler interface: 43 | - TODO 44 | 45 | Output: 46 | 47 | 48 | ''' -------------------------------------------------------------------------------- /gui/frames/center_frame/center_frame.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from .children.left_frame import LeftFrame 6 | from .children.mid_frame import MidFrame 7 | from .children.right_frame import RightFrame 8 | 9 | 10 | 11 | class CenterFrame(QFrame): 12 | 13 | def __init__(self): 14 | super().__init__() 15 | 16 | self.init_gui_elements() 17 | self.construct_gui() 18 | self.update_gui() 19 | 20 | 21 | def init_gui_elements(self): 22 | self.layout = QVBoxLayout() 23 | self.splitter = QSplitter(Qt.Horizontal) 24 | self.left_frame = LeftFrame() 25 | self.mid_frame = MidFrame() 26 | self.right_frame = RightFrame() 27 | 28 | 29 | def construct_gui(self): 30 | self.setLayout(self.layout) 31 | 32 | self.layout.addWidget(self.splitter) 33 | self.splitter.addWidget(self.left_frame) 34 | self.splitter.addWidget(self.mid_frame) 35 | self.splitter.addWidget(self.right_frame) 36 | 37 | 38 | def update_gui(self): 39 | self.setFrameShape(QFrame.StyledPanel) 40 | self.splitter.setSizes([ self.width()/12, self.width()/3, self.width()/12 ]) -------------------------------------------------------------------------------- /gui/frames/center_frame/children/left_frame.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from gui.widgets.file_browser import FileBrowser 6 | from gui.widgets.collections_browser import CollectionsBrowser 7 | from gui.widgets.online_browser import OnlineBrowser 8 | 9 | 10 | class LeftFrame(QFrame): 11 | 12 | def __init__(self): 13 | super().__init__() 14 | 15 | self.init_gui_elements() 16 | self.construct_gui() 17 | self.update_gui() 18 | 19 | 20 | def init_gui_elements(self): 21 | self.layout = QHBoxLayout() 22 | self.tabs_area = QTabWidget() 23 | 24 | self.file_browser = FileBrowser() 25 | self.collections_browser = CollectionsBrowser() 26 | self.online_browser = OnlineBrowser() 27 | 28 | 29 | def construct_gui(self): 30 | self.setLayout(self.layout) 31 | 32 | self.tabs_area.addTab(self.file_browser, 'Files') 33 | self.tabs_area.addTab(self.collections_browser, 'Collections') 34 | self.tabs_area.addTab(self.online_browser, 'Online') 35 | self.layout.addWidget(self.tabs_area) 36 | 37 | 38 | def update_gui(self): 39 | self.setFrameShape(QFrame.StyledPanel) -------------------------------------------------------------------------------- /gui/frames/center_frame/children/mid_frame.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from gui.widgets.map_manager import MapManager 6 | from gui.objects.display import Display 7 | from misc.callback import callback 8 | 9 | 10 | 11 | class MidFrame(QFrame): 12 | 13 | def __init__(self): 14 | super().__init__() 15 | 16 | self.init_gui_elements() 17 | self.construct_gui() 18 | self.update_gui() 19 | 20 | 21 | def init_gui_elements(self): 22 | self.layout = QVBoxLayout() 23 | self.map_manager = MapManager() 24 | self.display = Display() 25 | 26 | 27 | def construct_gui(self): 28 | self.setLayout(self.layout) 29 | 30 | #self.map_manager.currentChanged.connect(self.map_changed_event) 31 | #self.map_manager.tabCloseRequested.connect(self.tab_closing) 32 | 33 | self.layout.addWidget(self.map_manager) 34 | self.layout.addWidget(self.display) 35 | 36 | # TODO: label showing cursor pos 37 | # TODO: label showing pos of selected object 38 | 39 | 40 | def update_gui(self): 41 | self.setFrameShape(QFrame.StyledPanel) 42 | self.map_manager.setMovable(True) 43 | self.map_manager.setTabsClosable(True) -------------------------------------------------------------------------------- /gui/frames/center_frame/children/right_frame.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from gui.widgets.manager_switch import ManagerSwitch 6 | from gui.widgets.ipython_console import IPythonConsole 7 | 8 | 9 | class RightFrame(QFrame): 10 | 11 | def __init__(self): 12 | super().__init__() 13 | 14 | self.init_gui_elements() 15 | self.construct_gui() 16 | self.update_gui() 17 | 18 | 19 | def init_gui_elements(self): 20 | self.layout = QHBoxLayout() 21 | self.tabs_area = QTabWidget() 22 | 23 | self.layer_manager_switch = ManagerSwitch() 24 | self.replay_manager_switch = ManagerSwitch() 25 | self.graph_manager_switch = ManagerSwitch() 26 | self.ipython_console = IPythonConsole() 27 | 28 | 29 | def construct_gui(self): 30 | self.setLayout(self.layout) 31 | 32 | self.tabs_area.addTab(self.layer_manager_switch, 'Layers') 33 | self.tabs_area.addTab(self.replay_manager_switch, 'Replays') 34 | self.tabs_area.addTab(self.graph_manager_switch, 'Graphs') 35 | self.tabs_area.addTab(self.ipython_console, 'Console') 36 | self.layout.addWidget(self.tabs_area) 37 | 38 | 39 | def update_gui(self): 40 | self.setFrameShape(QFrame.StyledPanel) 41 | 42 | -------------------------------------------------------------------------------- /gui/frames/main_frame.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from .center_frame.center_frame import CenterFrame 6 | from .bottom_frame.bottom_frame import BottomFrame 7 | 8 | 9 | class MainFrame(QWidget): 10 | 11 | def __init__(self): 12 | super().__init__() 13 | 14 | self.init_gui_elements() 15 | self.construct_gui() 16 | self.update_gui() 17 | 18 | 19 | def init_gui_elements(self): 20 | self.layout = QVBoxLayout() 21 | self.splitter = QSplitter(Qt.Vertical) 22 | self.center_frame = CenterFrame() 23 | self.bottom_frame = BottomFrame() 24 | 25 | 26 | def construct_gui(self): 27 | self.setLayout(self.layout) 28 | 29 | self.layout.addWidget(self.splitter) 30 | self.splitter.addWidget(self.center_frame) 31 | self.splitter.addWidget(self.bottom_frame) 32 | 33 | 34 | def update_gui(self): 35 | self.splitter.setSizes([ self.height(), self.height()/8 ]) -------------------------------------------------------------------------------- /gui/objects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/gui/objects/__init__.py -------------------------------------------------------------------------------- /gui/objects/display.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from misc.callback import callback 6 | 7 | 8 | 9 | class Display(QGraphicsView): 10 | 11 | def __init__(self): 12 | QGraphicsView.__init__(self) 13 | 14 | self.setStyleSheet('background-color: #FEFEFE') 15 | self.setAlignment(Qt.AlignTop | Qt.AlignLeft) 16 | self.scene = None -------------------------------------------------------------------------------- /gui/objects/graph/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/gui/objects/graph/__init__.py -------------------------------------------------------------------------------- /gui/objects/graph/aim_plot.py: -------------------------------------------------------------------------------- 1 | import pyqtgraph 2 | from pyqtgraph import QtCore, QtGui 3 | 4 | 5 | class AimPlot(pyqtgraph.GraphicsObject): 6 | def __init__(self, t, d, cs_px, yoffset=0, color=(100, 100, 255, 100)): 7 | pyqtgraph.GraphicsObject.__init__(self) 8 | 9 | self.t = t # times 10 | self.d = d # distances 11 | self.cs_px = cs_px # circle size in osu!px 12 | 13 | self.yoffset = yoffset 14 | self.color = color 15 | 16 | self.px_h = self.pixelHeight() 17 | self.px_w = self.pixelWidth() 18 | 19 | self.generatePicture() 20 | 21 | 22 | def update_data(self, t, d, cs_px): 23 | self.t = t # times 24 | self.d = d # distances 25 | self.cs_px = cs_px # circle size in osu!px 26 | 27 | self.px_h = self.pixelHeight() 28 | self.px_w = self.pixelWidth() 29 | 30 | self.generatePicture() 31 | 32 | 33 | def generatePicture(self): 34 | ## pre-computing a QPicture object allows paint() to run much more quickly, 35 | ## rather than re-drawing the shapes every time. 36 | self.picture = QtGui.QPicture() 37 | 38 | p = QtGui.QPainter(self.picture) 39 | p.setPen(pyqtgraph.mkPen(color=self.color)) 40 | 41 | # This 20x aligns it right on the boundary of slider rendering in hitobject_plot if yoffset is +1 or -1 42 | y_mid = 25*self.px_h*self.yoffset 43 | 44 | radius_x = 20*self.px_w/self.cs_px 45 | radius_y = 20*self.px_h/self.cs_px 46 | 47 | # Render 48 | for t, d in zip(self.t, self.d): 49 | p.drawEllipse(QtCore.QPointF(t, y_mid), d*radius_x, d*radius_y) 50 | 51 | p.end() 52 | 53 | 54 | def paint(self, p, *args): 55 | p.drawPicture(0, 0, self.picture) 56 | 57 | 58 | def boundingRect(self): 59 | ## boundingRect _must_ indicate the entire area that will be drawn on 60 | ## or else we will get artifacts and possibly crashing. 61 | ## (in this case, QPicture does all the work of computing the bouning rect for us) 62 | return QtCore.QRectF(self.picture.boundingRect()) 63 | 64 | 65 | def viewRangeChanged(self): 66 | """ 67 | Called whenever the view coordinates of the ViewBox containing this item have changed. 68 | """ 69 | px_h = self.pixelHeight() 70 | px_w = self.pixelWidth() 71 | 72 | # Without pixel_height the render scales with how the view is zoomed in/out 73 | if self.px_h != px_h or self.px_w != px_w: 74 | self.px_h = px_h 75 | self.px_w = px_w 76 | 77 | self.generatePicture() -------------------------------------------------------------------------------- /gui/objects/graph/bar_plot.py: -------------------------------------------------------------------------------- 1 | from pyqtgraph.Qt import QtGui, QtCore 2 | import pyqtgraph 3 | 4 | import numpy as np 5 | 6 | 7 | 8 | class BarPlot(pyqtgraph.BarGraphItem): 9 | 10 | def __init__(self): 11 | super().__init__() 12 | 13 | 14 | def update_data(self, num_bins, data_y, color='b'): 15 | if type(num_bins) == type(None) or type(data_y) == type(None): 16 | self.setOpts(**{ 17 | 'x' : [], 18 | 'height' : [], 19 | 'width' : 0.5 20 | }) 21 | return 22 | 23 | # Filter out infinities 24 | data_y = data_y[np.isfinite(data_y.astype(np.float64))] 25 | 26 | # Calculate histogram data 27 | data_y, data_x = np.histogram(data_y, num_bins) 28 | data_x = (data_x[1:] + data_x[:-1])/2 29 | width = (data_x[-1] - data_x[0])/len(data_x) 30 | 31 | self.setOpts(**{ 32 | 'x' : data_x, 33 | 'height' : data_y, 34 | 'width' : width 35 | }) 36 | 37 | return data_x, data_y -------------------------------------------------------------------------------- /gui/objects/graph/hitobject_plot.py: -------------------------------------------------------------------------------- 1 | import pyqtgraph 2 | import numpy as np 3 | from pyqtgraph import QtCore, QtGui 4 | 5 | from analysis.osu.std.map_data import StdMapData 6 | 7 | 8 | class HitobjectPlot(pyqtgraph.GraphItem): 9 | 10 | HITOBJECT_RADIUS = 40 11 | 12 | def __init__(self, start_times, end_times, h_types, y_pos=0): 13 | pyqtgraph.GraphItem.__init__(self) 14 | pyqtgraph.setConfigOptions(antialias=True) 15 | 16 | self.pen = pyqtgraph.mkPen(width=HitobjectPlot.HITOBJECT_RADIUS) 17 | self.pen.setCapStyle(QtCore.Qt.RoundCap) 18 | self.setPen(self.pen) 19 | 20 | self.update_data(start_times, end_times, h_types, y_pos) 21 | 22 | 23 | def update_data(self, start_times, end_times, h_types, y_pos=0): 24 | try: 25 | if len(start_times) == 0 or len(end_times) == 0 or len(h_types) == 0: 26 | self.scatter.clear() 27 | self.pos = None 28 | return 29 | except ValueError: return 30 | 31 | pos = [] 32 | adj = [] 33 | size = [] 34 | 35 | obj_num = -1 36 | 37 | for start_time, end_time, h_type in zip(start_times, end_times, h_types): 38 | pos.append([ start_time, y_pos ]) 39 | size.append(HitobjectPlot.HITOBJECT_RADIUS) 40 | obj_num += 1 41 | 42 | # Slider end 43 | if h_type == StdMapData.TYPE_SLIDER: 44 | pos.append([ end_time, y_pos ]) 45 | size.append(0) 46 | obj_num += 1 47 | 48 | adj.append([ obj_num - 1, obj_num ]) 49 | else: 50 | adj.append([ obj_num, obj_num ]) 51 | 52 | pos = np.array(pos, dtype=np.float) 53 | adj = np.array(adj, dtype=np.int) 54 | 55 | self.setData(pos=pos, adj=adj, size=size, symbol='o', pxMode=True) -------------------------------------------------------------------------------- /gui/objects/graph/line_plot.py: -------------------------------------------------------------------------------- 1 | from pyqtgraph.Qt import QtGui, QtCore 2 | import pyqtgraph 3 | import numpy as np 4 | 5 | 6 | 7 | class LinePlot(pyqtgraph.PlotCurveItem): 8 | 9 | def __init__(self): 10 | super().__init__() 11 | 12 | 13 | def update_data(self, data_x, data_y): 14 | if type(data_x) == type(None) or type(data_y) == type(None): 15 | self.setData(x=[], y=[]) 16 | return 17 | 18 | # Filter out infinities 19 | inf_filter = np.isfinite(data_y.astype(np.float64)) 20 | data_x, data_y = data_x[inf_filter], data_y[inf_filter] 21 | 22 | self.setData(x=data_x, y=data_y) 23 | return data_x, data_y 24 | 25 | 26 | def update_xy(self, data_x, data_y): 27 | self.setData(x=data_x, y=data_y) -------------------------------------------------------------------------------- /gui/objects/graph/scatter_plot.py: -------------------------------------------------------------------------------- 1 | from pyqtgraph.Qt import QtGui, QtCore 2 | import pyqtgraph 3 | import numpy as np 4 | 5 | 6 | 7 | class ScatterPlot(pyqtgraph.ScatterPlotItem): 8 | 9 | def __init__(self): 10 | super().__init__() 11 | 12 | self.setSize(10) 13 | self.setPen(pyqtgraph.mkPen(None)) 14 | self.setBrush(pyqtgraph.mkBrush(255, 255, 255, 120)) 15 | 16 | 17 | def update_data(self, data_x, data_y): 18 | if type(data_x) == type(None) or type(data_y) == type(None): 19 | self.clear() 20 | return 21 | 22 | # Filter out infinities 23 | inf_filter = np.isfinite(data_y.astype(np.float64)) 24 | data_x, data_y = data_x[inf_filter], data_y[inf_filter] 25 | 26 | self.clear() 27 | self.addPoints(x=data_x, y=data_y) 28 | 29 | return data_x, data_y -------------------------------------------------------------------------------- /gui/objects/graph/score_plot.py: -------------------------------------------------------------------------------- 1 | from pyqtgraph.Qt import QtGui, QtCore 2 | import pyqtgraph 3 | import numpy as np 4 | 5 | from analysis.osu.std.score_data import StdScoreData 6 | 7 | 8 | class ScorePlot(pyqtgraph.ScatterPlotItem): 9 | 10 | def __init__(self, time, score_type, yoffset=0): 11 | super().__init__() 12 | 13 | self.setSize(10) 14 | self.setPen(pyqtgraph.mkPen(None)) 15 | 16 | self.update_data(time, score_type, yoffset) 17 | 18 | 19 | def update_data(self, time, score_type, yoffset=0): 20 | if type(time) == type(None) or type(time) == type(None): 21 | self.clear() 22 | return 23 | 24 | color_map = { 25 | StdScoreData.TYPE_MISS : (255, 100, 100, 255), # Red 26 | StdScoreData.TYPE_HITP : (100, 255, 100, 255), # Green 27 | StdScoreData.TYPE_HITR : ( 50, 150, 255, 255), # Blue 28 | StdScoreData.TYPE_AIMH : ( 150, 100, 255, 255), # Purple 29 | } 30 | 31 | y = [ yoffset for _ in range(len(time)) ] 32 | b = [ pyqtgraph.mkBrush(*color_map[a]) for a in score_type.values ] 33 | 34 | self.clear() 35 | self.addPoints(x=time, y=y, brush=b) 36 | 37 | return time, y -------------------------------------------------------------------------------- /gui/objects/graph/timing_plot.py: -------------------------------------------------------------------------------- 1 | import pyqtgraph 2 | from pyqtgraph import QtCore, QtGui 3 | 4 | 5 | class TimingPlot(pyqtgraph.GraphicsObject): 6 | def __init__(self, ts, te, yoffset=0, color=(100, 100, 255, 100)): 7 | pyqtgraph.GraphicsObject.__init__(self) 8 | 9 | self.ts = ts # start times 10 | self.te = te # end times 11 | 12 | self.yoffset = yoffset 13 | self.color = color 14 | 15 | self.px_h = self.pixelHeight() 16 | self.generatePicture() 17 | 18 | 19 | def update_data(self, ts, te): 20 | self.ts = ts # start times 21 | self.te = te # end times 22 | self.px_h = self.pixelHeight() 23 | 24 | self.generatePicture() 25 | 26 | 27 | def generatePicture(self): 28 | ## pre-computing a QPicture object allows paint() to run much more quickly, 29 | ## rather than re-drawing the shapes every time. 30 | self.picture = QtGui.QPicture() 31 | 32 | p = QtGui.QPainter(self.picture) 33 | p.setPen(pyqtgraph.mkPen(color=self.color)) 34 | 35 | # This 20x aligns it right on the boundary of slider rendering in hitobject_plot if yoffset is +1 or -1 36 | # Make it 25x so the horizontal line is visible 37 | y_mid = 25*self.px_h*self.yoffset 38 | 39 | # Calc the height of the vertical lines 40 | # If the |---| is on top of the notes, then make the top part shorter 41 | # If the |---| is on bottom of the notes, then make the bottom part shorter 42 | if self.yoffset < 0: 43 | y_top = 25*self.px_h 44 | y_btm = -12.5*self.px_h 45 | elif self.yoffset == 0: 46 | y_top = 25*self.px_h 47 | y_btm = -25*self.px_h 48 | else: 49 | y_top = 12.5*self.px_h 50 | y_btm = -25*self.px_h 51 | 52 | # Render 53 | for ts, te in zip(self.ts, self.te): 54 | p.drawLine(QtCore.QPointF(ts, y_btm + y_mid), QtCore.QPointF(ts, y_top + y_mid)) 55 | p.drawLine(QtCore.QPointF(ts, 0 + y_mid), QtCore.QPointF(te, 0 + y_mid)) 56 | p.drawLine(QtCore.QPointF(te, y_btm + y_mid), QtCore.QPointF(te, y_top + y_mid)) 57 | 58 | p.end() 59 | 60 | 61 | def paint(self, p, *args): 62 | p.drawPicture(0, 0, self.picture) 63 | 64 | 65 | def boundingRect(self): 66 | ## boundingRect _must_ indicate the entire area that will be drawn on 67 | ## or else we will get artifacts and possibly crashing. 68 | ## (in this case, QPicture does all the work of computing the bouning rect for us) 69 | return QtCore.QRectF(self.picture.boundingRect()) 70 | 71 | 72 | def viewRangeChanged(self): 73 | """ 74 | Called whenever the view coordinates of the ViewBox containing this item have changed. 75 | """ 76 | px_h = self.pixelHeight() 77 | 78 | # Without pixel_height the render scales with how the view is zoomed in/out 79 | if self.px_h != px_h: 80 | self.px_h = px_h 81 | self.generatePicture() -------------------------------------------------------------------------------- /gui/objects/group.py: -------------------------------------------------------------------------------- 1 | from misc.callback import callback 2 | 3 | 4 | class Group(): 5 | 6 | def __init__(self, name, parent): 7 | self.parent = parent 8 | self.name = name 9 | self.data = {} 10 | 11 | 12 | def __len__(self): 13 | return len(self.data) 14 | 15 | 16 | @callback 17 | def add_group(self, name): 18 | if name in self.data: return 19 | self.data[name] = Group(name, self) 20 | 21 | Group.add_group.emit(self.data[name]) 22 | 23 | 24 | @callback 25 | def rmv_group(self, name): 26 | if not name in self.data: return 27 | 28 | Group.rmv_group.emit(self.data[name]) 29 | del self.data[name] 30 | 31 | 32 | @callback 33 | def add_elem(self, name, elem): 34 | if name in self.data: return 35 | self.data[name] = elem 36 | 37 | Group.add_elem.emit(self, elem) 38 | 39 | 40 | @callback 41 | def rmv_elem(self, name): 42 | if not name in self.data: return 43 | 44 | Group.rmv_elem.emit(self, self.data[name]) 45 | del self.data[name] 46 | 47 | 48 | def child(self, name): 49 | return self.data[name] 50 | 51 | 52 | def get_structure(self): 53 | structure = {} 54 | for name, data in self.data.items(): 55 | try: structure[name] = data.get_structure() 56 | except: structure[name] = type(data) 57 | 58 | return structure -------------------------------------------------------------------------------- /gui/objects/layer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/gui/objects/layer/__init__.py -------------------------------------------------------------------------------- /gui/objects/layer/layer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from misc.callback import callback 6 | 7 | 8 | class Layer(QGraphicsItem): 9 | 10 | 11 | def __init__(self, name): 12 | QGraphicsItem.__init__(self) 13 | 14 | self.name = name 15 | 16 | 17 | def area_resize_event(self, width, height): 18 | raise NotImplementedError() 19 | 20 | # Reminder this needs to be put in the 21 | # implementation of derived class 22 | self.layer_changed() 23 | 24 | 25 | @callback 26 | def layer_changed(self): 27 | self.layer_changed.emit(inst=self) 28 | 29 | 30 | def boundingRect(self): 31 | return QRectF(0, 0, 0, 0) -------------------------------------------------------------------------------- /gui/objects/layer/layer_manager.py: -------------------------------------------------------------------------------- 1 | from misc.callback import callback 2 | from gui.objects.group import Group 3 | from gui.objects.scene import Scene 4 | 5 | 6 | class LayerManager(Group, Scene): 7 | 8 | def __init__(self): 9 | Group.__init__(self, 'root', self) 10 | Scene.__init__(self) 11 | 12 | 13 | @callback 14 | def add_layer(self, layer_name, layer, path=None): 15 | if not path: group = self 16 | else: 17 | names = path.split('.') 18 | group = self.child(names[0]) 19 | 20 | for name in names[1:]: 21 | group = self.child(name) 22 | 23 | group.add_elem(layer_name, layer) 24 | Scene.add_layer(self, layer) 25 | 26 | 27 | @callback 28 | def rmv_layer(self, layer_name, path=None): 29 | if not path: group = self 30 | else: 31 | names = path.split('.') 32 | group = self.child(names[0]) 33 | 34 | for name in names[1:]: 35 | group = group.child(name) 36 | 37 | layer = group.child(layer_name) 38 | Scene.rmw_layer(self, layer) 39 | group.rmv_elem(name) 40 | -------------------------------------------------------------------------------- /gui/objects/layer/layers/mania/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/gui/objects/layer/layers/mania/__init__.py -------------------------------------------------------------------------------- /gui/objects/layer/layers/mania/score_debug_layer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | import numpy as np 6 | 7 | from analysis.osu.mania.map_data import ManiaMapData 8 | from analysis.osu.mania.score_data import ManiaScoreData 9 | 10 | from misc.math_utils import * 11 | from generic.temporal import Temporal 12 | from gui.objects.layer.layer import Layer 13 | 14 | from osu.local.hitobject.mania.mania import Mania, ManiaSettings 15 | from analysis.osu.mania.action_data import ManiaActionData 16 | 17 | 18 | 19 | class ManiaScoreDebugLayer(Layer, Temporal): 20 | 21 | def __init__(self, data, time_updater): 22 | Layer.__init__(self, 'Score debug layer') 23 | Temporal.__init__(self) 24 | 25 | beatmap, replay = data 26 | 27 | self.map_data = ManiaMapData.get_hitobject_data(beatmap.hitobjects) 28 | self.columns = len(self.map_data) 29 | 30 | self.replay_data = ManiaActionData.get_replay_data(replay.play_data, self.columns) 31 | self.score_data = ManiaScoreData.get_score_data(self.replay_data, self.map_data) 32 | 33 | time_updater.connect(self.time_changed) 34 | self.time_changed.connect(lambda time: self.layer_changed()) 35 | 36 | ManiaSettings.set_viewable_time_interval.connect(self.layer_changed) 37 | 38 | 39 | def paint(self, painter, option, widget): 40 | if not self.time: return 41 | if len(self.score_data) <= 0: return 42 | 43 | painter.setPen(QColor(255, 0, 0, 255)) 44 | 45 | space_data = widget.width(), widget.height(), self.columns, self.time 46 | spatial_data = ManiaSettings.get_spatial_data(*space_data) 47 | 48 | start_time, end_time = spatial_data[0], spatial_data[1] 49 | 50 | for column in range(self.columns): 51 | start_idx = find(self.score_data[column][:,0], start_time) 52 | end_idx = find(self.score_data[column][:,0], end_time) 53 | end_idx = min(end_idx + 1, len(self.score_data[column])) 54 | 55 | for score in self.score_data[column][start_idx:end_idx]: 56 | time = score[0] 57 | column = score[1] 58 | offset = score[2] 59 | 60 | pos_x, pos_y, scaled_note_width, scaled_note_height = self.get_draw_data(*spatial_data[2:], column, time + offset, time + offset) 61 | painter.drawText(int(pos_x), int(pos_y), str(offset)) 62 | 63 | 64 | def get_draw_data(self, ratio_x, ratio_y, ratio_t, x_offset, y_offset, column, press_time, release_time): 65 | scaled_note_width = ManiaSettings.note_width*ratio_x 66 | scaled_note_height = (release_time - press_time)*ratio_t 67 | 68 | pos_x = x_offset + column*(ManiaSettings.note_width + ManiaSettings.note_seperation)*ratio_x 69 | pos_y = y_offset + (self.time - press_time)*ratio_t - scaled_note_height 70 | 71 | return pos_x, pos_y, scaled_note_width, scaled_note_height 72 | -------------------------------------------------------------------------------- /gui/objects/layer/layers/mania_data_2d_layer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from generic.temporal import Temporal 6 | from gui.objects.layer.layer import Layer 7 | from osu.local.hitobject.mania.mania import Mania, ManiaSettings 8 | 9 | 10 | class ManiaData2DLayer(Layer, Temporal): 11 | 12 | def __init__(self, name, data, draw_func, time_driver, color=(0, 50, 255, 50)): 13 | Layer.__init__(self, name) 14 | Temporal.__init__(self) 15 | 16 | time_driver.connect(self.time_changed) 17 | self.time_changed.connect(lambda time: self.layer_changed()) 18 | 19 | self.color = color 20 | self.data = data 21 | self.columns = data.shape[1] 22 | self.draw_func = draw_func 23 | 24 | ManiaSettings.set_note_height.connect(self.layer_changed) 25 | ManiaSettings.set_note_width.connect(self.layer_changed) 26 | ManiaSettings.set_note_seperation.connect(self.layer_changed) 27 | ManiaSettings.set_viewable_time_interval.connect(self.layer_changed) 28 | ManiaSettings.set_replay_opacity.connect(self.layer_changed) 29 | 30 | 31 | def paint(self, painter, option, widget): 32 | if not self.time: return 33 | 34 | space_data = widget.width(), widget.height(), self.columns, self.time 35 | spatial_data = ManiaSettings.get_spatial_data(*space_data) 36 | 37 | try: self.draw_func(painter, self.time, spatial_data, self.data, color=QColor(*self.color)) 38 | except Exception as e: print(e) -------------------------------------------------------------------------------- /gui/objects/layer/layers/std/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/gui/objects/layer/layers/std/__init__.py -------------------------------------------------------------------------------- /gui/objects/layer/layers/std/aimpoint_angle_text_layer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from analysis.osu.std.map_data import StdMapData 6 | from analysis.osu.std.map_metrics import StdMapMetrics 7 | 8 | from osu.local.hitobject.std.std import Std, StdSettings 9 | 10 | from misc.math_utils import * 11 | from misc.numpy_utils import NumpyUtils 12 | 13 | from generic.temporal import Temporal 14 | from gui.objects.layer.layer import Layer 15 | 16 | 17 | class AimpointAngleTextLayer(Layer, Temporal): 18 | 19 | def __init__(self, data, time_updater): 20 | Layer.__init__(self, 'Aimpoint angle text') 21 | 22 | beatmap = data 23 | map_data = StdMapData.get_aimpoint_data(beatmap.hitobjects) 24 | 25 | self.all_positions = StdMapData.all_positions(map_data) 26 | self.times, self.angles = StdMapMetrics.calc_angles(map_data) 27 | 28 | time_updater.connect(self.time_changed) 29 | self.time_changed.connect(lambda time: self.layer_changed()) 30 | 31 | StdSettings.set_view_time_back.connect(self.layer_changed) 32 | StdSettings.set_view_time_ahead.connect(self.layer_changed) 33 | 34 | 35 | def paint(self, painter, option, widget): 36 | if not self.time: return 37 | if len(self.all_positions) <= 0: return 38 | 39 | self.ratio_x = widget.width()/Std.PLAYFIELD_WIDTH 40 | self.ratio_y = widget.height()/Std.PLAYFIELD_HEIGHT 41 | 42 | start_idx = find(self.times, self.time - StdSettings.view_time_back) 43 | end_idx = find(self.times, self.time + StdSettings.view_time_ahead) 44 | end_idx = min(end_idx + 1, len(self.times)) 45 | 46 | painter.setPen(QColor(0, 0, 255, 255)) 47 | for i in range(start_idx, end_idx): 48 | pos_x, pos_y = self.all_positions[i + 1] 49 | angle = self.angles[i]*180/math.pi 50 | 51 | painter.drawText(pos_x*self.ratio_x, pos_y*self.ratio_y, str(round(angle, 2))) 52 | -------------------------------------------------------------------------------- /gui/objects/layer/layers/std/aimpoint_paths_layer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from gui.objects.layer.layer import Layer 6 | from osu.local.beatmap.beatmap_utility import * 7 | from analysis.osu.std.map_data import MapData 8 | 9 | from misc.pos import Pos 10 | from misc.numpy_utils import NumpyUtils 11 | 12 | 13 | 14 | class AimpointPathsLayer(Layer): 15 | 16 | def __init__(self, playfield): 17 | Layer.__init__(self, 'Aimpoint Paths') 18 | self.playfield = playfield 19 | 20 | 21 | def area_resize_event(self, width, height): 22 | self.ratio_x = width/BeatmapUtil.PLAYFIELD_WIDTH 23 | self.ratio_y = height/BeatmapUtil.PLAYFIELD_HEIGHT 24 | 25 | 26 | def paint(self, painter, option, widget): 27 | painter.setPen(QColor(255, 0, 0, 255)) 28 | 29 | aimpoints = MapData().set_data_hitobjects(self.playfield.visible_hitobjects) 30 | aimpoints.append_to_start(MapData.get_data_before(MapData.full_hitobject_data, NumpyUtils.first(aimpoints.start_times()))) 31 | aimpoints.append_to_end(MapData.get_data_after(MapData.full_hitobject_data, NumpyUtils.last(aimpoints.end_times()))) 32 | 33 | aimpoints_positions = aimpoints.all_positions() 34 | 35 | for i in range(1, len(aimpoints_positions)): 36 | prev_pos, curr_pos = Pos(*aimpoints_positions[i - 1]), Pos(*aimpoints_positions[i]) 37 | painter.drawLine(prev_pos.x*self.ratio_x, prev_pos.y*self.ratio_y, curr_pos.x*self.ratio_x, curr_pos.y*self.ratio_y) 38 | -------------------------------------------------------------------------------- /gui/objects/layer/layers/std/aimpoint_vector_layer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from analysis.osu.std.map_data import StdMapData 6 | from analysis.osu.std.map_metrics import StdMapMetrics 7 | 8 | from osu.local.hitobject.std.std import Std, StdSettings 9 | 10 | from misc.math_utils import * 11 | from misc.numpy_utils import NumpyUtils 12 | 13 | from generic.temporal import Temporal 14 | from gui.objects.layer.layer import Layer 15 | 16 | 17 | 18 | class AimpointVectorLayer(Layer, Temporal): 19 | 20 | def __init__(self, data, time_updater): 21 | Layer.__init__(self, 'Aimpoint vectors') 22 | Temporal.__init__(self) 23 | 24 | beatmap = data 25 | map_data = StdMapData.get_aimpoint_data(beatmap.hitobjects) 26 | 27 | self.all_positions = StdMapData.all_positions(map_data) 28 | self.forw_times, self.forw_vecs = StdMapMetrics.calc_forward_vel_vectors(map_data) 29 | self.back_times, self.back_vecs = StdMapMetrics.calc_backward_vel_vectors(map_data) 30 | 31 | time_updater.connect(self.time_changed) 32 | self.time_changed.connect(lambda time: self.layer_changed()) 33 | 34 | StdSettings.set_view_time_back.connect(self.layer_changed) 35 | StdSettings.set_view_time_ahead.connect(self.layer_changed) 36 | 37 | 38 | def paint(self, painter, option, widget): 39 | if not self.time: return 40 | if len(self.all_positions) <= 0: return 41 | 42 | self.ratio_x = widget.width()/Std.PLAYFIELD_WIDTH 43 | self.ratio_y = widget.height()/Std.PLAYFIELD_HEIGHT 44 | 45 | start_idx = find(self.back_times, self.time - StdSettings.view_time_back) 46 | end_idx = find(self.back_times, self.time + StdSettings.view_time_ahead) 47 | end_idx = min(end_idx + 1, len(self.back_times)) 48 | 49 | painter.setPen(QColor(255, 0, 0, 255)) 50 | for i in range(start_idx, end_idx): 51 | pos_x, pos_y = self.all_positions[i + 1] 52 | vec_x, vec_y = self.back_vecs[i]*100 53 | 54 | painter.drawLine(pos_x*self.ratio_x, pos_y*self.ratio_y, (pos_x + vec_x)*self.ratio_x, (pos_y + vec_y)*self.ratio_y) 55 | 56 | painter.setPen(QColor(0, 0, 128, 255)) 57 | for i in range(start_idx, end_idx): 58 | pos_x, pos_y = self.all_positions[i + 1] 59 | vec_x, vec_y = self.back_vecs[i]*100 60 | 61 | painter.drawLine(pos_x*self.ratio_x, pos_y*self.ratio_y, (pos_x - vec_x)*self.ratio_x, (pos_y - vec_y)*self.ratio_y) 62 | 63 | painter.setPen(QColor(0, 200, 0, 255)) 64 | for i in range(start_idx, end_idx): 65 | pos_x, pos_y = self.all_positions[i + 1] 66 | vec_x, vec_y = self.forw_vecs[i + 1]*100 67 | 68 | painter.drawLine(pos_x*self.ratio_x, pos_y*self.ratio_y, (pos_x + vec_x)*self.ratio_x, (pos_y + vec_y)*self.ratio_y) -------------------------------------------------------------------------------- /gui/objects/layer/layers/std/aimpoint_velocity_text_layer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from analysis.osu.std.map_metrics import MapMetrics 6 | from analysis.osu.std.map_data import MapData 7 | 8 | from misc.pos import Pos 9 | from misc.numpy_utils import NumpyUtils 10 | 11 | from gui.objects.layer.layer import Layer 12 | from osu.local.beatmap.beatmap_utility import * 13 | 14 | 15 | 16 | class AimpointVelocityTextLayer(Layer): 17 | 18 | def __init__(self, playfield): 19 | Layer.__init__(self, 'Aimpoint Velocity Text') 20 | self.playfield = playfield 21 | 22 | 23 | def area_resize_event(self, width, height): 24 | self.ratio_x = width/BeatmapUtil.PLAYFIELD_WIDTH 25 | self.ratio_y = height/BeatmapUtil.PLAYFIELD_HEIGHT 26 | 27 | 28 | def paint(self, painter, option, widget): 29 | painter.setPen(QColor(255, 0, 0, 255)) 30 | 31 | aimpoints = MapData().set_data_hitobjects(self.playfield.visible_hitobjects) 32 | aimpoints.append_to_start(MapData.get_data_before(MapData.full_hitobject_data, NumpyUtils.first(aimpoints.start_times()))) 33 | aimpoints.append_to_end(MapData.get_data_after(MapData.full_hitobject_data, NumpyUtils.last(aimpoints.end_times()))) 34 | 35 | time, vel = MapMetrics.calc_velocity(aimpoints) 36 | pos = aimpoints.all_positions() 37 | 38 | for i in range(1, len(vel)): 39 | prev_pos, curr_pos = pos[i - 1], pos[i] 40 | midpoint = Pos(*prev_pos).midpoint(Pos(*curr_pos)) 41 | 42 | painter.drawText(midpoint.x*self.ratio_x, midpoint.y*self.ratio_y, str(round(vel[i - 1], 2))) 43 | -------------------------------------------------------------------------------- /gui/objects/layer/layers/std/hitobject_aimpoint_layer.py: -------------------------------------------------------------------------------- 1 | from generic.temporal import Temporal 2 | from gui.objects.layer.layer import Layer 3 | 4 | from osu.local.hitobject.std.std import Std 5 | 6 | 7 | class HitobjectAimpointLayer(Layer, Temporal): 8 | 9 | def __init__(self, data, time_updater): 10 | Layer.__init__(self, 'Hitobject aimpoints') 11 | Temporal.__init__(self) 12 | 13 | self.beatmap = data 14 | 15 | time_updater.connect(self.time_changed) 16 | self.time_changed.connect(lambda time: self.layer_changed()) 17 | 18 | 19 | def paint(self, painter, option, widget): 20 | if not self.time: return 21 | 22 | ratio_x = widget.width()/Std.PLAYFIELD_WIDTH 23 | ratio_y = widget.height()/Std.PLAYFIELD_HEIGHT 24 | 25 | visible_hitobjects = Std.get_hitobjects_visible_at_time(self.beatmap, self.time) 26 | for visible_hitobject in visible_hitobjects: 27 | try: visible_hitobject.render_hitobject_aimpoints(painter, ratio_x, ratio_y) 28 | except AttributeError: pass -------------------------------------------------------------------------------- /gui/objects/layer/layers/std/hitobject_outline_layer.py: -------------------------------------------------------------------------------- 1 | from generic.temporal import Temporal 2 | from gui.objects.layer.layer import Layer 3 | 4 | from osu.local.hitobject.std.std import Std 5 | 6 | 7 | class HitobjectOutlineLayer(Layer, Temporal): 8 | 9 | def __init__(self, data, time_driver): 10 | Layer.__init__(self, 'Hitobject outlines') 11 | Temporal.__init__(self) 12 | 13 | self.beatmap = data 14 | 15 | time_driver.connect(self.time_changed) 16 | self.time_changed.connect(lambda time: self.layer_changed()) 17 | 18 | 19 | def paint(self, painter, option, widget): 20 | if not self.time: return 21 | 22 | ratio_x = widget.width()/Std.PLAYFIELD_WIDTH 23 | ratio_y = widget.height()/Std.PLAYFIELD_HEIGHT 24 | 25 | visible_hitobjects = Std.get_hitobjects_visible_at_time(self.beatmap, self.time) 26 | for visible_hitobject in visible_hitobjects: 27 | try: visible_hitobject.render_hitobject_outline(painter, ratio_x, ratio_y, self.time) 28 | except AttributeError: pass -------------------------------------------------------------------------------- /gui/objects/layer/layers/std/score_debug_layer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | import numpy as np 6 | 7 | from analysis.osu.std.replay_data import StdReplayData 8 | from analysis.osu.std.map_data import StdMapData 9 | from analysis.osu.std.score_data import StdScoreData 10 | 11 | from misc.math_utils import * 12 | from generic.temporal import Temporal 13 | from gui.objects.layer.layer import Layer 14 | 15 | from osu.local.hitobject.std.std import Std, StdSettings 16 | 17 | 18 | 19 | class StdScoreDebugLayer(Layer, Temporal): 20 | 21 | def __init__(self, data, time_updater): 22 | Layer.__init__(self, 'Score debug layer') 23 | Temporal.__init__(self) 24 | 25 | beatmap, replay = data 26 | 27 | self.aimpoint_data = StdMapData.get_aimpoint_data(beatmap.hitobjects) 28 | self.replay_data = StdReplayData.get_event_data(replay.play_data) 29 | self.score_data = StdScoreData.get_score_data(self.replay_data, self.aimpoint_data) 30 | 31 | time_updater.connect(self.time_changed) 32 | self.time_changed.connect(lambda time: self.layer_changed()) 33 | 34 | StdSettings.set_view_time_back.connect(self.layer_changed) 35 | StdSettings.set_view_time_ahead.connect(self.layer_changed) 36 | 37 | 38 | def paint(self, painter, option, widget): 39 | if not self.time: return 40 | if len(self.score_data) <= 0: return 41 | 42 | self.ratio_x = widget.width()/Std.PLAYFIELD_WIDTH 43 | self.ratio_y = widget.height()/Std.PLAYFIELD_HEIGHT 44 | 45 | painter.setPen(QColor(255, 0, 0, 255)) 46 | 47 | start_idx = find(self.score_data, self.time - StdSettings.view_time_back, selector=lambda event: event[0]) 48 | end_idx = find(self.score_data, self.time + StdSettings.view_time_ahead, selector=lambda event: event[0]) 49 | end_idx = min(end_idx + 1, len(self.score_data)) 50 | 51 | for score in self.score_data[start_idx:end_idx]: 52 | pos_x, pos_y = score[1] 53 | time_offset = score[2] 54 | pos_offset = score[3] 55 | 56 | painter.drawText(pos_x*self.ratio_x, pos_y*self.ratio_y, str(time_offset) + ' ' + str(pos_offset)) 57 | -------------------------------------------------------------------------------- /gui/objects/layer/layers/std_data_2d_layer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from generic.temporal import Temporal 6 | from gui.objects.layer.layer import Layer 7 | from osu.local.hitobject.std.std import Std, StdSettings 8 | 9 | 10 | class StdData2DLayer(Layer, Temporal): 11 | 12 | def __init__(self, name, data, draw_func, time_driver): 13 | Layer.__init__(self, name) 14 | Temporal.__init__(self) 15 | 16 | time_driver.connect(self.time_changed) 17 | self.time_changed.connect(lambda time: self.layer_changed()) 18 | 19 | self.data = data 20 | self.draw_func = draw_func 21 | 22 | StdSettings.set_cursor_radius.connect(self.layer_changed) 23 | StdSettings.set_cursor_thickness.connect(self.layer_changed) 24 | StdSettings.set_cursor_color.connect(self.layer_changed) 25 | StdSettings.set_k1_color.connect(self.layer_changed) 26 | StdSettings.set_k2_color.connect(self.layer_changed) 27 | StdSettings.set_m1_color.connect(self.layer_changed) 28 | StdSettings.set_m2_color.connect(self.layer_changed) 29 | StdSettings.set_view_time_back.connect(self.layer_changed) 30 | StdSettings.set_view_time_ahead.connect(self.layer_changed) 31 | 32 | 33 | def paint(self, painter, option, widget): 34 | if not self.time: return 35 | painter.setPen(QColor(0, 0, 0, 255)) 36 | 37 | ratio_x = widget.width()/Std.PLAYFIELD_WIDTH 38 | ratio_y = widget.height()/Std.PLAYFIELD_HEIGHT 39 | 40 | try: self.draw_func(painter, ratio_x, ratio_y, self.time, self.data) 41 | except Exception as e: print(e) -------------------------------------------------------------------------------- /gui/objects/scene.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | 6 | class Scene(QGraphicsScene): 7 | 8 | def __init__(self): 9 | QGraphicsScene.__init__(self) 10 | 11 | 12 | def add_layer(self, layer): 13 | print(self, layer) 14 | self.addItem(layer) 15 | self.update() 16 | 17 | layer.layer_changed.connect(self.update_layers, inst=layer) 18 | 19 | 20 | def rmv_layer(self, layer): 21 | layer.layer_changed.disconnect(self.update_layers, inst=layer) 22 | 23 | self.removeItem(layer) 24 | self.update() 25 | 26 | 27 | def update_layers(self, layer=None): 28 | self.update() -------------------------------------------------------------------------------- /gui/widgets/QContainer.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | 6 | class QContainer(QWidget): 7 | 8 | def __init__(self, layout): 9 | QWidget.__init__(self) 10 | 11 | self.layout = layout 12 | self.setLayout(self.layout) 13 | 14 | 15 | def addWidget(self, widget): 16 | self.layout.addWidget(widget) 17 | 18 | 19 | def rmvWidget(self, widget): 20 | self.layout.removeWidget(widget) 21 | 22 | 23 | def get(self): 24 | return self.layout -------------------------------------------------------------------------------- /gui/widgets/collections_browser.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | 6 | class CollectionsBrowser(QWidget): 7 | 8 | def __init__(self): 9 | QWidget.__init__(self) 10 | 11 | self.init_gui_elements() 12 | self.construct_gui() 13 | self.update_gui() 14 | 15 | 16 | def init_gui_elements(self): 17 | self.layout = QVBoxLayout() 18 | self.label = QLabel('Collections Browser') 19 | 20 | 21 | def construct_gui(self): 22 | self.setLayout(self.layout) 23 | self.layout.addWidget(self.label) 24 | 25 | 26 | def update_gui(self): 27 | self.label.setAlignment(Qt.AlignCenter) -------------------------------------------------------------------------------- /gui/widgets/data_2d_graph.py: -------------------------------------------------------------------------------- 1 | from pyqtgraph.Qt import QtGui, QtCore 2 | import numpy as np 3 | import pyqtgraph 4 | 5 | 6 | from misc.callback import callback 7 | from gui.objects.graph.scatter_plot import ScatterPlot 8 | from gui.objects.graph.line_plot import LinePlot 9 | from gui.objects.graph.bar_plot import BarPlot 10 | 11 | 12 | class Data2DGraph(pyqtgraph.PlotWidget): 13 | 14 | SCATTER_PLOT = 0 15 | LINE_PLOT = 1 16 | BAR_PLOT = 2 17 | 18 | MIN_TIME = -5000 19 | 20 | @callback 21 | def __init__(self, name, data_2d, temporal=True, plot_type=None): 22 | super().__init__() 23 | 24 | pyqtgraph.setConfigOptions(antialias=True) 25 | 26 | #self.showAxis('left', show=False) 27 | self.setLimits(xMin=Data2DGraph.MIN_TIME) 28 | self.setRange(xRange=(-100, 10000)) 29 | 30 | self.getViewBox().setMouseEnabled(y=False) 31 | self.getPlotItem().setTitle(name) 32 | 33 | if plot_type == Data2DGraph.SCATTER_PLOT: self.plot_item = ScatterPlot() 34 | elif plot_type == Data2DGraph.LINE_PLOT: self.plot_item = LinePlot() 35 | elif plot_type == Data2DGraph.BAR_PLOT: self.plot_item = BarPlot() 36 | else: self.plot_item = ScatterPlot() 37 | 38 | self.update_data(data_2d) 39 | self.addItem(self.plot_item) 40 | 41 | if temporal: 42 | self.timeline_marker = pyqtgraph.InfiniteLine(angle=90, movable=True) 43 | self.timeline_marker.setBounds((Data2DGraph.MIN_TIME + 100, None)) 44 | self.timeline_marker.sigPositionChanged.connect(self.time_changed_event) 45 | self.addItem(self.timeline_marker, ignoreBounds=True) 46 | 47 | self.__init__.emit(self) 48 | 49 | 50 | @callback 51 | def __del__(self): 52 | self.__del__.emit(self) 53 | 54 | 55 | def update_data(self, data_2d): 56 | if type(data_2d) == type(None): 57 | self.plot_item.update_data(None, None) 58 | return 59 | 60 | data_x, data_y = data_2d 61 | data_2d = self.plot_item.update_data(data_x, data_y) 62 | if data_2d == None: return 63 | 64 | # Update view 65 | data_x, data_y = data_2d 66 | data_width = data_x[-1] - data_x[0] 67 | 68 | if len(data_x) > 0: self.setRange(xRange=(data_x[0] - data_width*0.1, data_x[-1] + data_width*0.1)) 69 | if len(data_y) > 0: self.setRange(yRange=(min(data_y), max(data_y))) 70 | 71 | self.setLimits(xMin=data_x[0] - 1000, xMax=data_x[-1] + 1000) 72 | 73 | 74 | def update_graph_info(self, title=None, x_axis_label=None, y_axis_label=None): 75 | if title: self.getPlotItem().setTitle(title=title) 76 | if x_axis_label: self.getPlotItem().setLabel('bottom', text=x_axis_label) 77 | if y_axis_label: self.getPlotItem().setLabel('left', text=y_axis_label) 78 | 79 | 80 | def get_name(self): 81 | return self.getPlotItem().titleLabel.text 82 | 83 | 84 | @callback 85 | def time_changed_event(self, marker): 86 | self.time_changed_event.emit(marker.value()) -------------------------------------------------------------------------------- /gui/widgets/dockable_graph.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | 6 | class DockableGraph(QDockWidget): 7 | 8 | def __init__(self, graph, floating=True): 9 | super().__init__(graph.getPlotItem().titleLabel.text) 10 | 11 | self.graph = graph 12 | 13 | self.setAllowedAreas(Qt.AllDockWidgetAreas) 14 | self.setWidget(graph) 15 | 16 | self.setFloating(floating) -------------------------------------------------------------------------------- /gui/widgets/editable_value_field.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from misc.callback import callback 6 | 7 | 8 | class EditableValueField(QWidget): 9 | 10 | def __init__(self, min_val, max_val, name): 11 | QWidget.__init__(self) 12 | 13 | self.layout = QHBoxLayout() 14 | self.label = QLabel(name) 15 | self.text = QLineEdit() 16 | self.slider = QSlider(Qt.Orientation.Horizontal) 17 | 18 | self.setLayout(self.layout) 19 | self.layout.addWidget(self.label) 20 | self.layout.addWidget(self.text) 21 | self.layout.addWidget(self.slider) 22 | 23 | self.label.setFixedWidth(130) 24 | self.text.setFixedWidth(35) 25 | self.setFixedHeight(50) 26 | 27 | self.slider.setMinimum(min_val) 28 | self.slider.setMaximum(max_val) 29 | 30 | self.text.returnPressed.connect(self.__text_value_changed) 31 | self.slider.valueChanged.connect(self.__slider_value_changed) 32 | 33 | 34 | def set_val(self, val): 35 | self.slider.setValue(val) 36 | 37 | 38 | def __text_value_changed(self): 39 | val = float(self.text.text()) 40 | if val < self.slider.minimum(): self.text.setText(str(self.slider.minimum())) 41 | if val > self.slider.maximum(): self.text.setText(str(self.slider.maximum())) 42 | 43 | self.slider.blockSignals(True) 44 | self.slider.setValue(val) 45 | self.slider.blockSignals(False) 46 | 47 | self.value_changed(val) 48 | 49 | 50 | def __slider_value_changed(self, val): 51 | self.text.blockSignals(True) 52 | self.text.setText(str(val)) 53 | self.text.blockSignals(False) 54 | 55 | self.value_changed(val) 56 | 57 | 58 | @callback 59 | def value_changed(self, val): 60 | self.value_changed.emit(val, inst=self) -------------------------------------------------------------------------------- /gui/widgets/file_browser.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | 6 | class FileBrowser(QWidget): 7 | 8 | def __init__(self): 9 | QWidget.__init__(self) 10 | 11 | self.init_gui_elements() 12 | self.construct_gui() 13 | self.update_gui() 14 | 15 | 16 | def init_gui_elements(self): 17 | self.layout = QVBoxLayout() 18 | 19 | self.file_system_model = QFileSystemModel() 20 | self.file_tree_view = QTreeView() 21 | 22 | 23 | def construct_gui(self): 24 | self.setLayout(self.layout) 25 | self.layout.addWidget(self.file_tree_view) 26 | 27 | 28 | def update_gui(self): 29 | self.file_system_model.setNameFilters(('*.osu', '*.osr')) 30 | self.file_system_model.setNameFilterDisables(False) 31 | self.file_system_model.setRootPath(QDir.currentPath()) 32 | 33 | self.file_tree_view.setDragEnabled(True) 34 | self.file_tree_view.setSelectionMode(QAbstractItemView.ExtendedSelection) 35 | self.file_tree_view.setModel(self.file_system_model) 36 | self.file_tree_view.hideColumn(1) # Hide file size column 37 | self.file_tree_view.hideColumn(2) # Hide file type column 38 | self.file_tree_view.setRootIndex(self.file_system_model.index(QDir.currentPath())) 39 | self.file_tree_view.header().setSectionResizeMode(QHeaderView.ResizeToContents) 40 | self.file_tree_view.resizeColumnToContents(0) # Resize file name column -------------------------------------------------------------------------------- /gui/widgets/graph_manager.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from pyqtgraph.dockarea import * 6 | 7 | #from gui.widgets.temporal_hitobject_graph import TemporalHitobjectGraph 8 | from gui.objects.graph.line_plot import LinePlot 9 | 10 | from analysis.osu.std.map_metrics import StdMapMetrics 11 | 12 | 13 | 14 | class GraphManager(QWidget): 15 | 16 | def __init__(self): 17 | QWidget.__init__(self) 18 | 19 | self.init_gui_elements() 20 | self.construct_gui() 21 | self.update_gui() 22 | 23 | 24 | def init_gui_elements(self): 25 | self.layout = QVBoxLayout() 26 | self.dock_area = QMainWindow() 27 | 28 | self.graphs = {} 29 | 30 | 31 | def construct_gui(self): 32 | self.setLayout(self.layout) 33 | self.layout.addWidget(self.dock_area) 34 | 35 | 36 | def update_gui(self): 37 | self.dock_area.setCentralWidget(None) 38 | self.dock_area.setTabPosition(Qt.AllDockWidgetAreas, QTabWidget.North) 39 | self.dock_area.setDockNestingEnabled(True) 40 | 41 | 42 | def update_data(self): 43 | for graph in self.graphs.values(): 44 | graph[0].update_data() 45 | 46 | 47 | def is_graph_exist(self, graph_name): 48 | return graph_name in self.graphs 49 | 50 | 51 | def get_num_graphs(self): 52 | return len(self.dock_area.findChildren(QDockWidget)) 53 | 54 | 55 | def add_graph(self, graph): 56 | print('Adding graph for ' + str(graph.getPlotItem().titleLabel.text)) 57 | 58 | # TODO: Handle closing of floating docks 59 | dock = QDockWidget(graph.getPlotItem().titleLabel.text, self) 60 | dock.setAllowedAreas(Qt.AllDockWidgetAreas) 61 | dock.setWidget(graph) 62 | 63 | self.dock_area.addDockWidget(Qt.LeftDockWidgetArea, dock) # I have no idea why Left/Right is reversed 64 | self.graphs[graph.getPlotItem().titleLabel.text] = [ graph, dock ] 65 | 66 | 67 | def remove_graph(self, graph_name): 68 | self.dock_area.removeDockWidget(self.graphs[graph_name][1]) 69 | del self.graphs[graph_name] 70 | 71 | 72 | def clear(self): 73 | for dock in self.graphs.values(): 74 | dock[1].setParent(None) 75 | self.graphs = {} 76 | 77 | 78 | -------------------------------------------------------------------------------- /gui/widgets/ipython_console.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import asyncio 4 | 5 | from PyQt5.QtCore import * 6 | from PyQt5.QtWidgets import * 7 | from PyQt5.QtGui import * 8 | 9 | from qtconsole.rich_jupyter_widget import RichJupyterWidget 10 | from qtconsole.inprocess import QtInProcessKernelManager 11 | 12 | 13 | 14 | # Thanks https://stackoverflow.com/a/41070191 15 | class IPythonConsole(RichJupyterWidget): 16 | 17 | """ Convenience class for a live IPython console widget. We can replace the standard banner using the customBanner argument""" 18 | def __init__(self, customBanner=None, *args, **kwargs): 19 | super(IPythonConsole, self).__init__(*args, **kwargs) 20 | 21 | if customBanner is not None: 22 | self.banner = customBanner 23 | 24 | # IPython Python 3.8 support fix 25 | if sys.platform == 'win32' and sys.version_info >= (3, 8): 26 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 27 | 28 | self.font_size = 6 29 | self.kernel_manager = QtInProcessKernelManager() 30 | 31 | self.kernel_manager.start_kernel(show_banner=False) 32 | self.kernel_manager.kernel.gui = 'qt' 33 | 34 | self.kernel_client = self.kernel_manager.client() 35 | self.kernel_client.start_channels() 36 | 37 | def stop(): 38 | self.kernel_client.stop_channels() 39 | self.kernel_manager.shutdown_kernel() 40 | 41 | self.exit_requested.connect(stop) 42 | 43 | self.magic('load_ext autoreload') 44 | self.magic('autoreload 2') 45 | 46 | self.push_vars({ 'exit' : lambda: self.print_text('exit() has been disabled to avoid breaking features') }) 47 | self.push_vars({ 'help' : lambda: self.print_text('help() has been disabled to avoid breaking features') }) 48 | 49 | 50 | def push_vars(self, variableDict): 51 | """ 52 | Given a dictionary containing name / value pairs, push those variables 53 | to the Jupyter console widget 54 | """ 55 | self.kernel_manager.kernel.shell.push(variableDict) 56 | 57 | 58 | def del_var(self, var_name): 59 | self.kernel_manager.kernel.shell.del_var(var_name) 60 | 61 | 62 | def magic(self, command): 63 | command = command.split(' ') 64 | self.kernel_manager.kernel.shell.run_line_magic(command[0], ' '.join(command[1:])) 65 | 66 | 67 | def clear(self): 68 | """ 69 | Clears the terminal 70 | """ 71 | self._control.clear() 72 | 73 | 74 | def print_text(self, text): 75 | """ 76 | Prints some plain text to the console 77 | """ 78 | self._append_plain_text(text) 79 | 80 | 81 | def execute_command(self, command): 82 | """ 83 | Execute a command in the frame of the console widget 84 | """ 85 | self._execute(command, False) -------------------------------------------------------------------------------- /gui/widgets/manager_switch.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from gui.widgets.QContainer import QContainer 6 | 7 | from generic.switcher import Switcher 8 | 9 | 10 | 11 | class ManagerSwitch(QWidget, Switcher): 12 | 13 | def __init__(self): 14 | QWidget.__init__(self) 15 | Switcher.__init__(self) 16 | 17 | self.init_gui_elements() 18 | self.construct_gui() 19 | self.update_gui() 20 | 21 | 22 | def init_gui_elements(self): 23 | self.stack = QStackedWidget() 24 | self.layout = QVBoxLayout() 25 | 26 | 27 | def construct_gui(self): 28 | self.setLayout(self.layout) 29 | self.layout.addWidget(self.stack) 30 | 31 | self.switch.connect(self.__switch_manager, inst=self) 32 | 33 | 34 | def update_gui(self): 35 | pass 36 | 37 | 38 | def __add_manager(self, manager_gui): 39 | new_idx = self.stack.addWidget(manager_gui) 40 | self.stack.setCurrentIndex(new_idx) 41 | 42 | 43 | def __rmv_manager(self, manager_gui): 44 | old_mgr = self.stack.currentWidget() 45 | self.stack.removeWidget(old_mgr) 46 | 47 | 48 | def __switch_manager(self, old_manager, new_manager): 49 | if old_manager != None: self.__rmv_manager(old_manager) 50 | if new_manager != None: self.__add_manager(new_manager) -------------------------------------------------------------------------------- /gui/widgets/map_manager.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | from misc.callback import callback 6 | 7 | 8 | class MapManager(QTabBar): 9 | 10 | def __init__(self): 11 | super().__init__() 12 | 13 | # Allows lookup of the tab index the map is associated with 14 | self.data = {} 15 | 16 | # Add a placeholder tab and disable tab closing 17 | self.setTabsClosable(False) 18 | self.addTab('') 19 | 20 | self.tabMoved.connect(self.tab_moved_handler) 21 | self.tabCloseRequested.connect(self.map_close_event) 22 | self.currentChanged.connect(self.map_changed_event) 23 | 24 | 25 | def get_current_map(self): 26 | return self.tabData(self.currentIndex()) 27 | 28 | 29 | def get_current_map_data(self): 30 | # TODO: For there to be a central place gui elements get map data from 31 | # because currently it needs to be recalculated every time 32 | pass 33 | 34 | 35 | def add_map(self, beatmap, name): 36 | # Because adding tab switches tabs before tab data is filled in 37 | self.currentChanged.disconnect(self.map_changed_event) 38 | 39 | # Removes the initial placeholder tab 40 | if self.tabText(0) == '': 41 | self.removeTab(0) 42 | self.setTabsClosable(True) 43 | 44 | idx = self.addTab(name) 45 | self.currentChanged.connect(self.map_changed_event) 46 | 47 | # Associate tab index with the tab the beatmap info is stored 48 | self.data[beatmap] = idx 49 | self.setTabData(idx, beatmap) 50 | 51 | # Ensure map change events are fired 52 | if self.currentIndex() == idx: self.map_changed_event(idx) 53 | else: self.setCurrentIndex(idx) 54 | 55 | 56 | def rmv_map(self, idx): 57 | self.removeTab(idx) 58 | 59 | 60 | def index_of(self, beatmap): 61 | return self.data[beatmap] 62 | 63 | 64 | def tab_insert_handler(self, idx): 65 | print('insert', idx) 66 | 67 | 68 | # Makes sure the beatmap data is associated with the tab index it's located in 69 | def tab_moved_handler(self, idx_from, idx_to): 70 | beatmap_1 = self.tabData(idx_from) 71 | beatmap_2 = self.tabData(idx_to) 72 | 73 | self.data[beatmap_1] = idx_from 74 | self.data[beatmap_2] = idx_to 75 | 76 | 77 | @callback 78 | def map_close_event(self, idx): 79 | self.map_close_event.emit(self.tabData(idx)) 80 | del self.data[self.tabData(idx)] 81 | self.rmv_map(idx) 82 | 83 | if len(self.data) < 1: 84 | # Add a placeholder tab and disable tab closing 85 | self.setTabsClosable(False) 86 | self.addTab('') 87 | 88 | 89 | @callback 90 | def map_changed_event(self, idx): 91 | self.map_changed_event.emit(self.tabData(idx)) -------------------------------------------------------------------------------- /gui/widgets/online_browser.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import * 2 | from PyQt5.QtWidgets import * 3 | from PyQt5.QtGui import * 4 | 5 | 6 | class OnlineBrowser(QWidget): 7 | 8 | def __init__(self): 9 | QWidget.__init__(self) 10 | 11 | self.init_gui_elements() 12 | self.construct_gui() 13 | self.update_gui() 14 | 15 | 16 | def init_gui_elements(self): 17 | self.layout = QVBoxLayout() 18 | self.label = QLabel('Online Browser') 19 | 20 | 21 | def construct_gui(self): 22 | self.setLayout(self.layout) 23 | self.layout.addWidget(self.label) 24 | 25 | 26 | def update_gui(self): 27 | self.label.setAlignment(Qt.AlignCenter) -------------------------------------------------------------------------------- /gui/widgets/timeline.py: -------------------------------------------------------------------------------- 1 | from pyqtgraph.Qt import QtGui, QtCore 2 | import pyqtgraph 3 | 4 | from gui.objects.graph.hitobject_plot import HitobjectPlot 5 | from gui.objects.graph.line_plot import LinePlot 6 | from misc.callback import callback 7 | 8 | from analysis.osu.std.map_data import StdMapData 9 | from generic.switcher import Switcher 10 | 11 | 12 | class Timeline(pyqtgraph.PlotWidget, Switcher): 13 | 14 | MIN_TIME = -5000 15 | 16 | def __init__(self): 17 | pyqtgraph.PlotWidget.__init__(self) 18 | Switcher.__init__(self) 19 | 20 | pyqtgraph.setConfigOptions(antialias=True) 21 | 22 | #self.showAxis('left', show=False) 23 | self.setLimits(xMin=Timeline.MIN_TIME) 24 | self.setRange(xRange=(-100, 10000)) 25 | self.setRange(yRange=(-1, 1)) 26 | self.getViewBox().setMouseEnabled(y=False) 27 | 28 | self.hitobjects_plot = HitobjectPlot([], [], []) 29 | self.getPlotItem().addItem(self.hitobjects_plot, ignoreBounds=True) 30 | 31 | self.response_plots = [ ] 32 | 33 | self.timeline_marker = pyqtgraph.InfiniteLine(angle=90, movable=True) 34 | self.timeline_marker.setBounds((Timeline.MIN_TIME + 100, None)) 35 | 36 | self.getPlotItem().addItem(self.timeline_marker, ignoreBounds=True) 37 | self.timeline_marker.sigPositionChanged.connect(self.time_changed_event) 38 | 39 | self.y_mid_pos = 0 40 | 41 | 42 | def set_map(self, beatmap): 43 | if beatmap == None: 44 | self.hitobjects_plot.update_data([], [], []) 45 | return 46 | 47 | map_data = StdMapData.get_map_data(beatmap.hitobjects) 48 | self.hitobjects_plot.update_data(StdMapData.start_times(map_data), StdMapData.end_times(map_data), StdMapData.get_objects(map_data), y_pos=self.y_mid_pos) 49 | 50 | 51 | def save(self, name=None): 52 | if name == None: 53 | if not self.active: ValueError('Current switched value is None') 54 | else: name = self.active 55 | 56 | data = { 57 | 'view' : self.getPlotItem().viewRange(), 58 | 'marker' : self.timeline_marker.value() 59 | } 60 | 61 | Switcher.add(self, name, data, overwrite=True) 62 | 63 | 64 | def load(self, name): 65 | if not self.switch(name): 66 | raise ValueError('Nothing saved under the name %s' % name) 67 | 68 | data = self.get() 69 | 70 | self.setRange(xRange=data['view'][0]) 71 | self.setRange(yRange=data['view'][1]) 72 | self.timeline_marker.setValue(data['marker']) 73 | 74 | 75 | def set_misc_data(self, data): 76 | min_y = 0 77 | max_y = 0 78 | 79 | for response in data: 80 | response_plot = LinePlot() 81 | self.response_plots.append(response_plot) 82 | self.getPlotItem().addItem(response_plot, ignoreBounds=True) 83 | 84 | data_x, data_y = response 85 | max_y = max(max_y, max(data_y)) 86 | min_y = min(min_y, min(data_y)) 87 | 88 | response_plot.update_data(response) 89 | 90 | self.setRange(yRange=(min_y, max_y)) 91 | 92 | y_mid_pos = (min_y + max_y)/2 93 | if self.y_mid_pos != y_mid_pos: 94 | self.y_mid_pos = y_mid_pos 95 | self.hitobjects_plot.update_data(self.hitobjects_plot.data, self.y_mid_pos) 96 | 97 | 98 | @callback 99 | def time_changed_event(self, marker): 100 | self.time_changed_event.emit(marker.value()) 101 | -------------------------------------------------------------------------------- /misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/misc/__init__.py -------------------------------------------------------------------------------- /misc/bezier.py: -------------------------------------------------------------------------------- 1 | from .math_utils import bernstein 2 | from .pos import Pos 3 | 4 | 5 | class Bezier(): 6 | 7 | def __init__(self, curve_points): 8 | self.curve_points = [] 9 | self.curve_distances = [ 0 ] 10 | self.total_distance = 0 11 | 12 | # Estimate the length of the curve 13 | approx_length = 0 14 | for i in range(len(curve_points) - 1): 15 | approx_length += curve_points[i].distance_to(curve_points[i + 1]) 16 | 17 | # subdivide the curve 18 | ncurve = int(approx_length / 4.0) + 2 19 | for i in range(ncurve): 20 | self.curve_points.append(Bezier.point_at(curve_points, float(i) / float(ncurve - 1))) 21 | 22 | # find the distance of each point from the previous point 23 | for i in range(1, len(curve_points)): 24 | self.curve_distances.append(curve_points[i].distance_to(curve_points[i - 1])) 25 | self.total_distance += self.curve_distances[i] 26 | 27 | 28 | # Returns the points along the curve of the Bezier curve. 29 | def get_curve_points(self): 30 | return self.curve_points 31 | 32 | 33 | # Returns the distances between a point of the curve and the last point. 34 | def get_curve_distances(self): 35 | return self.curve_distances 36 | 37 | 38 | # Returns the total distances of this Bezier curve. 39 | def get_total_curve_distance(self): 40 | return self.total_distance 41 | 42 | 43 | @staticmethod 44 | def point_at(curve_points, t): 45 | c = Pos(0, 0) 46 | n = len(curve_points) 47 | 48 | for i in range(n): 49 | b = bernstein(i, n - 1, t) 50 | c += Pos(curve_points[i].x * b, curve_points[i].y * b) 51 | 52 | return c -------------------------------------------------------------------------------- /misc/callback.py: -------------------------------------------------------------------------------- 1 | 2 | # Custom implementation of qt signal alternative 3 | # because qt is retarded and doesn't support multiple inheritence 4 | # 5 | # Basically this is used as a decorator and adds a list of functions to the function. 6 | # When emit is called, all the functions in the list are run. Their returns are collected 7 | # into Func.returns as the function execute 8 | # NOTE: The when a function is connected to another function, the connection applies to all 9 | # instances of the class the function is defined in 10 | def callback(func): 11 | 12 | def Func(*args, **kwargs): 13 | return func(*args, **kwargs) 14 | 15 | class FuncHelper(): 16 | 17 | @staticmethod 18 | def add_callback(callback, inst=None): 19 | if not inst in Func.callbacks: 20 | Func.callbacks[inst] = set() 21 | 22 | Func.callbacks[inst].add(callback) 23 | return Func 24 | 25 | @staticmethod 26 | def rmv_callback(callback, inst=None): 27 | try: Func.callbacks[inst].remove(callback) 28 | except KeyError: pass 29 | return Func 30 | 31 | @staticmethod 32 | def exec_callbacks(*args, inst=None, **kwargs): 33 | if not inst in Func.callbacks: return Func 34 | 35 | Func.returns = {} 36 | # Copy because one of the callbacks can be a disconnect to the refered callbacks 37 | # This would effectively change the size of Func.callbacks, causing an exception 38 | for callback in Func.callbacks[inst].copy(): 39 | if Func.anti_recurse: continue 40 | 41 | Func.anti_recurse = True 42 | Func.returns[callback] = callback(*args, **kwargs) 43 | Func.anti_recurse = False 44 | 45 | return Func 46 | 47 | @staticmethod 48 | def get_return_for(callback): 49 | if not callback in Func.returns: return None 50 | else: return Func.returns[callback] 51 | 52 | # This is required to ensure emiting from a function it is connected to 53 | # will not result in infinite recursion via self reference. This causes 54 | # the connected function to only execute one time from per specific function's emit 55 | Func.anti_recurse = False 56 | Func.returns = {} 57 | 58 | Func.connect = FuncHelper.add_callback 59 | Func.disconnect = FuncHelper.rmv_callback 60 | Func.emit = FuncHelper.exec_callbacks 61 | Func.ret = FuncHelper.get_return_for 62 | 63 | Func.callbacks = {} 64 | return Func -------------------------------------------------------------------------------- /misc/frozen_cls.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | 4 | ''' 5 | Name: Frozen Class 6 | Description: 7 | Freezes the class's attributes; Prohibits other classes from setting new attributes for this class 8 | Ensures all attributes that the class may have are declared as part of the class's definition and in __init__ beforehand 9 | To be used as a class decorator 10 | Note: If a class has a base class that is a Frozen Class, then it needs to be initialized after setting the derived class's attributes 11 | ''' 12 | # Thanks https://stackoverflow.com/questions/3603502/prevent-creating-new-attributes-outside-init/29368642#29368642 13 | def FrozenCls(cls): 14 | 15 | def init(func): 16 | @wraps(func) 17 | def wrapper(self, *args, **kwargs): 18 | self.__frozen = False 19 | func(self, *args, **kwargs) 20 | self.__frozen = True 21 | 22 | return wrapper 23 | 24 | 25 | def frozen_setattr(self, name, value): 26 | if not self.__frozen or hasattr(self, name): 27 | object.__setattr__(self, name, value) 28 | else: 29 | raise AttributeError('Please declare %s in %s\'s __init__ before setting it from elsewhere.' % (name, cls.__name__)) 30 | 31 | cls.__frozen = False 32 | cls.__setattr__ = frozen_setattr 33 | cls.__init__ = init(cls.__init__) 34 | 35 | return cls 36 | -------------------------------------------------------------------------------- /misc/json_obj.py: -------------------------------------------------------------------------------- 1 | 2 | class JsonObj: 3 | 4 | def __init__(self, params={}): 5 | self.json(params) 6 | 7 | 8 | def __repr__(self): 9 | try: return self.name 10 | except AttributeError: 11 | return super().__repr__() 12 | 13 | 14 | def json(self, params={}): 15 | if params: 16 | for param in params: 17 | setattr(self, param, params[param]) 18 | return None 19 | else: 20 | d = {} 21 | members = [ attr for attr in dir(self) if not callable(getattr(self, attr)) and not attr.startswith("__") ] 22 | for member in members: 23 | d.update({ member: getattr(self, member) }) 24 | return d -------------------------------------------------------------------------------- /misc/line.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Vector2D(): 4 | 5 | def __init__(self, start, end): 6 | self.start = start 7 | self.end = end 8 | 9 | 10 | def __eq__(self, other): 11 | return (self.start == other.start) and (self.end == other.end) 12 | 13 | 14 | def length(self): 15 | return self.start.distance_to(self.end) 16 | 17 | 18 | def directional_angle(self): 19 | # TODO 20 | pass 21 | 22 | -------------------------------------------------------------------------------- /misc/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import traceback 3 | import config 4 | import pathlib 5 | 6 | 7 | class Logger(logging.getLoggerClass()): 8 | 9 | def __init__(self, name, level=logging.NOTSET): 10 | super().__init__(name, level=logging.DEBUG) 11 | 12 | formatter = logging.Formatter('%(levelname)s %(asctime)s [ %(name)s ] %(message)s') 13 | 14 | self.sh = logging.StreamHandler() 15 | self.sh.setFormatter(formatter) 16 | 17 | if 'db' in config.runtime_mode: self.sh.setLevel(logging.DEBUG) 18 | else: self.sh.setLevel(logging.INFO) 19 | 20 | self.addHandler(self.sh) 21 | 22 | # \TODO: Maybe break up the logging file if it goes over 1MB 23 | # get file size 24 | # if over 1MB, then rename current logging file to '{start_date}_{end_date}_{logger_name}.log' 25 | # cut-paste into logging folder named '{logger_name}' 26 | 27 | self.fh = logging.FileHandler(str(config.log_path / (name + '.log'))) 28 | self.fh.setFormatter(formatter) 29 | self.fh.setLevel(logging.INFO) 30 | self.addHandler(self.fh) 31 | 32 | 33 | def __del__(self): 34 | self.sh.close(); self.removeHandler(self.sh) 35 | self.fh.close(); self.removeHandler(self.fh) 36 | 37 | ''' 38 | def error(self, msg): 39 | msg = msg.strip() 40 | if msg == 'None' or msg == 'N/A' or len(msg) == 0: 41 | self.exception(msg) 42 | else: 43 | self.error(msg) 44 | 45 | 46 | def critical(self, msg): 47 | msg = msg.strip() 48 | if msg == 'None' or msg == 'N/A' or len(msg) == 0: 49 | self.exception(msg) 50 | else: 51 | self.critical(msg) 52 | ''' 53 | 54 | 55 | def exception(self, msg): 56 | msg = msg.strip() 57 | msg += '\n' + traceback.format_exc() 58 | self.error(msg) 59 | 60 | def testbench(self, msg): 61 | if 'tb' not in config.runtime_mode: return 62 | self.debug(msg) -------------------------------------------------------------------------------- /misc/math_utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | 5 | def sign(val): 6 | return abs(val)/val 7 | 8 | 9 | def parity(val): 10 | return (sign(val) - 1)/2 11 | 12 | 13 | def bound(min_val, max_val, value): 14 | return min(max(value, min_val), max_val) 15 | 16 | 17 | def value_to_percent(min_val, max_val, value): 18 | return 1.0 - ((max_val - bound(min_val, value, max_val)) / (max_val - min_val)) 19 | 20 | 21 | def percent_to_value(min_val, max_val, percent): 22 | return max_val - (1.0 - percent)*(max_val - min_val) 23 | 24 | 25 | def px_per_rad(radius): 26 | return math.pi / (4.0 * radius) 27 | 28 | 29 | # Linear interpolation 30 | # Percent: 0.0 -> 1.0 31 | def lerp(low, high, percent): 32 | return low * (1.0 - percent) + high*percent 33 | 34 | 35 | def binomialCoefficient(n, k): 36 | if k < 0 or k > n: return 0 37 | if k == 0 or k == n: return 1 38 | 39 | k = min(k, n - k) # Take advantage of geometry 40 | c = 1 41 | 42 | for i in range(k): 43 | c *= (n - i) / (i + 1) 44 | 45 | return c 46 | 47 | 48 | def bernstein(i, n, t): 49 | return binomialCoefficient(n, i) * (t**i) * ((1 - t)**(n - i)) 50 | 51 | 52 | # Thanks http://llvm.org/docs/doxygen/html/LEB128_8h_source.html 53 | def decodeULEB128(data): 54 | shift = 0 55 | value = (data[0] & 0x7F) << shift 56 | 57 | for byte in data[1:]: 58 | if byte >= 128: break 59 | 60 | value += (byte & 0x7F) << shift 61 | shift += 7 62 | 63 | return value 64 | 65 | 66 | def guassian(val, sigma, norm=True): 67 | guass = 1.0 / (2.0 * sigma*math.sqrt(math.pi)) * math.exp(-(val / (2.0/sigma))*(val / (2.0 *sigma))) 68 | 69 | if norm: return guass / guassian(0, sigma, False) 70 | else: return guass 71 | 72 | 73 | # Returns a triangle wave function 74 | def triangle(val, amplitude): 75 | return abs((math.fmod(val + (amplitude / 2.0), amplitude)) - (amplitude / 2.0)) 76 | 77 | 78 | def find(val_list, val, selector=None): 79 | if selector: 80 | val_list = [ selector(val) for val in val_list ] 81 | 82 | if not len(val_list): 83 | raise IndexError 84 | 85 | if len(val_list) < 2: 86 | return 0 87 | 88 | if val <= val_list[0]: return 0 89 | if val >= val_list[-1]: return len(val_list) - 1 90 | 91 | if len(val_list) < 3: 92 | raise IndexError 93 | 94 | start = 0 95 | end = len(val_list) - 1 96 | mid = int((start + end) / 2) 97 | 98 | while True: 99 | if val_list[mid] == val: 100 | return mid 101 | 102 | if val_list[mid] < val < val_list[mid + 1]: 103 | return mid 104 | 105 | if end - start == 0: 106 | return -1 107 | 108 | if val_list[mid] < val: start = mid + 1 109 | else: end = mid - 1 110 | mid = int((start + end) / 2) 111 | 112 | 113 | def prob_not(x): return 1.0 - x 114 | def prob_and(x, y): return x*y 115 | def prob_or(x, y): return (x + y) - x*y 116 | 117 | def prob_trials(initial_prob, trials): 118 | current_prob = initial_prob 119 | for _ in range(trials): 120 | current_prob = prob_or(current_prob, initial_prob) 121 | return current_prob -------------------------------------------------------------------------------- /misc/pos.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | """ 5 | Object holds 2D coordinate data and provides 2D geometric operations 6 | 7 | Input: 8 | x - x-coordinate 9 | y - y-coordinate 10 | """ 11 | class Pos(): 12 | 13 | def __init__(self, x, y): 14 | self.x = x 15 | self.y = y 16 | 17 | 18 | def __eq__(self, other): 19 | return self.x == other.x and self.y == other.y 20 | 21 | 22 | def __ne__(self, other): 23 | return not (self == other) 24 | 25 | 26 | def __add__(self, other): 27 | return Pos(self.x + other.x, self.y + other.y) 28 | 29 | 30 | def __sub__(self, other): 31 | return Pos(self.x - other.x, self.y - other.y) 32 | 33 | 34 | def __mul__(self, other): 35 | return Pos(self.x * other.x, self.y * other.y) 36 | 37 | 38 | def __truediv__(self, other): 39 | return Pos(self.x / other.x, self.y / other.y) 40 | 41 | 42 | def __str__(self): 43 | return '(' + str(self.x) + ', ' + str(self.y) + ')' 44 | 45 | 46 | def distance_to(self, pos): 47 | x_delta = self.x - pos.x 48 | y_delta = self.y - pos.y 49 | return math.sqrt(x_delta*x_delta + y_delta*y_delta) 50 | 51 | 52 | def midpoint(self, other): 53 | return (other + self)/Pos(2.0, 2.0) 54 | 55 | 56 | def slope(self, other): 57 | if self.x - other.x == 0: return float('inf') 58 | else: return (self.y - other.y) / (self.x - other.x) 59 | 60 | 61 | def nor(self): 62 | return Pos(-self.y, self.x) 63 | 64 | 65 | def rot(self, point, radians): 66 | # TODO 67 | pass 68 | 69 | 70 | def flip(self, axis_radians): 71 | # TODO 72 | pass 73 | 74 | 75 | def abs(self): 76 | return Pos(abs(self.x), abs(self.y)) 77 | 78 | 79 | def dot(self, other): 80 | return self.x*other.x + self.y*other.y 81 | 82 | 83 | def cross(self, other): 84 | return self.y*other.y - self.y*other.x -------------------------------------------------------------------------------- /osu/local/beatmap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abraker95/ultimate_osu_analyzer/8b211a01c2364d51b8bf08e045e9280ec3a04242/osu/local/beatmap/__init__.py -------------------------------------------------------------------------------- /osu/local/collection/collectionIO.py: -------------------------------------------------------------------------------- 1 | import math 2 | import lzma, struct, datetime 3 | 4 | from osu.local.hitobject.std.std import Std 5 | from osu.local.hitobject.mania.mania import Mania 6 | 7 | 8 | class CollectionIO(): 9 | 10 | def __init__(self, data): 11 | self.offset = 0 12 | self.version = None 13 | self.num_collections = None 14 | 15 | self.collections = {} 16 | 17 | self.__parse_version(data) 18 | for _ in range(self.num_collections): 19 | self.__add_entry(*self.__parse_collection(data)) 20 | 21 | 22 | def __add_entry(self, name, md5_map_hashes): 23 | self.collections[name] = md5_map_hashes 24 | 25 | 26 | def __parse_version(self, data): 27 | format_specifier = '