osu-db 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +58 -0
- data/Rakefile +1 -0
- data/lib/osu-db.rb +3 -0
- data/lib/osu-db/beatmap.rb +172 -0
- data/lib/osu-db/beatmapdb.rb +32 -0
- data/lib/osu-db/common.rb +112 -0
- data/lib/osu-db/mods.rb +76 -0
- data/lib/osu-db/score.rb +170 -0
- data/lib/osu-db/scoredb.rb +49 -0
- data/lib/osu-db/timeutil.rb +36 -0
- data/lib/osu-db/timing_point.rb +22 -0
- data/lib/osu-db/version.rb +6 -0
- data/osu-db.gemspec +19 -0
- metadata +61 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/osu-db.rb
ADDED
@@ -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
|
data/lib/osu-db/mods.rb
ADDED
@@ -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
|
+
|
data/lib/osu-db/score.rb
ADDED
@@ -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
|
data/osu-db.gemspec
ADDED
@@ -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: []
|