@rian8337/osu-droid-replay-analyzer 4.0.0-beta.53 → 4.0.0-beta.55
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 +355 -493
- package/package.json +5 -5
- package/typings/index.d.ts +25 -134
package/dist/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var osuBase = require('@rian8337/osu-base');
|
|
4
|
-
var osuDifficultyCalculator = require('@rian8337/osu-difficulty-calculator');
|
|
5
|
-
var osuRebalanceDifficultyCalculator = require('@rian8337/osu-rebalance-difficulty-calculator');
|
|
6
|
-
var unzipper = require('unzipper');
|
|
7
4
|
var javaDeserialization = require('java-deserialization');
|
|
8
5
|
var node_stream = require('node:stream');
|
|
6
|
+
var unzipper = require('unzipper');
|
|
9
7
|
|
|
10
8
|
function _interopNamespaceDefault(e) {
|
|
11
9
|
var n = Object.create(null);
|
|
@@ -747,42 +745,223 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
747
745
|
};
|
|
748
746
|
|
|
749
747
|
/**
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
* Stores generic information about an osu!droid replay.
|
|
753
|
-
*
|
|
754
|
-
* This is used when analyzing replays using replay analyzer.
|
|
748
|
+
* Utility to check whether relevant sliders in a beatmap are cheesed.
|
|
755
749
|
*/
|
|
756
|
-
class
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
this.
|
|
764
|
-
this.
|
|
765
|
-
this.
|
|
766
|
-
this.
|
|
767
|
-
|
|
750
|
+
class SliderCheeseChecker {
|
|
751
|
+
/**
|
|
752
|
+
* @param beatmap The beatmap to analyze.
|
|
753
|
+
* @param data The data of the replay.
|
|
754
|
+
* @param difficultyAttributes The difficulty attributes of the beatmap.
|
|
755
|
+
*/
|
|
756
|
+
constructor(beatmap, data, difficultyAttributes) {
|
|
757
|
+
this.beatmap = beatmap;
|
|
758
|
+
this.data = data;
|
|
759
|
+
this.difficultyAttributes = difficultyAttributes;
|
|
760
|
+
this.hitWindow50 = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise)
|
|
761
|
+
? new osuBase.PreciseDroidHitWindow(beatmap.difficulty.od).mehWindow
|
|
762
|
+
: new osuBase.DroidHitWindow(beatmap.difficulty.od).mehWindow;
|
|
763
|
+
this.isHardRock = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
768
764
|
}
|
|
769
765
|
/**
|
|
770
|
-
*
|
|
766
|
+
* Checks if relevant sliders in the given beatmap was cheesed.
|
|
771
767
|
*/
|
|
772
|
-
|
|
773
|
-
|
|
768
|
+
check() {
|
|
769
|
+
if (this.difficultyAttributes.difficultSliders.length === 0 ||
|
|
770
|
+
(this.difficultyAttributes.sliderFactor === 1 &&
|
|
771
|
+
this.difficultyAttributes.flashlightSliderFactor === 1 &&
|
|
772
|
+
this.difficultyAttributes.visualSliderFactor === 1)) {
|
|
773
|
+
return {
|
|
774
|
+
aimPenalty: 1,
|
|
775
|
+
flashlightPenalty: 1,
|
|
776
|
+
visualPenalty: 1,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
const cheesedDifficultyRatings = this.checkSliderCheesing();
|
|
780
|
+
return this.calculateSliderCheesePenalty(cheesedDifficultyRatings);
|
|
774
781
|
}
|
|
775
782
|
/**
|
|
776
|
-
*
|
|
783
|
+
* Checks for sliders that were cheesed.
|
|
777
784
|
*/
|
|
778
|
-
|
|
779
|
-
|
|
785
|
+
checkSliderCheesing() {
|
|
786
|
+
const { objects } = this.beatmap.hitObjects;
|
|
787
|
+
const cheesedDifficultyRatings = [];
|
|
788
|
+
// Current loop indices are stored for efficiency.
|
|
789
|
+
const cursorLoopIndices = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
|
|
790
|
+
const acceptableRadius = objects[0].radius * 2;
|
|
791
|
+
// Sort difficult sliders by index so that cursor loop indices work properly.
|
|
792
|
+
for (const difficultSlider of this.difficultyAttributes.difficultSliders
|
|
793
|
+
.slice()
|
|
794
|
+
.sort((a, b) => a.index - b.index)) {
|
|
795
|
+
if (difficultSlider.index >= this.data.hitObjectData.length) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const object = objects[difficultSlider.index];
|
|
799
|
+
const objectData = this.data.hitObjectData[difficultSlider.index];
|
|
800
|
+
// If a miss or slider break occurs, we disregard the check for that slider.
|
|
801
|
+
if (objectData.result === exports.HitResult.miss ||
|
|
802
|
+
-this.hitWindow50 > objectData.accuracy ||
|
|
803
|
+
objectData.accuracy >
|
|
804
|
+
Math.min(this.hitWindow50, object.duration)) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
808
|
+
// These time boundaries should consider the delta time between the previous and next
|
|
809
|
+
// object as well as their hit accuracy. However, they are somewhat complicated to
|
|
810
|
+
// compute and the accuracy gain is small. As such, let's settle with 50 hit window.
|
|
811
|
+
const minTimeLimit = object.startTime - this.hitWindow50;
|
|
812
|
+
const maxTimeLimit = object.startTime + this.hitWindow50;
|
|
813
|
+
// Get the closest tap distance across all cursors.
|
|
814
|
+
const closestDistances = [];
|
|
815
|
+
const closestGroupIndices = [];
|
|
816
|
+
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
817
|
+
const cursorGroups = this.data.cursorMovement[i].occurrenceGroups;
|
|
818
|
+
let closestDistance = Number.POSITIVE_INFINITY;
|
|
819
|
+
let closestIndex = cursorGroups.length;
|
|
820
|
+
for (let j = cursorLoopIndices[i]; j < cursorGroups.length; j = ++cursorLoopIndices[i]) {
|
|
821
|
+
const group = cursorGroups[j];
|
|
822
|
+
if (group.endTime < minTimeLimit) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (group.startTime > maxTimeLimit) {
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
if (group.startTime >= minTimeLimit) {
|
|
829
|
+
const position = this.getCursorPosition(group.down);
|
|
830
|
+
const distance = position.getDistance(objectStartPosition);
|
|
831
|
+
if (closestDistance > distance) {
|
|
832
|
+
closestDistance = distance;
|
|
833
|
+
closestIndex = j;
|
|
834
|
+
}
|
|
835
|
+
if (closestDistance <= acceptableRadius / 2) {
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
// Normally, we check if there are cursor presses within the group's active time.
|
|
840
|
+
// However, some funky workarounds are used throughout the game for replays, so
|
|
841
|
+
// for the time being we only check for cursor distances across the group.
|
|
842
|
+
const { allOccurrences } = group;
|
|
843
|
+
for (let k = 1; k < allOccurrences.length; ++k) {
|
|
844
|
+
const cursor = allOccurrences[k];
|
|
845
|
+
const prevCursor = allOccurrences[k - 1];
|
|
846
|
+
let distance = Number.POSITIVE_INFINITY;
|
|
847
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
848
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
849
|
+
switch (cursor.id) {
|
|
850
|
+
case exports.MovementType.up:
|
|
851
|
+
distance =
|
|
852
|
+
prevPosition.getDistance(objectStartPosition);
|
|
853
|
+
break;
|
|
854
|
+
case exports.MovementType.move:
|
|
855
|
+
for (let mSecPassed = Math.max(prevCursor.time, minTimeLimit); mSecPassed <=
|
|
856
|
+
Math.min(cursor.time, maxTimeLimit); ++mSecPassed) {
|
|
857
|
+
const t = (mSecPassed - prevCursor.time) /
|
|
858
|
+
(cursor.time - prevCursor.time);
|
|
859
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
860
|
+
distance =
|
|
861
|
+
cursorPosition.getDistance(objectStartPosition);
|
|
862
|
+
if (closestDistance > distance) {
|
|
863
|
+
closestDistance = distance;
|
|
864
|
+
closestIndex = j;
|
|
865
|
+
}
|
|
866
|
+
if (closestDistance <=
|
|
867
|
+
acceptableRadius / 2) {
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (closestDistance > distance) {
|
|
873
|
+
closestDistance = distance;
|
|
874
|
+
closestIndex = j;
|
|
875
|
+
}
|
|
876
|
+
if (closestDistance <= acceptableRadius / 2) {
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
closestDistances.push(closestDistance);
|
|
882
|
+
closestGroupIndices.push(closestIndex);
|
|
883
|
+
if (cursorLoopIndices[i] > 0) {
|
|
884
|
+
// Decrement the index. The previous group may also have a role on the next slider.
|
|
885
|
+
--cursorLoopIndices[i];
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const cursorIndex = closestDistances.indexOf(Math.min(...closestDistances));
|
|
889
|
+
const closestDistance = closestDistances[cursorIndex];
|
|
890
|
+
if (closestDistance > acceptableRadius / 2) {
|
|
891
|
+
cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const group = this.data.cursorMovement[cursorIndex].occurrenceGroups[closestGroupIndices[cursorIndex]];
|
|
895
|
+
let isCheesed = false;
|
|
896
|
+
// Track cursor movement to see if it lands on every tick.
|
|
897
|
+
let occurrenceLoopIndex = 1;
|
|
898
|
+
const { allOccurrences } = group;
|
|
899
|
+
for (let i = 1; i < object.nestedHitObjects.length; ++i) {
|
|
900
|
+
if (isCheesed) {
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
const tickWasHit = objectData.tickset[i - 1];
|
|
904
|
+
if (!tickWasHit) {
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
const nestedObject = object.nestedHitObjects[i];
|
|
908
|
+
const nestedPosition = nestedObject.getStackedPosition(osuBase.Modes.droid);
|
|
909
|
+
while (occurrenceLoopIndex < allOccurrences.length &&
|
|
910
|
+
allOccurrences[occurrenceLoopIndex].time <
|
|
911
|
+
nestedObject.startTime) {
|
|
912
|
+
++occurrenceLoopIndex;
|
|
913
|
+
}
|
|
914
|
+
if (occurrenceLoopIndex === allOccurrences.length) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const cursor = allOccurrences[occurrenceLoopIndex];
|
|
918
|
+
const prevCursor = allOccurrences[occurrenceLoopIndex - 1];
|
|
919
|
+
const currentPosition = this.getCursorPosition(cursor);
|
|
920
|
+
const prevPosition = this.getCursorPosition(prevCursor);
|
|
921
|
+
switch (cursor.id) {
|
|
922
|
+
case exports.MovementType.move: {
|
|
923
|
+
// Interpolate cursor position during nested object time.
|
|
924
|
+
const t = (nestedObject.startTime - prevCursor.time) /
|
|
925
|
+
(cursor.time - prevCursor.time);
|
|
926
|
+
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
927
|
+
const distance = cursorPosition.getDistance(nestedPosition);
|
|
928
|
+
isCheesed = distance > acceptableRadius;
|
|
929
|
+
break;
|
|
930
|
+
}
|
|
931
|
+
case exports.MovementType.up:
|
|
932
|
+
isCheesed =
|
|
933
|
+
prevPosition.getDistance(nestedPosition) >
|
|
934
|
+
acceptableRadius;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (isCheesed) {
|
|
938
|
+
cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return cheesedDifficultyRatings;
|
|
780
942
|
}
|
|
781
943
|
/**
|
|
782
|
-
*
|
|
944
|
+
* Calculates the slider cheese penalty.
|
|
783
945
|
*/
|
|
784
|
-
|
|
785
|
-
|
|
946
|
+
calculateSliderCheesePenalty(cheesedDifficultyRatings) {
|
|
947
|
+
const summedDifficultyRating = Math.min(1, cheesedDifficultyRatings.reduce((a, v) => a + v, 0));
|
|
948
|
+
return {
|
|
949
|
+
aimPenalty: Math.max(this.difficultyAttributes.sliderFactor, Math.pow(1 -
|
|
950
|
+
summedDifficultyRating *
|
|
951
|
+
this.difficultyAttributes.sliderFactor, 2)),
|
|
952
|
+
flashlightPenalty: Math.max(this.difficultyAttributes.flashlightSliderFactor, Math.pow(1 -
|
|
953
|
+
summedDifficultyRating *
|
|
954
|
+
this.difficultyAttributes.flashlightSliderFactor, 2)),
|
|
955
|
+
visualPenalty: Math.max(this.difficultyAttributes.visualSliderFactor, Math.pow(1 -
|
|
956
|
+
summedDifficultyRating *
|
|
957
|
+
this.difficultyAttributes.visualSliderFactor, 2)),
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
getCursorPosition(cursor) {
|
|
961
|
+
if (this.isHardRock) {
|
|
962
|
+
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
963
|
+
}
|
|
964
|
+
return cursor.position;
|
|
786
965
|
}
|
|
787
966
|
}
|
|
788
967
|
|
|
@@ -1188,7 +1367,7 @@ class ThreeFingerChecker {
|
|
|
1188
1367
|
*/
|
|
1189
1368
|
class IndexedHitObject {
|
|
1190
1369
|
/**
|
|
1191
|
-
* @param object The underlying
|
|
1370
|
+
* @param object The underlying hitobject.
|
|
1192
1371
|
* @param cursorIndex The cursor index that moves towards the hitobject.
|
|
1193
1372
|
* @param groupIndex The group index of the cursor within the cursor index that hits the hitobject.
|
|
1194
1373
|
* @param occurrenceIndex The occurrence index within the group of the cursor within the cursor index that hits the hitobject.
|
|
@@ -1205,7 +1384,7 @@ class IndexedHitObject {
|
|
|
1205
1384
|
this.occurrenceIndex = occurrenceIndex;
|
|
1206
1385
|
this.angle = angle;
|
|
1207
1386
|
this.is2Handed = is2Handed;
|
|
1208
|
-
this.endCursorPosition = this.object.
|
|
1387
|
+
this.endCursorPosition = this.object.getStackedEndPosition(osuBase.Modes.droid);
|
|
1209
1388
|
}
|
|
1210
1389
|
}
|
|
1211
1390
|
|
|
@@ -1216,22 +1395,24 @@ class IndexedHitObject {
|
|
|
1216
1395
|
class TwoHandChecker {
|
|
1217
1396
|
// private csvString: string;
|
|
1218
1397
|
/**
|
|
1219
|
-
* @param
|
|
1398
|
+
* @param beatmap The beatmap to analyze.
|
|
1399
|
+
* @param attributes The difficulty attributes to analyze.
|
|
1220
1400
|
* @param data The data of the replay.
|
|
1221
1401
|
*/
|
|
1222
|
-
constructor(
|
|
1402
|
+
constructor(beatmap, attributes, data) {
|
|
1223
1403
|
/**
|
|
1224
1404
|
* The hitobjects of the beatmap that have been assigned with their respective cursor index.
|
|
1225
1405
|
*/
|
|
1226
1406
|
this.indexedHitObjects = [];
|
|
1227
|
-
this.
|
|
1407
|
+
this.beatmap = beatmap;
|
|
1408
|
+
this.attributes = attributes;
|
|
1228
1409
|
this.data = data;
|
|
1229
|
-
const greatWindow = new osuBase.OsuHitWindow(
|
|
1230
|
-
|
|
1231
|
-
this.hitWindow =
|
|
1410
|
+
const greatWindow = new osuBase.OsuHitWindow(attributes.overallDifficulty).greatWindow *
|
|
1411
|
+
attributes.clockRate;
|
|
1412
|
+
this.hitWindow = attributes.mods.some((m) => m instanceof osuBase.ModPrecise)
|
|
1232
1413
|
? new osuBase.PreciseDroidHitWindow(osuBase.PreciseDroidHitWindow.greatWindowToOD(greatWindow))
|
|
1233
1414
|
: new osuBase.DroidHitWindow(osuBase.DroidHitWindow.greatWindowToOD(greatWindow));
|
|
1234
|
-
this.isHardRock =
|
|
1415
|
+
this.isHardRock = attributes.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
1235
1416
|
// this.csvString = `Mods,${
|
|
1236
1417
|
// data.convertedMods.reduce((a, m) => a + m.acronym, "") || "NM"
|
|
1237
1418
|
// }\nCombo,${data.maxCombo}\nAccuracy,"${(
|
|
@@ -1291,26 +1472,36 @@ class TwoHandChecker {
|
|
|
1291
1472
|
// ),
|
|
1292
1473
|
// this.csvString
|
|
1293
1474
|
// );
|
|
1294
|
-
let twoHandedNoteCount = 0;
|
|
1295
|
-
const maxStrain = Math.max(
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1475
|
+
// let twoHandedNoteCount = 0;
|
|
1476
|
+
// const maxStrain = Math.max(
|
|
1477
|
+
// ...this.attributes.objects.map((v) => v.aimStrainWithSliders),
|
|
1478
|
+
// );
|
|
1479
|
+
// if (maxStrain) {
|
|
1480
|
+
// twoHandedNoteCount = this.indexedHitObjects.reduce(
|
|
1481
|
+
// (total, object) => {
|
|
1482
|
+
// if (!object.is2Handed) {
|
|
1483
|
+
// return total;
|
|
1484
|
+
// }
|
|
1485
|
+
// return (
|
|
1486
|
+
// total +
|
|
1487
|
+
// 1 /
|
|
1488
|
+
// (1 +
|
|
1489
|
+
// Math.exp(
|
|
1490
|
+
// -(
|
|
1491
|
+
// (object.object.aimStrainWithSliders /
|
|
1492
|
+
// maxStrain) *
|
|
1493
|
+
// 12 -
|
|
1494
|
+
// 6
|
|
1495
|
+
// ),
|
|
1496
|
+
// ))
|
|
1497
|
+
// );
|
|
1498
|
+
// },
|
|
1499
|
+
// 0,
|
|
1500
|
+
// );
|
|
1501
|
+
// }
|
|
1310
1502
|
return {
|
|
1311
|
-
is2Hand:
|
|
1312
|
-
|
|
1313
|
-
twoHandedNoteCount: twoHandedNoteCount,
|
|
1503
|
+
is2Hand: 0 > this.attributes.aimNoteCount * 0.15,
|
|
1504
|
+
twoHandedNoteCount: 0,
|
|
1314
1505
|
};
|
|
1315
1506
|
}
|
|
1316
1507
|
/**
|
|
@@ -1318,8 +1509,7 @@ class TwoHandChecker {
|
|
|
1318
1509
|
*/
|
|
1319
1510
|
indexHitObjects() {
|
|
1320
1511
|
const indexes = [];
|
|
1321
|
-
for (let i = 0; i <
|
|
1322
|
-
Math.min(this.data.hitObjectData.length, this.calculator.objects.length); ++i) {
|
|
1512
|
+
for (let i = 0; i < this.data.hitObjectData.length; ++i) {
|
|
1323
1513
|
const indexedHitObject = this.getIndexedHitObject(i);
|
|
1324
1514
|
indexedHitObject.sliderCheesed = this.checkSliderCheesing(indexedHitObject, this.data.hitObjectData[i]);
|
|
1325
1515
|
indexes.push(indexedHitObject.cursorIndex);
|
|
@@ -1380,21 +1570,20 @@ class TwoHandChecker {
|
|
|
1380
1570
|
* @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.
|
|
1381
1571
|
*/
|
|
1382
1572
|
getIndexedHitObject(objectIndex) {
|
|
1383
|
-
const
|
|
1384
|
-
const { object } = diffObject;
|
|
1573
|
+
const object = this.beatmap.hitObjects.objects[objectIndex];
|
|
1385
1574
|
// We don't care about the first object and spinners.
|
|
1386
1575
|
if (objectIndex === 0 || object instanceof osuBase.Spinner) {
|
|
1387
|
-
return new IndexedHitObject(
|
|
1576
|
+
return new IndexedHitObject(object, -1, -1, -1, null, false);
|
|
1388
1577
|
}
|
|
1389
1578
|
// We don't care if the aim strain is too low.
|
|
1390
1579
|
// if (diffObject.aimStrainWithSliders < 200) {
|
|
1391
1580
|
// return new IndexedHitObject(diffObject, -1, -1, -1, null, false);
|
|
1392
1581
|
// }
|
|
1393
|
-
const prevObject = this.
|
|
1582
|
+
const prevObject = this.beatmap.hitObjects.objects[objectIndex - 1];
|
|
1394
1583
|
const prevObjectData = this.data.hitObjectData[objectIndex - 1];
|
|
1395
1584
|
if (prevObject instanceof osuBase.Spinner ||
|
|
1396
1585
|
prevObjectData.result === exports.HitResult.miss) {
|
|
1397
|
-
return new IndexedHitObject(
|
|
1586
|
+
return new IndexedHitObject(object, -1, -1, -1, null, false);
|
|
1398
1587
|
}
|
|
1399
1588
|
const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1400
1589
|
let prevObjectEndPosition = prevObject.getStackedEndPosition(osuBase.Modes.droid);
|
|
@@ -1415,7 +1604,7 @@ class TwoHandChecker {
|
|
|
1415
1604
|
.subtract(prevObjectEndPosition);
|
|
1416
1605
|
// Don't consider objects that are too close to each other.
|
|
1417
1606
|
if (prevToCurrentMovement.length <= object.radius) {
|
|
1418
|
-
return new IndexedHitObject(
|
|
1607
|
+
return new IndexedHitObject(object, -1, -1, -1, null, false);
|
|
1419
1608
|
}
|
|
1420
1609
|
// The case for a one-handed object is that there will be a slight movement in the cursor towards
|
|
1421
1610
|
// the next object in fast patterns. We should not be worried about slow patterns as they will only
|
|
@@ -1435,12 +1624,12 @@ class TwoHandChecker {
|
|
|
1435
1624
|
this.indexedHitObjects[objectIndex - 1].endCursorPosition =
|
|
1436
1625
|
prevObjectInformation.position;
|
|
1437
1626
|
if (prevObjectInformation.position.x === Number.POSITIVE_INFINITY) {
|
|
1438
|
-
return new IndexedHitObject(
|
|
1627
|
+
return new IndexedHitObject(object, -1, -1, -1, null, false);
|
|
1439
1628
|
}
|
|
1440
1629
|
if (prevObjectInformation.cursorIndex ===
|
|
1441
1630
|
objectInformation.cursorIndex &&
|
|
1442
1631
|
prevObjectInformation.groupIndex === objectInformation.groupIndex) {
|
|
1443
|
-
return new IndexedHitObject(
|
|
1632
|
+
return new IndexedHitObject(object, objectInformation.cursorIndex, objectInformation.groupIndex, prevObjectInformation.occurrenceIndex, 0, false);
|
|
1444
1633
|
}
|
|
1445
1634
|
const cursorData = this.data.cursorMovement[prevObjectInformation.cursorIndex];
|
|
1446
1635
|
// There can be multiple angles to which the cursor moves towards the next object.
|
|
@@ -1522,9 +1711,9 @@ class TwoHandChecker {
|
|
|
1522
1711
|
// }
|
|
1523
1712
|
// }
|
|
1524
1713
|
if (!Number.isFinite(finalAngle)) {
|
|
1525
|
-
return new IndexedHitObject(
|
|
1714
|
+
return new IndexedHitObject(object, -1, -1, -1, null, false);
|
|
1526
1715
|
}
|
|
1527
|
-
return new IndexedHitObject(
|
|
1716
|
+
return new IndexedHitObject(object, prevObjectInformation.cursorIndex, prevObjectInformation.groupIndex, prevObjectInformation.occurrenceIndex, finalAngle, is2Handed);
|
|
1528
1717
|
}
|
|
1529
1718
|
/**
|
|
1530
1719
|
* Gets the position of the cursor that presses an object.
|
|
@@ -1533,7 +1722,7 @@ class TwoHandChecker {
|
|
|
1533
1722
|
* @returns The position of the cursor that presses the object.
|
|
1534
1723
|
*/
|
|
1535
1724
|
getCursorPositionForObjectStart(objectIndex) {
|
|
1536
|
-
const object = this.
|
|
1725
|
+
const object = this.beatmap.hitObjects.objects[objectIndex];
|
|
1537
1726
|
const data = this.data.hitObjectData[objectIndex];
|
|
1538
1727
|
const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1539
1728
|
if (object instanceof osuBase.Spinner) {
|
|
@@ -1587,7 +1776,7 @@ class TwoHandChecker {
|
|
|
1587
1776
|
if (downPosition.getDistance(objectPosition) <= object.radius &&
|
|
1588
1777
|
Math.abs(down.time - object.startTime) <= hitWindow) {
|
|
1589
1778
|
if (objectIndex > 0) {
|
|
1590
|
-
const prevObject = this.
|
|
1779
|
+
const prevObject = this.beatmap.hitObjects.objects[objectIndex - 1];
|
|
1591
1780
|
if (down.time > prevObject.endTime) {
|
|
1592
1781
|
return {
|
|
1593
1782
|
position: downPosition,
|
|
@@ -1692,11 +1881,11 @@ class TwoHandChecker {
|
|
|
1692
1881
|
* @returns The position of the cursor at the object's end position.
|
|
1693
1882
|
*/
|
|
1694
1883
|
getCursorPositionForObjectEnd(objectIndex) {
|
|
1695
|
-
const object = this.
|
|
1884
|
+
const object = this.beatmap.hitObjects.objects[objectIndex];
|
|
1696
1885
|
if (!(object instanceof osuBase.Slider)) {
|
|
1697
1886
|
return this.getCursorPositionForObjectStart(objectIndex);
|
|
1698
1887
|
}
|
|
1699
|
-
const nextObject = this.
|
|
1888
|
+
const nextObject = this.beatmap.hitObjects.objects[objectIndex - 1];
|
|
1700
1889
|
let objectEndPosition = object.getStackedEndPosition(osuBase.Modes.droid);
|
|
1701
1890
|
if (object.lazyTravelDistance > 0 && nextObject) {
|
|
1702
1891
|
const nextStartPosition = nextObject.getStackedPosition(osuBase.Modes.droid);
|
|
@@ -1796,7 +1985,7 @@ class TwoHandChecker {
|
|
|
1796
1985
|
* @returns Whether the slider was cheesed.
|
|
1797
1986
|
*/
|
|
1798
1987
|
checkSliderCheesing(indexedHitObject, hitData) {
|
|
1799
|
-
if (!(indexedHitObject.object
|
|
1988
|
+
if (!(indexedHitObject.object instanceof osuBase.Slider) ||
|
|
1800
1989
|
hitData.result === exports.HitResult.miss ||
|
|
1801
1990
|
indexedHitObject.cursorIndex === -1) {
|
|
1802
1991
|
return false;
|
|
@@ -1812,223 +2001,30 @@ class TwoHandChecker {
|
|
|
1812
2001
|
}
|
|
1813
2002
|
|
|
1814
2003
|
/**
|
|
1815
|
-
*
|
|
2004
|
+
* Represents a replay data in an osu!droid replay version 1 and 2.
|
|
2005
|
+
*
|
|
2006
|
+
* Stores generic information about an osu!droid replay.
|
|
2007
|
+
*
|
|
2008
|
+
* This is used when analyzing replays using replay analyzer.
|
|
1816
2009
|
*/
|
|
1817
|
-
class
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
this.
|
|
1825
|
-
this.
|
|
1826
|
-
this.
|
|
1827
|
-
this.
|
|
1828
|
-
|
|
1829
|
-
: new osuBase.DroidHitWindow(beatmap.difficulty.od).mehWindow;
|
|
1830
|
-
this.isHardRock = difficultyAttributes.mods.some((m) => m instanceof osuBase.ModHardRock);
|
|
1831
|
-
}
|
|
1832
|
-
/**
|
|
1833
|
-
* Checks if relevant sliders in the given beatmap was cheesed.
|
|
1834
|
-
*/
|
|
1835
|
-
check() {
|
|
1836
|
-
if (this.difficultyAttributes.difficultSliders.length === 0 ||
|
|
1837
|
-
(this.difficultyAttributes.sliderFactor === 1 &&
|
|
1838
|
-
this.difficultyAttributes.flashlightSliderFactor === 1 &&
|
|
1839
|
-
this.difficultyAttributes.visualSliderFactor === 1)) {
|
|
1840
|
-
return {
|
|
1841
|
-
aimPenalty: 1,
|
|
1842
|
-
flashlightPenalty: 1,
|
|
1843
|
-
visualPenalty: 1,
|
|
1844
|
-
};
|
|
1845
|
-
}
|
|
1846
|
-
const cheesedDifficultyRatings = this.checkSliderCheesing();
|
|
1847
|
-
return this.calculateSliderCheesePenalty(cheesedDifficultyRatings);
|
|
1848
|
-
}
|
|
1849
|
-
/**
|
|
1850
|
-
* Checks for sliders that were cheesed.
|
|
1851
|
-
*/
|
|
1852
|
-
checkSliderCheesing() {
|
|
1853
|
-
const { objects } = this.beatmap.hitObjects;
|
|
1854
|
-
const cheesedDifficultyRatings = [];
|
|
1855
|
-
// Current loop indices are stored for efficiency.
|
|
1856
|
-
const cursorLoopIndices = osuBase.Utils.initializeArray(this.data.cursorMovement.length, 0);
|
|
1857
|
-
const acceptableRadius = objects[0].radius * 2;
|
|
1858
|
-
// Sort difficult sliders by index so that cursor loop indices work properly.
|
|
1859
|
-
for (const difficultSlider of this.difficultyAttributes.difficultSliders
|
|
1860
|
-
.slice()
|
|
1861
|
-
.sort((a, b) => a.index - b.index)) {
|
|
1862
|
-
if (difficultSlider.index >= this.data.hitObjectData.length) {
|
|
1863
|
-
continue;
|
|
1864
|
-
}
|
|
1865
|
-
const object = objects[difficultSlider.index];
|
|
1866
|
-
const objectData = this.data.hitObjectData[difficultSlider.index];
|
|
1867
|
-
// If a miss or slider break occurs, we disregard the check for that slider.
|
|
1868
|
-
if (objectData.result === exports.HitResult.miss ||
|
|
1869
|
-
-this.hitWindow50 > objectData.accuracy ||
|
|
1870
|
-
objectData.accuracy >
|
|
1871
|
-
Math.min(this.hitWindow50, object.duration)) {
|
|
1872
|
-
continue;
|
|
1873
|
-
}
|
|
1874
|
-
const objectStartPosition = object.getStackedPosition(osuBase.Modes.droid);
|
|
1875
|
-
// These time boundaries should consider the delta time between the previous and next
|
|
1876
|
-
// object as well as their hit accuracy. However, they are somewhat complicated to
|
|
1877
|
-
// compute and the accuracy gain is small. As such, let's settle with 50 hit window.
|
|
1878
|
-
const minTimeLimit = object.startTime - this.hitWindow50;
|
|
1879
|
-
const maxTimeLimit = object.startTime + this.hitWindow50;
|
|
1880
|
-
// Get the closest tap distance across all cursors.
|
|
1881
|
-
const closestDistances = [];
|
|
1882
|
-
const closestGroupIndices = [];
|
|
1883
|
-
for (let i = 0; i < this.data.cursorMovement.length; ++i) {
|
|
1884
|
-
const cursorGroups = this.data.cursorMovement[i].occurrenceGroups;
|
|
1885
|
-
let closestDistance = Number.POSITIVE_INFINITY;
|
|
1886
|
-
let closestIndex = cursorGroups.length;
|
|
1887
|
-
for (let j = cursorLoopIndices[i]; j < cursorGroups.length; j = ++cursorLoopIndices[i]) {
|
|
1888
|
-
const group = cursorGroups[j];
|
|
1889
|
-
if (group.endTime < minTimeLimit) {
|
|
1890
|
-
continue;
|
|
1891
|
-
}
|
|
1892
|
-
if (group.startTime > maxTimeLimit) {
|
|
1893
|
-
break;
|
|
1894
|
-
}
|
|
1895
|
-
if (group.startTime >= minTimeLimit) {
|
|
1896
|
-
const position = this.getCursorPosition(group.down);
|
|
1897
|
-
const distance = position.getDistance(objectStartPosition);
|
|
1898
|
-
if (closestDistance > distance) {
|
|
1899
|
-
closestDistance = distance;
|
|
1900
|
-
closestIndex = j;
|
|
1901
|
-
}
|
|
1902
|
-
if (closestDistance <= acceptableRadius / 2) {
|
|
1903
|
-
break;
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
// Normally, we check if there are cursor presses within the group's active time.
|
|
1907
|
-
// However, some funky workarounds are used throughout the game for replays, so
|
|
1908
|
-
// for the time being we only check for cursor distances across the group.
|
|
1909
|
-
const { allOccurrences } = group;
|
|
1910
|
-
for (let k = 1; k < allOccurrences.length; ++k) {
|
|
1911
|
-
const cursor = allOccurrences[k];
|
|
1912
|
-
const prevCursor = allOccurrences[k - 1];
|
|
1913
|
-
let distance = Number.POSITIVE_INFINITY;
|
|
1914
|
-
const currentPosition = this.getCursorPosition(cursor);
|
|
1915
|
-
const prevPosition = this.getCursorPosition(prevCursor);
|
|
1916
|
-
switch (cursor.id) {
|
|
1917
|
-
case exports.MovementType.up:
|
|
1918
|
-
distance =
|
|
1919
|
-
prevPosition.getDistance(objectStartPosition);
|
|
1920
|
-
break;
|
|
1921
|
-
case exports.MovementType.move:
|
|
1922
|
-
for (let mSecPassed = Math.max(prevCursor.time, minTimeLimit); mSecPassed <=
|
|
1923
|
-
Math.min(cursor.time, maxTimeLimit); ++mSecPassed) {
|
|
1924
|
-
const t = (mSecPassed - prevCursor.time) /
|
|
1925
|
-
(cursor.time - prevCursor.time);
|
|
1926
|
-
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
1927
|
-
distance =
|
|
1928
|
-
cursorPosition.getDistance(objectStartPosition);
|
|
1929
|
-
if (closestDistance > distance) {
|
|
1930
|
-
closestDistance = distance;
|
|
1931
|
-
closestIndex = j;
|
|
1932
|
-
}
|
|
1933
|
-
if (closestDistance <=
|
|
1934
|
-
acceptableRadius / 2) {
|
|
1935
|
-
break;
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
if (closestDistance > distance) {
|
|
1940
|
-
closestDistance = distance;
|
|
1941
|
-
closestIndex = j;
|
|
1942
|
-
}
|
|
1943
|
-
if (closestDistance <= acceptableRadius / 2) {
|
|
1944
|
-
break;
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
closestDistances.push(closestDistance);
|
|
1949
|
-
closestGroupIndices.push(closestIndex);
|
|
1950
|
-
if (cursorLoopIndices[i] > 0) {
|
|
1951
|
-
// Decrement the index. The previous group may also have a role on the next slider.
|
|
1952
|
-
--cursorLoopIndices[i];
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
const cursorIndex = closestDistances.indexOf(Math.min(...closestDistances));
|
|
1956
|
-
const closestDistance = closestDistances[cursorIndex];
|
|
1957
|
-
if (closestDistance > acceptableRadius / 2) {
|
|
1958
|
-
cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
|
|
1959
|
-
continue;
|
|
1960
|
-
}
|
|
1961
|
-
const group = this.data.cursorMovement[cursorIndex].occurrenceGroups[closestGroupIndices[cursorIndex]];
|
|
1962
|
-
let isCheesed = false;
|
|
1963
|
-
// Track cursor movement to see if it lands on every tick.
|
|
1964
|
-
let occurrenceLoopIndex = 1;
|
|
1965
|
-
const { allOccurrences } = group;
|
|
1966
|
-
for (let i = 1; i < object.nestedHitObjects.length; ++i) {
|
|
1967
|
-
if (isCheesed) {
|
|
1968
|
-
break;
|
|
1969
|
-
}
|
|
1970
|
-
const tickWasHit = objectData.tickset[i - 1];
|
|
1971
|
-
if (!tickWasHit) {
|
|
1972
|
-
continue;
|
|
1973
|
-
}
|
|
1974
|
-
const nestedObject = object.nestedHitObjects[i];
|
|
1975
|
-
const nestedPosition = nestedObject.getStackedPosition(osuBase.Modes.droid);
|
|
1976
|
-
while (occurrenceLoopIndex < allOccurrences.length &&
|
|
1977
|
-
allOccurrences[occurrenceLoopIndex].time <
|
|
1978
|
-
nestedObject.startTime) {
|
|
1979
|
-
++occurrenceLoopIndex;
|
|
1980
|
-
}
|
|
1981
|
-
if (occurrenceLoopIndex === allOccurrences.length) {
|
|
1982
|
-
continue;
|
|
1983
|
-
}
|
|
1984
|
-
const cursor = allOccurrences[occurrenceLoopIndex];
|
|
1985
|
-
const prevCursor = allOccurrences[occurrenceLoopIndex - 1];
|
|
1986
|
-
const currentPosition = this.getCursorPosition(cursor);
|
|
1987
|
-
const prevPosition = this.getCursorPosition(prevCursor);
|
|
1988
|
-
switch (cursor.id) {
|
|
1989
|
-
case exports.MovementType.move: {
|
|
1990
|
-
// Interpolate cursor position during nested object time.
|
|
1991
|
-
const t = (nestedObject.startTime - prevCursor.time) /
|
|
1992
|
-
(cursor.time - prevCursor.time);
|
|
1993
|
-
const cursorPosition = osuBase.Interpolation.lerp(prevPosition, currentPosition, t);
|
|
1994
|
-
const distance = cursorPosition.getDistance(nestedPosition);
|
|
1995
|
-
isCheesed = distance > acceptableRadius;
|
|
1996
|
-
break;
|
|
1997
|
-
}
|
|
1998
|
-
case exports.MovementType.up:
|
|
1999
|
-
isCheesed =
|
|
2000
|
-
prevPosition.getDistance(nestedPosition) >
|
|
2001
|
-
acceptableRadius;
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
if (isCheesed) {
|
|
2005
|
-
cheesedDifficultyRatings.push(difficultSlider.difficultyRating);
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
|
-
return cheesedDifficultyRatings;
|
|
2010
|
+
class ReplayData {
|
|
2011
|
+
constructor(values) {
|
|
2012
|
+
this.replayVersion = values.replayVersion;
|
|
2013
|
+
this.folderName = values.folderName;
|
|
2014
|
+
this.fileName = values.fileName;
|
|
2015
|
+
this.hash = values.hash;
|
|
2016
|
+
this.accuracy = values.accuracy;
|
|
2017
|
+
this.rank = values.rank;
|
|
2018
|
+
this.hit300k = values.hit300k;
|
|
2019
|
+
this.hit100k = values.hit100k;
|
|
2020
|
+
this.cursorMovement = values.cursorMovement;
|
|
2021
|
+
this.hitObjectData = values.hitObjectData;
|
|
2009
2022
|
}
|
|
2010
2023
|
/**
|
|
2011
|
-
*
|
|
2024
|
+
* Whether the replay's version is 3 or later.
|
|
2012
2025
|
*/
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
return {
|
|
2016
|
-
aimPenalty: Math.max(this.difficultyAttributes.sliderFactor, Math.pow(1 -
|
|
2017
|
-
summedDifficultyRating *
|
|
2018
|
-
this.difficultyAttributes.sliderFactor, 2)),
|
|
2019
|
-
flashlightPenalty: Math.max(this.difficultyAttributes.flashlightSliderFactor, Math.pow(1 -
|
|
2020
|
-
summedDifficultyRating *
|
|
2021
|
-
this.difficultyAttributes.flashlightSliderFactor, 2)),
|
|
2022
|
-
visualPenalty: Math.max(this.difficultyAttributes.visualSliderFactor, Math.pow(1 -
|
|
2023
|
-
summedDifficultyRating *
|
|
2024
|
-
this.difficultyAttributes.visualSliderFactor, 2)),
|
|
2025
|
-
};
|
|
2026
|
-
}
|
|
2027
|
-
getCursorPosition(cursor) {
|
|
2028
|
-
if (this.isHardRock) {
|
|
2029
|
-
return new osuBase.Vector2(cursor.position.x, osuBase.Playfield.baseSize.y - cursor.position.y);
|
|
2030
|
-
}
|
|
2031
|
-
return cursor.position;
|
|
2026
|
+
isReplayV3() {
|
|
2027
|
+
return this.replayVersion >= 3;
|
|
2032
2028
|
}
|
|
2033
2029
|
}
|
|
2034
2030
|
|
|
@@ -2040,20 +2036,6 @@ class SliderCheeseChecker {
|
|
|
2040
2036
|
* This is used when analyzing replays using replay analyzer.
|
|
2041
2037
|
*/
|
|
2042
2038
|
class ReplayV3Data extends ReplayData {
|
|
2043
|
-
/**
|
|
2044
|
-
* The complete mod string of this replay (mods, speed multiplier, and forced difficulty statistics combined).
|
|
2045
|
-
*/
|
|
2046
|
-
get completeModString() {
|
|
2047
|
-
return this.modString;
|
|
2048
|
-
}
|
|
2049
|
-
/**
|
|
2050
|
-
* The mod string of this replay (mods only).
|
|
2051
|
-
*/
|
|
2052
|
-
get modString() {
|
|
2053
|
-
return this.convertedMods.length > 0
|
|
2054
|
-
? this.convertedMods.map((v) => v.acronym).join("")
|
|
2055
|
-
: "No Mod";
|
|
2056
|
-
}
|
|
2057
2039
|
constructor(values) {
|
|
2058
2040
|
super(values);
|
|
2059
2041
|
this.time = values.time;
|
|
@@ -2061,87 +2043,10 @@ class ReplayV3Data extends ReplayData {
|
|
|
2061
2043
|
this.maxCombo = values.maxCombo;
|
|
2062
2044
|
this.isFullCombo = values.isFullCombo;
|
|
2063
2045
|
this.playerName = values.playerName;
|
|
2064
|
-
this.rawMods = values.rawMods;
|
|
2065
2046
|
this.convertedMods = values.convertedMods;
|
|
2066
2047
|
}
|
|
2067
2048
|
}
|
|
2068
2049
|
|
|
2069
|
-
/**
|
|
2070
|
-
* Represents a replay data for replay version 4.
|
|
2071
|
-
*
|
|
2072
|
-
* Stores generic information about an osu!droid replay.
|
|
2073
|
-
*
|
|
2074
|
-
* This is used when analyzing replays using replay analyzer.
|
|
2075
|
-
*/
|
|
2076
|
-
class ReplayV4Data extends ReplayV3Data {
|
|
2077
|
-
get completeModString() {
|
|
2078
|
-
let finalString = this.modString;
|
|
2079
|
-
const customStats = [];
|
|
2080
|
-
if (this.speedMultiplier !== 1) {
|
|
2081
|
-
customStats.push(`${this.speedMultiplier}x`);
|
|
2082
|
-
}
|
|
2083
|
-
if (this.forceAR !== undefined) {
|
|
2084
|
-
customStats.push(`AR${this.forceAR}`);
|
|
2085
|
-
}
|
|
2086
|
-
if (this.flashlightFollowDelay !== undefined) {
|
|
2087
|
-
customStats.push(`FLD${this.flashlightFollowDelay}`);
|
|
2088
|
-
}
|
|
2089
|
-
if (customStats.length > 0) {
|
|
2090
|
-
finalString += ` (${customStats.join(", ")})`;
|
|
2091
|
-
}
|
|
2092
|
-
return finalString;
|
|
2093
|
-
}
|
|
2094
|
-
constructor(values) {
|
|
2095
|
-
var _a, _b;
|
|
2096
|
-
super(values);
|
|
2097
|
-
this.forceAR = values.forceAR;
|
|
2098
|
-
this.speedMultiplier = (_a = values.speedMultiplier) !== null && _a !== void 0 ? _a : 1;
|
|
2099
|
-
this.flashlightFollowDelay = (_b = values.flashlightFollowDelay) !== null && _b !== void 0 ? _b : 0.12;
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
/**
|
|
2104
|
-
* Represents a replay data for replay version 5.
|
|
2105
|
-
*
|
|
2106
|
-
* Stores generic information about an osu!droid replay.
|
|
2107
|
-
*
|
|
2108
|
-
* This is used when analyzing replays using replay analyzer.
|
|
2109
|
-
*/
|
|
2110
|
-
class ReplayV5Data extends ReplayV4Data {
|
|
2111
|
-
get completeModString() {
|
|
2112
|
-
let finalString = this.modString;
|
|
2113
|
-
const customStats = [];
|
|
2114
|
-
if (this.speedMultiplier !== 1) {
|
|
2115
|
-
customStats.push(`${this.speedMultiplier}x`);
|
|
2116
|
-
}
|
|
2117
|
-
if (this.forceAR !== undefined) {
|
|
2118
|
-
customStats.push(`AR${this.forceAR}`);
|
|
2119
|
-
}
|
|
2120
|
-
if (this.forceOD !== undefined) {
|
|
2121
|
-
customStats.push(`OD${this.forceOD}`);
|
|
2122
|
-
}
|
|
2123
|
-
if (this.forceCS !== undefined) {
|
|
2124
|
-
customStats.push(`CS${this.forceCS}`);
|
|
2125
|
-
}
|
|
2126
|
-
if (this.forceHP !== undefined) {
|
|
2127
|
-
customStats.push(`HP${this.forceHP}`);
|
|
2128
|
-
}
|
|
2129
|
-
if (this.flashlightFollowDelay !== undefined) {
|
|
2130
|
-
customStats.push(`FLD${this.flashlightFollowDelay}`);
|
|
2131
|
-
}
|
|
2132
|
-
if (customStats.length > 0) {
|
|
2133
|
-
finalString += ` (${customStats.join(", ")})`;
|
|
2134
|
-
}
|
|
2135
|
-
return finalString;
|
|
2136
|
-
}
|
|
2137
|
-
constructor(values) {
|
|
2138
|
-
super(values);
|
|
2139
|
-
this.forceCS = values.forceCS;
|
|
2140
|
-
this.forceOD = values.forceOD;
|
|
2141
|
-
this.forceHP = values.forceHP;
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
2050
|
/**
|
|
2146
2051
|
* A replay analyzer that analyzes a replay from osu!droid.
|
|
2147
2052
|
*
|
|
@@ -2196,11 +2101,11 @@ class ReplayAnalyzer {
|
|
|
2196
2101
|
*/
|
|
2197
2102
|
this.twoHandedNoteCount = 0;
|
|
2198
2103
|
this.bufferOffset = 0;
|
|
2199
|
-
this.scoreID = values.scoreID;
|
|
2200
|
-
this.beatmap = values.map;
|
|
2201
|
-
this.difficultyAttributes = values.difficultyAttributes;
|
|
2202
|
-
if (this.beatmap
|
|
2203
|
-
this.
|
|
2104
|
+
this.scoreID = values === null || values === void 0 ? void 0 : values.scoreID;
|
|
2105
|
+
this.beatmap = values === null || values === void 0 ? void 0 : values.map;
|
|
2106
|
+
this.difficultyAttributes = values === null || values === void 0 ? void 0 : values.difficultyAttributes;
|
|
2107
|
+
if (this.beatmap instanceof osuBase.DroidPlayableBeatmap) {
|
|
2108
|
+
this.playableBeatmap = this.beatmap;
|
|
2204
2109
|
}
|
|
2205
2110
|
}
|
|
2206
2111
|
/**
|
|
@@ -2239,16 +2144,12 @@ class ReplayAnalyzer {
|
|
|
2239
2144
|
let negativeCount = 0;
|
|
2240
2145
|
let positiveTotal = 0;
|
|
2241
2146
|
let negativeTotal = 0;
|
|
2242
|
-
const
|
|
2243
|
-
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
|
|
2244
|
-
? this.beatmap.beatmap
|
|
2245
|
-
: this.beatmap;
|
|
2246
|
-
const { objects } = beatmap.hitObjects;
|
|
2147
|
+
const { objects } = this.beatmap.hitObjects;
|
|
2247
2148
|
const mods = this.data.isReplayV3()
|
|
2248
2149
|
? this.data.convertedMods
|
|
2249
2150
|
: (_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods.filter((m) => m.isApplicableToDroid())) !== null && _b !== void 0 ? _b : [];
|
|
2250
|
-
const adjustedDifficulty = new osuBase.BeatmapDifficulty(beatmap.difficulty);
|
|
2251
|
-
osuBase.ModUtil.applyModsToBeatmapDifficulty(adjustedDifficulty, osuBase.Modes.droid, mods
|
|
2151
|
+
const adjustedDifficulty = new osuBase.BeatmapDifficulty(this.beatmap.difficulty);
|
|
2152
|
+
osuBase.ModUtil.applyModsToBeatmapDifficulty(adjustedDifficulty, osuBase.Modes.droid, mods);
|
|
2252
2153
|
const mehWindow = mods.some((m) => m instanceof osuBase.ModPrecise)
|
|
2253
2154
|
? new osuBase.PreciseDroidHitWindow(adjustedDifficulty.od).mehWindow
|
|
2254
2155
|
: new osuBase.DroidHitWindow(adjustedDifficulty.od).mehWindow;
|
|
@@ -2304,15 +2205,15 @@ class ReplayAnalyzer {
|
|
|
2304
2205
|
/**
|
|
2305
2206
|
* Checks if a play is using 2 hands.
|
|
2306
2207
|
*
|
|
2307
|
-
* Requires `analyze()` to be called first and `
|
|
2208
|
+
* Requires `analyze()` to be called first as well as `beatmap` and `difficultyAttributes` to be defined.
|
|
2308
2209
|
*/
|
|
2309
2210
|
checkFor2Hand() {
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
!this.data) {
|
|
2211
|
+
var _a;
|
|
2212
|
+
if (!this.beatmap || !this.difficultyAttributes || !this.data) {
|
|
2313
2213
|
return;
|
|
2314
2214
|
}
|
|
2315
|
-
|
|
2215
|
+
(_a = this.playableBeatmap) !== null && _a !== void 0 ? _a : (this.playableBeatmap = this.constructPlayableBeatmap());
|
|
2216
|
+
const twoHandChecker = new TwoHandChecker(this.playableBeatmap, this.difficultyAttributes, this.data);
|
|
2316
2217
|
const result = twoHandChecker.check();
|
|
2317
2218
|
this.is2Hand = result.is2Hand;
|
|
2318
2219
|
this.twoHandedNoteCount = result.twoHandedNoteCount;
|
|
@@ -2338,6 +2239,9 @@ class ReplayAnalyzer {
|
|
|
2338
2239
|
*/
|
|
2339
2240
|
downloadReplay() {
|
|
2340
2241
|
return __awaiter(this, void 0, void 0, function* () {
|
|
2242
|
+
if (this.scoreID === undefined) {
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2341
2245
|
const apiRequestBuilder = new osuBase.DroidAPIRequestBuilder()
|
|
2342
2246
|
.setRequireAPIkey(false)
|
|
2343
2247
|
.setEndpoint("upload")
|
|
@@ -2407,7 +2311,6 @@ class ReplayAnalyzer {
|
|
|
2407
2311
|
isFullCombo: false,
|
|
2408
2312
|
maxCombo: 0,
|
|
2409
2313
|
playerName: "",
|
|
2410
|
-
rawMods: [],
|
|
2411
2314
|
score: 0,
|
|
2412
2315
|
time: new Date(0),
|
|
2413
2316
|
};
|
|
@@ -2423,53 +2326,34 @@ class ReplayAnalyzer {
|
|
|
2423
2326
|
resultObject.maxCombo = rawObject[4].readInt32BE(36);
|
|
2424
2327
|
resultObject.isFullCombo = resultObject.accuracy.value() === 1;
|
|
2425
2328
|
resultObject.playerName = rawObject[5];
|
|
2426
|
-
resultObject.
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
switch (true) {
|
|
2434
|
-
// Forced stats
|
|
2435
|
-
case s.startsWith("CS"):
|
|
2436
|
-
resultObject.forceCS = parseFloat(s.replace("CS", ""));
|
|
2437
|
-
break;
|
|
2438
|
-
case s.startsWith("AR"):
|
|
2439
|
-
resultObject.forceAR = parseFloat(s.replace("AR", ""));
|
|
2440
|
-
break;
|
|
2441
|
-
case s.startsWith("OD"):
|
|
2442
|
-
resultObject.forceOD = parseFloat(s.replace("OD", ""));
|
|
2443
|
-
break;
|
|
2444
|
-
case s.startsWith("HP"):
|
|
2445
|
-
resultObject.forceHP = parseFloat(s.replace("HP", ""));
|
|
2446
|
-
break;
|
|
2447
|
-
// FL follow delay
|
|
2448
|
-
case s.startsWith("FLD"):
|
|
2449
|
-
resultObject.flashlightFollowDelay =
|
|
2450
|
-
parseFloat(s.replace("FLD", "")) || 0.12;
|
|
2451
|
-
break;
|
|
2452
|
-
// Speed multiplier
|
|
2453
|
-
case s.startsWith("x"):
|
|
2454
|
-
resultObject.speedMultiplier =
|
|
2455
|
-
parseFloat(s.replace("x", "")) || 1;
|
|
2456
|
-
break;
|
|
2329
|
+
if (resultObject.replayVersion >= 7) {
|
|
2330
|
+
resultObject.convertedMods = osuBase.ModUtil.deserializeMods(rawObject[6]);
|
|
2331
|
+
}
|
|
2332
|
+
else {
|
|
2333
|
+
resultObject.convertedMods = this.convertDroidMods(resultObject.replayVersion, Object.values(rawObject[6].elements));
|
|
2334
|
+
if (resultObject.replayVersion >= 4) {
|
|
2335
|
+
osuBase.DroidLegacyModConverter.parseExtraModString(resultObject.convertedMods, rawObject[7].split("|"));
|
|
2457
2336
|
}
|
|
2458
2337
|
}
|
|
2338
|
+
resultObject.rank = this.calculateRank(resultObject);
|
|
2459
2339
|
}
|
|
2460
2340
|
let bufferIndex;
|
|
2461
|
-
switch (
|
|
2462
|
-
|
|
2463
|
-
case
|
|
2464
|
-
bufferIndex =
|
|
2341
|
+
switch (resultObject.replayVersion) {
|
|
2342
|
+
case 1:
|
|
2343
|
+
case 2:
|
|
2344
|
+
bufferIndex = 4;
|
|
2465
2345
|
break;
|
|
2466
|
-
|
|
2467
|
-
case
|
|
2346
|
+
case 3:
|
|
2347
|
+
case 7:
|
|
2468
2348
|
bufferIndex = 7;
|
|
2469
2349
|
break;
|
|
2470
|
-
|
|
2350
|
+
case 4:
|
|
2351
|
+
case 5:
|
|
2352
|
+
case 6:
|
|
2353
|
+
bufferIndex = 8;
|
|
2354
|
+
break;
|
|
2471
2355
|
default:
|
|
2472
|
-
|
|
2356
|
+
throw new Error("Unsupported replay version: " + resultObject.replayVersion);
|
|
2473
2357
|
}
|
|
2474
2358
|
const replayDataBufferArray = [];
|
|
2475
2359
|
while (bufferIndex < rawObject.length) {
|
|
@@ -2483,14 +2367,10 @@ class ReplayAnalyzer {
|
|
|
2483
2367
|
this.parseOldReplayInformation(resultObject);
|
|
2484
2368
|
switch (resultObject.replayVersion) {
|
|
2485
2369
|
case 3:
|
|
2486
|
-
this.data = new ReplayV3Data(resultObject);
|
|
2487
|
-
break;
|
|
2488
2370
|
case 4:
|
|
2489
|
-
this.data = new ReplayV4Data(resultObject);
|
|
2490
|
-
break;
|
|
2491
2371
|
case 5:
|
|
2492
2372
|
case 6:
|
|
2493
|
-
this.data = new
|
|
2373
|
+
this.data = new ReplayV3Data(resultObject);
|
|
2494
2374
|
break;
|
|
2495
2375
|
default:
|
|
2496
2376
|
this.data = new ReplayData(resultObject);
|
|
@@ -2499,38 +2379,44 @@ class ReplayAnalyzer {
|
|
|
2499
2379
|
/**
|
|
2500
2380
|
* Converts replay mods to droid mod string.
|
|
2501
2381
|
*/
|
|
2502
|
-
convertDroidMods(replayMods) {
|
|
2382
|
+
convertDroidMods(replayVersion, replayMods) {
|
|
2503
2383
|
const replayModsConstants = {
|
|
2504
|
-
MOD_AUTO: new osuBase.ModAuto()
|
|
2505
|
-
MOD_AUTOPILOT: new osuBase.ModAutopilot()
|
|
2506
|
-
MOD_NOFAIL: new osuBase.ModNoFail()
|
|
2507
|
-
MOD_EASY: new osuBase.ModEasy()
|
|
2508
|
-
MOD_HIDDEN: new osuBase.ModHidden()
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2384
|
+
MOD_AUTO: new osuBase.ModAuto(),
|
|
2385
|
+
MOD_AUTOPILOT: new osuBase.ModAutopilot(),
|
|
2386
|
+
MOD_NOFAIL: new osuBase.ModNoFail(),
|
|
2387
|
+
MOD_EASY: new osuBase.ModEasy(),
|
|
2388
|
+
MOD_HIDDEN: new osuBase.ModHidden(),
|
|
2389
|
+
MOD_TRACEABLE: new osuBase.ModTraceable(),
|
|
2390
|
+
MOD_HARDROCK: new osuBase.ModHardRock(),
|
|
2391
|
+
MOD_DOUBLETIME: new osuBase.ModDoubleTime(),
|
|
2392
|
+
MOD_HALFTIME: new osuBase.ModHalfTime(),
|
|
2393
|
+
MOD_NIGHTCORE: new osuBase.ModNightCore(),
|
|
2394
|
+
MOD_PRECISE: new osuBase.ModPrecise(),
|
|
2395
|
+
MOD_SMALLCIRCLE: new osuBase.ModSmallCircle(),
|
|
2396
|
+
MOD_REALLYEASY: new osuBase.ModReallyEasy(),
|
|
2397
|
+
MOD_RELAX: new osuBase.ModRelax(),
|
|
2398
|
+
MOD_PERFECT: new osuBase.ModPerfect(),
|
|
2399
|
+
MOD_SUDDENDEATH: new osuBase.ModSuddenDeath(),
|
|
2400
|
+
MOD_SCOREV2: new osuBase.ModScoreV2(),
|
|
2401
|
+
MOD_FLASHLIGHT: new osuBase.ModFlashlight(),
|
|
2521
2402
|
};
|
|
2522
|
-
|
|
2403
|
+
const mods = [];
|
|
2523
2404
|
for (const mod of replayMods) {
|
|
2524
2405
|
for (const property in replayModsConstants) {
|
|
2525
2406
|
if (!mod.includes(property)) {
|
|
2526
2407
|
continue;
|
|
2527
2408
|
}
|
|
2528
|
-
|
|
2529
|
-
|
|
2409
|
+
if (replayVersion <= 3 && mod === "MOD_NIGHTCORE") {
|
|
2410
|
+
// In replay v3, the NightCore mod is bugged. See ModOldNightCore's description.
|
|
2411
|
+
mods.push(new osuBase.ModOldNightCore());
|
|
2412
|
+
}
|
|
2413
|
+
else {
|
|
2414
|
+
mods.push(replayModsConstants[property]);
|
|
2415
|
+
}
|
|
2530
2416
|
break;
|
|
2531
2417
|
}
|
|
2532
2418
|
}
|
|
2533
|
-
return
|
|
2419
|
+
return mods;
|
|
2534
2420
|
}
|
|
2535
2421
|
parseMovementData(resultObject, replayDataBuffer) {
|
|
2536
2422
|
resultObject.cursorMovement.length = 0;
|
|
@@ -2605,10 +2491,7 @@ class ReplayAnalyzer {
|
|
|
2605
2491
|
if (resultObject.replayVersion >= 3) {
|
|
2606
2492
|
return;
|
|
2607
2493
|
}
|
|
2608
|
-
const objects = (_a =
|
|
2609
|
-
this.beatmap instanceof osuRebalanceDifficultyCalculator.DroidDifficultyCalculator
|
|
2610
|
-
? this.beatmap.beatmap
|
|
2611
|
-
: this.beatmap)) === null || _a === void 0 ? void 0 : _a.hitObjects.objects;
|
|
2494
|
+
const objects = (_a = this.beatmap) === null || _a === void 0 ? void 0 : _a.hitObjects.objects;
|
|
2612
2495
|
let grantsGekiOrKatu = true;
|
|
2613
2496
|
for (let i = 0; i < resultObject.hitObjectData.length; ++i) {
|
|
2614
2497
|
// Hit result
|
|
@@ -2677,35 +2560,16 @@ class ReplayAnalyzer {
|
|
|
2677
2560
|
}
|
|
2678
2561
|
constructPlayableBeatmap() {
|
|
2679
2562
|
var _a, _b;
|
|
2563
|
+
if (this.beatmap instanceof osuBase.DroidPlayableBeatmap) {
|
|
2564
|
+
return this.beatmap;
|
|
2565
|
+
}
|
|
2680
2566
|
if (!this.beatmap || !this.data) {
|
|
2681
2567
|
throw new Error("Beatmap and replay data must be defined.");
|
|
2682
2568
|
}
|
|
2683
2569
|
const mods = this.data.isReplayV3()
|
|
2684
|
-
? this.data.convertedMods
|
|
2685
|
-
: (_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods
|
|
2686
|
-
|
|
2687
|
-
[
|
|
2688
|
-
this.data.forceCS,
|
|
2689
|
-
this.data.forceAR,
|
|
2690
|
-
this.data.forceOD,
|
|
2691
|
-
this.data.forceHP,
|
|
2692
|
-
].some((v) => v !== undefined)) {
|
|
2693
|
-
mods.push(new osuBase.ModDifficultyAdjust({
|
|
2694
|
-
cs: this.data.forceCS,
|
|
2695
|
-
ar: this.data.forceAR,
|
|
2696
|
-
od: this.data.forceOD,
|
|
2697
|
-
hp: this.data.forceHP,
|
|
2698
|
-
}));
|
|
2699
|
-
}
|
|
2700
|
-
return (this.beatmap instanceof osuBase.Beatmap
|
|
2701
|
-
? this.beatmap
|
|
2702
|
-
: this.beatmap.beatmap).createPlayableBeatmap({
|
|
2703
|
-
mode: osuBase.Modes.droid,
|
|
2704
|
-
mods: mods,
|
|
2705
|
-
customSpeedMultiplier: this.data.isReplayV4()
|
|
2706
|
-
? this.data.speedMultiplier
|
|
2707
|
-
: 1,
|
|
2708
|
-
});
|
|
2570
|
+
? this.data.convertedMods
|
|
2571
|
+
: (_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods) !== null && _b !== void 0 ? _b : [];
|
|
2572
|
+
return this.beatmap.createDroidPlayableBeatmap(mods);
|
|
2709
2573
|
}
|
|
2710
2574
|
readByte(buffer) {
|
|
2711
2575
|
const num = buffer.readInt8(this.bufferOffset);
|
|
@@ -2752,8 +2616,6 @@ exports.ReplayAnalyzer = ReplayAnalyzer;
|
|
|
2752
2616
|
exports.ReplayData = ReplayData;
|
|
2753
2617
|
exports.ReplayObjectData = ReplayObjectData;
|
|
2754
2618
|
exports.ReplayV3Data = ReplayV3Data;
|
|
2755
|
-
exports.ReplayV4Data = ReplayV4Data;
|
|
2756
|
-
exports.ReplayV5Data = ReplayV5Data;
|
|
2757
2619
|
exports.SliderCheeseChecker = SliderCheeseChecker;
|
|
2758
2620
|
exports.ThreeFingerChecker = ThreeFingerChecker;
|
|
2759
2621
|
exports.TwoHandChecker = TwoHandChecker;
|