@rian8337/osu-droid-replay-analyzer 4.0.0-beta.15 → 4.0.0-beta.17
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 +668 -82
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/typings/index.d.ts +176 -29
package/dist/index.js
CHANGED
|
@@ -312,6 +312,574 @@ exports.HitResult = void 0;
|
|
|
312
312
|
HitResult[HitResult["great"] = 4] = "great";
|
|
313
313
|
})(exports.HitResult || (exports.HitResult = {}));
|
|
314
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Utility to check whether or not a beatmap is three-fingered for rebalance scores.
|
|
317
|
+
*/
|
|
318
|
+
class RebalanceThreeFingerChecker {
|
|
319
|
+
/**
|
|
320
|
+
* The beatmap that is being analyzed.
|
|
321
|
+
*/
|
|
322
|
+
beatmap;
|
|
323
|
+
/**
|
|
324
|
+
* The data of the replay.
|
|
325
|
+
*/
|
|
326
|
+
data;
|
|
327
|
+
/**
|
|
328
|
+
* The difficulty attributes of the beatmap.
|
|
329
|
+
*/
|
|
330
|
+
difficultyAttributes;
|
|
331
|
+
/**
|
|
332
|
+
* The hitobjects to be analyzed.
|
|
333
|
+
*
|
|
334
|
+
* This is being maintained separately due to possible change in object scale.
|
|
335
|
+
*/
|
|
336
|
+
hitObjects;
|
|
337
|
+
/**
|
|
338
|
+
* The distance threshold between cursors to assume that two cursors are
|
|
339
|
+
* actually pressed with 1 finger in osu!pixels.
|
|
340
|
+
*
|
|
341
|
+
* This is used to prevent cases where a player would lift their finger
|
|
342
|
+
* too fast to the point where the 4th cursor instance or beyond is recorded
|
|
343
|
+
* as 1st, 2nd, or 3rd cursor instance.
|
|
344
|
+
*/
|
|
345
|
+
cursorDistancingDistanceThreshold = 60;
|
|
346
|
+
/**
|
|
347
|
+
* The threshold for the amount of cursors that are assumed to be pressed
|
|
348
|
+
* by a single finger.
|
|
349
|
+
*/
|
|
350
|
+
cursorDistancingCountThreshold = 10;
|
|
351
|
+
/**
|
|
352
|
+
* The threshold for the time difference of cursors that are assumed to be pressed
|
|
353
|
+
* by a single finger, in milliseconds.
|
|
354
|
+
*/
|
|
355
|
+
cursorDistancingTimeThreshold = 1000;
|
|
356
|
+
/**
|
|
357
|
+
* The amount of notes that has a tap strain exceeding `strainThreshold`.
|
|
358
|
+
*/
|
|
359
|
+
strainNoteCount;
|
|
360
|
+
/**
|
|
361
|
+
* The ratio threshold between non-3 finger cursors and 3-finger cursors.
|
|
362
|
+
*
|
|
363
|
+
* Increasing this number will increase detection accuracy, however
|
|
364
|
+
* it also increases the chance of falsely flagged plays.
|
|
365
|
+
*/
|
|
366
|
+
threeFingerRatioThreshold = 0.01;
|
|
367
|
+
/**
|
|
368
|
+
* Extended sections of the beatmap for drag detection.
|
|
369
|
+
*/
|
|
370
|
+
beatmapSections = [];
|
|
371
|
+
/**
|
|
372
|
+
* The hit window of this beatmap. Keep in mind that speed-changing mods do not change hit window length in game logic.
|
|
373
|
+
*/
|
|
374
|
+
hitWindow;
|
|
375
|
+
/**
|
|
376
|
+
* A reprocessed break points to match right on object time.
|
|
377
|
+
*
|
|
378
|
+
* This is used to increase detection accuracy since break points do not start right at the
|
|
379
|
+
* start of the hitobject before it and do not end right at the first hitobject after it.
|
|
380
|
+
*/
|
|
381
|
+
breakPointAccurateTimes = [];
|
|
382
|
+
/**
|
|
383
|
+
* A cursor occurrence nested array that only contains `movementType.DOWN` movement ID occurrences.
|
|
384
|
+
*
|
|
385
|
+
* Each index represents the cursor index.
|
|
386
|
+
*/
|
|
387
|
+
downCursorInstances = [];
|
|
388
|
+
/**
|
|
389
|
+
* Nerf factors from all sections that were three-fingered.
|
|
390
|
+
*/
|
|
391
|
+
nerfFactors = [];
|
|
392
|
+
/**
|
|
393
|
+
* Whether this score uses the Precise mod.
|
|
394
|
+
*/
|
|
395
|
+
isPrecise;
|
|
396
|
+
/**
|
|
397
|
+
* @param beatmap The beatmap to analyze.
|
|
398
|
+
* @param data The data of the replay.
|
|
399
|
+
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
400
|
+
*/
|
|
401
|
+
constructor(beatmap, data, difficultyAttributes) {
|
|
402
|
+
this.beatmap = beatmap;
|
|
403
|
+
this.data = data;
|
|
404
|
+
this.difficultyAttributes = difficultyAttributes;
|
|
405
|
+
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
406
|
+
overallDifficulty: beatmap.difficulty.od,
|
|
407
|
+
mods: this.difficultyAttributes.mods.filter((m) => m.isApplicableToDroid() &&
|
|
408
|
+
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
409
|
+
convertOverallDifficulty: false,
|
|
410
|
+
}).overallDifficulty;
|
|
411
|
+
this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
412
|
+
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
413
|
+
this.strainNoteCount =
|
|
414
|
+
this.difficultyAttributes.possibleThreeFingeredSections.reduce((a, v) => a + v.lastObjectIndex - v.firstObjectIndex + 1, 0);
|
|
415
|
+
this.hitObjects = beatmap.hitObjects.objects;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Checks whether a beatmap is eligible to be detected for 3-finger.
|
|
419
|
+
*
|
|
420
|
+
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
421
|
+
*/
|
|
422
|
+
static isEligibleToDetect(difficultyAttributes) {
|
|
423
|
+
return difficultyAttributes.possibleThreeFingeredSections.length > 0;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Checks if the given beatmap is 3-fingered and also returns the final penalty.
|
|
427
|
+
*
|
|
428
|
+
* The beatmap will be separated into sections and each section will be determined
|
|
429
|
+
* whether or not it is dragged.
|
|
430
|
+
*
|
|
431
|
+
* After that, each section will be assigned a nerf factor based on whether or not
|
|
432
|
+
* the section is 3-fingered. These nerf factors will be summed up into a final
|
|
433
|
+
* nerf factor, taking beatmap difficulty into account.
|
|
434
|
+
*/
|
|
435
|
+
check() {
|
|
436
|
+
if (this.strainNoteCount === 0) {
|
|
437
|
+
return { is3Finger: false, penalty: 1 };
|
|
438
|
+
}
|
|
439
|
+
this.getAccurateBreakPoints();
|
|
440
|
+
this.filterCursorInstances();
|
|
441
|
+
this.getBeatmapSections();
|
|
442
|
+
this.calculateNerfFactors();
|
|
443
|
+
const finalPenalty = this.calculateFinalPenalty();
|
|
444
|
+
return { is3Finger: finalPenalty > 1, penalty: finalPenalty };
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Generates a new set of "accurate break points".
|
|
448
|
+
*
|
|
449
|
+
* This is done to increase detection accuracy since break points do not start right at the
|
|
450
|
+
* end of the hitobject before it and do not end right at the first hitobject after it.
|
|
451
|
+
*/
|
|
452
|
+
getAccurateBreakPoints() {
|
|
453
|
+
const objectData = this.data.hitObjectData;
|
|
454
|
+
for (const breakPoint of this.beatmap.events.breaks) {
|
|
455
|
+
const beforeIndex = osuBase.MathUtils.clamp(this.hitObjects.findIndex((o) => o.endTime >= breakPoint.startTime) - 1, 0, this.hitObjects.length - 2);
|
|
456
|
+
const objectBefore = this.hitObjects[beforeIndex];
|
|
457
|
+
const objectBeforeData = objectData[beforeIndex];
|
|
458
|
+
let timeBefore = objectBefore.endTime;
|
|
459
|
+
if (objectBefore instanceof osuBase.Circle) {
|
|
460
|
+
if (objectBeforeData.result !== exports.HitResult.miss) {
|
|
461
|
+
timeBefore += objectBeforeData.accuracy;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
timeBefore += this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
const afterIndex = beforeIndex + 1;
|
|
468
|
+
const objectAfter = this.hitObjects[afterIndex];
|
|
469
|
+
const objectAfterData = objectData[afterIndex];
|
|
470
|
+
let timeAfter = this.hitObjects[afterIndex].startTime;
|
|
471
|
+
if (objectAfter instanceof osuBase.Circle &&
|
|
472
|
+
objectAfterData.result !== exports.HitResult.miss) {
|
|
473
|
+
timeAfter += objectAfterData.accuracy;
|
|
474
|
+
}
|
|
475
|
+
this.breakPointAccurateTimes.push(new osuBase.BreakPoint({
|
|
476
|
+
startTime: timeBefore,
|
|
477
|
+
endTime: timeAfter,
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Filters the original cursor instances, returning only those with `movementType.DOWN` movement ID.
|
|
483
|
+
*
|
|
484
|
+
* This also filters cursors that are in break period or happen before start/after end of the beatmap.
|
|
485
|
+
*/
|
|
486
|
+
filterCursorInstances() {
|
|
487
|
+
const objectData = this.data.hitObjectData;
|
|
488
|
+
const firstObjectResult = objectData[0].result;
|
|
489
|
+
const lastObjectResult = objectData.at(-1).result;
|
|
490
|
+
const firstObject = this.hitObjects[0];
|
|
491
|
+
const lastObject = this.hitObjects.at(-1);
|
|
492
|
+
// For sliders, automatically set hit window length to be as lenient as possible.
|
|
493
|
+
let firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
494
|
+
if (firstObject instanceof osuBase.Circle) {
|
|
495
|
+
switch (firstObjectResult) {
|
|
496
|
+
case exports.HitResult.great:
|
|
497
|
+
firstObjectHitWindow = this.hitWindow.hitWindowFor300(this.isPrecise);
|
|
498
|
+
break;
|
|
499
|
+
case exports.HitResult.good:
|
|
500
|
+
firstObjectHitWindow = this.hitWindow.hitWindowFor100(this.isPrecise);
|
|
501
|
+
break;
|
|
502
|
+
default:
|
|
503
|
+
firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// For sliders, automatically set hit window length to be as lenient as possible.
|
|
507
|
+
let lastObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
508
|
+
if (lastObject instanceof osuBase.Circle) {
|
|
509
|
+
switch (lastObjectResult) {
|
|
510
|
+
case exports.HitResult.great:
|
|
511
|
+
lastObjectHitWindow = this.hitWindow.hitWindowFor300(this.isPrecise);
|
|
512
|
+
break;
|
|
513
|
+
case exports.HitResult.good:
|
|
514
|
+
lastObjectHitWindow = this.hitWindow.hitWindowFor100(this.isPrecise);
|
|
515
|
+
break;
|
|
516
|
+
default:
|
|
517
|
+
lastObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
else if (lastObject instanceof osuBase.Slider) {
|
|
521
|
+
lastObjectHitWindow = Math.min(lastObject.spanDuration, lastObjectHitWindow);
|
|
522
|
+
}
|
|
523
|
+
// These hit time uses hit window length as threshold.
|
|
524
|
+
// This is because cursors aren't recorded exactly at hit time,
|
|
525
|
+
// probably due to the game's behavior.
|
|
526
|
+
const firstObjectHitTime = firstObject.startTime - firstObjectHitWindow;
|
|
527
|
+
const lastObjectHitTime = lastObject.startTime + lastObjectHitWindow;
|
|
528
|
+
for (const cursorInstance of this.data.cursorMovement) {
|
|
529
|
+
const validOccurrences = [];
|
|
530
|
+
for (const group of cursorInstance.occurrenceGroups) {
|
|
531
|
+
if (group.startTime < firstObjectHitTime) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (group.startTime > lastObjectHitTime) {
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
if (this.breakPointAccurateTimes.some((v) => group.startTime >= v.startTime &&
|
|
538
|
+
group.endTime <= v.endTime)) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
validOccurrences.push(group.down);
|
|
542
|
+
}
|
|
543
|
+
this.downCursorInstances.push(validOccurrences);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Divides the beatmap into sections, which will be used to
|
|
548
|
+
* detect dragged sections and improve detection speed.
|
|
549
|
+
*/
|
|
550
|
+
getBeatmapSections() {
|
|
551
|
+
const cursorLookupIndices = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
552
|
+
for (const section of this.difficultyAttributes
|
|
553
|
+
.possibleThreeFingeredSections) {
|
|
554
|
+
const dragFingerIndex = this.findDragIndex(section);
|
|
555
|
+
const objects = [];
|
|
556
|
+
for (let i = section.firstObjectIndex; i <= section.lastObjectIndex; ++i) {
|
|
557
|
+
objects.push({
|
|
558
|
+
object: this.hitObjects[i],
|
|
559
|
+
...this.getObjectPressIndex(this.hitObjects[i], this.data.hitObjectData[i], cursorLookupIndices, dragFingerIndex),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
this.beatmapSections.push({
|
|
563
|
+
...section,
|
|
564
|
+
dragFingerIndex: dragFingerIndex,
|
|
565
|
+
objects: objects,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Obtains the index of the nearest cursor of which an object was pressed in terms of time.
|
|
571
|
+
*
|
|
572
|
+
* @param object The object to obtain the index for.
|
|
573
|
+
* @param objectData The hit data of the object.
|
|
574
|
+
* @param cursorLookupIndices The cursor indices to start looking for the cursor from, to save computation time.
|
|
575
|
+
* @param excludedIndices The cursor indices that should not be checked.
|
|
576
|
+
* @returns The index of the cursor, -1 if the object was missed or it's a spinner.
|
|
577
|
+
*/
|
|
578
|
+
getObjectPressIndex(object, objectData, cursorLookupIndices, ...excludedCursorIndices) {
|
|
579
|
+
if (objectData.result === exports.HitResult.miss || object instanceof osuBase.Spinner) {
|
|
580
|
+
return {
|
|
581
|
+
pressingCursorInstanceIndex: -1,
|
|
582
|
+
pressingCursorIndex: -1,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
586
|
+
// Check for sliderbreaks and treat them as misses.
|
|
587
|
+
if (object instanceof osuBase.Slider &&
|
|
588
|
+
objectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
589
|
+
return {
|
|
590
|
+
pressingCursorInstanceIndex: -1,
|
|
591
|
+
pressingCursorIndex: -1,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
// We are not directly using hit time to determine which cursor pressed the object
|
|
595
|
+
// to account for time difference between hit registration and object judgement.
|
|
596
|
+
let minHitTime = object.startTime;
|
|
597
|
+
let maxHitTime = object.startTime;
|
|
598
|
+
if (object instanceof osuBase.Circle) {
|
|
599
|
+
let hitWindowGap = hitWindow50;
|
|
600
|
+
switch (objectData.result) {
|
|
601
|
+
case exports.HitResult.great:
|
|
602
|
+
hitWindowGap = this.hitWindow.hitWindowFor300(this.isPrecise);
|
|
603
|
+
break;
|
|
604
|
+
case exports.HitResult.good:
|
|
605
|
+
hitWindowGap = this.hitWindow.hitWindowFor100(this.isPrecise);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
minHitTime -= hitWindowGap;
|
|
609
|
+
maxHitTime += hitWindowGap;
|
|
610
|
+
}
|
|
611
|
+
else if (object instanceof osuBase.Slider) {
|
|
612
|
+
minHitTime -= hitWindow50;
|
|
613
|
+
maxHitTime += Math.min(hitWindow50, object.spanDuration);
|
|
614
|
+
}
|
|
615
|
+
const hitTime = object.startTime + objectData.accuracy;
|
|
616
|
+
let nearestCursorInstanceIndex = null;
|
|
617
|
+
let nearestCursorIndex = null;
|
|
618
|
+
let nearestTime = Number.POSITIVE_INFINITY;
|
|
619
|
+
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
620
|
+
if (excludedCursorIndices.includes(i)) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
const cursors = this.downCursorInstances[i];
|
|
624
|
+
for (let j = cursorLookupIndices[i]; j < cursors.length; cursorLookupIndices[i] = ++j) {
|
|
625
|
+
const cursor = cursors[j];
|
|
626
|
+
if (cursor.time < minHitTime) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (cursor.time > maxHitTime) {
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
const deltaTime = Math.abs(hitTime - cursor.time);
|
|
633
|
+
if (deltaTime > nearestTime) {
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
nearestCursorInstanceIndex = i;
|
|
637
|
+
nearestCursorIndex = j;
|
|
638
|
+
nearestTime = deltaTime;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
pressingCursorInstanceIndex: nearestCursorInstanceIndex ?? -1,
|
|
643
|
+
pressingCursorIndex: nearestCursorIndex ?? -1,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Checks if a section is dragged and returns the index of the drag finger.
|
|
648
|
+
*
|
|
649
|
+
* If the section is not dragged, -1 will be returned.
|
|
650
|
+
*
|
|
651
|
+
* @param section The section to check.
|
|
652
|
+
*/
|
|
653
|
+
findDragIndex(section) {
|
|
654
|
+
const objectData = this.data.hitObjectData;
|
|
655
|
+
const firstObject = this.hitObjects[section.firstObjectIndex];
|
|
656
|
+
const lastObject = this.hitObjects[section.lastObjectIndex];
|
|
657
|
+
let firstObjectMinHitTime = firstObject.startTime;
|
|
658
|
+
if (firstObject instanceof osuBase.Circle) {
|
|
659
|
+
switch (objectData[section.firstObjectIndex].result) {
|
|
660
|
+
case exports.HitResult.great:
|
|
661
|
+
firstObjectMinHitTime -= this.hitWindow.hitWindowFor300(this.isPrecise);
|
|
662
|
+
break;
|
|
663
|
+
case exports.HitResult.good:
|
|
664
|
+
firstObjectMinHitTime -= this.hitWindow.hitWindowFor100(this.isPrecise);
|
|
665
|
+
break;
|
|
666
|
+
default:
|
|
667
|
+
firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
else if (firstObject instanceof osuBase.Slider) {
|
|
671
|
+
firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
672
|
+
}
|
|
673
|
+
let lastObjectMaxHitTime = lastObject.startTime;
|
|
674
|
+
if (lastObject instanceof osuBase.Circle) {
|
|
675
|
+
switch (objectData[section.lastObjectIndex].result) {
|
|
676
|
+
case exports.HitResult.great:
|
|
677
|
+
lastObjectMaxHitTime += this.hitWindow.hitWindowFor300(this.isPrecise);
|
|
678
|
+
break;
|
|
679
|
+
case exports.HitResult.good:
|
|
680
|
+
lastObjectMaxHitTime += this.hitWindow.hitWindowFor100(this.isPrecise);
|
|
681
|
+
break;
|
|
682
|
+
default:
|
|
683
|
+
lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else if (lastObject instanceof osuBase.Slider) {
|
|
687
|
+
lastObjectMaxHitTime += Math.min(this.hitWindow.hitWindowFor50(this.isPrecise), lastObject.spanDuration);
|
|
688
|
+
}
|
|
689
|
+
// Since there may be more than 1 cursor instance index,
|
|
690
|
+
// we check which cursor instance follows hitobjects all over.
|
|
691
|
+
const cursorIndices = [];
|
|
692
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
693
|
+
const c = this.data.cursorMovement[i];
|
|
694
|
+
if (c.occurrenceGroups.length === 0) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
// Do not include cursors that don't have an occurence in this section
|
|
698
|
+
// this speeds up checking process.
|
|
699
|
+
if (c.occurrenceGroups.filter((v) => v.startTime >= firstObjectMinHitTime &&
|
|
700
|
+
v.endTime <= lastObjectMaxHitTime).length === 0) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
// If this cursor instance doesn't move, it's not the cursor instance we want.
|
|
704
|
+
if (c.occurrenceGroups.filter((v) => v.moves.length > 0).length ===
|
|
705
|
+
0) {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
cursorIndices.push(i);
|
|
709
|
+
}
|
|
710
|
+
const sectionObjects = this.hitObjects.slice(section.firstObjectIndex, section.lastObjectIndex + 1);
|
|
711
|
+
const sectionReplayObjectData = objectData.slice(section.firstObjectIndex, section.lastObjectIndex + 1);
|
|
712
|
+
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
713
|
+
for (let i = 0; i < sectionObjects.length && cursorIndices.some((v) => v !== -1); ++i) {
|
|
714
|
+
const object = sectionObjects[i];
|
|
715
|
+
const objectData = sectionReplayObjectData[i];
|
|
716
|
+
if (object instanceof osuBase.Spinner ||
|
|
717
|
+
objectData.result === exports.HitResult.miss) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
// Exclude sliderbreaks.
|
|
721
|
+
if (object instanceof osuBase.Slider &&
|
|
722
|
+
objectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
726
|
+
const hitTime = object.startTime + objectData.accuracy;
|
|
727
|
+
// Observe the cursor position at the object's hit time.
|
|
728
|
+
for (let j = 0; j < cursorIndices.length; ++j) {
|
|
729
|
+
if (cursorIndices[j] === -1) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const cursorData = this.data.cursorMovement[cursorIndices[j]];
|
|
733
|
+
const cursorGroup = cursorData.occurrenceGroups.find((v) => v.isActiveAt(hitTime));
|
|
734
|
+
if (!cursorGroup) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const cursors = cursorGroup.allOccurrences;
|
|
738
|
+
let isInObject = false;
|
|
739
|
+
for (let k = 1; k < cursors.length; ++k) {
|
|
740
|
+
const cursor = cursors[k];
|
|
741
|
+
const prevCursor = cursors[k - 1];
|
|
742
|
+
// Only consider cursor at interval prev.time <= hitTime <= current.time.
|
|
743
|
+
if (prevCursor.time <= hitTime && cursor.time >= hitTime) {
|
|
744
|
+
switch (cursor.id) {
|
|
745
|
+
case exports.MovementType.up:
|
|
746
|
+
isInObject =
|
|
747
|
+
prevCursor.position.getDistance(objectPosition) <= object.radius;
|
|
748
|
+
break;
|
|
749
|
+
case exports.MovementType.move: {
|
|
750
|
+
// Interpolate movement.
|
|
751
|
+
const t = (hitTime - prevCursor.time) /
|
|
752
|
+
(cursor.time - prevCursor.time);
|
|
753
|
+
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));
|
|
754
|
+
isInObject =
|
|
755
|
+
objectPosition.getDistance(cursorPosition) <= object.radius;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (!isInObject) {
|
|
762
|
+
cursorIndices[j] = -1;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return cursorIndices.find((v) => v !== -1) ?? -1;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Creates nerf factors by scanning through objects.
|
|
770
|
+
*/
|
|
771
|
+
calculateNerfFactors() {
|
|
772
|
+
for (const beatmapSection of this.beatmapSections) {
|
|
773
|
+
const cursorAmounts = osuBase.Utils.initializeArray(this.downCursorInstances.length, 0);
|
|
774
|
+
const cursorVectorTimes = [];
|
|
775
|
+
for (const object of beatmapSection.objects) {
|
|
776
|
+
if (object.pressingCursorIndex === -1 ||
|
|
777
|
+
object.pressingCursorInstanceIndex ===
|
|
778
|
+
beatmapSection.dragFingerIndex) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
++cursorAmounts[object.pressingCursorInstanceIndex];
|
|
782
|
+
const cursor = this.downCursorInstances[object.pressingCursorInstanceIndex][object.pressingCursorIndex];
|
|
783
|
+
cursorVectorTimes.push({
|
|
784
|
+
vector: new osuBase.Vector2(cursor.position.x, cursor.position.y),
|
|
785
|
+
time: cursor.time,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
if (beatmapSection.dragFingerIndex !== -1) {
|
|
789
|
+
// Remove the drag index to prevent it from being picked up into the detection.
|
|
790
|
+
cursorAmounts.splice(beatmapSection.dragFingerIndex, 1);
|
|
791
|
+
}
|
|
792
|
+
// This index will be used to detect if a section is 3-fingered.
|
|
793
|
+
// If the section is dragged, the dragged instance will be ignored,
|
|
794
|
+
// hence why the index is 1 less than nondragged section.
|
|
795
|
+
const fingerSplitIndex = beatmapSection.dragFingerIndex !== -1 ? 2 : 3;
|
|
796
|
+
// Divide >=4th (3rd for drag) cursor instances with 1st + 2nd (+ 3rd for nondrag)
|
|
797
|
+
// to check if the section is 3-fingered.
|
|
798
|
+
const threeFingerRatio = cursorAmounts
|
|
799
|
+
.slice(fingerSplitIndex)
|
|
800
|
+
.reduce((acc, value) => acc + value, 0) /
|
|
801
|
+
Math.max(cursorAmounts
|
|
802
|
+
.slice(0, fingerSplitIndex)
|
|
803
|
+
.reduce((acc, value) => acc + value, 0), 1);
|
|
804
|
+
const similarPresses = [];
|
|
805
|
+
cursorVectorTimes.sort((a, b) => a.time - b.time);
|
|
806
|
+
for (const cursorVectorTime of cursorVectorTimes) {
|
|
807
|
+
const pressIndex = similarPresses.findIndex((v) => v.vector.getDistance(cursorVectorTime.vector) <=
|
|
808
|
+
this.cursorDistancingDistanceThreshold);
|
|
809
|
+
if (pressIndex !== -1) {
|
|
810
|
+
if (cursorVectorTime.time -
|
|
811
|
+
similarPresses[pressIndex].lastTime >=
|
|
812
|
+
this.cursorDistancingTimeThreshold) {
|
|
813
|
+
// If the previous press is too late, remove it from the
|
|
814
|
+
// list and register the current press as a new press.
|
|
815
|
+
similarPresses.splice(pressIndex, 1);
|
|
816
|
+
similarPresses.push({
|
|
817
|
+
vector: cursorVectorTime.vector,
|
|
818
|
+
count: 1,
|
|
819
|
+
lastTime: cursorVectorTime.time,
|
|
820
|
+
});
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
similarPresses[pressIndex].vector = cursorVectorTime.vector;
|
|
824
|
+
similarPresses[pressIndex].lastTime = cursorVectorTime.time;
|
|
825
|
+
++similarPresses[pressIndex].count;
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
similarPresses.push({
|
|
829
|
+
vector: cursorVectorTime.vector,
|
|
830
|
+
count: 1,
|
|
831
|
+
lastTime: cursorVectorTime.time,
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
// Sort by highest count; assume the order is 3rd, 4th, 5th, ... finger
|
|
836
|
+
const threeFingerPresses = similarPresses
|
|
837
|
+
.filter((v) => v.count >= this.cursorDistancingCountThreshold)
|
|
838
|
+
.sort((a, b) => b.count - a.count)
|
|
839
|
+
.slice(fingerSplitIndex);
|
|
840
|
+
if (threeFingerRatio > this.threeFingerRatioThreshold ||
|
|
841
|
+
threeFingerPresses.length > 0) {
|
|
842
|
+
const objectCount = beatmapSection.lastObjectIndex -
|
|
843
|
+
beatmapSection.firstObjectIndex +
|
|
844
|
+
1;
|
|
845
|
+
// We can ignore the first 3 (2 for drag) filled cursor instances
|
|
846
|
+
// since they are guaranteed not 3 finger.
|
|
847
|
+
const threeFingerCursorAmounts = cursorAmounts
|
|
848
|
+
.slice(fingerSplitIndex)
|
|
849
|
+
.filter((amount) => amount > 0);
|
|
850
|
+
// Finger factor applies more penalty if more fingers were used.
|
|
851
|
+
const fingerFactor = threeFingerRatio > this.threeFingerRatioThreshold
|
|
852
|
+
? threeFingerCursorAmounts.reduce((acc, value, index) => acc +
|
|
853
|
+
Math.pow(((index + 1) * value * objectCount) /
|
|
854
|
+
this.strainNoteCount, 0.9), 1)
|
|
855
|
+
: Math.pow(threeFingerPresses.reduce((acc, value, index) => acc +
|
|
856
|
+
Math.pow(((index + 1) *
|
|
857
|
+
(value.count /
|
|
858
|
+
(this
|
|
859
|
+
.cursorDistancingCountThreshold *
|
|
860
|
+
2)) *
|
|
861
|
+
objectCount) /
|
|
862
|
+
this.strainNoteCount, 0.2), 1), 0.2);
|
|
863
|
+
// Length factor applies more penalty if there are more 3-fingered object.
|
|
864
|
+
const lengthFactor = 1 + Math.pow(objectCount / this.strainNoteCount, 1.2);
|
|
865
|
+
this.nerfFactors.push({
|
|
866
|
+
strainFactor: Math.max(1, beatmapSection.sumStrain),
|
|
867
|
+
fingerFactor,
|
|
868
|
+
lengthFactor,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Calculates the final penalty.
|
|
875
|
+
*/
|
|
876
|
+
calculateFinalPenalty() {
|
|
877
|
+
return this.nerfFactors.reduce((a, n) => a +
|
|
878
|
+
0.015 *
|
|
879
|
+
Math.pow(n.strainFactor * n.fingerFactor * n.lengthFactor, 1.05), 1);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
315
883
|
/**
|
|
316
884
|
* Represents a replay data in an osu!droid replay.
|
|
317
885
|
*
|
|
@@ -386,10 +954,6 @@ class ThreeFingerChecker {
|
|
|
386
954
|
* The difficulty attributes of the beatmap.
|
|
387
955
|
*/
|
|
388
956
|
difficultyAttributes;
|
|
389
|
-
/**
|
|
390
|
-
* The true scale of objects.
|
|
391
|
-
*/
|
|
392
|
-
trueScale;
|
|
393
957
|
/**
|
|
394
958
|
* The distance threshold between cursors to assume that two cursors are
|
|
395
959
|
* actually pressed with 1 finger in osu!pixels.
|
|
@@ -466,20 +1030,16 @@ class ThreeFingerChecker {
|
|
|
466
1030
|
this.beatmap = beatmap;
|
|
467
1031
|
this.data = data;
|
|
468
1032
|
this.difficultyAttributes = difficultyAttributes;
|
|
469
|
-
const
|
|
470
|
-
|
|
1033
|
+
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
1034
|
+
overallDifficulty: beatmap.difficulty.od,
|
|
471
1035
|
mods: this.difficultyAttributes.mods.filter((m) => m.isApplicableToDroid() &&
|
|
472
1036
|
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
473
|
-
|
|
1037
|
+
convertOverallDifficulty: false,
|
|
1038
|
+
}).overallDifficulty;
|
|
474
1039
|
this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
|
|
475
|
-
this.hitWindow = new osuBase.DroidHitWindow(
|
|
1040
|
+
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
476
1041
|
this.strainNoteCount =
|
|
477
1042
|
this.difficultyAttributes.possibleThreeFingeredSections.reduce((a, v) => a + v.lastObjectIndex - v.firstObjectIndex + 1, 0);
|
|
478
|
-
const circleSize = new osuBase.MapStats({
|
|
479
|
-
cs: this.beatmap.difficulty.cs,
|
|
480
|
-
mods: this.difficultyAttributes.mods,
|
|
481
|
-
}).calculate({ mode: osuBase.Modes.droid }).cs;
|
|
482
|
-
this.trueScale = (1 - (0.7 * (circleSize - 5)) / 5) / 2;
|
|
483
1043
|
}
|
|
484
1044
|
/**
|
|
485
1045
|
* Checks whether a beatmap is eligible to be detected for 3-finger.
|
|
@@ -572,11 +1132,13 @@ class ThreeFingerChecker {
|
|
|
572
1132
|
filterCursorInstances() {
|
|
573
1133
|
const { objects } = this.beatmap.hitObjects;
|
|
574
1134
|
const objectData = this.data.hitObjectData;
|
|
1135
|
+
const firstObject = objects[0];
|
|
1136
|
+
const lastObject = objects.at(-1);
|
|
575
1137
|
const firstObjectResult = objectData[0].result;
|
|
576
1138
|
const lastObjectResult = objectData.at(-1).result;
|
|
577
1139
|
// For sliders, automatically set hit window length to be as lenient as possible.
|
|
578
1140
|
let firstObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
579
|
-
if (
|
|
1141
|
+
if (firstObject instanceof osuBase.Circle) {
|
|
580
1142
|
switch (firstObjectResult) {
|
|
581
1143
|
case exports.HitResult.great:
|
|
582
1144
|
firstObjectHitWindow = this.hitWindow.hitWindowFor300(this.isPrecise);
|
|
@@ -590,7 +1152,7 @@ class ThreeFingerChecker {
|
|
|
590
1152
|
}
|
|
591
1153
|
// For sliders, automatically set hit window length to be as lenient as possible.
|
|
592
1154
|
let lastObjectHitWindow = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
593
|
-
if (
|
|
1155
|
+
if (lastObject instanceof osuBase.Circle) {
|
|
594
1156
|
switch (lastObjectResult) {
|
|
595
1157
|
case exports.HitResult.great:
|
|
596
1158
|
lastObjectHitWindow = this.hitWindow.hitWindowFor300(this.isPrecise);
|
|
@@ -605,8 +1167,8 @@ class ThreeFingerChecker {
|
|
|
605
1167
|
// These hit time uses hit window length as threshold.
|
|
606
1168
|
// This is because cursors aren't recorded exactly at hit time,
|
|
607
1169
|
// probably due to the game's behavior.
|
|
608
|
-
const firstObjectHitTime =
|
|
609
|
-
const lastObjectHitTime =
|
|
1170
|
+
const firstObjectHitTime = firstObject.startTime - firstObjectHitWindow;
|
|
1171
|
+
const lastObjectHitTime = lastObject.startTime + lastObjectHitWindow;
|
|
610
1172
|
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
611
1173
|
const cursorInstance = this.data.cursorMovement[i];
|
|
612
1174
|
const validOccurrences = [];
|
|
@@ -728,7 +1290,7 @@ class ThreeFingerChecker {
|
|
|
728
1290
|
findDragIndex(sectionObjects, sectionReplayObjectData, cursorIndexes) {
|
|
729
1291
|
const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
|
|
730
1292
|
for (let i = 0; i < sectionObjects.length && cursorIndexes.every((v) => v !== -1); ++i) {
|
|
731
|
-
|
|
1293
|
+
const object = sectionObjects[i];
|
|
732
1294
|
const objectData = sectionReplayObjectData[i];
|
|
733
1295
|
if (object instanceof osuBase.Spinner ||
|
|
734
1296
|
objectData.result === exports.HitResult.miss) {
|
|
@@ -739,11 +1301,6 @@ class ThreeFingerChecker {
|
|
|
739
1301
|
objectData.accuracy === Math.floor(hitWindow50) + 13) {
|
|
740
1302
|
continue;
|
|
741
1303
|
}
|
|
742
|
-
if (object.droidScale !== this.trueScale) {
|
|
743
|
-
// Deep copy the instance so that we can assign scale.
|
|
744
|
-
object = osuBase.Utils.deepCopy(object);
|
|
745
|
-
object.droidScale = this.trueScale;
|
|
746
|
-
}
|
|
747
1304
|
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
748
1305
|
const hitTime = object.startTime + objectData.accuracy;
|
|
749
1306
|
// Observe the cursor position at the object's hit time.
|
|
@@ -770,7 +1327,7 @@ class ThreeFingerChecker {
|
|
|
770
1327
|
switch (cursor.id) {
|
|
771
1328
|
case exports.MovementType.up:
|
|
772
1329
|
isInObject =
|
|
773
|
-
prevCursor.position.getDistance(objectPosition) <= object.
|
|
1330
|
+
prevCursor.position.getDistance(objectPosition) <= object.radius;
|
|
774
1331
|
break;
|
|
775
1332
|
case exports.MovementType.move:
|
|
776
1333
|
// Interpolate movement.
|
|
@@ -781,7 +1338,7 @@ class ThreeFingerChecker {
|
|
|
781
1338
|
(cursor.time - prevCursor.time);
|
|
782
1339
|
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));
|
|
783
1340
|
isInObject =
|
|
784
|
-
objectPosition.getDistance(cursorPosition) <= object.
|
|
1341
|
+
objectPosition.getDistance(cursorPosition) <= object.radius;
|
|
785
1342
|
}
|
|
786
1343
|
}
|
|
787
1344
|
if (!isInObject) {
|
|
@@ -952,10 +1509,9 @@ class ThreeFingerChecker {
|
|
|
952
1509
|
* Calculates the final penalty.
|
|
953
1510
|
*/
|
|
954
1511
|
calculateFinalPenalty() {
|
|
955
|
-
return (
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
Math.pow(n.strainFactor * n.fingerFactor * n.lengthFactor, 1.05), 0));
|
|
1512
|
+
return this.nerfFactors.reduce((a, n) => a +
|
|
1513
|
+
0.015 *
|
|
1514
|
+
Math.pow(n.strainFactor * n.fingerFactor * n.lengthFactor, 1.05), 1);
|
|
959
1515
|
}
|
|
960
1516
|
}
|
|
961
1517
|
|
|
@@ -1057,12 +1613,13 @@ class TwoHandChecker {
|
|
|
1057
1613
|
constructor(calculator, data) {
|
|
1058
1614
|
this.calculator = calculator;
|
|
1059
1615
|
this.data = data;
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
mods:
|
|
1616
|
+
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
1617
|
+
overallDifficulty: calculator.beatmap.difficulty.od,
|
|
1618
|
+
mods: calculator.mods.filter((m) => m.isApplicableToDroid() &&
|
|
1063
1619
|
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
1064
|
-
|
|
1065
|
-
|
|
1620
|
+
convertOverallDifficulty: false,
|
|
1621
|
+
}).overallDifficulty;
|
|
1622
|
+
this.hitWindow = new osuBase.DroidHitWindow(od);
|
|
1066
1623
|
this.hitWindow50 = this.hitWindow.hitWindowFor50(calculator.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
1067
1624
|
// this.csvString = `Mods,${
|
|
1068
1625
|
// data.convertedMods.reduce((a, m) => a + m.acronym, "") || "NM"
|
|
@@ -1245,9 +1802,8 @@ class TwoHandChecker {
|
|
|
1245
1802
|
const prevToCurrentMovement = object
|
|
1246
1803
|
.getStackedPosition(osuBase.Modes.droid)
|
|
1247
1804
|
.subtract(prevObjectEndPosition);
|
|
1248
|
-
const radius = object.getRadius(osuBase.Modes.droid);
|
|
1249
1805
|
// Don't consider objects that are too close to each other.
|
|
1250
|
-
if (prevToCurrentMovement.length <= radius) {
|
|
1806
|
+
if (prevToCurrentMovement.length <= object.radius) {
|
|
1251
1807
|
return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
|
|
1252
1808
|
}
|
|
1253
1809
|
// The case for a one-handed object is that there will be a slight movement in the cursor towards
|
|
@@ -1376,7 +1932,6 @@ class TwoHandChecker {
|
|
|
1376
1932
|
cursorTime: object.startTime,
|
|
1377
1933
|
};
|
|
1378
1934
|
}
|
|
1379
|
-
const radius = object.getRadius(osuBase.Modes.droid);
|
|
1380
1935
|
let hitWindow = this.hitWindow50;
|
|
1381
1936
|
const isPrecise = this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise);
|
|
1382
1937
|
// For sliders, set the hit window to as lenient as possible.
|
|
@@ -1391,7 +1946,7 @@ class TwoHandChecker {
|
|
|
1391
1946
|
}
|
|
1392
1947
|
}
|
|
1393
1948
|
// TODO: what to do for head sliderbreaks?
|
|
1394
|
-
let nearestPosition = new osuBase.Vector2(Number.POSITIVE_INFINITY
|
|
1949
|
+
let nearestPosition = new osuBase.Vector2(Number.POSITIVE_INFINITY);
|
|
1395
1950
|
let nearestCursorIndex = 0;
|
|
1396
1951
|
let nearestGroupIndex = 0;
|
|
1397
1952
|
let nearestCursorGroupIndex = 0;
|
|
@@ -1399,12 +1954,12 @@ class TwoHandChecker {
|
|
|
1399
1954
|
const minimumActiveTime = object.startTime - hitWindow;
|
|
1400
1955
|
const maximumActiveTime = object.startTime + hitWindow;
|
|
1401
1956
|
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1402
|
-
if (nearestPosition.getDistance(objectPosition) <= radius) {
|
|
1957
|
+
if (nearestPosition.getDistance(objectPosition) <= object.radius) {
|
|
1403
1958
|
break;
|
|
1404
1959
|
}
|
|
1405
1960
|
const cursorData = this.data.cursorMovement[i];
|
|
1406
1961
|
for (let j = 0; j < cursorData.occurrenceGroups.length; ++j) {
|
|
1407
|
-
if (nearestPosition.getDistance(objectPosition) <= radius) {
|
|
1962
|
+
if (nearestPosition.getDistance(objectPosition) <= object.radius) {
|
|
1408
1963
|
break;
|
|
1409
1964
|
}
|
|
1410
1965
|
const cursorGroup = cursorData.occurrenceGroups[j];
|
|
@@ -1416,7 +1971,8 @@ class TwoHandChecker {
|
|
|
1416
1971
|
}
|
|
1417
1972
|
// Validate the down press first.
|
|
1418
1973
|
const { down } = cursorGroup;
|
|
1419
|
-
if (down.position.getDistance(objectPosition) <=
|
|
1974
|
+
if (down.position.getDistance(objectPosition) <=
|
|
1975
|
+
object.radius &&
|
|
1420
1976
|
Math.abs(down.time - object.startTime) <= hitWindow) {
|
|
1421
1977
|
if (objectIndex > 0) {
|
|
1422
1978
|
const prevObject = this.calculator.beatmap.hitObjects.objects[objectIndex - 1];
|
|
@@ -1487,12 +2043,13 @@ class TwoHandChecker {
|
|
|
1487
2043
|
nearestCursorGroupIndex = k;
|
|
1488
2044
|
nearestCursorTime = cursorGroup.down.time;
|
|
1489
2045
|
}
|
|
1490
|
-
if (distance <= radius) {
|
|
2046
|
+
if (distance <= object.radius) {
|
|
1491
2047
|
break;
|
|
1492
2048
|
}
|
|
1493
2049
|
}
|
|
1494
2050
|
}
|
|
1495
|
-
if (nearestPosition.getDistance(objectPosition) <=
|
|
2051
|
+
if (nearestPosition.getDistance(objectPosition) <=
|
|
2052
|
+
object.radius) {
|
|
1496
2053
|
break;
|
|
1497
2054
|
}
|
|
1498
2055
|
}
|
|
@@ -1540,7 +2097,7 @@ class TwoHandChecker {
|
|
|
1540
2097
|
else {
|
|
1541
2098
|
objectEndPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1542
2099
|
}
|
|
1543
|
-
let nearestPosition = new osuBase.Vector2(Number.POSITIVE_INFINITY
|
|
2100
|
+
let nearestPosition = new osuBase.Vector2(Number.POSITIVE_INFINITY);
|
|
1544
2101
|
let nearestCursorIndex = 0;
|
|
1545
2102
|
let nearestGroupIndex = 0;
|
|
1546
2103
|
let nearestCursorGroupIndex = 0;
|
|
@@ -1665,12 +2222,13 @@ class SliderCheeseChecker {
|
|
|
1665
2222
|
this.beatmap = beatmap;
|
|
1666
2223
|
this.data = data;
|
|
1667
2224
|
this.difficultyAttributes = difficultyAttributes;
|
|
1668
|
-
const
|
|
1669
|
-
|
|
2225
|
+
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
2226
|
+
overallDifficulty: beatmap.difficulty.od,
|
|
1670
2227
|
mods: this.difficultyAttributes.mods.filter((m) => m.isApplicableToDroid() &&
|
|
1671
2228
|
!osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
1672
|
-
|
|
1673
|
-
|
|
2229
|
+
convertOverallDifficulty: false,
|
|
2230
|
+
}).overallDifficulty;
|
|
2231
|
+
this.hitWindow50 = new osuBase.DroidHitWindow(od).hitWindowFor50(this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise));
|
|
1674
2232
|
}
|
|
1675
2233
|
/**
|
|
1676
2234
|
* Checks if relevant sliders in the given beatmap was cheesed.
|
|
@@ -1697,12 +2255,7 @@ class SliderCheeseChecker {
|
|
|
1697
2255
|
const cheesedDifficultyRatings = [];
|
|
1698
2256
|
// Current loop indices are stored for efficiency.
|
|
1699
2257
|
const cursorLoopIndices = osuBase.Utils.initializeArray(10, 0);
|
|
1700
|
-
const
|
|
1701
|
-
cs: this.beatmap.difficulty.cs,
|
|
1702
|
-
mods: this.difficultyAttributes.mods,
|
|
1703
|
-
}).calculate({ mode: osuBase.Modes.droid }).cs;
|
|
1704
|
-
const scale = osuBase.CircleSizeCalculator.standardCSToStandardScale(circleSize);
|
|
1705
|
-
const acceptableRadius = 64 * scale * 2.4;
|
|
2258
|
+
const acceptableRadius = objects[0].radius * 2;
|
|
1706
2259
|
// Sort difficult sliders by index so that cursor loop indices work properly.
|
|
1707
2260
|
for (const difficultSlider of this.difficultyAttributes.difficultSliders
|
|
1708
2261
|
.slice()
|
|
@@ -1716,12 +2269,7 @@ class SliderCheeseChecker {
|
|
|
1716
2269
|
objectData.accuracy === Math.floor(this.hitWindow50) + 13) {
|
|
1717
2270
|
continue;
|
|
1718
2271
|
}
|
|
1719
|
-
|
|
1720
|
-
if (object.droidScale !== scale) {
|
|
1721
|
-
// Deep clone the object so that we can assign scale properly.
|
|
1722
|
-
object = osuBase.Utils.deepCopy(object);
|
|
1723
|
-
object.droidScale = scale;
|
|
1724
|
-
}
|
|
2272
|
+
const object = objects[difficultSlider.index];
|
|
1725
2273
|
const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1726
2274
|
// These time boundaries should consider the delta time between the previous and next
|
|
1727
2275
|
// object as well as their hit accuracy. However, they are somewhat complicated to
|
|
@@ -1820,14 +2368,6 @@ class SliderCheeseChecker {
|
|
|
1820
2368
|
continue;
|
|
1821
2369
|
}
|
|
1822
2370
|
const nestedObject = object.nestedHitObjects[i];
|
|
1823
|
-
nestedObject.droidScale = scale;
|
|
1824
|
-
// Special treatment for slider tail where its treated as a "legacy tail" in osu!standard.
|
|
1825
|
-
// In that case, its time is assumed to be 36ms behind the slider's end time. However, that
|
|
1826
|
-
// is not the case for osu!droid.
|
|
1827
|
-
if (nestedObject instanceof osuBase.SliderTail) {
|
|
1828
|
-
nestedObject.startTime = object.endTime;
|
|
1829
|
-
nestedObject.endTime = object.endTime;
|
|
1830
|
-
}
|
|
1831
2371
|
const nestedPosition = nestedObject.getStackedPosition(osuBase.Modes.droid);
|
|
1832
2372
|
while (occurrenceLoopIndex < allOccurrences.length &&
|
|
1833
2373
|
allOccurrences[occurrenceLoopIndex].time <
|
|
@@ -1944,13 +2484,14 @@ class ReplayAnalyzer {
|
|
|
1944
2484
|
*/
|
|
1945
2485
|
hasBeenCheckedFor2Hand = false;
|
|
1946
2486
|
/**
|
|
1947
|
-
* Whether this
|
|
2487
|
+
* Whether this replay has been checked against slider cheesing.
|
|
1948
2488
|
*/
|
|
1949
2489
|
hasBeenCheckedForSliderCheesing = false;
|
|
1950
2490
|
/**
|
|
1951
2491
|
* The amount of two-handed objects.
|
|
1952
2492
|
*/
|
|
1953
2493
|
twoHandedNoteCount = 0;
|
|
2494
|
+
convertedBeatmap;
|
|
1954
2495
|
// Sizes of primitive data types in Java (in bytes)
|
|
1955
2496
|
BYTE_LENGTH = 1;
|
|
1956
2497
|
SHORT_LENGTH = 2;
|
|
@@ -2302,12 +2843,12 @@ class ReplayAnalyzer {
|
|
|
2302
2843
|
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
|
|
2303
2844
|
? this.beatmap.beatmap
|
|
2304
2845
|
: this.beatmap;
|
|
2305
|
-
const objects = beatmap.hitObjects
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2846
|
+
const { objects } = beatmap.hitObjects;
|
|
2847
|
+
const od = osuBase.calculateDroidDifficultyStatistics({
|
|
2848
|
+
overallDifficulty: beatmap.difficulty.od,
|
|
2308
2849
|
mods: this.data.convertedMods.filter((m) => !osuBase.ModUtil.speedChangingMods.some((v) => v.acronym === m.acronym)),
|
|
2309
|
-
}).
|
|
2310
|
-
const hitWindow50 = new osuBase.DroidHitWindow(
|
|
2850
|
+
}).overallDifficulty;
|
|
2851
|
+
const hitWindow50 = new osuBase.DroidHitWindow(od).hitWindowFor50(this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise));
|
|
2311
2852
|
// The accuracy of sliders is set to (50 hit window)ms + 13ms if their head was not hit:
|
|
2312
2853
|
// https://github.com/osudroid/osu-droid/blob/6306c68e3ffaf671eac794bf45cc95c0f3313a82/src/ru/nsu/ccfit/zuev/osu/game/Slider.java#L821
|
|
2313
2854
|
//
|
|
@@ -2321,8 +2862,8 @@ class ReplayAnalyzer {
|
|
|
2321
2862
|
accuracies.push(0);
|
|
2322
2863
|
continue;
|
|
2323
2864
|
}
|
|
2324
|
-
const accuracy = v
|
|
2325
|
-
if (o instanceof osuBase.Slider &&
|
|
2865
|
+
const { accuracy } = v;
|
|
2866
|
+
if (o instanceof osuBase.Slider && accuracy === sliderbreakHitOffset) {
|
|
2326
2867
|
accuracies.push(0);
|
|
2327
2868
|
continue;
|
|
2328
2869
|
}
|
|
@@ -2385,9 +2926,32 @@ class ReplayAnalyzer {
|
|
|
2385
2926
|
if (!this.beatmap || !this.data || !this.difficultyAttributes) {
|
|
2386
2927
|
return;
|
|
2387
2928
|
}
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2929
|
+
if (!this.convertedBeatmap) {
|
|
2930
|
+
const mods = this.data.convertedMods.slice();
|
|
2931
|
+
if ([
|
|
2932
|
+
this.data.forceCS,
|
|
2933
|
+
this.data.forceAR,
|
|
2934
|
+
this.data.forceOD,
|
|
2935
|
+
this.data.forceHP,
|
|
2936
|
+
].some((v) => v !== undefined)) {
|
|
2937
|
+
mods.push(new osuBase.ModDifficultyAdjust({
|
|
2938
|
+
cs: this.data.forceCS,
|
|
2939
|
+
ar: this.data.forceAR,
|
|
2940
|
+
od: this.data.forceOD,
|
|
2941
|
+
hp: this.data.forceHP,
|
|
2942
|
+
}));
|
|
2943
|
+
}
|
|
2944
|
+
this.convertedBeatmap ??= new osuBase.BeatmapConverter(this.beatmap instanceof osuBase.Beatmap
|
|
2945
|
+
? this.beatmap
|
|
2946
|
+
: this.beatmap.beatmap).convert({
|
|
2947
|
+
mode: osuBase.Modes.droid,
|
|
2948
|
+
mods: mods,
|
|
2949
|
+
customSpeedMultiplier: this.data.speedMultiplier,
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
const threeFingerChecker = this.difficultyAttributes.mode === "rebalance"
|
|
2953
|
+
? new RebalanceThreeFingerChecker(this.convertedBeatmap, this.data, this.difficultyAttributes)
|
|
2954
|
+
: new ThreeFingerChecker(this.convertedBeatmap, this.data, this.difficultyAttributes);
|
|
2391
2955
|
const result = threeFingerChecker.check();
|
|
2392
2956
|
this.is3Finger = result.is3Finger;
|
|
2393
2957
|
this.tapPenalty = result.penalty;
|
|
@@ -2419,9 +2983,30 @@ class ReplayAnalyzer {
|
|
|
2419
2983
|
if (!this.beatmap || !this.data || !this.difficultyAttributes) {
|
|
2420
2984
|
return;
|
|
2421
2985
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2986
|
+
if (!this.convertedBeatmap) {
|
|
2987
|
+
const mods = this.data.convertedMods.slice();
|
|
2988
|
+
if ([
|
|
2989
|
+
this.data.forceCS,
|
|
2990
|
+
this.data.forceAR,
|
|
2991
|
+
this.data.forceOD,
|
|
2992
|
+
this.data.forceHP,
|
|
2993
|
+
].some((v) => v !== undefined)) {
|
|
2994
|
+
mods.push(new osuBase.ModDifficultyAdjust({
|
|
2995
|
+
cs: this.data.forceCS,
|
|
2996
|
+
ar: this.data.forceAR,
|
|
2997
|
+
od: this.data.forceOD,
|
|
2998
|
+
hp: this.data.forceHP,
|
|
2999
|
+
}));
|
|
3000
|
+
}
|
|
3001
|
+
this.convertedBeatmap ??= new osuBase.BeatmapConverter(this.beatmap instanceof osuBase.Beatmap
|
|
3002
|
+
? this.beatmap
|
|
3003
|
+
: this.beatmap.beatmap).convert({
|
|
3004
|
+
mode: osuBase.Modes.droid,
|
|
3005
|
+
mods: mods,
|
|
3006
|
+
customSpeedMultiplier: this.data.speedMultiplier,
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
const sliderCheeseChecker = new SliderCheeseChecker(this.convertedBeatmap, this.data, this.difficultyAttributes);
|
|
2425
3010
|
this.sliderCheesePenalty = sliderCheeseChecker.check();
|
|
2426
3011
|
this.hasBeenCheckedForSliderCheesing = true;
|
|
2427
3012
|
}
|
|
@@ -2472,6 +3057,7 @@ class ReplayObjectData {
|
|
|
2472
3057
|
exports.CursorData = CursorData;
|
|
2473
3058
|
exports.CursorOccurrence = CursorOccurrence;
|
|
2474
3059
|
exports.CursorOccurrenceGroup = CursorOccurrenceGroup;
|
|
3060
|
+
exports.RebalanceThreeFingerChecker = RebalanceThreeFingerChecker;
|
|
2475
3061
|
exports.ReplayAnalyzer = ReplayAnalyzer;
|
|
2476
3062
|
exports.ReplayData = ReplayData;
|
|
2477
3063
|
exports.ReplayObjectData = ReplayObjectData;
|