@rian8337/osu-droid-replay-analyzer 2.0.0-alpha.3 → 2.0.0-alpha.32
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 +500 -216
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/typings/index.d.ts +34 -10
- package/dist/ReplayAnalyzer.js +0 -541
- package/dist/ReplayAnalyzer.js.map +0 -1
- package/dist/analysis/BeatmapSectionGenerator.js +0 -48
- package/dist/analysis/BeatmapSectionGenerator.js.map +0 -1
- package/dist/analysis/ThreeFingerChecker.js +0 -676
- package/dist/analysis/ThreeFingerChecker.js.map +0 -1
- package/dist/analysis/TwoHandChecker.js +0 -238
- package/dist/analysis/TwoHandChecker.js.map +0 -1
- package/dist/analysis/data/BeatmapSection.js +0 -18
- package/dist/analysis/data/BeatmapSection.js.map +0 -1
- package/dist/analysis/data/ThreeFingerBeatmapSection.js +0 -16
- package/dist/analysis/data/ThreeFingerBeatmapSection.js.map +0 -1
- package/dist/analysis/objects/IndexedHitObject.js +0 -18
- package/dist/analysis/objects/IndexedHitObject.js.map +0 -1
- package/dist/constants/hitResult.js +0 -26
- package/dist/constants/hitResult.js.map +0 -1
- package/dist/constants/movementType.js +0 -13
- package/dist/constants/movementType.js.map +0 -1
- package/dist/data/CursorData.js +0 -21
- package/dist/data/CursorData.js.map +0 -1
- package/dist/data/ReplayData.js +0 -36
- package/dist/data/ReplayData.js.map +0 -1
- package/dist/data/ReplayObjectData.js +0 -19
- package/dist/data/ReplayObjectData.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -29,25 +29,45 @@ function _interopNamespace(e) {
|
|
|
29
29
|
|
|
30
30
|
var javaDeserialization__namespace = /*#__PURE__*/_interopNamespace(javaDeserialization);
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Represents a cursor's occurrence.
|
|
34
|
+
*/
|
|
35
|
+
class CursorOccurrence {
|
|
36
|
+
/**
|
|
37
|
+
* The time of this occurrence.
|
|
38
|
+
*/
|
|
39
|
+
time;
|
|
40
|
+
/**
|
|
41
|
+
* The position of the occurrence.
|
|
42
|
+
*/
|
|
43
|
+
position;
|
|
44
|
+
/**
|
|
45
|
+
* The movement ID of the occurrence.
|
|
46
|
+
*/
|
|
47
|
+
id;
|
|
48
|
+
constructor(time, x, y, id) {
|
|
49
|
+
this.time = time;
|
|
50
|
+
this.position = new osuBase.Vector2(x, y);
|
|
51
|
+
this.id = id;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
32
55
|
/**
|
|
33
56
|
* Represents a cursor instance in an osu!droid replay.
|
|
34
57
|
*
|
|
35
|
-
* Stores cursor movement data
|
|
58
|
+
* Stores cursor movement data in the form of `CursorOccurrence`s.
|
|
36
59
|
*
|
|
37
60
|
* This is used when analyzing replays using replay analyzer.
|
|
38
61
|
*/
|
|
39
62
|
class CursorData {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
id;
|
|
63
|
+
/**
|
|
64
|
+
* The occurrences of this cursor instance.
|
|
65
|
+
*/
|
|
66
|
+
occurrences = [];
|
|
45
67
|
constructor(values) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.y = values.y;
|
|
50
|
-
this.id = values.id;
|
|
68
|
+
for (let i = 0; i < values.size; ++i) {
|
|
69
|
+
this.occurrences.push(new CursorOccurrence(values.time[i], values.x[i], values.y[i], values.id[i]));
|
|
70
|
+
}
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
73
|
|
|
@@ -327,7 +347,9 @@ class ThreeFingerChecker {
|
|
|
327
347
|
.includes(m.droidString)),
|
|
328
348
|
}).calculate();
|
|
329
349
|
this.hitWindow = new osuBase.DroidHitWindow(stats.od);
|
|
330
|
-
const strainNotes = map.objects.filter(
|
|
350
|
+
const strainNotes = map.objects.filter(
|
|
351
|
+
//@ts-expect-error: No overloads match, but this is fine.
|
|
352
|
+
(v) => v.originalTapStrain >= ThreeFingerChecker.strainThreshold);
|
|
331
353
|
this.strainNoteCount = strainNotes.length;
|
|
332
354
|
}
|
|
333
355
|
/**
|
|
@@ -352,14 +374,16 @@ class ThreeFingerChecker {
|
|
|
352
374
|
}
|
|
353
375
|
this.getAccurateBreakPoints();
|
|
354
376
|
this.filterCursorInstances();
|
|
355
|
-
if (this.downCursorInstances.filter((v) => v.
|
|
377
|
+
if (this.downCursorInstances.filter((v) => v.occurrences.length > 0)
|
|
378
|
+
.length <= 3) {
|
|
356
379
|
return { is3Finger: false, penalty: 1 };
|
|
357
380
|
}
|
|
358
381
|
this.getBeatmapSections();
|
|
359
382
|
this.detectDragPlay();
|
|
360
383
|
this.getDetailedBeatmapSections();
|
|
361
384
|
this.preventAccidentalTaps();
|
|
362
|
-
if (this.downCursorInstances.filter((v) => v.
|
|
385
|
+
if (this.downCursorInstances.filter((v) => v.occurrences.length > 0)
|
|
386
|
+
.length <= 3) {
|
|
363
387
|
return { is3Finger: false, penalty: 1 };
|
|
364
388
|
}
|
|
365
389
|
this.calculateNerfFactors();
|
|
@@ -478,22 +502,18 @@ class ThreeFingerChecker {
|
|
|
478
502
|
y: [],
|
|
479
503
|
id: [],
|
|
480
504
|
});
|
|
481
|
-
for (let j = 0; j < cursorInstance.
|
|
482
|
-
if (cursorInstance.
|
|
505
|
+
for (let j = 0; j < cursorInstance.occurrences.length; ++j) {
|
|
506
|
+
if (cursorInstance.occurrences[j].id !== exports.movementType.DOWN) {
|
|
483
507
|
continue;
|
|
484
508
|
}
|
|
485
|
-
const time = cursorInstance.
|
|
509
|
+
const time = cursorInstance.occurrences[j].time;
|
|
486
510
|
if (time < firstObjectHitTime || time > lastObjectHitTime) {
|
|
487
511
|
continue;
|
|
488
512
|
}
|
|
489
513
|
if (this.breakPointAccurateTimes.some((v) => time >= v.startTime && time <= v.endTime)) {
|
|
490
514
|
continue;
|
|
491
515
|
}
|
|
492
|
-
|
|
493
|
-
newCursorData.time.push(time);
|
|
494
|
-
newCursorData.x.push(cursorInstance.x[j]);
|
|
495
|
-
newCursorData.y.push(cursorInstance.y[j]);
|
|
496
|
-
newCursorData.id.push(cursorInstance.id[j]);
|
|
516
|
+
newCursorData.occurrences.push(new CursorOccurrence(time, cursorInstance.occurrences[j].position.x, cursorInstance.occurrences[j].position.y, cursorInstance.occurrences[j].id));
|
|
497
517
|
}
|
|
498
518
|
this.downCursorInstances.push(newCursorData);
|
|
499
519
|
}
|
|
@@ -579,16 +599,18 @@ class ThreeFingerChecker {
|
|
|
579
599
|
const cursorIndexes = [];
|
|
580
600
|
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
581
601
|
const c = this.data.cursorMovement[i];
|
|
582
|
-
if (c.
|
|
602
|
+
if (c.occurrences.length === 0) {
|
|
583
603
|
continue;
|
|
584
604
|
}
|
|
585
605
|
// Do not include cursors that don't have an occurence in this section
|
|
586
606
|
// this speeds up checking process.
|
|
587
|
-
if (c.
|
|
607
|
+
if (c.occurrences.filter((v) => v.time >= firstObjectMinHitTime &&
|
|
608
|
+
v.time <= lastObjectMaxHitTime).length === 0) {
|
|
588
609
|
continue;
|
|
589
610
|
}
|
|
590
611
|
// If this cursor instance doesn't move, it's not the cursor instance we want.
|
|
591
|
-
if (c.
|
|
612
|
+
if (c.occurrences.filter((v) => v.id === exports.movementType.MOVE)
|
|
613
|
+
.length === 0) {
|
|
592
614
|
continue;
|
|
593
615
|
}
|
|
594
616
|
cursorIndexes.push(i);
|
|
@@ -625,27 +647,28 @@ class ThreeFingerChecker {
|
|
|
625
647
|
// therefore the game emulates the movement between
|
|
626
648
|
// movementType.MOVE cursors.
|
|
627
649
|
const hitTime = o.object.startTime + s.accuracy;
|
|
628
|
-
const nextHitIndex = c.
|
|
650
|
+
const nextHitIndex = c.occurrences.findIndex((v) => v.time >= hitTime);
|
|
629
651
|
const hitIndex = nextHitIndex - 1;
|
|
630
652
|
if (hitIndex <= -1) {
|
|
631
653
|
cursorIndexes[j] = -1;
|
|
632
654
|
continue;
|
|
633
655
|
}
|
|
634
|
-
if (c.
|
|
656
|
+
if (c.occurrences[hitIndex].id === exports.movementType.UP) {
|
|
635
657
|
cursorIndexes[j] = -1;
|
|
636
658
|
continue;
|
|
637
659
|
}
|
|
638
|
-
const cursorPosition = new osuBase.Vector2(c.
|
|
660
|
+
const cursorPosition = new osuBase.Vector2(c.occurrences[hitIndex].position.x, c.occurrences[hitIndex].position.y);
|
|
639
661
|
let isInObject = false;
|
|
640
|
-
if (c.
|
|
641
|
-
c.
|
|
662
|
+
if (c.occurrences[nextHitIndex].id === exports.movementType.MOVE ||
|
|
663
|
+
c.occurrences[hitIndex].id === exports.movementType.MOVE) {
|
|
642
664
|
// Try to interpolate movement between two movementType.MOVE cursor every 1ms.
|
|
643
665
|
// This minimizes rounding error.
|
|
644
|
-
for (let mSecPassed = c.
|
|
645
|
-
const t = (mSecPassed - c.
|
|
646
|
-
(c.
|
|
647
|
-
|
|
648
|
-
cursorPosition.
|
|
666
|
+
for (let mSecPassed = c.occurrences[hitIndex].time; mSecPassed <= c.occurrences[nextHitIndex].time; ++mSecPassed) {
|
|
667
|
+
const t = (mSecPassed - c.occurrences[nextHitIndex].time) /
|
|
668
|
+
(c.occurrences[hitIndex].time -
|
|
669
|
+
c.occurrences[nextHitIndex].time);
|
|
670
|
+
cursorPosition.x = osuBase.Interpolation.lerp(c.occurrences[hitIndex].position.x, c.occurrences[nextHitIndex].position.x, t);
|
|
671
|
+
cursorPosition.y = osuBase.Interpolation.lerp(c.occurrences[hitIndex].position.y, c.occurrences[nextHitIndex].position.y, t);
|
|
649
672
|
if (o.object.stackedPosition.getDistance(cursorPosition) <= o.object.radius) {
|
|
650
673
|
isInObject = true;
|
|
651
674
|
break;
|
|
@@ -718,32 +741,24 @@ class ThreeFingerChecker {
|
|
|
718
741
|
* unnecessary taps.
|
|
719
742
|
*/
|
|
720
743
|
preventAccidentalTaps() {
|
|
721
|
-
let filledCursorAmount = this.downCursorInstances.filter((v) => v.
|
|
744
|
+
let filledCursorAmount = this.downCursorInstances.filter((v) => v.occurrences.length > 0).length;
|
|
722
745
|
if (filledCursorAmount <= 3) {
|
|
723
746
|
return;
|
|
724
747
|
}
|
|
725
748
|
const objects = this.map.objects;
|
|
726
|
-
const totalCursorAmount = this.downCursorInstances.reduce((acc, value) => acc + value.
|
|
749
|
+
const totalCursorAmount = this.downCursorInstances.reduce((acc, value) => acc + value.occurrences.length, 0);
|
|
727
750
|
for (let i = 0; i < this.downCursorInstances.length; ++i) {
|
|
728
751
|
if (filledCursorAmount <= 3) {
|
|
729
752
|
break;
|
|
730
753
|
}
|
|
731
754
|
const cursorInstance = this.downCursorInstances[i];
|
|
732
755
|
// Use an estimation for accidental tap threshold.
|
|
733
|
-
if (cursorInstance.
|
|
756
|
+
if (cursorInstance.occurrences.length <=
|
|
734
757
|
Math.ceil(objects.length / this.accidentalTapThreshold) &&
|
|
735
|
-
cursorInstance.
|
|
758
|
+
cursorInstance.occurrences.length / totalCursorAmount <
|
|
736
759
|
this.threeFingerRatioThreshold * 2) {
|
|
737
760
|
--filledCursorAmount;
|
|
738
|
-
|
|
739
|
-
const prop = property;
|
|
740
|
-
if (Array.isArray(cursorInstance[prop])) {
|
|
741
|
-
cursorInstance[prop].length = 0;
|
|
742
|
-
}
|
|
743
|
-
else {
|
|
744
|
-
cursorInstance[prop] = 0;
|
|
745
|
-
}
|
|
746
|
-
}
|
|
761
|
+
cursorInstance.occurrences.length = 0;
|
|
747
762
|
}
|
|
748
763
|
this.downCursorInstances[i] = cursorInstance;
|
|
749
764
|
}
|
|
@@ -773,13 +788,9 @@ class ThreeFingerChecker {
|
|
|
773
788
|
: this.hitWindow.hitWindowFor50(isPrecise));
|
|
774
789
|
// Filter cursor instances during section.
|
|
775
790
|
this.downCursorInstances.forEach((c) => {
|
|
776
|
-
const i = c.
|
|
791
|
+
const i = c.occurrences.findIndex((t) => t.time >= startTime);
|
|
777
792
|
if (i !== -1) {
|
|
778
|
-
c.
|
|
779
|
-
c.time.splice(0, i);
|
|
780
|
-
c.x.splice(0, i);
|
|
781
|
-
c.y.splice(0, i);
|
|
782
|
-
c.id.splice(0, i);
|
|
793
|
+
c.occurrences = c.occurrences.slice(i);
|
|
783
794
|
}
|
|
784
795
|
});
|
|
785
796
|
const cursorAmounts = [];
|
|
@@ -791,13 +802,13 @@ class ThreeFingerChecker {
|
|
|
791
802
|
}
|
|
792
803
|
const cursorData = this.downCursorInstances[i];
|
|
793
804
|
let amount = 0;
|
|
794
|
-
for (let j = 0; j < cursorData.
|
|
795
|
-
if (cursorData.
|
|
796
|
-
cursorData.
|
|
805
|
+
for (let j = 0; j < cursorData.occurrences.length; ++j) {
|
|
806
|
+
if (cursorData.occurrences[j].time >= startTime &&
|
|
807
|
+
cursorData.occurrences[j].time <= endTime) {
|
|
797
808
|
++amount;
|
|
798
809
|
cursorVectorTimes.push({
|
|
799
|
-
vector: new osuBase.Vector2(cursorData.
|
|
800
|
-
time: cursorData.
|
|
810
|
+
vector: new osuBase.Vector2(cursorData.occurrences[j].position.x, cursorData.occurrences[j].position.y),
|
|
811
|
+
time: cursorData.occurrences[j].time,
|
|
801
812
|
});
|
|
802
813
|
}
|
|
803
814
|
}
|
|
@@ -846,9 +857,7 @@ class ThreeFingerChecker {
|
|
|
846
857
|
// Sort by highest count; assume the order is 3rd, 4th, 5th, ... finger
|
|
847
858
|
const validPresses = similarPresses
|
|
848
859
|
.filter((v) => v.count >= this.cursorDistancingCountThreshold)
|
|
849
|
-
.sort((a, b) =>
|
|
850
|
-
return b.count - a.count;
|
|
851
|
-
})
|
|
860
|
+
.sort((a, b) => b.count - a.count)
|
|
852
861
|
.slice(2);
|
|
853
862
|
// Ignore cursor presses that are only 1 for now since they are very likely to be accidental
|
|
854
863
|
if ((threeFingerRatio > this.threeFingerRatioThreshold &&
|
|
@@ -860,7 +869,10 @@ class ThreeFingerChecker {
|
|
|
860
869
|
1;
|
|
861
870
|
const strainFactor = Math.pow(objects
|
|
862
871
|
.slice(beatmapSection.firstObjectIndex, beatmapSection.lastObjectIndex)
|
|
863
|
-
|
|
872
|
+
//@ts-expect-error: No overloads match, but this is fine.
|
|
873
|
+
.reduce(
|
|
874
|
+
//@ts-expect-error: No overloads match, but this is fine.
|
|
875
|
+
(acc, value) => acc +
|
|
864
876
|
value.originalTapStrain /
|
|
865
877
|
ThreeFingerChecker.strainThreshold, 0), 0.75);
|
|
866
878
|
// We can ignore the first 3 (2 for drag) filled cursor instances
|
|
@@ -872,7 +884,7 @@ class ThreeFingerChecker {
|
|
|
872
884
|
const fingerFactor = threeFingerRatio > this.threeFingerRatioThreshold
|
|
873
885
|
? threeFingerCursorAmounts.reduce((acc, value, index) => acc +
|
|
874
886
|
Math.pow(((index + 1) * value * objectCount) /
|
|
875
|
-
this.strainNoteCount, 0.
|
|
887
|
+
this.strainNoteCount, 0.9), 1)
|
|
876
888
|
: Math.pow(validPresses.reduce((acc, value, index) => acc +
|
|
877
889
|
Math.pow(((index + 1) *
|
|
878
890
|
(value.count /
|
|
@@ -907,20 +919,36 @@ class ThreeFingerChecker {
|
|
|
907
919
|
*/
|
|
908
920
|
class IndexedHitObject {
|
|
909
921
|
/**
|
|
910
|
-
* The index of the cursor that hits the hitobject.
|
|
922
|
+
* The accepted index of the cursor that hits the hitobject.
|
|
923
|
+
*/
|
|
924
|
+
acceptedCursorIndex;
|
|
925
|
+
/**
|
|
926
|
+
* The actual index of the cursor that hits the hitobject.
|
|
911
927
|
*/
|
|
912
|
-
|
|
928
|
+
actualCursorIndex;
|
|
929
|
+
/**
|
|
930
|
+
* The occurrence index of the cursor that hits the hitobject.
|
|
931
|
+
*/
|
|
932
|
+
occurrenceIndex;
|
|
933
|
+
/**
|
|
934
|
+
* If this is a slider, whether the slider was cheesed.
|
|
935
|
+
*/
|
|
936
|
+
sliderCheesed = false;
|
|
913
937
|
/**
|
|
914
938
|
* The underlying difficulty hitobject.
|
|
915
939
|
*/
|
|
916
940
|
object;
|
|
917
941
|
/**
|
|
918
942
|
* @param object The underlying difficulty hitobject.
|
|
919
|
-
* @param
|
|
943
|
+
* @param acceptedCursorIndex The accepted index of the cursor that hits the hitobject.
|
|
944
|
+
* @param actualCursorIndex The actual index of the cursor that hits the hitobject.
|
|
945
|
+
* @param occurrenceIndex The occurrence index of the cursor that hits the hitobject.
|
|
920
946
|
*/
|
|
921
|
-
constructor(object,
|
|
947
|
+
constructor(object, acceptedCursorIndex, actualCursorIndex, occurrenceIndex) {
|
|
922
948
|
this.object = object;
|
|
923
|
-
this.
|
|
949
|
+
this.acceptedCursorIndex = acceptedCursorIndex;
|
|
950
|
+
this.actualCursorIndex = actualCursorIndex;
|
|
951
|
+
this.occurrenceIndex = occurrenceIndex;
|
|
924
952
|
}
|
|
925
953
|
}
|
|
926
954
|
|
|
@@ -936,10 +964,6 @@ class TwoHandChecker {
|
|
|
936
964
|
* The data of the replay.
|
|
937
965
|
*/
|
|
938
966
|
data;
|
|
939
|
-
/**
|
|
940
|
-
* A cursor data array that only contains `movementType.DOWN` and `movementType.MOVE` movement ID occurrences.
|
|
941
|
-
*/
|
|
942
|
-
downMoveCursorInstances = [];
|
|
943
967
|
/**
|
|
944
968
|
* The hitobjects of the beatmap that have been assigned with their respective cursor index.
|
|
945
969
|
*/
|
|
@@ -974,177 +998,412 @@ class TwoHandChecker {
|
|
|
974
998
|
* Checks if a beatmap is two-handed.
|
|
975
999
|
*/
|
|
976
1000
|
check() {
|
|
977
|
-
this.
|
|
978
|
-
|
|
979
|
-
return false;
|
|
1001
|
+
if (this.data.cursorMovement.filter((v) => v.occurrences.length > 0)
|
|
1002
|
+
.length <= 1) {
|
|
1003
|
+
return { is2Hand: false, cursorIndexes: [] };
|
|
980
1004
|
}
|
|
981
1005
|
this.indexHitObjects();
|
|
982
1006
|
this.applyPenalty();
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
* Filters the original cursor instances, returning only those with `movementType.DOWN` and `movementType.MOVE` movement ID.
|
|
987
|
-
*/
|
|
988
|
-
filterCursorInstances() {
|
|
989
|
-
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
990
|
-
const cursorInstance = this.data.cursorMovement[i];
|
|
991
|
-
const newCursorData = {
|
|
992
|
-
size: 0,
|
|
993
|
-
time: [],
|
|
994
|
-
x: [],
|
|
995
|
-
y: [],
|
|
996
|
-
id: [],
|
|
997
|
-
};
|
|
998
|
-
for (let j = 0; j < cursorInstance.size; ++j) {
|
|
999
|
-
if (cursorInstance.id[j] === exports.movementType.UP) {
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1002
|
-
++newCursorData.size;
|
|
1003
|
-
newCursorData.time.push(cursorInstance.time[j]);
|
|
1004
|
-
newCursorData.x.push(cursorInstance.x[j]);
|
|
1005
|
-
newCursorData.y.push(cursorInstance.y[j]);
|
|
1006
|
-
newCursorData.id.push(cursorInstance.id[j]);
|
|
1007
|
-
}
|
|
1008
|
-
this.downMoveCursorInstances.push(newCursorData);
|
|
1007
|
+
const indexes = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
|
|
1008
|
+
for (const object of this.indexedHitObjects) {
|
|
1009
|
+
++indexes[object.acceptedCursorIndex];
|
|
1009
1010
|
}
|
|
1011
|
+
return {
|
|
1012
|
+
is2Hand: indexes.filter((v) => v > 0).length !== 1,
|
|
1013
|
+
cursorIndexes: this.indexedHitObjects.map((v) => v.acceptedCursorIndex),
|
|
1014
|
+
};
|
|
1010
1015
|
}
|
|
1011
1016
|
/**
|
|
1012
1017
|
* Converts hitobjects into indexed hit objects.
|
|
1013
1018
|
*/
|
|
1014
1019
|
indexHitObjects() {
|
|
1015
|
-
const
|
|
1016
|
-
const objectData = this.data.hitObjectData;
|
|
1020
|
+
const hitWindowOffset = this.getHitWindowOffset();
|
|
1017
1021
|
const indexes = [];
|
|
1018
1022
|
for (let i = 0; i < this.map.objects.length; ++i) {
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
indexes.push(index);
|
|
1023
|
-
this.indexedHitObjects.push(new IndexedHitObject(current, index));
|
|
1023
|
+
const indexedHitObject = this.getIndexedHitObject(i, hitWindowOffset);
|
|
1024
|
+
indexes.push(indexedHitObject.acceptedCursorIndex);
|
|
1025
|
+
this.indexedHitObjects.push(indexedHitObject);
|
|
1024
1026
|
}
|
|
1025
|
-
|
|
1026
|
-
const indexCounts = osuBase.Utils.initializeArray(this.downMoveCursorInstances.length, 0);
|
|
1027
|
+
const indexCounts = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
|
|
1027
1028
|
for (const index of indexes) {
|
|
1028
1029
|
if (index === -1) {
|
|
1029
1030
|
continue;
|
|
1030
1031
|
}
|
|
1031
1032
|
++indexCounts[index];
|
|
1032
1033
|
}
|
|
1033
|
-
const mainCursorIndex = indexCounts.
|
|
1034
|
+
const mainCursorIndex = indexCounts.length > 0
|
|
1035
|
+
? indexCounts.indexOf(Math.max(...indexCounts))
|
|
1036
|
+
: 0;
|
|
1034
1037
|
const ignoredCursorIndexes = [];
|
|
1035
1038
|
for (let i = 0; i < indexCounts.length; ++i) {
|
|
1036
|
-
if (indexCounts[i] < this.minCursorIndexCount
|
|
1039
|
+
if (indexCounts[i] < this.minCursorIndexCount &&
|
|
1040
|
+
i !== mainCursorIndex) {
|
|
1037
1041
|
ignoredCursorIndexes.push(i);
|
|
1038
1042
|
}
|
|
1039
1043
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
+
// Add cursor presses that don't fulfill minimum cursor
|
|
1045
|
+
// count to the farthest cursor index that isn't 0.
|
|
1046
|
+
let defaultMinCursorCountIndex = this.data.cursorMovement.length - 1;
|
|
1047
|
+
for (; defaultMinCursorCountIndex > 0; --defaultMinCursorCountIndex) {
|
|
1048
|
+
if (indexCounts[defaultMinCursorCountIndex] >=
|
|
1049
|
+
this.minCursorIndexCount &&
|
|
1050
|
+
!ignoredCursorIndexes.includes(defaultMinCursorCountIndex)) {
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
this.indexedHitObjects.forEach((indexedHitObject, i) => {
|
|
1055
|
+
if (indexedHitObject.acceptedCursorIndex === -1 ||
|
|
1056
|
+
indexedHitObject.actualCursorIndex === -1) {
|
|
1057
|
+
indexedHitObject.acceptedCursorIndex = mainCursorIndex;
|
|
1058
|
+
indexedHitObject.actualCursorIndex = mainCursorIndex;
|
|
1059
|
+
}
|
|
1060
|
+
if (ignoredCursorIndexes.includes(indexedHitObject.acceptedCursorIndex)) {
|
|
1061
|
+
indexedHitObject.acceptedCursorIndex =
|
|
1062
|
+
defaultMinCursorCountIndex;
|
|
1063
|
+
}
|
|
1064
|
+
// For sliders, we need to consider two cases. The first case is when the player doesn't drag
|
|
1065
|
+
// the slider. The second case is when the player drags the slider.
|
|
1066
|
+
if (indexedHitObject.object.object instanceof osuBase.Slider) {
|
|
1067
|
+
indexedHitObject.sliderCheesed = this.checkSliderCheesing(indexedHitObject, this.data.hitObjectData[i], hitWindowOffset);
|
|
1044
1068
|
}
|
|
1045
1069
|
});
|
|
1046
|
-
for (let i = 0; i < this.
|
|
1047
|
-
console.log("Index", i, "count:",
|
|
1070
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1071
|
+
console.log("Index", i, "count:", this.indexedHitObjects.filter((v) => v.acceptedCursorIndex === i).length);
|
|
1048
1072
|
}
|
|
1049
1073
|
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Gets the hit window offset to be applied to `getCursorIndex`.
|
|
1076
|
+
*/
|
|
1077
|
+
getHitWindowOffset() {
|
|
1078
|
+
const deltaTimes = [];
|
|
1079
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1080
|
+
const c = this.data.cursorMovement[i];
|
|
1081
|
+
for (let j = 0; j < c.occurrences.length; ++j) {
|
|
1082
|
+
if (c.occurrences[j].time <
|
|
1083
|
+
this.map.map.hitObjects.objects[0].startTime -
|
|
1084
|
+
this.hitWindow.hitWindowFor50()) {
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
if (c.occurrences[j].id !== exports.movementType.MOVE) {
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
const deltaTime = c.occurrences[j]?.time - c.occurrences[j - 1]?.time || 0;
|
|
1091
|
+
if (deltaTime > 0) {
|
|
1092
|
+
deltaTimes.push(deltaTime);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return Math.min(...deltaTimes);
|
|
1097
|
+
}
|
|
1050
1098
|
/**
|
|
1051
1099
|
* Gets the cursor index that hits the given object.
|
|
1052
1100
|
*
|
|
1053
|
-
* @param
|
|
1054
|
-
* @param
|
|
1101
|
+
* @param index The index of the object to check.
|
|
1102
|
+
* @param hitWindowOffset The offset for hit window to compensate for replay hit inaccuracies.
|
|
1055
1103
|
* @returns The cursor index that hits the given object, -1 if the index is not found, the object is a spinner, or the object was missed.
|
|
1056
1104
|
*/
|
|
1057
|
-
|
|
1105
|
+
getIndexedHitObject(index, hitWindowOffset) {
|
|
1106
|
+
const object = this.map.objects[index];
|
|
1107
|
+
const data = this.data.hitObjectData[index];
|
|
1058
1108
|
if (object.object instanceof osuBase.Spinner ||
|
|
1059
1109
|
data.result === exports.hitResult.RESULT_0) {
|
|
1060
|
-
return -1;
|
|
1110
|
+
return new IndexedHitObject(object, -1, -1, -1);
|
|
1061
1111
|
}
|
|
1062
1112
|
const isPrecise = this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise);
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1113
|
+
// For sliders, automatically set hit window to be as lenient as possible.
|
|
1114
|
+
let hitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
|
|
1115
|
+
if (!(object.object instanceof osuBase.Slider)) {
|
|
1116
|
+
switch (data.result) {
|
|
1117
|
+
case exports.hitResult.RESULT_300:
|
|
1118
|
+
hitWindowLength = this.hitWindow.hitWindowFor300(isPrecise);
|
|
1119
|
+
break;
|
|
1120
|
+
case exports.hitResult.RESULT_100:
|
|
1121
|
+
hitWindowLength = this.hitWindow.hitWindowFor100(isPrecise);
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1073
1124
|
}
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
const minimumHitTime =
|
|
1125
|
+
const startTime = object.object.startTime;
|
|
1126
|
+
const hitTime = startTime + data.accuracy;
|
|
1127
|
+
const minimumHitTime = startTime - hitWindowLength - hitWindowOffset;
|
|
1128
|
+
const maximumHitTime = startTime + hitWindowLength + hitWindowOffset;
|
|
1077
1129
|
const cursorInformations = [];
|
|
1078
|
-
for (let i = 0; i < this.
|
|
1079
|
-
const c = this.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1130
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1131
|
+
const c = this.data.cursorMovement[i];
|
|
1132
|
+
if (c.occurrences.length === 0) {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
let hitTimeBeforeIndex = osuBase.MathUtils.clamp(c.occurrences.findIndex((v) => v.time >= minimumHitTime), 1, c.occurrences.length - 1) - 1;
|
|
1136
|
+
let hitTimeAfterIndex = c.occurrences.findIndex(
|
|
1137
|
+
// There is a special case for sliders where the time leniency in droid is a lot bigger compared to PC.
|
|
1138
|
+
// To prevent slider end time from ending earlier than hit window leniency, we use the maximum value between both.
|
|
1139
|
+
(v) => v.time >= Math.max(object.object.endTime, maximumHitTime));
|
|
1140
|
+
if (hitTimeAfterIndex === -1) {
|
|
1141
|
+
// Maximum hit time or object end time may be out of bounds for every presses.
|
|
1142
|
+
// We set the index to the latest cursor occurrence if that happens.
|
|
1143
|
+
hitTimeAfterIndex = c.occurrences.length;
|
|
1144
|
+
}
|
|
1145
|
+
--hitTimeAfterIndex;
|
|
1146
|
+
// Sometimes a `movementType.UP` instance occurs at the same time as a `movementType.MOVE`
|
|
1147
|
+
// or a cursor is recorded twice in one time, therefore this check is required.
|
|
1148
|
+
while (c.occurrences[hitTimeBeforeIndex]?.time ===
|
|
1149
|
+
c.occurrences[hitTimeBeforeIndex - 1]?.time &&
|
|
1150
|
+
hitTimeBeforeIndex > 0) {
|
|
1151
|
+
--hitTimeBeforeIndex;
|
|
1152
|
+
}
|
|
1153
|
+
// We track the cursor movement along those indexes.
|
|
1154
|
+
// Current cursor position is in `hitTimeBeforeIndex`.
|
|
1155
|
+
let distance = Number.POSITIVE_INFINITY;
|
|
1156
|
+
let j = hitTimeBeforeIndex;
|
|
1157
|
+
for (j; j <= hitTimeAfterIndex; ++j) {
|
|
1158
|
+
const occurrence = c.occurrences[j];
|
|
1159
|
+
const nextOccurrence = c.occurrences[j + 1];
|
|
1160
|
+
const cursorPosition = occurrence.position;
|
|
1161
|
+
if (occurrence.time < minimumHitTime &&
|
|
1162
|
+
nextOccurrence?.id !== exports.movementType.MOVE) {
|
|
1084
1163
|
continue;
|
|
1085
1164
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1165
|
+
if (occurrence.time > hitTime + hitWindowOffset) {
|
|
1166
|
+
// Set distance to minimum just for the last.
|
|
1167
|
+
if (occurrence.id !== exports.movementType.UP) {
|
|
1168
|
+
distance = Math.min(distance, object.object.stackedPosition.getDistance(cursorPosition));
|
|
1169
|
+
}
|
|
1170
|
+
break;
|
|
1171
|
+
}
|
|
1172
|
+
if (occurrence.id === exports.movementType.UP) {
|
|
1089
1173
|
continue;
|
|
1090
1174
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1175
|
+
distance =
|
|
1176
|
+
object.object.stackedPosition.getDistance(cursorPosition);
|
|
1177
|
+
if (nextOccurrence?.id === exports.movementType.MOVE &&
|
|
1178
|
+
occurrence.time !== nextOccurrence.time &&
|
|
1179
|
+
!occurrence.position.equals(nextOccurrence.position)) {
|
|
1180
|
+
// If next cursor is a `move` instance and it doesn't go out of time
|
|
1181
|
+
// range, we interpolate cursor position between two occurrences.
|
|
1182
|
+
const nextPosition = nextOccurrence.position;
|
|
1183
|
+
const displacement = nextPosition.subtract(cursorPosition);
|
|
1184
|
+
for (let mSecPassed = Math.max(minimumHitTime, occurrence.time); mSecPassed <= Math.min(hitTime, nextOccurrence.time); ++mSecPassed) {
|
|
1185
|
+
const progress = (mSecPassed - occurrence.time) /
|
|
1186
|
+
(nextOccurrence.time - occurrence.time);
|
|
1187
|
+
distance = object.object.stackedPosition.getDistance(cursorPosition.add(displacement.scale(progress)));
|
|
1188
|
+
}
|
|
1093
1189
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1190
|
+
}
|
|
1191
|
+
if (distance > object.object.radius) {
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
// The case for a one-handed object is that there will be a slight movement in the cursor towards
|
|
1195
|
+
// the next object in fast patterns. We should not be worried about slow patterns as they will only
|
|
1196
|
+
// make a minimal difference and aim strain threshold should filter them out.
|
|
1197
|
+
// In order to verify if the player does that, we check if the movement towards the next
|
|
1198
|
+
// object is sufficient enough to be two-handed. This is done by checking if the movement
|
|
1199
|
+
// from the current object to the next object and the movement from the current object
|
|
1200
|
+
// to the significant move cursor occurrence produces an angle that is acute enough.
|
|
1201
|
+
// let isAngleFulfilled: boolean = false;
|
|
1202
|
+
// Aside of angles, we need to consider if the player dragged from the previous object to the current object.
|
|
1203
|
+
let isDragged = false;
|
|
1204
|
+
// Get the latest down or movement cursor occurrence.
|
|
1205
|
+
while (c.occurrences[j]?.id === exports.movementType.UP &&
|
|
1206
|
+
j > hitTimeBeforeIndex) {
|
|
1207
|
+
--j;
|
|
1208
|
+
}
|
|
1209
|
+
if (object.object instanceof osuBase.Circle) {
|
|
1210
|
+
// For circles, we only need to consider the actual press on the circle.
|
|
1211
|
+
// Therefore, we need to get the latest down cursor occurrence instead.
|
|
1212
|
+
while (c.occurrences[j]?.id !== exports.movementType.DOWN &&
|
|
1213
|
+
j > hitTimeBeforeIndex) {
|
|
1214
|
+
--j;
|
|
1099
1215
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1216
|
+
}
|
|
1217
|
+
// Theoretically there can only be 1 up occurrence, but this is a
|
|
1218
|
+
// consideration if the user manually adds cursor occurrences.
|
|
1219
|
+
if (c.occurrences[j]?.id === exports.movementType.UP) {
|
|
1220
|
+
++j;
|
|
1221
|
+
}
|
|
1222
|
+
// Some move instances move in the exact same place. Not sure why, most likely
|
|
1223
|
+
// because the position is recorded as int in the game and the movement is too small to
|
|
1224
|
+
// convert into +1 or -1.
|
|
1225
|
+
// let nextSignificantOccurrenceIndex: number = j + 1;
|
|
1226
|
+
// while (
|
|
1227
|
+
// c.occurrences[j] &&
|
|
1228
|
+
// c.occurrences[nextSignificantOccurrenceIndex] &&
|
|
1229
|
+
// c.occurrences[j].position.equals(
|
|
1230
|
+
// c.occurrences[nextSignificantOccurrenceIndex].position
|
|
1231
|
+
// )
|
|
1232
|
+
// ) {
|
|
1233
|
+
// ++nextSignificantOccurrenceIndex;
|
|
1234
|
+
// }
|
|
1235
|
+
// const nextSignificantOccurrence: CursorOccurrence =
|
|
1236
|
+
// c.occurrences[nextSignificantOccurrenceIndex];
|
|
1237
|
+
// const next: DifficultyHitObject | RebalanceDifficultyHitObject =
|
|
1238
|
+
// this.map.objects[index + 1];
|
|
1239
|
+
// Angle detection.
|
|
1240
|
+
/* if (nextSignificantOccurrence?.id === movementType.MOVE && next) {
|
|
1241
|
+
// Get the object's actual end position.
|
|
1242
|
+
let actualEndPosition: Vector2 =
|
|
1243
|
+
object.object.stackedEndPosition;
|
|
1244
|
+
|
|
1245
|
+
if (
|
|
1246
|
+
object.object instanceof Slider &&
|
|
1247
|
+
object.object.lazyEndPosition
|
|
1248
|
+
) {
|
|
1249
|
+
// For sliders, we take the closest distance between the lazy end position
|
|
1250
|
+
// and stacked end position towards the next significant cursor occurrence.
|
|
1251
|
+
// This assumes that the player takes the simpler movement.
|
|
1252
|
+
const lazyEndDistance: number =
|
|
1253
|
+
object.object.lazyEndPosition.getDistance(
|
|
1254
|
+
nextSignificantOccurrence.position
|
|
1255
|
+
);
|
|
1256
|
+
const actualEndDistance: number =
|
|
1257
|
+
object.object.stackedEndPosition.getDistance(
|
|
1258
|
+
nextSignificantOccurrence.position
|
|
1259
|
+
);
|
|
1260
|
+
|
|
1261
|
+
if (lazyEndDistance < actualEndDistance) {
|
|
1262
|
+
actualEndPosition = object.object.lazyEndPosition;
|
|
1117
1263
|
}
|
|
1118
1264
|
}
|
|
1265
|
+
|
|
1266
|
+
// Get the movement vector towards the next cursor occurrence by subtracting
|
|
1267
|
+
// the next cursor occurrence with the object's position.
|
|
1268
|
+
const movementVec: Vector2 =
|
|
1269
|
+
nextSignificantOccurrence.position.subtract(
|
|
1270
|
+
actualEndPosition
|
|
1271
|
+
);
|
|
1272
|
+
|
|
1273
|
+
const currentToNext: Vector2 =
|
|
1274
|
+
next.object.stackedPosition.subtract(actualEndPosition);
|
|
1275
|
+
|
|
1276
|
+
const dot: number = currentToNext.dot(movementVec);
|
|
1277
|
+
const det: number =
|
|
1278
|
+
currentToNext.x * movementVec.y -
|
|
1279
|
+
currentToNext.y * movementVec.x;
|
|
1280
|
+
|
|
1281
|
+
const movementToNextAngle: number = Math.abs(
|
|
1282
|
+
Math.atan2(det, dot)
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1285
|
+
isAngleFulfilled = movementToNextAngle < Math.PI / 6;
|
|
1286
|
+
} */
|
|
1287
|
+
// Dragging detection.
|
|
1288
|
+
let dragTimeThreshold = 0;
|
|
1289
|
+
const prev = this.map.objects[index - 1];
|
|
1290
|
+
if (prev) {
|
|
1291
|
+
// The previous object might be a slider, so we need to get
|
|
1292
|
+
// the hit data of it to get an accurate time threshold.
|
|
1293
|
+
const prevData = this.data.hitObjectData[index - 1];
|
|
1294
|
+
dragTimeThreshold = prev.object.startTime;
|
|
1295
|
+
if (!(prev.object instanceof osuBase.Spinner)) {
|
|
1296
|
+
dragTimeThreshold += prevData.accuracy;
|
|
1297
|
+
}
|
|
1298
|
+
if (prev.object instanceof osuBase.Slider) {
|
|
1299
|
+
dragTimeThreshold = Math.max(dragTimeThreshold, prev.object.endTime);
|
|
1300
|
+
}
|
|
1119
1301
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1302
|
+
let occurrenceStartIndex = hitTimeAfterIndex;
|
|
1303
|
+
while (c.occurrences[occurrenceStartIndex]?.time >=
|
|
1304
|
+
dragTimeThreshold &&
|
|
1305
|
+
occurrenceStartIndex > 0) {
|
|
1306
|
+
--occurrenceStartIndex;
|
|
1125
1307
|
}
|
|
1308
|
+
// The above loop will make the start index before or right when
|
|
1309
|
+
// the previous object was hit or ended, but we want the index after it.
|
|
1310
|
+
++occurrenceStartIndex;
|
|
1311
|
+
const dragOccurrences = c.occurrences.slice(occurrenceStartIndex, hitTimeAfterIndex);
|
|
1312
|
+
isDragged =
|
|
1313
|
+
dragOccurrences.length > 0 &&
|
|
1314
|
+
// We only care when the cursor approaches the current object. It doesn't
|
|
1315
|
+
// matter whether the previous object was pressed or dragged.
|
|
1316
|
+
dragOccurrences
|
|
1317
|
+
.at(-1)
|
|
1318
|
+
.position.getDistance(object.object.stackedPosition) <=
|
|
1319
|
+
object.object.radius &&
|
|
1320
|
+
dragOccurrences.every((v) => v.id === exports.movementType.MOVE);
|
|
1321
|
+
cursorInformations.push({
|
|
1322
|
+
// If the angle is fulfilled or the player dragged,
|
|
1323
|
+
// we set the cursor index to the main cursor index.
|
|
1324
|
+
acceptedCursorIndex: /* isAngleFulfilled || */ isDragged
|
|
1325
|
+
? -1
|
|
1326
|
+
: i,
|
|
1327
|
+
actualCursorIndex: i,
|
|
1328
|
+
occurrenceIndex: j,
|
|
1329
|
+
distanceDiff: distance,
|
|
1330
|
+
});
|
|
1126
1331
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1332
|
+
// Cursors have been filtered to see which of them is inside the object.
|
|
1333
|
+
// Now we look at which cursor is closest to the center of the object.
|
|
1334
|
+
const minDistanceDiff = Math.min(...cursorInformations.map((v) => v.distanceDiff));
|
|
1335
|
+
const acceptedCursorInformation = cursorInformations.find((c) => c.distanceDiff === minDistanceDiff);
|
|
1336
|
+
return new IndexedHitObject(object, acceptedCursorInformation?.acceptedCursorIndex ?? -1, acceptedCursorInformation?.actualCursorIndex ?? -1, acceptedCursorInformation?.occurrenceIndex ?? -1);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Checks whether a slider was cheesed.
|
|
1340
|
+
*
|
|
1341
|
+
* This is done by checking if a cursor follows a slider all the way to its end position.
|
|
1342
|
+
*
|
|
1343
|
+
* @param indexedHitObject The indexed slider.
|
|
1344
|
+
* @param hitData The hit data of the slider.
|
|
1345
|
+
* @param actualCursorIndex The actual cursor index that hit the slider.
|
|
1346
|
+
* @param hitWindowOffset The offset that was calculated by `getHitWindowOffset()`
|
|
1347
|
+
* @returns Whether the slider was cheesed.
|
|
1348
|
+
*/
|
|
1349
|
+
checkSliderCheesing(indexedHitObject, hitData, hitWindowOffset) {
|
|
1350
|
+
if (!(indexedHitObject.object.object instanceof osuBase.Slider) ||
|
|
1351
|
+
hitData.result === exports.hitResult.RESULT_0) {
|
|
1352
|
+
return false;
|
|
1129
1353
|
}
|
|
1130
|
-
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1354
|
+
let cursorLoopIndex = Math.max(0, indexedHitObject.occurrenceIndex);
|
|
1355
|
+
const c = this.data.cursorMovement[indexedHitObject.actualCursorIndex];
|
|
1356
|
+
const acceptableRadius = indexedHitObject.object.object.radius * 2.4;
|
|
1357
|
+
for (let i = 1; i < indexedHitObject.object.object.nestedHitObjects.length; ++i) {
|
|
1358
|
+
const tickWasHit = hitData.tickset[i - 1];
|
|
1359
|
+
if (!tickWasHit) {
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
const object = indexedHitObject.object.object.nestedHitObjects[i];
|
|
1363
|
+
let j = cursorLoopIndex;
|
|
1364
|
+
let cursorHitTick = false;
|
|
1365
|
+
for (j; j < c.occurrences.length; ++j) {
|
|
1366
|
+
if (c.occurrences[j].time <
|
|
1367
|
+
object.startTime - hitWindowOffset) {
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (c.occurrences[j].time >
|
|
1371
|
+
object.startTime + hitWindowOffset) {
|
|
1372
|
+
break;
|
|
1373
|
+
}
|
|
1374
|
+
if (c.occurrences[j].position.getDistance(object.stackedPosition) <= acceptableRadius) {
|
|
1375
|
+
cursorHitTick = true;
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (!cursorHitTick) {
|
|
1380
|
+
return true;
|
|
1381
|
+
}
|
|
1382
|
+
cursorLoopIndex = j;
|
|
1383
|
+
}
|
|
1384
|
+
return false;
|
|
1136
1385
|
}
|
|
1137
1386
|
/**
|
|
1138
1387
|
* Applies penalty to the original star rating instance.
|
|
1139
1388
|
*/
|
|
1140
1389
|
applyPenalty() {
|
|
1141
|
-
const beatmaps = new Array(this.
|
|
1390
|
+
const beatmaps = new Array(this.data.cursorMovement.length);
|
|
1142
1391
|
this.indexedHitObjects.forEach((o) => {
|
|
1143
|
-
if (!beatmaps[o.
|
|
1392
|
+
if (!beatmaps[o.acceptedCursorIndex]) {
|
|
1144
1393
|
const map = osuBase.Utils.deepCopy(this.map.map);
|
|
1145
|
-
|
|
1394
|
+
map.hitObjects.clear();
|
|
1395
|
+
beatmaps[o.acceptedCursorIndex] = map;
|
|
1146
1396
|
}
|
|
1147
|
-
beatmaps[o.
|
|
1397
|
+
beatmaps[o.acceptedCursorIndex].hitObjects.add(o.object.object);
|
|
1398
|
+
});
|
|
1399
|
+
// Preserve some values that aren't reasonable for them to be changed.
|
|
1400
|
+
const preservedValues = this.map.objects.map((v) => {
|
|
1401
|
+
return {
|
|
1402
|
+
noteDensity: v.noteDensity,
|
|
1403
|
+
overlappingFactor: v.overlappingFactor,
|
|
1404
|
+
rhythmStrain: v.rhythmStrain,
|
|
1405
|
+
rhythmMultiplier: v.rhythmMultiplier,
|
|
1406
|
+
};
|
|
1148
1407
|
});
|
|
1149
1408
|
this.map.objects.length = 0;
|
|
1150
1409
|
beatmaps.forEach((beatmap) => {
|
|
@@ -1155,15 +1414,32 @@ class TwoHandChecker {
|
|
|
1155
1414
|
starRating.map = beatmap;
|
|
1156
1415
|
starRating.generateDifficultyHitObjects();
|
|
1157
1416
|
starRating.objects[0].deltaTime =
|
|
1158
|
-
starRating.objects[0].
|
|
1159
|
-
this.indexedHitObjects[0].object.
|
|
1160
|
-
starRating.objects[0].strainTime = Math.max(
|
|
1417
|
+
starRating.objects[0].startTime -
|
|
1418
|
+
this.indexedHitObjects[0].object.startTime;
|
|
1419
|
+
starRating.objects[0].strainTime = Math.max(25, starRating.objects[0].deltaTime);
|
|
1161
1420
|
this.map.objects.push(...starRating.objects);
|
|
1162
1421
|
});
|
|
1163
|
-
this.map.objects.sort((a, b) =>
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1422
|
+
this.map.objects.sort((a, b) => a.startTime - b.startTime);
|
|
1423
|
+
// Reassign preserved values before calculating.
|
|
1424
|
+
for (let i = 0; i < this.map.objects.length; ++i) {
|
|
1425
|
+
const diffObject = this.map.objects[i];
|
|
1426
|
+
const indexedHitObject = this.indexedHitObjects[i];
|
|
1427
|
+
const preservedValue = preservedValues[i];
|
|
1428
|
+
diffObject.noteDensity = preservedValue.noteDensity;
|
|
1429
|
+
diffObject.overlappingFactor = preservedValue.overlappingFactor;
|
|
1430
|
+
diffObject.rhythmStrain = preservedValue.rhythmStrain;
|
|
1431
|
+
diffObject.rhythmMultiplier = preservedValue.rhythmMultiplier;
|
|
1432
|
+
// Set slider travel distance to 0 if the slider was cheesed.
|
|
1433
|
+
if (indexedHitObject.sliderCheesed) {
|
|
1434
|
+
diffObject.travelDistance = 0;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
// Do not include rhythm skill.
|
|
1438
|
+
this.map.calculateAim();
|
|
1439
|
+
this.map.calculateTap();
|
|
1440
|
+
this.map.calculateFlashlight();
|
|
1441
|
+
this.map.calculateVisual();
|
|
1442
|
+
this.map.calculateTotal();
|
|
1167
1443
|
}
|
|
1168
1444
|
}
|
|
1169
1445
|
|
|
@@ -1219,6 +1495,12 @@ class ReplayAnalyzer {
|
|
|
1219
1495
|
* Whether this replay has been checked against 2 hand usage.
|
|
1220
1496
|
*/
|
|
1221
1497
|
hasBeenCheckedFor2Hand = false;
|
|
1498
|
+
/**
|
|
1499
|
+
* The cursor indexes at which each object was hit.
|
|
1500
|
+
*
|
|
1501
|
+
* This is filled after 2 hand usage has been checked.
|
|
1502
|
+
*/
|
|
1503
|
+
twoHandCursorIndexes = [];
|
|
1222
1504
|
// Sizes of primitive data types in Java (in bytes)
|
|
1223
1505
|
BYTE_LENGTH = 1;
|
|
1224
1506
|
SHORT_LENGTH = 2;
|
|
@@ -1404,42 +1686,41 @@ class ReplayAnalyzer {
|
|
|
1404
1686
|
for (let x = 0; x < size; x++) {
|
|
1405
1687
|
const moveSize = replayDataBuffer.readInt32BE(bufferCounter);
|
|
1406
1688
|
bufferCounter += this.INT_LENGTH;
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
y: [],
|
|
1412
|
-
id: [],
|
|
1413
|
-
};
|
|
1689
|
+
const time = [];
|
|
1690
|
+
const x = [];
|
|
1691
|
+
const y = [];
|
|
1692
|
+
const id = [];
|
|
1414
1693
|
for (let i = 0; i < moveSize; i++) {
|
|
1415
|
-
|
|
1694
|
+
time[i] = replayDataBuffer.readInt32BE(bufferCounter);
|
|
1416
1695
|
bufferCounter += this.INT_LENGTH;
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
if (
|
|
1696
|
+
id[i] = time[i] & 3;
|
|
1697
|
+
time[i] >>= 2;
|
|
1698
|
+
if (id[i] !== exports.movementType.UP) {
|
|
1420
1699
|
if (resultObject.replayVersion >= 5) {
|
|
1421
|
-
|
|
1422
|
-
replayDataBuffer.readFloatBE(bufferCounter);
|
|
1700
|
+
x[i] = replayDataBuffer.readFloatBE(bufferCounter);
|
|
1423
1701
|
bufferCounter += this.FLOAT_LENGTH;
|
|
1424
|
-
|
|
1425
|
-
replayDataBuffer.readFloatBE(bufferCounter);
|
|
1702
|
+
y[i] = replayDataBuffer.readFloatBE(bufferCounter);
|
|
1426
1703
|
bufferCounter += this.FLOAT_LENGTH;
|
|
1427
1704
|
}
|
|
1428
1705
|
else {
|
|
1429
|
-
|
|
1430
|
-
replayDataBuffer.readInt16BE(bufferCounter);
|
|
1706
|
+
x[i] = replayDataBuffer.readInt16BE(bufferCounter);
|
|
1431
1707
|
bufferCounter += this.SHORT_LENGTH;
|
|
1432
|
-
|
|
1433
|
-
replayDataBuffer.readInt16BE(bufferCounter);
|
|
1708
|
+
y[i] = replayDataBuffer.readInt16BE(bufferCounter);
|
|
1434
1709
|
bufferCounter += this.SHORT_LENGTH;
|
|
1435
1710
|
}
|
|
1436
1711
|
}
|
|
1437
1712
|
else {
|
|
1438
|
-
|
|
1439
|
-
|
|
1713
|
+
x[i] = -1;
|
|
1714
|
+
y[i] = -1;
|
|
1440
1715
|
}
|
|
1441
1716
|
}
|
|
1442
|
-
resultObject.cursorMovement.push(
|
|
1717
|
+
resultObject.cursorMovement.push(new CursorData({
|
|
1718
|
+
size: moveSize,
|
|
1719
|
+
time: time,
|
|
1720
|
+
x: x,
|
|
1721
|
+
y: y,
|
|
1722
|
+
id: id,
|
|
1723
|
+
}));
|
|
1443
1724
|
}
|
|
1444
1725
|
const replayObjectLength = replayDataBuffer.readInt32BE(bufferCounter);
|
|
1445
1726
|
bufferCounter += this.INT_LENGTH;
|
|
@@ -1685,7 +1966,9 @@ class ReplayAnalyzer {
|
|
|
1685
1966
|
return;
|
|
1686
1967
|
}
|
|
1687
1968
|
const twoHandChecker = new TwoHandChecker(this.map, this.data);
|
|
1688
|
-
|
|
1969
|
+
const result = twoHandChecker.check();
|
|
1970
|
+
this.is2Hand = result.is2Hand;
|
|
1971
|
+
this.twoHandCursorIndexes = result.cursorIndexes;
|
|
1689
1972
|
this.hasBeenCheckedFor2Hand = true;
|
|
1690
1973
|
}
|
|
1691
1974
|
}
|
|
@@ -1720,6 +2003,7 @@ class ReplayObjectData {
|
|
|
1720
2003
|
}
|
|
1721
2004
|
|
|
1722
2005
|
exports.CursorData = CursorData;
|
|
2006
|
+
exports.CursorOccurrence = CursorOccurrence;
|
|
1723
2007
|
exports.ReplayAnalyzer = ReplayAnalyzer;
|
|
1724
2008
|
exports.ReplayData = ReplayData;
|
|
1725
2009
|
exports.ReplayObjectData = ReplayObjectData;
|