@rian8337/osu-droid-replay-analyzer 4.0.0-beta.87 → 4.0.0-beta.89

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.
Files changed (3) hide show
  1. package/dist/index.js +778 -784
  2. package/package.json +6 -6
  3. package/typings/index.d.ts +425 -438
package/dist/index.js CHANGED
@@ -46,274 +46,217 @@ exports.MovementType = void 0;
46
46
  })(exports.MovementType || (exports.MovementType = {}));
47
47
 
48
48
  /**
49
- * Represents a cursor's occurrence.
50
- */
51
- class CursorOccurrence {
52
- constructor(time, x, y, id) {
53
- this.time = time;
54
- this.position = new osuBase.Vector2(x, y);
55
- this.id = id;
56
- }
57
- /**
58
- * Returns a string representation of this `CursorOccurrence`.
59
- */
60
- toString() {
61
- let str = `${this.time.toString()}ms `;
62
- switch (this.id) {
63
- case exports.MovementType.down:
64
- str += "Down";
65
- break;
66
- case exports.MovementType.up:
67
- str += "Up";
68
- break;
69
- case exports.MovementType.move:
70
- str += "Move";
71
- break;
72
- }
73
- if (this.id !== exports.MovementType.up) {
74
- str += ` (${this.position.x.toFixed(2)}, ${this.position.y.toFixed(2)})`;
75
- }
76
- return str;
77
- }
78
- }
79
-
80
- /**
81
- * Represents a group of cursor occurrences representing a cursor instance's
82
- * movement when a player places their finger on the screen.
49
+ * Utility to check whether relevant sliders in a beatmap are cheesed for rebalance scores..
83
50
  */
84
- class CursorOccurrenceGroup {
85
- /**
86
- * The cursor occurrence of movement type `movementType.DOWN`.
87
- */
88
- get down() {
89
- return this._down;
90
- }
91
- /**
92
- * The cursor occurrence of movement type `movementType.DOWN`.
93
- */
94
- set down(value) {
95
- if (value.id !== exports.MovementType.down) {
96
- throw new TypeError("Attempting to set the down cursor occurrence to one with different movement type.");
97
- }
98
- this._down = value;
99
- }
100
- /**
101
- * The cursor occurrences of movement type `movementType.MOVE`.
102
- */
103
- get moves() {
104
- return this._moves;
105
- }
106
- /**
107
- * The cursor occurrence of movement type `movementType.UP`.
108
- *
109
- * May not exist, such as when the player holds their cursor until the end of a beatmap.
110
- */
111
- get up() {
112
- return this._up;
113
- }
114
- /**
115
- * The cursor occurrence of movement type `movementType.UP`.
116
- *
117
- * May not exist, such as when the player holds their cursor until the end of a beatmap.
118
- */
119
- set up(value) {
120
- if (value && value.id !== exports.MovementType.up) {
121
- throw new TypeError("Attempting to set the up cursor occurrence to one with different movement type.");
122
- }
123
- this._up = value;
124
- }
125
- /**
126
- * The time at which this cursor occurrence group starts.
127
- */
128
- get startTime() {
129
- return this._down.time;
130
- }
131
- /**
132
- * The time at which this cursor occurrence group ends.
133
- */
134
- get endTime() {
135
- var _a, _b, _c, _d;
136
- return (_d = (_b = (_a = this._up) === null || _a === void 0 ? void 0 : _a.time) !== null && _b !== void 0 ? _b : (_c = this._moves.at(-1)) === null || _c === void 0 ? void 0 : _c.time) !== null && _d !== void 0 ? _d : this._down.time;
137
- }
51
+ class RebalanceSliderCheeseChecker {
138
52
  /**
139
- * The duration this cursor occurrence group is active for.
53
+ * @param beatmap The beatmap to analyze.
54
+ * @param data The data of the replay.
55
+ * @param difficultyAttributes The difficulty attributes of the beatmap.
140
56
  */
141
- get duration() {
142
- return this.endTime - this.startTime;
57
+ constructor(beatmap, data, difficultyAttributes) {
58
+ this.beatmap = beatmap;
59
+ this.data = data;
60
+ this.difficultyAttributes = difficultyAttributes;
61
+ this.hitWindow50 = difficultyAttributes.mods.has(osuBase.ModPrecise)
62
+ ? new osuBase.PreciseDroidHitWindow(beatmap.difficulty.od).mehWindow
63
+ : new osuBase.DroidHitWindow(beatmap.difficulty.od).mehWindow;
64
+ this.isHardRock = difficultyAttributes.mods.has(osuBase.ModHardRock);
143
65
  }
144
66
  /**
145
- * All cursor occurrences in this group.
146
- *
147
- * This iterates all occurrences and as such should be used sparingly or stored locally.
67
+ * Checks if relevant sliders in the given beatmap was cheesed.
148
68
  */
149
- get allOccurrences() {
150
- const cursors = [this._down, ...this._moves];
151
- if (this._up) {
152
- cursors.push(this._up);
69
+ check() {
70
+ if (this.difficultyAttributes.difficultSliders.length === 0 ||
71
+ this.difficultyAttributes.sliderFactor === 1) {
72
+ return {
73
+ aimPenalty: 1,
74
+ flashlightPenalty: 1,
75
+ };
153
76
  }
154
- return cursors;
155
- }
156
- constructor(down, moves, up) {
157
- this._down = down;
158
- this._moves = moves;
159
- // Re-set down cursor occurrence for checking.
160
- this.down = down;
161
- this.up = up;
162
- }
163
- /**
164
- * Determines whether this cursor occurrence group is active at the specified time.
165
- *
166
- * @param time The time.
167
- * @returns Whether this cursor occurrence group is active at the specified time.
168
- */
169
- isActiveAt(time) {
170
- return time >= this.startTime && time <= this.endTime;
77
+ const cheesedDifficultyRatings = this.checkSliderCheesing();
78
+ return this.calculateSliderCheesePenalty(cheesedDifficultyRatings);
171
79
  }
172
80
  /**
173
- * Finds the cursor occurrence that is active at a given time.
174
- *
175
- * @param time The time.
176
- * @returns The cursor occurrence at the given time, `null` if not found.
81
+ * Checks for sliders that were cheesed.
177
82
  */
178
- cursorAt(time) {
179
- var _a;
180
- if (!this.isActiveAt(time)) {
181
- return null;
182
- }
183
- if (this._down.time === time) {
184
- return this._down;
185
- }
186
- if (((_a = this._up) === null || _a === void 0 ? void 0 : _a.time) === time) {
187
- return this._up;
188
- }
189
- let l = 0;
190
- let r = this._moves.length - 2;
191
- while (l <= r) {
192
- const pivot = l + ((r - l) >> 1);
193
- if (this._moves[pivot].time < time) {
194
- l = pivot + 1;
83
+ checkSliderCheesing() {
84
+ const { objects } = this.beatmap.hitObjects;
85
+ const cheesedDifficultyRatings = [];
86
+ // Current loop indices are stored for efficiency.
87
+ const cursorLoopIndices = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
88
+ const acceptableRadius = objects[0].radius * 2;
89
+ // Sort difficult sliders by index so that cursor loop indices work properly.
90
+ for (const difficultSlider of this.difficultyAttributes.difficultSliders
91
+ .slice()
92
+ .sort((a, b) => a.index - b.index)) {
93
+ if (difficultSlider.index >= this.data.hitObjectData.length) {
94
+ continue;
195
95
  }
196
- else if (this._moves[pivot].time > time) {
197
- r = pivot - 1;
96
+ const object = objects[difficultSlider.index];
97
+ const objectData = this.data.hitObjectData[difficultSlider.index];
98
+ // If a miss or slider break occurs, we disregard the check for that slider.
99
+ if (objectData.result === osuBase.HitResult.miss ||
100
+ -this.hitWindow50 > objectData.accuracy ||
101
+ objectData.accuracy >
102
+ Math.min(this.hitWindow50, object.duration)) {
103
+ continue;
198
104
  }
199
- else {
200
- return this._moves[pivot];
105
+ const objectStartPosition = object.stackedPosition;
106
+ // These time boundaries should consider the delta time between the previous and next
107
+ // object as well as their hit accuracy. However, they are somewhat complicated to
108
+ // compute and the accuracy gain is small. As such, let's settle with 50 hit window.
109
+ const minTimeLimit = object.startTime - this.hitWindow50;
110
+ const maxTimeLimit = object.startTime + this.hitWindow50;
111
+ // Get the closest tap distance across all cursors.
112
+ const closestDistances = [];
113
+ const closestGroupIndices = [];
114
+ for (let i = 0; i < this.data.cursorMovement.length; ++i) {
115
+ const cursorGroups = this.data.cursorMovement[i].occurrenceGroups;
116
+ let closestDistance = Number.POSITIVE_INFINITY;
117
+ let closestIndex = cursorGroups.length;
118
+ for (let j = cursorLoopIndices[i]; j < cursorGroups.length; j = ++cursorLoopIndices[i]) {
119
+ const group = cursorGroups[j];
120
+ if (group.endTime < minTimeLimit) {
121
+ continue;
122
+ }
123
+ if (group.startTime > maxTimeLimit) {
124
+ break;
125
+ }
126
+ if (group.startTime >= minTimeLimit) {
127
+ const position = this.getCursorPosition(group.down);
128
+ const distance = position.getDistance(objectStartPosition);
129
+ if (closestDistance > distance) {
130
+ closestDistance = distance;
131
+ closestIndex = j;
132
+ }
133
+ if (closestDistance <= acceptableRadius / 2) {
134
+ break;
135
+ }
136
+ }
137
+ // Normally, we check if there are cursor presses within the group's active time.
138
+ // However, some funky workarounds are used throughout the game for replays, so
139
+ // for the time being we only check for cursor distances across the group.
140
+ const { allOccurrences } = group;
141
+ for (let k = 1; k < allOccurrences.length; ++k) {
142
+ const cursor = allOccurrences[k];
143
+ const prevCursor = allOccurrences[k - 1];
144
+ let distance = Number.POSITIVE_INFINITY;
145
+ const currentPosition = this.getCursorPosition(cursor);
146
+ const prevPosition = this.getCursorPosition(prevCursor);
147
+ switch (cursor.id) {
148
+ case exports.MovementType.up:
149
+ distance =
150
+ prevPosition.getDistance(objectStartPosition);
151
+ break;
152
+ case exports.MovementType.move:
153
+ for (let mSecPassed = Math.max(prevCursor.time, minTimeLimit); mSecPassed <=
154
+ Math.min(cursor.time, maxTimeLimit); ++mSecPassed) {
155
+ const t = (mSecPassed - prevCursor.time) /
156
+ (cursor.time - prevCursor.time);
157
+ const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
158
+ distance =
159
+ cursorPosition.getDistance(objectStartPosition);
160
+ if (closestDistance > distance) {
161
+ closestDistance = distance;
162
+ closestIndex = j;
163
+ }
164
+ if (closestDistance <=
165
+ acceptableRadius / 2) {
166
+ break;
167
+ }
168
+ }
169
+ }
170
+ if (closestDistance > distance) {
171
+ closestDistance = distance;
172
+ closestIndex = j;
173
+ }
174
+ if (closestDistance <= acceptableRadius / 2) {
175
+ break;
176
+ }
177
+ }
178
+ }
179
+ closestDistances.push(closestDistance);
180
+ closestGroupIndices.push(closestIndex);
181
+ if (cursorLoopIndices[i] > 0) {
182
+ // Decrement the index. The previous group may also have a role on the next slider.
183
+ --cursorLoopIndices[i];
184
+ }
201
185
  }
202
- }
203
- // l will be the first cursor occurrence with time > this._moves[l].time, but we want the one before it
204
- return this._moves[l - 1];
205
- }
206
- /**
207
- * Returns a string representation of this `CursorOccurrenceGroup`.
208
- */
209
- toString() {
210
- return `Down: ${this.down.time.toString()}ms (${this.down.position.x.toFixed(2)}, ${this.down.position.y.toFixed(2)}) | Moves: ${this.moves.length.toString()} | Up: ${this.up ? `${this.up.time.toString()}ms` : "N/A"}`;
211
- }
212
- }
213
-
214
- /**
215
- * Represents a cursor instance in an osu!droid replay.
216
- *
217
- * Stores cursor movement data in the form of `CursorOccurrenceGroup`s.
218
- *
219
- * This is used when analyzing replays using replay analyzer.
220
- */
221
- class CursorData {
222
- /**
223
- * The time at which the first occurrence of this cursor instance occurs.
224
- *
225
- * Will return `null` if there are no occurrences.
226
- */
227
- get earliestOccurrenceTime() {
228
- var _a, _b;
229
- return (_b = (_a = this.occurrenceGroups.at(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : null;
230
- }
231
- /**
232
- * The time at which the latest occurrence of this cursor instance occurs.
233
- *
234
- * Will return `null` if there are no occurrences.
235
- */
236
- get latestOccurrenceTime() {
237
- var _a, _b;
238
- return (_b = (_a = this.occurrenceGroups.at(-1)) === null || _a === void 0 ? void 0 : _a.endTime) !== null && _b !== void 0 ? _b : null;
239
- }
240
- /**
241
- * The amount of cursor occurrences of this cursor instance.
242
- */
243
- get totalOccurrences() {
244
- return this.occurrenceGroups.reduce((a, v) => {
245
- // Down cursor.
246
- ++a;
247
- // Move cursors.
248
- a += v.moves.length;
249
- if (v.up) {
250
- // Up cursor.
251
- ++a;
186
+ const cursorIndex = closestDistances.indexOf(Math.min(...closestDistances));
187
+ const closestDistance = closestDistances[cursorIndex];
188
+ if (closestDistance > acceptableRadius / 2) {
189
+ cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
190
+ continue;
252
191
  }
253
- return a;
254
- }, 0);
255
- }
256
- /**
257
- * All cursor occurrences of this cursor instnace.
258
- *
259
- * This iterates all occurrence groups and as such should be used sparingly or stored locally.
260
- */
261
- get allOccurrences() {
262
- return this.occurrenceGroups.flatMap((v) => v.allOccurrences);
263
- }
264
- constructor(values) {
265
- /**
266
- * The occurrence groups of this cursor instance.
267
- */
268
- this.occurrenceGroups = [];
269
- let downOccurrence = null;
270
- let moveOccurrences = [];
271
- for (let i = 0; i < values.size; ++i) {
272
- const occurrence = new CursorOccurrence(values.time[i], values.x[i], values.y[i], values.id[i]);
273
- switch (occurrence.id) {
274
- case exports.MovementType.down:
275
- downOccurrence = occurrence;
276
- break;
277
- case exports.MovementType.move:
278
- moveOccurrences.push(occurrence);
192
+ const group = this.data.cursorMovement[cursorIndex].occurrenceGroups[closestGroupIndices[cursorIndex]];
193
+ let isCheesed = false;
194
+ // Track cursor movement to see if it lands on every tick.
195
+ let occurrenceLoopIndex = 1;
196
+ const { allOccurrences } = group;
197
+ for (let i = 1; i < object.nestedHitObjects.length; ++i) {
198
+ if (isCheesed) {
279
199
  break;
280
- case exports.MovementType.up:
281
- if (downOccurrence) {
282
- this.occurrenceGroups.push(new CursorOccurrenceGroup(downOccurrence, moveOccurrences, occurrence));
283
- downOccurrence = null;
200
+ }
201
+ const tickWasHit = objectData.tickset[i - 1];
202
+ if (!tickWasHit) {
203
+ continue;
204
+ }
205
+ const nestedObject = object.nestedHitObjects[i];
206
+ const nestedPosition = nestedObject.stackedPosition;
207
+ while (occurrenceLoopIndex < allOccurrences.length &&
208
+ allOccurrences[occurrenceLoopIndex].time <
209
+ nestedObject.startTime) {
210
+ ++occurrenceLoopIndex;
211
+ }
212
+ if (occurrenceLoopIndex === allOccurrences.length) {
213
+ continue;
214
+ }
215
+ const cursor = allOccurrences[occurrenceLoopIndex];
216
+ const prevCursor = allOccurrences[occurrenceLoopIndex - 1];
217
+ const currentPosition = this.getCursorPosition(cursor);
218
+ const prevPosition = this.getCursorPosition(prevCursor);
219
+ switch (cursor.id) {
220
+ case exports.MovementType.move: {
221
+ // Interpolate cursor position during nested object time.
222
+ const t = (nestedObject.startTime - prevCursor.time) /
223
+ (cursor.time - prevCursor.time);
224
+ const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
225
+ const distance = cursorPosition.getDistance(nestedPosition);
226
+ isCheesed = distance > acceptableRadius;
227
+ break;
284
228
  }
285
- moveOccurrences = [];
229
+ case exports.MovementType.up:
230
+ isCheesed =
231
+ prevPosition.getDistance(nestedPosition) >
232
+ acceptableRadius;
233
+ }
234
+ }
235
+ if (isCheesed) {
236
+ cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
286
237
  }
287
238
  }
288
- // Add the final cursor occurrence group as the loop may not catch it for special cases.
289
- if (downOccurrence && moveOccurrences.length > 0) {
290
- this.occurrenceGroups.push(new CursorOccurrenceGroup(downOccurrence, moveOccurrences));
291
- }
239
+ return cheesedDifficultyRatings;
292
240
  }
293
- }
294
-
295
- /**
296
- * The result of a hit in an osu!droid replay.
297
- */
298
- exports.HitResult = void 0;
299
- (function (HitResult) {
300
241
  /**
301
- * Miss (0).
302
- */
303
- HitResult[HitResult["miss"] = 1] = "miss";
304
- /**
305
- * Meh (50).
306
- */
307
- HitResult[HitResult["meh"] = 2] = "meh";
308
- /**
309
- * Good (100).
310
- */
311
- HitResult[HitResult["good"] = 3] = "good";
312
- /**
313
- * Great (300).
242
+ * Calculates the slider cheese penalty.
314
243
  */
315
- HitResult[HitResult["great"] = 4] = "great";
316
- })(exports.HitResult || (exports.HitResult = {}));
244
+ calculateSliderCheesePenalty(cheesedDifficultyRatings) {
245
+ const summedDifficultyRating = Math.min(1, cheesedDifficultyRatings.reduce((a, v) => a + v, 0));
246
+ return {
247
+ aimPenalty: Math.max(this.difficultyAttributes.sliderFactor, Math.pow(1 -
248
+ summedDifficultyRating *
249
+ this.difficultyAttributes.sliderFactor, 2)),
250
+ flashlightPenalty: 1,
251
+ };
252
+ }
253
+ getCursorPosition(cursor) {
254
+ if (this.isHardRock) {
255
+ return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
256
+ }
257
+ return cursor.position;
258
+ }
259
+ }
317
260
 
318
261
  /**
319
262
  * Utility to check whether or not a beatmap is three-fingered for rebalance scores.
@@ -399,7 +342,7 @@ class RebalanceThreeFingerChecker {
399
342
  const objectBeforeData = objectData[beforeIndex];
400
343
  let timeBefore = objectBefore.endTime;
401
344
  if (objectBefore instanceof osuBase.Circle) {
402
- if (objectBeforeData.result !== exports.HitResult.miss) {
345
+ if (objectBeforeData.result !== osuBase.HitResult.miss) {
403
346
  timeBefore += objectBeforeData.accuracy;
404
347
  }
405
348
  else {
@@ -411,7 +354,7 @@ class RebalanceThreeFingerChecker {
411
354
  const objectAfterData = objectData[afterIndex];
412
355
  let timeAfter = objectAfter.startTime;
413
356
  if (objectAfter instanceof osuBase.Circle &&
414
- objectAfterData.result !== exports.HitResult.miss) {
357
+ objectAfterData.result !== osuBase.HitResult.miss) {
415
358
  timeAfter += objectAfterData.accuracy;
416
359
  }
417
360
  this.breakPointAccurateTimes.push(new osuBase.BreakPoint(timeBefore, timeAfter));
@@ -433,10 +376,10 @@ class RebalanceThreeFingerChecker {
433
376
  let firstObjectHitWindow = this.hitWindow.mehWindow;
434
377
  if (firstObject instanceof osuBase.Circle) {
435
378
  switch (firstObjectResult) {
436
- case exports.HitResult.great:
379
+ case osuBase.HitResult.great:
437
380
  firstObjectHitWindow = this.hitWindow.greatWindow;
438
381
  break;
439
- case exports.HitResult.good:
382
+ case osuBase.HitResult.good:
440
383
  firstObjectHitWindow = this.hitWindow.okWindow;
441
384
  break;
442
385
  default:
@@ -447,10 +390,10 @@ class RebalanceThreeFingerChecker {
447
390
  let lastObjectHitWindow = this.hitWindow.mehWindow;
448
391
  if (lastObject instanceof osuBase.Circle) {
449
392
  switch (lastObjectResult) {
450
- case exports.HitResult.great:
393
+ case osuBase.HitResult.great:
451
394
  lastObjectHitWindow = this.hitWindow.greatWindow;
452
395
  break;
453
- case exports.HitResult.good:
396
+ case osuBase.HitResult.good:
454
397
  lastObjectHitWindow = this.hitWindow.okWindow;
455
398
  break;
456
399
  default:
@@ -519,7 +462,7 @@ class RebalanceThreeFingerChecker {
519
462
  * @returns The index of the cursor, -1 if the object was missed or it's a spinner.
520
463
  */
521
464
  getObjectAimIndex(object, objectData, cursorGroupIndices, cursorIndices) {
522
- if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
465
+ if (objectData.result === osuBase.HitResult.miss || object instanceof osuBase.Spinner) {
523
466
  return -1;
524
467
  }
525
468
  // Check for sliderbreaks and treat them as misses.
@@ -565,351 +508,138 @@ class RebalanceThreeFingerChecker {
565
508
  const currentPosition = this.getCursorPosition(cursor);
566
509
  const prevPosition = this.getCursorPosition(prevCursor);
567
510
  switch (cursor.id) {
568
- case exports.MovementType.up:
569
- distance = prevPosition.getDistance(objectPosition);
570
- break;
571
- case exports.MovementType.move: {
572
- // Interpolate movement.
573
- const t = (hitTime - prevCursor.time) /
574
- (cursor.time - prevCursor.time);
575
- const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
576
- distance =
577
- objectPosition.getDistance(cursorPosition);
578
- break;
579
- }
580
- case exports.MovementType.down:
581
- continue;
582
- }
583
- if (closestDistance > distance) {
584
- closestDistance = distance;
585
- nearestCursorIndex = i;
586
- }
587
- }
588
- // Reset cursor index pointer on end of group.
589
- if (cursorIndices[i] === cursors.length) {
590
- cursorIndices[i] = 1;
591
- }
592
- break;
593
- }
594
- // The previous object may still be hit with the same cursor group or cursor index.
595
- cursorGroupIndices[i] = Math.max(0, cursorGroupIndices[i] - 1);
596
- cursorIndices[i] = Math.max(1, cursorIndices[i] - 1);
597
- }
598
- return nearestCursorIndex;
599
- }
600
- /**
601
- * Obtains the index of the nearest cursor of which an object was pressed in terms of time.
602
- *
603
- * @param object The object to obtain the index for.
604
- * @param objectData The hit data of the object.
605
- * @param cursorLookupIndices The cursor indices to start looking for the cursor from, to save computation time.
606
- * @param excludedIndices The cursor indices that should not be checked.
607
- * @returns The index of the cursor, -1 if the object was missed or it's a spinner.
608
- */
609
- getObjectPressIndex(object, objectData, cursorLookupIndices) {
610
- if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
611
- return -1;
612
- }
613
- // Check for sliderbreaks and treat them as misses.
614
- if (object instanceof osuBase.Slider &&
615
- (-this.hitWindow.mehWindow > objectData.accuracy ||
616
- objectData.accuracy >
617
- Math.min(this.hitWindow.mehWindow, object.duration))) {
618
- return -1;
619
- }
620
- const hitTime = object.startTime + objectData.accuracy;
621
- let nearestCursorInstanceIndex = -1;
622
- let nearestTime = Number.POSITIVE_INFINITY;
623
- for (let i = 0; i < this.downCursorInstances.length; ++i) {
624
- const cursors = this.downCursorInstances[i];
625
- let cursorNearestTime = Number.POSITIVE_INFINITY;
626
- for (let j = cursorLookupIndices[i]; j < cursors.length; cursorLookupIndices[i] = ++j) {
627
- const cursor = cursors[j];
628
- if (cursor.time > hitTime) {
629
- break;
630
- }
631
- cursorNearestTime = hitTime - cursor.time;
632
- }
633
- if (cursorNearestTime < nearestTime) {
634
- nearestCursorInstanceIndex = i;
635
- nearestTime = cursorNearestTime;
636
- }
637
- }
638
- return nearestCursorInstanceIndex;
639
- }
640
- /**
641
- * Creates nerf factors by scanning through objects.
642
- */
643
- calculateNerfFactors() {
644
- for (const beatmapSection of this.beatmapSections) {
645
- const threeFingerCursorCounts = osuBase.Utils.initializeArray(Math.max(0, this.downCursorInstances.length - 2), 0);
646
- for (const object of beatmapSection.objects) {
647
- if (object.pressingCursorInstanceIndex === -1) {
648
- continue;
649
- }
650
- if (object.aimingCursorInstanceIndex < 3) {
651
- // The aim cursor is in the first three cursors. They are counted as non-3 finger.
652
- switch (object.pressingCursorInstanceIndex) {
653
- case 0:
654
- case 1:
655
- case 2:
656
- break;
657
- default:
658
- ++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 3];
659
- break;
660
- }
661
- }
662
- else {
663
- // The aim cursor is somewhere else. only count the first 2 cursors as non-3 finger.
664
- switch (object.pressingCursorInstanceIndex) {
665
- case 0:
666
- case 1:
667
- break;
668
- default:
669
- ++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 2];
670
- break;
671
- }
672
- }
673
- }
674
- const threeFingerCursorCount = threeFingerCursorCounts.reduce((a, v) => a + v, 0);
675
- if (threeFingerCursorCount === 0) {
676
- continue;
677
- }
678
- const sectionObjectCount = beatmapSection.objects.length;
679
- const threeFingeredObjectRatio = threeFingerCursorCount / sectionObjectCount;
680
- const strainFactor = Math.max(1, beatmapSection.sumStrain * threeFingeredObjectRatio);
681
- // Finger factor applies more penalty if more fingers were used.
682
- const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
683
- Math.pow(((index + 1) * count) / sectionObjectCount, 0.9), 1);
684
- // Length factor applies more penalty if there are more 3-fingered object.
685
- const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 0.8);
686
- this.nerfFactors.push({
687
- strainFactor: strainFactor,
688
- fingerFactor: fingerFactor,
689
- lengthFactor: lengthFactor,
690
- });
691
- }
692
- }
693
- /**
694
- * Calculates the final penalty.
695
- */
696
- calculateFinalPenalty() {
697
- return this.nerfFactors.reduce((a, n) => a +
698
- 0.015 *
699
- Math.pow(n.strainFactor * n.fingerFactor * n.lengthFactor, 1.05), 1);
700
- }
701
- getCursorPosition(cursor) {
702
- if (this.isHardRock) {
703
- return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
704
- }
705
- return cursor.position;
706
- }
707
- }
708
-
709
- /**
710
- * Utility to check whether relevant sliders in a beatmap are cheesed for rebalance scores..
711
- */
712
- class RebalanceSliderCheeseChecker {
713
- /**
714
- * @param beatmap The beatmap to analyze.
715
- * @param data The data of the replay.
716
- * @param difficultyAttributes The difficulty attributes of the beatmap.
717
- */
718
- constructor(beatmap, data, difficultyAttributes) {
719
- this.beatmap = beatmap;
720
- this.data = data;
721
- this.difficultyAttributes = difficultyAttributes;
722
- this.hitWindow50 = difficultyAttributes.mods.has(osuBase.ModPrecise)
723
- ? new osuBase.PreciseDroidHitWindow(beatmap.difficulty.od).mehWindow
724
- : new osuBase.DroidHitWindow(beatmap.difficulty.od).mehWindow;
725
- this.isHardRock = difficultyAttributes.mods.has(osuBase.ModHardRock);
726
- }
727
- /**
728
- * Checks if relevant sliders in the given beatmap was cheesed.
729
- */
730
- check() {
731
- if (this.difficultyAttributes.difficultSliders.length === 0 ||
732
- this.difficultyAttributes.sliderFactor === 1) {
733
- return {
734
- aimPenalty: 1,
735
- flashlightPenalty: 1,
736
- };
737
- }
738
- const cheesedDifficultyRatings = this.checkSliderCheesing();
739
- return this.calculateSliderCheesePenalty(cheesedDifficultyRatings);
740
- }
741
- /**
742
- * Checks for sliders that were cheesed.
743
- */
744
- checkSliderCheesing() {
745
- const { objects } = this.beatmap.hitObjects;
746
- const cheesedDifficultyRatings = [];
747
- // Current loop indices are stored for efficiency.
748
- const cursorLoopIndices = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
749
- const acceptableRadius = objects[0].radius * 2;
750
- // Sort difficult sliders by index so that cursor loop indices work properly.
751
- for (const difficultSlider of this.difficultyAttributes.difficultSliders
752
- .slice()
753
- .sort((a, b) => a.index - b.index)) {
754
- if (difficultSlider.index >= this.data.hitObjectData.length) {
755
- continue;
756
- }
757
- const object = objects[difficultSlider.index];
758
- const objectData = this.data.hitObjectData[difficultSlider.index];
759
- // If a miss or slider break occurs, we disregard the check for that slider.
760
- if (objectData.result === exports.HitResult.miss ||
761
- -this.hitWindow50 > objectData.accuracy ||
762
- objectData.accuracy >
763
- Math.min(this.hitWindow50, object.duration)) {
764
- continue;
765
- }
766
- const objectStartPosition = object.stackedPosition;
767
- // These time boundaries should consider the delta time between the previous and next
768
- // object as well as their hit accuracy. However, they are somewhat complicated to
769
- // compute and the accuracy gain is small. As such, let's settle with 50 hit window.
770
- const minTimeLimit = object.startTime - this.hitWindow50;
771
- const maxTimeLimit = object.startTime + this.hitWindow50;
772
- // Get the closest tap distance across all cursors.
773
- const closestDistances = [];
774
- const closestGroupIndices = [];
775
- for (let i = 0; i < this.data.cursorMovement.length; ++i) {
776
- const cursorGroups = this.data.cursorMovement[i].occurrenceGroups;
777
- let closestDistance = Number.POSITIVE_INFINITY;
778
- let closestIndex = cursorGroups.length;
779
- for (let j = cursorLoopIndices[i]; j < cursorGroups.length; j = ++cursorLoopIndices[i]) {
780
- const group = cursorGroups[j];
781
- if (group.endTime < minTimeLimit) {
782
- continue;
783
- }
784
- if (group.startTime > maxTimeLimit) {
785
- break;
786
- }
787
- if (group.startTime >= minTimeLimit) {
788
- const position = this.getCursorPosition(group.down);
789
- const distance = position.getDistance(objectStartPosition);
790
- if (closestDistance > distance) {
791
- closestDistance = distance;
792
- closestIndex = j;
793
- }
794
- if (closestDistance <= acceptableRadius / 2) {
511
+ case exports.MovementType.up:
512
+ distance = prevPosition.getDistance(objectPosition);
795
513
  break;
796
- }
797
- }
798
- // Normally, we check if there are cursor presses within the group's active time.
799
- // However, some funky workarounds are used throughout the game for replays, so
800
- // for the time being we only check for cursor distances across the group.
801
- const { allOccurrences } = group;
802
- for (let k = 1; k < allOccurrences.length; ++k) {
803
- const cursor = allOccurrences[k];
804
- const prevCursor = allOccurrences[k - 1];
805
- let distance = Number.POSITIVE_INFINITY;
806
- const currentPosition = this.getCursorPosition(cursor);
807
- const prevPosition = this.getCursorPosition(prevCursor);
808
- switch (cursor.id) {
809
- case exports.MovementType.up:
810
- distance =
811
- prevPosition.getDistance(objectStartPosition);
812
- break;
813
- case exports.MovementType.move:
814
- for (let mSecPassed = Math.max(prevCursor.time, minTimeLimit); mSecPassed <=
815
- Math.min(cursor.time, maxTimeLimit); ++mSecPassed) {
816
- const t = (mSecPassed - prevCursor.time) /
817
- (cursor.time - prevCursor.time);
818
- const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
819
- distance =
820
- cursorPosition.getDistance(objectStartPosition);
821
- if (closestDistance > distance) {
822
- closestDistance = distance;
823
- closestIndex = j;
824
- }
825
- if (closestDistance <=
826
- acceptableRadius / 2) {
827
- break;
828
- }
829
- }
830
- }
831
- if (closestDistance > distance) {
832
- closestDistance = distance;
833
- closestIndex = j;
834
- }
835
- if (closestDistance <= acceptableRadius / 2) {
514
+ case exports.MovementType.move: {
515
+ // Interpolate movement.
516
+ const t = (hitTime - prevCursor.time) /
517
+ (cursor.time - prevCursor.time);
518
+ const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
519
+ distance =
520
+ objectPosition.getDistance(cursorPosition);
836
521
  break;
837
522
  }
523
+ case exports.MovementType.down:
524
+ continue;
525
+ }
526
+ if (closestDistance > distance) {
527
+ closestDistance = distance;
528
+ nearestCursorIndex = i;
838
529
  }
839
530
  }
840
- closestDistances.push(closestDistance);
841
- closestGroupIndices.push(closestIndex);
842
- if (cursorLoopIndices[i] > 0) {
843
- // Decrement the index. The previous group may also have a role on the next slider.
844
- --cursorLoopIndices[i];
531
+ // Reset cursor index pointer on end of group.
532
+ if (cursorIndices[i] === cursors.length) {
533
+ cursorIndices[i] = 1;
845
534
  }
535
+ break;
846
536
  }
847
- const cursorIndex = closestDistances.indexOf(Math.min(...closestDistances));
848
- const closestDistance = closestDistances[cursorIndex];
849
- if (closestDistance > acceptableRadius / 2) {
850
- cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
851
- continue;
852
- }
853
- const group = this.data.cursorMovement[cursorIndex].occurrenceGroups[closestGroupIndices[cursorIndex]];
854
- let isCheesed = false;
855
- // Track cursor movement to see if it lands on every tick.
856
- let occurrenceLoopIndex = 1;
857
- const { allOccurrences } = group;
858
- for (let i = 1; i < object.nestedHitObjects.length; ++i) {
859
- if (isCheesed) {
537
+ // The previous object may still be hit with the same cursor group or cursor index.
538
+ cursorGroupIndices[i] = Math.max(0, cursorGroupIndices[i] - 1);
539
+ cursorIndices[i] = Math.max(1, cursorIndices[i] - 1);
540
+ }
541
+ return nearestCursorIndex;
542
+ }
543
+ /**
544
+ * Obtains the index of the nearest cursor of which an object was pressed in terms of time.
545
+ *
546
+ * @param object The object to obtain the index for.
547
+ * @param objectData The hit data of the object.
548
+ * @param cursorLookupIndices The cursor indices to start looking for the cursor from, to save computation time.
549
+ * @param excludedIndices The cursor indices that should not be checked.
550
+ * @returns The index of the cursor, -1 if the object was missed or it's a spinner.
551
+ */
552
+ getObjectPressIndex(object, objectData, cursorLookupIndices) {
553
+ if (objectData.result === osuBase.HitResult.miss || object instanceof osuBase.Spinner) {
554
+ return -1;
555
+ }
556
+ // Check for sliderbreaks and treat them as misses.
557
+ if (object instanceof osuBase.Slider &&
558
+ (-this.hitWindow.mehWindow > objectData.accuracy ||
559
+ objectData.accuracy >
560
+ Math.min(this.hitWindow.mehWindow, object.duration))) {
561
+ return -1;
562
+ }
563
+ const hitTime = object.startTime + objectData.accuracy;
564
+ let nearestCursorInstanceIndex = -1;
565
+ let nearestTime = Number.POSITIVE_INFINITY;
566
+ for (let i = 0; i < this.downCursorInstances.length; ++i) {
567
+ const cursors = this.downCursorInstances[i];
568
+ let cursorNearestTime = Number.POSITIVE_INFINITY;
569
+ for (let j = cursorLookupIndices[i]; j < cursors.length; cursorLookupIndices[i] = ++j) {
570
+ const cursor = cursors[j];
571
+ if (cursor.time > hitTime) {
860
572
  break;
861
573
  }
862
- const tickWasHit = objectData.tickset[i - 1];
863
- if (!tickWasHit) {
574
+ cursorNearestTime = hitTime - cursor.time;
575
+ }
576
+ if (cursorNearestTime < nearestTime) {
577
+ nearestCursorInstanceIndex = i;
578
+ nearestTime = cursorNearestTime;
579
+ }
580
+ }
581
+ return nearestCursorInstanceIndex;
582
+ }
583
+ /**
584
+ * Creates nerf factors by scanning through objects.
585
+ */
586
+ calculateNerfFactors() {
587
+ for (const beatmapSection of this.beatmapSections) {
588
+ const threeFingerCursorCounts = osuBase.Utils.initializeArray(Math.max(0, this.downCursorInstances.length - 2), 0);
589
+ for (const object of beatmapSection.objects) {
590
+ if (object.pressingCursorInstanceIndex === -1) {
864
591
  continue;
865
592
  }
866
- const nestedObject = object.nestedHitObjects[i];
867
- const nestedPosition = nestedObject.stackedPosition;
868
- while (occurrenceLoopIndex < allOccurrences.length &&
869
- allOccurrences[occurrenceLoopIndex].time <
870
- nestedObject.startTime) {
871
- ++occurrenceLoopIndex;
872
- }
873
- if (occurrenceLoopIndex === allOccurrences.length) {
874
- continue;
593
+ if (object.aimingCursorInstanceIndex < 3) {
594
+ // The aim cursor is in the first three cursors. They are counted as non-3 finger.
595
+ switch (object.pressingCursorInstanceIndex) {
596
+ case 0:
597
+ case 1:
598
+ case 2:
599
+ break;
600
+ default:
601
+ ++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 3];
602
+ break;
603
+ }
875
604
  }
876
- const cursor = allOccurrences[occurrenceLoopIndex];
877
- const prevCursor = allOccurrences[occurrenceLoopIndex - 1];
878
- const currentPosition = this.getCursorPosition(cursor);
879
- const prevPosition = this.getCursorPosition(prevCursor);
880
- switch (cursor.id) {
881
- case exports.MovementType.move: {
882
- // Interpolate cursor position during nested object time.
883
- const t = (nestedObject.startTime - prevCursor.time) /
884
- (cursor.time - prevCursor.time);
885
- const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
886
- const distance = cursorPosition.getDistance(nestedPosition);
887
- isCheesed = distance > acceptableRadius;
888
- break;
605
+ else {
606
+ // The aim cursor is somewhere else. only count the first 2 cursors as non-3 finger.
607
+ switch (object.pressingCursorInstanceIndex) {
608
+ case 0:
609
+ case 1:
610
+ break;
611
+ default:
612
+ ++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 2];
613
+ break;
889
614
  }
890
- case exports.MovementType.up:
891
- isCheesed =
892
- prevPosition.getDistance(nestedPosition) >
893
- acceptableRadius;
894
615
  }
895
616
  }
896
- if (isCheesed) {
897
- cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
617
+ const threeFingerCursorCount = threeFingerCursorCounts.reduce((a, v) => a + v, 0);
618
+ if (threeFingerCursorCount === 0) {
619
+ continue;
898
620
  }
621
+ const sectionObjectCount = beatmapSection.objects.length;
622
+ const threeFingeredObjectRatio = threeFingerCursorCount / sectionObjectCount;
623
+ const strainFactor = Math.max(1, beatmapSection.sumStrain * threeFingeredObjectRatio);
624
+ // Finger factor applies more penalty if more fingers were used.
625
+ const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
626
+ Math.pow(((index + 1) * count) / sectionObjectCount, 0.9), 1);
627
+ // Length factor applies more penalty if there are more 3-fingered object.
628
+ const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 0.8);
629
+ this.nerfFactors.push({
630
+ strainFactor: strainFactor,
631
+ fingerFactor: fingerFactor,
632
+ lengthFactor: lengthFactor,
633
+ });
899
634
  }
900
- return cheesedDifficultyRatings;
901
635
  }
902
636
  /**
903
- * Calculates the slider cheese penalty.
637
+ * Calculates the final penalty.
904
638
  */
905
- calculateSliderCheesePenalty(cheesedDifficultyRatings) {
906
- const summedDifficultyRating = Math.min(1, cheesedDifficultyRatings.reduce((a, v) => a + v, 0));
907
- return {
908
- aimPenalty: Math.max(this.difficultyAttributes.sliderFactor, Math.pow(1 -
909
- summedDifficultyRating *
910
- this.difficultyAttributes.sliderFactor, 2)),
911
- flashlightPenalty: 1,
912
- };
639
+ calculateFinalPenalty() {
640
+ return this.nerfFactors.reduce((a, n) => a +
641
+ 0.015 *
642
+ Math.pow(n.strainFactor * n.fingerFactor * n.lengthFactor, 1.05), 1);
913
643
  }
914
644
  getCursorPosition(cursor) {
915
645
  if (this.isHardRock) {
@@ -919,38 +649,6 @@ class RebalanceSliderCheeseChecker {
919
649
  }
920
650
  }
921
651
 
922
- /******************************************************************************
923
- Copyright (c) Microsoft Corporation.
924
-
925
- Permission to use, copy, modify, and/or distribute this software for any
926
- purpose with or without fee is hereby granted.
927
-
928
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
929
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
930
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
931
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
932
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
933
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
934
- PERFORMANCE OF THIS SOFTWARE.
935
- ***************************************************************************** */
936
- /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
937
-
938
-
939
- function __awaiter(thisArg, _arguments, P, generator) {
940
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
941
- return new (P || (P = Promise))(function (resolve, reject) {
942
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
943
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
944
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
945
- step((generator = generator.apply(thisArg, _arguments || [])).next());
946
- });
947
- }
948
-
949
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
950
- var e = new Error(message);
951
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
952
- };
953
-
954
652
  /**
955
653
  * Utility to check whether relevant sliders in a beatmap are cheesed for live scores.
956
654
  */
@@ -1003,10 +701,16 @@ class SliderCheeseChecker {
1003
701
  const object = objects[difficultSlider.index];
1004
702
  const objectData = this.data.hitObjectData[difficultSlider.index];
1005
703
  // If a miss or slider break occurs, we disregard the check for that slider.
1006
- if (objectData.result === exports.HitResult.miss ||
1007
- -this.hitWindow50 > objectData.accuracy ||
1008
- objectData.accuracy >
1009
- Math.min(this.hitWindow50, object.duration)) {
704
+ if (objectData.result === osuBase.HitResult.miss) {
705
+ continue;
706
+ }
707
+ let lateHitThreshold = this.hitWindow50;
708
+ // Before replay version 8, the slider head's hit window is capped to the duration of the slider.
709
+ if (this.data.replayVersion < 8) {
710
+ lateHitThreshold = Math.min(lateHitThreshold, object.duration);
711
+ }
712
+ if (objectData.accuracy < -this.hitWindow50 ||
713
+ objectData.accuracy > lateHitThreshold) {
1010
714
  continue;
1011
715
  }
1012
716
  const objectStartPosition = object.stackedPosition;
@@ -1251,7 +955,7 @@ class ThreeFingerChecker {
1251
955
  const objectBeforeData = objectData[beforeIndex];
1252
956
  let timeBefore = objectBefore.endTime;
1253
957
  if (objectBefore instanceof osuBase.Circle) {
1254
- if (objectBeforeData.result !== exports.HitResult.miss) {
958
+ if (objectBeforeData.result !== osuBase.HitResult.miss) {
1255
959
  timeBefore += objectBeforeData.accuracy;
1256
960
  }
1257
961
  else {
@@ -1263,7 +967,7 @@ class ThreeFingerChecker {
1263
967
  const objectAfterData = objectData[afterIndex];
1264
968
  let timeAfter = objectAfter.startTime;
1265
969
  if (objectAfter instanceof osuBase.Circle &&
1266
- objectAfterData.result !== exports.HitResult.miss) {
970
+ objectAfterData.result !== osuBase.HitResult.miss) {
1267
971
  timeAfter += objectAfterData.accuracy;
1268
972
  }
1269
973
  this.breakPointAccurateTimes.push(new osuBase.BreakPoint(timeBefore, timeAfter));
@@ -1285,10 +989,10 @@ class ThreeFingerChecker {
1285
989
  let firstObjectHitWindow = this.hitWindow.mehWindow;
1286
990
  if (firstObject instanceof osuBase.Circle) {
1287
991
  switch (firstObjectResult) {
1288
- case exports.HitResult.great:
992
+ case osuBase.HitResult.great:
1289
993
  firstObjectHitWindow = this.hitWindow.greatWindow;
1290
994
  break;
1291
- case exports.HitResult.good:
995
+ case osuBase.HitResult.good:
1292
996
  firstObjectHitWindow = this.hitWindow.okWindow;
1293
997
  break;
1294
998
  default:
@@ -1299,10 +1003,10 @@ class ThreeFingerChecker {
1299
1003
  let lastObjectHitWindow = this.hitWindow.mehWindow;
1300
1004
  if (lastObject instanceof osuBase.Circle) {
1301
1005
  switch (lastObjectResult) {
1302
- case exports.HitResult.great:
1006
+ case osuBase.HitResult.great:
1303
1007
  lastObjectHitWindow = this.hitWindow.greatWindow;
1304
1008
  break;
1305
- case exports.HitResult.good:
1009
+ case osuBase.HitResult.good:
1306
1010
  lastObjectHitWindow = this.hitWindow.okWindow;
1307
1011
  break;
1308
1012
  default:
@@ -1369,18 +1073,23 @@ class ThreeFingerChecker {
1369
1073
  * @param cursorGroupIndices The cursor indices to start looking for the cursor group from, to save computation time.
1370
1074
  * @param cursorIndices The cursor indices to start looking for the cursor from, to save computation time.
1371
1075
  * @returns The index of the cursor, -1 if the object was missed or it's a spinner.
1372
- */
1373
- getObjectAimIndex(object, objectData, cursorGroupIndices, cursorIndices) {
1374
- if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
1375
- return -1;
1376
- }
1377
- // Check for sliderbreaks and treat them as misses.
1378
- if (object instanceof osuBase.Slider &&
1379
- (-this.hitWindow.mehWindow > objectData.accuracy ||
1380
- objectData.accuracy >
1381
- Math.min(this.hitWindow.mehWindow, object.duration))) {
1076
+ */
1077
+ getObjectAimIndex(object, objectData, cursorGroupIndices, cursorIndices) {
1078
+ if (objectData.result === osuBase.HitResult.miss || object instanceof osuBase.Spinner) {
1382
1079
  return -1;
1383
1080
  }
1081
+ // Check for sliderbreaks and treat them as misses.
1082
+ if (object instanceof osuBase.Slider) {
1083
+ let lateHitThreshold = this.hitWindow.mehWindow;
1084
+ // Before replay version 8, the slider head's hit window is capped to the duration of the slider.
1085
+ if (this.data.replayVersion < 8) {
1086
+ lateHitThreshold = Math.min(lateHitThreshold, object.duration);
1087
+ }
1088
+ if (objectData.accuracy < -this.hitWindow.mehWindow ||
1089
+ objectData.accuracy > lateHitThreshold) {
1090
+ return -1;
1091
+ }
1092
+ }
1384
1093
  const hitTime = object.startTime + objectData.accuracy;
1385
1094
  const objectPosition = object.stackedPosition;
1386
1095
  // We are maintaining the closest distance to the object.
@@ -1459,15 +1168,20 @@ class ThreeFingerChecker {
1459
1168
  * @returns The index of the cursor, -1 if the object was missed or it's a spinner.
1460
1169
  */
1461
1170
  getObjectPressIndex(object, objectData, cursorLookupIndices) {
1462
- if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
1171
+ if (objectData.result === osuBase.HitResult.miss || object instanceof osuBase.Spinner) {
1463
1172
  return -1;
1464
1173
  }
1465
1174
  // Check for sliderbreaks and treat them as misses.
1466
- if (object instanceof osuBase.Slider &&
1467
- (-this.hitWindow.mehWindow > objectData.accuracy ||
1468
- objectData.accuracy >
1469
- Math.min(this.hitWindow.mehWindow, object.duration))) {
1470
- return -1;
1175
+ if (object instanceof osuBase.Slider) {
1176
+ let lateHitThreshold = this.hitWindow.mehWindow;
1177
+ // Before replay version 8, the slider head's hit window is capped to the duration of the slider.
1178
+ if (this.data.replayVersion < 8) {
1179
+ lateHitThreshold = Math.min(lateHitThreshold, object.duration);
1180
+ }
1181
+ if (objectData.accuracy < -this.hitWindow.mehWindow ||
1182
+ objectData.accuracy > lateHitThreshold) {
1183
+ return -1;
1184
+ }
1471
1185
  }
1472
1186
  const hitTime = object.startTime + objectData.accuracy;
1473
1187
  let nearestCursorInstanceIndex = -1;
@@ -1778,7 +1492,7 @@ class TwoHandChecker {
1778
1492
  const prevObject = this.beatmap.hitObjects.objects[objectIndex - 1];
1779
1493
  const prevObjectData = this.data.hitObjectData[objectIndex - 1];
1780
1494
  if (prevObject instanceof osuBase.Spinner ||
1781
- prevObjectData.result === exports.HitResult.miss) {
1495
+ prevObjectData.result === osuBase.HitResult.miss) {
1782
1496
  return new IndexedHitObject(object, -1, -1, -1, null, false);
1783
1497
  }
1784
1498
  const objectStartPosition = object.stackedPosition;
@@ -1933,10 +1647,10 @@ class TwoHandChecker {
1933
1647
  // For sliders, set the hit window to as lenient as possible.
1934
1648
  if (object instanceof osuBase.Circle) {
1935
1649
  switch (data.result) {
1936
- case exports.HitResult.great:
1650
+ case osuBase.HitResult.great:
1937
1651
  hitWindow = this.hitWindow.greatWindow;
1938
1652
  break;
1939
- case exports.HitResult.good:
1653
+ case osuBase.HitResult.good:
1940
1654
  hitWindow = this.hitWindow.okWindow;
1941
1655
  break;
1942
1656
  }
@@ -2080,119 +1794,366 @@ class TwoHandChecker {
2080
1794
  if (!(object instanceof osuBase.Slider)) {
2081
1795
  return this.getCursorPositionForObjectStart(objectIndex);
2082
1796
  }
2083
- const nextObject = this.beatmap.hitObjects.objects[objectIndex - 1];
2084
- let objectEndPosition = object.stackedEndPosition;
2085
- if (object.distance > 0 && nextObject) {
2086
- const endPosition = object.stackedEndPosition;
2087
- const nextStartPosition = nextObject.stackedPosition;
2088
- const lazyEndMovement = nextStartPosition.subtract(endPosition);
2089
- const actualEndMovement = nextStartPosition.subtract(objectEndPosition);
2090
- if (lazyEndMovement.length < actualEndMovement.length) {
2091
- objectEndPosition = endPosition;
2092
- }
1797
+ const nextObject = this.beatmap.hitObjects.objects[objectIndex - 1];
1798
+ let objectEndPosition = object.stackedEndPosition;
1799
+ if (object.distance > 0 && nextObject) {
1800
+ const endPosition = object.stackedEndPosition;
1801
+ const nextStartPosition = nextObject.stackedPosition;
1802
+ const lazyEndMovement = nextStartPosition.subtract(endPosition);
1803
+ const actualEndMovement = nextStartPosition.subtract(objectEndPosition);
1804
+ if (lazyEndMovement.length < actualEndMovement.length) {
1805
+ objectEndPosition = endPosition;
1806
+ }
1807
+ }
1808
+ else {
1809
+ objectEndPosition = object.stackedPosition;
1810
+ }
1811
+ let nearestPosition = new osuBase.Vector2(Number.POSITIVE_INFINITY);
1812
+ let nearestCursorIndex = 0;
1813
+ let nearestGroupIndex = 0;
1814
+ let nearestCursorGroupIndex = 0;
1815
+ let nearestCursorTime = 0;
1816
+ for (let i = 0; i < this.data.cursorMovement.length; ++i) {
1817
+ const cursorData = this.data.cursorMovement[i];
1818
+ for (let j = 0; j < cursorData.occurrenceGroups.length; ++j) {
1819
+ const cursorGroup = cursorData.occurrenceGroups[j];
1820
+ if (cursorGroup.endTime < object.startTime) {
1821
+ continue;
1822
+ }
1823
+ if (cursorGroup.startTime > object.endTime) {
1824
+ break;
1825
+ }
1826
+ const cursors = cursorGroup.allOccurrences;
1827
+ for (let k = 0; k < cursors.length; ++k) {
1828
+ const cursor = cursors[k];
1829
+ let cursorPosition;
1830
+ switch (cursor.id) {
1831
+ case exports.MovementType.down:
1832
+ cursorPosition = this.getCursorPosition(cursor);
1833
+ break;
1834
+ case exports.MovementType.up: {
1835
+ const prevCursor = cursors[k - 1];
1836
+ cursorPosition = this.getCursorPosition(prevCursor);
1837
+ break;
1838
+ }
1839
+ case exports.MovementType.move: {
1840
+ const prevCursor = cursors[k - 1];
1841
+ const t = osuBase.MathUtils.clamp((object.endTime - prevCursor.time) /
1842
+ (cursor.time - prevCursor.time), 0, 1);
1843
+ cursorPosition = osuBase.Interpolation.lerp(this.getCursorPosition(prevCursor), this.getCursorPosition(cursor), t);
1844
+ break;
1845
+ }
1846
+ }
1847
+ if (cursorPosition.getDistance(objectEndPosition) <
1848
+ nearestPosition.getDistance(objectEndPosition)) {
1849
+ nearestPosition = cursorPosition;
1850
+ nearestCursorIndex = i;
1851
+ nearestGroupIndex = j;
1852
+ switch (cursor.id) {
1853
+ case exports.MovementType.down:
1854
+ nearestCursorGroupIndex = k;
1855
+ nearestCursorTime = cursor.time;
1856
+ break;
1857
+ case exports.MovementType.up:
1858
+ nearestCursorGroupIndex = k - 1;
1859
+ nearestCursorTime = cursors[k - 1].time;
1860
+ break;
1861
+ case exports.MovementType.move:
1862
+ nearestCursorGroupIndex = k;
1863
+ nearestCursorTime = object.endTime;
1864
+ break;
1865
+ }
1866
+ }
1867
+ }
1868
+ }
1869
+ }
1870
+ if (nearestPosition.getDistance(objectEndPosition) ===
1871
+ Number.POSITIVE_INFINITY) {
1872
+ return {
1873
+ position: nearestPosition,
1874
+ cursorIndex: Number.POSITIVE_INFINITY,
1875
+ groupIndex: Number.POSITIVE_INFINITY,
1876
+ occurrenceIndex: Number.POSITIVE_INFINITY,
1877
+ cursorTime: object.endTime,
1878
+ };
1879
+ }
1880
+ return {
1881
+ position: nearestPosition,
1882
+ cursorIndex: nearestCursorIndex,
1883
+ groupIndex: nearestGroupIndex,
1884
+ occurrenceIndex: nearestCursorGroupIndex,
1885
+ cursorTime: nearestCursorTime,
1886
+ };
1887
+ }
1888
+ /**
1889
+ * Checks whether a slider was cheesed.
1890
+ *
1891
+ * This is done by checking if a cursor follows a slider all the way to its end position.
1892
+ *
1893
+ * @param indexedHitObject The indexed slider.
1894
+ * @param hitData The hit data of the slider.
1895
+ * @returns Whether the slider was cheesed.
1896
+ */
1897
+ checkSliderCheesing(indexedHitObject, hitData) {
1898
+ if (!(indexedHitObject.object instanceof osuBase.Slider) ||
1899
+ hitData.result === osuBase.HitResult.miss ||
1900
+ indexedHitObject.cursorIndex === -1) {
1901
+ return false;
1902
+ }
1903
+ return false;
1904
+ }
1905
+ getCursorPosition(cursor) {
1906
+ if (this.isHardRock) {
1907
+ return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
1908
+ }
1909
+ return cursor.position;
1910
+ }
1911
+ }
1912
+
1913
+ /**
1914
+ * Represents a cursor's occurrence.
1915
+ */
1916
+ class CursorOccurrence {
1917
+ constructor(time, x, y, id) {
1918
+ this.time = time;
1919
+ this.position = new osuBase.Vector2(x, y);
1920
+ this.id = id;
1921
+ }
1922
+ /**
1923
+ * Returns a string representation of this `CursorOccurrence`.
1924
+ */
1925
+ toString() {
1926
+ let str = `${this.time.toString()}ms `;
1927
+ switch (this.id) {
1928
+ case exports.MovementType.down:
1929
+ str += "Down";
1930
+ break;
1931
+ case exports.MovementType.up:
1932
+ str += "Up";
1933
+ break;
1934
+ case exports.MovementType.move:
1935
+ str += "Move";
1936
+ break;
1937
+ }
1938
+ if (this.id !== exports.MovementType.up) {
1939
+ str += ` (${this.position.x.toFixed(2)}, ${this.position.y.toFixed(2)})`;
1940
+ }
1941
+ return str;
1942
+ }
1943
+ }
1944
+
1945
+ /**
1946
+ * Represents a group of cursor occurrences representing a cursor instance's
1947
+ * movement when a player places their finger on the screen.
1948
+ */
1949
+ class CursorOccurrenceGroup {
1950
+ /**
1951
+ * The cursor occurrence of movement type `movementType.DOWN`.
1952
+ */
1953
+ get down() {
1954
+ return this._down;
1955
+ }
1956
+ /**
1957
+ * The cursor occurrence of movement type `movementType.DOWN`.
1958
+ */
1959
+ set down(value) {
1960
+ if (value.id !== exports.MovementType.down) {
1961
+ throw new TypeError("Attempting to set the down cursor occurrence to one with different movement type.");
1962
+ }
1963
+ this._down = value;
1964
+ }
1965
+ /**
1966
+ * The cursor occurrences of movement type `movementType.MOVE`.
1967
+ */
1968
+ get moves() {
1969
+ return this._moves;
1970
+ }
1971
+ /**
1972
+ * The cursor occurrence of movement type `movementType.UP`.
1973
+ *
1974
+ * May not exist, such as when the player holds their cursor until the end of a beatmap.
1975
+ */
1976
+ get up() {
1977
+ return this._up;
1978
+ }
1979
+ /**
1980
+ * The cursor occurrence of movement type `movementType.UP`.
1981
+ *
1982
+ * May not exist, such as when the player holds their cursor until the end of a beatmap.
1983
+ */
1984
+ set up(value) {
1985
+ if (value && value.id !== exports.MovementType.up) {
1986
+ throw new TypeError("Attempting to set the up cursor occurrence to one with different movement type.");
1987
+ }
1988
+ this._up = value;
1989
+ }
1990
+ /**
1991
+ * The time at which this cursor occurrence group starts.
1992
+ */
1993
+ get startTime() {
1994
+ return this._down.time;
1995
+ }
1996
+ /**
1997
+ * The time at which this cursor occurrence group ends.
1998
+ */
1999
+ get endTime() {
2000
+ var _a, _b, _c, _d;
2001
+ return (_d = (_b = (_a = this._up) === null || _a === void 0 ? void 0 : _a.time) !== null && _b !== void 0 ? _b : (_c = this._moves.at(-1)) === null || _c === void 0 ? void 0 : _c.time) !== null && _d !== void 0 ? _d : this._down.time;
2002
+ }
2003
+ /**
2004
+ * The duration this cursor occurrence group is active for.
2005
+ */
2006
+ get duration() {
2007
+ return this.endTime - this.startTime;
2008
+ }
2009
+ /**
2010
+ * All cursor occurrences in this group.
2011
+ *
2012
+ * This iterates all occurrences and as such should be used sparingly or stored locally.
2013
+ */
2014
+ get allOccurrences() {
2015
+ const cursors = [this._down, ...this._moves];
2016
+ if (this._up) {
2017
+ cursors.push(this._up);
2018
+ }
2019
+ return cursors;
2020
+ }
2021
+ constructor(down, moves, up) {
2022
+ this._down = down;
2023
+ this._moves = moves;
2024
+ // Re-set down cursor occurrence for checking.
2025
+ this.down = down;
2026
+ this.up = up;
2027
+ }
2028
+ /**
2029
+ * Determines whether this cursor occurrence group is active at the specified time.
2030
+ *
2031
+ * @param time The time.
2032
+ * @returns Whether this cursor occurrence group is active at the specified time.
2033
+ */
2034
+ isActiveAt(time) {
2035
+ return time >= this.startTime && time <= this.endTime;
2036
+ }
2037
+ /**
2038
+ * Finds the cursor occurrence that is active at a given time.
2039
+ *
2040
+ * @param time The time.
2041
+ * @returns The cursor occurrence at the given time, `null` if not found.
2042
+ */
2043
+ cursorAt(time) {
2044
+ var _a;
2045
+ if (!this.isActiveAt(time)) {
2046
+ return null;
2047
+ }
2048
+ if (this._down.time === time) {
2049
+ return this._down;
2093
2050
  }
2094
- else {
2095
- objectEndPosition = object.stackedPosition;
2051
+ if (((_a = this._up) === null || _a === void 0 ? void 0 : _a.time) === time) {
2052
+ return this._up;
2096
2053
  }
2097
- let nearestPosition = new osuBase.Vector2(Number.POSITIVE_INFINITY);
2098
- let nearestCursorIndex = 0;
2099
- let nearestGroupIndex = 0;
2100
- let nearestCursorGroupIndex = 0;
2101
- let nearestCursorTime = 0;
2102
- for (let i = 0; i < this.data.cursorMovement.length; ++i) {
2103
- const cursorData = this.data.cursorMovement[i];
2104
- for (let j = 0; j < cursorData.occurrenceGroups.length; ++j) {
2105
- const cursorGroup = cursorData.occurrenceGroups[j];
2106
- if (cursorGroup.endTime < object.startTime) {
2107
- continue;
2108
- }
2109
- if (cursorGroup.startTime > object.endTime) {
2110
- break;
2111
- }
2112
- const cursors = cursorGroup.allOccurrences;
2113
- for (let k = 0; k < cursors.length; ++k) {
2114
- const cursor = cursors[k];
2115
- let cursorPosition;
2116
- switch (cursor.id) {
2117
- case exports.MovementType.down:
2118
- cursorPosition = this.getCursorPosition(cursor);
2119
- break;
2120
- case exports.MovementType.up: {
2121
- const prevCursor = cursors[k - 1];
2122
- cursorPosition = this.getCursorPosition(prevCursor);
2123
- break;
2124
- }
2125
- case exports.MovementType.move: {
2126
- const prevCursor = cursors[k - 1];
2127
- const t = osuBase.MathUtils.clamp((object.endTime - prevCursor.time) /
2128
- (cursor.time - prevCursor.time), 0, 1);
2129
- cursorPosition = osuBase.Interpolation.lerp(this.getCursorPosition(prevCursor), this.getCursorPosition(cursor), t);
2130
- break;
2131
- }
2132
- }
2133
- if (cursorPosition.getDistance(objectEndPosition) <
2134
- nearestPosition.getDistance(objectEndPosition)) {
2135
- nearestPosition = cursorPosition;
2136
- nearestCursorIndex = i;
2137
- nearestGroupIndex = j;
2138
- switch (cursor.id) {
2139
- case exports.MovementType.down:
2140
- nearestCursorGroupIndex = k;
2141
- nearestCursorTime = cursor.time;
2142
- break;
2143
- case exports.MovementType.up:
2144
- nearestCursorGroupIndex = k - 1;
2145
- nearestCursorTime = cursors[k - 1].time;
2146
- break;
2147
- case exports.MovementType.move:
2148
- nearestCursorGroupIndex = k;
2149
- nearestCursorTime = object.endTime;
2150
- break;
2151
- }
2152
- }
2153
- }
2054
+ let l = 0;
2055
+ let r = this._moves.length - 2;
2056
+ while (l <= r) {
2057
+ const pivot = l + ((r - l) >> 1);
2058
+ if (this._moves[pivot].time < time) {
2059
+ l = pivot + 1;
2060
+ }
2061
+ else if (this._moves[pivot].time > time) {
2062
+ r = pivot - 1;
2063
+ }
2064
+ else {
2065
+ return this._moves[pivot];
2154
2066
  }
2155
2067
  }
2156
- if (nearestPosition.getDistance(objectEndPosition) ===
2157
- Number.POSITIVE_INFINITY) {
2158
- return {
2159
- position: nearestPosition,
2160
- cursorIndex: Number.POSITIVE_INFINITY,
2161
- groupIndex: Number.POSITIVE_INFINITY,
2162
- occurrenceIndex: Number.POSITIVE_INFINITY,
2163
- cursorTime: object.endTime,
2164
- };
2165
- }
2166
- return {
2167
- position: nearestPosition,
2168
- cursorIndex: nearestCursorIndex,
2169
- groupIndex: nearestGroupIndex,
2170
- occurrenceIndex: nearestCursorGroupIndex,
2171
- cursorTime: nearestCursorTime,
2172
- };
2068
+ // l will be the first cursor occurrence with time > this._moves[l].time, but we want the one before it
2069
+ return this._moves[l - 1];
2173
2070
  }
2174
2071
  /**
2175
- * Checks whether a slider was cheesed.
2072
+ * Returns a string representation of this `CursorOccurrenceGroup`.
2073
+ */
2074
+ toString() {
2075
+ return `Down: ${this.down.time.toString()}ms (${this.down.position.x.toFixed(2)}, ${this.down.position.y.toFixed(2)}) | Moves: ${this.moves.length.toString()} | Up: ${this.up ? `${this.up.time.toString()}ms` : "N/A"}`;
2076
+ }
2077
+ }
2078
+
2079
+ /**
2080
+ * Represents a cursor instance in an osu!droid replay.
2081
+ *
2082
+ * Stores cursor movement data in the form of `CursorOccurrenceGroup`s.
2083
+ *
2084
+ * This is used when analyzing replays using replay analyzer.
2085
+ */
2086
+ class CursorData {
2087
+ /**
2088
+ * The time at which the first occurrence of this cursor instance occurs.
2176
2089
  *
2177
- * This is done by checking if a cursor follows a slider all the way to its end position.
2090
+ * Will return `null` if there are no occurrences.
2091
+ */
2092
+ get earliestOccurrenceTime() {
2093
+ var _a, _b;
2094
+ return (_b = (_a = this.occurrenceGroups.at(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : null;
2095
+ }
2096
+ /**
2097
+ * The time at which the latest occurrence of this cursor instance occurs.
2178
2098
  *
2179
- * @param indexedHitObject The indexed slider.
2180
- * @param hitData The hit data of the slider.
2181
- * @returns Whether the slider was cheesed.
2099
+ * Will return `null` if there are no occurrences.
2182
2100
  */
2183
- checkSliderCheesing(indexedHitObject, hitData) {
2184
- if (!(indexedHitObject.object instanceof osuBase.Slider) ||
2185
- hitData.result === exports.HitResult.miss ||
2186
- indexedHitObject.cursorIndex === -1) {
2187
- return false;
2188
- }
2189
- return false;
2101
+ get latestOccurrenceTime() {
2102
+ var _a, _b;
2103
+ return (_b = (_a = this.occurrenceGroups.at(-1)) === null || _a === void 0 ? void 0 : _a.endTime) !== null && _b !== void 0 ? _b : null;
2190
2104
  }
2191
- getCursorPosition(cursor) {
2192
- if (this.isHardRock) {
2193
- return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
2105
+ /**
2106
+ * The amount of cursor occurrences of this cursor instance.
2107
+ */
2108
+ get totalOccurrences() {
2109
+ return this.occurrenceGroups.reduce((a, v) => {
2110
+ // Down cursor.
2111
+ ++a;
2112
+ // Move cursors.
2113
+ a += v.moves.length;
2114
+ if (v.up) {
2115
+ // Up cursor.
2116
+ ++a;
2117
+ }
2118
+ return a;
2119
+ }, 0);
2120
+ }
2121
+ /**
2122
+ * All cursor occurrences of this cursor instnace.
2123
+ *
2124
+ * This iterates all occurrence groups and as such should be used sparingly or stored locally.
2125
+ */
2126
+ get allOccurrences() {
2127
+ return this.occurrenceGroups.flatMap((v) => v.allOccurrences);
2128
+ }
2129
+ constructor(values) {
2130
+ /**
2131
+ * The occurrence groups of this cursor instance.
2132
+ */
2133
+ this.occurrenceGroups = [];
2134
+ let downOccurrence = null;
2135
+ let moveOccurrences = [];
2136
+ for (let i = 0; i < values.size; ++i) {
2137
+ const occurrence = new CursorOccurrence(values.time[i], values.x[i], values.y[i], values.id[i]);
2138
+ switch (occurrence.id) {
2139
+ case exports.MovementType.down:
2140
+ downOccurrence = occurrence;
2141
+ break;
2142
+ case exports.MovementType.move:
2143
+ moveOccurrences.push(occurrence);
2144
+ break;
2145
+ case exports.MovementType.up:
2146
+ if (downOccurrence) {
2147
+ this.occurrenceGroups.push(new CursorOccurrenceGroup(downOccurrence, moveOccurrences, occurrence));
2148
+ downOccurrence = null;
2149
+ }
2150
+ moveOccurrences = [];
2151
+ }
2152
+ }
2153
+ // Add the final cursor occurrence group as the loop may not catch it for special cases.
2154
+ if (downOccurrence && moveOccurrences.length > 0) {
2155
+ this.occurrenceGroups.push(new CursorOccurrenceGroup(downOccurrence, moveOccurrences));
2194
2156
  }
2195
- return cursor.position;
2196
2157
  }
2197
2158
  }
2198
2159
 
@@ -2225,13 +2186,24 @@ class ReplayData {
2225
2186
  }
2226
2187
 
2227
2188
  /**
2228
- * Represents a replay data for replay version 3.
2189
+ * Represents a replay data for replay version 3 and later.
2229
2190
  *
2230
2191
  * Stores generic information about an osu!droid replay.
2231
2192
  *
2232
2193
  * This is used when analyzing replays using replay analyzer.
2233
2194
  */
2234
2195
  class ReplayV3Data extends ReplayData {
2196
+ /**
2197
+ * The total score achieved in the play, after applying score multiplier from mods.
2198
+ */
2199
+ get totalScore() {
2200
+ var _a;
2201
+ if (this.replayVersion < 8) {
2202
+ return this.score;
2203
+ }
2204
+ (_a = this.scoreMultiplier) !== null && _a !== void 0 ? _a : (this.scoreMultiplier = osuBase.ModUtil.calculateScoreMultiplier(this.convertedMods.values(), osuBase.Modes.droid));
2205
+ return Math.round(Math.fround(this.score * this.scoreMultiplier));
2206
+ }
2235
2207
  constructor(values) {
2236
2208
  super(values);
2237
2209
  this.time = values.time;
@@ -2243,6 +2215,38 @@ class ReplayV3Data extends ReplayData {
2243
2215
  }
2244
2216
  }
2245
2217
 
2218
+ /******************************************************************************
2219
+ Copyright (c) Microsoft Corporation.
2220
+
2221
+ Permission to use, copy, modify, and/or distribute this software for any
2222
+ purpose with or without fee is hereby granted.
2223
+
2224
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
2225
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
2226
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
2227
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
2228
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
2229
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
2230
+ PERFORMANCE OF THIS SOFTWARE.
2231
+ ***************************************************************************** */
2232
+ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
2233
+
2234
+
2235
+ function __awaiter(thisArg, _arguments, P, generator) {
2236
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
2237
+ return new (P || (P = Promise))(function (resolve, reject) {
2238
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
2239
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
2240
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
2241
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
2242
+ });
2243
+ }
2244
+
2245
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
2246
+ var e = new Error(message);
2247
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
2248
+ };
2249
+
2246
2250
  /**
2247
2251
  * A replay analyzer that analyzes a replay from osu!droid.
2248
2252
  *
@@ -2351,15 +2355,20 @@ class ReplayAnalyzer {
2351
2355
  for (let i = 0; i < hitObjectData.length; ++i) {
2352
2356
  const v = hitObjectData[i];
2353
2357
  const o = objects[i];
2354
- if (o instanceof osuBase.Spinner || v.result === exports.HitResult.miss) {
2358
+ if (o instanceof osuBase.Spinner || v.result === osuBase.HitResult.miss) {
2355
2359
  continue;
2356
2360
  }
2357
2361
  const { accuracy } = v;
2358
- if (o instanceof osuBase.Slider &&
2359
- // Do not include slider breaks.
2360
- (-mehWindow > accuracy ||
2361
- accuracy > Math.min(mehWindow, o.duration))) {
2362
- continue;
2362
+ // Do not include slider breaks.
2363
+ if (o instanceof osuBase.Slider) {
2364
+ let lateHitThreshold = mehWindow;
2365
+ // Before replay version 8, the slider head's hit window is capped to the duration of the slider.
2366
+ if (this.data.replayVersion < 8) {
2367
+ lateHitThreshold = Math.min(mehWindow, o.duration);
2368
+ }
2369
+ if (-mehWindow > accuracy || accuracy > lateHitThreshold) {
2370
+ continue;
2371
+ }
2363
2372
  }
2364
2373
  accuracies.push(accuracy);
2365
2374
  if (accuracy >= 0) {
@@ -2396,7 +2405,7 @@ class ReplayAnalyzer {
2396
2405
  for (let i = 0; i < data.hitObjectData.length; ++i) {
2397
2406
  const object = beatmap.hitObjects.objects[i];
2398
2407
  const objectData = data.hitObjectData[i];
2399
- if (objectData.result === exports.HitResult.miss ||
2408
+ if (objectData.result === osuBase.HitResult.miss ||
2400
2409
  !(object instanceof osuBase.Slider)) {
2401
2410
  continue;
2402
2411
  }
@@ -2439,16 +2448,16 @@ class ReplayAnalyzer {
2439
2448
  let { result } = objectData;
2440
2449
  if (object instanceof osuBase.Circle) {
2441
2450
  if (hitAccuracy <= hitWindow.greatWindow) {
2442
- result = exports.HitResult.great;
2451
+ result = osuBase.HitResult.great;
2443
2452
  }
2444
2453
  else if (hitAccuracy <= hitWindow.okWindow) {
2445
- result = exports.HitResult.good;
2454
+ result = osuBase.HitResult.good;
2446
2455
  }
2447
2456
  else if (hitAccuracy <= hitWindow.mehWindow) {
2448
- result = exports.HitResult.meh;
2457
+ result = osuBase.HitResult.meh;
2449
2458
  }
2450
2459
  else {
2451
- result = exports.HitResult.miss;
2460
+ result = osuBase.HitResult.miss;
2452
2461
  }
2453
2462
  }
2454
2463
  else if (object instanceof osuBase.Slider) {
@@ -2461,34 +2470,34 @@ class ReplayAnalyzer {
2461
2470
  }
2462
2471
  }
2463
2472
  if (ticksObtained === object.nestedHitObjects.length) {
2464
- result = exports.HitResult.great;
2473
+ result = osuBase.HitResult.great;
2465
2474
  }
2466
2475
  else if (ticksObtained >=
2467
2476
  Math.trunc(object.nestedHitObjects.length / 2)) {
2468
- result = exports.HitResult.good;
2477
+ result = osuBase.HitResult.good;
2469
2478
  }
2470
2479
  else if (ticksObtained > 0) {
2471
- result = exports.HitResult.meh;
2480
+ result = osuBase.HitResult.meh;
2472
2481
  }
2473
2482
  else {
2474
- result = exports.HitResult.miss;
2483
+ result = osuBase.HitResult.miss;
2475
2484
  }
2476
2485
  }
2477
2486
  else {
2478
- result = exports.HitResult.miss;
2487
+ result = osuBase.HitResult.miss;
2479
2488
  }
2480
2489
  }
2481
2490
  switch (result) {
2482
- case exports.HitResult.miss:
2491
+ case osuBase.HitResult.miss:
2483
2492
  ++accuracy.nmiss;
2484
2493
  break;
2485
- case exports.HitResult.meh:
2494
+ case osuBase.HitResult.meh:
2486
2495
  ++accuracy.n50;
2487
2496
  break;
2488
- case exports.HitResult.good:
2497
+ case osuBase.HitResult.good:
2489
2498
  ++accuracy.n100;
2490
2499
  break;
2491
- case exports.HitResult.great:
2500
+ case osuBase.HitResult.great:
2492
2501
  ++accuracy.n300;
2493
2502
  break;
2494
2503
  }
@@ -2666,6 +2675,7 @@ class ReplayAnalyzer {
2666
2675
  break;
2667
2676
  case 3:
2668
2677
  case 7:
2678
+ case 8:
2669
2679
  bufferIndex = 7;
2670
2680
  break;
2671
2681
  case 4:
@@ -2782,7 +2792,7 @@ class ReplayAnalyzer {
2782
2792
  const replayObjectData = {
2783
2793
  accuracy: 0,
2784
2794
  tickset: [],
2785
- result: exports.HitResult.miss,
2795
+ result: osuBase.HitResult.miss,
2786
2796
  };
2787
2797
  replayObjectData.accuracy = this.readShort(replayDataBuffer);
2788
2798
  const len = this.readByte(replayDataBuffer);
@@ -2823,21 +2833,21 @@ class ReplayAnalyzer {
2823
2833
  : true
2824
2834
  : false;
2825
2835
  switch (hitObjectData.result) {
2826
- case exports.HitResult.miss:
2836
+ case osuBase.HitResult.miss:
2827
2837
  ++resultObject.accuracy.nmiss;
2828
2838
  grantsGekiOrKatu = false;
2829
2839
  break;
2830
- case exports.HitResult.meh:
2840
+ case osuBase.HitResult.meh:
2831
2841
  ++resultObject.accuracy.n50;
2832
2842
  grantsGekiOrKatu = false;
2833
2843
  break;
2834
- case exports.HitResult.good:
2844
+ case osuBase.HitResult.good:
2835
2845
  ++resultObject.accuracy.n100;
2836
2846
  if (grantsGekiOrKatu && isNextNewCombo) {
2837
2847
  ++resultObject.hit100k;
2838
2848
  }
2839
2849
  break;
2840
- case exports.HitResult.great:
2850
+ case osuBase.HitResult.great:
2841
2851
  ++resultObject.accuracy.n300;
2842
2852
  if (grantsGekiOrKatu && isNextNewCombo) {
2843
2853
  ++resultObject.hit300k;
@@ -2912,21 +2922,6 @@ class ReplayAnalyzer {
2912
2922
  }
2913
2923
  }
2914
2924
 
2915
- /**
2916
- * Represents a hitobject in an osu!droid replay.
2917
- *
2918
- * Stores information about hitobjects in an osu!droid replay such as hit offset, tickset, and hit result.
2919
- *
2920
- * This is used when analyzing replays using replay analyzer.
2921
- */
2922
- class ReplayObjectData {
2923
- constructor(values) {
2924
- this.accuracy = values.accuracy;
2925
- this.tickset = values.tickset;
2926
- this.result = values.result;
2927
- }
2928
- }
2929
-
2930
2925
  exports.CursorData = CursorData;
2931
2926
  exports.CursorOccurrence = CursorOccurrence;
2932
2927
  exports.CursorOccurrenceGroup = CursorOccurrenceGroup;
@@ -2934,7 +2929,6 @@ exports.RebalanceSliderCheeseChecker = RebalanceSliderCheeseChecker;
2934
2929
  exports.RebalanceThreeFingerChecker = RebalanceThreeFingerChecker;
2935
2930
  exports.ReplayAnalyzer = ReplayAnalyzer;
2936
2931
  exports.ReplayData = ReplayData;
2937
- exports.ReplayObjectData = ReplayObjectData;
2938
2932
  exports.ReplayV3Data = ReplayV3Data;
2939
2933
  exports.SliderCheeseChecker = SliderCheeseChecker;
2940
2934
  exports.ThreeFingerChecker = ThreeFingerChecker;