@rian8337/osu-droid-replay-analyzer 2.0.0-alpha.4 → 2.0.0-alpha.7
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 +310 -190
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/typings/index.d.ts +653 -650
- 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
|
}
|
|
@@ -936,10 +945,6 @@ class TwoHandChecker {
|
|
|
936
945
|
* The data of the replay.
|
|
937
946
|
*/
|
|
938
947
|
data;
|
|
939
|
-
/**
|
|
940
|
-
* A cursor data array that only contains `movementType.DOWN` and `movementType.MOVE` movement ID occurrences.
|
|
941
|
-
*/
|
|
942
|
-
downMoveCursorInstances = [];
|
|
943
948
|
/**
|
|
944
949
|
* The hitobjects of the beatmap that have been assigned with their respective cursor index.
|
|
945
950
|
*/
|
|
@@ -955,6 +960,18 @@ class TwoHandChecker {
|
|
|
955
960
|
* those that do not worth any strain.
|
|
956
961
|
*/
|
|
957
962
|
minCursorIndexCount = 5;
|
|
963
|
+
/**
|
|
964
|
+
* The approximated difficulty of the current object such that the object is likely to be 2-handed by a player.
|
|
965
|
+
*
|
|
966
|
+
* This scales with an object's angle and speed relative to the previous object.
|
|
967
|
+
* Acute angles or fast speed will accumulate this number. Conversely, wide angles or slow speed will decay this number.
|
|
968
|
+
*/
|
|
969
|
+
currentAngleDiffApproxDefault = 10;
|
|
970
|
+
/**
|
|
971
|
+
* The threshold at which objects will be started getting considered to be 2-handable.
|
|
972
|
+
*/
|
|
973
|
+
currentAngleDiffApproxThreshold = 200;
|
|
974
|
+
assignCurrentIndexToOne = false;
|
|
958
975
|
/**
|
|
959
976
|
* @param map The beatmap to analyze.
|
|
960
977
|
* @param data The data of the replay.
|
|
@@ -974,56 +991,35 @@ class TwoHandChecker {
|
|
|
974
991
|
* Checks if a beatmap is two-handed.
|
|
975
992
|
*/
|
|
976
993
|
check() {
|
|
977
|
-
this.
|
|
978
|
-
|
|
994
|
+
if (this.data.cursorMovement.filter((v) => v.occurrences.length > 0)
|
|
995
|
+
.length <= 1) {
|
|
979
996
|
return false;
|
|
980
997
|
}
|
|
981
998
|
this.indexHitObjects();
|
|
982
999
|
this.applyPenalty();
|
|
983
1000
|
return true;
|
|
984
1001
|
}
|
|
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);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
1002
|
/**
|
|
1012
1003
|
* Converts hitobjects into indexed hit objects.
|
|
1013
1004
|
*/
|
|
1014
1005
|
indexHitObjects() {
|
|
1015
|
-
const
|
|
1016
|
-
const objectData = this.data.hitObjectData;
|
|
1006
|
+
const hitWindowOffset = this.getHitWindowOffset();
|
|
1017
1007
|
const indexes = [];
|
|
1008
|
+
let overallDiffApprox = this.currentAngleDiffApproxDefault;
|
|
1018
1009
|
for (let i = 0; i < this.map.objects.length; ++i) {
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
const index = this.
|
|
1010
|
+
const diff = i > 0 ? this.getSpacingAngleDiffApprox(i) : 1;
|
|
1011
|
+
overallDiffApprox = osuBase.MathUtils.clamp(overallDiffApprox * diff, this.currentAngleDiffApproxDefault, this.currentAngleDiffApproxThreshold + 100);
|
|
1012
|
+
const index = overallDiffApprox >= this.currentAngleDiffApproxThreshold
|
|
1013
|
+
? this.getCursorIndex(i, hitWindowOffset)
|
|
1014
|
+
: 0;
|
|
1022
1015
|
indexes.push(index);
|
|
1023
|
-
this.indexedHitObjects.push(new IndexedHitObject(
|
|
1016
|
+
this.indexedHitObjects.push(new IndexedHitObject(this.map.objects[i], index));
|
|
1024
1017
|
}
|
|
1018
|
+
// const notFound = this.indexedHitObjects.filter(v => v.cursorIndex === -1);
|
|
1019
|
+
console.log("Spinners:", this.map.map.hitObjects.spinners);
|
|
1020
|
+
console.log("Misses:", this.data.accuracy.nmiss);
|
|
1025
1021
|
console.log(indexes.filter((v) => v !== -1).length, "cursors found,", indexes.filter((v) => v === -1).length, "not found");
|
|
1026
|
-
const indexCounts = osuBase.Utils.initializeArray(this.
|
|
1022
|
+
const indexCounts = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
|
|
1027
1023
|
for (const index of indexes) {
|
|
1028
1024
|
if (index === -1) {
|
|
1029
1025
|
continue;
|
|
@@ -1043,105 +1039,231 @@ class TwoHandChecker {
|
|
|
1043
1039
|
indexedHitObject.cursorIndex = mainCursorIndex;
|
|
1044
1040
|
}
|
|
1045
1041
|
});
|
|
1046
|
-
for (let i = 0; i < this.
|
|
1042
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1047
1043
|
console.log("Index", i, "count:", indexes.filter((v) => v === i).length);
|
|
1048
1044
|
}
|
|
1049
1045
|
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Gets the approximation of an object's difficulty such that the object is likely to be 2-handed.
|
|
1048
|
+
*
|
|
1049
|
+
* @param index The index of the object.
|
|
1050
|
+
*/
|
|
1051
|
+
getSpacingAngleDiffApprox(index) {
|
|
1052
|
+
const object = this.map.objects[index];
|
|
1053
|
+
if (object.object instanceof osuBase.Spinner) {
|
|
1054
|
+
return 0.1;
|
|
1055
|
+
}
|
|
1056
|
+
const angleDiff = object.angle !== null
|
|
1057
|
+
? 0.5 + Math.pow(Math.cos(object.angle / 2), 2)
|
|
1058
|
+
: 1;
|
|
1059
|
+
const speedDiff = object.lazyJumpDistance / Math.max(1, object.deltaTime);
|
|
1060
|
+
// Make decay slower.
|
|
1061
|
+
return Math.max(0.8, angleDiff * speedDiff);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Gets the hit window offset to be applied to `getCursorIndex`.
|
|
1065
|
+
*/
|
|
1066
|
+
getHitWindowOffset() {
|
|
1067
|
+
const deltaTimes = [];
|
|
1068
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1069
|
+
const c = this.data.cursorMovement[i];
|
|
1070
|
+
for (let j = 0; j < c.occurrences.length; ++j) {
|
|
1071
|
+
if (c.occurrences[j].time <
|
|
1072
|
+
this.map.map.hitObjects.objects[0].startTime -
|
|
1073
|
+
this.hitWindow.hitWindowFor50()) {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (c.occurrences[j].id !== exports.movementType.MOVE) {
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
const deltaTime = c.occurrences[j]?.time - c.occurrences[j - 1]?.time || 0;
|
|
1080
|
+
if (deltaTime > 0) {
|
|
1081
|
+
deltaTimes.push(deltaTime);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return Math.min(...deltaTimes);
|
|
1086
|
+
}
|
|
1050
1087
|
/**
|
|
1051
1088
|
* Gets the cursor index that hits the given object.
|
|
1052
1089
|
*
|
|
1053
|
-
* @param
|
|
1054
|
-
* @param
|
|
1090
|
+
* @param index The index of the object to check.
|
|
1091
|
+
* @param hitWindowOffset The offset for hit window to compensate for replay hit inaccuracies.
|
|
1055
1092
|
* @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
1093
|
*/
|
|
1057
|
-
getCursorIndex(
|
|
1094
|
+
getCursorIndex(index, hitWindowOffset) {
|
|
1095
|
+
const object = this.map.objects[index];
|
|
1096
|
+
const data = this.data.hitObjectData[index];
|
|
1058
1097
|
if (object.object instanceof osuBase.Spinner ||
|
|
1059
1098
|
data.result === exports.hitResult.RESULT_0) {
|
|
1060
1099
|
return -1;
|
|
1061
1100
|
}
|
|
1062
1101
|
const isPrecise = this.data.convertedMods.some((m) => m instanceof osuBase.ModPrecise);
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1102
|
+
const isSlider = object.object instanceof osuBase.Slider;
|
|
1103
|
+
// For sliders, automatically set hit window to be as lenient as possible.
|
|
1104
|
+
let hitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
|
|
1105
|
+
if (!isSlider) {
|
|
1106
|
+
switch (data.result) {
|
|
1107
|
+
case exports.hitResult.RESULT_300:
|
|
1108
|
+
hitWindowLength = this.hitWindow.hitWindowFor300(isPrecise);
|
|
1109
|
+
break;
|
|
1110
|
+
case exports.hitResult.RESULT_100:
|
|
1111
|
+
hitWindowLength = this.hitWindow.hitWindowFor100(isPrecise);
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1073
1114
|
}
|
|
1074
|
-
const
|
|
1075
|
-
const
|
|
1076
|
-
const minimumHitTime =
|
|
1115
|
+
const startTime = object.object.startTime;
|
|
1116
|
+
const hitTime = startTime + data.accuracy;
|
|
1117
|
+
const minimumHitTime = startTime - hitWindowLength - hitWindowOffset;
|
|
1118
|
+
const maximumHitTime = startTime + hitWindowLength + hitWindowOffset;
|
|
1077
1119
|
const cursorInformations = [];
|
|
1078
|
-
for (let i = 0; i < this.
|
|
1079
|
-
const c = this.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1120
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1121
|
+
const c = this.data.cursorMovement[i];
|
|
1122
|
+
if (c.occurrences.length === 0) {
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
let hitTimeBeforeIndex = osuBase.MathUtils.clamp(c.occurrences.findIndex((v) => v.time >= minimumHitTime), 1, c.occurrences.length - 1) - 1;
|
|
1126
|
+
let hitTimeAfterIndex = c.occurrences.findIndex(
|
|
1127
|
+
// There is a special case for sliders where the time leniency in droid is a lot bigger compared to PC.
|
|
1128
|
+
// To prevent slider end time from ending earlier than hit window leniency, we use the maximum value between both.
|
|
1129
|
+
(v) => v.time >= Math.max(object.object.endTime, maximumHitTime));
|
|
1130
|
+
if (hitTimeAfterIndex === -1) {
|
|
1131
|
+
// Maximum hit time or object end time may be out of bounds for every presses.
|
|
1132
|
+
// We set the index to the latest cursor occurrence if that happens.
|
|
1133
|
+
hitTimeAfterIndex = c.occurrences.length;
|
|
1134
|
+
}
|
|
1135
|
+
--hitTimeAfterIndex;
|
|
1136
|
+
// Sometimes a `movementType.UP` instance occurs at the same time as a `movementType.MOVE`
|
|
1137
|
+
// or a cursor is recorded twice in one time, therefore this check is required.
|
|
1138
|
+
while (c.occurrences[hitTimeBeforeIndex]?.time ===
|
|
1139
|
+
c.occurrences[hitTimeBeforeIndex - 1]?.time &&
|
|
1140
|
+
hitTimeBeforeIndex > 0) {
|
|
1141
|
+
--hitTimeBeforeIndex;
|
|
1142
|
+
}
|
|
1143
|
+
// We track the cursor movement along those indexes.
|
|
1144
|
+
// Current cursor position is in `hitTimeBeforeIndex`.
|
|
1145
|
+
let distance = Number.POSITIVE_INFINITY;
|
|
1146
|
+
let acceptableRadius = object.object.radius;
|
|
1147
|
+
// Sliders have a bigger radius tolerance due to slider ball.
|
|
1148
|
+
if (isSlider) {
|
|
1149
|
+
acceptableRadius *= 2.4;
|
|
1150
|
+
}
|
|
1151
|
+
let j = hitTimeBeforeIndex;
|
|
1152
|
+
for (j; j <= hitTimeAfterIndex; ++j) {
|
|
1153
|
+
const occurrence = c.occurrences[j];
|
|
1154
|
+
const nextOccurrence = c.occurrences[j + 1];
|
|
1155
|
+
const cursorPosition = occurrence.position;
|
|
1156
|
+
if (occurrence.time > hitTime + hitWindowOffset + 10) {
|
|
1157
|
+
// Give an additional 10ms in case registration in-game is late.
|
|
1158
|
+
// Set distance to minimum just for the last.
|
|
1159
|
+
if (occurrence.id !== exports.movementType.UP) {
|
|
1160
|
+
distance = Math.min(distance, object.object.stackedPosition.getDistance(cursorPosition));
|
|
1161
|
+
}
|
|
1162
|
+
break;
|
|
1085
1163
|
}
|
|
1086
|
-
|
|
1087
|
-
// so just skip it to save time.
|
|
1088
|
-
if (c.time[j + 1] === c.time[j]) {
|
|
1164
|
+
if (occurrence.id === exports.movementType.UP) {
|
|
1089
1165
|
continue;
|
|
1090
1166
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1167
|
+
distance =
|
|
1168
|
+
object.object.stackedPosition.getDistance(cursorPosition);
|
|
1169
|
+
if (nextOccurrence &&
|
|
1170
|
+
nextOccurrence.id === exports.movementType.MOVE &&
|
|
1171
|
+
occurrence.time !== nextOccurrence.time &&
|
|
1172
|
+
!occurrence.position.equals(nextOccurrence.position)) {
|
|
1173
|
+
// If next cursor is a `move` instance and it doesn't go out of time
|
|
1174
|
+
// range, we interpolate cursor position between two occurrences.
|
|
1175
|
+
const nextPosition = nextOccurrence.position;
|
|
1176
|
+
const displacement = nextPosition.subtract(cursorPosition);
|
|
1177
|
+
for (let mSecPassed = Math.max(minimumHitTime, occurrence.time); mSecPassed <=
|
|
1178
|
+
Math.min(hitTime + hitWindowOffset, nextOccurrence.time); ++mSecPassed) {
|
|
1179
|
+
const progress = (mSecPassed - occurrence.time) /
|
|
1180
|
+
(nextOccurrence.time - occurrence.time);
|
|
1181
|
+
distance = object.object.stackedPosition.getDistance(cursorPosition.add(displacement.scale(progress)));
|
|
1182
|
+
}
|
|
1093
1183
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1184
|
+
}
|
|
1185
|
+
if (distance > acceptableRadius) {
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
let acceptedCursorIndex = i;
|
|
1189
|
+
if (isSlider) {
|
|
1190
|
+
this.assignCurrentIndexToOne = acceptedCursorIndex % 2 === 0;
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
// Get the latest down instance.
|
|
1194
|
+
while (c.occurrences[j]?.id !== exports.movementType.DOWN &&
|
|
1195
|
+
j > hitTimeBeforeIndex) {
|
|
1196
|
+
--j;
|
|
1099
1197
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1198
|
+
if (c.occurrences[j].id === exports.movementType.DOWN) {
|
|
1199
|
+
let isAngleFulfilled = false;
|
|
1200
|
+
// Special case where a cursor is "dragged" into the next object.
|
|
1201
|
+
if (c.occurrences[j + 1]?.id === exports.movementType.MOVE &&
|
|
1202
|
+
c.occurrences[j + 2]?.id === exports.movementType.UP &&
|
|
1203
|
+
// Some move instances move in the exact same place. Not sure why, most likely
|
|
1204
|
+
// because the position is recorded as int in the game and the movement is too small to
|
|
1205
|
+
// convert into +1 or -1.
|
|
1206
|
+
!c.occurrences[j].position.equals(c.occurrences[j + 1].position)) {
|
|
1207
|
+
const movementVec = c.occurrences[j + 1].position.subtract(object.object.endPosition);
|
|
1208
|
+
if (this.map.objects[index + 1]) {
|
|
1209
|
+
const next = this.map.objects[index + 1];
|
|
1210
|
+
const currentToNext = next.object.stackedPosition.subtract(object.object.endPosition);
|
|
1211
|
+
const dot = currentToNext.dot(movementVec);
|
|
1212
|
+
const det = currentToNext.x * movementVec.y -
|
|
1213
|
+
currentToNext.y * movementVec.x;
|
|
1214
|
+
const movementToNextAngle = Math.abs(Math.atan2(det, dot));
|
|
1215
|
+
isAngleFulfilled =
|
|
1216
|
+
movementToNextAngle < Math.PI / 6;
|
|
1116
1217
|
}
|
|
1218
|
+
if (this.map.objects[index - 1] && !isAngleFulfilled) {
|
|
1219
|
+
const prev = this.map.objects[index - 1];
|
|
1220
|
+
const extendedCurrent = object.object.endPosition
|
|
1221
|
+
.subtract(prev.object.stackedPosition)
|
|
1222
|
+
.scale(1.5)
|
|
1223
|
+
.subtract(object.object.endPosition);
|
|
1224
|
+
const dot = extendedCurrent.dot(movementVec);
|
|
1225
|
+
const det = extendedCurrent.x * movementVec.y -
|
|
1226
|
+
extendedCurrent.y * movementVec.x;
|
|
1227
|
+
const extendedCurrentToMovementAngle = Math.abs(Math.atan2(det, dot));
|
|
1228
|
+
isAngleFulfilled =
|
|
1229
|
+
extendedCurrentToMovementAngle < Math.PI / 6;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (isAngleFulfilled) {
|
|
1233
|
+
this.assignCurrentIndexToOne = true;
|
|
1234
|
+
acceptedCursorIndex = 0;
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
acceptedCursorIndex = this.assignCurrentIndexToOne
|
|
1238
|
+
? 1
|
|
1239
|
+
: 0;
|
|
1240
|
+
this.assignCurrentIndexToOne =
|
|
1241
|
+
!this.assignCurrentIndexToOne;
|
|
1117
1242
|
}
|
|
1118
1243
|
}
|
|
1119
1244
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
if (cursorInformations.length === 0) {
|
|
1128
|
-
return -1;
|
|
1245
|
+
cursorInformations.push({
|
|
1246
|
+
cursorIndex: acceptedCursorIndex,
|
|
1247
|
+
distanceDiff: distance,
|
|
1248
|
+
});
|
|
1129
1249
|
}
|
|
1130
|
-
//
|
|
1131
|
-
|
|
1132
|
-
|
|
1250
|
+
// Cursors have been filtered to see which of them is inside the object.
|
|
1251
|
+
// Now we look at which cursor is closest to the center of the object.
|
|
1252
|
+
const minDistanceDiff = Math.min(...cursorInformations.map((v) => {
|
|
1253
|
+
return v.distanceDiff;
|
|
1133
1254
|
}));
|
|
1134
|
-
return (cursorInformations.find((c) => c.
|
|
1135
|
-
?.cursorIndex);
|
|
1255
|
+
return (cursorInformations.find((c) => c.distanceDiff === minDistanceDiff)
|
|
1256
|
+
?.cursorIndex ?? -1);
|
|
1136
1257
|
}
|
|
1137
1258
|
/**
|
|
1138
1259
|
* Applies penalty to the original star rating instance.
|
|
1139
1260
|
*/
|
|
1140
1261
|
applyPenalty() {
|
|
1141
|
-
const beatmaps = new Array(this.
|
|
1262
|
+
const beatmaps = new Array(this.data.cursorMovement.length);
|
|
1142
1263
|
this.indexedHitObjects.forEach((o) => {
|
|
1143
1264
|
if (!beatmaps[o.cursorIndex]) {
|
|
1144
1265
|
const map = osuBase.Utils.deepCopy(this.map.map);
|
|
1266
|
+
map.hitObjects.clear();
|
|
1145
1267
|
beatmaps[o.cursorIndex] = map;
|
|
1146
1268
|
}
|
|
1147
1269
|
beatmaps[o.cursorIndex].hitObjects.add(o.object.object);
|
|
@@ -1155,14 +1277,12 @@ class TwoHandChecker {
|
|
|
1155
1277
|
starRating.map = beatmap;
|
|
1156
1278
|
starRating.generateDifficultyHitObjects();
|
|
1157
1279
|
starRating.objects[0].deltaTime =
|
|
1158
|
-
starRating.objects[0].
|
|
1159
|
-
this.indexedHitObjects[0].object.
|
|
1160
|
-
starRating.objects[0].strainTime = Math.max(
|
|
1280
|
+
starRating.objects[0].startTime -
|
|
1281
|
+
this.indexedHitObjects[0].object.startTime;
|
|
1282
|
+
starRating.objects[0].strainTime = Math.max(25, starRating.objects[0].deltaTime);
|
|
1161
1283
|
this.map.objects.push(...starRating.objects);
|
|
1162
1284
|
});
|
|
1163
|
-
this.map.objects.sort((a, b) =>
|
|
1164
|
-
return a.startTime - b.startTime;
|
|
1165
|
-
});
|
|
1285
|
+
this.map.objects.sort((a, b) => a.startTime - b.startTime);
|
|
1166
1286
|
this.map.calculateAll();
|
|
1167
1287
|
}
|
|
1168
1288
|
}
|
|
@@ -1404,42 +1524,41 @@ class ReplayAnalyzer {
|
|
|
1404
1524
|
for (let x = 0; x < size; x++) {
|
|
1405
1525
|
const moveSize = replayDataBuffer.readInt32BE(bufferCounter);
|
|
1406
1526
|
bufferCounter += this.INT_LENGTH;
|
|
1407
|
-
const
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
y: [],
|
|
1412
|
-
id: [],
|
|
1413
|
-
};
|
|
1527
|
+
const time = [];
|
|
1528
|
+
const x = [];
|
|
1529
|
+
const y = [];
|
|
1530
|
+
const id = [];
|
|
1414
1531
|
for (let i = 0; i < moveSize; i++) {
|
|
1415
|
-
|
|
1532
|
+
time[i] = replayDataBuffer.readInt32BE(bufferCounter);
|
|
1416
1533
|
bufferCounter += this.INT_LENGTH;
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
if (
|
|
1534
|
+
id[i] = time[i] & 3;
|
|
1535
|
+
time[i] >>= 2;
|
|
1536
|
+
if (id[i] !== exports.movementType.UP) {
|
|
1420
1537
|
if (resultObject.replayVersion >= 5) {
|
|
1421
|
-
|
|
1422
|
-
replayDataBuffer.readFloatBE(bufferCounter);
|
|
1538
|
+
x[i] = replayDataBuffer.readFloatBE(bufferCounter);
|
|
1423
1539
|
bufferCounter += this.FLOAT_LENGTH;
|
|
1424
|
-
|
|
1425
|
-
replayDataBuffer.readFloatBE(bufferCounter);
|
|
1540
|
+
y[i] = replayDataBuffer.readFloatBE(bufferCounter);
|
|
1426
1541
|
bufferCounter += this.FLOAT_LENGTH;
|
|
1427
1542
|
}
|
|
1428
1543
|
else {
|
|
1429
|
-
|
|
1430
|
-
replayDataBuffer.readInt16BE(bufferCounter);
|
|
1544
|
+
x[i] = replayDataBuffer.readInt16BE(bufferCounter);
|
|
1431
1545
|
bufferCounter += this.SHORT_LENGTH;
|
|
1432
|
-
|
|
1433
|
-
replayDataBuffer.readInt16BE(bufferCounter);
|
|
1546
|
+
y[i] = replayDataBuffer.readInt16BE(bufferCounter);
|
|
1434
1547
|
bufferCounter += this.SHORT_LENGTH;
|
|
1435
1548
|
}
|
|
1436
1549
|
}
|
|
1437
1550
|
else {
|
|
1438
|
-
|
|
1439
|
-
|
|
1551
|
+
x[i] = -1;
|
|
1552
|
+
y[i] = -1;
|
|
1440
1553
|
}
|
|
1441
1554
|
}
|
|
1442
|
-
resultObject.cursorMovement.push(
|
|
1555
|
+
resultObject.cursorMovement.push(new CursorData({
|
|
1556
|
+
size: moveSize,
|
|
1557
|
+
time: time,
|
|
1558
|
+
x: x,
|
|
1559
|
+
y: y,
|
|
1560
|
+
id: id,
|
|
1561
|
+
}));
|
|
1443
1562
|
}
|
|
1444
1563
|
const replayObjectLength = replayDataBuffer.readInt32BE(bufferCounter);
|
|
1445
1564
|
bufferCounter += this.INT_LENGTH;
|
|
@@ -1720,6 +1839,7 @@ class ReplayObjectData {
|
|
|
1720
1839
|
}
|
|
1721
1840
|
|
|
1722
1841
|
exports.CursorData = CursorData;
|
|
1842
|
+
exports.CursorOccurrence = CursorOccurrence;
|
|
1723
1843
|
exports.ReplayAnalyzer = ReplayAnalyzer;
|
|
1724
1844
|
exports.ReplayData = ReplayData;
|
|
1725
1845
|
exports.ReplayObjectData = ReplayObjectData;
|