├── 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