4
4
from matplotlib .path import Path
5
5
import matplotlib .patches as mpatches
6
6
7
+ from enum import IntFlag
8
+
7
9
from . import beatmap as osu_beatmap
8
10
from ._util .bsearch import bsearch
11
+ from ._util .binfile import *
12
+
13
+ from functools import reduce
14
+
15
+ class Mod (IntFlag ):
16
+ DT = 0x40
17
+ HR = 0x10
9
18
10
19
class Replay :
11
20
def __init__ (self , file ):
12
21
self .game_mode = read_byte (file )
13
22
14
23
# Sem minigames
15
- if self .game_mode != 0 :
16
- raise RuntimeError ("Not a osu!std replay" )
24
+ assert self .game_mode == 0 , "Not a osu!std replay"
17
25
18
26
# Versão do osu! e hash do mapa. A gente ignora.
19
27
self .osu_version = read_int (file )
@@ -43,7 +51,7 @@ def __init__(self, file):
43
51
self .accuracy = (self .n_300s + self .n_100s / 3 + self .n_50s / 6 ) / total
44
52
45
53
# Mods (ignora)
46
- self .mods = read_int (file )
54
+ self .mods = Mod ( read_int (file ) )
47
55
48
56
# Gráfico de vida. Vide site para o formato.
49
57
life_graph = read_binary_string (file )
@@ -70,6 +78,10 @@ def __init__(self, file):
70
78
# Não usado
71
79
_ = read_long (file )
72
80
81
+ def has_mods (self , * mods ):
82
+ mask = reduce (lambda x , y : x | y , mods )
83
+ return bool (self .mods & mask )
84
+
73
85
def frame (self , time ):
74
86
index = bsearch (self .data , time , lambda f : f [0 ])
75
87
@@ -84,232 +96,6 @@ def frame(self, time):
84
96
85
97
return self .data [index ][1 :]
86
98
87
- def read_byte (file ):
88
- return ord (file .read (1 ))
89
-
90
- def read_short (file ):
91
- return read_byte (file ) + (read_byte (file ) << 8 )
92
-
93
- def read_int (file ):
94
- return read_short (file ) + (read_short (file ) << 16 )
95
-
96
- def read_long (file ):
97
- return read_int (file ) + (read_int (file ) << 32 )
98
-
99
- def read_uleb128 (file ):
100
- n = 0
101
- i = 0
102
- while True :
103
- byte = read_byte (file )
104
- n += (byte & 0x7F ) << i
105
- if byte & 0x80 != 0 :
106
- i += 7
107
- else :
108
- return n
109
-
110
- def read_binary_string (file ):
111
- while True :
112
- flag = read_byte (file )
113
- if flag == 0x00 :
114
- return ""
115
- elif flag == 0x0b :
116
- length = read_uleb128 (file )
117
- return file .read (length ).decode ('utf8' )
118
- else :
119
- raise RuntimeError ("Invalid file" )
120
-
121
99
def load (filename ):
122
100
with open (filename , "rb" ) as file :
123
- return Replay (file )
124
-
125
- def draw_cursor (ax , x , y , z ):
126
- keys = int (z )
127
- if keys & 0x01 :
128
- cursor = 'ro'
129
- elif keys & 0x02 :
130
- cursor = 'go'
131
- else :
132
- cursor = 'y+'
133
- ax .plot ([x ], [384 - y ], cursor )
134
-
135
- def draw_slider (ax , beatmap , radius , t , obj ):
136
- path = obj [5 ].split ("|" )
137
- stype = path .pop (0 )
138
-
139
- points = [(obj [0 ], 384 - obj [1 ])] + [(int (x ), 384 - int (y )) for x , y in [t .split (':' ) for t in path ]]
140
- codes = []
141
-
142
- if stype == 'L' :
143
- codes .append (Path .MOVETO )
144
- for i in range (1 , len (points )):
145
- codes .append (Path .LINETO )
146
-
147
- elif stype == 'P' : # Círculo perfeito
148
- codes .append (Path .MOVETO )
149
- codes .append (Path .CURVE3 )
150
- codes .append (Path .CURVE3 )
151
-
152
- else : # Bezier
153
- n = 0
154
- for i in range (0 , len (points )):
155
- n += 1
156
- repeated = i + 1 < len (points ) and points [i ] == points [i + 1 ]
157
- if i + 1 == len (points ) or n == 4 or repeated :
158
- if n == 2 :
159
- codes .append (Path .MOVETO )
160
- codes .append (Path .LINETO )
161
- elif n == 3 :
162
- codes .append (Path .MOVETO )
163
- codes .append (Path .CURVE3 )
164
- codes .append (Path .CURVE3 )
165
- elif n == 4 :
166
- codes .append (Path .MOVETO )
167
- codes .append (Path .CURVE4 )
168
- codes .append (Path .CURVE4 )
169
- codes .append (Path .CURVE4 )
170
- n = 0
171
-
172
- preempt , fade = beatmap .approach_rate ()
173
- duration = beatmap .slider_duration (obj )
174
- if t < obj [2 ] + preempt + duration :
175
- alpha = 1
176
- else :
177
- alpha = 1 - (t - (obj [2 ] + preempt + duration )) / osu_beatmap .SLIDER_FADEOUT
178
-
179
- slider = mpatches .PathPatch (
180
- Path (points , codes ),
181
- fc = "none" , capstyle = 'round' , transform = ax .transData , fill = False ,
182
- edgecolor = (1.0 , 1.0 , 1.0 , alpha ), linewidth = radius , joinstyle = 'bevel' , zorder = - 2
183
- )
184
- ax .add_patch (slider )
185
-
186
- slider = mpatches .PathPatch (
187
- Path (points , codes ),
188
- fc = "none" , capstyle = 'round' , transform = ax .transData , fill = False ,
189
- edgecolor = "#333333" , linewidth = radius - 4 , joinstyle = 'bevel' , zorder = - 1
190
- )
191
- ax .add_patch (slider )
192
-
193
- last_nc = 0
194
- current_color = 0
195
-
196
- def draw_hit_objects (ax , beatmap , t , objs ):
197
- global current_color , last_nc
198
-
199
- circles = ([], [], [])
200
- radius = (27.2 - 2.24 * beatmap ['CircleSize' ])
201
- combo_colors = [(1.0 , 0 , 0 ), (0 , 1.0 , 0 ), (0 , 1.0 , 0 ), (1.0 , 1.0 , 0 )]
202
-
203
- # Sliders
204
- for obj in objs :
205
- color = current_color
206
- if obj [3 ] & 4 and obj [2 ] > last_nc :
207
- last_nc = obj [2 ]
208
- current_color += 1
209
- current_color %= len (combo_colors )
210
- color = current_color
211
- elif obj [2 ] < last_nc :
212
- color = (len (combo_colors ) + current_color - 1 ) % len (combo_colors )
213
-
214
- # Spinner
215
- if obj [3 ] & 8 :
216
- ax .plot ([obj [0 ]], [obj [1 ]], 'yo' , fillstyle = 'none' , markersize = 128 )
217
-
218
- else :
219
- # Slider
220
- if obj [3 ] & 2 :
221
- draw_slider (ax , beatmap , radius , t , obj )
222
-
223
- # Círculo (Slider também tem)
224
- circles [0 ].append (obj [0 ])
225
- circles [1 ].append (384 - obj [1 ])
226
-
227
- preempt , fade = beatmap .approach_rate ()
228
-
229
- if t > obj [2 ] + preempt :
230
- alpha = 1 - (t - obj [2 ] - preempt ) / 100
231
- elif t < obj [2 ] + fade :
232
- alpha = 1 - ((obj [2 ] + fade ) - t ) / fade
233
- else :
234
- alpha = 1
235
- alpha = max ([0 , min (1 , alpha )])
236
-
237
-
238
- r , g , b = combo_colors [color ]
239
- circles [2 ].append ((r , g , b , alpha ))
240
-
241
- ax .scatter (circles [0 ], circles [1 ], color = circles [2 ], s = radius ** 2 )
242
-
243
- def cursor_state (replay , time ):
244
- tt = 0
245
- for t in replay :
246
- w , x , y , z = t
247
-
248
- if w > 0 :
249
- tt += w
250
- else :
251
- continue
252
-
253
- if tt > time :
254
- break
255
-
256
- return x , y , z
257
-
258
- def preview (beatmap , replay_data ):
259
- plt .ion ()
260
-
261
- fig = plt .figure (facecolor = '#333333' )
262
- ax = fig .add_subplot (1 , 1 , 1 )
263
-
264
- ax .clear ()
265
- ax .axis ('off' )
266
- fig .canvas .draw ()
267
-
268
- plt .show ()
269
-
270
- delta = 0
271
- tt = 0
272
- for t in replay_data [0 ]:
273
- w , x , y , z = t
274
-
275
- if w > 0 :
276
- tt += w
277
-
278
- sec = w / 1000
279
-
280
- # Frame skip
281
- if delta > sec :
282
- delta -= sec
283
- continue
284
-
285
- # Espera o tempo especificado entre o estado atual e o anterior
286
- # no arquivo de replay
287
- if sec > 0 :
288
- time .sleep (sec - delta )
289
- delta = 0
290
-
291
- start = time .time ()
292
-
293
- # Limpa tudo
294
- ax .clear ()
295
- ax .axis ('off' )
296
- ax .set_xlim ((- 32 , 512 + 32 ))
297
- ax .set_ylim ((- 32 , 384 + 32 ))
298
- fig .canvas .draw_idle ()
299
-
300
- # Desenha o objeto do mapa
301
- objs = beatmap .visible_objects (tt )
302
-
303
- if len (objs ) > 0 :
304
- draw_hit_objects (ax , beatmap , tt , objs )
305
-
306
- draw_cursor (ax , x , y , z )
307
-
308
- for d in replay_data [1 :]:
309
- x , y , z = cursor_state (d , tt )
310
- draw_cursor (ax , x , y , z )
311
-
312
- fig .canvas .draw ()
313
-
314
- # Calcula o atraso
315
- delta += time .time () - start
101
+ return Replay (file )
0 commit comments