@rian8337/osu-droid-replay-analyzer 4.0.0-beta.23 → 4.0.0-beta.25
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 +802 -858
- package/package.json +5 -5
- package/typings/index.d.ts +223 -111
package/dist/index.js
CHANGED
|
@@ -294,19 +294,15 @@ exports.HitResult = void 0;
|
|
|
294
294
|
* Utility to check whether or not a beatmap is three-fingered for rebalance scores.
|
|
295
295
|
*/
|
|
296
296
|
class RebalanceThreeFingerChecker {
|
|
297
|
+
get hitWindow50() {
|
|
298
|
+
return this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
299
|
+
}
|
|
297
300
|
/**
|
|
298
301
|
* @param beatmap The beatmap to analyze.
|
|
299
302
|
* @param data The data of the replay.
|
|
300
303
|
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
301
304
|
*/
|
|
302
305
|
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
306
|
/**
|
|
311
307
|
* Extended sections of the beatmap for drag detection.
|
|
312
308
|
*/
|
|
@@ -333,15 +329,12 @@ class RebalanceThreeFingerChecker {
|
|
|
333
329
|
this.difficultyAttributes = difficultyAttributes;
|
|
334
330
|
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
335
331
|
overallDifficulty: beatmap.difficulty.od,
|
|
336
|
-
mods: osuBase.ModUtil.removeSpeedChangingMods(
|
|
332
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(difficultyAttributes.mods),
|
|
337
333
|
convertOverallDifficulty: false,
|
|
338
334
|
}).overallDifficulty;
|
|
339
|
-
this.isPrecise =
|
|
335
|
+
this.isPrecise = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
336
|
+
this.isHardRock = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
340
337
|
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
341
|
-
this.hitObjects = beatmap.hitObjects.objects;
|
|
342
|
-
this.objectRadiusThreshold = this.hitObjects[0].radius;
|
|
343
|
-
// Only allow object radius threshold to deviate slightly from actual object radius.
|
|
344
|
-
this.maximumObjectRadiusThreshold = this.objectRadiusThreshold * 1.05;
|
|
345
338
|
}
|
|
346
339
|
/**
|
|
347
340
|
* Checks whether a beatmap is eligible to be detected for 3-finger.
|
|
@@ -362,7 +355,8 @@ class RebalanceThreeFingerChecker {
|
|
|
362
355
|
* nerf factor, taking beatmap difficulty into account.
|
|
363
356
|
*/
|
|
364
357
|
check() {
|
|
365
|
-
if (this.difficultyAttributes
|
|
358
|
+
if (!RebalanceThreeFingerChecker.isEligibleToDetect(this.difficultyAttributes) ||
|
|
359
|
+
this.data.cursorMovement.filter((v) => v.occurrenceGroups.length > 0).length <= 3) {
|
|
366
360
|
return { is3Finger: false, penalty: 1 };
|
|
367
361
|
}
|
|
368
362
|
this.getAccurateBreakPoints();
|
|
@@ -382,10 +376,11 @@ class RebalanceThreeFingerChecker {
|
|
|
382
376
|
* end of the hitobject before it and do not end right at the first hitobject after it.
|
|
383
377
|
*/
|
|
384
378
|
getAccurateBreakPoints() {
|
|
379
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
385
380
|
const objectData = this.data.hitObjectData;
|
|
386
381
|
for (const breakPoint of this.beatmap.events.breaks) {
|
|
387
|
-
const beforeIndex = osuBase.MathUtils.clamp(
|
|
388
|
-
const objectBefore =
|
|
382
|
+
const beforeIndex = osuBase.MathUtils.clamp(objects.findIndex((o) => o.endTime >= breakPoint.startTime) - 1, 0, objects.length - 2);
|
|
383
|
+
const objectBefore = objects[beforeIndex];
|
|
389
384
|
const objectBeforeData = objectData[beforeIndex];
|
|
390
385
|
let timeBefore = objectBefore.endTime;
|
|
391
386
|
if (objectBefore instanceof osuBase.Circle) {
|
|
@@ -397,9 +392,9 @@ class RebalanceThreeFingerChecker {
|
|
|
397
392
|
}
|
|
398
393
|
}
|
|
399
394
|
const afterIndex = beforeIndex + 1;
|
|
400
|
-
const objectAfter =
|
|
395
|
+
const objectAfter = objects[afterIndex];
|
|
401
396
|
const objectAfterData = objectData[afterIndex];
|
|
402
|
-
let timeAfter =
|
|
397
|
+
let timeAfter = objectAfter.startTime;
|
|
403
398
|
if (objectAfter instanceof osuBase.Circle &&
|
|
404
399
|
objectAfterData.result !== exports.HitResult.miss) {
|
|
405
400
|
timeAfter += objectAfterData.accuracy;
|
|
@@ -416,11 +411,12 @@ class RebalanceThreeFingerChecker {
|
|
|
416
411
|
* This also filters cursors that are in break period or happen before start/after end of the beatmap.
|
|
417
412
|
*/
|
|
418
413
|
filterCursorInstances() {
|
|
414
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
419
415
|
const objectData = this.data.hitObjectData;
|
|
420
416
|
const firstObjectResult = objectData[0].result;
|
|
421
417
|
const lastObjectResult = objectData.at(-1).result;
|
|
422
|
-
const firstObject =
|
|
423
|
-
const lastObject =
|
|
418
|
+
const firstObject = objects[0];
|
|
419
|
+
const lastObject = objects.at(-1);
|
|
424
420
|
// For sliders, automatically set hit window length to be as lenient as possible.
|
|
425
421
|
let firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
426
422
|
if (firstObject instanceof osuBase.Circle) {
|
|
@@ -480,289 +476,202 @@ class RebalanceThreeFingerChecker {
|
|
|
480
476
|
* detect dragged sections and improve detection speed.
|
|
481
477
|
*/
|
|
482
478
|
getBeatmapSections() {
|
|
483
|
-
const
|
|
479
|
+
const beatmapObjects = this.beatmap.hitObjects.objects;
|
|
480
|
+
const aimCursorGroupLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
481
|
+
// This intentionally starts from 1 because we need to look at the previous cursor.
|
|
482
|
+
const aimCursorLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 1);
|
|
483
|
+
const pressCursorLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
484
484
|
for (const section of this.difficultyAttributes
|
|
485
485
|
.possibleThreeFingeredSections) {
|
|
486
|
-
const dragFingerIndex = this.findDragIndex(section);
|
|
487
486
|
const objects = [];
|
|
488
487
|
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
489
|
-
|
|
488
|
+
const object = beatmapObjects[i];
|
|
489
|
+
const objectData = this.data.hitObjectData[i];
|
|
490
|
+
objects.push({
|
|
491
|
+
object: object,
|
|
492
|
+
aimingCursorInstanceIndex: this.getObjectAimIndex(object, objectData, aimCursorGroupLookupIndices, aimCursorLookupIndices),
|
|
493
|
+
pressingCursorInstanceIndex: this.getObjectPressIndex(object, objectData, pressCursorLookupIndices),
|
|
494
|
+
});
|
|
490
495
|
}
|
|
491
|
-
this.beatmapSections.push(Object.assign(Object.assign({}, section), {
|
|
496
|
+
this.beatmapSections.push(Object.assign(Object.assign({}, section), { objects: objects }));
|
|
492
497
|
}
|
|
493
498
|
}
|
|
494
499
|
/**
|
|
495
|
-
* Obtains the index of the
|
|
500
|
+
* Obtains the index of the cursor that aimed the object at the nearest time.
|
|
496
501
|
*
|
|
497
502
|
* @param object The object to obtain the index for.
|
|
498
503
|
* @param objectData The hit data of the object.
|
|
499
|
-
* @param
|
|
500
|
-
* @param
|
|
504
|
+
* @param cursorInstanceIndices The cursor indices to start looking for the cursor instance from, to save computation time.
|
|
505
|
+
* @param cursorGroupIndices The cursor indices to start looking for the cursor group from, to save computation time.
|
|
506
|
+
* @param cursorIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
501
507
|
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
502
508
|
*/
|
|
503
|
-
|
|
509
|
+
getObjectAimIndex(object, objectData, cursorGroupIndices, cursorIndices) {
|
|
504
510
|
if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
|
|
505
|
-
return
|
|
506
|
-
pressingCursorInstanceIndex: -1,
|
|
507
|
-
pressingCursorIndex: -1,
|
|
508
|
-
};
|
|
511
|
+
return -1;
|
|
509
512
|
}
|
|
510
|
-
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
511
513
|
// Check for sliderbreaks and treat them as misses.
|
|
512
514
|
if (object instanceof osuBase.Slider &&
|
|
513
|
-
objectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
514
|
-
return
|
|
515
|
-
pressingCursorInstanceIndex: -1,
|
|
516
|
-
pressingCursorIndex: -1,
|
|
517
|
-
};
|
|
518
|
-
}
|
|
519
|
-
// We are not directly using hit time to determine which cursor pressed the object
|
|
520
|
-
// to account for time difference between hit registration and object judgement.
|
|
521
|
-
let minHitTime = object.startTime;
|
|
522
|
-
let maxHitTime = object.startTime;
|
|
523
|
-
if (object instanceof osuBase.Circle) {
|
|
524
|
-
let hitWindowGap = hitWindow50;
|
|
525
|
-
switch (objectData.result) {
|
|
526
|
-
case exports.HitResult.great:
|
|
527
|
-
hitWindowGap = this.hitWindow.hitWindowFor300(this.isPrecise);
|
|
528
|
-
break;
|
|
529
|
-
case exports.HitResult.good:
|
|
530
|
-
hitWindowGap = this.hitWindow.hitWindowFor100(this.isPrecise);
|
|
531
|
-
break;
|
|
532
|
-
}
|
|
533
|
-
minHitTime -= hitWindowGap;
|
|
534
|
-
maxHitTime += hitWindowGap;
|
|
535
|
-
}
|
|
536
|
-
else if (object instanceof osuBase.Slider) {
|
|
537
|
-
minHitTime -= hitWindow50;
|
|
538
|
-
maxHitTime += Math.min(hitWindow50, object.spanDuration);
|
|
515
|
+
objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
|
|
516
|
+
return -1;
|
|
539
517
|
}
|
|
540
518
|
const hitTime = object.startTime + objectData.accuracy;
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
519
|
+
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
520
|
+
// We are maintaining the closest distance to the object.
|
|
521
|
+
// This is because the radius that is calculated is using an estimation.
|
|
522
|
+
// As such, it does not reflect the actual object radius in gameplay.
|
|
523
|
+
let closestDistance = Number.POSITIVE_INFINITY;
|
|
524
|
+
let nearestCursorIndex = -1;
|
|
525
|
+
// Observe the cursor position at the object's hit time.
|
|
526
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
527
|
+
const cursorData = this.data.cursorMovement[i];
|
|
528
|
+
for (let j = cursorGroupIndices[i]; j < cursorData.occurrenceGroups.length; cursorGroupIndices[i] = ++j) {
|
|
529
|
+
const cursorGroup = cursorData.occurrenceGroups[j];
|
|
530
|
+
if (cursorGroup.endTime < hitTime) {
|
|
531
|
+
// Reset cursor index pointer.
|
|
532
|
+
cursorIndices[i] = 1;
|
|
552
533
|
continue;
|
|
553
534
|
}
|
|
554
|
-
if (
|
|
535
|
+
if (cursorGroup.startTime > hitTime) {
|
|
555
536
|
break;
|
|
556
537
|
}
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
538
|
+
const cursors = cursorGroup.allOccurrences;
|
|
539
|
+
for (let k = cursorIndices[i]; k < cursors.length; cursorIndices[i] = ++k) {
|
|
540
|
+
const cursor = cursors[k];
|
|
541
|
+
const prevCursor = cursors[k - 1];
|
|
542
|
+
// Cursor is past the object's hit time.
|
|
543
|
+
if (prevCursor.time > hitTime) {
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
// Cursor is before the object's hit time.
|
|
547
|
+
if (hitTime > cursor.time) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
let distance;
|
|
551
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
552
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
553
|
+
switch (cursor.id) {
|
|
554
|
+
case exports.MovementType.up:
|
|
555
|
+
distance = prevPosition.getDistance(objectPosition);
|
|
556
|
+
break;
|
|
557
|
+
case exports.MovementType.move: {
|
|
558
|
+
// Interpolate movement.
|
|
559
|
+
const t = (hitTime - prevCursor.time) /
|
|
560
|
+
(cursor.time - prevCursor.time);
|
|
561
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
562
|
+
distance =
|
|
563
|
+
objectPosition.getDistance(cursorPosition);
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case exports.MovementType.down:
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (closestDistance > distance) {
|
|
570
|
+
closestDistance = distance;
|
|
571
|
+
nearestCursorIndex = i;
|
|
572
|
+
}
|
|
560
573
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
574
|
+
// Reset cursor index pointer on end of group.
|
|
575
|
+
if (cursorIndices[i] === cursors.length) {
|
|
576
|
+
cursorIndices[i] = 1;
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
564
579
|
}
|
|
580
|
+
// The previous object may still be hit with the same cursor group or cursor index.
|
|
581
|
+
cursorGroupIndices[i] = Math.max(0, cursorGroupIndices[i] - 1);
|
|
582
|
+
cursorIndices[i] = Math.max(1, cursorIndices[i] - 1);
|
|
565
583
|
}
|
|
566
|
-
return
|
|
567
|
-
pressingCursorInstanceIndex: nearestCursorInstanceIndex !== null && nearestCursorInstanceIndex !== void 0 ? nearestCursorInstanceIndex : -1,
|
|
568
|
-
pressingCursorIndex: nearestCursorIndex !== null && nearestCursorIndex !== void 0 ? nearestCursorIndex : -1,
|
|
569
|
-
};
|
|
584
|
+
return nearestCursorIndex;
|
|
570
585
|
}
|
|
571
586
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
* If the section is not dragged, -1 will be returned.
|
|
587
|
+
* Obtains the index of the nearest cursor of which an object was pressed in terms of time.
|
|
575
588
|
*
|
|
576
|
-
* @param
|
|
589
|
+
* @param object The object to obtain the index for.
|
|
590
|
+
* @param objectData The hit data of the object.
|
|
591
|
+
* @param cursorLookupIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
592
|
+
* @param excludedIndices The cursor indices that should not be checked.
|
|
593
|
+
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
577
594
|
*/
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
582
|
-
const firstObject = this.hitObjects[section.firstObjectIndex];
|
|
583
|
-
const firstObjectData = objectData[section.firstObjectIndex];
|
|
584
|
-
const lastObject = this.hitObjects[section.lastObjectIndex];
|
|
585
|
-
const lastObjectData = objectData[section.lastObjectIndex];
|
|
586
|
-
let firstObjectHitTime = firstObject.startTime;
|
|
587
|
-
let lastObjectHitTime = lastObject.startTime;
|
|
588
|
-
// Check for slider breaks.
|
|
589
|
-
if (firstObject instanceof osuBase.Slider &&
|
|
590
|
-
firstObjectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
591
|
-
firstObjectHitTime -= hitWindow50;
|
|
592
|
-
}
|
|
593
|
-
else {
|
|
594
|
-
firstObjectHitTime += firstObjectData.accuracy;
|
|
595
|
-
}
|
|
596
|
-
if (lastObject instanceof osuBase.Slider &&
|
|
597
|
-
lastObjectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
598
|
-
lastObjectHitTime += Math.min(hitWindow50, lastObject.nestedHitObjects[1].startTime - lastObject.startTime);
|
|
595
|
+
getObjectPressIndex(object, objectData, cursorLookupIndices) {
|
|
596
|
+
if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
|
|
597
|
+
return -1;
|
|
599
598
|
}
|
|
600
|
-
|
|
601
|
-
|
|
599
|
+
// Check for sliderbreaks and treat them as misses.
|
|
600
|
+
if (object instanceof osuBase.Slider &&
|
|
601
|
+
objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
|
|
602
|
+
return -1;
|
|
602
603
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
// Do not include cursors that don't have an occurence in this section
|
|
614
|
-
// this speeds up checking process.
|
|
615
|
-
if (c.occurrenceGroups.filter((v) => v.startTime <= firstObjectHitTime &&
|
|
616
|
-
v.endTime >= lastObjectHitTime).length === 0) {
|
|
617
|
-
continue;
|
|
618
|
-
}
|
|
619
|
-
// If this cursor instance doesn't move, it's not the cursor instance we want.
|
|
620
|
-
if (c.occurrenceGroups.filter((v) => v.moves.length > 0).length ===
|
|
621
|
-
0) {
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
cursorInstanceIndices.push(i);
|
|
625
|
-
cursorGroupIndices.push(0);
|
|
626
|
-
// Start checking from the second cursor.
|
|
627
|
-
cursorIndices.push(1);
|
|
628
|
-
}
|
|
629
|
-
const sectionObjects = this.hitObjects.slice(section.firstObjectIndex, section.lastObjectIndex + 1);
|
|
630
|
-
const sectionReplayObjectData = objectData.slice(section.firstObjectIndex, section.lastObjectIndex + 1);
|
|
631
|
-
for (let i = 0; i < sectionObjects.length &&
|
|
632
|
-
cursorInstanceIndices.some((v) => v !== -1); ++i) {
|
|
633
|
-
const object = sectionObjects[i];
|
|
634
|
-
const objectData = sectionReplayObjectData[i];
|
|
635
|
-
if (object instanceof osuBase.Spinner ||
|
|
636
|
-
objectData.result === exports.HitResult.miss) {
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
// Exclude sliderbreaks.
|
|
640
|
-
if (object instanceof osuBase.Slider &&
|
|
641
|
-
objectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
645
|
-
const hitTime = object.startTime + objectData.accuracy;
|
|
646
|
-
// Observe the cursor position at the object's hit time.
|
|
647
|
-
for (let j = 0; j < cursorInstanceIndices.length; ++j) {
|
|
648
|
-
if (cursorInstanceIndices[j] === -1) {
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
const cursorData = this.data.cursorMovement[cursorInstanceIndices[j]];
|
|
652
|
-
for (let k = cursorGroupIndices[j]; k < cursorData.occurrenceGroups.length; cursorGroupIndices[j] = ++k) {
|
|
653
|
-
const cursorGroup = cursorData.occurrenceGroups[k];
|
|
654
|
-
if (!cursorGroup.isActiveAt(hitTime)) {
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
const cursors = cursorGroup.allOccurrences;
|
|
658
|
-
// We are maintaining the closest distance of cursors to the object.
|
|
659
|
-
// This is because the radius that is calculated is using an estimation. As such,
|
|
660
|
-
// it does not perfectly reflect the actual object radius in gameplay.
|
|
661
|
-
let closestDistance = this.maximumObjectRadiusThreshold;
|
|
662
|
-
for (let l = cursorIndices[j]; l < cursors.length; cursorIndices[j] = ++l) {
|
|
663
|
-
const cursor = cursors[l];
|
|
664
|
-
const prevCursor = cursors[l - 1];
|
|
665
|
-
// Cursor is past the object's hit time.
|
|
666
|
-
if (prevCursor.time > hitTime) {
|
|
667
|
-
break;
|
|
668
|
-
}
|
|
669
|
-
// Cursor is before the object's hit time.
|
|
670
|
-
if (hitTime > cursor.time) {
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
let distance;
|
|
674
|
-
switch (cursor.id) {
|
|
675
|
-
case exports.MovementType.up:
|
|
676
|
-
distance =
|
|
677
|
-
prevCursor.position.getDistance(objectPosition);
|
|
678
|
-
break;
|
|
679
|
-
case exports.MovementType.move: {
|
|
680
|
-
// Interpolate movement.
|
|
681
|
-
const t = (hitTime - prevCursor.time) /
|
|
682
|
-
(cursor.time - prevCursor.time);
|
|
683
|
-
const cursorPosition = osuBase.Interpolation.lerp(prevCursor.position, cursor.position, t);
|
|
684
|
-
distance =
|
|
685
|
-
objectPosition.getDistance(cursorPosition);
|
|
686
|
-
break;
|
|
687
|
-
}
|
|
688
|
-
case exports.MovementType.down:
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
|
-
closestDistance = Math.min(closestDistance, distance);
|
|
692
|
-
}
|
|
693
|
-
// At this point, the object is guaranteed to be hit, but the distance may not reach the threshold
|
|
694
|
-
// due to radius estimation. In order to work around this, we store the farthest distance from all
|
|
695
|
-
// processed cursors so far and use it as threshold rather than the actual object radius.
|
|
696
|
-
if (closestDistance <= this.maximumObjectRadiusThreshold &&
|
|
697
|
-
closestDistance > this.objectRadiusThreshold) {
|
|
698
|
-
this.objectRadiusThreshold = closestDistance;
|
|
699
|
-
}
|
|
700
|
-
if (closestDistance > this.objectRadiusThreshold) {
|
|
701
|
-
cursorInstanceIndices[j] = -1;
|
|
702
|
-
}
|
|
604
|
+
const hitTime = object.startTime + objectData.accuracy;
|
|
605
|
+
let nearestCursorInstanceIndex = -1;
|
|
606
|
+
let nearestTime = Number.POSITIVE_INFINITY;
|
|
607
|
+
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
608
|
+
const cursors = this.downCursorInstances[i];
|
|
609
|
+
let cursorNearestTime = Number.POSITIVE_INFINITY;
|
|
610
|
+
for (let j = cursorLookupIndices[i]; j < cursors.length; cursorLookupIndices[i] = ++j) {
|
|
611
|
+
const cursor = cursors[j];
|
|
612
|
+
if (cursor.time > hitTime) {
|
|
703
613
|
break;
|
|
704
614
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
615
|
+
cursorNearestTime = hitTime - cursor.time;
|
|
616
|
+
}
|
|
617
|
+
if (cursorNearestTime < nearestTime) {
|
|
618
|
+
nearestCursorInstanceIndex = i;
|
|
619
|
+
nearestTime = cursorNearestTime;
|
|
708
620
|
}
|
|
709
621
|
}
|
|
710
|
-
return
|
|
622
|
+
return nearestCursorInstanceIndex;
|
|
711
623
|
}
|
|
712
624
|
/**
|
|
713
625
|
* Creates nerf factors by scanning through objects.
|
|
714
626
|
*/
|
|
715
627
|
calculateNerfFactors() {
|
|
716
628
|
for (const beatmapSection of this.beatmapSections) {
|
|
717
|
-
const
|
|
629
|
+
const threeFingerCursorCounts = osuBase.Utils.initializeArray(Math.max(0, this.downCursorInstances.length - 2), 0);
|
|
718
630
|
for (const object of beatmapSection.objects) {
|
|
719
|
-
if (object.
|
|
720
|
-
object.pressingCursorInstanceIndex ===
|
|
721
|
-
beatmapSection.dragFingerIndex) {
|
|
631
|
+
if (object.pressingCursorInstanceIndex === -1) {
|
|
722
632
|
continue;
|
|
723
633
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
634
|
+
if (object.aimingCursorInstanceIndex < 3) {
|
|
635
|
+
// The aim cursor is in the first three cursors. They are counted as non-3 finger.
|
|
636
|
+
switch (object.pressingCursorInstanceIndex) {
|
|
637
|
+
case 0:
|
|
638
|
+
case 1:
|
|
639
|
+
case 2:
|
|
640
|
+
break;
|
|
641
|
+
default:
|
|
642
|
+
++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 3];
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
// The aim cursor is somewhere else. only count the first 2 cursors as non-3 finger.
|
|
648
|
+
switch (object.pressingCursorInstanceIndex) {
|
|
649
|
+
case 0:
|
|
650
|
+
case 1:
|
|
651
|
+
break;
|
|
652
|
+
default:
|
|
653
|
+
++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 2];
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
729
657
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const fingerSplitIndex = beatmapSection.dragFingerIndex !== -1 ? 2 : 3;
|
|
734
|
-
// Divide >=4th (3rd for drag) cursor instances with 1st + 2nd (+ 3rd for nondrag)
|
|
735
|
-
// to check if the section is 3-fingered.
|
|
736
|
-
const threeFingerRatio = cursorCounts
|
|
737
|
-
.slice(fingerSplitIndex)
|
|
738
|
-
.reduce((acc, value) => acc + value, 0) /
|
|
739
|
-
Math.max(cursorCounts
|
|
740
|
-
.slice(0, fingerSplitIndex)
|
|
741
|
-
.reduce((acc, value) => acc + value, 0), 1);
|
|
742
|
-
if (threeFingerRatio > this.threeFingerRatioThreshold) {
|
|
743
|
-
const threeFingeredObjectCount = cursorCounts
|
|
744
|
-
.slice(fingerSplitIndex)
|
|
745
|
-
.reduce((acc, value) => acc + value, 0);
|
|
746
|
-
const sectionObjectCount = beatmapSection.lastObjectIndex -
|
|
747
|
-
beatmapSection.firstObjectIndex +
|
|
748
|
-
1;
|
|
749
|
-
const threeFingeredObjectRatio = threeFingeredObjectCount / sectionObjectCount;
|
|
750
|
-
// We can ignore the first 3 (2 for drag) filled cursor instances
|
|
751
|
-
// since they are guaranteed not 3 finger.
|
|
752
|
-
const threeFingerCursorCounts = cursorCounts
|
|
753
|
-
.slice(fingerSplitIndex)
|
|
754
|
-
.filter((amount) => amount > 0);
|
|
755
|
-
// Finger factor applies more penalty if more fingers were used.
|
|
756
|
-
const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
|
|
757
|
-
Math.pow((index + 1) * count * threeFingeredObjectRatio, 0.9), 1);
|
|
758
|
-
// Length factor applies more penalty if there are more 3-fingered object.
|
|
759
|
-
const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 1.2);
|
|
760
|
-
this.nerfFactors.push({
|
|
761
|
-
strainFactor: beatmapSection.sumStrain * threeFingeredObjectRatio,
|
|
762
|
-
fingerFactor: fingerFactor,
|
|
763
|
-
lengthFactor: lengthFactor,
|
|
764
|
-
});
|
|
658
|
+
const threeFingerCursorCount = threeFingerCursorCounts.reduce((a, v) => a + v, 0);
|
|
659
|
+
if (threeFingerCursorCount === 0) {
|
|
660
|
+
continue;
|
|
765
661
|
}
|
|
662
|
+
const sectionObjectCount = beatmapSection.objects.length;
|
|
663
|
+
const threeFingeredObjectRatio = threeFingerCursorCount / sectionObjectCount;
|
|
664
|
+
const strainFactor = Math.max(1, beatmapSection.sumStrain * threeFingeredObjectRatio);
|
|
665
|
+
// Finger factor applies more penalty if more fingers were used.
|
|
666
|
+
const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
|
|
667
|
+
Math.pow(((index + 1) * count) / sectionObjectCount, 0.9), 1);
|
|
668
|
+
// Length factor applies more penalty if there are more 3-fingered object.
|
|
669
|
+
const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 0.8);
|
|
670
|
+
this.nerfFactors.push({
|
|
671
|
+
strainFactor: strainFactor,
|
|
672
|
+
fingerFactor: fingerFactor,
|
|
673
|
+
lengthFactor: lengthFactor,
|
|
674
|
+
});
|
|
766
675
|
}
|
|
767
676
|
}
|
|
768
677
|
/**
|
|
@@ -773,6 +682,12 @@ class RebalanceThreeFingerChecker {
|
|
|
773
682
|
0.015 *
|
|
774
683
|
Math.pow(n.strainFactor * n.fingerFactor * n.lengthFactor, 1.05), 1);
|
|
775
684
|
}
|
|
685
|
+
getCursorPosition(cursor) {
|
|
686
|
+
if (this.isHardRock) {
|
|
687
|
+
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
688
|
+
}
|
|
689
|
+
return cursor.position;
|
|
690
|
+
}
|
|
776
691
|
}
|
|
777
692
|
|
|
778
693
|
/******************************************************************************
|
|
@@ -808,9 +723,9 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
808
723
|
};
|
|
809
724
|
|
|
810
725
|
/**
|
|
811
|
-
* Represents a replay data in an osu!droid replay.
|
|
726
|
+
* Represents a replay data in an osu!droid replay version 1 and 2.
|
|
812
727
|
*
|
|
813
|
-
* Stores generic information about an osu!droid replay
|
|
728
|
+
* Stores generic information about an osu!droid replay.
|
|
814
729
|
*
|
|
815
730
|
* This is used when analyzing replays using replay analyzer.
|
|
816
731
|
*/
|
|
@@ -820,45 +735,46 @@ class ReplayData {
|
|
|
820
735
|
this.folderName = values.folderName;
|
|
821
736
|
this.fileName = values.fileName;
|
|
822
737
|
this.hash = values.hash;
|
|
823
|
-
this.
|
|
824
|
-
this.
|
|
825
|
-
this.
|
|
826
|
-
this.
|
|
827
|
-
this.maxCombo = values.maxCombo || 0;
|
|
828
|
-
this.accuracy = values.accuracy || new osuBase.Accuracy({});
|
|
829
|
-
this.isFullCombo = values.isFullCombo || false;
|
|
830
|
-
this.playerName = values.playerName || "";
|
|
831
|
-
this.rawMods = values.rawMods || "";
|
|
832
|
-
this.rank = values.rank || "";
|
|
833
|
-
this.convertedMods = values.convertedMods || [];
|
|
738
|
+
this.accuracy = values.accuracy;
|
|
739
|
+
this.rank = values.rank;
|
|
740
|
+
this.hit300k = values.hit300k;
|
|
741
|
+
this.hit100k = values.hit100k;
|
|
834
742
|
this.cursorMovement = values.cursorMovement;
|
|
835
743
|
this.hitObjectData = values.hitObjectData;
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
this.
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Whether the replay's version is 3 or later.
|
|
747
|
+
*/
|
|
748
|
+
isReplayV3() {
|
|
749
|
+
return this.replayVersion >= 3;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Whether the replay's version is 4 or later.
|
|
753
|
+
*/
|
|
754
|
+
isReplayV4() {
|
|
755
|
+
return this.replayVersion >= 4;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Whether the replay's version is 5 or later.
|
|
759
|
+
*/
|
|
760
|
+
isReplayV5() {
|
|
761
|
+
return this.replayVersion >= 5;
|
|
842
762
|
}
|
|
843
763
|
}
|
|
844
764
|
|
|
845
765
|
/**
|
|
846
|
-
* Utility to check whether or not a beatmap is three-fingered.
|
|
766
|
+
* Utility to check whether or not a beatmap is three-fingered for rebalance scores.
|
|
847
767
|
*/
|
|
848
768
|
class ThreeFingerChecker {
|
|
769
|
+
get hitWindow50() {
|
|
770
|
+
return this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
771
|
+
}
|
|
849
772
|
/**
|
|
850
773
|
* @param beatmap The beatmap to analyze.
|
|
851
774
|
* @param data The data of the replay.
|
|
852
775
|
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
853
776
|
*/
|
|
854
777
|
constructor(beatmap, data, difficultyAttributes) {
|
|
855
|
-
/**
|
|
856
|
-
* The ratio threshold between non-3 finger cursors and 3-finger cursors.
|
|
857
|
-
*
|
|
858
|
-
* Increasing this number will increase detection accuracy, however
|
|
859
|
-
* it also increases the chance of falsely flagged plays.
|
|
860
|
-
*/
|
|
861
|
-
this.threeFingerRatioThreshold = 0.01;
|
|
862
778
|
/**
|
|
863
779
|
* Extended sections of the beatmap for drag detection.
|
|
864
780
|
*/
|
|
@@ -885,12 +801,12 @@ class ThreeFingerChecker {
|
|
|
885
801
|
this.difficultyAttributes = difficultyAttributes;
|
|
886
802
|
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
887
803
|
overallDifficulty: beatmap.difficulty.od,
|
|
888
|
-
mods: osuBase.ModUtil.removeSpeedChangingMods(
|
|
804
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(difficultyAttributes.mods),
|
|
889
805
|
convertOverallDifficulty: false,
|
|
890
806
|
}).overallDifficulty;
|
|
891
|
-
this.isPrecise =
|
|
807
|
+
this.isPrecise = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
808
|
+
this.isHardRock = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
892
809
|
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
893
|
-
this.hitObjects = beatmap.hitObjects.objects;
|
|
894
810
|
}
|
|
895
811
|
/**
|
|
896
812
|
* Checks whether a beatmap is eligible to be detected for 3-finger.
|
|
@@ -911,7 +827,8 @@ class ThreeFingerChecker {
|
|
|
911
827
|
* nerf factor, taking beatmap difficulty into account.
|
|
912
828
|
*/
|
|
913
829
|
check() {
|
|
914
|
-
if (this.difficultyAttributes
|
|
830
|
+
if (!ThreeFingerChecker.isEligibleToDetect(this.difficultyAttributes) ||
|
|
831
|
+
this.data.cursorMovement.filter((v) => v.occurrenceGroups.length > 0).length <= 3) {
|
|
915
832
|
return { is3Finger: false, penalty: 1 };
|
|
916
833
|
}
|
|
917
834
|
this.getAccurateBreakPoints();
|
|
@@ -931,10 +848,11 @@ class ThreeFingerChecker {
|
|
|
931
848
|
* end of the hitobject before it and do not end right at the first hitobject after it.
|
|
932
849
|
*/
|
|
933
850
|
getAccurateBreakPoints() {
|
|
851
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
934
852
|
const objectData = this.data.hitObjectData;
|
|
935
853
|
for (const breakPoint of this.beatmap.events.breaks) {
|
|
936
|
-
const beforeIndex = osuBase.MathUtils.clamp(
|
|
937
|
-
const objectBefore =
|
|
854
|
+
const beforeIndex = osuBase.MathUtils.clamp(objects.findIndex((o) => o.endTime >= breakPoint.startTime) - 1, 0, objects.length - 2);
|
|
855
|
+
const objectBefore = objects[beforeIndex];
|
|
938
856
|
const objectBeforeData = objectData[beforeIndex];
|
|
939
857
|
let timeBefore = objectBefore.endTime;
|
|
940
858
|
if (objectBefore instanceof osuBase.Circle) {
|
|
@@ -946,9 +864,9 @@ class ThreeFingerChecker {
|
|
|
946
864
|
}
|
|
947
865
|
}
|
|
948
866
|
const afterIndex = beforeIndex + 1;
|
|
949
|
-
const objectAfter =
|
|
867
|
+
const objectAfter = objects[afterIndex];
|
|
950
868
|
const objectAfterData = objectData[afterIndex];
|
|
951
|
-
let timeAfter =
|
|
869
|
+
let timeAfter = objectAfter.startTime;
|
|
952
870
|
if (objectAfter instanceof osuBase.Circle &&
|
|
953
871
|
objectAfterData.result !== exports.HitResult.miss) {
|
|
954
872
|
timeAfter += objectAfterData.accuracy;
|
|
@@ -965,11 +883,12 @@ class ThreeFingerChecker {
|
|
|
965
883
|
* This also filters cursors that are in break period or happen before start/after end of the beatmap.
|
|
966
884
|
*/
|
|
967
885
|
filterCursorInstances() {
|
|
886
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
968
887
|
const objectData = this.data.hitObjectData;
|
|
969
888
|
const firstObjectResult = objectData[0].result;
|
|
970
889
|
const lastObjectResult = objectData.at(-1).result;
|
|
971
|
-
const firstObject =
|
|
972
|
-
const lastObject =
|
|
890
|
+
const firstObject = objects[0];
|
|
891
|
+
const lastObject = objects.at(-1);
|
|
973
892
|
// For sliders, automatically set hit window length to be as lenient as possible.
|
|
974
893
|
let firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
975
894
|
if (firstObject instanceof osuBase.Circle) {
|
|
@@ -1029,280 +948,217 @@ class ThreeFingerChecker {
|
|
|
1029
948
|
* detect dragged sections and improve detection speed.
|
|
1030
949
|
*/
|
|
1031
950
|
getBeatmapSections() {
|
|
1032
|
-
const
|
|
951
|
+
const beatmapObjects = this.beatmap.hitObjects.objects;
|
|
952
|
+
const aimCursorGroupLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
953
|
+
// This intentionally starts from 1 because we need to look at the previous cursor.
|
|
954
|
+
const aimCursorLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 1);
|
|
955
|
+
const pressCursorLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
1033
956
|
for (const section of this.difficultyAttributes
|
|
1034
957
|
.possibleThreeFingeredSections) {
|
|
1035
|
-
const dragFingerIndex = this.findDragIndex(section);
|
|
1036
958
|
const objects = [];
|
|
1037
959
|
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
1038
|
-
|
|
960
|
+
const object = beatmapObjects[i];
|
|
961
|
+
const objectData = this.data.hitObjectData[i];
|
|
962
|
+
objects.push({
|
|
963
|
+
object: object,
|
|
964
|
+
aimingCursorInstanceIndex: this.getObjectAimIndex(object, objectData, aimCursorGroupLookupIndices, aimCursorLookupIndices),
|
|
965
|
+
pressingCursorInstanceIndex: this.getObjectPressIndex(object, objectData, pressCursorLookupIndices),
|
|
966
|
+
});
|
|
1039
967
|
}
|
|
1040
|
-
this.beatmapSections.push(Object.assign(Object.assign({}, section), {
|
|
968
|
+
this.beatmapSections.push(Object.assign(Object.assign({}, section), { objects: objects }));
|
|
1041
969
|
}
|
|
1042
970
|
}
|
|
1043
971
|
/**
|
|
1044
|
-
* Obtains the index of the
|
|
972
|
+
* Obtains the index of the cursor that aimed the object at the nearest time.
|
|
1045
973
|
*
|
|
1046
974
|
* @param object The object to obtain the index for.
|
|
1047
975
|
* @param objectData The hit data of the object.
|
|
1048
|
-
* @param
|
|
1049
|
-
* @param
|
|
976
|
+
* @param cursorInstanceIndices The cursor indices to start looking for the cursor instance from, to save computation time.
|
|
977
|
+
* @param cursorGroupIndices The cursor indices to start looking for the cursor group from, to save computation time.
|
|
978
|
+
* @param cursorIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
1050
979
|
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
1051
980
|
*/
|
|
1052
|
-
|
|
981
|
+
getObjectAimIndex(object, objectData, cursorGroupIndices, cursorIndices) {
|
|
1053
982
|
if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
|
|
1054
|
-
return
|
|
1055
|
-
pressingCursorInstanceIndex: -1,
|
|
1056
|
-
pressingCursorIndex: -1,
|
|
1057
|
-
};
|
|
983
|
+
return -1;
|
|
1058
984
|
}
|
|
1059
|
-
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1060
985
|
// Check for sliderbreaks and treat them as misses.
|
|
1061
986
|
if (object instanceof osuBase.Slider &&
|
|
1062
|
-
objectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
1063
|
-
return
|
|
1064
|
-
pressingCursorInstanceIndex: -1,
|
|
1065
|
-
pressingCursorIndex: -1,
|
|
1066
|
-
};
|
|
987
|
+
objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
|
|
988
|
+
return -1;
|
|
1067
989
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
990
|
+
const hitTime = object.startTime + objectData.accuracy;
|
|
991
|
+
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
992
|
+
// We are maintaining the closest distance to the object.
|
|
993
|
+
// This is because the radius that is calculated is using an estimation.
|
|
994
|
+
// As such, it does not reflect the actual object radius in gameplay.
|
|
995
|
+
let closestDistance = Number.POSITIVE_INFINITY;
|
|
996
|
+
let nearestCursorIndex = -1;
|
|
997
|
+
// Observe the cursor position at the object's hit time.
|
|
998
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
999
|
+
const cursorData = this.data.cursorMovement[i];
|
|
1000
|
+
for (let j = cursorGroupIndices[i]; j < cursorData.occurrenceGroups.length; cursorGroupIndices[i] = ++j) {
|
|
1001
|
+
const cursorGroup = cursorData.occurrenceGroups[j];
|
|
1002
|
+
if (cursorGroup.endTime < hitTime) {
|
|
1003
|
+
// Reset cursor index pointer.
|
|
1004
|
+
cursorIndices[i] = 1;
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (cursorGroup.startTime > hitTime) {
|
|
1080
1008
|
break;
|
|
1009
|
+
}
|
|
1010
|
+
const cursors = cursorGroup.allOccurrences;
|
|
1011
|
+
for (let k = cursorIndices[i]; k < cursors.length; cursorIndices[i] = ++k) {
|
|
1012
|
+
const cursor = cursors[k];
|
|
1013
|
+
const prevCursor = cursors[k - 1];
|
|
1014
|
+
// Cursor is past the object's hit time.
|
|
1015
|
+
if (prevCursor.time > hitTime) {
|
|
1016
|
+
break;
|
|
1017
|
+
}
|
|
1018
|
+
// Cursor is before the object's hit time.
|
|
1019
|
+
if (hitTime > cursor.time) {
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
let distance;
|
|
1023
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
1024
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
1025
|
+
switch (cursor.id) {
|
|
1026
|
+
case exports.MovementType.up:
|
|
1027
|
+
distance = prevPosition.getDistance(objectPosition);
|
|
1028
|
+
break;
|
|
1029
|
+
case exports.MovementType.move: {
|
|
1030
|
+
// Interpolate movement.
|
|
1031
|
+
const t = (hitTime - prevCursor.time) /
|
|
1032
|
+
(cursor.time - prevCursor.time);
|
|
1033
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
1034
|
+
distance =
|
|
1035
|
+
objectPosition.getDistance(cursorPosition);
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
case exports.MovementType.down:
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
if (closestDistance > distance) {
|
|
1042
|
+
closestDistance = distance;
|
|
1043
|
+
nearestCursorIndex = i;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
// Reset cursor index pointer on end of group.
|
|
1047
|
+
if (cursorIndices[i] === cursors.length) {
|
|
1048
|
+
cursorIndices[i] = 1;
|
|
1049
|
+
}
|
|
1050
|
+
break;
|
|
1081
1051
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1052
|
+
// The previous object may still be hit with the same cursor group or cursor index.
|
|
1053
|
+
cursorGroupIndices[i] = Math.max(0, cursorGroupIndices[i] - 1);
|
|
1054
|
+
cursorIndices[i] = Math.max(1, cursorIndices[i] - 1);
|
|
1055
|
+
}
|
|
1056
|
+
return nearestCursorIndex;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Obtains the index of the nearest cursor of which an object was pressed in terms of time.
|
|
1060
|
+
*
|
|
1061
|
+
* @param object The object to obtain the index for.
|
|
1062
|
+
* @param objectData The hit data of the object.
|
|
1063
|
+
* @param cursorLookupIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
1064
|
+
* @param excludedIndices The cursor indices that should not be checked.
|
|
1065
|
+
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
1066
|
+
*/
|
|
1067
|
+
getObjectPressIndex(object, objectData, cursorLookupIndices) {
|
|
1068
|
+
if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
|
|
1069
|
+
return -1;
|
|
1084
1070
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1071
|
+
// Check for sliderbreaks and treat them as misses.
|
|
1072
|
+
if (object instanceof osuBase.Slider &&
|
|
1073
|
+
objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
|
|
1074
|
+
return -1;
|
|
1088
1075
|
}
|
|
1089
1076
|
const hitTime = object.startTime + objectData.accuracy;
|
|
1090
|
-
let nearestCursorInstanceIndex =
|
|
1091
|
-
let nearestCursorIndex = null;
|
|
1077
|
+
let nearestCursorInstanceIndex = -1;
|
|
1092
1078
|
let nearestTime = Number.POSITIVE_INFINITY;
|
|
1093
1079
|
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
1094
|
-
if (excludedCursorIndices.includes(i)) {
|
|
1095
|
-
continue;
|
|
1096
|
-
}
|
|
1097
1080
|
const cursors = this.downCursorInstances[i];
|
|
1081
|
+
let cursorNearestTime = Number.POSITIVE_INFINITY;
|
|
1098
1082
|
for (let j = cursorLookupIndices[i]; j < cursors.length; cursorLookupIndices[i] = ++j) {
|
|
1099
1083
|
const cursor = cursors[j];
|
|
1100
|
-
if (cursor.time
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
1103
|
-
if (cursor.time > maxHitTime) {
|
|
1104
|
-
break;
|
|
1105
|
-
}
|
|
1106
|
-
const deltaTime = Math.abs(hitTime - cursor.time);
|
|
1107
|
-
if (deltaTime > nearestTime) {
|
|
1084
|
+
if (cursor.time > hitTime) {
|
|
1108
1085
|
break;
|
|
1109
1086
|
}
|
|
1087
|
+
cursorNearestTime = hitTime - cursor.time;
|
|
1088
|
+
}
|
|
1089
|
+
if (cursorNearestTime < nearestTime) {
|
|
1110
1090
|
nearestCursorInstanceIndex = i;
|
|
1111
|
-
|
|
1112
|
-
nearestTime = deltaTime;
|
|
1091
|
+
nearestTime = cursorNearestTime;
|
|
1113
1092
|
}
|
|
1114
1093
|
}
|
|
1115
|
-
return
|
|
1116
|
-
pressingCursorInstanceIndex: nearestCursorInstanceIndex !== null && nearestCursorInstanceIndex !== void 0 ? nearestCursorInstanceIndex : -1,
|
|
1117
|
-
pressingCursorIndex: nearestCursorIndex !== null && nearestCursorIndex !== void 0 ? nearestCursorIndex : -1,
|
|
1118
|
-
};
|
|
1094
|
+
return nearestCursorInstanceIndex;
|
|
1119
1095
|
}
|
|
1120
1096
|
/**
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
* If the section is not dragged, -1 will be returned.
|
|
1124
|
-
*
|
|
1125
|
-
* @param section The section to check.
|
|
1097
|
+
* Creates nerf factors by scanning through objects.
|
|
1126
1098
|
*/
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1099
|
+
calculateNerfFactors() {
|
|
1100
|
+
for (const beatmapSection of this.beatmapSections) {
|
|
1101
|
+
const threeFingerCursorCounts = osuBase.Utils.initializeArray(Math.max(0, this.downCursorInstances.length - 2), 0);
|
|
1102
|
+
for (const object of beatmapSection.objects) {
|
|
1103
|
+
if (object.pressingCursorInstanceIndex === -1) {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
if (object.aimingCursorInstanceIndex < 3) {
|
|
1107
|
+
// The aim cursor is in the first three cursors. They are counted as non-3 finger.
|
|
1108
|
+
switch (object.pressingCursorInstanceIndex) {
|
|
1109
|
+
case 0:
|
|
1110
|
+
case 1:
|
|
1111
|
+
case 2:
|
|
1112
|
+
break;
|
|
1113
|
+
default:
|
|
1114
|
+
++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 3];
|
|
1115
|
+
break;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
// The aim cursor is somewhere else. only count the first 2 cursors as non-3 finger.
|
|
1120
|
+
switch (object.pressingCursorInstanceIndex) {
|
|
1121
|
+
case 0:
|
|
1122
|
+
case 1:
|
|
1123
|
+
break;
|
|
1124
|
+
default:
|
|
1125
|
+
++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 2];
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1143
1129
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
let lastObjectMaxHitTime = lastObject.startTime;
|
|
1149
|
-
if (lastObject instanceof osuBase.Circle) {
|
|
1150
|
-
switch (objectData[section.lastObjectIndex].result) {
|
|
1151
|
-
case exports.HitResult.great:
|
|
1152
|
-
lastObjectMaxHitTime += this.hitWindow.hitWindowFor300(this.isPrecise);
|
|
1153
|
-
break;
|
|
1154
|
-
case exports.HitResult.good:
|
|
1155
|
-
lastObjectMaxHitTime += this.hitWindow.hitWindowFor100(this.isPrecise);
|
|
1156
|
-
break;
|
|
1157
|
-
default:
|
|
1158
|
-
lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1130
|
+
const threeFingerCursorCount = threeFingerCursorCounts.reduce((a, v) => a + v, 0);
|
|
1131
|
+
if (threeFingerCursorCount === 0) {
|
|
1132
|
+
continue;
|
|
1159
1133
|
}
|
|
1134
|
+
const sectionObjectCount = beatmapSection.objects.length;
|
|
1135
|
+
const threeFingeredObjectRatio = threeFingerCursorCount / sectionObjectCount;
|
|
1136
|
+
const strainFactor = Math.max(1, beatmapSection.sumStrain * threeFingeredObjectRatio);
|
|
1137
|
+
// Finger factor applies more penalty if more fingers were used.
|
|
1138
|
+
const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
|
|
1139
|
+
Math.pow(((index + 1) * count) / sectionObjectCount, 0.9), 1);
|
|
1140
|
+
// Length factor applies more penalty if there are more 3-fingered object.
|
|
1141
|
+
const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 0.8);
|
|
1142
|
+
this.nerfFactors.push({
|
|
1143
|
+
strainFactor: strainFactor,
|
|
1144
|
+
fingerFactor: fingerFactor,
|
|
1145
|
+
lengthFactor: lengthFactor,
|
|
1146
|
+
});
|
|
1160
1147
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
// this speeds up checking process.
|
|
1174
|
-
if (c.occurrenceGroups.filter((v) => v.startTime >= firstObjectMinHitTime &&
|
|
1175
|
-
v.endTime <= lastObjectMaxHitTime).length === 0) {
|
|
1176
|
-
continue;
|
|
1177
|
-
}
|
|
1178
|
-
// If this cursor instance doesn't move, it's not the cursor instance we want.
|
|
1179
|
-
if (c.occurrenceGroups.filter((v) => v.moves.length > 0).length ===
|
|
1180
|
-
0) {
|
|
1181
|
-
continue;
|
|
1182
|
-
}
|
|
1183
|
-
cursorIndices.push(i);
|
|
1184
|
-
}
|
|
1185
|
-
const sectionObjects = this.hitObjects.slice(section.firstObjectIndex, section.lastObjectIndex + 1);
|
|
1186
|
-
const sectionReplayObjectData = objectData.slice(section.firstObjectIndex, section.lastObjectIndex + 1);
|
|
1187
|
-
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1188
|
-
for (let i = 0; i < sectionObjects.length && cursorIndices.some((v) => v !== -1); ++i) {
|
|
1189
|
-
const object = sectionObjects[i];
|
|
1190
|
-
const objectData = sectionReplayObjectData[i];
|
|
1191
|
-
if (object instanceof osuBase.Spinner ||
|
|
1192
|
-
objectData.result === exports.HitResult.miss) {
|
|
1193
|
-
continue;
|
|
1194
|
-
}
|
|
1195
|
-
// Exclude sliderbreaks.
|
|
1196
|
-
if (object instanceof osuBase.Slider &&
|
|
1197
|
-
objectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
1198
|
-
continue;
|
|
1199
|
-
}
|
|
1200
|
-
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1201
|
-
const hitTime = object.startTime + objectData.accuracy;
|
|
1202
|
-
// Observe the cursor position at the object's hit time.
|
|
1203
|
-
for (let j = 0; j < cursorIndices.length; ++j) {
|
|
1204
|
-
if (cursorIndices[j] === -1) {
|
|
1205
|
-
continue;
|
|
1206
|
-
}
|
|
1207
|
-
const cursorData = this.data.cursorMovement[cursorIndices[j]];
|
|
1208
|
-
const cursorGroup = cursorData.occurrenceGroups.find((v) => v.isActiveAt(hitTime));
|
|
1209
|
-
if (!cursorGroup) {
|
|
1210
|
-
continue;
|
|
1211
|
-
}
|
|
1212
|
-
const cursors = cursorGroup.allOccurrences;
|
|
1213
|
-
let isInObject = false;
|
|
1214
|
-
for (let k = 1; k < cursors.length; ++k) {
|
|
1215
|
-
const cursor = cursors[k];
|
|
1216
|
-
const prevCursor = cursors[k - 1];
|
|
1217
|
-
// Only consider cursor at interval prev.time <= hitTime <= current.time.
|
|
1218
|
-
if (prevCursor.time <= hitTime && cursor.time >= hitTime) {
|
|
1219
|
-
switch (cursor.id) {
|
|
1220
|
-
case exports.MovementType.up:
|
|
1221
|
-
isInObject =
|
|
1222
|
-
prevCursor.position.getDistance(objectPosition) <= object.radius;
|
|
1223
|
-
break;
|
|
1224
|
-
case exports.MovementType.move: {
|
|
1225
|
-
// Interpolate movement.
|
|
1226
|
-
const t = (hitTime - prevCursor.time) /
|
|
1227
|
-
(cursor.time - prevCursor.time);
|
|
1228
|
-
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));
|
|
1229
|
-
isInObject =
|
|
1230
|
-
objectPosition.getDistance(cursorPosition) <= object.radius;
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
break;
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
if (!isInObject) {
|
|
1237
|
-
cursorIndices[j] = -1;
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
return (_a = cursorIndices.find((v) => v !== -1)) !== null && _a !== void 0 ? _a : -1;
|
|
1242
|
-
}
|
|
1243
|
-
/**
|
|
1244
|
-
* Creates nerf factors by scanning through objects.
|
|
1245
|
-
*/
|
|
1246
|
-
calculateNerfFactors() {
|
|
1247
|
-
for (const beatmapSection of this.beatmapSections) {
|
|
1248
|
-
const cursorCounts = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
1249
|
-
for (const object of beatmapSection.objects) {
|
|
1250
|
-
if (object.pressingCursorIndex === -1 ||
|
|
1251
|
-
object.pressingCursorInstanceIndex ===
|
|
1252
|
-
beatmapSection.dragFingerIndex) {
|
|
1253
|
-
continue;
|
|
1254
|
-
}
|
|
1255
|
-
++cursorCounts[object.pressingCursorInstanceIndex];
|
|
1256
|
-
}
|
|
1257
|
-
if (beatmapSection.dragFingerIndex !== -1) {
|
|
1258
|
-
// Remove the drag index to prevent it from being picked up into the detection.
|
|
1259
|
-
cursorCounts.splice(beatmapSection.dragFingerIndex, 1);
|
|
1260
|
-
}
|
|
1261
|
-
// This index will be used to detect if a section is 3-fingered.
|
|
1262
|
-
// If the section is dragged, the dragged instance will be ignored,
|
|
1263
|
-
// hence why the index is 1 less than nondragged section.
|
|
1264
|
-
const fingerSplitIndex = beatmapSection.dragFingerIndex !== -1 ? 2 : 3;
|
|
1265
|
-
// Divide >=4th (3rd for drag) cursor instances with 1st + 2nd (+ 3rd for nondrag)
|
|
1266
|
-
// to check if the section is 3-fingered.
|
|
1267
|
-
const threeFingerRatio = cursorCounts
|
|
1268
|
-
.slice(fingerSplitIndex)
|
|
1269
|
-
.reduce((acc, value) => acc + value, 0) /
|
|
1270
|
-
Math.max(cursorCounts
|
|
1271
|
-
.slice(0, fingerSplitIndex)
|
|
1272
|
-
.reduce((acc, value) => acc + value, 0), 1);
|
|
1273
|
-
if (threeFingerRatio > this.threeFingerRatioThreshold) {
|
|
1274
|
-
const threeFingeredObjectCount = cursorCounts
|
|
1275
|
-
.slice(fingerSplitIndex)
|
|
1276
|
-
.reduce((acc, value) => acc + value, 0);
|
|
1277
|
-
const sectionObjectCount = beatmapSection.lastObjectIndex -
|
|
1278
|
-
beatmapSection.firstObjectIndex +
|
|
1279
|
-
1;
|
|
1280
|
-
const threeFingeredObjectRatio = threeFingeredObjectCount / sectionObjectCount;
|
|
1281
|
-
// We can ignore the first 3 (2 for drag) filled cursor instances
|
|
1282
|
-
// since they are guaranteed not 3 finger.
|
|
1283
|
-
const threeFingerCursorCounts = cursorCounts
|
|
1284
|
-
.slice(fingerSplitIndex)
|
|
1285
|
-
.filter((amount) => amount > 0);
|
|
1286
|
-
// Finger factor applies more penalty if more fingers were used.
|
|
1287
|
-
const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
|
|
1288
|
-
Math.pow((index + 1) * count * threeFingeredObjectRatio, 0.9), 1);
|
|
1289
|
-
// Length factor applies more penalty if there are more 3-fingered object.
|
|
1290
|
-
const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 1.2);
|
|
1291
|
-
this.nerfFactors.push({
|
|
1292
|
-
strainFactor: beatmapSection.sumStrain * threeFingeredObjectRatio,
|
|
1293
|
-
fingerFactor: fingerFactor,
|
|
1294
|
-
lengthFactor: lengthFactor,
|
|
1295
|
-
});
|
|
1296
|
-
}
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Calculates the final penalty.
|
|
1151
|
+
*/
|
|
1152
|
+
calculateFinalPenalty() {
|
|
1153
|
+
return this.nerfFactors.reduce((a, n) => a +
|
|
1154
|
+
0.015 *
|
|
1155
|
+
Math.pow(n.strainFactor * n.fingerFactor * n.lengthFactor, 1.05), 1);
|
|
1156
|
+
}
|
|
1157
|
+
getCursorPosition(cursor) {
|
|
1158
|
+
if (this.isHardRock) {
|
|
1159
|
+
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
1297
1160
|
}
|
|
1298
|
-
|
|
1299
|
-
/**
|
|
1300
|
-
* Calculates the final penalty.
|
|
1301
|
-
*/
|
|
1302
|
-
calculateFinalPenalty() {
|
|
1303
|
-
return this.nerfFactors.reduce((a, n) => a +
|
|
1304
|
-
0.015 *
|
|
1305
|
-
Math.pow(n.strainFactor * n.fingerFactor * n.lengthFactor, 1.05), 1);
|
|
1161
|
+
return cursor.position;
|
|
1306
1162
|
}
|
|
1307
1163
|
}
|
|
1308
1164
|
|
|
@@ -1351,11 +1207,12 @@ class TwoHandChecker {
|
|
|
1351
1207
|
this.data = data;
|
|
1352
1208
|
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
1353
1209
|
overallDifficulty: calculator.beatmap.difficulty.od,
|
|
1354
|
-
mods: osuBase.ModUtil.removeSpeedChangingMods(
|
|
1210
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(calculator.mods),
|
|
1355
1211
|
convertOverallDifficulty: false,
|
|
1356
1212
|
}).overallDifficulty;
|
|
1357
1213
|
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
1358
1214
|
this.hitWindow50 = this.hitWindow.hitWindowFor50(calculator.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
1215
|
+
this.isHardRock = calculator.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
1359
1216
|
// this.csvString = `Mods,${
|
|
1360
1217
|
// data.convertedMods.reduce((a, m) => a + m.acronym, "") || "NM"
|
|
1361
1218
|
// }\nCombo,${data.maxCombo}\nAccuracy,"${(
|
|
@@ -1576,13 +1433,15 @@ class TwoHandChecker {
|
|
|
1576
1433
|
for (let i = prevObjectInformation.occurrenceIndex + 1; i < cursors.length; ++i) {
|
|
1577
1434
|
const cursor = cursors[i];
|
|
1578
1435
|
const prevCursor = cursors[i - 1];
|
|
1579
|
-
|
|
1436
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
1437
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
1438
|
+
if (currentPosition.equals(prevPosition)) {
|
|
1580
1439
|
continue;
|
|
1581
1440
|
}
|
|
1582
1441
|
if (cursor.id === exports.MovementType.up) {
|
|
1583
1442
|
break;
|
|
1584
1443
|
}
|
|
1585
|
-
const currentMovement =
|
|
1444
|
+
const currentMovement = currentPosition.subtract(prevPosition);
|
|
1586
1445
|
const dot = prevToCurrentCursorMovement.dot(currentMovement);
|
|
1587
1446
|
const det = prevToCurrentCursorMovement.x * currentMovement.y -
|
|
1588
1447
|
prevToCurrentCursorMovement.y * currentMovement.x;
|
|
@@ -1668,7 +1527,7 @@ class TwoHandChecker {
|
|
|
1668
1527
|
};
|
|
1669
1528
|
}
|
|
1670
1529
|
let hitWindow = this.hitWindow50;
|
|
1671
|
-
const isPrecise = this.
|
|
1530
|
+
const isPrecise = this.calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
1672
1531
|
// For sliders, set the hit window to as lenient as possible.
|
|
1673
1532
|
if (object instanceof osuBase.Circle) {
|
|
1674
1533
|
switch (data.result) {
|
|
@@ -1706,14 +1565,14 @@ class TwoHandChecker {
|
|
|
1706
1565
|
}
|
|
1707
1566
|
// Validate the down press first.
|
|
1708
1567
|
const { down } = cursorGroup;
|
|
1709
|
-
|
|
1710
|
-
|
|
1568
|
+
const downPosition = this.getCursorPosition(down);
|
|
1569
|
+
if (downPosition.getDistance(objectPosition) <= object.radius &&
|
|
1711
1570
|
Math.abs(down.time - object.startTime) <= hitWindow) {
|
|
1712
1571
|
if (objectIndex > 0) {
|
|
1713
1572
|
const prevObject = this.calculator.beatmap.hitObjects.objects[objectIndex - 1];
|
|
1714
1573
|
if (down.time > prevObject.endTime) {
|
|
1715
1574
|
return {
|
|
1716
|
-
position:
|
|
1575
|
+
position: downPosition,
|
|
1717
1576
|
cursorIndex: i,
|
|
1718
1577
|
groupIndex: j,
|
|
1719
1578
|
occurrenceIndex: 0,
|
|
@@ -1723,7 +1582,7 @@ class TwoHandChecker {
|
|
|
1723
1582
|
}
|
|
1724
1583
|
else {
|
|
1725
1584
|
return {
|
|
1726
|
-
position:
|
|
1585
|
+
position: downPosition,
|
|
1727
1586
|
cursorIndex: i,
|
|
1728
1587
|
groupIndex: j,
|
|
1729
1588
|
occurrenceIndex: 0,
|
|
@@ -1743,7 +1602,7 @@ class TwoHandChecker {
|
|
|
1743
1602
|
}
|
|
1744
1603
|
let cursorPosition;
|
|
1745
1604
|
if (cursor.id === exports.MovementType.up) {
|
|
1746
|
-
cursorPosition = prevCursor
|
|
1605
|
+
cursorPosition = this.getCursorPosition(prevCursor);
|
|
1747
1606
|
const distance = cursorPosition.getDistance(objectPosition);
|
|
1748
1607
|
if (distance <
|
|
1749
1608
|
nearestPosition.getDistance(objectPosition)) {
|
|
@@ -1768,7 +1627,7 @@ class TwoHandChecker {
|
|
|
1768
1627
|
}
|
|
1769
1628
|
const t = (cursorGroup.down.time - prevCursor.time) /
|
|
1770
1629
|
(cursor.time - prevCursor.time);
|
|
1771
|
-
cursorPosition = osuBase.Interpolation.lerp(prevCursor
|
|
1630
|
+
cursorPosition = osuBase.Interpolation.lerp(this.getCursorPosition(prevCursor), this.getCursorPosition(cursor), t);
|
|
1772
1631
|
const distance = cursorPosition.getDistance(objectPosition);
|
|
1773
1632
|
if (distance <
|
|
1774
1633
|
nearestPosition.getDistance(objectPosition)) {
|
|
@@ -1853,18 +1712,18 @@ class TwoHandChecker {
|
|
|
1853
1712
|
let cursorPosition;
|
|
1854
1713
|
switch (cursor.id) {
|
|
1855
1714
|
case exports.MovementType.down:
|
|
1856
|
-
cursorPosition = cursor
|
|
1715
|
+
cursorPosition = this.getCursorPosition(cursor);
|
|
1857
1716
|
break;
|
|
1858
1717
|
case exports.MovementType.up: {
|
|
1859
1718
|
const prevCursor = cursors[k - 1];
|
|
1860
|
-
cursorPosition = prevCursor
|
|
1719
|
+
cursorPosition = this.getCursorPosition(prevCursor);
|
|
1861
1720
|
break;
|
|
1862
1721
|
}
|
|
1863
1722
|
case exports.MovementType.move: {
|
|
1864
1723
|
const prevCursor = cursors[k - 1];
|
|
1865
1724
|
const t = osuBase.MathUtils.clamp((object.endTime - prevCursor.time) /
|
|
1866
1725
|
(cursor.time - prevCursor.time), 0, 1);
|
|
1867
|
-
cursorPosition = osuBase.Interpolation.lerp(prevCursor
|
|
1726
|
+
cursorPosition = osuBase.Interpolation.lerp(this.getCursorPosition(prevCursor), this.getCursorPosition(cursor), t);
|
|
1868
1727
|
break;
|
|
1869
1728
|
}
|
|
1870
1729
|
}
|
|
@@ -1926,6 +1785,12 @@ class TwoHandChecker {
|
|
|
1926
1785
|
}
|
|
1927
1786
|
return false;
|
|
1928
1787
|
}
|
|
1788
|
+
getCursorPosition(cursor) {
|
|
1789
|
+
if (this.isHardRock) {
|
|
1790
|
+
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
1791
|
+
}
|
|
1792
|
+
return cursor.position;
|
|
1793
|
+
}
|
|
1929
1794
|
}
|
|
1930
1795
|
|
|
1931
1796
|
/**
|
|
@@ -1943,10 +1808,11 @@ class SliderCheeseChecker {
|
|
|
1943
1808
|
this.difficultyAttributes = difficultyAttributes;
|
|
1944
1809
|
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
1945
1810
|
overallDifficulty: beatmap.difficulty.od,
|
|
1946
|
-
mods: osuBase.ModUtil.removeSpeedChangingMods(
|
|
1811
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(difficultyAttributes.mods),
|
|
1947
1812
|
convertOverallDifficulty: false,
|
|
1948
1813
|
}).overallDifficulty;
|
|
1949
|
-
this.hitWindow50 = new osuBase.DroidHitWindow(od).hitWindowFor50(
|
|
1814
|
+
this.hitWindow50 = new osuBase.DroidHitWindow(od).hitWindowFor50(difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
1815
|
+
this.isHardRock = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
1950
1816
|
}
|
|
1951
1817
|
/**
|
|
1952
1818
|
* Checks if relevant sliders in the given beatmap was cheesed.
|
|
@@ -1972,7 +1838,7 @@ class SliderCheeseChecker {
|
|
|
1972
1838
|
const { objects } = this.beatmap.hitObjects;
|
|
1973
1839
|
const cheesedDifficultyRatings = [];
|
|
1974
1840
|
// Current loop indices are stored for efficiency.
|
|
1975
|
-
const cursorLoopIndices = osuBase.Utils.initializeArray(
|
|
1841
|
+
const cursorLoopIndices = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
|
|
1976
1842
|
const acceptableRadius = objects[0].radius * 2;
|
|
1977
1843
|
// Sort difficult sliders by index so that cursor loop indices work properly.
|
|
1978
1844
|
for (const difficultSlider of this.difficultyAttributes.difficultSliders
|
|
@@ -2010,7 +1876,8 @@ class SliderCheeseChecker {
|
|
|
2010
1876
|
break;
|
|
2011
1877
|
}
|
|
2012
1878
|
if (group.startTime >= minTimeLimit) {
|
|
2013
|
-
const
|
|
1879
|
+
const position = this.getCursorPosition(group.down);
|
|
1880
|
+
const distance = position.getDistance(objectStartPosition);
|
|
2014
1881
|
if (closestDistance > distance) {
|
|
2015
1882
|
closestDistance = distance;
|
|
2016
1883
|
closestIndex = j;
|
|
@@ -2024,20 +1891,22 @@ class SliderCheeseChecker {
|
|
|
2024
1891
|
// for the time being we only check for cursor distances across the group.
|
|
2025
1892
|
const { allOccurrences } = group;
|
|
2026
1893
|
for (let k = 1; k < allOccurrences.length; ++k) {
|
|
2027
|
-
const
|
|
2028
|
-
const
|
|
1894
|
+
const cursor = allOccurrences[k];
|
|
1895
|
+
const prevCursor = allOccurrences[k - 1];
|
|
2029
1896
|
let distance = Number.POSITIVE_INFINITY;
|
|
2030
|
-
|
|
1897
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
1898
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
1899
|
+
switch (cursor.id) {
|
|
2031
1900
|
case exports.MovementType.up:
|
|
2032
1901
|
distance =
|
|
2033
|
-
|
|
1902
|
+
prevPosition.getDistance(objectStartPosition);
|
|
2034
1903
|
break;
|
|
2035
1904
|
case exports.MovementType.move:
|
|
2036
|
-
for (let mSecPassed = Math.max(
|
|
2037
|
-
Math.min(
|
|
2038
|
-
const t = (mSecPassed -
|
|
2039
|
-
(
|
|
2040
|
-
const cursorPosition =
|
|
1905
|
+
for (let mSecPassed = Math.max(prevCursor.time, minTimeLimit); mSecPassed <=
|
|
1906
|
+
Math.min(cursor.time, maxTimeLimit); ++mSecPassed) {
|
|
1907
|
+
const t = (mSecPassed - prevCursor.time) /
|
|
1908
|
+
(cursor.time - prevCursor.time);
|
|
1909
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
2041
1910
|
distance =
|
|
2042
1911
|
cursorPosition.getDistance(objectStartPosition);
|
|
2043
1912
|
if (closestDistance > distance) {
|
|
@@ -2095,21 +1964,24 @@ class SliderCheeseChecker {
|
|
|
2095
1964
|
if (occurrenceLoopIndex === allOccurrences.length) {
|
|
2096
1965
|
continue;
|
|
2097
1966
|
}
|
|
2098
|
-
const
|
|
2099
|
-
const
|
|
2100
|
-
|
|
1967
|
+
const cursor = allOccurrences[occurrenceLoopIndex];
|
|
1968
|
+
const prevCursor = allOccurrences[occurrenceLoopIndex - 1];
|
|
1969
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
1970
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
1971
|
+
switch (cursor.id) {
|
|
2101
1972
|
case exports.MovementType.move: {
|
|
2102
1973
|
// Interpolate cursor position during nested object time.
|
|
2103
|
-
const t = (nestedObject.startTime -
|
|
2104
|
-
(
|
|
2105
|
-
const cursorPosition =
|
|
1974
|
+
const t = (nestedObject.startTime - prevCursor.time) /
|
|
1975
|
+
(cursor.time - prevCursor.time);
|
|
1976
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
2106
1977
|
const distance = cursorPosition.getDistance(nestedPosition);
|
|
2107
1978
|
isCheesed = distance > acceptableRadius;
|
|
2108
1979
|
break;
|
|
2109
1980
|
}
|
|
2110
1981
|
case exports.MovementType.up:
|
|
2111
1982
|
isCheesed =
|
|
2112
|
-
|
|
1983
|
+
prevPosition.getDistance(nestedPosition) >
|
|
1984
|
+
acceptableRadius;
|
|
2113
1985
|
}
|
|
2114
1986
|
}
|
|
2115
1987
|
if (isCheesed) {
|
|
@@ -2135,6 +2007,65 @@ class SliderCheeseChecker {
|
|
|
2135
2007
|
this.difficultyAttributes.visualSliderFactor, 2)),
|
|
2136
2008
|
};
|
|
2137
2009
|
}
|
|
2010
|
+
getCursorPosition(cursor) {
|
|
2011
|
+
if (this.isHardRock) {
|
|
2012
|
+
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
2013
|
+
}
|
|
2014
|
+
return cursor.position;
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
/**
|
|
2019
|
+
* Represents a replay data for replay version 3.
|
|
2020
|
+
*
|
|
2021
|
+
* Stores generic information about an osu!droid replay.
|
|
2022
|
+
*
|
|
2023
|
+
* This is used when analyzing replays using replay analyzer.
|
|
2024
|
+
*/
|
|
2025
|
+
class ReplayV3Data extends ReplayData {
|
|
2026
|
+
constructor(values) {
|
|
2027
|
+
super(values);
|
|
2028
|
+
this.time = values.time;
|
|
2029
|
+
this.score = values.score;
|
|
2030
|
+
this.maxCombo = values.maxCombo;
|
|
2031
|
+
this.isFullCombo = values.isFullCombo;
|
|
2032
|
+
this.playerName = values.playerName;
|
|
2033
|
+
this.rawMods = values.rawMods;
|
|
2034
|
+
this.convertedMods = values.convertedMods;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/**
|
|
2039
|
+
* Represents a replay data for replay version 4.
|
|
2040
|
+
*
|
|
2041
|
+
* Stores generic information about an osu!droid replay.
|
|
2042
|
+
*
|
|
2043
|
+
* This is used when analyzing replays using replay analyzer.
|
|
2044
|
+
*/
|
|
2045
|
+
class ReplayV4Data extends ReplayV3Data {
|
|
2046
|
+
constructor(values) {
|
|
2047
|
+
var _a, _b;
|
|
2048
|
+
super(values);
|
|
2049
|
+
this.forceAR = values.forceAR;
|
|
2050
|
+
this.speedMultiplier = (_a = values.speedMultiplier) !== null && _a !== void 0 ? _a : 1;
|
|
2051
|
+
this.flashlightFollowDelay = (_b = values.flashlightFollowDelay) !== null && _b !== void 0 ? _b : 0.12;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* Represents a replay data for replay version 5.
|
|
2057
|
+
*
|
|
2058
|
+
* Stores generic information about an osu!droid replay.
|
|
2059
|
+
*
|
|
2060
|
+
* This is used when analyzing replays using replay analyzer.
|
|
2061
|
+
*/
|
|
2062
|
+
class ReplayV5Data extends ReplayV4Data {
|
|
2063
|
+
constructor(values) {
|
|
2064
|
+
super(values);
|
|
2065
|
+
this.forceCS = values.forceCS;
|
|
2066
|
+
this.forceOD = values.forceOD;
|
|
2067
|
+
this.forceHP = values.forceHP;
|
|
2068
|
+
}
|
|
2138
2069
|
}
|
|
2139
2070
|
|
|
2140
2071
|
/**
|
|
@@ -2190,6 +2121,7 @@ class ReplayAnalyzer {
|
|
|
2190
2121
|
* The amount of two-handed objects.
|
|
2191
2122
|
*/
|
|
2192
2123
|
this.twoHandedNoteCount = 0;
|
|
2124
|
+
this.bufferOffset = 0;
|
|
2193
2125
|
this.scoreID = values.scoreID;
|
|
2194
2126
|
this.beatmap = values.map;
|
|
2195
2127
|
this.difficultyAttributes = values.difficultyAttributes;
|
|
@@ -2218,6 +2150,119 @@ class ReplayAnalyzer {
|
|
|
2218
2150
|
return this;
|
|
2219
2151
|
});
|
|
2220
2152
|
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Gets hit error information of the replay.
|
|
2155
|
+
*
|
|
2156
|
+
* `analyze()` must be called before calling this.
|
|
2157
|
+
*/
|
|
2158
|
+
calculateHitError() {
|
|
2159
|
+
var _a, _b;
|
|
2160
|
+
if (!this.data || !this.beatmap) {
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
const hitObjectData = this.data.hitObjectData;
|
|
2164
|
+
let positiveCount = 0;
|
|
2165
|
+
let negativeCount = 0;
|
|
2166
|
+
let positiveTotal = 0;
|
|
2167
|
+
let negativeTotal = 0;
|
|
2168
|
+
const beatmap = this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2169
|
+
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
|
|
2170
|
+
? this.beatmap.beatmap
|
|
2171
|
+
: this.beatmap;
|
|
2172
|
+
const { objects } = beatmap.hitObjects;
|
|
2173
|
+
const mods = this.data.isReplayV3()
|
|
2174
|
+
? this.data.convertedMods
|
|
2175
|
+
: (_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods.filter((m) => m.isApplicableToDroid())) !== null && _b !== void 0 ? _b : [];
|
|
2176
|
+
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
2177
|
+
overallDifficulty: beatmap.difficulty.od,
|
|
2178
|
+
mods: osuBase.ModUtil.removeSpeedChangingMods(mods),
|
|
2179
|
+
}).overallDifficulty;
|
|
2180
|
+
const hitWindow50 = new osuBase.DroidHitWindow(od).hitWindowFor50(mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
2181
|
+
// The accuracy of sliders is set to (50 hit window)ms + 13ms if their head was not hit:
|
|
2182
|
+
// https://github.com/osudroid/osu-droid/blob/6306c68e3ffaf671eac794bf45cc95c0f3313a82/src/ru/nsu/ccfit/zuev/osu/game/Slider.java#L821
|
|
2183
|
+
//
|
|
2184
|
+
// In such cases, the slider is skipped.
|
|
2185
|
+
const sliderbreakHitOffset = Math.floor(hitWindow50) + 13;
|
|
2186
|
+
const accuracies = [];
|
|
2187
|
+
for (let i = 0; i < hitObjectData.length; ++i) {
|
|
2188
|
+
const v = hitObjectData[i];
|
|
2189
|
+
const o = objects[i];
|
|
2190
|
+
if (o instanceof osuBase.Spinner || v.result === exports.HitResult.miss) {
|
|
2191
|
+
accuracies.push(0);
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
const { accuracy } = v;
|
|
2195
|
+
if (o instanceof osuBase.Slider && accuracy === sliderbreakHitOffset) {
|
|
2196
|
+
accuracies.push(0);
|
|
2197
|
+
continue;
|
|
2198
|
+
}
|
|
2199
|
+
accuracies.push(accuracy);
|
|
2200
|
+
if (accuracy >= 0) {
|
|
2201
|
+
positiveTotal += accuracy;
|
|
2202
|
+
++positiveCount;
|
|
2203
|
+
}
|
|
2204
|
+
else {
|
|
2205
|
+
negativeTotal += accuracy;
|
|
2206
|
+
++negativeCount;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return {
|
|
2210
|
+
positiveAvg: positiveTotal / positiveCount || 0,
|
|
2211
|
+
negativeAvg: negativeTotal / negativeCount || 0,
|
|
2212
|
+
unstableRate: osuBase.MathUtils.calculateStandardDeviation(accuracies) * 10,
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Checks if a play is using 3 fingers.
|
|
2217
|
+
*
|
|
2218
|
+
* Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
|
|
2219
|
+
*/
|
|
2220
|
+
checkFor3Finger() {
|
|
2221
|
+
var _a;
|
|
2222
|
+
if (!this.beatmap || !this.data || !this.difficultyAttributes) {
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
(_a = this.playableBeatmap) !== null && _a !== void 0 ? _a : (this.playableBeatmap = this.constructPlayableBeatmap());
|
|
2226
|
+
const threeFingerChecker = this.difficultyAttributes.mode === "rebalance"
|
|
2227
|
+
? new RebalanceThreeFingerChecker(this.playableBeatmap, this.data, this.difficultyAttributes)
|
|
2228
|
+
: new ThreeFingerChecker(this.playableBeatmap, this.data, this.difficultyAttributes);
|
|
2229
|
+
const result = threeFingerChecker.check();
|
|
2230
|
+
this.is3Finger = result.is3Finger;
|
|
2231
|
+
this.tapPenalty = result.penalty;
|
|
2232
|
+
this.hasBeenCheckedFor3Finger = true;
|
|
2233
|
+
}
|
|
2234
|
+
/**
|
|
2235
|
+
* Checks if a play is using 2 hands.
|
|
2236
|
+
*
|
|
2237
|
+
* Requires `analyze()` to be called first and `map` to be defined as `DroidDifficultyCalculator`.
|
|
2238
|
+
*/
|
|
2239
|
+
checkFor2Hand() {
|
|
2240
|
+
if (!(this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2241
|
+
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator) ||
|
|
2242
|
+
!this.data) {
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
const twoHandChecker = new TwoHandChecker(this.beatmap, this.data);
|
|
2246
|
+
const result = twoHandChecker.check();
|
|
2247
|
+
this.is2Hand = result.is2Hand;
|
|
2248
|
+
this.twoHandedNoteCount = result.twoHandedNoteCount;
|
|
2249
|
+
this.hasBeenCheckedFor2Hand = true;
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Checks if a play has cheesed sliders.
|
|
2253
|
+
*
|
|
2254
|
+
* Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
|
|
2255
|
+
*/
|
|
2256
|
+
checkForSliderCheesing() {
|
|
2257
|
+
var _a;
|
|
2258
|
+
if (!this.beatmap || !this.data || !this.difficultyAttributes) {
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
(_a = this.playableBeatmap) !== null && _a !== void 0 ? _a : (this.playableBeatmap = this.constructPlayableBeatmap());
|
|
2262
|
+
const sliderCheeseChecker = new SliderCheeseChecker(this.playableBeatmap, this.data, this.difficultyAttributes);
|
|
2263
|
+
this.sliderCheesePenalty = sliderCheeseChecker.check();
|
|
2264
|
+
this.hasBeenCheckedForSliderCheesing = true;
|
|
2265
|
+
}
|
|
2221
2266
|
/**
|
|
2222
2267
|
* Downloads the given score ID's replay.
|
|
2223
2268
|
*/
|
|
@@ -2264,7 +2309,6 @@ class ReplayAnalyzer {
|
|
|
2264
2309
|
* Parses a replay after being downloaded and converted to a buffer.
|
|
2265
2310
|
*/
|
|
2266
2311
|
parseReplay() {
|
|
2267
|
-
var _a, _b, _c;
|
|
2268
2312
|
if (!this.fixedODR) {
|
|
2269
2313
|
return;
|
|
2270
2314
|
}
|
|
@@ -2275,7 +2319,7 @@ class ReplayAnalyzer {
|
|
|
2275
2319
|
try {
|
|
2276
2320
|
rawObject = javaDeserialization__namespace.parse(this.fixedODR);
|
|
2277
2321
|
}
|
|
2278
|
-
catch (
|
|
2322
|
+
catch (_a) {
|
|
2279
2323
|
return;
|
|
2280
2324
|
}
|
|
2281
2325
|
const resultObject = {
|
|
@@ -2286,49 +2330,16 @@ class ReplayAnalyzer {
|
|
|
2286
2330
|
cursorMovement: [],
|
|
2287
2331
|
hitObjectData: [],
|
|
2288
2332
|
accuracy: new osuBase.Accuracy({ n300: 0 }),
|
|
2289
|
-
rank: "",
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
switch (true) {
|
|
2300
|
-
case resultObject.accuracy.value() === 1:
|
|
2301
|
-
if (isHidden) {
|
|
2302
|
-
resultObject.rank = "XH";
|
|
2303
|
-
}
|
|
2304
|
-
else {
|
|
2305
|
-
resultObject.rank = "X";
|
|
2306
|
-
}
|
|
2307
|
-
break;
|
|
2308
|
-
case hit300Ratio > 0.9 &&
|
|
2309
|
-
resultObject.accuracy.n50 / totalHits < 0.01 &&
|
|
2310
|
-
!resultObject.accuracy.nmiss:
|
|
2311
|
-
if (isHidden) {
|
|
2312
|
-
resultObject.rank = "SH";
|
|
2313
|
-
}
|
|
2314
|
-
else {
|
|
2315
|
-
resultObject.rank = "S";
|
|
2316
|
-
}
|
|
2317
|
-
break;
|
|
2318
|
-
case (hit300Ratio > 0.8 && !resultObject.accuracy.nmiss) ||
|
|
2319
|
-
hit300Ratio > 0.9:
|
|
2320
|
-
resultObject.rank = "A";
|
|
2321
|
-
break;
|
|
2322
|
-
case (hit300Ratio > 0.7 && !resultObject.accuracy.nmiss) ||
|
|
2323
|
-
hit300Ratio > 0.8:
|
|
2324
|
-
resultObject.rank = "B";
|
|
2325
|
-
break;
|
|
2326
|
-
case hit300Ratio > 0.6:
|
|
2327
|
-
resultObject.rank = "C";
|
|
2328
|
-
break;
|
|
2329
|
-
default:
|
|
2330
|
-
resultObject.rank = "D";
|
|
2331
|
-
}
|
|
2333
|
+
rank: "D",
|
|
2334
|
+
convertedMods: [],
|
|
2335
|
+
hit100k: 0,
|
|
2336
|
+
hit300k: 0,
|
|
2337
|
+
isFullCombo: false,
|
|
2338
|
+
maxCombo: 0,
|
|
2339
|
+
playerName: "",
|
|
2340
|
+
rawMods: "",
|
|
2341
|
+
score: 0,
|
|
2342
|
+
time: new Date(0),
|
|
2332
2343
|
};
|
|
2333
2344
|
if (resultObject.replayVersion >= 3) {
|
|
2334
2345
|
resultObject.time = new Date(Number(rawObject[4].readBigUInt64BE(0)));
|
|
@@ -2346,7 +2357,7 @@ class ReplayAnalyzer {
|
|
|
2346
2357
|
resultObject.playerName = rawObject[5];
|
|
2347
2358
|
resultObject.rawMods = rawObject[6].elements;
|
|
2348
2359
|
resultObject.convertedMods = osuBase.ModUtil.droidStringToMods(this.convertDroidMods(rawObject[6].elements));
|
|
2349
|
-
|
|
2360
|
+
resultObject.rank = this.calculateRank(resultObject);
|
|
2350
2361
|
}
|
|
2351
2362
|
if (resultObject.replayVersion >= 4) {
|
|
2352
2363
|
const str = rawObject[7].split("|");
|
|
@@ -2398,32 +2409,87 @@ class ReplayAnalyzer {
|
|
|
2398
2409
|
}
|
|
2399
2410
|
// Merge all cursor movement and hit object data section into one for better control when parsing
|
|
2400
2411
|
const replayDataBuffer = Buffer.concat(replayDataBufferArray);
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2412
|
+
this.bufferOffset = 0;
|
|
2413
|
+
this.parseMovementData(resultObject, replayDataBuffer);
|
|
2414
|
+
this.parseHitObjectData(resultObject, replayDataBuffer);
|
|
2415
|
+
this.parseOldReplayInformation(resultObject);
|
|
2416
|
+
switch (resultObject.replayVersion) {
|
|
2417
|
+
case 3:
|
|
2418
|
+
this.data = new ReplayV3Data(resultObject);
|
|
2419
|
+
break;
|
|
2420
|
+
case 4:
|
|
2421
|
+
this.data = new ReplayV4Data(resultObject);
|
|
2422
|
+
break;
|
|
2423
|
+
case 5:
|
|
2424
|
+
this.data = new ReplayV5Data(resultObject);
|
|
2425
|
+
break;
|
|
2426
|
+
default:
|
|
2427
|
+
this.data = new ReplayData(resultObject);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Converts replay mods to droid mod string.
|
|
2432
|
+
*/
|
|
2433
|
+
convertDroidMods(replayMods) {
|
|
2434
|
+
const replayModsConstants = {
|
|
2435
|
+
MOD_AUTO: new osuBase.ModAuto().droidString,
|
|
2436
|
+
MOD_AUTOPILOT: new osuBase.ModAutopilot().droidString,
|
|
2437
|
+
MOD_NOFAIL: new osuBase.ModNoFail().droidString,
|
|
2438
|
+
MOD_EASY: new osuBase.ModEasy().droidString,
|
|
2439
|
+
MOD_HIDDEN: new osuBase.ModHidden().droidString,
|
|
2440
|
+
MOD_HARDROCK: new osuBase.ModHardRock().droidString,
|
|
2441
|
+
MOD_DOUBLETIME: new osuBase.ModDoubleTime().droidString,
|
|
2442
|
+
MOD_HALFTIME: new osuBase.ModHalfTime().droidString,
|
|
2443
|
+
MOD_NIGHTCORE: new osuBase.ModNightCore().droidString,
|
|
2444
|
+
MOD_PRECISE: new osuBase.ModPrecise().droidString,
|
|
2445
|
+
MOD_SMALLCIRCLE: new osuBase.ModSmallCircle().droidString,
|
|
2446
|
+
MOD_SPEEDUP: new osuBase.ModSpeedUp().droidString,
|
|
2447
|
+
MOD_REALLYEASY: new osuBase.ModReallyEasy().droidString,
|
|
2448
|
+
MOD_RELAX: new osuBase.ModRelax().droidString,
|
|
2449
|
+
MOD_PERFECT: new osuBase.ModPerfect().droidString,
|
|
2450
|
+
MOD_SUDDENDEATH: new osuBase.ModSuddenDeath().droidString,
|
|
2451
|
+
MOD_SCOREV2: new osuBase.ModScoreV2().droidString,
|
|
2452
|
+
MOD_FLASHLIGHT: new osuBase.ModFlashlight().droidString,
|
|
2453
|
+
};
|
|
2454
|
+
let modString = "";
|
|
2455
|
+
for (const mod of replayMods) {
|
|
2456
|
+
for (const property in replayModsConstants) {
|
|
2457
|
+
if (!mod.includes(property)) {
|
|
2458
|
+
continue;
|
|
2459
|
+
}
|
|
2460
|
+
modString +=
|
|
2461
|
+
replayModsConstants[property];
|
|
2462
|
+
break;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
return modString;
|
|
2466
|
+
}
|
|
2467
|
+
parseMovementData(resultObject, replayDataBuffer) {
|
|
2468
|
+
resultObject.cursorMovement.length = 0;
|
|
2469
|
+
const size = this.readInt(replayDataBuffer);
|
|
2470
|
+
for (let i = 0; i < size; i++) {
|
|
2471
|
+
const moveSize = this.readInt(replayDataBuffer);
|
|
2406
2472
|
const time = [];
|
|
2407
2473
|
const x = [];
|
|
2408
2474
|
const y = [];
|
|
2409
2475
|
const id = [];
|
|
2410
|
-
for (let
|
|
2411
|
-
time[
|
|
2412
|
-
id[
|
|
2413
|
-
time[
|
|
2414
|
-
if (id[
|
|
2476
|
+
for (let j = 0; j < moveSize; j++) {
|
|
2477
|
+
time[j] = this.readInt(replayDataBuffer);
|
|
2478
|
+
id[j] = time[j] & 3;
|
|
2479
|
+
time[j] >>= 2;
|
|
2480
|
+
if (id[j] !== exports.MovementType.up) {
|
|
2415
2481
|
if (resultObject.replayVersion >= 5) {
|
|
2416
|
-
x[
|
|
2417
|
-
y[
|
|
2482
|
+
x[j] = this.readFloat(replayDataBuffer);
|
|
2483
|
+
y[j] = this.readFloat(replayDataBuffer);
|
|
2418
2484
|
}
|
|
2419
2485
|
else {
|
|
2420
|
-
x[
|
|
2421
|
-
y[
|
|
2486
|
+
x[j] = this.readShort(replayDataBuffer);
|
|
2487
|
+
y[j] = this.readShort(replayDataBuffer);
|
|
2422
2488
|
}
|
|
2423
2489
|
}
|
|
2424
2490
|
else {
|
|
2425
|
-
x[
|
|
2426
|
-
y[
|
|
2491
|
+
x[j] = -1;
|
|
2492
|
+
y[j] = -1;
|
|
2427
2493
|
}
|
|
2428
2494
|
}
|
|
2429
2495
|
resultObject.cursorMovement.push(new CursorData({
|
|
@@ -2434,7 +2500,10 @@ class ReplayAnalyzer {
|
|
|
2434
2500
|
id: id,
|
|
2435
2501
|
}));
|
|
2436
2502
|
}
|
|
2437
|
-
|
|
2503
|
+
}
|
|
2504
|
+
parseHitObjectData(resultObject, replayDataBuffer) {
|
|
2505
|
+
resultObject.hitObjectData.length = 0;
|
|
2506
|
+
const replayObjectLength = this.readInt(replayDataBuffer);
|
|
2438
2507
|
// Parse result data
|
|
2439
2508
|
for (let i = 0; i < replayObjectLength; i++) {
|
|
2440
2509
|
const replayObjectData = {
|
|
@@ -2442,12 +2511,12 @@ class ReplayAnalyzer {
|
|
|
2442
2511
|
tickset: [],
|
|
2443
2512
|
result: exports.HitResult.miss,
|
|
2444
2513
|
};
|
|
2445
|
-
replayObjectData.accuracy = this.readShort(replayDataBuffer
|
|
2446
|
-
const len = this.readByte(replayDataBuffer
|
|
2514
|
+
replayObjectData.accuracy = this.readShort(replayDataBuffer);
|
|
2515
|
+
const len = this.readByte(replayDataBuffer);
|
|
2447
2516
|
if (len > 0) {
|
|
2448
2517
|
const bytes = [];
|
|
2449
2518
|
for (let j = 0; j < len; j++) {
|
|
2450
|
-
bytes.push(this.readByte(replayDataBuffer
|
|
2519
|
+
bytes.push(this.readByte(replayDataBuffer));
|
|
2451
2520
|
}
|
|
2452
2521
|
// Int/int division in Java; numbers must be truncated to get actual number
|
|
2453
2522
|
for (let j = 0; j < len * 8; j++) {
|
|
@@ -2457,265 +2526,137 @@ class ReplayAnalyzer {
|
|
|
2457
2526
|
}
|
|
2458
2527
|
}
|
|
2459
2528
|
if (resultObject.replayVersion >= 1) {
|
|
2460
|
-
replayObjectData.result = this.readByte(replayDataBuffer
|
|
2529
|
+
replayObjectData.result = this.readByte(replayDataBuffer);
|
|
2461
2530
|
}
|
|
2462
2531
|
resultObject.hitObjectData.push(replayObjectData);
|
|
2463
2532
|
}
|
|
2464
|
-
// Parse max combo, hit results, and accuracy in old replay version
|
|
2465
|
-
if (resultObject.replayVersion < 3) {
|
|
2466
|
-
const objects = (_a = (this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2467
|
-
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
|
|
2468
|
-
? this.beatmap.beatmap
|
|
2469
|
-
: this.beatmap)) === null || _a === void 0 ? void 0 : _a.hitObjects.objects;
|
|
2470
|
-
let grantsGekiOrKatu = true;
|
|
2471
|
-
for (let i = 0; i < resultObject.hitObjectData.length; ++i) {
|
|
2472
|
-
// Hit result
|
|
2473
|
-
const hitObjectData = resultObject.hitObjectData[i];
|
|
2474
|
-
const isNextNewCombo = objects
|
|
2475
|
-
? i + 1 !== objects.length
|
|
2476
|
-
? objects[i + 1].isNewCombo
|
|
2477
|
-
: true
|
|
2478
|
-
: false;
|
|
2479
|
-
switch (hitObjectData.result) {
|
|
2480
|
-
case exports.HitResult.miss:
|
|
2481
|
-
++resultObject.accuracy.nmiss;
|
|
2482
|
-
grantsGekiOrKatu = false;
|
|
2483
|
-
break;
|
|
2484
|
-
case exports.HitResult.meh:
|
|
2485
|
-
++resultObject.accuracy.n50;
|
|
2486
|
-
grantsGekiOrKatu = false;
|
|
2487
|
-
break;
|
|
2488
|
-
case exports.HitResult.good:
|
|
2489
|
-
++resultObject.accuracy.n100;
|
|
2490
|
-
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
2491
|
-
(_b = resultObject.hit100k) !== null && _b !== void 0 ? _b : (resultObject.hit100k = 0);
|
|
2492
|
-
++resultObject.hit100k;
|
|
2493
|
-
}
|
|
2494
|
-
break;
|
|
2495
|
-
case exports.HitResult.great:
|
|
2496
|
-
++resultObject.accuracy.n300;
|
|
2497
|
-
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
2498
|
-
(_c = resultObject.hit300k) !== null && _c !== void 0 ? _c : (resultObject.hit300k = 0);
|
|
2499
|
-
++resultObject.hit300k;
|
|
2500
|
-
}
|
|
2501
|
-
break;
|
|
2502
|
-
}
|
|
2503
|
-
if (isNextNewCombo) {
|
|
2504
|
-
grantsGekiOrKatu = true;
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
|
-
determineRank();
|
|
2508
|
-
}
|
|
2509
|
-
this.data = new ReplayData(resultObject);
|
|
2510
2533
|
}
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
calculateHitError() {
|
|
2517
|
-
if (!this.data || !this.beatmap) {
|
|
2518
|
-
return null;
|
|
2534
|
+
parseOldReplayInformation(resultObject) {
|
|
2535
|
+
var _a, _b, _c;
|
|
2536
|
+
// Parse max combo, hit results, and accuracy in old replay version
|
|
2537
|
+
if (resultObject.replayVersion >= 3) {
|
|
2538
|
+
return;
|
|
2519
2539
|
}
|
|
2520
|
-
const
|
|
2521
|
-
let positiveCount = 0;
|
|
2522
|
-
let negativeCount = 0;
|
|
2523
|
-
let positiveTotal = 0;
|
|
2524
|
-
let negativeTotal = 0;
|
|
2525
|
-
const beatmap = this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2540
|
+
const objects = (_a = (this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2526
2541
|
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
|
|
2527
2542
|
? this.beatmap.beatmap
|
|
2528
|
-
: this.beatmap;
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
return {
|
|
2564
|
-
positiveAvg: positiveTotal / positiveCount || 0,
|
|
2565
|
-
negativeAvg: negativeTotal / negativeCount || 0,
|
|
2566
|
-
unstableRate: osuBase.MathUtils.calculateStandardDeviation(accuracies) * 10,
|
|
2567
|
-
};
|
|
2568
|
-
}
|
|
2569
|
-
/**
|
|
2570
|
-
* Converts replay mods to droid mod string.
|
|
2571
|
-
*/
|
|
2572
|
-
convertDroidMods(replayMods) {
|
|
2573
|
-
const replayModsConstants = {
|
|
2574
|
-
MOD_AUTO: new osuBase.ModAuto().droidString,
|
|
2575
|
-
MOD_AUTOPILOT: new osuBase.ModAutopilot().droidString,
|
|
2576
|
-
MOD_NOFAIL: new osuBase.ModNoFail().droidString,
|
|
2577
|
-
MOD_EASY: new osuBase.ModEasy().droidString,
|
|
2578
|
-
MOD_HIDDEN: new osuBase.ModHidden().droidString,
|
|
2579
|
-
MOD_HARDROCK: new osuBase.ModHardRock().droidString,
|
|
2580
|
-
MOD_DOUBLETIME: new osuBase.ModDoubleTime().droidString,
|
|
2581
|
-
MOD_HALFTIME: new osuBase.ModHalfTime().droidString,
|
|
2582
|
-
MOD_NIGHTCORE: new osuBase.ModNightCore().droidString,
|
|
2583
|
-
MOD_PRECISE: new osuBase.ModPrecise().droidString,
|
|
2584
|
-
MOD_SMALLCIRCLE: new osuBase.ModSmallCircle().droidString,
|
|
2585
|
-
MOD_SPEEDUP: new osuBase.ModSpeedUp().droidString,
|
|
2586
|
-
MOD_REALLYEASY: new osuBase.ModReallyEasy().droidString,
|
|
2587
|
-
MOD_RELAX: new osuBase.ModRelax().droidString,
|
|
2588
|
-
MOD_PERFECT: new osuBase.ModPerfect().droidString,
|
|
2589
|
-
MOD_SUDDENDEATH: new osuBase.ModSuddenDeath().droidString,
|
|
2590
|
-
MOD_SCOREV2: new osuBase.ModScoreV2().droidString,
|
|
2591
|
-
MOD_FLASHLIGHT: new osuBase.ModFlashlight().droidString,
|
|
2592
|
-
};
|
|
2593
|
-
let modString = "";
|
|
2594
|
-
for (const mod of replayMods) {
|
|
2595
|
-
for (const property in replayModsConstants) {
|
|
2596
|
-
if (!mod.includes(property)) {
|
|
2597
|
-
continue;
|
|
2598
|
-
}
|
|
2599
|
-
modString +=
|
|
2600
|
-
replayModsConstants[property];
|
|
2601
|
-
break;
|
|
2543
|
+
: this.beatmap)) === null || _a === void 0 ? void 0 : _a.hitObjects.objects;
|
|
2544
|
+
let grantsGekiOrKatu = true;
|
|
2545
|
+
for (let i = 0; i < resultObject.hitObjectData.length; ++i) {
|
|
2546
|
+
// Hit result
|
|
2547
|
+
const hitObjectData = resultObject.hitObjectData[i];
|
|
2548
|
+
const isNextNewCombo = objects
|
|
2549
|
+
? i + 1 !== objects.length
|
|
2550
|
+
? objects[i + 1].isNewCombo
|
|
2551
|
+
: true
|
|
2552
|
+
: false;
|
|
2553
|
+
switch (hitObjectData.result) {
|
|
2554
|
+
case exports.HitResult.miss:
|
|
2555
|
+
++resultObject.accuracy.nmiss;
|
|
2556
|
+
grantsGekiOrKatu = false;
|
|
2557
|
+
break;
|
|
2558
|
+
case exports.HitResult.meh:
|
|
2559
|
+
++resultObject.accuracy.n50;
|
|
2560
|
+
grantsGekiOrKatu = false;
|
|
2561
|
+
break;
|
|
2562
|
+
case exports.HitResult.good:
|
|
2563
|
+
++resultObject.accuracy.n100;
|
|
2564
|
+
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
2565
|
+
(_b = resultObject.hit100k) !== null && _b !== void 0 ? _b : (resultObject.hit100k = 0);
|
|
2566
|
+
++resultObject.hit100k;
|
|
2567
|
+
}
|
|
2568
|
+
break;
|
|
2569
|
+
case exports.HitResult.great:
|
|
2570
|
+
++resultObject.accuracy.n300;
|
|
2571
|
+
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
2572
|
+
(_c = resultObject.hit300k) !== null && _c !== void 0 ? _c : (resultObject.hit300k = 0);
|
|
2573
|
+
++resultObject.hit300k;
|
|
2574
|
+
}
|
|
2575
|
+
break;
|
|
2602
2576
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
}
|
|
2606
|
-
/**
|
|
2607
|
-
* Checks if a play is using 3 fingers.
|
|
2608
|
-
*
|
|
2609
|
-
* Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
|
|
2610
|
-
*/
|
|
2611
|
-
checkFor3Finger() {
|
|
2612
|
-
var _a;
|
|
2613
|
-
if (!this.beatmap || !this.data || !this.difficultyAttributes) {
|
|
2614
|
-
return;
|
|
2615
|
-
}
|
|
2616
|
-
if (!this.convertedBeatmap) {
|
|
2617
|
-
const mods = this.data.convertedMods.slice();
|
|
2618
|
-
if ([
|
|
2619
|
-
this.data.forceCS,
|
|
2620
|
-
this.data.forceAR,
|
|
2621
|
-
this.data.forceOD,
|
|
2622
|
-
this.data.forceHP,
|
|
2623
|
-
].some((v) => v !== undefined)) {
|
|
2624
|
-
mods.push(new osuBase.ModDifficultyAdjust({
|
|
2625
|
-
cs: this.data.forceCS,
|
|
2626
|
-
ar: this.data.forceAR,
|
|
2627
|
-
od: this.data.forceOD,
|
|
2628
|
-
hp: this.data.forceHP,
|
|
2629
|
-
}));
|
|
2577
|
+
if (isNextNewCombo) {
|
|
2578
|
+
grantsGekiOrKatu = true;
|
|
2630
2579
|
}
|
|
2631
|
-
(_a = this.convertedBeatmap) !== null && _a !== void 0 ? _a : (this.convertedBeatmap = new osuBase.BeatmapConverter(this.beatmap instanceof osuBase.Beatmap
|
|
2632
|
-
? this.beatmap
|
|
2633
|
-
: this.beatmap.beatmap).convert({
|
|
2634
|
-
mode: osuBase.Modes.droid,
|
|
2635
|
-
mods: mods,
|
|
2636
|
-
customSpeedMultiplier: this.data.speedMultiplier,
|
|
2637
|
-
}));
|
|
2638
2580
|
}
|
|
2639
|
-
|
|
2640
|
-
? new RebalanceThreeFingerChecker(this.convertedBeatmap, this.data, this.difficultyAttributes)
|
|
2641
|
-
: new ThreeFingerChecker(this.convertedBeatmap, this.data, this.difficultyAttributes);
|
|
2642
|
-
const result = threeFingerChecker.check();
|
|
2643
|
-
this.is3Finger = result.is3Finger;
|
|
2644
|
-
this.tapPenalty = result.penalty;
|
|
2645
|
-
this.hasBeenCheckedFor3Finger = true;
|
|
2581
|
+
resultObject.rank = this.calculateRank(resultObject);
|
|
2646
2582
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2583
|
+
calculateRank(resultObject) {
|
|
2584
|
+
var _a, _b;
|
|
2585
|
+
const totalHits = resultObject.accuracy.n300 +
|
|
2586
|
+
resultObject.accuracy.n100 +
|
|
2587
|
+
resultObject.accuracy.n50 +
|
|
2588
|
+
resultObject.accuracy.nmiss;
|
|
2589
|
+
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;
|
|
2590
|
+
const hit300Ratio = resultObject.accuracy.n300 / totalHits;
|
|
2591
|
+
switch (true) {
|
|
2592
|
+
case resultObject.accuracy.value() === 1:
|
|
2593
|
+
return isHidden ? "XH" : "X";
|
|
2594
|
+
case hit300Ratio > 0.9 &&
|
|
2595
|
+
resultObject.accuracy.n50 / totalHits < 0.01 &&
|
|
2596
|
+
!resultObject.accuracy.nmiss:
|
|
2597
|
+
return isHidden ? "SH" : "S";
|
|
2598
|
+
case (hit300Ratio > 0.8 && !resultObject.accuracy.nmiss) ||
|
|
2599
|
+
hit300Ratio > 0.9:
|
|
2600
|
+
return "A";
|
|
2601
|
+
case (hit300Ratio > 0.7 && !resultObject.accuracy.nmiss) ||
|
|
2602
|
+
hit300Ratio > 0.8:
|
|
2603
|
+
return "B";
|
|
2604
|
+
case hit300Ratio > 0.6:
|
|
2605
|
+
return "C";
|
|
2606
|
+
default:
|
|
2607
|
+
return "D";
|
|
2657
2608
|
}
|
|
2658
|
-
const twoHandChecker = new TwoHandChecker(this.beatmap, this.data);
|
|
2659
|
-
const result = twoHandChecker.check();
|
|
2660
|
-
this.is2Hand = result.is2Hand;
|
|
2661
|
-
this.twoHandedNoteCount = result.twoHandedNoteCount;
|
|
2662
|
-
this.hasBeenCheckedFor2Hand = true;
|
|
2663
2609
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
if (!this.convertedBeatmap) {
|
|
2675
|
-
const mods = this.data.convertedMods.slice();
|
|
2676
|
-
if ([
|
|
2610
|
+
constructPlayableBeatmap() {
|
|
2611
|
+
var _a, _b;
|
|
2612
|
+
if (!this.beatmap || !this.data) {
|
|
2613
|
+
throw new Error("Beatmap and replay data must be defined.");
|
|
2614
|
+
}
|
|
2615
|
+
const mods = this.data.isReplayV3()
|
|
2616
|
+
? this.data.convertedMods.slice()
|
|
2617
|
+
: (_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods.slice()) !== null && _b !== void 0 ? _b : [];
|
|
2618
|
+
if (this.data.isReplayV5() &&
|
|
2619
|
+
[
|
|
2677
2620
|
this.data.forceCS,
|
|
2678
2621
|
this.data.forceAR,
|
|
2679
2622
|
this.data.forceOD,
|
|
2680
2623
|
this.data.forceHP,
|
|
2681
2624
|
].some((v) => v !== undefined)) {
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
}));
|
|
2688
|
-
}
|
|
2689
|
-
(_a = this.convertedBeatmap) !== null && _a !== void 0 ? _a : (this.convertedBeatmap = new osuBase.BeatmapConverter(this.beatmap instanceof osuBase.Beatmap
|
|
2690
|
-
? this.beatmap
|
|
2691
|
-
: this.beatmap.beatmap).convert({
|
|
2692
|
-
mode: osuBase.Modes.droid,
|
|
2693
|
-
mods: mods,
|
|
2694
|
-
customSpeedMultiplier: this.data.speedMultiplier,
|
|
2625
|
+
mods.push(new osuBase.ModDifficultyAdjust({
|
|
2626
|
+
cs: this.data.forceCS,
|
|
2627
|
+
ar: this.data.forceAR,
|
|
2628
|
+
od: this.data.forceOD,
|
|
2629
|
+
hp: this.data.forceHP,
|
|
2695
2630
|
}));
|
|
2696
2631
|
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2632
|
+
return (this.beatmap instanceof osuBase.Beatmap
|
|
2633
|
+
? this.beatmap
|
|
2634
|
+
: this.beatmap.beatmap).createPlayableBeatmap({
|
|
2635
|
+
mode: osuBase.Modes.droid,
|
|
2636
|
+
mods: mods,
|
|
2637
|
+
customSpeedMultiplier: this.data.isReplayV4()
|
|
2638
|
+
? this.data.speedMultiplier
|
|
2639
|
+
: 1,
|
|
2640
|
+
});
|
|
2700
2641
|
}
|
|
2701
|
-
readByte(buffer
|
|
2702
|
-
const num = buffer.readInt8(
|
|
2703
|
-
|
|
2642
|
+
readByte(buffer) {
|
|
2643
|
+
const num = buffer.readInt8(this.bufferOffset);
|
|
2644
|
+
this.bufferOffset += 1;
|
|
2704
2645
|
return num;
|
|
2705
2646
|
}
|
|
2706
|
-
readShort(buffer
|
|
2707
|
-
const num = buffer.readInt16BE(
|
|
2708
|
-
|
|
2647
|
+
readShort(buffer) {
|
|
2648
|
+
const num = buffer.readInt16BE(this.bufferOffset);
|
|
2649
|
+
this.bufferOffset += 2;
|
|
2709
2650
|
return num;
|
|
2710
2651
|
}
|
|
2711
|
-
readInt(buffer
|
|
2712
|
-
const num = buffer.readInt32BE(
|
|
2713
|
-
|
|
2652
|
+
readInt(buffer) {
|
|
2653
|
+
const num = buffer.readInt32BE(this.bufferOffset);
|
|
2654
|
+
this.bufferOffset += 4;
|
|
2714
2655
|
return num;
|
|
2715
2656
|
}
|
|
2716
|
-
readFloat(buffer
|
|
2717
|
-
const num = buffer.readFloatBE(
|
|
2718
|
-
|
|
2657
|
+
readFloat(buffer) {
|
|
2658
|
+
const num = buffer.readFloatBE(this.bufferOffset);
|
|
2659
|
+
this.bufferOffset += 4;
|
|
2719
2660
|
return num;
|
|
2720
2661
|
}
|
|
2721
2662
|
}
|
|
@@ -2742,6 +2683,9 @@ exports.RebalanceThreeFingerChecker = RebalanceThreeFingerChecker;
|
|
|
2742
2683
|
exports.ReplayAnalyzer = ReplayAnalyzer;
|
|
2743
2684
|
exports.ReplayData = ReplayData;
|
|
2744
2685
|
exports.ReplayObjectData = ReplayObjectData;
|
|
2686
|
+
exports.ReplayV3Data = ReplayV3Data;
|
|
2687
|
+
exports.ReplayV4Data = ReplayV4Data;
|
|
2688
|
+
exports.ReplayV5Data = ReplayV5Data;
|
|
2745
2689
|
exports.SliderCheeseChecker = SliderCheeseChecker;
|
|
2746
2690
|
exports.ThreeFingerChecker = ThreeFingerChecker;
|
|
2747
2691
|
exports.TwoHandChecker = TwoHandChecker;
|