├── ct.ico ├── .gitignore ├── assets ├── ct.ico ├── oppai.exe └── sectionpass.wav ├── submodules ├── oppai.exe └── FsBeatmapParser │ ├── FsBeatmapProcessor.fsproj │ ├── FsBeatmapProcessor.sln │ ├── Editor.fs │ ├── Colours.fs │ ├── Difficulty.fs │ ├── TimingPoints.fs │ ├── Events.fs │ ├── Metadata.fs │ ├── JunUtils.fs │ ├── HitObjects.fs │ ├── BeatmapProcessor.fs │ ├── General.fs │ ├── Program.fs │ └── FsBeatmap.fs ├── .gitmodules ├── App.config ├── Properties ├── Settings.settings ├── Settings.Designer.cs ├── AssemblyInfo.cs ├── Resources.Designer.cs └── Resources.resx ├── README.md ├── packages.config ├── Program.cs ├── todo.txt ├── circle-tracker.sln ├── Updater.cs ├── circle-tracker.csproj ├── MainForm.cs ├── DifficultyCalculator.cs ├── MainForm.Designer.cs └── Tracker.cs /ct.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunOrange/circle-tracker/HEAD/ct.ico -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | bin/ 4 | obj/ 5 | Releases/ 6 | packages/ 7 | .vs/ 8 | -------------------------------------------------------------------------------- /assets/ct.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunOrange/circle-tracker/HEAD/assets/ct.ico -------------------------------------------------------------------------------- /assets/oppai.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunOrange/circle-tracker/HEAD/assets/oppai.exe -------------------------------------------------------------------------------- /submodules/oppai.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunOrange/circle-tracker/HEAD/submodules/oppai.exe -------------------------------------------------------------------------------- /assets/sectionpass.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FunOrange/circle-tracker/HEAD/assets/sectionpass.wav -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodules/ProcessMemoryDataFinder"] 2 | path = submodules/ProcessMemoryDataFinder 3 | url = https://github.com/Piotrekol/ProcessMemoryDataFinder.git 4 | -------------------------------------------------------------------------------- /App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Properties/Settings.settings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # circle-tracker 2 | A program I made for tracking training progress in osu. 3 | 4 | Please watch this video for more information: 5 | https://www.youtube.com/watch?v=_65AvmAjlpY 6 | 7 | TODO: add more stuff here 8 | 9 | ## Build troubleshooting 10 | Make sure all projects are set to build in Configuration Manager 11 | https://www.primordialcode.com/blog/post/referenced-project-targeted-different-framework-family -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/FsBeatmapProcessor.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Library 5 | net471 6 | FsBeatmapProcessor 7 | 8 | 9 | 10 | AnyCPU 11 | bin\Debug\ 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace circle_tracker.Properties 12 | { 13 | 14 | 15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] 17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase 18 | { 19 | 20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 21 | 22 | public static Settings Default 23 | { 24 | get 25 | { 26 | return defaultInstance; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/FsBeatmapProcessor.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29926.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsBeatmapProcessor", "FsBeatmapProcessor.fsproj", "{03F784F7-CE81-453E-B03B-92FBBC951672}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {03F784F7-CE81-453E-B03B-92FBBC951672}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {03F784F7-CE81-453E-B03B-92FBBC951672}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {03F784F7-CE81-453E-B03B-92FBBC951672}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {03F784F7-CE81-453E-B03B-92FBBC951672}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {AD0A92CB-9EFD-4C28-BFCA-6038D5751BF6} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("circle-tracker")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("circle-tracker")] 13 | [assembly: AssemblyCopyright("Copyright © 2020")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("862ff93d-5032-4e99-9a96-c4624fec158d")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using System.Windows.Forms; 8 | 9 | namespace Circle_Tracker 10 | { 11 | static class Program 12 | { 13 | /// 14 | /// The main entry point for the application. 15 | /// 16 | [STAThread] 17 | static void Main() 18 | { 19 | if (!EnsureSingleInstance()) 20 | { 21 | MessageBox.Show("Another instance of circle tracker is already running.", "Error"); 22 | return; 23 | } 24 | Application.EnableVisualStyles(); 25 | Application.SetCompatibleTextRenderingDefault(false); 26 | Application.Run(new MainForm()); 27 | } 28 | static bool EnsureSingleInstance() 29 | { 30 | Process currentProcess = Process.GetCurrentProcess(); 31 | 32 | var runningProcess = (from process in Process.GetProcesses() 33 | where 34 | process.Id != currentProcess.Id && 35 | process.ProcessName.Equals( 36 | currentProcess.ProcessName, 37 | StringComparison.Ordinal) 38 | select process).FirstOrDefault(); 39 | 40 | return (runningProcess != null) ? false : true; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/Editor.fs: -------------------------------------------------------------------------------- 1 | module Editor 2 | 3 | open System 4 | open JunUtils 5 | 6 | type EditorSetting = 7 | | Bookmarks of list 8 | | DistanceSpacing of decimal 9 | | BeatDivisor of decimal 10 | | GridSize of int 11 | | TimelineZoom of decimal 12 | | Comment of string 13 | 14 | let tryParseEditorOption line : EditorSetting option = 15 | match line with 16 | | Regex @"(.+)\s?:\s?(.+)" [key; value] -> 17 | match key with 18 | | "Bookmarks" -> 19 | match tryParseCsvInt value with 20 | | Some(list) -> Some(Bookmarks(list)) 21 | | None -> printfn "Error parsing %s" value; Some(Comment(line)) 22 | | "DistanceSpacing" -> Some(DistanceSpacing(decimal value)) 23 | | "BeatDivisor" -> Some(BeatDivisor(decimal value)) 24 | | "GridSize" -> Some(GridSize(int value)) 25 | | "TimelineZoom" -> Some(TimelineZoom(decimal value)) 26 | | _ -> Some(Comment(line)) 27 | | _ -> Some(Comment(line)) 28 | 29 | let editorSettingToString es = 30 | match es with 31 | | Bookmarks bookmarks -> sprintf "Bookmarks: %s" (String.Join(",", List.map int bookmarks)) 32 | | DistanceSpacing ds -> sprintf "DistanceSpacing: %M" ds 33 | | BeatDivisor bd -> sprintf "BeatDivisor: %M" bd 34 | | GridSize gs -> sprintf "GridSize: %d" gs 35 | | TimelineZoom tz -> sprintf "TimelineZoom: %M" tz 36 | | Comment comment -> comment 37 | 38 | let parseEditorSection : string list -> EditorSetting list = parseSectionUsing tryParseEditorOption 39 | -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | --- BUGS --- 2 | [] Plays get submitted while watching replays or spectating 3 | -> Check user to ignore replays/spectates from other users 4 | [DONE] Map Complete doesn't work when playing multi 5 | [DONE] Play time is inaccurate; need to subtract starting object time 6 | [DONE] Check proper bpm with HT, DT, NC 7 | [DONE] Game data doesn't get updated in multi 8 | [DONE] Startup directory depends on Drive C () 9 | [DONE] Fix 0 bpm bug once and for all 10 | [DONE] Add new logo to notification tray icon 11 | [DONE] Fixed incorrect number of hits being submitted 12 | 13 | --- FEATURES --- 14 | [] ignore non-std game modes 15 | [DONE] ignore relax, autopilot 16 | [DONE] Calculate accuracy manually from 300s, 100s, 50s, misses (accuracy reading sometimes takes a while to update) 17 | [] Store plays in local buffer while internet unavailable 18 | [] Add collection to spreadsheet 19 | [DONE] Store "#ERROR! fix" user setting 20 | [DONE] Automatically extend named ranges to bottom of sheet 21 | [DONE] Automatically add more rows if sheet becomes full 22 | [DONE] Extended info (time) 23 | [DONE] Add welcome message pointing to youtube tutorial 24 | [DONE] Detect multiple instances running 25 | [DONE] Confirm spreadsheet timezone with user 26 | [DONE] Add playcount 27 | [DONE] Automatically update Named Ranges 28 | [DONE] Add new circle tracker logo 29 | [DONE] Automatically update raw data headers 30 | [DONE] Option to use semicolons instead of commas for function formulas 31 | [DONE] Extended info (Accuracy, Rank) 32 | [DONE] Extended info (300s, 100s, 50s) 33 | [DONE] Extended mods (EZ, FL) 34 | [DONE] Extended mods (HT) 35 | [DONE] EZ HT AR LUTs 36 | [DONE] EZ HT OD LUTs 37 | [DONE] Minimize to system tray 38 | [DONE] Launch on Windows startup 39 | [DONE] Async game variables update 40 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/Colours.fs: -------------------------------------------------------------------------------- 1 | module Colours 2 | 3 | open JunUtils 4 | 5 | type Colour = 6 | { 7 | r : int; 8 | g : int; 9 | b : int; 10 | } 11 | 12 | type ColourSetting = 13 | | Combo of int * Colour 14 | | SliderTrackOverride of Colour 15 | | SliderBorder of Colour 16 | | Comment of string 17 | 18 | let tryParseColour str : Colour option = 19 | match tryParseCsvInt str with 20 | | Some(nums) -> 21 | match nums with 22 | | [r; g; b] -> 23 | Some({ 24 | r = r; 25 | g = g; 26 | b = b; 27 | }) 28 | | _ -> parseError str 29 | | _ -> parseError str 30 | 31 | let tryParseColourOption line : ColourSetting option = 32 | match line with 33 | | Regex @"(.+?)\s?:\s?(.*)" [key; value] -> 34 | match key with 35 | 36 | // Combo Colour 37 | | Regex @"^Combo(\d+)$" [comboNumber] -> 38 | match tryParseColour value with 39 | | Some(col) -> Some(Combo(int comboNumber, col)) 40 | | _ -> Some(Comment(line)) 41 | 42 | // SliderTrackOverride 43 | | "SliderTrackOverride" -> 44 | match tryParseColour value with 45 | | Some(col) -> Some(SliderTrackOverride(col)) 46 | | _ -> Some(Comment(line)) 47 | 48 | // SliderBorder 49 | | "SliderTrackOverride" -> 50 | match tryParseColour value with 51 | | Some(col) -> Some(SliderBorder(col)) 52 | | _ -> Some(Comment(line)) 53 | 54 | | _ -> parseError line 55 | | _ -> Some(Comment(line)) 56 | 57 | let colourSettingToString cs = 58 | match cs with 59 | | Combo (num, col) -> sprintf "Combo%d : %d,%d,%d" num col.r col.g col.b 60 | | SliderTrackOverride col -> sprintf "SliderTrackOverride : %d,%d,%d" col.r col.g col.b 61 | | SliderBorder col -> sprintf "SliderBorder : %d,%d,%d" col.r col.g col.b 62 | | Comment comment -> comment 63 | 64 | let parseColourSection : string list -> ColourSetting list = parseSectionUsing tryParseColourOption 65 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/Difficulty.fs: -------------------------------------------------------------------------------- 1 | module Difficulty 2 | 3 | open JunUtils 4 | 5 | type DifficultySetting = 6 | | HPDrainRate of decimal 7 | | CircleSize of decimal 8 | | OverallDifficulty of decimal 9 | | ApproachRate of decimal 10 | | SliderMultiplier of decimal 11 | | SliderTickRate of decimal 12 | | Comment of string 13 | 14 | // isn't there an easier way to do this... 15 | let isHPDrainRate = function HPDrainRate _ -> true | _ -> false 16 | let isCircleSize = function CircleSize _ -> true | _ -> false 17 | let isOverallDifficulty = function OverallDifficulty _ -> true | _ -> false 18 | let isApproachRate = function ApproachRate _ -> true | _ -> false 19 | let isSliderMultiplier = function SliderMultiplier _ -> true | _ -> false 20 | let isSliderTickRate = function SliderTickRate _ -> true | _ -> false 21 | let isComment = function Comment _ -> true | _ -> false 22 | let getHPDrainRate = function HPDrainRate x -> x | _ -> 0M 23 | let getCircleSize = function CircleSize x -> x | _ -> 0M 24 | let getOverallDifficulty = function OverallDifficulty x -> x | _ -> 0M 25 | let getApproachRate = function ApproachRate x -> x | _ -> 0M 26 | let getSliderMultiplier = function SliderMultiplier x -> x | _ -> 0M 27 | let getSliderTickRate = function SliderTickRate x -> x | _ -> 0M 28 | let getComment = function Comment x -> x | _ -> "" 29 | 30 | let tryParseDifficultyOption line : DifficultySetting option = 31 | match line with 32 | | Regex @"(.+?)\s?:\s?(.*)" [key; value] -> 33 | match key with 34 | | "HPDrainRate" -> Some(HPDrainRate( decimal value )) 35 | | "CircleSize" -> Some(CircleSize( decimal value )) 36 | | "OverallDifficulty" -> Some(OverallDifficulty( decimal value )) 37 | | "ApproachRate" -> Some(ApproachRate( decimal value )) 38 | | "SliderMultiplier" -> Some(SliderMultiplier( decimal value )) 39 | | "SliderTickRate" -> Some(SliderTickRate( decimal value )) 40 | | _ -> Some(Comment(line)) 41 | | _ -> Some(Comment(line)) 42 | 43 | let difficultySettingToString ds = 44 | match ds with 45 | | HPDrainRate hp -> sprintf "HPDrainRate:%M" hp 46 | | CircleSize cs -> sprintf "CircleSize:%M" cs 47 | | OverallDifficulty od -> sprintf "OverallDifficulty:%M" od 48 | | ApproachRate ar -> sprintf "ApproachRate:%M" ar 49 | | SliderMultiplier sm -> sprintf "SliderMultiplier:%M" sm 50 | | SliderTickRate tick -> sprintf "SliderTickRate:%M" tick 51 | | Comment comment -> comment 52 | 53 | 54 | let parseDifficultySection : string list -> DifficultySetting list = parseSectionUsing tryParseDifficultyOption 55 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/TimingPoints.fs: -------------------------------------------------------------------------------- 1 | module TimingPoints 2 | 3 | open JunUtils 4 | 5 | 6 | type Tp = 7 | { 8 | time : int; 9 | beatLength : decimal; 10 | meter : int; 11 | sampleSet : int; 12 | sampleIndex : int; 13 | volume : int; 14 | uninherited : bool; 15 | effects : int; 16 | } 17 | 18 | type TimingPoint = 19 | | TimingPoint of Tp 20 | | Comment of string 21 | 22 | let isTimingPoint = function TimingPoint _ -> true | _ -> false 23 | let getTimingPoint = function TimingPoint x -> x | _ -> { time = 0; beatLength = 0M; meter = 0; sampleSet = 0; sampleIndex = 0; volume = 0; uninherited = false; effects = 0; } 24 | let getTimingPointBeatLength = function TimingPoint x -> x.beatLength | _ -> 0M 25 | 26 | let isNotTimingPointComment hobj = 27 | match hobj with 28 | | Comment _ -> false 29 | | _ -> true 30 | 31 | let removeTimingPointComments (objs:list) = (List.filter isNotTimingPointComment objs) 32 | 33 | // timing point syntax: 34 | // time,beatLength,meter,sampleSet,sampleIndex,volume,uninherited,effects 35 | let tryParseTimingPoint line : TimingPoint option = 36 | 37 | let values = parseCsv line 38 | //printfn "Parsing timing point: '%A'" values 39 | match values with 40 | | [Decimal t; Decimal bl; Int m; Int ss; Int si; Int v; Bool ui; Int fx] -> 41 | //printfn "%s" line 42 | //printfn "matches: [Decimal t; Decimal bl; Int m; Int ss; Int si; Int v; Bool ui; Int fx]" 43 | //printfn "" 44 | Some(TimingPoint({ 45 | time = int t; // some maps save this as decimal... 46 | beatLength = bl; 47 | meter = m; 48 | sampleSet = ss; 49 | sampleIndex = si; 50 | volume = v; 51 | uninherited = ui; 52 | effects = fx; 53 | })) 54 | | [Decimal t; Decimal bl; Int m; Int ss; Int si; Int v] -> // v5 doesn't have inherited timing points or effects 55 | //printfn "%s" line 56 | //printfn "matches: [Decimal t; Decimal bl; Int m; Int ss; Int si; Int v]" 57 | //printfn "" 58 | Some(TimingPoint({ 59 | time = int t; // some maps save this as decimal... 60 | beatLength = bl; 61 | meter = m; 62 | sampleSet = ss; 63 | sampleIndex = si; 64 | volume = v; 65 | uninherited = true; 66 | effects = 0; 67 | })) 68 | | _ -> 69 | //printfn "Unrecognized timing point: '%s'" line 70 | Some(Comment(line)) 71 | 72 | let timingPointToString tp = 73 | match tp with 74 | | TimingPoint tp -> sprintf "%d,%M,%d,%d,%d,%d,%d,%d" tp.time tp.beatLength tp.meter tp.sampleSet tp.sampleIndex tp.volume (if tp.uninherited then 1 else 0) tp.effects 75 | | Comment c -> c 76 | 77 | let parseTimingPointSection : string list -> TimingPoint list = parseSectionUsing tryParseTimingPoint 78 | -------------------------------------------------------------------------------- /Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace circle_tracker.Properties 12 | { 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources 26 | { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Resources() 34 | { 35 | } 36 | 37 | /// 38 | /// Returns the cached ResourceManager instance used by this class. 39 | /// 40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 41 | internal static global::System.Resources.ResourceManager ResourceManager 42 | { 43 | get 44 | { 45 | if ((resourceMan == null)) 46 | { 47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("circle_tracker.Properties.Resources", typeof(Resources).Assembly); 48 | resourceMan = temp; 49 | } 50 | return resourceMan; 51 | } 52 | } 53 | 54 | /// 55 | /// Overrides the current thread's CurrentUICulture property for all 56 | /// resource lookups using this strongly typed resource class. 57 | /// 58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 59 | internal static global::System.Globalization.CultureInfo Culture 60 | { 61 | get 62 | { 63 | return resourceCulture; 64 | } 65 | set 66 | { 67 | resourceCulture = value; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/Events.fs: -------------------------------------------------------------------------------- 1 | module Events 2 | 3 | open JunUtils 4 | open System 5 | 6 | type Background = 7 | { 8 | startTime : int; 9 | filename : string; 10 | xOffset : int; 11 | yOffset : int; 12 | } 13 | 14 | type Video = 15 | { 16 | startTime : int; 17 | filename : string; 18 | xOffset : int; 19 | yOffset : int; 20 | } 21 | 22 | type Break = 23 | { 24 | startTime : int; 25 | endTime : int; 26 | } 27 | 28 | type BeatmapEvent = 29 | | Background of Background 30 | | Video of Video 31 | | Break of Break 32 | | Comment of String 33 | 34 | let isBackground = function Background _ -> true | _ -> false 35 | let getBackgroundFilename = function Background x -> x.filename | _ -> "" 36 | 37 | // Background syntax: 0,0,filename,xOffset,yOffset 38 | let tryParseBackground vals : Background option = 39 | match vals with 40 | | ["0"; _; f; Int x; Int y] -> 41 | Some({ 42 | Background.startTime = 0; 43 | filename = f; 44 | xOffset = x; 45 | yOffset = y; 46 | }) 47 | | ["0"; _; f;] -> 48 | Some({ 49 | Background.startTime = 0; 50 | filename = f; 51 | xOffset = 0; 52 | yOffset = 0; 53 | }) 54 | | _ -> None 55 | 56 | 57 | // Video syntax: Video,startTime,filename,xOffset,yOffset 58 | let tryParseVideo vals : Video option = 59 | match vals with 60 | | ["1"; Int s; f; Int x; Int y] | ["Video"; Int s; f; Int x; Int y] -> 61 | Some({ 62 | Video.startTime = s; 63 | filename = f; 64 | xOffset = x; 65 | yOffset = y; 66 | }) 67 | | _ -> None 68 | 69 | 70 | // Break syntax: 2,startTime,endTime 71 | let tryParseBreak vals : Break option = 72 | match vals with 73 | | ["2"; Int s; Int e;] | ["Break"; Int s; Int e;] -> 74 | Some({ 75 | startTime = s; 76 | endTime = e; 77 | }) 78 | | _ -> None 79 | 80 | 81 | let tryParseEvent line : BeatmapEvent option = 82 | let values = parseCsv line 83 | match values.[0] with 84 | 85 | // Background syntax: 0,0,filename,xOffset,yOffset 86 | | "0" -> 87 | match tryParseBackground values with 88 | | Some(bg) -> Some(Background(bg)) 89 | | _ -> Some(Comment(line)) 90 | 91 | // Video syntax: Video,startTime,filename,xOffset,yOffset 92 | | "1" | "Video" -> 93 | match tryParseVideo values with 94 | | Some(bg) -> Some(Video(bg)) 95 | | _ -> Some(Comment(line)) 96 | 97 | // Break syntax: 2,startTime,endTime 98 | | "2" -> 99 | match tryParseBreak values with 100 | | Some(br) -> Some(Break(br)) 101 | | _ -> Some(Comment(line)) 102 | 103 | | _ -> Some(Comment(line)) 104 | 105 | let eventToString ev = 106 | match ev with 107 | | Background bg -> sprintf "0,0,\"%s\",%d,%d" bg.filename bg.xOffset bg.yOffset 108 | | Video vid -> sprintf "Video,%d,\"%s\"" vid.startTime vid.filename 109 | | Break br -> sprintf "2,%d,%d" br.startTime br.endTime 110 | | Comment comment -> comment 111 | 112 | let parseEventSection : string list -> BeatmapEvent list = parseSectionUsing tryParseEvent 113 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/Metadata.fs: -------------------------------------------------------------------------------- 1 | module Metadata 2 | 3 | open JunUtils 4 | open System 5 | 6 | type MetadataInfo = 7 | | Title of string 8 | | TitleUnicode of string 9 | | Artist of string 10 | | ArtistUnicode of string 11 | | Creator of string 12 | | Version of string 13 | | Source of string 14 | | SearchTerms of list // Tags 15 | | BeatmapID of int 16 | | BeatmapSetID of int 17 | | Comment of string 18 | 19 | // isn't there an easier way to do this... 20 | let isTitle = function Title _ -> true | _ -> false 21 | let isTitleUnicode = function TitleUnicode _ -> true | _ -> false 22 | let isArtist = function Artist _ -> true | _ -> false 23 | let isArtistUnicode = function ArtistUnicode _ -> true | _ -> false 24 | let isCreator = function Creator _ -> true | _ -> false 25 | let isVersion = function Version _ -> true | _ -> false 26 | let isSource = function Source _ -> true | _ -> false 27 | let isSearchTerms = function SearchTerms _ -> true | _ -> false 28 | let isBeatmapID = function BeatmapID _ -> true | _ -> false 29 | let isBeatmapSetID = function BeatmapSetID _ -> true | _ -> false 30 | let isComment = function Comment _ -> true | _ -> false 31 | let getTitle = function Title x -> x | _ -> "" 32 | let getTitleUnicode = function TitleUnicode x -> x | _ -> "" 33 | let getArtist = function Artist x -> x | _ -> "" 34 | let getArtistUnicode = function ArtistUnicode x -> x | _ -> "" 35 | let getCreator = function Creator x -> x | _ -> "" 36 | let getVersion = function Version x -> x | _ -> "" 37 | let getSource = function Source x -> x | _ -> "" 38 | let getSearchTerms = function SearchTerms x -> x | _ -> [] 39 | let getBeatmapID = function BeatmapID x -> x | _ -> 0 40 | let getBeatmapSetID = function BeatmapSetID x -> x | _ -> 0 41 | let getComment = function Comment x -> x | _ -> "" 42 | 43 | let tryParseMetadataField line : MetadataInfo option = 44 | match line with 45 | | Regex @"(.+?)\s?:\s?(.*)" [key; value] -> 46 | match key with 47 | | "Title" -> Some(Title( value )) 48 | | "TitleUnicode" -> Some(TitleUnicode( value )) 49 | | "Artist" -> Some(Artist( value )) 50 | | "ArtistUnicode" -> Some(ArtistUnicode( value )) 51 | | "Creator" -> Some(Creator( value )) 52 | | "Version" -> Some(Version( value )) 53 | | "Source" -> Some(Source( value )) 54 | | "Tags" -> Some(SearchTerms( parseSpaceSeparatedList value )) 55 | | "BeatmapID" -> Some(BeatmapID( int value )) 56 | | "BeatmapSetID" -> Some(BeatmapSetID( int value )) 57 | | _ -> Some(Comment(line)) 58 | | _ -> Some(Comment(line)) 59 | 60 | let metadataToString m = 61 | match m with 62 | | Title t -> sprintf "Title:%s" t 63 | | TitleUnicode t -> sprintf "TitleUnicode:%s" t 64 | | Artist a -> sprintf "Artist:%s" a 65 | | ArtistUnicode a -> sprintf "ArtistUnicode:%s" a 66 | | Creator c -> sprintf "Creator:%s" c 67 | | Version v -> sprintf "Version:%s" v 68 | | Source s -> sprintf "Source:%s" s 69 | | SearchTerms s -> sprintf "Tags:%s" (String.Join(" ", s)) 70 | | BeatmapID b -> sprintf "BeatmapID:%d" b 71 | | BeatmapSetID b -> sprintf "BeatmapSetID:%d" b 72 | | Comment c -> c 73 | 74 | let parseMetadataSection : string list -> MetadataInfo list = parseSectionUsing tryParseMetadataField 75 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/JunUtils.fs: -------------------------------------------------------------------------------- 1 | module JunUtils 2 | 3 | open System 4 | open System.Text.RegularExpressions 5 | open System.Globalization 6 | 7 | let tryParseWith (tryParseFunc: string -> bool * _) = tryParseFunc >> function 8 | | true, v -> Some v 9 | | false, _ -> None 10 | 11 | let isTypeOf (tryParseFunc: string -> bool * _) = tryParseFunc >> function 12 | | true, v -> true 13 | | false, _ -> false 14 | 15 | let tryParseInt = tryParseWith System.Int32.TryParse 16 | let tryParseSingle = tryParseWith (fun num -> System.Single.TryParse(num, NumberStyles.AllowDecimalPoint ||| NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture) ) 17 | let tryParseDouble = tryParseWith (fun num -> System.Double.TryParse(num, NumberStyles.AllowDecimalPoint ||| NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture) ) 18 | let tryParseDecimal = tryParseWith (fun num -> System.Decimal.TryParse(num, NumberStyles.AllowDecimalPoint ||| NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture) ) 19 | let tryParseBool input = 20 | match input with 21 | | "true" -> Some(true) 22 | | "false" -> Some(false) 23 | | "1" -> Some(true) 24 | | "0" -> Some(false) 25 | | _ -> None 26 | 27 | // active patterns for converting strings to other data types 28 | let (|Int|_|) = tryParseInt 29 | let (|Single|_|) = tryParseSingle 30 | let (|Double|_|) = tryParseDouble 31 | let (|Bool|_|) = tryParseBool 32 | let (|Decimal|_|) = tryParseDecimal 33 | 34 | let isInt = isTypeOf System.Int32.TryParse 35 | let isSingle = isTypeOf System.Single.TryParse 36 | let isDouble = isTypeOf System.Double.TryParse 37 | let isDecimal = isTypeOf System.Decimal.TryParse 38 | let isBool = isTypeOf System.Boolean.TryParse 39 | 40 | let toBool str = 41 | match str with 42 | | "1" -> true 43 | | _ -> false 44 | 45 | // other parsing functions 46 | let (|Regex|_|) pattern input = 47 | let m = Regex.Match(input, pattern) 48 | if m.Success then Some(List.tail [ for g in m.Groups -> g.Value ]) 49 | else None 50 | 51 | let removeOuterQuotes (s:string) = 52 | match s with 53 | | Regex "^\"(.+)\"$" [inquotes] -> inquotes 54 | | _ -> s 55 | 56 | let split separator (s:string) = 57 | let quoteCount = (Seq.filter ((=) '"') s) |> Seq.length 58 | let fixedStr = 59 | match quoteCount % 2 with 60 | | 1 -> s.Replace("\"", "") 61 | | _ -> s 62 | let values = ResizeArray<_>() 63 | let rec gather start i = 64 | let add () = fixedStr.Substring(start,i-start) |> values.Add 65 | if i = fixedStr.Length then add() 66 | elif fixedStr.[i] = '"' then inQuotes start (i+1) 67 | elif fixedStr.[i] = separator then add(); gather (i+1) (i+1) 68 | else gather start (i+1) 69 | and inQuotes start i = 70 | if fixedStr.[i] = '"' then gather start (i+1) 71 | else inQuotes start (i+1) 72 | gather 0 0 73 | 74 | values.ToArray() 75 | |> Array.toList 76 | |> List.map removeOuterQuotes 77 | 78 | let parseCsv = split ',' 79 | let parseSpaceSeparatedList = split ' ' 80 | 81 | let tryParseCsvInt (str:string) : list option = 82 | let items = parseCsv str 83 | let validInts = List.fold (fun acc cur -> acc && (isInt cur)) true items 84 | match validInts with 85 | | true -> Some(List.map int items) 86 | | false -> None 87 | 88 | 89 | // check if a list of strings match the expected types when casted 90 | // actually don't need this... can just active pattern match on the list 91 | let rec typesMatch vals types : bool = 92 | match types with 93 | | t::ts -> 94 | match t with 95 | | "int" -> 96 | match vals with 97 | | v::vs -> 98 | if (isInt v) 99 | then (typesMatch vs ts) // keep going!!! 100 | else false 101 | | _ -> false // out of values? 102 | | "decimal" -> 103 | match vals with 104 | | v::vs -> 105 | if (isDecimal v) 106 | then (typesMatch vs ts) // keep going!!! 107 | else false 108 | | _ -> false // out of values? 109 | | _ -> 110 | match vals with 111 | | v::vs -> (typesMatch vs ts) // keep going!!! 112 | | _ -> false // out of values? 113 | | [] -> true // reached end, all checks passed 114 | 115 | 116 | // print functions 117 | let parseError obj = 118 | printfn "Error parsing %A" obj 119 | None 120 | 121 | // parse an entire section 122 | let parseSectionUsing parserfn lines = 123 | let rec parseSectionUsing' parserfn lines result = 124 | match lines with 125 | | head::tail -> 126 | match parserfn head with 127 | | Some(data) -> parseSectionUsing' parserfn tail (result @ [data]) 128 | | None -> parseSectionUsing' parserfn tail result 129 | | [] -> result 130 | parseSectionUsing' parserfn lines [] 131 | 132 | let multiply (i:int) (d:decimal) = int ( (decimal i) * d ) 133 | let divide (i:int) (d:decimal) = int ( (decimal i) / d ) 134 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/HitObjects.fs: -------------------------------------------------------------------------------- 1 | module HitObjects 2 | 3 | open System 4 | open JunUtils 5 | 6 | type ObjNoEndTime = 7 | { 8 | x : int; 9 | y : int; 10 | time : int; 11 | typeval : int; 12 | hitSound : int; 13 | remainder : string; // just don't bother trying to parse everything else 14 | } 15 | 16 | type ObjWithEndTime = 17 | { 18 | x : int; 19 | y : int; 20 | time : int; 21 | typeval : int; 22 | hitSound : int; 23 | endTime : int; 24 | remainder : string; // just don't bother trying to parse everything else 25 | } 26 | 27 | 28 | type HitObject = 29 | | HitCircle of ObjNoEndTime 30 | | Slider of ObjNoEndTime 31 | | Spinner of ObjWithEndTime 32 | | Hold of ObjWithEndTime 33 | | Comment of string 34 | 35 | let isNotHitObjectComment hobj = 36 | match hobj with 37 | | Comment _ -> false 38 | | _ -> true 39 | 40 | let removeHitObjectComments (objs:list) = (List.filter isNotHitObjectComment objs) 41 | 42 | 43 | let tryParseObjNoEndTime vals : ObjNoEndTime option = 44 | match vals with 45 | | x::y::ti::ty::hs::rest -> 46 | if (typesMatch [x;y;ti;ty;hs] ["int";"int";"int";"int";"int"]) then 47 | Some({ 48 | x = int x; 49 | y = int y; 50 | time = int ti; 51 | typeval = int ty; 52 | hitSound = int hs; 53 | remainder = String.Join(",", rest); 54 | }) 55 | else parseError vals 56 | | _ -> None 57 | 58 | 59 | let tryParseSpinner vals : ObjWithEndTime option = 60 | match vals with 61 | | x::y::ti::ty::hs::et::rest -> 62 | if (typesMatch [x;y;ti;ty;hs;et] ["int";"int";"int";"int";"int";"int"]) then 63 | Some({ 64 | x = int x; 65 | y = int y; 66 | time = int ti; 67 | typeval = int ty; 68 | endTime = int et; 69 | hitSound = int hs; 70 | remainder = String.Join(",", rest); 71 | }) 72 | else parseError vals 73 | | _ -> None 74 | 75 | 76 | let tryParseHold vals : ObjWithEndTime option = 77 | match vals with 78 | | x::y::ti::ty::hs::endtimeHitsample::rest -> 79 | if (typesMatch [x;y;ti;ty;hs] ["int";"int";"int";"int";"int"]) then 80 | match endtimeHitsample with 81 | | Regex "^(\d+):(.+)" [endtime; remainder] -> 82 | Some({ 83 | x = int x; 84 | y = int y; 85 | time = int ti; 86 | typeval = int ty; 87 | hitSound = int hs; 88 | endTime = int endtime; 89 | remainder = remainder + String.Join(",", rest); 90 | }) 91 | | _ -> parseError vals 92 | else parseError vals 93 | | _ -> None 94 | 95 | 96 | let tryParseHitObject line : HitObject option = 97 | let vals = parseCsv line 98 | match vals with 99 | | _::_::_::typeval::rest -> 100 | match int typeval with 101 | 102 | // bit 0 high => HitCircle 103 | | typebyte when (typebyte &&& 1) <> 0 -> 104 | match tryParseObjNoEndTime vals with 105 | | Some(obj) -> Some(HitCircle(obj)) 106 | | None -> Some(Comment(line)) 107 | 108 | // bit 1 high => HitCircle 109 | | typebyte when (typebyte &&& 2) <> 0 -> 110 | match tryParseObjNoEndTime vals with 111 | | Some(obj) -> Some(Slider(obj)) 112 | | None -> Some(Comment(line)) 113 | 114 | // bit 3 high => Spinner 115 | | typebyte when (typebyte &&& 8) <> 0 -> 116 | match tryParseSpinner vals with 117 | | Some(obj) -> Some(Spinner(obj)) 118 | | None -> Some(Comment(line)) 119 | 120 | // bit 7 high => osu mania hold 121 | | typebyte when (typebyte &&& 128) <> 0 -> 122 | match tryParseHold vals with 123 | | Some(obj) -> Some(Hold(obj)) 124 | | None -> Some(Comment(line)) 125 | 126 | | _ -> Some(Comment(line)) 127 | | _ -> Some(Comment(line)) 128 | 129 | let commaString r = 130 | if r = "" 131 | then "" 132 | else "," + r 133 | 134 | let hitObjectToString obj = 135 | match obj with 136 | | HitCircle c -> sprintf "%d,%d,%d,%d,%d%s" c.x c.y c.time c.typeval c.hitSound (commaString c.remainder) 137 | | Slider s -> sprintf "%d,%d,%d,%d,%d%s" s.x s.y s.time s.typeval s.hitSound (commaString s.remainder) 138 | | Spinner s -> sprintf "%d,%d,%d,%d,%d,%d%s" s.x s.y s.time s.typeval s.hitSound s.endTime (commaString s.remainder) 139 | | Hold h -> sprintf "%d,%d,%d,%d,%d,%d:%s" h.x h.y h.time h.typeval h.hitSound h.endTime h.remainder 140 | | Comment comment -> comment 141 | 142 | let parseHitObjectSection : string list -> HitObject list = parseSectionUsing tryParseHitObject 143 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/BeatmapProcessor.fs: -------------------------------------------------------------------------------- 1 | module BeatmapParser 2 | 3 | open System.IO 4 | open JunUtils 5 | open General 6 | open Editor 7 | open Metadata 8 | open Difficulty 9 | open Events 10 | open TimingPoints 11 | open Colours 12 | open HitObjects 13 | 14 | type BeatmapInternalRepresentation = 15 | { 16 | general : GeneralInfo list; 17 | editor : EditorSetting list; 18 | metadata : MetadataInfo list; 19 | difficulty : DifficultySetting list; 20 | events : BeatmapEvent list; 21 | timingPoints : TimingPoint list; 22 | colours : ColourSetting list; 23 | hitObjects : HitObject list; 24 | } 25 | 26 | type Section = 27 | | General 28 | | Editor 29 | | Metadata 30 | | Difficulty 31 | | Events 32 | | TimingPoints 33 | | Colours 34 | | HitObjects 35 | 36 | let isSectionHeader line = 37 | match line with 38 | | Regex "^\[(.+)\]" [header] -> 39 | match header with 40 | | "General" -> true 41 | | "Editor" -> true 42 | | "Metadata" -> true 43 | | "Difficulty" -> true 44 | | "Events" -> true 45 | | "TimingPoints" -> true 46 | | "Colours" -> true 47 | | "HitObjects" -> true 48 | | _ -> false 49 | | _ -> false 50 | 51 | 52 | let splitSections (fileLines:list) : list> = 53 | 54 | // get list of line numbers where a section header is placed 55 | let mutable headerIndices = [] 56 | for i = 0 to (fileLines.Length - 1) do 57 | if isSectionHeader fileLines.[i] then 58 | headerIndices <- headerIndices @ [i] 59 | 60 | let rec getSections (sectionDividers:list) (remainingLines:list) : list> = 61 | match sectionDividers with 62 | | sd1::sd2::sds -> 63 | remainingLines.[sd1..sd2-1] :: (getSections (sd2::sds) remainingLines) 64 | | [lastsd] -> [remainingLines.[lastsd..]] 65 | | _ -> [] // file contains no sections headers? 66 | 67 | getSections headerIndices fileLines 68 | 69 | 70 | let whichSection (sectionLines:list) : Section = 71 | assert (sectionLines.Length > 0) 72 | match sectionLines.[0] with 73 | | "[General]" -> General 74 | | "[Editor]" -> Editor 75 | | "[Metadata]" -> Metadata 76 | | "[Difficulty]" -> Difficulty 77 | | "[Events]" -> Events 78 | | "[TimingPoints]" -> TimingPoints 79 | | "[Colours]" -> Colours 80 | | "[HitObjects]" -> HitObjects 81 | | _ -> assert false; General // should never happen 82 | 83 | 84 | let parseSections (sections: list> ) = 85 | 86 | let headerIs headerName (section: string list ) = 87 | section.[0] = (sprintf "[%s]" headerName) 88 | 89 | let consolidatedSections = 90 | [ 91 | sections |> List.filter (headerIs "General") |> List.concat; 92 | sections |> List.filter (headerIs "Editor") |> List.concat; 93 | sections |> List.filter (headerIs "Metadata") |> List.concat; 94 | sections |> List.filter (headerIs "Difficulty") |> List.concat; 95 | sections |> List.filter (headerIs "Events") |> List.concat; 96 | sections |> List.filter (headerIs "TimingPoints") |> List.concat; 97 | sections |> List.filter (headerIs "Colours") |> List.concat; 98 | sections |> List.filter (headerIs "HitObjects") |> List.concat; 99 | ] 100 | let ret = { 101 | general = parseGeneralSection consolidatedSections.[0]; 102 | editor = parseEditorSection consolidatedSections.[1]; 103 | metadata = parseMetadataSection consolidatedSections.[2]; 104 | difficulty = parseDifficultySection consolidatedSections.[3]; 105 | events = parseEventSection consolidatedSections.[4]; 106 | timingPoints = parseTimingPointSection consolidatedSections.[5]; 107 | colours = parseColourSection consolidatedSections.[6]; 108 | hitObjects = parseHitObjectSection consolidatedSections.[7]; 109 | } 110 | 111 | let tps = ret.timingPoints |> List.filter isTimingPoint 112 | let comments = ret.timingPoints |> List.filter (fun x -> not (isTimingPoint x)) 113 | 114 | //printfn "################################################" 115 | //printfn "Parsed Timing Points:" 116 | //printfn "TimingPoints: %d" (List.length tps) 117 | //List.iter (fun tp -> printfn "%s" (timingPointToString tp)) tps 118 | //printfn "\n" 119 | //printfn "Comments: %d" (List.length comments) 120 | //List.iter (fun comment -> printfn "'%s'" (timingPointToString comment)) comments 121 | //printfn "################################################" 122 | //printfn "" 123 | ret 124 | 125 | 126 | 127 | let parseBeatmapFile filename = 128 | //printfn "################################################" 129 | //printfn "" 130 | //printfn "Parsing file:" 131 | //printfn "%s" filename 132 | //printfn "" 133 | //printfn "################################################" 134 | 135 | File.ReadAllLines filename 136 | |> Array.toList 137 | |> splitSections 138 | |> parseSections 139 | 140 | 141 | -------------------------------------------------------------------------------- /circle-tracker.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30105.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "circle-tracker", "circle-tracker.csproj", "{862FF93D-5032-4E99-9A96-C4624FEC158D}" 7 | ProjectSection(ProjectDependencies) = postProject 8 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E} = {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E} 9 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC} = {044EE1F2-8090-4E91-962D-17D8CFA6B9FC} 10 | EndProjectSection 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OsuMemoryDataProvider", "submodules\ProcessMemoryDataFinder\OsuMemoryDataProvider\OsuMemoryDataProvider.csproj", "{044EE1F2-8090-4E91-962D-17D8CFA6B9FC}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProcessMemoryDataFinder", "submodules\ProcessMemoryDataFinder\ProcessMemoryDataFinder\ProcessMemoryDataFinder.csproj", "{1EB11518-95EC-41FA-97EA-D74F4F76610F}" 15 | EndProject 16 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "FsBeatmapProcessor", "submodules\FsBeatmapParser\FsBeatmapProcessor.fsproj", "{2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|x64 = Release|x64 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Debug|x64.ActiveCfg = Debug|Any CPU 31 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Debug|x64.Build.0 = Debug|Any CPU 32 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Debug|x86.ActiveCfg = Debug|Any CPU 33 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Debug|x86.Build.0 = Debug|Any CPU 34 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Release|x64.ActiveCfg = Release|Any CPU 37 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Release|x64.Build.0 = Release|Any CPU 38 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Release|x86.ActiveCfg = Release|Any CPU 39 | {862FF93D-5032-4E99-9A96-C4624FEC158D}.Release|x86.Build.0 = Release|Any CPU 40 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC}.Debug|Any CPU.ActiveCfg = Debug|x86 41 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC}.Debug|Any CPU.Build.0 = Debug|x86 42 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC}.Debug|x64.ActiveCfg = Debug|x86 43 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC}.Debug|x86.ActiveCfg = Debug|x86 44 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC}.Debug|x86.Build.0 = Debug|x86 45 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC}.Release|Any CPU.ActiveCfg = Release|x86 46 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC}.Release|Any CPU.Build.0 = Release|x86 47 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC}.Release|x64.ActiveCfg = Release|x86 48 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC}.Release|x86.ActiveCfg = Release|x86 49 | {044EE1F2-8090-4E91-962D-17D8CFA6B9FC}.Release|x86.Build.0 = Release|x86 50 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Debug|Any CPU.ActiveCfg = Debug|x86 51 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Debug|Any CPU.Build.0 = Debug|x86 52 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Debug|x64.ActiveCfg = Debug|x64 53 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Debug|x64.Build.0 = Debug|x64 54 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Debug|x86.ActiveCfg = Debug|x86 55 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Debug|x86.Build.0 = Debug|x86 56 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Release|Any CPU.ActiveCfg = Release|x86 57 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Release|Any CPU.Build.0 = Release|x86 58 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Release|x64.ActiveCfg = Release|x64 59 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Release|x64.Build.0 = Release|x64 60 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Release|x86.ActiveCfg = Release|x86 61 | {1EB11518-95EC-41FA-97EA-D74F4F76610F}.Release|x86.Build.0 = Release|x86 62 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Debug|x64.ActiveCfg = Debug|Any CPU 65 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Debug|x64.Build.0 = Debug|Any CPU 66 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Debug|x86.ActiveCfg = Debug|Any CPU 67 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Debug|x86.Build.0 = Debug|Any CPU 68 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Release|Any CPU.Build.0 = Release|Any CPU 70 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Release|x64.ActiveCfg = Release|Any CPU 71 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Release|x64.Build.0 = Release|Any CPU 72 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Release|x86.ActiveCfg = Release|Any CPU 73 | {2E3CEE47-B8C3-42E4-9D32-AC93A019EC1E}.Release|x86.Build.0 = Release|Any CPU 74 | EndGlobalSection 75 | GlobalSection(SolutionProperties) = preSolution 76 | HideSolutionNode = FALSE 77 | EndGlobalSection 78 | GlobalSection(ExtensibilityGlobals) = postSolution 79 | SolutionGuid = {91E49ABA-0503-4F89-96E2-468C67663CEB} 80 | EndGlobalSection 81 | EndGlobal 82 | -------------------------------------------------------------------------------- /Properties/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /Updater.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Win32; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Net.Http; 7 | using System.Runtime.InteropServices; 8 | using System.Text; 9 | using System.Windows.Forms; 10 | 11 | namespace Circle_Tracker 12 | { 13 | #region Generated JSON Classes 14 | public class Author 15 | { 16 | public string login { get; set; } 17 | public int id { get; set; } 18 | public string node_id { get; set; } 19 | public string avatar_url { get; set; } 20 | public string gravatar_id { get; set; } 21 | public string url { get; set; } 22 | public string html_url { get; set; } 23 | public string followers_url { get; set; } 24 | public string following_url { get; set; } 25 | public string gists_url { get; set; } 26 | public string starred_url { get; set; } 27 | public string subscriptions_url { get; set; } 28 | public string organizations_url { get; set; } 29 | public string repos_url { get; set; } 30 | public string events_url { get; set; } 31 | public string received_events_url { get; set; } 32 | public string type { get; set; } 33 | public bool site_admin { get; set; } 34 | 35 | } 36 | 37 | public class Uploader 38 | { 39 | public string login { get; set; } 40 | public int id { get; set; } 41 | public string node_id { get; set; } 42 | public string avatar_url { get; set; } 43 | public string gravatar_id { get; set; } 44 | public string url { get; set; } 45 | public string html_url { get; set; } 46 | public string followers_url { get; set; } 47 | public string following_url { get; set; } 48 | public string gists_url { get; set; } 49 | public string starred_url { get; set; } 50 | public string subscriptions_url { get; set; } 51 | public string organizations_url { get; set; } 52 | public string repos_url { get; set; } 53 | public string events_url { get; set; } 54 | public string received_events_url { get; set; } 55 | public string type { get; set; } 56 | public bool site_admin { get; set; } 57 | 58 | } 59 | 60 | public class Asset 61 | { 62 | public string url { get; set; } 63 | public int id { get; set; } 64 | public string node_id { get; set; } 65 | public string name { get; set; } 66 | public object label { get; set; } 67 | public Uploader uploader { get; set; } 68 | public string content_type { get; set; } 69 | public string state { get; set; } 70 | public int size { get; set; } 71 | public int download_count { get; set; } 72 | public DateTime created_at { get; set; } 73 | public DateTime updated_at { get; set; } 74 | public string browser_download_url { get; set; } 75 | 76 | } 77 | 78 | public class Release 79 | { 80 | public string url { get; set; } 81 | public string assets_url { get; set; } 82 | public string upload_url { get; set; } 83 | public string html_url { get; set; } 84 | public int id { get; set; } 85 | public string node_id { get; set; } 86 | public string tag_name { get; set; } 87 | public string target_commitish { get; set; } 88 | public string name { get; set; } 89 | public bool draft { get; set; } 90 | public Author author { get; set; } 91 | public bool prerelease { get; set; } 92 | public DateTime created_at { get; set; } 93 | public DateTime published_at { get; set; } 94 | public List assets { get; set; } 95 | public string tarball_url { get; set; } 96 | public string zipball_url { get; set; } 97 | public string body { get; set; } 98 | 99 | } 100 | #endregion 101 | 102 | class Updater 103 | { 104 | static readonly string CURRENT_RELEASE_TAG = "v16"; // Jun: REMEMBER TO CHANGE THIS every time you make a new release. 105 | static HttpClient client; 106 | static Updater() 107 | { 108 | client = new HttpClient(); 109 | client.BaseAddress = new Uri(@"https://api.github.com/"); 110 | client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); 111 | client.DefaultRequestHeaders.Add("User-Agent", "Circle-Tracker"); 112 | } 113 | public static async void CheckForUpdates() 114 | { 115 | Release latestRelease = null; 116 | try 117 | { 118 | var response = await client.GetAsync("/repos/FunOrange/circle-tracker/releases/latest"); 119 | if (response.IsSuccessStatusCode) 120 | { 121 | string responseJson = await response.Content.ReadAsStringAsync(); 122 | latestRelease = JsonConvert.DeserializeObject(responseJson); 123 | } 124 | } 125 | catch 126 | { 127 | MessageBox.Show($"Update check failed. You have version {CURRENT_RELEASE_TAG}. Check for updates here: https://github.com/FunOrange/circle-tracker/releases/latest {Environment.NewLine}e.Message", "Error"); 128 | } 129 | 130 | if (latestRelease.tag_name != CURRENT_RELEASE_TAG) 131 | { 132 | var result = MessageBox.Show( 133 | $"Release Notes:{Environment.NewLine}{latestRelease.body}{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}" + 134 | $"Would you like to download it?", 135 | "Update Available!", 136 | MessageBoxButtons.YesNo 137 | ); 138 | if (result == DialogResult.Yes) 139 | { 140 | MessageBox.Show($"Please make sure to move these files over to the new install folder!{Environment.NewLine}{Environment.NewLine}" + 141 | $"credentials.json{Environment.NewLine}" + 142 | $"user_settings.txt{Environment.NewLine}{Environment.NewLine}" + 143 | $"The download will begin after this window is closed."); 144 | Process.Start(latestRelease.assets[0].browser_download_url); 145 | } 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /circle-tracker.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {862FF93D-5032-4E99-9A96-C4624FEC158D} 8 | WinExe 9 | circle_tracker 10 | circle-tracker 11 | v4.7.1 12 | 512 13 | true 14 | true 15 | 16 | 17 | x86 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | ct.ico 37 | 38 | 39 | 40 | packages\Google.Apis.1.49.0\lib\net45\Google.Apis.dll 41 | 42 | 43 | packages\Google.Apis.Auth.1.49.0\lib\net45\Google.Apis.Auth.dll 44 | 45 | 46 | packages\Google.Apis.Auth.1.49.0\lib\net45\Google.Apis.Auth.PlatformServices.dll 47 | 48 | 49 | packages\Google.Apis.Core.1.49.0\lib\net45\Google.Apis.Core.dll 50 | 51 | 52 | packages\Google.Apis.1.49.0\lib\net45\Google.Apis.PlatformServices.dll 53 | 54 | 55 | packages\Google.Apis.Sheets.v4.1.49.0.2069\lib\net45\Google.Apis.Sheets.v4.dll 56 | 57 | 58 | packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Form 76 | 77 | 78 | MainForm.cs 79 | 80 | 81 | 82 | 83 | 84 | 85 | MainForm.cs 86 | 87 | 88 | ResXFileCodeGenerator 89 | Resources.Designer.cs 90 | Designer 91 | 92 | 93 | True 94 | Resources.resx 95 | 96 | 97 | 98 | SettingsSingleFileGenerator 99 | Settings.Designer.cs 100 | 101 | 102 | True 103 | Settings.settings 104 | True 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | Always 114 | 115 | 116 | Always 117 | 118 | 119 | 120 | 121 | 122 | {2e3cee47-b8c3-42e4-9d32-ac93a019ec1e} 123 | FsBeatmapProcessor 124 | 125 | 126 | {044ee1f2-8090-4e91-962d-17d8cfa6b9fc} 127 | OsuMemoryDataProvider 128 | 129 | 130 | 131 | 132 | {F935DC20-1CF0-11D0-ADB9-00C04FD58A0B} 133 | 1 134 | 0 135 | 0 136 | tlbimp 137 | False 138 | True 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/General.fs: -------------------------------------------------------------------------------- 1 | module General 2 | 3 | open JunUtils 4 | 5 | type GeneralInfo = 6 | | AudioFilename of string 7 | | AudioLeadIn of int 8 | | PreviewTime of int 9 | | Countdown of int 10 | | SampleSet of string 11 | | StackLeniency of decimal 12 | | Mode of int 13 | | LetterboxInBreaks of bool 14 | | UseSkinSprites of bool 15 | | OverlayPosition of string 16 | | SkinPreference of string 17 | | EpilepsyWarning of bool 18 | | CountdownOffset of int 19 | | SpecialStyle of bool 20 | | WidescreenStoryboard of bool 21 | | SamplesMatchPlaybackRate of bool 22 | | Comment of string 23 | 24 | // isn't there an easier way to do this... 25 | let isAudioFilename = function AudioFilename _ -> true | _ -> false 26 | let isAudioLeadIn = function AudioLeadIn _ -> true | _ -> false 27 | let isPreviewTime = function PreviewTime _ -> true | _ -> false 28 | let isCountdown = function Countdown _ -> true | _ -> false 29 | let isSampleSet = function SampleSet _ -> true | _ -> false 30 | let isStackLeniency = function StackLeniency _ -> true | _ -> false 31 | let isMode = function Mode _ -> true | _ -> false 32 | let isLetterboxInBreaks = function LetterboxInBreaks _ -> true | _ -> false 33 | let isUseSkinSprites = function UseSkinSprites _ -> true | _ -> false 34 | let isOverlayPosition = function OverlayPosition _ -> true | _ -> false 35 | let isSkinPreference = function SkinPreference _ -> true | _ -> false 36 | let isEpilepsyWarning = function EpilepsyWarning _ -> true | _ -> false 37 | let isCountdownOffset = function CountdownOffset _ -> true | _ -> false 38 | let isSpecialStyle = function SpecialStyle _ -> true | _ -> false 39 | let isWidescreenStoryboard = function WidescreenStoryboard _ -> true | _ -> false 40 | let isSamplesMatchPlaybackRate = function SamplesMatchPlaybackRate _ -> true | _ -> false 41 | let isComment = function Comment _ -> true | _ -> false 42 | let getAudioFilename = function AudioFilename x -> x | _ -> "" 43 | let getAudioLeadIn = function AudioLeadIn x -> x | _ -> 0 44 | let getPreviewTime = function PreviewTime x -> x | _ -> 0 45 | let getCountdown = function Countdown x -> x | _ -> 0 46 | let getSampleSet = function SampleSet x -> x | _ -> "" 47 | let getStackLeniency = function StackLeniency x -> x | _ -> 0M 48 | let getMode = function Mode x -> x | _ -> 0 49 | let getLetterboxInBreaks = function LetterboxInBreaks x -> x | _ -> false 50 | let getUseSkinSprites = function UseSkinSprites x -> x | _ -> false 51 | let getOverlayPosition = function OverlayPosition x -> x | _ -> "" 52 | let getSkinPreference = function SkinPreference x -> x | _ -> "" 53 | let getEpilepsyWarning = function EpilepsyWarning x -> x | _ -> false 54 | let getCountdownOffset = function CountdownOffset x -> x | _ -> 0 55 | let getSpecialStyle = function SpecialStyle x -> x | _ -> false 56 | let getWidescreenStoryboard = function WidescreenStoryboard x -> x | _ -> false 57 | let getSamplesMatchPlaybackRate = function SamplesMatchPlaybackRate x -> x | _ -> false 58 | let getComment = function Comment x -> x | _ -> "" 59 | 60 | let tryParseGeneralInfo line : GeneralInfo option = 61 | match line with 62 | | Regex @"(.+)\s?:\s?(.+)" [key; value] -> 63 | match key with 64 | | "AudioFilename" -> Some(AudioFilename(value)) 65 | | "AudioLeadIn" -> Some(AudioLeadIn(int value)) 66 | | "PreviewTime" -> Some(PreviewTime(int value)) 67 | | "Countdown" -> Some(Countdown(int value)) 68 | | "SampleSet" -> Some(SampleSet(value)) 69 | | "StackLeniency" -> Some(StackLeniency(decimal value)) 70 | | "Mode" -> Some(Mode(int value)) 71 | | "LetterboxInBreaks" -> Some(LetterboxInBreaks(toBool value)) 72 | | "UseSkinSprites" -> Some(UseSkinSprites(toBool value)) 73 | | "OverlayPosition" -> Some(OverlayPosition(value)) 74 | | "SkinPreference" -> Some(SkinPreference(value)) 75 | | "EpilepsyWarning" -> Some(EpilepsyWarning(toBool value)) 76 | | "CountdownOffset" -> Some(CountdownOffset(int value)) 77 | | "SpecialStyle" -> Some(SpecialStyle(toBool value)) 78 | | "WidescreenStoryboard" -> Some(WidescreenStoryboard(toBool value)) 79 | | "SamplesMatchPlaybackRate" -> Some(SamplesMatchPlaybackRate(toBool value)) 80 | | _ -> Some(Comment(line)) 81 | | _ -> Some(Comment(line)) 82 | 83 | let generalInfoToString g = 84 | match g with 85 | | AudioFilename audiofilename -> sprintf "AudioFilename: %s" audiofilename 86 | | AudioLeadIn leadin -> sprintf "AudioLeadIn: %d" leadin 87 | | PreviewTime preview -> sprintf "PreviewTime: %d" preview 88 | | Countdown countdown -> sprintf "Countdown: %d" countdown 89 | | SampleSet ss -> sprintf "SampleSet: %s" ss 90 | | StackLeniency sl -> sprintf "StackLeniency: %M" sl 91 | | Mode mode -> sprintf "Mode: %d" mode 92 | | LetterboxInBreaks lb -> sprintf "LetterboxInBreaks: %d" (if lb then 1 else 0) 93 | | UseSkinSprites usess -> sprintf "UseSkinSprites: %d" (if usess then 1 else 0) 94 | | OverlayPosition overlaypos -> sprintf "OverlayPosition: %s" overlaypos 95 | | SkinPreference sp -> sprintf "SkinPreference: %s" sp 96 | | EpilepsyWarning ew -> sprintf "EpilepsyWarning: %d" (if ew then 1 else 0) 97 | | CountdownOffset co -> sprintf "CountdownOffset: %d" co 98 | | SpecialStyle ss -> sprintf "SpecialStyle: %d" (if ss then 1 else 0) 99 | | WidescreenStoryboard ws -> sprintf "WidescreenStoryboard: %d" (if ws then 1 else 0) 100 | | SamplesMatchPlaybackRate smp -> sprintf "SamplesMatchPlaybackRate: %d" (if smp then 1 else 0) 101 | | Comment comment -> comment 102 | 103 | let parseGeneralSection : string list -> GeneralInfo list = parseSectionUsing tryParseGeneralInfo 104 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/Program.fs: -------------------------------------------------------------------------------- 1 | // Learn more about F# at http://fsharp.org 2 | module Program 3 | 4 | (* 5 | open System.IO 6 | open General 7 | open Editor 8 | open Metadata 9 | open Difficulty 10 | open Events 11 | open TimingPoints 12 | open Colours 13 | open HitObjects 14 | open BeatmapParser 15 | open FsBeatmap 16 | 17 | //let exampleGeneral = [ 18 | // "AudioFilename: audio.mp3"; 19 | // "AudioLeadIn: 0"; 20 | // "PreviewTime: 186942"; 21 | // "Countdown: 0"; 22 | // "SampleSet: Normal"; 23 | // "StackLeniency: 0.3"; 24 | // "Mode: 0"; 25 | // "LetterboxInBreaks: 0"; 26 | // "WidescreenStoryboard: 0"; 27 | //] 28 | 29 | //let exampleEditor = [ 30 | // "Bookmarks: 71327,179557,191442,192711,239327"; 31 | // "DistanceSpacing: 1"; 32 | // "BeatDivisor: 4"; 33 | // "GridSize: 16"; 34 | // "TimelineZoom: 3.099998"; 35 | //] 36 | 37 | //let exampleMetadata = [ 38 | // "Title:Quicksand"; 39 | // "TitleUnicode:Quicksand"; 40 | // "Artist:Camellia"; 41 | // "ArtistUnicode:かめりあ"; 42 | // "Creator:Sheepcraft"; 43 | // "Version:Liquefaction"; 44 | // "Source:"; 45 | // "Tags:galaxy burst edp cametek"; 46 | // "BeatmapID:1593674"; 47 | // "BeatmapSetID:757566"; 48 | //] 49 | 50 | //let diffOptions = [ 51 | // "HPDrainRate:5"; 52 | // "CircleSize:4.2"; 53 | // "OverallDifficulty:9"; 54 | // "ApproachRate:9.7"; 55 | // "SliderMultiplier:2"; 56 | // "SliderTickRate:1"; 57 | //] 58 | 59 | //let events = [ 60 | // "//Background and Video events"; 61 | // "0,0,\"lolwhyyoureadingthepicturefilename.jpg\",0,0"; 62 | // "//Break Periods"; 63 | // "//Storyboard Layer 0 (Background)"; 64 | // "//Storyboard Layer 1 (Fail)"; 65 | // "//Storyboard Layer 2 (Pass)"; 66 | // "//Storyboard Layer 3 (Foreground)"; 67 | // "//Storyboard Layer 4 (Overlay)"; 68 | // "//Storyboard Sound Samples"; 69 | //] 70 | 71 | //let timingPoints = [ 72 | // "460,600,4,2,1,50,1,0"; 73 | // "2260,-100,4,2,1,50,0,0"; 74 | // "22060,-90.9090909090909,4,2,1,60,0,0"; 75 | // "60460,-71.4285714285714,4,2,1,65,0,0"; 76 | // "67660,-50,4,2,1,65,0,0"; 77 | // "70960,-71.4285714285714,4,2,1,65,0,0"; 78 | // "72460,-52.6315789473684,4,2,1,65,0,0"; 79 | // "74110,-71.4285714285714,4,2,1,65,0,0"; 80 | // "79660,-133.333333333333,4,2,1,65,0,0"; 81 | // "82060,-66.6666666666667,4,2,1,65,0,0"; 82 | // "83860,-55.5555555555556,4,2,1,75,0,0"; 83 | // "84460,-50,4,2,1,75,0,0"; 84 | // "85060,-52.6315789473684,4,2,1,75,0,0"; 85 | // "103660,-52.6315789473684,4,2,1,85,0,0"; 86 | // "119860,-133.333333333333,4,2,1,75,0,0"; 87 | // "120460,-66.6666666666667,4,2,1,85,0,0"; 88 | // "121060,-52.6315789473684,4,2,1,85,0,0"; 89 | // "121660,-57.1428571428571,4,2,1,85,0,0"; 90 | // "122860,-100,4,2,1,75,0,0"; 91 | // "125260,-100,4,2,1,65,0,0"; 92 | // "127660,-100,4,2,1,60,0,0"; 93 | // "146860,-90.9090909090909,4,2,1,60,0,0"; 94 | // "185260,-71.4285714285714,4,2,1,65,0,0"; 95 | // "192460,-50,4,2,1,65,0,0"; 96 | // "195760,-71.4285714285714,4,2,1,65,0,0"; 97 | // "197260,-52.6315789473684,4,2,1,65,0,0"; 98 | // "198910,-100,4,2,1,65,0,0"; 99 | // "199660,-71.4285714285714,4,2,1,65,0,0"; 100 | // "204460,-133.333333333333,4,2,1,65,0,0"; 101 | // "206860,-66.6666666666667,4,2,1,65,0,0"; 102 | // "208660,-52.6315789473684,4,2,1,75,0,0"; 103 | // "228460,-52.6315789473684,4,2,1,85,0,0"; 104 | // "244660,-71.4285714285714,4,2,1,75,0,0"; 105 | // "245260,-52.6315789473684,4,2,1,85,0,0"; 106 | // "247660,-100,4,2,1,65,0,0"; 107 | // "266860,-50,4,2,1,85,0,1"; 108 | // "274060,-66.6666666666667,4,2,1,85,0,1"; 109 | // "274660,-50,4,2,1,85,0,1"; 110 | // "275860,-66.6666666666667,4,2,1,50,0,0"; 111 | // "281260,-133.333333333333,4,2,1,40,0,0"; 112 | // "298060,-100,4,2,1,40,0,0"; 113 | // "302260,-100,4,2,1,5,0,0"; 114 | //] 115 | 116 | //let exampleColours = [ 117 | // "Combo1 : 102,243,78"; 118 | // "Combo2 : 120,211,245"; 119 | // "Combo3 : 244,230,47"; 120 | // "Combo4 : 252,152,244"; 121 | // ""; 122 | //] 123 | 124 | //let exampleHitObjects = [| 125 | // "15,36,2260,5,0,1:0:0:0:"; 126 | // "114,106,2710,1,2,0:0:0:0:"; 127 | // "114,106,2785,1,2,0:0:0:0:"; 128 | // "114,106,2860,2,0,L|60:100,3,40,2|0|0|0,1:0|0:0|3:0|3:0,0:0:0:0:"; 129 | // "315,22,3760,5,0,3:0:0:0:"; 130 | // "315,22,3910,1,0,3:0:0:0:"; 131 | // "360,87,4060,1,2,3:2:0:0:"; 132 | // "296,173,4360,2,0,L|300:220,1,40,0|0,3:0|3:0,0:0:0:0:"; 133 | // "299,212,4960,1,0,3:0:0:0:"; 134 | // "481,321,5260,6,0,L|441:317,1,40,2|0,1:0|0:0,0:0:0:0:"; 135 | // "373,368,5560,1,0,3:0:0:0:"; 136 | // "373,368,5710,2,0,P|311:372|252:359,1,120,0|0,3:0|1:0,0:0:0:0:"; 137 | // "188,313,6310,2,0,L|148:316,1,40,0|2,3:0|1:0,0:0:0:0:"; 138 | // "83,366,6610,1,0,3:0:0:0:"; 139 | // "83,366,6760,1,0,3:0:0:0:"; 140 | // "31,303,6910,1,0,1:0:0:0:"; 141 | // "90,184,7210,2,0,L|95:243,1,40,0|0,3:0|3:0,0:0:0:0:"; 142 | // "260,276,7660,6,0,L|319:270,1,40,2|0,1:0|0:0,0:0:0:0:"; 143 | // "356,213,7960,1,0,3:0:0:0:"; 144 | // "356,213,8110,1,0,3:0:0:0:"; 145 | // "434,336,8560,2,0,P|447:282|439:241,1,80,0|2,3:0|0:0,0:0:0:0:"; 146 | // "277,195,9160,1,0,3:0:0:0:"; 147 | // "277,195,9310,1,0,3:0:0:0:"; 148 | // "436,247,9610,1,0,3:0:0:0:"; 149 | // "444,258,9910,1,0,3:0:0:0:"; 150 | // "501,171,10060,6,0,P|478:159|451:152,1,40,2|0,1:0|3:0,0:0:0:0:"; 151 | // "383,175,10360,1,0,0:0:0:0:"; 152 | // "383,175,10510,2,0,P|361:185|330:190,1,40,0|2,3:0|0:0,0:0:0:0:"; 153 | // "270,132,10810,1,0,3:0:0:0:"; 154 | // "270,132,10960,1,0,1:0:0:0:"; 155 | // "409,16,11260,6,0,P|371:8|332:15,1,80,2|0,0:0|3:0,0:0:0:0:"; 156 | // "255,54,11710,2,0,L|251:14,2,40,0|2|0,1:0|0:0|1:0,0:0:0:0:"; 157 | // "169,40,12160,1,0,3:0:0:0:"; 158 | // "33,108,12460,6,0,P|28:137|26:177,1,40,2|0,1:0|0:0,0:0:0:0:"; 159 | // "72,220,12760,1,0,3:0:0:0:"; 160 | // "72,220,12910,1,0,3:0:0:0:"; 161 | // "12,324,13210,1,0,3:0:0:0:"; 162 | // "12,324,13360,2,0,P|57:346|111:341,1,80,0|2,3:0|0:0,0:0:0:0:"; 163 | // "211,293,13960,1,0,3:0:0:0:"; 164 | // "211,293,14110,1,0,3:0:0:0:"; 165 | // "331,363,14410,1,0,3:0:0:0:"; 166 | // "331,363,14560,1,0,3:0:0:0:"; 167 | // "482,269,14860,6,0,L|476:209,1,40,2|0,1:0|0:0,0:0:0:0:"; 168 | // "421,156,15160,1,0,3:0:0:0:"; 169 | // "421,156,15310,1,0,3:0:0:0:"; 170 | // "474,48,15610,1,0,3:0:0:0:"; 171 | // "474,48,15760,2,0,P|436:32|384:33,1,80,0|2,3:0|0:0,0:0:0:0:"; 172 | // "261,121,16360,1,0,3:0:0:0:"; 173 | // "261,121,16510,2,0,L|256:73,1,40,0|2,3:0|0:0,0:0:0:0:"; 174 | // "181,32,16810,2,0,L|177:71,1,40,0|0,3:0|1:0,0:0:0:0:"; 175 | // "123,131,17110,1,0,3:0:0:0:"; 176 | // "41,90,17260,5,2,1:0:0:0:"; 177 | // "41,90,17335,1,2,0:0:0:0:"; 178 | // "41,90,17410,1,2,0:0:0:0:"; 179 | // "11,166,17560,1,0,0:0:0:0:"; 180 | // "35,242,17710,1,2,3:2:0:0:"; 181 | // "3,360,18010,1,0,3:0:0:0:"; 182 | // "3,360,18160,2,0,P|71:359|91:345,1,80,0|2,3:0|0:0,0:0:0:0:"; 183 | // "206,298,18760,1,0,1:0:0:0:"; 184 | // "206,298,18910,1,0,3:0:0:0:"; 185 | // "270,348,19060,1,2,3:2:0:0:"; 186 | // "386,283,19360,1,0,3:0:0:0:"; 187 | // "512,384,19660,6,0,L|508:344,1,40,2|0,1:0|0:0,0:0:0:0:"; 188 | // "475,272,19960,1,0,3:0:0:0:"; 189 | // "475,272,20110,2,0,L|482:164,1,80,0|0,3:0|0:0,0:0:0:0:"; 190 | // "438,95,20560,1,0,3:0:0:0:"; 191 | // "438,95,20710,1,0,3:0:0:0:"; 192 | // "376,147,20860,1,2,0:0:0:0:"; 193 | // "376,147,21010,1,0,3:0:0:0:"; 194 | // "312,59,21160,6,0,L|264:63,1,40,2|0,3:2|0:0,0:0:0:0:"; 195 | // "217,143,21460,2,0,L|169:139,1,40,2|0,1:0|0:0,0:0:0:0:"; 196 | // "120,39,21760,2,0,L|72:43,1,40,2|0,1:0|0:0,0:0:0:0:"; 197 | // "4,143,22060,6,0,P|4:162|4:196,1,44.0000013427735,2|0,1:0|0:0,0:0:0:0:"; 198 | // "62,241,22360,1,2,3:2:0:0:"; 199 | // "169,196,22660,1,2,0:0:0:0:"; 200 | // "261,262,22960,1,2,3:2:0:0:"; 201 | //|] 202 | 203 | //let printTimingPoint (t:Tp) = 204 | // printfn "%d,%M,%d,%d,%d,%d,%d,%d" t.time t.beatLength t.meter t.sampleSet t.sampleIndex t.volume (if t.uninherited then 1 else 0) t.effects 205 | 206 | [] 207 | let main argv = 208 | //let generalOptions = parseGeneralSection exampleGeneral 209 | //let editorOptions = parseEditorSection exampleEditor 210 | //let metadata = parseMetadataSection exampleMetadata 211 | //let diffopts = parseDifficultySection diffOptions 212 | //let evts = parseEventSection events 213 | //let timingpoints = parseTimingPointSection timingPoints 214 | //let colours = parseColourSection exampleColours 215 | //let hitobjects = parseHitObjectSection (exampleHitObjects |> Array.toList) 216 | //printfn "hey, no crash!" 217 | 218 | //let beatmap = new Beatmap @"C:\Program Files\osu!\Songs\758101 Co shu Nie - asphyxia (TV edit)\Co shu Nie - asphyxia (TV edit) (Monstrata) [Asphyxia's Asphyxiating Extra].osu" 219 | let beatmap = new Beatmap @"C:\Users\funor\scratchsdlfkjasdlfkj.txt"; 220 | beatmap.AudioFilename <- "audio 240bpm.mp3" 221 | beatmap.ModifyTiming 1.20M 222 | beatmap.TitleUnicode <- "おちんちん気持ちいい" 223 | for i = 0 to 10 do 224 | beatmap.ApproachRate <- decimal i 225 | beatmap.Save() 226 | 227 | 0 228 | 229 | *) 230 | -------------------------------------------------------------------------------- /MainForm.cs: -------------------------------------------------------------------------------- 1 | using OsuMemoryDataProvider; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Data; 6 | using System.Drawing; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Text; 10 | using System.Windows.Forms; 11 | using IWshRuntimeLibrary; 12 | using System.Threading.Tasks; 13 | using System.Runtime.InteropServices; 14 | 15 | namespace Circle_Tracker 16 | { 17 | public partial class MainForm : Form 18 | { 19 | private readonly Tracker tracker; 20 | private bool MinimizeToTrayEnabled = true; 21 | private string ShortcutAddress; 22 | public MainForm() 23 | { 24 | Directory.SetCurrentDirectory(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)); 25 | InitializeComponent(); 26 | //this.Icon = Properties.Resources.iconbars; 27 | ShortcutAddress = Environment.GetEnvironmentVariable("appdata") + @"\Microsoft\Windows\Start Menu\Programs\Startup" + @"\circle-tracker.lnk"; 28 | bool shortcutExists = ShortcutExists(); 29 | startupCheckBox.Checked = shortcutExists; 30 | if (shortcutExists) 31 | { 32 | // overwrite the existing shortcut just in it's an older version 33 | TryDeleteShortcut(); 34 | CreateShortcut(); 35 | } 36 | 37 | try 38 | { 39 | Updater.CheckForUpdates(); 40 | } 41 | catch 42 | { 43 | // exception we probably don't care about 44 | } 45 | 46 | tracker = new Tracker(this); 47 | 48 | songsFolderTextBox.Text = tracker.SongsFolder; 49 | sheetNameTextBox.Text = tracker.SheetName; 50 | spreadsheetIdTextBox.Text = tracker.SpreadsheetId; 51 | soundEnabledCheckbox.Checked = tracker.SubmitSoundEnabled; 52 | altSepCheckBox.Checked = tracker.UseAltFuncSeparator; 53 | 54 | tracker.InitGoogleAPI(silent:true); 55 | SetCredentialsFound(System.IO.File.Exists("credentials.json")); 56 | updateGameVariablesTimer.Start(); 57 | updateFormTimer.Start(); 58 | secondsCounterTimer.Start(); 59 | } 60 | 61 | public void SetCredentialsFound(bool found) 62 | { 63 | credentialsLabel.Text = found ? "Found" : "Missing"; 64 | credentialsLabel.ForeColor = found ? Color.Green : Color.Red; 65 | } 66 | 67 | public void UpdateControls() 68 | { 69 | groupBox2.BackColor = (tracker.SheetsApiReady && tracker.GameState == OsuMemoryStatus.Playing) ? Color.FromArgb(214, 241, 216) : SystemColors.Control; 70 | hitsTextBox.Text = tracker.TotalBeatmapHits.ToString() + $" ({tracker.Play300c}, {tracker.Play100c}, {tracker.Play50c}, {tracker.PlayMissc})"; 71 | timeTextBox.Text = tracker.Time.ToString(); 72 | beatmapTextBox.Text = tracker.BeatmapString; 73 | starsTextBox.Text = tracker.BeatmapStars.ToString("0.00"); 74 | aimTextBox.Text = tracker.BeatmapAim.ToString("0.00"); 75 | speedTextBox.Text = tracker.BeatmapSpeed.ToString("0.00"); 76 | modsTextBox.Text = tracker.GetModsString(); 77 | textBoxCS.Text = tracker.BeatmapCs.ToString("0.0"); 78 | textBoxAR.Text = tracker.BeatmapAr.ToString("0.0"); 79 | textBoxOD.Text = tracker.BeatmapOd.ToString("0.0"); 80 | accTextBox.Text = tracker.Accuracy.ToString("0.00") + "%"; 81 | bpmTextBox.Text = tracker.BeatmapBpm.ToString(); 82 | 83 | if (tracker.BeatmapString == null || tracker.BeatmapString == "") 84 | beatmapTextBox.BackColor = Color.Pink; 85 | else 86 | beatmapTextBox.BackColor = SystemColors.Control; 87 | 88 | bool valuesAreBad = (tracker.BeatmapStars == 0 89 | && tracker.BeatmapAim == 0 90 | && tracker.BeatmapSpeed == 0 91 | && tracker.BeatmapCs == 0 92 | && tracker.BeatmapAr == 0 93 | && tracker.BeatmapOd == 0); 94 | starsTextBox.BackColor = valuesAreBad ? Color.Pink : SystemColors.Control; 95 | aimTextBox.BackColor = valuesAreBad ? Color.Pink : SystemColors.Control; 96 | speedTextBox.BackColor = valuesAreBad ? Color.Pink : SystemColors.Control; 97 | textBoxCS.BackColor = valuesAreBad ? Color.Pink : SystemColors.Control; 98 | textBoxAR.BackColor = valuesAreBad ? Color.Pink : SystemColors.Control; 99 | textBoxOD.BackColor = valuesAreBad ? Color.Pink : SystemColors.Control; 100 | } 101 | 102 | private void songsFolderTextBox_TextChanged(object sender, EventArgs e) 103 | { 104 | tracker.SetSongsFolder(songsFolderTextBox.Text); 105 | } 106 | 107 | public void SetSheetsApiReady(bool val) 108 | { 109 | statusLabel.Text = val ? "Connected" : "Not connected"; 110 | statusLabel.ForeColor = val ? System.Drawing.Color.Green : System.Drawing.Color.Red; 111 | } 112 | 113 | private void ConnectApiButton_Click(object sender, EventArgs e) 114 | { 115 | tracker.InitGoogleAPI(); 116 | } 117 | 118 | private void spreadsheetIdTextBox_TextChanged(object sender, EventArgs e) 119 | { 120 | tracker.SpreadsheetId = spreadsheetIdTextBox.Text; 121 | } 122 | 123 | private void sheetNameTextBox_TextChanged(object sender, EventArgs e) 124 | { 125 | tracker.SheetName = sheetNameTextBox.Text; 126 | } 127 | 128 | private void soundEnabledCheckbox_CheckedChanged(object sender, EventArgs e) 129 | { 130 | tracker.SubmitSoundEnabled = soundEnabledCheckbox.Checked; 131 | } 132 | 133 | private async void updateGameVariablesTimer_Tick(object sender, EventArgs e) 134 | { 135 | #if DEBUG 136 | await Task.Run(() => tracker.TickWrapper()); 137 | #else 138 | try 139 | { 140 | await Task.Run(() => tracker.Tick()); 141 | } 142 | catch (TaskCanceledException) 143 | { 144 | // do nothing? idk why this occurs. it's probably okay to ignore it. 145 | } 146 | catch (Exception ex) 147 | { 148 | updateGameVariablesTimer.Stop(); 149 | using (StreamWriter outputFile = new StreamWriter(Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "errorlog.txt"))) 150 | { 151 | outputFile.WriteLine("-------------------"); 152 | outputFile.WriteLine(DateTime.Now); 153 | outputFile.WriteLine("-------------------"); 154 | outputFile.WriteLine(ex.ToString()); 155 | outputFile.WriteLine(""); 156 | outputFile.WriteLine(""); 157 | } 158 | MessageBox.Show($"Exception occurred: {Environment.NewLine}{Environment.NewLine}" + 159 | $"{ex.ToString()}{Environment.NewLine}{Environment.NewLine}" 160 | , "Error"); 161 | MessageBox.Show($"If this problem keeps happening, send errorlog.txt to FunOrange. This file is located inside the circle tracker folder."); 162 | updateGameVariablesTimer.Start(); 163 | } 164 | #endif 165 | } 166 | public void StopUpdateTimer() 167 | { 168 | updateGameVariablesTimer.Stop(); 169 | } 170 | 171 | private void startupCheckBox_CheckedChanged(object sender, EventArgs e) 172 | { 173 | if (startupCheckBox.Checked) 174 | { 175 | CreateShortcut(); 176 | } 177 | else 178 | { 179 | TryDeleteShortcut(); 180 | } 181 | } 182 | 183 | private void notifyIcon_MouseDoubleClick(object sender, MouseEventArgs e) 184 | { 185 | Show(); 186 | WindowState = FormWindowState.Normal; 187 | notifyIcon.Visible = false; 188 | } 189 | 190 | private void MinimizeToTray(object sender, EventArgs e) 191 | { 192 | if (MinimizeToTrayEnabled) 193 | { 194 | Hide(); 195 | notifyIcon.Visible = true; 196 | } 197 | } 198 | private void CreateShortcut() 199 | { 200 | object shDesktop = (object)"Desktop"; 201 | WshShell shell = new WshShell(); 202 | IWshShortcut shortcut = (IWshShortcut)shell.CreateShortcut(ShortcutAddress); 203 | shortcut.Description = "Circle Tracker (startup shortcut)"; 204 | shortcut.Hotkey = ""; 205 | shortcut.TargetPath = System.Reflection.Assembly.GetExecutingAssembly().Location; 206 | shortcut.Save(); 207 | } 208 | private void TryDeleteShortcut() 209 | { 210 | try 211 | { 212 | System.IO.File.Delete(ShortcutAddress); 213 | } 214 | catch 215 | { 216 | // couldn't delete (eg. shortcut in use...) 217 | } 218 | } 219 | private bool ShortcutExists() 220 | { 221 | return System.IO.File.Exists(ShortcutAddress); 222 | } 223 | 224 | private void updateFormTimer_Tick(object sender, EventArgs e) 225 | { 226 | UpdateControls(); 227 | } 228 | 229 | private void altSepCheckBox_CheckedChanged(object sender, EventArgs e) 230 | { 231 | tracker.UseAltFuncSeparator = altSepCheckBox.Checked; 232 | } 233 | 234 | private void MainForm_FormClosing(object sender, FormClosingEventArgs e) 235 | { 236 | tracker.SaveSettings(); 237 | } 238 | 239 | private void secondsCounterTimer_Tick(object sender, EventArgs e) => tracker.TickEverySecond(); 240 | public void UpdateTime() 241 | { 242 | timeLabel.Text = 243 | $"Playing: {tracker.PlayingSeconds} " + 244 | $"Idle: {tracker.IdleSeconds} " + 245 | $"Efficiency: {100 * tracker.PlayingSeconds / (float)(tracker.PlayingSeconds + tracker.IdleSeconds), 0}% "; 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /DifficultyCalculator.cs: -------------------------------------------------------------------------------- 1 | using Google.Apis.Util; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | 7 | namespace Circle_Tracker 8 | { 9 | class DifficultyCalculator 10 | { 11 | static List<(decimal, decimal)> ApproachRateTableHTEZ = new List<(decimal, decimal)> { 12 | (0M, -5M), 13 | (1M, -4.33M), 14 | (2M, -3.67M), 15 | (3M, -3M), 16 | (4M, -2.33M), 17 | (5M, -1.67M), 18 | (6M, -1M), 19 | (7M, -0.33M), 20 | (8M, 0.33M), 21 | (9M, 1M), 22 | (10M,1.67M) 23 | }; 24 | static List<(decimal, decimal)> ApproachRateTableHT = new List<(decimal, decimal)> { 25 | (0M, -5M), 26 | (1M, -3.67M), 27 | (2M, -2.33M), 28 | (3M, -1M), 29 | (4M, 0.33M), 30 | (5M, 1.67M), 31 | (6M, 3.33M), 32 | (7M, 5M), 33 | (8M, 6.33M), 34 | (9M, 7.67M), 35 | (10M,9M) 36 | }; 37 | static List<(decimal, decimal)> ApproachRateTableHTHR = new List<(decimal, decimal)> { 38 | (0M, -5M), 39 | (1M, -3.13M), 40 | (2M, -1.27M), 41 | (3M, 0.6M), 42 | (4M, 2.67M), 43 | (5M, 5M), 44 | (6M, 6.87M), 45 | (7M, 8.73M), 46 | (8M, 9M), 47 | (9M, 9M), 48 | (10M,9M) 49 | }; 50 | static List<(decimal, decimal)> ApproachRateTableEZ = new List<(decimal, decimal)> { 51 | (0M, 0M), 52 | (1M, 0.5M), 53 | (2M, 1M), 54 | (3M, 1.5M), 55 | (4M, 2M), 56 | (5M, 2.5M), 57 | (6M, 3M), 58 | (7M, 3.5M), 59 | (8M, 4M), 60 | (9M, 4.5M), 61 | (10M,5M) 62 | }; 63 | static List<(decimal, decimal)> ApproachRateTableHR = new List<(decimal, decimal)> { 64 | (0M, 0M), 65 | (1M, 1.4M), 66 | (2M, 2.8M), 67 | (3M, 4.2M), 68 | (4M, 5.6M), 69 | (5M, 7M), 70 | (6M, 8.4M), 71 | (7M, 9.8M), 72 | (8M, 10M), 73 | (9M, 10M), 74 | (10M, 10M) 75 | }; 76 | static List<(decimal, decimal)> ApproachRateTableDTEZ = new List<(decimal, decimal)> { 77 | (0M, 5M), 78 | (1M, 5.27M), 79 | (2M, 5.53M), 80 | (3M, 5.8M), 81 | (4M, 6.07M), 82 | (5M, 6.33M), 83 | (6M, 6.6M), 84 | (7M, 6.87M), 85 | (8M, 7.13M), 86 | (9M, 7.4M), 87 | (10M,7.67M) 88 | }; 89 | static List<(decimal, decimal)> ApproachRateTableDT = new List<(decimal, decimal)> { 90 | (0M, 5M), 91 | (1M, 5.4M), 92 | (2M, 6.07M), 93 | (3M, 6.6M), 94 | (4M, 7.13M), 95 | (5M, 7.67M), 96 | (6M, 8.33M), 97 | (7M, 9M), 98 | (8M, 9.67M), 99 | (9M, 10.33M), 100 | (10M, 11M) 101 | }; 102 | static List<(decimal, decimal)> ApproachRateTableDTHR = new List<(decimal, decimal)> { 103 | (0M, 5M), 104 | (1M, 5.747M), 105 | (2M, 6.493M), 106 | (3M, 7.24M), 107 | (4M, 8.07M), 108 | (5M, 9M), 109 | (6M, 9.93M), 110 | (7M, 10.87M), 111 | (8M, 11M), 112 | (9M, 11M), 113 | (10M, 11M) 114 | }; 115 | 116 | static List<(decimal, decimal)> OverallDifficultyTableHTEZ = new List<(decimal, decimal)> { 117 | (0M, -4.42M), 118 | (1M, -3.75M), 119 | (2M, -3.08M), 120 | (3M, -2.42M), 121 | (4M, -1.75M), 122 | (5M, -1.08M), 123 | (6M, -0.42M), 124 | (7M, 0.25M), 125 | (8M, 0.92M), 126 | (9M, 1.54M), 127 | (10M, 2.25M) 128 | }; 129 | static List<(decimal, decimal)> OverallDifficultyTableHT = new List<(decimal, decimal)> { 130 | (0M, -4.42M), 131 | (1M, -3.08M), 132 | (2M, -1.75M), 133 | (3M, -0.42M), 134 | (4M, 0.92M), 135 | (5M, 2.25M), 136 | (6M, 3.54M), 137 | (7M, 4.92M), 138 | (8M, 6.25M), 139 | (9M, 7.54M), 140 | (10M, 8.92M) 141 | }; 142 | static List<(decimal, decimal)> OverallDifficultyTableHTHR = new List<(decimal, decimal)> { 143 | (0M, 4.42M), 144 | (1M, 2.42M), 145 | (2M, 0.64M), 146 | (3M, 1.36M), 147 | (4M, 3.14M), 148 | (5M, 4.92M), 149 | (6M, 6.92M), 150 | (7M, 8.69M), 151 | (8M, 8.92M), 152 | (9M, 8.92M), 153 | (10M, 8.92M), 154 | }; 155 | static List<(decimal, decimal)> OverallDifficultyTableEZ = new List<(decimal, decimal)> { 156 | (0M, 0M), 157 | (1M, 0.5M), 158 | (2M, 1M), 159 | (3M, 1.5M), 160 | (4M, 2M), 161 | (5M, 2.5M), 162 | (6M, 3M), 163 | (7M, 3.5M), 164 | (8M, 4M), 165 | (9M, 4.5M), 166 | (10M, 5M), 167 | }; 168 | static List<(decimal, decimal)> OverallDifficultyTableHR = new List<(decimal, decimal)> { 169 | (0M, 0M), 170 | (1M, 1.4M), 171 | (2M, 2.8M), 172 | (3M, 4.2M), 173 | (4M, 5.6M), 174 | (5M, 7M), 175 | (6M, 8.4M), 176 | (7M, 9.8M), 177 | (8M, 10M), 178 | (9M, 10M), 179 | (10M,10M) 180 | }; 181 | static List<(decimal, decimal)> OverallDifficultyTableDTEZ = new List<(decimal, decimal)> { 182 | (0M, 4.42M), 183 | (1M, 4.75M), 184 | (2M, 5.08M), 185 | (3M, 5.42M), 186 | (4M, 5.75M), 187 | (5M, 6.08M), 188 | (6M, 6.42M), 189 | (7M, 6.75M), 190 | (8M, 7.08M), 191 | (9M, 7.42M), 192 | (10M, 7.75M) 193 | }; 194 | static List<(decimal, decimal)> OverallDifficultyTableDT = new List<(decimal, decimal)> { 195 | (0M, 4.42M), 196 | (1M, 5.08M), 197 | (2M, 5.75M), 198 | (3M, 6.42M), 199 | (4M, 7.08M), 200 | (5M, 7.75M), 201 | (6M, 8.42M), 202 | (7M, 9.08M), 203 | (8M, 9.75M), 204 | (9M, 10.42M), 205 | (10M,11.08M) 206 | }; 207 | static List<(decimal, decimal)> OverallDifficultyTableDTHR = new List<(decimal, decimal)> { 208 | (0M, 4.42M), 209 | (1M, 5.42M), 210 | (2M, 6.31M), 211 | (3M, 7.31M), 212 | (4M, 8.19M), 213 | (5M, 9.08M), 214 | (6M, 10.08M), 215 | (7M, 10.97M), 216 | (8M, 11.08M), 217 | (9M, 11.08M), 218 | (10M,11.08M) 219 | }; 220 | 221 | public static decimal CalculateARWithHTEZ(decimal od) => LerpValueUsingLUT(od, ApproachRateTableHTEZ); 222 | public static decimal CalculateARWithHT(decimal od) => LerpValueUsingLUT(od, ApproachRateTableHT); 223 | public static decimal CalculateARWithHTHR(decimal od) => LerpValueUsingLUT(od, ApproachRateTableHTHR); 224 | public static decimal CalculateARWithEZ(decimal od) => LerpValueUsingLUT(od, ApproachRateTableEZ); 225 | public static decimal CalculateARWithHR(decimal od) => LerpValueUsingLUT(od, ApproachRateTableHR); 226 | public static decimal CalculateARWithDT(decimal od) => LerpValueUsingLUT(od, ApproachRateTableDT); 227 | public static decimal CalculateARWithDTEZ(decimal od) => LerpValueUsingLUT(od, ApproachRateTableDTEZ); 228 | public static decimal CalculateARWithDTHR(decimal od) => LerpValueUsingLUT(od, ApproachRateTableDTHR); 229 | 230 | public static decimal CalculateODWithHTEZ(decimal od) => LerpValueUsingLUT(od, OverallDifficultyTableHTEZ); 231 | public static decimal CalculateODWithHT(decimal od) => LerpValueUsingLUT(od, OverallDifficultyTableHT); 232 | public static decimal CalculateODWithHTHR(decimal od) => LerpValueUsingLUT(od, OverallDifficultyTableHTHR); 233 | public static decimal CalculateODWithEZ(decimal od) => LerpValueUsingLUT(od, OverallDifficultyTableEZ); 234 | public static decimal CalculateODWithHR(decimal od) => LerpValueUsingLUT(od, OverallDifficultyTableHR); 235 | public static decimal CalculateODWithDT(decimal od) => LerpValueUsingLUT(od, OverallDifficultyTableDT); 236 | public static decimal CalculateODWithDTEZ(decimal od) => LerpValueUsingLUT(od, OverallDifficultyTableDTEZ); 237 | public static decimal CalculateODWithDTHR(decimal od) => LerpValueUsingLUT(od, OverallDifficultyTableDTHR); 238 | 239 | private static decimal LerpValueUsingLUT(decimal input, List<(decimal, decimal)> lut) { 240 | if (input < 0M || input > 10M) 241 | return 0M; 242 | 243 | var values = lut.Select(indexValuePair => indexValuePair.Item2); 244 | decimal maxValue = values.Max(); 245 | 246 | int lowerIndex = (int)input; 247 | int upperIndex = Math.Min(lowerIndex + 1, 10); // clamp to 10 248 | 249 | decimal lowerValue = lut[lowerIndex].Item2; 250 | decimal upperValue = lut[upperIndex].Item2; 251 | decimal stepDifference = upperValue - lowerValue; 252 | decimal stepFraction = input - (int)input; // decimal part 253 | 254 | if (lowerValue != maxValue && upperValue == maxValue) 255 | { 256 | // special case: piece-wise linear interval. Starts with a positive slope, then plateaus to max value. 257 | // Linearly interpolate based on the slope of the previous interval 258 | decimal prevIntervalLowerValue = lut[lowerIndex - 1].Item2; 259 | decimal prevIntervalUpperValue = lut[lowerIndex].Item2; 260 | decimal previousSlope = prevIntervalUpperValue - prevIntervalLowerValue; 261 | // Clamp the lerped value to maxValue 262 | decimal lerp = lowerValue + stepFraction * previousSlope; 263 | return Math.Min(lerp, maxValue); 264 | } 265 | else 266 | return lowerValue + stepFraction * stepDifference; 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /submodules/FsBeatmapParser/FsBeatmap.fs: -------------------------------------------------------------------------------- 1 | namespace FsBeatmapProcessor 2 | 3 | open JunUtils 4 | open BeatmapParser 5 | open General 6 | open Editor 7 | open Metadata 8 | open Difficulty 9 | open Events 10 | open TimingPoints 11 | open Colours 12 | open HitObjects 13 | open System 14 | open System.IO 15 | 16 | // C# Facing Interface 17 | 18 | type GameMode = 19 | | osu = 0 20 | | Taiko = 1 21 | | CatchtheBeat = 2 22 | | Mania = 3 23 | 24 | [] 25 | type Beatmap(file, repr, cl) = 26 | 27 | member internal this.generalTryGetOr isfn getfn noneval = 28 | match this.changelist.general |> List.tryFind isfn with 29 | | Some x -> getfn x 30 | | None -> 31 | match this.originalFileRepresentation.general |> List.tryFind isfn with 32 | | Some x -> getfn x 33 | | None -> noneval 34 | 35 | member internal this.metadataTryGetOr isfn getfn noneval = 36 | match this.changelist.metadata |> List.tryFind isfn with 37 | | Some x -> getfn x 38 | | None -> 39 | match this.originalFileRepresentation.metadata |> List.tryFind isfn with 40 | | Some x -> getfn x 41 | | None -> noneval 42 | 43 | member internal this.difficultyTryGetOr isfn getfn noneval = 44 | match this.changelist.difficulty |> List.tryFind isfn with 45 | | Some x -> getfn x 46 | | None -> 47 | match this.originalFileRepresentation.difficulty |> List.tryFind isfn with 48 | | Some x -> getfn x 49 | | None -> noneval 50 | 51 | (* internal state/representation of beatmap *) 52 | member val public Filename = file with get, set 53 | member internal this.originalFileRepresentation = repr 54 | member val internal changelist = cl with get, set 55 | 56 | (* Public *) 57 | member public this.Valid with get() = (this.originalFileRepresentation.general <> []) 58 | 59 | // main constructor 60 | new(file) = 61 | let changelist = {general=[]; editor=[]; metadata=[]; difficulty=[]; events=[]; timingPoints=[]; colours=[]; hitObjects=[]} 62 | Beatmap(file, parseBeatmapFile file, changelist) 63 | 64 | // copy constructor 65 | new(other:Beatmap) = 66 | Beatmap(other.Filename, other.originalFileRepresentation, other.changelist) 67 | 68 | member public this.Save() = this.Save(this.Filename) 69 | member public this.Save(outputPath) = 70 | let replaceOrAppend replaceCondition thingToInsert destlist = 71 | let mutable retlist = [] 72 | let mutable replaceSuccess = false 73 | for candidate in destlist do 74 | if (replaceCondition candidate) 75 | then retlist <- retlist @ [thingToInsert]; replaceSuccess <- true 76 | else retlist <- retlist @ [candidate] 77 | if replaceSuccess 78 | then retlist 79 | else retlist @ [thingToInsert] 80 | 81 | let rec applyGeneralChanges changelist targetlist = 82 | match changelist with 83 | | change::changes -> 84 | match change with 85 | | AudioFilename _ -> applyGeneralChanges changes (replaceOrAppend isAudioFilename change targetlist) 86 | | AudioLeadIn _ -> applyGeneralChanges changes (replaceOrAppend isAudioLeadIn change targetlist) 87 | | PreviewTime _ -> applyGeneralChanges changes (replaceOrAppend isPreviewTime change targetlist) 88 | | Countdown _ -> applyGeneralChanges changes (replaceOrAppend isCountdown change targetlist) 89 | | SampleSet _ -> applyGeneralChanges changes (replaceOrAppend isSampleSet change targetlist) 90 | | StackLeniency _ -> applyGeneralChanges changes (replaceOrAppend isStackLeniency change targetlist) 91 | | Mode _ -> applyGeneralChanges changes (replaceOrAppend isMode change targetlist) 92 | | LetterboxInBreaks _ -> applyGeneralChanges changes (replaceOrAppend isLetterboxInBreaks change targetlist) 93 | | UseSkinSprites _ -> applyGeneralChanges changes (replaceOrAppend isUseSkinSprites change targetlist) 94 | | OverlayPosition _ -> applyGeneralChanges changes (replaceOrAppend isOverlayPosition change targetlist) 95 | | SkinPreference _ -> applyGeneralChanges changes (replaceOrAppend isSkinPreference change targetlist) 96 | | EpilepsyWarning _ -> applyGeneralChanges changes (replaceOrAppend isEpilepsyWarning change targetlist) 97 | | CountdownOffset _ -> applyGeneralChanges changes (replaceOrAppend isCountdownOffset change targetlist) 98 | | SpecialStyle _ -> applyGeneralChanges changes (replaceOrAppend isSpecialStyle change targetlist) 99 | | WidescreenStoryboard _ -> applyGeneralChanges changes (replaceOrAppend isWidescreenStoryboard change targetlist) 100 | | SamplesMatchPlaybackRate _ -> applyGeneralChanges changes (replaceOrAppend isSamplesMatchPlaybackRate change targetlist) 101 | | _ -> targetlist 102 | | [] -> targetlist 103 | 104 | let rec applyMetadataChanges changelist targetlist = 105 | match changelist with 106 | | change::changes -> 107 | match change with 108 | | Title _ -> applyMetadataChanges changes (replaceOrAppend isTitle change targetlist) 109 | | TitleUnicode _ -> applyMetadataChanges changes (replaceOrAppend isTitleUnicode change targetlist) 110 | | Artist _ -> applyMetadataChanges changes (replaceOrAppend isArtist change targetlist) 111 | | ArtistUnicode _ -> applyMetadataChanges changes (replaceOrAppend isArtistUnicode change targetlist) 112 | | Creator _ -> applyMetadataChanges changes (replaceOrAppend isCreator change targetlist) 113 | | Version _ -> applyMetadataChanges changes (replaceOrAppend isVersion change targetlist) 114 | | Source _ -> applyMetadataChanges changes (replaceOrAppend isSource change targetlist) 115 | | SearchTerms _ -> applyMetadataChanges changes (replaceOrAppend isSearchTerms change targetlist) 116 | | BeatmapID _ -> applyMetadataChanges changes (replaceOrAppend isBeatmapID change targetlist) 117 | | BeatmapSetID _ -> applyMetadataChanges changes (replaceOrAppend isBeatmapSetID change targetlist) 118 | | _ -> targetlist 119 | | [] -> targetlist 120 | 121 | let rec applyDifficultyChanges changelist targetlist = 122 | match changelist with 123 | | change::changes -> 124 | match change with 125 | | HPDrainRate _ -> applyDifficultyChanges changes (replaceOrAppend isHPDrainRate change targetlist) 126 | | CircleSize _ -> applyDifficultyChanges changes (replaceOrAppend isCircleSize change targetlist) 127 | | OverallDifficulty _ -> applyDifficultyChanges changes (replaceOrAppend isOverallDifficulty change targetlist) 128 | | ApproachRate _ -> applyDifficultyChanges changes (replaceOrAppend isApproachRate change targetlist) 129 | | SliderMultiplier _ -> applyDifficultyChanges changes (replaceOrAppend isSliderMultiplier change targetlist) 130 | | SliderTickRate _ -> applyDifficultyChanges changes (replaceOrAppend isSliderTickRate change targetlist) 131 | | _ -> targetlist 132 | | [] -> targetlist 133 | 134 | let exportGeneral = applyGeneralChanges (List.rev this.changelist.general) this.originalFileRepresentation.general 135 | let exportEditor = this.originalFileRepresentation.editor 136 | let exportMetadata = applyMetadataChanges (List.rev this.changelist.metadata) this.originalFileRepresentation.metadata 137 | let exportDifficulty = applyDifficultyChanges (List.rev this.changelist.difficulty) this.originalFileRepresentation.difficulty 138 | let exportEvents = if this.changelist.events = [] then this.originalFileRepresentation.events else this.changelist.events 139 | let exportTimingPoints = if this.changelist.timingPoints = [] then this.originalFileRepresentation.timingPoints else this.changelist.timingPoints 140 | let exportColours = this.originalFileRepresentation.colours 141 | let exportHitObjects = if this.changelist.hitObjects = [] then this.originalFileRepresentation.hitObjects else this.changelist.hitObjects 142 | 143 | let exportFileLines = ["osu file format v14";""] @ (List.map generalInfoToString exportGeneral) @ (List.map editorSettingToString exportEditor) @ (List.map metadataToString exportMetadata) @ (List.map difficultySettingToString exportDifficulty) @ (List.map eventToString exportEvents) @ (List.map timingPointToString exportTimingPoints) @ (List.map colourSettingToString exportColours) @ (List.map hitObjectToString exportHitObjects) 144 | File.WriteAllLines(outputPath, exportFileLines, Text.Encoding.UTF8) 145 | () 146 | 147 | member public this.RemoveSpinners() = 148 | let objects = 149 | if (this.changelist.hitObjects |> removeHitObjectComments) = [] then 150 | this.originalFileRepresentation.hitObjects 151 | else 152 | this.changelist.hitObjects 153 | 154 | let notSpinner = function 155 | | Spinner _ -> false 156 | | _ -> true 157 | 158 | 159 | let newHitObjects = List.filter notSpinner objects 160 | 161 | this.changelist <- {this.changelist with hitObjects = newHitObjects} 162 | 163 | member public this.SetRate (rate:decimal) = 164 | let originalPreviewTime = match this.originalFileRepresentation.general |> List.tryFind isPreviewTime with 165 | | Some previewTime -> getPreviewTime previewTime 166 | | None -> 0 167 | let newPreviewTime = int ((1M / rate) * decimal (originalPreviewTime)) 168 | let newGeneral = PreviewTime(newPreviewTime) :: this.changelist.general 169 | 170 | let newEvents = this.originalFileRepresentation.events 171 | |> List.map (function 172 | | Video b -> Video { b with startTime = (divide b.startTime rate)} 173 | | Break b -> Break { b with startTime = (divide b.startTime rate); 174 | endTime = (divide b.endTime rate)} 175 | | other -> other) 176 | 177 | let newTimingPoints = this.originalFileRepresentation.timingPoints 178 | |> List.map (function 179 | | TimingPoint tp -> 180 | if tp.uninherited 181 | then (TimingPoint { tp with time = divide tp.time rate; 182 | beatLength = tp.beatLength / rate }) 183 | else (TimingPoint { tp with time = divide tp.time rate }) 184 | | comment -> comment) 185 | 186 | let newHitObjects = this.originalFileRepresentation.hitObjects 187 | |> List.map (function 188 | | HitCircle c -> HitCircle { c with time = (divide c.time rate) } 189 | | Slider s -> HitCircle { s with time = (divide s.time rate) } 190 | | Spinner spin -> Spinner { spin with time = (divide spin.time rate); 191 | endTime = (divide spin.endTime rate)} 192 | | Hold h -> Hold { h with time = (divide h.time rate); 193 | endTime = (divide h.endTime rate)} 194 | | comment -> comment) 195 | 196 | this.changelist <- {this.changelist with general = newGeneral; 197 | events = newEvents; 198 | timingPoints = newTimingPoints; 199 | hitObjects = newHitObjects; } 200 | 201 | // get song dominant bpm 202 | member public this.Bpm with get() = 203 | //printfn "@@@ bpm func:" 204 | if (this.originalFileRepresentation.hitObjects |> removeHitObjectComments |> List.length) = 0 then 205 | //printfn "@@@ this.originalFileRepresentation.hitObjects length = 0, return 0" 206 | 0M 207 | else if (this.originalFileRepresentation.timingPoints |> removeTimingPointComments |> List.length) = 0 then 208 | //printfn "@@@ this.originalFileRepresentation.timingPoints length = 0, return 0" 209 | 0M 210 | else 211 | 212 | let rec beatLengthDurations (timingPoints:list) lastObject : list = 213 | match timingPoints with 214 | | tp1::tp2::tps -> 215 | let duration = tp2.time - tp1.time 216 | (tp1.beatLength, duration)::(beatLengthDurations timingPoints.Tail lastObject) 217 | 218 | | [tp1] -> 219 | // duration: start time -> last hit object 220 | let endtime = 221 | match lastObject with 222 | | HitCircle x -> x.time 223 | | Slider x -> x.time 224 | | Spinner x -> x.time 225 | | Hold x -> x.time 226 | | _ -> 0 227 | let duration = endtime - tp1.time 228 | let duration' = if duration > 0 then duration else 0 229 | [(tp1.beatLength, duration')] 230 | 231 | | [] -> // shouldn't get here normally 232 | [] 233 | 234 | // someone please teach me how to write f# 235 | let lastObject = 236 | if this.changelist.timingPoints = [] then 237 | this.originalFileRepresentation.hitObjects |> removeHitObjectComments |> List.rev |> List.head 238 | else 239 | this.changelist.hitObjects |> removeHitObjectComments |> List.rev |> List.head 240 | 241 | // printfn "@@@ last object: %s" (hitObjectToString lastObject) 242 | let timingpoints = if this.changelist.timingPoints = [] then this.originalFileRepresentation.timingPoints else this.changelist.timingPoints 243 | //printfn "@@@ timing points: %A" timingpoints 244 | let bpmTimingPoints = 245 | timingpoints 246 | |> List.filter isTimingPoint 247 | |> List.map getTimingPoint 248 | |> List.filter (fun tp -> tp.uninherited) 249 | // printfn "@@@ bpmTimingPoints: %A" bpmTimingPoints 250 | 251 | if bpmTimingPoints = [] then 252 | // printfn "@@@ bpmTimingPoints is empty, return 0" 253 | 0M 254 | else 255 | 256 | let durations = beatLengthDurations bpmTimingPoints lastObject 257 | //printf "@@@ durations: %A" durations 258 | let grouped1 = List.groupBy (fun (beatlength, _) -> beatlength) durations 259 | //printfn "@@@ grouped1: %A" grouped1 260 | let grouped2 = grouped1 |> List.map (fun (bl', tupleList) -> (bl', List.map (fun (_, duration) -> duration) tupleList)) 261 | // printfn "@@@ grouped2: %A" grouped2 262 | let groupedSums = grouped2 |> List.map (fun (beatLength, durations) -> (beatLength, List.sum durations)) 263 | // printfn "@@@ groupedSums: %A" groupedSums 264 | 265 | let (maxBeatLength, _) = List.maxBy (fun (beatLength, durationSum) -> durationSum) groupedSums 266 | // printfn "@@@ maxBeatLength: %A" maxBeatLength 267 | // printfn "return %A" (60000M / maxBeatLength) 268 | 60000M / maxBeatLength 269 | 270 | member public this.MinBpm with get() = 271 | let bpmTimingPoints = 272 | (if this.changelist.timingPoints = [] then this.originalFileRepresentation.timingPoints else this.changelist.timingPoints) 273 | |> List.filter isTimingPoint 274 | |> List.map getTimingPoint 275 | |> List.filter (fun tp -> tp.uninherited) 276 | if bpmTimingPoints = [] then 0M else 277 | let maxBeatLengthTimingPoint = bpmTimingPoints |> List.maxBy (fun tp -> tp.beatLength) 278 | 60000M / maxBeatLengthTimingPoint.beatLength 279 | 280 | member public this.MaxBpm with get() = 281 | let bpmTimingPoints = 282 | (if this.changelist.timingPoints = [] then this.originalFileRepresentation.timingPoints else this.changelist.timingPoints) 283 | |> List.filter isTimingPoint 284 | |> List.map getTimingPoint 285 | |> List.filter (fun tp -> tp.uninherited) 286 | if bpmTimingPoints = [] then 0M else 287 | let minBeatLengthTimingPoint = bpmTimingPoints |> List.minBy (fun tp -> tp.beatLength) 288 | 60000M / minBeatLengthTimingPoint.beatLength 289 | 290 | 291 | 292 | (* General and Metadata *) 293 | 294 | member public this.AudioFilename 295 | with get() = this.generalTryGetOr isAudioFilename getAudioFilename "" 296 | and set(x:string) = this.changelist <- {this.changelist with general=(AudioFilename(x)::this.changelist.general)} 297 | 298 | member public this.Mode 299 | with get() = enum(this.generalTryGetOr isMode getMode 0) 300 | and set(x:GameMode) = this.changelist <- {this.changelist with general=(Mode(int x)::this.changelist.general)} 301 | 302 | member public this.Title 303 | with get() = this.metadataTryGetOr isTitle getTitle "" 304 | and set(x:string) = this.changelist <- {this.changelist with metadata=(Title(x)::this.changelist.metadata)} 305 | 306 | member public this.TitleUnicode 307 | with get() = this.metadataTryGetOr isTitleUnicode getTitleUnicode "" 308 | and set(x:string) = this.changelist <- {this.changelist with metadata=(TitleUnicode(x)::this.changelist.metadata)} 309 | 310 | member public this.Artist 311 | with get() = this.metadataTryGetOr isArtist getArtist "" 312 | and set(x:string) = this.changelist <- {this.changelist with metadata=(Artist(x)::this.changelist.metadata)} 313 | 314 | member public this.ArtistUnicode 315 | with get() = this.metadataTryGetOr isArtistUnicode getArtistUnicode "" 316 | and set(x:string) = this.changelist <- {this.changelist with metadata=(ArtistUnicode(x)::this.changelist.metadata)} 317 | 318 | member public this.Creator 319 | with get() = this.metadataTryGetOr isCreator getCreator "" 320 | and set(x:string) = this.changelist <- {this.changelist with metadata=(Creator(x)::this.changelist.metadata)} 321 | 322 | member public this.Version 323 | with get() = this.metadataTryGetOr isVersion getVersion "" 324 | and set(x:string) = this.changelist <- {this.changelist with metadata=(Metadata.Version(x)::this.changelist.metadata)} 325 | 326 | member public this.Source 327 | with get() = this.metadataTryGetOr isSource getSource "" 328 | and set(x:string) = this.changelist <- {this.changelist with metadata=(Source(x)::this.changelist.metadata)} 329 | 330 | member public this.Tags 331 | with get() = ResizeArray (this.metadataTryGetOr isSearchTerms getSearchTerms []) 332 | and set(x:ResizeArray) = this.changelist <- {this.changelist with metadata=(SearchTerms(Seq.toList x)::this.changelist.metadata)} 333 | 334 | member public this.BeatmapID 335 | with get() = this.metadataTryGetOr isBeatmapID getBeatmapID 0 336 | and set(x:int) = this.changelist <- {this.changelist with metadata=(BeatmapID(x)::this.changelist.metadata)} 337 | 338 | 339 | (* Difficulty Settings *) 340 | member public this.HPDrainRate 341 | with get() = this.difficultyTryGetOr isHPDrainRate getHPDrainRate -1M 342 | and set(x:decimal) = this.changelist <- {this.changelist with difficulty=(HPDrainRate(x)::this.changelist.difficulty)} 343 | 344 | member public this.CircleSize 345 | with get() = this.difficultyTryGetOr isCircleSize getCircleSize -1M 346 | and set(x:decimal) = this.changelist <- {this.changelist with difficulty=(CircleSize(x)::this.changelist.difficulty)} 347 | 348 | member public this.OverallDifficulty 349 | with get() = this.difficultyTryGetOr isOverallDifficulty getOverallDifficulty -1M 350 | and set(x:decimal) = this.changelist <- {this.changelist with difficulty=(OverallDifficulty(x)::this.changelist.difficulty)} 351 | 352 | member public this.ApproachRate 353 | with get() = this.difficultyTryGetOr isApproachRate getApproachRate -1M 354 | and set(x:decimal) = this.changelist <- {this.changelist with difficulty=(ApproachRate(x)::this.changelist.difficulty)} 355 | 356 | member public this.SliderTickRate 357 | with get() = this.difficultyTryGetOr isSliderTickRate getSliderTickRate -1M 358 | and set(x:decimal) = this.changelist <- {this.changelist with difficulty=(SliderTickRate(x)::this.changelist.difficulty)} 359 | 360 | 361 | (* Events *) 362 | member public this.Background 363 | with get() = 364 | match this.originalFileRepresentation.events |> List.tryFind isBackground with 365 | | Some x -> (getBackgroundFilename x) 366 | | None -> "" 367 | 368 | (* Other *) 369 | member public this.HitObjectCount 370 | with get() = this.originalFileRepresentation.hitObjects 371 | |> List.filter (function 372 | | Comment _ -> false 373 | | _ -> true) 374 | |> List.length 375 | 376 | member public this.FirstHitObjectTime 377 | with get() = 378 | let hitObjects = this.originalFileRepresentation.hitObjects 379 | |> List.filter (function 380 | | Comment _ -> false 381 | | _ -> true) 382 | match hitObjects.Head with 383 | | HitCircle o -> o.time 384 | | Slider o -> o.time 385 | | Spinner o -> o.time 386 | | Hold o -> o.time 387 | | _ -> 0 388 | -------------------------------------------------------------------------------- /MainForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace Circle_Tracker 2 | { 3 | partial class MainForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.components = new System.ComponentModel.Container(); 32 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); 33 | this.label1 = new System.Windows.Forms.Label(); 34 | this.hitsTextBox = new System.Windows.Forms.TextBox(); 35 | this.label2 = new System.Windows.Forms.Label(); 36 | this.timeTextBox = new System.Windows.Forms.TextBox(); 37 | this.label3 = new System.Windows.Forms.Label(); 38 | this.songsFolderTextBox = new System.Windows.Forms.TextBox(); 39 | this.label4 = new System.Windows.Forms.Label(); 40 | this.beatmapTextBox = new System.Windows.Forms.TextBox(); 41 | this.aimLabel = new System.Windows.Forms.Label(); 42 | this.starsLabel = new System.Windows.Forms.Label(); 43 | this.aimTextBox = new System.Windows.Forms.TextBox(); 44 | this.starsTextBox = new System.Windows.Forms.TextBox(); 45 | this.speedLabel = new System.Windows.Forms.Label(); 46 | this.speedTextBox = new System.Windows.Forms.TextBox(); 47 | this.ConnectApiButton = new System.Windows.Forms.Button(); 48 | this.groupBox1 = new System.Windows.Forms.GroupBox(); 49 | this.credentialsLabel = new System.Windows.Forms.Label(); 50 | this.altSepCheckBox = new System.Windows.Forms.CheckBox(); 51 | this.soundEnabledCheckbox = new System.Windows.Forms.CheckBox(); 52 | this.label8 = new System.Windows.Forms.Label(); 53 | this.statusLabel = new System.Windows.Forms.Label(); 54 | this.label6 = new System.Windows.Forms.Label(); 55 | this.label7 = new System.Windows.Forms.Label(); 56 | this.label5 = new System.Windows.Forms.Label(); 57 | this.sheetNameTextBox = new System.Windows.Forms.TextBox(); 58 | this.spreadsheetIdTextBox = new System.Windows.Forms.TextBox(); 59 | this.groupBox2 = new System.Windows.Forms.GroupBox(); 60 | this.modsTextBox = new System.Windows.Forms.TextBox(); 61 | this.textBoxCS = new System.Windows.Forms.TextBox(); 62 | this.accLabel = new System.Windows.Forms.Label(); 63 | this.label12 = new System.Windows.Forms.Label(); 64 | this.label13 = new System.Windows.Forms.Label(); 65 | this.label11 = new System.Windows.Forms.Label(); 66 | this.bpmTextBox = new System.Windows.Forms.TextBox(); 67 | this.textBoxOD = new System.Windows.Forms.TextBox(); 68 | this.label10 = new System.Windows.Forms.Label(); 69 | this.label9 = new System.Windows.Forms.Label(); 70 | this.textBoxAR = new System.Windows.Forms.TextBox(); 71 | this.accTextBox = new System.Windows.Forms.TextBox(); 72 | this.panel1 = new System.Windows.Forms.Panel(); 73 | this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); 74 | this.updateGameVariablesTimer = new System.Windows.Forms.Timer(this.components); 75 | this.timer1 = new System.Windows.Forms.Timer(this.components); 76 | this.startupCheckBox = new System.Windows.Forms.CheckBox(); 77 | this.notifyIcon = new System.Windows.Forms.NotifyIcon(this.components); 78 | this.button1 = new System.Windows.Forms.Button(); 79 | this.updateFormTimer = new System.Windows.Forms.Timer(this.components); 80 | this.secondsCounterTimer = new System.Windows.Forms.Timer(this.components); 81 | this.timeLabel = new System.Windows.Forms.Label(); 82 | this.groupBox1.SuspendLayout(); 83 | this.groupBox2.SuspendLayout(); 84 | this.panel1.SuspendLayout(); 85 | this.tableLayoutPanel1.SuspendLayout(); 86 | this.SuspendLayout(); 87 | // 88 | // label1 89 | // 90 | this.label1.AutoSize = true; 91 | this.label1.Location = new System.Drawing.Point(15, 160); 92 | this.label1.Name = "label1"; 93 | this.label1.Size = new System.Drawing.Size(23, 13); 94 | this.label1.TabIndex = 0; 95 | this.label1.Text = "hits"; 96 | // 97 | // hitsTextBox 98 | // 99 | this.hitsTextBox.Location = new System.Drawing.Point(54, 157); 100 | this.hitsTextBox.Name = "hitsTextBox"; 101 | this.hitsTextBox.ReadOnly = true; 102 | this.hitsTextBox.Size = new System.Drawing.Size(100, 20); 103 | this.hitsTextBox.TabIndex = 1; 104 | // 105 | // label2 106 | // 107 | this.label2.AutoSize = true; 108 | this.label2.Location = new System.Drawing.Point(15, 134); 109 | this.label2.Name = "label2"; 110 | this.label2.Size = new System.Drawing.Size(26, 13); 111 | this.label2.TabIndex = 0; 112 | this.label2.Text = "time"; 113 | // 114 | // timeTextBox 115 | // 116 | this.timeTextBox.Location = new System.Drawing.Point(54, 131); 117 | this.timeTextBox.Name = "timeTextBox"; 118 | this.timeTextBox.ReadOnly = true; 119 | this.timeTextBox.Size = new System.Drawing.Size(64, 20); 120 | this.timeTextBox.TabIndex = 1; 121 | // 122 | // label3 123 | // 124 | this.label3.AutoSize = true; 125 | this.label3.Location = new System.Drawing.Point(6, 9); 126 | this.label3.Margin = new System.Windows.Forms.Padding(3, 6, 3, 0); 127 | this.label3.Name = "label3"; 128 | this.label3.Size = new System.Drawing.Size(64, 13); 129 | this.label3.TabIndex = 0; 130 | this.label3.Text = "songs folder"; 131 | // 132 | // songsFolderTextBox 133 | // 134 | this.songsFolderTextBox.Dock = System.Windows.Forms.DockStyle.Fill; 135 | this.songsFolderTextBox.Location = new System.Drawing.Point(76, 6); 136 | this.songsFolderTextBox.Name = "songsFolderTextBox"; 137 | this.songsFolderTextBox.Size = new System.Drawing.Size(457, 20); 138 | this.songsFolderTextBox.TabIndex = 1; 139 | this.songsFolderTextBox.TextChanged += new System.EventHandler(this.songsFolderTextBox_TextChanged); 140 | // 141 | // label4 142 | // 143 | this.label4.AutoSize = true; 144 | this.label4.Location = new System.Drawing.Point(6, 34); 145 | this.label4.Margin = new System.Windows.Forms.Padding(3, 6, 3, 0); 146 | this.label4.Name = "label4"; 147 | this.label4.Size = new System.Drawing.Size(48, 13); 148 | this.label4.TabIndex = 0; 149 | this.label4.Text = "beatmap"; 150 | // 151 | // beatmapTextBox 152 | // 153 | this.beatmapTextBox.BackColor = System.Drawing.SystemColors.Control; 154 | this.beatmapTextBox.Dock = System.Windows.Forms.DockStyle.Fill; 155 | this.beatmapTextBox.Location = new System.Drawing.Point(76, 31); 156 | this.beatmapTextBox.Name = "beatmapTextBox"; 157 | this.beatmapTextBox.ReadOnly = true; 158 | this.beatmapTextBox.Size = new System.Drawing.Size(457, 20); 159 | this.beatmapTextBox.TabIndex = 1; 160 | this.beatmapTextBox.TextChanged += new System.EventHandler(this.songsFolderTextBox_TextChanged); 161 | // 162 | // aimLabel 163 | // 164 | this.aimLabel.AutoSize = true; 165 | this.aimLabel.Location = new System.Drawing.Point(15, 73); 166 | this.aimLabel.Name = "aimLabel"; 167 | this.aimLabel.Size = new System.Drawing.Size(23, 13); 168 | this.aimLabel.TabIndex = 0; 169 | this.aimLabel.Text = "aim"; 170 | // 171 | // starsLabel 172 | // 173 | this.starsLabel.AutoSize = true; 174 | this.starsLabel.Location = new System.Drawing.Point(15, 47); 175 | this.starsLabel.Name = "starsLabel"; 176 | this.starsLabel.Size = new System.Drawing.Size(29, 13); 177 | this.starsLabel.TabIndex = 0; 178 | this.starsLabel.Text = "stars"; 179 | // 180 | // aimTextBox 181 | // 182 | this.aimTextBox.Location = new System.Drawing.Point(54, 70); 183 | this.aimTextBox.Name = "aimTextBox"; 184 | this.aimTextBox.ReadOnly = true; 185 | this.aimTextBox.Size = new System.Drawing.Size(39, 20); 186 | this.aimTextBox.TabIndex = 1; 187 | // 188 | // starsTextBox 189 | // 190 | this.starsTextBox.Location = new System.Drawing.Point(54, 44); 191 | this.starsTextBox.Name = "starsTextBox"; 192 | this.starsTextBox.ReadOnly = true; 193 | this.starsTextBox.Size = new System.Drawing.Size(39, 20); 194 | this.starsTextBox.TabIndex = 1; 195 | // 196 | // speedLabel 197 | // 198 | this.speedLabel.AutoSize = true; 199 | this.speedLabel.Location = new System.Drawing.Point(15, 98); 200 | this.speedLabel.Name = "speedLabel"; 201 | this.speedLabel.Size = new System.Drawing.Size(36, 13); 202 | this.speedLabel.TabIndex = 0; 203 | this.speedLabel.Text = "speed"; 204 | // 205 | // speedTextBox 206 | // 207 | this.speedTextBox.Location = new System.Drawing.Point(54, 95); 208 | this.speedTextBox.Name = "speedTextBox"; 209 | this.speedTextBox.ReadOnly = true; 210 | this.speedTextBox.Size = new System.Drawing.Size(39, 20); 211 | this.speedTextBox.TabIndex = 1; 212 | // 213 | // ConnectApiButton 214 | // 215 | this.ConnectApiButton.Location = new System.Drawing.Point(6, 179); 216 | this.ConnectApiButton.Name = "ConnectApiButton"; 217 | this.ConnectApiButton.Size = new System.Drawing.Size(284, 46); 218 | this.ConnectApiButton.TabIndex = 2; 219 | this.ConnectApiButton.Text = "Connect Google Sheets API"; 220 | this.ConnectApiButton.UseVisualStyleBackColor = true; 221 | this.ConnectApiButton.Click += new System.EventHandler(this.ConnectApiButton_Click); 222 | // 223 | // groupBox1 224 | // 225 | this.groupBox1.Controls.Add(this.credentialsLabel); 226 | this.groupBox1.Controls.Add(this.altSepCheckBox); 227 | this.groupBox1.Controls.Add(this.soundEnabledCheckbox); 228 | this.groupBox1.Controls.Add(this.label8); 229 | this.groupBox1.Controls.Add(this.statusLabel); 230 | this.groupBox1.Controls.Add(this.label6); 231 | this.groupBox1.Controls.Add(this.label7); 232 | this.groupBox1.Controls.Add(this.label5); 233 | this.groupBox1.Controls.Add(this.sheetNameTextBox); 234 | this.groupBox1.Controls.Add(this.spreadsheetIdTextBox); 235 | this.groupBox1.Controls.Add(this.ConnectApiButton); 236 | this.groupBox1.Location = new System.Drawing.Point(9, 62); 237 | this.groupBox1.Name = "groupBox1"; 238 | this.groupBox1.Size = new System.Drawing.Size(297, 237); 239 | this.groupBox1.TabIndex = 3; 240 | this.groupBox1.TabStop = false; 241 | this.groupBox1.Text = "Google Sheets Integration"; 242 | // 243 | // credentialsLabel 244 | // 245 | this.credentialsLabel.AutoSize = true; 246 | this.credentialsLabel.ForeColor = System.Drawing.Color.Red; 247 | this.credentialsLabel.Location = new System.Drawing.Point(245, 25); 248 | this.credentialsLabel.Name = "credentialsLabel"; 249 | this.credentialsLabel.Size = new System.Drawing.Size(42, 13); 250 | this.credentialsLabel.TabIndex = 6; 251 | this.credentialsLabel.Text = "Missing"; 252 | // 253 | // altSepCheckBox 254 | // 255 | this.altSepCheckBox.AutoSize = true; 256 | this.altSepCheckBox.Location = new System.Drawing.Point(6, 156); 257 | this.altSepCheckBox.Name = "altSepCheckBox"; 258 | this.altSepCheckBox.Size = new System.Drawing.Size(248, 17); 259 | this.altSepCheckBox.TabIndex = 6; 260 | this.altSepCheckBox.Text = "Toggle this to fix #ERROR! in Beatmap column"; 261 | this.altSepCheckBox.UseVisualStyleBackColor = true; 262 | this.altSepCheckBox.CheckedChanged += new System.EventHandler(this.altSepCheckBox_CheckedChanged); 263 | // 264 | // soundEnabledCheckbox 265 | // 266 | this.soundEnabledCheckbox.AutoSize = true; 267 | this.soundEnabledCheckbox.Location = new System.Drawing.Point(6, 138); 268 | this.soundEnabledCheckbox.Name = "soundEnabledCheckbox"; 269 | this.soundEnabledCheckbox.Size = new System.Drawing.Size(198, 17); 270 | this.soundEnabledCheckbox.TabIndex = 6; 271 | this.soundEnabledCheckbox.Text = "Play sound when writing a new entry"; 272 | this.soundEnabledCheckbox.UseVisualStyleBackColor = true; 273 | this.soundEnabledCheckbox.CheckedChanged += new System.EventHandler(this.soundEnabledCheckbox_CheckedChanged); 274 | // 275 | // label8 276 | // 277 | this.label8.AutoSize = true; 278 | this.label8.Location = new System.Drawing.Point(162, 25); 279 | this.label8.Name = "label8"; 280 | this.label8.Size = new System.Drawing.Size(83, 13); 281 | this.label8.TabIndex = 5; 282 | this.label8.Text = "credentials.json:"; 283 | // 284 | // statusLabel 285 | // 286 | this.statusLabel.AutoSize = true; 287 | this.statusLabel.ForeColor = System.Drawing.Color.Red; 288 | this.statusLabel.Location = new System.Drawing.Point(45, 25); 289 | this.statusLabel.Name = "statusLabel"; 290 | this.statusLabel.Size = new System.Drawing.Size(78, 13); 291 | this.statusLabel.TabIndex = 6; 292 | this.statusLabel.Text = "Not connected"; 293 | // 294 | // label6 295 | // 296 | this.label6.AutoSize = true; 297 | this.label6.Location = new System.Drawing.Point(6, 25); 298 | this.label6.Name = "label6"; 299 | this.label6.Size = new System.Drawing.Size(43, 13); 300 | this.label6.TabIndex = 5; 301 | this.label6.Text = "Status: "; 302 | // 303 | // label7 304 | // 305 | this.label7.AutoSize = true; 306 | this.label7.Location = new System.Drawing.Point(6, 90); 307 | this.label7.Name = "label7"; 308 | this.label7.Size = new System.Drawing.Size(66, 13); 309 | this.label7.TabIndex = 4; 310 | this.label7.Text = "Sheet Name"; 311 | // 312 | // label5 313 | // 314 | this.label5.AutoSize = true; 315 | this.label5.Location = new System.Drawing.Point(6, 47); 316 | this.label5.Name = "label5"; 317 | this.label5.Size = new System.Drawing.Size(81, 13); 318 | this.label5.TabIndex = 4; 319 | this.label5.Text = "Spreadsheet ID"; 320 | // 321 | // sheetNameTextBox 322 | // 323 | this.sheetNameTextBox.Location = new System.Drawing.Point(6, 107); 324 | this.sheetNameTextBox.Name = "sheetNameTextBox"; 325 | this.sheetNameTextBox.Size = new System.Drawing.Size(284, 20); 326 | this.sheetNameTextBox.TabIndex = 3; 327 | this.sheetNameTextBox.TextChanged += new System.EventHandler(this.sheetNameTextBox_TextChanged); 328 | // 329 | // spreadsheetIdTextBox 330 | // 331 | this.spreadsheetIdTextBox.Location = new System.Drawing.Point(6, 64); 332 | this.spreadsheetIdTextBox.Name = "spreadsheetIdTextBox"; 333 | this.spreadsheetIdTextBox.Size = new System.Drawing.Size(284, 20); 334 | this.spreadsheetIdTextBox.TabIndex = 3; 335 | this.spreadsheetIdTextBox.TextChanged += new System.EventHandler(this.spreadsheetIdTextBox_TextChanged); 336 | // 337 | // groupBox2 338 | // 339 | this.groupBox2.Controls.Add(this.modsTextBox); 340 | this.groupBox2.Controls.Add(this.textBoxCS); 341 | this.groupBox2.Controls.Add(this.starsTextBox); 342 | this.groupBox2.Controls.Add(this.accLabel); 343 | this.groupBox2.Controls.Add(this.label1); 344 | this.groupBox2.Controls.Add(this.label2); 345 | this.groupBox2.Controls.Add(this.label12); 346 | this.groupBox2.Controls.Add(this.aimLabel); 347 | this.groupBox2.Controls.Add(this.label13); 348 | this.groupBox2.Controls.Add(this.label11); 349 | this.groupBox2.Controls.Add(this.speedLabel); 350 | this.groupBox2.Controls.Add(this.bpmTextBox); 351 | this.groupBox2.Controls.Add(this.textBoxOD); 352 | this.groupBox2.Controls.Add(this.speedTextBox); 353 | this.groupBox2.Controls.Add(this.label10); 354 | this.groupBox2.Controls.Add(this.label9); 355 | this.groupBox2.Controls.Add(this.textBoxAR); 356 | this.groupBox2.Controls.Add(this.starsLabel); 357 | this.groupBox2.Controls.Add(this.accTextBox); 358 | this.groupBox2.Controls.Add(this.aimTextBox); 359 | this.groupBox2.Controls.Add(this.hitsTextBox); 360 | this.groupBox2.Controls.Add(this.timeTextBox); 361 | this.groupBox2.Location = new System.Drawing.Point(321, 62); 362 | this.groupBox2.Name = "groupBox2"; 363 | this.groupBox2.Size = new System.Drawing.Size(204, 211); 364 | this.groupBox2.TabIndex = 4; 365 | this.groupBox2.TabStop = false; 366 | this.groupBox2.Text = "Beatmap Info"; 367 | // 368 | // modsTextBox 369 | // 370 | this.modsTextBox.Location = new System.Drawing.Point(54, 18); 371 | this.modsTextBox.Name = "modsTextBox"; 372 | this.modsTextBox.ReadOnly = true; 373 | this.modsTextBox.Size = new System.Drawing.Size(86, 20); 374 | this.modsTextBox.TabIndex = 1; 375 | // 376 | // textBoxCS 377 | // 378 | this.textBoxCS.Location = new System.Drawing.Point(142, 44); 379 | this.textBoxCS.Name = "textBoxCS"; 380 | this.textBoxCS.ReadOnly = true; 381 | this.textBoxCS.Size = new System.Drawing.Size(39, 20); 382 | this.textBoxCS.TabIndex = 1; 383 | // 384 | // accLabel 385 | // 386 | this.accLabel.AutoSize = true; 387 | this.accLabel.Location = new System.Drawing.Point(15, 186); 388 | this.accLabel.Name = "accLabel"; 389 | this.accLabel.Size = new System.Drawing.Size(25, 13); 390 | this.accLabel.TabIndex = 0; 391 | this.accLabel.Text = "acc"; 392 | // 393 | // label12 394 | // 395 | this.label12.AutoSize = true; 396 | this.label12.Location = new System.Drawing.Point(108, 73); 397 | this.label12.Name = "label12"; 398 | this.label12.Size = new System.Drawing.Size(22, 13); 399 | this.label12.TabIndex = 0; 400 | this.label12.Text = "AR"; 401 | // 402 | // label13 403 | // 404 | this.label13.AutoSize = true; 405 | this.label13.Location = new System.Drawing.Point(125, 134); 406 | this.label13.Name = "label13"; 407 | this.label13.Size = new System.Drawing.Size(27, 13); 408 | this.label13.TabIndex = 0; 409 | this.label13.Text = "bpm"; 410 | // 411 | // label11 412 | // 413 | this.label11.AutoSize = true; 414 | this.label11.Location = new System.Drawing.Point(108, 98); 415 | this.label11.Name = "label11"; 416 | this.label11.Size = new System.Drawing.Size(23, 13); 417 | this.label11.TabIndex = 0; 418 | this.label11.Text = "OD"; 419 | // 420 | // bpmTextBox 421 | // 422 | this.bpmTextBox.Location = new System.Drawing.Point(159, 131); 423 | this.bpmTextBox.Name = "bpmTextBox"; 424 | this.bpmTextBox.ReadOnly = true; 425 | this.bpmTextBox.Size = new System.Drawing.Size(39, 20); 426 | this.bpmTextBox.TabIndex = 1; 427 | // 428 | // textBoxOD 429 | // 430 | this.textBoxOD.Location = new System.Drawing.Point(142, 95); 431 | this.textBoxOD.Name = "textBoxOD"; 432 | this.textBoxOD.ReadOnly = true; 433 | this.textBoxOD.Size = new System.Drawing.Size(39, 20); 434 | this.textBoxOD.TabIndex = 1; 435 | // 436 | // label10 437 | // 438 | this.label10.AutoSize = true; 439 | this.label10.Location = new System.Drawing.Point(108, 47); 440 | this.label10.Name = "label10"; 441 | this.label10.Size = new System.Drawing.Size(21, 13); 442 | this.label10.TabIndex = 0; 443 | this.label10.Text = "CS"; 444 | // 445 | // label9 446 | // 447 | this.label9.AutoSize = true; 448 | this.label9.Location = new System.Drawing.Point(15, 21); 449 | this.label9.Name = "label9"; 450 | this.label9.Size = new System.Drawing.Size(32, 13); 451 | this.label9.TabIndex = 0; 452 | this.label9.Text = "mods"; 453 | // 454 | // textBoxAR 455 | // 456 | this.textBoxAR.Location = new System.Drawing.Point(142, 70); 457 | this.textBoxAR.Name = "textBoxAR"; 458 | this.textBoxAR.ReadOnly = true; 459 | this.textBoxAR.Size = new System.Drawing.Size(39, 20); 460 | this.textBoxAR.TabIndex = 1; 461 | // 462 | // accTextBox 463 | // 464 | this.accTextBox.Location = new System.Drawing.Point(54, 183); 465 | this.accTextBox.Name = "accTextBox"; 466 | this.accTextBox.ReadOnly = true; 467 | this.accTextBox.Size = new System.Drawing.Size(100, 20); 468 | this.accTextBox.TabIndex = 1; 469 | // 470 | // panel1 471 | // 472 | this.panel1.Controls.Add(this.tableLayoutPanel1); 473 | this.panel1.Dock = System.Windows.Forms.DockStyle.Top; 474 | this.panel1.Location = new System.Drawing.Point(0, 0); 475 | this.panel1.Name = "panel1"; 476 | this.panel1.Size = new System.Drawing.Size(539, 56); 477 | this.panel1.TabIndex = 5; 478 | // 479 | // tableLayoutPanel1 480 | // 481 | this.tableLayoutPanel1.ColumnCount = 2; 482 | this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 70F)); 483 | this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); 484 | this.tableLayoutPanel1.Controls.Add(this.songsFolderTextBox, 1, 0); 485 | this.tableLayoutPanel1.Controls.Add(this.label3, 0, 0); 486 | this.tableLayoutPanel1.Controls.Add(this.beatmapTextBox, 1, 1); 487 | this.tableLayoutPanel1.Controls.Add(this.label4, 0, 1); 488 | this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; 489 | this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); 490 | this.tableLayoutPanel1.Name = "tableLayoutPanel1"; 491 | this.tableLayoutPanel1.Padding = new System.Windows.Forms.Padding(3); 492 | this.tableLayoutPanel1.RowCount = 2; 493 | this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); 494 | this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 50F)); 495 | this.tableLayoutPanel1.Size = new System.Drawing.Size(539, 56); 496 | this.tableLayoutPanel1.TabIndex = 0; 497 | // 498 | // updateGameVariablesTimer 499 | // 500 | this.updateGameVariablesTimer.Interval = 500; 501 | this.updateGameVariablesTimer.Tick += new System.EventHandler(this.updateGameVariablesTimer_Tick); 502 | // 503 | // startupCheckBox 504 | // 505 | this.startupCheckBox.AutoSize = true; 506 | this.startupCheckBox.Location = new System.Drawing.Point(321, 314); 507 | this.startupCheckBox.Name = "startupCheckBox"; 508 | this.startupCheckBox.Size = new System.Drawing.Size(161, 17); 509 | this.startupCheckBox.TabIndex = 6; 510 | this.startupCheckBox.Text = "Launch on Windows Startup"; 511 | this.startupCheckBox.UseVisualStyleBackColor = true; 512 | this.startupCheckBox.CheckedChanged += new System.EventHandler(this.startupCheckBox_CheckedChanged); 513 | // 514 | // notifyIcon 515 | // 516 | this.notifyIcon.BalloonTipIcon = System.Windows.Forms.ToolTipIcon.Info; 517 | this.notifyIcon.BalloonTipText = "Info"; 518 | this.notifyIcon.BalloonTipTitle = "Hey"; 519 | this.notifyIcon.Icon = ((System.Drawing.Icon)(resources.GetObject("notifyIcon.Icon"))); 520 | this.notifyIcon.Text = "Circle Tracker"; 521 | this.notifyIcon.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.notifyIcon_MouseDoubleClick); 522 | // 523 | // button1 524 | // 525 | this.button1.Location = new System.Drawing.Point(321, 285); 526 | this.button1.Name = "button1"; 527 | this.button1.Size = new System.Drawing.Size(204, 23); 528 | this.button1.TabIndex = 7; 529 | this.button1.Text = "Minimize to Tray"; 530 | this.button1.UseVisualStyleBackColor = true; 531 | this.button1.Click += new System.EventHandler(this.MinimizeToTray); 532 | // 533 | // updateFormTimer 534 | // 535 | this.updateFormTimer.Interval = 60; 536 | this.updateFormTimer.Tick += new System.EventHandler(this.updateFormTimer_Tick); 537 | // 538 | // secondsCounterTimer 539 | // 540 | this.secondsCounterTimer.Interval = 1000; 541 | this.secondsCounterTimer.Tick += new System.EventHandler(this.secondsCounterTimer_Tick); 542 | // 543 | // timeLabel 544 | // 545 | this.timeLabel.AutoSize = true; 546 | this.timeLabel.Location = new System.Drawing.Point(9, 314); 547 | this.timeLabel.Name = "timeLabel"; 548 | this.timeLabel.Size = new System.Drawing.Size(122, 13); 549 | this.timeLabel.TabIndex = 8; 550 | this.timeLabel.Text = "Playing: Idle: Efficiency: "; 551 | // 552 | // MainForm 553 | // 554 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 555 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 556 | this.BackColor = System.Drawing.SystemColors.Control; 557 | this.ClientSize = new System.Drawing.Size(539, 338); 558 | this.Controls.Add(this.timeLabel); 559 | this.Controls.Add(this.button1); 560 | this.Controls.Add(this.panel1); 561 | this.Controls.Add(this.startupCheckBox); 562 | this.Controls.Add(this.groupBox2); 563 | this.Controls.Add(this.groupBox1); 564 | this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); 565 | this.Name = "MainForm"; 566 | this.Text = "Circle Tracker"; 567 | this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing); 568 | this.groupBox1.ResumeLayout(false); 569 | this.groupBox1.PerformLayout(); 570 | this.groupBox2.ResumeLayout(false); 571 | this.groupBox2.PerformLayout(); 572 | this.panel1.ResumeLayout(false); 573 | this.tableLayoutPanel1.ResumeLayout(false); 574 | this.tableLayoutPanel1.PerformLayout(); 575 | this.ResumeLayout(false); 576 | this.PerformLayout(); 577 | 578 | } 579 | 580 | #endregion 581 | 582 | private System.Windows.Forms.Label label1; 583 | private System.Windows.Forms.TextBox hitsTextBox; 584 | private System.Windows.Forms.Label label2; 585 | private System.Windows.Forms.TextBox timeTextBox; 586 | private System.Windows.Forms.Label label3; 587 | private System.Windows.Forms.TextBox songsFolderTextBox; 588 | private System.Windows.Forms.Label label4; 589 | private System.Windows.Forms.TextBox beatmapTextBox; 590 | private System.Windows.Forms.Label aimLabel; 591 | private System.Windows.Forms.Label starsLabel; 592 | private System.Windows.Forms.TextBox aimTextBox; 593 | private System.Windows.Forms.TextBox starsTextBox; 594 | private System.Windows.Forms.Label speedLabel; 595 | private System.Windows.Forms.TextBox speedTextBox; 596 | private System.Windows.Forms.Button ConnectApiButton; 597 | private System.Windows.Forms.GroupBox groupBox1; 598 | private System.Windows.Forms.Label statusLabel; 599 | private System.Windows.Forms.Label label6; 600 | private System.Windows.Forms.Label label5; 601 | private System.Windows.Forms.TextBox spreadsheetIdTextBox; 602 | private System.Windows.Forms.Label label7; 603 | private System.Windows.Forms.TextBox sheetNameTextBox; 604 | private System.Windows.Forms.GroupBox groupBox2; 605 | private System.Windows.Forms.Panel panel1; 606 | private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; 607 | private System.Windows.Forms.Label credentialsLabel; 608 | private System.Windows.Forms.Label label8; 609 | private System.Windows.Forms.TextBox modsTextBox; 610 | private System.Windows.Forms.Label label9; 611 | private System.Windows.Forms.CheckBox soundEnabledCheckbox; 612 | private System.Windows.Forms.Timer updateGameVariablesTimer; 613 | private System.Windows.Forms.TextBox textBoxCS; 614 | private System.Windows.Forms.Label label12; 615 | private System.Windows.Forms.Label label11; 616 | private System.Windows.Forms.TextBox textBoxOD; 617 | private System.Windows.Forms.Label label10; 618 | private System.Windows.Forms.TextBox textBoxAR; 619 | private System.Windows.Forms.Timer timer1; 620 | private System.Windows.Forms.CheckBox startupCheckBox; 621 | private System.Windows.Forms.NotifyIcon notifyIcon; 622 | private System.Windows.Forms.Button button1; 623 | private System.Windows.Forms.Timer updateFormTimer; 624 | private System.Windows.Forms.Label accLabel; 625 | private System.Windows.Forms.TextBox accTextBox; 626 | private System.Windows.Forms.CheckBox altSepCheckBox; 627 | private System.Windows.Forms.Label label13; 628 | private System.Windows.Forms.TextBox bpmTextBox; 629 | private System.Windows.Forms.Timer secondsCounterTimer; 630 | private System.Windows.Forms.Label timeLabel; 631 | } 632 | } 633 | 634 | -------------------------------------------------------------------------------- /Tracker.cs: -------------------------------------------------------------------------------- 1 | using FsBeatmapProcessor; 2 | using Google; 3 | using Google.Apis.Auth.OAuth2; 4 | using Google.Apis.Sheets.v4; 5 | using Google.Apis.Sheets.v4.Data; 6 | using Newtonsoft.Json.Linq; 7 | using OsuMemoryDataProvider; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Diagnostics; 11 | using System.Drawing; 12 | using System.Globalization; 13 | using System.IO; 14 | using System.Linq; 15 | using System.Media; 16 | using System.Security.Cryptography.X509Certificates; 17 | using System.Text; 18 | using System.Text.RegularExpressions; 19 | using System.Threading.Tasks; 20 | using System.Windows.Forms; 21 | 22 | namespace Circle_Tracker 23 | { 24 | class Tracker 25 | { 26 | private readonly IOsuMemoryReader osuReader; 27 | private MainForm form; 28 | 29 | 30 | private static List<(string, string)> DataRanges = new List<(string, string)>() 31 | { 32 | ("Date and Time", "play_date"), 33 | ("Beatmap", "beatmap_string"), 34 | ("HD", "HD"), 35 | ("HR", "HR"), 36 | ("DT", "DT"), 37 | ("BPM", "bpm"), 38 | ("Aim", "aim"), 39 | ("Speed", "speed"), 40 | ("Stars", "stars"), 41 | ("CS", "CS"), 42 | ("AR", "AR"), 43 | ("OD", "OD"), 44 | ("Objects Hit", "hits"), 45 | ("Acc", "acc"), 46 | ("300s", "num300s"), 47 | ("100s", "num100s"), 48 | ("50s", "num50s"), 49 | ("Miss", "misses"), 50 | ("EZ", "EZ"), 51 | ("HT", "HT"), 52 | ("FL", "FL"), 53 | ("Map Complete", "complete"), 54 | ("Playcount", "playcount"), 55 | ("Time (s)", "time_seconds") 56 | }; 57 | 58 | public int IdleSeconds = 0; 59 | public int PlayingSeconds = 0; 60 | 61 | // Beatmap 62 | public string SongsFolder { get; set; } 63 | private Beatmap currentBeatmap; 64 | public string BeatmapPath { get; set; } 65 | public int BeatmapID { get; set; } 66 | public int BeatmapSetID { get; set; } 67 | public string BeatmapString { get; set; } 68 | public int BeatmapBpm { get; set; } 69 | 70 | // Sound 71 | private string soundFilename; 72 | public bool SubmitSoundEnabled { get; set; } 73 | 74 | // game variables 75 | public bool IsReplay { get; set; } = false; 76 | public bool MemoryReadError { get; set; } = false; 77 | public OsuMemoryStatus GameState { get; set; } 78 | public string Username { get; set; } 79 | public string PlayingUser { get; set; } 80 | public int RawMods { get; set; } = 0; 81 | public bool Hidden { get; set; } = false; 82 | public bool Hardrock { get; set; } = false; 83 | public bool Doubletime { get; set; } = false; 84 | public bool EZ { get; set; } = false; 85 | public bool Halftime { get; set; } = false; 86 | public bool Flashlight { get; set; } = false; 87 | public bool Auto { get; set; } = false; 88 | 89 | public decimal BeatmapStars { get; private set; } 90 | public decimal BeatmapAim { get; private set; } 91 | public decimal BeatmapSpeed { get; private set; } 92 | public decimal BeatmapCs { get; private set; } 93 | public decimal BeatmapAr { get; private set; } 94 | public decimal BeatmapOd { get; private set; } 95 | public int Play300c { get; set; } = 0; 96 | public int Play100c { get; set; } = 0; 97 | public int Play50c { get; set; } = 0; 98 | public int PlayMissc { get; set; } = 0; 99 | public int TotalBeatmapHits { get; set; } = 0; 100 | public decimal LastPostedAcc { get; set; } = 0; 101 | public decimal Accuracy { get; set; } = 0; 102 | public int Time { get; set; } = 0; 103 | 104 | // Google Sheets API 105 | private DateTime LastPostTime { get; set; } 106 | private bool TickLock { get; set; } = false; 107 | private bool SpreadsheetTimezoneVerified { get; set; } = false; 108 | public bool SheetsApiReady { get; set; } = false; 109 | public bool UseAltFuncSeparator { get; set; } = false; 110 | public string SpreadsheetId { get; set; } 111 | public string SheetName { get; set; } 112 | public int SheetRows { get; set; } 113 | SheetsService GoogleSheetsService; 114 | Spreadsheet UserSpreadsheet = null; 115 | Sheet RawDataSheet = null; 116 | 117 | public Tracker(MainForm f) 118 | { 119 | form = f; 120 | LoadSettings(); 121 | osuReader = OsuMemoryReader.Instance.GetInstanceForWindowTitleHint(""); 122 | int _; 123 | GameState = osuReader.GetCurrentStatus(out _); 124 | soundFilename = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "assets", "sectionpass.wav"); 125 | LastPostTime = DateTime.Now; 126 | 127 | // First time running circle tracker 128 | if (!File.Exists("user_settings.txt")) 129 | { 130 | MessageBox.Show($"Thank you for trying out circle tracker!{Environment.NewLine}" + 131 | $"For instructions on how to get set up, watch my tutorial video on Youtube. " + 132 | $"If you need help with anything, feel free to message FunOrange on osu!, twitter, discord, etc.", 133 | "Welcome!"); 134 | } 135 | } 136 | 137 | public void SaveSettings() 138 | { 139 | string[] lines = { 140 | SongsFolder, 141 | SpreadsheetId, 142 | SheetName, 143 | SubmitSoundEnabled ? "1" : "0", 144 | SpreadsheetTimezoneVerified ? "1" : "0", 145 | UseAltFuncSeparator ? "1" : "0", 146 | Username 147 | }; 148 | File.WriteAllLines("user_settings.txt", lines, Encoding.UTF8); 149 | } 150 | private void LoadSettings() 151 | { 152 | // Defaults 153 | SongsFolder = ""; 154 | SpreadsheetId = ""; 155 | SheetName = "Raw Data"; 156 | SubmitSoundEnabled = true; 157 | SpreadsheetTimezoneVerified = false; 158 | UseAltFuncSeparator = false; 159 | Username = ""; 160 | 161 | // Load Settings 162 | if (File.Exists("user_settings.txt")) 163 | { 164 | var lines = File.ReadAllLines("user_settings.txt"); 165 | for (int i = 0; i < lines.Length; i++) 166 | { 167 | switch (i) 168 | { 169 | case 0: SongsFolder = lines[0]; break; 170 | case 1: SpreadsheetId = lines[1]; break; 171 | case 2: SheetName = lines[2]; break; 172 | case 3: SubmitSoundEnabled = lines[3] == "1"; break; 173 | case 4: SpreadsheetTimezoneVerified = lines[4] == "1"; break; 174 | case 5: UseAltFuncSeparator = lines[5] == "1"; break; 175 | case 6: Username = lines[6]; break; 176 | } 177 | } 178 | } 179 | } 180 | 181 | private bool PlayDataValid (PlayContainer pc) { 182 | try 183 | { 184 | decimal acc = (decimal)pc.Acc; 185 | } 186 | catch (Exception) 187 | { 188 | return false; 189 | } 190 | return !( 191 | pc.Acc == 100 && 192 | pc.C300 == 0 && 193 | pc.C100 == 0 && 194 | pc.C50 == 0 && 195 | pc.CGeki == 0 && 196 | pc.CKatsu == 0 && 197 | pc.CMiss == 0 && 198 | pc.Combo == 0 && 199 | pc.Hp == 200 && 200 | pc.MaxCombo == 0 && 201 | pc.Score == 0 202 | ); 203 | } 204 | 205 | internal void SetSongsFolder(string folder) 206 | { 207 | SongsFolder = folder; 208 | } 209 | 210 | private Beatmap BeatmapConstructorWrapper(string beatmapFilename) 211 | { 212 | Beatmap newBeatmap = new Beatmap(beatmapFilename); 213 | if (newBeatmap.ApproachRate == -1M) 214 | newBeatmap.ApproachRate = newBeatmap.OverallDifficulty; // i can't believe this is how old maps used to work... 215 | return newBeatmap; 216 | } 217 | public bool TrySwitchBeatmap() 218 | { 219 | string beatmapPathTemp = ""; 220 | try 221 | { 222 | beatmapPathTemp = Path.Combine(SongsFolder, osuReader.GetMapFolderName(), osuReader.GetOsuFileName()); 223 | } 224 | catch 225 | { 226 | return false; 227 | } 228 | 229 | if (!File.Exists(beatmapPathTemp)) 230 | return false; 231 | 232 | if (beatmapPathTemp == "") 233 | return false; 234 | 235 | var beatmapLines = File.ReadLines(beatmapPathTemp); 236 | if (beatmapLines.Count() == 0) 237 | return false; 238 | string versionLine = beatmapLines.First(); 239 | Match m = Regex.Match(versionLine, @"osu file format v(\d+)"); 240 | if (!m.Success) 241 | return false; 242 | int version = int.Parse(m.Groups[1].ToString()); 243 | 244 | // commit to new beatmap 245 | BeatmapPath = beatmapPathTemp; 246 | currentBeatmap = BeatmapConstructorWrapper(BeatmapPath); 247 | BeatmapBpm = (int)System.Math.Round(currentBeatmap.Bpm); 248 | BeatmapString = osuReader.GetSongString(); 249 | BeatmapSetID = (int)osuReader.GetMapSetId(); 250 | BeatmapID = osuReader.GetMapId(); 251 | 252 | // oppai 253 | (BeatmapStars, BeatmapAim, BeatmapSpeed) = oppai(BeatmapPath, GetModsString()); 254 | 255 | return true; 256 | } 257 | public void TickEverySecond() 258 | { 259 | int _; 260 | OsuMemoryStatus newGameState = osuReader.GetCurrentStatus(out _); 261 | if (newGameState == OsuMemoryStatus.Playing) 262 | PlayingSeconds += 1; 263 | else 264 | IdleSeconds += 1; 265 | form.UpdateTime(); 266 | } 267 | public void Tick() 268 | { 269 | // update current game state 270 | int _; 271 | OsuMemoryStatus newGameState = osuReader.GetCurrentStatus(out _); 272 | bool songSelectGameState = 273 | newGameState == OsuMemoryStatus.SongSelect 274 | || newGameState == OsuMemoryStatus.MultiplayerRoom 275 | || newGameState == OsuMemoryStatus.MultiplayerSongSelect; 276 | 277 | // update current beatmap 278 | string beatmapFilename = osuReader.GetOsuFileName(); 279 | if (beatmapFilename != Path.GetFileName(BeatmapPath) && beatmapFilename != "") 280 | { 281 | TrySwitchBeatmap(); 282 | } 283 | 284 | // look for read error 285 | MemoryReadError = songSelectGameState && (beatmapFilename == ""); 286 | if (MemoryReadError) 287 | BeatmapString = null; 288 | 289 | 290 | if (newGameState != GameState) // state transition 291 | { 292 | if (GameState == OsuMemoryStatus.Playing && newGameState != OsuMemoryStatus.Playing) // beatmap quit 293 | { 294 | bool beatmapCompleted = newGameState == OsuMemoryStatus.ResultsScreen; 295 | TryPostBeatmapEntryToGoogleSheets(beatmapCompleted); 296 | // reset game variables 297 | Play300c = 0; 298 | Play100c = 0; 299 | Play50c = 0; 300 | PlayMissc = 0; 301 | Accuracy = 0; 302 | TotalBeatmapHits = 0; 303 | Time = 0; 304 | } 305 | GameState = newGameState; 306 | } 307 | 308 | // update mods 309 | if (songSelectGameState && currentBeatmap != null) 310 | { 311 | RawMods = osuReader.GetMods(); 312 | if (RawMods != -1) // invalid 313 | { 314 | //Console.WriteLine(RawMods); 315 | Hidden = (RawMods & 8) != 0 ? true : false; 316 | Hardrock = (RawMods & 16) != 0 ? true : false; 317 | Doubletime = ((RawMods & 64) != 0 || (RawMods & 0b001001000000) != 0) ? true : false; 318 | EZ = (RawMods & 2) != 0 ? true : false; 319 | Halftime = (RawMods & 256) != 0 ? true : false; 320 | Flashlight = (RawMods & 1024) != 0 ? true : false; 321 | Auto = (RawMods & 2048) != 0 ? true : false; 322 | (BeatmapStars, BeatmapAim, BeatmapSpeed) = oppai(BeatmapPath, GetModsString()); 323 | 324 | // CS AR OD 325 | // FIXME: no support for HalfTime 326 | BeatmapCs = currentBeatmap.CircleSize * (Hardrock ? 1.3M : EZ ? 0.5M : 1); 327 | if (Halftime && !Doubletime && !Hardrock && EZ) // HTEZ 328 | { 329 | BeatmapAr = DifficultyCalculator.CalculateARWithHTEZ(currentBeatmap.ApproachRate); 330 | BeatmapOd = DifficultyCalculator.CalculateODWithHTEZ(currentBeatmap.OverallDifficulty); 331 | } 332 | else if (Halftime && !Doubletime && !Hardrock && !EZ) // HT 333 | { 334 | BeatmapAr = DifficultyCalculator.CalculateARWithHT(currentBeatmap.ApproachRate); 335 | BeatmapOd = DifficultyCalculator.CalculateODWithHT(currentBeatmap.OverallDifficulty); 336 | } 337 | else if (Halftime && !Doubletime && Hardrock && !EZ) // HTHR 338 | { 339 | BeatmapAr = DifficultyCalculator.CalculateARWithHTHR(currentBeatmap.ApproachRate); 340 | BeatmapOd = DifficultyCalculator.CalculateODWithHTHR(currentBeatmap.OverallDifficulty); 341 | } 342 | else if (!Halftime && !Doubletime && !Hardrock && EZ) // EZ 343 | { 344 | BeatmapAr = DifficultyCalculator.CalculateARWithEZ(currentBeatmap.ApproachRate); 345 | BeatmapOd = DifficultyCalculator.CalculateODWithEZ(currentBeatmap.OverallDifficulty); 346 | } 347 | else if (!Halftime && !Doubletime && Hardrock && !EZ) // HR 348 | { 349 | BeatmapAr = DifficultyCalculator.CalculateARWithHR(currentBeatmap.ApproachRate); 350 | BeatmapOd = DifficultyCalculator.CalculateODWithHR(currentBeatmap.OverallDifficulty); 351 | } 352 | else if (!Halftime && Doubletime && !Hardrock && EZ) // DTEZ 353 | { 354 | BeatmapAr = DifficultyCalculator.CalculateARWithDTEZ(currentBeatmap.ApproachRate); 355 | BeatmapOd = DifficultyCalculator.CalculateODWithDTEZ(currentBeatmap.OverallDifficulty); 356 | } 357 | else if (!Halftime && Doubletime && !Hardrock && !EZ) // DT 358 | { 359 | BeatmapAr = DifficultyCalculator.CalculateARWithDT(currentBeatmap.ApproachRate); 360 | BeatmapOd = DifficultyCalculator.CalculateODWithDT(currentBeatmap.OverallDifficulty); 361 | } 362 | else if (!Halftime && Doubletime && Hardrock && !EZ) // DTHR 363 | { 364 | BeatmapAr = DifficultyCalculator.CalculateARWithDTHR(currentBeatmap.ApproachRate); 365 | BeatmapOd = DifficultyCalculator.CalculateODWithDTHR(currentBeatmap.OverallDifficulty); 366 | } 367 | else // NoMod 368 | { 369 | BeatmapCs = currentBeatmap.CircleSize; 370 | BeatmapAr = currentBeatmap.ApproachRate; 371 | BeatmapOd = currentBeatmap.OverallDifficulty; 372 | } 373 | } 374 | } 375 | 376 | // read gameplay data 377 | if (newGameState == OsuMemoryStatus.Playing) 378 | { 379 | // read in new game variables 380 | IsReplay = osuReader.IsReplay(); 381 | PlayContainer playData = new PlayContainer(); 382 | osuReader.GetPlayData(playData); 383 | 384 | if (PlayDataValid(playData)) 385 | { 386 | decimal newAcc = (decimal)playData.Acc; 387 | int new300c = playData.C300; 388 | int new100c = playData.C100; 389 | int new50c = playData.C50; 390 | int newMissc = playData.CMiss; 391 | int newHits = playData.C300 + playData.C100 + playData.C50; 392 | int newSongTime = osuReader.ReadPlayTime(); 393 | 394 | // update hits 395 | if (newMissc > PlayMissc) 396 | { 397 | PlayMissc = newMissc; 398 | } 399 | if (newHits > TotalBeatmapHits && newHits - TotalBeatmapHits < 50) // safety measure: counters can't decrease; can't increment by more than 50 400 | { 401 | Accuracy = newAcc; 402 | Play300c = new300c; 403 | Play100c = new100c; 404 | Play50c = new50c; 405 | TotalBeatmapHits = newHits; 406 | } 407 | 408 | // detect retry 409 | if (newSongTime < Time && Time > 0) 410 | { 411 | //Console.WriteLine($"Beatmap retry; newSongTime {newSongTime} cachedSongTime {Time} Hits {TotalBeatmapHits}"); 412 | TryPostBeatmapEntryToGoogleSheets(false); 413 | // reset game variables 414 | Play300c = 0; 415 | Play100c = 0; 416 | Play50c = 0; 417 | PlayMissc = 0; 418 | TotalBeatmapHits = 0; 419 | Time = 0; 420 | } 421 | else 422 | { 423 | // update time 424 | Time = newSongTime; 425 | } 426 | } 427 | } 428 | } 429 | 430 | public string GetModsString() 431 | { 432 | string mods = ""; 433 | if (Auto) mods += "AT"; 434 | if (EZ) mods += "EZ"; 435 | if (Halftime) mods += "HT"; 436 | if (Hidden) mods += "HD"; 437 | if (Hardrock) mods += "HR"; 438 | if (Doubletime) mods += "DT"; 439 | if (Flashlight) mods += "FL"; 440 | return mods; 441 | } 442 | 443 | public string getFunctionSeparator() 444 | { 445 | return UseAltFuncSeparator ? ";" : ","; 446 | } 447 | 448 | private (decimal, decimal, decimal) oppai(string beatmapPath, string mods) 449 | { 450 | if (!File.Exists(beatmapPath)) 451 | return (0, 0, 0); 452 | 453 | if (mods != "") mods = $" +{mods}"; 454 | Process oppai = new Process 455 | { 456 | StartInfo = new ProcessStartInfo 457 | { 458 | FileName = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "assets", "oppai.exe"), 459 | Arguments = $"\"{beatmapPath}\" {mods} -ojson", 460 | UseShellExecute = false, 461 | RedirectStandardOutput = true, 462 | CreateNoWindow = true, 463 | StandardOutputEncoding = Encoding.UTF8 464 | } 465 | }; 466 | oppai.Start(); 467 | string oppaiOutput = oppai.StandardOutput.ReadToEnd(); 468 | oppai.WaitForExit(); 469 | 470 | // json parsing 471 | JObject oppaiData; 472 | try 473 | { 474 | oppaiData = JObject.Parse(oppaiOutput); 475 | } 476 | catch 477 | { 478 | return (0, 0, 0); 479 | } 480 | string errstr = oppaiData.GetValue("errstr").ToObject(); 481 | if (errstr != "no error") 482 | { 483 | //Console.WriteLine("Could not calculate difficulty"); 484 | return (0, 0, 0); 485 | } 486 | decimal stars = oppaiData.GetValue("stars").ToObject(); 487 | decimal aimStars = oppaiData.GetValue("aim_stars").ToObject(); 488 | decimal speedStars = oppaiData.GetValue("speed_stars").ToObject(); 489 | return (stars, aimStars, speedStars); 490 | } 491 | 492 | public void InitGoogleAPI(bool silent = false) 493 | { 494 | bool credentialsFound = File.Exists("credentials.json"); 495 | form.SetCredentialsFound(credentialsFound); 496 | if (!credentialsFound) 497 | { 498 | if (!silent) 499 | MessageBox.Show("credentials.json not found."); 500 | SetSheetsApiReady(false); 501 | return; 502 | } 503 | if (SpreadsheetId == "") 504 | { 505 | if (!silent) 506 | MessageBox.Show("Please enter a spreadsheet ID."); 507 | SetSheetsApiReady(false); 508 | return; 509 | } 510 | if (SheetName == "") 511 | { 512 | if (!silent) 513 | MessageBox.Show("Please enter a sheet name."); 514 | SetSheetsApiReady(false); 515 | return; 516 | } 517 | string[] Scopes = { SheetsService.Scope.Spreadsheets }; 518 | string ApplicationName = "Circle Tracker"; 519 | GoogleCredential credential; 520 | using (var stream = new FileStream("credentials.json", FileMode.Open, FileAccess.Read)) 521 | { 522 | credential = GoogleCredential.FromStream(stream) 523 | .CreateScoped(Scopes); 524 | } 525 | GoogleSheetsService = new SheetsService(new Google.Apis.Services.BaseClientService.Initializer() 526 | { 527 | HttpClientInitializer = credential, 528 | ApplicationName = ApplicationName 529 | }); 530 | 531 | // Get entire Spreadsheet 532 | var getSheetRequest = GoogleSheetsService.Spreadsheets.Get(SpreadsheetId); 533 | try 534 | { 535 | UserSpreadsheet = getSheetRequest.Execute(); 536 | } 537 | catch (GoogleApiException e) 538 | { 539 | if (!silent) 540 | MessageBox.Show(e.Message, "Google Sheets API Error"); 541 | SetSheetsApiReady(false); 542 | return; 543 | } 544 | catch (Exception e) 545 | { 546 | MessageBox.Show(e.Message); 547 | return; 548 | } 549 | 550 | // Get Raw Data sheet 551 | try 552 | { 553 | RawDataSheet = UserSpreadsheet.Sheets.First((shit) => shit.Properties.Title == SheetName); 554 | } 555 | catch 556 | { 557 | if (!silent) 558 | MessageBox.Show($"No sheet with the name {SheetName} found."); 559 | SetSheetsApiReady(false); 560 | return; 561 | } 562 | SheetRows = (int)RawDataSheet.Properties.GridProperties.RowCount; 563 | 564 | // Write headers (Row 1) 565 | try 566 | { 567 | WriteHeaders(); 568 | } 569 | catch (GoogleApiException e) 570 | { 571 | if (!silent) 572 | { 573 | MessageBox.Show(e.Message, "Google Sheets API Error"); 574 | 575 | if (e.Message.Contains("Unable to parse range")) 576 | MessageBox.Show("Try checking if you entered the correct thing for sheet name. Sheet name refers to the name of a 'tab' in the spreadsheet, not the name of the entire spreadsheet"); 577 | if (e.Message.Contains("Requested entity was not found")) 578 | MessageBox.Show("Try double checking to see if the Spreadsheet ID is correct."); 579 | } 580 | SetSheetsApiReady(false); 581 | return; 582 | } 583 | 584 | // Try to write playcount to W2 585 | string range = $"'{SheetName}'!W2"; 586 | ValueRange valueRange = new ValueRange(); 587 | valueRange.Values = new List> { new List() { $"=ARRAYFORMULA(IF(ISBLANK(hits) = false{getFunctionSeparator()} hits^0{getFunctionSeparator()}))" } }; 588 | var writeRequest = GoogleSheetsService.Spreadsheets.Values.Update(valueRange, SpreadsheetId, range); 589 | writeRequest.ValueInputOption = SpreadsheetsResource.ValuesResource.UpdateRequest.ValueInputOptionEnum.USERENTERED; 590 | try 591 | { 592 | writeRequest.Execute(); 593 | } 594 | catch (GoogleApiException e) 595 | { 596 | if (!silent) 597 | MessageBox.Show(e.Message, $"Google Sheets API Error: Unable to Write Playcount to {range}"); 598 | SetSheetsApiReady(false); 599 | return; 600 | } 601 | 602 | // Add in Missing Named Ranges (if any) 603 | try 604 | { 605 | AddMissingNamedRanges(UserSpreadsheet, RawDataSheet); 606 | } 607 | catch (GoogleApiException e) 608 | { 609 | if (!silent) 610 | MessageBox.Show(e.Message, "Google Sheets API Error: Unable to Add Named Ranges"); 611 | SetSheetsApiReady(false); 612 | return; 613 | } 614 | 615 | // Extend ranges if necessary 616 | ResizeNamedRanges(UserSpreadsheet, SheetRows); 617 | 618 | // Prompt Correct Timezone 619 | PromptTimezone(UserSpreadsheet); 620 | 621 | SetSheetsApiReady(true); 622 | } 623 | 624 | private void ResizeNamedRanges(Spreadsheet spreadsheet, int rows) 625 | { 626 | var definedNamedRanges = DataRanges.Select(x => x.Item2).ToList(); 627 | var rangesToUpdate = new List(); 628 | if (spreadsheet.NamedRanges != null) 629 | { 630 | rangesToUpdate = spreadsheet.NamedRanges 631 | .Where(nr => definedNamedRanges.Contains(nr.Name)) 632 | .Where(nr => nr.Range.EndRowIndex != rows) 633 | .ToList(); 634 | } 635 | List rangeUpdateRequests = new List(); 636 | foreach (NamedRange nr in rangesToUpdate) 637 | { 638 | var req = new Request(); 639 | req.UpdateNamedRange = new UpdateNamedRangeRequest(); 640 | req.UpdateNamedRange.NamedRange = nr; 641 | req.UpdateNamedRange.NamedRange.Range.EndRowIndex = rows; 642 | req.UpdateNamedRange.Fields = "Range"; 643 | rangeUpdateRequests.Add(req); 644 | } 645 | 646 | if (rangeUpdateRequests.Count > 0) 647 | { 648 | var reqs = new BatchUpdateSpreadsheetRequest(); 649 | reqs.Requests = rangeUpdateRequests; 650 | SpreadsheetsResource.BatchUpdateRequest batchRequest = GoogleSheetsService.Spreadsheets.BatchUpdate(reqs, SpreadsheetId); 651 | batchRequest.Execute(); 652 | } 653 | } 654 | 655 | private void WriteHeaders() 656 | { 657 | string range = $"'{SheetName}'!A1:1"; 658 | ValueRange valueRange = new ValueRange(); 659 | var rawDataHeaders = DataRanges.Select(x => (object)x.Item1).ToList(); 660 | valueRange.Values = new List> { rawDataHeaders }; 661 | SpreadsheetsResource.ValuesResource.UpdateRequest writeRequest = GoogleSheetsService.Spreadsheets.Values.Update(valueRange, SpreadsheetId, range); 662 | writeRequest.ValueInputOption = SpreadsheetsResource.ValuesResource.UpdateRequest.ValueInputOptionEnum.USERENTERED; 663 | writeRequest.Execute(); 664 | } 665 | 666 | private void PromptTimezone(Spreadsheet spreadsheet) 667 | { 668 | if (!SpreadsheetTimezoneVerified) 669 | { 670 | var userResponse = MessageBox.Show($"Your spreadsheet timezone is set to {spreadsheet.Properties.TimeZone}.{Environment.NewLine}{Environment.NewLine}" + 671 | "Is this correct?", 672 | "Confirm Timezone", 673 | MessageBoxButtons.YesNo); 674 | if (userResponse == DialogResult.Yes) 675 | { 676 | SpreadsheetTimezoneVerified = true; 677 | MessageBox.Show("cool", "Cool"); 678 | } 679 | else 680 | { 681 | MessageBox.Show("Please change this in your spreadsheet. Go to File > Spreadsheet Settings and then change the timezone there."); 682 | } 683 | } 684 | } 685 | 686 | private void AddMissingNamedRanges(Spreadsheet spreadsheet, Sheet rawDataSheet) 687 | { 688 | var namedRanges = DataRanges.Select(x => x.Item2).ToList(); 689 | var existingRanges = new List(); 690 | if (spreadsheet.NamedRanges != null) 691 | existingRanges = spreadsheet.NamedRanges.Select((namedRange) => namedRange.Name).ToList(); 692 | List addMissingRangeRequests = new List(); 693 | for (int i = 0; i < namedRanges.Count; i++) 694 | { 695 | if (!existingRanges.Contains(namedRanges[i])) 696 | { 697 | // update named ranges to include this data column 698 | var req = new Request(); 699 | req.AddNamedRange = new AddNamedRangeRequest(); 700 | req.AddNamedRange.NamedRange = new NamedRange(); 701 | req.AddNamedRange.NamedRange.Name = namedRanges[i]; 702 | req.AddNamedRange.NamedRange.Range = new GridRange(); 703 | req.AddNamedRange.NamedRange.Range.SheetId = rawDataSheet.Properties.SheetId; 704 | req.AddNamedRange.NamedRange.Range.StartColumnIndex = i; 705 | req.AddNamedRange.NamedRange.Range.EndColumnIndex = i + 1; 706 | req.AddNamedRange.NamedRange.Range.StartRowIndex = 1; 707 | req.AddNamedRange.NamedRange.Range.EndRowIndex = SheetRows; 708 | addMissingRangeRequests.Add(req); 709 | } 710 | } 711 | if (addMissingRangeRequests.Count > 0) 712 | { 713 | var reqs = new BatchUpdateSpreadsheetRequest(); 714 | reqs.Requests = addMissingRangeRequests; 715 | SpreadsheetsResource.BatchUpdateRequest batchRequest = GoogleSheetsService.Spreadsheets.BatchUpdate(reqs, SpreadsheetId); 716 | batchRequest.Execute(); 717 | string message = $"The following Named Ranges have been added to your spreadsheet:{Environment.NewLine}{Environment.NewLine}"; 718 | message += String.Join(Environment.NewLine, addMissingRangeRequests.Select(r => $"・{r.AddNamedRange.NamedRange.Name}")); 719 | MessageBox.Show(message, "Congratulations"); 720 | } 721 | } 722 | 723 | public void TickWrapper() 724 | { 725 | if (TickLock) 726 | return; 727 | TickLock = true; // acquire lock 728 | Tick(); 729 | TickLock = false; // release lock 730 | } 731 | private void TryPostBeatmapEntryToGoogleSheets(bool complete) 732 | { 733 | try 734 | { 735 | PostBeatmapEntryToGoogleSheets(complete); 736 | } 737 | catch (NullReferenceException) 738 | { 739 | // Game variable probably wasn't loaded or read (blame OsuMemoryDataProvider) 740 | MessageBox.Show("Could not detect current beatmap. " + 741 | $"Sorry, this part of the program is RNG and I don't know how to get it working consistently. " + 742 | $"This problem is not related to which version of Circle Tracker you are using. {Environment.NewLine}" + 743 | $"From experience, the following things will sometimes get it working again:{Environment.NewLine}{Environment.NewLine}" + 744 | $" 1. Restart Circle Tracker{ Environment.NewLine}" + 745 | $" 2. Restart osu! and Circle Tracker{ Environment.NewLine}" + 746 | $" 3. Restart your PC{ Environment.NewLine}" 747 | , "oops"); 748 | form.StopUpdateTimer(); 749 | } 750 | } 751 | 752 | private void PostBeatmapEntryToGoogleSheets(bool complete) 753 | { 754 | if (!SheetsApiReady) 755 | { 756 | //Console.WriteLine("PostBeatmapEntryToGoogleSheets: Google Sheets API has not yet been setup."); 757 | return; 758 | } 759 | 760 | if (IsReplay) 761 | return; 762 | 763 | // duplicate post bug. Fix -> set a 5 second cooldown in between consecutive posts 764 | var timeSinceLastPost = DateTime.Now.Subtract(LastPostTime); 765 | if (timeSinceLastPost.TotalSeconds < 5) 766 | return; 767 | LastPostTime = DateTime.Now; 768 | 769 | // minimum hits to submit 770 | if (TotalBeatmapHits < 40) return; 771 | 772 | // Calculate accuracy manually (readings of 0% or 100% are usually inaccurate) 773 | decimal calculatedAccuracy = 774 | 100 * (300M * Play300c + 100M * Play100c + 50M * Play50c) 775 | / (300M * (Play300c + Play100c + Play50c + PlayMissc)); 776 | 777 | string dateTimeFormat = "yyyy'-'MM'-'dd h':'mm tt"; 778 | string escapedName = BeatmapString.Replace("\"", "\"\""); 779 | string mods = GetModsString(); 780 | if (mods != "") mods = $" +{mods}"; 781 | int playTime = (int)((Time - currentBeatmap.FirstHitObjectTime) / (Doubletime ? 1.5f : Halftime ? 0.75f : 1) / 1000); 782 | 783 | var range = $"'{SheetName}'!A:J"; 784 | var valueRange = new ValueRange(); 785 | var writeData = new List() { 786 | /*A: Date & Time*/ DateTime.Now.ToString(dateTimeFormat, CultureInfo.InvariantCulture), 787 | /*B: Beatmap */ $"=HYPERLINK(\"https://osu.ppy.sh/beatmapsets/{BeatmapSetID}#osu/{BeatmapID}\"{getFunctionSeparator()} \"{escapedName + mods}\")", 788 | /*C: Hidden */ Hidden ? "1":"", 789 | /*D: Hardrock */ Hardrock ? "1":"", 790 | /*E: Doubletime */ Doubletime ? "1":"", 791 | /*F: BPM */ BeatmapBpm * (Doubletime ? 1.5M : Halftime ? 0.75M : 1), 792 | /*G: Aim */ BeatmapAim, 793 | /*H: Speed */ BeatmapSpeed, 794 | /*I: Stars */ BeatmapStars, 795 | /*J: CS */ BeatmapCs, 796 | /*K: AR */ BeatmapAr, 797 | /*L: OD */ BeatmapOd, 798 | /*M: Hits */ TotalBeatmapHits, 799 | /*N: Acc */ (Accuracy == 0 || Accuracy == 100) ? calculatedAccuracy : Accuracy, 800 | /*O: 300c */ Play300c, 801 | /*P: 100c */ Play100c, 802 | /*Q: 50c */ Play50c, 803 | /*R: Missc */ PlayMissc, 804 | /*S: EZ */ EZ ? "1":"", 805 | /*T: HT */ Halftime ? "1":"", 806 | /*U: FL */ Flashlight ? "1":"", 807 | /*V: complete */ complete ? "1":"0", 808 | /*W: playcount */ "", // (this is provided by a formula in row 2) 809 | /*X: time */ playTime 810 | }; 811 | valueRange.Values = new List> { writeData }; 812 | var appendRequest = GoogleSheetsService.Spreadsheets.Values.Append(valueRange, SpreadsheetId, range); 813 | appendRequest.ValueInputOption = SpreadsheetsResource.ValuesResource.AppendRequest.ValueInputOptionEnum.USERENTERED; 814 | 815 | AppendValuesResponse TryExecuteAppendRequest(SpreadsheetsResource.ValuesResource.AppendRequest appReq, bool rethrowException) 816 | { 817 | try { return appReq.Execute(); } 818 | catch (Exception e) 819 | { 820 | if (!rethrowException) 821 | return null; 822 | throw e; // max retry count exceeded; throw exception 823 | } 824 | } 825 | AppendValuesResponse appendResponse = null; 826 | int MAX_SUBMIT_ATTEMPTS = 4; 827 | for (int i = 0; i < MAX_SUBMIT_ATTEMPTS; i++) 828 | { 829 | appendResponse = TryExecuteAppendRequest(appendRequest, rethrowException: i == (MAX_SUBMIT_ATTEMPTS - 1)); 830 | if (appendResponse != null) 831 | break; // success 832 | } 833 | 834 | // play submit success 835 | if (SubmitSoundEnabled) 836 | { 837 | using (SoundPlayer player = new SoundPlayer(soundFilename)) 838 | { 839 | player.Play(); 840 | } 841 | } 842 | 843 | int updatedRow = 0; 844 | foreach (Match m in new Regex(@"\d+").Matches(appendResponse.Updates.UpdatedRange)) 845 | { 846 | int parsedInt = int.Parse(m.Value); 847 | if (parsedInt > updatedRow) 848 | updatedRow = parsedInt; 849 | } 850 | if (updatedRow > SheetRows) 851 | { 852 | // Add 100 more rows and update named ranges 853 | var req = new Request(); 854 | req.AppendDimension = new AppendDimensionRequest(); 855 | req.AppendDimension.Dimension = "ROWS"; 856 | req.AppendDimension.SheetId = RawDataSheet.Properties.SheetId; 857 | req.AppendDimension.Length = 100; 858 | var b1 = new BatchUpdateSpreadsheetRequest(); 859 | b1.Requests = new List() { req }; 860 | var b2 = GoogleSheetsService.Spreadsheets.BatchUpdate(b1, SpreadsheetId); 861 | b2.Execute(); 862 | 863 | // Update Named Ranges 864 | ResizeNamedRanges(UserSpreadsheet, updatedRow + 100); 865 | SheetRows = updatedRow + 100; 866 | } 867 | } 868 | void SetSheetsApiReady(bool val) 869 | { 870 | SheetsApiReady = val; 871 | form.SetSheetsApiReady(val); 872 | } 873 | 874 | } 875 | } 876 | --------------------------------------------------------------------------------