@rian8337/osu-droid-replay-analyzer 4.0.0-beta.19 → 4.0.0-beta.20
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 +165 -251
- package/package.json +4 -4
- package/typings/index.d.ts +12 -49
package/dist/index.js
CHANGED
|
@@ -368,9 +368,6 @@ class RebalanceThreeFingerChecker {
|
|
|
368
368
|
return { is3Finger: false, penalty: 1 };
|
|
369
369
|
}
|
|
370
370
|
this.getBeatmapSections();
|
|
371
|
-
if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
|
|
372
|
-
return { is3Finger: false, penalty: 1 };
|
|
373
|
-
}
|
|
374
371
|
this.calculateNerfFactors();
|
|
375
372
|
const finalPenalty = this.calculateFinalPenalty();
|
|
376
373
|
return { is3Finger: finalPenalty > 1, penalty: finalPenalty };
|
|
@@ -834,25 +831,6 @@ class ThreeFingerChecker {
|
|
|
834
831
|
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
835
832
|
*/
|
|
836
833
|
constructor(beatmap, data, difficultyAttributes) {
|
|
837
|
-
/**
|
|
838
|
-
* The distance threshold between cursors to assume that two cursors are
|
|
839
|
-
* actually pressed with 1 finger in osu!pixels.
|
|
840
|
-
*
|
|
841
|
-
* This is used to prevent cases where a player would lift their finger
|
|
842
|
-
* too fast to the point where the 4th cursor instance or beyond is recorded
|
|
843
|
-
* as 1st, 2nd, or 3rd cursor instance.
|
|
844
|
-
*/
|
|
845
|
-
this.cursorDistancingDistanceThreshold = 60;
|
|
846
|
-
/**
|
|
847
|
-
* The threshold for the amount of cursors that are assumed to be pressed
|
|
848
|
-
* by a single finger.
|
|
849
|
-
*/
|
|
850
|
-
this.cursorDistancingCountThreshold = 10;
|
|
851
|
-
/**
|
|
852
|
-
* The threshold for the time difference of cursors that are assumed to be pressed
|
|
853
|
-
* by a single finger, in milliseconds.
|
|
854
|
-
*/
|
|
855
|
-
this.cursorDistancingTimeThreshold = 1000;
|
|
856
834
|
/**
|
|
857
835
|
* The ratio threshold between non-3 finger cursors and 3-finger cursors.
|
|
858
836
|
*
|
|
@@ -864,14 +842,6 @@ class ThreeFingerChecker {
|
|
|
864
842
|
* Extended sections of the beatmap for drag detection.
|
|
865
843
|
*/
|
|
866
844
|
this.beatmapSections = [];
|
|
867
|
-
/**
|
|
868
|
-
* This threshold is used to filter out accidental taps.
|
|
869
|
-
*
|
|
870
|
-
* Increasing this number makes the filtration more sensitive, however it
|
|
871
|
-
* will also increase the chance of 3-fingered plays getting out from
|
|
872
|
-
* being flagged.
|
|
873
|
-
*/
|
|
874
|
-
this.accidentalTapThreshold = 400;
|
|
875
845
|
/**
|
|
876
846
|
* A reprocessed break points to match right on object time.
|
|
877
847
|
*
|
|
@@ -899,8 +869,7 @@ class ThreeFingerChecker {
|
|
|
899
869
|
}).overallDifficulty;
|
|
900
870
|
this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
901
871
|
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
902
|
-
this.
|
|
903
|
-
this.difficultyAttributes.possibleThreeFingeredSections.reduce((a, v) => a + v.lastObjectIndex - v.firstObjectIndex + 1, 0);
|
|
872
|
+
this.hitObjects = beatmap.hitObjects.objects;
|
|
904
873
|
}
|
|
905
874
|
/**
|
|
906
875
|
* Checks whether a beatmap is eligible to be detected for 3-finger.
|
|
@@ -921,7 +890,7 @@ class ThreeFingerChecker {
|
|
|
921
890
|
* nerf factor, taking beatmap difficulty into account.
|
|
922
891
|
*/
|
|
923
892
|
check() {
|
|
924
|
-
if (this.
|
|
893
|
+
if (this.difficultyAttributes.possibleThreeFingeredSections.length === 0) {
|
|
925
894
|
return { is3Finger: false, penalty: 1 };
|
|
926
895
|
}
|
|
927
896
|
this.getAccurateBreakPoints();
|
|
@@ -930,11 +899,6 @@ class ThreeFingerChecker {
|
|
|
930
899
|
return { is3Finger: false, penalty: 1 };
|
|
931
900
|
}
|
|
932
901
|
this.getBeatmapSections();
|
|
933
|
-
this.detectDragPlay();
|
|
934
|
-
this.preventAccidentalTaps();
|
|
935
|
-
if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
|
|
936
|
-
return { is3Finger: false, penalty: 1 };
|
|
937
|
-
}
|
|
938
902
|
this.calculateNerfFactors();
|
|
939
903
|
const finalPenalty = this.calculateFinalPenalty();
|
|
940
904
|
return { is3Finger: finalPenalty > 1, penalty: finalPenalty };
|
|
@@ -943,42 +907,31 @@ class ThreeFingerChecker {
|
|
|
943
907
|
* Generates a new set of "accurate break points".
|
|
944
908
|
*
|
|
945
909
|
* This is done to increase detection accuracy since break points do not start right at the
|
|
946
|
-
*
|
|
910
|
+
* end of the hitobject before it and do not end right at the first hitobject after it.
|
|
947
911
|
*/
|
|
948
912
|
getAccurateBreakPoints() {
|
|
949
|
-
const { objects } = this.beatmap.hitObjects;
|
|
950
913
|
const objectData = this.data.hitObjectData;
|
|
951
914
|
for (const breakPoint of this.beatmap.events.breaks) {
|
|
952
|
-
const beforeIndex = osuBase.MathUtils.clamp(
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
let
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
default:
|
|
964
|
-
beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
915
|
+
const beforeIndex = osuBase.MathUtils.clamp(this.hitObjects.findIndex((o) => o.endTime >= breakPoint.startTime) - 1, 0, this.hitObjects.length - 2);
|
|
916
|
+
const objectBefore = this.hitObjects[beforeIndex];
|
|
917
|
+
const objectBeforeData = objectData[beforeIndex];
|
|
918
|
+
let timeBefore = objectBefore.endTime;
|
|
919
|
+
if (objectBefore instanceof osuBase.Circle) {
|
|
920
|
+
if (objectBeforeData.result !== exports.HitResult.miss) {
|
|
921
|
+
timeBefore += objectBeforeData.accuracy;
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
timeBefore += this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
925
|
+
}
|
|
965
926
|
}
|
|
966
|
-
timeBefore += beforeIndexHitWindowLength;
|
|
967
927
|
const afterIndex = beforeIndex + 1;
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
let
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
break;
|
|
975
|
-
case exports.HitResult.good:
|
|
976
|
-
afterIndexHitWindowLength = this.hitWindow.hitWindowFor100(this.isPrecise);
|
|
977
|
-
break;
|
|
978
|
-
default:
|
|
979
|
-
afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
928
|
+
const objectAfter = this.hitObjects[afterIndex];
|
|
929
|
+
const objectAfterData = objectData[afterIndex];
|
|
930
|
+
let timeAfter = this.hitObjects[afterIndex].startTime;
|
|
931
|
+
if (objectAfter instanceof osuBase.Circle &&
|
|
932
|
+
objectAfterData.result !== exports.HitResult.miss) {
|
|
933
|
+
timeAfter += objectAfterData.accuracy;
|
|
980
934
|
}
|
|
981
|
-
timeAfter += afterIndexHitWindowLength;
|
|
982
935
|
this.breakPointAccurateTimes.push(new osuBase.BreakPoint({
|
|
983
936
|
startTime: timeBefore,
|
|
984
937
|
endTime: timeAfter,
|
|
@@ -991,12 +944,11 @@ class ThreeFingerChecker {
|
|
|
991
944
|
* This also filters cursors that are in break period or happen before start/after end of the beatmap.
|
|
992
945
|
*/
|
|
993
946
|
filterCursorInstances() {
|
|
994
|
-
const { objects } = this.beatmap.hitObjects;
|
|
995
947
|
const objectData = this.data.hitObjectData;
|
|
996
|
-
const firstObject = objects[0];
|
|
997
|
-
const lastObject = objects.at(-1);
|
|
998
948
|
const firstObjectResult = objectData[0].result;
|
|
999
949
|
const lastObjectResult = objectData.at(-1).result;
|
|
950
|
+
const firstObject = this.hitObjects[0];
|
|
951
|
+
const lastObject = this.hitObjects.at(-1);
|
|
1000
952
|
// For sliders, automatically set hit window length to be as lenient as possible.
|
|
1001
953
|
let firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1002
954
|
if (firstObject instanceof osuBase.Circle) {
|
|
@@ -1025,16 +977,17 @@ class ThreeFingerChecker {
|
|
|
1025
977
|
lastObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1026
978
|
}
|
|
1027
979
|
}
|
|
980
|
+
else if (lastObject instanceof osuBase.Slider) {
|
|
981
|
+
lastObjectHitWindow = Math.min(lastObject.spanDuration, lastObjectHitWindow);
|
|
982
|
+
}
|
|
1028
983
|
// These hit time uses hit window length as threshold.
|
|
1029
984
|
// This is because cursors aren't recorded exactly at hit time,
|
|
1030
985
|
// probably due to the game's behavior.
|
|
1031
986
|
const firstObjectHitTime = firstObject.startTime - firstObjectHitWindow;
|
|
1032
987
|
const lastObjectHitTime = lastObject.startTime + lastObjectHitWindow;
|
|
1033
|
-
for (
|
|
1034
|
-
const cursorInstance = this.data.cursorMovement[i];
|
|
988
|
+
for (const cursorInstance of this.data.cursorMovement) {
|
|
1035
989
|
const validOccurrences = [];
|
|
1036
|
-
for (
|
|
1037
|
-
const group = cursorInstance.occurrenceGroups[j];
|
|
990
|
+
for (const group of cursorInstance.occurrenceGroups) {
|
|
1038
991
|
if (group.startTime < firstObjectHitTime) {
|
|
1039
992
|
continue;
|
|
1040
993
|
}
|
|
@@ -1055,20 +1008,93 @@ class ThreeFingerChecker {
|
|
|
1055
1008
|
* detect dragged sections and improve detection speed.
|
|
1056
1009
|
*/
|
|
1057
1010
|
getBeatmapSections() {
|
|
1011
|
+
const cursorLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
1058
1012
|
for (const section of this.difficultyAttributes
|
|
1059
1013
|
.possibleThreeFingeredSections) {
|
|
1060
|
-
this.
|
|
1014
|
+
const dragFingerIndex = this.findDragIndex(section);
|
|
1015
|
+
const objects = [];
|
|
1016
|
+
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
1017
|
+
objects.push(Object.assign({ object: this.hitObjects[i] }, this.getObjectPressIndex(this.hitObjects[i], this.data.hitObjectData[i], cursorLookupIndices, dragFingerIndex)));
|
|
1018
|
+
}
|
|
1019
|
+
this.beatmapSections.push(Object.assign(Object.assign({}, section), { dragFingerIndex: dragFingerIndex, objects: objects }));
|
|
1061
1020
|
}
|
|
1062
1021
|
}
|
|
1063
1022
|
/**
|
|
1064
|
-
*
|
|
1023
|
+
* Obtains the index of the nearest cursor of which an object was pressed in terms of time.
|
|
1024
|
+
*
|
|
1025
|
+
* @param object The object to obtain the index for.
|
|
1026
|
+
* @param objectData The hit data of the object.
|
|
1027
|
+
* @param cursorLookupIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
1028
|
+
* @param excludedIndices The cursor indices that should not be checked.
|
|
1029
|
+
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
1065
1030
|
*/
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1031
|
+
getObjectPressIndex(object, objectData, cursorLookupIndices, ...excludedCursorIndices) {
|
|
1032
|
+
if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
|
|
1033
|
+
return {
|
|
1034
|
+
pressingCursorInstanceIndex: -1,
|
|
1035
|
+
pressingCursorIndex: -1,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1039
|
+
// Check for sliderbreaks and treat them as misses.
|
|
1040
|
+
if (object instanceof osuBase.Slider &&
|
|
1041
|
+
objectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
1042
|
+
return {
|
|
1043
|
+
pressingCursorInstanceIndex: -1,
|
|
1044
|
+
pressingCursorIndex: -1,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
// We are not directly using hit time to determine which cursor pressed the object
|
|
1048
|
+
// to account for time difference between hit registration and object judgement.
|
|
1049
|
+
let minHitTime = object.startTime;
|
|
1050
|
+
let maxHitTime = object.startTime;
|
|
1051
|
+
if (object instanceof osuBase.Circle) {
|
|
1052
|
+
let hitWindowGap = hitWindow50;
|
|
1053
|
+
switch (objectData.result) {
|
|
1054
|
+
case exports.HitResult.great:
|
|
1055
|
+
hitWindowGap = this.hitWindow.hitWindowFor300(this.isPrecise);
|
|
1056
|
+
break;
|
|
1057
|
+
case exports.HitResult.good:
|
|
1058
|
+
hitWindowGap = this.hitWindow.hitWindowFor100(this.isPrecise);
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
minHitTime -= hitWindowGap;
|
|
1062
|
+
maxHitTime += hitWindowGap;
|
|
1063
|
+
}
|
|
1064
|
+
else if (object instanceof osuBase.Slider) {
|
|
1065
|
+
minHitTime -= hitWindow50;
|
|
1066
|
+
maxHitTime += Math.min(hitWindow50, object.spanDuration);
|
|
1067
|
+
}
|
|
1068
|
+
const hitTime = object.startTime + objectData.accuracy;
|
|
1069
|
+
let nearestCursorInstanceIndex = null;
|
|
1070
|
+
let nearestCursorIndex = null;
|
|
1071
|
+
let nearestTime = Number.POSITIVE_INFINITY;
|
|
1072
|
+
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
1073
|
+
if (excludedCursorIndices.includes(i)) {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
const cursors = this.downCursorInstances[i];
|
|
1077
|
+
for (let j = cursorLookupIndices[i]; j < cursors.length; cursorLookupIndices[i] = ++j) {
|
|
1078
|
+
const cursor = cursors[j];
|
|
1079
|
+
if (cursor.time < minHitTime) {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (cursor.time > maxHitTime) {
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1085
|
+
const deltaTime = Math.abs(hitTime - cursor.time);
|
|
1086
|
+
if (deltaTime > nearestTime) {
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
nearestCursorInstanceIndex = i;
|
|
1090
|
+
nearestCursorIndex = j;
|
|
1091
|
+
nearestTime = deltaTime;
|
|
1092
|
+
}
|
|
1071
1093
|
}
|
|
1094
|
+
return {
|
|
1095
|
+
pressingCursorInstanceIndex: nearestCursorInstanceIndex !== null && nearestCursorInstanceIndex !== void 0 ? nearestCursorInstanceIndex : -1,
|
|
1096
|
+
pressingCursorIndex: nearestCursorIndex !== null && nearestCursorIndex !== void 0 ? nearestCursorIndex : -1,
|
|
1097
|
+
};
|
|
1072
1098
|
}
|
|
1073
1099
|
/**
|
|
1074
1100
|
* Checks if a section is dragged and returns the index of the drag finger.
|
|
@@ -1077,11 +1103,11 @@ class ThreeFingerChecker {
|
|
|
1077
1103
|
*
|
|
1078
1104
|
* @param section The section to check.
|
|
1079
1105
|
*/
|
|
1080
|
-
|
|
1081
|
-
|
|
1106
|
+
findDragIndex(section) {
|
|
1107
|
+
var _a;
|
|
1082
1108
|
const objectData = this.data.hitObjectData;
|
|
1083
|
-
const firstObject =
|
|
1084
|
-
const lastObject =
|
|
1109
|
+
const firstObject = this.hitObjects[section.firstObjectIndex];
|
|
1110
|
+
const lastObject = this.hitObjects[section.lastObjectIndex];
|
|
1085
1111
|
let firstObjectMinHitTime = firstObject.startTime;
|
|
1086
1112
|
if (firstObject instanceof osuBase.Circle) {
|
|
1087
1113
|
switch (objectData[section.firstObjectIndex].result) {
|
|
@@ -1095,7 +1121,7 @@ class ThreeFingerChecker {
|
|
|
1095
1121
|
firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1096
1122
|
}
|
|
1097
1123
|
}
|
|
1098
|
-
else {
|
|
1124
|
+
else if (firstObject instanceof osuBase.Slider) {
|
|
1099
1125
|
firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1100
1126
|
}
|
|
1101
1127
|
let lastObjectMaxHitTime = lastObject.startTime;
|
|
@@ -1111,12 +1137,12 @@ class ThreeFingerChecker {
|
|
|
1111
1137
|
lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1112
1138
|
}
|
|
1113
1139
|
}
|
|
1114
|
-
else {
|
|
1115
|
-
lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1140
|
+
else if (lastObject instanceof osuBase.Slider) {
|
|
1141
|
+
lastObjectMaxHitTime += Math.min(this.hitWindow.hitWindowFor50(this.isPrecise), lastObject.spanDuration);
|
|
1116
1142
|
}
|
|
1117
1143
|
// Since there may be more than 1 cursor instance index,
|
|
1118
1144
|
// we check which cursor instance follows hitobjects all over.
|
|
1119
|
-
const
|
|
1145
|
+
const cursorIndices = [];
|
|
1120
1146
|
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1121
1147
|
const c = this.data.cursorMovement[i];
|
|
1122
1148
|
if (c.occurrenceGroups.length === 0) {
|
|
@@ -1133,21 +1159,12 @@ class ThreeFingerChecker {
|
|
|
1133
1159
|
0) {
|
|
1134
1160
|
continue;
|
|
1135
1161
|
}
|
|
1136
|
-
|
|
1162
|
+
cursorIndices.push(i);
|
|
1137
1163
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
/**
|
|
1141
|
-
* Finds the drag index of the section.
|
|
1142
|
-
*
|
|
1143
|
-
* @param sectionObjects The objects in the section.
|
|
1144
|
-
* @param sectionReplayObjectData The hitobject data of all objects in the section.
|
|
1145
|
-
* @param cursorIndexes The indexes of the cursor instance that has at least an occurrence in the section.
|
|
1146
|
-
*/
|
|
1147
|
-
findDragIndex(sectionObjects, sectionReplayObjectData, cursorIndexes) {
|
|
1148
|
-
var _a;
|
|
1164
|
+
const sectionObjects = this.hitObjects.slice(section.firstObjectIndex, section.lastObjectIndex + 1);
|
|
1165
|
+
const sectionReplayObjectData = objectData.slice(section.firstObjectIndex, section.lastObjectIndex + 1);
|
|
1149
1166
|
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1150
|
-
for (let i = 0; i < sectionObjects.length &&
|
|
1167
|
+
for (let i = 0; i < sectionObjects.length && cursorIndices.some((v) => v !== -1); ++i) {
|
|
1151
1168
|
const object = sectionObjects[i];
|
|
1152
1169
|
const objectData = sectionReplayObjectData[i];
|
|
1153
1170
|
if (object instanceof osuBase.Spinner ||
|
|
@@ -1162,201 +1179,98 @@ class ThreeFingerChecker {
|
|
|
1162
1179
|
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1163
1180
|
const hitTime = object.startTime + objectData.accuracy;
|
|
1164
1181
|
// Observe the cursor position at the object's hit time.
|
|
1165
|
-
for (let j = 0; j <
|
|
1166
|
-
if (
|
|
1182
|
+
for (let j = 0; j < cursorIndices.length; ++j) {
|
|
1183
|
+
if (cursorIndices[j] === -1) {
|
|
1167
1184
|
continue;
|
|
1168
1185
|
}
|
|
1169
|
-
const cursorData = this.data.cursorMovement[
|
|
1186
|
+
const cursorData = this.data.cursorMovement[cursorIndices[j]];
|
|
1170
1187
|
const cursorGroup = cursorData.occurrenceGroups.find((v) => v.isActiveAt(hitTime));
|
|
1171
1188
|
if (!cursorGroup) {
|
|
1172
1189
|
continue;
|
|
1173
1190
|
}
|
|
1174
1191
|
const cursors = cursorGroup.allOccurrences;
|
|
1192
|
+
let isInObject = false;
|
|
1175
1193
|
for (let k = 1; k < cursors.length; ++k) {
|
|
1176
1194
|
const cursor = cursors[k];
|
|
1177
1195
|
const prevCursor = cursors[k - 1];
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
prevCursor.position.getDistance(objectPosition) <= object.radius;
|
|
1189
|
-
break;
|
|
1190
|
-
case exports.MovementType.move:
|
|
1191
|
-
// Interpolate movement.
|
|
1192
|
-
for (let mSecPassed = prevCursor.time; !isInObject &&
|
|
1193
|
-
mSecPassed <=
|
|
1194
|
-
Math.min(cursor.time, object.startTime + hitWindow50); ++mSecPassed) {
|
|
1195
|
-
const t = (mSecPassed - prevCursor.time) /
|
|
1196
|
+
// Only consider cursor at interval prev.time <= hitTime <= current.time.
|
|
1197
|
+
if (prevCursor.time <= hitTime && cursor.time >= hitTime) {
|
|
1198
|
+
switch (cursor.id) {
|
|
1199
|
+
case exports.MovementType.up:
|
|
1200
|
+
isInObject =
|
|
1201
|
+
prevCursor.position.getDistance(objectPosition) <= object.radius;
|
|
1202
|
+
break;
|
|
1203
|
+
case exports.MovementType.move: {
|
|
1204
|
+
// Interpolate movement.
|
|
1205
|
+
const t = (hitTime - prevCursor.time) /
|
|
1196
1206
|
(cursor.time - prevCursor.time);
|
|
1197
1207
|
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));
|
|
1198
1208
|
isInObject =
|
|
1199
1209
|
objectPosition.getDistance(cursorPosition) <= object.radius;
|
|
1200
1210
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
cursorIndexes[j] = -1;
|
|
1211
|
+
}
|
|
1212
|
+
break;
|
|
1204
1213
|
}
|
|
1205
1214
|
}
|
|
1215
|
+
if (!isInObject) {
|
|
1216
|
+
cursorIndices[j] = -1;
|
|
1217
|
+
}
|
|
1206
1218
|
}
|
|
1207
1219
|
}
|
|
1208
|
-
return (_a =
|
|
1209
|
-
}
|
|
1210
|
-
/**
|
|
1211
|
-
* Attempts to prevent accidental taps from being flagged.
|
|
1212
|
-
*
|
|
1213
|
-
* This detection will filter cursors that don't hit
|
|
1214
|
-
* any object in beatmap sections, thus eliminating any
|
|
1215
|
-
* unnecessary taps.
|
|
1216
|
-
*/
|
|
1217
|
-
preventAccidentalTaps() {
|
|
1218
|
-
let filledCursorAmount = this.downCursorInstances.filter((v) => v.length > 0).length;
|
|
1219
|
-
if (filledCursorAmount <= 3) {
|
|
1220
|
-
return;
|
|
1221
|
-
}
|
|
1222
|
-
const { objects } = this.beatmap.hitObjects;
|
|
1223
|
-
const totalCursorAmount = this.downCursorInstances.reduce((acc, value) => acc + value.length, 0);
|
|
1224
|
-
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
1225
|
-
if (filledCursorAmount <= 3) {
|
|
1226
|
-
break;
|
|
1227
|
-
}
|
|
1228
|
-
const cursorInstances = this.downCursorInstances[i];
|
|
1229
|
-
// Use an estimation for accidental tap threshold.
|
|
1230
|
-
if (cursorInstances.length <=
|
|
1231
|
-
Math.ceil(objects.length / this.accidentalTapThreshold) &&
|
|
1232
|
-
cursorInstances.length / totalCursorAmount <
|
|
1233
|
-
this.threeFingerRatioThreshold * 2) {
|
|
1234
|
-
--filledCursorAmount;
|
|
1235
|
-
cursorInstances.length = 0;
|
|
1236
|
-
}
|
|
1237
|
-
this.downCursorInstances[i] = cursorInstances;
|
|
1238
|
-
}
|
|
1220
|
+
return (_a = cursorIndices.find((v) => v !== -1)) !== null && _a !== void 0 ? _a : -1;
|
|
1239
1221
|
}
|
|
1240
1222
|
/**
|
|
1241
1223
|
* Creates nerf factors by scanning through objects.
|
|
1242
|
-
*
|
|
1243
|
-
* This check will ignore all objects with speed strain below `strainThreshold`.
|
|
1244
1224
|
*/
|
|
1245
1225
|
calculateNerfFactors() {
|
|
1246
|
-
const { objects } = this.beatmap.hitObjects;
|
|
1247
|
-
const objectData = this.data.hitObjectData;
|
|
1248
|
-
// We only filter cursor instances that are above the strain threshold.
|
|
1249
|
-
// This minimalizes the amount of cursor instances to analyze.
|
|
1250
1226
|
for (const beatmapSection of this.beatmapSections) {
|
|
1251
|
-
const
|
|
1252
|
-
const
|
|
1253
|
-
(
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
: -this.hitWindow.hitWindowFor50(this.isPrecise));
|
|
1257
|
-
const endTime = objects[beatmapSection.lastObjectIndex].endTime +
|
|
1258
|
-
(objectData[beatmapSection.lastObjectIndex].result !==
|
|
1259
|
-
exports.HitResult.miss
|
|
1260
|
-
? objectData[beatmapSection.lastObjectIndex].accuracy
|
|
1261
|
-
: this.hitWindow.hitWindowFor50(this.isPrecise));
|
|
1262
|
-
const cursorAmounts = [];
|
|
1263
|
-
const sectionCursors = [];
|
|
1264
|
-
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
1265
|
-
// Do not include drag cursor instance.
|
|
1266
|
-
if (i === dragIndex) {
|
|
1227
|
+
const cursorCounts = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
1228
|
+
for (const object of beatmapSection.objects) {
|
|
1229
|
+
if (object.pressingCursorIndex === -1 ||
|
|
1230
|
+
object.pressingCursorInstanceIndex ===
|
|
1231
|
+
beatmapSection.dragFingerIndex) {
|
|
1267
1232
|
continue;
|
|
1268
1233
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
++amount;
|
|
1275
|
-
sectionCursors.push(cursors[j]);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
|
-
cursorAmounts.push(amount);
|
|
1234
|
+
++cursorCounts[object.pressingCursorInstanceIndex];
|
|
1235
|
+
}
|
|
1236
|
+
if (beatmapSection.dragFingerIndex !== -1) {
|
|
1237
|
+
// Remove the drag index to prevent it from being picked up into the detection.
|
|
1238
|
+
cursorCounts.splice(beatmapSection.dragFingerIndex, 1);
|
|
1279
1239
|
}
|
|
1280
|
-
sectionCursors.sort((a, b) => a.time - b.time);
|
|
1281
1240
|
// This index will be used to detect if a section is 3-fingered.
|
|
1282
1241
|
// If the section is dragged, the dragged instance will be ignored,
|
|
1283
1242
|
// hence why the index is 1 less than nondragged section.
|
|
1284
|
-
const fingerSplitIndex =
|
|
1243
|
+
const fingerSplitIndex = beatmapSection.dragFingerIndex !== -1 ? 2 : 3;
|
|
1285
1244
|
// Divide >=4th (3rd for drag) cursor instances with 1st + 2nd (+ 3rd for nondrag)
|
|
1286
1245
|
// to check if the section is 3-fingered.
|
|
1287
|
-
const threeFingerRatio =
|
|
1246
|
+
const threeFingerRatio = cursorCounts
|
|
1288
1247
|
.slice(fingerSplitIndex)
|
|
1289
1248
|
.reduce((acc, value) => acc + value, 0) /
|
|
1290
|
-
|
|
1249
|
+
Math.max(cursorCounts
|
|
1291
1250
|
.slice(0, fingerSplitIndex)
|
|
1251
|
+
.reduce((acc, value) => acc + value, 0), 1);
|
|
1252
|
+
if (threeFingerRatio > this.threeFingerRatioThreshold) {
|
|
1253
|
+
const threeFingeredObjectCount = cursorCounts
|
|
1254
|
+
.slice(fingerSplitIndex)
|
|
1292
1255
|
.reduce((acc, value) => acc + value, 0);
|
|
1293
|
-
|
|
1294
|
-
for (const sectionCursor of sectionCursors) {
|
|
1295
|
-
const pressIndex = similarPresses.findIndex((v) => v.lastPosition.getDistance(sectionCursor.position) <=
|
|
1296
|
-
this.cursorDistancingDistanceThreshold);
|
|
1297
|
-
if (pressIndex !== -1) {
|
|
1298
|
-
if (sectionCursor.time -
|
|
1299
|
-
similarPresses[pressIndex].lastTime >=
|
|
1300
|
-
this.cursorDistancingTimeThreshold) {
|
|
1301
|
-
similarPresses.splice(pressIndex, 1);
|
|
1302
|
-
similarPresses.push({
|
|
1303
|
-
lastPosition: sectionCursor.position,
|
|
1304
|
-
count: 1,
|
|
1305
|
-
lastTime: sectionCursor.time,
|
|
1306
|
-
});
|
|
1307
|
-
continue;
|
|
1308
|
-
}
|
|
1309
|
-
similarPresses[pressIndex].lastPosition =
|
|
1310
|
-
sectionCursor.position;
|
|
1311
|
-
similarPresses[pressIndex].lastTime = sectionCursor.time;
|
|
1312
|
-
++similarPresses[pressIndex].count;
|
|
1313
|
-
}
|
|
1314
|
-
else {
|
|
1315
|
-
similarPresses.push({
|
|
1316
|
-
lastPosition: sectionCursor.position,
|
|
1317
|
-
count: 1,
|
|
1318
|
-
lastTime: sectionCursor.time,
|
|
1319
|
-
});
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
// Sort by highest count; assume the order is 3rd, 4th, 5th, ... finger
|
|
1323
|
-
const validPresses = similarPresses
|
|
1324
|
-
.filter((v) => v.count >= this.cursorDistancingCountThreshold)
|
|
1325
|
-
.sort((a, b) => b.count - a.count)
|
|
1326
|
-
.slice(2);
|
|
1327
|
-
// Ignore cursor presses that are only 1 for now since they are very likely to be accidental
|
|
1328
|
-
if ((threeFingerRatio > this.threeFingerRatioThreshold &&
|
|
1329
|
-
cursorAmounts.filter((v) => v > 1).length >
|
|
1330
|
-
fingerSplitIndex) ||
|
|
1331
|
-
validPresses.length > 0) {
|
|
1332
|
-
// Strain factor
|
|
1333
|
-
const objectCount = beatmapSection.lastObjectIndex -
|
|
1256
|
+
const sectionObjectCount = beatmapSection.lastObjectIndex -
|
|
1334
1257
|
beatmapSection.firstObjectIndex +
|
|
1335
1258
|
1;
|
|
1259
|
+
const threeFingeredObjectRatio = threeFingeredObjectCount / sectionObjectCount;
|
|
1336
1260
|
// We can ignore the first 3 (2 for drag) filled cursor instances
|
|
1337
1261
|
// since they are guaranteed not 3 finger.
|
|
1338
|
-
const
|
|
1262
|
+
const threeFingerCursorCounts = cursorCounts
|
|
1339
1263
|
.slice(fingerSplitIndex)
|
|
1340
1264
|
.filter((amount) => amount > 0);
|
|
1341
1265
|
// Finger factor applies more penalty if more fingers were used.
|
|
1342
|
-
const fingerFactor =
|
|
1343
|
-
|
|
1344
|
-
Math.pow(((index + 1) * value * objectCount) /
|
|
1345
|
-
this.strainNoteCount, 0.9), 1)
|
|
1346
|
-
: Math.pow(validPresses.reduce((acc, value, index) => acc +
|
|
1347
|
-
Math.pow(((index + 1) *
|
|
1348
|
-
(value.count /
|
|
1349
|
-
(this
|
|
1350
|
-
.cursorDistancingCountThreshold *
|
|
1351
|
-
2)) *
|
|
1352
|
-
objectCount) /
|
|
1353
|
-
this.strainNoteCount, 0.2), 1), 0.2);
|
|
1266
|
+
const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
|
|
1267
|
+
Math.pow((index + 1) * count * threeFingeredObjectRatio, 0.9), 1);
|
|
1354
1268
|
// Length factor applies more penalty if there are more 3-fingered object.
|
|
1355
|
-
const lengthFactor = 1 + Math.pow(
|
|
1269
|
+
const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 1.2);
|
|
1356
1270
|
this.nerfFactors.push({
|
|
1357
|
-
strainFactor:
|
|
1358
|
-
fingerFactor,
|
|
1359
|
-
lengthFactor,
|
|
1271
|
+
strainFactor: beatmapSection.sumStrain * threeFingeredObjectRatio,
|
|
1272
|
+
fingerFactor: fingerFactor,
|
|
1273
|
+
lengthFactor: lengthFactor,
|
|
1360
1274
|
});
|
|
1361
1275
|
}
|
|
1362
1276
|
}
|
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.20",
|
|
4
4
|
"description": "A replay analyzer for analyzing osu!droid replay files.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"osu",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@rian8337/osu-base": "^4.0.0-beta.19",
|
|
38
|
-
"@rian8337/osu-difficulty-calculator": "^4.0.0-beta.
|
|
39
|
-
"@rian8337/osu-rebalance-difficulty-calculator": "^4.0.0-beta.
|
|
38
|
+
"@rian8337/osu-difficulty-calculator": "^4.0.0-beta.20",
|
|
39
|
+
"@rian8337/osu-rebalance-difficulty-calculator": "^4.0.0-beta.20",
|
|
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": "8e6d7449a4a949f0158f10b9130378e7368bd8aa"
|
|
50
50
|
}
|
package/typings/index.d.ts
CHANGED
|
@@ -861,28 +861,11 @@ declare class ThreeFingerChecker {
|
|
|
861
861
|
*/
|
|
862
862
|
readonly difficultyAttributes: ExtendedDroidDifficultyAttributes$1;
|
|
863
863
|
/**
|
|
864
|
-
* The
|
|
865
|
-
* actually pressed with 1 finger in osu!pixels.
|
|
864
|
+
* The hitobjects to be analyzed.
|
|
866
865
|
*
|
|
867
|
-
* This is
|
|
868
|
-
* too fast to the point where the 4th cursor instance or beyond is recorded
|
|
869
|
-
* as 1st, 2nd, or 3rd cursor instance.
|
|
870
|
-
*/
|
|
871
|
-
private readonly cursorDistancingDistanceThreshold;
|
|
872
|
-
/**
|
|
873
|
-
* The threshold for the amount of cursors that are assumed to be pressed
|
|
874
|
-
* by a single finger.
|
|
875
|
-
*/
|
|
876
|
-
private readonly cursorDistancingCountThreshold;
|
|
877
|
-
/**
|
|
878
|
-
* The threshold for the time difference of cursors that are assumed to be pressed
|
|
879
|
-
* by a single finger, in milliseconds.
|
|
880
|
-
*/
|
|
881
|
-
private readonly cursorDistancingTimeThreshold;
|
|
882
|
-
/**
|
|
883
|
-
* The amount of notes that has a tap strain exceeding `strainThreshold`.
|
|
866
|
+
* This is being maintained separately due to possible change in object scale.
|
|
884
867
|
*/
|
|
885
|
-
private readonly
|
|
868
|
+
private readonly hitObjects;
|
|
886
869
|
/**
|
|
887
870
|
* The ratio threshold between non-3 finger cursors and 3-finger cursors.
|
|
888
871
|
*
|
|
@@ -894,14 +877,6 @@ declare class ThreeFingerChecker {
|
|
|
894
877
|
* Extended sections of the beatmap for drag detection.
|
|
895
878
|
*/
|
|
896
879
|
private readonly beatmapSections;
|
|
897
|
-
/**
|
|
898
|
-
* This threshold is used to filter out accidental taps.
|
|
899
|
-
*
|
|
900
|
-
* Increasing this number makes the filtration more sensitive, however it
|
|
901
|
-
* will also increase the chance of 3-fingered plays getting out from
|
|
902
|
-
* being flagged.
|
|
903
|
-
*/
|
|
904
|
-
private readonly accidentalTapThreshold;
|
|
905
880
|
/**
|
|
906
881
|
* The hit window of this beatmap. Keep in mind that speed-changing mods do not change hit window length in game logic.
|
|
907
882
|
*/
|
|
@@ -954,7 +929,7 @@ declare class ThreeFingerChecker {
|
|
|
954
929
|
* Generates a new set of "accurate break points".
|
|
955
930
|
*
|
|
956
931
|
* This is done to increase detection accuracy since break points do not start right at the
|
|
957
|
-
*
|
|
932
|
+
* end of the hitobject before it and do not end right at the first hitobject after it.
|
|
958
933
|
*/
|
|
959
934
|
private getAccurateBreakPoints;
|
|
960
935
|
/**
|
|
@@ -969,9 +944,15 @@ declare class ThreeFingerChecker {
|
|
|
969
944
|
*/
|
|
970
945
|
private getBeatmapSections;
|
|
971
946
|
/**
|
|
972
|
-
*
|
|
947
|
+
* Obtains the index of the nearest cursor of which an object was pressed in terms of time.
|
|
948
|
+
*
|
|
949
|
+
* @param object The object to obtain the index for.
|
|
950
|
+
* @param objectData The hit data of the object.
|
|
951
|
+
* @param cursorLookupIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
952
|
+
* @param excludedIndices The cursor indices that should not be checked.
|
|
953
|
+
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
973
954
|
*/
|
|
974
|
-
private
|
|
955
|
+
private getObjectPressIndex;
|
|
975
956
|
/**
|
|
976
957
|
* Checks if a section is dragged and returns the index of the drag finger.
|
|
977
958
|
*
|
|
@@ -979,27 +960,9 @@ declare class ThreeFingerChecker {
|
|
|
979
960
|
*
|
|
980
961
|
* @param section The section to check.
|
|
981
962
|
*/
|
|
982
|
-
private checkDrag;
|
|
983
|
-
/**
|
|
984
|
-
* Finds the drag index of the section.
|
|
985
|
-
*
|
|
986
|
-
* @param sectionObjects The objects in the section.
|
|
987
|
-
* @param sectionReplayObjectData The hitobject data of all objects in the section.
|
|
988
|
-
* @param cursorIndexes The indexes of the cursor instance that has at least an occurrence in the section.
|
|
989
|
-
*/
|
|
990
963
|
private findDragIndex;
|
|
991
|
-
/**
|
|
992
|
-
* Attempts to prevent accidental taps from being flagged.
|
|
993
|
-
*
|
|
994
|
-
* This detection will filter cursors that don't hit
|
|
995
|
-
* any object in beatmap sections, thus eliminating any
|
|
996
|
-
* unnecessary taps.
|
|
997
|
-
*/
|
|
998
|
-
private preventAccidentalTaps;
|
|
999
964
|
/**
|
|
1000
965
|
* Creates nerf factors by scanning through objects.
|
|
1001
|
-
*
|
|
1002
|
-
* This check will ignore all objects with speed strain below `strainThreshold`.
|
|
1003
966
|
*/
|
|
1004
967
|
private calculateNerfFactors;
|
|
1005
968
|
/**
|