@@ -111,19 +111,20 @@ export default class Replay {
111
111
112
112
let offset = 0 ;
113
113
for ( let i = 0 ; i < numEvents ; i ++ ) {
114
- const time = buffer . readDoubleLE ( offset ) ;
114
+ const event = new Event ( ) ;
115
+ event . time = buffer . readDoubleLE ( offset ) ;
115
116
offset += 8 ;
116
- const touchInfo = buffer . readInt16LE ( offset ) ;
117
+ event . touchInfo = buffer . readInt16LE ( offset ) ;
117
118
offset += 2 ;
118
- const type = buffer . readUInt8 ( offset ) ;
119
+ event . type = buffer . readUInt8 ( offset ) ;
119
120
offset += 2 ; // 1 + 1 padding
120
- const groundInfo = buffer . readFloatLE ( offset ) ;
121
+ event . groundInfo = buffer . readFloatLE ( offset ) ;
121
122
offset += 4 ;
122
- if ( type < 0 || type > 7 ) {
123
- throw new Error ( `Invalid event type value=${ type } at event offset=${ offset } ` ) ;
123
+ if ( event . type < 0 || event . type > 7 ) {
124
+ throw new Error ( `Invalid event type value=${ event . type } at event offset=${ offset } ` ) ;
124
125
}
125
126
126
- events . push ( { time , touchInfo , type , groundInfo } ) ;
127
+ events . push ( event ) ;
127
128
}
128
129
129
130
return events ;
@@ -230,49 +231,121 @@ export default class Replay {
230
231
* Get time of replay in milliseconds.
231
232
*/
232
233
public getTime ( ) : FinishState {
233
- // First check if last event was a touch event in ride(s) event data.
234
- const lastEvent = this . rides . reduce ( ( prev : undefined | Event , ride ) => {
235
- const prevTime = prev ? prev . time : 0 ;
236
- const lastRideEvent = ride . events . length > 0 ? ride . events [ ride . events . length - 1 ] : undefined ;
237
- const lastRideEventTime = lastRideEvent ? lastRideEvent . time : 0 ;
238
- if ( lastRideEventTime > prevTime ) {
239
- return lastRideEvent ;
240
- }
241
- return prev ;
234
+ // First get the last event of (each) ride.
235
+ const lastEvents = this . rides . map ( ( ride ) =>
236
+ ride . events . length > 0 ? ride . events [ ride . events . length - 1 ] : undefined ,
237
+ ) ;
238
+
239
+ // Time of each ride's last event, in milliseconds.
240
+ const lastEventTimes = lastEvents . map ( ( event : undefined | Event ) => ( event ? event . timeInMilliseconds : undefined ) ) ;
241
+
242
+ // Time of the final event(s) (= the event(s) with the highest time), in milliseconds.
243
+ const finalEventTime = lastEventTimes . reduce ( ( prev : undefined | number , time : undefined | number ) => {
244
+ if ( ! prev ) {
245
+ return time ;
246
+ } else if ( ! time ) {
247
+ return prev ;
248
+ } else return time > prev ? time : prev ;
242
249
} , undefined ) ;
243
250
244
- // Highest frame time.
245
- const maxFrames = this . rides . reduce ( ( prev , ride ) => {
246
- return ride . frames . length > prev ? ride . frames . length : prev ;
251
+ // Get all Touch/Apple events with finalEventTime. Other events with finalEventTime are filtered out.
252
+ // (For example player 2 might have a GroundTouch event with the same time as player 1's flower Touch.)
253
+ const finalEvents = < Event [ ] > [ ] ;
254
+ for ( const ride of this . rides ) {
255
+ const touchOrAppleEvents = ride . events . filter (
256
+ ( event ) =>
257
+ event . timeInMilliseconds === finalEventTime &&
258
+ ( event . type === EventType . Touch || event . type === EventType . Apple ) ,
259
+ ) ;
260
+ finalEvents . push ( ...touchOrAppleEvents ) ;
261
+ }
262
+
263
+ // Time of each ride's last frame, in milliseconds.
264
+ const FrameDuration = 1000 / 30.0 ;
265
+ const lastFrameTimes = this . rides . map ( ( ride ) => ride . frames . length * FrameDuration ) ;
266
+
267
+ // Highest frame time, in milliseconds.
268
+ const finalFrameTime = lastFrameTimes . reduce ( ( prev , time ) => {
269
+ return time > prev ? time : prev ;
247
270
} , 0 ) ;
248
- const maxFrameTime = maxFrames * 33.333 ;
249
271
250
- // If no touch event, return approximate frame time.
251
- if ( ( lastEvent && lastEvent . type !== EventType . Touch ) || ! lastEvent ) {
272
+ // Not finished if the final events don't include a Touch or Apple event, so return approximate frame time.
273
+ if ( finalEvents . length === 0 ) {
252
274
return {
253
275
finished : false ,
254
276
reason : ReplayFinishStateReason . NoTouch ,
255
- time : Math . round ( maxFrameTime ) ,
277
+ time : Math . round ( finalFrameTime ) ,
256
278
} ;
257
279
}
258
280
259
- // Set to highest event time.
260
- const maxEventTime = lastEvent . time * ( 0.001 / ( 0.182 * 0.0024 ) ) * 1000 ;
281
+ // Info about whether each ride's last event is within ~1 frame of time from the end of that ride.
282
+ // In finished rides, flower Touch is sometimes slightly more than FrameDuration away from the end,
283
+ // so a small extra tolerance is allowed. (In singleplayer replays, the extra time can range at least
284
+ // from 6.0e-13 to 4.9e-8 ms, and in multiplayer replays at least from 0.333 ms to 3.333 ms.)
285
+ const extraTolerance = this . rides . length > 1 ? 3.5 : 1.0e-7 ;
286
+ const rideHasLastFrameEvent = lastFrameTimes . map ( ( lastFrameTime , idx ) => {
287
+ const lastEventTime = lastEventTimes [ idx ] ;
288
+ return lastEventTime ? lastFrameTime <= lastEventTime + FrameDuration + extraTolerance : false ;
289
+ } ) ;
261
290
262
- // If event difference to frame time is >1 frames of time, probably not finished?
263
- if ( maxFrameTime > maxEventTime + 33.333 ) {
291
+ // If the highest event time is not within ~1 frame of time from the end, probably not finished?
292
+ const finalEventIdx = lastEventTimes . lastIndexOf ( finalEventTime ) ;
293
+ if ( ! rideHasLastFrameEvent [ finalEventIdx ] ) {
264
294
return {
265
295
finished : false ,
266
296
reason : ReplayFinishStateReason . FrameDifference ,
267
- time : Math . round ( maxFrameTime ) ,
297
+ time : Math . round ( finalFrameTime ) ,
268
298
} ;
269
299
}
270
300
271
- // Otherwise probably finished?
301
+ // For multiplayer replays where one player waits at the flower for the other to take an apple.
302
+ let waitingAtFlower = false ;
303
+ let endsWithAppleTake = false ;
304
+
305
+ let isFinished = false ;
306
+ for ( const [ idx , ride ] of this . rides . entries ( ) ) {
307
+ // Potentially finished, if ride ends in a Touch or Apple event.
308
+ const lastEvent = lastEvents [ idx ] ;
309
+ if ( lastEvent && rideHasLastFrameEvent [ idx ] && ! isFinished ) {
310
+ if ( lastEvent . type === EventType . Touch ) {
311
+ if (
312
+ ride . events . length >= 2 &&
313
+ ride . events [ ride . events . length - 2 ] . type === EventType . Touch &&
314
+ ride . events [ ride . events . length - 2 ] . time !== lastEvent . time
315
+ ) {
316
+ // Probably ended at flower, but not all apples were taken.
317
+ isFinished = false ;
318
+ waitingAtFlower = true ;
319
+ } else {
320
+ // Otherwise probably finished, but false positives are possible (e.g., dying to killer).
321
+ isFinished = true ;
322
+ }
323
+ } else if ( lastEvent . type === EventType . Apple ) {
324
+ endsWithAppleTake = true ;
325
+ if ( ride . events . length >= 3 ) {
326
+ const endAppleCount = ride . events . filter (
327
+ ( event ) => event . type === EventType . Apple && event . time === lastEvent . time ,
328
+ ) . length ;
329
+ const endTouchEventCount = ride . events . filter (
330
+ ( event ) => event . type === EventType . Touch && event . time === lastEvent . time ,
331
+ ) . length ;
332
+ if ( endTouchEventCount >= endAppleCount + 1 ) {
333
+ // Apple(s) and flower taken at the same time. N apples and a flower will
334
+ // generate (at least) N+1 Touch events, followed by N Apple events.
335
+ isFinished = true ;
336
+ }
337
+ }
338
+ }
339
+ }
340
+ }
341
+
342
+ // Multiplayer finish.
343
+ if ( waitingAtFlower && endsWithAppleTake ) isFinished = true ;
344
+
272
345
return {
273
- finished : true ,
274
- reason : ReplayFinishStateReason . Touch ,
275
- time : Math . floor ( maxEventTime ) ,
346
+ finished : isFinished ,
347
+ reason : isFinished ? ReplayFinishStateReason . Touch : ReplayFinishStateReason . NoTouch ,
348
+ time : isFinished && finalEventTime ? Math . floor ( finalEventTime ) : Math . round ( finalFrameTime ) ,
276
349
} ;
277
350
}
278
351
0 commit comments