@rian8337/osu-droid-replay-analyzer 4.0.0-beta.24 → 4.0.0-beta.26
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 +808 -886
- package/package.json +5 -5
- package/typings/index.d.ts +224 -112
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
|
*/
|
|
@@ -331,17 +327,9 @@ class RebalanceThreeFingerChecker {
|
|
|
331
327
|
this.beatmap = beatmap;
|
|
332
328
|
this.data = data;
|
|
333
329
|
this.difficultyAttributes = difficultyAttributes;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
convertOverallDifficulty: false,
|
|
338
|
-
}).overallDifficulty;
|
|
339
|
-
this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
340
|
-
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;
|
|
330
|
+
this.isPrecise = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
331
|
+
this.isHardRock = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
332
|
+
this.hitWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od);
|
|
345
333
|
}
|
|
346
334
|
/**
|
|
347
335
|
* Checks whether a beatmap is eligible to be detected for 3-finger.
|
|
@@ -362,7 +350,8 @@ class RebalanceThreeFingerChecker {
|
|
|
362
350
|
* nerf factor, taking beatmap difficulty into account.
|
|
363
351
|
*/
|
|
364
352
|
check() {
|
|
365
|
-
if (this.difficultyAttributes
|
|
353
|
+
if (!RebalanceThreeFingerChecker.isEligibleToDetect(this.difficultyAttributes) ||
|
|
354
|
+
this.data.cursorMovement.filter((v) => v.occurrenceGroups.length > 0).length <= 3) {
|
|
366
355
|
return { is3Finger: false, penalty: 1 };
|
|
367
356
|
}
|
|
368
357
|
this.getAccurateBreakPoints();
|
|
@@ -382,10 +371,11 @@ class RebalanceThreeFingerChecker {
|
|
|
382
371
|
* end of the hitobject before it and do not end right at the first hitobject after it.
|
|
383
372
|
*/
|
|
384
373
|
getAccurateBreakPoints() {
|
|
374
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
385
375
|
const objectData = this.data.hitObjectData;
|
|
386
376
|
for (const breakPoint of this.beatmap.events.breaks) {
|
|
387
|
-
const beforeIndex = osuBase.MathUtils.clamp(
|
|
388
|
-
const objectBefore =
|
|
377
|
+
const beforeIndex = osuBase.MathUtils.clamp(objects.findIndex((o) => o.endTime >= breakPoint.startTime) - 1, 0, objects.length - 2);
|
|
378
|
+
const objectBefore = objects[beforeIndex];
|
|
389
379
|
const objectBeforeData = objectData[beforeIndex];
|
|
390
380
|
let timeBefore = objectBefore.endTime;
|
|
391
381
|
if (objectBefore instanceof osuBase.Circle) {
|
|
@@ -397,9 +387,9 @@ class RebalanceThreeFingerChecker {
|
|
|
397
387
|
}
|
|
398
388
|
}
|
|
399
389
|
const afterIndex = beforeIndex + 1;
|
|
400
|
-
const objectAfter =
|
|
390
|
+
const objectAfter = objects[afterIndex];
|
|
401
391
|
const objectAfterData = objectData[afterIndex];
|
|
402
|
-
let timeAfter =
|
|
392
|
+
let timeAfter = objectAfter.startTime;
|
|
403
393
|
if (objectAfter instanceof osuBase.Circle &&
|
|
404
394
|
objectAfterData.result !== exports.HitResult.miss) {
|
|
405
395
|
timeAfter += objectAfterData.accuracy;
|
|
@@ -416,11 +406,12 @@ class RebalanceThreeFingerChecker {
|
|
|
416
406
|
* This also filters cursors that are in break period or happen before start/after end of the beatmap.
|
|
417
407
|
*/
|
|
418
408
|
filterCursorInstances() {
|
|
409
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
419
410
|
const objectData = this.data.hitObjectData;
|
|
420
411
|
const firstObjectResult = objectData[0].result;
|
|
421
412
|
const lastObjectResult = objectData.at(-1).result;
|
|
422
|
-
const firstObject =
|
|
423
|
-
const lastObject =
|
|
413
|
+
const firstObject = objects[0];
|
|
414
|
+
const lastObject = objects.at(-1);
|
|
424
415
|
// For sliders, automatically set hit window length to be as lenient as possible.
|
|
425
416
|
let firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
426
417
|
if (firstObject instanceof osuBase.Circle) {
|
|
@@ -480,289 +471,202 @@ class RebalanceThreeFingerChecker {
|
|
|
480
471
|
* detect dragged sections and improve detection speed.
|
|
481
472
|
*/
|
|
482
473
|
getBeatmapSections() {
|
|
483
|
-
const
|
|
474
|
+
const beatmapObjects = this.beatmap.hitObjects.objects;
|
|
475
|
+
const aimCursorGroupLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
476
|
+
// This intentionally starts from 1 because we need to look at the previous cursor.
|
|
477
|
+
const aimCursorLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 1);
|
|
478
|
+
const pressCursorLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
484
479
|
for (const section of this.difficultyAttributes
|
|
485
480
|
.possibleThreeFingeredSections) {
|
|
486
|
-
const dragFingerIndex = this.findDragIndex(section);
|
|
487
481
|
const objects = [];
|
|
488
482
|
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
489
|
-
|
|
483
|
+
const object = beatmapObjects[i];
|
|
484
|
+
const objectData = this.data.hitObjectData[i];
|
|
485
|
+
objects.push({
|
|
486
|
+
object: object,
|
|
487
|
+
aimingCursorInstanceIndex: this.getObjectAimIndex(object, objectData, aimCursorGroupLookupIndices, aimCursorLookupIndices),
|
|
488
|
+
pressingCursorInstanceIndex: this.getObjectPressIndex(object, objectData, pressCursorLookupIndices),
|
|
489
|
+
});
|
|
490
490
|
}
|
|
491
|
-
this.beatmapSections.push(Object.assign(Object.assign({}, section), {
|
|
491
|
+
this.beatmapSections.push(Object.assign(Object.assign({}, section), { objects: objects }));
|
|
492
492
|
}
|
|
493
493
|
}
|
|
494
494
|
/**
|
|
495
|
-
* Obtains the index of the
|
|
495
|
+
* Obtains the index of the cursor that aimed the object at the nearest time.
|
|
496
496
|
*
|
|
497
497
|
* @param object The object to obtain the index for.
|
|
498
498
|
* @param objectData The hit data of the object.
|
|
499
|
-
* @param
|
|
500
|
-
* @param
|
|
499
|
+
* @param cursorInstanceIndices The cursor indices to start looking for the cursor instance from, to save computation time.
|
|
500
|
+
* @param cursorGroupIndices The cursor indices to start looking for the cursor group from, to save computation time.
|
|
501
|
+
* @param cursorIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
501
502
|
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
502
503
|
*/
|
|
503
|
-
|
|
504
|
+
getObjectAimIndex(object, objectData, cursorGroupIndices, cursorIndices) {
|
|
504
505
|
if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
|
|
505
|
-
return
|
|
506
|
-
pressingCursorInstanceIndex: -1,
|
|
507
|
-
pressingCursorIndex: -1,
|
|
508
|
-
};
|
|
506
|
+
return -1;
|
|
509
507
|
}
|
|
510
|
-
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
511
508
|
// Check for sliderbreaks and treat them as misses.
|
|
512
509
|
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);
|
|
510
|
+
objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
|
|
511
|
+
return -1;
|
|
539
512
|
}
|
|
540
513
|
const hitTime = object.startTime + objectData.accuracy;
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
514
|
+
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
515
|
+
// We are maintaining the closest distance to the object.
|
|
516
|
+
// This is because the radius that is calculated is using an estimation.
|
|
517
|
+
// As such, it does not reflect the actual object radius in gameplay.
|
|
518
|
+
let closestDistance = Number.POSITIVE_INFINITY;
|
|
519
|
+
let nearestCursorIndex = -1;
|
|
520
|
+
// Observe the cursor position at the object's hit time.
|
|
521
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
522
|
+
const cursorData = this.data.cursorMovement[i];
|
|
523
|
+
for (let j = cursorGroupIndices[i]; j < cursorData.occurrenceGroups.length; cursorGroupIndices[i] = ++j) {
|
|
524
|
+
const cursorGroup = cursorData.occurrenceGroups[j];
|
|
525
|
+
if (cursorGroup.endTime < hitTime) {
|
|
526
|
+
// Reset cursor index pointer.
|
|
527
|
+
cursorIndices[i] = 1;
|
|
552
528
|
continue;
|
|
553
529
|
}
|
|
554
|
-
if (
|
|
530
|
+
if (cursorGroup.startTime > hitTime) {
|
|
555
531
|
break;
|
|
556
532
|
}
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
533
|
+
const cursors = cursorGroup.allOccurrences;
|
|
534
|
+
for (let k = cursorIndices[i]; k < cursors.length; cursorIndices[i] = ++k) {
|
|
535
|
+
const cursor = cursors[k];
|
|
536
|
+
const prevCursor = cursors[k - 1];
|
|
537
|
+
// Cursor is past the object's hit time.
|
|
538
|
+
if (prevCursor.time > hitTime) {
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
// Cursor is before the object's hit time.
|
|
542
|
+
if (hitTime > cursor.time) {
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
let distance;
|
|
546
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
547
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
548
|
+
switch (cursor.id) {
|
|
549
|
+
case exports.MovementType.up:
|
|
550
|
+
distance = prevPosition.getDistance(objectPosition);
|
|
551
|
+
break;
|
|
552
|
+
case exports.MovementType.move: {
|
|
553
|
+
// Interpolate movement.
|
|
554
|
+
const t = (hitTime - prevCursor.time) /
|
|
555
|
+
(cursor.time - prevCursor.time);
|
|
556
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
557
|
+
distance =
|
|
558
|
+
objectPosition.getDistance(cursorPosition);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case exports.MovementType.down:
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (closestDistance > distance) {
|
|
565
|
+
closestDistance = distance;
|
|
566
|
+
nearestCursorIndex = i;
|
|
567
|
+
}
|
|
560
568
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
569
|
+
// Reset cursor index pointer on end of group.
|
|
570
|
+
if (cursorIndices[i] === cursors.length) {
|
|
571
|
+
cursorIndices[i] = 1;
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
564
574
|
}
|
|
575
|
+
// The previous object may still be hit with the same cursor group or cursor index.
|
|
576
|
+
cursorGroupIndices[i] = Math.max(0, cursorGroupIndices[i] - 1);
|
|
577
|
+
cursorIndices[i] = Math.max(1, cursorIndices[i] - 1);
|
|
565
578
|
}
|
|
566
|
-
return
|
|
567
|
-
pressingCursorInstanceIndex: nearestCursorInstanceIndex !== null && nearestCursorInstanceIndex !== void 0 ? nearestCursorInstanceIndex : -1,
|
|
568
|
-
pressingCursorIndex: nearestCursorIndex !== null && nearestCursorIndex !== void 0 ? nearestCursorIndex : -1,
|
|
569
|
-
};
|
|
579
|
+
return nearestCursorIndex;
|
|
570
580
|
}
|
|
571
581
|
/**
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
* If the section is not dragged, -1 will be returned.
|
|
582
|
+
* Obtains the index of the nearest cursor of which an object was pressed in terms of time.
|
|
575
583
|
*
|
|
576
|
-
* @param
|
|
584
|
+
* @param object The object to obtain the index for.
|
|
585
|
+
* @param objectData The hit data of the object.
|
|
586
|
+
* @param cursorLookupIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
587
|
+
* @param excludedIndices The cursor indices that should not be checked.
|
|
588
|
+
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
577
589
|
*/
|
|
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);
|
|
590
|
+
getObjectPressIndex(object, objectData, cursorLookupIndices) {
|
|
591
|
+
if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
|
|
592
|
+
return -1;
|
|
599
593
|
}
|
|
600
|
-
|
|
601
|
-
|
|
594
|
+
// Check for sliderbreaks and treat them as misses.
|
|
595
|
+
if (object instanceof osuBase.Slider &&
|
|
596
|
+
objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
|
|
597
|
+
return -1;
|
|
602
598
|
}
|
|
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
|
-
}
|
|
599
|
+
const hitTime = object.startTime + objectData.accuracy;
|
|
600
|
+
let nearestCursorInstanceIndex = -1;
|
|
601
|
+
let nearestTime = Number.POSITIVE_INFINITY;
|
|
602
|
+
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
603
|
+
const cursors = this.downCursorInstances[i];
|
|
604
|
+
let cursorNearestTime = Number.POSITIVE_INFINITY;
|
|
605
|
+
for (let j = cursorLookupIndices[i]; j < cursors.length; cursorLookupIndices[i] = ++j) {
|
|
606
|
+
const cursor = cursors[j];
|
|
607
|
+
if (cursor.time > hitTime) {
|
|
703
608
|
break;
|
|
704
609
|
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
610
|
+
cursorNearestTime = hitTime - cursor.time;
|
|
611
|
+
}
|
|
612
|
+
if (cursorNearestTime < nearestTime) {
|
|
613
|
+
nearestCursorInstanceIndex = i;
|
|
614
|
+
nearestTime = cursorNearestTime;
|
|
708
615
|
}
|
|
709
616
|
}
|
|
710
|
-
return
|
|
617
|
+
return nearestCursorInstanceIndex;
|
|
711
618
|
}
|
|
712
619
|
/**
|
|
713
620
|
* Creates nerf factors by scanning through objects.
|
|
714
621
|
*/
|
|
715
622
|
calculateNerfFactors() {
|
|
716
623
|
for (const beatmapSection of this.beatmapSections) {
|
|
717
|
-
const
|
|
624
|
+
const threeFingerCursorCounts = osuBase.Utils.initializeArray(Math.max(0, this.downCursorInstances.length - 2), 0);
|
|
718
625
|
for (const object of beatmapSection.objects) {
|
|
719
|
-
if (object.
|
|
720
|
-
object.pressingCursorInstanceIndex ===
|
|
721
|
-
beatmapSection.dragFingerIndex) {
|
|
626
|
+
if (object.pressingCursorInstanceIndex === -1) {
|
|
722
627
|
continue;
|
|
723
628
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
629
|
+
if (object.aimingCursorInstanceIndex < 3) {
|
|
630
|
+
// The aim cursor is in the first three cursors. They are counted as non-3 finger.
|
|
631
|
+
switch (object.pressingCursorInstanceIndex) {
|
|
632
|
+
case 0:
|
|
633
|
+
case 1:
|
|
634
|
+
case 2:
|
|
635
|
+
break;
|
|
636
|
+
default:
|
|
637
|
+
++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 3];
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
// The aim cursor is somewhere else. only count the first 2 cursors as non-3 finger.
|
|
643
|
+
switch (object.pressingCursorInstanceIndex) {
|
|
644
|
+
case 0:
|
|
645
|
+
case 1:
|
|
646
|
+
break;
|
|
647
|
+
default:
|
|
648
|
+
++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 2];
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
729
652
|
}
|
|
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
|
-
});
|
|
653
|
+
const threeFingerCursorCount = threeFingerCursorCounts.reduce((a, v) => a + v, 0);
|
|
654
|
+
if (threeFingerCursorCount === 0) {
|
|
655
|
+
continue;
|
|
765
656
|
}
|
|
657
|
+
const sectionObjectCount = beatmapSection.objects.length;
|
|
658
|
+
const threeFingeredObjectRatio = threeFingerCursorCount / sectionObjectCount;
|
|
659
|
+
const strainFactor = Math.max(1, beatmapSection.sumStrain * threeFingeredObjectRatio);
|
|
660
|
+
// Finger factor applies more penalty if more fingers were used.
|
|
661
|
+
const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
|
|
662
|
+
Math.pow(((index + 1) * count) / sectionObjectCount, 0.9), 1);
|
|
663
|
+
// Length factor applies more penalty if there are more 3-fingered object.
|
|
664
|
+
const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 0.8);
|
|
665
|
+
this.nerfFactors.push({
|
|
666
|
+
strainFactor: strainFactor,
|
|
667
|
+
fingerFactor: fingerFactor,
|
|
668
|
+
lengthFactor: lengthFactor,
|
|
669
|
+
});
|
|
766
670
|
}
|
|
767
671
|
}
|
|
768
672
|
/**
|
|
@@ -773,6 +677,12 @@ class RebalanceThreeFingerChecker {
|
|
|
773
677
|
0.015 *
|
|
774
678
|
Math.pow(n.strainFactor * n.fingerFactor * n.lengthFactor, 1.05), 1);
|
|
775
679
|
}
|
|
680
|
+
getCursorPosition(cursor) {
|
|
681
|
+
if (this.isHardRock) {
|
|
682
|
+
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
683
|
+
}
|
|
684
|
+
return cursor.position;
|
|
685
|
+
}
|
|
776
686
|
}
|
|
777
687
|
|
|
778
688
|
/******************************************************************************
|
|
@@ -808,9 +718,9 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
808
718
|
};
|
|
809
719
|
|
|
810
720
|
/**
|
|
811
|
-
* Represents a replay data in an osu!droid replay.
|
|
721
|
+
* Represents a replay data in an osu!droid replay version 1 and 2.
|
|
812
722
|
*
|
|
813
|
-
* Stores generic information about an osu!droid replay
|
|
723
|
+
* Stores generic information about an osu!droid replay.
|
|
814
724
|
*
|
|
815
725
|
* This is used when analyzing replays using replay analyzer.
|
|
816
726
|
*/
|
|
@@ -820,45 +730,46 @@ class ReplayData {
|
|
|
820
730
|
this.folderName = values.folderName;
|
|
821
731
|
this.fileName = values.fileName;
|
|
822
732
|
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 || [];
|
|
733
|
+
this.accuracy = values.accuracy;
|
|
734
|
+
this.rank = values.rank;
|
|
735
|
+
this.hit300k = values.hit300k;
|
|
736
|
+
this.hit100k = values.hit100k;
|
|
834
737
|
this.cursorMovement = values.cursorMovement;
|
|
835
738
|
this.hitObjectData = values.hitObjectData;
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
this.
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Whether the replay's version is 3 or later.
|
|
742
|
+
*/
|
|
743
|
+
isReplayV3() {
|
|
744
|
+
return this.replayVersion >= 3;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Whether the replay's version is 4 or later.
|
|
748
|
+
*/
|
|
749
|
+
isReplayV4() {
|
|
750
|
+
return this.replayVersion >= 4;
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Whether the replay's version is 5 or later.
|
|
754
|
+
*/
|
|
755
|
+
isReplayV5() {
|
|
756
|
+
return this.replayVersion >= 5;
|
|
842
757
|
}
|
|
843
758
|
}
|
|
844
759
|
|
|
845
760
|
/**
|
|
846
|
-
* Utility to check whether or not a beatmap is three-fingered.
|
|
761
|
+
* Utility to check whether or not a beatmap is three-fingered for rebalance scores.
|
|
847
762
|
*/
|
|
848
763
|
class ThreeFingerChecker {
|
|
764
|
+
get hitWindow50() {
|
|
765
|
+
return this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
766
|
+
}
|
|
849
767
|
/**
|
|
850
768
|
* @param beatmap The beatmap to analyze.
|
|
851
769
|
* @param data The data of the replay.
|
|
852
770
|
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
853
771
|
*/
|
|
854
772
|
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
773
|
/**
|
|
863
774
|
* Extended sections of the beatmap for drag detection.
|
|
864
775
|
*/
|
|
@@ -883,14 +794,9 @@ class ThreeFingerChecker {
|
|
|
883
794
|
this.beatmap = beatmap;
|
|
884
795
|
this.data = data;
|
|
885
796
|
this.difficultyAttributes = difficultyAttributes;
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
convertOverallDifficulty: false,
|
|
890
|
-
}).overallDifficulty;
|
|
891
|
-
this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
892
|
-
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
893
|
-
this.hitObjects = beatmap.hitObjects.objects;
|
|
797
|
+
this.isPrecise = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
798
|
+
this.isHardRock = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
799
|
+
this.hitWindow = new osuBase.DroidHitWindow(beatmap.difficulty.od);
|
|
894
800
|
}
|
|
895
801
|
/**
|
|
896
802
|
* Checks whether a beatmap is eligible to be detected for 3-finger.
|
|
@@ -911,7 +817,8 @@ class ThreeFingerChecker {
|
|
|
911
817
|
* nerf factor, taking beatmap difficulty into account.
|
|
912
818
|
*/
|
|
913
819
|
check() {
|
|
914
|
-
if (this.difficultyAttributes
|
|
820
|
+
if (!ThreeFingerChecker.isEligibleToDetect(this.difficultyAttributes) ||
|
|
821
|
+
this.data.cursorMovement.filter((v) => v.occurrenceGroups.length > 0).length <= 3) {
|
|
915
822
|
return { is3Finger: false, penalty: 1 };
|
|
916
823
|
}
|
|
917
824
|
this.getAccurateBreakPoints();
|
|
@@ -931,10 +838,11 @@ class ThreeFingerChecker {
|
|
|
931
838
|
* end of the hitobject before it and do not end right at the first hitobject after it.
|
|
932
839
|
*/
|
|
933
840
|
getAccurateBreakPoints() {
|
|
841
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
934
842
|
const objectData = this.data.hitObjectData;
|
|
935
843
|
for (const breakPoint of this.beatmap.events.breaks) {
|
|
936
|
-
const beforeIndex = osuBase.MathUtils.clamp(
|
|
937
|
-
const objectBefore =
|
|
844
|
+
const beforeIndex = osuBase.MathUtils.clamp(objects.findIndex((o) => o.endTime >= breakPoint.startTime) - 1, 0, objects.length - 2);
|
|
845
|
+
const objectBefore = objects[beforeIndex];
|
|
938
846
|
const objectBeforeData = objectData[beforeIndex];
|
|
939
847
|
let timeBefore = objectBefore.endTime;
|
|
940
848
|
if (objectBefore instanceof osuBase.Circle) {
|
|
@@ -946,9 +854,9 @@ class ThreeFingerChecker {
|
|
|
946
854
|
}
|
|
947
855
|
}
|
|
948
856
|
const afterIndex = beforeIndex + 1;
|
|
949
|
-
const objectAfter =
|
|
857
|
+
const objectAfter = objects[afterIndex];
|
|
950
858
|
const objectAfterData = objectData[afterIndex];
|
|
951
|
-
let timeAfter =
|
|
859
|
+
let timeAfter = objectAfter.startTime;
|
|
952
860
|
if (objectAfter instanceof osuBase.Circle &&
|
|
953
861
|
objectAfterData.result !== exports.HitResult.miss) {
|
|
954
862
|
timeAfter += objectAfterData.accuracy;
|
|
@@ -965,11 +873,12 @@ class ThreeFingerChecker {
|
|
|
965
873
|
* This also filters cursors that are in break period or happen before start/after end of the beatmap.
|
|
966
874
|
*/
|
|
967
875
|
filterCursorInstances() {
|
|
876
|
+
const objects = this.beatmap.hitObjects.objects;
|
|
968
877
|
const objectData = this.data.hitObjectData;
|
|
969
878
|
const firstObjectResult = objectData[0].result;
|
|
970
879
|
const lastObjectResult = objectData.at(-1).result;
|
|
971
|
-
const firstObject =
|
|
972
|
-
const lastObject =
|
|
880
|
+
const firstObject = objects[0];
|
|
881
|
+
const lastObject = objects.at(-1);
|
|
973
882
|
// For sliders, automatically set hit window length to be as lenient as possible.
|
|
974
883
|
let firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
975
884
|
if (firstObject instanceof osuBase.Circle) {
|
|
@@ -1029,280 +938,217 @@ class ThreeFingerChecker {
|
|
|
1029
938
|
* detect dragged sections and improve detection speed.
|
|
1030
939
|
*/
|
|
1031
940
|
getBeatmapSections() {
|
|
1032
|
-
const
|
|
941
|
+
const beatmapObjects = this.beatmap.hitObjects.objects;
|
|
942
|
+
const aimCursorGroupLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
943
|
+
// This intentionally starts from 1 because we need to look at the previous cursor.
|
|
944
|
+
const aimCursorLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 1);
|
|
945
|
+
const pressCursorLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
1033
946
|
for (const section of this.difficultyAttributes
|
|
1034
947
|
.possibleThreeFingeredSections) {
|
|
1035
|
-
const dragFingerIndex = this.findDragIndex(section);
|
|
1036
948
|
const objects = [];
|
|
1037
949
|
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
1038
|
-
|
|
950
|
+
const object = beatmapObjects[i];
|
|
951
|
+
const objectData = this.data.hitObjectData[i];
|
|
952
|
+
objects.push({
|
|
953
|
+
object: object,
|
|
954
|
+
aimingCursorInstanceIndex: this.getObjectAimIndex(object, objectData, aimCursorGroupLookupIndices, aimCursorLookupIndices),
|
|
955
|
+
pressingCursorInstanceIndex: this.getObjectPressIndex(object, objectData, pressCursorLookupIndices),
|
|
956
|
+
});
|
|
1039
957
|
}
|
|
1040
|
-
this.beatmapSections.push(Object.assign(Object.assign({}, section), {
|
|
958
|
+
this.beatmapSections.push(Object.assign(Object.assign({}, section), { objects: objects }));
|
|
1041
959
|
}
|
|
1042
960
|
}
|
|
1043
961
|
/**
|
|
1044
|
-
* Obtains the index of the
|
|
962
|
+
* Obtains the index of the cursor that aimed the object at the nearest time.
|
|
1045
963
|
*
|
|
1046
964
|
* @param object The object to obtain the index for.
|
|
1047
965
|
* @param objectData The hit data of the object.
|
|
1048
|
-
* @param
|
|
1049
|
-
* @param
|
|
966
|
+
* @param cursorInstanceIndices The cursor indices to start looking for the cursor instance from, to save computation time.
|
|
967
|
+
* @param cursorGroupIndices The cursor indices to start looking for the cursor group from, to save computation time.
|
|
968
|
+
* @param cursorIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
1050
969
|
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
1051
970
|
*/
|
|
1052
|
-
|
|
971
|
+
getObjectAimIndex(object, objectData, cursorGroupIndices, cursorIndices) {
|
|
1053
972
|
if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
|
|
1054
|
-
return
|
|
1055
|
-
pressingCursorInstanceIndex: -1,
|
|
1056
|
-
pressingCursorIndex: -1,
|
|
1057
|
-
};
|
|
973
|
+
return -1;
|
|
1058
974
|
}
|
|
1059
|
-
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
1060
975
|
// Check for sliderbreaks and treat them as misses.
|
|
1061
976
|
if (object instanceof osuBase.Slider &&
|
|
1062
|
-
objectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
1063
|
-
return
|
|
1064
|
-
pressingCursorInstanceIndex: -1,
|
|
1065
|
-
pressingCursorIndex: -1,
|
|
1066
|
-
};
|
|
977
|
+
objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
|
|
978
|
+
return -1;
|
|
1067
979
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
980
|
+
const hitTime = object.startTime + objectData.accuracy;
|
|
981
|
+
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
982
|
+
// We are maintaining the closest distance to the object.
|
|
983
|
+
// This is because the radius that is calculated is using an estimation.
|
|
984
|
+
// As such, it does not reflect the actual object radius in gameplay.
|
|
985
|
+
let closestDistance = Number.POSITIVE_INFINITY;
|
|
986
|
+
let nearestCursorIndex = -1;
|
|
987
|
+
// Observe the cursor position at the object's hit time.
|
|
988
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
989
|
+
const cursorData = this.data.cursorMovement[i];
|
|
990
|
+
for (let j = cursorGroupIndices[i]; j < cursorData.occurrenceGroups.length; cursorGroupIndices[i] = ++j) {
|
|
991
|
+
const cursorGroup = cursorData.occurrenceGroups[j];
|
|
992
|
+
if (cursorGroup.endTime < hitTime) {
|
|
993
|
+
// Reset cursor index pointer.
|
|
994
|
+
cursorIndices[i] = 1;
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
if (cursorGroup.startTime > hitTime) {
|
|
1080
998
|
break;
|
|
999
|
+
}
|
|
1000
|
+
const cursors = cursorGroup.allOccurrences;
|
|
1001
|
+
for (let k = cursorIndices[i]; k < cursors.length; cursorIndices[i] = ++k) {
|
|
1002
|
+
const cursor = cursors[k];
|
|
1003
|
+
const prevCursor = cursors[k - 1];
|
|
1004
|
+
// Cursor is past the object's hit time.
|
|
1005
|
+
if (prevCursor.time > hitTime) {
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
// Cursor is before the object's hit time.
|
|
1009
|
+
if (hitTime > cursor.time) {
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
let distance;
|
|
1013
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
1014
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
1015
|
+
switch (cursor.id) {
|
|
1016
|
+
case exports.MovementType.up:
|
|
1017
|
+
distance = prevPosition.getDistance(objectPosition);
|
|
1018
|
+
break;
|
|
1019
|
+
case exports.MovementType.move: {
|
|
1020
|
+
// Interpolate movement.
|
|
1021
|
+
const t = (hitTime - prevCursor.time) /
|
|
1022
|
+
(cursor.time - prevCursor.time);
|
|
1023
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
1024
|
+
distance =
|
|
1025
|
+
objectPosition.getDistance(cursorPosition);
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
case exports.MovementType.down:
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (closestDistance > distance) {
|
|
1032
|
+
closestDistance = distance;
|
|
1033
|
+
nearestCursorIndex = i;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
// Reset cursor index pointer on end of group.
|
|
1037
|
+
if (cursorIndices[i] === cursors.length) {
|
|
1038
|
+
cursorIndices[i] = 1;
|
|
1039
|
+
}
|
|
1040
|
+
break;
|
|
1081
1041
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1042
|
+
// The previous object may still be hit with the same cursor group or cursor index.
|
|
1043
|
+
cursorGroupIndices[i] = Math.max(0, cursorGroupIndices[i] - 1);
|
|
1044
|
+
cursorIndices[i] = Math.max(1, cursorIndices[i] - 1);
|
|
1045
|
+
}
|
|
1046
|
+
return nearestCursorIndex;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Obtains the index of the nearest cursor of which an object was pressed in terms of time.
|
|
1050
|
+
*
|
|
1051
|
+
* @param object The object to obtain the index for.
|
|
1052
|
+
* @param objectData The hit data of the object.
|
|
1053
|
+
* @param cursorLookupIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
1054
|
+
* @param excludedIndices The cursor indices that should not be checked.
|
|
1055
|
+
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
1056
|
+
*/
|
|
1057
|
+
getObjectPressIndex(object, objectData, cursorLookupIndices) {
|
|
1058
|
+
if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
|
|
1059
|
+
return -1;
|
|
1084
1060
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1061
|
+
// Check for sliderbreaks and treat them as misses.
|
|
1062
|
+
if (object instanceof osuBase.Slider &&
|
|
1063
|
+
objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
|
|
1064
|
+
return -1;
|
|
1088
1065
|
}
|
|
1089
1066
|
const hitTime = object.startTime + objectData.accuracy;
|
|
1090
|
-
let nearestCursorInstanceIndex =
|
|
1091
|
-
let nearestCursorIndex = null;
|
|
1067
|
+
let nearestCursorInstanceIndex = -1;
|
|
1092
1068
|
let nearestTime = Number.POSITIVE_INFINITY;
|
|
1093
1069
|
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
1094
|
-
if (excludedCursorIndices.includes(i)) {
|
|
1095
|
-
continue;
|
|
1096
|
-
}
|
|
1097
1070
|
const cursors = this.downCursorInstances[i];
|
|
1071
|
+
let cursorNearestTime = Number.POSITIVE_INFINITY;
|
|
1098
1072
|
for (let j = cursorLookupIndices[i]; j < cursors.length; cursorLookupIndices[i] = ++j) {
|
|
1099
1073
|
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) {
|
|
1074
|
+
if (cursor.time > hitTime) {
|
|
1108
1075
|
break;
|
|
1109
1076
|
}
|
|
1077
|
+
cursorNearestTime = hitTime - cursor.time;
|
|
1078
|
+
}
|
|
1079
|
+
if (cursorNearestTime < nearestTime) {
|
|
1110
1080
|
nearestCursorInstanceIndex = i;
|
|
1111
|
-
|
|
1112
|
-
nearestTime = deltaTime;
|
|
1081
|
+
nearestTime = cursorNearestTime;
|
|
1113
1082
|
}
|
|
1114
1083
|
}
|
|
1115
|
-
return
|
|
1116
|
-
pressingCursorInstanceIndex: nearestCursorInstanceIndex !== null && nearestCursorInstanceIndex !== void 0 ? nearestCursorInstanceIndex : -1,
|
|
1117
|
-
pressingCursorIndex: nearestCursorIndex !== null && nearestCursorIndex !== void 0 ? nearestCursorIndex : -1,
|
|
1118
|
-
};
|
|
1084
|
+
return nearestCursorInstanceIndex;
|
|
1119
1085
|
}
|
|
1120
1086
|
/**
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
* If the section is not dragged, -1 will be returned.
|
|
1124
|
-
*
|
|
1125
|
-
* @param section The section to check.
|
|
1087
|
+
* Creates nerf factors by scanning through objects.
|
|
1126
1088
|
*/
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1089
|
+
calculateNerfFactors() {
|
|
1090
|
+
for (const beatmapSection of this.beatmapSections) {
|
|
1091
|
+
const threeFingerCursorCounts = osuBase.Utils.initializeArray(Math.max(0, this.downCursorInstances.length - 2), 0);
|
|
1092
|
+
for (const object of beatmapSection.objects) {
|
|
1093
|
+
if (object.pressingCursorInstanceIndex === -1) {
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (object.aimingCursorInstanceIndex < 3) {
|
|
1097
|
+
// The aim cursor is in the first three cursors. They are counted as non-3 finger.
|
|
1098
|
+
switch (object.pressingCursorInstanceIndex) {
|
|
1099
|
+
case 0:
|
|
1100
|
+
case 1:
|
|
1101
|
+
case 2:
|
|
1102
|
+
break;
|
|
1103
|
+
default:
|
|
1104
|
+
++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 3];
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
// The aim cursor is somewhere else. only count the first 2 cursors as non-3 finger.
|
|
1110
|
+
switch (object.pressingCursorInstanceIndex) {
|
|
1111
|
+
case 0:
|
|
1112
|
+
case 1:
|
|
1113
|
+
break;
|
|
1114
|
+
default:
|
|
1115
|
+
++threeFingerCursorCounts[object.pressingCursorInstanceIndex - 2];
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1143
1119
|
}
|
|
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);
|
|
1120
|
+
const threeFingerCursorCount = threeFingerCursorCounts.reduce((a, v) => a + v, 0);
|
|
1121
|
+
if (threeFingerCursorCount === 0) {
|
|
1122
|
+
continue;
|
|
1159
1123
|
}
|
|
1124
|
+
const sectionObjectCount = beatmapSection.objects.length;
|
|
1125
|
+
const threeFingeredObjectRatio = threeFingerCursorCount / sectionObjectCount;
|
|
1126
|
+
const strainFactor = Math.max(1, beatmapSection.sumStrain * threeFingeredObjectRatio);
|
|
1127
|
+
// Finger factor applies more penalty if more fingers were used.
|
|
1128
|
+
const fingerFactor = threeFingerCursorCounts.reduce((acc, count, index) => acc +
|
|
1129
|
+
Math.pow(((index + 1) * count) / sectionObjectCount, 0.9), 1);
|
|
1130
|
+
// Length factor applies more penalty if there are more 3-fingered object.
|
|
1131
|
+
const lengthFactor = 1 + Math.pow(threeFingeredObjectRatio, 0.8);
|
|
1132
|
+
this.nerfFactors.push({
|
|
1133
|
+
strainFactor: strainFactor,
|
|
1134
|
+
fingerFactor: fingerFactor,
|
|
1135
|
+
lengthFactor: lengthFactor,
|
|
1136
|
+
});
|
|
1160
1137
|
}
|
|
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
|
-
}
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Calculates the final penalty.
|
|
1141
|
+
*/
|
|
1142
|
+
calculateFinalPenalty() {
|
|
1143
|
+
return this.nerfFactors.reduce((a, n) => a +
|
|
1144
|
+
0.015 *
|
|
1145
|
+
Math.pow(n.strainFactor * n.fingerFactor * n.lengthFactor, 1.05), 1);
|
|
1146
|
+
}
|
|
1147
|
+
getCursorPosition(cursor) {
|
|
1148
|
+
if (this.isHardRock) {
|
|
1149
|
+
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
1297
1150
|
}
|
|
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);
|
|
1151
|
+
return cursor.position;
|
|
1306
1152
|
}
|
|
1307
1153
|
}
|
|
1308
1154
|
|
|
@@ -1349,13 +1195,11 @@ class TwoHandChecker {
|
|
|
1349
1195
|
this.indexedHitObjects = [];
|
|
1350
1196
|
this.calculator = calculator;
|
|
1351
1197
|
this.data = data;
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
convertOverallDifficulty: false,
|
|
1356
|
-
}).overallDifficulty;
|
|
1357
|
-
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
1198
|
+
const greatWindow = new osuBase.OsuHitWindow(calculator.attributes.overallDifficulty).hitWindowFor300() * calculator.attributes.clockRate;
|
|
1199
|
+
const isPrecise = calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
1200
|
+
this.hitWindow = new osuBase.DroidHitWindow(osuBase.DroidHitWindow.hitWindow300ToOD(greatWindow, isPrecise));
|
|
1358
1201
|
this.hitWindow50 = this.hitWindow.hitWindowFor50(calculator.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
1202
|
+
this.isHardRock = calculator.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
1359
1203
|
// this.csvString = `Mods,${
|
|
1360
1204
|
// data.convertedMods.reduce((a, m) => a + m.acronym, "") || "NM"
|
|
1361
1205
|
// }\nCombo,${data.maxCombo}\nAccuracy,"${(
|
|
@@ -1576,13 +1420,15 @@ class TwoHandChecker {
|
|
|
1576
1420
|
for (let i = prevObjectInformation.occurrenceIndex + 1; i < cursors.length; ++i) {
|
|
1577
1421
|
const cursor = cursors[i];
|
|
1578
1422
|
const prevCursor = cursors[i - 1];
|
|
1579
|
-
|
|
1423
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
1424
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
1425
|
+
if (currentPosition.equals(prevPosition)) {
|
|
1580
1426
|
continue;
|
|
1581
1427
|
}
|
|
1582
1428
|
if (cursor.id === exports.MovementType.up) {
|
|
1583
1429
|
break;
|
|
1584
1430
|
}
|
|
1585
|
-
const currentMovement =
|
|
1431
|
+
const currentMovement = currentPosition.subtract(prevPosition);
|
|
1586
1432
|
const dot = prevToCurrentCursorMovement.dot(currentMovement);
|
|
1587
1433
|
const det = prevToCurrentCursorMovement.x * currentMovement.y -
|
|
1588
1434
|
prevToCurrentCursorMovement.y * currentMovement.x;
|
|
@@ -1668,7 +1514,7 @@ class TwoHandChecker {
|
|
|
1668
1514
|
};
|
|
1669
1515
|
}
|
|
1670
1516
|
let hitWindow = this.hitWindow50;
|
|
1671
|
-
const isPrecise = this.
|
|
1517
|
+
const isPrecise = this.calculator.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
1672
1518
|
// For sliders, set the hit window to as lenient as possible.
|
|
1673
1519
|
if (object instanceof osuBase.Circle) {
|
|
1674
1520
|
switch (data.result) {
|
|
@@ -1706,14 +1552,14 @@ class TwoHandChecker {
|
|
|
1706
1552
|
}
|
|
1707
1553
|
// Validate the down press first.
|
|
1708
1554
|
const { down } = cursorGroup;
|
|
1709
|
-
|
|
1710
|
-
|
|
1555
|
+
const downPosition = this.getCursorPosition(down);
|
|
1556
|
+
if (downPosition.getDistance(objectPosition) <= object.radius &&
|
|
1711
1557
|
Math.abs(down.time - object.startTime) <= hitWindow) {
|
|
1712
1558
|
if (objectIndex > 0) {
|
|
1713
1559
|
const prevObject = this.calculator.beatmap.hitObjects.objects[objectIndex - 1];
|
|
1714
1560
|
if (down.time > prevObject.endTime) {
|
|
1715
1561
|
return {
|
|
1716
|
-
position:
|
|
1562
|
+
position: downPosition,
|
|
1717
1563
|
cursorIndex: i,
|
|
1718
1564
|
groupIndex: j,
|
|
1719
1565
|
occurrenceIndex: 0,
|
|
@@ -1723,7 +1569,7 @@ class TwoHandChecker {
|
|
|
1723
1569
|
}
|
|
1724
1570
|
else {
|
|
1725
1571
|
return {
|
|
1726
|
-
position:
|
|
1572
|
+
position: downPosition,
|
|
1727
1573
|
cursorIndex: i,
|
|
1728
1574
|
groupIndex: j,
|
|
1729
1575
|
occurrenceIndex: 0,
|
|
@@ -1743,7 +1589,7 @@ class TwoHandChecker {
|
|
|
1743
1589
|
}
|
|
1744
1590
|
let cursorPosition;
|
|
1745
1591
|
if (cursor.id === exports.MovementType.up) {
|
|
1746
|
-
cursorPosition = prevCursor
|
|
1592
|
+
cursorPosition = this.getCursorPosition(prevCursor);
|
|
1747
1593
|
const distance = cursorPosition.getDistance(objectPosition);
|
|
1748
1594
|
if (distance <
|
|
1749
1595
|
nearestPosition.getDistance(objectPosition)) {
|
|
@@ -1768,7 +1614,7 @@ class TwoHandChecker {
|
|
|
1768
1614
|
}
|
|
1769
1615
|
const t = (cursorGroup.down.time - prevCursor.time) /
|
|
1770
1616
|
(cursor.time - prevCursor.time);
|
|
1771
|
-
cursorPosition = osuBase.Interpolation.lerp(prevCursor
|
|
1617
|
+
cursorPosition = osuBase.Interpolation.lerp(this.getCursorPosition(prevCursor), this.getCursorPosition(cursor), t);
|
|
1772
1618
|
const distance = cursorPosition.getDistance(objectPosition);
|
|
1773
1619
|
if (distance <
|
|
1774
1620
|
nearestPosition.getDistance(objectPosition)) {
|
|
@@ -1853,18 +1699,18 @@ class TwoHandChecker {
|
|
|
1853
1699
|
let cursorPosition;
|
|
1854
1700
|
switch (cursor.id) {
|
|
1855
1701
|
case exports.MovementType.down:
|
|
1856
|
-
cursorPosition = cursor
|
|
1702
|
+
cursorPosition = this.getCursorPosition(cursor);
|
|
1857
1703
|
break;
|
|
1858
1704
|
case exports.MovementType.up: {
|
|
1859
1705
|
const prevCursor = cursors[k - 1];
|
|
1860
|
-
cursorPosition = prevCursor
|
|
1706
|
+
cursorPosition = this.getCursorPosition(prevCursor);
|
|
1861
1707
|
break;
|
|
1862
1708
|
}
|
|
1863
1709
|
case exports.MovementType.move: {
|
|
1864
1710
|
const prevCursor = cursors[k - 1];
|
|
1865
1711
|
const t = osuBase.MathUtils.clamp((object.endTime - prevCursor.time) /
|
|
1866
1712
|
(cursor.time - prevCursor.time), 0, 1);
|
|
1867
|
-
cursorPosition = osuBase.Interpolation.lerp(prevCursor
|
|
1713
|
+
cursorPosition = osuBase.Interpolation.lerp(this.getCursorPosition(prevCursor), this.getCursorPosition(cursor), t);
|
|
1868
1714
|
break;
|
|
1869
1715
|
}
|
|
1870
1716
|
}
|
|
@@ -1926,6 +1772,12 @@ class TwoHandChecker {
|
|
|
1926
1772
|
}
|
|
1927
1773
|
return false;
|
|
1928
1774
|
}
|
|
1775
|
+
getCursorPosition(cursor) {
|
|
1776
|
+
if (this.isHardRock) {
|
|
1777
|
+
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
1778
|
+
}
|
|
1779
|
+
return cursor.position;
|
|
1780
|
+
}
|
|
1929
1781
|
}
|
|
1930
1782
|
|
|
1931
1783
|
/**
|
|
@@ -1941,12 +1793,8 @@ class SliderCheeseChecker {
|
|
|
1941
1793
|
this.beatmap = beatmap;
|
|
1942
1794
|
this.data = data;
|
|
1943
1795
|
this.difficultyAttributes = difficultyAttributes;
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
mods: osuBase.ModUtil.removeSpeedChangingMods(this.data.convertedMods),
|
|
1947
|
-
convertOverallDifficulty: false,
|
|
1948
|
-
}).overallDifficulty;
|
|
1949
|
-
this.hitWindow50 = new osuBase.DroidHitWindow(od).hitWindowFor50(this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
1796
|
+
this.hitWindow50 = new osuBase.DroidHitWindow(beatmap.difficulty.od).hitWindowFor50(difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
1797
|
+
this.isHardRock = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
1950
1798
|
}
|
|
1951
1799
|
/**
|
|
1952
1800
|
* Checks if relevant sliders in the given beatmap was cheesed.
|
|
@@ -1972,7 +1820,7 @@ class SliderCheeseChecker {
|
|
|
1972
1820
|
const { objects } = this.beatmap.hitObjects;
|
|
1973
1821
|
const cheesedDifficultyRatings = [];
|
|
1974
1822
|
// Current loop indices are stored for efficiency.
|
|
1975
|
-
const cursorLoopIndices = osuBase.Utils.initializeArray(
|
|
1823
|
+
const cursorLoopIndices = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
|
|
1976
1824
|
const acceptableRadius = objects[0].radius * 2;
|
|
1977
1825
|
// Sort difficult sliders by index so that cursor loop indices work properly.
|
|
1978
1826
|
for (const difficultSlider of this.difficultyAttributes.difficultSliders
|
|
@@ -2010,7 +1858,8 @@ class SliderCheeseChecker {
|
|
|
2010
1858
|
break;
|
|
2011
1859
|
}
|
|
2012
1860
|
if (group.startTime >= minTimeLimit) {
|
|
2013
|
-
const
|
|
1861
|
+
const position = this.getCursorPosition(group.down);
|
|
1862
|
+
const distance = position.getDistance(objectStartPosition);
|
|
2014
1863
|
if (closestDistance > distance) {
|
|
2015
1864
|
closestDistance = distance;
|
|
2016
1865
|
closestIndex = j;
|
|
@@ -2024,20 +1873,22 @@ class SliderCheeseChecker {
|
|
|
2024
1873
|
// for the time being we only check for cursor distances across the group.
|
|
2025
1874
|
const { allOccurrences } = group;
|
|
2026
1875
|
for (let k = 1; k < allOccurrences.length; ++k) {
|
|
2027
|
-
const
|
|
2028
|
-
const
|
|
1876
|
+
const cursor = allOccurrences[k];
|
|
1877
|
+
const prevCursor = allOccurrences[k - 1];
|
|
2029
1878
|
let distance = Number.POSITIVE_INFINITY;
|
|
2030
|
-
|
|
1879
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
1880
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
1881
|
+
switch (cursor.id) {
|
|
2031
1882
|
case exports.MovementType.up:
|
|
2032
1883
|
distance =
|
|
2033
|
-
|
|
1884
|
+
prevPosition.getDistance(objectStartPosition);
|
|
2034
1885
|
break;
|
|
2035
1886
|
case exports.MovementType.move:
|
|
2036
|
-
for (let mSecPassed = Math.max(
|
|
2037
|
-
Math.min(
|
|
2038
|
-
const t = (mSecPassed -
|
|
2039
|
-
(
|
|
2040
|
-
const cursorPosition =
|
|
1887
|
+
for (let mSecPassed = Math.max(prevCursor.time, minTimeLimit); mSecPassed <=
|
|
1888
|
+
Math.min(cursor.time, maxTimeLimit); ++mSecPassed) {
|
|
1889
|
+
const t = (mSecPassed - prevCursor.time) /
|
|
1890
|
+
(cursor.time - prevCursor.time);
|
|
1891
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
2041
1892
|
distance =
|
|
2042
1893
|
cursorPosition.getDistance(objectStartPosition);
|
|
2043
1894
|
if (closestDistance > distance) {
|
|
@@ -2095,21 +1946,24 @@ class SliderCheeseChecker {
|
|
|
2095
1946
|
if (occurrenceLoopIndex === allOccurrences.length) {
|
|
2096
1947
|
continue;
|
|
2097
1948
|
}
|
|
2098
|
-
const
|
|
2099
|
-
const
|
|
2100
|
-
|
|
1949
|
+
const cursor = allOccurrences[occurrenceLoopIndex];
|
|
1950
|
+
const prevCursor = allOccurrences[occurrenceLoopIndex - 1];
|
|
1951
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
1952
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
1953
|
+
switch (cursor.id) {
|
|
2101
1954
|
case exports.MovementType.move: {
|
|
2102
1955
|
// Interpolate cursor position during nested object time.
|
|
2103
|
-
const t = (nestedObject.startTime -
|
|
2104
|
-
(
|
|
2105
|
-
const cursorPosition =
|
|
1956
|
+
const t = (nestedObject.startTime - prevCursor.time) /
|
|
1957
|
+
(cursor.time - prevCursor.time);
|
|
1958
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
2106
1959
|
const distance = cursorPosition.getDistance(nestedPosition);
|
|
2107
1960
|
isCheesed = distance > acceptableRadius;
|
|
2108
1961
|
break;
|
|
2109
1962
|
}
|
|
2110
1963
|
case exports.MovementType.up:
|
|
2111
1964
|
isCheesed =
|
|
2112
|
-
|
|
1965
|
+
prevPosition.getDistance(nestedPosition) >
|
|
1966
|
+
acceptableRadius;
|
|
2113
1967
|
}
|
|
2114
1968
|
}
|
|
2115
1969
|
if (isCheesed) {
|
|
@@ -2135,6 +1989,65 @@ class SliderCheeseChecker {
|
|
|
2135
1989
|
this.difficultyAttributes.visualSliderFactor, 2)),
|
|
2136
1990
|
};
|
|
2137
1991
|
}
|
|
1992
|
+
getCursorPosition(cursor) {
|
|
1993
|
+
if (this.isHardRock) {
|
|
1994
|
+
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
1995
|
+
}
|
|
1996
|
+
return cursor.position;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
/**
|
|
2001
|
+
* Represents a replay data for replay version 3.
|
|
2002
|
+
*
|
|
2003
|
+
* Stores generic information about an osu!droid replay.
|
|
2004
|
+
*
|
|
2005
|
+
* This is used when analyzing replays using replay analyzer.
|
|
2006
|
+
*/
|
|
2007
|
+
class ReplayV3Data extends ReplayData {
|
|
2008
|
+
constructor(values) {
|
|
2009
|
+
super(values);
|
|
2010
|
+
this.time = values.time;
|
|
2011
|
+
this.score = values.score;
|
|
2012
|
+
this.maxCombo = values.maxCombo;
|
|
2013
|
+
this.isFullCombo = values.isFullCombo;
|
|
2014
|
+
this.playerName = values.playerName;
|
|
2015
|
+
this.rawMods = values.rawMods;
|
|
2016
|
+
this.convertedMods = values.convertedMods;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/**
|
|
2021
|
+
* Represents a replay data for replay version 4.
|
|
2022
|
+
*
|
|
2023
|
+
* Stores generic information about an osu!droid replay.
|
|
2024
|
+
*
|
|
2025
|
+
* This is used when analyzing replays using replay analyzer.
|
|
2026
|
+
*/
|
|
2027
|
+
class ReplayV4Data extends ReplayV3Data {
|
|
2028
|
+
constructor(values) {
|
|
2029
|
+
var _a, _b;
|
|
2030
|
+
super(values);
|
|
2031
|
+
this.forceAR = values.forceAR;
|
|
2032
|
+
this.speedMultiplier = (_a = values.speedMultiplier) !== null && _a !== void 0 ? _a : 1;
|
|
2033
|
+
this.flashlightFollowDelay = (_b = values.flashlightFollowDelay) !== null && _b !== void 0 ? _b : 0.12;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* Represents a replay data for replay version 5.
|
|
2039
|
+
*
|
|
2040
|
+
* Stores generic information about an osu!droid replay.
|
|
2041
|
+
*
|
|
2042
|
+
* This is used when analyzing replays using replay analyzer.
|
|
2043
|
+
*/
|
|
2044
|
+
class ReplayV5Data extends ReplayV4Data {
|
|
2045
|
+
constructor(values) {
|
|
2046
|
+
super(values);
|
|
2047
|
+
this.forceCS = values.forceCS;
|
|
2048
|
+
this.forceOD = values.forceOD;
|
|
2049
|
+
this.forceHP = values.forceHP;
|
|
2050
|
+
}
|
|
2138
2051
|
}
|
|
2139
2052
|
|
|
2140
2053
|
/**
|
|
@@ -2190,6 +2103,7 @@ class ReplayAnalyzer {
|
|
|
2190
2103
|
* The amount of two-handed objects.
|
|
2191
2104
|
*/
|
|
2192
2105
|
this.twoHandedNoteCount = 0;
|
|
2106
|
+
this.bufferOffset = 0;
|
|
2193
2107
|
this.scoreID = values.scoreID;
|
|
2194
2108
|
this.beatmap = values.map;
|
|
2195
2109
|
this.difficultyAttributes = values.difficultyAttributes;
|
|
@@ -2218,6 +2132,117 @@ class ReplayAnalyzer {
|
|
|
2218
2132
|
return this;
|
|
2219
2133
|
});
|
|
2220
2134
|
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Gets hit error information of the replay.
|
|
2137
|
+
*
|
|
2138
|
+
* `analyze()` must be called before calling this.
|
|
2139
|
+
*/
|
|
2140
|
+
calculateHitError() {
|
|
2141
|
+
var _a, _b;
|
|
2142
|
+
if (!this.data || !this.beatmap) {
|
|
2143
|
+
return null;
|
|
2144
|
+
}
|
|
2145
|
+
const hitObjectData = this.data.hitObjectData;
|
|
2146
|
+
let positiveCount = 0;
|
|
2147
|
+
let negativeCount = 0;
|
|
2148
|
+
let positiveTotal = 0;
|
|
2149
|
+
let negativeTotal = 0;
|
|
2150
|
+
const beatmap = this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2151
|
+
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
|
|
2152
|
+
? this.beatmap.beatmap
|
|
2153
|
+
: this.beatmap;
|
|
2154
|
+
const { objects } = beatmap.hitObjects;
|
|
2155
|
+
const mods = this.data.isReplayV3()
|
|
2156
|
+
? this.data.convertedMods
|
|
2157
|
+
: (_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods.filter((m) => m.isApplicableToDroid())) !== null && _b !== void 0 ? _b : [];
|
|
2158
|
+
const adjustedDifficulty = new osuBase.BeatmapDifficulty(beatmap.difficulty);
|
|
2159
|
+
osuBase.ModUtil.applyModsToBeatmapDifficulty(adjustedDifficulty, osuBase.Modes.droid, mods, this.data.isReplayV4() ? this.data.speedMultiplier : 1);
|
|
2160
|
+
const hitWindow50 = new osuBase.DroidHitWindow(adjustedDifficulty.od).hitWindowFor50(mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
2161
|
+
// The accuracy of sliders is set to (50 hit window)ms + 13ms if their head was not hit:
|
|
2162
|
+
// https://github.com/osudroid/osu-droid/blob/6306c68e3ffaf671eac794bf45cc95c0f3313a82/src/ru/nsu/ccfit/zuev/osu/game/Slider.java#L821
|
|
2163
|
+
//
|
|
2164
|
+
// In such cases, the slider is skipped.
|
|
2165
|
+
const sliderbreakHitOffset = Math.floor(hitWindow50) + 13;
|
|
2166
|
+
const accuracies = [];
|
|
2167
|
+
for (let i = 0; i < hitObjectData.length; ++i) {
|
|
2168
|
+
const v = hitObjectData[i];
|
|
2169
|
+
const o = objects[i];
|
|
2170
|
+
if (o instanceof osuBase.Spinner || v.result === exports.HitResult.miss) {
|
|
2171
|
+
accuracies.push(0);
|
|
2172
|
+
continue;
|
|
2173
|
+
}
|
|
2174
|
+
const { accuracy } = v;
|
|
2175
|
+
if (o instanceof osuBase.Slider && accuracy === sliderbreakHitOffset) {
|
|
2176
|
+
accuracies.push(0);
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
accuracies.push(accuracy);
|
|
2180
|
+
if (accuracy >= 0) {
|
|
2181
|
+
positiveTotal += accuracy;
|
|
2182
|
+
++positiveCount;
|
|
2183
|
+
}
|
|
2184
|
+
else {
|
|
2185
|
+
negativeTotal += accuracy;
|
|
2186
|
+
++negativeCount;
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
return {
|
|
2190
|
+
positiveAvg: positiveTotal / positiveCount || 0,
|
|
2191
|
+
negativeAvg: negativeTotal / negativeCount || 0,
|
|
2192
|
+
unstableRate: osuBase.MathUtils.calculateStandardDeviation(accuracies) * 10,
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* Checks if a play is using 3 fingers.
|
|
2197
|
+
*
|
|
2198
|
+
* Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
|
|
2199
|
+
*/
|
|
2200
|
+
checkFor3Finger() {
|
|
2201
|
+
var _a;
|
|
2202
|
+
if (!this.beatmap || !this.data || !this.difficultyAttributes) {
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
(_a = this.playableBeatmap) !== null && _a !== void 0 ? _a : (this.playableBeatmap = this.constructPlayableBeatmap());
|
|
2206
|
+
const threeFingerChecker = this.difficultyAttributes.mode === "rebalance"
|
|
2207
|
+
? new RebalanceThreeFingerChecker(this.playableBeatmap, this.data, this.difficultyAttributes)
|
|
2208
|
+
: new ThreeFingerChecker(this.playableBeatmap, this.data, this.difficultyAttributes);
|
|
2209
|
+
const result = threeFingerChecker.check();
|
|
2210
|
+
this.is3Finger = result.is3Finger;
|
|
2211
|
+
this.tapPenalty = result.penalty;
|
|
2212
|
+
this.hasBeenCheckedFor3Finger = true;
|
|
2213
|
+
}
|
|
2214
|
+
/**
|
|
2215
|
+
* Checks if a play is using 2 hands.
|
|
2216
|
+
*
|
|
2217
|
+
* Requires `analyze()` to be called first and `map` to be defined as `DroidDifficultyCalculator`.
|
|
2218
|
+
*/
|
|
2219
|
+
checkFor2Hand() {
|
|
2220
|
+
if (!(this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2221
|
+
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator) ||
|
|
2222
|
+
!this.data) {
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
const twoHandChecker = new TwoHandChecker(this.beatmap, this.data);
|
|
2226
|
+
const result = twoHandChecker.check();
|
|
2227
|
+
this.is2Hand = result.is2Hand;
|
|
2228
|
+
this.twoHandedNoteCount = result.twoHandedNoteCount;
|
|
2229
|
+
this.hasBeenCheckedFor2Hand = true;
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* Checks if a play has cheesed sliders.
|
|
2233
|
+
*
|
|
2234
|
+
* Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
|
|
2235
|
+
*/
|
|
2236
|
+
checkForSliderCheesing() {
|
|
2237
|
+
var _a;
|
|
2238
|
+
if (!this.beatmap || !this.data || !this.difficultyAttributes) {
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
(_a = this.playableBeatmap) !== null && _a !== void 0 ? _a : (this.playableBeatmap = this.constructPlayableBeatmap());
|
|
2242
|
+
const sliderCheeseChecker = new SliderCheeseChecker(this.playableBeatmap, this.data, this.difficultyAttributes);
|
|
2243
|
+
this.sliderCheesePenalty = sliderCheeseChecker.check();
|
|
2244
|
+
this.hasBeenCheckedForSliderCheesing = true;
|
|
2245
|
+
}
|
|
2221
2246
|
/**
|
|
2222
2247
|
* Downloads the given score ID's replay.
|
|
2223
2248
|
*/
|
|
@@ -2264,7 +2289,6 @@ class ReplayAnalyzer {
|
|
|
2264
2289
|
* Parses a replay after being downloaded and converted to a buffer.
|
|
2265
2290
|
*/
|
|
2266
2291
|
parseReplay() {
|
|
2267
|
-
var _a, _b, _c;
|
|
2268
2292
|
if (!this.fixedODR) {
|
|
2269
2293
|
return;
|
|
2270
2294
|
}
|
|
@@ -2275,7 +2299,7 @@ class ReplayAnalyzer {
|
|
|
2275
2299
|
try {
|
|
2276
2300
|
rawObject = javaDeserialization__namespace.parse(this.fixedODR);
|
|
2277
2301
|
}
|
|
2278
|
-
catch (
|
|
2302
|
+
catch (_a) {
|
|
2279
2303
|
return;
|
|
2280
2304
|
}
|
|
2281
2305
|
const resultObject = {
|
|
@@ -2286,67 +2310,32 @@ class ReplayAnalyzer {
|
|
|
2286
2310
|
cursorMovement: [],
|
|
2287
2311
|
hitObjectData: [],
|
|
2288
2312
|
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
|
-
}
|
|
2313
|
+
rank: "D",
|
|
2314
|
+
convertedMods: [],
|
|
2315
|
+
hit100k: 0,
|
|
2316
|
+
hit300k: 0,
|
|
2317
|
+
isFullCombo: false,
|
|
2318
|
+
maxCombo: 0,
|
|
2319
|
+
playerName: "",
|
|
2320
|
+
rawMods: [],
|
|
2321
|
+
score: 0,
|
|
2322
|
+
time: new Date(0),
|
|
2332
2323
|
};
|
|
2333
2324
|
if (resultObject.replayVersion >= 3) {
|
|
2334
|
-
resultObject.time
|
|
2325
|
+
resultObject.time.setTime(Number(rawObject[4].readBigUInt64BE(0)));
|
|
2335
2326
|
resultObject.hit300k = rawObject[4].readInt32BE(8);
|
|
2327
|
+
resultObject.accuracy.n300 = rawObject[4].readInt32BE(12);
|
|
2336
2328
|
resultObject.hit100k = rawObject[4].readInt32BE(16);
|
|
2329
|
+
resultObject.accuracy.n100 = rawObject[4].readInt32BE(20);
|
|
2330
|
+
resultObject.accuracy.n50 = rawObject[4].readInt32BE(24);
|
|
2331
|
+
resultObject.accuracy.nmiss = rawObject[4].readInt32BE(28);
|
|
2337
2332
|
resultObject.score = rawObject[4].readInt32BE(32);
|
|
2338
2333
|
resultObject.maxCombo = rawObject[4].readInt32BE(36);
|
|
2339
|
-
resultObject.accuracy = new osuBase.Accuracy({
|
|
2340
|
-
n300: rawObject[4].readInt32BE(12),
|
|
2341
|
-
n100: rawObject[4].readInt32BE(20),
|
|
2342
|
-
n50: rawObject[4].readInt32BE(24),
|
|
2343
|
-
nmiss: rawObject[4].readInt32BE(28),
|
|
2344
|
-
});
|
|
2345
2334
|
resultObject.isFullCombo = !!rawObject[4][44];
|
|
2346
2335
|
resultObject.playerName = rawObject[5];
|
|
2347
|
-
resultObject.rawMods = rawObject[6].elements;
|
|
2348
|
-
resultObject.convertedMods = osuBase.ModUtil.droidStringToMods(this.convertDroidMods(
|
|
2349
|
-
|
|
2336
|
+
resultObject.rawMods = Object.values(rawObject[6].elements);
|
|
2337
|
+
resultObject.convertedMods = osuBase.ModUtil.droidStringToMods(this.convertDroidMods(resultObject.rawMods));
|
|
2338
|
+
resultObject.rank = this.calculateRank(resultObject);
|
|
2350
2339
|
}
|
|
2351
2340
|
if (resultObject.replayVersion >= 4) {
|
|
2352
2341
|
const str = rawObject[7].split("|");
|
|
@@ -2398,32 +2387,87 @@ class ReplayAnalyzer {
|
|
|
2398
2387
|
}
|
|
2399
2388
|
// Merge all cursor movement and hit object data section into one for better control when parsing
|
|
2400
2389
|
const replayDataBuffer = Buffer.concat(replayDataBufferArray);
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2390
|
+
this.bufferOffset = 0;
|
|
2391
|
+
this.parseMovementData(resultObject, replayDataBuffer);
|
|
2392
|
+
this.parseHitObjectData(resultObject, replayDataBuffer);
|
|
2393
|
+
this.parseOldReplayInformation(resultObject);
|
|
2394
|
+
switch (resultObject.replayVersion) {
|
|
2395
|
+
case 3:
|
|
2396
|
+
this.data = new ReplayV3Data(resultObject);
|
|
2397
|
+
break;
|
|
2398
|
+
case 4:
|
|
2399
|
+
this.data = new ReplayV4Data(resultObject);
|
|
2400
|
+
break;
|
|
2401
|
+
case 5:
|
|
2402
|
+
this.data = new ReplayV5Data(resultObject);
|
|
2403
|
+
break;
|
|
2404
|
+
default:
|
|
2405
|
+
this.data = new ReplayData(resultObject);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Converts replay mods to droid mod string.
|
|
2410
|
+
*/
|
|
2411
|
+
convertDroidMods(replayMods) {
|
|
2412
|
+
const replayModsConstants = {
|
|
2413
|
+
MOD_AUTO: new osuBase.ModAuto().droidString,
|
|
2414
|
+
MOD_AUTOPILOT: new osuBase.ModAutopilot().droidString,
|
|
2415
|
+
MOD_NOFAIL: new osuBase.ModNoFail().droidString,
|
|
2416
|
+
MOD_EASY: new osuBase.ModEasy().droidString,
|
|
2417
|
+
MOD_HIDDEN: new osuBase.ModHidden().droidString,
|
|
2418
|
+
MOD_HARDROCK: new osuBase.ModHardRock().droidString,
|
|
2419
|
+
MOD_DOUBLETIME: new osuBase.ModDoubleTime().droidString,
|
|
2420
|
+
MOD_HALFTIME: new osuBase.ModHalfTime().droidString,
|
|
2421
|
+
MOD_NIGHTCORE: new osuBase.ModNightCore().droidString,
|
|
2422
|
+
MOD_PRECISE: new osuBase.ModPrecise().droidString,
|
|
2423
|
+
MOD_SMALLCIRCLE: new osuBase.ModSmallCircle().droidString,
|
|
2424
|
+
MOD_SPEEDUP: new osuBase.ModSpeedUp().droidString,
|
|
2425
|
+
MOD_REALLYEASY: new osuBase.ModReallyEasy().droidString,
|
|
2426
|
+
MOD_RELAX: new osuBase.ModRelax().droidString,
|
|
2427
|
+
MOD_PERFECT: new osuBase.ModPerfect().droidString,
|
|
2428
|
+
MOD_SUDDENDEATH: new osuBase.ModSuddenDeath().droidString,
|
|
2429
|
+
MOD_SCOREV2: new osuBase.ModScoreV2().droidString,
|
|
2430
|
+
MOD_FLASHLIGHT: new osuBase.ModFlashlight().droidString,
|
|
2431
|
+
};
|
|
2432
|
+
let modString = "";
|
|
2433
|
+
for (const mod of replayMods) {
|
|
2434
|
+
for (const property in replayModsConstants) {
|
|
2435
|
+
if (!mod.includes(property)) {
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
modString +=
|
|
2439
|
+
replayModsConstants[property];
|
|
2440
|
+
break;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return modString;
|
|
2444
|
+
}
|
|
2445
|
+
parseMovementData(resultObject, replayDataBuffer) {
|
|
2446
|
+
resultObject.cursorMovement.length = 0;
|
|
2447
|
+
const size = this.readInt(replayDataBuffer);
|
|
2448
|
+
for (let i = 0; i < size; i++) {
|
|
2449
|
+
const moveSize = this.readInt(replayDataBuffer);
|
|
2406
2450
|
const time = [];
|
|
2407
2451
|
const x = [];
|
|
2408
2452
|
const y = [];
|
|
2409
2453
|
const id = [];
|
|
2410
|
-
for (let
|
|
2411
|
-
time[
|
|
2412
|
-
id[
|
|
2413
|
-
time[
|
|
2414
|
-
if (id[
|
|
2454
|
+
for (let j = 0; j < moveSize; j++) {
|
|
2455
|
+
time[j] = this.readInt(replayDataBuffer);
|
|
2456
|
+
id[j] = time[j] & 3;
|
|
2457
|
+
time[j] >>= 2;
|
|
2458
|
+
if (id[j] !== exports.MovementType.up) {
|
|
2415
2459
|
if (resultObject.replayVersion >= 5) {
|
|
2416
|
-
x[
|
|
2417
|
-
y[
|
|
2460
|
+
x[j] = this.readFloat(replayDataBuffer);
|
|
2461
|
+
y[j] = this.readFloat(replayDataBuffer);
|
|
2418
2462
|
}
|
|
2419
2463
|
else {
|
|
2420
|
-
x[
|
|
2421
|
-
y[
|
|
2464
|
+
x[j] = this.readShort(replayDataBuffer);
|
|
2465
|
+
y[j] = this.readShort(replayDataBuffer);
|
|
2422
2466
|
}
|
|
2423
2467
|
}
|
|
2424
2468
|
else {
|
|
2425
|
-
x[
|
|
2426
|
-
y[
|
|
2469
|
+
x[j] = -1;
|
|
2470
|
+
y[j] = -1;
|
|
2427
2471
|
}
|
|
2428
2472
|
}
|
|
2429
2473
|
resultObject.cursorMovement.push(new CursorData({
|
|
@@ -2434,7 +2478,10 @@ class ReplayAnalyzer {
|
|
|
2434
2478
|
id: id,
|
|
2435
2479
|
}));
|
|
2436
2480
|
}
|
|
2437
|
-
|
|
2481
|
+
}
|
|
2482
|
+
parseHitObjectData(resultObject, replayDataBuffer) {
|
|
2483
|
+
resultObject.hitObjectData.length = 0;
|
|
2484
|
+
const replayObjectLength = this.readInt(replayDataBuffer);
|
|
2438
2485
|
// Parse result data
|
|
2439
2486
|
for (let i = 0; i < replayObjectLength; i++) {
|
|
2440
2487
|
const replayObjectData = {
|
|
@@ -2442,12 +2489,12 @@ class ReplayAnalyzer {
|
|
|
2442
2489
|
tickset: [],
|
|
2443
2490
|
result: exports.HitResult.miss,
|
|
2444
2491
|
};
|
|
2445
|
-
replayObjectData.accuracy = this.readShort(replayDataBuffer
|
|
2446
|
-
const len = this.readByte(replayDataBuffer
|
|
2492
|
+
replayObjectData.accuracy = this.readShort(replayDataBuffer);
|
|
2493
|
+
const len = this.readByte(replayDataBuffer);
|
|
2447
2494
|
if (len > 0) {
|
|
2448
2495
|
const bytes = [];
|
|
2449
2496
|
for (let j = 0; j < len; j++) {
|
|
2450
|
-
bytes.push(this.readByte(replayDataBuffer
|
|
2497
|
+
bytes.push(this.readByte(replayDataBuffer));
|
|
2451
2498
|
}
|
|
2452
2499
|
// Int/int division in Java; numbers must be truncated to get actual number
|
|
2453
2500
|
for (let j = 0; j < len * 8; j++) {
|
|
@@ -2457,265 +2504,137 @@ class ReplayAnalyzer {
|
|
|
2457
2504
|
}
|
|
2458
2505
|
}
|
|
2459
2506
|
if (resultObject.replayVersion >= 1) {
|
|
2460
|
-
replayObjectData.result = this.readByte(replayDataBuffer
|
|
2507
|
+
replayObjectData.result = this.readByte(replayDataBuffer);
|
|
2461
2508
|
}
|
|
2462
2509
|
resultObject.hitObjectData.push(replayObjectData);
|
|
2463
2510
|
}
|
|
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
2511
|
}
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
calculateHitError() {
|
|
2517
|
-
if (!this.data || !this.beatmap) {
|
|
2518
|
-
return null;
|
|
2512
|
+
parseOldReplayInformation(resultObject) {
|
|
2513
|
+
var _a, _b, _c;
|
|
2514
|
+
// Parse max combo, hit results, and accuracy in old replay version
|
|
2515
|
+
if (resultObject.replayVersion >= 3) {
|
|
2516
|
+
return;
|
|
2519
2517
|
}
|
|
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 ||
|
|
2518
|
+
const objects = (_a = (this.beatmap instanceof osuDifficultyCalculator.DroidDifficultyCalculator ||
|
|
2526
2519
|
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
|
|
2527
2520
|
? 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;
|
|
2521
|
+
: this.beatmap)) === null || _a === void 0 ? void 0 : _a.hitObjects.objects;
|
|
2522
|
+
let grantsGekiOrKatu = true;
|
|
2523
|
+
for (let i = 0; i < resultObject.hitObjectData.length; ++i) {
|
|
2524
|
+
// Hit result
|
|
2525
|
+
const hitObjectData = resultObject.hitObjectData[i];
|
|
2526
|
+
const isNextNewCombo = objects
|
|
2527
|
+
? i + 1 !== objects.length
|
|
2528
|
+
? objects[i + 1].isNewCombo
|
|
2529
|
+
: true
|
|
2530
|
+
: false;
|
|
2531
|
+
switch (hitObjectData.result) {
|
|
2532
|
+
case exports.HitResult.miss:
|
|
2533
|
+
++resultObject.accuracy.nmiss;
|
|
2534
|
+
grantsGekiOrKatu = false;
|
|
2535
|
+
break;
|
|
2536
|
+
case exports.HitResult.meh:
|
|
2537
|
+
++resultObject.accuracy.n50;
|
|
2538
|
+
grantsGekiOrKatu = false;
|
|
2539
|
+
break;
|
|
2540
|
+
case exports.HitResult.good:
|
|
2541
|
+
++resultObject.accuracy.n100;
|
|
2542
|
+
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
2543
|
+
(_b = resultObject.hit100k) !== null && _b !== void 0 ? _b : (resultObject.hit100k = 0);
|
|
2544
|
+
++resultObject.hit100k;
|
|
2545
|
+
}
|
|
2546
|
+
break;
|
|
2547
|
+
case exports.HitResult.great:
|
|
2548
|
+
++resultObject.accuracy.n300;
|
|
2549
|
+
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
2550
|
+
(_c = resultObject.hit300k) !== null && _c !== void 0 ? _c : (resultObject.hit300k = 0);
|
|
2551
|
+
++resultObject.hit300k;
|
|
2552
|
+
}
|
|
2553
|
+
break;
|
|
2602
2554
|
}
|
|
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
|
-
}));
|
|
2555
|
+
if (isNextNewCombo) {
|
|
2556
|
+
grantsGekiOrKatu = true;
|
|
2630
2557
|
}
|
|
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
2558
|
}
|
|
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;
|
|
2559
|
+
resultObject.rank = this.calculateRank(resultObject);
|
|
2646
2560
|
}
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2561
|
+
calculateRank(resultObject) {
|
|
2562
|
+
var _a, _b;
|
|
2563
|
+
const totalHits = resultObject.accuracy.n300 +
|
|
2564
|
+
resultObject.accuracy.n100 +
|
|
2565
|
+
resultObject.accuracy.n50 +
|
|
2566
|
+
resultObject.accuracy.nmiss;
|
|
2567
|
+
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;
|
|
2568
|
+
const hit300Ratio = resultObject.accuracy.n300 / totalHits;
|
|
2569
|
+
switch (true) {
|
|
2570
|
+
case resultObject.accuracy.value() === 1:
|
|
2571
|
+
return isHidden ? "XH" : "X";
|
|
2572
|
+
case hit300Ratio > 0.9 &&
|
|
2573
|
+
resultObject.accuracy.n50 / totalHits < 0.01 &&
|
|
2574
|
+
!resultObject.accuracy.nmiss:
|
|
2575
|
+
return isHidden ? "SH" : "S";
|
|
2576
|
+
case (hit300Ratio > 0.8 && !resultObject.accuracy.nmiss) ||
|
|
2577
|
+
hit300Ratio > 0.9:
|
|
2578
|
+
return "A";
|
|
2579
|
+
case (hit300Ratio > 0.7 && !resultObject.accuracy.nmiss) ||
|
|
2580
|
+
hit300Ratio > 0.8:
|
|
2581
|
+
return "B";
|
|
2582
|
+
case hit300Ratio > 0.6:
|
|
2583
|
+
return "C";
|
|
2584
|
+
default:
|
|
2585
|
+
return "D";
|
|
2657
2586
|
}
|
|
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
2587
|
}
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
if (!this.convertedBeatmap) {
|
|
2675
|
-
const mods = this.data.convertedMods.slice();
|
|
2676
|
-
if ([
|
|
2588
|
+
constructPlayableBeatmap() {
|
|
2589
|
+
var _a, _b;
|
|
2590
|
+
if (!this.beatmap || !this.data) {
|
|
2591
|
+
throw new Error("Beatmap and replay data must be defined.");
|
|
2592
|
+
}
|
|
2593
|
+
const mods = this.data.isReplayV3()
|
|
2594
|
+
? this.data.convertedMods.slice()
|
|
2595
|
+
: (_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods.slice()) !== null && _b !== void 0 ? _b : [];
|
|
2596
|
+
if (this.data.isReplayV5() &&
|
|
2597
|
+
[
|
|
2677
2598
|
this.data.forceCS,
|
|
2678
2599
|
this.data.forceAR,
|
|
2679
2600
|
this.data.forceOD,
|
|
2680
2601
|
this.data.forceHP,
|
|
2681
2602
|
].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,
|
|
2603
|
+
mods.push(new osuBase.ModDifficultyAdjust({
|
|
2604
|
+
cs: this.data.forceCS,
|
|
2605
|
+
ar: this.data.forceAR,
|
|
2606
|
+
od: this.data.forceOD,
|
|
2607
|
+
hp: this.data.forceHP,
|
|
2695
2608
|
}));
|
|
2696
2609
|
}
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2610
|
+
return (this.beatmap instanceof osuBase.Beatmap
|
|
2611
|
+
? this.beatmap
|
|
2612
|
+
: this.beatmap.beatmap).createPlayableBeatmap({
|
|
2613
|
+
mode: osuBase.Modes.droid,
|
|
2614
|
+
mods: mods,
|
|
2615
|
+
customSpeedMultiplier: this.data.isReplayV4()
|
|
2616
|
+
? this.data.speedMultiplier
|
|
2617
|
+
: 1,
|
|
2618
|
+
});
|
|
2700
2619
|
}
|
|
2701
|
-
readByte(buffer
|
|
2702
|
-
const num = buffer.readInt8(
|
|
2703
|
-
|
|
2620
|
+
readByte(buffer) {
|
|
2621
|
+
const num = buffer.readInt8(this.bufferOffset);
|
|
2622
|
+
this.bufferOffset += 1;
|
|
2704
2623
|
return num;
|
|
2705
2624
|
}
|
|
2706
|
-
readShort(buffer
|
|
2707
|
-
const num = buffer.readInt16BE(
|
|
2708
|
-
|
|
2625
|
+
readShort(buffer) {
|
|
2626
|
+
const num = buffer.readInt16BE(this.bufferOffset);
|
|
2627
|
+
this.bufferOffset += 2;
|
|
2709
2628
|
return num;
|
|
2710
2629
|
}
|
|
2711
|
-
readInt(buffer
|
|
2712
|
-
const num = buffer.readInt32BE(
|
|
2713
|
-
|
|
2630
|
+
readInt(buffer) {
|
|
2631
|
+
const num = buffer.readInt32BE(this.bufferOffset);
|
|
2632
|
+
this.bufferOffset += 4;
|
|
2714
2633
|
return num;
|
|
2715
2634
|
}
|
|
2716
|
-
readFloat(buffer
|
|
2717
|
-
const num = buffer.readFloatBE(
|
|
2718
|
-
|
|
2635
|
+
readFloat(buffer) {
|
|
2636
|
+
const num = buffer.readFloatBE(this.bufferOffset);
|
|
2637
|
+
this.bufferOffset += 4;
|
|
2719
2638
|
return num;
|
|
2720
2639
|
}
|
|
2721
2640
|
}
|
|
@@ -2742,6 +2661,9 @@ exports.RebalanceThreeFingerChecker = RebalanceThreeFingerChecker;
|
|
|
2742
2661
|
exports.ReplayAnalyzer = ReplayAnalyzer;
|
|
2743
2662
|
exports.ReplayData = ReplayData;
|
|
2744
2663
|
exports.ReplayObjectData = ReplayObjectData;
|
|
2664
|
+
exports.ReplayV3Data = ReplayV3Data;
|
|
2665
|
+
exports.ReplayV4Data = ReplayV4Data;
|
|
2666
|
+
exports.ReplayV5Data = ReplayV5Data;
|
|
2745
2667
|
exports.SliderCheeseChecker = SliderCheeseChecker;
|
|
2746
2668
|
exports.ThreeFingerChecker = ThreeFingerChecker;
|
|
2747
2669
|
exports.TwoHandChecker = TwoHandChecker;
|