osu-db 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in osu-db.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Zejun Wu
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,58 @@
1
+ # Osu::DB
2
+
3
+ A tool to manipulate osu! beatmap and local scores database.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'osu-db'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install osu-db
18
+
19
+ ## Usage
20
+
21
+ Load datebase
22
+
23
+ require 'osu-db'
24
+
25
+ beatmapdb = Osu::DB::BeatmapDB.new
26
+ beatmapdb.load(IO.read('osu!.db'))
27
+
28
+ scoredb = Osu::DB::ScoreDB.new
29
+ scoredb.load(IO.read('scores.db'))
30
+
31
+ Let's have fun with touhou music
32
+
33
+ beatmapdb.select{|i| i.source =~ /touhou/i}.shuffle.each do |i|
34
+ puts "#{i.artist_unicode || i.artist} - #{i.title_unicode || i.title}"
35
+ `mplayer "#{i.audio_path}" 2>&1 >/dev/null`
36
+ end
37
+
38
+ Calculate the average accuracy
39
+
40
+ scores = scoredb.select{|i| i.game_mode == :osu!}
41
+ p scores.inject(0){|i, j| i + j.accuracy} / scores.size
42
+
43
+ Print all taiko scores
44
+
45
+ beatmaps = beatmapdb.select{|i| i.mode == :Taiko}.map(&:beatmapcode)
46
+ scores = scoredb.select{|i| beatmaps.include? i.beatmapcode}
47
+ scores.sort{|i, j| i.datetime <=> j.datetime}.each do |i|
48
+ puts "%10s %7d %3d %s" % [i.user, i.score, i.combo, i.mods]
49
+ end
50
+
51
+
52
+ ## Contributing
53
+
54
+ 1. Fork it
55
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
56
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
57
+ 4. Push to the branch (`git push origin my-new-feature`)
58
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,3 @@
1
+ require 'osu-db/version'
2
+ require 'osu-db/scoredb'
3
+ require 'osu-db/beatmapdb'
@@ -0,0 +1,172 @@
1
+ require 'osu-db/common'
2
+
3
+ module Osu
4
+ module DB
5
+ =begin rdoc
6
+ == Structure of osu!.db
7
+ * *str*[rdoc-ref:StringIO#read_str]
8
+ #artist, #artist_unicode, #title, #title_unicode, #creator, #version:
9
+ metadata section in .osu
10
+ * *str*[rdoc-ref:StringIO#read_str] #audio_filename: general section in .osu
11
+ * *str*[rdoc-ref:StringIO#read_str] #beatmapcode: digest of Beatmap
12
+ * *str*[rdoc-ref:StringIO#read_str] #osu_filename:
13
+ * *int*[rdoc-ref:StringIO#read_int] #type:
14
+ see BeatmapType[rdoc-ref:Osu::DB]
15
+ * *int*[rdoc-ref:StringIO#read_int] #circles, #sliders, #spinners:
16
+ the number of circles, sliders and spinners
17
+ * *time*[rdoc-ref:StringIO#read_time] #last_edit:
18
+ * *int*[rdoc-ref:StringIO#read_int]
19
+ #approach_rate, #circle_size, #hp_drain_rate, #overall_difficulty:
20
+ difficulty section in .osu
21
+ * *double*[rdoc-ref:StringIO#read_double] #slider_multiplier:
22
+ difficulty section in .osu
23
+ * *int*[rdoc-ref:StringIO#read_int] #draining_time:
24
+ or play time, in second
25
+ * *int*[rdoc-ref:StringIO#read_int] #total_time:
26
+ the offset of the last hit object, in millisecond
27
+ * *int*[rdoc-ref:StringIO#read_int] #preview_time:
28
+ general section in .osu, in millisecond
29
+ * *TimingPoint* #timing_points: timing points section in .osu
30
+ * *int*[rdoc-ref:StringIO#read_int] #beatmapid, #beatmapsetid, #threadid:
31
+ metadata section in .osu
32
+ * *int*[rdoc-ref:StringIO#read_int] #ratings:
33
+ user ratings for different GameMode[rdoc-ref:Osu::DB]
34
+ * *int*[rdoc-ref:StringIO#read_int] #your_offset:
35
+ * *float*[rdoc-ref:StringIO#read_float] #stack_leniency:
36
+ general section in .osu
37
+ * *int*[rdoc-ref:StringIO#read_int] #mode:
38
+ see GameMode[rdoc-ref:Osu::DB]
39
+ * *str*[rdoc-ref:StringIO#read_str] #source, #tags:
40
+ metadata section in .osu
41
+ * *str*[rdoc-ref:StringIO#read_int] #online_offset:
42
+ * *str*[rdoc-ref:StringIO#read_str] #letterbox:
43
+ general section in .osu
44
+ * *bool*[rdoc-ref:StringIO#read_bool] #played?:
45
+ * *time*[rdoc-ref:StringIO#read_time] #last_play:
46
+ * _0_
47
+ * *str*[rdoc-ref:StringIO#read_str] #path:
48
+ beatmap directory
49
+ * *time*[rdoc-ref:StringIO#read_time] #last_sync:
50
+ * *bool*[rdoc-ref:StringIO#read_bool]
51
+ #ignore_hitsound?, #ignore_skin?, #disable_storyboard?:
52
+ visual settings
53
+ * *int*[rdoc-ref:StringIO#read_int] #background_dim:
54
+ visual settings
55
+ * _0_
56
+ * _unknown_
57
+ * _0|7_
58
+ =end
59
+ class Beatmap
60
+ attr_reader :artist, :artist_unicode, :title, :title_unicode, :creator,
61
+ :version, :audio_filename, :beatmapcode, :osu_filename,
62
+ :type, :circles, :sliders, :spinners, :last_edit,
63
+ :approach_rate, :circle_size, :hp_drain_rate,
64
+ :overall_difficulty, :slider_multiplier,
65
+ :draining_time, :total_time, :preview_time,
66
+ :timing_points, :beatmapid, :beatmapsetid, :threadid,
67
+ :ratings, :your_offset, :stack_leniency, :mode,
68
+ :source, :tags, :online_offset, :letterbox,
69
+ :played, :last_play, :zero1, :path, :last_sync,
70
+ :ignore_hitsound, :ignore_skin, :disable_storyboard,
71
+ :background_dim, :zero2, :int1, :int2
72
+
73
+ alias :played? :played
74
+ alias :ignore_hitsound? :ignore_hitsound
75
+ alias :ignore_skin? :ignore_skin
76
+ alias :disable_storyboard? :disable_storyboard
77
+
78
+ def initialize(ios = nil)
79
+ load(ios) if ios
80
+ end
81
+
82
+ def audio_path
83
+ "#{path}\\#{audio_filename}".gsub('\\', File::SEPARATOR)
84
+ end
85
+
86
+ def osu_path
87
+ "#{path}\\#{osu_filename}".gsub('\\', File::SEPARATOR)
88
+ end
89
+
90
+ def beatmap_url
91
+ "http://osu.ppy.sh/b/#{beatmapid}"
92
+ end
93
+
94
+ def beatmapset_url
95
+ "http://osu.ppy.sh/s/#{beatmapsetid}"
96
+ end
97
+
98
+ def thread_url
99
+ "http://osu.ppy.sh/forum/t/#{threadid}"
100
+ end
101
+
102
+ def load(ios)
103
+ @artist = ios.read_str
104
+ @artist_unicode = ios.read_str
105
+ @title = ios.read_str
106
+ @title_unicode = ios.read_str
107
+ @creator = ios.read_str
108
+ @version = ios.read_str
109
+ @audio_filename = ios.read_str
110
+ @beatmapcode = ios.read_str
111
+ @osu_filename = ios.read_str
112
+
113
+ @type = BeatmapType[ios.read_int 1]
114
+ @circles, @sliders, @spinners = *ios.unpack(6, 'v*')
115
+ @last_edit = ios.read_time
116
+ # approach_rate(??) might be different from that in .osu
117
+ @approach_rate, @circle_size, @hp_drain_rate, @overall_difficulty =
118
+ *ios.unpack(4, 'C*')
119
+ @slider_multiplier = ios.read_double
120
+ @draining_time, @total_time, @preview_time = *ios.unpack(12, 'V*')
121
+ # PreviewTime: -1
122
+ @preview_time = nil if @preview_time == 0xFFFFFFFF
123
+
124
+ n = ios.read_int 4
125
+ @timing_points = Array.new(n) do
126
+ bpm = ios.read_double
127
+ offset = ios.read_double
128
+ type = ios.read_bool
129
+ (type ? RegularTimingPoint : InheritedTimingPoint).new(offset, bpm)
130
+ end
131
+
132
+ @beatmapid = ios.read_int 4
133
+ @beatmapsetid = ios.read_int 4
134
+ @threadid = ios.read_int 4
135
+ @ratings = ios.unpack(4, 'C*')
136
+ @your_offset = ios.read_signed_int 2
137
+ @stack_leniency = ios.read_float
138
+ @mode = GameMode[ios.read_int 1]
139
+ @source = ios.read_str
140
+ @tags = ios.read_str
141
+ @online_offset = ios.read_signed_int 2
142
+
143
+
144
+ @letterbox = ios.read_str
145
+
146
+ @played = !ios.read_bool
147
+ @last_play = ios.read_time
148
+ # if !@played && @last_play != nil
149
+ # raise DBCorruptError, "played=%s doesn't match last_play=%s" %
150
+ # [@played, @last_play].map{|i| i.inspect}
151
+ # end
152
+
153
+ @zero1 = ios.read_int 1 # TODO: =0
154
+ raise if @zero1 != 0
155
+ @path = ios.read_str
156
+ @last_sync = ios.read_time
157
+
158
+ @ignore_hitsound = ios.read_bool
159
+ @ignore_skin = ios.read_bool
160
+ @disable_storyboard = ios.read_bool
161
+ @background_dim = ios.read_int 1
162
+ # @disable_video is not saved
163
+
164
+ @zero2 = ios.read_int 1 # TODO: =0
165
+ raise if @zero2 != 0
166
+ @int1 = ios.read_int 4 # TODO: almost=0
167
+ @int2 = ios.read_int 1 # TODO: almost=0, otherwise=7
168
+ raise unless @int2 == 0 || @int2 == 7 # 0 iff original mode only
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,32 @@
1
+ require 'forwardable'
2
+ require 'osu-db/common'
3
+ require 'osu-db/beatmap'
4
+
5
+ module Osu
6
+ module DB
7
+ class BeatmapDB
8
+ include Enumerable
9
+ extend Forwardable
10
+
11
+ def_delegators :beatmaps, :each
12
+
13
+ attr_reader :beatmaps
14
+
15
+ def load(str)
16
+ ios = StringIO.new(str, 'rb')
17
+ ios.read_version
18
+
19
+ folders = ios.read_int 4
20
+ dummy1 = ios.read_int 4
21
+ dummy2 = ios.read_int 4
22
+ dummy3 = ios.read_int 1
23
+ user = ios.read_str
24
+
25
+ n = ios.read_int 4
26
+ @beatmaps = Array.new(n) do
27
+ beatmap = Beatmap.new(ios)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,112 @@
1
+ require 'stringio'
2
+ require 'osu-db/mods'
3
+ require 'osu-db/timeutil'
4
+ require 'osu-db/timing_point'
5
+
6
+ module Osu
7
+ module DB
8
+ # There are mainly three different beatmap types:
9
+ # +:Ranked+:: beatmap with a heart icon
10
+ # +:Approved+:: beatmap with a flame icon
11
+ # +:Pending+:: others, including pending, mod requests and graveyard
12
+ # There is also a special type:
13
+ # +:ToBeDecided+:: the type information in osu!.db is out of date
14
+ BeatmapType = {
15
+ 0 => :ToBeDecided, # Type info is out of date
16
+ 2 => :Pending,
17
+ 4 => :Ranked,
18
+ 5 => :Approved
19
+ }
20
+
21
+ # There are four game modes in osu!:
22
+ # +:osu!+:: http://osu.ppy.sh/wiki/Standard
23
+ # +:Taiko+:: http://osu.ppy.sh/wiki/Taiko
24
+ # +:CatchTheBeat+:: http://osu.ppy.sh/wiki/Catch_The_Beat
25
+ # <tt>:'osu!mania'</tt>:: http://osu.ppy.sh/wiki/Osu!mania
26
+ GameMode = [
27
+ :osu!,
28
+ :Taiko,
29
+ :CatchTheBeat,
30
+ :'osu!mania'
31
+ ]
32
+
33
+ class UnsupportedVersionError < RuntimeError
34
+ end
35
+
36
+ class DBCorruptError < RuntimeError
37
+ end
38
+
39
+ class StringIO < ::StringIO
40
+ def unpack(bytesize, format)
41
+ read(bytesize).unpack(format)
42
+ end
43
+
44
+ def read_int(bytesize)
45
+ unpack(bytesize, "C#{bytesize}").reverse.inject{|h, l| h << 8 | l}
46
+ end
47
+
48
+ def read_signed_int(bytesize)
49
+ len = 8 * bytesize
50
+ ret = read_int(bytesize)
51
+ ret[len - 1] == 0 ? ret : ret - (1 << len)
52
+ end
53
+
54
+ def read_float
55
+ unpack(4, 'e')[0]
56
+ end
57
+
58
+ def read_double
59
+ unpack(8, 'E')[0]
60
+ end
61
+
62
+ def read_bool
63
+ flag = read_int(1)
64
+ if flag == 0 || flag == 1
65
+ flag != 0
66
+ else
67
+ raise DBCorruptError, "0x00 or 0x01 expected, got #{'0x%02x' % flag}"
68
+ end
69
+ end
70
+
71
+ def read_7bit_encoded_int
72
+ ret, off = 0, 0
73
+ loop do
74
+ byte = read_int(1)
75
+ ret |= (byte & 0x7F) << off
76
+ off += 7
77
+ break if byte & 0x80 == 0
78
+ end
79
+ ret
80
+ end
81
+
82
+ def read_time
83
+ ticks = read_int(8)
84
+ ticks == 0 ? nil : TimeUtil.ticks_to_time(ticks)
85
+ end
86
+
87
+ def read_str
88
+ tag = read_int(1)
89
+ if tag == 0
90
+ nil
91
+ elsif tag == 0x0b
92
+ len = read_7bit_encoded_int
93
+ read(len)
94
+ else
95
+ raise DBCorruptError, "0x00 or 0x0b expected, got #{'0x%02x' % tag}"
96
+ end
97
+ end
98
+
99
+ VERSION_MIN = 0x01330689
100
+ VERSION_MAX = 0x0133068D
101
+
102
+ def read_version
103
+ version = read_int(4)
104
+ if (VERSION_MIN .. VERSION_MAX).include? version
105
+ version
106
+ else
107
+ raise UnsupportedVersionError, "version = #{'0x%08x' % version}"
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,76 @@
1
+ module Osu
2
+ module DB
3
+ class Mod
4
+ def initialize(index, name)
5
+ @index = index
6
+ @name = name
7
+ end
8
+
9
+ def to_i
10
+ 1 << @index
11
+ end
12
+
13
+ def to_s
14
+ @name
15
+ end
16
+
17
+ # Easy
18
+ EZ = Mod.new(1, 'Easy')
19
+ # No Fail
20
+ NF = Mod.new(0, 'NoFail')
21
+ # Half Time
22
+ HT = Mod.new(8, 'HalfTime')
23
+ # Hard Rock
24
+ HR = Mod.new(4, 'HardRock')
25
+ # Sudden Death
26
+ SD = Mod.new(5, 'SuddenDeath')
27
+ # Perfect (based on SD)
28
+ PF = Mod.new(14, '+Perfect')
29
+ # Double Time
30
+ DT = Mod.new(6, 'DoubleTime')
31
+ # Night Core (based on DT)
32
+ NC = Mod.new(9, '+NightCore')
33
+ # Hidden
34
+ HD = Mod.new(3, 'Hidden')
35
+ # Flash Light
36
+ FL = Mod.new(10, 'FlashLight')
37
+ # Relax
38
+ RL = Mod.new(-1, 'Relax')
39
+ # Auto Pilot
40
+ AP = Mod.new(-1, 'AutoPilot')
41
+ # Spun Out
42
+ SO = Mod.new(12, 'SpunOut')
43
+ # Auto
44
+
45
+ # Return all ranked mods
46
+ def self.all
47
+ [EZ, NF, HT, HR, SD, PF, DT, NC, HD, FL, SO]
48
+ end
49
+ end
50
+
51
+ class Mods
52
+ include Enumerable
53
+
54
+ def initialize(mods)
55
+ @mods = mods
56
+ end
57
+
58
+ def include?(mod)
59
+ @mods & mod.to_i != 0
60
+ end
61
+
62
+ def to_a
63
+ Mod.all.select{|mod| include? mod}
64
+ end
65
+
66
+ def to_i
67
+ @mods
68
+ end
69
+
70
+ def to_s
71
+ to_a.map{|mod| mod.to_s} * ','
72
+ end
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,170 @@
1
+ require 'osu-db/common'
2
+
3
+ module Osu
4
+ module DB
5
+ =begin rdoc
6
+ == Structure of scores.db
7
+ * *str*[rdoc-ref:StringIO#read_str] #beatmapcode: digest of Beatmap
8
+ * *str*[rdoc-ref:StringIO#read_str] #user: username
9
+ * *str*[rdoc-ref:StringIO#read_str] #scorecode: digest of Score
10
+ * *int*[rdoc-ref:StringIO#read_int] #x300, #x100, #x50:
11
+ the number of 300s, 100s and 50s
12
+ * *int*[rdoc-ref:StringIO#read_int] #geki, #katsu, #miss:
13
+ these attributes may have different meaning is special modes
14
+ * *int*[rdoc-ref:StringIO#read_int] #score, #combo:
15
+ score and max combo
16
+ * *bool*[rdoc-ref:StringIO#read_bool] #perfect?:
17
+ full combo or not
18
+ * *bitset*[rdoc-ref:StringIO#read_int] #mods:
19
+ mods or game modifiers, see Mod and Mods
20
+ * *time*[rdoc-ref:StringIO#read_time] #datetime:
21
+ played time
22
+ * _0xFFFFFFFF_
23
+ * *int*[rdoc-ref:StringIO#read_int] #scoreid:
24
+ score id
25
+ =end
26
+ class Score
27
+ attr_reader :game_mode, :beatmapcode, :user, :scorecode,
28
+ :x300, :x100, :x50, :geki, :katsu, :miss,
29
+ :score, :combo, :perfect, :mods, :datetime, :dummy, :scoreid
30
+
31
+ alias :perfect? :perfect
32
+ alias :full_combo :perfect
33
+ alias :full_combo? :full_combo
34
+
35
+ def initialize(game_mode, ios = nil)
36
+ @game_mode = game_mode
37
+ load(ios) if ios
38
+ end
39
+
40
+ def hits
41
+ raise NotImplementedError
42
+ end
43
+
44
+ def accuracy
45
+ raise NotImplementedError
46
+ end
47
+
48
+ def grade
49
+ raise NotImplementedError
50
+ end
51
+
52
+ def load(ios)
53
+ ios.read_version
54
+
55
+ @beatmapcode = ios.read_str
56
+ @user = ios.read_str
57
+
58
+ @scorecode = ios.read_str
59
+ @x300, @x100, @x50, @geki, @katsu, @miss = *ios.unpack(12, 'v6')
60
+ @score = ios.read_int(4)
61
+ @combo = ios.read_int(2)
62
+ @perfect = ios.read_bool
63
+ @mods = Mods.new(ios.read_int(5))
64
+ @datetime = ios.read_time
65
+ @dummy = ios.read_int(4) # TODO: always = 0xFFFFFFFF
66
+ @scoreid = ios.read_int(4)
67
+ end
68
+ end
69
+
70
+ # Score of Standard Mode
71
+ class OsuScore < Score
72
+ def hits
73
+ x300 + x100 + x50 + miss
74
+ end
75
+
76
+ def accuracy
77
+ (300 * x300 + 100 * x100 + 50 * x50) / (300.0 * hits)
78
+ end
79
+
80
+ def grade
81
+ if x300 == hits
82
+ :SS # SS = 100% accuracy
83
+ elsif 10 * x300 > 9 * hits && 100 * x50 < hits && miss == 0
84
+ :S # S = Over 90% 300s, less than 1% 50s and no miss.
85
+ elsif 10 * x300 > 8 * hits && miss == 0 || 10 * x300 > 9 * hits
86
+ :A # A = Over 80% 300s and no miss OR over 90% 300s.
87
+ elsif 10 * x300 > 7 * hits && miss == 0 || 10 * x300 > 8 * hits
88
+ :B # B = Over 70% 300s and no miss OR over 80% 300s.
89
+ elsif 10 * x300 > 6 * hits
90
+ :C # C = Over 60% 300s.
91
+ else
92
+ :D
93
+ end
94
+ end
95
+ end
96
+
97
+ # Score of Taiko Mode
98
+ class TaikoScore < Score
99
+ alias :great :x300
100
+ alias :good :x100
101
+
102
+ def hits
103
+ great + good + miss
104
+ end
105
+
106
+ def accuracy
107
+ (great + good * 0.5) / hits
108
+ end
109
+
110
+ def grade
111
+ if x300 == hits
112
+ :SS
113
+ elsif 10 * x300 > 9 * hits && miss == 0
114
+ :S
115
+ elsif 10 * x300 > 8 * hits && miss == 0 || 10 * x300 > 9 * hits
116
+ :A
117
+ elsif 10 * x300 > 7 * hits && miss == 0 || 10 * x300 > 8 * hits
118
+ :B
119
+ elsif 10 * x300 > 6 * hits
120
+ :C
121
+ else
122
+ :D
123
+ end
124
+ end
125
+ end
126
+
127
+ # Score of Catch The Beat Mode
128
+ class CTBScore < Score
129
+ alias :droplet_miss :katsu
130
+
131
+ def hits
132
+ x300 + x100 + x50 + droplet_miss + miss
133
+ end
134
+
135
+ def accuracy
136
+ (x300 + x100 + x50).to_f / hits
137
+ end
138
+
139
+ def grade
140
+ acc = accuracy
141
+ [0.85, :D, 0.90, :C, 0.94, :B, 0.98, :A].each_slice(2) do |a, g|
142
+ return g if acc <= a
143
+ end
144
+ acc < 1 ? :S : :SS
145
+ end
146
+ end
147
+
148
+ # Score of osu!mania Mode
149
+ class ManiaScore < Score
150
+ alias :max :geki
151
+ alias :x200 :katsu
152
+
153
+ def hits
154
+ max + x300 + x200 + x100 + x50 + miss
155
+ end
156
+
157
+ def accuracy
158
+ (300 * (max + x300) + 200 * x200 + 100 * x100 + 50 * x50) / (300.0 * hits)
159
+ end
160
+
161
+ def grade
162
+ acc = accuracy
163
+ [0.70, :D, 0.80, :C, 0.90, :B, 0.95, :A].each_slice(2) do |a, g|
164
+ return g if acc <= a
165
+ end
166
+ acc < 1 ? :S : :SS
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,49 @@
1
+ require 'osu-db/common'
2
+ require 'osu-db/score'
3
+
4
+ module Osu
5
+ module DB
6
+ class ScoreDB
7
+ include Enumerable
8
+
9
+ @@game_mode_score = {
10
+ :osu! => OsuScore,
11
+ :Taiko => TaikoScore,
12
+ :CatchTheBeat => CTBScore,
13
+ :'osu!mania' => ManiaScore
14
+ }
15
+
16
+ attr_reader :scores
17
+
18
+ def each
19
+ scores.each do |_, v|
20
+ v.each do |score|
21
+ yield score
22
+ end
23
+ end
24
+ end
25
+
26
+ def load(str)
27
+ ios = StringIO.new(str, "rb")
28
+ @scores = Hash.new{[]}
29
+
30
+ ios.read_version
31
+ n = ios.read_int(4)
32
+
33
+ n.times do
34
+ beatmapcode = ios.read_str
35
+ m = ios.read_int(4)
36
+ m.times do
37
+ game_mode = GameMode[ios.read_int 1]
38
+ if game_mode && @@game_mode_score[game_mode]
39
+ score = @@game_mode_score[game_mode].new(game_mode, ios)
40
+ else
41
+ score = Score.new(game_mode, ios)
42
+ end
43
+ @scores[beatmapcode] <<= score
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ module Osu
2
+ module DB
3
+ # Conversion between System.DateTime.Ticks in .NET and Time in Ruby
4
+ # - http://msdn.microsoft.com/en-us/library/system.datetime.ticks.aspx
5
+ # - http://www.ruby-doc.org/core/Time.html
6
+ module TimeUtil
7
+ # A single tick represents one hundred nanoseconds or one ten-millionth
8
+ # of a second.
9
+ TICKS_PER_SEC = 10 ** 7
10
+
11
+ # The value of DateTime.Ticks represents the number of 100-nanosecond
12
+ # intervals that have elapsed since 12:00:00 midnight, January 1, 0001,
13
+ # which represents DateTime.MinValue. It does not include the number of
14
+ # ticks that are attributable to leap seconds.
15
+ EPOCH_TO_TICKS = 0 - Time.utc(1, 1, 1).to_i * TICKS_PER_SEC
16
+
17
+ # Convert DateTime.Ticks to Time
18
+ def self.ticks_to_time(ticks)
19
+ if ticks.kind_of? Numeric
20
+ Time.at(Rational(ticks - EPOCH_TO_TICKS, TICKS_PER_SEC))
21
+ else
22
+ ticks
23
+ end
24
+ end
25
+
26
+ # Convert Time to DateTime.Ticks
27
+ def self.time_to_ticks(time)
28
+ if time.kind_of? Numeric
29
+ time.to_i
30
+ else
31
+ EPOCH_TO_TICKS + (time.to_r * TICKS_PER_SEC).to_i
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,22 @@
1
+ module Osu
2
+ module DB
3
+ class TimingPoint
4
+ private_class_method :new
5
+
6
+ attr_reader :offset, :bpm
7
+
8
+ def initialize(offset, bmp)
9
+ @offset = offset
10
+ @bpm = bmp
11
+ end
12
+ end
13
+
14
+ class RegularTimingPoint < TimingPoint
15
+ public_class_method :new
16
+ end
17
+
18
+ class InheritedTimingPoint < TimingPoint
19
+ public_class_method :new
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ module Osu
2
+ module DB
3
+ # :nodoc:
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'osu-db/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "osu-db"
8
+ gem.version = Osu::DB::VERSION
9
+ gem.authors = ["Zejun Wu"]
10
+ gem.email = ["zejun.wu@gmail.com"]
11
+ gem.description = %q{A tool to manipulate osu! beatmap and local scores database.}
12
+ gem.summary = %q{Library to manipulate osu! database}
13
+ gem.homepage = "https://github.com/watashi/osu-db"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: osu-db
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Zejun Wu
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-02 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A tool to manipulate osu! beatmap and local scores database.
15
+ email:
16
+ - zejun.wu@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - Gemfile
23
+ - LICENSE.txt
24
+ - README.md
25
+ - Rakefile
26
+ - lib/osu-db.rb
27
+ - lib/osu-db/beatmap.rb
28
+ - lib/osu-db/beatmapdb.rb
29
+ - lib/osu-db/common.rb
30
+ - lib/osu-db/mods.rb
31
+ - lib/osu-db/score.rb
32
+ - lib/osu-db/scoredb.rb
33
+ - lib/osu-db/timeutil.rb
34
+ - lib/osu-db/timing_point.rb
35
+ - lib/osu-db/version.rb
36
+ - osu-db.gemspec
37
+ homepage: https://github.com/watashi/osu-db
38
+ licenses: []
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubyforge_project:
57
+ rubygems_version: 1.8.23
58
+ signing_key:
59
+ specification_version: 3
60
+ summary: Library to manipulate osu! database
61
+ test_files: []