@rian8337/osu-droid-replay-analyzer 3.2.1 → 4.0.0-beta.1

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,110 +362,26 @@ 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;
466
377
  /**
467
- * The strain threshold to start detecting for 3-fingered section.
468
- *
469
- * Increasing this number will result in less sections being flagged.
378
+ * The difficulty attributes of the beatmap.
379
+ */
380
+ difficultyAttributes;
381
+ /**
382
+ * The true scale of objects.
470
383
  */
471
- static strainThreshold = 175;
384
+ trueScale;
472
385
  /**
473
386
  * The distance threshold between cursors to assume that two cursors are
474
387
  * actually pressed with 1 finger in osu!pixels.
@@ -500,22 +413,7 @@ class ThreeFingerChecker {
500
413
  */
501
414
  threeFingerRatioThreshold = 0.01;
502
415
  /**
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`.
416
+ * Extended sections of the beatmap for drag detection.
519
417
  */
520
418
  beatmapSections = [];
521
419
  /**
@@ -543,38 +441,45 @@ class ThreeFingerChecker {
543
441
  * Each index represents the cursor index.
544
442
  */
545
443
  downCursorInstances = [];
546
- /**
547
- * All cursor occurrences in the replay.
548
- *
549
- * Each index represents the cursor index.
550
- */
551
- allCursorInstances;
552
444
  /**
553
445
  * Nerf factors from all sections that were three-fingered.
554
446
  */
555
447
  nerfFactors = [];
556
448
  /**
557
- * @param calculator The difficulty calculator to analyze.
449
+ * Whether this score uses the Precise mod.
450
+ */
451
+ isPrecise;
452
+ /**
453
+ * @param beatmap The beatmap to analyze.
558
454
  * @param data The data of the replay.
455
+ * @param difficultyAttributes The difficulty attributes of the beatmap.
559
456
  */
560
- constructor(calculator, data) {
561
- this.calculator = calculator;
457
+ constructor(beatmap, data, difficultyAttributes) {
458
+ this.beatmap = beatmap;
562
459
  this.data = data;
563
- this.allCursorInstances = data.cursorMovement.map((v) => v.allOccurrences);
460
+ this.difficultyAttributes = difficultyAttributes;
564
461
  const stats = new osuBase.MapStats({
565
- od: this.calculator.beatmap.difficulty.od,
566
- mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
462
+ od: this.beatmap.difficulty.od,
463
+ mods: this.difficultyAttributes.mods.filter((m) => m.isApplicableToDroid() &&
567
464
  !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
568
465
  }).calculate();
466
+ this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
569
467
  this.hitWindow = new osuBase.DroidHitWindow(stats.od);
570
- const strainNotes = (calculator.objects).filter((v) => v.originalTapStrain >= ThreeFingerChecker.strainThreshold);
571
- this.strainNoteCount = strainNotes.length;
468
+ this.strainNoteCount =
469
+ this.difficultyAttributes.possibleThreeFingeredSections.reduce((a, v) => a + v.lastObjectIndex - v.firstObjectIndex + 1, 0);
470
+ const circleSize = new osuBase.MapStats({
471
+ cs: this.beatmap.difficulty.cs,
472
+ mods: this.difficultyAttributes.mods,
473
+ }).calculate({ mode: osuBase.Modes.droid }).cs;
474
+ this.trueScale = (1 - (0.7 * (circleSize - 5)) / 5) / 2;
572
475
  }
573
476
  /**
574
477
  * Checks whether a beatmap is eligible to be detected for 3-finger.
478
+ *
479
+ * @param difficultyAttributes The difficulty attributes of the beatmap.
575
480
  */
576
- static isEligibleToDetect(map) {
577
- return map.objects.some((v) => v.originalTapStrain >= this.strainThreshold);
481
+ static isEligibleToDetect(difficultyAttributes) {
482
+ return difficultyAttributes.possibleThreeFingeredSections.length > 0;
578
483
  }
579
484
  /**
580
485
  * Checks if the given beatmap is 3-fingered and also returns the final penalty.
@@ -597,7 +502,6 @@ class ThreeFingerChecker {
597
502
  }
598
503
  this.getBeatmapSections();
599
504
  this.detectDragPlay();
600
- this.getDetailedBeatmapSections();
601
505
  this.preventAccidentalTaps();
602
506
  if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
603
507
  return { is3Finger: false, penalty: 1 };
@@ -613,50 +517,43 @@ class ThreeFingerChecker {
613
517
  * start of the hitobject before it and do not end right at the first hitobject after it.
614
518
  */
615
519
  getAccurateBreakPoints() {
616
- const objects = this.calculator.objects;
520
+ const { objects } = this.beatmap.hitObjects;
617
521
  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;
522
+ for (const breakPoint of this.beatmap.events.breaks) {
523
+ const beforeIndex = osuBase.MathUtils.clamp(objects.findIndex((o) => o.endTime >= breakPoint.startTime) - 1, 0, objects.length - 2);
524
+ let timeBefore = objects[beforeIndex].endTime;
622
525
  // For sliders and spinners, automatically set hit window length to be as lenient as possible.
623
- let beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
526
+ let beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
624
527
  switch (objectData[beforeIndex].result) {
625
528
  case exports.HitResult.great:
626
- beforeIndexHitWindowLength =
627
- this.hitWindow.hitWindowFor300(isPrecise);
529
+ beforeIndexHitWindowLength = this.hitWindow.hitWindowFor300(this.isPrecise);
628
530
  break;
629
531
  case exports.HitResult.good:
630
- beforeIndexHitWindowLength =
631
- this.hitWindow.hitWindowFor100(isPrecise);
532
+ beforeIndexHitWindowLength = this.hitWindow.hitWindowFor100(this.isPrecise);
632
533
  break;
633
534
  default:
634
- beforeIndexHitWindowLength =
635
- this.hitWindow.hitWindowFor50(isPrecise);
535
+ beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
636
536
  }
637
537
  timeBefore += beforeIndexHitWindowLength;
638
538
  const afterIndex = beforeIndex + 1;
639
- let timeAfter = objects[afterIndex].object.startTime;
539
+ let timeAfter = objects[afterIndex].startTime;
640
540
  // For sliders and spinners, automatically set hit window length to be as lenient as possible.
641
- let afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
541
+ let afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
642
542
  switch (objectData[afterIndex].result) {
643
543
  case exports.HitResult.great:
644
- afterIndexHitWindowLength =
645
- this.hitWindow.hitWindowFor300(isPrecise);
544
+ afterIndexHitWindowLength = this.hitWindow.hitWindowFor300(this.isPrecise);
646
545
  break;
647
546
  case exports.HitResult.good:
648
- afterIndexHitWindowLength =
649
- this.hitWindow.hitWindowFor100(isPrecise);
547
+ afterIndexHitWindowLength = this.hitWindow.hitWindowFor100(this.isPrecise);
650
548
  break;
651
549
  default:
652
- afterIndexHitWindowLength =
653
- this.hitWindow.hitWindowFor50(isPrecise);
550
+ afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
654
551
  }
655
552
  timeAfter += afterIndexHitWindowLength;
656
- this.breakPointAccurateTimes.push({
553
+ this.breakPointAccurateTimes.push(new osuBase.BreakPoint({
657
554
  startTime: timeBefore,
658
555
  endTime: timeAfter,
659
- });
556
+ }));
660
557
  }
661
558
  }
662
559
  /**
@@ -665,59 +562,54 @@ class ThreeFingerChecker {
665
562
  * This also filters cursors that are in break period or happen before start/after end of the beatmap.
666
563
  */
667
564
  filterCursorInstances() {
668
- const objects = this.calculator.objects;
565
+ const { objects } = this.beatmap.hitObjects;
669
566
  const objectData = this.data.hitObjectData;
670
567
  const firstObjectResult = objectData[0].result;
671
568
  const lastObjectResult = objectData.at(-1).result;
672
- const isPrecise = this.calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
673
569
  // 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) {
570
+ let firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
571
+ if (objects[0] instanceof osuBase.Circle) {
676
572
  switch (firstObjectResult) {
677
573
  case exports.HitResult.great:
678
- firstObjectHitWindow =
679
- this.hitWindow.hitWindowFor300(isPrecise);
574
+ firstObjectHitWindow = this.hitWindow.hitWindowFor300(this.isPrecise);
680
575
  break;
681
576
  case exports.HitResult.good:
682
- firstObjectHitWindow =
683
- this.hitWindow.hitWindowFor100(isPrecise);
577
+ firstObjectHitWindow = this.hitWindow.hitWindowFor100(this.isPrecise);
684
578
  break;
685
579
  default:
686
- firstObjectHitWindow =
687
- this.hitWindow.hitWindowFor50(isPrecise);
580
+ firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
688
581
  }
689
582
  }
690
583
  // 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) {
584
+ let lastObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
585
+ if (objects.at(-1) instanceof osuBase.Circle) {
693
586
  switch (lastObjectResult) {
694
587
  case exports.HitResult.great:
695
- lastObjectHitWindow =
696
- this.hitWindow.hitWindowFor300(isPrecise);
588
+ lastObjectHitWindow = this.hitWindow.hitWindowFor300(this.isPrecise);
697
589
  break;
698
590
  case exports.HitResult.good:
699
- lastObjectHitWindow =
700
- this.hitWindow.hitWindowFor100(isPrecise);
591
+ lastObjectHitWindow = this.hitWindow.hitWindowFor100(this.isPrecise);
701
592
  break;
702
593
  default:
703
- lastObjectHitWindow =
704
- this.hitWindow.hitWindowFor50(isPrecise);
594
+ lastObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
705
595
  }
706
596
  }
707
597
  // These hit time uses hit window length as threshold.
708
598
  // This is because cursors aren't recorded exactly at hit time,
709
599
  // probably due to the game's behavior.
710
- const firstObjectHitTime = objects[0].object.startTime - firstObjectHitWindow;
711
- const lastObjectHitTime = objects.at(-1).object.startTime + lastObjectHitWindow;
600
+ const firstObjectHitTime = objects[0].startTime - firstObjectHitWindow;
601
+ const lastObjectHitTime = objects.at(-1).startTime + lastObjectHitWindow;
712
602
  for (let i = 0; i < this.data.cursorMovement.length; ++i) {
713
603
  const cursorInstance = this.data.cursorMovement[i];
714
604
  const validOccurrences = [];
715
605
  for (let j = 0; j < cursorInstance.occurrenceGroups.length; ++j) {
716
606
  const group = cursorInstance.occurrenceGroups[j];
717
- if (group.startTime < firstObjectHitTime ||
718
- group.endTime > lastObjectHitTime) {
607
+ if (group.startTime < firstObjectHitTime) {
719
608
  continue;
720
609
  }
610
+ if (group.startTime > lastObjectHitTime) {
611
+ break;
612
+ }
721
613
  if (this.breakPointAccurateTimes.some((v) => group.startTime >= v.startTime &&
722
614
  group.endTime <= v.endTime)) {
723
615
  continue;
@@ -732,14 +624,13 @@ class ThreeFingerChecker {
732
624
  * detect dragged sections and improve detection speed.
733
625
  */
734
626
  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,
627
+ for (const section of this.difficultyAttributes
628
+ .possibleThreeFingeredSections) {
629
+ this.beatmapSections.push({
630
+ ...section,
740
631
  isDragged: false,
741
632
  dragFingerIndex: -1,
742
- }));
633
+ });
743
634
  }
744
635
  }
745
636
  /**
@@ -760,48 +651,41 @@ class ThreeFingerChecker {
760
651
  * @param section The section to check.
761
652
  */
762
653
  checkDrag(section) {
763
- const objects = this.calculator.objects;
654
+ const { objects } = this.beatmap.hitObjects;
764
655
  const objectData = this.data.hitObjectData;
765
- const isPrecise = this.calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
766
656
  const firstObject = objects[section.firstObjectIndex];
767
657
  const lastObject = objects[section.lastObjectIndex];
768
- let firstObjectMinHitTime = firstObject.object.startTime;
769
- if (firstObject.object instanceof osuBase.Circle) {
658
+ let firstObjectMinHitTime = firstObject.startTime;
659
+ if (firstObject instanceof osuBase.Circle) {
770
660
  switch (objectData[section.firstObjectIndex].result) {
771
661
  case exports.HitResult.great:
772
- firstObjectMinHitTime -=
773
- this.hitWindow.hitWindowFor300(isPrecise);
662
+ firstObjectMinHitTime -= this.hitWindow.hitWindowFor300(this.isPrecise);
774
663
  break;
775
664
  case exports.HitResult.good:
776
- firstObjectMinHitTime -=
777
- this.hitWindow.hitWindowFor100(isPrecise);
665
+ firstObjectMinHitTime -= this.hitWindow.hitWindowFor100(this.isPrecise);
778
666
  break;
779
667
  default:
780
- firstObjectMinHitTime -=
781
- this.hitWindow.hitWindowFor50(isPrecise);
668
+ firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(this.isPrecise);
782
669
  }
783
670
  }
784
671
  else {
785
- firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(isPrecise);
672
+ firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(this.isPrecise);
786
673
  }
787
- let lastObjectMaxHitTime = lastObject.object.startTime;
788
- if (lastObject.object instanceof osuBase.Circle) {
674
+ let lastObjectMaxHitTime = lastObject.startTime;
675
+ if (lastObject instanceof osuBase.Circle) {
789
676
  switch (objectData[section.lastObjectIndex].result) {
790
677
  case exports.HitResult.great:
791
- lastObjectMaxHitTime +=
792
- this.hitWindow.hitWindowFor300(isPrecise);
678
+ lastObjectMaxHitTime += this.hitWindow.hitWindowFor300(this.isPrecise);
793
679
  break;
794
680
  case exports.HitResult.good:
795
- lastObjectMaxHitTime +=
796
- this.hitWindow.hitWindowFor100(isPrecise);
681
+ lastObjectMaxHitTime += this.hitWindow.hitWindowFor100(this.isPrecise);
797
682
  break;
798
683
  default:
799
- lastObjectMaxHitTime +=
800
- this.hitWindow.hitWindowFor50(isPrecise);
684
+ lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(this.isPrecise);
801
685
  }
802
686
  }
803
687
  else {
804
- lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(isPrecise);
688
+ lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(this.isPrecise);
805
689
  }
806
690
  // Since there may be more than 1 cursor instance index,
807
691
  // we check which cursor instance follows hitobjects all over.
@@ -834,22 +718,26 @@ class ThreeFingerChecker {
834
718
  * @param cursorIndexes The indexes of the cursor instance that has at least an occurrence in the section.
835
719
  */
836
720
  findDragIndex(sectionObjects, sectionReplayObjectData, cursorIndexes) {
837
- const isPrecise = this.calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
838
- const hitWindow50 = this.hitWindow.hitWindowFor50(isPrecise);
721
+ const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
839
722
  for (let i = 0; i < sectionObjects.length && cursorIndexes.every((v) => v !== -1); ++i) {
840
- const object = sectionObjects[i];
723
+ let object = sectionObjects[i];
841
724
  const objectData = sectionReplayObjectData[i];
842
- if (object.object instanceof osuBase.Spinner ||
725
+ if (object instanceof osuBase.Spinner ||
843
726
  objectData.result === exports.HitResult.miss) {
844
727
  continue;
845
728
  }
846
729
  // Exclude sliderbreaks.
847
- if (object.object instanceof osuBase.Slider &&
730
+ if (object instanceof osuBase.Slider &&
848
731
  objectData.accuracy === Math.floor(hitWindow50) + 13) {
849
732
  continue;
850
733
  }
851
- const objectPosition = object.object.getStackedPosition(osuBase.Modes.droid);
852
- const hitTime = object.object.startTime + objectData.accuracy;
734
+ if (object.droidScale !== this.trueScale) {
735
+ // Deep copy the instance so that we can assign scale.
736
+ object = osuBase.Utils.deepCopy(object);
737
+ object.droidScale = this.trueScale;
738
+ }
739
+ const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
740
+ const hitTime = object.startTime + objectData.accuracy;
853
741
  // Observe the cursor position at the object's hit time.
854
742
  for (let j = 0; j < cursorIndexes.length; ++j) {
855
743
  if (cursorIndexes[j] === -1) {
@@ -864,30 +752,28 @@ class ThreeFingerChecker {
864
752
  for (let k = 1; k < cursors.length; ++k) {
865
753
  const cursor = cursors[k];
866
754
  const prevCursor = cursors[k - 1];
867
- if (prevCursor.time <
868
- object.object.startTime - hitWindow50) {
755
+ if (prevCursor.time < object.startTime - hitWindow50) {
869
756
  continue;
870
757
  }
871
- if (prevCursor.time >
872
- object.object.startTime + hitWindow50) {
758
+ if (prevCursor.time > object.startTime + hitWindow50) {
873
759
  break;
874
760
  }
875
761
  let isInObject = false;
876
762
  switch (cursor.id) {
877
763
  case exports.MovementType.up:
878
764
  isInObject =
879
- prevCursor.position.getDistance(objectPosition) <= object.object.getRadius(osuBase.Modes.droid);
765
+ prevCursor.position.getDistance(objectPosition) <= object.getRadius(osuBase.Modes.droid);
880
766
  break;
881
767
  case exports.MovementType.move:
882
768
  // Interpolate movement.
883
769
  for (let mSecPassed = prevCursor.time; !isInObject &&
884
770
  mSecPassed <=
885
- Math.min(cursor.time, object.object.startTime + hitWindow50); ++mSecPassed) {
771
+ Math.min(cursor.time, object.startTime + hitWindow50); ++mSecPassed) {
886
772
  const t = (mSecPassed - prevCursor.time) /
887
773
  (cursor.time - prevCursor.time);
888
774
  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
775
  isInObject =
890
- objectPosition.getDistance(cursorPosition) <= object.object.getRadius(osuBase.Modes.droid);
776
+ objectPosition.getDistance(cursorPosition) <= object.getRadius(osuBase.Modes.droid);
891
777
  }
892
778
  }
893
779
  if (!isInObject) {
@@ -898,51 +784,6 @@ class ThreeFingerChecker {
898
784
  }
899
785
  return cursorIndexes.find((v) => v !== -1) ?? -1;
900
786
  }
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
787
  /**
947
788
  * Attempts to prevent accidental taps from being flagged.
948
789
  *
@@ -955,7 +796,7 @@ class ThreeFingerChecker {
955
796
  if (filledCursorAmount <= 3) {
956
797
  return;
957
798
  }
958
- const objects = this.calculator.objects;
799
+ const { objects } = this.beatmap.hitObjects;
959
800
  const totalCursorAmount = this.downCursorInstances.reduce((acc, value) => acc + value.length, 0);
960
801
  for (let i = 0; i < this.downCursorInstances.length; ++i) {
961
802
  if (filledCursorAmount <= 3) {
@@ -979,23 +820,22 @@ class ThreeFingerChecker {
979
820
  * This check will ignore all objects with speed strain below `strainThreshold`.
980
821
  */
981
822
  calculateNerfFactors() {
982
- const objects = this.calculator.objects;
823
+ const { objects } = this.beatmap.hitObjects;
983
824
  const objectData = this.data.hitObjectData;
984
- const isPrecise = this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise);
985
825
  // We only filter cursor instances that are above the strain threshold.
986
826
  // This minimalizes the amount of cursor instances to analyze.
987
827
  for (const beatmapSection of this.beatmapSections) {
988
828
  const dragIndex = beatmapSection.dragFingerIndex;
989
- const startTime = objects[beatmapSection.firstObjectIndex].object.startTime +
829
+ const startTime = objects[beatmapSection.firstObjectIndex].startTime +
990
830
  (objectData[beatmapSection.firstObjectIndex].result !==
991
831
  exports.HitResult.miss
992
832
  ? objectData[beatmapSection.firstObjectIndex].accuracy
993
- : -this.hitWindow.hitWindowFor50(isPrecise));
994
- const endTime = objects[beatmapSection.lastObjectIndex].object.endTime +
833
+ : -this.hitWindow.hitWindowFor50(this.isPrecise));
834
+ const endTime = objects[beatmapSection.lastObjectIndex].endTime +
995
835
  (objectData[beatmapSection.lastObjectIndex].result !==
996
836
  exports.HitResult.miss
997
837
  ? objectData[beatmapSection.lastObjectIndex].accuracy
998
- : this.hitWindow.hitWindowFor50(isPrecise));
838
+ : this.hitWindow.hitWindowFor50(this.isPrecise));
999
839
  const cursorAmounts = [];
1000
840
  const cursorVectorTimes = [];
1001
841
  for (let i = 0; i < this.downCursorInstances.length; ++i) {
@@ -1071,11 +911,6 @@ class ThreeFingerChecker {
1071
911
  const objectCount = beatmapSection.lastObjectIndex -
1072
912
  beatmapSection.firstObjectIndex +
1073
913
  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
914
  // We can ignore the first 3 (2 for drag) filled cursor instances
1080
915
  // since they are guaranteed not 3 finger.
1081
916
  const threeFingerCursorAmounts = cursorAmounts
@@ -1097,7 +932,7 @@ class ThreeFingerChecker {
1097
932
  // Length factor applies more penalty if there are more 3-fingered object.
1098
933
  const lengthFactor = 1 + Math.pow(objectCount / this.strainNoteCount, 1.2);
1099
934
  this.nerfFactors.push({
1100
- strainFactor: Math.max(1, strainFactor),
935
+ strainFactor: Math.max(1, beatmapSection.sumStrain),
1101
936
  fingerFactor,
1102
937
  lengthFactor,
1103
938
  });
@@ -1797,48 +1632,46 @@ class TwoHandChecker {
1797
1632
  */
1798
1633
  class SliderCheeseChecker {
1799
1634
  /**
1800
- * The difficulty calculator that is being analyzed.
1635
+ * The beatmap that is being analyzed.
1801
1636
  */
1802
- calculator;
1637
+ beatmap;
1803
1638
  /**
1804
1639
  * The data of the replay.
1805
1640
  */
1806
1641
  data;
1807
1642
  /**
1808
- * The osu!droid hitwindow of the analyzed beatmap.
1643
+ * The difficulty attributes of the beatmap.
1809
1644
  */
1810
- hitWindow;
1645
+ difficultyAttributes;
1811
1646
  /**
1812
1647
  * The 50 osu!droid hit window of the analyzed beatmap.
1813
1648
  */
1814
1649
  hitWindow50;
1815
1650
  /**
1816
- * The indexes of objects that were cheesed.
1651
+ * The difficulty ratings of sliders that were cheesed.
1817
1652
  */
1818
- cheesedObjectIndexes = [];
1653
+ cheesedDifficultyRatings = [];
1819
1654
  /**
1820
- * @param calculator The difficulty calculator to analyze.
1655
+ * @param beatmap The beatmap to analyze.
1821
1656
  * @param data The data of the replay.
1657
+ * @param difficultyAttributes The difficulty attributes of the beatmap.
1822
1658
  */
1823
- constructor(calculator, data) {
1824
- this.calculator = calculator;
1659
+ constructor(beatmap, data, difficultyAttributes) {
1660
+ this.beatmap = beatmap;
1825
1661
  this.data = data;
1662
+ this.difficultyAttributes = difficultyAttributes;
1826
1663
  const stats = new osuBase.MapStats({
1827
- od: this.calculator.beatmap.difficulty.od,
1828
- mods: this.calculator.mods.filter((m) => m.isApplicableToDroid() &&
1664
+ od: this.beatmap.difficulty.od,
1665
+ mods: this.difficultyAttributes.mods.filter((m) => m.isApplicableToDroid() &&
1829
1666
  !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
1830
1667
  }).calculate();
1831
- this.hitWindow = new osuBase.DroidHitWindow(stats.od);
1832
- this.hitWindow50 = this.hitWindow.hitWindowFor50(calculator.mods.some((m) => m instanceof osuBase.ModPrecise));
1668
+ this.hitWindow50 = new osuBase.DroidHitWindow(stats.od).hitWindowFor50(this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise));
1833
1669
  }
1834
1670
  /**
1835
1671
  * 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
1672
  */
1840
1673
  check() {
1841
- if (this.calculator.attributes.sliderCount === 0) {
1674
+ if (this.difficultyAttributes.difficultSliders.length === 0) {
1842
1675
  return {
1843
1676
  aimPenalty: 1,
1844
1677
  flashlightPenalty: 1,
@@ -1854,21 +1687,29 @@ class SliderCheeseChecker {
1854
1687
  checkSliderCheesing() {
1855
1688
  // Current loop indexes are stored for efficiency.
1856
1689
  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)) {
1690
+ const circleSize = new osuBase.MapStats({
1691
+ cs: this.beatmap.difficulty.cs,
1692
+ mods: this.difficultyAttributes.mods,
1693
+ }).calculate({ mode: osuBase.Modes.droid }).cs;
1694
+ const scale = (1 - (0.7 * (circleSize - 5)) / 5) / 2;
1695
+ const acceptableRadius = 128 * scale;
1696
+ for (const difficultSlider of this.difficultyAttributes
1697
+ .difficultSliders) {
1698
+ if (difficultSlider.index >= this.data.hitObjectData.length) {
1864
1699
  continue;
1865
1700
  }
1866
- const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
1867
- const objectData = this.data.hitObjectData[i];
1701
+ const objectData = this.data.hitObjectData[difficultSlider.index];
1868
1702
  // If a slider break occurs, we disregard the check for that slider.
1869
1703
  if (objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
1870
1704
  continue;
1871
1705
  }
1706
+ let object = (this.beatmap.hitObjects.objects[difficultSlider.index]);
1707
+ if (object.droidScale !== scale) {
1708
+ // Deep clone the object so that we can assign scale properly.
1709
+ object = osuBase.Utils.deepCopy(object);
1710
+ object.droidScale = scale;
1711
+ }
1712
+ const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
1872
1713
  let isCheesed = false;
1873
1714
  for (let j = 0; j < this.data.cursorMovement.length; ++j) {
1874
1715
  if (isCheesed) {
@@ -1899,7 +1740,12 @@ class SliderCheeseChecker {
1899
1740
  if (!tickWasHit) {
1900
1741
  continue;
1901
1742
  }
1902
- const nestedObject = object.nestedHitObjects[l];
1743
+ let nestedObject = object.nestedHitObjects[l];
1744
+ if (nestedObject.droidScale !== scale) {
1745
+ // Deep clone the object so that we can assign scale properly
1746
+ nestedObject = osuBase.Utils.deepCopy(nestedObject);
1747
+ nestedObject.droidScale = scale;
1748
+ }
1903
1749
  const nestedPosition = nestedObject.getStackedPosition(osuBase.Modes.droid);
1904
1750
  while (occurrenceLoopIndex < allOccurrences.length &&
1905
1751
  allOccurrences[occurrenceLoopIndex].time <
@@ -1938,7 +1784,7 @@ class SliderCheeseChecker {
1938
1784
  }
1939
1785
  }
1940
1786
  if (isCheesed) {
1941
- this.cheesedObjectIndexes.push(i);
1787
+ this.cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
1942
1788
  }
1943
1789
  }
1944
1790
  }
@@ -1946,32 +1792,17 @@ class SliderCheeseChecker {
1946
1792
  * Calculates the slider cheese penalty.
1947
1793
  */
1948
1794
  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();
1795
+ const summedDifficultyRating = this.cheesedDifficultyRatings.reduce((a, v) => a + v, 0);
1971
1796
  return {
1972
- aimPenalty: this.calculator.aim / calculator.aim,
1973
- flashlightPenalty: this.calculator.flashlight / calculator.flashlight,
1974
- visualPenalty: this.calculator.visual / calculator.visual,
1797
+ aimPenalty: Math.pow(1 -
1798
+ summedDifficultyRating *
1799
+ this.difficultyAttributes.sliderFactor, 3),
1800
+ flashlightPenalty: Math.pow(1 -
1801
+ summedDifficultyRating *
1802
+ this.difficultyAttributes.flashlightSliderFactor, 3),
1803
+ visualPenalty: Math.pow(1 -
1804
+ summedDifficultyRating *
1805
+ this.difficultyAttributes.visualSliderFactor, 3),
1975
1806
  };
1976
1807
  }
1977
1808
  }
@@ -2008,6 +1839,10 @@ class ReplayAnalyzer {
2008
1839
  * The beatmap that is being analyzed. `DroidDifficultyCalculator` or `RebalanceDroidDifficultyCalculator` is required for three finger or two hand analyzing.
2009
1840
  */
2010
1841
  beatmap;
1842
+ /**
1843
+ * The difficulty attributes of the beatmap.
1844
+ */
1845
+ difficultyAttributes;
2011
1846
  /**
2012
1847
  * The results of the analyzer. `null` when initialized.
2013
1848
  */
@@ -2053,6 +1888,10 @@ class ReplayAnalyzer {
2053
1888
  constructor(values) {
2054
1889
  this.scoreID = values.scoreID;
2055
1890
  this.beatmap = values.map;
1891
+ this.difficultyAttributes = values.difficultyAttributes;
1892
+ if (this.beatmap && !(this.beatmap instanceof osuBase.Beatmap)) {
1893
+ this.difficultyAttributes = this.beatmap.attributes;
1894
+ }
2056
1895
  }
2057
1896
  /**
2058
1897
  * Analyzes a replay.
@@ -2094,10 +1933,10 @@ class ReplayAnalyzer {
2094
1933
  */
2095
1934
  decompress() {
2096
1935
  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
1936
+ const stream = new node_stream.Readable();
1937
+ stream.push(this.originalODR);
1938
+ stream.push(null);
1939
+ stream
2101
1940
  .pipe(unzipper.Parse())
2102
1941
  .on("entry", async (entry) => {
2103
1942
  const fileName = entry.path;
@@ -2489,15 +2328,15 @@ class ReplayAnalyzer {
2489
2328
  /**
2490
2329
  * Checks if a play is using 3 fingers.
2491
2330
  *
2492
- * Requires `analyze()` to be called first and `map` to be defined as `DroidDifficultyCalculator`.
2331
+ * Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
2493
2332
  */
2494
2333
  checkFor3Finger() {
2495
- if (!(this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
2496
- this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator) ||
2497
- !this.data) {
2334
+ if (!this.beatmap || !this.data || !this.difficultyAttributes) {
2498
2335
  return;
2499
2336
  }
2500
- const threeFingerChecker = new ThreeFingerChecker(this.beatmap, this.data);
2337
+ const threeFingerChecker = new ThreeFingerChecker(this.beatmap instanceof osuBase.Beatmap
2338
+ ? this.beatmap
2339
+ : this.beatmap.beatmap, this.data, this.difficultyAttributes);
2501
2340
  const result = threeFingerChecker.check();
2502
2341
  this.is3Finger = result.is3Finger;
2503
2342
  this.tapPenalty = result.penalty;
@@ -2523,15 +2362,15 @@ class ReplayAnalyzer {
2523
2362
  /**
2524
2363
  * Checks if a play has cheesed sliders.
2525
2364
  *
2526
- * Requires `analyze()` to be called first and `map` to be defined as `DroidDifficultyCalculator`.
2365
+ * Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
2527
2366
  */
2528
2367
  checkForSliderCheesing() {
2529
- if (!(this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
2530
- this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator) ||
2531
- !this.data) {
2368
+ if (!this.beatmap || !this.data || !this.difficultyAttributes) {
2532
2369
  return;
2533
2370
  }
2534
- const sliderCheeseChecker = new SliderCheeseChecker(this.beatmap, this.data);
2371
+ const sliderCheeseChecker = new SliderCheeseChecker(this.beatmap instanceof osuBase.Beatmap
2372
+ ? this.beatmap
2373
+ : this.beatmap.beatmap, this.data, this.difficultyAttributes);
2535
2374
  this.sliderCheesePenalty = sliderCheeseChecker.check();
2536
2375
  this.hasBeenCheckedForSliderCheesing = true;
2537
2376
  }