@rian8337/osu-droid-replay-analyzer 4.0.0-beta.96 → 4.0.0-beta.98

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
@@ -1327,7 +1327,7 @@ class TwoHandChecker {
1327
1327
  // this.csvString = `Mods,${
1328
1328
  // data.convertedMods.reduce((a, m) => a + m.acronym, "") || "NM"
1329
1329
  // }\nCombo,${data.maxCombo}\nAccuracy,"${(
1330
- // data.accuracy.value() * 100
1330
+ // data.accuracy.value * 100
1331
1331
  // ).toFixed(2)}% [${data.accuracy.n300}/${data.accuracy.n100}/${
1332
1332
  // data.accuracy.n50
1333
1333
  // }/${
@@ -2195,15 +2195,22 @@ class ReplayData {
2195
2195
  */
2196
2196
  class ReplayV3Data extends ReplayData {
2197
2197
  /**
2198
- * The total score achieved in the play, after applying score multiplier from mods.
2198
+ * Obtains the total score achieved in the play, after applying score multiplier from mods.
2199
+ *
2200
+ * @param difficulty The `BeatmapDifficulty` of the beatmap that was played. Required to correctly compute
2201
+ * the score multiplier of difficulty-dependent mods. If not provided, those mods fall back to their
2202
+ * difficulty-independent multiplier.
2199
2203
  */
2200
- get totalScore() {
2201
- var _a;
2204
+ getTotalScore(difficulty = null) {
2205
+ let baseScore = this.score;
2202
2206
  if (this.replayVersion < 8) {
2203
- return this.score;
2207
+ // Replay versions 3 to 7 store the score after applying score multiplier
2208
+ // from mods, so we need to reverse it to get the base score.
2209
+ const legacyScoreMultiplier = new osuBase.DroidLegacyScoreMultiplierCalculator(difficulty).calculateFor(this.convertedMods.values());
2210
+ baseScore = Math.round(this.score / legacyScoreMultiplier);
2204
2211
  }
2205
- (_a = this.scoreMultiplier) !== null && _a !== void 0 ? _a : (this.scoreMultiplier = osuBase.ModUtil.calculateScoreMultiplier(this.convertedMods.values(), osuBase.Modes.Droid));
2206
- return Math.round(Math.fround(this.score * this.scoreMultiplier));
2212
+ const scoreMultiplier = new osuBase.DroidScoreMultiplierCalculator(difficulty).calculateFor(this.convertedMods.values());
2213
+ return Math.round(baseScore * scoreMultiplier);
2207
2214
  }
2208
2215
  constructor(values) {
2209
2216
  super(values);
@@ -2331,7 +2338,6 @@ class ReplayAnalyzer {
2331
2338
  * `analyze()` must be called before calling this, and `beatmap` must be defined.
2332
2339
  */
2333
2340
  calculateHitError() {
2334
- var _a, _b;
2335
2341
  if (!this.data || !this.beatmap) {
2336
2342
  return null;
2337
2343
  }
@@ -2341,14 +2347,7 @@ class ReplayAnalyzer {
2341
2347
  let positiveTotal = 0;
2342
2348
  let negativeTotal = 0;
2343
2349
  const { objects } = this.beatmap.hitObjects;
2344
- const mods = this.data.isReplayV3()
2345
- ? this.data.convertedMods
2346
- : ((_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods) !== null && _b !== void 0 ? _b : new osuBase.ModMap());
2347
- const adjustedDifficulty = new osuBase.BeatmapDifficulty(this.beatmap.difficulty);
2348
- osuBase.ModUtil.applyModsToBeatmapDifficulty(adjustedDifficulty, osuBase.Modes.Droid, mods);
2349
- const mehWindow = mods.has(osuBase.ModPrecise)
2350
- ? new osuBase.PreciseDroidHitWindow(adjustedDifficulty.od).mehWindow
2351
- : new osuBase.DroidHitWindow(adjustedDifficulty.od).mehWindow;
2350
+ const { mehWindow } = this.getBeatmapHitWindow();
2352
2351
  const accuracies = [];
2353
2352
  for (let i = 0; i < hitObjectData.length; ++i) {
2354
2353
  const v = hitObjectData[i];
@@ -2385,7 +2384,7 @@ class ReplayAnalyzer {
2385
2384
  };
2386
2385
  }
2387
2386
  /**
2388
- * Obtains the amount of slider ticks and ends hit in the replay.
2387
+ * Obtains the amount of slider nested objects hit in the replay.
2389
2388
  *
2390
2389
  * This requires `analyze()` to be called first and `beatmap` to be defined.
2391
2390
  *
@@ -2397,27 +2396,46 @@ class ReplayAnalyzer {
2397
2396
  return null;
2398
2397
  }
2399
2398
  const sliderInformation = {
2400
- tick: { obtained: 0, total: beatmap.hitObjects.sliderTicks },
2401
- end: { obtained: 0, total: beatmap.hitObjects.sliders },
2399
+ head: { obtained: 0, total: beatmap.hitObjects.sliders },
2400
+ tick: { obtained: 0, total: 0 },
2401
+ repeat: { obtained: 0, total: 0 },
2402
+ end: { obtained: 0, total: 0 },
2402
2403
  };
2404
+ const hitWindow = this.getBeatmapHitWindow();
2403
2405
  for (let i = 0; i < data.hitObjectData.length; ++i) {
2404
2406
  const object = beatmap.hitObjects.objects[i];
2405
2407
  const objectData = data.hitObjectData[i];
2406
- if (objectData.result === osuBase.HitResult.Miss ||
2407
- !(object instanceof osuBase.Slider)) {
2408
+ if (!(object instanceof osuBase.Slider)) {
2408
2409
  continue;
2409
2410
  }
2410
- // Exclude the head circle.
2411
+ let lateHitThreshold = hitWindow.mehWindow;
2412
+ // Before replay version 8, the slider head's hit window is capped to the duration of the slider.
2413
+ if (data.replayVersion < 8) {
2414
+ lateHitThreshold = Math.min(lateHitThreshold, object.duration);
2415
+ }
2416
+ if (-hitWindow.mehWindow <= objectData.accuracy &&
2417
+ objectData.accuracy <= lateHitThreshold) {
2418
+ ++sliderInformation.head.obtained;
2419
+ }
2411
2420
  for (let j = 1; j < object.nestedHitObjects.length; ++j) {
2412
2421
  const nested = object.nestedHitObjects[j];
2413
- if (!objectData.tickset[j - 1]) {
2414
- continue;
2415
- }
2416
- if (nested instanceof osuBase.SliderTick) {
2417
- ++sliderInformation.tick.obtained;
2422
+ let tickInformation;
2423
+ switch (true) {
2424
+ case nested instanceof osuBase.SliderTick:
2425
+ tickInformation = sliderInformation.tick;
2426
+ break;
2427
+ case nested instanceof osuBase.SliderRepeat:
2428
+ tickInformation = sliderInformation.repeat;
2429
+ break;
2430
+ case nested instanceof osuBase.SliderTail:
2431
+ tickInformation = sliderInformation.end;
2432
+ break;
2433
+ default:
2434
+ continue;
2418
2435
  }
2419
- else if (nested instanceof osuBase.SliderTail) {
2420
- ++sliderInformation.end.obtained;
2436
+ ++tickInformation.total;
2437
+ if (objectData.tickset[j - 1]) {
2438
+ ++tickInformation.obtained;
2421
2439
  }
2422
2440
  }
2423
2441
  }
@@ -2567,8 +2585,16 @@ class ReplayAnalyzer {
2567
2585
  .setRequireAPIkey(false)
2568
2586
  .setEndpoint("upload")
2569
2587
  .addParameter("", `${this.scoreID.toString()}.odr`);
2570
- const result = yield apiRequestBuilder.sendRequest();
2588
+ let result;
2589
+ try {
2590
+ result = yield apiRequestBuilder.sendRequest();
2591
+ }
2592
+ catch (e) {
2593
+ console.error(`Error retrieving replay for score ID ${this.scoreID.toString()}: ${e.message}`);
2594
+ return null;
2595
+ }
2571
2596
  if (result.statusCode !== 200) {
2597
+ console.error(`Error retrieving replay for score ID ${this.scoreID.toString()}: ${result.describeFailure()}`);
2572
2598
  return null;
2573
2599
  }
2574
2600
  return result.data;
@@ -2646,9 +2672,16 @@ class ReplayAnalyzer {
2646
2672
  resultObject.accuracy.n100 = buf.readInt32BE(20);
2647
2673
  resultObject.accuracy.n50 = buf.readInt32BE(24);
2648
2674
  resultObject.accuracy.nmiss = buf.readInt32BE(28);
2649
- resultObject.score = buf.readInt32BE(32);
2650
- resultObject.maxCombo = buf.readInt32BE(36);
2651
- resultObject.isFullCombo = resultObject.accuracy.value() === 1;
2675
+ if (resultObject.replayVersion >= 9) {
2676
+ // From replay version 9 onwards, the score is stored as a long (8 bytes) instead of an int (4 bytes).
2677
+ resultObject.score = Number(buf.readBigInt64BE(32));
2678
+ resultObject.maxCombo = buf.readInt32BE(40);
2679
+ }
2680
+ else {
2681
+ resultObject.score = buf.readInt32BE(32);
2682
+ resultObject.maxCombo = buf.readInt32BE(36);
2683
+ }
2684
+ resultObject.isFullCombo = resultObject.accuracy.value === 1;
2652
2685
  resultObject.playerName = rawObject[5];
2653
2686
  if (resultObject.replayVersion >= 7) {
2654
2687
  resultObject.convertedMods = osuBase.ModUtil.deserializeMods(JSON.parse(rawObject[6]));
@@ -2674,6 +2707,7 @@ class ReplayAnalyzer {
2674
2707
  case 3:
2675
2708
  case 7:
2676
2709
  case 8:
2710
+ case 9:
2677
2711
  bufferIndex = 7;
2678
2712
  break;
2679
2713
  case 4:
@@ -2867,7 +2901,7 @@ class ReplayAnalyzer {
2867
2901
  resultObject.convertedMods.has(osuBase.ModFlashlight);
2868
2902
  const hit300Ratio = resultObject.accuracy.n300 / totalHits;
2869
2903
  switch (true) {
2870
- case resultObject.accuracy.value() === 1:
2904
+ case resultObject.accuracy.value === 1:
2871
2905
  return isHidden ? "XH" : "X";
2872
2906
  case hit300Ratio > 0.9 &&
2873
2907
  resultObject.accuracy.n50 / totalHits < 0.01 &&
@@ -2898,6 +2932,20 @@ class ReplayAnalyzer {
2898
2932
  : (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods;
2899
2933
  return this.beatmap.createDroidPlayableBeatmap(mods);
2900
2934
  }
2935
+ getBeatmapHitWindow() {
2936
+ var _a, _b;
2937
+ if (!this.data || !this.beatmap) {
2938
+ return null;
2939
+ }
2940
+ const mods = this.data.isReplayV3()
2941
+ ? this.data.convertedMods
2942
+ : ((_b = (_a = this.difficultyAttributes) === null || _a === void 0 ? void 0 : _a.mods) !== null && _b !== void 0 ? _b : new osuBase.ModMap());
2943
+ const adjustedDifficulty = new osuBase.BeatmapDifficulty(this.beatmap.difficulty);
2944
+ osuBase.ModUtil.applyModsToBeatmapDifficulty(adjustedDifficulty, osuBase.Modes.Droid, mods);
2945
+ return mods.has(osuBase.ModPrecise)
2946
+ ? new osuBase.PreciseDroidHitWindow(adjustedDifficulty.od)
2947
+ : new osuBase.DroidHitWindow(adjustedDifficulty.od);
2948
+ }
2901
2949
  readByte(buffer) {
2902
2950
  const num = buffer.readInt8(this.bufferOffset);
2903
2951
  this.bufferOffset += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rian8337/osu-droid-replay-analyzer",
3
- "version": "4.0.0-beta.96",
3
+ "version": "4.0.0-beta.98",
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.96",
38
- "@rian8337/osu-difficulty-calculator": "4.0.0-beta.96",
39
- "@rian8337/osu-rebalance-difficulty-calculator": "4.0.0-beta.96",
37
+ "@rian8337/osu-base": "4.0.0-beta.98",
38
+ "@rian8337/osu-difficulty-calculator": "4.0.0-beta.98",
39
+ "@rian8337/osu-rebalance-difficulty-calculator": "4.0.0-beta.98",
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": "13af8d35ad74141c270c6b985941b5f2a14e248b"
49
+ "gitHead": "0d1f9256f0caee5123379cf5a8b3c92d1dcfa7bf"
50
50
  }
@@ -1,4 +1,4 @@
1
- import { Vector2, HitResult, Accuracy, ScoreRank, ModMap, DroidPlayableBeatmap, Beatmap, HitWindow } from '@rian8337/osu-base';
1
+ import { Vector2, HitResult, Accuracy, ScoreRank, ModMap, BeatmapDifficulty, DroidPlayableBeatmap, Beatmap, HitWindow } from '@rian8337/osu-base';
2
2
  import { IExtendedDroidDifficultyAttributes } from '@rian8337/osu-rebalance-difficulty-calculator';
3
3
  import { IExtendedDroidDifficultyAttributes as IExtendedDroidDifficultyAttributes$1 } from '@rian8337/osu-difficulty-calculator';
4
4
 
@@ -340,7 +340,7 @@ declare class ReplayV3Data extends ReplayData {
340
340
  * Between replay version 3 and 7, this is the final score after applying score multiplier from mods.
341
341
  * From replay version 8 onwards, this is the score before applying score multiplier from mods.
342
342
  *
343
- * The {@link totalScore} getter can be used to get the total score across all replay versions.
343
+ * The {@link getTotalScore} method can be used to get the total score across all replay versions.
344
344
  */
345
345
  readonly score: number;
346
346
  /**
@@ -360,10 +360,13 @@ declare class ReplayV3Data extends ReplayData {
360
360
  */
361
361
  readonly convertedMods: ModMap;
362
362
  /**
363
- * The total score achieved in the play, after applying score multiplier from mods.
363
+ * Obtains the total score achieved in the play, after applying score multiplier from mods.
364
+ *
365
+ * @param difficulty The `BeatmapDifficulty` of the beatmap that was played. Required to correctly compute
366
+ * the score multiplier of difficulty-dependent mods. If not provided, those mods fall back to their
367
+ * difficulty-independent multiplier.
364
368
  */
365
- get totalScore(): number;
366
- private scoreMultiplier?;
369
+ getTotalScore(difficulty?: BeatmapDifficulty | null): number;
367
370
  constructor(values: ReplayInformation);
368
371
  }
369
372
 
@@ -1000,10 +1003,18 @@ type ExportedReplayJSONV4 = ExportedReplayJSON<4, ExportedReplayJSONDataV4>;
1000
1003
  * Represents hit information about sliders in a beatmap.
1001
1004
  */
1002
1005
  interface SliderHitInformation {
1006
+ /**
1007
+ * Hit information of slider heads.
1008
+ */
1009
+ readonly head: SliderNestedHitObjectInformation;
1003
1010
  /**
1004
1011
  * Hit information of slider ticks.
1005
1012
  */
1006
1013
  readonly tick: SliderNestedHitObjectInformation;
1014
+ /**
1015
+ * Hit information of slider repeats.
1016
+ */
1017
+ readonly repeat: SliderNestedHitObjectInformation;
1007
1018
  /**
1008
1019
  * hit information of slider ends.
1009
1020
  */
@@ -1020,7 +1031,7 @@ interface SliderNestedHitObjectInformation {
1020
1031
  /**
1021
1032
  * The amount of the nested hit objects in the beatmap.
1022
1033
  */
1023
- readonly total: number;
1034
+ total: number;
1024
1035
  }
1025
1036
 
1026
1037
  interface HitErrorInformation {
@@ -1123,7 +1134,7 @@ declare class ReplayAnalyzer {
1123
1134
  */
1124
1135
  calculateHitError(): HitErrorInformation | null;
1125
1136
  /**
1126
- * Obtains the amount of slider ticks and ends hit in the replay.
1137
+ * Obtains the amount of slider nested objects hit in the replay.
1127
1138
  *
1128
1139
  * This requires `analyze()` to be called first and `beatmap` to be defined.
1129
1140
  *
@@ -1182,6 +1193,7 @@ declare class ReplayAnalyzer {
1182
1193
  private parseOldReplayInformation;
1183
1194
  private calculateRank;
1184
1195
  private constructPlayableBeatmap;
1196
+ private getBeatmapHitWindow;
1185
1197
  private readByte;
1186
1198
  private readShort;
1187
1199
  private readInt;