diff --git a/.gitignore b/.gitignore index 85a7190..c49b6ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ target .direnv .envrc -data +data/db.sqlite +data/jackets diff --git a/Cargo.lock b/Cargo.lock index 0af8c67..74707d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1292,6 +1292,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kd-tree" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89ee4e60e82cf7024e5e94618c646fbf61ce7501dc5898b3d12786442d3682" +dependencies = [ + "num-traits", + "ordered-float", + "paste", + "typenum", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1659,6 +1671,15 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "ordered-float" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +dependencies = [ + "num-traits", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -2399,8 +2420,10 @@ name = "shimmeringmoon" version = "0.1.0" dependencies = [ "chrono", + "csv", "edit-distance", "image", + "kd-tree", "num", "plotlib", "poise", @@ -2408,6 +2431,7 @@ dependencies = [ "sqlx", "tesseract", "tokio", + "typenum", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1b72b73..7888eb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,10 @@ edition = "2021" [dependencies] chrono = "0.4.38" +csv = "1.3.0" edit-distance = "2.1.0" image = "0.25.1" +kd-tree = "0.6.0" num = "0.4.3" plotlib = "0.5.1" poise = "0.6.1" @@ -14,6 +16,7 @@ prettytable-rs = "0.10.0" sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "chrono"] } tesseract = "0.15.1" tokio = {version="1.38.0", features=["rt-multi-thread"]} +typenum = "1.17.0" [profile.dev.package.sqlx-macros] opt-level = 3 diff --git a/data/charts.csv b/data/charts.csv new file mode 100644 index 0000000..bd10e1b --- /dev/null +++ b/data/charts.csv @@ -0,0 +1,1241 @@ +Testify,BYD,12,12,0,"2,221",-,-,0 +Tempestissimo,BYD,11,11.5,0,"1,540",-,-,0 +Arcana Eden,BYD,11,11.4,0,"2,134",-,-,0 +Pentiment,BYD,11,11.4,0,"1,741",-,-,0 +Arghena,FTR,11,11.3,0,1444,-,-,0 +Fracture Ray,FTR,11,11.3,0,"1,279",-,-,0 +Grievous Lady,FTR,11,11.3,0,"1,450",-,-,0 +World Ender,BYD,11,11.2,0,"1,661",-,-,0 +Vicious [ANTi] Heroism,BYD,11,11.1,0,1772,-,-,0 +Abstruse Dilemma,FTR,11,11.1,0,"1,467",-,-,0 +Aegleseeker,FTR,11,11.1,0,"1,568",-,-,0 +PRAGMATISM -RESURRECTION-,BYD,11,11,0,"1,502",-,-,0 +SAIKYO STRONGER,FTR,11,11,0,"1,384",-,-,0 +Einherjar Joker,BYD,10+,10.9,0,1857,-,-,0 +"Infinite Strife,",BYD,10+,10.9,0,"1,633",-,-,0 +Distorted Fate,ETR,10+,10.9,0,"1,402",-,-,0 +#1f1e33,FTR,10+,10.9,0,"1,576",-,-,0 +BUCHiGiRE Berserker,FTR,10+,10.9,0,"1,412",-,-,0 +CHAOS,FTR,10+,10.9,0,"1,369",-,-,0 +Dantalion,FTR,10+,10.9,0,"1,478",-,-,0 +LAMIA,FTR,10+,10.9,0,"1,385",-,-,0 +Misdeed -la bonté de Dieu et l'origine du mal-,FTR,10+,10.9,0,"1,522",-,-,0 +TEmPTaTiON,FTR,10+,10.9,0,"1,099",-,-,0 +TeraVolt,FTR,10+,10.9,0,1008,-,-,0 +Heavensdoor,BYD,10+,10.8,0,"1,534",-,-,0 +Désive,ETR,10+,10.8,0,"1,340",-,-,0 +Divine Light of Myriad,FTR,10+,10.8,0,"1,021",-,-,0 +͟͝͞Ⅱ́̕,FTR,10+,10.8,0,"1,051",-,-,0 +Meta-Mysteria,FTR,10+,10.8,0,1309,-,-,0 +Ringed Genesis,FTR,10+,10.8,0,"1,146",-,-,0 +Spider's Thread,FTR,10+,10.8,0,"1,203",-,-,0 +Testify,FTR,10+,10.8,0,"1,766",-,-,0 +World Vanquisher,FTR,10+,10.8,0,"1,452",-,-,0 +ω4,FTR,10+,10.8,0,"1,393",-,-,0 +AMAZING MIGHTYYYY!!!!,FTR,10+,10.7,0,"1,249",-,-,0 +Axium Crisis,FTR,10+,10.7,0,"1,094",-,-,0 +Cyaegha,FTR,10+,10.7,0,"1,368",-,-,0 +GENOCIDER,FTR,10+,10.7,0,"1,483",-,-,0 +Halcyon,FTR,10+,10.7,0,"1,227",-,-,0 +KYOREN ROMANCE,FTR,10+,10.7,0,"1,519",-,-,0 +ouroboros -twin stroke of the end-,FTR,10+,10.7,0,"1,369",-,-,0 +PRIMITIVE LIGHTS,FTR,10+,10.7,0,"1,524",-,-,0 +REKKA RESONANCE,FTR,10+,10.7,0,"1,212",-,-,0 +Singularity,FTR,10+,10.7,0,"1,105",-,-,0 +Stasis,FTR,10+,10.7,0,"1,521",-,-,0 +Tempestissimo,FTR,10+,10.7,0,"1,254",-,-,0 +Purple Verse,BYD,10,10.6,0,1202,-,-,0 +AttraqtiA,FTR,10,10.6,0,"1,433",-,-,0 +corps-sans-organes,FTR,10,10.6,0,"1,077",-,-,0 +cyanine,FTR,10,10.6,0,"1,171",-,-,0 +GLORY: ROAD,FTR,10,10.6,0,"1,479",-,-,0 +Live Fast Die Young,FTR,10,10.6,0,"1,292",-,-,0 +Manic Jeer,FTR,10,10.6,0,"1,286",-,-,0 +NULL APOPHENIA,FTR,10,10.6,0,"1,299",-,-,0 +Overwhelm,FTR,10,10.6,0,"1,251",-,-,0 +Qovat,FTR,10,10.6,0,1299,-,-,0 +Seclusion,FTR,10,10.6,0,"1,132",-,-,0 +Sheriruth (Laur Remix),FTR,10,10.6,0,"1,134",-,-,0 +UNKNOWN LEVELS,FTR,10,10.6,0,"1,149",-,-,0 +Libertas,BYD,10,10.5,0,1048,-,-,0 +overdead.,BYD,10,10.5,0,"1,503",-,-,0 +trappola bewitching,BYD,10,10.5,0,"1,086",-,-,0 +Aleph-0,FTR,10,10.5,0,919,-,-,0 +Arcana Eden,FTR,10,10.5,0,"1,792",-,-,0 +Aurgelmir,FTR,10,10.5,0,"1,100",-,-,0 +Back to Basics,FTR,10,10.5,0,"1,544",-,-,0 +eden,FTR,10,10.5,0,1365,-,-,0 +Ego Eimi,FTR,10,10.5,0,1223,-,-,0 +"Good bye, Merry-Go-Round.",FTR,10,10.5,0,"1,084",-,-,0 +Last Celebration,FTR,10,10.5,0,"1,475",-,-,0 +Lightning Screw,FTR,10,10.5,0,"1,192",-,-,0 +MIRINAE,FTR,10,10.5,0,"1,277",-,-,0 +To the Milky Way,FTR,10,10.5,0,"1,392",-,-,0 +ultradiaxon-N3,FTR,10,10.5,0,"1,228",-,-,0 +Wish Upon a Snow,FTR,10,10.5,0,1309,-,-,0 +XTREME,FTR,10,10.5,0,1258,-,-,0 +IMPACT,BYD,10,10.4,0,"1,392",-,-,0 +Singularity VVVIP,BYD,10,10.4,0,"1,114",-,-,0 +Twilight Concerto,ETR,10,10.4,0,962,-,-,0 +[X],FTR,10,10.4,0,"1,190",-,-,0 +Chronicle,FTR,10,10.4,0,"1,264",-,-,0 +Climax,FTR,10,10.4,0,"1,367",-,-,0 +Crimson Throne,FTR,10,10.4,0,"1,313",-,-,0 +Felis,FTR,10,10.4,0,"1,153",-,-,0 +Garakuta Doll Play,FTR,10,10.4,0,"1,035",-,-,0 +GIMME DA BLOOD,FTR,10,10.4,0,"1,093",-,-,0 +Ikazuchi,FTR,10,10.4,0,"1,347",-,-,0 +Kissing Lucifer,FTR,10,10.4,0,"1,183",-,-,0 +Lethal Voltage,FTR,10,10.4,0,"1,497",-,-,0 +Lucid Traveler,FTR,10,10.4,0,"1,341",-,-,0 +PUPA,FTR,10,10.4,0,"1,099",-,-,0 +Rise of the World,FTR,10,10.4,0,"1,176",-,-,0 +Tiferet,FTR,10,10.4,0,"1,086",-,-,0 +Valhalla:0,FTR,10,10.4,0,"1,173",-,-,0 +γuarδina,FTR,10,10.4,0,"1,120",-,-,0 +99 Glooms,FTR,10,10.3,0,"1,294",-,-,0 +Ether Strike,FTR,10,10.3,0,"1,170",-,-,0 +"Hiiro Gekka, Kyoushou no Zetsu (nayuta 2017 ver.)",FTR,10,10.3,0,"1,126",-,-,0 +IZANA,FTR,10,10.3,0,976,-,-,0 +Metallic Punisher,FTR,10,10.3,0,"1,238",-,-,0 +Nirv lucE,FTR,10,10.3,0,980,-,-,0 +Pentiment,FTR,10,10.3,0,"1,345",-,-,0 +To: Alice Liddell,FTR,10,10.3,0,998,-,-,0 +Viyella's Tears,FTR,10,10.3,0,"1,403",-,-,0 +OMAKENO Stroke,BYD,10,10.2,0,931,-,-,0 +Quon (DJ Noriken),BYD,10,10.2,0,"1,044",-,-,0 +Capella,FTR,10,10.2,0,"1,159",-,-,0 +conflict,FTR,10,10.2,0,"1,056",-,-,0 +G e n g a o z o,FTR,10,10.2,0,"1,353",-,-,0 +Löschen,FTR,10,10.2,0,"1,235",-,-,0 +Magnolia,FTR,10,10.2,0,895,-,-,0 +Malicious Mischance,FTR,10,10.2,0,"1,126",-,-,0 +Medusa,FTR,10,10.2,0,931,-,-,0 +NEO WINGS,FTR,10,10.2,0,"1,328",-,-,0 +Redolent Shape,FTR,10,10.2,0,"1,088",-,-,0 +αterlβus,FTR,10,10.2,0,"1,030",-,-,0 +lastendconductor,BYD,10,10.1,0,"1,339",-,-,0 +Party Vinyl,BYD,10,10.1,0,946,-,-,0 +Logos,FTR,10,10.1,0,1040,-,-,0 +PRAGMATISM,FTR,10,10.1,0,942,-,-,0 +Scarlet Lance,FTR,10,10.1,0,"1,130",-,-,0 +Sheriruth,FTR,10,10.1,0,"1,151",-,-,0 +Trrricksters!!,FTR,10,10.1,0,"1,183",-,-,0 +Bookmaker (2D Version),BYD,10,10,0,"1,287",-,-,0 +Red and Blue and Green,BYD,10,10,0,"1,194",-,-,0 +Alexandrite,FTR,10,10,0,"1,040",-,-,0 +Bamboo,FTR,10,10,0,"1,050",-,-,0 +Chromafill,FTR,10,10,0,"1,130",-,-,0 +Free Myself,FTR,10,10,0,"1,132",-,-,0 +HIVEMIND,FTR,10,10,0,"1,252",-,-,0 +Kanbu de Tomatte Sugu Tokeru,FTR,10,10,0,1108,-,-,0 +Masquerade Legion,FTR,10,10,0,"1,064",-,-,0 +Mirzam,FTR,10,10,0,"1,303",-,-,0 +Modelista,FTR,10,10,0,"1,010",-,-,0 +Oshama Scramble!,FTR,10,10,0,"1,073",-,-,0 +trappola bewitching,FTR,10,10,0,"1,086",-,-,0 +Vicious Heroism,FTR,10,10,0,"1,150",-,-,0 +Xanatos,FTR,10,10,0,"1,232",-,-,0 +Dement ~after legend~,BYD,9+,9.9,0,"1,040",-,-,0 +Ignotus Afterburn,BYD,9+,9.9,0,"1,077",-,-,0 +La'qryma of the Wasteland,BYD,9+,9.9,0,"1,161",-,-,0 +7thSense,FTR,9+,9.9,0,925,-,-,0 +Amekagura,FTR,9+,9.9,0,1076,-,-,0 +Antagonism,FTR,9+,9.9,0,"1,142",-,-,0 +Arcahv,FTR,9+,9.9,0,"1,065",-,-,0 +Astra walkthrough,FTR,9+,9.9,0,"1,191",-,-,0 +Awaken In Ruins,FTR,9+,9.9,0,754,-,-,0 +Beautiful Dreamer,FTR,9+,9.9,0,"1,139",-,-,0 +Désive,FTR,9+,9.9,0,"1,273",-,-,0 +Devillic Sphere,FTR,9+,9.9,0,"1,129",-,-,0 +Genesis (Morrigan feat. Lily),FTR,9+,9.9,0,867,-,-,0 +Heavensdoor,FTR,9+,9.9,0,"1,101",-,-,0 +"Infinite Strife,",FTR,9+,9.9,0,"1,511",-,-,0 +INTERNET YAMERO,FTR,9+,9.9,0,"1,222",-,-,0 +MAXRAGE,FTR,9+,9.9,0,"1,184",-,-,0 +Nameless Passion,FTR,9+,9.9,0,1223,-,-,0 +New York Back Raise,FTR,9+,9.9,0,"1,091",-,-,0 +Nhelv,FTR,9+,9.9,0,"1,108",-,-,0 +SOUNDWiTCH,FTR,9+,9.9,0,785,-,-,0 +Summer Fireworks of Love,FTR,9+,9.9,0,"1,088",-,-,0 +The Survivor (Game Edit),FTR,9+,9.9,0,"1,100",-,-,0 +Trap Crow,FTR,9+,9.9,0,"1,074",-,-,0 +World Ender,FTR,9+,9.9,0,"1,225",-,-,0 +Cybernecia Catharsis,BYD,9+,9.8,0,991,-,-,0 +FREEF4LL,BYD,9+,9.8,0,"1,336",-,-,0 +GOODTEK (Arcaea Edit),BYD,9+,9.8,0,"1,103",-,-,0 +Kanagawa Cyber Culvert,BYD,9+,9.8,0,"1,121",-,-,0 +Lost Civilization,BYD,9+,9.8,0,"1,061",-,-,0 +MORNINGLOOM,ETR,9+,9.8,0,"1,035",-,-,0 +Ai Drew,FTR,9+,9.8,0,"1,066",-,-,0 +Ascent,FTR,9+,9.8,0,"1,023",-,-,0 +Black Territory,FTR,9+,9.8,0,"1,195",-,-,0 +Callima Karma,FTR,9+,9.8,0,"1,222",-,-,0 +Corruption,FTR,9+,9.8,0,1.293,-,-,0 +Defection,FTR,9+,9.8,0,"1,141",-,-,0 +DX Choseinou Full Metal Shojo,FTR,9+,9.8,0,808,-,-,0 +Einherjar Joker,FTR,9+,9.8,0,"1,159",-,-,0 +Far Away Light,FTR,9+,9.8,0,"1,322",-,-,0 +FLUFFY FLASH,FTR,9+,9.8,0,"1,329",-,-,0 +Galaxy Friends,FTR,9+,9.8,0,"1,013",-,-,0 +Got hive of Ra,FTR,9+,9.8,0,794,-,-,0 +Haze of Autumn,FTR,9+,9.8,0,"1,077",-,-,0 +Heavenly caress,FTR,9+,9.8,0,"1,560",-,-,0 +Hybris (The one who shattered),FTR,9+,9.8,0,"1,196",-,-,0 +init(),FTR,9+,9.8,0,"1,204",-,-,0 +Kanjou no Matenrou ~Arr.Demetori,FTR,9+,9.8,0,1294,-,-,0 +Linear Accelerator,FTR,9+,9.8,0,905,-,-,0 +LIVHT MY WΔY,FTR,9+,9.8,0,954,-,-,0 +Lost Desire,FTR,9+,9.8,0,"1,154",-,-,0 +Macrocosmic Modulation,FTR,9+,9.8,0,"1,117",-,-,0 +Memory Forest,FTR,9+,9.8,0,978,-,-,0 +Old School Salvage,FTR,9+,9.8,0,"1,316",-,-,0 +Ouvertüre,FTR,9+,9.8,0,913,-,-,0 +Protoflicker,FTR,9+,9.8,0,"1,042",-,-,0 +Scarlet Cage,FTR,9+,9.8,0,"1,195",-,-,0 +Technicolour,FTR,9+,9.8,0,1140,-,-,0 +THE ULTIMACY,FTR,9+,9.8,0,"1,304",-,-,0 +To the Furthest Dream,FTR,9+,9.8,0,1102,-,-,0 +World Fragments III (radio edit),FTR,9+,9.8,0,"1,387",-,-,0 +Xeraphinite,FTR,9+,9.8,0,"1,048",-,-,0 +Last | Eternity,BYD,9+,9.7,0,790,-,-,0 +Moonheart,BYD,9+,9.7,0,"1,139",-,-,0 +Oblivia,BYD,9+,9.7,0,1021,-,-,0 +qualia -ideaesthesia-,BYD,9+,9.7,0,"1,288",-,-,0 +Teriqma,BYD,9+,9.7,0,954,-,-,0 +Innocence,ETR,9+,9.7,0,1157,-,-,0 +IONOSTREAM,ETR,9+,9.7,0,871,-,-,0 +Altale,FTR,9+,9.7,0,690,-,-,0 +AlterAle,FTR,9+,9.7,0,"1,277",-,-,0 +amygdata,FTR,9+,9.7,0,"1,199",-,-,0 +B.B.K.K.B.K.K.,FTR,9+,9.7,0,976,-,-,0 +BADTEK,FTR,9+,9.7,0,916,-,-,0 +BATTLE NO.1,FTR,9+,9.7,0,"1,042",-,-,0 +Black Lotus,FTR,9+,9.7,0,965,-,-,0 +BLRINK,FTR,9+,9.7,0,"1,015",-,-,0 +Dazzle hop,FTR,9+,9.7,0,"1,022",-,-,0 +Dreadnought,FTR,9+,9.7,0,"1,099",-,-,0 +Filament,FTR,9+,9.7,0,991,-,-,0 +goldenslaughterer,FTR,9+,9.7,0,"1,326",-,-,0 +Heart Jackin',FTR,9+,9.7,0,"1,112",-,-,0 +Hotarubi no Yuki,FTR,9+,9.7,0,991,-,-,0 +Let's Rock (Arcaea mix),FTR,9+,9.7,0,"1,177",-,-,0 +Lethaeus,FTR,9+,9.7,0,900,-,-,0 +Lost in the Abyss,FTR,9+,9.7,0,"1,179",-,-,0 +Luna Rossa,FTR,9+,9.7,0,920,-,-,0 +Mazy Metroplex,FTR,9+,9.7,0,952,-,-,0 +Monochrome Princess,FTR,9+,9.7,0,974,-,-,0 +nέo κósmo,FTR,9+,9.7,0,979,-,-,0 +RGB,FTR,9+,9.7,0,1131,-,-,0 +SACRIFICE feat. ayame,FTR,9+,9.7,0,958,-,-,0 +Sulfur,FTR,9+,9.7,0,"1,045",-,-,0 +SUPERNOVA,FTR,9+,9.7,0,"1,123",-,-,0 +The Message,FTR,9+,9.7,0,992,-,-,0 +Ultimate taste,FTR,9+,9.7,0,"1,405",-,-,0 +Vandalism,FTR,9+,9.7,0,"1,087",-,-,0 +µ,FTR,9+,9.7,0,"1,256",-,-,0 +Infinity Heaven,BYD,9,9.6,0,986,-,-,0 +Last | Moment,BYD,9,9.6,0,888,-,-,0 +next to you,BYD,9,9.6,0,954,-,-,0 +Purgatorium,BYD,9,9.6,0,"1,051",-,-,0 +A Wandering Melody of Love,FTR,9,9.6,0,931,-,-,0 +Alone & Lorn,FTR,9,9.6,0,970,-,-,0 +Astral tale,FTR,9,9.6,0,884,-,-,0 +Avant Raze,FTR,9,9.6,0,"1,125",-,-,0 +carmine:scythe,FTR,9,9.6,0,"1,164",-,-,0 +Distorted Fate,FTR,9,9.6,0,"1,172",-,-,0 +ENERGY SYNERGY MATRIX,FTR,9,9.6,0,922,-,-,0 +Fallensquare,FTR,9,9.6,0,703,-,-,0 +IMPACT,FTR,9,9.6,0,"1,231",-,-,0 +Lazy Addiction,FTR,9,9.6,0,"1,031",-,-,0 +Loveless Dress,FTR,9,9.6,0,850,-,-,0 +LunarOrbit -believe in the Espebranch road-,FTR,9,9.6,0,"1,058",-,-,0 +Purple Verse,FTR,9,9.6,0,"1,023",-,-,0 +Quon (Feryquitous),FTR,9,9.6,0,991,-,-,0 +Stratoliner,FTR,9,9.6,0,877,-,-,0 +Vindication,FTR,9,9.6,0,"1,075",-,-,0 +Antithese,BYD,9,9.5,0,968,-,-,0 +Fairytale,BYD,9,9.5,0,932,-,-,0 +Lumia,BYD,9,9.5,0,814,-,-,0 +Jingle,ETR,9,9.5,0,"1,047",-,-,0 +〇、,FTR,9,9.5,0,708,-,-,0 +Cybernecia Catharsis,FTR,9,9.5,0,946,-,-,0 +DataErr0r,FTR,9,9.5,0,955,-,-,0 +DRG,FTR,9,9.5,0,872,-,-,0 +Dynitikós,FTR,9,9.5,0,986,-,-,0 +Evening in Scarlet,FTR,9,9.5,0,922,-,-,0 +felys final remix,FTR,9,9.5,0,1130,-,-,0 +GIMMICK,FTR,9,9.5,0,733,-,-,0 +Illegal Paradise,FTR,9,9.5,0,"1,061",-,-,0 +MAHOROBA,FTR,9,9.5,0,828,-,-,0 +NULCTRL,FTR,9,9.5,0,715,-,-,0 +OMAKENO Stroke,FTR,9,9.5,0,869,-,-,0 +On And On!! feat. Jenga,FTR,9,9.5,0,836,-,-,0 +Remind the Souls (Short Version),FTR,9,9.5,0,945,-,-,0 +Specta,FTR,9,9.5,0,"1,096",-,-,0 +STAGER (ALL STAGE CLEAR),FTR,9,9.5,0,"1,004",-,-,0 +Transient Space,FTR,9,9.5,0,805,-,-,0 +Twilight Concerto,FTR,9,9.5,0,803,-,-,0 +ΟΔΥΣΣΕΙΑ,FTR,9,9.5,0,"1,092",-,-,0 +dropdead,PRS,9,9.5,0,1323,-,-,0 +Fracture Ray,PRS,9,9.5,0,"1,343",-,-,0 +Tempestissimo,PRS,9,9.5,0,"1,034",-,-,0 +inkar-usi,BYD,9,9.4,0,857,-,-,0 +MERLIN,BYD,9,9.4,0,881,-,-,0 +HELLOHELL,ETR,9,9.4,0,770,-,-,0 +AI[UE]OON,FTR,9,9.4,0,989,-,-,0 +Be There,FTR,9,9.4,0,982,-,-,0 +CROSS†OVER,FTR,9,9.4,0,"1,094",-,-,0 +CROSS†SOUL,FTR,9,9.4,0,"1,081",-,-,0 +Crystal Gravity,FTR,9,9.4,0,872,-,-,0 +Dreamin' Attraction!!,FTR,9,9.4,0,"1,129",-,-,0 +Equilibrium,FTR,9,9.4,0,951,-,-,0 +FANTA5Y,FTR,9,9.4,0,965,-,-,0 +Final Step!,FTR,9,9.4,0,"1,056",-,-,0 +Head BONK ache,FTR,9,9.4,0,"1,061",-,-,0 +Impure Bird,FTR,9,9.4,0,805,-,-,0 +Lapis,FTR,9,9.4,0,920,-,-,0 +lastendconductor,FTR,9,9.4,0,"1,209",-,-,0 +Let you DIVE! (nitro rmx),FTR,9,9.4,0,"1,049",-,-,0 +Party Vinyl,FTR,9,9.4,0,800,-,-,0 +Raven's Pride,FTR,9,9.4,0,"1,030",-,-,0 +Red and Blue,FTR,9,9.4,0,845,-,-,0 +Teriqma,FTR,9,9.4,0,873,-,-,0 +VECTOЯ,FTR,9,9.4,0,"1,299",-,-,0 +with U,FTR,9,9.4,0,932,-,-,0 +Yosakura Fubuki,FTR,9,9.4,0,931,-,-,0 +Your voice so... feat. Such,FTR,9,9.4,0,"1,013",-,-,0 +Arghena,PRS,9,9.4,0,1082,-,-,0 +SAIKYO STRONGER,PRS,9,9.4,0,"1,067",-,-,0 +Testify,PRS,9,9.4,0,"1,225",-,-,0 +Auxesia,FTR,9,9.3,0,"1,000",-,-,0 +Blaster,FTR,9,9.3,0,"1,002",-,-,0 +Blocked Library,FTR,9,9.3,0,850,-,-,0 +Can I Friend You on Bassbook? Lol,FTR,9,9.3,0,861,-,-,0 +False Embellishment,FTR,9,9.3,0,969,-,-,0 +Feels So Right feat. Renko,FTR,9,9.3,0,947,-,-,0 +Floating World,FTR,9,9.3,0,"1,047",-,-,0 +Glow,FTR,9,9.3,0,916,-,-,0 +GOODTEK (Arcaea Edit),FTR,9,9.3,0,968,-,-,0 +Heart,FTR,9,9.3,0,872,-,-,0 +Ignotus,FTR,9,9.3,0,"1,225",-,-,0 +Lost Emotion feat. nomico,FTR,9,9.3,0,"1,123",-,-,0 +MANTIS (Arcaea Ultra-Bloodrush VIP),FTR,9,9.3,0,"1,014",-,-,0 +Oracle,FTR,9,9.3,0,963,-,-,0 +PICO-Pico-Translation!,FTR,9,9.3,0,"1,049",-,-,0 +Sakura Fubuki,FTR,9,9.3,0,837,-,-,0 +Syro,FTR,9,9.3,0,"1,150",-,-,0 +The Formula,FTR,9,9.3,0,957,-,-,0 +Grievous Lady,PRS,9,9.3,0,"1,194",-,-,0 +Alice à la mode,FTR,9,9.2,0,872,-,-,0 +Anökumene,FTR,9,9.2,0,851,-,-,0 +cocoro*cosmetic,FTR,9,9.2,0,"1,025",-,-,0 +Dancin' on a Cat's Paw,FTR,9,9.2,0,891,-,-,0 +Leave All Behind,FTR,9,9.2,0,828,-,-,0 +Libertas,FTR,9,9.2,0,947,-,-,0 +Lost Civilization,FTR,9,9.2,0,986,-,-,0 +Phantasia,FTR,9,9.2,0,952,-,-,0 +Redraw the Colorless World,FTR,9,9.2,0,886,-,-,0 +Rugie,FTR,9,9.2,0,975,-,-,0 +Strongholds,FTR,9,9.2,0,922,-,-,0 +Used to be,FTR,9,9.2,0,799,-,-,0 +#1f1e33,PRS,9,9.2,0,"1,144",-,-,0 +Alice's Suitcase,FTR,9,9.1,0,999,-,-,0 +Blue Rose,FTR,9,9.1,0,955,-,-,0 +dropdead,FTR,9,9.1,0,823,-,-,0 +Essence of Twilight,FTR,9,9.1,0,"1,204",-,-,0 +Faint Light (Arcaea Edit),FTR,9,9.1,0,809,-,-,0 +Iconoclast,FTR,9,9.1,0,795,-,-,0 +Journey,FTR,9,9.1,0,997,-,-,0 +La'qryma of the Wasteland,FTR,9,9.1,0,956,-,-,0 +Life is PIANO,FTR,9,9.1,0,674,-,-,0 +qualia -ideaesthesia-,FTR,9,9.1,0,"1,022",-,-,0 +Small Cloud Sugar Candy,FTR,9,9.1,0,919,-,-,0 +Aegleseeker,PRS,9,9.1,0,"1,235",-,-,0 +Shades of Light in a Transcendent Realm,BYD,9,9,0,"1,042",-,-,0 +Vexaria,BYD,9,9,0,785,-,-,0 +Coastal Highway,FTR,9,9,0,732,-,-,0 +Empire of Winter,FTR,9,9,0,920,-,-,0 +Flyburg and Endroll,FTR,9,9,0,930,-,-,0 +Galactic Love,FTR,9,9,0,813,-,-,0 +Jump,FTR,9,9,0,841,-,-,0 +Kanagawa Cyber Culvert,FTR,9,9,0,"1,111",-,-,0 +Last,FTR,9,9,0,831,-,-,0 +Lawless Point,FTR,9,9,0,838,-,-,0 +Primeval Texture,FTR,9,9,0,810,-,-,0 +ReviXy,FTR,9,9,0,"1,047",-,-,0 +Turbocharger,FTR,9,9,0,979,-,-,0 +Suomi,ETR,8+,8.9,0,732,-,-,0 +Chelsea,FTR,8+,8.9,0,650,-,-,0 +Chronostasis,FTR,8+,8.9,0,916,-,-,0 +Evoltex (poppi'n mix),FTR,8+,8.9,0,775,-,-,0 +Flashback,FTR,8+,8.9,0,856,-,-,0 +Give Me a Nightmare,FTR,8+,8.9,0,948,-,-,0 +Lights of Muse,FTR,8+,8.9,0,580,-,-,0 +Maze No.9,FTR,8+,8.9,0,775,-,-,0 +memoryfactory.lzh,FTR,8+,8.9,0,672,-,-,0 +MERLIN,FTR,8+,8.9,0,712,-,-,0 +Abstruse Dilemma,PRS,8+,8.9,0,"1,127",-,-,0 +Chronicle,PRS,8+,8.9,0,"1,077",-,-,0 +GENOCIDER,PRS,8+,8.9,0,"1,025",-,-,0 +͟͝͞Ⅱ́̕,PRS,8+,8.9,0,816,-,-,0 +Lucid Traveler,PRS,8+,8.9,0,"1,006",-,-,0 +REKKA RESONANCE,PRS,8+,8.9,0,"1,051",-,-,0 +TeraVolt,PRS,8+,8.9,0,804,-,-,0 +To the Milky Way,PRS,8+,8.9,0,"1,154",-,-,0 +Clotho and the stargazer,ETR,8+,8.8,0,1031,-,-,0 +Hidden Rainbows of Epicurus,ETR,8+,8.8,0,884,-,-,0 +Antithese,FTR,8+,8.8,0,877,-,-,0 +Cosmica,FTR,8+,8.8,0,773,-,-,0 +Cosmo Pop Funclub,FTR,8+,8.8,0,809,-,-,0 +CYCLES,FTR,8+,8.8,0,695,-,-,0 +Dialnote,FTR,8+,8.8,0,684,-,-,0 +MORNINGLOOM,FTR,8+,8.8,0,940,-,-,0 +next to you,FTR,8+,8.8,0,824,-,-,0 +Particle Arts,FTR,8+,8.8,0,925,-,-,0 +Solitary Dream,FTR,8+,8.8,0,972,-,-,0 +Surrender,FTR,8+,8.8,0,925,-,-,0 +Vivid Theory,FTR,8+,8.8,0,885,-,-,0 +Aleph-0,PRS,8+,8.8,0,579,-,-,0 +NULL APOPHENIA,PRS,8+,8.8,0,"1,098",-,-,0 +PRIMITIVE LIGHTS,PRS,8+,8.8,0,"1,073",-,-,0 +Beside You,FTR,8+,8.7,0,703,-,-,0 +Call My Name feat. Yukacco,FTR,8+,8.7,0,921,-,-,0 +cry of viyella,FTR,8+,8.7,0,791,-,-,0 +Grimheart,FTR,8+,8.7,0,959,-,-,0 +IONOSTREAM,FTR,8+,8.7,0,818,-,-,0 +Paper Witch,FTR,8+,8.7,0,793,-,-,0 +Quon (DJ Noriken),FTR,8+,8.7,0,749,-,-,0 +Senkyou,FTR,8+,8.7,0,964,-,-,0 +"Tsuki ni Murakumo, Hana ni Kaze",FTR,8+,8.7,0,740,-,-,0 +WAIT FOR DAWN,FTR,8+,8.7,0,861,-,-,0 +Arcana Eden,PRS,8+,8.7,0,"1,310",-,-,0 +Back to Basics,PRS,8+,8.7,0,"1,115",-,-,0 +Pentiment,PRS,8+,8.7,0,"1,055",-,-,0 +April showers,FTR,8,8.6,0,697,-,-,0 +enchanted love,FTR,8,8.6,0,759,-,-,0 +FREEF4LL,FTR,8,8.6,0,"1,023",-,-,0 +Gekka (Short Version),FTR,8,8.6,0,817,-,-,0 +Silent Rush,FTR,8,8.6,0,941,-,-,0 +BUCHiGiRE Berserker,PRS,8,8.6,0,"1,046",-,-,0 +Ego Eimi,PRS,8,8.6,0,937,-,-,0 +HIVEMIND,PRS,8,8.6,0,988,-,-,0 +PRAGMATISM,PRS,8,8.6,0,855,-,-,0 +Sayonara Hatsukoi,ETR,8,8.5,0,728,-,-,0 +Altair (feat. *spiLa*),FTR,8,8.5,0,830,-,-,0 +Babaroque,FTR,8,8.5,0,808,-,-,0 +Dandelion,FTR,8,8.5,0,921,-,-,0 +DDD,FTR,8,8.5,0,653,-,-,0 +Harutopia ~Utopia of Spring~,FTR,8,8.5,0,"1,061",-,-,0 +Innocence,FTR,8,8.5,0,"1,023",-,-,0 +Reinvent,FTR,8,8.5,0,852,-,-,0 +syūten,FTR,8,8.5,0,592,-,-,0 +Aurgelmir,PRS,8,8.5,0,765,-,-,0 +Axium Crisis,PRS,8,8.5,0,"1,065",-,-,0 +LAMIA,PRS,8,8.5,0,885,-,-,0 +Live Fast Die Young,PRS,8,8.5,0,837,-,-,0 +Misdeed -la bonté de Dieu et l'origine du mal-,PRS,8,8.5,0,"1,070",-,-,0 +Overwhelm,PRS,8,8.5,0,"1,038",-,-,0 +To the Furthest Dream,PRS,8,8.5,0,994,-,-,0 +ultradiaxon-N3,PRS,8,8.5,0,896,-,-,0 +Eccentric Tale,FTR,8,8.4,0,732,-,-,0 +INTERNET OVERDOSE,FTR,8,8.4,0,657,-,-,0 +Lumia,FTR,8,8.4,0,961,-,-,0 +Moonheart,FTR,8,8.4,0,947,-,-,0 +Purgatorium,FTR,8,8.4,0,983,-,-,0 +Rabbit In The Black Room,FTR,8,8.4,0,772,-,-,0 +REconstruction,FTR,8,8.4,0,825,-,-,0 +Snow White,FTR,8,8.4,0,978,-,-,0 +Cyaegha,PRS,8,8.4,0,984,-,-,0 +Ringed Genesis,PRS,8,8.4,0,860,-,-,0 +Ävril -Flicka i krans-,FTR,8,8.3,0,851,-,-,0 +Bookmaker (2D Version),FTR,8,8.3,0,"1,124",-,-,0 +Dot to Dot feat. Shully,FTR,8,8.3,0,739,-,-,0 +Oblivia,FTR,8,8.3,0,956,-,-,0 +san skia,FTR,8,8.3,0,"1,046",-,-,0 +Shades of Light in a Transcendent Realm,FTR,8,8.3,0,"1,067",-,-,0 +Tie me down gently,FTR,8,8.3,0,724,-,-,0 +AttraqtiA,PRS,8,8.3,0,"1,002",-,-,0 +Ether Strike,PRS,8,8.3,0,837,-,-,0 +IZANA,PRS,8,8.3,0,836,-,-,0 +MIRINAE,PRS,8,8.3,0,881,-,-,0 +1F√,FTR,8,8.2,0,758,-,-,0 +blue comet,FTR,8,8.2,0,776,-,-,0 +Genesis (Iris),FTR,8,8.2,0,713,-,-,0 +Hall of Mirrors,FTR,8,8.2,0,898,-,-,0 +Lucifer,FTR,8,8.2,0,861,-,-,0 +One Last Drive,FTR,8,8.2,0,885,-,-,0 +Dantalion,PRS,8,8.2,0,843,-,-,0 +eden,PRS,8,8.2,0,1194,-,-,0 +Halcyon,PRS,8,8.2,0,943,-,-,0 +Qovat,PRS,8,8.2,0,765,-,-,0 +TEmPTaTiON,PRS,8,8.2,0,768,-,-,0 +Bullet Waiting for Me (James Landino remix),FTR,8,8.1,0,701,-,-,0 +Diode,FTR,8,8.1,0,709,-,-,0 +Hikari,FTR,8,8.1,0,684,-,-,0 +I've heard it said,FTR,8,8.1,0,864,-,-,0 +G e n g a o z o,PRS,8,8.1,0,870,-,-,0 +Rise of the World,PRS,8,8.1,0,843,-,-,0 +UNKNOWN LEVELS,PRS,8,8.1,0,771,-,-,0 +Prism,FTR,8,8,0,785,-,-,0 +Relentless,FTR,8,8,0,"1,015",-,-,0 +world.execute(me);,FTR,8,8,0,851,-,-,0 +Crimson Throne,PRS,8,8,0,"1,079",-,-,0 +Désive,PRS,8,8,0,"1,051",-,-,0 +γuarδina,PRS,8,8,0,678,-,-,0 +Brand new world,FTR,7+,7.8,0,787,-,-,0 +Clotho and the stargazer,FTR,7+,7.8,0,"1,021",-,-,0 +Dement ~after legend~,FTR,7+,7.8,0,970,-,-,0 +Dream goes on,FTR,7+,7.8,0,719,-,-,0 +First Snow,FTR,7+,7.8,0,578,-,-,0 +Infinity Heaven,FTR,7+,7.8,0,853,-,-,0 +inkar-usi,FTR,7+,7.8,0,463,-,-,0 +Jingle,FTR,7+,7.8,0,848,-,-,0 +Moonlight of Sand Castle,FTR,7+,7.8,0,645,-,-,0 +Paradise,FTR,7+,7.8,0,729,-,-,0 +Rise,FTR,7+,7.8,0,788,-,-,0 +Romance Wars,FTR,7+,7.8,0,641,-,-,0 +Suomi,FTR,7+,7.8,0,818,-,-,0 +[X],PRS,7+,7.8,0,782,-,-,0 +7thSense,PRS,7+,7.8,0,739,-,-,0 +99 Glooms,PRS,7+,7.8,0,774,-,-,0 +AMAZING MIGHTYYYY!!!!,PRS,7+,7.8,0,776,-,-,0 +Antagonism,PRS,7+,7.8,0,738,-,-,0 +Arcahv,PRS,7+,7.8,0,884,-,-,0 +Astra walkthrough,PRS,7+,7.8,0,886,-,-,0 +Beautiful Dreamer,PRS,7+,7.8,0,773,-,-,0 +BLRINK,PRS,7+,7.8,0,683,-,-,0 +Capella,PRS,7+,7.8,0,904,-,-,0 +carmine:scythe,PRS,7+,7.8,0,710,-,-,0 +Climax,PRS,7+,7.8,0,988,-,-,0 +corps-sans-organes,PRS,7+,7.8,0,615,-,-,0 +cyanine,PRS,7+,7.8,0,947,-,-,0 +Devillic Sphere,PRS,7+,7.8,0,692,-,-,0 +Distorted Fate,PRS,7+,7.8,0,890,-,-,0 +Divine Light of Myriad,PRS,7+,7.8,0,835,-,-,0 +Far Away Light,PRS,7+,7.8,0,769,-,-,0 +Filament,PRS,7+,7.8,0,780,-,-,0 +Hybris (The one who shattered),PRS,7+,7.8,0,973,-,-,0 +IMPACT,PRS,7+,7.8,0,913,-,-,0 +INTERNET YAMERO,PRS,7+,7.8,0,987,-,-,0 +Kissing Lucifer,PRS,7+,7.8,0,770,-,-,0 +KYOREN ROMANCE,PRS,7+,7.8,0,982,-,-,0 +Lethal Voltage,PRS,7+,7.8,0,922,-,-,0 +Lightning Screw,PRS,7+,7.8,0,811,-,-,0 +Logos,PRS,7+,7.8,0,798,-,-,0 +Lost Desire,PRS,7+,7.8,0,871,-,-,0 +Macrocosmic Modulation,PRS,7+,7.8,0,789,-,-,0 +Magnolia,PRS,7+,7.8,0,726,-,-,0 +Malicious Mischance,PRS,7+,7.8,0,706,-,-,0 +Manic Jeer,PRS,7+,7.8,0,875,-,-,0 +Mazy Metroplex,PRS,7+,7.8,0,704,-,-,0 +Meta-Mysteria,PRS,7+,7.8,0,905,-,-,0 +Monochrome Princess,PRS,7+,7.8,0,756,-,-,0 +nέo κósmo,PRS,7+,7.8,0,791,-,-,0 +Old School Salvage,PRS,7+,7.8,0,950,-,-,0 +Party Vinyl,PRS,7+,7.8,0,543,-,-,0 +PUPA,PRS,7+,7.8,0,684,-,-,0 +Red and Blue,PRS,7+,7.8,0,597,-,-,0 +RGB,PRS,7+,7.8,0,803,-,-,0 +Sheriruth (Laur Remix),PRS,7+,7.8,0,795,-,-,0 +Singularity,PRS,7+,7.8,0,678,-,-,0 +Spider's Thread,PRS,7+,7.8,0,764,-,-,0 +Tiferet,PRS,7+,7.8,0,720,-,-,0 +Trap Crow,PRS,7+,7.8,0,898,-,-,0 +Twilight Concerto,PRS,7+,7.8,0,634,-,-,0 +Viyella's Tears,PRS,7+,7.8,0,942,-,-,0 +WAIT FOR DAWN,PRS,7+,7.8,0,636,-,-,0 +World Ender,PRS,7+,7.8,0,850,-,-,0 +XTREME,PRS,7+,7.8,0,831,-,-,0 +ΟΔΥΣΣΕΙΑ,PRS,7+,7.8,0,909,-,-,0 +ω4,PRS,7+,7.8,0,943,-,-,0 +Testify,PST,7+,7.8,0,"1,001",-,-,0 +HELLOHELL,FTR,7,7.5,0,673,-,-,0 +Hidden Rainbows of Epicurus,FTR,7,7.5,0,783,-,-,0 +A Wandering Melody of Love,PRS,7,7.5,0,670,-,-,0 +Alexandrite,PRS,7,7.5,0,699,-,-,0 +Amekagura,PRS,7,7.5,0,851,-,-,0 +amygdata,PRS,7,7.5,0,711,-,-,0 +Awaken In Ruins,PRS,7,7.5,0,570,-,-,0 +Be There,PRS,7,7.5,0,792,-,-,0 +Black Territory,PRS,7,7.5,0,785,-,-,0 +Callima Karma,PRS,7,7.5,0,989,-,-,0 +CHAOS,PRS,7,7.5,0,848,-,-,0 +Chronostasis,PRS,7,7.5,0,812,-,-,0 +conflict,PRS,7,7.5,0,731,-,-,0 +Dynitikós,PRS,7,7.5,0,894,-,-,0 +Fallensquare,PRS,7,7.5,0,486,-,-,0 +Felis,PRS,7,7.5,0,624,-,-,0 +Free Myself,PRS,7,7.5,0,785,-,-,0 +GIMME DA BLOOD,PRS,7,7.5,0,737,-,-,0 +Glow,PRS,7,7.5,0,666,-,-,0 +"Good bye, Merry-Go-Round.",PRS,7,7.5,0,696,-,-,0 +Heavenly caress,PRS,7,7.5,0,909,-,-,0 +Heavensdoor,PRS,7,7.5,0,869,-,-,0 +Ikazuchi,PRS,7,7.5,0,976,-,-,0 +"Infinite Strife,",PRS,7,7.5,0,"1,081",-,-,0 +init(),PRS,7,7.5,0,957,-,-,0 +Kanbu de Tomatte Sugu Tokeru,PRS,7,7.5,0,714,-,-,0 +lastendconductor,PRS,7,7.5,0,901,-,-,0 +LIVHT MY WΔY,PRS,7,7.5,0,738,-,-,0 +Mirzam,PRS,7,7.5,0,885,-,-,0 +Modelista,PRS,7,7.5,0,691,-,-,0 +NEO WINGS,PRS,7,7.5,0,840,-,-,0 +NULCTRL,PRS,7,7.5,0,410,-,-,0 +ouroboros -twin stroke of the end-,PRS,7,7.5,0,832,-,-,0 +Ouvertüre,PRS,7,7.5,0,786,-,-,0 +Primeval Texture,PRS,7,7.5,0,525,-,-,0 +Purple Verse,PRS,7,7.5,0,664,-,-,0 +Redolent Shape,PRS,7,7.5,0,717,-,-,0 +SACRIFICE feat. ayame,PRS,7,7.5,0,779,-,-,0 +Scarlet Cage,PRS,7,7.5,0,711,-,-,0 +Seclusion,PRS,7,7.5,0,762,-,-,0 +Sheriruth,PRS,7,7.5,0,832,-,-,0 +To: Alice Liddell,PRS,7,7.5,0,774,-,-,0 +Trrricksters!!,PRS,7,7.5,0,784,-,-,0 +Valhalla:0,PRS,7,7.5,0,893,-,-,0 +Vicious Heroism,PRS,7,7.5,0,756,-,-,0 +Wish Upon a Snow,PRS,7,7.5,0,1095,-,-,0 +Xanatos,PRS,7,7.5,0,938,-,-,0 +Xeraphinite,PRS,7,7.5,0,643,-,-,0 +αterlβus,PRS,7,7.5,0,668,-,-,0 +µ,PRS,7,7.5,0,986,-,-,0 +Arghena,PST,7,7.5,0,883,-,-,0 +Blossoms,FTR,7,7,0,655,-,-,0 +Fairytale,FTR,7,7,0,782,-,-,0 +Sayonara Hatsukoi,FTR,7,7,0,666,-,-,0 +Vexaria,FTR,7,7,0,734,-,-,0 +Astral tale,PRS,7,7,0,642,-,-,0 +B.B.K.K.B.K.K.,PRS,7,7,0,708,-,-,0 +BADTEK,PRS,7,7,0,595,-,-,0 +Blaster,PRS,7,7,0,635,-,-,0 +Chromafill,PRS,7,7,0,890,-,-,0 +cocoro*cosmetic,PRS,7,7,0,687,-,-,0 +CROSS†SOUL,PRS,7,7,0,823,-,-,0 +Cybernecia Catharsis,PRS,7,7,0,655,-,-,0 +DataErr0r,PRS,7,7,0,785,-,-,0 +Dreadnought,PRS,7,7,0,897,-,-,0 +Dreamin' Attraction!!,PRS,7,7,0,785,-,-,0 +DRG,PRS,7,7,0,624,-,-,0 +Einherjar Joker,PRS,7,7,0,767,-,-,0 +ENERGY SYNERGY MATRIX,PRS,7,7,0,608,-,-,0 +Essence of Twilight,PRS,7,7,0,767,-,-,0 +Evening in Scarlet,PRS,7,7,0,638,-,-,0 +Evoltex (poppi'n mix),PRS,7,7,0,627,-,-,0 +FANTA5Y,PRS,7,7,0,715,-,-,0 +Floating World,PRS,7,7,0,793,-,-,0 +FREEF4LL,PRS,7,7,0,782,-,-,0 +Genesis (Morrigan feat. Lily),PRS,7,7,0,838,-,-,0 +GLORY: ROAD,PRS,7,7,0,999,-,-,0 +Haze of Autumn,PRS,7,7,0,733,-,-,0 +Head BONK ache,PRS,7,7,0,725,-,-,0 +Hotarubi no Yuki,PRS,7,7,0,733,-,-,0 +Iconoclast,PRS,7,7,0,593,-,-,0 +Illegal Paradise,PRS,7,7,0,585,-,-,0 +Last,PRS,7,7,0,781,-,-,0 +Let's Rock (Arcaea mix),PRS,7,7,0,963,-,-,0 +Löschen,PRS,7,7,0,850,-,-,0 +Lost Civilization,PRS,7,7,0,690,-,-,0 +Lost Emotion feat. nomico,PRS,7,7,0,780,-,-,0 +MANTIS (Arcaea Ultra-Bloodrush VIP),PRS,7,7,0,760,-,-,0 +Medusa,PRS,7,7,0,645,-,-,0 +Metallic Punisher,PRS,7,7,0,846,-,-,0 +Nameless Passion,PRS,7,7,0,890,-,-,0 +New York Back Raise,PRS,7,7,0,803,-,-,0 +next to you,PRS,7,7,0,583,-,-,0 +Nirv lucE,PRS,7,7,0,547,-,-,0 +On And On!! feat. Jenga,PRS,7,7,0,616,-,-,0 +Protoflicker,PRS,7,7,0,633,-,-,0 +qualia -ideaesthesia-,PRS,7,7,0,875,-,-,0 +Raven's Pride,PRS,7,7,0,797,-,-,0 +Remind the Souls (Short Version),PRS,7,7,0,756,-,-,0 +ReviXy,PRS,7,7,0,729,-,-,0 +Scarlet Lance,PRS,7,7,0,644,-,-,0 +Solitary Dream,PRS,7,7,0,854,-,-,0 +Stasis,PRS,7,7,0,935,-,-,0 +Technicolour,PRS,7,7,0,812,-,-,0 +THE ULTIMACY,PRS,7,7,0,919,-,-,0 +Transient Space,PRS,7,7,0,607,-,-,0 +Vandalism,PRS,7,7,0,685,-,-,0 +VECTOЯ,PRS,7,7,0,"1,002",-,-,0 +World Fragments III (radio edit),PRS,7,7,0,999,-,-,0 +Yosakura Fubuki,PRS,7,7,0,582,-,-,0 +Pentiment,PST,7,7,0,911,-,-,0 +Tempestissimo,PST,7,7,0,919,-,-,0 +〇、,PRS,6,6.5,0,519,-,-,0 +1F√,PRS,6,6.5,0,539,-,-,0 +Ai Drew,PRS,6,6.5,0,732,-,-,0 +AI[UE]OON,PRS,6,6.5,0,623,-,-,0 +Alice à la mode,PRS,6,6.5,0,618,-,-,0 +Alone & Lorn,PRS,6,6.5,0,742,-,-,0 +Anökumene,PRS,6,6.5,0,587,-,-,0 +Auxesia,PRS,6,6.5,0,648,-,-,0 +Avant Raze,PRS,6,6.5,0,727,-,-,0 +Babaroque,PRS,6,6.5,0,645,-,-,0 +Bamboo,PRS,6,6.5,0,631,-,-,0 +BATTLE NO.1,PRS,6,6.5,0,677,-,-,0 +Black Lotus,PRS,6,6.5,0,687,-,-,0 +Blocked Library,PRS,6,6.5,0,622,-,-,0 +Bookmaker (2D Version),PRS,6,6.5,0,728,-,-,0 +Can I Friend You on Bassbook? Lol,PRS,6,6.5,0,548,-,-,0 +Corruption,PRS,6,6.5,0,847,-,-,0 +CROSS†OVER,PRS,6,6.5,0,810,-,-,0 +Crystal Gravity,PRS,6,6.5,0,653,-,-,0 +Dazzle hop,PRS,6,6.5,0,571,-,-,0 +DDD,PRS,6,6.5,0,484,-,-,0 +Defection,PRS,6,6.5,0,800,-,-,0 +Empire of Winter,PRS,6,6.5,0,662,-,-,0 +Equilibrium,PRS,6,6.5,0,724,-,-,0 +False Embellishment,PRS,6,6.5,0,684,-,-,0 +Feels So Right feat. Renko,PRS,6,6.5,0,636,-,-,0 +felys final remix,PRS,6,6.5,0,771,-,-,0 +Final Step!,PRS,6,6.5,0,684,-,-,0 +FLUFFY FLASH,PRS,6,6.5,0,946,-,-,0 +Galactic Love,PRS,6,6.5,0,694,-,-,0 +Garakuta Doll Play,PRS,6,6.5,0,572,-,-,0 +Gekka (Short Version),PRS,6,6.5,0,628,-,-,0 +GIMMICK,PRS,6,6.5,0,598,-,-,0 +goldenslaughterer,PRS,6,6.5,0,880,-,-,0 +GOODTEK (Arcaea Edit),PRS,6,6.5,0,632,-,-,0 +Got hive of Ra,PRS,6,6.5,0,509,-,-,0 +"Hiiro Gekka, Kyoushou no Zetsu (nayuta 2017 ver.)",PRS,6,6.5,0,709,-,-,0 +Ignotus,PRS,6,6.5,0,809,-,-,0 +Innocence,PRS,6,6.5,0,811,-,-,0 +INTERNET OVERDOSE,PRS,6,6.5,0,578,-,-,0 +Kanjou no Matenrou ~Arr.Demetori,PRS,6,6.5,0,929,-,-,0 +La'qryma of the Wasteland,PRS,6,6.5,0,651,-,-,0 +Last Celebration,PRS,6,6.5,0,994,-,-,0 +Let you DIVE! (nitro rmx),PRS,6,6.5,0,819,-,-,0 +Lethaeus,PRS,6,6.5,0,717,-,-,0 +Lost in the Abyss,PRS,6,6.5,0,908,-,-,0 +Loveless Dress,PRS,6,6.5,0,630,-,-,0 +MAHOROBA,PRS,6,6.5,0,632,-,-,0 +Masquerade Legion,PRS,6,6.5,0,698,-,-,0 +MAXRAGE,PRS,6,6.5,0,760,-,-,0 +MORNINGLOOM,PRS,6,6.5,0,829,-,-,0 +Nhelv,PRS,6,6.5,0,913,-,-,0 +OMAKENO Stroke,PRS,6,6.5,0,616,-,-,0 +Oshama Scramble!,PRS,6,6.5,0,568,-,-,0 +Quon (Feryquitous),PRS,6,6.5,0,718,-,-,0 +Redraw the Colorless World,PRS,6,6.5,0,680,-,-,0 +Reinvent,PRS,6,6.5,0,703,-,-,0 +Relentless,PRS,6,6.5,0,722,-,-,0 +Sakura Fubuki,PRS,6,6.5,0,571,-,-,0 +Small Cloud Sugar Candy,PRS,6,6.5,0,727,-,-,0 +SOUNDWiTCH,PRS,6,6.5,0,488,-,-,0 +Specta,PRS,6,6.5,0,706,-,-,0 +STAGER (ALL STAGE CLEAR),PRS,6,6.5,0,559,-,-,0 +Stratoliner,PRS,6,6.5,0,702,-,-,0 +Summer Fireworks of Love,PRS,6,6.5,0,641,-,-,0 +Surrender,PRS,6,6.5,0,721,-,-,0 +Syro,PRS,6,6.5,0,829,-,-,0 +Teriqma,PRS,6,6.5,0,579,-,-,0 +The Message,PRS,6,6.5,0,630,-,-,0 +The Survivor (Game Edit),PRS,6,6.5,0,750,-,-,0 +Ultimate taste,PRS,6,6.5,0,"1,008",-,-,0 +Used to be,PRS,6,6.5,0,675,-,-,0 +Vindication,PRS,6,6.5,0,899,-,-,0 +with U,PRS,6,6.5,0,649,-,-,0 +Your voice so... feat. Such,PRS,6,6.5,0,677,-,-,0 +Grievous Lady,PST,6,6.5,0,956,-,-,0 +Alice's Suitcase,PRS,6,6,0,660,-,-,0 +Altair (feat. *spiLa*),PRS,6,6,0,828,-,-,0 +AlterAle,PRS,6,6,0,881,-,-,0 +Ascent,PRS,6,6,0,625,-,-,0 +Ävril -Flicka i krans-,PRS,6,6,0,607,-,-,0 +Beside You,PRS,6,6,0,634,-,-,0 +Blue Rose,PRS,6,6,0,669,-,-,0 +Call My Name feat. Yukacco,PRS,6,6,0,653,-,-,0 +Chelsea,PRS,6,6,0,503,-,-,0 +Coastal Highway,PRS,6,6,0,479,-,-,0 +Cosmo Pop Funclub,PRS,6,6,0,591,-,-,0 +cry of viyella,PRS,6,6,0,492,-,-,0 +Dancin' on a Cat's Paw,PRS,6,6,0,593,-,-,0 +Dandelion,PRS,6,6,0,698,-,-,0 +Dement ~after legend~,PRS,6,6,0,756,-,-,0 +Dialnote,PRS,6,6,0,548,-,-,0 +Dot to Dot feat. Shully,PRS,6,6,0,522,-,-,0 +DX Choseinou Full Metal Shojo,PRS,6,6,0,556,-,-,0 +Faint Light (Arcaea Edit),PRS,6,6,0,711,-,-,0 +Flyburg and Endroll,PRS,6,6,0,650,-,-,0 +Galaxy Friends,PRS,6,6,0,572,-,-,0 +Hikari,PRS,6,6,0,450,-,-,0 +I've heard it said,PRS,6,6,0,664,-,-,0 +IONOSTREAM,PRS,6,6,0,890,-,-,0 +Journey,PRS,6,6,0,778,-,-,0 +Jump,PRS,6,6,0,622,-,-,0 +Lapis,PRS,6,6,0,520,-,-,0 +Lawless Point,PRS,6,6,0,630,-,-,0 +Lazy Addiction,PRS,6,6,0,647,-,-,0 +Life is PIANO,PRS,6,6,0,437,-,-,0 +Lights of Muse,PRS,6,6,0,438,-,-,0 +Linear Accelerator,PRS,6,6,0,488,-,-,0 +Luna Rossa,PRS,6,6,0,578,-,-,0 +LunarOrbit -believe in the Espebranch road-,PRS,6,6,0,757,-,-,0 +Memory Forest,PRS,6,6,0,726,-,-,0 +Particle Arts,PRS,6,6,0,637,-,-,0 +PICO-Pico-Translation!,PRS,6,6,0,723,-,-,0 +Purgatorium,PRS,6,6,0,641,-,-,0 +Quon (DJ Noriken),PRS,6,6,0,496,-,-,0 +REconstruction,PRS,6,6,0,469,-,-,0 +Rugie,PRS,6,6,0,754,-,-,0 +san skia,PRS,6,6,0,783,-,-,0 +Shades of Light in a Transcendent Realm,PRS,6,6,0,789,-,-,0 +Sulfur,PRS,6,6,0,608,-,-,0 +SUPERNOVA,PRS,6,6,0,564,-,-,0 +The Formula,PRS,6,6,0,587,-,-,0 +trappola bewitching,PRS,6,6,0,541,-,-,0 +"Tsuki ni Murakumo, Hana ni Kaze",PRS,6,6,0,610,-,-,0 +Turbocharger,PRS,6,6,0,659,-,-,0 +Fracture Ray,PST,6,6,0,983,-,-,0 +Altale,PRS,5,5.5,0,422,-,-,0 +April showers,PRS,5,5.5,0,544,-,-,0 +Diode,PRS,5,5.5,0,472,-,-,0 +Eccentric Tale,PRS,5,5.5,0,483,-,-,0 +Genesis (Iris),PRS,5,5.5,0,399,-,-,0 +Give Me a Nightmare,PRS,5,5.5,0,817,-,-,0 +Hall of Mirrors,PRS,5,5.5,0,553,-,-,0 +Heart Jackin',PRS,5,5.5,0,543,-,-,0 +Hidden Rainbows of Epicurus,PRS,5,5.5,0,644,-,-,0 +Impure Bird,PRS,5,5.5,0,518,-,-,0 +Infinity Heaven,PRS,5,5.5,0,545,-,-,0 +Jingle,PRS,5,5.5,0,741,-,-,0 +Kanagawa Cyber Culvert,PRS,5,5.5,0,707,-,-,0 +Leave All Behind,PRS,5,5.5,0,488,-,-,0 +Libertas,PRS,5,5.5,0,514,-,-,0 +Lucifer,PRS,5,5.5,0,506,-,-,0 +Lumia,PRS,5,5.5,0,438,-,-,0 +memoryfactory.lzh,PRS,5,5.5,0,448,-,-,0 +MERLIN,PRS,5,5.5,0,428,-,-,0 +Moonheart,PRS,5,5.5,0,566,-,-,0 +One Last Drive,PRS,5,5.5,0,564,-,-,0 +Oracle,PRS,5,5.5,0,508,-,-,0 +Phantasia,PRS,5,5.5,0,579,-,-,0 +Senkyou,PRS,5,5.5,0,722,-,-,0 +World Vanquisher,PRS,5,5.5,0,759,-,-,0 +#1f1e33,PST,5,5.5,0,765,-,-,0 +Abstruse Dilemma,PST,5,5.5,0,983,-,-,0 +Aegleseeker,PST,5,5.5,0,"1,235",-,-,0 +Aleph-0,PST,5,5.5,0,388,-,-,0 +Arcana Eden,PST,5,5.5,0,"1,097",-,-,0 +Axium Crisis,PST,5,5.5,0,685,-,-,0 +CHAOS,PST,5,5.5,0,646,-,-,0 +Cyaegha,PST,5,5.5,0,760,-,-,0 +Ego Eimi,PST,5,5.5,0,801,-,-,0 +Ether Strike,PST,5,5.5,0,659,-,-,0 +Halcyon,PST,5,5.5,0,662,-,-,0 +Misdeed -la bonté de Dieu et l'origine du mal-,PST,5,5.5,0,814,-,-,0 +PRIMITIVE LIGHTS,PST,5,5.5,0,785,-,-,0 +Ringed Genesis,PST,5,5.5,0,672,-,-,0 +SAIKYO STRONGER,PST,5,5.5,0,654,-,-,0 +Sheriruth,PST,5,5.5,0,611,-,-,0 +Antithese,PRS,5,5,0,826,-,-,0 +blue comet,PRS,5,5,0,645,-,-,0 +Clotho and the stargazer,PRS,5,5,0,745,-,-,0 +Cosmica,PRS,5,5,0,556,-,-,0 +CYCLES,PRS,5,5,0,430,-,-,0 +Dream goes on,PRS,5,5,0,751,-,-,0 +Flashback,PRS,5,5,0,488,-,-,0 +Grimheart,PRS,5,5,0,699,-,-,0 +Heart,PRS,5,5,0,575,-,-,0 +HELLOHELL,PRS,5,5,0,445,-,-,0 +Moonlight of Sand Castle,PRS,5,5,0,394,-,-,0 +Oblivia,PRS,5,5,0,517,-,-,0 +Paper Witch,PRS,5,5,0,487,-,-,0 +Prism,PRS,5,5,0,544,-,-,0 +Rabbit In The Black Room,PRS,5,5,0,467,-,-,0 +Silent Rush,PRS,5,5,0,674,-,-,0 +Snow White,PRS,5,5,0,672,-,-,0 +Strongholds,PRS,5,5,0,778,-,-,0 +Suomi,PRS,5,5,0,550,-,-,0 +syūten,PRS,5,5,0,446,-,-,0 +Tie me down gently,PRS,5,5,0,535,-,-,0 +Vexaria,PRS,5,5,0,534,-,-,0 +Vivid Theory,PRS,5,5,0,658,-,-,0 +world.execute(me);,PRS,5,5,0,582,-,-,0 +Back to Basics,PST,5,5,0,717,-,-,0 +BUCHiGiRE Berserker,PST,5,5,0,850,-,-,0 +Chronicle,PST,5,5,0,928,-,-,0 +Dantalion,PST,5,5,0,730,-,-,0 +Désive,PST,5,5,0,"1,007",-,-,0 +͟͝͞Ⅱ́̕,PST,5,5,0,690,-,-,0 +IZANA,PST,5,5,0,609,-,-,0 +LAMIA,PST,5,5,0,826,-,-,0 +Meta-Mysteria,PST,5,5,0,844,-,-,0 +Overwhelm,PST,5,5,0,854,-,-,0 +REKKA RESONANCE,PST,5,5,0,860,-,-,0 +TEmPTaTiON,PST,5,5,0,627,-,-,0 +TeraVolt,PST,5,5,0,675,-,-,0 +To the Milky Way,PST,5,5,0,928,-,-,0 +Bullet Waiting for Me (James Landino remix),PRS,4,4.5,0,390,-,-,0 +enchanted love,PRS,4,4.5,0,617,-,-,0 +First Snow,PRS,4,4.5,0,442,-,-,0 +Harutopia ~Utopia of Spring~,PRS,4,4.5,0,706,-,-,0 +Sayonara Hatsukoi,PRS,4,4.5,0,305,-,-,0 +[X],PST,4,4.5,0,594,-,-,0 +Alexandrite,PST,4,4.5,0,507,-,-,0 +AMAZING MIGHTYYYY!!!!,PST,4,4.5,0,624,-,-,0 +Antagonism,PST,4,4.5,0,431,-,-,0 +Arcahv,PST,4,4.5,0,818,-,-,0 +Astra walkthrough,PST,4,4.5,0,769,-,-,0 +Astral tale,PST,4,4.5,0,445,-,-,0 +Aurgelmir,PST,4,4.5,0,540,-,-,0 +B.B.K.K.B.K.K.,PST,4,4.5,0,669,-,-,0 +Bookmaker (2D Version),PST,4,4.5,0,538,-,-,0 +Capella,PST,4,4.5,0,884,-,-,0 +Climax,PST,4,4.5,0,773,-,-,0 +conflict,PST,4,4.5,0,520,-,-,0 +corps-sans-organes,PST,4,4.5,0,438,-,-,0 +Distorted Fate,PST,4,4.5,0,756,-,-,0 +Divine Light of Myriad,PST,4,4.5,0,798,-,-,0 +Dreamin' Attraction!!,PST,4,4.5,0,592,-,-,0 +eden,PST,4,4.5,0,826,-,-,0 +Essence of Twilight,PST,4,4.5,0,556,-,-,0 +Far Away Light,PST,4,4.5,0,656,-,-,0 +Filament,PST,4,4.5,0,582,-,-,0 +Free Myself,PST,4,4.5,0,662,-,-,0 +Garakuta Doll Play,PST,4,4.5,0,444,-,-,0 +GENOCIDER,PST,4,4.5,0,832,-,-,0 +GLORY: ROAD,PST,4,4.5,0,868,-,-,0 +"Good bye, Merry-Go-Round.",PST,4,4.5,0,679,-,-,0 +Heavensdoor,PST,4,4.5,0,766,-,-,0 +Hybris (The one who shattered),PST,4,4.5,0,757,-,-,0 +INTERNET YAMERO,PST,4,4.5,0,677,-,-,0 +Kissing Lucifer,PST,4,4.5,0,639,-,-,0 +Lethal Voltage,PST,4,4.5,0,724,-,-,0 +Lightning Screw,PST,4,4.5,0,611,-,-,0 +Live Fast Die Young,PST,4,4.5,0,565,-,-,0 +LIVHT MY WΔY,PST,4,4.5,0,673,-,-,0 +Lucid Traveler,PST,4,4.5,0,634,-,-,0 +Magnolia,PST,4,4.5,0,625,-,-,0 +Medusa,PST,4,4.5,0,550,-,-,0 +MIRINAE,PST,4,4.5,0,646,-,-,0 +Monochrome Princess,PST,4,4.5,0,621,-,-,0 +New York Back Raise,PST,4,4.5,0,762,-,-,0 +next to you,PST,4,4.5,0,454,-,-,0 +NULL APOPHENIA,PST,4,4.5,0,990,-,-,0 +ouroboros -twin stroke of the end-,PST,4,4.5,0,575,-,-,0 +PRAGMATISM,PST,4,4.5,0,476,-,-,0 +Qovat,PST,4,4.5,0,519,-,-,0 +qualia -ideaesthesia-,PST,4,4.5,0,550,-,-,0 +Redolent Shape,PST,4,4.5,0,570,-,-,0 +Relentless,PST,4,4.5,0,607,-,-,0 +Rise of the World,PST,4,4.5,0,722,-,-,0 +Singularity,PST,4,4.5,0,534,-,-,0 +Spider's Thread,PST,4,4.5,0,794,-,-,0 +Stasis,PST,4,4.5,0,848,-,-,0 +Tiferet,PST,4,4.5,0,450,-,-,0 +To the Furthest Dream,PST,4,4.5,0,836,-,-,0 +Trap Crow,PST,4,4.5,0,876,-,-,0 +Trrricksters!!,PST,4,4.5,0,561,-,-,0 +Twilight Concerto,PST,4,4.5,0,584,-,-,0 +ultradiaxon-N3,PST,4,4.5,0,605,-,-,0 +Valhalla:0,PST,4,4.5,0,624,-,-,0 +Wish Upon a Snow,PST,4,4.5,0,1031,-,-,0 +XTREME,PST,4,4.5,0,752,-,-,0 +Yosakura Fubuki,PST,4,4.5,0,416,-,-,0 +Blossoms,PRS,4,4,0,383,-,-,0 +Brand new world,PRS,4,4,0,432,-,-,0 +inkar-usi,PRS,4,4,0,326,-,-,0 +Paradise,PRS,4,4,0,349,-,-,0 +Rise,PRS,4,4,0,599,-,-,0 +Romance Wars,PRS,4,4,0,378,-,-,0 +99 Glooms,PST,4,4,0,637,-,-,0 +Amekagura,PST,4,4,0,741,-,-,0 +amygdata,PST,4,4,0,504,-,-,0 +AttraqtiA,PST,4,4,0,732,-,-,0 +BADTEK,PST,4,4,0,532,-,-,0 +Be There,PST,4,4,0,592,-,-,0 +Beautiful Dreamer,PST,4,4,0,611,-,-,0 +Blaster,PST,4,4,0,571,-,-,0 +carmine:scythe,PST,4,4,0,493,-,-,0 +Chromafill,PST,4,4,0,757,-,-,0 +Crimson Throne,PST,4,4,0,727,-,-,0 +CROSS†OVER,PST,4,4,0,653,-,-,0 +CROSS†SOUL,PST,4,4,0,606,-,-,0 +cyanine,PST,4,4,0,661,-,-,0 +Cybernecia Catharsis,PST,4,4,0,377,-,-,0 +Devillic Sphere,PST,4,4,0,490,-,-,0 +Dreadnought,PST,4,4,0,715,-,-,0 +Dynitikós,PST,4,4,0,683,-,-,0 +Einherjar Joker,PST,4,4,0,582,-,-,0 +Evening in Scarlet,PST,4,4,0,530,-,-,0 +Felis,PST,4,4,0,463,-,-,0 +FREEF4LL,PST,4,4,0,589,-,-,0 +G e n g a o z o,PST,4,4,0,580,-,-,0 +Gekka (Short Version),PST,4,4,0,559,-,-,0 +Genesis (Morrigan feat. Lily),PST,4,4,0,587,-,-,0 +Glow,PST,4,4,0,504,-,-,0 +goldenslaughterer,PST,4,4,0,486,-,-,0 +GOODTEK (Arcaea Edit),PST,4,4,0,449,-,-,0 +Head BONK ache,PST,4,4,0,570,-,-,0 +HIVEMIND,PST,4,4,0,995,-,-,0 +Iconoclast,PST,4,4,0,443,-,-,0 +"Infinite Strife,",PST,4,4,0,888,-,-,0 +init(),PST,4,4,0,713,-,-,0 +Jump,PST,4,4,0,531,-,-,0 +Kanbu de Tomatte Sugu Tokeru,PST,4,4,0,584,-,-,0 +KYOREN ROMANCE,PST,4,4,0,821,-,-,0 +Last,PST,4,4,0,680,-,-,0 +Logos,PST,4,4,0,697,-,-,0 +Lost Civilization,PST,4,4,0,462,-,-,0 +Lost Desire,PST,4,4,0,684,-,-,0 +Macrocosmic Modulation,PST,4,4,0,608,-,-,0 +Malicious Mischance,PST,4,4,0,510,-,-,0 +Manic Jeer,PST,4,4,0,698,-,-,0 +Mirzam,PST,4,4,0,662,-,-,0 +NEO WINGS,PST,4,4,0,591,-,-,0 +nέo κósmo,PST,4,4,0,603,-,-,0 +Old School Salvage,PST,4,4,0,705,-,-,0 +Party Vinyl,PST,4,4,0,337,-,-,0 +Phantasia,PST,4,4,0,544,-,-,0 +PUPA,PST,4,4,0,374,-,-,0 +Purple Verse,PST,4,4,0,558,-,-,0 +Quon (Feryquitous),PST,4,4,0,547,-,-,0 +Red and Blue,PST,4,4,0,464,-,-,0 +Redraw the Colorless World,PST,4,4,0,568,-,-,0 +RGB,PST,4,4,0,652,-,-,0 +Scarlet Cage,PST,4,4,0,570,-,-,0 +Scarlet Lance,PST,4,4,0,517,-,-,0 +Seclusion,PST,4,4,0,544,-,-,0 +Sheriruth (Laur Remix),PST,4,4,0,671,-,-,0 +Solitary Dream,PST,4,4,0,712,-,-,0 +Sulfur,PST,4,4,0,458,-,-,0 +To: Alice Liddell,PST,4,4,0,535,-,-,0 +UNKNOWN LEVELS,PST,4,4,0,591,-,-,0 +Used to be,PST,4,4,0,537,-,-,0 +Vicious Heroism,PST,4,4,0,430,-,-,0 +Vindication,PST,4,4,0,721,-,-,0 +Viyella's Tears,PST,4,4,0,716,-,-,0 +World Ender,PST,4,4,0,616,-,-,0 +Xanatos,PST,4,4,0,735,-,-,0 +αterlβus,PST,4,4,0,505,-,-,0 +γuarδina,PST,4,4,0,507,-,-,0 +ΟΔΥΣΣΕΙΑ,PST,4,4,0,717,-,-,0 +Fairytale,PRS,3,3.5,0,511,-,-,0 +Maze No.9,PRS,3,3.5,0,445,-,-,0 +A Wandering Melody of Love,PST,3,3.5,0,422,-,-,0 +Ai Drew,PST,3,3.5,0,694,-,-,0 +AI[UE]OON,PST,3,3.5,0,436,-,-,0 +Alice's Suitcase,PST,3,3.5,0,644,-,-,0 +Auxesia,PST,3,3.5,0,385,-,-,0 +Avant Raze,PST,3,3.5,0,580,-,-,0 +Awaken In Ruins,PST,3,3.5,0,412,-,-,0 +BATTLE NO.1,PST,3,3.5,0,476,-,-,0 +Blocked Library,PST,3,3.5,0,449,-,-,0 +BLRINK,PST,3,3.5,0,400,-,-,0 +blue comet,PST,3,3.5,0,494,-,-,0 +Call My Name feat. Yukacco,PST,3,3.5,0,591,-,-,0 +Callima Karma,PST,3,3.5,0,"1,024",-,-,0 +Chronostasis,PST,3,3.5,0,619,-,-,0 +cry of viyella,PST,3,3.5,0,414,-,-,0 +Crystal Gravity,PST,3,3.5,0,571,-,-,0 +Dazzle hop,PST,3,3.5,0,561,-,-,0 +Defection,PST,3,3.5,0,588,-,-,0 +Dement ~after legend~,PST,3,3.5,0,548,-,-,0 +DRG,PST,3,3.5,0,380,-,-,0 +Empire of Winter,PST,3,3.5,0,484,-,-,0 +ENERGY SYNERGY MATRIX,PST,3,3.5,0,471,-,-,0 +Equilibrium,PST,3,3.5,0,587,-,-,0 +False Embellishment,PST,3,3.5,0,576,-,-,0 +Feels So Right feat. Renko,PST,3,3.5,0,594,-,-,0 +Final Step!,PST,3,3.5,0,625,-,-,0 +FLUFFY FLASH,PST,3,3.5,0,787,-,-,0 +Galaxy Friends,PST,3,3.5,0,474,-,-,0 +GIMME DA BLOOD,PST,3,3.5,0,582,-,-,0 +Give Me a Nightmare,PST,3,3.5,0,676,-,-,0 +Haze of Autumn,PST,3,3.5,0,533,-,-,0 +Heavenly caress,PST,3,3.5,0,855,-,-,0 +"Hiiro Gekka, Kyoushou no Zetsu (nayuta 2017 ver.)",PST,3,3.5,0,719,-,-,0 +Hotarubi no Yuki,PST,3,3.5,0,589,-,-,0 +I've heard it said,PST,3,3.5,0,510,-,-,0 +Ignotus,PST,3,3.5,0,579,-,-,0 +Ikazuchi,PST,3,3.5,0,656,-,-,0 +IMPACT,PST,3,3.5,0,723,-,-,0 +IONOSTREAM,PST,3,3.5,0,845,-,-,0 +Kanjou no Matenrou ~Arr.Demetori,PST,3,3.5,0,736,-,-,0 +La'qryma of the Wasteland,PST,3,3.5,0,447,-,-,0 +Last Celebration,PST,3,3.5,0,969,-,-,0 +lastendconductor,PST,3,3.5,0,726,-,-,0 +Lethaeus,PST,3,3.5,0,480,-,-,0 +Libertas,PST,3,3.5,0,476,-,-,0 +Life is PIANO,PST,3,3.5,0,398,-,-,0 +Löschen,PST,3,3.5,0,642,-,-,0 +Lost Emotion feat. nomico,PST,3,3.5,0,602,-,-,0 +Lost in the Abyss,PST,3,3.5,0,907,-,-,0 +Loveless Dress,PST,3,3.5,0,537,-,-,0 +Lucifer,PST,3,3.5,0,424,-,-,0 +LunarOrbit -believe in the Espebranch road-,PST,3,3.5,0,624,-,-,0 +Masquerade Legion,PST,3,3.5,0,510,-,-,0 +MAXRAGE,PST,3,3.5,0,696,-,-,0 +Memory Forest,PST,3,3.5,0,639,-,-,0 +Modelista,PST,3,3.5,0,418,-,-,0 +Oblivia,PST,3,3.5,0,574,-,-,0 +Oshama Scramble!,PST,3,3.5,0,508,-,-,0 +Particle Arts,PST,3,3.5,0,529,-,-,0 +Remind the Souls (Short Version),PST,3,3.5,0,561,-,-,0 +SACRIFICE feat. ayame,PST,3,3.5,0,548,-,-,0 +Sakura Fubuki,PST,3,3.5,0,526,-,-,0 +san skia,PST,3,3.5,0,670,-,-,0 +SOUNDWiTCH,PST,3,3.5,0,315,-,-,0 +Specta,PST,3,3.5,0,390,-,-,0 +Stratoliner,PST,3,3.5,0,535,-,-,0 +Syro,PST,3,3.5,0,535,-,-,0 +Technicolour,PST,3,3.5,0,664,-,-,0 +The Survivor (Game Edit),PST,3,3.5,0,817,-,-,0 +Ultimate taste,PST,3,3.5,0,828,-,-,0 +Vandalism,PST,3,3.5,0,668,-,-,0 +World Fragments III (radio edit),PST,3,3.5,0,777,-,-,0 +world.execute(me);,PST,3,3.5,0,452,-,-,0 +Your voice so... feat. Such,PST,3,3.5,0,469,-,-,0 +µ,PST,3,3.5,0,825,-,-,0 +ω4,PST,3,3.5,0,816,-,-,0 +7thSense,PST,3,3,0,360,-,-,0 +Alone & Lorn,PST,3,3,0,643,-,-,0 +AlterAle,PST,3,3,0,728,-,-,0 +Ascent,PST,3,3,0,455,-,-,0 +Ävril -Flicka i krans-,PST,3,3,0,459,-,-,0 +Babaroque,PST,3,3,0,402,-,-,0 +Bamboo,PST,3,3,0,499,-,-,0 +Black Lotus,PST,3,3,0,653,-,-,0 +Black Territory,PST,3,3,0,627,-,-,0 +Can I Friend You on Bassbook? Lol,PST,3,3,0,415,-,-,0 +Chelsea,PST,3,3,0,388,-,-,0 +Coastal Highway,PST,3,3,0,355,-,-,0 +cocoro*cosmetic,PST,3,3,0,525,-,-,0 +Corruption,PST,3,3,0,664,-,-,0 +Dancin' on a Cat's Paw,PST,3,3,0,417,-,-,0 +DataErr0r,PST,3,3,0,502,-,-,0 +Dialnote,PST,3,3,0,393,-,-,0 +Dot to Dot feat. Shully,PST,3,3,0,453,-,-,0 +DX Choseinou Full Metal Shojo,PST,3,3,0,327,-,-,0 +Faint Light (Arcaea Edit),PST,3,3,0,587,-,-,0 +Fallensquare,PST,3,3,0,316,-,-,0 +FANTA5Y,PST,3,3,0,536,-,-,0 +felys final remix,PST,3,3,0,626,-,-,0 +Floating World,PST,3,3,0,753,-,-,0 +Flyburg and Endroll,PST,3,3,0,413,-,-,0 +Galactic Love,PST,3,3,0,513,-,-,0 +Got hive of Ra,PST,3,3,0,322,-,-,0 +Hall of Mirrors,PST,3,3,0,535,-,-,0 +Heart Jackin',PST,3,3,0,506,-,-,0 +Innocence,PST,3,3,0,734,-,-,0 +INTERNET OVERDOSE,PST,3,3,0,430,-,-,0 +Journey,PST,3,3,0,551,-,-,0 +Lawless Point,PST,3,3,0,476,-,-,0 +Let you DIVE! (nitro rmx),PST,3,3,0,608,-,-,0 +Let's Rock (Arcaea mix),PST,3,3,0,541,-,-,0 +Luna Rossa,PST,3,3,0,443,-,-,0 +MAHOROBA,PST,3,3,0,422,-,-,0 +MANTIS (Arcaea Ultra-Bloodrush VIP),PST,3,3,0,575,-,-,0 +Maze No.9,PST,3,3,0,494,-,-,0 +Mazy Metroplex,PST,3,3,0,453,-,-,0 +MERLIN,PST,3,3,0,315,-,-,0 +Metallic Punisher,PST,3,3,0,500,-,-,0 +MORNINGLOOM,PST,3,3,0,710,-,-,0 +Nameless Passion,PST,3,3,0,681,-,-,0 +Nhelv,PST,3,3,0,647,-,-,0 +NULCTRL,PST,3,3,0,407,-,-,0 +OMAKENO Stroke,PST,3,3,0,502,-,-,0 +On And On!! feat. Jenga,PST,3,3,0,370,-,-,0 +Oracle,PST,3,3,0,425,-,-,0 +Primeval Texture,PST,3,3,0,346,-,-,0 +Protoflicker,PST,3,3,0,510,-,-,0 +Raven's Pride,PST,3,3,0,697,-,-,0 +ReviXy,PST,3,3,0,538,-,-,0 +Rugie,PST,3,3,0,566,-,-,0 +Senkyou,PST,3,3,0,627,-,-,0 +Shades of Light in a Transcendent Realm,PST,3,3,0,517,-,-,0 +Small Cloud Sugar Candy,PST,3,3,0,548,-,-,0 +STAGER (ALL STAGE CLEAR),PST,3,3,0,453,-,-,0 +Summer Fireworks of Love,PST,3,3,0,478,-,-,0 +SUPERNOVA,PST,3,3,0,664,-,-,0 +Surrender,PST,3,3,0,550,-,-,0 +Teriqma,PST,3,3,0,345,-,-,0 +The Formula,PST,3,3,0,427,-,-,0 +The Message,PST,3,3,0,536,-,-,0 +THE ULTIMACY,PST,3,3,0,749,-,-,0 +Tie me down gently,PST,3,3,0,581,-,-,0 +trappola bewitching,PST,3,3,0,415,-,-,0 +"Tsuki ni Murakumo, Hana ni Kaze",PST,3,3,0,468,-,-,0 +VECTOЯ,PST,3,3,0,675,-,-,0 +Xeraphinite,PST,3,3,0,383,-,-,0 +〇、,PST,2,2.5,0,368,-,-,0 +1F√,PST,2,2.5,0,411,-,-,0 +Alice à la mode,PST,2,2.5,0,436,-,-,0 +Altair (feat. *spiLa*),PST,2,2.5,0,670,-,-,0 +Altale,PST,2,2.5,0,375,-,-,0 +Anökumene,PST,2,2.5,0,412,-,-,0 +Beside You,PST,2,2.5,0,526,-,-,0 +Bullet Waiting for Me (James Landino remix),PST,2,2.5,0,325,-,-,0 +Cosmica,PST,2,2.5,0,428,-,-,0 +Cosmo Pop Funclub,PST,2,2.5,0,352,-,-,0 +Dandelion,PST,2,2.5,0,391,-,-,0 +DDD,PST,2,2.5,0,363,-,-,0 +Diode,PST,2,2.5,0,452,-,-,0 +First Snow,PST,2,2.5,0,366,-,-,0 +GIMMICK,PST,2,2.5,0,421,-,-,0 +Grimheart,PST,2,2.5,0,728,-,-,0 +HELLOHELL,PST,2,2.5,0,466,-,-,0 +Hidden Rainbows of Epicurus,PST,2,2.5,0,595,-,-,0 +Hikari,PST,2,2.5,0,237,-,-,0 +Jingle,PST,2,2.5,0,530,-,-,0 +Lazy Addiction,PST,2,2.5,0,462,-,-,0 +Lights of Muse,PST,2,2.5,0,333,-,-,0 +Lumia,PST,2,2.5,0,469,-,-,0 +memoryfactory.lzh,PST,2,2.5,0,308,-,-,0 +Moonheart,PST,2,2.5,0,449,-,-,0 +Nirv lucE,PST,2,2.5,0,297,-,-,0 +One Last Drive,PST,2,2.5,0,604,-,-,0 +Ouvertüre,PST,2,2.5,0,583,-,-,0 +Purgatorium,PST,2,2.5,0,609,-,-,0 +Quon (DJ Noriken),PST,2,2.5,0,425,-,-,0 +REconstruction,PST,2,2.5,0,347,-,-,0 +Reinvent,PST,2,2.5,0,570,-,-,0 +Rise,PST,2,2.5,0,322,-,-,0 +Silent Rush,PST,2,2.5,0,416,-,-,0 +Snow White,PST,2,2.5,0,486,-,-,0 +Strongholds,PST,2,2.5,0,570,-,-,0 +Transient Space,PST,2,2.5,0,419,-,-,0 +Turbocharger,PST,2,2.5,0,419,-,-,0 +Vexaria,PST,2,2.5,0,369,-,-,0 +WAIT FOR DAWN,PST,2,2.5,0,596,-,-,0 +with U,PST,2,2.5,0,448,-,-,0 +World Vanquisher,PST,2,2.5,0,529,-,-,0 +Antithese,PST,2,2,0,845,-,-,0 +April showers,PST,2,2,0,363,-,-,0 +Blue Rose,PST,2,2,0,595,-,-,0 +Brand new world,PST,2,2,0,322,-,-,0 +Clotho and the stargazer,PST,2,2,0,519,-,-,0 +CYCLES,PST,2,2,0,389,-,-,0 +Eccentric Tale,PST,2,2,0,313,-,-,0 +enchanted love,PST,2,2,0,436,-,-,0 +Evoltex (poppi'n mix),PST,2,2,0,297,-,-,0 +Flashback,PST,2,2,0,356,-,-,0 +Genesis (Iris),PST,2,2,0,275,-,-,0 +Illegal Paradise,PST,2,2,0,535,-,-,0 +Impure Bird,PST,2,2,0,350,-,-,0 +inkar-usi,PST,2,2,0,276,-,-,0 +Lapis,PST,2,2,0,391,-,-,0 +Leave All Behind,PST,2,2,0,380,-,-,0 +Linear Accelerator,PST,2,2,0,438,-,-,0 +Paper Witch,PST,2,2,0,348,-,-,0 +PICO-Pico-Translation!,PST,2,2,0,543,-,-,0 +Prism,PST,2,2,0,476,-,-,0 +Rabbit In The Black Room,PST,2,2,0,373,-,-,0 +Suomi,PST,2,2,0,368,-,-,0 +syūten,PST,2,2,0,395,-,-,0 +Vivid Theory,PST,2,2,0,447,-,-,0 +Dream goes on,PST,1,1.5,0,532,-,-,0 +dropdead,PST,1,1.5,0,44,-,-,0 +Infinity Heaven,PST,1,1.5,0,336,-,-,0 +Moonlight of Sand Castle,PST,1,1.5,0,418,-,-,0 +Sayonara Hatsukoi,PST,1,1.5,0,205,-,-,0 +Blossoms,PST,1,1,0,275,-,-,0 +Fairytale,PST,1,1,0,336,-,-,0 +Harutopia ~Utopia of Spring~,PST,1,1,0,444,-,-,0 +Heart,PST,1,1,0,428,-,-,0 +Kanagawa Cyber Culvert,PST,1,1,0,375,-,-,0 +Paradise,PST,1,1,0,253,-,-,0 +Romance Wars,PST,1,1,0,423,-,-,0 + diff --git a/data/jackets.csv b/data/jackets.csv new file mode 100644 index 0000000..8b3f413 --- /dev/null +++ b/data/jackets.csv @@ -0,0 +1,4 @@ +filename,song_id +grievous-lady,7 +einherjar-joker,14 +einherjar-joker-byd,14 diff --git a/flake.nix b/flake.nix index 757ce7d..e7858e5 100644 --- a/flake.nix +++ b/flake.nix @@ -27,6 +27,7 @@ ]) rust-analyzer-nightly ruff + imagemagick clang llvmPackages.clang diff --git a/schema.sql b/schema.sql index 350633f..0bf02ea 100644 --- a/schema.sql +++ b/schema.sql @@ -1,13 +1,13 @@ # {{{ users create table IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, + id INTEGER NOT NULL PRIMARY KEY, discord_id TEXT UNIQUE NOT NULL, nickname TEXT UNIQUE ); # }}} # {{{ songs CREATE TABLE IF NOT EXISTS songs ( - id INTEGER PRIMARY KEY, + id INTEGER NOT NULL PRIMARY KEY, title TEXT NOT NULL, ocr_alias TEXT, artist TEXT, @@ -17,7 +17,7 @@ CREATE TABLE IF NOT EXISTS songs ( # }}} # {{{ charts CREATE TABLE IF NOT EXISTS charts ( - id INTEGER PRIMARY KEY, + id INTEGER NOT NULL PRIMARY KEY, song_id INTEGER NOT NULL, difficulty TEXT NOT NULL CHECK (difficulty IN ('PST','PRS','FTR','ETR','BYD')), @@ -32,7 +32,7 @@ CREATE TABLE IF NOT EXISTS charts ( # }}} # {{{ plays CREATE TABLE IF NOT EXISTS plays ( - id INTEGER PRIMARY KEY, + id INTEGER NOT NULL PRIMARY KEY, chart_id INTEGER NOT NULL, user_id INTEGER NOT NULL, discord_attachment_id TEXT, diff --git a/src/chart.rs b/src/chart.rs index 412dddf..a6ead39 100644 --- a/src/chart.rs +++ b/src/chart.rs @@ -1,7 +1,8 @@ -use sqlx::prelude::FromRow; +use sqlx::{prelude::FromRow, SqlitePool}; -use crate::context::{Error, UserContext}; +use crate::context::Error; +// {{{ Difficuly #[derive(Debug, Clone, Copy, sqlx::Type)] pub enum Difficulty { PST, @@ -12,12 +13,32 @@ pub enum Difficulty { } impl Difficulty { + pub const DIFFICULTIES: [Difficulty; 5] = + [Self::PST, Self::PRS, Self::FTR, Self::ETR, Self::BYD]; + + pub const DIFFICULTY_STRINGS: [&'static str; 5] = ["PST", "PRS", "FTR", "ETR", "BYD"]; + #[inline] pub fn to_index(self) -> usize { self as usize } } +impl TryFrom for Difficulty { + type Error = String; + + fn try_from(value: String) -> Result { + for (i, s) in Self::DIFFICULTY_STRINGS.iter().enumerate() { + if value == **s { + return Ok(Self::DIFFICULTIES[i]); + } + } + + Err(format!("Cannot convert {} to difficulty", value)) + } +} +// }}} +// {{{ Song #[derive(Debug, Clone, FromRow)] pub struct Song { pub id: u32, @@ -25,29 +46,39 @@ pub struct Song { pub ocr_alias: Option, pub artist: Option, } - -#[derive(Debug, Clone, Copy, FromRow)] +// }}} +// {{{ Chart +#[derive(Debug, Clone, FromRow)] pub struct Chart { pub id: u32, pub song_id: u32, pub difficulty: Difficulty, - pub level: u32, + pub level: String, // TODO: this could become an enum pub note_count: u32, pub chart_constant: u32, } - +// }}} +// {{{ Cache #[derive(Debug, Clone)] pub struct CachedSong { - song: Song, + pub song: Song, charts: [Option; 5], } impl CachedSong { + #[inline] pub fn new(song: Song, charts: [Option; 5]) -> Self { Self { song, charts } } + + #[inline] + pub fn lookup(&self, difficulty: Difficulty) -> Option<&Chart> { + self.charts + .get(difficulty.to_index()) + .and_then(|c| c.as_ref()) + } } #[derive(Debug, Clone, Default)] @@ -56,28 +87,48 @@ pub struct SongCache { } impl SongCache { - pub async fn new(ctx: &UserContext) -> Result { + #[inline] + pub fn lookup(&self, id: u32) -> Option<&CachedSong> { + self.songs.get(id as usize).and_then(|i| i.as_ref()) + } + + // {{{ Populate cache + pub async fn new(pool: &SqlitePool) -> Result { let mut result = Self::default(); - let songs: Vec = sqlx::query_as("SELECT * FROM songs") - .fetch_all(&ctx.db) - .await?; + let songs = sqlx::query!("SELECT * FROM songs").fetch_all(pool).await?; for song in songs { + let song = Song { + id: song.id as u32, + title: song.title, + ocr_alias: song.ocr_alias, + artist: song.artist, + }; + let song_id = song.id as usize; if song_id >= result.songs.len() { - result.songs.resize(song_id, None); + result.songs.resize(song_id + 1, None); } - let charts: Vec = sqlx::query_as("SELECT * FROM charts WHERE song_id=?") - .bind(song.id) - .fetch_all(&ctx.db) + let charts = sqlx::query!("SELECT * FROM charts WHERE song_id=?", song.id) + .fetch_all(pool) .await?; - let mut chart_cache = [None; 5]; + let mut chart_cache: [Option<_>; 5] = Default::default(); for chart in charts { - chart_cache[chart.difficulty.to_index()] = Some(chart); + let chart = Chart { + id: chart.id as u32, + song_id: chart.song_id as u32, + difficulty: Difficulty::try_from(chart.difficulty)?, + level: chart.level, + chart_constant: chart.chart_constant as u32, + note_count: chart.note_count as u32, + }; + + let index = chart.difficulty.to_index(); + chart_cache[index] = Some(chart); } result.songs[song_id] = Some(CachedSong::new(song, chart_cache)); @@ -85,4 +136,6 @@ impl SongCache { Ok(result) } + // }}} } +// }}} diff --git a/src/commands.rs b/src/commands.rs index d0b49fe..1db839c 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,13 +1,11 @@ +use std::fmt::Display; + use crate::context::{Context, Error}; -use crate::score::ImageCropper; +use crate::score::{CreatePlay, ImageCropper}; use crate::user::User; use image::imageops::FilterType; -use poise::serenity_prelude::{ - CreateAttachment, CreateEmbed, CreateEmbedAuthor, CreateMessage, Timestamp, -}; +use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage}; use poise::{serenity_prelude as serenity, CreateReply}; -use prettytable::format::{FormatBuilder, LinePosition, LineSeparator}; -use prettytable::{row, Table}; /// Show this help menu #[poise::command(prefix_command, track_edits, slash_command)] @@ -40,19 +38,52 @@ pub async fn score(_ctx: Context<'_>) -> Result<(), Error> { Ok(()) } +// {{{ Send error embed with image +async fn error_with_image( + ctx: Context<'_>, + bytes: &[u8], + filename: &str, + message: &str, + err: impl Display, +) -> Result<(), Error> { + let error_attachement = CreateAttachment::bytes(bytes, filename); + let msg = CreateMessage::default().embed( + CreateEmbed::default() + .title(message) + .attachment(filename) + .description(format!("{}", err)), + ); + + ctx.channel_id() + .send_files(ctx.http(), [error_attachement], msg) + .await?; + + Ok(()) +} +// }}} + /// Identify scores from attached images. #[poise::command(prefix_command, slash_command)] pub async fn magic( ctx: Context<'_>, #[description = "Images containing scores"] files: Vec, ) -> Result<(), Error> { - println!("{:?}", User::from_context(&ctx).await); + let user = match User::from_context(&ctx).await { + Ok(user) => user, + Err(_) => { + ctx.say("You are not an user in my database, sorry!") + .await?; + return Ok(()); + } + }; if files.len() == 0 { ctx.reply("No images found attached to message").await?; } else { + let mut embeds: Vec = vec![]; + let mut attachements: Vec = vec![]; let handle = ctx - .reply(format!("Processing: 0/{} images", files.len())) + .reply(format!("Processed 0/{} scores", files.len())) .await?; for (i, file) in files.iter().enumerate() { @@ -62,76 +93,110 @@ pub async fn magic( let format = image::guess_format(&bytes)?; // Image pre-processing - let mut image = image::load_from_memory_with_format(&bytes, format)? - .resize(1024, 1024, FilterType::Nearest) - .grayscale() - .blur(1.); - image.invert(); - - // {{{ Table experiment - let table_format = FormatBuilder::new() - .separators( - &[LinePosition::Title], - LineSeparator::new('─', '┬', '┌', '┐'), - ) - .padding(1, 1) - .build(); - let mut table = Table::new(); - table.set_format(table_format); - table.set_titles(row!["Chart", "Level", "Score", "Rating"]); - table.add_row(row!["Quon", "BYD 10", "10000807", "12.3 (-132)"]); - table.add_row(row!["Monochrome princess", "FTR 9+", " 9380807", "10.2"]); - table.add_row(row!["Grievous lady", "FTR 11", " 9286787", "11.2"]); - table.add_row(row!["Fracture ray", "FTR 11", " 8990891", "11.0"]); - table.add_row(row!["Shades of Light", "FTR 9+", "10000976", " 9.3 (-13)"]); - ctx.say(format!("```\n{}\n```", table.to_string())).await?; - // }}} - - let icon_attachement = CreateAttachment::file( - &tokio::fs::File::open("./data/jackets/grievous.png").await?, - "grievous.png", - ) - .await?; - let msg = CreateMessage::default().embed( - CreateEmbed::default() - .title("Grievous lady [FTR 11]") - .thumbnail("attachment://grievous.png") - .field("Score", "998302 (+8973)", true) - .field("Rating", "12.2 (+.6)", true) - .field("Grade", "EX+", true) - .field("ζ-Score", "982108 (+347)", true) - .field("ζ-Rating", "11.5 (+.45)", true) - .field("ζ-Grade", "EX", true) - .field("Status", "FR (-243F)", true) - .field("Max recall", "308/1073", true) - .field("Breakdown", "894/342/243/23", true), + let image = image::load_from_memory_with_format(&bytes, format)?.resize( + 1024, + 1024, + FilterType::Nearest, ); - ctx.channel_id() - .send_files(ctx.http(), [icon_attachement], msg) - .await?; + // // {{{ Table experiment + // let table_format = FormatBuilder::new() + // .separators( + // &[LinePosition::Title], + // LineSeparator::new('─', '┬', '┌', '┐'), + // ) + // .padding(1, 1) + // .build(); + // let mut table = Table::new(); + // table.set_format(table_format); + // table.set_titles(row!["Chart", "Level", "Score", "Rating"]); + // table.add_row(row!["Quon", "BYD 10", "10000807", "12.3 (-132)"]); + // table.add_row(row!["Monochrome princess", "FTR 9+", " 9380807", "10.2"]); + // table.add_row(row!["Grievous lady", "FTR 11", " 9286787", "11.2"]); + // table.add_row(row!["Fracture ray", "FTR 11", " 8990891", "11.0"]); + // table.add_row(row!["Shades of Light", "FTR 9+", "10000976", " 9.3 (-13)"]); + // ctx.say(format!("```\n{}\n```", table.to_string())).await?; + // // }}} + // // {{{ Embed experiment + // let icon_attachement = CreateAttachment::file( + // &tokio::fs::File::open("./data/jackets/grievous.png").await?, + // "grievous.png", + // ) + // .await?; + // let msg = CreateMessage::default().embed( + // CreateEmbed::default() + // .title("Grievous lady [FTR 11]") + // .thumbnail("attachment://grievous.png") + // .field("Score", "998302 (+8973)", true) + // .field("Rating", "12.2 (+.6)", true) + // .field("Grade", "EX+", true) + // .field("ζ-Score", "982108 (+347)", true) + // .field("ζ-Rating", "11.5 (+.45)", true) + // .field("ζ-Grade", "EX", true) + // .field("Status", "FR (-243F)", true) + // .field("Max recall", "308/1073", true) + // .field("Breakdown", "894/342/243/23", true), + // ); + // + // ctx.channel_id() + // .send_files(ctx.http(), [icon_attachement], msg) + // .await?; + // // }}} // Create cropper and run OCR let mut cropper = ImageCropper::default(); - let score_readout = match cropper.read_score(&image) { + + let (jacket, cached_song) = match cropper.read_jacket(ctx.data(), &image) { + // {{{ Jacket recognition error handling + Err(err) => { + error_with_image( + ctx, + &cropper.bytes, + &file.filename, + "Error while detecting jacket", + err, + ) + .await?; + + continue; + } + // }}} + Ok(j) => j, + }; + + let mut image = image.grayscale().blur(1.); + + let difficulty = match cropper.read_difficulty(&image) { // {{{ OCR error handling Err(err) => { - let error_attachement = - CreateAttachment::bytes(cropper.bytes, &file.filename); - let msg = CreateMessage::default().embed( - CreateEmbed::default() - .title("Could not read score from picture") - .attachment(&file.filename) - .description(format!("{}", err)) - .author( - CreateEmbedAuthor::new(&ctx.author().name) - .icon_url(ctx.author().face()), - ) - .timestamp(Timestamp::now()), - ); - ctx.channel_id() - .send_files(ctx.http(), [error_attachement], msg) - .await?; + error_with_image( + ctx, + &cropper.bytes, + &file.filename, + "Could not read score from picture", + &err, + ) + .await?; + + continue; + } + // }}} + Ok(d) => d, + }; + + image.invert(); + + let score = match cropper.read_score(&image) { + // {{{ OCR error handling + Err(err) => { + error_with_image( + ctx, + &cropper.bytes, + &file.filename, + "Could not read score from picture", + &err, + ) + .await?; continue; } @@ -139,31 +204,44 @@ pub async fn magic( Ok(score) => score, }; - // Reply with attachement & readout - let attachement = CreateAttachment::bytes(cropper.bytes, &file.filename); - let reply = CreateReply::default() - .attachment(attachement) - .content(format!("Score: {:?}", score_readout)) - .reply(true); - ctx.send(reply).await?; + let song = &cached_song.song; + let chart = cached_song.lookup(difficulty).ok_or_else(|| { + format!( + "Could not find difficulty {:?} for song {}", + difficulty, song.title + ) + })?; - // Edit progress reply - let progress_reply = CreateReply::default() - .content(format!("Processing: {}/{} images", i + 1, files.len())) - .reply(true); - handle.edit(ctx, progress_reply).await?; + let play = CreatePlay::new(score, chart, &user) + .with_attachment(file) + .save(&ctx.data()) + .await?; + + let (embed, attachement) = play.to_embed(&song, &chart, &jacket).await?; + embeds.push(embed); + attachements.push(attachement); } else { ctx.reply("One of the attached files is not an image!") .await?; continue; } + + let edited = CreateReply::default().reply(true).content(format!( + "Processed {}/{} scores", + i + 1, + files.len() + )); + + handle.edit(ctx, edited).await?; } - // Finish off progress reply - let progress_reply = CreateReply::default() - .content(format!("All images have been processed!")) - .reply(true); - handle.edit(ctx, progress_reply).await?; + handle.delete(ctx).await?; + + let msg = CreateMessage::new().embeds(embeds); + + ctx.channel_id() + .send_files(ctx.http(), attachements, msg) + .await?; } Ok(()) diff --git a/src/context.rs b/src/context.rs index 52069ad..f6d725f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,6 +1,8 @@ +use std::path::PathBuf; + use sqlx::SqlitePool; -use crate::chart::SongCache; +use crate::{chart::SongCache, jacket::JacketCache}; // Types used by all command functions pub type Error = Box; @@ -8,16 +10,22 @@ pub type Context<'a> = poise::Context<'a, UserContext, Error>; // Custom user data passed to all command functions pub struct UserContext { + pub data_dir: PathBuf, pub db: SqlitePool, pub song_cache: SongCache, + pub jacket_cache: JacketCache, } impl UserContext { #[inline] - pub fn new(db: SqlitePool) -> Self { - Self { + pub async fn new(data_dir: PathBuf, db: SqlitePool) -> Result { + let song_cache = SongCache::new(&db).await?; + let jacket_cache = JacketCache::new(&data_dir)?; + Ok(Self { + data_dir, db, - song_cache: SongCache::default(), - } + song_cache, + jacket_cache, + }) } } diff --git a/src/jacket.rs b/src/jacket.rs new file mode 100644 index 0000000..4171b69 --- /dev/null +++ b/src/jacket.rs @@ -0,0 +1,122 @@ +use std::path::PathBuf; + +use image::{GenericImageView, Rgba}; +use kd_tree::{KdMap, KdPoint}; +use num::Integer; + +use crate::context::Error; + +/// How many sub-segments to split each side into +const SPLIT_FACTOR: u32 = 5; +const IMAGE_VEC_DIM: usize = (SPLIT_FACTOR * SPLIT_FACTOR * 3) as usize; + +#[derive(Debug, Clone)] +pub struct ImageVec { + pub colors: [f32; IMAGE_VEC_DIM], +} + +#[derive(Debug, Clone)] +pub struct Jacket { + pub song_id: u32, + pub path: PathBuf, +} + +impl ImageVec { + // {{{ (Image => vector) encoding + fn from_image(image: &impl GenericImageView>) -> ImageVec { + let mut colors = [0.0; IMAGE_VEC_DIM]; + let chunk_width = image.width() / SPLIT_FACTOR; + let chunk_height = image.height() / SPLIT_FACTOR; + for i in 0..(SPLIT_FACTOR * SPLIT_FACTOR) { + let (iy, ix) = i.div_rem(&SPLIT_FACTOR); + let cropped = image.view( + chunk_width * ix, + chunk_height * iy, + chunk_width, + chunk_height, + ); + + let mut r = 0; + let mut g = 0; + let mut b = 0; + let mut count = 0; + + for (_, _, pixel) in cropped.pixels() { + r += pixel.0[0] as u64; + g += pixel.0[1] as u64; + b += pixel.0[2] as u64; + count += 1; + } + + let count = count as f64; + let r = r as f64 / count; + let g = g as f64 / count; + let b = b as f64 / count; + colors[i as usize * 3 + 0] = r as f32; + colors[i as usize * 3 + 1] = g as f32; + colors[i as usize * 3 + 2] = b as f32; + } + + Self { colors } + } + // }}} +} + +impl KdPoint for ImageVec { + type Dim = typenum::U75; + type Scalar = f32; + + fn dim() -> usize { + IMAGE_VEC_DIM + } + + fn at(&self, i: usize) -> Self::Scalar { + self.colors[i] + } +} + +pub struct JacketCache { + tree: KdMap, +} + +impl JacketCache { + // {{{ Generate tree + pub fn new(data_dir: &PathBuf) -> Result { + let jacket_csv_path = data_dir.join("jackets.csv"); + let mut reader = csv::Reader::from_path(jacket_csv_path)?; + + let mut entries = vec![]; + + for record in reader.records() { + let record = record?; + let filename = &record[0]; + let song_id = u32::from_str_radix(&record[1], 10)?; + let image_path = data_dir.join(format!("jackets/{}.png", filename)); + let image = image::io::Reader::open(&image_path)?.decode()?; + let jacket = Jacket { + song_id, + path: image_path, + }; + + entries.push((ImageVec::from_image(&image), jacket)) + } + + let result = Self { + tree: KdMap::build_by_ordered_float(entries), + }; + + Ok(result) + } + // }}} + // {{{ Recognise + #[inline] + pub fn recognise( + &self, + image: &impl GenericImageView>, + ) -> Option<(f32, &Jacket)> { + self.tree + .nearest(&ImageVec::from_image(image)) + .map(|p| (p.squared_distance.sqrt(), &p.item.1)) + } + // }}} +} diff --git a/src/main.rs b/src/main.rs index 3abe091..15f63ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,15 +4,14 @@ mod chart; mod commands; mod context; +mod jacket; mod score; mod user; -use chart::SongCache; use context::{Error, UserContext}; use poise::serenity_prelude as serenity; -use score::score_to_zeta_score; use sqlx::sqlite::SqlitePoolOptions; -use std::{env::var, sync::Arc, time::Duration}; +use std::{env::var, path::PathBuf, str::FromStr, sync::Arc, time::Duration}; // {{{ Error handler async fn on_error(error: poise::FrameworkError<'_, UserContext, Error>) { @@ -40,9 +39,6 @@ async fn main() { .await .unwrap(); - println!("{:?}", score_to_zeta_score(9966677, 1303)); - println!("{:?}", score_to_zeta_score(9970525, 1303)); - // {{{ Poise options let options = poise::FrameworkOptions { commands: vec![commands::help(), commands::score()], @@ -64,8 +60,7 @@ async fn main() { Box::pin(async move { println!("Logged in as {}", _ready.user.name); poise::builtins::register_globally(ctx, &framework.options().commands).await?; - let mut ctx = UserContext::new(pool); - ctx.song_cache = SongCache::new(&ctx).await?; + let ctx = UserContext::new(PathBuf::from_str(&data_dir)?, pool).await?; Ok(ctx) }) }) diff --git a/src/score.rs b/src/score.rs index dec73b9..d1ab755 100644 --- a/src/score.rs +++ b/src/score.rs @@ -1,13 +1,15 @@ #![allow(dead_code)] -use std::{io::Cursor, sync::OnceLock, time::Instant}; +use std::{fmt::Display, io::Cursor, sync::OnceLock}; -use image::DynamicImage; +use image::{DynamicImage, GenericImageView}; use num::Rational64; +use poise::serenity_prelude::{Attachment, AttachmentId, CreateAttachment, CreateEmbed}; use tesseract::{PageSegMode, Tesseract}; use crate::{ - chart::{Chart, Difficulty}, + chart::{CachedSong, Chart, Difficulty, Song}, context::{Error, UserContext}, + jacket::Jacket, user::User, }; @@ -125,9 +127,9 @@ impl RelativeRect { let p = (aspect_ratio - low_ratio) / (high_ratio - low_ratio); return Some(Self::new( lerp(p, low.x, high.x), - lerp(p, low.y, high.y) - 0.005, + lerp(p, low.y, high.y), lerp(p, low.width, high.width), - lerp(p, low.height, high.height) + 2. * 0.005, + lerp(p, low.height, high.height), dimensions, )); } @@ -138,6 +140,34 @@ impl RelativeRect { } // }}} // {{{ Data points +fn process_datapoints(rects: &mut Vec) { + rects.sort_by_key(|r| (r.dimensions.aspect_ratio() * 1000.0).floor() as u32); + + // Filter datapoints that are close together + let mut i = 0; + while i < rects.len() - 1 { + let low = rects[i]; + let high = rects[i + 1]; + + if (low.dimensions.aspect_ratio() - high.dimensions.aspect_ratio()).abs() < 0.001 { + // TODO: we could interpolate here but oh well + rects.remove(i + 1); + } + + i += 1; + } +} + +fn widen_by(rects: &mut Vec, x: f32, y: f32) { + for rect in rects { + rect.x -= x; + rect.y -= y; + rect.width += 2. * x; + rect.height += 2. * y; + } +} + +// {{{ Score fn score_rects() -> &'static [RelativeRect] { static CELL: OnceLock> = OnceLock::new(); CELL.get_or_init(|| { @@ -152,74 +182,138 @@ fn score_rects() -> &'static [RelativeRect] { AbsoluteRect::new(1069, 868, 636, 112, ImageDimensions::new(2732, 2048)).to_relative(), AbsoluteRect::new(1125, 510, 534, 93, ImageDimensions::new(2778, 1284)).to_relative(), ]; - rects.sort_by_key(|r| (r.dimensions.aspect_ratio() * 1000.0).floor() as u32); - - // Filter datapoints that are close together - let mut i = 0; - while i < rects.len() - 1 { - let low = rects[i]; - let high = rects[i + 1]; - - if (low.dimensions.aspect_ratio() - high.dimensions.aspect_ratio()).abs() < 0.001 { - // TODO: we could interpolate here but oh well - rects.remove(i + 1); - } - - i += 1; - } - - rects - }) -} - -fn difficulty_rects() -> &'static [RelativeRect] { - static CELL: OnceLock> = OnceLock::new(); - CELL.get_or_init(|| { - let mut rects: Vec = vec![ - AbsoluteRect::new(642, 287, 284, 51, ImageDimensions::new(1560, 720)).to_relative(), - AbsoluteRect::new(651, 285, 305, 55, ImageDimensions::new(1600, 720)).to_relative(), - AbsoluteRect::new(748, 485, 503, 82, ImageDimensions::new(2000, 1200)).to_relative(), - AbsoluteRect::new(841, 683, 500, 92, ImageDimensions::new(2160, 1620)).to_relative(), - AbsoluteRect::new(851, 707, 532, 91, ImageDimensions::new(2224, 1668)).to_relative(), - AbsoluteRect::new(1037, 462, 476, 89, ImageDimensions::new(2532, 1170)).to_relative(), - AbsoluteRect::new(973, 653, 620, 105, ImageDimensions::new(2560, 1600)).to_relative(), - AbsoluteRect::new(1069, 868, 636, 112, ImageDimensions::new(2732, 2048)).to_relative(), - AbsoluteRect::new(1125, 510, 534, 93, ImageDimensions::new(2778, 1284)).to_relative(), - ]; - rects.sort_by_key(|r| (r.dimensions.aspect_ratio() * 1000.0).floor() as u32); + process_datapoints(&mut rects); + widen_by(&mut rects, 0.0, 0.005); rects }) } // }}} -// {{{ Plays -/// Returns the zeta score and the number of shinies -pub fn score_to_zeta_score(score: u32, note_count: u32) -> (u32, u32) { - // Smallest possible difference between (zeta-)scores - let increment = Rational64::new_raw(5000000, note_count as i64).reduced(); - let zeta_increment = Rational64::new_raw(2000000, note_count as i64).reduced(); +// {{{ Difficulty +fn difficulty_rects() -> &'static [RelativeRect] { + static CELL: OnceLock> = OnceLock::new(); + CELL.get_or_init(|| { + let mut rects: Vec = vec![ + AbsoluteRect::new(232, 203, 104, 23, ImageDimensions::new(1560, 720)).to_relative(), + AbsoluteRect::new(252, 204, 99, 21, ImageDimensions::new(1600, 720)).to_relative(), + AbsoluteRect::new(146, 356, 155, 34, ImageDimensions::new(2000, 1200)).to_relative(), + AbsoluteRect::new(155, 546, 167, 38, ImageDimensions::new(2160, 1620)).to_relative(), + AbsoluteRect::new(163, 562, 175, 38, ImageDimensions::new(2224, 1668)).to_relative(), + AbsoluteRect::new(378, 332, 161, 34, ImageDimensions::new(2532, 1170)).to_relative(), + AbsoluteRect::new(183, 487, 197, 44, ImageDimensions::new(2560, 1600)).to_relative(), + AbsoluteRect::new(198, 692, 219, 46, ImageDimensions::new(2732, 2048)).to_relative(), + AbsoluteRect::new(414, 364, 177, 38, ImageDimensions::new(2778, 1284)).to_relative(), + ]; + process_datapoints(&mut rects); + rects + }) +} +// }}} +// {{{ Jacket +fn jacket_rects() -> &'static [RelativeRect] { + static CELL: OnceLock> = OnceLock::new(); + CELL.get_or_init(|| { + let mut rects: Vec = vec![ + AbsoluteRect::new(171, 268, 375, 376, ImageDimensions::new(1560, 720)).to_relative(), + AbsoluteRect::new(190, 267, 376, 377, ImageDimensions::new(1600, 720)).to_relative(), + AbsoluteRect::new(46, 456, 590, 585, ImageDimensions::new(2000, 1200)).to_relative(), + AbsoluteRect::new(51, 655, 633, 632, ImageDimensions::new(2160, 1620)).to_relative(), + AbsoluteRect::new(53, 675, 654, 653, ImageDimensions::new(2224, 1668)).to_relative(), + AbsoluteRect::new(274, 434, 614, 611, ImageDimensions::new(2532, 1170)).to_relative(), + AbsoluteRect::new(58, 617, 753, 750, ImageDimensions::new(2560, 1600)).to_relative(), + AbsoluteRect::new(65, 829, 799, 800, ImageDimensions::new(2732, 2048)).to_relative(), + AbsoluteRect::new(300, 497, 670, 670, ImageDimensions::new(2778, 1284)).to_relative(), + ]; + process_datapoints(&mut rects); + rects + }) +} +// }}} +// }}} +// {{{ Score +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Score(pub u32); - let score = Rational64::from_integer(score as i64); - let score_units = (score / increment).floor(); +impl Score { + // {{{ Score => ζ-Score + /// Returns the zeta score and the number of shinies + pub fn to_zeta(self, note_count: u32) -> (Score, u32) { + // Smallest possible difference between (zeta-)scores + let increment = Rational64::new_raw(5000000, note_count as i64).reduced(); + let zeta_increment = Rational64::new_raw(2000000, note_count as i64).reduced(); - let non_shiny_score = (score_units * increment).floor(); - let shinies = score - non_shiny_score; + let score = Rational64::from_integer(self.0 as i64); + let score_units = (score / increment).floor(); - let zeta_score_units = Rational64::from_integer(2) * score_units + shinies; - let zeta_score = (zeta_increment * zeta_score_units).floor().to_integer() as u32; + let non_shiny_score = (score_units * increment).floor(); + let shinies = score - non_shiny_score; - (zeta_score, shinies.to_integer() as u32) + let zeta_score_units = Rational64::from_integer(2) * score_units + shinies; + let zeta_score = Score((zeta_increment * zeta_score_units).floor().to_integer() as u32); + + (zeta_score, shinies.to_integer() as u32) + } + // }}} + // {{{ Score => Play rating + #[inline] + pub fn play_rating(self, chart_constant: u32) -> i32 { + chart_constant as i32 + + if self.0 >= 10000000 { + 200 + } else if self.0 >= 9800000 { + 100 + (self.0 as i32 - 9_800_000) / 20_000 + } else { + (self.0 as i32 - 9_500_000) / 10_000 + } + } + // }}} + // {{{ Score => grade + #[inline] + // TODO: Perhaps make an enum for this + pub fn grade(self) -> &'static str { + let score = self.0; + if score > 9900000 { + "EX+" + } else if score > 9800000 { + "EX" + } else if score > 9500000 { + "AA" + } else if score > 9200000 { + "A" + } else if score > 8900000 { + "B" + } else if score > 8600000 { + "C" + } else { + "D" + } + } + // }}} } +impl Display for Score { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let score = self.0; + write!( + f, + "{}'{}'{}", + score / 1000000, + (score / 1000) % 1000, + score % 1000 + ) + } +} +// }}} +// {{{ Plays // {{{ Create play #[derive(Debug, Clone)] pub struct CreatePlay { chart_id: u32, user_id: u32, - discord_attachment_id: Option, + discord_attachment_id: Option, // Actual score data - score: u32, - zeta_score: Option, + score: Score, + zeta_score: Score, // Optional score details max_recall: Option, @@ -232,13 +326,13 @@ pub struct CreatePlay { impl CreatePlay { #[inline] - pub fn new(score: u32, chart: Chart, user: User) -> Self { + pub fn new(score: Score, chart: &Chart, user: &User) -> Self { Self { chart_id: chart.id, user_id: user.id, discord_attachment_id: None, score, - zeta_score: Some(score_to_zeta_score(score, chart.note_count).0), + zeta_score: score.to_zeta(chart.note_count as u32).0, max_recall: None, far_notes: None, // TODO: populate these @@ -247,52 +341,121 @@ impl CreatePlay { } } + #[inline] + pub fn with_attachment(mut self, attachment: &Attachment) -> Self { + self.discord_attachment_id = Some(attachment.id); + self + } + + // {{{ Save pub async fn save(self, ctx: &UserContext) -> Result { - let play = sqlx::query_as!( - Play, + let attachment_id = self.discord_attachment_id.map(|i| i.get() as i64); + let play = sqlx::query!( " - INSERT INTO plays( - user_id,chart_id,discord_attachment_id, - score,zeta_score,max_recall,far_notes - ) - VALUES(?,?,?,?,?,?,?) - RETURNING * + INSERT INTO plays( + user_id,chart_id,discord_attachment_id, + score,zeta_score,max_recall,far_notes + ) + VALUES(?,?,?,?,?,?,?) + RETURNING id, created_at ", self.user_id, self.chart_id, - self.discord_attachment_id, - self.score, - self.zeta_score, + attachment_id, + self.score.0, + self.zeta_score.0, self.max_recall, self.far_notes ) .fetch_one(&ctx.db) .await?; - Ok(play) + Ok(Play { + id: play.id as u32, + created_at: play.created_at, + chart_id: self.chart_id, + user_id: self.user_id, + discord_attachment_id: self.discord_attachment_id, + score: self.score, + zeta_score: self.zeta_score, + max_recall: self.max_recall, + far_notes: self.far_notes, + creation_ptt: self.creation_ptt, + creation_zeta_ptt: self.creation_zeta_ptt, + }) } + // }}} } // }}} // {{{ Play #[derive(Debug, Clone, sqlx::FromRow)] pub struct Play { - id: i64, - chart_id: i64, - user_id: i64, - discord_attachment_id: Option, + id: u32, + chart_id: u32, + user_id: u32, + discord_attachment_id: Option, // Actual score data - score: i64, - zeta_score: Option, + score: Score, + zeta_score: Score, // Optional score details - max_recall: Option, - far_notes: Option, + max_recall: Option, + far_notes: Option, // Creation data created_at: chrono::NaiveDateTime, - creation_ptt: Option, - creation_zeta_ptt: Option, + creation_ptt: Option, + creation_zeta_ptt: Option, +} + +impl Play { + // {{{ Play to embed + pub async fn to_embed( + &self, + song: &Song, + chart: &Chart, + jacket: &Jacket, + ) -> Result<(CreateEmbed, CreateAttachment), Error> { + let (_, shiny_count) = self.score.to_zeta(chart.note_count); + + let attachement_name = format!("{:?}-{:?}.png", song.id, self.score.0); + let icon_attachement = CreateAttachment::file( + &tokio::fs::File::open(&jacket.path).await?, + &attachement_name, + ) + .await?; + + let embed = CreateEmbed::default() + .title(&song.title) + .thumbnail(format!("attachment://{}", &attachement_name)) + .field("Score", format!("{} (+?)", self.score), true) + .field( + "Rating", + format!( + "{:.2} (+?)", + (self.score.play_rating(chart.chart_constant)) as f32 / 100. + ), + true, + ) + .field("Grade", self.score.grade(), true) + .field("ζ-Score", format!("{} (+?)", self.zeta_score), true) + .field( + "ζ-Rating", + format!( + "{:.2} (+?)", + (self.zeta_score.play_rating(chart.chart_constant)) as f32 / 100. + ), + true, + ) + .field("ζ-Grade", self.zeta_score.grade(), true) + .field("Status", "?", true) + .field("Max recall", "?", true) + .field("Breakdown", format!("{}/?/?/?", shiny_count), true); + + Ok((embed, icon_attachement)) + } + // }}} } // }}} // {{{ Tests @@ -305,12 +468,13 @@ mod score_tests { // note counts for note_count in 200..=2000 { for shiny_count in 0..=note_count { - let score = 10000000 + shiny_count; + let score = Score(10000000 + shiny_count); let zeta_score_units = 4 * (note_count - shiny_count) + 5 * shiny_count; + let (zeta_score, computed_shiny_count) = score.to_zeta(note_count); let expected_zeta_score = Rational64::from_integer(zeta_score_units as i64) * Rational64::new_raw(2000000, note_count as i64).reduced(); - let (zeta_score, computed_shiny_count) = score_to_zeta_score(score, note_count); - assert_eq!(zeta_score, expected_zeta_score.to_integer() as u32); + + assert_eq!(zeta_score, Score(expected_zeta_score.to_integer() as u32)); assert_eq!(computed_shiny_count, shiny_count); } } @@ -318,19 +482,6 @@ mod score_tests { } // }}} // }}} -// {{{ Ocr types -#[derive(Debug, Clone, Copy)] -pub struct ScoreReadout { - pub score: u32, - pub difficulty: Difficulty, -} - -impl ScoreReadout { - pub fn new(score: u32, difficulty: Difficulty) -> Self { - Self { score, difficulty } - } -} -// }}} // {{{ Run OCR /// Caches a byte vector in order to prevent reallocation #[derive(Debug, Clone, Default)] @@ -352,18 +503,19 @@ impl ImageCropper { Ok(()) } - pub fn read_score(&mut self, image: &DynamicImage) -> Result { - let rect = + // {{{ Read score + pub fn read_score(&mut self, image: &DynamicImage) -> Result { + self.crop_image_to_bytes( + &image, RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), score_rects()) .ok_or_else(|| "Could not find score area in picture")? - .to_absolute(); - self.crop_image_to_bytes(&image, rect)?; + .to_absolute(), + )?; let mut t = Tesseract::new(None, Some("eng"))? // .set_variable("classify_bln_numeric_mode", "1'")? .set_variable("tessedit_char_whitelist", "0123456789'")? .set_image_from_mem(&self.bytes)?; - t.set_page_seg_mode(PageSegMode::PsmRawLine); t = t.recognize()?; @@ -378,8 +530,72 @@ impl ImageCropper { .filter(|char| *char != ' ' && *char != '\'') .collect(); - let int = u32::from_str_radix(&text, 10)?; - Ok(ScoreReadout::new(int, Difficulty::FTR)) + let score = u32::from_str_radix(&text, 10)?; + Ok(Score(score)) } + // }}} + // {{{ Read difficulty + pub fn read_difficulty(&mut self, image: &DynamicImage) -> Result { + self.crop_image_to_bytes( + &image, + RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), difficulty_rects()) + .ok_or_else(|| "Could not find difficulty area in picture")? + .to_absolute(), + )?; + + let mut t = Tesseract::new(None, Some("eng"))?.set_image_from_mem(&self.bytes)?; + t.set_page_seg_mode(PageSegMode::PsmRawLine); + t = t.recognize()?; + + if t.mean_text_conf() < 10 { + Err("Difficulty text is not readable.")?; + } + + let text: &str = &t.get_text()?; + let text = text.trim(); + + let difficulty = Difficulty::DIFFICULTIES + .iter() + .zip(Difficulty::DIFFICULTY_STRINGS) + .min_by_key(|(_, difficulty_string)| { + edit_distance::edit_distance(difficulty_string, text) + }) + .map(|(difficulty, _)| *difficulty) + .ok_or_else(|| format!("Unrecognised difficulty '{}'", text))?; + + Ok(difficulty) + } + // }}} + // {{{ Read jacket + pub fn read_jacket<'a>( + &mut self, + ctx: &'a UserContext, + image: &DynamicImage, + ) -> Result<(&'a Jacket, &'a CachedSong), Error> { + let rect = + RelativeRect::from_aspect_ratio(ImageDimensions::from_image(image), jacket_rects()) + .ok_or_else(|| "Could not find jacket area in picture")? + .to_absolute(); + + let cropped = image.view(rect.x, rect.y, rect.width, rect.height); + let (distance, jacket) = ctx + .jacket_cache + .recognise(&*cropped) + .ok_or_else(|| "Could not recognise jacket")?; + + if distance > 100.0 { + // Save image to be sent to discord + self.crop_image_to_bytes(&image, rect)?; + Err("No known jacket looks like this")?; + } + + let song = ctx + .song_cache + .lookup(jacket.song_id) + .ok_or_else(|| format!("Could not find song with id {}", jacket.song_id))?; + + Ok((jacket, song)) + } + // }}} } // }}} diff --git a/src/user.rs b/src/user.rs index 0b18984..11bea55 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,6 +1,6 @@ use crate::context::{Context, Error}; -#[derive(Debug, Clone, sqlx::FromRow)] +#[derive(Debug, Clone)] pub struct User { pub id: u32, pub discord_id: String, @@ -10,11 +10,14 @@ pub struct User { impl User { pub async fn from_context(ctx: &Context<'_>) -> Result { let id = ctx.author().id.get().to_string(); - let user = sqlx::query_as("SELECT * FROM users WHERE discord_id = ?") - .bind(id) + let user = sqlx::query!("SELECT * FROM users WHERE discord_id = ?", id) .fetch_one(&ctx.data().db) .await?; - Ok(user) + Ok(User { + id: user.id as u32, + discord_id: user.discord_id, + nickname: user.nickname, + }) } }