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.
- 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: []
|