@rian8337/osu-droid-replay-analyzer 3.0.0-beta.1 → 3.0.0-beta.11

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.
package/dist/index.js CHANGED
@@ -29,6 +29,27 @@ function _interopNamespace(e) {
29
29
 
30
30
  var javaDeserialization__namespace = /*#__PURE__*/_interopNamespace(javaDeserialization);
31
31
 
32
+ /**
33
+ * Movement types of a cursor in an osu!droid replay.
34
+ *
35
+ * The cursor movement is represented as a player's action on the screen.
36
+ */
37
+ exports.MovementType = void 0;
38
+ (function (MovementType) {
39
+ /**
40
+ * The player places their finger on the screen.
41
+ */
42
+ MovementType[MovementType["down"] = 0] = "down";
43
+ /**
44
+ * The player drags their finger on the screen.
45
+ */
46
+ MovementType[MovementType["move"] = 1] = "move";
47
+ /**
48
+ * The player releases their finger from the screen.
49
+ */
50
+ MovementType[MovementType["up"] = 2] = "up";
51
+ })(exports.MovementType || (exports.MovementType = {}));
52
+
32
53
  /**
33
54
  * Represents a cursor's occurrence.
34
55
  */
@@ -52,21 +73,218 @@ class CursorOccurrence {
52
73
  }
53
74
  }
54
75
 
76
+ /**
77
+ * Represents a group of cursor occurrences representing a cursor instance's
78
+ * movement when a player places their finger on the screen.
79
+ */
80
+ class CursorOccurrenceGroup {
81
+ /**
82
+ * The cursor occurrence of movement type `movementType.DOWN`.
83
+ */
84
+ get down() {
85
+ return this._down;
86
+ }
87
+ /**
88
+ * The cursor occurrence of movement type `movementType.DOWN`.
89
+ */
90
+ set down(value) {
91
+ if (value.id !== exports.MovementType.down) {
92
+ throw new TypeError("Attempting to set the down cursor occurrence to one with different movement type.");
93
+ }
94
+ this._down = value;
95
+ }
96
+ /**
97
+ * The cursor occurrences of movement type `movementType.MOVE`.
98
+ */
99
+ get moves() {
100
+ return this._moves;
101
+ }
102
+ /**
103
+ * The cursor occurrence of movement type `movementType.UP`.
104
+ *
105
+ * May not exist, such as when the player holds their cursor until the end of a beatmap.
106
+ */
107
+ get up() {
108
+ return this._up;
109
+ }
110
+ /**
111
+ * The cursor occurrence of movement type `movementType.UP`.
112
+ *
113
+ * May not exist, such as when the player holds their cursor until the end of a beatmap.
114
+ */
115
+ set up(value) {
116
+ if (value && value.id !== exports.MovementType.up) {
117
+ throw new TypeError("Attempting to set the up cursor occurrence to one with different movement type.");
118
+ }
119
+ this._up = value;
120
+ }
121
+ /**
122
+ * The time at which this cursor occurrence group starts.
123
+ */
124
+ get startTime() {
125
+ return this._down.time;
126
+ }
127
+ /**
128
+ * The time at which this cursor occurrence group ends.
129
+ */
130
+ get endTime() {
131
+ if (this._up) {
132
+ return this._up.time;
133
+ }
134
+ return this._moves.at(-1)?.time ?? this._down.time;
135
+ }
136
+ /**
137
+ * All cursor occurrences in this group.
138
+ *
139
+ * This iterates all occurrences and as such should be used sparingly or stored locally.
140
+ */
141
+ get allOccurrences() {
142
+ const cursors = [this._down, ...this._moves];
143
+ if (this._up) {
144
+ cursors.push(this._up);
145
+ }
146
+ return cursors;
147
+ }
148
+ /**
149
+ * The cursor occurrence of movement type `movementType.DOWN`.
150
+ */
151
+ _down;
152
+ /**
153
+ * The cursor occurrences of movement type `movementType.MOVE`.
154
+ */
155
+ _moves;
156
+ /**
157
+ * The cursor occurrence of movement type `movementType.UP`.
158
+ *
159
+ * May not exist, such as when the player holds their cursor until the end of a beatmap.
160
+ */
161
+ _up;
162
+ constructor(down, moves, up) {
163
+ this._down = down;
164
+ this._moves = moves;
165
+ // Re-set down cursor occurrence for checking.
166
+ this.down = down;
167
+ this.up = up;
168
+ }
169
+ /**
170
+ * Determines whether this cursor occurrence group is active at the specified time.
171
+ *
172
+ * @param time The time.
173
+ * @returns Whether this cursor occurrence group is active at the specified time.
174
+ */
175
+ isActiveAt(time) {
176
+ return time >= this.startTime && time <= this.endTime;
177
+ }
178
+ /**
179
+ * Finds the cursor occurrence that is active at a given time.
180
+ *
181
+ * @param time The time.
182
+ * @returns The cursor occurrence at the given time, `null` if not found.
183
+ */
184
+ cursorAt(time) {
185
+ if (!this.isActiveAt(time)) {
186
+ return null;
187
+ }
188
+ if (this._down.time === time) {
189
+ return this._down;
190
+ }
191
+ if (this._up?.time === time) {
192
+ return this._up;
193
+ }
194
+ let l = 0;
195
+ let r = this._moves.length - 2;
196
+ while (l <= r) {
197
+ const pivot = l + ((r - l) >> 1);
198
+ if (this._moves[pivot].time < time) {
199
+ l = pivot + 1;
200
+ }
201
+ else if (this._moves[pivot].time > time) {
202
+ r = pivot - 1;
203
+ }
204
+ else {
205
+ return this._moves[pivot];
206
+ }
207
+ }
208
+ // l will be the first cursor occurrence with time > this._moves[l].time, but we want the one before it
209
+ return this._moves[l - 1];
210
+ }
211
+ }
212
+
55
213
  /**
56
214
  * Represents a cursor instance in an osu!droid replay.
57
215
  *
58
- * Stores cursor movement data in the form of `CursorOccurrence`s.
216
+ * Stores cursor movement data in the form of `CursorOccurrenceGroup`s.
59
217
  *
60
218
  * This is used when analyzing replays using replay analyzer.
61
219
  */
62
220
  class CursorData {
63
221
  /**
64
- * The occurrences of this cursor instance.
222
+ * The occurrence groups of this cursor instance.
223
+ */
224
+ occurrenceGroups = [];
225
+ /**
226
+ * The time at which the first occurrence of this cursor instance occurs.
227
+ *
228
+ * Will return `null` if there are no occurrences.
65
229
  */
66
- occurrences = [];
230
+ get earliestOccurrenceTime() {
231
+ return this.occurrenceGroups.at(0)?.startTime ?? null;
232
+ }
233
+ /**
234
+ * The time at which the latest occurrence of this cursor instance occurs.
235
+ *
236
+ * Will return `null` if there are no occurrences.
237
+ */
238
+ get latestOccurrenceTime() {
239
+ return this.occurrenceGroups.at(-1)?.endTime ?? null;
240
+ }
241
+ /**
242
+ * The amount of cursor occurrences of this cursor instance.
243
+ */
244
+ get totalOccurrences() {
245
+ return this.occurrenceGroups.reduce((a, v) => {
246
+ // Down cursor.
247
+ ++a;
248
+ // Move cursors.
249
+ a += v.moves.length;
250
+ if (v.up) {
251
+ // Up cursor.
252
+ ++a;
253
+ }
254
+ return a;
255
+ }, 0);
256
+ }
257
+ /**
258
+ * All cursor occurrences of this cursor instnace.
259
+ *
260
+ * This iterates all occurrence groups and as such should be used sparingly or stored locally.
261
+ */
262
+ get allOccurrences() {
263
+ return this.occurrenceGroups.flatMap((v) => v.allOccurrences);
264
+ }
67
265
  constructor(values) {
266
+ let downOccurrence = null;
267
+ let moveOccurrences = [];
68
268
  for (let i = 0; i < values.size; ++i) {
69
- this.occurrences.push(new CursorOccurrence(values.time[i], values.x[i], values.y[i], values.id[i]));
269
+ const occurrence = new CursorOccurrence(values.time[i], values.x[i], values.y[i], values.id[i]);
270
+ switch (occurrence.id) {
271
+ case exports.MovementType.down:
272
+ downOccurrence = occurrence;
273
+ break;
274
+ case exports.MovementType.move:
275
+ moveOccurrences.push(occurrence);
276
+ break;
277
+ case exports.MovementType.up:
278
+ if (downOccurrence) {
279
+ this.occurrenceGroups.push(new CursorOccurrenceGroup(downOccurrence, moveOccurrences, occurrence));
280
+ downOccurrence = null;
281
+ }
282
+ moveOccurrences = [];
283
+ }
284
+ }
285
+ // Add the final cursor occurrence group as the loop may not catch it for special cases.
286
+ if (downOccurrence && moveOccurrences.length > 0) {
287
+ this.occurrenceGroups.push(new CursorOccurrenceGroup(downOccurrence, moveOccurrences));
70
288
  }
71
289
  }
72
290
  }
@@ -74,35 +292,25 @@ class CursorData {
74
292
  /**
75
293
  * The result of a hit in an osu!droid replay.
76
294
  */
77
- exports.hitResult = void 0;
78
- (function (hitResult) {
295
+ exports.HitResult = void 0;
296
+ (function (HitResult) {
79
297
  /**
80
298
  * Miss (0).
81
299
  */
82
- hitResult[hitResult["RESULT_0"] = 1] = "RESULT_0";
300
+ HitResult[HitResult["miss"] = 1] = "miss";
83
301
  /**
84
302
  * Meh (50).
85
303
  */
86
- hitResult[hitResult["RESULT_50"] = 2] = "RESULT_50";
304
+ HitResult[HitResult["meh"] = 2] = "meh";
87
305
  /**
88
- * Great (100).
306
+ * Good (100).
89
307
  */
90
- hitResult[hitResult["RESULT_100"] = 3] = "RESULT_100";
308
+ HitResult[HitResult["good"] = 3] = "good";
91
309
  /**
92
- * Good (300).
310
+ * Great (300).
93
311
  */
94
- hitResult[hitResult["RESULT_300"] = 4] = "RESULT_300";
95
- })(exports.hitResult || (exports.hitResult = {}));
96
-
97
- /**
98
- * Movement type of a cursor in an osu!droid replay.
99
- */
100
- exports.movementType = void 0;
101
- (function (movementType) {
102
- movementType[movementType["DOWN"] = 0] = "DOWN";
103
- movementType[movementType["MOVE"] = 1] = "MOVE";
104
- movementType[movementType["UP"] = 2] = "UP";
105
- })(exports.movementType || (exports.movementType = {}));
312
+ HitResult[HitResult["great"] = 4] = "great";
313
+ })(exports.HitResult || (exports.HitResult = {}));
106
314
 
107
315
  /**
108
316
  * Represents a replay data in an osu!droid replay.
@@ -327,9 +535,17 @@ class ThreeFingerChecker {
327
535
  */
328
536
  breakPointAccurateTimes = [];
329
537
  /**
330
- * A cursor data array that only contains `movementType.DOWN` movement ID occurrences.
538
+ * A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
539
+ *
540
+ * Each index represents the cursor index.
331
541
  */
332
542
  downCursorInstances = [];
543
+ /**
544
+ * All cursor occurrences in the replay.
545
+ *
546
+ * Each index represents the cursor index.
547
+ */
548
+ allCursorInstances;
333
549
  /**
334
550
  * Nerf factors from all sections that were three-fingered.
335
551
  */
@@ -341,10 +557,12 @@ class ThreeFingerChecker {
341
557
  constructor(calculator, data) {
342
558
  this.calculator = calculator;
343
559
  this.data = data;
560
+ this.allCursorInstances = data.cursorMovement.map((v) => v.allOccurrences);
344
561
  const stats = new osuBase.MapStats({
345
562
  od: this.calculator.beatmap.difficulty.od,
346
563
  mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
347
- !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
564
+ !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym) &&
565
+ !(m instanceof osuBase.ModPrecise)),
348
566
  }).calculate();
349
567
  this.hitWindow = new osuBase.DroidHitWindow(stats.od);
350
568
  const strainNotes = calculator.objects.filter(
@@ -374,16 +592,14 @@ class ThreeFingerChecker {
374
592
  }
375
593
  this.getAccurateBreakPoints();
376
594
  this.filterCursorInstances();
377
- if (this.downCursorInstances.filter((v) => v.occurrences.length > 0)
378
- .length <= 3) {
595
+ if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
379
596
  return { is3Finger: false, penalty: 1 };
380
597
  }
381
598
  this.getBeatmapSections();
382
599
  this.detectDragPlay();
383
600
  this.getDetailedBeatmapSections();
384
601
  this.preventAccidentalTaps();
385
- if (this.downCursorInstances.filter((v) => v.occurrences.length > 0)
386
- .length <= 3) {
602
+ if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
387
603
  return { is3Finger: false, penalty: 1 };
388
604
  }
389
605
  this.calculateNerfFactors();
@@ -406,11 +622,11 @@ class ThreeFingerChecker {
406
622
  // For sliders and spinners, automatically set hit window length to be as lenient as possible.
407
623
  let beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
408
624
  switch (objectData[beforeIndex].result) {
409
- case exports.hitResult.RESULT_300:
625
+ case exports.HitResult.great:
410
626
  beforeIndexHitWindowLength =
411
627
  this.hitWindow.hitWindowFor300(isPrecise);
412
628
  break;
413
- case exports.hitResult.RESULT_100:
629
+ case exports.HitResult.good:
414
630
  beforeIndexHitWindowLength =
415
631
  this.hitWindow.hitWindowFor100(isPrecise);
416
632
  break;
@@ -424,11 +640,11 @@ class ThreeFingerChecker {
424
640
  // For sliders and spinners, automatically set hit window length to be as lenient as possible.
425
641
  let afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
426
642
  switch (objectData[afterIndex].result) {
427
- case exports.hitResult.RESULT_300:
643
+ case exports.HitResult.great:
428
644
  afterIndexHitWindowLength =
429
645
  this.hitWindow.hitWindowFor300(isPrecise);
430
646
  break;
431
- case exports.hitResult.RESULT_100:
647
+ case exports.HitResult.good:
432
648
  afterIndexHitWindowLength =
433
649
  this.hitWindow.hitWindowFor100(isPrecise);
434
650
  break;
@@ -458,11 +674,11 @@ class ThreeFingerChecker {
458
674
  let firstObjectHitWindow = this.hitWindow.hitWindowFor50(isPrecise);
459
675
  if (objects[0].object instanceof osuBase.Circle) {
460
676
  switch (firstObjectResult) {
461
- case exports.hitResult.RESULT_300:
677
+ case exports.HitResult.great:
462
678
  firstObjectHitWindow =
463
679
  this.hitWindow.hitWindowFor300(isPrecise);
464
680
  break;
465
- case exports.hitResult.RESULT_100:
681
+ case exports.HitResult.good:
466
682
  firstObjectHitWindow =
467
683
  this.hitWindow.hitWindowFor100(isPrecise);
468
684
  break;
@@ -475,11 +691,11 @@ class ThreeFingerChecker {
475
691
  let lastObjectHitWindow = this.hitWindow.hitWindowFor50(isPrecise);
476
692
  if (objects.at(-1).object instanceof osuBase.Circle) {
477
693
  switch (lastObjectResult) {
478
- case exports.hitResult.RESULT_300:
694
+ case exports.HitResult.great:
479
695
  lastObjectHitWindow =
480
696
  this.hitWindow.hitWindowFor300(isPrecise);
481
697
  break;
482
- case exports.hitResult.RESULT_100:
698
+ case exports.HitResult.good:
483
699
  lastObjectHitWindow =
484
700
  this.hitWindow.hitWindowFor100(isPrecise);
485
701
  break;
@@ -495,27 +711,20 @@ class ThreeFingerChecker {
495
711
  const lastObjectHitTime = objects.at(-1).object.startTime + lastObjectHitWindow;
496
712
  for (let i = 0; i < this.data.cursorMovement.length; ++i) {
497
713
  const cursorInstance = this.data.cursorMovement[i];
498
- const newCursorData = new CursorData({
499
- size: 0,
500
- time: [],
501
- x: [],
502
- y: [],
503
- id: [],
504
- });
505
- for (let j = 0; j < cursorInstance.occurrences.length; ++j) {
506
- if (cursorInstance.occurrences[j].id !== exports.movementType.DOWN) {
714
+ const validOccurrences = [];
715
+ for (let j = 0; j < cursorInstance.occurrenceGroups.length; ++j) {
716
+ const group = cursorInstance.occurrenceGroups[j];
717
+ if (group.startTime < firstObjectHitTime ||
718
+ group.endTime > lastObjectHitTime) {
507
719
  continue;
508
720
  }
509
- const time = cursorInstance.occurrences[j].time;
510
- if (time < firstObjectHitTime || time > lastObjectHitTime) {
721
+ if (this.breakPointAccurateTimes.some((v) => group.startTime >= v.startTime &&
722
+ group.endTime <= v.endTime)) {
511
723
  continue;
512
724
  }
513
- if (this.breakPointAccurateTimes.some((v) => time >= v.startTime && time <= v.endTime)) {
514
- continue;
515
- }
516
- newCursorData.occurrences.push(new CursorOccurrence(time, cursorInstance.occurrences[j].position.x, cursorInstance.occurrences[j].position.y, cursorInstance.occurrences[j].id));
725
+ validOccurrences.push(group.down);
517
726
  }
518
- this.downCursorInstances.push(newCursorData);
727
+ this.downCursorInstances.push(validOccurrences);
519
728
  }
520
729
  }
521
730
  /**
@@ -559,11 +768,11 @@ class ThreeFingerChecker {
559
768
  let firstObjectMinHitTime = firstObject.object.startTime;
560
769
  if (firstObject.object instanceof osuBase.Circle) {
561
770
  switch (objectData[section.firstObjectIndex].result) {
562
- case exports.hitResult.RESULT_300:
771
+ case exports.HitResult.great:
563
772
  firstObjectMinHitTime -=
564
773
  this.hitWindow.hitWindowFor300(isPrecise);
565
774
  break;
566
- case exports.hitResult.RESULT_100:
775
+ case exports.HitResult.good:
567
776
  firstObjectMinHitTime -=
568
777
  this.hitWindow.hitWindowFor100(isPrecise);
569
778
  break;
@@ -578,11 +787,11 @@ class ThreeFingerChecker {
578
787
  let lastObjectMaxHitTime = lastObject.object.startTime;
579
788
  if (lastObject.object instanceof osuBase.Circle) {
580
789
  switch (objectData[section.lastObjectIndex].result) {
581
- case exports.hitResult.RESULT_300:
790
+ case exports.HitResult.great:
582
791
  lastObjectMaxHitTime +=
583
792
  this.hitWindow.hitWindowFor300(isPrecise);
584
793
  break;
585
- case exports.hitResult.RESULT_100:
794
+ case exports.HitResult.good:
586
795
  lastObjectMaxHitTime +=
587
796
  this.hitWindow.hitWindowFor100(isPrecise);
588
797
  break;
@@ -599,18 +808,18 @@ class ThreeFingerChecker {
599
808
  const cursorIndexes = [];
600
809
  for (let i = 0; i < this.data.cursorMovement.length; ++i) {
601
810
  const c = this.data.cursorMovement[i];
602
- if (c.occurrences.length === 0) {
811
+ if (c.occurrenceGroups.length === 0) {
603
812
  continue;
604
813
  }
605
814
  // Do not include cursors that don't have an occurence in this section
606
815
  // this speeds up checking process.
607
- if (c.occurrences.filter((v) => v.time >= firstObjectMinHitTime &&
608
- v.time <= lastObjectMaxHitTime).length === 0) {
816
+ if (c.occurrenceGroups.filter((v) => v.startTime >= firstObjectMinHitTime &&
817
+ v.endTime <= lastObjectMaxHitTime).length === 0) {
609
818
  continue;
610
819
  }
611
820
  // If this cursor instance doesn't move, it's not the cursor instance we want.
612
- if (c.occurrences.filter((v) => v.id === exports.movementType.MOVE)
613
- .length === 0) {
821
+ if (c.occurrenceGroups.filter((v) => v.moves.length > 0).length ===
822
+ 0) {
614
823
  continue;
615
824
  }
616
825
  cursorIndexes.push(i);
@@ -626,7 +835,7 @@ class ThreeFingerChecker {
626
835
  */
627
836
  findDragIndex(sectionObjects, sectionReplayObjectData, cursorIndexes) {
628
837
  let objectIndex = sectionObjects.findIndex((v, i) => !(v.object instanceof osuBase.Spinner) &&
629
- sectionReplayObjectData[i].result !== exports.hitResult.RESULT_0);
838
+ sectionReplayObjectData[i].result !== exports.HitResult.miss);
630
839
  if (objectIndex === -1) {
631
840
  return -1;
632
841
  }
@@ -637,7 +846,7 @@ class ThreeFingerChecker {
637
846
  const o = sectionObjects[objectIndex];
638
847
  const s = sectionReplayObjectData[objectIndex];
639
848
  ++objectIndex;
640
- if (s.result === exports.hitResult.RESULT_0) {
849
+ if (s.result === exports.HitResult.miss) {
641
850
  continue;
642
851
  }
643
852
  // Get the cursor instance that is closest to the object's hit time.
@@ -647,29 +856,32 @@ class ThreeFingerChecker {
647
856
  // therefore the game emulates the movement between
648
857
  // movementType.MOVE cursors.
649
858
  const hitTime = o.object.startTime + s.accuracy;
650
- const nextHitIndex = c.occurrences.findIndex((v) => v.time >= hitTime);
651
- const hitIndex = nextHitIndex - 1;
652
- if (hitIndex <= -1) {
653
- cursorIndexes[j] = -1;
859
+ const cursorGroup = c.occurrenceGroups.find((v) => v.isActiveAt(hitTime));
860
+ if (!cursorGroup) {
654
861
  continue;
655
862
  }
656
- if (c.occurrences[hitIndex].id === exports.movementType.UP) {
863
+ const cursors = cursorGroup.allOccurrences;
864
+ const nextHitIndex = cursors.findIndex((v) => v.time >= hitTime);
865
+ const hitIndex = nextHitIndex - 1;
866
+ if (hitIndex <= -1) {
657
867
  cursorIndexes[j] = -1;
658
868
  continue;
659
869
  }
660
- const cursorPosition = new osuBase.Vector2(c.occurrences[hitIndex].position.x, c.occurrences[hitIndex].position.y);
870
+ const cursorPosition = new osuBase.Vector2(cursors[hitIndex].position.x, cursors[hitIndex].position.y);
661
871
  let isInObject = false;
662
- if (c.occurrences[nextHitIndex].id === exports.movementType.MOVE ||
663
- c.occurrences[hitIndex].id === exports.movementType.MOVE) {
872
+ if (cursors[nextHitIndex].id === exports.MovementType.move) {
664
873
  // Try to interpolate movement between two movementType.MOVE cursor every 1ms.
665
874
  // This minimizes rounding error.
666
- for (let mSecPassed = c.occurrences[hitIndex].time; mSecPassed <= c.occurrences[nextHitIndex].time; ++mSecPassed) {
667
- const t = (mSecPassed - c.occurrences[nextHitIndex].time) /
668
- (c.occurrences[hitIndex].time -
669
- c.occurrences[nextHitIndex].time);
670
- cursorPosition.x = osuBase.Interpolation.lerp(c.occurrences[hitIndex].position.x, c.occurrences[nextHitIndex].position.x, t);
671
- cursorPosition.y = osuBase.Interpolation.lerp(c.occurrences[hitIndex].position.y, c.occurrences[nextHitIndex].position.y, t);
672
- if (o.object.stackedPosition.getDistance(cursorPosition) <= o.object.radius) {
875
+ for (let mSecPassed = cursors[hitIndex].time; mSecPassed <= cursors[nextHitIndex].time; ++mSecPassed) {
876
+ const t = (mSecPassed - cursors[nextHitIndex].time) /
877
+ (cursors[hitIndex].time -
878
+ cursors[nextHitIndex].time);
879
+ cursorPosition.x = osuBase.Interpolation.lerp(cursors[hitIndex].position.x, cursors[nextHitIndex].position.x, t);
880
+ cursorPosition.y = osuBase.Interpolation.lerp(cursors[hitIndex].position.y, cursors[nextHitIndex].position.y, t);
881
+ if (o.object
882
+ .getStackedPosition(osuBase.modes.droid)
883
+ .getDistance(cursorPosition) <=
884
+ o.object.getRadius(osuBase.modes.droid)) {
673
885
  isInObject = true;
674
886
  break;
675
887
  }
@@ -677,8 +889,10 @@ class ThreeFingerChecker {
677
889
  }
678
890
  else {
679
891
  isInObject =
680
- o.object.stackedPosition.getDistance(cursorPosition) <=
681
- o.object.radius;
892
+ o.object
893
+ .getStackedPosition(osuBase.modes.droid)
894
+ .getDistance(cursorPosition) <=
895
+ o.object.getRadius(osuBase.modes.droid);
682
896
  }
683
897
  if (!isInObject) {
684
898
  cursorIndexes[j] = -1;
@@ -741,26 +955,26 @@ class ThreeFingerChecker {
741
955
  * unnecessary taps.
742
956
  */
743
957
  preventAccidentalTaps() {
744
- let filledCursorAmount = this.downCursorInstances.filter((v) => v.occurrences.length > 0).length;
958
+ let filledCursorAmount = this.downCursorInstances.filter((v) => v.length > 0).length;
745
959
  if (filledCursorAmount <= 3) {
746
960
  return;
747
961
  }
748
962
  const objects = this.calculator.objects;
749
- const totalCursorAmount = this.downCursorInstances.reduce((acc, value) => acc + value.occurrences.length, 0);
963
+ const totalCursorAmount = this.downCursorInstances.reduce((acc, value) => acc + value.length, 0);
750
964
  for (let i = 0; i < this.downCursorInstances.length; ++i) {
751
965
  if (filledCursorAmount <= 3) {
752
966
  break;
753
967
  }
754
- const cursorInstance = this.downCursorInstances[i];
968
+ const cursorInstances = this.downCursorInstances[i];
755
969
  // Use an estimation for accidental tap threshold.
756
- if (cursorInstance.occurrences.length <=
970
+ if (cursorInstances.length <=
757
971
  Math.ceil(objects.length / this.accidentalTapThreshold) &&
758
- cursorInstance.occurrences.length / totalCursorAmount <
972
+ cursorInstances.length / totalCursorAmount <
759
973
  this.threeFingerRatioThreshold * 2) {
760
974
  --filledCursorAmount;
761
- cursorInstance.occurrences.length = 0;
975
+ cursorInstances.length = 0;
762
976
  }
763
- this.downCursorInstances[i] = cursorInstance;
977
+ this.downCursorInstances[i] = cursorInstances;
764
978
  }
765
979
  }
766
980
  /**
@@ -778,19 +992,19 @@ class ThreeFingerChecker {
778
992
  const dragIndex = beatmapSection.dragFingerIndex;
779
993
  const startTime = objects[beatmapSection.firstObjectIndex].object.startTime +
780
994
  (objectData[beatmapSection.firstObjectIndex].result !==
781
- exports.hitResult.RESULT_0
995
+ exports.HitResult.miss
782
996
  ? objectData[beatmapSection.firstObjectIndex].accuracy
783
997
  : -this.hitWindow.hitWindowFor50(isPrecise));
784
998
  const endTime = objects[beatmapSection.lastObjectIndex].object.endTime +
785
999
  (objectData[beatmapSection.lastObjectIndex].result !==
786
- exports.hitResult.RESULT_0
1000
+ exports.HitResult.miss
787
1001
  ? objectData[beatmapSection.lastObjectIndex].accuracy
788
1002
  : this.hitWindow.hitWindowFor50(isPrecise));
789
1003
  // Filter cursor instances during section.
790
1004
  this.downCursorInstances.forEach((c) => {
791
- const i = c.occurrences.findIndex((t) => t.time >= startTime);
1005
+ const i = c.findIndex((t) => t.time >= startTime);
792
1006
  if (i !== -1) {
793
- c.occurrences = c.occurrences.slice(i);
1007
+ c = c.slice(i);
794
1008
  }
795
1009
  });
796
1010
  const cursorAmounts = [];
@@ -800,15 +1014,15 @@ class ThreeFingerChecker {
800
1014
  if (i === dragIndex) {
801
1015
  continue;
802
1016
  }
803
- const cursorData = this.downCursorInstances[i];
1017
+ const cursors = this.downCursorInstances[i];
804
1018
  let amount = 0;
805
- for (let j = 0; j < cursorData.occurrences.length; ++j) {
806
- if (cursorData.occurrences[j].time >= startTime &&
807
- cursorData.occurrences[j].time <= endTime) {
1019
+ for (let j = 0; j < cursors.length; ++j) {
1020
+ if (cursors[j].time >= startTime &&
1021
+ cursors[j].time <= endTime) {
808
1022
  ++amount;
809
1023
  cursorVectorTimes.push({
810
- vector: new osuBase.Vector2(cursorData.occurrences[j].position.x, cursorData.occurrences[j].position.y),
811
- time: cursorData.occurrences[j].time,
1024
+ vector: new osuBase.Vector2(cursors[j].position.x, cursors[j].position.y),
1025
+ time: cursors[j].time,
812
1026
  });
813
1027
  }
814
1028
  }
@@ -976,6 +1190,13 @@ class TwoHandChecker {
976
1190
  * those that do not worth any strain.
977
1191
  */
978
1192
  minCursorIndexCount = 5;
1193
+ /**
1194
+ * A cursor occurrence nested array containing all cursor occurrences.
1195
+ *
1196
+ * Each index represents the cursor index.
1197
+ */
1198
+ // TODO: replace with group at some point
1199
+ allCursorOccurrences = [];
979
1200
  /**
980
1201
  * @param calculator The difficulty calculator to analyze.
981
1202
  * @param data The data of the replay.
@@ -983,10 +1204,12 @@ class TwoHandChecker {
983
1204
  constructor(calculator, data) {
984
1205
  this.calculator = calculator;
985
1206
  this.data = data;
1207
+ this.allCursorOccurrences = data.cursorMovement.map((v) => v.allOccurrences);
986
1208
  const stats = new osuBase.MapStats({
987
1209
  od: this.calculator.beatmap.difficulty.od,
988
1210
  mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
989
- !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
1211
+ !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym) &&
1212
+ !(m instanceof osuBase.ModPrecise)),
990
1213
  }).calculate();
991
1214
  this.hitWindow = new osuBase.DroidHitWindow(stats.od);
992
1215
  }
@@ -994,8 +1217,7 @@ class TwoHandChecker {
994
1217
  * Checks if a beatmap is two-handed.
995
1218
  */
996
1219
  check() {
997
- if (this.data.cursorMovement.filter((v) => v.occurrences.length > 0)
998
- .length <= 1) {
1220
+ if (this.data.cursorMovement.filter((v) => v.occurrenceGroups.length > 0).length <= 1) {
999
1221
  return { is2Hand: false, cursorIndexes: [] };
1000
1222
  }
1001
1223
  this.indexHitObjects();
@@ -1074,16 +1296,15 @@ class TwoHandChecker {
1074
1296
  const deltaTimes = [];
1075
1297
  for (let i = 0; i < this.data.cursorMovement.length; ++i) {
1076
1298
  const c = this.data.cursorMovement[i];
1077
- for (let j = 0; j < c.occurrences.length; ++j) {
1078
- if (c.occurrences[j].time <
1299
+ for (let j = 0; j < c.occurrenceGroups.length; ++j) {
1300
+ const group = c.occurrenceGroups[j];
1301
+ const previousGroup = c.occurrenceGroups[j - 1];
1302
+ if (group.startTime <
1079
1303
  this.calculator.beatmap.hitObjects.objects[0].startTime -
1080
1304
  this.hitWindow.hitWindowFor50()) {
1081
1305
  continue;
1082
1306
  }
1083
- if (c.occurrences[j].id !== exports.movementType.MOVE) {
1084
- continue;
1085
- }
1086
- const deltaTime = c.occurrences[j]?.time - c.occurrences[j - 1]?.time || 0;
1307
+ const deltaTime = group.startTime - previousGroup.endTime;
1087
1308
  if (deltaTime > 0) {
1088
1309
  deltaTimes.push(deltaTime);
1089
1310
  }
@@ -1102,7 +1323,7 @@ class TwoHandChecker {
1102
1323
  const object = this.calculator.objects[index];
1103
1324
  const data = this.data.hitObjectData[index];
1104
1325
  if (object.object instanceof osuBase.Spinner ||
1105
- data.result === exports.hitResult.RESULT_0) {
1326
+ data.result === exports.HitResult.miss) {
1106
1327
  return new IndexedHitObject(object, -1, -1, -1);
1107
1328
  }
1108
1329
  const isPrecise = this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise);
@@ -1110,10 +1331,10 @@ class TwoHandChecker {
1110
1331
  let hitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
1111
1332
  if (!(object.object instanceof osuBase.Slider)) {
1112
1333
  switch (data.result) {
1113
- case exports.hitResult.RESULT_300:
1334
+ case exports.HitResult.great:
1114
1335
  hitWindowLength = this.hitWindow.hitWindowFor300(isPrecise);
1115
1336
  break;
1116
- case exports.hitResult.RESULT_100:
1337
+ case exports.HitResult.good:
1117
1338
  hitWindowLength = this.hitWindow.hitWindowFor100(isPrecise);
1118
1339
  break;
1119
1340
  }
@@ -1123,26 +1344,26 @@ class TwoHandChecker {
1123
1344
  const minimumHitTime = startTime - hitWindowLength - hitWindowOffset;
1124
1345
  const maximumHitTime = startTime + hitWindowLength + hitWindowOffset;
1125
1346
  const cursorInformations = [];
1126
- for (let i = 0; i < this.data.cursorMovement.length; ++i) {
1127
- const c = this.data.cursorMovement[i];
1128
- if (c.occurrences.length === 0) {
1347
+ for (let i = 0; i < this.allCursorOccurrences.length; ++i) {
1348
+ const c = this.allCursorOccurrences[i];
1349
+ if (c.length === 0) {
1129
1350
  continue;
1130
1351
  }
1131
- let hitTimeBeforeIndex = osuBase.MathUtils.clamp(c.occurrences.findIndex((v) => v.time >= minimumHitTime), 1, c.occurrences.length - 1) - 1;
1132
- let hitTimeAfterIndex = c.occurrences.findIndex(
1352
+ let hitTimeBeforeIndex = osuBase.MathUtils.clamp(c.findIndex((v) => v.time >= minimumHitTime), 1, c.length - 1) - 1;
1353
+ let hitTimeAfterIndex = c.findIndex(
1133
1354
  // There is a special case for sliders where the time leniency in droid is a lot bigger compared to PC.
1134
1355
  // To prevent slider end time from ending earlier than hit window leniency, we use the maximum value between both.
1135
1356
  (v) => v.time >= Math.max(object.object.endTime, maximumHitTime));
1136
1357
  if (hitTimeAfterIndex === -1) {
1137
1358
  // Maximum hit time or object end time may be out of bounds for every presses.
1138
1359
  // We set the index to the latest cursor occurrence if that happens.
1139
- hitTimeAfterIndex = c.occurrences.length;
1360
+ hitTimeAfterIndex = c.length;
1140
1361
  }
1141
1362
  --hitTimeAfterIndex;
1142
1363
  // Sometimes a `movementType.UP` instance occurs at the same time as a `movementType.MOVE`
1143
1364
  // or a cursor is recorded twice in one time, therefore this check is required.
1144
- while (c.occurrences[hitTimeBeforeIndex]?.time ===
1145
- c.occurrences[hitTimeBeforeIndex - 1]?.time &&
1365
+ while (c[hitTimeBeforeIndex]?.time ===
1366
+ c[hitTimeBeforeIndex - 1]?.time &&
1146
1367
  hitTimeBeforeIndex > 0) {
1147
1368
  --hitTimeBeforeIndex;
1148
1369
  }
@@ -1151,26 +1372,29 @@ class TwoHandChecker {
1151
1372
  let distance = Number.POSITIVE_INFINITY;
1152
1373
  let j = hitTimeBeforeIndex;
1153
1374
  for (j; j <= hitTimeAfterIndex; ++j) {
1154
- const occurrence = c.occurrences[j];
1155
- const nextOccurrence = c.occurrences[j + 1];
1375
+ const occurrence = c[j];
1376
+ const nextOccurrence = c[j + 1];
1156
1377
  const cursorPosition = occurrence.position;
1157
1378
  if (occurrence.time < minimumHitTime &&
1158
- nextOccurrence?.id !== exports.movementType.MOVE) {
1379
+ nextOccurrence?.id !== exports.MovementType.move) {
1159
1380
  continue;
1160
1381
  }
1161
1382
  if (occurrence.time > hitTime + hitWindowOffset) {
1162
1383
  // Set distance to minimum just for the last.
1163
- if (occurrence.id !== exports.movementType.UP) {
1164
- distance = Math.min(distance, object.object.stackedPosition.getDistance(cursorPosition));
1384
+ if (occurrence.id !== exports.MovementType.up) {
1385
+ distance = Math.min(distance, object.object
1386
+ .getStackedPosition(osuBase.modes.droid)
1387
+ .getDistance(cursorPosition));
1165
1388
  }
1166
1389
  break;
1167
1390
  }
1168
- if (occurrence.id === exports.movementType.UP) {
1391
+ if (occurrence.id === exports.MovementType.up) {
1169
1392
  continue;
1170
1393
  }
1171
- distance =
1172
- object.object.stackedPosition.getDistance(cursorPosition);
1173
- if (nextOccurrence?.id === exports.movementType.MOVE &&
1394
+ distance = object.object
1395
+ .getStackedPosition(osuBase.modes.droid)
1396
+ .getDistance(cursorPosition);
1397
+ if (nextOccurrence?.id === exports.MovementType.move &&
1174
1398
  occurrence.time !== nextOccurrence.time &&
1175
1399
  !occurrence.position.equals(nextOccurrence.position)) {
1176
1400
  // If next cursor is a `move` instance and it doesn't go out of time
@@ -1180,11 +1404,13 @@ class TwoHandChecker {
1180
1404
  for (let mSecPassed = Math.max(minimumHitTime, occurrence.time); mSecPassed <= Math.min(hitTime, nextOccurrence.time); ++mSecPassed) {
1181
1405
  const progress = (mSecPassed - occurrence.time) /
1182
1406
  (nextOccurrence.time - occurrence.time);
1183
- distance = object.object.stackedPosition.getDistance(cursorPosition.add(displacement.scale(progress)));
1407
+ distance = object.object
1408
+ .getStackedPosition(osuBase.modes.droid)
1409
+ .getDistance(cursorPosition.add(displacement.scale(progress)));
1184
1410
  }
1185
1411
  }
1186
1412
  }
1187
- if (distance > object.object.radius) {
1413
+ if (distance > object.object.getRadius(osuBase.modes.droid)) {
1188
1414
  continue;
1189
1415
  }
1190
1416
  // The case for a one-handed object is that there will be a slight movement in the cursor towards
@@ -1198,21 +1424,20 @@ class TwoHandChecker {
1198
1424
  // Aside of angles, we need to consider if the player dragged from the previous object to the current object.
1199
1425
  let isDragged = false;
1200
1426
  // Get the latest down or movement cursor occurrence.
1201
- while (c.occurrences[j]?.id === exports.movementType.UP &&
1202
- j > hitTimeBeforeIndex) {
1427
+ while (c[j]?.id === exports.MovementType.up && j > hitTimeBeforeIndex) {
1203
1428
  --j;
1204
1429
  }
1205
1430
  if (object.object instanceof osuBase.Circle) {
1206
1431
  // For circles, we only need to consider the actual press on the circle.
1207
1432
  // Therefore, we need to get the latest down cursor occurrence instead.
1208
- while (c.occurrences[j]?.id !== exports.movementType.DOWN &&
1433
+ while (c[j]?.id !== exports.MovementType.down &&
1209
1434
  j > hitTimeBeforeIndex) {
1210
1435
  --j;
1211
1436
  }
1212
1437
  }
1213
1438
  // Theoretically there can only be 1 up occurrence, but this is a
1214
1439
  // consideration if the user manually adds cursor occurrences.
1215
- if (c.occurrences[j]?.id === exports.movementType.UP) {
1440
+ if (c[j]?.id === exports.MovementType.up) {
1216
1441
  ++j;
1217
1442
  }
1218
1443
  // Some move instances move in the exact same place. Not sure why, most likely
@@ -1220,16 +1445,16 @@ class TwoHandChecker {
1220
1445
  // convert into +1 or -1.
1221
1446
  // let nextSignificantOccurrenceIndex: number = j + 1;
1222
1447
  // while (
1223
- // c.occurrences[j] &&
1224
- // c.occurrences[nextSignificantOccurrenceIndex] &&
1225
- // c.occurrences[j].position.equals(
1226
- // c.occurrences[nextSignificantOccurrenceIndex].position
1448
+ // c[j] &&
1449
+ // c[nextSignificantOccurrenceIndex] &&
1450
+ // c[j].position.equals(
1451
+ // c[nextSignificantOccurrenceIndex].position
1227
1452
  // )
1228
1453
  // ) {
1229
1454
  // ++nextSignificantOccurrenceIndex;
1230
1455
  // }
1231
1456
  // const nextSignificantOccurrence: CursorOccurrence =
1232
- // c.occurrences[nextSignificantOccurrenceIndex];
1457
+ // c[nextSignificantOccurrenceIndex];
1233
1458
  // const next: DifficultyHitObject | RebalanceDifficultyHitObject =
1234
1459
  // this.map.objects[index + 1];
1235
1460
  // Angle detection.
@@ -1267,7 +1492,7 @@ class TwoHandChecker {
1267
1492
  );
1268
1493
 
1269
1494
  const currentToNext: Vector2 =
1270
- next.object.stackedPosition.subtract(actualEndPosition);
1495
+ next.object.getStackedPosition(modes.droid).subtract(actualEndPosition);
1271
1496
 
1272
1497
  const dot: number = currentToNext.dot(movementVec);
1273
1498
  const det: number =
@@ -1296,24 +1521,22 @@ class TwoHandChecker {
1296
1521
  }
1297
1522
  }
1298
1523
  let occurrenceStartIndex = hitTimeAfterIndex;
1299
- while (c.occurrences[occurrenceStartIndex]?.time >=
1300
- dragTimeThreshold &&
1524
+ while (c[occurrenceStartIndex]?.time >= dragTimeThreshold &&
1301
1525
  occurrenceStartIndex > 0) {
1302
1526
  --occurrenceStartIndex;
1303
1527
  }
1304
1528
  // The above loop will make the start index before or right when
1305
1529
  // the previous object was hit or ended, but we want the index after it.
1306
1530
  ++occurrenceStartIndex;
1307
- const dragOccurrences = c.occurrences.slice(occurrenceStartIndex, hitTimeAfterIndex);
1531
+ const dragOccurrences = c.slice(occurrenceStartIndex, hitTimeAfterIndex);
1308
1532
  isDragged =
1309
1533
  dragOccurrences.length > 0 &&
1310
1534
  // We only care when the cursor approaches the current object. It doesn't
1311
1535
  // matter whether the previous object was pressed or dragged.
1312
1536
  dragOccurrences
1313
1537
  .at(-1)
1314
- .position.getDistance(object.object.stackedPosition) <=
1315
- object.object.radius &&
1316
- dragOccurrences.every((v) => v.id === exports.movementType.MOVE);
1538
+ .position.getDistance(object.object.getStackedPosition(osuBase.modes.droid)) <= object.object.getRadius(osuBase.modes.droid) &&
1539
+ dragOccurrences.every((v) => v.id === exports.MovementType.move);
1317
1540
  cursorInformations.push({
1318
1541
  // If the angle is fulfilled or the player dragged,
1319
1542
  // we set the cursor index to the main cursor index.
@@ -1344,12 +1567,12 @@ class TwoHandChecker {
1344
1567
  */
1345
1568
  checkSliderCheesing(indexedHitObject, hitData, hitWindowOffset) {
1346
1569
  if (!(indexedHitObject.object.object instanceof osuBase.Slider) ||
1347
- hitData.result === exports.hitResult.RESULT_0) {
1570
+ hitData.result === exports.HitResult.miss) {
1348
1571
  return false;
1349
1572
  }
1350
1573
  let cursorLoopIndex = Math.max(0, indexedHitObject.occurrenceIndex);
1351
- const c = this.data.cursorMovement[indexedHitObject.actualCursorIndex];
1352
- const acceptableRadius = indexedHitObject.object.object.radius * 2.4;
1574
+ const c = this.allCursorOccurrences[indexedHitObject.actualCursorIndex];
1575
+ const acceptableRadius = indexedHitObject.object.object.getRadius(osuBase.modes.droid) * 2.4;
1353
1576
  for (let i = 1; i < indexedHitObject.object.object.nestedHitObjects.length; ++i) {
1354
1577
  const tickWasHit = hitData.tickset[i - 1];
1355
1578
  if (!tickWasHit) {
@@ -1358,16 +1581,14 @@ class TwoHandChecker {
1358
1581
  const object = indexedHitObject.object.object.nestedHitObjects[i];
1359
1582
  let j = cursorLoopIndex;
1360
1583
  let cursorHitTick = false;
1361
- for (j; j < c.occurrences.length; ++j) {
1362
- if (c.occurrences[j].time <
1363
- object.startTime - hitWindowOffset) {
1584
+ for (j; j < c.length; ++j) {
1585
+ if (c[j].time < object.startTime - hitWindowOffset) {
1364
1586
  continue;
1365
1587
  }
1366
- if (c.occurrences[j].time >
1367
- object.startTime + hitWindowOffset) {
1588
+ if (c[j].time > object.startTime + hitWindowOffset) {
1368
1589
  break;
1369
1590
  }
1370
- if (c.occurrences[j].position.getDistance(object.stackedPosition) <= acceptableRadius) {
1591
+ if (c[j].position.getDistance(object.getStackedPosition(osuBase.modes.droid)) <= acceptableRadius) {
1371
1592
  cursorHitTick = true;
1372
1593
  break;
1373
1594
  }
@@ -1602,7 +1823,7 @@ class ReplayAnalyzer {
1602
1823
  resultObject.isFullCombo = !!rawObject[4][44];
1603
1824
  resultObject.playerName = rawObject[5];
1604
1825
  resultObject.rawMods = rawObject[6].elements;
1605
- resultObject.convertedMods = this.convertMods(rawObject[6].elements);
1826
+ resultObject.convertedMods = osuBase.ModUtil.droidStringToMods(this.convertDroidMods(rawObject[6].elements));
1606
1827
  // Determine rank
1607
1828
  const totalHits = resultObject.accuracy.n300 +
1608
1829
  resultObject.accuracy.n100 +
@@ -1688,7 +1909,7 @@ class ReplayAnalyzer {
1688
1909
  bufferCounter += this.INT_LENGTH;
1689
1910
  id[i] = time[i] & 3;
1690
1911
  time[i] >>= 2;
1691
- if (id[i] !== exports.movementType.UP) {
1912
+ if (id[i] !== exports.MovementType.up) {
1692
1913
  if (resultObject.replayVersion >= 5) {
1693
1914
  x[i] = replayDataBuffer.readFloatBE(bufferCounter);
1694
1915
  bufferCounter += this.FLOAT_LENGTH;
@@ -1768,21 +1989,21 @@ class ReplayAnalyzer {
1768
1989
  const hitObjectData = resultObject.hitObjectData[i];
1769
1990
  const isNextNewCombo = i + 1 !== objects.length ? objects[i + 1].isNewCombo : true;
1770
1991
  switch (hitObjectData.result) {
1771
- case exports.hitResult.RESULT_0:
1992
+ case exports.HitResult.miss:
1772
1993
  ++hit0;
1773
1994
  grantsGekiOrKatu = false;
1774
1995
  break;
1775
- case exports.hitResult.RESULT_50:
1996
+ case exports.HitResult.meh:
1776
1997
  ++hit50;
1777
1998
  grantsGekiOrKatu = false;
1778
1999
  break;
1779
- case exports.hitResult.RESULT_100:
2000
+ case exports.HitResult.good:
1780
2001
  ++hit100;
1781
2002
  if (grantsGekiOrKatu && isNextNewCombo) {
1782
2003
  ++hit100k;
1783
2004
  }
1784
2005
  break;
1785
- case exports.hitResult.RESULT_300:
2006
+ case exports.HitResult.great:
1786
2007
  ++hit300;
1787
2008
  if (grantsGekiOrKatu && isNextNewCombo) {
1788
2009
  ++hit300k;
@@ -1866,7 +2087,7 @@ class ReplayAnalyzer {
1866
2087
  for (let i = 0; i < hitObjectData.length; ++i) {
1867
2088
  const v = hitObjectData[i];
1868
2089
  const o = objects[i];
1869
- if (o instanceof osuBase.Spinner || v.result === exports.hitResult.RESULT_0) {
2090
+ if (o instanceof osuBase.Spinner || v.result === exports.HitResult.miss) {
1870
2091
  continue;
1871
2092
  }
1872
2093
  const accuracy = v.accuracy;
@@ -1882,7 +2103,7 @@ class ReplayAnalyzer {
1882
2103
  return {
1883
2104
  positiveAvg: positiveTotal / positiveCount || 0,
1884
2105
  negativeAvg: negativeTotal / negativeCount || 0,
1885
- unstableRate: osuBase.MathUtils.calculateStandardDeviation(hitObjectData.map((v, i) => v.result !== exports.hitResult.RESULT_0 &&
2106
+ unstableRate: osuBase.MathUtils.calculateStandardDeviation(hitObjectData.map((v, i) => v.result !== exports.HitResult.miss &&
1886
2107
  !(objects[i] instanceof osuBase.Spinner)
1887
2108
  ? v.accuracy
1888
2109
  : 0)) * 10,
@@ -1924,12 +2145,6 @@ class ReplayAnalyzer {
1924
2145
  }
1925
2146
  return modString;
1926
2147
  }
1927
- /**
1928
- * Converts replay mods to regular mod string.
1929
- */
1930
- convertMods(replayMods) {
1931
- return osuBase.ModUtil.droidStringToMods(this.convertDroidMods(replayMods));
1932
- }
1933
2148
  /**
1934
2149
  * Checks if a play is using 3 fingers.
1935
2150
  *
@@ -1997,6 +2212,7 @@ class ReplayObjectData {
1997
2212
 
1998
2213
  exports.CursorData = CursorData;
1999
2214
  exports.CursorOccurrence = CursorOccurrence;
2215
+ exports.CursorOccurrenceGroup = CursorOccurrenceGroup;
2000
2216
  exports.ReplayAnalyzer = ReplayAnalyzer;
2001
2217
  exports.ReplayData = ReplayData;
2002
2218
  exports.ReplayObjectData = ReplayObjectData;