@rian8337/osu-droid-replay-analyzer 1.4.12-alpha.4 → 1.4.12-alpha.7
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.
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ThreeFingerChecker = void 0;
|
|
4
4
|
const osu_base_1 = require("@rian8337/osu-base");
|
|
5
|
-
const
|
|
5
|
+
const hitResult_1 = require("../constants/hitResult");
|
|
6
|
+
const movementType_1 = require("../constants/movementType");
|
|
6
7
|
const CursorData_1 = require("../data/CursorData");
|
|
7
8
|
const BeatmapSectionGenerator_1 = require("./BeatmapSectionGenerator");
|
|
8
9
|
const ThreeFingerBeatmapSection_1 = require("./data/ThreeFingerBeatmapSection");
|
|
@@ -15,25 +16,6 @@ class ThreeFingerChecker {
|
|
|
15
16
|
* @param data The data of the replay.
|
|
16
17
|
*/
|
|
17
18
|
constructor(map, data) {
|
|
18
|
-
/**
|
|
19
|
-
* The distance threshold between cursors to assume that two cursors are
|
|
20
|
-
* actually pressed with 1 finger in osu!pixels.
|
|
21
|
-
*
|
|
22
|
-
* This is used to prevent cases where a player would lift their finger
|
|
23
|
-
* too fast to the point where the 4th cursor instance or beyond is recorded
|
|
24
|
-
* as 1st, 2nd, or 3rd cursor instance.
|
|
25
|
-
*/
|
|
26
|
-
this.cursorDistancingDistanceThreshold = 60;
|
|
27
|
-
/**
|
|
28
|
-
* The threshold for the amount of cursors that are assumed to be pressed
|
|
29
|
-
* by a single finger.
|
|
30
|
-
*/
|
|
31
|
-
this.cursorDistancingCountThreshold = 10;
|
|
32
|
-
/**
|
|
33
|
-
* The threshold for the time difference of cursors that are assumed to be pressed
|
|
34
|
-
* by a single finger, in milliseconds.
|
|
35
|
-
*/
|
|
36
|
-
this.cursorDistancingTimeThreshold = 1000;
|
|
37
19
|
/**
|
|
38
20
|
* The ratio threshold between non-3 finger cursors and 3-finger cursors.
|
|
39
21
|
*
|
|
@@ -49,7 +31,7 @@ class ThreeFingerChecker {
|
|
|
49
31
|
* Note that this value does not account for the speed multiplier of
|
|
50
32
|
* the play, similar to the way replay object data is stored.
|
|
51
33
|
*/
|
|
52
|
-
this.maxSectionDeltaTime =
|
|
34
|
+
this.maxSectionDeltaTime = 1000;
|
|
53
35
|
/**
|
|
54
36
|
* The minimum object count required to make a beatmap section.
|
|
55
37
|
*
|
|
@@ -60,14 +42,6 @@ class ThreeFingerChecker {
|
|
|
60
42
|
* The sections of the beatmap that was cut based on `maxSectionDeltaTime` and `minSectionObjectCount`.
|
|
61
43
|
*/
|
|
62
44
|
this.beatmapSections = [];
|
|
63
|
-
/**
|
|
64
|
-
* This threshold is used to filter out accidental taps.
|
|
65
|
-
*
|
|
66
|
-
* Increasing this number makes the filtration more sensitive, however it
|
|
67
|
-
* will also increase the chance of 3-fingered plays getting out from
|
|
68
|
-
* being flagged.
|
|
69
|
-
*/
|
|
70
|
-
this.accidentalTapThreshold = 400;
|
|
71
45
|
/**
|
|
72
46
|
* A reprocessed break points to match right on object time.
|
|
73
47
|
*
|
|
@@ -79,6 +53,11 @@ class ThreeFingerChecker {
|
|
|
79
53
|
* A cursor data array that only contains `movementType.DOWN` movement ID occurrences.
|
|
80
54
|
*/
|
|
81
55
|
this.downCursorInstances = [];
|
|
56
|
+
/**
|
|
57
|
+
* The indexes of cursors that presses objects that fall under {@link beatmapSections beatmap sections},
|
|
58
|
+
* mapped by each object's unadjusted start time.
|
|
59
|
+
*/
|
|
60
|
+
this.objectCursorIndexes = new Map();
|
|
82
61
|
/**
|
|
83
62
|
* Nerf factors from all sections that were three-fingered.
|
|
84
63
|
*/
|
|
@@ -121,9 +100,9 @@ class ThreeFingerChecker {
|
|
|
121
100
|
return { is3Finger: false, penalty: 1 };
|
|
122
101
|
}
|
|
123
102
|
this.getBeatmapSections();
|
|
124
|
-
this.
|
|
103
|
+
this.assignFingerIndexes();
|
|
125
104
|
this.getDetailedBeatmapSections();
|
|
126
|
-
this.
|
|
105
|
+
this.assignCursorIndexesToObjects();
|
|
127
106
|
if (this.downCursorInstances.filter((v) => v.size > 0).length <= 3) {
|
|
128
107
|
return { is3Finger: false, penalty: 1 };
|
|
129
108
|
}
|
|
@@ -147,11 +126,11 @@ class ThreeFingerChecker {
|
|
|
147
126
|
// For sliders and spinners, automatically set hit window length to be as lenient as possible.
|
|
148
127
|
let beforeIndexHitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
|
|
149
128
|
switch (objectData[beforeIndex].result) {
|
|
150
|
-
case
|
|
129
|
+
case hitResult_1.hitResult.RESULT_300:
|
|
151
130
|
beforeIndexHitWindowLength =
|
|
152
131
|
this.hitWindow.hitWindowFor300(isPrecise);
|
|
153
132
|
break;
|
|
154
|
-
case
|
|
133
|
+
case hitResult_1.hitResult.RESULT_100:
|
|
155
134
|
beforeIndexHitWindowLength =
|
|
156
135
|
this.hitWindow.hitWindowFor100(isPrecise);
|
|
157
136
|
break;
|
|
@@ -165,11 +144,11 @@ class ThreeFingerChecker {
|
|
|
165
144
|
// For sliders and spinners, automatically set hit window length to be as lenient as possible.
|
|
166
145
|
let afterIndexHitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
|
|
167
146
|
switch (objectData[afterIndex].result) {
|
|
168
|
-
case
|
|
147
|
+
case hitResult_1.hitResult.RESULT_300:
|
|
169
148
|
afterIndexHitWindowLength =
|
|
170
149
|
this.hitWindow.hitWindowFor300(isPrecise);
|
|
171
150
|
break;
|
|
172
|
-
case
|
|
151
|
+
case hitResult_1.hitResult.RESULT_100:
|
|
173
152
|
afterIndexHitWindowLength =
|
|
174
153
|
this.hitWindow.hitWindowFor100(isPrecise);
|
|
175
154
|
break;
|
|
@@ -199,11 +178,11 @@ class ThreeFingerChecker {
|
|
|
199
178
|
let firstObjectHitWindow = this.hitWindow.hitWindowFor50(isPrecise);
|
|
200
179
|
if (objects[0].object instanceof osu_base_1.Circle) {
|
|
201
180
|
switch (firstObjectResult) {
|
|
202
|
-
case
|
|
181
|
+
case hitResult_1.hitResult.RESULT_300:
|
|
203
182
|
firstObjectHitWindow =
|
|
204
183
|
this.hitWindow.hitWindowFor300(isPrecise);
|
|
205
184
|
break;
|
|
206
|
-
case
|
|
185
|
+
case hitResult_1.hitResult.RESULT_100:
|
|
207
186
|
firstObjectHitWindow =
|
|
208
187
|
this.hitWindow.hitWindowFor100(isPrecise);
|
|
209
188
|
break;
|
|
@@ -216,11 +195,11 @@ class ThreeFingerChecker {
|
|
|
216
195
|
let lastObjectHitWindow = this.hitWindow.hitWindowFor50(isPrecise);
|
|
217
196
|
if (objects.at(-1).object instanceof osu_base_1.Circle) {
|
|
218
197
|
switch (lastObjectResult) {
|
|
219
|
-
case
|
|
198
|
+
case hitResult_1.hitResult.RESULT_300:
|
|
220
199
|
lastObjectHitWindow =
|
|
221
200
|
this.hitWindow.hitWindowFor300(isPrecise);
|
|
222
201
|
break;
|
|
223
|
-
case
|
|
202
|
+
case hitResult_1.hitResult.RESULT_100:
|
|
224
203
|
lastObjectHitWindow =
|
|
225
204
|
this.hitWindow.hitWindowFor100(isPrecise);
|
|
226
205
|
break;
|
|
@@ -244,7 +223,7 @@ class ThreeFingerChecker {
|
|
|
244
223
|
id: [],
|
|
245
224
|
});
|
|
246
225
|
for (let j = 0; j < cursorInstance.size; ++j) {
|
|
247
|
-
if (cursorInstance.id[j] !==
|
|
226
|
+
if (cursorInstance.id[j] !== movementType_1.movementType.DOWN) {
|
|
248
227
|
continue;
|
|
249
228
|
}
|
|
250
229
|
const time = cursorInstance.time[j];
|
|
@@ -273,162 +252,113 @@ class ThreeFingerChecker {
|
|
|
273
252
|
this.beatmapSections.push(new ThreeFingerBeatmapSection_1.ThreeFingerBeatmapSection({
|
|
274
253
|
firstObjectIndex: beatmapSection.firstObjectIndex,
|
|
275
254
|
lastObjectIndex: beatmapSection.lastObjectIndex,
|
|
276
|
-
|
|
277
|
-
dragFingerIndex: -1,
|
|
255
|
+
mainFingerIndex: -1,
|
|
278
256
|
}));
|
|
279
257
|
}
|
|
280
258
|
}
|
|
281
259
|
/**
|
|
282
|
-
*
|
|
260
|
+
* Assigns main finger indexes for each beatmap section.
|
|
283
261
|
*/
|
|
284
|
-
|
|
285
|
-
for (
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
262
|
+
assignFingerIndexes() {
|
|
263
|
+
for (const section of this.beatmapSections) {
|
|
264
|
+
let currentIndex = this.getMainFingerIndex(section);
|
|
265
|
+
if (currentIndex === null) {
|
|
266
|
+
const firstObject = this.map.objects[section.firstObjectIndex];
|
|
267
|
+
const firstObjectData = this.data.hitObjectData[section.firstObjectIndex];
|
|
268
|
+
const lastObject = this.map.objects[section.lastObjectIndex];
|
|
269
|
+
const lastObjectData = this.data.hitObjectData[section.lastObjectIndex];
|
|
270
|
+
const startTime = firstObject.object.startTime + firstObjectData.accuracy;
|
|
271
|
+
const endTime = lastObject.object.endTime + lastObjectData.accuracy;
|
|
272
|
+
// Get the cursor instance index in the section that has the most movementType.MOVE instance.
|
|
273
|
+
const moveInstanceAmounts = [];
|
|
274
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
275
|
+
let amount = 0;
|
|
276
|
+
const c = this.data.cursorMovement[i];
|
|
277
|
+
for (let j = 0; j < c.size; ++j) {
|
|
278
|
+
if (c.time[j] < startTime) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (c.time[j] > endTime) {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
if (c.id[j] === movementType_1.movementType.MOVE) {
|
|
285
|
+
++amount;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
moveInstanceAmounts.push(amount);
|
|
289
|
+
}
|
|
290
|
+
currentIndex = moveInstanceAmounts.indexOf(Math.max(...moveInstanceAmounts));
|
|
291
|
+
}
|
|
292
|
+
section.mainFingerIndex = currentIndex;
|
|
289
293
|
}
|
|
290
294
|
}
|
|
291
295
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* If the section is not dragged, -1 will be returned.
|
|
296
|
+
* Gets the main finger index of a beatmap section.
|
|
295
297
|
*
|
|
296
|
-
* @param section The section
|
|
298
|
+
* @param section The beatmap section.
|
|
297
299
|
*/
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
const isPrecise = this.map.mods.some((m) => m instanceof osu_base_1.ModPrecise);
|
|
302
|
-
const firstObject = objects[section.firstObjectIndex];
|
|
303
|
-
const lastObject = objects[section.lastObjectIndex];
|
|
304
|
-
let firstObjectMinHitTime = firstObject.object.startTime;
|
|
305
|
-
if (firstObject.object instanceof osu_base_1.Circle) {
|
|
306
|
-
switch (objectData[section.firstObjectIndex].result) {
|
|
307
|
-
case __1.hitResult.RESULT_300:
|
|
308
|
-
firstObjectMinHitTime -=
|
|
309
|
-
this.hitWindow.hitWindowFor300(isPrecise);
|
|
310
|
-
break;
|
|
311
|
-
case __1.hitResult.RESULT_100:
|
|
312
|
-
firstObjectMinHitTime -=
|
|
313
|
-
this.hitWindow.hitWindowFor100(isPrecise);
|
|
314
|
-
break;
|
|
315
|
-
default:
|
|
316
|
-
firstObjectMinHitTime -=
|
|
317
|
-
this.hitWindow.hitWindowFor50(isPrecise);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(isPrecise);
|
|
322
|
-
}
|
|
323
|
-
let lastObjectMaxHitTime = lastObject.object.startTime;
|
|
324
|
-
if (lastObject.object instanceof osu_base_1.Circle) {
|
|
325
|
-
switch (objectData[section.lastObjectIndex].result) {
|
|
326
|
-
case __1.hitResult.RESULT_300:
|
|
327
|
-
lastObjectMaxHitTime +=
|
|
328
|
-
this.hitWindow.hitWindowFor300(isPrecise);
|
|
329
|
-
break;
|
|
330
|
-
case __1.hitResult.RESULT_100:
|
|
331
|
-
lastObjectMaxHitTime +=
|
|
332
|
-
this.hitWindow.hitWindowFor100(isPrecise);
|
|
333
|
-
break;
|
|
334
|
-
default:
|
|
335
|
-
lastObjectMaxHitTime +=
|
|
336
|
-
this.hitWindow.hitWindowFor50(isPrecise);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(isPrecise);
|
|
341
|
-
}
|
|
342
|
-
// Since there may be more than 1 cursor instance index,
|
|
343
|
-
// we check which cursor instance follows hitobjects all over.
|
|
344
|
-
const cursorIndexes = [];
|
|
300
|
+
getMainFingerIndex(section) {
|
|
301
|
+
const validCursorIndexes = new Set();
|
|
302
|
+
const timeDiscrepancy = 10;
|
|
345
303
|
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
continue;
|
|
304
|
+
if (this.data.cursorMovement[i].size > 0) {
|
|
305
|
+
validCursorIndexes.add(i);
|
|
349
306
|
}
|
|
350
|
-
// Do not include cursors that don't have an occurence in this section
|
|
351
|
-
// this speeds up checking process.
|
|
352
|
-
if (c.time.filter((v) => v >= firstObjectMinHitTime && v <= lastObjectMaxHitTime).length === 0) {
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
// If this cursor instance doesn't move, it's not the cursor instance we want.
|
|
356
|
-
if (c.id.filter((v) => v === __1.movementType.MOVE).length === 0) {
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
cursorIndexes.push(i);
|
|
360
307
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Finds the drag index of the section.
|
|
365
|
-
*
|
|
366
|
-
* @param sectionObjects The objects in the section.
|
|
367
|
-
* @param sectionReplayObjectData The hitobject data of all objects in the section.
|
|
368
|
-
* @param cursorIndexes The indexes of the cursor instance that has at least an occurrence in the section.
|
|
369
|
-
*/
|
|
370
|
-
findDragIndex(sectionObjects, sectionReplayObjectData, cursorIndexes) {
|
|
371
|
-
let objectIndex = sectionObjects.findIndex((v, i) => !(v.object instanceof osu_base_1.Spinner) &&
|
|
372
|
-
sectionReplayObjectData[i].result !== __1.hitResult.RESULT_0);
|
|
373
|
-
if (objectIndex === -1) {
|
|
374
|
-
return -1;
|
|
375
|
-
}
|
|
376
|
-
while (cursorIndexes.length > 0) {
|
|
377
|
-
if (objectIndex === sectionObjects.length) {
|
|
308
|
+
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
309
|
+
if (validCursorIndexes.size === 0) {
|
|
378
310
|
break;
|
|
379
311
|
}
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
++objectIndex;
|
|
383
|
-
if (s.result === __1.hitResult.RESULT_0) {
|
|
312
|
+
const object = this.map.objects[i];
|
|
313
|
+
if (object.object instanceof osu_base_1.Spinner) {
|
|
384
314
|
continue;
|
|
385
315
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// therefore the game emulates the movement between
|
|
391
|
-
// movementType.MOVE cursors.
|
|
392
|
-
const hitTime = o.object.startTime + s.accuracy;
|
|
393
|
-
const nextHitIndex = c.time.findIndex((v) => v >= hitTime);
|
|
394
|
-
const hitIndex = nextHitIndex - 1;
|
|
395
|
-
if (hitIndex <= -1) {
|
|
396
|
-
cursorIndexes[j] = -1;
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
if (c.id[hitIndex] === __1.movementType.UP) {
|
|
400
|
-
cursorIndexes[j] = -1;
|
|
316
|
+
const objectData = this.data.hitObjectData[i];
|
|
317
|
+
const hitTime = object.startTime + objectData.accuracy;
|
|
318
|
+
for (let j = 0; j < this.data.cursorMovement.length; ++j) {
|
|
319
|
+
if (!validCursorIndexes.has(j)) {
|
|
401
320
|
continue;
|
|
402
321
|
}
|
|
403
|
-
const
|
|
404
|
-
let
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
322
|
+
const c = this.data.cursorMovement[j];
|
|
323
|
+
for (let k = 0; k < c.size; ++k) {
|
|
324
|
+
if (c.time[k] < hitTime - timeDiscrepancy ||
|
|
325
|
+
c.id[k] === movementType_1.movementType.UP) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (c.time[k] > hitTime + timeDiscrepancy) {
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
// Calculate the initial cursor position assuming it's a DOWN movement type.
|
|
332
|
+
const cursorPosition = new osu_base_1.Vector2(c.x[k], c.y[k]);
|
|
333
|
+
let isInObject = false;
|
|
334
|
+
// But if the next cursor is a move instance, then we extend through
|
|
335
|
+
// time between both cursors and interpolate cursor position.
|
|
336
|
+
if (c.id[k + 1] === movementType_1.movementType.MOVE) {
|
|
337
|
+
// Loop every 1ms to increase accuracy and minimalize error.
|
|
338
|
+
for (let mSecPassed = c.time[k]; mSecPassed <= c.time[k + 1]; ++mSecPassed) {
|
|
339
|
+
const t = (mSecPassed - c.time[k + 1]) /
|
|
340
|
+
(c.time[k] - c.time[k + 1]);
|
|
341
|
+
cursorPosition.x = osu_base_1.Interpolation.lerp(c.x[k], c.x[k + 1], t);
|
|
342
|
+
cursorPosition.y = osu_base_1.Interpolation.lerp(c.y[k], c.y[k + 1], t);
|
|
343
|
+
if (object.object.stackedPosition.getDistance(cursorPosition) <= object.object.radius) {
|
|
344
|
+
isInObject = true;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
417
347
|
}
|
|
418
348
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
349
|
+
else {
|
|
350
|
+
isInObject =
|
|
351
|
+
object.object.stackedPosition.getDistance(cursorPosition) <= object.object.radius;
|
|
352
|
+
}
|
|
353
|
+
if (!isInObject) {
|
|
354
|
+
validCursorIndexes.delete(j);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
427
357
|
}
|
|
428
358
|
}
|
|
429
|
-
cursorIndexes = cursorIndexes.filter((v) => v !== -1);
|
|
430
359
|
}
|
|
431
|
-
|
|
360
|
+
const [first] = validCursorIndexes;
|
|
361
|
+
return first ?? null;
|
|
432
362
|
}
|
|
433
363
|
/**
|
|
434
364
|
* Redivides the beatmap into sections.
|
|
@@ -457,18 +387,15 @@ class ThreeFingerChecker {
|
|
|
457
387
|
newBeatmapSections.push({
|
|
458
388
|
firstObjectIndex: newFirstObjectIndex,
|
|
459
389
|
lastObjectIndex: i,
|
|
460
|
-
|
|
461
|
-
dragFingerIndex: beatmapSection.dragFingerIndex,
|
|
390
|
+
mainFingerIndex: beatmapSection.mainFingerIndex,
|
|
462
391
|
});
|
|
463
392
|
}
|
|
464
393
|
}
|
|
465
394
|
// Don't forget to manually add the last beatmap section, which would otherwise be ignored.
|
|
466
395
|
if (inSpeedSection) {
|
|
467
396
|
newBeatmapSections.push({
|
|
397
|
+
...beatmapSection,
|
|
468
398
|
firstObjectIndex: newFirstObjectIndex,
|
|
469
|
-
lastObjectIndex: beatmapSection.lastObjectIndex,
|
|
470
|
-
isDragged: beatmapSection.isDragged,
|
|
471
|
-
dragFingerIndex: beatmapSection.dragFingerIndex,
|
|
472
399
|
});
|
|
473
400
|
}
|
|
474
401
|
}
|
|
@@ -476,41 +403,57 @@ class ThreeFingerChecker {
|
|
|
476
403
|
this.beatmapSections.push(...newBeatmapSections);
|
|
477
404
|
}
|
|
478
405
|
/**
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
* This detection will filter cursors that don't hit
|
|
482
|
-
* any object in beatmap sections, thus eliminating any
|
|
483
|
-
* unnecessary taps.
|
|
406
|
+
* Assigns cursor indexes for each object that is included in {@link beatmapSections beatmap sections}.
|
|
484
407
|
*/
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
408
|
+
assignCursorIndexesToObjects() {
|
|
409
|
+
const isPrecise = this.map.mods.some((m) => m instanceof osu_base_1.ModPrecise);
|
|
410
|
+
for (const section of this.beatmapSections) {
|
|
411
|
+
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
412
|
+
const object = this.map.objects[i];
|
|
413
|
+
const objectData = this.data.hitObjectData[i];
|
|
414
|
+
if (object.object instanceof osu_base_1.Spinner ||
|
|
415
|
+
objectData.result === hitResult_1.hitResult.RESULT_0) {
|
|
416
|
+
this.objectCursorIndexes.set(object.object.startTime, section.mainFingerIndex);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
let hitWindowLeniency = this.hitWindow.hitWindowFor50(isPrecise);
|
|
420
|
+
// For sliders, automatically set hit window length to be as lenient as possible.
|
|
421
|
+
if (object.object instanceof osu_base_1.Circle) {
|
|
422
|
+
switch (objectData.result) {
|
|
423
|
+
case hitResult_1.hitResult.RESULT_300:
|
|
424
|
+
hitWindowLeniency =
|
|
425
|
+
this.hitWindow.hitWindowFor300(isPrecise);
|
|
426
|
+
break;
|
|
427
|
+
case hitResult_1.hitResult.RESULT_100:
|
|
428
|
+
hitWindowLeniency =
|
|
429
|
+
this.hitWindow.hitWindowFor100(isPrecise);
|
|
430
|
+
break;
|
|
507
431
|
}
|
|
508
|
-
|
|
509
|
-
|
|
432
|
+
}
|
|
433
|
+
const hitTime = object.object.startTime + objectData.accuracy;
|
|
434
|
+
// We only care about the object timing; position doesn't matter.
|
|
435
|
+
let smallestIndex = section.mainFingerIndex;
|
|
436
|
+
let smallestDeltaTime = Number.POSITIVE_INFINITY;
|
|
437
|
+
for (let j = 0; j < this.downCursorInstances.length; ++j) {
|
|
438
|
+
const c = this.downCursorInstances[j];
|
|
439
|
+
for (let k = 0; k < c.size; ++k) {
|
|
440
|
+
if (c.time[k] <
|
|
441
|
+
object.object.startTime - hitWindowLeniency) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const deltaTime = Math.abs(hitTime - c.time[k]);
|
|
445
|
+
// Don't continue if it's past object hit time.
|
|
446
|
+
if (c.time[k] > hitTime) {
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
if (smallestDeltaTime > deltaTime) {
|
|
450
|
+
smallestDeltaTime = deltaTime;
|
|
451
|
+
smallestIndex = j;
|
|
452
|
+
}
|
|
510
453
|
}
|
|
511
454
|
}
|
|
455
|
+
this.objectCursorIndexes.set(object.object.startTime, smallestIndex);
|
|
512
456
|
}
|
|
513
|
-
this.downCursorInstances[i] = cursorInstance;
|
|
514
457
|
}
|
|
515
458
|
}
|
|
516
459
|
/**
|
|
@@ -520,132 +463,53 @@ class ThreeFingerChecker {
|
|
|
520
463
|
*/
|
|
521
464
|
calculateNerfFactors() {
|
|
522
465
|
const objects = this.map.objects;
|
|
523
|
-
const objectData = this.data.hitObjectData;
|
|
524
|
-
const isPrecise = this.data.convertedMods.some((m) => m instanceof osu_base_1.ModPrecise);
|
|
525
466
|
// We only filter cursor instances that are above the strain threshold.
|
|
526
467
|
// This minimalizes the amount of cursor instances to analyze.
|
|
527
468
|
for (const beatmapSection of this.beatmapSections) {
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
(
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
: this.hitWindow.hitWindowFor50(isPrecise));
|
|
539
|
-
// Filter cursor instances during section.
|
|
540
|
-
this.downCursorInstances.forEach((c) => {
|
|
541
|
-
const i = c.time.findIndex((t) => t >= startTime);
|
|
542
|
-
if (i !== -1) {
|
|
543
|
-
c.size -= i;
|
|
544
|
-
c.time.splice(0, i);
|
|
545
|
-
c.x.splice(0, i);
|
|
546
|
-
c.y.splice(0, i);
|
|
547
|
-
c.id.splice(0, i);
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
const cursorAmounts = [];
|
|
551
|
-
const cursorVectorTimes = [];
|
|
552
|
-
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
553
|
-
// Do not include drag cursor instance.
|
|
554
|
-
if (i === dragIndex) {
|
|
469
|
+
const cursorAmounts = osu_base_1.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
470
|
+
for (let i = beatmapSection.firstObjectIndex; i <= beatmapSection.lastObjectIndex; ++i) {
|
|
471
|
+
++cursorAmounts[this.objectCursorIndexes.get(objects[i].object.startTime)];
|
|
472
|
+
}
|
|
473
|
+
let validCursorPressCount = 0;
|
|
474
|
+
let invalidCursorPressCount = 0;
|
|
475
|
+
const validIndexes = [beatmapSection.mainFingerIndex];
|
|
476
|
+
for (let i = 0; i < cursorAmounts.length; ++i) {
|
|
477
|
+
// Ignore the main cursor index.
|
|
478
|
+
if (i === beatmapSection.mainFingerIndex) {
|
|
555
479
|
continue;
|
|
556
480
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (cursorData.time[j] >= startTime &&
|
|
561
|
-
cursorData.time[j] <= endTime) {
|
|
562
|
-
++amount;
|
|
563
|
-
cursorVectorTimes.push({
|
|
564
|
-
vector: new osu_base_1.Vector2(cursorData.x[j], cursorData.y[j]),
|
|
565
|
-
time: cursorData.time[j],
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
cursorAmounts.push(amount);
|
|
570
|
-
}
|
|
571
|
-
// This index will be used to detect if a section is 3-fingered.
|
|
572
|
-
// If the section is dragged, the dragged instance will be ignored,
|
|
573
|
-
// hence why the index is 1 less than nondragged section.
|
|
574
|
-
const fingerSplitIndex = dragIndex !== -1 ? 2 : 3;
|
|
575
|
-
// Divide >=4th (3rd for drag) cursor instances with 1st + 2nd (+ 3rd for nondrag)
|
|
576
|
-
// to check if the section is 3-fingered.
|
|
577
|
-
const threeFingerRatio = cursorAmounts
|
|
578
|
-
.slice(fingerSplitIndex)
|
|
579
|
-
.reduce((acc, value) => acc + value, 0) /
|
|
580
|
-
cursorAmounts
|
|
581
|
-
.slice(0, fingerSplitIndex)
|
|
582
|
-
.reduce((acc, value) => acc + value, 0);
|
|
583
|
-
const similarPresses = [];
|
|
584
|
-
for (const cursorVectorTime of cursorVectorTimes) {
|
|
585
|
-
const pressIndex = similarPresses.findIndex((v) => v.vector.getDistance(cursorVectorTime.vector) <=
|
|
586
|
-
this.cursorDistancingDistanceThreshold);
|
|
587
|
-
if (pressIndex !== -1) {
|
|
588
|
-
if (cursorVectorTime.time -
|
|
589
|
-
similarPresses[pressIndex].lastTime >=
|
|
590
|
-
this.cursorDistancingTimeThreshold) {
|
|
591
|
-
similarPresses.splice(pressIndex, 1);
|
|
592
|
-
similarPresses.push({
|
|
593
|
-
vector: cursorVectorTime.vector,
|
|
594
|
-
count: 1,
|
|
595
|
-
lastTime: cursorVectorTime.time,
|
|
596
|
-
});
|
|
597
|
-
continue;
|
|
598
|
-
}
|
|
599
|
-
similarPresses[pressIndex].vector = cursorVectorTime.vector;
|
|
600
|
-
similarPresses[pressIndex].lastTime = cursorVectorTime.time;
|
|
601
|
-
++similarPresses[pressIndex].count;
|
|
481
|
+
// Add 2f cursor presses to valid cursor count and the rest to invalid cursor count.
|
|
482
|
+
if (validIndexes.length <= 2) {
|
|
483
|
+
validCursorPressCount += cursorAmounts[i];
|
|
602
484
|
}
|
|
603
485
|
else {
|
|
604
|
-
|
|
605
|
-
vector: cursorVectorTime.vector,
|
|
606
|
-
count: 1,
|
|
607
|
-
lastTime: cursorVectorTime.time,
|
|
608
|
-
});
|
|
486
|
+
invalidCursorPressCount += cursorAmounts[i];
|
|
609
487
|
}
|
|
488
|
+
validIndexes.push(i);
|
|
610
489
|
}
|
|
611
|
-
// Sort by highest count; assume the order is 3rd, 4th, 5th, ... finger
|
|
612
|
-
const validPresses = similarPresses
|
|
613
|
-
.filter((v) => v.count >= this.cursorDistancingCountThreshold)
|
|
614
|
-
.sort((a, b) => {
|
|
615
|
-
return b.count - a.count;
|
|
616
|
-
})
|
|
617
|
-
.slice(2);
|
|
618
490
|
// Ignore cursor presses that are only 1 for now since they are very likely to be accidental
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
491
|
+
if (invalidCursorPressCount / validCursorPressCount >
|
|
492
|
+
this.threeFingerRatioThreshold &&
|
|
493
|
+
cursorAmounts.filter((v) => v > 1).length > 3) {
|
|
622
494
|
// Strain factor
|
|
623
495
|
const objectCount = beatmapSection.lastObjectIndex -
|
|
624
496
|
beatmapSection.firstObjectIndex +
|
|
625
497
|
1;
|
|
626
498
|
const strainFactor = Math.pow(objects
|
|
627
|
-
.slice(beatmapSection.firstObjectIndex, beatmapSection.lastObjectIndex)
|
|
499
|
+
.slice(beatmapSection.firstObjectIndex, beatmapSection.lastObjectIndex + 1)
|
|
628
500
|
.reduce((acc, value) => acc +
|
|
629
501
|
value.originalTapStrain /
|
|
630
|
-
ThreeFingerChecker.strainThreshold, 0), 0.
|
|
631
|
-
// We can ignore the first 3 (2 for drag) filled cursor instances
|
|
632
|
-
// since they are guaranteed not 3 finger.
|
|
633
|
-
const threeFingerCursorAmounts = cursorAmounts
|
|
634
|
-
.slice(fingerSplitIndex)
|
|
635
|
-
.filter((amount) => amount > 0);
|
|
502
|
+
ThreeFingerChecker.strainThreshold, 0), 0.875);
|
|
636
503
|
// Finger factor applies more penalty if more fingers were used.
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
2)) *
|
|
647
|
-
objectCount) /
|
|
648
|
-
this.strainNoteCount, 0.2), 1), 0.2);
|
|
504
|
+
let fingerFactor = 1;
|
|
505
|
+
for (let i = 0, penaltyIndex = 1; i < cursorAmounts.length; ++i) {
|
|
506
|
+
if (validIndexes.includes(i)) {
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
fingerFactor += Math.pow((penaltyIndex * cursorAmounts[i] * objectCount) /
|
|
510
|
+
this.strainNoteCount, 0.8);
|
|
511
|
+
++penaltyIndex;
|
|
512
|
+
}
|
|
649
513
|
// Length factor applies more penalty if there are more 3-fingered object.
|
|
650
514
|
const lengthFactor = 1 + Math.pow(objectCount / this.strainNoteCount, 1.2);
|
|
651
515
|
this.nerfFactors.push({
|
|
@@ -672,4 +536,4 @@ exports.ThreeFingerChecker = ThreeFingerChecker;
|
|
|
672
536
|
*
|
|
673
537
|
* Increasing this number will result in less sections being flagged.
|
|
674
538
|
*/
|
|
675
|
-
ThreeFingerChecker.strainThreshold =
|
|
539
|
+
ThreeFingerChecker.strainThreshold = 200;
|
|
@@ -3,13 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ThreeFingerBeatmapSection = void 0;
|
|
4
4
|
const BeatmapSection_1 = require("./BeatmapSection");
|
|
5
5
|
/**
|
|
6
|
-
* A section of a beatmap
|
|
6
|
+
* A section of a beatmap with extra information used for detecting three-finger usage.
|
|
7
7
|
*/
|
|
8
8
|
class ThreeFingerBeatmapSection extends BeatmapSection_1.BeatmapSection {
|
|
9
9
|
constructor(values) {
|
|
10
10
|
super(values.firstObjectIndex, values.lastObjectIndex);
|
|
11
|
-
this.
|
|
12
|
-
this.dragFingerIndex = values.dragFingerIndex;
|
|
11
|
+
this.mainFingerIndex = values.mainFingerIndex;
|
|
13
12
|
}
|
|
14
13
|
}
|
|
15
14
|
exports.ThreeFingerBeatmapSection = ThreeFingerBeatmapSection;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rian8337/osu-droid-replay-analyzer",
|
|
3
|
-
"version": "1.4.12-alpha.
|
|
3
|
+
"version": "1.4.12-alpha.7",
|
|
4
4
|
"description": "A replay analyzer for analyzing osu!droid replay files.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"osu",
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "3db8ea957f7c4f6bf018224232f9db054b90f9b8"
|
|
49
49
|
}
|