@rian8337/osu-droid-replay-analyzer 4.0.0-beta.16 → 4.0.0-beta.18

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
@@ -51,18 +51,6 @@ exports.MovementType = void 0;
51
51
  * Represents a cursor's occurrence.
52
52
  */
53
53
  class CursorOccurrence {
54
- /**
55
- * The time of this occurrence.
56
- */
57
- time;
58
- /**
59
- * The position of the occurrence.
60
- */
61
- position;
62
- /**
63
- * The movement ID of the occurrence.
64
- */
65
- id;
66
54
  constructor(time, x, y, id) {
67
55
  this.time = time;
68
56
  this.position = new osuBase.Vector2(x, y);
@@ -125,7 +113,8 @@ class CursorOccurrenceGroup {
125
113
  * The time at which this cursor occurrence group ends.
126
114
  */
127
115
  get endTime() {
128
- return this._up?.time ?? this._moves.at(-1)?.time ?? this._down.time;
116
+ var _a, _b, _c, _d;
117
+ 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;
129
118
  }
130
119
  /**
131
120
  * The duration this cursor occurrence group is active for.
@@ -145,20 +134,6 @@ class CursorOccurrenceGroup {
145
134
  }
146
135
  return cursors;
147
136
  }
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
137
  constructor(down, moves, up) {
163
138
  this._down = down;
164
139
  this._moves = moves;
@@ -182,13 +157,14 @@ class CursorOccurrenceGroup {
182
157
  * @returns The cursor occurrence at the given time, `null` if not found.
183
158
  */
184
159
  cursorAt(time) {
160
+ var _a;
185
161
  if (!this.isActiveAt(time)) {
186
162
  return null;
187
163
  }
188
164
  if (this._down.time === time) {
189
165
  return this._down;
190
166
  }
191
- if (this._up?.time === time) {
167
+ if (((_a = this._up) === null || _a === void 0 ? void 0 : _a.time) === time) {
192
168
  return this._up;
193
169
  }
194
170
  let l = 0;
@@ -218,17 +194,14 @@ class CursorOccurrenceGroup {
218
194
  * This is used when analyzing replays using replay analyzer.
219
195
  */
220
196
  class CursorData {
221
- /**
222
- * The occurrence groups of this cursor instance.
223
- */
224
- occurrenceGroups = [];
225
197
  /**
226
198
  * The time at which the first occurrence of this cursor instance occurs.
227
199
  *
228
200
  * Will return `null` if there are no occurrences.
229
201
  */
230
202
  get earliestOccurrenceTime() {
231
- return this.occurrenceGroups.at(0)?.startTime ?? null;
203
+ var _a, _b;
204
+ return (_b = (_a = this.occurrenceGroups.at(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : null;
232
205
  }
233
206
  /**
234
207
  * The time at which the latest occurrence of this cursor instance occurs.
@@ -236,7 +209,8 @@ class CursorData {
236
209
  * Will return `null` if there are no occurrences.
237
210
  */
238
211
  get latestOccurrenceTime() {
239
- return this.occurrenceGroups.at(-1)?.endTime ?? null;
212
+ var _a, _b;
213
+ return (_b = (_a = this.occurrenceGroups.at(-1)) === null || _a === void 0 ? void 0 : _a.endTime) !== null && _b !== void 0 ? _b : null;
240
214
  }
241
215
  /**
242
216
  * The amount of cursor occurrences of this cursor instance.
@@ -263,6 +237,10 @@ class CursorData {
263
237
  return this.occurrenceGroups.flatMap((v) => v.allOccurrences);
264
238
  }
265
239
  constructor(values) {
240
+ /**
241
+ * The occurrence groups of this cursor instance.
242
+ */
243
+ this.occurrenceGroups = [];
266
244
  let downOccurrence = null;
267
245
  let moveOccurrences = [];
268
246
  for (let i = 0; i < values.size; ++i) {
@@ -316,102 +294,50 @@ exports.HitResult = void 0;
316
294
  * Utility to check whether or not a beatmap is three-fingered for rebalance scores.
317
295
  */
318
296
  class RebalanceThreeFingerChecker {
319
- /**
320
- * The beatmap that is being analyzed.
321
- */
322
- beatmap;
323
- /**
324
- * The data of the replay.
325
- */
326
- data;
327
- /**
328
- * The difficulty attributes of the beatmap.
329
- */
330
- difficultyAttributes;
331
- /**
332
- * The hitobjects to be analyzed.
333
- *
334
- * This is being maintained separately due to possible change in object scale.
335
- */
336
- hitObjects;
337
- /**
338
- * The distance threshold between cursors to assume that two cursors are
339
- * actually pressed with 1 finger in osu!pixels.
340
- *
341
- * This is used to prevent cases where a player would lift their finger
342
- * too fast to the point where the 4th cursor instance or beyond is recorded
343
- * as 1st, 2nd, or 3rd cursor instance.
344
- */
345
- cursorDistancingDistanceThreshold = 60;
346
- /**
347
- * The threshold for the amount of cursors that are assumed to be pressed
348
- * by a single finger.
349
- */
350
- cursorDistancingCountThreshold = 10;
351
- /**
352
- * The threshold for the time difference of cursors that are assumed to be pressed
353
- * by a single finger, in milliseconds.
354
- */
355
- cursorDistancingTimeThreshold = 1000;
356
- /**
357
- * The amount of notes that has a tap strain exceeding `strainThreshold`.
358
- */
359
- strainNoteCount;
360
- /**
361
- * The ratio threshold between non-3 finger cursors and 3-finger cursors.
362
- *
363
- * Increasing this number will increase detection accuracy, however
364
- * it also increases the chance of falsely flagged plays.
365
- */
366
- threeFingerRatioThreshold = 0.01;
367
- /**
368
- * Extended sections of the beatmap for drag detection.
369
- */
370
- beatmapSections = [];
371
- /**
372
- * The hit window of this beatmap. Keep in mind that speed-changing mods do not change hit window length in game logic.
373
- */
374
- hitWindow;
375
- /**
376
- * A reprocessed break points to match right on object time.
377
- *
378
- * This is used to increase detection accuracy since break points do not start right at the
379
- * start of the hitobject before it and do not end right at the first hitobject after it.
380
- */
381
- breakPointAccurateTimes = [];
382
- /**
383
- * A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
384
- *
385
- * Each index represents the cursor index.
386
- */
387
- downCursorInstances = [];
388
- /**
389
- * Nerf factors from all sections that were three-fingered.
390
- */
391
- nerfFactors = [];
392
- /**
393
- * Whether this score uses the Precise mod.
394
- */
395
- isPrecise;
396
297
  /**
397
298
  * @param beatmap The beatmap to analyze.
398
299
  * @param data The data of the replay.
399
300
  * @param difficultyAttributes The difficulty attributes of the beatmap.
400
301
  */
401
302
  constructor(beatmap, data, difficultyAttributes) {
303
+ /**
304
+ * The ratio threshold between non-3 finger cursors and 3-finger cursors.
305
+ *
306
+ * Increasing this number will increase detection accuracy, however
307
+ * it also increases the chance of falsely flagged plays.
308
+ */
309
+ this.threeFingerRatioThreshold = 0.01;
310
+ /**
311
+ * Extended sections of the beatmap for drag detection.
312
+ */
313
+ this.beatmapSections = [];
314
+ /**
315
+ * A reprocessed break points to match right on object time.
316
+ *
317
+ * This is used to increase detection accuracy since break points do not start right at the
318
+ * start of the hitobject before it and do not end right at the first hitobject after it.
319
+ */
320
+ this.breakPointAccurateTimes = [];
321
+ /**
322
+ * A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
323
+ *
324
+ * Each index represents the cursor index.
325
+ */
326
+ this.downCursorInstances = [];
327
+ /**
328
+ * Nerf factors from all sections that were three-fingered.
329
+ */
330
+ this.nerfFactors = [];
402
331
  this.beatmap = beatmap;
403
332
  this.data = data;
404
333
  this.difficultyAttributes = difficultyAttributes;
405
334
  const od = osuBase.calculateDroidDifficultyStatistics({
406
335
  overallDifficulty: beatmap.difficulty.od,
407
- mods: this.difficultyAttributes.mods.filter((m) => m.isApplicableToDroid() &&
408
- !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
336
+ mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
409
337
  convertOverallDifficulty: false,
410
338
  }).overallDifficulty;
411
339
  this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
412
340
  this.hitWindow = new osuBase.DroidHitWindow(od);
413
- this.strainNoteCount =
414
- this.difficultyAttributes.possibleThreeFingeredSections.reduce((a, v) => a + v.lastObjectIndex - v.firstObjectIndex + 1, 0);
415
341
  this.hitObjects = beatmap.hitObjects.objects;
416
342
  }
417
343
  /**
@@ -433,12 +359,18 @@ class RebalanceThreeFingerChecker {
433
359
  * nerf factor, taking beatmap difficulty into account.
434
360
  */
435
361
  check() {
436
- if (this.strainNoteCount === 0) {
362
+ if (this.difficultyAttributes.possibleThreeFingeredSections.length === 0) {
437
363
  return { is3Finger: false, penalty: 1 };
438
364
  }
439
365
  this.getAccurateBreakPoints();
440
366
  this.filterCursorInstances();
367
+ if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
368
+ return { is3Finger: false, penalty: 1 };
369
+ }
441
370
  this.getBeatmapSections();
371
+ if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
372
+ return { is3Finger: false, penalty: 1 };
373
+ }
442
374
  this.calculateNerfFactors();
443
375
  const finalPenalty = this.calculateFinalPenalty();
444
376
  return { is3Finger: finalPenalty > 1, penalty: finalPenalty };
@@ -554,16 +486,9 @@ class RebalanceThreeFingerChecker {
554
486
  const dragFingerIndex = this.findDragIndex(section);
555
487
  const objects = [];
556
488
  for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
557
- objects.push({
558
- object: this.hitObjects[i],
559
- ...this.getObjectPressIndex(this.hitObjects[i], this.data.hitObjectData[i], cursorLookupIndices, dragFingerIndex),
560
- });
489
+ objects.push(Object.assign({ object: this.hitObjects[i] }, this.getObjectPressIndex(this.hitObjects[i], this.data.hitObjectData[i], cursorLookupIndices, dragFingerIndex)));
561
490
  }
562
- this.beatmapSections.push({
563
- ...section,
564
- dragFingerIndex: dragFingerIndex,
565
- objects: objects,
566
- });
491
+ this.beatmapSections.push(Object.assign(Object.assign({}, section), { dragFingerIndex: dragFingerIndex, objects: objects }));
567
492
  }
568
493
  }
569
494
  /**
@@ -639,8 +564,8 @@ class RebalanceThreeFingerChecker {
639
564
  }
640
565
  }
641
566
  return {
642
- pressingCursorInstanceIndex: nearestCursorInstanceIndex ?? -1,
643
- pressingCursorIndex: nearestCursorIndex ?? -1,
567
+ pressingCursorInstanceIndex: nearestCursorInstanceIndex !== null && nearestCursorInstanceIndex !== void 0 ? nearestCursorInstanceIndex : -1,
568
+ pressingCursorIndex: nearestCursorIndex !== null && nearestCursorIndex !== void 0 ? nearestCursorIndex : -1,
644
569
  };
645
570
  }
646
571
  /**
@@ -651,6 +576,7 @@ class RebalanceThreeFingerChecker {
651
576
  * @param section The section to check.
652
577
  */
653
578
  findDragIndex(section) {
579
+ var _a;
654
580
  const objectData = this.data.hitObjectData;
655
581
  const firstObject = this.hitObjects[section.firstObjectIndex];
656
582
  const lastObject = this.hitObjects[section.lastObjectIndex];
@@ -763,31 +689,25 @@ class RebalanceThreeFingerChecker {
763
689
  }
764
690
  }
765
691
  }
766
- return cursorIndices.find((v) => v !== -1) ?? -1;
692
+ return (_a = cursorIndices.find((v) => v !== -1)) !== null && _a !== void 0 ? _a : -1;
767
693
  }
768
694
  /**
769
695
  * Creates nerf factors by scanning through objects.
770
696
  */
771
697
  calculateNerfFactors() {
772
698
  for (const beatmapSection of this.beatmapSections) {
773
- const cursorAmounts = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
774
- const cursorVectorTimes = [];
699
+ const cursorCounts = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
775
700
  for (const object of beatmapSection.objects) {
776
701
  if (object.pressingCursorIndex === -1 ||
777
702
  object.pressingCursorInstanceIndex ===
778
703
  beatmapSection.dragFingerIndex) {
779
704
  continue;
780
705
  }
781
- ++cursorAmounts[object.pressingCursorInstanceIndex];
782
- const cursor = this.downCursorInstances[object.pressingCursorInstanceIndex][object.pressingCursorIndex];
783
- cursorVectorTimes.push({
784
- vector: new osuBase.Vector2(cursor.position.x, cursor.position.y),
785
- time: cursor.time,
786
- });
706
+ ++cursorCounts[object.pressingCursorInstanceIndex];
787
707
  }
788
708
  if (beatmapSection.dragFingerIndex !== -1) {
789
709
  // Remove the drag index to prevent it from being picked up into the detection.
790
- cursorAmounts.splice(beatmapSection.dragFingerIndex, 1);
710
+ cursorCounts.splice(beatmapSection.dragFingerIndex, 1);
791
711
  }
792
712
  // This index will be used to detect if a section is 3-fingered.
793
713
  // If the section is dragged, the dragged instance will be ignored,
@@ -795,77 +715,34 @@ class RebalanceThreeFingerChecker {
795
715
  const fingerSplitIndex = beatmapSection.dragFingerIndex !== -1 ? 2 : 3;
796
716
  // Divide >=4th (3rd for drag) cursor instances with 1st + 2nd (+ 3rd for nondrag)
797
717
  // to check if the section is 3-fingered.
798
- const threeFingerRatio = cursorAmounts
718
+ const threeFingerRatio = cursorCounts
799
719
  .slice(fingerSplitIndex)
800
720
  .reduce((acc, value) => acc + value, 0) /
801
- Math.max(cursorAmounts
721
+ Math.max(cursorCounts
802
722
  .slice(0, fingerSplitIndex)
803
723
  .reduce((acc, value) => acc + value, 0), 1);
804
- const similarPresses = [];
805
- cursorVectorTimes.sort((a, b) => a.time - b.time);
806
- for (const cursorVectorTime of cursorVectorTimes) {
807
- const pressIndex = similarPresses.findIndex((v) => v.vector.getDistance(cursorVectorTime.vector) <=
808
- this.cursorDistancingDistanceThreshold);
809
- if (pressIndex !== -1) {
810
- if (cursorVectorTime.time -
811
- similarPresses[pressIndex].lastTime >=
812
- this.cursorDistancingTimeThreshold) {
813
- // If the previous press is too late, remove it from the
814
- // list and register the current press as a new press.
815
- similarPresses.splice(pressIndex, 1);
816
- similarPresses.push({
817
- vector: cursorVectorTime.vector,
818
- count: 1,
819
- lastTime: cursorVectorTime.time,
820
- });
821
- continue;
822
- }
823
- similarPresses[pressIndex].vector = cursorVectorTime.vector;
824
- similarPresses[pressIndex].lastTime = cursorVectorTime.time;
825
- ++similarPresses[pressIndex].count;
826
- }
827
- else {
828
- similarPresses.push({
829
- vector: cursorVectorTime.vector,
830
- count: 1,
831
- lastTime: cursorVectorTime.time,
832
- });
833
- }
834
- }
835
- // Sort by highest count; assume the order is 3rd, 4th, 5th, ... finger
836
- const threeFingerPresses = similarPresses
837
- .filter((v) => v.count >= this.cursorDistancingCountThreshold)
838
- .sort((a, b) => b.count - a.count)
839
- .slice(fingerSplitIndex);
840
- if (threeFingerRatio > this.threeFingerRatioThreshold ||
841
- threeFingerPresses.length > 0) {
842
- const objectCount = beatmapSection.lastObjectIndex -
724
+ if (threeFingerRatio > this.threeFingerRatioThreshold) {
725
+ const threeFingeredObjectCount = cursorCounts
726
+ .slice(fingerSplitIndex)
727
+ .reduce((acc, value) => acc + value, 0);
728
+ const sectionObjectCount = beatmapSection.lastObjectIndex -
843
729
  beatmapSection.firstObjectIndex +
844
730
  1;
731
+ const threeFingeredObjectRatio = threeFingeredObjectCount / sectionObjectCount;
845
732
  // We can ignore the first 3 (2 for drag) filled cursor instances
846
733
  // since they are guaranteed not 3 finger.
847
- const threeFingerCursorAmounts = cursorAmounts
734
+ const threeFingerCursorCounts = cursorCounts
848
735
  .slice(fingerSplitIndex)
849
736
  .filter((amount) => amount > 0);
850
737
  // Finger factor applies more penalty if more fingers were used.
851
- const fingerFactor = threeFingerRatio > this.threeFingerRatioThreshold
852
- ? threeFingerCursorAmounts.reduce((acc, value, index) => acc +
853
- Math.pow(((index + 1) * value * objectCount) /
854
- this.strainNoteCount, 0.9), 1)
855
- : Math.pow(threeFingerPresses.reduce((acc, value, index) => acc +
856
- Math.pow(((index + 1) *
857
- (value.count /
858
- (this
859
- .cursorDistancingCountThreshold *
860
- 2)) *
861
- objectCount) /
862
- this.strainNoteCount, 0.2), 1), 0.2);
738
+ const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
739
+ Math.pow((index + 1) * count * threeFingeredObjectRatio, 0.9), 1);
863
740
  // Length factor applies more penalty if there are more 3-fingered object.
864
- const lengthFactor = 1 + Math.pow(objectCount / this.strainNoteCount, 1.2);
741
+ const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 1.2);
865
742
  this.nerfFactors.push({
866
- strainFactor: Math.max(1, beatmapSection.sumStrain),
867
- fingerFactor,
868
- lengthFactor,
743
+ strainFactor: beatmapSection.sumStrain * threeFingeredObjectRatio,
744
+ fingerFactor: fingerFactor,
745
+ lengthFactor: lengthFactor,
869
746
  });
870
747
  }
871
748
  }
@@ -880,6 +757,38 @@ class RebalanceThreeFingerChecker {
880
757
  }
881
758
  }
882
759
 
760
+ /******************************************************************************
761
+ Copyright (c) Microsoft Corporation.
762
+
763
+ Permission to use, copy, modify, and/or distribute this software for any
764
+ purpose with or without fee is hereby granted.
765
+
766
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
767
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
768
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
769
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
770
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
771
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
772
+ PERFORMANCE OF THIS SOFTWARE.
773
+ ***************************************************************************** */
774
+ /* global Reflect, Promise, SuppressedError, Symbol */
775
+
776
+
777
+ function __awaiter(thisArg, _arguments, P, generator) {
778
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
779
+ return new (P || (P = Promise))(function (resolve, reject) {
780
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
781
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
782
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
783
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
784
+ });
785
+ }
786
+
787
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
788
+ var e = new Error(message);
789
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
790
+ };
791
+
883
792
  /**
884
793
  * Represents a replay data in an osu!droid replay.
885
794
  *
@@ -888,29 +797,6 @@ class RebalanceThreeFingerChecker {
888
797
  * This is used when analyzing replays using replay analyzer.
889
798
  */
890
799
  class ReplayData {
891
- replayVersion;
892
- folderName;
893
- fileName;
894
- hash;
895
- time;
896
- hit300k;
897
- hit100k;
898
- score;
899
- maxCombo;
900
- accuracy;
901
- isFullCombo;
902
- playerName;
903
- rawMods;
904
- rank;
905
- convertedMods;
906
- cursorMovement;
907
- hitObjectData;
908
- speedMultiplier;
909
- forceCS;
910
- forceAR;
911
- forceOD;
912
- forceHP;
913
- flashlightFollowDelay;
914
800
  constructor(values) {
915
801
  this.replayVersion = values.replayVersion;
916
802
  this.folderName = values.folderName;
@@ -942,98 +828,73 @@ class ReplayData {
942
828
  * Utility to check whether or not a beatmap is three-fingered.
943
829
  */
944
830
  class ThreeFingerChecker {
945
- /**
946
- * The beatmap that is being analyzed.
947
- */
948
- beatmap;
949
- /**
950
- * The data of the replay.
951
- */
952
- data;
953
- /**
954
- * The difficulty attributes of the beatmap.
955
- */
956
- difficultyAttributes;
957
- /**
958
- * The distance threshold between cursors to assume that two cursors are
959
- * actually pressed with 1 finger in osu!pixels.
960
- *
961
- * This is used to prevent cases where a player would lift their finger
962
- * too fast to the point where the 4th cursor instance or beyond is recorded
963
- * as 1st, 2nd, or 3rd cursor instance.
964
- */
965
- cursorDistancingDistanceThreshold = 60;
966
- /**
967
- * The threshold for the amount of cursors that are assumed to be pressed
968
- * by a single finger.
969
- */
970
- cursorDistancingCountThreshold = 10;
971
- /**
972
- * The threshold for the time difference of cursors that are assumed to be pressed
973
- * by a single finger, in milliseconds.
974
- */
975
- cursorDistancingTimeThreshold = 1000;
976
- /**
977
- * The amount of notes that has a tap strain exceeding `strainThreshold`.
978
- */
979
- strainNoteCount;
980
- /**
981
- * The ratio threshold between non-3 finger cursors and 3-finger cursors.
982
- *
983
- * Increasing this number will increase detection accuracy, however
984
- * it also increases the chance of falsely flagged plays.
985
- */
986
- threeFingerRatioThreshold = 0.01;
987
- /**
988
- * Extended sections of the beatmap for drag detection.
989
- */
990
- beatmapSections = [];
991
- /**
992
- * This threshold is used to filter out accidental taps.
993
- *
994
- * Increasing this number makes the filtration more sensitive, however it
995
- * will also increase the chance of 3-fingered plays getting out from
996
- * being flagged.
997
- */
998
- accidentalTapThreshold = 400;
999
- /**
1000
- * The hit window of this beatmap. Keep in mind that speed-changing mods do not change hit window length in game logic.
1001
- */
1002
- hitWindow;
1003
- /**
1004
- * A reprocessed break points to match right on object time.
1005
- *
1006
- * This is used to increase detection accuracy since break points do not start right at the
1007
- * start of the hitobject before it and do not end right at the first hitobject after it.
1008
- */
1009
- breakPointAccurateTimes = [];
1010
- /**
1011
- * A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
1012
- *
1013
- * Each index represents the cursor index.
1014
- */
1015
- downCursorInstances = [];
1016
- /**
1017
- * Nerf factors from all sections that were three-fingered.
1018
- */
1019
- nerfFactors = [];
1020
- /**
1021
- * Whether this score uses the Precise mod.
1022
- */
1023
- isPrecise;
1024
831
  /**
1025
832
  * @param beatmap The beatmap to analyze.
1026
833
  * @param data The data of the replay.
1027
834
  * @param difficultyAttributes The difficulty attributes of the beatmap.
1028
835
  */
1029
836
  constructor(beatmap, data, difficultyAttributes) {
837
+ /**
838
+ * The distance threshold between cursors to assume that two cursors are
839
+ * actually pressed with 1 finger in osu!pixels.
840
+ *
841
+ * This is used to prevent cases where a player would lift their finger
842
+ * too fast to the point where the 4th cursor instance or beyond is recorded
843
+ * as 1st, 2nd, or 3rd cursor instance.
844
+ */
845
+ this.cursorDistancingDistanceThreshold = 60;
846
+ /**
847
+ * The threshold for the amount of cursors that are assumed to be pressed
848
+ * by a single finger.
849
+ */
850
+ this.cursorDistancingCountThreshold = 10;
851
+ /**
852
+ * The threshold for the time difference of cursors that are assumed to be pressed
853
+ * by a single finger, in milliseconds.
854
+ */
855
+ this.cursorDistancingTimeThreshold = 1000;
856
+ /**
857
+ * The ratio threshold between non-3 finger cursors and 3-finger cursors.
858
+ *
859
+ * Increasing this number will increase detection accuracy, however
860
+ * it also increases the chance of falsely flagged plays.
861
+ */
862
+ this.threeFingerRatioThreshold = 0.01;
863
+ /**
864
+ * Extended sections of the beatmap for drag detection.
865
+ */
866
+ this.beatmapSections = [];
867
+ /**
868
+ * This threshold is used to filter out accidental taps.
869
+ *
870
+ * Increasing this number makes the filtration more sensitive, however it
871
+ * will also increase the chance of 3-fingered plays getting out from
872
+ * being flagged.
873
+ */
874
+ this.accidentalTapThreshold = 400;
875
+ /**
876
+ * A reprocessed break points to match right on object time.
877
+ *
878
+ * This is used to increase detection accuracy since break points do not start right at the
879
+ * start of the hitobject before it and do not end right at the first hitobject after it.
880
+ */
881
+ this.breakPointAccurateTimes = [];
882
+ /**
883
+ * A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
884
+ *
885
+ * Each index represents the cursor index.
886
+ */
887
+ this.downCursorInstances = [];
888
+ /**
889
+ * Nerf factors from all sections that were three-fingered.
890
+ */
891
+ this.nerfFactors = [];
1030
892
  this.beatmap = beatmap;
1031
893
  this.data = data;
1032
894
  this.difficultyAttributes = difficultyAttributes;
1033
895
  const od = osuBase.calculateDroidDifficultyStatistics({
1034
896
  overallDifficulty: beatmap.difficulty.od,
1035
- mods: this.difficultyAttributes.mods.filter((m) => m.isApplicableToDroid() &&
1036
- !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
897
+ mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
1037
898
  convertOverallDifficulty: false,
1038
899
  }).overallDifficulty;
1039
900
  this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
@@ -1196,11 +1057,7 @@ class ThreeFingerChecker {
1196
1057
  getBeatmapSections() {
1197
1058
  for (const section of this.difficultyAttributes
1198
1059
  .possibleThreeFingeredSections) {
1199
- this.beatmapSections.push({
1200
- ...section,
1201
- isDragged: false,
1202
- dragFingerIndex: -1,
1203
- });
1060
+ this.beatmapSections.push(Object.assign(Object.assign({}, section), { isDragged: false, dragFingerIndex: -1 }));
1204
1061
  }
1205
1062
  }
1206
1063
  /**
@@ -1288,6 +1145,7 @@ class ThreeFingerChecker {
1288
1145
  * @param cursorIndexes The indexes of the cursor instance that has at least an occurrence in the section.
1289
1146
  */
1290
1147
  findDragIndex(sectionObjects, sectionReplayObjectData, cursorIndexes) {
1148
+ var _a;
1291
1149
  const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
1292
1150
  for (let i = 0; i < sectionObjects.length && cursorIndexes.every((v) => v !== -1); ++i) {
1293
1151
  const object = sectionObjects[i];
@@ -1347,7 +1205,7 @@ class ThreeFingerChecker {
1347
1205
  }
1348
1206
  }
1349
1207
  }
1350
- return cursorIndexes.find((v) => v !== -1) ?? -1;
1208
+ return (_a = cursorIndexes.find((v) => v !== -1)) !== null && _a !== void 0 ? _a : -1;
1351
1209
  }
1352
1210
  /**
1353
1211
  * Attempts to prevent accidental taps from being flagged.
@@ -1402,7 +1260,7 @@ class ThreeFingerChecker {
1402
1260
  ? objectData[beatmapSection.lastObjectIndex].accuracy
1403
1261
  : this.hitWindow.hitWindowFor50(this.isPrecise));
1404
1262
  const cursorAmounts = [];
1405
- const cursorVectorTimes = [];
1263
+ const sectionCursors = [];
1406
1264
  for (let i = 0; i < this.downCursorInstances.length; ++i) {
1407
1265
  // Do not include drag cursor instance.
1408
1266
  if (i === dragIndex) {
@@ -1414,14 +1272,12 @@ class ThreeFingerChecker {
1414
1272
  if (cursors[j].time >= startTime &&
1415
1273
  cursors[j].time <= endTime) {
1416
1274
  ++amount;
1417
- cursorVectorTimes.push({
1418
- vector: new osuBase.Vector2(cursors[j].position.x, cursors[j].position.y),
1419
- time: cursors[j].time,
1420
- });
1275
+ sectionCursors.push(cursors[j]);
1421
1276
  }
1422
1277
  }
1423
1278
  cursorAmounts.push(amount);
1424
1279
  }
1280
+ sectionCursors.sort((a, b) => a.time - b.time);
1425
1281
  // This index will be used to detect if a section is 3-fingered.
1426
1282
  // If the section is dragged, the dragged instance will be ignored,
1427
1283
  // hence why the index is 1 less than nondragged section.
@@ -1435,31 +1291,31 @@ class ThreeFingerChecker {
1435
1291
  .slice(0, fingerSplitIndex)
1436
1292
  .reduce((acc, value) => acc + value, 0);
1437
1293
  const similarPresses = [];
1438
- cursorVectorTimes.sort((a, b) => a.time - b.time);
1439
- for (const cursorVectorTime of cursorVectorTimes) {
1440
- const pressIndex = similarPresses.findIndex((v) => v.vector.getDistance(cursorVectorTime.vector) <=
1294
+ for (const sectionCursor of sectionCursors) {
1295
+ const pressIndex = similarPresses.findIndex((v) => v.lastPosition.getDistance(sectionCursor.position) <=
1441
1296
  this.cursorDistancingDistanceThreshold);
1442
1297
  if (pressIndex !== -1) {
1443
- if (cursorVectorTime.time -
1298
+ if (sectionCursor.time -
1444
1299
  similarPresses[pressIndex].lastTime >=
1445
1300
  this.cursorDistancingTimeThreshold) {
1446
1301
  similarPresses.splice(pressIndex, 1);
1447
1302
  similarPresses.push({
1448
- vector: cursorVectorTime.vector,
1303
+ lastPosition: sectionCursor.position,
1449
1304
  count: 1,
1450
- lastTime: cursorVectorTime.time,
1305
+ lastTime: sectionCursor.time,
1451
1306
  });
1452
1307
  continue;
1453
1308
  }
1454
- similarPresses[pressIndex].vector = cursorVectorTime.vector;
1455
- similarPresses[pressIndex].lastTime = cursorVectorTime.time;
1309
+ similarPresses[pressIndex].lastPosition =
1310
+ sectionCursor.position;
1311
+ similarPresses[pressIndex].lastTime = sectionCursor.time;
1456
1312
  ++similarPresses[pressIndex].count;
1457
1313
  }
1458
1314
  else {
1459
1315
  similarPresses.push({
1460
- vector: cursorVectorTime.vector,
1316
+ lastPosition: sectionCursor.position,
1461
1317
  count: 1,
1462
- lastTime: cursorVectorTime.time,
1318
+ lastTime: sectionCursor.time,
1463
1319
  });
1464
1320
  }
1465
1321
  }
@@ -1519,49 +1375,6 @@ class ThreeFingerChecker {
1519
1375
  * Contains information about which cursor index hits a hitobject.
1520
1376
  */
1521
1377
  class IndexedHitObject {
1522
- /**
1523
- * The cursor index that hits the hitobject.
1524
- *
1525
- * If -1, the detection was unable to find any cursor that attempted to hit
1526
- * the hitobject or it did not meet the criteria for detection.
1527
- */
1528
- cursorIndex;
1529
- /**
1530
- * The group index of the cursor within the cursor index that hits the hitobject.
1531
- *
1532
- * If -1, the detection was unable to find any cursor that attempted to hit
1533
- * the hitobject or it did not meet the criteria for detection.
1534
- */
1535
- groupIndex;
1536
- /**
1537
- * The occurrence index within the group of the cursor within the cursor index that hits the hitobject.
1538
- *
1539
- * If -1, the detection was unable to find any cursor that attempted to hit
1540
- * the hitobject or it did not meet the criteria for detection.
1541
- */
1542
- occurrenceIndex;
1543
- /**
1544
- * The angle of the movement of the cursor towards the next hitobject.
1545
- */
1546
- angle;
1547
- /**
1548
- * If this is a slider, whether the slider was cheesed.
1549
- */
1550
- sliderCheesed = false;
1551
- /**
1552
- * The underlying difficulty hitobject.
1553
- */
1554
- object;
1555
- /**
1556
- * The position of the cursor at the end of this hitobject.
1557
- *
1558
- * Will be altered during detection.
1559
- */
1560
- endCursorPosition;
1561
- /**
1562
- * Whether the hitobject is likely two-handed.
1563
- */
1564
- is2Handed;
1565
1378
  /**
1566
1379
  * @param object The underlying difficulty hitobject.
1567
1380
  * @param cursorIndex The cursor index that moves towards the hitobject.
@@ -1570,6 +1383,10 @@ class IndexedHitObject {
1570
1383
  * @param angle The angle of the movement of the cursor that moves towards the hitobject.
1571
1384
  */
1572
1385
  constructor(object, cursorIndex, groupIndex, occurrenceIndex, angle, is2Handed) {
1386
+ /**
1387
+ * If this is a slider, whether the slider was cheesed.
1388
+ */
1389
+ this.sliderCheesed = false;
1573
1390
  this.object = object;
1574
1391
  this.cursorIndex = cursorIndex;
1575
1392
  this.groupIndex = groupIndex;
@@ -1585,38 +1402,21 @@ class IndexedHitObject {
1585
1402
  * Utility to check whether or not a beatmap is two-handed.
1586
1403
  */
1587
1404
  class TwoHandChecker {
1588
- /**
1589
- * The difficulty calculator that is being analyzed.
1590
- */
1591
- calculator;
1592
- /**
1593
- * The data of the replay.
1594
- */
1595
- data;
1596
- /**
1597
- * The hitobjects of the beatmap that have been assigned with their respective cursor index.
1598
- */
1599
- indexedHitObjects = [];
1600
- /**
1601
- * The osu!droid hitwindow of the analyzed beatmap.
1602
- */
1603
- hitWindow;
1604
- /**
1605
- * The 50 osu!droid hit window of the analyzed beatmap.
1606
- */
1607
- hitWindow50;
1608
1405
  // private csvString: string;
1609
1406
  /**
1610
1407
  * @param calculator The difficulty calculator to analyze.
1611
1408
  * @param data The data of the replay.
1612
1409
  */
1613
1410
  constructor(calculator, data) {
1411
+ /**
1412
+ * The hitobjects of the beatmap that have been assigned with their respective cursor index.
1413
+ */
1414
+ this.indexedHitObjects = [];
1614
1415
  this.calculator = calculator;
1615
1416
  this.data = data;
1616
1417
  const od = osuBase.calculateDroidDifficultyStatistics({
1617
1418
  overallDifficulty: calculator.beatmap.difficulty.od,
1618
- mods: calculator.mods.filter((m) => m.isApplicableToDroid() &&
1619
- !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
1419
+ mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
1620
1420
  convertOverallDifficulty: false,
1621
1421
  }).overallDifficulty;
1622
1422
  this.hitWindow = new osuBase.DroidHitWindow(od);
@@ -2197,22 +1997,6 @@ class TwoHandChecker {
2197
1997
  * Utility to check whether relevant sliders in a beatmap are cheesed.
2198
1998
  */
2199
1999
  class SliderCheeseChecker {
2200
- /**
2201
- * The beatmap that is being analyzed.
2202
- */
2203
- beatmap;
2204
- /**
2205
- * The data of the replay.
2206
- */
2207
- data;
2208
- /**
2209
- * The difficulty attributes of the beatmap.
2210
- */
2211
- difficultyAttributes;
2212
- /**
2213
- * The 50 osu!droid hit window of the analyzed beatmap.
2214
- */
2215
- hitWindow50;
2216
2000
  /**
2217
2001
  * @param beatmap The beatmap to analyze.
2218
2002
  * @param data The data of the replay.
@@ -2224,8 +2008,7 @@ class SliderCheeseChecker {
2224
2008
  this.difficultyAttributes = difficultyAttributes;
2225
2009
  const od = osuBase.calculateDroidDifficultyStatistics({
2226
2010
  overallDifficulty: beatmap.difficulty.od,
2227
- mods: this.difficultyAttributes.mods.filter((m) => m.isApplicableToDroid() &&
2228
- !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
2011
+ mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
2229
2012
  convertOverallDifficulty: false,
2230
2013
  }).overallDifficulty;
2231
2014
  this.hitWindow50 = new osuBase.DroidHitWindow(od).hitWindowFor50(this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise));
@@ -2427,78 +2210,57 @@ class SliderCheeseChecker {
2427
2210
  * Once analyzed, the result can be accessed via the `data` property.
2428
2211
  */
2429
2212
  class ReplayAnalyzer {
2430
- /**
2431
- * The score ID of the replay.
2432
- */
2433
- scoreID;
2434
- /**
2435
- * The original odr file of the replay.
2436
- */
2437
- originalODR = null;
2438
- /**
2439
- * The fixed odr file of the replay.
2440
- */
2441
- fixedODR = null;
2442
- /**
2443
- * Whether or not the play is considered using >=3 finger abuse.
2444
- */
2445
- is3Finger;
2446
- /**
2447
- * Whether or not the play is considered 2-handed.
2448
- */
2449
- is2Hand;
2450
- /**
2451
- * The beatmap that is being analyzed. `DroidDifficultyCalculator` or `RebalanceDroidDifficultyCalculator` is required for three finger or two hand analyzing.
2452
- */
2453
- beatmap;
2454
- /**
2455
- * The difficulty attributes of the beatmap.
2456
- */
2457
- difficultyAttributes;
2458
- /**
2459
- * The results of the analyzer. `null` when initialized.
2460
- */
2461
- data = null;
2462
- /**
2463
- * Penalty value used to penalize dpp for 2-hand.
2464
- */
2465
- aimPenalty = 1;
2466
- /**
2467
- * Penalty value used to penalize dpp for 3 finger abuse.
2468
- */
2469
- tapPenalty = 1;
2470
- /**
2471
- * Penalty values used to penalize dpp for slider cheesing.
2472
- */
2473
- sliderCheesePenalty = {
2474
- aimPenalty: 1,
2475
- flashlightPenalty: 1,
2476
- visualPenalty: 1,
2477
- };
2478
- /**
2479
- * Whether this replay has been checked against 3 finger usage.
2480
- */
2481
- hasBeenCheckedFor3Finger = false;
2482
- /**
2483
- * Whether this replay has been checked against 2 hand usage.
2484
- */
2485
- hasBeenCheckedFor2Hand = false;
2486
- /**
2487
- * Whether this replay has been checked against slider cheesing.
2488
- */
2489
- hasBeenCheckedForSliderCheesing = false;
2490
- /**
2491
- * The amount of two-handed objects.
2492
- */
2493
- twoHandedNoteCount = 0;
2494
- convertedBeatmap;
2495
- // Sizes of primitive data types in Java (in bytes)
2496
- BYTE_LENGTH = 1;
2497
- SHORT_LENGTH = 2;
2498
- INT_LENGTH = 4;
2499
- FLOAT_LENGTH = 4;
2500
- LONG_LENGTH = 8;
2501
2213
  constructor(values) {
2214
+ /**
2215
+ * The original odr file of the replay.
2216
+ */
2217
+ this.originalODR = null;
2218
+ /**
2219
+ * The fixed odr file of the replay.
2220
+ */
2221
+ this.fixedODR = null;
2222
+ /**
2223
+ * The results of the analyzer. `null` when initialized.
2224
+ */
2225
+ this.data = null;
2226
+ /**
2227
+ * Penalty value used to penalize dpp for 2-hand.
2228
+ */
2229
+ this.aimPenalty = 1;
2230
+ /**
2231
+ * Penalty value used to penalize dpp for 3 finger abuse.
2232
+ */
2233
+ this.tapPenalty = 1;
2234
+ /**
2235
+ * Penalty values used to penalize dpp for slider cheesing.
2236
+ */
2237
+ this.sliderCheesePenalty = {
2238
+ aimPenalty: 1,
2239
+ flashlightPenalty: 1,
2240
+ visualPenalty: 1,
2241
+ };
2242
+ /**
2243
+ * Whether this replay has been checked against 3 finger usage.
2244
+ */
2245
+ this.hasBeenCheckedFor3Finger = false;
2246
+ /**
2247
+ * Whether this replay has been checked against 2 hand usage.
2248
+ */
2249
+ this.hasBeenCheckedFor2Hand = false;
2250
+ /**
2251
+ * Whether this replay has been checked against slider cheesing.
2252
+ */
2253
+ this.hasBeenCheckedForSliderCheesing = false;
2254
+ /**
2255
+ * The amount of two-handed objects.
2256
+ */
2257
+ this.twoHandedNoteCount = 0;
2258
+ // Sizes of primitive data types in Java (in bytes)
2259
+ this.BYTE_LENGTH = 1;
2260
+ this.SHORT_LENGTH = 2;
2261
+ this.INT_LENGTH = 4;
2262
+ this.FLOAT_LENGTH = 4;
2263
+ this.LONG_LENGTH = 8;
2502
2264
  this.scoreID = values.scoreID;
2503
2265
  this.beatmap = values.map;
2504
2266
  this.difficultyAttributes = values.difficultyAttributes;
@@ -2509,35 +2271,39 @@ class ReplayAnalyzer {
2509
2271
  /**
2510
2272
  * Analyzes a replay.
2511
2273
  */
2512
- async analyze() {
2513
- if (!this.originalODR && !this.fixedODR) {
2514
- this.originalODR = await this.downloadReplay();
2515
- }
2516
- if (!this.originalODR) {
2517
- return this;
2518
- }
2519
- if (!this.fixedODR) {
2520
- this.fixedODR = await this.decompress().catch(() => null);
2521
- }
2522
- if (!this.fixedODR) {
2274
+ analyze() {
2275
+ return __awaiter(this, void 0, void 0, function* () {
2276
+ if (!this.originalODR && !this.fixedODR) {
2277
+ this.originalODR = yield this.downloadReplay();
2278
+ }
2279
+ if (!this.originalODR) {
2280
+ return this;
2281
+ }
2282
+ if (!this.fixedODR) {
2283
+ this.fixedODR = yield this.decompress().catch(() => null);
2284
+ }
2285
+ if (!this.fixedODR) {
2286
+ return this;
2287
+ }
2288
+ this.parseReplay();
2523
2289
  return this;
2524
- }
2525
- this.parseReplay();
2526
- return this;
2290
+ });
2527
2291
  }
2528
2292
  /**
2529
2293
  * Downloads the given score ID's replay.
2530
2294
  */
2531
- async downloadReplay() {
2532
- const apiRequestBuilder = new osuBase.DroidAPIRequestBuilder()
2533
- .setRequireAPIkey(false)
2534
- .setEndpoint("upload")
2535
- .addParameter("", `${this.scoreID}.odr`);
2536
- const result = await apiRequestBuilder.sendRequest();
2537
- if (result.statusCode !== 200) {
2538
- return null;
2539
- }
2540
- return result.data;
2295
+ downloadReplay() {
2296
+ return __awaiter(this, void 0, void 0, function* () {
2297
+ const apiRequestBuilder = new osuBase.DroidAPIRequestBuilder()
2298
+ .setRequireAPIkey(false)
2299
+ .setEndpoint("upload")
2300
+ .addParameter("", `${this.scoreID}.odr`);
2301
+ const result = yield apiRequestBuilder.sendRequest();
2302
+ if (result.statusCode !== 200) {
2303
+ return null;
2304
+ }
2305
+ return result.data;
2306
+ });
2541
2307
  }
2542
2308
  /**
2543
2309
  * Decompresses a replay.
@@ -2551,15 +2317,15 @@ class ReplayAnalyzer {
2551
2317
  stream.push(null);
2552
2318
  stream
2553
2319
  .pipe(unzipper.Parse())
2554
- .on("entry", async (entry) => {
2320
+ .on("entry", (entry) => __awaiter(this, void 0, void 0, function* () {
2555
2321
  const fileName = entry.path;
2556
2322
  if (fileName === "data") {
2557
- return resolve(await entry.buffer());
2323
+ return resolve(yield entry.buffer());
2558
2324
  }
2559
2325
  else {
2560
2326
  entry.autodrain();
2561
2327
  }
2562
- })
2328
+ }))
2563
2329
  .on("error", (e) => {
2564
2330
  setTimeout(() => reject(e), 2000);
2565
2331
  });
@@ -2569,6 +2335,7 @@ class ReplayAnalyzer {
2569
2335
  * Parses a replay after being downloaded and converted to a buffer.
2570
2336
  */
2571
2337
  parseReplay() {
2338
+ var _a, _b, _c;
2572
2339
  if (!this.fixedODR) {
2573
2340
  return;
2574
2341
  }
@@ -2579,7 +2346,7 @@ class ReplayAnalyzer {
2579
2346
  try {
2580
2347
  rawObject = javaDeserialization__namespace.parse(this.fixedODR);
2581
2348
  }
2582
- catch {
2349
+ catch (_d) {
2583
2350
  return;
2584
2351
  }
2585
2352
  const resultObject = {
@@ -2593,11 +2360,12 @@ class ReplayAnalyzer {
2593
2360
  rank: "",
2594
2361
  };
2595
2362
  const determineRank = () => {
2363
+ var _a, _b;
2596
2364
  const totalHits = resultObject.accuracy.n300 +
2597
2365
  resultObject.accuracy.n100 +
2598
2366
  resultObject.accuracy.n50 +
2599
2367
  resultObject.accuracy.nmiss;
2600
- const isHidden = resultObject.convertedMods?.some((m) => m instanceof osuBase.ModHidden || m instanceof osuBase.ModFlashlight) ?? false;
2368
+ const isHidden = (_b = (_a = resultObject.convertedMods) === null || _a === void 0 ? void 0 : _a.some((m) => m instanceof osuBase.ModHidden || m instanceof osuBase.ModFlashlight)) !== null && _b !== void 0 ? _b : false;
2601
2369
  const hit300Ratio = resultObject.accuracy.n300 / totalHits;
2602
2370
  switch (true) {
2603
2371
  case resultObject.accuracy.value() === 1:
@@ -2780,10 +2548,10 @@ class ReplayAnalyzer {
2780
2548
  }
2781
2549
  // Parse max combo, hit results, and accuracy in old replay version
2782
2550
  if (resultObject.replayVersion < 3) {
2783
- const objects = (this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
2551
+ const objects = (_a = (this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
2784
2552
  this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
2785
2553
  ? this.beatmap.beatmap
2786
- : this.beatmap)?.hitObjects.objects;
2554
+ : this.beatmap)) === null || _a === void 0 ? void 0 : _a.hitObjects.objects;
2787
2555
  let grantsGekiOrKatu = true;
2788
2556
  for (let i = 0; i < resultObject.hitObjectData.length; ++i) {
2789
2557
  // Hit result
@@ -2805,14 +2573,14 @@ class ReplayAnalyzer {
2805
2573
  case exports.HitResult.good:
2806
2574
  ++resultObject.accuracy.n100;
2807
2575
  if (grantsGekiOrKatu && isNextNewCombo) {
2808
- resultObject.hit100k ??= 0;
2576
+ (_b = resultObject.hit100k) !== null && _b !== void 0 ? _b : (resultObject.hit100k = 0);
2809
2577
  ++resultObject.hit100k;
2810
2578
  }
2811
2579
  break;
2812
2580
  case exports.HitResult.great:
2813
2581
  ++resultObject.accuracy.n300;
2814
2582
  if (grantsGekiOrKatu && isNextNewCombo) {
2815
- resultObject.hit300k ??= 0;
2583
+ (_c = resultObject.hit300k) !== null && _c !== void 0 ? _c : (resultObject.hit300k = 0);
2816
2584
  ++resultObject.hit300k;
2817
2585
  }
2818
2586
  break;
@@ -2846,7 +2614,7 @@ class ReplayAnalyzer {
2846
2614
  const { objects } = beatmap.hitObjects;
2847
2615
  const od = osuBase.calculateDroidDifficultyStatistics({
2848
2616
  overallDifficulty: beatmap.difficulty.od,
2849
- mods: this.data.convertedMods.filter((m) => !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
2617
+ mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
2850
2618
  }).overallDifficulty;
2851
2619
  const hitWindow50 = new osuBase.DroidHitWindow(od).hitWindowFor50(this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise));
2852
2620
  // The accuracy of sliders is set to (50 hit window)ms + 13ms if their head was not hit:
@@ -2923,6 +2691,7 @@ class ReplayAnalyzer {
2923
2691
  * Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
2924
2692
  */
2925
2693
  checkFor3Finger() {
2694
+ var _a;
2926
2695
  if (!this.beatmap || !this.data || !this.difficultyAttributes) {
2927
2696
  return;
2928
2697
  }
@@ -2941,13 +2710,13 @@ class ReplayAnalyzer {
2941
2710
  hp: this.data.forceHP,
2942
2711
  }));
2943
2712
  }
2944
- this.convertedBeatmap ??= new osuBase.BeatmapConverter(this.beatmap instanceof osuBase.Beatmap
2713
+ (_a = this.convertedBeatmap) !== null && _a !== void 0 ? _a : (this.convertedBeatmap = new osuBase.BeatmapConverter(this.beatmap instanceof osuBase.Beatmap
2945
2714
  ? this.beatmap
2946
2715
  : this.beatmap.beatmap).convert({
2947
2716
  mode: osuBase.Modes.droid,
2948
2717
  mods: mods,
2949
2718
  customSpeedMultiplier: this.data.speedMultiplier,
2950
- });
2719
+ }));
2951
2720
  }
2952
2721
  const threeFingerChecker = this.difficultyAttributes.mode === "rebalance"
2953
2722
  ? new RebalanceThreeFingerChecker(this.convertedBeatmap, this.data, this.difficultyAttributes)
@@ -2980,6 +2749,7 @@ class ReplayAnalyzer {
2980
2749
  * Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
2981
2750
  */
2982
2751
  checkForSliderCheesing() {
2752
+ var _a;
2983
2753
  if (!this.beatmap || !this.data || !this.difficultyAttributes) {
2984
2754
  return;
2985
2755
  }
@@ -2998,13 +2768,13 @@ class ReplayAnalyzer {
2998
2768
  hp: this.data.forceHP,
2999
2769
  }));
3000
2770
  }
3001
- this.convertedBeatmap ??= new osuBase.BeatmapConverter(this.beatmap instanceof osuBase.Beatmap
2771
+ (_a = this.convertedBeatmap) !== null && _a !== void 0 ? _a : (this.convertedBeatmap = new osuBase.BeatmapConverter(this.beatmap instanceof osuBase.Beatmap
3002
2772
  ? this.beatmap
3003
2773
  : this.beatmap.beatmap).convert({
3004
2774
  mode: osuBase.Modes.droid,
3005
2775
  mods: mods,
3006
2776
  customSpeedMultiplier: this.data.speedMultiplier,
3007
- });
2777
+ }));
3008
2778
  }
3009
2779
  const sliderCheeseChecker = new SliderCheeseChecker(this.convertedBeatmap, this.data, this.difficultyAttributes);
3010
2780
  this.sliderCheesePenalty = sliderCheeseChecker.check();
@@ -3020,33 +2790,6 @@ class ReplayAnalyzer {
3020
2790
  * This is used when analyzing replays using replay analyzer.
3021
2791
  */
3022
2792
  class ReplayObjectData {
3023
- /**
3024
- * For circles, this is the offset at which the circle was hit. If the hit accuracy is 10000, it means the circle was tapped too late ([game source code](https://github.com/osudroid/osu-droid/blob/6306c68e3ffaf671eac794bf45cc95c0f3313a82/src/ru/nsu/ccfit/zuev/osu/game/HitCircle.java#L298-L306)).
3025
- *
3026
- * For sliders, this is the offset at which the slider head was hit. For
3027
- * sliderbreaks, the accuracy would be `Math.floor(<hit window 50>ms) + 13ms` ([game source code](https://github.com/osudroid/osu-droid/blob/6306c68e3ffaf671eac794bf45cc95c0f3313a82/src/ru/nsu/ccfit/zuev/osu/game/Slider.java#L821)).
3028
- *
3029
- * For spinners, this is the total amount at which the spinner was spinned:
3030
- * ```js
3031
- * const rotations = Math.floor(data.accuracy / 4);
3032
- * ```
3033
- * The remainder of the division denotes the hit result of the spinner:
3034
- * - `HitResult.great`: 3
3035
- * - `HitResult.good`: 2
3036
- * - `HitResult.meh`: 1
3037
- * - `HitResult.miss`: 0
3038
- */
3039
- accuracy;
3040
- /**
3041
- * The tickset of the hitobject.
3042
- *
3043
- * This is used to determine whether or not a slider event (tick, repeat, and end) is hit based on the order they appear.
3044
- */
3045
- tickset;
3046
- /**
3047
- * The bitwise hit result of the hitobject.
3048
- */
3049
- result;
3050
2793
  constructor(values) {
3051
2794
  this.accuracy = values.accuracy;
3052
2795
  this.tickset = values.tickset;