osu-db 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []