@rian8337/osu-droid-replay-analyzer 4.0.0-beta.53 → 4.0.0-beta.55

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
@@ -1,11 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  var osuBase = require('@rian8337/osu-base');
4
- var osuDifficultyCalculator = require('@rian8337/osu-difficulty-calculator');
5
- var osuRebalanceDifficultyCalculator = require('@rian8337/osu-rebalance-difficulty-calculator');
6
- var unzipper = require('unzipper');
7
4
  var javaDeserialization = require('java-deserialization');
8
5
  var node_stream = require('node:stream');
6
+ var unzipper = require('unzipper');
9
7
 
10
8
  function _interopNamespaceDefault(e) {
11
9
  var n = Object.create(null);
@@ -747,42 +745,223 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
747
745
  };
748
746
 
749
747
  /**
750
- * Represents a replay data in an osu!droid replay version 1 and 2.
751
- *
752
- * Stores generic information about an osu!droid replay.
753
- *
754
- * This is used when analyzing replays using replay analyzer.
748
+ * Utility to check whether relevant sliders in a beatmap are cheesed.
755
749
  */
756
- class ReplayData {
757
- constructor(values) {
758
- this.replayVersion = values.replayVersion;
759
- this.folderName = values.folderName;
760
- this.fileName = values.fileName;
761
- this.hash = values.hash;
762
- this.accuracy = values.accuracy;
763
- this.rank = values.rank;
764
- this.hit300k = values.hit300k;
765
- this.hit100k = values.hit100k;
766
- this.cursorMovement = values.cursorMovement;
767
- this.hitObjectData = values.hitObjectData;
750
+ class SliderCheeseChecker {
751
+ /**
752
+ * @param beatmap The beatmap to analyze.
753
+ * @param data The data of the replay.
754
+ * @param difficultyAttributes The difficulty attributes of the beatmap.
755
+ */
756
+ constructor(beatmap, data, difficultyAttributes) {
757
+ this.beatmap = beatmap;
758
+ this.data = data;
759
+ this.difficultyAttributes = difficultyAttributes;
760
+ this.hitWindow50 = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise)
761
+ ? new osuBase.PreciseDroidHitWindow(beatmap.difficulty.od).mehWindow
762
+ : new osuBase.DroidHitWindow(beatmap.difficulty.od).mehWindow;
763
+ this.isHardRock = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHardRock);
768
764
  }
769
765
  /**
770
- * Whether the replay's version is 3 or later.
766
+ * Checks if relevant sliders in the given beatmap was cheesed.
771
767
  */
772
- isReplayV3() {
773
- return this.replayVersion >= 3;
768
+ check() {
769
+ if (this.difficultyAttributes.difficultSliders.length === 0 ||
770
+ (this.difficultyAttributes.sliderFactor === 1 &&
771
+ this.difficultyAttributes.flashlightSliderFactor === 1 &&
772
+ this.difficultyAttributes.visualSliderFactor === 1)) {
773
+ return {
774
+ aimPenalty: 1,
775
+ flashlightPenalty: 1,
776
+ visualPenalty: 1,
777
+ };
778
+ }
779
+ const cheesedDifficultyRatings = this.checkSliderCheesing();
780
+ return this.calculateSliderCheesePenalty(cheesedDifficultyRatings);
774
781
  }
775
782
  /**
776
- * Whether the replay's version is 4 or later.
783
+ * Checks for sliders that were cheesed.
777
784
  */
778
- isReplayV4() {
779
- return this.replayVersion >= 4;
785
+ checkSliderCheesing() {
786
+ const { objects } = this.beatmap.hitObjects;
787
+ const cheesedDifficultyRatings = [];
788
+ // Current loop indices are stored for efficiency.
789
+ const cursorLoopIndices = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
790
+ const acceptableRadius = objects[0].radius * 2;
791
+ // Sort difficult sliders by index so that cursor loop indices work properly.
792
+ for (const difficultSlider of this.difficultyAttributes.difficultSliders
793
+ .slice()
794
+ .sort((a, b) => a.index - b.index)) {
795
+ if (difficultSlider.index >= this.data.hitObjectData.length) {
796
+ continue;
797
+ }
798
+ const object = objects[difficultSlider.index];
799
+ const objectData = this.data.hitObjectData[difficultSlider.index];
800
+ // If a miss or slider break occurs, we disregard the check for that slider.
801
+ if (objectData.result === exports.HitResult.miss ||
802
+ -this.hitWindow50 > objectData.accuracy ||
803
+ objectData.accuracy >
804
+ Math.min(this.hitWindow50, object.duration)) {
805
+ continue;
806
+ }
807
+ const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
808
+ // These time boundaries should consider the delta time between the previous and next
809
+ // object as well as their hit accuracy. However, they are somewhat complicated to
810
+ // compute and the accuracy gain is small. As such, let's settle with 50 hit window.
811
+ const minTimeLimit = object.startTime - this.hitWindow50;
812
+ const maxTimeLimit = object.startTime + this.hitWindow50;
813
+ // Get the closest tap distance across all cursors.
814
+ const closestDistances = [];
815
+ const closestGroupIndices = [];
816
+ for (let i = 0; i < this.data.cursorMovement.length; ++i) {
817
+ const cursorGroups = this.data.cursorMovement[i].occurrenceGroups;
818
+ let closestDistance = Number.POSITIVE_INFINITY;
819
+ let closestIndex = cursorGroups.length;
820
+ for (let j = cursorLoopIndices[i]; j < cursorGroups.length; j = ++cursorLoopIndices[i]) {
821
+ const group = cursorGroups[j];
822
+ if (group.endTime < minTimeLimit) {
823
+ continue;
824
+ }
825
+ if (group.startTime > maxTimeLimit) {
826
+ break;
827
+ }
828
+ if (group.startTime >= minTimeLimit) {
829
+ const position = this.getCursorPosition(group.down);
830
+ const distance = position.getDistance(objectStartPosition);
831
+ if (closestDistance > distance) {
832
+ closestDistance = distance;
833
+ closestIndex = j;
834
+ }
835
+ if (closestDistance <= acceptableRadius / 2) {
836
+ break;
837
+ }
838
+ }
839
+ // Normally, we check if there are cursor presses within the group's active time.
840
+ // However, some funky workarounds are used throughout the game for replays, so
841
+ // for the time being we only check for cursor distances across the group.
842
+ const { allOccurrences } = group;
843
+ for (let k = 1; k < allOccurrences.length; ++k) {
844
+ const cursor = allOccurrences[k];
845
+ const prevCursor = allOccurrences[k - 1];
846
+ let distance = Number.POSITIVE_INFINITY;
847
+ const currentPosition = this.getCursorPosition(cursor);
848
+ const prevPosition = this.getCursorPosition(prevCursor);
849
+ switch (cursor.id) {
850
+ case exports.MovementType.up:
851
+ distance =
852
+ prevPosition.getDistance(objectStartPosition);
853
+ break;
854
+ case exports.MovementType.move:
855
+ for (let mSecPassed = Math.max(prevCursor.time, minTimeLimit); mSecPassed <=
856
+ Math.min(cursor.time, maxTimeLimit); ++mSecPassed) {
857
+ const t = (mSecPassed - prevCursor.time) /
858
+ (cursor.time - prevCursor.time);
859
+ const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
860
+ distance =
861
+ cursorPosition.getDistance(objectStartPosition);
862
+ if (closestDistance > distance) {
863
+ closestDistance = distance;
864
+ closestIndex = j;
865
+ }
866
+ if (closestDistance <=
867
+ acceptableRadius / 2) {
868
+ break;
869
+ }
870
+ }
871
+ }
872
+ if (closestDistance > distance) {
873
+ closestDistance = distance;
874
+ closestIndex = j;
875
+ }
876
+ if (closestDistance <= acceptableRadius / 2) {
877
+ break;
878
+ }
879
+ }
880
+ }
881
+ closestDistances.push(closestDistance);
882
+ closestGroupIndices.push(closestIndex);
883
+ if (cursorLoopIndices[i] > 0) {
884
+ // Decrement the index. The previous group may also have a role on the next slider.
885
+ --cursorLoopIndices[i];
886
+ }
887
+ }
888
+ const cursorIndex = closestDistances.indexOf(Math.min(...closestDistances));
889
+ const closestDistance = closestDistances[cursorIndex];
890
+ if (closestDistance > acceptableRadius / 2) {
891
+ cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
892
+ continue;
893
+ }
894
+ const group = this.data.cursorMovement[cursorIndex].occurrenceGroups[closestGroupIndices[cursorIndex]];
895
+ let isCheesed = false;
896
+ // Track cursor movement to see if it lands on every tick.
897
+ let occurrenceLoopIndex = 1;
898
+ const { allOccurrences } = group;
899
+ for (let i = 1; i < object.nestedHitObjects.length; ++i) {
900
+ if (isCheesed) {
901
+ break;
902
+ }
903
+ const tickWasHit = objectData.tickset[i - 1];
904
+ if (!tickWasHit) {
905
+ continue;
906
+ }
907
+ const nestedObject = object.nestedHitObjects[i];
908
+ const nestedPosition = nestedObject.getStackedPosition(osuBase.Modes.droid);
909
+ while (occurrenceLoopIndex < allOccurrences.length &&
910
+ allOccurrences[occurrenceLoopIndex].time <
911
+ nestedObject.startTime) {
912
+ ++occurrenceLoopIndex;
913
+ }
914
+ if (occurrenceLoopIndex === allOccurrences.length) {
915
+ continue;
916
+ }
917
+ const cursor = allOccurrences[occurrenceLoopIndex];
918
+ const prevCursor = allOccurrences[occurrenceLoopIndex - 1];
919
+ const currentPosition = this.getCursorPosition(cursor);
920
+ const prevPosition = this.getCursorPosition(prevCursor);
921
+ switch (cursor.id) {
922
+ case exports.MovementType.move: {
923
+ // Interpolate cursor position during nested object time.
924
+ const t = (nestedObject.startTime - prevCursor.time) /
925
+ (cursor.time - prevCursor.time);
926
+ const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
927
+ const distance = cursorPosition.getDistance(nestedPosition);
928
+ isCheesed = distance > acceptableRadius;
929
+ break;
930
+ }
931
+ case exports.MovementType.up:
932
+ isCheesed =
933
+ prevPosition.getDistance(nestedPosition) >
934
+ acceptableRadius;
935
+ }
936
+ }
937
+ if (isCheesed) {
938
+ cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
939
+ }
940
+ }
941
+ return cheesedDifficultyRatings;
780
942
  }
781
943
  /**
782
- * Whether the replay's version is 5 or later.
944
+ * Calculates the slider cheese penalty.
783
945
  */
784
- isReplayV5() {
785
- return this.replayVersion >= 5;
946
+ calculateSliderCheesePenalty(cheesedDifficultyRatings) {
947
+ const summedDifficultyRating = Math.min(1, cheesedDifficultyRatings.reduce((a, v) => a + v, 0));
948
+ return {
949
+ aimPenalty: Math.max(this.difficultyAttributes.sliderFactor, Math.pow(1 -
950
+ summedDifficultyRating *
951
+ this.difficultyAttributes.sliderFactor, 2)),
952
+ flashlightPenalty: Math.max(this.difficultyAttributes.flashlightSliderFactor, Math.pow(1 -
953
+ summedDifficultyRating *
954
+ this.difficultyAttributes.flashlightSliderFactor, 2)),
955
+ visualPenalty: Math.max(this.difficultyAttributes.visualSliderFactor, Math.pow(1 -
956
+ summedDifficultyRating *
957
+ this.difficultyAttributes.visualSliderFactor, 2)),
958
+ };
959
+ }
960
+ getCursorPosition(cursor) {
961
+ if (this.isHardRock) {
962
+ return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
963
+ }
964
+ return cursor.position;
786
965
  }
787
966
  }
788
967
 
@@ -1188,7 +1367,7 @@ class ThreeFingerChecker {
1188
1367
  */
1189
1368
  class IndexedHitObject {
1190
1369
  /**
1191
- * @param object The underlying difficulty hitobject.
1370
+ * @param object The underlying hitobject.
1192
1371
  * @param cursorIndex The cursor index that moves towards the hitobject.
1193
1372
  * @param groupIndex The group index of the cursor within the cursor index that hits the hitobject.
1194
1373
  * @param occurrenceIndex The occurrence index within the group of the cursor within the cursor index that hits the hitobject.
@@ -1205,7 +1384,7 @@ class IndexedHitObject {
1205
1384
  this.occurrenceIndex = occurrenceIndex;
1206
1385
  this.angle = angle;
1207
1386
  this.is2Handed = is2Handed;
1208
- this.endCursorPosition = this.object.object.getStackedEndPosition(osuBase.Modes.droid);
1387
+ this.endCursorPosition = this.object.getStackedEndPosition(osuBase.Modes.droid);
1209
1388
  }
1210
1389
  }
1211
1390
 
@@ -1216,22 +1395,24 @@ class IndexedHitObject {
1216
1395
  class TwoHandChecker {
1217
1396
  // private csvString: string;
1218
1397
  /**
1219
- * @param calculator The difficulty calculator to analyze.
1398
+ * @param beatmap The beatmap to analyze.
1399
+ * @param attributes The difficulty attributes to analyze.
1220
1400
  * @param data The data of the replay.
1221
1401
  */
1222
- constructor(calculator, data) {
1402
+ constructor(beatmap, attributes, data) {
1223
1403
  /**
1224
1404
  * The hitobjects of the beatmap that have been assigned with their respective cursor index.
1225
1405
  */
1226
1406
  this.indexedHitObjects = [];
1227
- this.calculator = calculator;
1407
+ this.beatmap = beatmap;
1408
+ this.attributes = attributes;
1228
1409
  this.data = data;
1229
- const greatWindow = new osuBase.OsuHitWindow(calculator.attributes.overallDifficulty)
1230
- .greatWindow * calculator.attributes.clockRate;
1231
- this.hitWindow = calculator.mods.some((m) => m instanceof osuBase.ModPrecise)
1410
+ const greatWindow = new osuBase.OsuHitWindow(attributes.overallDifficulty).greatWindow *
1411
+ attributes.clockRate;
1412
+ this.hitWindow = attributes.mods.some((m) => m instanceof osuBase.ModPrecise)
1232
1413
  ? new osuBase.PreciseDroidHitWindow(osuBase.PreciseDroidHitWindow.greatWindowToOD(greatWindow))
1233
1414
  : new osuBase.DroidHitWindow(osuBase.DroidHitWindow.greatWindowToOD(greatWindow));
1234
- this.isHardRock = calculator.mods.some((m) => m instanceof osuBase.ModHardRock);
1415
+ this.isHardRock = attributes.mods.some((m) => m instanceof osuBase.ModHardRock);
1235
1416
  // this.csvString = `Mods,${
1236
1417
  // data.convertedMods.reduce((a, m) => a + m.acronym, "") || "NM"
1237
1418
  // }\nCombo,${data.maxCombo}\nAccuracy,"${(
@@ -1291,26 +1472,36 @@ class TwoHandChecker {
1291
1472
  // ),
1292
1473
  // this.csvString
1293
1474
  // );
1294
- let twoHandedNoteCount = 0;
1295
- const maxStrain = Math.max(...this.calculator.objects.map((v) => v.aimStrainWithSliders));
1296
- if (maxStrain) {
1297
- twoHandedNoteCount = this.indexedHitObjects.reduce((total, object) => {
1298
- if (!object.is2Handed) {
1299
- return total;
1300
- }
1301
- return (total +
1302
- 1 /
1303
- (1 +
1304
- Math.exp(-((object.object.aimStrainWithSliders /
1305
- maxStrain) *
1306
- 12 -
1307
- 6))));
1308
- }, 0);
1309
- }
1475
+ // let twoHandedNoteCount = 0;
1476
+ // const maxStrain = Math.max(
1477
+ // ...this.attributes.objects.map((v) => v.aimStrainWithSliders),
1478
+ // );
1479
+ // if (maxStrain) {
1480
+ // twoHandedNoteCount = this.indexedHitObjects.reduce(
1481
+ // (total, object) => {
1482
+ // if (!object.is2Handed) {
1483
+ // return total;
1484
+ // }
1485
+ // return (
1486
+ // total +
1487
+ // 1 /
1488
+ // (1 +
1489
+ // Math.exp(
1490
+ // -(
1491
+ // (object.object.aimStrainWithSliders /
1492
+ // maxStrain) *
1493
+ // 12 -
1494
+ // 6
1495
+ // ),
1496
+ // ))
1497
+ // );
1498
+ // },
1499
+ // 0,
1500
+ // );
1501
+ // }
1310
1502
  return {
1311
- is2Hand: twoHandedNoteCount >
1312
- this.calculator.attributes.aimNoteCount * 0.15,
1313
- twoHandedNoteCount: twoHandedNoteCount,
1503
+ is2Hand: 0 > this.attributes.aimNoteCount * 0.15,
1504
+ twoHandedNoteCount: 0,
1314
1505
  };
1315
1506
  }
1316
1507
  /**
@@ -1318,8 +1509,7 @@ class TwoHandChecker {
1318
1509
  */
1319
1510
  indexHitObjects() {
1320
1511
  const indexes = [];
1321
- for (let i = 0; i <
1322
- Math.min(this.data.hitObjectData.length, this.calculator.objects.length); ++i) {
1512
+ for (let i = 0; i < this.data.hitObjectData.length; ++i) {
1323
1513
  const indexedHitObject = this.getIndexedHitObject(i);
1324
1514
  indexedHitObject.sliderCheesed = this.checkSliderCheesing(indexedHitObject, this.data.hitObjectData[i]);
1325
1515
  indexes.push(indexedHitObject.cursorIndex);
@@ -1380,21 +1570,20 @@ class TwoHandChecker {
1380
1570
  * @returns The cursor index that hits the given object, -1 if the index is not found, the object is a spinner, or the object was missed.
1381
1571
  */
1382
1572
  getIndexedHitObject(objectIndex) {
1383
- const diffObject = this.calculator.objects[objectIndex];
1384
- const { object } = diffObject;
1573
+ const object = this.beatmap.hitObjects.objects[objectIndex];
1385
1574
  // We don't care about the first object and spinners.
1386
1575
  if (objectIndex === 0 || object instanceof osuBase.Spinner) {
1387
- return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
1576
+ return new IndexedHitObject(object, -1, -1, -1, null, false);
1388
1577
  }
1389
1578
  // We don't care if the aim strain is too low.
1390
1579
  // if (diffObject.aimStrainWithSliders < 200) {
1391
1580
  // return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
1392
1581
  // }
1393
- const prevObject = this.calculator.beatmap.hitObjects.objects[objectIndex - 1];
1582
+ const prevObject = this.beatmap.hitObjects.objects[objectIndex - 1];
1394
1583
  const prevObjectData = this.data.hitObjectData[objectIndex - 1];
1395
1584
  if (prevObject instanceof osuBase.Spinner ||
1396
1585
  prevObjectData.result === exports.HitResult.miss) {
1397
- return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
1586
+ return new IndexedHitObject(object, -1, -1, -1, null, false);
1398
1587
  }
1399
1588
  const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
1400
1589
  let prevObjectEndPosition = prevObject.getStackedEndPosition(osuBase.Modes.droid);
@@ -1415,7 +1604,7 @@ class TwoHandChecker {
1415
1604
  .subtract(prevObjectEndPosition);
1416
1605
  // Don't consider objects that are too close to each other.
1417
1606
  if (prevToCurrentMovement.length <= object.radius) {
1418
- return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
1607
+ return new IndexedHitObject(object, -1, -1, -1, null, false);
1419
1608
  }
1420
1609
  // The case for a one-handed object is that there will be a slight movement in the cursor towards
1421
1610
  // the next object in fast patterns. We should not be worried about slow patterns as they will only
@@ -1435,12 +1624,12 @@ class TwoHandChecker {
1435
1624
  this.indexedHitObjects[objectIndex - 1].endCursorPosition =
1436
1625
  prevObjectInformation.position;
1437
1626
  if (prevObjectInformation.position.x === Number.POSITIVE_INFINITY) {
1438
- return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
1627
+ return new IndexedHitObject(object, -1, -1, -1, null, false);
1439
1628
  }
1440
1629
  if (prevObjectInformation.cursorIndex ===
1441
1630
  objectInformation.cursorIndex &&
1442
1631
  prevObjectInformation.groupIndex === objectInformation.groupIndex) {
1443
- return new IndexedHitObject(diffObject, objectInformation.cursorIndex, objectInformation.groupIndex, prevObjectInformation.occurrenceIndex, 0, false);
1632
+ return new IndexedHitObject(object, objectInformation.cursorIndex, objectInformation.groupIndex, prevObjectInformation.occurrenceIndex, 0, false);
1444
1633
  }
1445
1634
  const cursorData = this.data.cursorMovement[prevObjectInformation.cursorIndex];
1446
1635
  // There can be multiple angles to which the cursor moves towards the next object.
@@ -1522,9 +1711,9 @@ class TwoHandChecker {
1522
1711
  // }
1523
1712
  // }
1524
1713
  if (!Number.isFinite(finalAngle)) {
1525
- return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
1714
+ return new IndexedHitObject(object, -1, -1, -1, null, false);
1526
1715
  }
1527
- return new IndexedHitObject(diffObject, prevObjectInformation.cursorIndex, prevObjectInformation.groupIndex, prevObjectInformation.occurrenceIndex, finalAngle, is2Handed);
1716
+ return new IndexedHitObject(object, prevObjectInformation.cursorIndex, prevObjectInformation.groupIndex, prevObjectInformation.occurrenceIndex, finalAngle, is2Handed);
1528
1717
  }
1529
1718
  /**
1530
1719
  * Gets the position of the cursor that presses an object.
@@ -1533,7 +1722,7 @@ class TwoHandChecker {
1533
1722
  * @returns The position of the cursor that presses the object.
1534
1723
  */
1535
1724
  getCursorPositionForObjectStart(objectIndex) {
1536
- const object = this.calculator.beatmap.hitObjects.objects[objectIndex];
1725
+ const object = this.beatmap.hitObjects.objects[objectIndex];
1537
1726
  const data = this.data.hitObjectData[objectIndex];
1538
1727
  const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
1539
1728
  if (object instanceof osuBase.Spinner) {
@@ -1587,7 +1776,7 @@ class TwoHandChecker {
1587
1776
  if (downPosition.getDistance(objectPosition) <= object.radius &&
1588
1777
  Math.abs(down.time - object.startTime) <= hitWindow) {
1589
1778
  if (objectIndex > 0) {
1590
- const prevObject = this.calculator.beatmap.hitObjects.objects[objectIndex - 1];
1779
+ const prevObject = this.beatmap.hitObjects.objects[objectIndex - 1];
1591
1780
  if (down.time > prevObject.endTime) {
1592
1781
  return {
1593
1782
  position: downPosition,
@@ -1692,11 +1881,11 @@ class TwoHandChecker {
1692
1881
  * @returns The position of the cursor at the object's end position.
1693
1882
  */
1694
1883
  getCursorPositionForObjectEnd(objectIndex) {
1695
- const object = this.calculator.beatmap.hitObjects.objects[objectIndex];
1884
+ const object = this.beatmap.hitObjects.objects[objectIndex];
1696
1885
  if (!(object instanceof osuBase.Slider)) {
1697
1886
  return this.getCursorPositionForObjectStart(objectIndex);
1698
1887
  }
1699
- const nextObject = this.calculator.beatmap.hitObjects.objects[objectIndex - 1];
1888
+ const nextObject = this.beatmap.hitObjects.objects[objectIndex - 1];
1700
1889
  let objectEndPosition = object.getStackedEndPosition(osuBase.Modes.droid);
1701
1890
  if (object.lazyTravelDistance > 0 && nextObject) {
1702
1891
  const nextStartPosition = nextObject.getStackedPosition(osuBase.Modes.droid);
@@ -1796,7 +1985,7 @@ class TwoHandChecker {
1796
1985
  * @returns Whether the slider was cheesed.
1797
1986
  */
1798
1987
  checkSliderCheesing(indexedHitObject, hitData) {
1799
- if (!(indexedHitObject.object.object instanceof osuBase.Slider) ||
1988
+ if (!(indexedHitObject.object instanceof osuBase.Slider) ||
1800
1989
  hitData.result === exports.HitResult.miss ||
1801
1990
  indexedHitObject.cursorIndex === -1) {
1802
1991
  return false;
@@ -1812,223 +2001,30 @@ class TwoHandChecker {
1812
2001
  }
1813
2002
 
1814
2003
  /**
1815
- * Utility to check whether relevant sliders in a beatmap are cheesed.
2004
+ * Represents a replay data in an osu!droid replay version 1 and 2.
2005
+ *
2006
+ * Stores generic information about an osu!droid replay.
2007
+ *
2008
+ * This is used when analyzing replays using replay analyzer.
1816
2009
  */
1817
- class SliderCheeseChecker {
1818
- /**
1819
- * @param beatmap The beatmap to analyze.
1820
- * @param data The data of the replay.
1821
- * @param difficultyAttributes The difficulty attributes of the beatmap.
1822
- */
1823
- constructor(beatmap, data, difficultyAttributes) {
1824
- this.beatmap = beatmap;
1825
- this.data = data;
1826
- this.difficultyAttributes = difficultyAttributes;
1827
- this.hitWindow50 = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise)
1828
- ? new osuBase.PreciseDroidHitWindow(beatmap.difficulty.od).mehWindow
1829
- : new osuBase.DroidHitWindow(beatmap.difficulty.od).mehWindow;
1830
- this.isHardRock = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHardRock);
1831
- }
1832
- /**
1833
- * Checks if relevant sliders in the given beatmap was cheesed.
1834
- */
1835
- check() {
1836
- if (this.difficultyAttributes.difficultSliders.length === 0 ||
1837
- (this.difficultyAttributes.sliderFactor === 1 &&
1838
- this.difficultyAttributes.flashlightSliderFactor === 1 &&
1839
- this.difficultyAttributes.visualSliderFactor === 1)) {
1840
- return {
1841
- aimPenalty: 1,
1842
- flashlightPenalty: 1,
1843
- visualPenalty: 1,
1844
- };
1845
- }
1846
- const cheesedDifficultyRatings = this.checkSliderCheesing();
1847
- return this.calculateSliderCheesePenalty(cheesedDifficultyRatings);
1848
- }
1849
- /**
1850
- * Checks for sliders that were cheesed.
1851
- */
1852
- checkSliderCheesing() {
1853
- const { objects } = this.beatmap.hitObjects;
1854
- const cheesedDifficultyRatings = [];
1855
- // Current loop indices are stored for efficiency.
1856
- const cursorLoopIndices = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
1857
- const acceptableRadius = objects[0].radius * 2;
1858
- // Sort difficult sliders by index so that cursor loop indices work properly.
1859
- for (const difficultSlider of this.difficultyAttributes.difficultSliders
1860
- .slice()
1861
- .sort((a, b) => a.index - b.index)) {
1862
- if (difficultSlider.index >= this.data.hitObjectData.length) {
1863
- continue;
1864
- }
1865
- const object = objects[difficultSlider.index];
1866
- const objectData = this.data.hitObjectData[difficultSlider.index];
1867
- // If a miss or slider break occurs, we disregard the check for that slider.
1868
- if (objectData.result === exports.HitResult.miss ||
1869
- -this.hitWindow50 > objectData.accuracy ||
1870
- objectData.accuracy >
1871
- Math.min(this.hitWindow50, object.duration)) {
1872
- continue;
1873
- }
1874
- const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
1875
- // These time boundaries should consider the delta time between the previous and next
1876
- // object as well as their hit accuracy. However, they are somewhat complicated to
1877
- // compute and the accuracy gain is small. As such, let's settle with 50 hit window.
1878
- const minTimeLimit = object.startTime - this.hitWindow50;
1879
- const maxTimeLimit = object.startTime + this.hitWindow50;
1880
- // Get the closest tap distance across all cursors.
1881
- const closestDistances = [];
1882
- const closestGroupIndices = [];
1883
- for (let i = 0; i < this.data.cursorMovement.length; ++i) {
1884
- const cursorGroups = this.data.cursorMovement[i].occurrenceGroups;
1885
- let closestDistance = Number.POSITIVE_INFINITY;
1886
- let closestIndex = cursorGroups.length;
1887
- for (let j = cursorLoopIndices[i]; j < cursorGroups.length; j = ++cursorLoopIndices[i]) {
1888
- const group = cursorGroups[j];
1889
- if (group.endTime < minTimeLimit) {
1890
- continue;
1891
- }
1892
- if (group.startTime > maxTimeLimit) {
1893
- break;
1894
- }
1895
- if (group.startTime >= minTimeLimit) {
1896
- const position = this.getCursorPosition(group.down);
1897
- const distance = position.getDistance(objectStartPosition);
1898
- if (closestDistance > distance) {
1899
- closestDistance = distance;
1900
- closestIndex = j;
1901
- }
1902
- if (closestDistance <= acceptableRadius / 2) {
1903
- break;
1904
- }
1905
- }
1906
- // Normally, we check if there are cursor presses within the group's active time.
1907
- // However, some funky workarounds are used throughout the game for replays, so
1908
- // for the time being we only check for cursor distances across the group.
1909
- const { allOccurrences } = group;
1910
- for (let k = 1; k < allOccurrences.length; ++k) {
1911
- const cursor = allOccurrences[k];
1912
- const prevCursor = allOccurrences[k - 1];
1913
- let distance = Number.POSITIVE_INFINITY;
1914
- const currentPosition = this.getCursorPosition(cursor);
1915
- const prevPosition = this.getCursorPosition(prevCursor);
1916
- switch (cursor.id) {
1917
- case exports.MovementType.up:
1918
- distance =
1919
- prevPosition.getDistance(objectStartPosition);
1920
- break;
1921
- case exports.MovementType.move:
1922
- for (let mSecPassed = Math.max(prevCursor.time, minTimeLimit); mSecPassed <=
1923
- Math.min(cursor.time, maxTimeLimit); ++mSecPassed) {
1924
- const t = (mSecPassed - prevCursor.time) /
1925
- (cursor.time - prevCursor.time);
1926
- const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
1927
- distance =
1928
- cursorPosition.getDistance(objectStartPosition);
1929
- if (closestDistance > distance) {
1930
- closestDistance = distance;
1931
- closestIndex = j;
1932
- }
1933
- if (closestDistance <=
1934
- acceptableRadius / 2) {
1935
- break;
1936
- }
1937
- }
1938
- }
1939
- if (closestDistance > distance) {
1940
- closestDistance = distance;
1941
- closestIndex = j;
1942
- }
1943
- if (closestDistance <= acceptableRadius / 2) {
1944
- break;
1945
- }
1946
- }
1947
- }
1948
- closestDistances.push(closestDistance);
1949
- closestGroupIndices.push(closestIndex);
1950
- if (cursorLoopIndices[i] > 0) {
1951
- // Decrement the index. The previous group may also have a role on the next slider.
1952
- --cursorLoopIndices[i];
1953
- }
1954
- }
1955
- const cursorIndex = closestDistances.indexOf(Math.min(...closestDistances));
1956
- const closestDistance = closestDistances[cursorIndex];
1957
- if (closestDistance > acceptableRadius / 2) {
1958
- cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
1959
- continue;
1960
- }
1961
- const group = this.data.cursorMovement[cursorIndex].occurrenceGroups[closestGroupIndices[cursorIndex]];
1962
- let isCheesed = false;
1963
- // Track cursor movement to see if it lands on every tick.
1964
- let occurrenceLoopIndex = 1;
1965
- const { allOccurrences } = group;
1966
- for (let i = 1; i < object.nestedHitObjects.length; ++i) {
1967
- if (isCheesed) {
1968
- break;
1969
- }
1970
- const tickWasHit = objectData.tickset[i - 1];
1971
- if (!tickWasHit) {
1972
- continue;
1973
- }
1974
- const nestedObject = object.nestedHitObjects[i];
1975
- const nestedPosition = nestedObject.getStackedPosition(osuBase.Modes.droid);
1976
- while (occurrenceLoopIndex < allOccurrences.length &&
1977
- allOccurrences[occurrenceLoopIndex].time <
1978
- nestedObject.startTime) {
1979
- ++occurrenceLoopIndex;
1980
- }
1981
- if (occurrenceLoopIndex === allOccurrences.length) {
1982
- continue;
1983
- }
1984
- const cursor = allOccurrences[occurrenceLoopIndex];
1985
- const prevCursor = allOccurrences[occurrenceLoopIndex - 1];
1986
- const currentPosition = this.getCursorPosition(cursor);
1987
- const prevPosition = this.getCursorPosition(prevCursor);
1988
- switch (cursor.id) {
1989
- case exports.MovementType.move: {
1990
- // Interpolate cursor position during nested object time.
1991
- const t = (nestedObject.startTime - prevCursor.time) /
1992
- (cursor.time - prevCursor.time);
1993
- const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
1994
- const distance = cursorPosition.getDistance(nestedPosition);
1995
- isCheesed = distance > acceptableRadius;
1996
- break;
1997
- }
1998
- case exports.MovementType.up:
1999
- isCheesed =
2000
- prevPosition.getDistance(nestedPosition) >
2001
- acceptableRadius;
2002
- }
2003
- }
2004
- if (isCheesed) {
2005
- cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
2006
- }
2007
- }
2008
- return cheesedDifficultyRatings;
2010
+ class ReplayData {
2011
+ constructor(values) {
2012
+ this.replayVersion = values.replayVersion;
2013
+ this.folderName = values.folderName;
2014
+ this.fileName = values.fileName;
2015
+ this.hash = values.hash;
2016
+ this.accuracy = values.accuracy;
2017
+ this.rank = values.rank;
2018
+ this.hit300k = values.hit300k;
2019
+ this.hit100k = values.hit100k;
2020
+ this.cursorMovement = values.cursorMovement;
2021
+ this.hitObjectData = values.hitObjectData;
2009
2022
  }
2010
2023
  /**
2011
- * Calculates the slider cheese penalty.
2024
+ * Whether the replay's version is 3 or later.
2012
2025
  */
2013
- calculateSliderCheesePenalty(cheesedDifficultyRatings) {
2014
- const summedDifficultyRating = Math.min(1, cheesedDifficultyRatings.reduce((a, v) => a + v, 0));
2015
- return {
2016
- aimPenalty: Math.max(this.difficultyAttributes.sliderFactor, Math.pow(1 -
2017
- summedDifficultyRating *
2018
- this.difficultyAttributes.sliderFactor, 2)),
2019
- flashlightPenalty: Math.max(this.difficultyAttributes.flashlightSliderFactor, Math.pow(1 -
2020
- summedDifficultyRating *
2021
- this.difficultyAttributes.flashlightSliderFactor, 2)),
2022
- visualPenalty: Math.max(this.difficultyAttributes.visualSliderFactor, Math.pow(1 -
2023
- summedDifficultyRating *
2024
- this.difficultyAttributes.visualSliderFactor, 2)),
2025
- };
2026
- }
2027
- getCursorPosition(cursor) {
2028
- if (this.isHardRock) {
2029
- return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
2030
- }
2031
- return cursor.position;
2026
+ isReplayV3() {
2027
+ return this.replayVersion >= 3;
2032
2028
  }
2033
2029
  }
2034
2030
 
@@ -2040,20 +2036,6 @@ class SliderCheeseChecker {
2040
2036
  * This is used when analyzing replays using replay analyzer.
2041
2037
  */
2042
2038
  class ReplayV3Data extends ReplayData {
2043
- /**
2044
- * The complete mod string of this replay (mods, speed multiplier, and forced difficulty statistics combined).
2045
- */
2046
- get completeModString() {
2047
- return this.modString;
2048
- }
2049
- /**
2050
- * The mod string of this replay (mods only).
2051
- */
2052
- get modString() {
2053
- return this.convertedMods.length > 0
2054
- ? this.convertedMods.map((v) => v.acronym).join("")
2055
- : "No Mod";
2056
- }
2057
2039
  constructor(values) {
2058
2040
  super(values);
2059
2041
  this.time = values.time;
@@ -2061,87 +2043,10 @@ class ReplayV3Data extends ReplayData {
2061
2043
  this.maxCombo = values.maxCombo;
2062
2044
  this.isFullCombo = values.isFullCombo;
2063
2045
  this.playerName = values.playerName;
2064
- this.rawMods = values.rawMods;
2065
2046
  this.convertedMods = values.convertedMods;
2066
2047
  }
2067
2048
  }
2068
2049
 
2069
- /**
2070
- * Represents a replay data for replay version 4.
2071
- *
2072
- * Stores generic information about an osu!droid replay.
2073
- *
2074
- * This is used when analyzing replays using replay analyzer.
2075
- */
2076
- class ReplayV4Data extends ReplayV3Data {
2077
- get completeModString() {
2078
- let finalString = this.modString;
2079
- const customStats = [];
2080
- if (this.speedMultiplier !== 1) {
2081
- customStats.push(`${this.speedMultiplier}x`);
2082
- }
2083
- if (this.forceAR !== undefined) {
2084
- customStats.push(`AR${this.forceAR}`);
2085
- }
2086
- if (this.flashlightFollowDelay !== undefined) {
2087
- customStats.push(`FLD${this.flashlightFollowDelay}`);
2088
- }
2089
- if (customStats.length > 0) {
2090
- finalString += ` (${customStats.join(", ")})`;
2091
- }
2092
- return finalString;
2093
- }
2094
- constructor(values) {
2095
- var _a, _b;
2096
- super(values);
2097
- this.forceAR = values.forceAR;
2098
- this.speedMultiplier = (_a = values.speedMultiplier) !== null && _a !== void 0 ? _a : 1;
2099
- this.flashlightFollowDelay = (_b = values.flashlightFollowDelay) !== null && _b !== void 0 ? _b : 0.12;
2100
- }
2101
- }
2102
-
2103
- /**
2104
- * Represents a replay data for replay version 5.
2105
- *
2106
- * Stores generic information about an osu!droid replay.
2107
- *
2108
- * This is used when analyzing replays using replay analyzer.
2109
- */
2110
- class ReplayV5Data extends ReplayV4Data {
2111
- get completeModString() {
2112
- let finalString = this.modString;
2113
- const customStats = [];
2114
- if (this.speedMultiplier !== 1) {
2115
- customStats.push(`${this.speedMultiplier}x`);
2116
- }
2117
- if (this.forceAR !== undefined) {
2118
- customStats.push(`AR${this.forceAR}`);
2119
- }
2120
- if (this.forceOD !== undefined) {
2121
- customStats.push(`OD${this.forceOD}`);
2122
- }
2123
- if (this.forceCS !== undefined) {
2124
- customStats.push(`CS${this.forceCS}`);
2125
- }
2126
- if (this.forceHP !== undefined) {
2127
- customStats.push(`HP${this.forceHP}`);
2128
- }
2129
- if (this.flashlightFollowDelay !== undefined) {
2130
- customStats.push(`FLD${this.flashlightFollowDelay}`);
2131
- }
2132
- if (customStats.length > 0) {
2133
- finalString += ` (${customStats.join(", ")})`;
2134
- }
2135
- return finalString;
2136
- }
2137
- constructor(values) {
2138
- super(values);
2139
- this.forceCS = values.forceCS;
2140
- this.forceOD = values.forceOD;
2141
- this.forceHP = values.forceHP;
2142
- }
2143
- }
2144
-
2145
2050
  /**
2146
2051
  * A replay analyzer that analyzes a replay from osu!droid.
2147
2052
  *
@@ -2196,11 +2101,11 @@ class ReplayAnalyzer {
2196
2101
  */
2197
2102
  this.twoHandedNoteCount = 0;
2198
2103
  this.bufferOffset = 0;
2199
- this.scoreID = values.scoreID;
2200
- this.beatmap = values.map;
2201
- this.difficultyAttributes = values.difficultyAttributes;
2202
- if (this.beatmap && !(this.beatmap instanceof osuBase.Beatmap)) {
2203
- this.difficultyAttributes = this.beatmap.attributes;
2104
+ this.scoreID = values === null || values === void 0 ? void 0 : values.scoreID;
2105
+ this.beatmap = values === null || values === void 0 ? void 0 : values.map;
2106
+ this.difficultyAttributes = values === null || values === void 0 ? void 0 : values.difficultyAttributes;
2107
+ if (this.beatmap instanceof osuBase.DroidPlayableBeatmap) {
2108
+ this.playableBeatmap = this.beatmap;
2204
2109
  }
2205
2110
  }
2206
2111
  /**
@@ -2239,16 +2144,12 @@ class ReplayAnalyzer {
2239
2144
  let negativeCount = 0;
2240
2145
  let positiveTotal = 0;
2241
2146
  let negativeTotal = 0;
2242
- const beatmap = this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
2243
- this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
2244
- ? this.beatmap.beatmap
2245
- : this.beatmap;
2246
- const { objects } = beatmap.hitObjects;
2147
+ const { objects } = this.beatmap.hitObjects;
2247
2148
  const mods = this.data.isReplayV3()
2248
2149
  ? this.data.convertedMods
2249
2150
  : (_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods.filter((m) => m.isApplicableToDroid())) !== null && _b !== void 0 ? _b : [];
2250
- const adjustedDifficulty = new osuBase.BeatmapDifficulty(beatmap.difficulty);
2251
- osuBase.ModUtil.applyModsToBeatmapDifficulty(adjustedDifficulty, osuBase.Modes.droid, mods, this.data.isReplayV4() ? this.data.speedMultiplier : 1);
2151
+ const adjustedDifficulty = new osuBase.BeatmapDifficulty(this.beatmap.difficulty);
2152
+ osuBase.ModUtil.applyModsToBeatmapDifficulty(adjustedDifficulty, osuBase.Modes.droid, mods);
2252
2153
  const mehWindow = mods.some((m) => m instanceof osuBase.ModPrecise)
2253
2154
  ? new osuBase.PreciseDroidHitWindow(adjustedDifficulty.od).mehWindow
2254
2155
  : new osuBase.DroidHitWindow(adjustedDifficulty.od).mehWindow;
@@ -2304,15 +2205,15 @@ class ReplayAnalyzer {
2304
2205
  /**
2305
2206
  * Checks if a play is using 2 hands.
2306
2207
  *
2307
- * Requires `analyze()` to be called first and `map` to be defined as `DroidDifficultyCalculator`.
2208
+ * Requires `analyze()` to be called first as well as `beatmap` and `difficultyAttributes` to be defined.
2308
2209
  */
2309
2210
  checkFor2Hand() {
2310
- if (!(this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
2311
- this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator) ||
2312
- !this.data) {
2211
+ var _a;
2212
+ if (!this.beatmap || !this.difficultyAttributes || !this.data) {
2313
2213
  return;
2314
2214
  }
2315
- const twoHandChecker = new TwoHandChecker(this.beatmap, this.data);
2215
+ (_a = this.playableBeatmap) !== null && _a !== void 0 ? _a : (this.playableBeatmap = this.constructPlayableBeatmap());
2216
+ const twoHandChecker = new TwoHandChecker(this.playableBeatmap, this.difficultyAttributes, this.data);
2316
2217
  const result = twoHandChecker.check();
2317
2218
  this.is2Hand = result.is2Hand;
2318
2219
  this.twoHandedNoteCount = result.twoHandedNoteCount;
@@ -2338,6 +2239,9 @@ class ReplayAnalyzer {
2338
2239
  */
2339
2240
  downloadReplay() {
2340
2241
  return __awaiter(this, void 0, void 0, function* () {
2242
+ if (this.scoreID === undefined) {
2243
+ return null;
2244
+ }
2341
2245
  const apiRequestBuilder = new osuBase.DroidAPIRequestBuilder()
2342
2246
  .setRequireAPIkey(false)
2343
2247
  .setEndpoint("upload")
@@ -2407,7 +2311,6 @@ class ReplayAnalyzer {
2407
2311
  isFullCombo: false,
2408
2312
  maxCombo: 0,
2409
2313
  playerName: "",
2410
- rawMods: [],
2411
2314
  score: 0,
2412
2315
  time: new Date(0),
2413
2316
  };
@@ -2423,53 +2326,34 @@ class ReplayAnalyzer {
2423
2326
  resultObject.maxCombo = rawObject[4].readInt32BE(36);
2424
2327
  resultObject.isFullCombo = resultObject.accuracy.value() === 1;
2425
2328
  resultObject.playerName = rawObject[5];
2426
- resultObject.rawMods = Object.values(rawObject[6].elements);
2427
- resultObject.convertedMods = osuBase.ModUtil.droidStringToMods(this.convertDroidMods(resultObject.rawMods));
2428
- resultObject.rank = this.calculateRank(resultObject);
2429
- }
2430
- if (resultObject.replayVersion >= 4) {
2431
- const str = rawObject[7].split("|");
2432
- for (const s of str) {
2433
- switch (true) {
2434
- // Forced stats
2435
- case s.startsWith("CS"):
2436
- resultObject.forceCS = parseFloat(s.replace("CS", ""));
2437
- break;
2438
- case s.startsWith("AR"):
2439
- resultObject.forceAR = parseFloat(s.replace("AR", ""));
2440
- break;
2441
- case s.startsWith("OD"):
2442
- resultObject.forceOD = parseFloat(s.replace("OD", ""));
2443
- break;
2444
- case s.startsWith("HP"):
2445
- resultObject.forceHP = parseFloat(s.replace("HP", ""));
2446
- break;
2447
- // FL follow delay
2448
- case s.startsWith("FLD"):
2449
- resultObject.flashlightFollowDelay =
2450
- parseFloat(s.replace("FLD", "")) || 0.12;
2451
- break;
2452
- // Speed multiplier
2453
- case s.startsWith("x"):
2454
- resultObject.speedMultiplier =
2455
- parseFloat(s.replace("x", "")) || 1;
2456
- break;
2329
+ if (resultObject.replayVersion >= 7) {
2330
+ resultObject.convertedMods = osuBase.ModUtil.deserializeMods(rawObject[6]);
2331
+ }
2332
+ else {
2333
+ resultObject.convertedMods = this.convertDroidMods(resultObject.replayVersion, Object.values(rawObject[6].elements));
2334
+ if (resultObject.replayVersion >= 4) {
2335
+ osuBase.DroidLegacyModConverter.parseExtraModString(resultObject.convertedMods, rawObject[7].split("|"));
2457
2336
  }
2458
2337
  }
2338
+ resultObject.rank = this.calculateRank(resultObject);
2459
2339
  }
2460
2340
  let bufferIndex;
2461
- switch (true) {
2462
- // replay v4 and above
2463
- case resultObject.replayVersion >= 4:
2464
- bufferIndex = 8;
2341
+ switch (resultObject.replayVersion) {
2342
+ case 1:
2343
+ case 2:
2344
+ bufferIndex = 4;
2465
2345
  break;
2466
- // replay v3
2467
- case resultObject.replayVersion === 3:
2346
+ case 3:
2347
+ case 7:
2468
2348
  bufferIndex = 7;
2469
2349
  break;
2470
- // replay v1 and v2
2350
+ case 4:
2351
+ case 5:
2352
+ case 6:
2353
+ bufferIndex = 8;
2354
+ break;
2471
2355
  default:
2472
- bufferIndex = 4;
2356
+ throw new Error("Unsupported replay version: " + resultObject.replayVersion);
2473
2357
  }
2474
2358
  const replayDataBufferArray = [];
2475
2359
  while (bufferIndex < rawObject.length) {
@@ -2483,14 +2367,10 @@ class ReplayAnalyzer {
2483
2367
  this.parseOldReplayInformation(resultObject);
2484
2368
  switch (resultObject.replayVersion) {
2485
2369
  case 3:
2486
- this.data = new ReplayV3Data(resultObject);
2487
- break;
2488
2370
  case 4:
2489
- this.data = new ReplayV4Data(resultObject);
2490
- break;
2491
2371
  case 5:
2492
2372
  case 6:
2493
- this.data = new ReplayV5Data(resultObject);
2373
+ this.data = new ReplayV3Data(resultObject);
2494
2374
  break;
2495
2375
  default:
2496
2376
  this.data = new ReplayData(resultObject);
@@ -2499,38 +2379,44 @@ class ReplayAnalyzer {
2499
2379
  /**
2500
2380
  * Converts replay mods to droid mod string.
2501
2381
  */
2502
- convertDroidMods(replayMods) {
2382
+ convertDroidMods(replayVersion, replayMods) {
2503
2383
  const replayModsConstants = {
2504
- MOD_AUTO: new osuBase.ModAuto().droidString,
2505
- MOD_AUTOPILOT: new osuBase.ModAutopilot().droidString,
2506
- MOD_NOFAIL: new osuBase.ModNoFail().droidString,
2507
- MOD_EASY: new osuBase.ModEasy().droidString,
2508
- MOD_HIDDEN: new osuBase.ModHidden().droidString,
2509
- MOD_HARDROCK: new osuBase.ModHardRock().droidString,
2510
- MOD_DOUBLETIME: new osuBase.ModDoubleTime().droidString,
2511
- MOD_HALFTIME: new osuBase.ModHalfTime().droidString,
2512
- MOD_NIGHTCORE: new osuBase.ModNightCore().droidString,
2513
- MOD_PRECISE: new osuBase.ModPrecise().droidString,
2514
- MOD_SMALLCIRCLE: new osuBase.ModSmallCircle().droidString,
2515
- MOD_REALLYEASY: new osuBase.ModReallyEasy().droidString,
2516
- MOD_RELAX: new osuBase.ModRelax().droidString,
2517
- MOD_PERFECT: new osuBase.ModPerfect().droidString,
2518
- MOD_SUDDENDEATH: new osuBase.ModSuddenDeath().droidString,
2519
- MOD_SCOREV2: new osuBase.ModScoreV2().droidString,
2520
- MOD_FLASHLIGHT: new osuBase.ModFlashlight().droidString,
2384
+ MOD_AUTO: new osuBase.ModAuto(),
2385
+ MOD_AUTOPILOT: new osuBase.ModAutopilot(),
2386
+ MOD_NOFAIL: new osuBase.ModNoFail(),
2387
+ MOD_EASY: new osuBase.ModEasy(),
2388
+ MOD_HIDDEN: new osuBase.ModHidden(),
2389
+ MOD_TRACEABLE: new osuBase.ModTraceable(),
2390
+ MOD_HARDROCK: new osuBase.ModHardRock(),
2391
+ MOD_DOUBLETIME: new osuBase.ModDoubleTime(),
2392
+ MOD_HALFTIME: new osuBase.ModHalfTime(),
2393
+ MOD_NIGHTCORE: new osuBase.ModNightCore(),
2394
+ MOD_PRECISE: new osuBase.ModPrecise(),
2395
+ MOD_SMALLCIRCLE: new osuBase.ModSmallCircle(),
2396
+ MOD_REALLYEASY: new osuBase.ModReallyEasy(),
2397
+ MOD_RELAX: new osuBase.ModRelax(),
2398
+ MOD_PERFECT: new osuBase.ModPerfect(),
2399
+ MOD_SUDDENDEATH: new osuBase.ModSuddenDeath(),
2400
+ MOD_SCOREV2: new osuBase.ModScoreV2(),
2401
+ MOD_FLASHLIGHT: new osuBase.ModFlashlight(),
2521
2402
  };
2522
- let modString = "";
2403
+ const mods = [];
2523
2404
  for (const mod of replayMods) {
2524
2405
  for (const property in replayModsConstants) {
2525
2406
  if (!mod.includes(property)) {
2526
2407
  continue;
2527
2408
  }
2528
- modString +=
2529
- replayModsConstants[property];
2409
+ if (replayVersion <= 3 && mod === "MOD_NIGHTCORE") {
2410
+ // In replay v3, the NightCore mod is bugged. See ModOldNightCore's description.
2411
+ mods.push(new osuBase.ModOldNightCore());
2412
+ }
2413
+ else {
2414
+ mods.push(replayModsConstants[property]);
2415
+ }
2530
2416
  break;
2531
2417
  }
2532
2418
  }
2533
- return modString;
2419
+ return mods;
2534
2420
  }
2535
2421
  parseMovementData(resultObject, replayDataBuffer) {
2536
2422
  resultObject.cursorMovement.length = 0;
@@ -2605,10 +2491,7 @@ class ReplayAnalyzer {
2605
2491
  if (resultObject.replayVersion >= 3) {
2606
2492
  return;
2607
2493
  }
2608
- const objects = (_a = (this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
2609
- this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
2610
- ? this.beatmap.beatmap
2611
- : this.beatmap)) === null || _a === void 0 ? void 0 : _a.hitObjects.objects;
2494
+ const objects = (_a = this.beatmap) === null || _a === void 0 ? void 0 : _a.hitObjects.objects;
2612
2495
  let grantsGekiOrKatu = true;
2613
2496
  for (let i = 0; i < resultObject.hitObjectData.length; ++i) {
2614
2497
  // Hit result
@@ -2677,35 +2560,16 @@ class ReplayAnalyzer {
2677
2560
  }
2678
2561
  constructPlayableBeatmap() {
2679
2562
  var _a, _b;
2563
+ if (this.beatmap instanceof osuBase.DroidPlayableBeatmap) {
2564
+ return this.beatmap;
2565
+ }
2680
2566
  if (!this.beatmap || !this.data) {
2681
2567
  throw new Error("Beatmap and replay data must be defined.");
2682
2568
  }
2683
2569
  const mods = this.data.isReplayV3()
2684
- ? this.data.convertedMods.slice()
2685
- : (_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods.slice()) !== null && _b !== void 0 ? _b : [];
2686
- if (this.data.isReplayV5() &&
2687
- [
2688
- this.data.forceCS,
2689
- this.data.forceAR,
2690
- this.data.forceOD,
2691
- this.data.forceHP,
2692
- ].some((v) => v !== undefined)) {
2693
- mods.push(new osuBase.ModDifficultyAdjust({
2694
- cs: this.data.forceCS,
2695
- ar: this.data.forceAR,
2696
- od: this.data.forceOD,
2697
- hp: this.data.forceHP,
2698
- }));
2699
- }
2700
- return (this.beatmap instanceof osuBase.Beatmap
2701
- ? this.beatmap
2702
- : this.beatmap.beatmap).createPlayableBeatmap({
2703
- mode: osuBase.Modes.droid,
2704
- mods: mods,
2705
- customSpeedMultiplier: this.data.isReplayV4()
2706
- ? this.data.speedMultiplier
2707
- : 1,
2708
- });
2570
+ ? this.data.convertedMods
2571
+ : (_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods) !== null && _b !== void 0 ? _b : [];
2572
+ return this.beatmap.createDroidPlayableBeatmap(mods);
2709
2573
  }
2710
2574
  readByte(buffer) {
2711
2575
  const num = buffer.readInt8(this.bufferOffset);
@@ -2752,8 +2616,6 @@ exports.ReplayAnalyzer = ReplayAnalyzer;
2752
2616
  exports.ReplayData = ReplayData;
2753
2617
  exports.ReplayObjectData = ReplayObjectData;
2754
2618
  exports.ReplayV3Data = ReplayV3Data;
2755
- exports.ReplayV4Data = ReplayV4Data;
2756
- exports.ReplayV5Data = ReplayV5Data;
2757
2619
  exports.SliderCheeseChecker = SliderCheeseChecker;
2758
2620
  exports.ThreeFingerChecker = ThreeFingerChecker;
2759
2621
  exports.TwoHandChecker = TwoHandChecker;