@rian8337/osu-droid-replay-analyzer 4.0.0-beta.16 → 4.0.0-beta.18
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 +282 -539
- package/package.json +7 -7
- package/typings/index.d.ts +0 -835
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -51,18 +51,6 @@ exports.MovementType = void 0;
|
|
|
51
51
|
* Represents a cursor's occurrence.
|
|
52
52
|
*/
|
|
53
53
|
class CursorOccurrence {
|
|
54
|
-
/**
|
|
55
|
-
* The time of this occurrence.
|
|
56
|
-
*/
|
|
57
|
-
time;
|
|
58
|
-
/**
|
|
59
|
-
* The position of the occurrence.
|
|
60
|
-
*/
|
|
61
|
-
position;
|
|
62
|
-
/**
|
|
63
|
-
* The movement ID of the occurrence.
|
|
64
|
-
*/
|
|
65
|
-
id;
|
|
66
54
|
constructor(time, x, y, id) {
|
|
67
55
|
this.time = time;
|
|
68
56
|
this.position = new osuBase.Vector2(x, y);
|
|
@@ -125,7 +113,8 @@ class CursorOccurrenceGroup {
|
|
|
125
113
|
* The time at which this cursor occurrence group ends.
|
|
126
114
|
*/
|
|
127
115
|
get endTime() {
|
|
128
|
-
|
|
116
|
+
var _a, _b, _c, _d;
|
|
117
|
+
return (_d = (_b = (_a = this._up) === null || _a === void 0 ? void 0 : _a.time) !== null && _b !== void 0 ? _b : (_c = this._moves.at(-1)) === null || _c === void 0 ? void 0 : _c.time) !== null && _d !== void 0 ? _d : this._down.time;
|
|
129
118
|
}
|
|
130
119
|
/**
|
|
131
120
|
* The duration this cursor occurrence group is active for.
|
|
@@ -145,20 +134,6 @@ class CursorOccurrenceGroup {
|
|
|
145
134
|
}
|
|
146
135
|
return cursors;
|
|
147
136
|
}
|
|
148
|
-
/**
|
|
149
|
-
* The cursor occurrence of movement type `movementType.DOWN`.
|
|
150
|
-
*/
|
|
151
|
-
_down;
|
|
152
|
-
/**
|
|
153
|
-
* The cursor occurrences of movement type `movementType.MOVE`.
|
|
154
|
-
*/
|
|
155
|
-
_moves;
|
|
156
|
-
/**
|
|
157
|
-
* The cursor occurrence of movement type `movementType.UP`.
|
|
158
|
-
*
|
|
159
|
-
* May not exist, such as when the player holds their cursor until the end of a beatmap.
|
|
160
|
-
*/
|
|
161
|
-
_up;
|
|
162
137
|
constructor(down, moves, up) {
|
|
163
138
|
this._down = down;
|
|
164
139
|
this._moves = moves;
|
|
@@ -182,13 +157,14 @@ class CursorOccurrenceGroup {
|
|
|
182
157
|
* @returns The cursor occurrence at the given time, `null` if not found.
|
|
183
158
|
*/
|
|
184
159
|
cursorAt(time) {
|
|
160
|
+
var _a;
|
|
185
161
|
if (!this.isActiveAt(time)) {
|
|
186
162
|
return null;
|
|
187
163
|
}
|
|
188
164
|
if (this._down.time === time) {
|
|
189
165
|
return this._down;
|
|
190
166
|
}
|
|
191
|
-
if (this._up
|
|
167
|
+
if (((_a = this._up) === null || _a === void 0 ? void 0 : _a.time) === time) {
|
|
192
168
|
return this._up;
|
|
193
169
|
}
|
|
194
170
|
let l = 0;
|
|
@@ -218,17 +194,14 @@ class CursorOccurrenceGroup {
|
|
|
218
194
|
* This is used when analyzing replays using replay analyzer.
|
|
219
195
|
*/
|
|
220
196
|
class CursorData {
|
|
221
|
-
/**
|
|
222
|
-
* The occurrence groups of this cursor instance.
|
|
223
|
-
*/
|
|
224
|
-
occurrenceGroups = [];
|
|
225
197
|
/**
|
|
226
198
|
* The time at which the first occurrence of this cursor instance occurs.
|
|
227
199
|
*
|
|
228
200
|
* Will return `null` if there are no occurrences.
|
|
229
201
|
*/
|
|
230
202
|
get earliestOccurrenceTime() {
|
|
231
|
-
|
|
203
|
+
var _a, _b;
|
|
204
|
+
return (_b = (_a = this.occurrenceGroups.at(0)) === null || _a === void 0 ? void 0 : _a.startTime) !== null && _b !== void 0 ? _b : null;
|
|
232
205
|
}
|
|
233
206
|
/**
|
|
234
207
|
* The time at which the latest occurrence of this cursor instance occurs.
|
|
@@ -236,7 +209,8 @@ class CursorData {
|
|
|
236
209
|
* Will return `null` if there are no occurrences.
|
|
237
210
|
*/
|
|
238
211
|
get latestOccurrenceTime() {
|
|
239
|
-
|
|
212
|
+
var _a, _b;
|
|
213
|
+
return (_b = (_a = this.occurrenceGroups.at(-1)) === null || _a === void 0 ? void 0 : _a.endTime) !== null && _b !== void 0 ? _b : null;
|
|
240
214
|
}
|
|
241
215
|
/**
|
|
242
216
|
* The amount of cursor occurrences of this cursor instance.
|
|
@@ -263,6 +237,10 @@ class CursorData {
|
|
|
263
237
|
return this.occurrenceGroups.flatMap((v) => v.allOccurrences);
|
|
264
238
|
}
|
|
265
239
|
constructor(values) {
|
|
240
|
+
/**
|
|
241
|
+
* The occurrence groups of this cursor instance.
|
|
242
|
+
*/
|
|
243
|
+
this.occurrenceGroups = [];
|
|
266
244
|
let downOccurrence = null;
|
|
267
245
|
let moveOccurrences = [];
|
|
268
246
|
for (let i = 0; i < values.size; ++i) {
|
|
@@ -316,102 +294,50 @@ exports.HitResult = void 0;
|
|
|
316
294
|
* Utility to check whether or not a beatmap is three-fingered for rebalance scores.
|
|
317
295
|
*/
|
|
318
296
|
class RebalanceThreeFingerChecker {
|
|
319
|
-
/**
|
|
320
|
-
* The beatmap that is being analyzed.
|
|
321
|
-
*/
|
|
322
|
-
beatmap;
|
|
323
|
-
/**
|
|
324
|
-
* The data of the replay.
|
|
325
|
-
*/
|
|
326
|
-
data;
|
|
327
|
-
/**
|
|
328
|
-
* The difficulty attributes of the beatmap.
|
|
329
|
-
*/
|
|
330
|
-
difficultyAttributes;
|
|
331
|
-
/**
|
|
332
|
-
* The hitobjects to be analyzed.
|
|
333
|
-
*
|
|
334
|
-
* This is being maintained separately due to possible change in object scale.
|
|
335
|
-
*/
|
|
336
|
-
hitObjects;
|
|
337
|
-
/**
|
|
338
|
-
* The distance threshold between cursors to assume that two cursors are
|
|
339
|
-
* actually pressed with 1 finger in osu!pixels.
|
|
340
|
-
*
|
|
341
|
-
* This is used to prevent cases where a player would lift their finger
|
|
342
|
-
* too fast to the point where the 4th cursor instance or beyond is recorded
|
|
343
|
-
* as 1st, 2nd, or 3rd cursor instance.
|
|
344
|
-
*/
|
|
345
|
-
cursorDistancingDistanceThreshold = 60;
|
|
346
|
-
/**
|
|
347
|
-
* The threshold for the amount of cursors that are assumed to be pressed
|
|
348
|
-
* by a single finger.
|
|
349
|
-
*/
|
|
350
|
-
cursorDistancingCountThreshold = 10;
|
|
351
|
-
/**
|
|
352
|
-
* The threshold for the time difference of cursors that are assumed to be pressed
|
|
353
|
-
* by a single finger, in milliseconds.
|
|
354
|
-
*/
|
|
355
|
-
cursorDistancingTimeThreshold = 1000;
|
|
356
|
-
/**
|
|
357
|
-
* The amount of notes that has a tap strain exceeding `strainThreshold`.
|
|
358
|
-
*/
|
|
359
|
-
strainNoteCount;
|
|
360
|
-
/**
|
|
361
|
-
* The ratio threshold between non-3 finger cursors and 3-finger cursors.
|
|
362
|
-
*
|
|
363
|
-
* Increasing this number will increase detection accuracy, however
|
|
364
|
-
* it also increases the chance of falsely flagged plays.
|
|
365
|
-
*/
|
|
366
|
-
threeFingerRatioThreshold = 0.01;
|
|
367
|
-
/**
|
|
368
|
-
* Extended sections of the beatmap for drag detection.
|
|
369
|
-
*/
|
|
370
|
-
beatmapSections = [];
|
|
371
|
-
/**
|
|
372
|
-
* The hit window of this beatmap. Keep in mind that speed-changing mods do not change hit window length in game logic.
|
|
373
|
-
*/
|
|
374
|
-
hitWindow;
|
|
375
|
-
/**
|
|
376
|
-
* A reprocessed break points to match right on object time.
|
|
377
|
-
*
|
|
378
|
-
* This is used to increase detection accuracy since break points do not start right at the
|
|
379
|
-
* start of the hitobject before it and do not end right at the first hitobject after it.
|
|
380
|
-
*/
|
|
381
|
-
breakPointAccurateTimes = [];
|
|
382
|
-
/**
|
|
383
|
-
* A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
|
|
384
|
-
*
|
|
385
|
-
* Each index represents the cursor index.
|
|
386
|
-
*/
|
|
387
|
-
downCursorInstances = [];
|
|
388
|
-
/**
|
|
389
|
-
* Nerf factors from all sections that were three-fingered.
|
|
390
|
-
*/
|
|
391
|
-
nerfFactors = [];
|
|
392
|
-
/**
|
|
393
|
-
* Whether this score uses the Precise mod.
|
|
394
|
-
*/
|
|
395
|
-
isPrecise;
|
|
396
297
|
/**
|
|
397
298
|
* @param beatmap The beatmap to analyze.
|
|
398
299
|
* @param data The data of the replay.
|
|
399
300
|
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
400
301
|
*/
|
|
401
302
|
constructor(beatmap, data, difficultyAttributes) {
|
|
303
|
+
/**
|
|
304
|
+
* The ratio threshold between non-3 finger cursors and 3-finger cursors.
|
|
305
|
+
*
|
|
306
|
+
* Increasing this number will increase detection accuracy, however
|
|
307
|
+
* it also increases the chance of falsely flagged plays.
|
|
308
|
+
*/
|
|
309
|
+
this.threeFingerRatioThreshold = 0.01;
|
|
310
|
+
/**
|
|
311
|
+
* Extended sections of the beatmap for drag detection.
|
|
312
|
+
*/
|
|
313
|
+
this.beatmapSections = [];
|
|
314
|
+
/**
|
|
315
|
+
* A reprocessed break points to match right on object time.
|
|
316
|
+
*
|
|
317
|
+
* This is used to increase detection accuracy since break points do not start right at the
|
|
318
|
+
* start of the hitobject before it and do not end right at the first hitobject after it.
|
|
319
|
+
*/
|
|
320
|
+
this.breakPointAccurateTimes = [];
|
|
321
|
+
/**
|
|
322
|
+
* A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
|
|
323
|
+
*
|
|
324
|
+
* Each index represents the cursor index.
|
|
325
|
+
*/
|
|
326
|
+
this.downCursorInstances = [];
|
|
327
|
+
/**
|
|
328
|
+
* Nerf factors from all sections that were three-fingered.
|
|
329
|
+
*/
|
|
330
|
+
this.nerfFactors = [];
|
|
402
331
|
this.beatmap = beatmap;
|
|
403
332
|
this.data = data;
|
|
404
333
|
this.difficultyAttributes = difficultyAttributes;
|
|
405
334
|
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
406
335
|
overallDifficulty: beatmap.difficulty.od,
|
|
407
|
-
mods:
|
|
408
|
-
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
336
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
|
|
409
337
|
convertOverallDifficulty: false,
|
|
410
338
|
}).overallDifficulty;
|
|
411
339
|
this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
412
340
|
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
413
|
-
this.strainNoteCount =
|
|
414
|
-
this.difficultyAttributes.possibleThreeFingeredSections.reduce((a, v) => a + v.lastObjectIndex - v.firstObjectIndex + 1, 0);
|
|
415
341
|
this.hitObjects = beatmap.hitObjects.objects;
|
|
416
342
|
}
|
|
417
343
|
/**
|
|
@@ -433,12 +359,18 @@ class RebalanceThreeFingerChecker {
|
|
|
433
359
|
* nerf factor, taking beatmap difficulty into account.
|
|
434
360
|
*/
|
|
435
361
|
check() {
|
|
436
|
-
if (this.
|
|
362
|
+
if (this.difficultyAttributes.possibleThreeFingeredSections.length === 0) {
|
|
437
363
|
return { is3Finger: false, penalty: 1 };
|
|
438
364
|
}
|
|
439
365
|
this.getAccurateBreakPoints();
|
|
440
366
|
this.filterCursorInstances();
|
|
367
|
+
if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
|
|
368
|
+
return { is3Finger: false, penalty: 1 };
|
|
369
|
+
}
|
|
441
370
|
this.getBeatmapSections();
|
|
371
|
+
if (this.downCursorInstances.filter((v) => v.length > 0).length <= 3) {
|
|
372
|
+
return { is3Finger: false, penalty: 1 };
|
|
373
|
+
}
|
|
442
374
|
this.calculateNerfFactors();
|
|
443
375
|
const finalPenalty = this.calculateFinalPenalty();
|
|
444
376
|
return { is3Finger: finalPenalty > 1, penalty: finalPenalty };
|
|
@@ -554,16 +486,9 @@ class RebalanceThreeFingerChecker {
|
|
|
554
486
|
const dragFingerIndex = this.findDragIndex(section);
|
|
555
487
|
const objects = [];
|
|
556
488
|
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
557
|
-
objects.push({
|
|
558
|
-
object: this.hitObjects[i],
|
|
559
|
-
...this.getObjectPressIndex(this.hitObjects[i], this.data.hitObjectData[i], cursorLookupIndices, dragFingerIndex),
|
|
560
|
-
});
|
|
489
|
+
objects.push(Object.assign({ object: this.hitObjects[i] }, this.getObjectPressIndex(this.hitObjects[i], this.data.hitObjectData[i], cursorLookupIndices, dragFingerIndex)));
|
|
561
490
|
}
|
|
562
|
-
this.beatmapSections.push({
|
|
563
|
-
...section,
|
|
564
|
-
dragFingerIndex: dragFingerIndex,
|
|
565
|
-
objects: objects,
|
|
566
|
-
});
|
|
491
|
+
this.beatmapSections.push(Object.assign(Object.assign({}, section), { dragFingerIndex: dragFingerIndex, objects: objects }));
|
|
567
492
|
}
|
|
568
493
|
}
|
|
569
494
|
/**
|
|
@@ -639,8 +564,8 @@ class RebalanceThreeFingerChecker {
|
|
|
639
564
|
}
|
|
640
565
|
}
|
|
641
566
|
return {
|
|
642
|
-
pressingCursorInstanceIndex: nearestCursorInstanceIndex
|
|
643
|
-
pressingCursorIndex: nearestCursorIndex
|
|
567
|
+
pressingCursorInstanceIndex: nearestCursorInstanceIndex !== null && nearestCursorInstanceIndex !== void 0 ? nearestCursorInstanceIndex : -1,
|
|
568
|
+
pressingCursorIndex: nearestCursorIndex !== null && nearestCursorIndex !== void 0 ? nearestCursorIndex : -1,
|
|
644
569
|
};
|
|
645
570
|
}
|
|
646
571
|
/**
|
|
@@ -651,6 +576,7 @@ class RebalanceThreeFingerChecker {
|
|
|
651
576
|
* @param section The section to check.
|
|
652
577
|
*/
|
|
653
578
|
findDragIndex(section) {
|
|
579
|
+
var _a;
|
|
654
580
|
const objectData = this.data.hitObjectData;
|
|
655
581
|
const firstObject = this.hitObjects[section.firstObjectIndex];
|
|
656
582
|
const lastObject = this.hitObjects[section.lastObjectIndex];
|
|
@@ -763,31 +689,25 @@ class RebalanceThreeFingerChecker {
|
|
|
763
689
|
}
|
|
764
690
|
}
|
|
765
691
|
}
|
|
766
|
-
return cursorIndices.find((v) => v !== -1)
|
|
692
|
+
return (_a = cursorIndices.find((v) => v !== -1)) !== null && _a !== void 0 ? _a : -1;
|
|
767
693
|
}
|
|
768
694
|
/**
|
|
769
695
|
* Creates nerf factors by scanning through objects.
|
|
770
696
|
*/
|
|
771
697
|
calculateNerfFactors() {
|
|
772
698
|
for (const beatmapSection of this.beatmapSections) {
|
|
773
|
-
const
|
|
774
|
-
const cursorVectorTimes = [];
|
|
699
|
+
const cursorCounts = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
775
700
|
for (const object of beatmapSection.objects) {
|
|
776
701
|
if (object.pressingCursorIndex === -1 ||
|
|
777
702
|
object.pressingCursorInstanceIndex ===
|
|
778
703
|
beatmapSection.dragFingerIndex) {
|
|
779
704
|
continue;
|
|
780
705
|
}
|
|
781
|
-
++
|
|
782
|
-
const cursor = this.downCursorInstances[object.pressingCursorInstanceIndex][object.pressingCursorIndex];
|
|
783
|
-
cursorVectorTimes.push({
|
|
784
|
-
vector: new osuBase.Vector2(cursor.position.x, cursor.position.y),
|
|
785
|
-
time: cursor.time,
|
|
786
|
-
});
|
|
706
|
+
++cursorCounts[object.pressingCursorInstanceIndex];
|
|
787
707
|
}
|
|
788
708
|
if (beatmapSection.dragFingerIndex !== -1) {
|
|
789
709
|
// Remove the drag index to prevent it from being picked up into the detection.
|
|
790
|
-
|
|
710
|
+
cursorCounts.splice(beatmapSection.dragFingerIndex, 1);
|
|
791
711
|
}
|
|
792
712
|
// This index will be used to detect if a section is 3-fingered.
|
|
793
713
|
// If the section is dragged, the dragged instance will be ignored,
|
|
@@ -795,77 +715,34 @@ class RebalanceThreeFingerChecker {
|
|
|
795
715
|
const fingerSplitIndex = beatmapSection.dragFingerIndex !== -1 ? 2 : 3;
|
|
796
716
|
// Divide >=4th (3rd for drag) cursor instances with 1st + 2nd (+ 3rd for nondrag)
|
|
797
717
|
// to check if the section is 3-fingered.
|
|
798
|
-
const threeFingerRatio =
|
|
718
|
+
const threeFingerRatio = cursorCounts
|
|
799
719
|
.slice(fingerSplitIndex)
|
|
800
720
|
.reduce((acc, value) => acc + value, 0) /
|
|
801
|
-
Math.max(
|
|
721
|
+
Math.max(cursorCounts
|
|
802
722
|
.slice(0, fingerSplitIndex)
|
|
803
723
|
.reduce((acc, value) => acc + value, 0), 1);
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
if (pressIndex !== -1) {
|
|
810
|
-
if (cursorVectorTime.time -
|
|
811
|
-
similarPresses[pressIndex].lastTime >=
|
|
812
|
-
this.cursorDistancingTimeThreshold) {
|
|
813
|
-
// If the previous press is too late, remove it from the
|
|
814
|
-
// list and register the current press as a new press.
|
|
815
|
-
similarPresses.splice(pressIndex, 1);
|
|
816
|
-
similarPresses.push({
|
|
817
|
-
vector: cursorVectorTime.vector,
|
|
818
|
-
count: 1,
|
|
819
|
-
lastTime: cursorVectorTime.time,
|
|
820
|
-
});
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
similarPresses[pressIndex].vector = cursorVectorTime.vector;
|
|
824
|
-
similarPresses[pressIndex].lastTime = cursorVectorTime.time;
|
|
825
|
-
++similarPresses[pressIndex].count;
|
|
826
|
-
}
|
|
827
|
-
else {
|
|
828
|
-
similarPresses.push({
|
|
829
|
-
vector: cursorVectorTime.vector,
|
|
830
|
-
count: 1,
|
|
831
|
-
lastTime: cursorVectorTime.time,
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
// Sort by highest count; assume the order is 3rd, 4th, 5th, ... finger
|
|
836
|
-
const threeFingerPresses = similarPresses
|
|
837
|
-
.filter((v) => v.count >= this.cursorDistancingCountThreshold)
|
|
838
|
-
.sort((a, b) => b.count - a.count)
|
|
839
|
-
.slice(fingerSplitIndex);
|
|
840
|
-
if (threeFingerRatio > this.threeFingerRatioThreshold ||
|
|
841
|
-
threeFingerPresses.length > 0) {
|
|
842
|
-
const objectCount = beatmapSection.lastObjectIndex -
|
|
724
|
+
if (threeFingerRatio > this.threeFingerRatioThreshold) {
|
|
725
|
+
const threeFingeredObjectCount = cursorCounts
|
|
726
|
+
.slice(fingerSplitIndex)
|
|
727
|
+
.reduce((acc, value) => acc + value, 0);
|
|
728
|
+
const sectionObjectCount = beatmapSection.lastObjectIndex -
|
|
843
729
|
beatmapSection.firstObjectIndex +
|
|
844
730
|
1;
|
|
731
|
+
const threeFingeredObjectRatio = threeFingeredObjectCount / sectionObjectCount;
|
|
845
732
|
// We can ignore the first 3 (2 for drag) filled cursor instances
|
|
846
733
|
// since they are guaranteed not 3 finger.
|
|
847
|
-
const
|
|
734
|
+
const threeFingerCursorCounts = cursorCounts
|
|
848
735
|
.slice(fingerSplitIndex)
|
|
849
736
|
.filter((amount) => amount > 0);
|
|
850
737
|
// Finger factor applies more penalty if more fingers were used.
|
|
851
|
-
const fingerFactor =
|
|
852
|
-
|
|
853
|
-
Math.pow(((index + 1) * value * objectCount) /
|
|
854
|
-
this.strainNoteCount, 0.9), 1)
|
|
855
|
-
: Math.pow(threeFingerPresses.reduce((acc, value, index) => acc +
|
|
856
|
-
Math.pow(((index + 1) *
|
|
857
|
-
(value.count /
|
|
858
|
-
(this
|
|
859
|
-
.cursorDistancingCountThreshold *
|
|
860
|
-
2)) *
|
|
861
|
-
objectCount) /
|
|
862
|
-
this.strainNoteCount, 0.2), 1), 0.2);
|
|
738
|
+
const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
|
|
739
|
+
Math.pow((index + 1) * count * threeFingeredObjectRatio, 0.9), 1);
|
|
863
740
|
// Length factor applies more penalty if there are more 3-fingered object.
|
|
864
|
-
const lengthFactor = 1 + Math.pow(
|
|
741
|
+
const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 1.2);
|
|
865
742
|
this.nerfFactors.push({
|
|
866
|
-
strainFactor:
|
|
867
|
-
fingerFactor,
|
|
868
|
-
lengthFactor,
|
|
743
|
+
strainFactor: beatmapSection.sumStrain * threeFingeredObjectRatio,
|
|
744
|
+
fingerFactor: fingerFactor,
|
|
745
|
+
lengthFactor: lengthFactor,
|
|
869
746
|
});
|
|
870
747
|
}
|
|
871
748
|
}
|
|
@@ -880,6 +757,38 @@ class RebalanceThreeFingerChecker {
|
|
|
880
757
|
}
|
|
881
758
|
}
|
|
882
759
|
|
|
760
|
+
/******************************************************************************
|
|
761
|
+
Copyright (c) Microsoft Corporation.
|
|
762
|
+
|
|
763
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
764
|
+
purpose with or without fee is hereby granted.
|
|
765
|
+
|
|
766
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
767
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
768
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
769
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
770
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
771
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
772
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
773
|
+
***************************************************************************** */
|
|
774
|
+
/* global Reflect, Promise, SuppressedError, Symbol */
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
778
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
779
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
780
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
781
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
782
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
783
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
788
|
+
var e = new Error(message);
|
|
789
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
790
|
+
};
|
|
791
|
+
|
|
883
792
|
/**
|
|
884
793
|
* Represents a replay data in an osu!droid replay.
|
|
885
794
|
*
|
|
@@ -888,29 +797,6 @@ class RebalanceThreeFingerChecker {
|
|
|
888
797
|
* This is used when analyzing replays using replay analyzer.
|
|
889
798
|
*/
|
|
890
799
|
class ReplayData {
|
|
891
|
-
replayVersion;
|
|
892
|
-
folderName;
|
|
893
|
-
fileName;
|
|
894
|
-
hash;
|
|
895
|
-
time;
|
|
896
|
-
hit300k;
|
|
897
|
-
hit100k;
|
|
898
|
-
score;
|
|
899
|
-
maxCombo;
|
|
900
|
-
accuracy;
|
|
901
|
-
isFullCombo;
|
|
902
|
-
playerName;
|
|
903
|
-
rawMods;
|
|
904
|
-
rank;
|
|
905
|
-
convertedMods;
|
|
906
|
-
cursorMovement;
|
|
907
|
-
hitObjectData;
|
|
908
|
-
speedMultiplier;
|
|
909
|
-
forceCS;
|
|
910
|
-
forceAR;
|
|
911
|
-
forceOD;
|
|
912
|
-
forceHP;
|
|
913
|
-
flashlightFollowDelay;
|
|
914
800
|
constructor(values) {
|
|
915
801
|
this.replayVersion = values.replayVersion;
|
|
916
802
|
this.folderName = values.folderName;
|
|
@@ -942,98 +828,73 @@ class ReplayData {
|
|
|
942
828
|
* Utility to check whether or not a beatmap is three-fingered.
|
|
943
829
|
*/
|
|
944
830
|
class ThreeFingerChecker {
|
|
945
|
-
/**
|
|
946
|
-
* The beatmap that is being analyzed.
|
|
947
|
-
*/
|
|
948
|
-
beatmap;
|
|
949
|
-
/**
|
|
950
|
-
* The data of the replay.
|
|
951
|
-
*/
|
|
952
|
-
data;
|
|
953
|
-
/**
|
|
954
|
-
* The difficulty attributes of the beatmap.
|
|
955
|
-
*/
|
|
956
|
-
difficultyAttributes;
|
|
957
|
-
/**
|
|
958
|
-
* The distance threshold between cursors to assume that two cursors are
|
|
959
|
-
* actually pressed with 1 finger in osu!pixels.
|
|
960
|
-
*
|
|
961
|
-
* This is used to prevent cases where a player would lift their finger
|
|
962
|
-
* too fast to the point where the 4th cursor instance or beyond is recorded
|
|
963
|
-
* as 1st, 2nd, or 3rd cursor instance.
|
|
964
|
-
*/
|
|
965
|
-
cursorDistancingDistanceThreshold = 60;
|
|
966
|
-
/**
|
|
967
|
-
* The threshold for the amount of cursors that are assumed to be pressed
|
|
968
|
-
* by a single finger.
|
|
969
|
-
*/
|
|
970
|
-
cursorDistancingCountThreshold = 10;
|
|
971
|
-
/**
|
|
972
|
-
* The threshold for the time difference of cursors that are assumed to be pressed
|
|
973
|
-
* by a single finger, in milliseconds.
|
|
974
|
-
*/
|
|
975
|
-
cursorDistancingTimeThreshold = 1000;
|
|
976
|
-
/**
|
|
977
|
-
* The amount of notes that has a tap strain exceeding `strainThreshold`.
|
|
978
|
-
*/
|
|
979
|
-
strainNoteCount;
|
|
980
|
-
/**
|
|
981
|
-
* The ratio threshold between non-3 finger cursors and 3-finger cursors.
|
|
982
|
-
*
|
|
983
|
-
* Increasing this number will increase detection accuracy, however
|
|
984
|
-
* it also increases the chance of falsely flagged plays.
|
|
985
|
-
*/
|
|
986
|
-
threeFingerRatioThreshold = 0.01;
|
|
987
|
-
/**
|
|
988
|
-
* Extended sections of the beatmap for drag detection.
|
|
989
|
-
*/
|
|
990
|
-
beatmapSections = [];
|
|
991
|
-
/**
|
|
992
|
-
* This threshold is used to filter out accidental taps.
|
|
993
|
-
*
|
|
994
|
-
* Increasing this number makes the filtration more sensitive, however it
|
|
995
|
-
* will also increase the chance of 3-fingered plays getting out from
|
|
996
|
-
* being flagged.
|
|
997
|
-
*/
|
|
998
|
-
accidentalTapThreshold = 400;
|
|
999
|
-
/**
|
|
1000
|
-
* The hit window of this beatmap. Keep in mind that speed-changing mods do not change hit window length in game logic.
|
|
1001
|
-
*/
|
|
1002
|
-
hitWindow;
|
|
1003
|
-
/**
|
|
1004
|
-
* A reprocessed break points to match right on object time.
|
|
1005
|
-
*
|
|
1006
|
-
* This is used to increase detection accuracy since break points do not start right at the
|
|
1007
|
-
* start of the hitobject before it and do not end right at the first hitobject after it.
|
|
1008
|
-
*/
|
|
1009
|
-
breakPointAccurateTimes = [];
|
|
1010
|
-
/**
|
|
1011
|
-
* A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
|
|
1012
|
-
*
|
|
1013
|
-
* Each index represents the cursor index.
|
|
1014
|
-
*/
|
|
1015
|
-
downCursorInstances = [];
|
|
1016
|
-
/**
|
|
1017
|
-
* Nerf factors from all sections that were three-fingered.
|
|
1018
|
-
*/
|
|
1019
|
-
nerfFactors = [];
|
|
1020
|
-
/**
|
|
1021
|
-
* Whether this score uses the Precise mod.
|
|
1022
|
-
*/
|
|
1023
|
-
isPrecise;
|
|
1024
831
|
/**
|
|
1025
832
|
* @param beatmap The beatmap to analyze.
|
|
1026
833
|
* @param data The data of the replay.
|
|
1027
834
|
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
1028
835
|
*/
|
|
1029
836
|
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
|
+
/**
|
|
857
|
+
* The ratio threshold between non-3 finger cursors and 3-finger cursors.
|
|
858
|
+
*
|
|
859
|
+
* Increasing this number will increase detection accuracy, however
|
|
860
|
+
* it also increases the chance of falsely flagged plays.
|
|
861
|
+
*/
|
|
862
|
+
this.threeFingerRatioThreshold = 0.01;
|
|
863
|
+
/**
|
|
864
|
+
* Extended sections of the beatmap for drag detection.
|
|
865
|
+
*/
|
|
866
|
+
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
|
+
/**
|
|
876
|
+
* A reprocessed break points to match right on object time.
|
|
877
|
+
*
|
|
878
|
+
* This is used to increase detection accuracy since break points do not start right at the
|
|
879
|
+
* start of the hitobject before it and do not end right at the first hitobject after it.
|
|
880
|
+
*/
|
|
881
|
+
this.breakPointAccurateTimes = [];
|
|
882
|
+
/**
|
|
883
|
+
* A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
|
|
884
|
+
*
|
|
885
|
+
* Each index represents the cursor index.
|
|
886
|
+
*/
|
|
887
|
+
this.downCursorInstances = [];
|
|
888
|
+
/**
|
|
889
|
+
* Nerf factors from all sections that were three-fingered.
|
|
890
|
+
*/
|
|
891
|
+
this.nerfFactors = [];
|
|
1030
892
|
this.beatmap = beatmap;
|
|
1031
893
|
this.data = data;
|
|
1032
894
|
this.difficultyAttributes = difficultyAttributes;
|
|
1033
895
|
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
1034
896
|
overallDifficulty: beatmap.difficulty.od,
|
|
1035
|
-
mods:
|
|
1036
|
-
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
897
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
|
|
1037
898
|
convertOverallDifficulty: false,
|
|
1038
899
|
}).overallDifficulty;
|
|
1039
900
|
this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
@@ -1196,11 +1057,7 @@ class ThreeFingerChecker {
|
|
|
1196
1057
|
getBeatmapSections() {
|
|
1197
1058
|
for (const section of this.difficultyAttributes
|
|
1198
1059
|
.possibleThreeFingeredSections) {
|
|
1199
|
-
this.beatmapSections.push({
|
|
1200
|
-
...section,
|
|
1201
|
-
isDragged: false,
|
|
1202
|
-
dragFingerIndex: -1,
|
|
1203
|
-
});
|
|
1060
|
+
this.beatmapSections.push(Object.assign(Object.assign({}, section), { isDragged: false, dragFingerIndex: -1 }));
|
|
1204
1061
|
}
|
|
1205
1062
|
}
|
|
1206
1063
|
/**
|
|
@@ -1288,6 +1145,7 @@ class ThreeFingerChecker {
|
|
|
1288
1145
|
* @param cursorIndexes The indexes of the cursor instance that has at least an occurrence in the section.
|
|
1289
1146
|
*/
|
|
1290
1147
|
findDragIndex(sectionObjects, sectionReplayObjectData, cursorIndexes) {
|
|
1148
|
+
var _a;
|
|
1291
1149
|
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1292
1150
|
for (let i = 0; i < sectionObjects.length && cursorIndexes.every((v) => v !== -1); ++i) {
|
|
1293
1151
|
const object = sectionObjects[i];
|
|
@@ -1347,7 +1205,7 @@ class ThreeFingerChecker {
|
|
|
1347
1205
|
}
|
|
1348
1206
|
}
|
|
1349
1207
|
}
|
|
1350
|
-
return cursorIndexes.find((v) => v !== -1)
|
|
1208
|
+
return (_a = cursorIndexes.find((v) => v !== -1)) !== null && _a !== void 0 ? _a : -1;
|
|
1351
1209
|
}
|
|
1352
1210
|
/**
|
|
1353
1211
|
* Attempts to prevent accidental taps from being flagged.
|
|
@@ -1402,7 +1260,7 @@ class ThreeFingerChecker {
|
|
|
1402
1260
|
? objectData[beatmapSection.lastObjectIndex].accuracy
|
|
1403
1261
|
: this.hitWindow.hitWindowFor50(this.isPrecise));
|
|
1404
1262
|
const cursorAmounts = [];
|
|
1405
|
-
const
|
|
1263
|
+
const sectionCursors = [];
|
|
1406
1264
|
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
1407
1265
|
// Do not include drag cursor instance.
|
|
1408
1266
|
if (i === dragIndex) {
|
|
@@ -1414,14 +1272,12 @@ class ThreeFingerChecker {
|
|
|
1414
1272
|
if (cursors[j].time >= startTime &&
|
|
1415
1273
|
cursors[j].time <= endTime) {
|
|
1416
1274
|
++amount;
|
|
1417
|
-
|
|
1418
|
-
vector: new osuBase.Vector2(cursors[j].position.x, cursors[j].position.y),
|
|
1419
|
-
time: cursors[j].time,
|
|
1420
|
-
});
|
|
1275
|
+
sectionCursors.push(cursors[j]);
|
|
1421
1276
|
}
|
|
1422
1277
|
}
|
|
1423
1278
|
cursorAmounts.push(amount);
|
|
1424
1279
|
}
|
|
1280
|
+
sectionCursors.sort((a, b) => a.time - b.time);
|
|
1425
1281
|
// This index will be used to detect if a section is 3-fingered.
|
|
1426
1282
|
// If the section is dragged, the dragged instance will be ignored,
|
|
1427
1283
|
// hence why the index is 1 less than nondragged section.
|
|
@@ -1435,31 +1291,31 @@ class ThreeFingerChecker {
|
|
|
1435
1291
|
.slice(0, fingerSplitIndex)
|
|
1436
1292
|
.reduce((acc, value) => acc + value, 0);
|
|
1437
1293
|
const similarPresses = [];
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
const pressIndex = similarPresses.findIndex((v) => v.vector.getDistance(cursorVectorTime.vector) <=
|
|
1294
|
+
for (const sectionCursor of sectionCursors) {
|
|
1295
|
+
const pressIndex = similarPresses.findIndex((v) => v.lastPosition.getDistance(sectionCursor.position) <=
|
|
1441
1296
|
this.cursorDistancingDistanceThreshold);
|
|
1442
1297
|
if (pressIndex !== -1) {
|
|
1443
|
-
if (
|
|
1298
|
+
if (sectionCursor.time -
|
|
1444
1299
|
similarPresses[pressIndex].lastTime >=
|
|
1445
1300
|
this.cursorDistancingTimeThreshold) {
|
|
1446
1301
|
similarPresses.splice(pressIndex, 1);
|
|
1447
1302
|
similarPresses.push({
|
|
1448
|
-
|
|
1303
|
+
lastPosition: sectionCursor.position,
|
|
1449
1304
|
count: 1,
|
|
1450
|
-
lastTime:
|
|
1305
|
+
lastTime: sectionCursor.time,
|
|
1451
1306
|
});
|
|
1452
1307
|
continue;
|
|
1453
1308
|
}
|
|
1454
|
-
similarPresses[pressIndex].
|
|
1455
|
-
|
|
1309
|
+
similarPresses[pressIndex].lastPosition =
|
|
1310
|
+
sectionCursor.position;
|
|
1311
|
+
similarPresses[pressIndex].lastTime = sectionCursor.time;
|
|
1456
1312
|
++similarPresses[pressIndex].count;
|
|
1457
1313
|
}
|
|
1458
1314
|
else {
|
|
1459
1315
|
similarPresses.push({
|
|
1460
|
-
|
|
1316
|
+
lastPosition: sectionCursor.position,
|
|
1461
1317
|
count: 1,
|
|
1462
|
-
lastTime:
|
|
1318
|
+
lastTime: sectionCursor.time,
|
|
1463
1319
|
});
|
|
1464
1320
|
}
|
|
1465
1321
|
}
|
|
@@ -1519,49 +1375,6 @@ class ThreeFingerChecker {
|
|
|
1519
1375
|
* Contains information about which cursor index hits a hitobject.
|
|
1520
1376
|
*/
|
|
1521
1377
|
class IndexedHitObject {
|
|
1522
|
-
/**
|
|
1523
|
-
* The cursor index that hits the hitobject.
|
|
1524
|
-
*
|
|
1525
|
-
* If -1, the detection was unable to find any cursor that attempted to hit
|
|
1526
|
-
* the hitobject or it did not meet the criteria for detection.
|
|
1527
|
-
*/
|
|
1528
|
-
cursorIndex;
|
|
1529
|
-
/**
|
|
1530
|
-
* The group index of the cursor within the cursor index that hits the hitobject.
|
|
1531
|
-
*
|
|
1532
|
-
* If -1, the detection was unable to find any cursor that attempted to hit
|
|
1533
|
-
* the hitobject or it did not meet the criteria for detection.
|
|
1534
|
-
*/
|
|
1535
|
-
groupIndex;
|
|
1536
|
-
/**
|
|
1537
|
-
* The occurrence index within the group of the cursor within the cursor index that hits the hitobject.
|
|
1538
|
-
*
|
|
1539
|
-
* If -1, the detection was unable to find any cursor that attempted to hit
|
|
1540
|
-
* the hitobject or it did not meet the criteria for detection.
|
|
1541
|
-
*/
|
|
1542
|
-
occurrenceIndex;
|
|
1543
|
-
/**
|
|
1544
|
-
* The angle of the movement of the cursor towards the next hitobject.
|
|
1545
|
-
*/
|
|
1546
|
-
angle;
|
|
1547
|
-
/**
|
|
1548
|
-
* If this is a slider, whether the slider was cheesed.
|
|
1549
|
-
*/
|
|
1550
|
-
sliderCheesed = false;
|
|
1551
|
-
/**
|
|
1552
|
-
* The underlying difficulty hitobject.
|
|
1553
|
-
*/
|
|
1554
|
-
object;
|
|
1555
|
-
/**
|
|
1556
|
-
* The position of the cursor at the end of this hitobject.
|
|
1557
|
-
*
|
|
1558
|
-
* Will be altered during detection.
|
|
1559
|
-
*/
|
|
1560
|
-
endCursorPosition;
|
|
1561
|
-
/**
|
|
1562
|
-
* Whether the hitobject is likely two-handed.
|
|
1563
|
-
*/
|
|
1564
|
-
is2Handed;
|
|
1565
1378
|
/**
|
|
1566
1379
|
* @param object The underlying difficulty hitobject.
|
|
1567
1380
|
* @param cursorIndex The cursor index that moves towards the hitobject.
|
|
@@ -1570,6 +1383,10 @@ class IndexedHitObject {
|
|
|
1570
1383
|
* @param angle The angle of the movement of the cursor that moves towards the hitobject.
|
|
1571
1384
|
*/
|
|
1572
1385
|
constructor(object, cursorIndex, groupIndex, occurrenceIndex, angle, is2Handed) {
|
|
1386
|
+
/**
|
|
1387
|
+
* If this is a slider, whether the slider was cheesed.
|
|
1388
|
+
*/
|
|
1389
|
+
this.sliderCheesed = false;
|
|
1573
1390
|
this.object = object;
|
|
1574
1391
|
this.cursorIndex = cursorIndex;
|
|
1575
1392
|
this.groupIndex = groupIndex;
|
|
@@ -1585,38 +1402,21 @@ class IndexedHitObject {
|
|
|
1585
1402
|
* Utility to check whether or not a beatmap is two-handed.
|
|
1586
1403
|
*/
|
|
1587
1404
|
class TwoHandChecker {
|
|
1588
|
-
/**
|
|
1589
|
-
* The difficulty calculator that is being analyzed.
|
|
1590
|
-
*/
|
|
1591
|
-
calculator;
|
|
1592
|
-
/**
|
|
1593
|
-
* The data of the replay.
|
|
1594
|
-
*/
|
|
1595
|
-
data;
|
|
1596
|
-
/**
|
|
1597
|
-
* The hitobjects of the beatmap that have been assigned with their respective cursor index.
|
|
1598
|
-
*/
|
|
1599
|
-
indexedHitObjects = [];
|
|
1600
|
-
/**
|
|
1601
|
-
* The osu!droid hitwindow of the analyzed beatmap.
|
|
1602
|
-
*/
|
|
1603
|
-
hitWindow;
|
|
1604
|
-
/**
|
|
1605
|
-
* The 50 osu!droid hit window of the analyzed beatmap.
|
|
1606
|
-
*/
|
|
1607
|
-
hitWindow50;
|
|
1608
1405
|
// private csvString: string;
|
|
1609
1406
|
/**
|
|
1610
1407
|
* @param calculator The difficulty calculator to analyze.
|
|
1611
1408
|
* @param data The data of the replay.
|
|
1612
1409
|
*/
|
|
1613
1410
|
constructor(calculator, data) {
|
|
1411
|
+
/**
|
|
1412
|
+
* The hitobjects of the beatmap that have been assigned with their respective cursor index.
|
|
1413
|
+
*/
|
|
1414
|
+
this.indexedHitObjects = [];
|
|
1614
1415
|
this.calculator = calculator;
|
|
1615
1416
|
this.data = data;
|
|
1616
1417
|
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
1617
1418
|
overallDifficulty: calculator.beatmap.difficulty.od,
|
|
1618
|
-
mods:
|
|
1619
|
-
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
1419
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
|
|
1620
1420
|
convertOverallDifficulty: false,
|
|
1621
1421
|
}).overallDifficulty;
|
|
1622
1422
|
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
@@ -2197,22 +1997,6 @@ class TwoHandChecker {
|
|
|
2197
1997
|
* Utility to check whether relevant sliders in a beatmap are cheesed.
|
|
2198
1998
|
*/
|
|
2199
1999
|
class SliderCheeseChecker {
|
|
2200
|
-
/**
|
|
2201
|
-
* The beatmap that is being analyzed.
|
|
2202
|
-
*/
|
|
2203
|
-
beatmap;
|
|
2204
|
-
/**
|
|
2205
|
-
* The data of the replay.
|
|
2206
|
-
*/
|
|
2207
|
-
data;
|
|
2208
|
-
/**
|
|
2209
|
-
* The difficulty attributes of the beatmap.
|
|
2210
|
-
*/
|
|
2211
|
-
difficultyAttributes;
|
|
2212
|
-
/**
|
|
2213
|
-
* The 50 osu!droid hit window of the analyzed beatmap.
|
|
2214
|
-
*/
|
|
2215
|
-
hitWindow50;
|
|
2216
2000
|
/**
|
|
2217
2001
|
* @param beatmap The beatmap to analyze.
|
|
2218
2002
|
* @param data The data of the replay.
|
|
@@ -2224,8 +2008,7 @@ class SliderCheeseChecker {
|
|
|
2224
2008
|
this.difficultyAttributes = difficultyAttributes;
|
|
2225
2009
|
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
2226
2010
|
overallDifficulty: beatmap.difficulty.od,
|
|
2227
|
-
mods:
|
|
2228
|
-
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
2011
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
|
|
2229
2012
|
convertOverallDifficulty: false,
|
|
2230
2013
|
}).overallDifficulty;
|
|
2231
2014
|
this.hitWindow50 = new osuBase.DroidHitWindow(od).hitWindowFor50(this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
@@ -2427,78 +2210,57 @@ class SliderCheeseChecker {
|
|
|
2427
2210
|
* Once analyzed, the result can be accessed via the `data` property.
|
|
2428
2211
|
*/
|
|
2429
2212
|
class ReplayAnalyzer {
|
|
2430
|
-
/**
|
|
2431
|
-
* The score ID of the replay.
|
|
2432
|
-
*/
|
|
2433
|
-
scoreID;
|
|
2434
|
-
/**
|
|
2435
|
-
* The original odr file of the replay.
|
|
2436
|
-
*/
|
|
2437
|
-
originalODR = null;
|
|
2438
|
-
/**
|
|
2439
|
-
* The fixed odr file of the replay.
|
|
2440
|
-
*/
|
|
2441
|
-
fixedODR = null;
|
|
2442
|
-
/**
|
|
2443
|
-
* Whether or not the play is considered using >=3 finger abuse.
|
|
2444
|
-
*/
|
|
2445
|
-
is3Finger;
|
|
2446
|
-
/**
|
|
2447
|
-
* Whether or not the play is considered 2-handed.
|
|
2448
|
-
*/
|
|
2449
|
-
is2Hand;
|
|
2450
|
-
/**
|
|
2451
|
-
* The beatmap that is being analyzed. `DroidDifficultyCalculator` or `RebalanceDroidDifficultyCalculator` is required for three finger or two hand analyzing.
|
|
2452
|
-
*/
|
|
2453
|
-
beatmap;
|
|
2454
|
-
/**
|
|
2455
|
-
* The difficulty attributes of the beatmap.
|
|
2456
|
-
*/
|
|
2457
|
-
difficultyAttributes;
|
|
2458
|
-
/**
|
|
2459
|
-
* The results of the analyzer. `null` when initialized.
|
|
2460
|
-
*/
|
|
2461
|
-
data = null;
|
|
2462
|
-
/**
|
|
2463
|
-
* Penalty value used to penalize dpp for 2-hand.
|
|
2464
|
-
*/
|
|
2465
|
-
aimPenalty = 1;
|
|
2466
|
-
/**
|
|
2467
|
-
* Penalty value used to penalize dpp for 3 finger abuse.
|
|
2468
|
-
*/
|
|
2469
|
-
tapPenalty = 1;
|
|
2470
|
-
/**
|
|
2471
|
-
* Penalty values used to penalize dpp for slider cheesing.
|
|
2472
|
-
*/
|
|
2473
|
-
sliderCheesePenalty = {
|
|
2474
|
-
aimPenalty: 1,
|
|
2475
|
-
flashlightPenalty: 1,
|
|
2476
|
-
visualPenalty: 1,
|
|
2477
|
-
};
|
|
2478
|
-
/**
|
|
2479
|
-
* Whether this replay has been checked against 3 finger usage.
|
|
2480
|
-
*/
|
|
2481
|
-
hasBeenCheckedFor3Finger = false;
|
|
2482
|
-
/**
|
|
2483
|
-
* Whether this replay has been checked against 2 hand usage.
|
|
2484
|
-
*/
|
|
2485
|
-
hasBeenCheckedFor2Hand = false;
|
|
2486
|
-
/**
|
|
2487
|
-
* Whether this replay has been checked against slider cheesing.
|
|
2488
|
-
*/
|
|
2489
|
-
hasBeenCheckedForSliderCheesing = false;
|
|
2490
|
-
/**
|
|
2491
|
-
* The amount of two-handed objects.
|
|
2492
|
-
*/
|
|
2493
|
-
twoHandedNoteCount = 0;
|
|
2494
|
-
convertedBeatmap;
|
|
2495
|
-
// Sizes of primitive data types in Java (in bytes)
|
|
2496
|
-
BYTE_LENGTH = 1;
|
|
2497
|
-
SHORT_LENGTH = 2;
|
|
2498
|
-
INT_LENGTH = 4;
|
|
2499
|
-
FLOAT_LENGTH = 4;
|
|
2500
|
-
LONG_LENGTH = 8;
|
|
2501
2213
|
constructor(values) {
|
|
2214
|
+
/**
|
|
2215
|
+
* The original odr file of the replay.
|
|
2216
|
+
*/
|
|
2217
|
+
this.originalODR = null;
|
|
2218
|
+
/**
|
|
2219
|
+
* The fixed odr file of the replay.
|
|
2220
|
+
*/
|
|
2221
|
+
this.fixedODR = null;
|
|
2222
|
+
/**
|
|
2223
|
+
* The results of the analyzer. `null` when initialized.
|
|
2224
|
+
*/
|
|
2225
|
+
this.data = null;
|
|
2226
|
+
/**
|
|
2227
|
+
* Penalty value used to penalize dpp for 2-hand.
|
|
2228
|
+
*/
|
|
2229
|
+
this.aimPenalty = 1;
|
|
2230
|
+
/**
|
|
2231
|
+
* Penalty value used to penalize dpp for 3 finger abuse.
|
|
2232
|
+
*/
|
|
2233
|
+
this.tapPenalty = 1;
|
|
2234
|
+
/**
|
|
2235
|
+
* Penalty values used to penalize dpp for slider cheesing.
|
|
2236
|
+
*/
|
|
2237
|
+
this.sliderCheesePenalty = {
|
|
2238
|
+
aimPenalty: 1,
|
|
2239
|
+
flashlightPenalty: 1,
|
|
2240
|
+
visualPenalty: 1,
|
|
2241
|
+
};
|
|
2242
|
+
/**
|
|
2243
|
+
* Whether this replay has been checked against 3 finger usage.
|
|
2244
|
+
*/
|
|
2245
|
+
this.hasBeenCheckedFor3Finger = false;
|
|
2246
|
+
/**
|
|
2247
|
+
* Whether this replay has been checked against 2 hand usage.
|
|
2248
|
+
*/
|
|
2249
|
+
this.hasBeenCheckedFor2Hand = false;
|
|
2250
|
+
/**
|
|
2251
|
+
* Whether this replay has been checked against slider cheesing.
|
|
2252
|
+
*/
|
|
2253
|
+
this.hasBeenCheckedForSliderCheesing = false;
|
|
2254
|
+
/**
|
|
2255
|
+
* The amount of two-handed objects.
|
|
2256
|
+
*/
|
|
2257
|
+
this.twoHandedNoteCount = 0;
|
|
2258
|
+
// Sizes of primitive data types in Java (in bytes)
|
|
2259
|
+
this.BYTE_LENGTH = 1;
|
|
2260
|
+
this.SHORT_LENGTH = 2;
|
|
2261
|
+
this.INT_LENGTH = 4;
|
|
2262
|
+
this.FLOAT_LENGTH = 4;
|
|
2263
|
+
this.LONG_LENGTH = 8;
|
|
2502
2264
|
this.scoreID = values.scoreID;
|
|
2503
2265
|
this.beatmap = values.map;
|
|
2504
2266
|
this.difficultyAttributes = values.difficultyAttributes;
|
|
@@ -2509,35 +2271,39 @@ class ReplayAnalyzer {
|
|
|
2509
2271
|
/**
|
|
2510
2272
|
* Analyzes a replay.
|
|
2511
2273
|
*/
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
this.originalODR
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
this.fixedODR
|
|
2521
|
-
|
|
2522
|
-
|
|
2274
|
+
analyze() {
|
|
2275
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2276
|
+
if (!this.originalODR && !this.fixedODR) {
|
|
2277
|
+
this.originalODR = yield this.downloadReplay();
|
|
2278
|
+
}
|
|
2279
|
+
if (!this.originalODR) {
|
|
2280
|
+
return this;
|
|
2281
|
+
}
|
|
2282
|
+
if (!this.fixedODR) {
|
|
2283
|
+
this.fixedODR = yield this.decompress().catch(() => null);
|
|
2284
|
+
}
|
|
2285
|
+
if (!this.fixedODR) {
|
|
2286
|
+
return this;
|
|
2287
|
+
}
|
|
2288
|
+
this.parseReplay();
|
|
2523
2289
|
return this;
|
|
2524
|
-
}
|
|
2525
|
-
this.parseReplay();
|
|
2526
|
-
return this;
|
|
2290
|
+
});
|
|
2527
2291
|
}
|
|
2528
2292
|
/**
|
|
2529
2293
|
* Downloads the given score ID's replay.
|
|
2530
2294
|
*/
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
.
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2295
|
+
downloadReplay() {
|
|
2296
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
2297
|
+
const apiRequestBuilder = new osuBase.DroidAPIRequestBuilder()
|
|
2298
|
+
.setRequireAPIkey(false)
|
|
2299
|
+
.setEndpoint("upload")
|
|
2300
|
+
.addParameter("", `${this.scoreID}.odr`);
|
|
2301
|
+
const result = yield apiRequestBuilder.sendRequest();
|
|
2302
|
+
if (result.statusCode !== 200) {
|
|
2303
|
+
return null;
|
|
2304
|
+
}
|
|
2305
|
+
return result.data;
|
|
2306
|
+
});
|
|
2541
2307
|
}
|
|
2542
2308
|
/**
|
|
2543
2309
|
* Decompresses a replay.
|
|
@@ -2551,15 +2317,15 @@ class ReplayAnalyzer {
|
|
|
2551
2317
|
stream.push(null);
|
|
2552
2318
|
stream
|
|
2553
2319
|
.pipe(unzipper.Parse())
|
|
2554
|
-
.on("entry",
|
|
2320
|
+
.on("entry", (entry) => __awaiter(this, void 0, void 0, function* () {
|
|
2555
2321
|
const fileName = entry.path;
|
|
2556
2322
|
if (fileName === "data") {
|
|
2557
|
-
return resolve(
|
|
2323
|
+
return resolve(yield entry.buffer());
|
|
2558
2324
|
}
|
|
2559
2325
|
else {
|
|
2560
2326
|
entry.autodrain();
|
|
2561
2327
|
}
|
|
2562
|
-
})
|
|
2328
|
+
}))
|
|
2563
2329
|
.on("error", (e) => {
|
|
2564
2330
|
setTimeout(() => reject(e), 2000);
|
|
2565
2331
|
});
|
|
@@ -2569,6 +2335,7 @@ class ReplayAnalyzer {
|
|
|
2569
2335
|
* Parses a replay after being downloaded and converted to a buffer.
|
|
2570
2336
|
*/
|
|
2571
2337
|
parseReplay() {
|
|
2338
|
+
var _a, _b, _c;
|
|
2572
2339
|
if (!this.fixedODR) {
|
|
2573
2340
|
return;
|
|
2574
2341
|
}
|
|
@@ -2579,7 +2346,7 @@ class ReplayAnalyzer {
|
|
|
2579
2346
|
try {
|
|
2580
2347
|
rawObject = javaDeserialization__namespace.parse(this.fixedODR);
|
|
2581
2348
|
}
|
|
2582
|
-
catch {
|
|
2349
|
+
catch (_d) {
|
|
2583
2350
|
return;
|
|
2584
2351
|
}
|
|
2585
2352
|
const resultObject = {
|
|
@@ -2593,11 +2360,12 @@ class ReplayAnalyzer {
|
|
|
2593
2360
|
rank: "",
|
|
2594
2361
|
};
|
|
2595
2362
|
const determineRank = () => {
|
|
2363
|
+
var _a, _b;
|
|
2596
2364
|
const totalHits = resultObject.accuracy.n300 +
|
|
2597
2365
|
resultObject.accuracy.n100 +
|
|
2598
2366
|
resultObject.accuracy.n50 +
|
|
2599
2367
|
resultObject.accuracy.nmiss;
|
|
2600
|
-
const isHidden = resultObject.convertedMods
|
|
2368
|
+
const isHidden = (_b = (_a = resultObject.convertedMods) === null || _a === void 0 ? void 0 : _a.some((m) => m instanceof osuBase.ModHidden || m instanceof osuBase.ModFlashlight)) !== null && _b !== void 0 ? _b : false;
|
|
2601
2369
|
const hit300Ratio = resultObject.accuracy.n300 / totalHits;
|
|
2602
2370
|
switch (true) {
|
|
2603
2371
|
case resultObject.accuracy.value() === 1:
|
|
@@ -2780,10 +2548,10 @@ class ReplayAnalyzer {
|
|
|
2780
2548
|
}
|
|
2781
2549
|
// Parse max combo, hit results, and accuracy in old replay version
|
|
2782
2550
|
if (resultObject.replayVersion < 3) {
|
|
2783
|
-
const objects = (this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2551
|
+
const objects = (_a = (this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2784
2552
|
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
|
|
2785
2553
|
? this.beatmap.beatmap
|
|
2786
|
-
: this.beatmap)
|
|
2554
|
+
: this.beatmap)) === null || _a === void 0 ? void 0 : _a.hitObjects.objects;
|
|
2787
2555
|
let grantsGekiOrKatu = true;
|
|
2788
2556
|
for (let i = 0; i < resultObject.hitObjectData.length; ++i) {
|
|
2789
2557
|
// Hit result
|
|
@@ -2805,14 +2573,14 @@ class ReplayAnalyzer {
|
|
|
2805
2573
|
case exports.HitResult.good:
|
|
2806
2574
|
++resultObject.accuracy.n100;
|
|
2807
2575
|
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
2808
|
-
resultObject.hit100k
|
|
2576
|
+
(_b = resultObject.hit100k) !== null && _b !== void 0 ? _b : (resultObject.hit100k = 0);
|
|
2809
2577
|
++resultObject.hit100k;
|
|
2810
2578
|
}
|
|
2811
2579
|
break;
|
|
2812
2580
|
case exports.HitResult.great:
|
|
2813
2581
|
++resultObject.accuracy.n300;
|
|
2814
2582
|
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
2815
|
-
resultObject.hit300k
|
|
2583
|
+
(_c = resultObject.hit300k) !== null && _c !== void 0 ? _c : (resultObject.hit300k = 0);
|
|
2816
2584
|
++resultObject.hit300k;
|
|
2817
2585
|
}
|
|
2818
2586
|
break;
|
|
@@ -2846,7 +2614,7 @@ class ReplayAnalyzer {
|
|
|
2846
2614
|
const { objects } = beatmap.hitObjects;
|
|
2847
2615
|
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
2848
2616
|
overallDifficulty: beatmap.difficulty.od,
|
|
2849
|
-
mods:
|
|
2617
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
|
|
2850
2618
|
}).overallDifficulty;
|
|
2851
2619
|
const hitWindow50 = new osuBase.DroidHitWindow(od).hitWindowFor50(this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise));
|
|
2852
2620
|
// The accuracy of sliders is set to (50 hit window)ms + 13ms if their head was not hit:
|
|
@@ -2923,6 +2691,7 @@ class ReplayAnalyzer {
|
|
|
2923
2691
|
* Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
|
|
2924
2692
|
*/
|
|
2925
2693
|
checkFor3Finger() {
|
|
2694
|
+
var _a;
|
|
2926
2695
|
if (!this.beatmap || !this.data || !this.difficultyAttributes) {
|
|
2927
2696
|
return;
|
|
2928
2697
|
}
|
|
@@ -2941,13 +2710,13 @@ class ReplayAnalyzer {
|
|
|
2941
2710
|
hp: this.data.forceHP,
|
|
2942
2711
|
}));
|
|
2943
2712
|
}
|
|
2944
|
-
this.convertedBeatmap
|
|
2713
|
+
(_a = this.convertedBeatmap) !== null && _a !== void 0 ? _a : (this.convertedBeatmap = new osuBase.BeatmapConverter(this.beatmap instanceof osuBase.Beatmap
|
|
2945
2714
|
? this.beatmap
|
|
2946
2715
|
: this.beatmap.beatmap).convert({
|
|
2947
2716
|
mode: osuBase.Modes.droid,
|
|
2948
2717
|
mods: mods,
|
|
2949
2718
|
customSpeedMultiplier: this.data.speedMultiplier,
|
|
2950
|
-
});
|
|
2719
|
+
}));
|
|
2951
2720
|
}
|
|
2952
2721
|
const threeFingerChecker = this.difficultyAttributes.mode === "rebalance"
|
|
2953
2722
|
? new RebalanceThreeFingerChecker(this.convertedBeatmap, this.data, this.difficultyAttributes)
|
|
@@ -2980,6 +2749,7 @@ class ReplayAnalyzer {
|
|
|
2980
2749
|
* Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
|
|
2981
2750
|
*/
|
|
2982
2751
|
checkForSliderCheesing() {
|
|
2752
|
+
var _a;
|
|
2983
2753
|
if (!this.beatmap || !this.data || !this.difficultyAttributes) {
|
|
2984
2754
|
return;
|
|
2985
2755
|
}
|
|
@@ -2998,13 +2768,13 @@ class ReplayAnalyzer {
|
|
|
2998
2768
|
hp: this.data.forceHP,
|
|
2999
2769
|
}));
|
|
3000
2770
|
}
|
|
3001
|
-
this.convertedBeatmap
|
|
2771
|
+
(_a = this.convertedBeatmap) !== null && _a !== void 0 ? _a : (this.convertedBeatmap = new osuBase.BeatmapConverter(this.beatmap instanceof osuBase.Beatmap
|
|
3002
2772
|
? this.beatmap
|
|
3003
2773
|
: this.beatmap.beatmap).convert({
|
|
3004
2774
|
mode: osuBase.Modes.droid,
|
|
3005
2775
|
mods: mods,
|
|
3006
2776
|
customSpeedMultiplier: this.data.speedMultiplier,
|
|
3007
|
-
});
|
|
2777
|
+
}));
|
|
3008
2778
|
}
|
|
3009
2779
|
const sliderCheeseChecker = new SliderCheeseChecker(this.convertedBeatmap, this.data, this.difficultyAttributes);
|
|
3010
2780
|
this.sliderCheesePenalty = sliderCheeseChecker.check();
|
|
@@ -3020,33 +2790,6 @@ class ReplayAnalyzer {
|
|
|
3020
2790
|
* This is used when analyzing replays using replay analyzer.
|
|
3021
2791
|
*/
|
|
3022
2792
|
class ReplayObjectData {
|
|
3023
|
-
/**
|
|
3024
|
-
* For circles, this is the offset at which the circle was hit. If the hit accuracy is 10000, it means the circle was tapped too late ([game source code](https://github.com/osudroid/osu-droid/blob/6306c68e3ffaf671eac794bf45cc95c0f3313a82/src/ru/nsu/ccfit/zuev/osu/game/HitCircle.java#L298-L306)).
|
|
3025
|
-
*
|
|
3026
|
-
* For sliders, this is the offset at which the slider head was hit. For
|
|
3027
|
-
* sliderbreaks, the accuracy would be `Math.floor(<hit window 50>ms) + 13ms` ([game source code](https://github.com/osudroid/osu-droid/blob/6306c68e3ffaf671eac794bf45cc95c0f3313a82/src/ru/nsu/ccfit/zuev/osu/game/Slider.java#L821)).
|
|
3028
|
-
*
|
|
3029
|
-
* For spinners, this is the total amount at which the spinner was spinned:
|
|
3030
|
-
* ```js
|
|
3031
|
-
* const rotations = Math.floor(data.accuracy / 4);
|
|
3032
|
-
* ```
|
|
3033
|
-
* The remainder of the division denotes the hit result of the spinner:
|
|
3034
|
-
* - `HitResult.great`: 3
|
|
3035
|
-
* - `HitResult.good`: 2
|
|
3036
|
-
* - `HitResult.meh`: 1
|
|
3037
|
-
* - `HitResult.miss`: 0
|
|
3038
|
-
*/
|
|
3039
|
-
accuracy;
|
|
3040
|
-
/**
|
|
3041
|
-
* The tickset of the hitobject.
|
|
3042
|
-
*
|
|
3043
|
-
* This is used to determine whether or not a slider event (tick, repeat, and end) is hit based on the order they appear.
|
|
3044
|
-
*/
|
|
3045
|
-
tickset;
|
|
3046
|
-
/**
|
|
3047
|
-
* The bitwise hit result of the hitobject.
|
|
3048
|
-
*/
|
|
3049
|
-
result;
|
|
3050
2793
|
constructor(values) {
|
|
3051
2794
|
this.accuracy = values.accuracy;
|
|
3052
2795
|
this.tickset = values.tickset;
|