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