@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 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.strainNoteCount =
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.strainNoteCount === 0) {
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
- * start of the hitobject before it and do not end right at the first hitobject after it.
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(objects.findIndex((o) => o.endTime >= breakPoint.startTime) - 1, 0, objects.length - 2);
953
- let timeBefore = objects[beforeIndex].endTime;
954
- // For sliders and spinners, automatically set hit window length to be as lenient as possible.
955
- let beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
956
- switch (objectData[beforeIndex].result) {
957
- case exports.HitResult.great:
958
- beforeIndexHitWindowLength = this.hitWindow.hitWindowFor300(this.isPrecise);
959
- break;
960
- case exports.HitResult.good:
961
- beforeIndexHitWindowLength = this.hitWindow.hitWindowFor100(this.isPrecise);
962
- break;
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
- let timeAfter = objects[afterIndex].startTime;
969
- // For sliders and spinners, automatically set hit window length to be as lenient as possible.
970
- let afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(this.isPrecise);
971
- switch (objectData[afterIndex].result) {
972
- case exports.HitResult.great:
973
- afterIndexHitWindowLength = this.hitWindow.hitWindowFor300(this.isPrecise);
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 (let i = 0; i < this.data.cursorMovement.length; ++i) {
1034
- const cursorInstance = this.data.cursorMovement[i];
988
+ for (const cursorInstance of this.data.cursorMovement) {
1035
989
  const validOccurrences = [];
1036
- for (let j = 0; j < cursorInstance.occurrenceGroups.length; ++j) {
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.beatmapSections.push(Object.assign(Object.assign({}, section), { isDragged: false, dragFingerIndex: -1 }));
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
- * Checks whether or not each beatmap sections is dragged.
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
- detectDragPlay() {
1067
- for (let i = 0; i < this.beatmapSections.length; ++i) {
1068
- const dragIndex = this.checkDrag(this.beatmapSections[i]);
1069
- this.beatmapSections[i].dragFingerIndex = dragIndex;
1070
- this.beatmapSections[i].isDragged = dragIndex !== -1;
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
- checkDrag(section) {
1081
- const { objects } = this.beatmap.hitObjects;
1106
+ findDragIndex(section) {
1107
+ var _a;
1082
1108
  const objectData = this.data.hitObjectData;
1083
- const firstObject = objects[section.firstObjectIndex];
1084
- const lastObject = objects[section.lastObjectIndex];
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 cursorIndexes = [];
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
- cursorIndexes.push(i);
1162
+ cursorIndices.push(i);
1137
1163
  }
1138
- return this.findDragIndex(objects.slice(section.firstObjectIndex, section.lastObjectIndex + 1), objectData.slice(section.firstObjectIndex, section.lastObjectIndex + 1), cursorIndexes);
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 && cursorIndexes.every((v) => v !== -1); ++i) {
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 < cursorIndexes.length; ++j) {
1166
- if (cursorIndexes[j] === -1) {
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[cursorIndexes[j]];
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
- if (prevCursor.time < object.startTime - hitWindow50) {
1179
- continue;
1180
- }
1181
- if (prevCursor.time > object.startTime + hitWindow50) {
1182
- break;
1183
- }
1184
- let isInObject = false;
1185
- switch (cursor.id) {
1186
- case exports.MovementType.up:
1187
- isInObject =
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
- if (!isInObject) {
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 = cursorIndexes.find((v) => v !== -1)) !== null && _a !== void 0 ? _a : -1;
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 dragIndex = beatmapSection.dragFingerIndex;
1252
- const startTime = objects[beatmapSection.firstObjectIndex].startTime +
1253
- (objectData[beatmapSection.firstObjectIndex].result !==
1254
- exports.HitResult.miss
1255
- ? objectData[beatmapSection.firstObjectIndex].accuracy
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
- const cursors = this.downCursorInstances[i];
1270
- let amount = 0;
1271
- for (let j = 0; j < cursors.length; ++j) {
1272
- if (cursors[j].time >= startTime &&
1273
- cursors[j].time <= endTime) {
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 = dragIndex !== -1 ? 2 : 3;
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 = cursorAmounts
1246
+ const threeFingerRatio = cursorCounts
1288
1247
  .slice(fingerSplitIndex)
1289
1248
  .reduce((acc, value) => acc + value, 0) /
1290
- cursorAmounts
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
- const similarPresses = [];
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 threeFingerCursorAmounts = cursorAmounts
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 = threeFingerRatio > this.threeFingerRatioThreshold
1343
- ? threeFingerCursorAmounts.reduce((acc, value, index) => acc +
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(objectCount / this.strainNoteCount, 1.2);
1269
+ const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 1.2);
1356
1270
  this.nerfFactors.push({
1357
- strainFactor: Math.max(1, beatmapSection.sumStrain),
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.19",
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.19",
39
- "@rian8337/osu-rebalance-difficulty-calculator": "^4.0.0-beta.19",
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": "45f7c976ba683027dc3a4d29de8bf289aadf7707"
49
+ "gitHead": "8e6d7449a4a949f0158f10b9130378e7368bd8aa"
50
50
  }
@@ -861,28 +861,11 @@ declare class ThreeFingerChecker {
861
861
  */
862
862
  readonly difficultyAttributes: ExtendedDroidDifficultyAttributes$1;
863
863
  /**
864
- * The distance threshold between cursors to assume that two cursors are
865
- * actually pressed with 1 finger in osu!pixels.
864
+ * The hitobjects to be analyzed.
866
865
  *
867
- * This is used to prevent cases where a player would lift their finger
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 strainNoteCount;
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
- * start of the hitobject before it and do not end right at the first hitobject after it.
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
- * Checks whether or not each beatmap sections is dragged.
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 detectDragPlay;
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
  /**