@rian8337/osu-droid-replay-analyzer 4.0.0-beta.76 → 4.0.0-beta.77
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 +223 -2
- package/package.json +5 -5
- package/typings/index.d.ts +46 -4
package/dist/index.js
CHANGED
|
@@ -709,6 +709,224 @@ 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
|
+
visualPenalty: 1,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
const cheesedDifficultyRatings = this.checkSliderCheesing();
|
|
744
|
+
return this.calculateSliderCheesePenalty(cheesedDifficultyRatings);
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Checks for sliders that were cheesed.
|
|
748
|
+
*/
|
|
749
|
+
checkSliderCheesing() {
|
|
750
|
+
const { objects } = this.beatmap.hitObjects;
|
|
751
|
+
const cheesedDifficultyRatings = [];
|
|
752
|
+
// Current loop indices are stored for efficiency.
|
|
753
|
+
const cursorLoopIndices = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
|
|
754
|
+
const acceptableRadius = objects[0].radius * 2;
|
|
755
|
+
// Sort difficult sliders by index so that cursor loop indices work properly.
|
|
756
|
+
for (const difficultSlider of this.difficultyAttributes.difficultSliders
|
|
757
|
+
.slice()
|
|
758
|
+
.sort((a, b) => a.index - b.index)) {
|
|
759
|
+
if (difficultSlider.index >= this.data.hitObjectData.length) {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const object = objects[difficultSlider.index];
|
|
763
|
+
const objectData = this.data.hitObjectData[difficultSlider.index];
|
|
764
|
+
// If a miss or slider break occurs, we disregard the check for that slider.
|
|
765
|
+
if (objectData.result === exports.HitResult.miss ||
|
|
766
|
+
-this.hitWindow50 > objectData.accuracy ||
|
|
767
|
+
objectData.accuracy >
|
|
768
|
+
Math.min(this.hitWindow50, object.duration)) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const objectStartPosition = object.stackedPosition;
|
|
772
|
+
// These time boundaries should consider the delta time between the previous and next
|
|
773
|
+
// object as well as their hit accuracy. However, they are somewhat complicated to
|
|
774
|
+
// compute and the accuracy gain is small. As such, let's settle with 50 hit window.
|
|
775
|
+
const minTimeLimit = object.startTime - this.hitWindow50;
|
|
776
|
+
const maxTimeLimit = object.startTime + this.hitWindow50;
|
|
777
|
+
// Get the closest tap distance across all cursors.
|
|
778
|
+
const closestDistances = [];
|
|
779
|
+
const closestGroupIndices = [];
|
|
780
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
781
|
+
const cursorGroups = this.data.cursorMovement[i].occurrenceGroups;
|
|
782
|
+
let closestDistance = Number.POSITIVE_INFINITY;
|
|
783
|
+
let closestIndex = cursorGroups.length;
|
|
784
|
+
for (let j = cursorLoopIndices[i]; j < cursorGroups.length; j = ++cursorLoopIndices[i]) {
|
|
785
|
+
const group = cursorGroups[j];
|
|
786
|
+
if (group.endTime < minTimeLimit) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (group.startTime > maxTimeLimit) {
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
if (group.startTime >= minTimeLimit) {
|
|
793
|
+
const position = this.getCursorPosition(group.down);
|
|
794
|
+
const distance = position.getDistance(objectStartPosition);
|
|
795
|
+
if (closestDistance > distance) {
|
|
796
|
+
closestDistance = distance;
|
|
797
|
+
closestIndex = j;
|
|
798
|
+
}
|
|
799
|
+
if (closestDistance <= acceptableRadius / 2) {
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// Normally, we check if there are cursor presses within the group's active time.
|
|
804
|
+
// However, some funky workarounds are used throughout the game for replays, so
|
|
805
|
+
// for the time being we only check for cursor distances across the group.
|
|
806
|
+
const { allOccurrences } = group;
|
|
807
|
+
for (let k = 1; k < allOccurrences.length; ++k) {
|
|
808
|
+
const cursor = allOccurrences[k];
|
|
809
|
+
const prevCursor = allOccurrences[k - 1];
|
|
810
|
+
let distance = Number.POSITIVE_INFINITY;
|
|
811
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
812
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
813
|
+
switch (cursor.id) {
|
|
814
|
+
case exports.MovementType.up:
|
|
815
|
+
distance =
|
|
816
|
+
prevPosition.getDistance(objectStartPosition);
|
|
817
|
+
break;
|
|
818
|
+
case exports.MovementType.move:
|
|
819
|
+
for (let mSecPassed = Math.max(prevCursor.time, minTimeLimit); mSecPassed <=
|
|
820
|
+
Math.min(cursor.time, maxTimeLimit); ++mSecPassed) {
|
|
821
|
+
const t = (mSecPassed - prevCursor.time) /
|
|
822
|
+
(cursor.time - prevCursor.time);
|
|
823
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
824
|
+
distance =
|
|
825
|
+
cursorPosition.getDistance(objectStartPosition);
|
|
826
|
+
if (closestDistance > distance) {
|
|
827
|
+
closestDistance = distance;
|
|
828
|
+
closestIndex = j;
|
|
829
|
+
}
|
|
830
|
+
if (closestDistance <=
|
|
831
|
+
acceptableRadius / 2) {
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (closestDistance > distance) {
|
|
837
|
+
closestDistance = distance;
|
|
838
|
+
closestIndex = j;
|
|
839
|
+
}
|
|
840
|
+
if (closestDistance <= acceptableRadius / 2) {
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
closestDistances.push(closestDistance);
|
|
846
|
+
closestGroupIndices.push(closestIndex);
|
|
847
|
+
if (cursorLoopIndices[i] > 0) {
|
|
848
|
+
// Decrement the index. The previous group may also have a role on the next slider.
|
|
849
|
+
--cursorLoopIndices[i];
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const cursorIndex = closestDistances.indexOf(Math.min(...closestDistances));
|
|
853
|
+
const closestDistance = closestDistances[cursorIndex];
|
|
854
|
+
if (closestDistance > acceptableRadius / 2) {
|
|
855
|
+
cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
const group = this.data.cursorMovement[cursorIndex].occurrenceGroups[closestGroupIndices[cursorIndex]];
|
|
859
|
+
let isCheesed = false;
|
|
860
|
+
// Track cursor movement to see if it lands on every tick.
|
|
861
|
+
let occurrenceLoopIndex = 1;
|
|
862
|
+
const { allOccurrences } = group;
|
|
863
|
+
for (let i = 1; i < object.nestedHitObjects.length; ++i) {
|
|
864
|
+
if (isCheesed) {
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
const tickWasHit = objectData.tickset[i - 1];
|
|
868
|
+
if (!tickWasHit) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const nestedObject = object.nestedHitObjects[i];
|
|
872
|
+
const nestedPosition = nestedObject.stackedPosition;
|
|
873
|
+
while (occurrenceLoopIndex < allOccurrences.length &&
|
|
874
|
+
allOccurrences[occurrenceLoopIndex].time <
|
|
875
|
+
nestedObject.startTime) {
|
|
876
|
+
++occurrenceLoopIndex;
|
|
877
|
+
}
|
|
878
|
+
if (occurrenceLoopIndex === allOccurrences.length) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
const cursor = allOccurrences[occurrenceLoopIndex];
|
|
882
|
+
const prevCursor = allOccurrences[occurrenceLoopIndex - 1];
|
|
883
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
884
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
885
|
+
switch (cursor.id) {
|
|
886
|
+
case exports.MovementType.move: {
|
|
887
|
+
// Interpolate cursor position during nested object time.
|
|
888
|
+
const t = (nestedObject.startTime - prevCursor.time) /
|
|
889
|
+
(cursor.time - prevCursor.time);
|
|
890
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
891
|
+
const distance = cursorPosition.getDistance(nestedPosition);
|
|
892
|
+
isCheesed = distance > acceptableRadius;
|
|
893
|
+
break;
|
|
894
|
+
}
|
|
895
|
+
case exports.MovementType.up:
|
|
896
|
+
isCheesed =
|
|
897
|
+
prevPosition.getDistance(nestedPosition) >
|
|
898
|
+
acceptableRadius;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (isCheesed) {
|
|
902
|
+
cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return cheesedDifficultyRatings;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Calculates the slider cheese penalty.
|
|
909
|
+
*/
|
|
910
|
+
calculateSliderCheesePenalty(cheesedDifficultyRatings) {
|
|
911
|
+
const summedDifficultyRating = Math.min(1, cheesedDifficultyRatings.reduce((a, v) => a + v, 0));
|
|
912
|
+
return {
|
|
913
|
+
aimPenalty: Math.max(this.difficultyAttributes.sliderFactor, Math.pow(1 -
|
|
914
|
+
summedDifficultyRating *
|
|
915
|
+
this.difficultyAttributes.sliderFactor, 2)),
|
|
916
|
+
flashlightPenalty: Math.max(this.difficultyAttributes.flashlightSliderFactor, Math.pow(1 -
|
|
917
|
+
summedDifficultyRating *
|
|
918
|
+
this.difficultyAttributes.flashlightSliderFactor, 2)),
|
|
919
|
+
visualPenalty: 1,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
getCursorPosition(cursor) {
|
|
923
|
+
if (this.isHardRock) {
|
|
924
|
+
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
925
|
+
}
|
|
926
|
+
return cursor.position;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
712
930
|
/******************************************************************************
|
|
713
931
|
Copyright (c) Microsoft Corporation.
|
|
714
932
|
|
|
@@ -742,7 +960,7 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
742
960
|
};
|
|
743
961
|
|
|
744
962
|
/**
|
|
745
|
-
* Utility to check whether relevant sliders in a beatmap are cheesed.
|
|
963
|
+
* Utility to check whether relevant sliders in a beatmap are cheesed for live scores.
|
|
746
964
|
*/
|
|
747
965
|
class SliderCheeseChecker {
|
|
748
966
|
/**
|
|
@@ -2342,7 +2560,9 @@ class ReplayAnalyzer {
|
|
|
2342
2560
|
return;
|
|
2343
2561
|
}
|
|
2344
2562
|
(_a = this.playableBeatmap) !== null && _a !== void 0 ? _a : (this.playableBeatmap = this.constructPlayableBeatmap());
|
|
2345
|
-
const sliderCheeseChecker =
|
|
2563
|
+
const sliderCheeseChecker = this.difficultyAttributes.mode === "rebalance"
|
|
2564
|
+
? new RebalanceSliderCheeseChecker(this.playableBeatmap, this.data, this.difficultyAttributes)
|
|
2565
|
+
: new SliderCheeseChecker(this.playableBeatmap, this.data, this.difficultyAttributes);
|
|
2346
2566
|
this.sliderCheesePenalty = sliderCheeseChecker.check();
|
|
2347
2567
|
this.hasBeenCheckedForSliderCheesing = true;
|
|
2348
2568
|
}
|
|
@@ -2725,6 +2945,7 @@ class ReplayObjectData {
|
|
|
2725
2945
|
exports.CursorData = CursorData;
|
|
2726
2946
|
exports.CursorOccurrence = CursorOccurrence;
|
|
2727
2947
|
exports.CursorOccurrenceGroup = CursorOccurrenceGroup;
|
|
2948
|
+
exports.RebalanceSliderCheeseChecker = RebalanceSliderCheeseChecker;
|
|
2728
2949
|
exports.RebalanceThreeFingerChecker = RebalanceThreeFingerChecker;
|
|
2729
2950
|
exports.ReplayAnalyzer = ReplayAnalyzer;
|
|
2730
2951
|
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.
|
|
3
|
+
"version": "4.0.0-beta.77",
|
|
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.
|
|
38
|
-
"@rian8337/osu-difficulty-calculator": "^4.0.0-beta.
|
|
39
|
-
"@rian8337/osu-rebalance-difficulty-calculator": "^4.0.0-beta.
|
|
37
|
+
"@rian8337/osu-base": "^4.0.0-beta.77",
|
|
38
|
+
"@rian8337/osu-difficulty-calculator": "^4.0.0-beta.77",
|
|
39
|
+
"@rian8337/osu-rebalance-difficulty-calculator": "^4.0.0-beta.77",
|
|
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": "
|
|
49
|
+
"gitHead": "13ad454ea657dbd5821d0857c4da6dc7ab0fe965"
|
|
50
50
|
}
|
package/typings/index.d.ts
CHANGED
|
@@ -726,6 +726,48 @@ interface SliderCheeseInformation {
|
|
|
726
726
|
visualPenalty: number;
|
|
727
727
|
}
|
|
728
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Utility to check whether relevant sliders in a beatmap are cheesed for rebalance scores..
|
|
731
|
+
*/
|
|
732
|
+
declare class RebalanceSliderCheeseChecker {
|
|
733
|
+
/**
|
|
734
|
+
* The beatmap that is being analyzed.
|
|
735
|
+
*/
|
|
736
|
+
readonly beatmap: DroidPlayableBeatmap;
|
|
737
|
+
/**
|
|
738
|
+
* The data of the replay.
|
|
739
|
+
*/
|
|
740
|
+
readonly data: ReplayData;
|
|
741
|
+
/**
|
|
742
|
+
* The difficulty attributes of the beatmap.
|
|
743
|
+
*/
|
|
744
|
+
readonly difficultyAttributes: IExtendedDroidDifficultyAttributes;
|
|
745
|
+
/**
|
|
746
|
+
* The 50 osu!droid hit window of the analyzed beatmap.
|
|
747
|
+
*/
|
|
748
|
+
private readonly hitWindow50;
|
|
749
|
+
private readonly isHardRock;
|
|
750
|
+
/**
|
|
751
|
+
* @param beatmap The beatmap to analyze.
|
|
752
|
+
* @param data The data of the replay.
|
|
753
|
+
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
754
|
+
*/
|
|
755
|
+
constructor(beatmap: DroidPlayableBeatmap, data: ReplayData, difficultyAttributes: IExtendedDroidDifficultyAttributes);
|
|
756
|
+
/**
|
|
757
|
+
* Checks if relevant sliders in the given beatmap was cheesed.
|
|
758
|
+
*/
|
|
759
|
+
check(): SliderCheeseInformation;
|
|
760
|
+
/**
|
|
761
|
+
* Checks for sliders that were cheesed.
|
|
762
|
+
*/
|
|
763
|
+
private checkSliderCheesing;
|
|
764
|
+
/**
|
|
765
|
+
* Calculates the slider cheese penalty.
|
|
766
|
+
*/
|
|
767
|
+
private calculateSliderCheesePenalty;
|
|
768
|
+
private getCursorPosition;
|
|
769
|
+
}
|
|
770
|
+
|
|
729
771
|
/**
|
|
730
772
|
* Represents hit information about sliders in a beatmap.
|
|
731
773
|
*/
|
|
@@ -919,7 +961,7 @@ declare class ReplayAnalyzer {
|
|
|
919
961
|
}
|
|
920
962
|
|
|
921
963
|
/**
|
|
922
|
-
* Utility to check whether relevant sliders in a beatmap are cheesed.
|
|
964
|
+
* Utility to check whether relevant sliders in a beatmap are cheesed for live scores.
|
|
923
965
|
*/
|
|
924
966
|
declare class SliderCheeseChecker {
|
|
925
967
|
/**
|
|
@@ -933,7 +975,7 @@ declare class SliderCheeseChecker {
|
|
|
933
975
|
/**
|
|
934
976
|
* The difficulty attributes of the beatmap.
|
|
935
977
|
*/
|
|
936
|
-
readonly difficultyAttributes: IExtendedDroidDifficultyAttributes$1
|
|
978
|
+
readonly difficultyAttributes: IExtendedDroidDifficultyAttributes$1;
|
|
937
979
|
/**
|
|
938
980
|
* The 50 osu!droid hit window of the analyzed beatmap.
|
|
939
981
|
*/
|
|
@@ -944,7 +986,7 @@ declare class SliderCheeseChecker {
|
|
|
944
986
|
* @param data The data of the replay.
|
|
945
987
|
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
946
988
|
*/
|
|
947
|
-
constructor(beatmap: DroidPlayableBeatmap, data: ReplayData, difficultyAttributes: IExtendedDroidDifficultyAttributes$1
|
|
989
|
+
constructor(beatmap: DroidPlayableBeatmap, data: ReplayData, difficultyAttributes: IExtendedDroidDifficultyAttributes$1);
|
|
948
990
|
/**
|
|
949
991
|
* Checks if relevant sliders in the given beatmap was cheesed.
|
|
950
992
|
*/
|
|
@@ -1161,4 +1203,4 @@ declare class TwoHandChecker {
|
|
|
1161
1203
|
private getCursorPosition;
|
|
1162
1204
|
}
|
|
1163
1205
|
|
|
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 };
|
|
1206
|
+
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 };
|