@rian8337/osu-droid-replay-analyzer 3.2.0 → 4.0.0-beta.0

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,16 +1,13 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
3
  var osuBase = require('@rian8337/osu-base');
6
4
  var osuDifficultyCalculator = require('@rian8337/osu-difficulty-calculator');
7
5
  var osuRebalanceDifficultyCalculator = require('@rian8337/osu-rebalance-difficulty-calculator');
8
6
  var unzipper = require('unzipper');
9
7
  var javaDeserialization = require('java-deserialization');
10
- var stream = require('stream');
8
+ var node_stream = require('node:stream');
11
9
 
12
- function _interopNamespace(e) {
13
- if (e && e.__esModule) return e;
10
+ function _interopNamespaceDefault(e) {
14
11
  var n = Object.create(null);
15
12
  if (e) {
16
13
  Object.keys(e).forEach(function (k) {
@@ -23,11 +20,11 @@ function _interopNamespace(e) {
23
20
  }
24
21
  });
25
22
  }
26
- n["default"] = e;
23
+ n.default = e;
27
24
  return Object.freeze(n);
28
25
  }
29
26
 
30
- var javaDeserialization__namespace = /*#__PURE__*/_interopNamespace(javaDeserialization);
27
+ var javaDeserialization__namespace = /*#__PURE__*/_interopNamespaceDefault(javaDeserialization);
31
28
 
32
29
  /**
33
30
  * Movement types of a cursor in an osu!droid replay.
@@ -365,104 +362,22 @@ class ReplayData {
365
362
  }
366
363
  }
367
364
 
368
- /**
369
- * A beatmap section generator that generates beatmap section based on aim/speed strain.
370
- */
371
- class BeatmapSectionGenerator {
372
- /**
373
- * Generates `BeatmapSection`s for the specified beatmap.
374
- *
375
- * @param calculator The difficulty calculator to generate for.
376
- * @param minSectionObjectCount The maximum delta time allowed between two beatmap sections.
377
- * Increasing this number decreases the amount of beatmap sections in general. Note that this value does not account for the speed multiplier of
378
- * the play, similar to the way replay object data is stored.
379
- * @param maxSectionDeltaTime The minimum object count required to make a beatmap section. Increasing this number decreases the amount of beatmap sections.
380
- */
381
- static generateSections(calculator, minSectionObjectCount, maxSectionDeltaTime) {
382
- const beatmapSections = [];
383
- let firstObjectIndex = 0;
384
- for (let i = 0; i < calculator.objects.length - 1; ++i) {
385
- const current = calculator.objects[i];
386
- const next = calculator.objects[i + 1];
387
- const realDeltaTime = next.object.startTime - current.object.endTime;
388
- if (realDeltaTime >= maxSectionDeltaTime) {
389
- // Ignore sections that don't meet object count requirement.
390
- if (i - firstObjectIndex < minSectionObjectCount) {
391
- firstObjectIndex = i + 1;
392
- continue;
393
- }
394
- beatmapSections.push({
395
- firstObjectIndex,
396
- lastObjectIndex: i,
397
- });
398
- firstObjectIndex = i + 1;
399
- }
400
- }
401
- // Don't forget to manually add the last beatmap section, which would otherwise be ignored.
402
- if (calculator.objects.length - firstObjectIndex >
403
- minSectionObjectCount) {
404
- beatmapSections.push({
405
- firstObjectIndex,
406
- lastObjectIndex: calculator.objects.length - 1,
407
- });
408
- }
409
- return beatmapSections;
410
- }
411
- }
412
-
413
- /**
414
- * Represents a section of a beatmap.
415
- */
416
- class BeatmapSection {
417
- /**
418
- * The index of the first `DifficultyHitObject` of this beatmap section.
419
- */
420
- firstObjectIndex;
421
- /**
422
- * The index of the last `DifficultyHitObject` of this beatmap section.
423
- */
424
- lastObjectIndex;
425
- /**
426
- * @param firstObjectIndex The index of the first `DifficultyHitObject` of this beatmap section.
427
- * @param lastObjectIndex The index of the last `DifficultyHitObject` of this beatmap section.
428
- */
429
- constructor(firstObjectIndex, lastObjectIndex) {
430
- this.firstObjectIndex = firstObjectIndex;
431
- this.lastObjectIndex = lastObjectIndex;
432
- }
433
- }
434
-
435
- /**
436
- * A section of a beatmap with extra information used for detecting three-finger usage.
437
- */
438
- class ThreeFingerBeatmapSection extends BeatmapSection {
439
- /**
440
- * Whether or not this beatmap section is dragged.
441
- */
442
- isDragged;
443
- /**
444
- * The index of the cursor that is dragging this section.
445
- */
446
- dragFingerIndex;
447
- constructor(values) {
448
- super(values.firstObjectIndex, values.lastObjectIndex);
449
- this.isDragged = values.isDragged;
450
- this.dragFingerIndex = values.dragFingerIndex;
451
- }
452
- }
453
-
454
365
  /**
455
366
  * Utility to check whether or not a beatmap is three-fingered.
456
367
  */
457
368
  class ThreeFingerChecker {
458
369
  /**
459
- * The difficulty calculator that is being analyzed.
370
+ * The beatmap that is being analyzed.
460
371
  */
461
- calculator;
372
+ beatmap;
462
373
  /**
463
374
  * The data of the replay.
464
375
  */
465
376
  data;
377
+ /**
378
+ * The difficulty attributes of the beatmap.
379
+ */
380
+ difficultyAttributes;
466
381
  /**
467
382
  * The strain threshold to start detecting for 3-fingered section.
468
383
  *
@@ -500,22 +415,7 @@ class ThreeFingerChecker {
500
415
  */
501
416
  threeFingerRatioThreshold = 0.01;
502
417
  /**
503
- * The maximum delta time allowed between two beatmap sections.
504
- *
505
- * Increasing this number decreases the amount of beatmap sections in general.
506
- *
507
- * Note that this value does not account for the speed multiplier of
508
- * the play, similar to the way replay object data is stored.
509
- */
510
- maxSectionDeltaTime = 2000;
511
- /**
512
- * The minimum object count required to make a beatmap section.
513
- *
514
- * Increasing this number decreases the amount of beatmap sections.
515
- */
516
- minSectionObjectCount = 5;
517
- /**
518
- * The sections of the beatmap that was cut based on `maxSectionDeltaTime` and `minSectionObjectCount`.
418
+ * Extended sections of the beatmap for drag detection.
519
419
  */
520
420
  beatmapSections = [];
521
421
  /**
@@ -543,38 +443,40 @@ class ThreeFingerChecker {
543
443
  * Each index represents the cursor index.
544
444
  */
545
445
  downCursorInstances = [];
546
- /**
547
- * All cursor occurrences in the replay.
548
- *
549
- * Each index represents the cursor index.
550
- */
551
- allCursorInstances;
552
446
  /**
553
447
  * Nerf factors from all sections that were three-fingered.
554
448
  */
555
449
  nerfFactors = [];
556
450
  /**
557
- * @param calculator The difficulty calculator to analyze.
451
+ * Whether this score uses the Precise mod.
452
+ */
453
+ isPrecise;
454
+ /**
455
+ * @param beatmap The beatmap to analyze.
558
456
  * @param data The data of the replay.
457
+ * @param difficultyAttributes The difficulty attributes of the beatmap.
559
458
  */
560
- constructor(calculator, data) {
561
- this.calculator = calculator;
459
+ constructor(beatmap, data, difficultyAttributes) {
460
+ this.beatmap = beatmap;
562
461
  this.data = data;
563
- this.allCursorInstances = data.cursorMovement.map((v) => v.allOccurrences);
462
+ this.difficultyAttributes = difficultyAttributes;
564
463
  const stats = new osuBase.MapStats({
565
- od: this.calculator.beatmap.difficulty.od,
566
- mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
464
+ od: this.beatmap.difficulty.od,
465
+ mods: this.difficultyAttributes.mods.filter((m) => m.isApplicableToDroid() &&
567
466
  !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
568
467
  }).calculate();
468
+ this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
569
469
  this.hitWindow = new osuBase.DroidHitWindow(stats.od);
570
- const strainNotes = (calculator.objects).filter((v) => v.originalTapStrain >= ThreeFingerChecker.strainThreshold);
571
- this.strainNoteCount = strainNotes.length;
470
+ this.strainNoteCount =
471
+ this.difficultyAttributes.possibleThreeFingeredSections.reduce((a, v) => a + v.lastObjectIndex - v.firstObjectIndex + 1, 0);
572
472
  }
573
473
  /**
574
474
  * Checks whether a beatmap is eligible to be detected for 3-finger.
475
+ *
476
+ * @param difficultyAttributes The difficulty attributes of the beatmap.
575
477
  */
576
- static isEligibleToDetect(map) {
577
- return map.objects.some((v) => v.originalTapStrain >= this.strainThreshold);
478
+ static isEligibleToDetect(difficultyAttributes) {
479
+ return difficultyAttributes.possibleThreeFingeredSections.length > 0;
578
480
  }
579
481
  /**
580
482
  * Checks if the given beatmap is 3-fingered and also returns the final penalty.
@@ -597,7 +499,6 @@ class ThreeFingerChecker {
597
499
  }
598
500
  this.getBeatmapSections();
599
501
  this.detectDragPlay();
600
- this.getDetailedBeatmapSections();
601
502
  this.preventAccidentalTaps();
602
503
  if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
603
504
  return { is3Finger: false, penalty: 1 };
@@ -613,50 +514,43 @@ class ThreeFingerChecker {
613
514
  * start of the hitobject before it and do not end right at the first hitobject after it.
614
515
  */
615
516
  getAccurateBreakPoints() {
616
- const objects = this.calculator.objects;
517
+ const { objects } = this.beatmap.hitObjects;
617
518
  const objectData = this.data.hitObjectData;
618
- const isPrecise = this.calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
619
- for (const breakPoint of this.calculator.beatmap.events.breaks) {
620
- const beforeIndex = osuBase.MathUtils.clamp(objects.findIndex((o) => o.object.endTime >= breakPoint.startTime) - 1, 0, objects.length - 2);
621
- let timeBefore = objects[beforeIndex].object.endTime;
519
+ for (const breakPoint of this.beatmap.events.breaks) {
520
+ const beforeIndex = osuBase.MathUtils.clamp(objects.findIndex((o) => o.endTime >= breakPoint.startTime) - 1, 0, objects.length - 2);
521
+ let timeBefore = objects[beforeIndex].endTime;
622
522
  // For sliders and spinners, automatically set hit window length to be as lenient as possible.
623
- let beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
523
+ let beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
624
524
  switch (objectData[beforeIndex].result) {
625
525
  case exports.HitResult.great:
626
- beforeIndexHitWindowLength =
627
- this.hitWindow.hitWindowFor300(isPrecise);
526
+ beforeIndexHitWindowLength = this.hitWindow.hitWindowFor300(this.isPrecise);
628
527
  break;
629
528
  case exports.HitResult.good:
630
- beforeIndexHitWindowLength =
631
- this.hitWindow.hitWindowFor100(isPrecise);
529
+ beforeIndexHitWindowLength = this.hitWindow.hitWindowFor100(this.isPrecise);
632
530
  break;
633
531
  default:
634
- beforeIndexHitWindowLength =
635
- this.hitWindow.hitWindowFor50(isPrecise);
532
+ beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
636
533
  }
637
534
  timeBefore += beforeIndexHitWindowLength;
638
535
  const afterIndex = beforeIndex + 1;
639
- let timeAfter = objects[afterIndex].object.startTime;
536
+ let timeAfter = objects[afterIndex].startTime;
640
537
  // For sliders and spinners, automatically set hit window length to be as lenient as possible.
641
- let afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
538
+ let afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
642
539
  switch (objectData[afterIndex].result) {
643
540
  case exports.HitResult.great:
644
- afterIndexHitWindowLength =
645
- this.hitWindow.hitWindowFor300(isPrecise);
541
+ afterIndexHitWindowLength = this.hitWindow.hitWindowFor300(this.isPrecise);
646
542
  break;
647
543
  case exports.HitResult.good:
648
- afterIndexHitWindowLength =
649
- this.hitWindow.hitWindowFor100(isPrecise);
544
+ afterIndexHitWindowLength = this.hitWindow.hitWindowFor100(this.isPrecise);
650
545
  break;
651
546
  default:
652
- afterIndexHitWindowLength =
653
- this.hitWindow.hitWindowFor50(isPrecise);
547
+ afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
654
548
  }
655
549
  timeAfter += afterIndexHitWindowLength;
656
- this.breakPointAccurateTimes.push({
550
+ this.breakPointAccurateTimes.push(new osuBase.BreakPoint({
657
551
  startTime: timeBefore,
658
552
  endTime: timeAfter,
659
- });
553
+ }));
660
554
  }
661
555
  }
662
556
  /**
@@ -665,50 +559,43 @@ class ThreeFingerChecker {
665
559
  * This also filters cursors that are in break period or happen before start/after end of the beatmap.
666
560
  */
667
561
  filterCursorInstances() {
668
- const objects = this.calculator.objects;
562
+ const { objects } = this.beatmap.hitObjects;
669
563
  const objectData = this.data.hitObjectData;
670
564
  const firstObjectResult = objectData[0].result;
671
565
  const lastObjectResult = objectData.at(-1).result;
672
- const isPrecise = this.calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
673
566
  // For sliders, automatically set hit window length to be as lenient as possible.
674
- let firstObjectHitWindow = this.hitWindow.hitWindowFor50(isPrecise);
675
- if (objects[0].object instanceof osuBase.Circle) {
567
+ let firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
568
+ if (objects[0] instanceof osuBase.Circle) {
676
569
  switch (firstObjectResult) {
677
570
  case exports.HitResult.great:
678
- firstObjectHitWindow =
679
- this.hitWindow.hitWindowFor300(isPrecise);
571
+ firstObjectHitWindow = this.hitWindow.hitWindowFor300(this.isPrecise);
680
572
  break;
681
573
  case exports.HitResult.good:
682
- firstObjectHitWindow =
683
- this.hitWindow.hitWindowFor100(isPrecise);
574
+ firstObjectHitWindow = this.hitWindow.hitWindowFor100(this.isPrecise);
684
575
  break;
685
576
  default:
686
- firstObjectHitWindow =
687
- this.hitWindow.hitWindowFor50(isPrecise);
577
+ firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
688
578
  }
689
579
  }
690
580
  // For sliders, automatically set hit window length to be as lenient as possible.
691
- let lastObjectHitWindow = this.hitWindow.hitWindowFor50(isPrecise);
692
- if (objects.at(-1).object instanceof osuBase.Circle) {
581
+ let lastObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
582
+ if (objects.at(-1) instanceof osuBase.Circle) {
693
583
  switch (lastObjectResult) {
694
584
  case exports.HitResult.great:
695
- lastObjectHitWindow =
696
- this.hitWindow.hitWindowFor300(isPrecise);
585
+ lastObjectHitWindow = this.hitWindow.hitWindowFor300(this.isPrecise);
697
586
  break;
698
587
  case exports.HitResult.good:
699
- lastObjectHitWindow =
700
- this.hitWindow.hitWindowFor100(isPrecise);
588
+ lastObjectHitWindow = this.hitWindow.hitWindowFor100(this.isPrecise);
701
589
  break;
702
590
  default:
703
- lastObjectHitWindow =
704
- this.hitWindow.hitWindowFor50(isPrecise);
591
+ lastObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
705
592
  }
706
593
  }
707
594
  // These hit time uses hit window length as threshold.
708
595
  // This is because cursors aren't recorded exactly at hit time,
709
596
  // probably due to the game's behavior.
710
- const firstObjectHitTime = objects[0].object.startTime - firstObjectHitWindow;
711
- const lastObjectHitTime = objects.at(-1).object.startTime + lastObjectHitWindow;
597
+ const firstObjectHitTime = objects[0].startTime - firstObjectHitWindow;
598
+ const lastObjectHitTime = objects.at(-1).startTime + lastObjectHitWindow;
712
599
  for (let i = 0; i < this.data.cursorMovement.length; ++i) {
713
600
  const cursorInstance = this.data.cursorMovement[i];
714
601
  const validOccurrences = [];
@@ -732,14 +619,13 @@ class ThreeFingerChecker {
732
619
  * detect dragged sections and improve detection speed.
733
620
  */
734
621
  getBeatmapSections() {
735
- const beatmapSections = BeatmapSectionGenerator.generateSections(this.calculator, this.minSectionObjectCount, this.maxSectionDeltaTime);
736
- for (const beatmapSection of beatmapSections) {
737
- this.beatmapSections.push(new ThreeFingerBeatmapSection({
738
- firstObjectIndex: beatmapSection.firstObjectIndex,
739
- lastObjectIndex: beatmapSection.lastObjectIndex,
622
+ for (const section of this.difficultyAttributes
623
+ .possibleThreeFingeredSections) {
624
+ this.beatmapSections.push({
625
+ ...section,
740
626
  isDragged: false,
741
627
  dragFingerIndex: -1,
742
- }));
628
+ });
743
629
  }
744
630
  }
745
631
  /**
@@ -760,48 +646,41 @@ class ThreeFingerChecker {
760
646
  * @param section The section to check.
761
647
  */
762
648
  checkDrag(section) {
763
- const objects = this.calculator.objects;
649
+ const { objects } = this.beatmap.hitObjects;
764
650
  const objectData = this.data.hitObjectData;
765
- const isPrecise = this.calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
766
651
  const firstObject = objects[section.firstObjectIndex];
767
652
  const lastObject = objects[section.lastObjectIndex];
768
- let firstObjectMinHitTime = firstObject.object.startTime;
769
- if (firstObject.object instanceof osuBase.Circle) {
653
+ let firstObjectMinHitTime = firstObject.startTime;
654
+ if (firstObject instanceof osuBase.Circle) {
770
655
  switch (objectData[section.firstObjectIndex].result) {
771
656
  case exports.HitResult.great:
772
- firstObjectMinHitTime -=
773
- this.hitWindow.hitWindowFor300(isPrecise);
657
+ firstObjectMinHitTime -= this.hitWindow.hitWindowFor300(this.isPrecise);
774
658
  break;
775
659
  case exports.HitResult.good:
776
- firstObjectMinHitTime -=
777
- this.hitWindow.hitWindowFor100(isPrecise);
660
+ firstObjectMinHitTime -= this.hitWindow.hitWindowFor100(this.isPrecise);
778
661
  break;
779
662
  default:
780
- firstObjectMinHitTime -=
781
- this.hitWindow.hitWindowFor50(isPrecise);
663
+ firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(this.isPrecise);
782
664
  }
783
665
  }
784
666
  else {
785
- firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(isPrecise);
667
+ firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(this.isPrecise);
786
668
  }
787
- let lastObjectMaxHitTime = lastObject.object.startTime;
788
- if (lastObject.object instanceof osuBase.Circle) {
669
+ let lastObjectMaxHitTime = lastObject.startTime;
670
+ if (lastObject instanceof osuBase.Circle) {
789
671
  switch (objectData[section.lastObjectIndex].result) {
790
672
  case exports.HitResult.great:
791
- lastObjectMaxHitTime +=
792
- this.hitWindow.hitWindowFor300(isPrecise);
673
+ lastObjectMaxHitTime += this.hitWindow.hitWindowFor300(this.isPrecise);
793
674
  break;
794
675
  case exports.HitResult.good:
795
- lastObjectMaxHitTime +=
796
- this.hitWindow.hitWindowFor100(isPrecise);
676
+ lastObjectMaxHitTime += this.hitWindow.hitWindowFor100(this.isPrecise);
797
677
  break;
798
678
  default:
799
- lastObjectMaxHitTime +=
800
- this.hitWindow.hitWindowFor50(isPrecise);
679
+ lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(this.isPrecise);
801
680
  }
802
681
  }
803
682
  else {
804
- lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(isPrecise);
683
+ lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(this.isPrecise);
805
684
  }
806
685
  // Since there may be more than 1 cursor instance index,
807
686
  // we check which cursor instance follows hitobjects all over.
@@ -834,22 +713,21 @@ class ThreeFingerChecker {
834
713
  * @param cursorIndexes The indexes of the cursor instance that has at least an occurrence in the section.
835
714
  */
836
715
  findDragIndex(sectionObjects, sectionReplayObjectData, cursorIndexes) {
837
- const isPrecise = this.calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
838
- const hitWindow50 = this.hitWindow.hitWindowFor50(isPrecise);
716
+ const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
839
717
  for (let i = 0; i < sectionObjects.length && cursorIndexes.every((v) => v !== -1); ++i) {
840
718
  const object = sectionObjects[i];
841
719
  const objectData = sectionReplayObjectData[i];
842
- if (object.object instanceof osuBase.Spinner ||
720
+ if (object instanceof osuBase.Spinner ||
843
721
  objectData.result === exports.HitResult.miss) {
844
722
  continue;
845
723
  }
846
724
  // Exclude sliderbreaks.
847
- if (object.object instanceof osuBase.Slider &&
725
+ if (object instanceof osuBase.Slider &&
848
726
  objectData.accuracy === Math.floor(hitWindow50) + 13) {
849
727
  continue;
850
728
  }
851
- const objectPosition = object.object.getStackedPosition(osuBase.Modes.droid);
852
- const hitTime = object.object.startTime + objectData.accuracy;
729
+ const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
730
+ const hitTime = object.startTime + objectData.accuracy;
853
731
  // Observe the cursor position at the object's hit time.
854
732
  for (let j = 0; j < cursorIndexes.length; ++j) {
855
733
  if (cursorIndexes[j] === -1) {
@@ -864,30 +742,28 @@ class ThreeFingerChecker {
864
742
  for (let k = 1; k < cursors.length; ++k) {
865
743
  const cursor = cursors[k];
866
744
  const prevCursor = cursors[k - 1];
867
- if (prevCursor.time <
868
- object.object.startTime - hitWindow50) {
745
+ if (prevCursor.time < object.startTime - hitWindow50) {
869
746
  continue;
870
747
  }
871
- if (prevCursor.time >
872
- object.object.startTime + hitWindow50) {
748
+ if (prevCursor.time > object.startTime + hitWindow50) {
873
749
  break;
874
750
  }
875
751
  let isInObject = false;
876
752
  switch (cursor.id) {
877
753
  case exports.MovementType.up:
878
754
  isInObject =
879
- prevCursor.position.getDistance(objectPosition) <= object.object.getRadius(osuBase.Modes.droid);
755
+ prevCursor.position.getDistance(objectPosition) <= object.getRadius(osuBase.Modes.droid);
880
756
  break;
881
757
  case exports.MovementType.move:
882
758
  // Interpolate movement.
883
759
  for (let mSecPassed = prevCursor.time; !isInObject &&
884
760
  mSecPassed <=
885
- Math.min(cursor.time, object.object.startTime + hitWindow50); ++mSecPassed) {
761
+ Math.min(cursor.time, object.startTime + hitWindow50); ++mSecPassed) {
886
762
  const t = (mSecPassed - prevCursor.time) /
887
763
  (cursor.time - prevCursor.time);
888
764
  const cursorPosition = new osuBase.Vector2(osuBase.Interpolation.lerp(prevCursor.position.x, cursor.position.x, t), osuBase.Interpolation.lerp(prevCursor.position.y, cursor.position.y, t));
889
765
  isInObject =
890
- objectPosition.getDistance(cursorPosition) <= object.object.getRadius(osuBase.Modes.droid);
766
+ objectPosition.getDistance(cursorPosition) <= object.getRadius(osuBase.Modes.droid);
891
767
  }
892
768
  }
893
769
  if (!isInObject) {
@@ -898,51 +774,6 @@ class ThreeFingerChecker {
898
774
  }
899
775
  return cursorIndexes.find((v) => v !== -1) ?? -1;
900
776
  }
901
- /**
902
- * Redivides the beatmap into sections.
903
- *
904
- * The result will be used to detect for three-fingered
905
- * sections.
906
- */
907
- getDetailedBeatmapSections() {
908
- const objects = this.calculator.objects;
909
- const newBeatmapSections = [];
910
- for (const beatmapSection of this.beatmapSections) {
911
- let inSpeedSection = false;
912
- let newFirstObjectIndex = beatmapSection.firstObjectIndex;
913
- for (let i = beatmapSection.firstObjectIndex; i <= beatmapSection.lastObjectIndex; ++i) {
914
- if (!inSpeedSection &&
915
- objects[i].originalTapStrain >=
916
- ThreeFingerChecker.strainThreshold) {
917
- inSpeedSection = true;
918
- newFirstObjectIndex = i;
919
- continue;
920
- }
921
- if (inSpeedSection &&
922
- objects[i].originalTapStrain <
923
- ThreeFingerChecker.strainThreshold) {
924
- inSpeedSection = false;
925
- newBeatmapSections.push({
926
- firstObjectIndex: newFirstObjectIndex,
927
- lastObjectIndex: i,
928
- isDragged: beatmapSection.isDragged,
929
- dragFingerIndex: beatmapSection.dragFingerIndex,
930
- });
931
- }
932
- }
933
- // Don't forget to manually add the last beatmap section, which would otherwise be ignored.
934
- if (inSpeedSection) {
935
- newBeatmapSections.push({
936
- firstObjectIndex: newFirstObjectIndex,
937
- lastObjectIndex: beatmapSection.lastObjectIndex,
938
- isDragged: beatmapSection.isDragged,
939
- dragFingerIndex: beatmapSection.dragFingerIndex,
940
- });
941
- }
942
- }
943
- this.beatmapSections.length = 0;
944
- this.beatmapSections.push(...newBeatmapSections);
945
- }
946
777
  /**
947
778
  * Attempts to prevent accidental taps from being flagged.
948
779
  *
@@ -955,7 +786,7 @@ class ThreeFingerChecker {
955
786
  if (filledCursorAmount <= 3) {
956
787
  return;
957
788
  }
958
- const objects = this.calculator.objects;
789
+ const { objects } = this.beatmap.hitObjects;
959
790
  const totalCursorAmount = this.downCursorInstances.reduce((acc, value) => acc + value.length, 0);
960
791
  for (let i = 0; i < this.downCursorInstances.length; ++i) {
961
792
  if (filledCursorAmount <= 3) {
@@ -979,23 +810,22 @@ class ThreeFingerChecker {
979
810
  * This check will ignore all objects with speed strain below `strainThreshold`.
980
811
  */
981
812
  calculateNerfFactors() {
982
- const objects = this.calculator.objects;
813
+ const { objects } = this.beatmap.hitObjects;
983
814
  const objectData = this.data.hitObjectData;
984
- const isPrecise = this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise);
985
815
  // We only filter cursor instances that are above the strain threshold.
986
816
  // This minimalizes the amount of cursor instances to analyze.
987
817
  for (const beatmapSection of this.beatmapSections) {
988
818
  const dragIndex = beatmapSection.dragFingerIndex;
989
- const startTime = objects[beatmapSection.firstObjectIndex].object.startTime +
819
+ const startTime = objects[beatmapSection.firstObjectIndex].startTime +
990
820
  (objectData[beatmapSection.firstObjectIndex].result !==
991
821
  exports.HitResult.miss
992
822
  ? objectData[beatmapSection.firstObjectIndex].accuracy
993
- : -this.hitWindow.hitWindowFor50(isPrecise));
994
- const endTime = objects[beatmapSection.lastObjectIndex].object.endTime +
823
+ : -this.hitWindow.hitWindowFor50(this.isPrecise));
824
+ const endTime = objects[beatmapSection.lastObjectIndex].endTime +
995
825
  (objectData[beatmapSection.lastObjectIndex].result !==
996
826
  exports.HitResult.miss
997
827
  ? objectData[beatmapSection.lastObjectIndex].accuracy
998
- : this.hitWindow.hitWindowFor50(isPrecise));
828
+ : this.hitWindow.hitWindowFor50(this.isPrecise));
999
829
  const cursorAmounts = [];
1000
830
  const cursorVectorTimes = [];
1001
831
  for (let i = 0; i < this.downCursorInstances.length; ++i) {
@@ -1071,11 +901,6 @@ class ThreeFingerChecker {
1071
901
  const objectCount = beatmapSection.lastObjectIndex -
1072
902
  beatmapSection.firstObjectIndex +
1073
903
  1;
1074
- const strainFactor = Math.pow(objects
1075
- .slice(beatmapSection.firstObjectIndex, beatmapSection.lastObjectIndex)
1076
- .reduce((acc, value) => acc +
1077
- value.originalTapStrain /
1078
- ThreeFingerChecker.strainThreshold, 0), 0.75);
1079
904
  // We can ignore the first 3 (2 for drag) filled cursor instances
1080
905
  // since they are guaranteed not 3 finger.
1081
906
  const threeFingerCursorAmounts = cursorAmounts
@@ -1097,7 +922,7 @@ class ThreeFingerChecker {
1097
922
  // Length factor applies more penalty if there are more 3-fingered object.
1098
923
  const lengthFactor = 1 + Math.pow(objectCount / this.strainNoteCount, 1.2);
1099
924
  this.nerfFactors.push({
1100
- strainFactor: Math.max(1, strainFactor),
925
+ strainFactor: Math.max(1, beatmapSection.sumStrain),
1101
926
  fingerFactor,
1102
927
  lengthFactor,
1103
928
  });
@@ -1797,48 +1622,46 @@ class TwoHandChecker {
1797
1622
  */
1798
1623
  class SliderCheeseChecker {
1799
1624
  /**
1800
- * The difficulty calculator that is being analyzed.
1625
+ * The beatmap that is being analyzed.
1801
1626
  */
1802
- calculator;
1627
+ beatmap;
1803
1628
  /**
1804
1629
  * The data of the replay.
1805
1630
  */
1806
1631
  data;
1807
1632
  /**
1808
- * The osu!droid hitwindow of the analyzed beatmap.
1633
+ * The difficulty attributes of the beatmap.
1809
1634
  */
1810
- hitWindow;
1635
+ difficultyAttributes;
1811
1636
  /**
1812
1637
  * The 50 osu!droid hit window of the analyzed beatmap.
1813
1638
  */
1814
1639
  hitWindow50;
1815
1640
  /**
1816
- * The indexes of objects that were cheesed.
1641
+ * The difficulty ratings of sliders that were cheesed.
1817
1642
  */
1818
- cheesedObjectIndexes = [];
1643
+ cheesedDifficultyRatings = [];
1819
1644
  /**
1820
- * @param calculator The difficulty calculator to analyze.
1645
+ * @param beatmap The beatmap to analyze.
1821
1646
  * @param data The data of the replay.
1647
+ * @param difficultyAttributes The difficulty attributes of the beatmap.
1822
1648
  */
1823
- constructor(calculator, data) {
1824
- this.calculator = calculator;
1649
+ constructor(beatmap, data, difficultyAttributes) {
1650
+ this.beatmap = beatmap;
1825
1651
  this.data = data;
1652
+ this.difficultyAttributes = difficultyAttributes;
1826
1653
  const stats = new osuBase.MapStats({
1827
- od: this.calculator.beatmap.difficulty.od,
1828
- mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
1654
+ od: this.beatmap.difficulty.od,
1655
+ mods: this.difficultyAttributes.mods.filter((m) => m.isApplicableToDroid() &&
1829
1656
  !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
1830
1657
  }).calculate();
1831
- this.hitWindow = new osuBase.DroidHitWindow(stats.od);
1832
- this.hitWindow50 = this.hitWindow.hitWindowFor50(calculator.mods.some((m) => m instanceof osuBase.ModPrecise));
1658
+ this.hitWindow50 = new osuBase.DroidHitWindow(stats.od).hitWindowFor50(this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise));
1833
1659
  }
1834
1660
  /**
1835
1661
  * Checks if relevant sliders in the given beatmap was cheesed.
1836
- *
1837
- * Returns a number that can be passed to a `DroidPerformanceCalculator`
1838
- * to alter the aim performance value.
1839
1662
  */
1840
1663
  check() {
1841
- if (this.calculator.attributes.sliderCount === 0) {
1664
+ if (this.difficultyAttributes.difficultSliders.length === 0) {
1842
1665
  return {
1843
1666
  aimPenalty: 1,
1844
1667
  flashlightPenalty: 1,
@@ -1854,17 +1677,15 @@ class SliderCheeseChecker {
1854
1677
  checkSliderCheesing() {
1855
1678
  // Current loop indexes are stored for efficiency.
1856
1679
  const cursorLoopIndexes = osuBase.Utils.initializeArray(10, 0);
1857
- const acceptableRadius = this.calculator.objects[0].object.getRadius(osuBase.Modes.droid) * 2;
1858
- for (let i = 0; i <
1859
- Math.min(this.data.hitObjectData.length, this.calculator.objects.length); ++i) {
1860
- const diffObject = this.calculator.objects[i];
1861
- const { object } = diffObject;
1862
- if (diffObject.travelDistance === 0 ||
1863
- !(object instanceof osuBase.Slider)) {
1680
+ const acceptableRadius = this.beatmap.hitObjects.objects[0].getRadius(osuBase.Modes.droid) * 2;
1681
+ for (const difficultSlider of this.difficultyAttributes
1682
+ .difficultSliders) {
1683
+ if (difficultSlider.index >= this.data.hitObjectData.length) {
1864
1684
  continue;
1865
1685
  }
1686
+ const object = (this.beatmap.hitObjects.objects[difficultSlider.index]);
1866
1687
  const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
1867
- const objectData = this.data.hitObjectData[i];
1688
+ const objectData = this.data.hitObjectData[difficultSlider.index];
1868
1689
  // If a slider break occurs, we disregard the check for that slider.
1869
1690
  if (objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
1870
1691
  continue;
@@ -1938,7 +1759,7 @@ class SliderCheeseChecker {
1938
1759
  }
1939
1760
  }
1940
1761
  if (isCheesed) {
1941
- this.cheesedObjectIndexes.push(i);
1762
+ this.cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
1942
1763
  }
1943
1764
  }
1944
1765
  }
@@ -1946,32 +1767,16 @@ class SliderCheeseChecker {
1946
1767
  * Calculates the slider cheese penalty.
1947
1768
  */
1948
1769
  calculateSliderCheesePenalty() {
1949
- if (this.cheesedObjectIndexes.length === 0) {
1950
- return {
1951
- aimPenalty: 1,
1952
- flashlightPenalty: 1,
1953
- visualPenalty: 1,
1954
- };
1955
- }
1956
- let calculator;
1957
- if (this.calculator instanceof osuDifficultyCalculator.DroidDifficultyCalculator) {
1958
- calculator = new osuDifficultyCalculator.DroidDifficultyCalculator(this.calculator.beatmap);
1959
- }
1960
- else {
1961
- calculator = new osuRebalanceDifficultyCalculator.DroidDifficultyCalculator(this.calculator.beatmap);
1962
- }
1963
- Object.assign(calculator.attributes, this.calculator.attributes);
1964
- calculator.mods = this.calculator.mods;
1965
- calculator.stats = this.calculator.stats;
1966
- calculator.generateDifficultyHitObjects();
1967
- for (const index of this.cheesedObjectIndexes) {
1968
- calculator.objects[index].travelDistance = 0;
1969
- }
1970
- calculator.calculateAll();
1770
+ const summedDifficultyRating = this.cheesedDifficultyRatings.reduce((a, v) => a + v, 0);
1971
1771
  return {
1972
- aimPenalty: this.calculator.aim / calculator.aim,
1973
- flashlightPenalty: this.calculator.flashlight / calculator.flashlight,
1974
- visualPenalty: this.calculator.visual / calculator.visual,
1772
+ aimPenalty: 1 -
1773
+ summedDifficultyRating * this.difficultyAttributes.sliderFactor,
1774
+ flashlightPenalty: 1 -
1775
+ summedDifficultyRating *
1776
+ this.difficultyAttributes.flashlightSliderFactor,
1777
+ visualPenalty: 1 -
1778
+ summedDifficultyRating *
1779
+ this.difficultyAttributes.visualSliderFactor,
1975
1780
  };
1976
1781
  }
1977
1782
  }
@@ -2008,6 +1813,10 @@ class ReplayAnalyzer {
2008
1813
  * The beatmap that is being analyzed. `DroidDifficultyCalculator` or `RebalanceDroidDifficultyCalculator` is required for three finger or two hand analyzing.
2009
1814
  */
2010
1815
  beatmap;
1816
+ /**
1817
+ * The difficulty attributes of the beatmap.
1818
+ */
1819
+ difficultyAttributes;
2011
1820
  /**
2012
1821
  * The results of the analyzer. `null` when initialized.
2013
1822
  */
@@ -2053,6 +1862,10 @@ class ReplayAnalyzer {
2053
1862
  constructor(values) {
2054
1863
  this.scoreID = values.scoreID;
2055
1864
  this.beatmap = values.map;
1865
+ this.difficultyAttributes = values.difficultyAttributes;
1866
+ if (this.beatmap && !(this.beatmap instanceof osuBase.Beatmap)) {
1867
+ this.difficultyAttributes = this.beatmap.attributes;
1868
+ }
2056
1869
  }
2057
1870
  /**
2058
1871
  * Analyzes a replay.
@@ -2094,10 +1907,10 @@ class ReplayAnalyzer {
2094
1907
  */
2095
1908
  decompress() {
2096
1909
  return new Promise((resolve, reject) => {
2097
- const stream$1 = new stream.Readable();
2098
- stream$1.push(this.originalODR);
2099
- stream$1.push(null);
2100
- stream$1
1910
+ const stream = new node_stream.Readable();
1911
+ stream.push(this.originalODR);
1912
+ stream.push(null);
1913
+ stream
2101
1914
  .pipe(unzipper.Parse())
2102
1915
  .on("entry", async (entry) => {
2103
1916
  const fileName = entry.path;
@@ -2489,15 +2302,15 @@ class ReplayAnalyzer {
2489
2302
  /**
2490
2303
  * Checks if a play is using 3 fingers.
2491
2304
  *
2492
- * Requires `analyze()` to be called first and `map` to be defined as `DroidDifficultyCalculator`.
2305
+ * Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
2493
2306
  */
2494
2307
  checkFor3Finger() {
2495
- if (!(this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
2496
- this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator) ||
2497
- !this.data) {
2308
+ if (!this.beatmap || !this.data || !this.difficultyAttributes) {
2498
2309
  return;
2499
2310
  }
2500
- const threeFingerChecker = new ThreeFingerChecker(this.beatmap, this.data);
2311
+ const threeFingerChecker = new ThreeFingerChecker(this.beatmap instanceof osuBase.Beatmap
2312
+ ? this.beatmap
2313
+ : this.beatmap.beatmap, this.data, this.difficultyAttributes);
2501
2314
  const result = threeFingerChecker.check();
2502
2315
  this.is3Finger = result.is3Finger;
2503
2316
  this.tapPenalty = result.penalty;
@@ -2523,15 +2336,15 @@ class ReplayAnalyzer {
2523
2336
  /**
2524
2337
  * Checks if a play has cheesed sliders.
2525
2338
  *
2526
- * Requires `analyze()` to be called first and `map` to be defined as `DroidDifficultyCalculator`.
2339
+ * Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
2527
2340
  */
2528
2341
  checkForSliderCheesing() {
2529
- if (!(this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
2530
- this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator) ||
2531
- !this.data) {
2342
+ if (!this.beatmap || !this.data || !this.difficultyAttributes) {
2532
2343
  return;
2533
2344
  }
2534
- const sliderCheeseChecker = new SliderCheeseChecker(this.beatmap, this.data);
2345
+ const sliderCheeseChecker = new SliderCheeseChecker(this.beatmap instanceof osuBase.Beatmap
2346
+ ? this.beatmap
2347
+ : this.beatmap.beatmap, this.data, this.difficultyAttributes);
2535
2348
  this.sliderCheesePenalty = sliderCheeseChecker.check();
2536
2349
  this.hasBeenCheckedForSliderCheesing = true;
2537
2350
  }