@rian8337/osu-droid-replay-analyzer 4.0.0-beta.21 → 4.0.0-beta.23

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 CHANGED
@@ -339,6 +339,9 @@ class RebalanceThreeFingerChecker {
339
339
  this.isPrecise = this.difficultyAttributes.mods.some((m) => m instanceof osuBase.ModPrecise);
340
340
  this.hitWindow = new osuBase.DroidHitWindow(od);
341
341
  this.hitObjects = beatmap.hitObjects.objects;
342
+ this.objectRadiusThreshold = this.hitObjects[0].radius;
343
+ // Only allow object radius threshold to deviate slightly from actual object radius.
344
+ this.maximumObjectRadiusThreshold = this.objectRadiusThreshold * 1.05;
342
345
  }
343
346
  /**
344
347
  * Checks whether a beatmap is eligible to be detected for 3-finger.
@@ -575,42 +578,32 @@ class RebalanceThreeFingerChecker {
575
578
  findDragIndex(section) {
576
579
  var _a;
577
580
  const objectData = this.data.hitObjectData;
581
+ const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
578
582
  const firstObject = this.hitObjects[section.firstObjectIndex];
583
+ const firstObjectData = objectData[section.firstObjectIndex];
579
584
  const lastObject = this.hitObjects[section.lastObjectIndex];
580
- let firstObjectMinHitTime = firstObject.startTime;
581
- if (firstObject instanceof osuBase.Circle) {
582
- switch (objectData[section.firstObjectIndex].result) {
583
- case exports.HitResult.great:
584
- firstObjectMinHitTime -= this.hitWindow.hitWindowFor300(this.isPrecise);
585
- break;
586
- case exports.HitResult.good:
587
- firstObjectMinHitTime -= this.hitWindow.hitWindowFor100(this.isPrecise);
588
- break;
589
- default:
590
- firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(this.isPrecise);
591
- }
585
+ const lastObjectData = objectData[section.lastObjectIndex];
586
+ let firstObjectHitTime = firstObject.startTime;
587
+ let lastObjectHitTime = lastObject.startTime;
588
+ // Check for slider breaks.
589
+ if (firstObject instanceof osuBase.Slider &&
590
+ firstObjectData.accuracy === Math.floor(hitWindow50) + 13) {
591
+ firstObjectHitTime -= hitWindow50;
592
592
  }
593
- else if (firstObject instanceof osuBase.Slider) {
594
- firstObjectMinHitTime -= this.hitWindow.hitWindowFor50(this.isPrecise);
593
+ else {
594
+ firstObjectHitTime += firstObjectData.accuracy;
595
595
  }
596
- let lastObjectMaxHitTime = lastObject.startTime;
597
- if (lastObject instanceof osuBase.Circle) {
598
- switch (objectData[section.lastObjectIndex].result) {
599
- case exports.HitResult.great:
600
- lastObjectMaxHitTime += this.hitWindow.hitWindowFor300(this.isPrecise);
601
- break;
602
- case exports.HitResult.good:
603
- lastObjectMaxHitTime += this.hitWindow.hitWindowFor100(this.isPrecise);
604
- break;
605
- default:
606
- lastObjectMaxHitTime += this.hitWindow.hitWindowFor50(this.isPrecise);
607
- }
596
+ if (lastObject instanceof osuBase.Slider &&
597
+ lastObjectData.accuracy === Math.floor(hitWindow50) + 13) {
598
+ lastObjectHitTime += Math.min(hitWindow50, lastObject.nestedHitObjects[1].startTime - lastObject.startTime);
608
599
  }
609
- else if (lastObject instanceof osuBase.Slider) {
610
- lastObjectMaxHitTime += Math.min(this.hitWindow.hitWindowFor50(this.isPrecise), lastObject.spanDuration);
600
+ else {
601
+ lastObjectHitTime += lastObjectData.accuracy;
611
602
  }
612
603
  // Since there may be more than 1 cursor instance index,
613
604
  // we check which cursor instance follows hitobjects all over.
605
+ const cursorInstanceIndices = [];
606
+ const cursorGroupIndices = [];
614
607
  const cursorIndices = [];
615
608
  for (let i = 0; i < this.data.cursorMovement.length; ++i) {
616
609
  const c = this.data.cursorMovement[i];
@@ -619,8 +612,8 @@ class RebalanceThreeFingerChecker {
619
612
  }
620
613
  // Do not include cursors that don't have an occurence in this section
621
614
  // this speeds up checking process.
622
- if (c.occurrenceGroups.filter((v) => v.startTime >= firstObjectMinHitTime &&
623
- v.endTime <= lastObjectMaxHitTime).length === 0) {
615
+ if (c.occurrenceGroups.filter((v) => v.startTime <= firstObjectHitTime &&
616
+ v.endTime >= lastObjectHitTime).length === 0) {
624
617
  continue;
625
618
  }
626
619
  // If this cursor instance doesn't move, it's not the cursor instance we want.
@@ -628,12 +621,15 @@ class RebalanceThreeFingerChecker {
628
621
  0) {
629
622
  continue;
630
623
  }
631
- cursorIndices.push(i);
624
+ cursorInstanceIndices.push(i);
625
+ cursorGroupIndices.push(0);
626
+ // Start checking from the second cursor.
627
+ cursorIndices.push(1);
632
628
  }
633
629
  const sectionObjects = this.hitObjects.slice(section.firstObjectIndex, section.lastObjectIndex + 1);
634
630
  const sectionReplayObjectData = objectData.slice(section.firstObjectIndex, section.lastObjectIndex + 1);
635
- const hitWindow50 = this.hitWindow.hitWindowFor50(this.isPrecise);
636
- for (let i = 0; i < sectionObjects.length && cursorIndices.some((v) => v !== -1); ++i) {
631
+ for (let i = 0; i < sectionObjects.length &&
632
+ cursorInstanceIndices.some((v) => v !== -1); ++i) {
637
633
  const object = sectionObjects[i];
638
634
  const objectData = sectionReplayObjectData[i];
639
635
  if (object instanceof osuBase.Spinner ||
@@ -648,45 +644,70 @@ class RebalanceThreeFingerChecker {
648
644
  const objectPosition = object.getStackedPosition(osuBase.Modes.droid);
649
645
  const hitTime = object.startTime + objectData.accuracy;
650
646
  // Observe the cursor position at the object's hit time.
651
- for (let j = 0; j < cursorIndices.length; ++j) {
652
- if (cursorIndices[j] === -1) {
647
+ for (let j = 0; j < cursorInstanceIndices.length; ++j) {
648
+ if (cursorInstanceIndices[j] === -1) {
653
649
  continue;
654
650
  }
655
- const cursorData = this.data.cursorMovement[cursorIndices[j]];
656
- const cursorGroup = cursorData.occurrenceGroups.find((v) => v.isActiveAt(hitTime));
657
- if (!cursorGroup) {
658
- continue;
659
- }
660
- const cursors = cursorGroup.allOccurrences;
661
- let isInObject = false;
662
- for (let k = 1; k < cursors.length; ++k) {
663
- const cursor = cursors[k];
664
- const prevCursor = cursors[k - 1];
665
- // Only consider cursor at interval prev.time <= hitTime <= current.time.
666
- if (prevCursor.time <= hitTime && cursor.time >= hitTime) {
651
+ const cursorData = this.data.cursorMovement[cursorInstanceIndices[j]];
652
+ for (let k = cursorGroupIndices[j]; k < cursorData.occurrenceGroups.length; cursorGroupIndices[j] = ++k) {
653
+ const cursorGroup = cursorData.occurrenceGroups[k];
654
+ if (!cursorGroup.isActiveAt(hitTime)) {
655
+ continue;
656
+ }
657
+ const cursors = cursorGroup.allOccurrences;
658
+ // We are maintaining the closest distance of cursors to the object.
659
+ // This is because the radius that is calculated is using an estimation. As such,
660
+ // it does not perfectly reflect the actual object radius in gameplay.
661
+ let closestDistance = this.maximumObjectRadiusThreshold;
662
+ for (let l = cursorIndices[j]; l < cursors.length; cursorIndices[j] = ++l) {
663
+ const cursor = cursors[l];
664
+ const prevCursor = cursors[l - 1];
665
+ // Cursor is past the object's hit time.
666
+ if (prevCursor.time > hitTime) {
667
+ break;
668
+ }
669
+ // Cursor is before the object's hit time.
670
+ if (hitTime > cursor.time) {
671
+ continue;
672
+ }
673
+ let distance;
667
674
  switch (cursor.id) {
668
675
  case exports.MovementType.up:
669
- isInObject =
670
- prevCursor.position.getDistance(objectPosition) <= object.radius;
676
+ distance =
677
+ prevCursor.position.getDistance(objectPosition);
671
678
  break;
672
679
  case exports.MovementType.move: {
673
680
  // Interpolate movement.
674
681
  const t = (hitTime - prevCursor.time) /
675
682
  (cursor.time - prevCursor.time);
676
- const cursorPosition = new osuBase.Vector2(osuBase.Interpolation.lerp(prevCursor.position.x, cursor.position.x, t), osuBase.Interpolation.lerp(prevCursor.position.y, cursor.position.y, t));
677
- isInObject =
678
- objectPosition.getDistance(cursorPosition) <= object.radius;
683
+ const cursorPosition = osuBase.Interpolation.lerp(prevCursor.position, cursor.position, t);
684
+ distance =
685
+ objectPosition.getDistance(cursorPosition);
686
+ break;
679
687
  }
688
+ case exports.MovementType.down:
689
+ continue;
680
690
  }
681
- break;
691
+ closestDistance = Math.min(closestDistance, distance);
682
692
  }
693
+ // At this point, the object is guaranteed to be hit, but the distance may not reach the threshold
694
+ // due to radius estimation. In order to work around this, we store the farthest distance from all
695
+ // processed cursors so far and use it as threshold rather than the actual object radius.
696
+ if (closestDistance <= this.maximumObjectRadiusThreshold &&
697
+ closestDistance > this.objectRadiusThreshold) {
698
+ this.objectRadiusThreshold = closestDistance;
699
+ }
700
+ if (closestDistance > this.objectRadiusThreshold) {
701
+ cursorInstanceIndices[j] = -1;
702
+ }
703
+ break;
683
704
  }
684
- if (!isInObject) {
685
- cursorIndices[j] = -1;
686
- }
705
+ // The previous object may still be hit with the same cursor group or cursor index.
706
+ cursorGroupIndices[j] = Math.max(0, cursorGroupIndices[j] - 1);
707
+ cursorIndices[j] = Math.max(1, cursorIndices[j] - 1);
687
708
  }
688
709
  }
689
- return (_a = cursorIndices.find((v) => v !== -1)) !== null && _a !== void 0 ? _a : -1;
710
+ return (_a = cursorInstanceIndices.find((v) => v !== -1)) !== null && _a !== void 0 ? _a : -1;
690
711
  }
691
712
  /**
692
713
  * Creates nerf factors by scanning through objects.
@@ -1747,7 +1768,7 @@ class TwoHandChecker {
1747
1768
  }
1748
1769
  const t = (cursorGroup.down.time - prevCursor.time) /
1749
1770
  (cursor.time - prevCursor.time);
1750
- cursorPosition = new osuBase.Vector2(osuBase.Interpolation.lerp(prevCursor.position.x, cursor.position.x, t), osuBase.Interpolation.lerp(prevCursor.position.y, cursor.position.y, t));
1771
+ cursorPosition = osuBase.Interpolation.lerp(prevCursor.position, cursor.position, t);
1751
1772
  const distance = cursorPosition.getDistance(objectPosition);
1752
1773
  if (distance <
1753
1774
  nearestPosition.getDistance(objectPosition)) {
@@ -1843,7 +1864,7 @@ class TwoHandChecker {
1843
1864
  const prevCursor = cursors[k - 1];
1844
1865
  const t = osuBase.MathUtils.clamp((object.endTime - prevCursor.time) /
1845
1866
  (cursor.time - prevCursor.time), 0, 1);
1846
- cursorPosition = new osuBase.Vector2(osuBase.Interpolation.lerp(prevCursor.position.x, cursor.position.x, t), osuBase.Interpolation.lerp(prevCursor.position.y, cursor.position.y, t));
1867
+ cursorPosition = osuBase.Interpolation.lerp(prevCursor.position, cursor.position, t);
1847
1868
  break;
1848
1869
  }
1849
1870
  }
@@ -2169,12 +2190,6 @@ class ReplayAnalyzer {
2169
2190
  * The amount of two-handed objects.
2170
2191
  */
2171
2192
  this.twoHandedNoteCount = 0;
2172
- // Sizes of primitive data types in Java (in bytes)
2173
- this.BYTE_LENGTH = 1;
2174
- this.SHORT_LENGTH = 2;
2175
- this.INT_LENGTH = 4;
2176
- this.FLOAT_LENGTH = 4;
2177
- this.LONG_LENGTH = 8;
2178
2193
  this.scoreID = values.scoreID;
2179
2194
  this.beatmap = values.map;
2180
2195
  this.difficultyAttributes = values.difficultyAttributes;
@@ -2383,34 +2398,27 @@ class ReplayAnalyzer {
2383
2398
  }
2384
2399
  // Merge all cursor movement and hit object data section into one for better control when parsing
2385
2400
  const replayDataBuffer = Buffer.concat(replayDataBufferArray);
2386
- let bufferCounter = 0;
2387
- const size = replayDataBuffer.readInt32BE(bufferCounter);
2388
- bufferCounter += this.INT_LENGTH;
2401
+ const bufferCounter = { counter: 0 };
2402
+ const size = this.readInt(replayDataBuffer, bufferCounter);
2389
2403
  // Parse movement data
2390
2404
  for (let x = 0; x < size; x++) {
2391
- const moveSize = replayDataBuffer.readInt32BE(bufferCounter);
2392
- bufferCounter += this.INT_LENGTH;
2405
+ const moveSize = this.readInt(replayDataBuffer, bufferCounter);
2393
2406
  const time = [];
2394
2407
  const x = [];
2395
2408
  const y = [];
2396
2409
  const id = [];
2397
2410
  for (let i = 0; i < moveSize; i++) {
2398
- time[i] = replayDataBuffer.readInt32BE(bufferCounter);
2399
- bufferCounter += this.INT_LENGTH;
2411
+ time[i] = this.readInt(replayDataBuffer, bufferCounter);
2400
2412
  id[i] = time[i] & 3;
2401
2413
  time[i] >>= 2;
2402
2414
  if (id[i] !== exports.MovementType.up) {
2403
2415
  if (resultObject.replayVersion >= 5) {
2404
- x[i] = replayDataBuffer.readFloatBE(bufferCounter);
2405
- bufferCounter += this.FLOAT_LENGTH;
2406
- y[i] = replayDataBuffer.readFloatBE(bufferCounter);
2407
- bufferCounter += this.FLOAT_LENGTH;
2416
+ x[i] = this.readFloat(replayDataBuffer, bufferCounter);
2417
+ y[i] = this.readFloat(replayDataBuffer, bufferCounter);
2408
2418
  }
2409
2419
  else {
2410
- x[i] = replayDataBuffer.readInt16BE(bufferCounter);
2411
- bufferCounter += this.SHORT_LENGTH;
2412
- y[i] = replayDataBuffer.readInt16BE(bufferCounter);
2413
- bufferCounter += this.SHORT_LENGTH;
2420
+ x[i] = this.readShort(replayDataBuffer, bufferCounter);
2421
+ y[i] = this.readShort(replayDataBuffer, bufferCounter);
2414
2422
  }
2415
2423
  }
2416
2424
  else {
@@ -2426,8 +2434,7 @@ class ReplayAnalyzer {
2426
2434
  id: id,
2427
2435
  }));
2428
2436
  }
2429
- const replayObjectLength = replayDataBuffer.readInt32BE(bufferCounter);
2430
- bufferCounter += this.INT_LENGTH;
2437
+ const replayObjectLength = this.readInt(replayDataBuffer, bufferCounter);
2431
2438
  // Parse result data
2432
2439
  for (let i = 0; i < replayObjectLength; i++) {
2433
2440
  const replayObjectData = {
@@ -2435,16 +2442,12 @@ class ReplayAnalyzer {
2435
2442
  tickset: [],
2436
2443
  result: exports.HitResult.miss,
2437
2444
  };
2438
- replayObjectData.accuracy =
2439
- replayDataBuffer.readInt16BE(bufferCounter);
2440
- bufferCounter += this.SHORT_LENGTH;
2441
- const len = replayDataBuffer.readInt8(bufferCounter);
2442
- bufferCounter += this.BYTE_LENGTH;
2445
+ replayObjectData.accuracy = this.readShort(replayDataBuffer, bufferCounter);
2446
+ const len = this.readByte(replayDataBuffer, bufferCounter);
2443
2447
  if (len > 0) {
2444
2448
  const bytes = [];
2445
2449
  for (let j = 0; j < len; j++) {
2446
- bytes.push(replayDataBuffer.readInt8(bufferCounter));
2447
- bufferCounter += this.BYTE_LENGTH;
2450
+ bytes.push(this.readByte(replayDataBuffer, bufferCounter));
2448
2451
  }
2449
2452
  // Int/int division in Java; numbers must be truncated to get actual number
2450
2453
  for (let j = 0; j < len * 8; j++) {
@@ -2454,9 +2457,7 @@ class ReplayAnalyzer {
2454
2457
  }
2455
2458
  }
2456
2459
  if (resultObject.replayVersion >= 1) {
2457
- replayObjectData.result =
2458
- replayDataBuffer.readInt8(bufferCounter);
2459
- bufferCounter += this.BYTE_LENGTH;
2460
+ replayObjectData.result = this.readByte(replayDataBuffer, bufferCounter);
2460
2461
  }
2461
2462
  resultObject.hitObjectData.push(replayObjectData);
2462
2463
  }
@@ -2570,21 +2571,24 @@ class ReplayAnalyzer {
2570
2571
  */
2571
2572
  convertDroidMods(replayMods) {
2572
2573
  const replayModsConstants = {
2573
- MOD_NOFAIL: "n",
2574
- MOD_EASY: "e",
2575
- MOD_HIDDEN: "h",
2576
- MOD_HARDROCK: "r",
2577
- MOD_DOUBLETIME: "d",
2578
- MOD_HALFTIME: "t",
2579
- MOD_NIGHTCORE: "c",
2580
- MOD_PRECISE: "s",
2581
- MOD_SMALLCIRCLE: "m",
2582
- MOD_SPEEDUP: "b",
2583
- MOD_REALLYEASY: "l",
2584
- MOD_PERFECT: "f",
2585
- MOD_SUDDENDEATH: "u",
2586
- MOD_SCOREV2: "v",
2587
- MOD_FLASHLIGHT: "i",
2574
+ MOD_AUTO: new osuBase.ModAuto().droidString,
2575
+ MOD_AUTOPILOT: new osuBase.ModAutopilot().droidString,
2576
+ MOD_NOFAIL: new osuBase.ModNoFail().droidString,
2577
+ MOD_EASY: new osuBase.ModEasy().droidString,
2578
+ MOD_HIDDEN: new osuBase.ModHidden().droidString,
2579
+ MOD_HARDROCK: new osuBase.ModHardRock().droidString,
2580
+ MOD_DOUBLETIME: new osuBase.ModDoubleTime().droidString,
2581
+ MOD_HALFTIME: new osuBase.ModHalfTime().droidString,
2582
+ MOD_NIGHTCORE: new osuBase.ModNightCore().droidString,
2583
+ MOD_PRECISE: new osuBase.ModPrecise().droidString,
2584
+ MOD_SMALLCIRCLE: new osuBase.ModSmallCircle().droidString,
2585
+ MOD_SPEEDUP: new osuBase.ModSpeedUp().droidString,
2586
+ MOD_REALLYEASY: new osuBase.ModReallyEasy().droidString,
2587
+ MOD_RELAX: new osuBase.ModRelax().droidString,
2588
+ MOD_PERFECT: new osuBase.ModPerfect().droidString,
2589
+ MOD_SUDDENDEATH: new osuBase.ModSuddenDeath().droidString,
2590
+ MOD_SCOREV2: new osuBase.ModScoreV2().droidString,
2591
+ MOD_FLASHLIGHT: new osuBase.ModFlashlight().droidString,
2588
2592
  };
2589
2593
  let modString = "";
2590
2594
  for (const mod of replayMods) {
@@ -2694,6 +2698,26 @@ class ReplayAnalyzer {
2694
2698
  this.sliderCheesePenalty = sliderCheeseChecker.check();
2695
2699
  this.hasBeenCheckedForSliderCheesing = true;
2696
2700
  }
2701
+ readByte(buffer, counter) {
2702
+ const num = buffer.readInt8(counter.counter);
2703
+ counter.counter += 1;
2704
+ return num;
2705
+ }
2706
+ readShort(buffer, counter) {
2707
+ const num = buffer.readInt16BE(counter.counter);
2708
+ counter.counter += 2;
2709
+ return num;
2710
+ }
2711
+ readInt(buffer, counter) {
2712
+ const num = buffer.readInt32BE(counter.counter);
2713
+ counter.counter += 4;
2714
+ return num;
2715
+ }
2716
+ readFloat(buffer, counter) {
2717
+ const num = buffer.readFloatBE(counter.counter);
2718
+ counter.counter += 4;
2719
+ return num;
2720
+ }
2697
2721
  }
2698
2722
 
2699
2723
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rian8337/osu-droid-replay-analyzer",
3
- "version": "4.0.0-beta.21",
3
+ "version": "4.0.0-beta.23",
4
4
  "description": "A replay analyzer for analyzing osu!droid replay files.",
5
5
  "keywords": [
6
6
  "osu",
@@ -34,9 +34,9 @@
34
34
  "url": "https://github.com/Rian8337/osu-droid-module/issues"
35
35
  },
36
36
  "dependencies": {
37
- "@rian8337/osu-base": "^4.0.0-beta.19",
38
- "@rian8337/osu-difficulty-calculator": "^4.0.0-beta.21",
39
- "@rian8337/osu-rebalance-difficulty-calculator": "^4.0.0-beta.21",
37
+ "@rian8337/osu-base": "^4.0.0-beta.23",
38
+ "@rian8337/osu-difficulty-calculator": "^4.0.0-beta.23",
39
+ "@rian8337/osu-rebalance-difficulty-calculator": "^4.0.0-beta.23",
40
40
  "java-deserialization": "^0.1.0",
41
41
  "unzipper": "^0.10.11"
42
42
  },
@@ -46,5 +46,5 @@
46
46
  "publishConfig": {
47
47
  "access": "public"
48
48
  },
49
- "gitHead": "be2833348b74ce7b1ce9fff3fd29af04b778955e"
49
+ "gitHead": "62063ddac57c3a60426c7206268a0573cc6db6dd"
50
50
  }
@@ -572,6 +572,14 @@ declare class RebalanceThreeFingerChecker {
572
572
  * Whether this score uses the Precise mod.
573
573
  */
574
574
  private readonly isPrecise;
575
+ /**
576
+ * The maximum object radius that is acceptable when checking for cursor position in drag detection.
577
+ */
578
+ private objectRadiusThreshold;
579
+ /**
580
+ * The maximum value of object radius threshold.
581
+ */
582
+ private readonly maximumObjectRadiusThreshold;
575
583
  /**
576
584
  * @param beatmap The beatmap to analyze.
577
585
  * @param data The data of the replay.
@@ -733,11 +741,6 @@ declare class ReplayAnalyzer {
733
741
  */
734
742
  twoHandedNoteCount: number;
735
743
  private convertedBeatmap?;
736
- private readonly BYTE_LENGTH;
737
- private readonly SHORT_LENGTH;
738
- private readonly INT_LENGTH;
739
- private readonly FLOAT_LENGTH;
740
- private readonly LONG_LENGTH;
741
744
  constructor(values: {
742
745
  /**
743
746
  * The ID of the score.
@@ -802,6 +805,10 @@ declare class ReplayAnalyzer {
802
805
  * Requires `analyze()` to be called first and `map` and `difficultyAttributes` to be defined.
803
806
  */
804
807
  checkForSliderCheesing(): void;
808
+ private readByte;
809
+ private readShort;
810
+ private readInt;
811
+ private readFloat;
805
812
  }
806
813
 
807
814
  /**