@rian8337/osu-droid-replay-analyzer 4.0.0-beta.76 → 4.0.0-beta.78

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
@@ -709,6 +709,222 @@ class RebalanceThreeFingerChecker {
709
709
  }
710
710
  }
711
711
 
712
+ /**
713
+ * Utility to check whether relevant sliders in a beatmap are cheesed for rebalance scores..
714
+ */
715
+ class RebalanceSliderCheeseChecker {
716
+ /**
717
+ * @param beatmap The beatmap to analyze.
718
+ * @param data The data of the replay.
719
+ * @param difficultyAttributes The difficulty attributes of the beatmap.
720
+ */
721
+ constructor(beatmap, data, difficultyAttributes) {
722
+ this.beatmap = beatmap;
723
+ this.data = data;
724
+ this.difficultyAttributes = difficultyAttributes;
725
+ this.hitWindow50 = difficultyAttributes.mods.has(osuBase.ModPrecise)
726
+ ? new osuBase.PreciseDroidHitWindow(beatmap.difficulty.od).mehWindow
727
+ : new osuBase.DroidHitWindow(beatmap.difficulty.od).mehWindow;
728
+ this.isHardRock = difficultyAttributes.mods.has(osuBase.ModHardRock);
729
+ }
730
+ /**
731
+ * Checks if relevant sliders in the given beatmap was cheesed.
732
+ */
733
+ check() {
734
+ if (this.difficultyAttributes.difficultSliders.length === 0 ||
735
+ (this.difficultyAttributes.sliderFactor === 1 &&
736
+ this.difficultyAttributes.flashlightSliderFactor === 1)) {
737
+ return {
738
+ aimPenalty: 1,
739
+ flashlightPenalty: 1,
740
+ };
741
+ }
742
+ const cheesedDifficultyRatings = this.checkSliderCheesing();
743
+ return this.calculateSliderCheesePenalty(cheesedDifficultyRatings);
744
+ }
745
+ /**
746
+ * Checks for sliders that were cheesed.
747
+ */
748
+ checkSliderCheesing() {
749
+ const { objects } = this.beatmap.hitObjects;
750
+ const cheesedDifficultyRatings = [];
751
+ // Current loop indices are stored for efficiency.
752
+ const cursorLoopIndices = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
753
+ const acceptableRadius = objects[0].radius * 2;
754
+ // Sort difficult sliders by index so that cursor loop indices work properly.
755
+ for (const difficultSlider of this.difficultyAttributes.difficultSliders
756
+ .slice()
757
+ .sort((a, b) => a.index - b.index)) {
758
+ if (difficultSlider.index >= this.data.hitObjectData.length) {
759
+ continue;
760
+ }
761
+ const object = objects[difficultSlider.index];
762
+ const objectData = this.data.hitObjectData[difficultSlider.index];
763
+ // If a miss or slider break occurs, we disregard the check for that slider.
764
+ if (objectData.result === exports.HitResult.miss ||
765
+ -this.hitWindow50 > objectData.accuracy ||
766
+ objectData.accuracy >
767
+ Math.min(this.hitWindow50, object.duration)) {
768
+ continue;
769
+ }
770
+ const objectStartPosition = object.stackedPosition;
771
+ // These time boundaries should consider the delta time between the previous and next
772
+ // object as well as their hit accuracy. However, they are somewhat complicated to
773
+ // compute and the accuracy gain is small. As such, let's settle with 50 hit window.
774
+ const minTimeLimit = object.startTime - this.hitWindow50;
775
+ const maxTimeLimit = object.startTime + this.hitWindow50;
776
+ // Get the closest tap distance across all cursors.
777
+ const closestDistances = [];
778
+ const closestGroupIndices = [];
779
+ for (let i = 0; i < this.data.cursorMovement.length; ++i) {
780
+ const cursorGroups = this.data.cursorMovement[i].occurrenceGroups;
781
+ let closestDistance = Number.POSITIVE_INFINITY;
782
+ let closestIndex = cursorGroups.length;
783
+ for (let j = cursorLoopIndices[i]; j < cursorGroups.length; j = ++cursorLoopIndices[i]) {
784
+ const group = cursorGroups[j];
785
+ if (group.endTime < minTimeLimit) {
786
+ continue;
787
+ }
788
+ if (group.startTime > maxTimeLimit) {
789
+ break;
790
+ }
791
+ if (group.startTime >= minTimeLimit) {
792
+ const position = this.getCursorPosition(group.down);
793
+ const distance = position.getDistance(objectStartPosition);
794
+ if (closestDistance > distance) {
795
+ closestDistance = distance;
796
+ closestIndex = j;
797
+ }
798
+ if (closestDistance <= acceptableRadius / 2) {
799
+ break;
800
+ }
801
+ }
802
+ // Normally, we check if there are cursor presses within the group's active time.
803
+ // However, some funky workarounds are used throughout the game for replays, so
804
+ // for the time being we only check for cursor distances across the group.
805
+ const { allOccurrences } = group;
806
+ for (let k = 1; k < allOccurrences.length; ++k) {
807
+ const cursor = allOccurrences[k];
808
+ const prevCursor = allOccurrences[k - 1];
809
+ let distance = Number.POSITIVE_INFINITY;
810
+ const currentPosition = this.getCursorPosition(cursor);
811
+ const prevPosition = this.getCursorPosition(prevCursor);
812
+ switch (cursor.id) {
813
+ case exports.MovementType.up:
814
+ distance =
815
+ prevPosition.getDistance(objectStartPosition);
816
+ break;
817
+ case exports.MovementType.move:
818
+ for (let mSecPassed = Math.max(prevCursor.time, minTimeLimit); mSecPassed <=
819
+ Math.min(cursor.time, maxTimeLimit); ++mSecPassed) {
820
+ const t = (mSecPassed - prevCursor.time) /
821
+ (cursor.time - prevCursor.time);
822
+ const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
823
+ distance =
824
+ cursorPosition.getDistance(objectStartPosition);
825
+ if (closestDistance > distance) {
826
+ closestDistance = distance;
827
+ closestIndex = j;
828
+ }
829
+ if (closestDistance <=
830
+ acceptableRadius / 2) {
831
+ break;
832
+ }
833
+ }
834
+ }
835
+ if (closestDistance > distance) {
836
+ closestDistance = distance;
837
+ closestIndex = j;
838
+ }
839
+ if (closestDistance <= acceptableRadius / 2) {
840
+ break;
841
+ }
842
+ }
843
+ }
844
+ closestDistances.push(closestDistance);
845
+ closestGroupIndices.push(closestIndex);
846
+ if (cursorLoopIndices[i] > 0) {
847
+ // Decrement the index. The previous group may also have a role on the next slider.
848
+ --cursorLoopIndices[i];
849
+ }
850
+ }
851
+ const cursorIndex = closestDistances.indexOf(Math.min(...closestDistances));
852
+ const closestDistance = closestDistances[cursorIndex];
853
+ if (closestDistance > acceptableRadius / 2) {
854
+ cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
855
+ continue;
856
+ }
857
+ const group = this.data.cursorMovement[cursorIndex].occurrenceGroups[closestGroupIndices[cursorIndex]];
858
+ let isCheesed = false;
859
+ // Track cursor movement to see if it lands on every tick.
860
+ let occurrenceLoopIndex = 1;
861
+ const { allOccurrences } = group;
862
+ for (let i = 1; i < object.nestedHitObjects.length; ++i) {
863
+ if (isCheesed) {
864
+ break;
865
+ }
866
+ const tickWasHit = objectData.tickset[i - 1];
867
+ if (!tickWasHit) {
868
+ continue;
869
+ }
870
+ const nestedObject = object.nestedHitObjects[i];
871
+ const nestedPosition = nestedObject.stackedPosition;
872
+ while (occurrenceLoopIndex < allOccurrences.length &&
873
+ allOccurrences[occurrenceLoopIndex].time <
874
+ nestedObject.startTime) {
875
+ ++occurrenceLoopIndex;
876
+ }
877
+ if (occurrenceLoopIndex === allOccurrences.length) {
878
+ continue;
879
+ }
880
+ const cursor = allOccurrences[occurrenceLoopIndex];
881
+ const prevCursor = allOccurrences[occurrenceLoopIndex - 1];
882
+ const currentPosition = this.getCursorPosition(cursor);
883
+ const prevPosition = this.getCursorPosition(prevCursor);
884
+ switch (cursor.id) {
885
+ case exports.MovementType.move: {
886
+ // Interpolate cursor position during nested object time.
887
+ const t = (nestedObject.startTime - prevCursor.time) /
888
+ (cursor.time - prevCursor.time);
889
+ const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
890
+ const distance = cursorPosition.getDistance(nestedPosition);
891
+ isCheesed = distance > acceptableRadius;
892
+ break;
893
+ }
894
+ case exports.MovementType.up:
895
+ isCheesed =
896
+ prevPosition.getDistance(nestedPosition) >
897
+ acceptableRadius;
898
+ }
899
+ }
900
+ if (isCheesed) {
901
+ cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
902
+ }
903
+ }
904
+ return cheesedDifficultyRatings;
905
+ }
906
+ /**
907
+ * Calculates the slider cheese penalty.
908
+ */
909
+ calculateSliderCheesePenalty(cheesedDifficultyRatings) {
910
+ const summedDifficultyRating = Math.min(1, cheesedDifficultyRatings.reduce((a, v) => a + v, 0));
911
+ return {
912
+ aimPenalty: Math.max(this.difficultyAttributes.sliderFactor, Math.pow(1 -
913
+ summedDifficultyRating *
914
+ this.difficultyAttributes.sliderFactor, 2)),
915
+ flashlightPenalty: Math.max(this.difficultyAttributes.flashlightSliderFactor, Math.pow(1 -
916
+ summedDifficultyRating *
917
+ this.difficultyAttributes.flashlightSliderFactor, 2)),
918
+ };
919
+ }
920
+ getCursorPosition(cursor) {
921
+ if (this.isHardRock) {
922
+ return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
923
+ }
924
+ return cursor.position;
925
+ }
926
+ }
927
+
712
928
  /******************************************************************************
713
929
  Copyright (c) Microsoft Corporation.
714
930
 
@@ -742,7 +958,7 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
742
958
  };
743
959
 
744
960
  /**
745
- * Utility to check whether relevant sliders in a beatmap are cheesed.
961
+ * Utility to check whether relevant sliders in a beatmap are cheesed for live scores.
746
962
  */
747
963
  class SliderCheeseChecker {
748
964
  /**
@@ -765,12 +981,10 @@ class SliderCheeseChecker {
765
981
  check() {
766
982
  if (this.difficultyAttributes.difficultSliders.length === 0 ||
767
983
  (this.difficultyAttributes.sliderFactor === 1 &&
768
- this.difficultyAttributes.flashlightSliderFactor === 1 &&
769
- this.difficultyAttributes.visualSliderFactor === 1)) {
984
+ this.difficultyAttributes.flashlightSliderFactor === 1)) {
770
985
  return {
771
986
  aimPenalty: 1,
772
987
  flashlightPenalty: 1,
773
- visualPenalty: 1,
774
988
  };
775
989
  }
776
990
  const cheesedDifficultyRatings = this.checkSliderCheesing();
@@ -949,9 +1163,6 @@ class SliderCheeseChecker {
949
1163
  flashlightPenalty: Math.max(this.difficultyAttributes.flashlightSliderFactor, Math.pow(1 -
950
1164
  summedDifficultyRating *
951
1165
  this.difficultyAttributes.flashlightSliderFactor, 2)),
952
- visualPenalty: Math.max(this.difficultyAttributes.visualSliderFactor, Math.pow(1 -
953
- summedDifficultyRating *
954
- this.difficultyAttributes.visualSliderFactor, 2)),
955
1166
  };
956
1167
  }
957
1168
  getCursorPosition(cursor) {
@@ -1494,7 +1705,7 @@ class TwoHandChecker {
1494
1705
  // );
1495
1706
  // }
1496
1707
  return {
1497
- is2Hand: 0 > this.attributes.aimNoteCount * 0.15,
1708
+ is2Hand: false, // 0 > this.attributes.aimNoteCount * 0.15,
1498
1709
  twoHandedNoteCount: 0,
1499
1710
  };
1500
1711
  }
@@ -2076,7 +2287,6 @@ class ReplayAnalyzer {
2076
2287
  this.sliderCheesePenalty = {
2077
2288
  aimPenalty: 1,
2078
2289
  flashlightPenalty: 1,
2079
- visualPenalty: 1,
2080
2290
  };
2081
2291
  /**
2082
2292
  * Whether this replay has been checked against 3 finger usage.
@@ -2342,7 +2552,9 @@ class ReplayAnalyzer {
2342
2552
  return;
2343
2553
  }
2344
2554
  (_a = this.playableBeatmap) !== null && _a !== void 0 ? _a : (this.playableBeatmap = this.constructPlayableBeatmap());
2345
- const sliderCheeseChecker = new SliderCheeseChecker(this.playableBeatmap, this.data, this.difficultyAttributes);
2555
+ const sliderCheeseChecker = this.difficultyAttributes.mode === "rebalance"
2556
+ ? new RebalanceSliderCheeseChecker(this.playableBeatmap, this.data, this.difficultyAttributes)
2557
+ : new SliderCheeseChecker(this.playableBeatmap, this.data, this.difficultyAttributes);
2346
2558
  this.sliderCheesePenalty = sliderCheeseChecker.check();
2347
2559
  this.hasBeenCheckedForSliderCheesing = true;
2348
2560
  }
@@ -2725,6 +2937,7 @@ class ReplayObjectData {
2725
2937
  exports.CursorData = CursorData;
2726
2938
  exports.CursorOccurrence = CursorOccurrence;
2727
2939
  exports.CursorOccurrenceGroup = CursorOccurrenceGroup;
2940
+ exports.RebalanceSliderCheeseChecker = RebalanceSliderCheeseChecker;
2728
2941
  exports.RebalanceThreeFingerChecker = RebalanceThreeFingerChecker;
2729
2942
  exports.ReplayAnalyzer = ReplayAnalyzer;
2730
2943
  exports.ReplayData = ReplayData;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rian8337/osu-droid-replay-analyzer",
3
- "version": "4.0.0-beta.76",
3
+ "version": "4.0.0-beta.78",
4
4
  "description": "A replay analyzer for analyzing osu!droid replay files.",
5
5
  "keywords": [
6
6
  "osu",
@@ -34,9 +34,9 @@
34
34
  "url": "https://github.com/Rian8337/osu-droid-module/issues"
35
35
  },
36
36
  "dependencies": {
37
- "@rian8337/osu-base": "^4.0.0-beta.74",
38
- "@rian8337/osu-difficulty-calculator": "^4.0.0-beta.76",
39
- "@rian8337/osu-rebalance-difficulty-calculator": "^4.0.0-beta.76",
37
+ "@rian8337/osu-base": "^4.0.0-beta.78",
38
+ "@rian8337/osu-difficulty-calculator": "^4.0.0-beta.78",
39
+ "@rian8337/osu-rebalance-difficulty-calculator": "^4.0.0-beta.78",
40
40
  "java-deserialization": "^0.1.0",
41
41
  "unzipper": "^0.10.11"
42
42
  },
@@ -46,5 +46,5 @@
46
46
  "publishConfig": {
47
47
  "access": "public"
48
48
  },
49
- "gitHead": "d843f2cf65e2e44c87654a2be675d6ed925193ea"
49
+ "gitHead": "9b8c41ad3aeb0b98cb0818c9169ad3ad4e98b95e"
50
50
  }
@@ -720,10 +720,48 @@ interface SliderCheeseInformation {
720
720
  * The value used to penalize the flashlight performance value, from 0 to 1.
721
721
  */
722
722
  flashlightPenalty: number;
723
+ }
724
+
725
+ /**
726
+ * Utility to check whether relevant sliders in a beatmap are cheesed for rebalance scores..
727
+ */
728
+ declare class RebalanceSliderCheeseChecker {
729
+ /**
730
+ * The beatmap that is being analyzed.
731
+ */
732
+ readonly beatmap: DroidPlayableBeatmap;
733
+ /**
734
+ * The data of the replay.
735
+ */
736
+ readonly data: ReplayData;
737
+ /**
738
+ * The difficulty attributes of the beatmap.
739
+ */
740
+ readonly difficultyAttributes: IExtendedDroidDifficultyAttributes;
741
+ /**
742
+ * The 50 osu!droid hit window of the analyzed beatmap.
743
+ */
744
+ private readonly hitWindow50;
745
+ private readonly isHardRock;
746
+ /**
747
+ * @param beatmap The beatmap to analyze.
748
+ * @param data The data of the replay.
749
+ * @param difficultyAttributes The difficulty attributes of the beatmap.
750
+ */
751
+ constructor(beatmap: DroidPlayableBeatmap, data: ReplayData, difficultyAttributes: IExtendedDroidDifficultyAttributes);
752
+ /**
753
+ * Checks if relevant sliders in the given beatmap was cheesed.
754
+ */
755
+ check(): SliderCheeseInformation;
756
+ /**
757
+ * Checks for sliders that were cheesed.
758
+ */
759
+ private checkSliderCheesing;
723
760
  /**
724
- * The value used to penalize the visual performance value, from 0 to 1.
761
+ * Calculates the slider cheese penalty.
725
762
  */
726
- visualPenalty: number;
763
+ private calculateSliderCheesePenalty;
764
+ private getCursorPosition;
727
765
  }
728
766
 
729
767
  /**
@@ -919,7 +957,7 @@ declare class ReplayAnalyzer {
919
957
  }
920
958
 
921
959
  /**
922
- * Utility to check whether relevant sliders in a beatmap are cheesed.
960
+ * Utility to check whether relevant sliders in a beatmap are cheesed for live scores.
923
961
  */
924
962
  declare class SliderCheeseChecker {
925
963
  /**
@@ -933,7 +971,7 @@ declare class SliderCheeseChecker {
933
971
  /**
934
972
  * The difficulty attributes of the beatmap.
935
973
  */
936
- readonly difficultyAttributes: IExtendedDroidDifficultyAttributes$1 | IExtendedDroidDifficultyAttributes;
974
+ readonly difficultyAttributes: IExtendedDroidDifficultyAttributes$1;
937
975
  /**
938
976
  * The 50 osu!droid hit window of the analyzed beatmap.
939
977
  */
@@ -944,7 +982,7 @@ declare class SliderCheeseChecker {
944
982
  * @param data The data of the replay.
945
983
  * @param difficultyAttributes The difficulty attributes of the beatmap.
946
984
  */
947
- constructor(beatmap: DroidPlayableBeatmap, data: ReplayData, difficultyAttributes: IExtendedDroidDifficultyAttributes$1 | IExtendedDroidDifficultyAttributes);
985
+ constructor(beatmap: DroidPlayableBeatmap, data: ReplayData, difficultyAttributes: IExtendedDroidDifficultyAttributes$1);
948
986
  /**
949
987
  * Checks if relevant sliders in the given beatmap was cheesed.
950
988
  */
@@ -1161,4 +1199,4 @@ declare class TwoHandChecker {
1161
1199
  private getCursorPosition;
1162
1200
  }
1163
1201
 
1164
- export { CursorData, type CursorInformation, CursorOccurrence, CursorOccurrenceGroup, type ExportedReplayJSON, type ExportedReplayJSONDataV1, type ExportedReplayJSONDataV2, type ExportedReplayJSONDataV3, type ExportedReplayJSONV1, type ExportedReplayJSONV2, type ExportedReplayJSONV3, type HitErrorInformation, HitResult, MovementType, RebalanceThreeFingerChecker, ReplayAnalyzer, ReplayData, ReplayObjectData, ReplayV3Data, SliderCheeseChecker, type SliderCheeseInformation, type SliderHitInformation, type SliderNestedHitObjectInformation, ThreeFingerChecker, type ThreeFingerInformation, TwoHandChecker, type TwoHandInformation };
1202
+ export { CursorData, type CursorInformation, CursorOccurrence, CursorOccurrenceGroup, type ExportedReplayJSON, type ExportedReplayJSONDataV1, type ExportedReplayJSONDataV2, type ExportedReplayJSONDataV3, type ExportedReplayJSONV1, type ExportedReplayJSONV2, type ExportedReplayJSONV3, type HitErrorInformation, HitResult, MovementType, RebalanceSliderCheeseChecker, RebalanceThreeFingerChecker, ReplayAnalyzer, ReplayData, ReplayObjectData, ReplayV3Data, SliderCheeseChecker, type SliderCheeseInformation, type SliderHitInformation, type SliderNestedHitObjectInformation, ThreeFingerChecker, type ThreeFingerInformation, TwoHandChecker, type TwoHandInformation };