@rian8337/osu-droid-replay-analyzer 2.0.0-alpha.2 → 2.0.0-alpha.5
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/package.json +7 -7
- package/typings/index.d.ts +80 -1
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rian8337/osu-droid-replay-analyzer",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.5",
|
|
4
4
|
"description": "A replay analyzer for analyzing osu!droid replay files.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"osu",
|
|
@@ -22,18 +22,18 @@
|
|
|
22
22
|
"url": "git+https://github.com/Rian8337/osu-droid-module.git"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "rollup -c ../../rollup.config.js",
|
|
25
|
+
"build": "dts-gen -m java-deserialization -o -f ./node_modules/java-deserialization/src/index.d.ts && rollup -c ../../rollup.config.js",
|
|
26
26
|
"lint": "eslint --ext ts",
|
|
27
|
-
"
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
28
|
"test": "echo \"No tests for this module\""
|
|
29
29
|
},
|
|
30
30
|
"bugs": {
|
|
31
31
|
"url": "https://github.com/Rian8337/osu-droid-module/issues"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@rian8337/osu-base": "^2.0.0-alpha.
|
|
35
|
-
"@rian8337/osu-difficulty-calculator": "^2.0.0-alpha.
|
|
36
|
-
"@rian8337/osu-rebalance-difficulty-calculator": "^2.0.0-alpha.
|
|
34
|
+
"@rian8337/osu-base": "^2.0.0-alpha.5",
|
|
35
|
+
"@rian8337/osu-difficulty-calculator": "^2.0.0-alpha.5",
|
|
36
|
+
"@rian8337/osu-rebalance-difficulty-calculator": "^2.0.0-alpha.5",
|
|
37
37
|
"java-deserialization": "^0.1.0",
|
|
38
38
|
"unzipper": "^0.10.11"
|
|
39
39
|
},
|
|
@@ -45,5 +45,5 @@
|
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
|
-
"gitHead": "
|
|
48
|
+
"gitHead": "8e75c869d11ed11a58bd9a8d568b72507f6555f0"
|
|
49
49
|
}
|
package/typings/index.d.ts
CHANGED
|
@@ -52,6 +52,85 @@ declare class CursorData implements CursorInformation {
|
|
|
52
52
|
constructor(values: CursorInformation);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Represents an exported replay's JSON structure.
|
|
57
|
+
*/
|
|
58
|
+
interface ExportedReplayJSON {
|
|
59
|
+
/**
|
|
60
|
+
* The version of the exported replay.
|
|
61
|
+
*/
|
|
62
|
+
version: number;
|
|
63
|
+
/**
|
|
64
|
+
* Data of the exported replay.
|
|
65
|
+
*/
|
|
66
|
+
replaydata: {
|
|
67
|
+
/**
|
|
68
|
+
* The path towards the beatmap's `.osu` file from the song directory of the game.
|
|
69
|
+
*/
|
|
70
|
+
filename: string;
|
|
71
|
+
/**
|
|
72
|
+
* The name of the player.
|
|
73
|
+
*/
|
|
74
|
+
playername: string;
|
|
75
|
+
/**
|
|
76
|
+
* The name of the replay file.
|
|
77
|
+
*/
|
|
78
|
+
replayfile: string;
|
|
79
|
+
/**
|
|
80
|
+
* Droid modifications that are used in the replay.
|
|
81
|
+
*/
|
|
82
|
+
mod: string;
|
|
83
|
+
/**
|
|
84
|
+
* The amount of total score achieved.
|
|
85
|
+
*/
|
|
86
|
+
score: number;
|
|
87
|
+
/**
|
|
88
|
+
* The maximum combo achieved.
|
|
89
|
+
*/
|
|
90
|
+
combo: number;
|
|
91
|
+
/**
|
|
92
|
+
* The rank achieved in the replay.
|
|
93
|
+
*/
|
|
94
|
+
mark: string;
|
|
95
|
+
/**
|
|
96
|
+
* The amount of geki hits in the replay.
|
|
97
|
+
*/
|
|
98
|
+
h300k: number;
|
|
99
|
+
/**
|
|
100
|
+
* The amount of great hits in the replay.
|
|
101
|
+
*/
|
|
102
|
+
h300: number;
|
|
103
|
+
/**
|
|
104
|
+
* The amount of katu hits in the replay.
|
|
105
|
+
*/
|
|
106
|
+
h100k: number;
|
|
107
|
+
/**
|
|
108
|
+
* The amount of good hits in the replay.
|
|
109
|
+
*/
|
|
110
|
+
h100: number;
|
|
111
|
+
/**
|
|
112
|
+
* The amount of meh hits in the replay.
|
|
113
|
+
*/
|
|
114
|
+
h50: number;
|
|
115
|
+
/**
|
|
116
|
+
* The amount of misses in the replay.
|
|
117
|
+
*/
|
|
118
|
+
misses: number;
|
|
119
|
+
/**
|
|
120
|
+
* Accuracy gained in the replay.
|
|
121
|
+
*/
|
|
122
|
+
accuracy: number;
|
|
123
|
+
/**
|
|
124
|
+
* The epoch date at which the score was set, in milliseconds.
|
|
125
|
+
*/
|
|
126
|
+
time: number;
|
|
127
|
+
/**
|
|
128
|
+
* Whether the score is a full combo (1 is `true`, 0 is `false`).
|
|
129
|
+
*/
|
|
130
|
+
perfect: number;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
55
134
|
/**
|
|
56
135
|
* The result of a hit in an osu!droid replay.
|
|
57
136
|
*/
|
|
@@ -571,4 +650,4 @@ declare class ThreeFingerChecker {
|
|
|
571
650
|
private calculateFinalPenalty;
|
|
572
651
|
}
|
|
573
652
|
|
|
574
|
-
export { CursorData, CursorInformation, ReplayAnalyzer, ReplayData, ReplayInformation, ReplayObjectData, ThreeFingerChecker, ThreeFingerInformation, hitResult, movementType };
|
|
653
|
+
export { CursorData, CursorInformation, ExportedReplayJSON, HitErrorInformation, ReplayAnalyzer, ReplayData, ReplayInformation, ReplayObjectData, ThreeFingerChecker, ThreeFingerInformation, hitResult, movementType };
|
package/dist/ReplayAnalyzer.js
DELETED
|
@@ -1,541 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
|
|
5
|
-
}) : (function(o, m, k, k2) {
|
|
6
|
-
if (k2 === undefined) k2 = k;
|
|
7
|
-
o[k2] = m[k];
|
|
8
|
-
}));
|
|
9
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
10
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
11
|
-
}) : function(o, v) {
|
|
12
|
-
o["default"] = v;
|
|
13
|
-
});
|
|
14
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
15
|
-
if (mod && mod.__esModule) return mod;
|
|
16
|
-
var result = {};
|
|
17
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
18
|
-
__setModuleDefault(result, mod);
|
|
19
|
-
return result;
|
|
20
|
-
};
|
|
21
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.ReplayAnalyzer = void 0;
|
|
23
|
-
const osu_base_1 = require("@rian8337/osu-base");
|
|
24
|
-
const osu_difficulty_calculator_1 = require("@rian8337/osu-difficulty-calculator");
|
|
25
|
-
const osu_rebalance_difficulty_calculator_1 = require("@rian8337/osu-rebalance-difficulty-calculator");
|
|
26
|
-
const unzipper_1 = require("unzipper");
|
|
27
|
-
const javaDeserialization = __importStar(require("java-deserialization"));
|
|
28
|
-
const stream_1 = require("stream");
|
|
29
|
-
const ReplayData_1 = require("./data/ReplayData");
|
|
30
|
-
const ThreeFingerChecker_1 = require("./analysis/ThreeFingerChecker");
|
|
31
|
-
const TwoHandChecker_1 = require("./analysis/TwoHandChecker");
|
|
32
|
-
const movementType_1 = require("./constants/movementType");
|
|
33
|
-
const hitResult_1 = require("./constants/hitResult");
|
|
34
|
-
/**
|
|
35
|
-
* A replay analyzer that analyzes a replay from osu!droid.
|
|
36
|
-
*
|
|
37
|
-
* Created by reverse engineering the replay parser from the game itself, which can be found {@link https://github.com/osudroid/osu-droid/blob/master/src/ru/nsu/ccfit/zuev/osu/scoring/Replay.java here}.
|
|
38
|
-
*
|
|
39
|
-
* Once analyzed, the result can be accessed via the `data` property.
|
|
40
|
-
*/
|
|
41
|
-
class ReplayAnalyzer {
|
|
42
|
-
constructor(values) {
|
|
43
|
-
/**
|
|
44
|
-
* The original odr file of the replay.
|
|
45
|
-
*/
|
|
46
|
-
this.originalODR = null;
|
|
47
|
-
/**
|
|
48
|
-
* The fixed odr file of the replay.
|
|
49
|
-
*/
|
|
50
|
-
this.fixedODR = null;
|
|
51
|
-
/**
|
|
52
|
-
* The results of the analyzer. `null` when initialized.
|
|
53
|
-
*/
|
|
54
|
-
this.data = null;
|
|
55
|
-
/**
|
|
56
|
-
* Penalty value used to penalize dpp for 2-hand.
|
|
57
|
-
*/
|
|
58
|
-
this.aimPenalty = 1;
|
|
59
|
-
/**
|
|
60
|
-
* Penalty value used to penalize dpp for 3 finger abuse.
|
|
61
|
-
*/
|
|
62
|
-
this.tapPenalty = 1;
|
|
63
|
-
/**
|
|
64
|
-
* Whether this replay has been checked against 3 finger usage.
|
|
65
|
-
*/
|
|
66
|
-
this.hasBeenCheckedFor3Finger = false;
|
|
67
|
-
/**
|
|
68
|
-
* Whether this replay has been checked against 2 hand usage.
|
|
69
|
-
*/
|
|
70
|
-
this.hasBeenCheckedFor2Hand = false;
|
|
71
|
-
// Sizes of primitive data types in Java (in bytes)
|
|
72
|
-
this.BYTE_LENGTH = 1;
|
|
73
|
-
this.SHORT_LENGTH = 2;
|
|
74
|
-
this.INT_LENGTH = 4;
|
|
75
|
-
this.FLOAT_LENGTH = 4;
|
|
76
|
-
this.LONG_LENGTH = 8;
|
|
77
|
-
this.scoreID = values.scoreID;
|
|
78
|
-
this.map = values.map;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Analyzes a replay.
|
|
82
|
-
*/
|
|
83
|
-
async analyze() {
|
|
84
|
-
if (!this.originalODR && !this.fixedODR) {
|
|
85
|
-
this.originalODR = await this.downloadReplay();
|
|
86
|
-
}
|
|
87
|
-
if (!this.originalODR) {
|
|
88
|
-
return this;
|
|
89
|
-
}
|
|
90
|
-
if (!this.fixedODR) {
|
|
91
|
-
this.fixedODR = await this.decompress().catch(() => {
|
|
92
|
-
return null;
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
if (!this.fixedODR) {
|
|
96
|
-
return this;
|
|
97
|
-
}
|
|
98
|
-
this.parseReplay();
|
|
99
|
-
return this;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Downloads the given score ID's replay.
|
|
103
|
-
*/
|
|
104
|
-
async downloadReplay() {
|
|
105
|
-
const apiRequestBuilder = new osu_base_1.DroidAPIRequestBuilder()
|
|
106
|
-
.setRequireAPIkey(false)
|
|
107
|
-
.setEndpoint("upload")
|
|
108
|
-
.addParameter("", `${this.scoreID}.odr`);
|
|
109
|
-
const result = await apiRequestBuilder.sendRequest();
|
|
110
|
-
if (result.statusCode !== 200) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
return result.data;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Decompresses a replay.
|
|
117
|
-
*
|
|
118
|
-
* The decompressed replay is in a form of Java object. This will be converted to a buffer and deserialized to read data from the replay.
|
|
119
|
-
*/
|
|
120
|
-
decompress() {
|
|
121
|
-
return new Promise((resolve, reject) => {
|
|
122
|
-
const stream = new stream_1.Readable();
|
|
123
|
-
stream.push(this.originalODR);
|
|
124
|
-
stream.push(null);
|
|
125
|
-
stream
|
|
126
|
-
.pipe((0, unzipper_1.Parse)())
|
|
127
|
-
.on("entry", async (entry) => {
|
|
128
|
-
const fileName = entry.path;
|
|
129
|
-
if (fileName === "data") {
|
|
130
|
-
return resolve(await entry.buffer());
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
entry.autodrain();
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
.on("error", (e) => {
|
|
137
|
-
setTimeout(() => reject(e), 2000);
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Parses a replay after being downloaded and converted to a buffer.
|
|
143
|
-
*/
|
|
144
|
-
parseReplay() {
|
|
145
|
-
// javaDeserialization can only somewhat parse some string field
|
|
146
|
-
// the rest will be a buffer that we need to manually parse
|
|
147
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
-
let rawObject;
|
|
149
|
-
try {
|
|
150
|
-
rawObject = javaDeserialization.parse(this.fixedODR);
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const resultObject = {
|
|
156
|
-
replayVersion: rawObject[0].version,
|
|
157
|
-
folderName: rawObject[1],
|
|
158
|
-
fileName: rawObject[2],
|
|
159
|
-
hash: rawObject[3],
|
|
160
|
-
cursorMovement: [],
|
|
161
|
-
hitObjectData: [],
|
|
162
|
-
};
|
|
163
|
-
if (resultObject.replayVersion >= 3) {
|
|
164
|
-
resultObject.time = new Date(Number(rawObject[4].readBigUInt64BE(0)));
|
|
165
|
-
resultObject.hit300k = rawObject[4].readInt32BE(8);
|
|
166
|
-
resultObject.hit100k = rawObject[4].readInt32BE(16);
|
|
167
|
-
resultObject.score = rawObject[4].readInt32BE(32);
|
|
168
|
-
resultObject.maxCombo = rawObject[4].readInt32BE(36);
|
|
169
|
-
resultObject.accuracy = new osu_base_1.Accuracy({
|
|
170
|
-
n300: rawObject[4].readInt32BE(12),
|
|
171
|
-
n100: rawObject[4].readInt32BE(20),
|
|
172
|
-
n50: rawObject[4].readInt32BE(24),
|
|
173
|
-
nmiss: rawObject[4].readInt32BE(28),
|
|
174
|
-
});
|
|
175
|
-
resultObject.isFullCombo = !!rawObject[4][44];
|
|
176
|
-
resultObject.playerName = rawObject[5];
|
|
177
|
-
resultObject.rawMods = rawObject[6].elements;
|
|
178
|
-
resultObject.convertedMods = this.convertMods(rawObject[6].elements);
|
|
179
|
-
// Determine rank
|
|
180
|
-
const totalHits = resultObject.accuracy.n300 +
|
|
181
|
-
resultObject.accuracy.n100 +
|
|
182
|
-
resultObject.accuracy.n50 +
|
|
183
|
-
resultObject.accuracy.nmiss;
|
|
184
|
-
const isHidden = resultObject.convertedMods.some((m) => m instanceof osu_base_1.ModHidden || m instanceof osu_base_1.ModFlashlight);
|
|
185
|
-
const hit300Ratio = resultObject.accuracy.n300 / totalHits;
|
|
186
|
-
switch (true) {
|
|
187
|
-
case resultObject.accuracy.value() === 1:
|
|
188
|
-
if (isHidden) {
|
|
189
|
-
resultObject.rank = "XH";
|
|
190
|
-
}
|
|
191
|
-
else {
|
|
192
|
-
resultObject.rank = "X";
|
|
193
|
-
}
|
|
194
|
-
break;
|
|
195
|
-
case hit300Ratio > 0.9 &&
|
|
196
|
-
resultObject.accuracy.n50 / totalHits < 0.01 &&
|
|
197
|
-
!resultObject.accuracy.nmiss:
|
|
198
|
-
if (isHidden) {
|
|
199
|
-
resultObject.rank = "SH";
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
resultObject.rank = "S";
|
|
203
|
-
}
|
|
204
|
-
break;
|
|
205
|
-
case (hit300Ratio > 0.8 && !resultObject.accuracy.nmiss) ||
|
|
206
|
-
hit300Ratio > 0.9:
|
|
207
|
-
resultObject.rank = "A";
|
|
208
|
-
break;
|
|
209
|
-
case (hit300Ratio > 0.7 && !resultObject.accuracy.nmiss) ||
|
|
210
|
-
hit300Ratio > 0.8:
|
|
211
|
-
resultObject.rank = "B";
|
|
212
|
-
break;
|
|
213
|
-
case hit300Ratio > 0.6:
|
|
214
|
-
resultObject.rank = "C";
|
|
215
|
-
break;
|
|
216
|
-
default:
|
|
217
|
-
resultObject.rank = "D";
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (resultObject.replayVersion >= 4) {
|
|
221
|
-
const s = rawObject[7].split("|");
|
|
222
|
-
resultObject.speedModification =
|
|
223
|
-
parseFloat(s[0].replace("x", "")) || 1;
|
|
224
|
-
if (s.length > 1) {
|
|
225
|
-
resultObject.forcedAR = parseFloat(s[1].replace("AR", ""));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
let bufferIndex;
|
|
229
|
-
switch (true) {
|
|
230
|
-
// replay v4 and above
|
|
231
|
-
case resultObject.replayVersion >= 4:
|
|
232
|
-
bufferIndex = 8;
|
|
233
|
-
break;
|
|
234
|
-
// replay v3
|
|
235
|
-
case resultObject.replayVersion === 3:
|
|
236
|
-
bufferIndex = 7;
|
|
237
|
-
break;
|
|
238
|
-
// replay v1 and v2
|
|
239
|
-
default:
|
|
240
|
-
bufferIndex = 4;
|
|
241
|
-
}
|
|
242
|
-
const replayDataBufferArray = [];
|
|
243
|
-
for (bufferIndex; bufferIndex < rawObject.length; ++bufferIndex) {
|
|
244
|
-
replayDataBufferArray.push(rawObject[bufferIndex]);
|
|
245
|
-
}
|
|
246
|
-
// Merge all cursor movement and hit object data section into one for better control when parsing
|
|
247
|
-
const replayDataBuffer = Buffer.concat(replayDataBufferArray);
|
|
248
|
-
let bufferCounter = 0;
|
|
249
|
-
const size = replayDataBuffer.readInt32BE(bufferCounter);
|
|
250
|
-
bufferCounter += this.INT_LENGTH;
|
|
251
|
-
// Parse movement data
|
|
252
|
-
for (let x = 0; x < size; x++) {
|
|
253
|
-
const moveSize = replayDataBuffer.readInt32BE(bufferCounter);
|
|
254
|
-
bufferCounter += this.INT_LENGTH;
|
|
255
|
-
const moveArray = {
|
|
256
|
-
size: moveSize,
|
|
257
|
-
time: [],
|
|
258
|
-
x: [],
|
|
259
|
-
y: [],
|
|
260
|
-
id: [],
|
|
261
|
-
};
|
|
262
|
-
for (let i = 0; i < moveSize; i++) {
|
|
263
|
-
moveArray.time[i] = replayDataBuffer.readInt32BE(bufferCounter);
|
|
264
|
-
bufferCounter += this.INT_LENGTH;
|
|
265
|
-
moveArray.id[i] = moveArray.time[i] & 3;
|
|
266
|
-
moveArray.time[i] >>= 2;
|
|
267
|
-
if (moveArray.id[i] !== movementType_1.movementType.UP) {
|
|
268
|
-
if (resultObject.replayVersion >= 5) {
|
|
269
|
-
moveArray.x[i] =
|
|
270
|
-
replayDataBuffer.readFloatBE(bufferCounter);
|
|
271
|
-
bufferCounter += this.FLOAT_LENGTH;
|
|
272
|
-
moveArray.y[i] =
|
|
273
|
-
replayDataBuffer.readFloatBE(bufferCounter);
|
|
274
|
-
bufferCounter += this.FLOAT_LENGTH;
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
moveArray.x[i] =
|
|
278
|
-
replayDataBuffer.readInt16BE(bufferCounter);
|
|
279
|
-
bufferCounter += this.SHORT_LENGTH;
|
|
280
|
-
moveArray.y[i] =
|
|
281
|
-
replayDataBuffer.readInt16BE(bufferCounter);
|
|
282
|
-
bufferCounter += this.SHORT_LENGTH;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
moveArray.x[i] = -1;
|
|
287
|
-
moveArray.y[i] = -1;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
resultObject.cursorMovement.push(moveArray);
|
|
291
|
-
}
|
|
292
|
-
const replayObjectLength = replayDataBuffer.readInt32BE(bufferCounter);
|
|
293
|
-
bufferCounter += this.INT_LENGTH;
|
|
294
|
-
// Parse result data
|
|
295
|
-
for (let i = 0; i < replayObjectLength; i++) {
|
|
296
|
-
const replayObjectData = {
|
|
297
|
-
accuracy: 0,
|
|
298
|
-
tickset: [],
|
|
299
|
-
result: 0,
|
|
300
|
-
};
|
|
301
|
-
replayObjectData.accuracy =
|
|
302
|
-
replayDataBuffer.readInt16BE(bufferCounter);
|
|
303
|
-
bufferCounter += this.SHORT_LENGTH;
|
|
304
|
-
const len = replayDataBuffer.readInt8(bufferCounter);
|
|
305
|
-
bufferCounter += this.BYTE_LENGTH;
|
|
306
|
-
if (len > 0) {
|
|
307
|
-
const bytes = [];
|
|
308
|
-
for (let j = 0; j < len; j++) {
|
|
309
|
-
bytes.push(replayDataBuffer.readInt8(bufferCounter));
|
|
310
|
-
bufferCounter += this.BYTE_LENGTH;
|
|
311
|
-
}
|
|
312
|
-
// Int/int division in Java; numbers must be truncated to get actual number
|
|
313
|
-
for (let j = 0; j < len * 8; j++) {
|
|
314
|
-
replayObjectData.tickset[j] =
|
|
315
|
-
(bytes[len - Math.trunc(j / 8) - 1] &
|
|
316
|
-
(1 << Math.trunc(j % 8))) !==
|
|
317
|
-
0;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
if (resultObject.replayVersion >= 1) {
|
|
321
|
-
replayObjectData.result =
|
|
322
|
-
replayDataBuffer.readInt8(bufferCounter);
|
|
323
|
-
bufferCounter += this.BYTE_LENGTH;
|
|
324
|
-
}
|
|
325
|
-
resultObject.hitObjectData.push(replayObjectData);
|
|
326
|
-
}
|
|
327
|
-
// Parse max combo, hit results, and accuracy in old replay version
|
|
328
|
-
if (resultObject.replayVersion < 3 && this.map) {
|
|
329
|
-
let hit300 = 0;
|
|
330
|
-
let hit300k = 0;
|
|
331
|
-
let hit100 = 0;
|
|
332
|
-
let hit100k = 0;
|
|
333
|
-
let hit50 = 0;
|
|
334
|
-
let hit0 = 0;
|
|
335
|
-
let grantsGekiOrKatu = true;
|
|
336
|
-
const objects = (this.map instanceof osu_difficulty_calculator_1.DroidStarRating ||
|
|
337
|
-
this.map instanceof osu_rebalance_difficulty_calculator_1.DroidStarRating
|
|
338
|
-
? this.map.map
|
|
339
|
-
: this.map).objects;
|
|
340
|
-
for (let i = 0; i < resultObject.hitObjectData.length; ++i) {
|
|
341
|
-
// Hit result
|
|
342
|
-
const hitObjectData = resultObject.hitObjectData[i];
|
|
343
|
-
const isNextNewCombo = i + 1 !== objects.length ? objects[i + 1].isNewCombo : true;
|
|
344
|
-
switch (hitObjectData.result) {
|
|
345
|
-
case hitResult_1.hitResult.RESULT_0:
|
|
346
|
-
++hit0;
|
|
347
|
-
grantsGekiOrKatu = false;
|
|
348
|
-
break;
|
|
349
|
-
case hitResult_1.hitResult.RESULT_50:
|
|
350
|
-
++hit50;
|
|
351
|
-
grantsGekiOrKatu = false;
|
|
352
|
-
break;
|
|
353
|
-
case hitResult_1.hitResult.RESULT_100:
|
|
354
|
-
++hit100;
|
|
355
|
-
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
356
|
-
++hit100k;
|
|
357
|
-
}
|
|
358
|
-
break;
|
|
359
|
-
case hitResult_1.hitResult.RESULT_300:
|
|
360
|
-
++hit300;
|
|
361
|
-
if (grantsGekiOrKatu && isNextNewCombo) {
|
|
362
|
-
++hit300k;
|
|
363
|
-
}
|
|
364
|
-
break;
|
|
365
|
-
}
|
|
366
|
-
if (isNextNewCombo) {
|
|
367
|
-
grantsGekiOrKatu = true;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
resultObject.hit300k = hit300k;
|
|
371
|
-
resultObject.hit100k = hit100k;
|
|
372
|
-
resultObject.accuracy = new osu_base_1.Accuracy({
|
|
373
|
-
n300: hit300,
|
|
374
|
-
n100: hit100,
|
|
375
|
-
n50: hit50,
|
|
376
|
-
nmiss: hit0,
|
|
377
|
-
nobjects: hit300 + hit100 + hit50 + hit0,
|
|
378
|
-
});
|
|
379
|
-
// Determine rank
|
|
380
|
-
const totalHits = resultObject.accuracy.n300 +
|
|
381
|
-
resultObject.accuracy.n100 +
|
|
382
|
-
resultObject.accuracy.n50 +
|
|
383
|
-
resultObject.accuracy.nmiss;
|
|
384
|
-
const isHidden = resultObject.convertedMods?.some((m) => m instanceof osu_base_1.ModHidden || m instanceof osu_base_1.ModFlashlight) ?? false;
|
|
385
|
-
const hit300Ratio = resultObject.accuracy.n300 / totalHits;
|
|
386
|
-
switch (true) {
|
|
387
|
-
case resultObject.accuracy.value() === 1:
|
|
388
|
-
if (isHidden) {
|
|
389
|
-
resultObject.rank = "XH";
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
resultObject.rank = "X";
|
|
393
|
-
}
|
|
394
|
-
break;
|
|
395
|
-
case hit300Ratio > 0.9 &&
|
|
396
|
-
resultObject.accuracy.n50 / totalHits < 0.01 &&
|
|
397
|
-
!resultObject.accuracy.nmiss:
|
|
398
|
-
if (isHidden) {
|
|
399
|
-
resultObject.rank = "SH";
|
|
400
|
-
}
|
|
401
|
-
else {
|
|
402
|
-
resultObject.rank = "S";
|
|
403
|
-
}
|
|
404
|
-
break;
|
|
405
|
-
case (hit300Ratio > 0.8 && !resultObject.accuracy.nmiss) ||
|
|
406
|
-
hit300Ratio > 0.9:
|
|
407
|
-
resultObject.rank = "A";
|
|
408
|
-
break;
|
|
409
|
-
case (hit300Ratio > 0.7 && !resultObject.accuracy.nmiss) ||
|
|
410
|
-
hit300Ratio > 0.8:
|
|
411
|
-
resultObject.rank = "B";
|
|
412
|
-
break;
|
|
413
|
-
case hit300Ratio > 0.6:
|
|
414
|
-
resultObject.rank = "C";
|
|
415
|
-
break;
|
|
416
|
-
default:
|
|
417
|
-
resultObject.rank = "D";
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
this.data = new ReplayData_1.ReplayData(resultObject);
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* Gets hit error information of the replay.
|
|
424
|
-
*
|
|
425
|
-
* `analyze()` must be called before calling this.
|
|
426
|
-
*/
|
|
427
|
-
calculateHitError() {
|
|
428
|
-
if (!this.data || !this.map) {
|
|
429
|
-
return null;
|
|
430
|
-
}
|
|
431
|
-
const hitObjectData = this.data.hitObjectData;
|
|
432
|
-
let positiveCount = 0;
|
|
433
|
-
let negativeCount = 0;
|
|
434
|
-
let positiveTotal = 0;
|
|
435
|
-
let negativeTotal = 0;
|
|
436
|
-
const objects = (this.map instanceof osu_difficulty_calculator_1.DroidStarRating ||
|
|
437
|
-
this.map instanceof osu_rebalance_difficulty_calculator_1.DroidStarRating
|
|
438
|
-
? this.map.map
|
|
439
|
-
: this.map).objects;
|
|
440
|
-
for (let i = 0; i < hitObjectData.length; ++i) {
|
|
441
|
-
const v = hitObjectData[i];
|
|
442
|
-
const o = objects[i];
|
|
443
|
-
if (o instanceof osu_base_1.Spinner || v.result === hitResult_1.hitResult.RESULT_0) {
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
const accuracy = v.accuracy;
|
|
447
|
-
if (accuracy >= 0) {
|
|
448
|
-
positiveTotal += accuracy;
|
|
449
|
-
++positiveCount;
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
negativeTotal += accuracy;
|
|
453
|
-
++negativeCount;
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return {
|
|
457
|
-
positiveAvg: positiveTotal / positiveCount || 0,
|
|
458
|
-
negativeAvg: negativeTotal / negativeCount || 0,
|
|
459
|
-
unstableRate: osu_base_1.MathUtils.calculateStandardDeviation(hitObjectData.map((v, i) => v.result !== hitResult_1.hitResult.RESULT_0 &&
|
|
460
|
-
!(objects[i] instanceof osu_base_1.Spinner)
|
|
461
|
-
? v.accuracy
|
|
462
|
-
: 0)) * 10,
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Converts replay mods to droid mod string.
|
|
467
|
-
*/
|
|
468
|
-
convertDroidMods(replayMods) {
|
|
469
|
-
const replayModsConstants = {
|
|
470
|
-
MOD_NOFAIL: "n",
|
|
471
|
-
MOD_EASY: "e",
|
|
472
|
-
MOD_HIDDEN: "h",
|
|
473
|
-
MOD_HARDROCK: "r",
|
|
474
|
-
MOD_DOUBLETIME: "d",
|
|
475
|
-
MOD_HALFTIME: "t",
|
|
476
|
-
MOD_NIGHTCORE: "c",
|
|
477
|
-
MOD_PRECISE: "s",
|
|
478
|
-
MOD_SMALLCIRCLE: "m",
|
|
479
|
-
MOD_SPEEDUP: "b",
|
|
480
|
-
MOD_REALLYEASY: "l",
|
|
481
|
-
MOD_PERFECT: "f",
|
|
482
|
-
MOD_SUDDENDEATH: "u",
|
|
483
|
-
MOD_SCOREV2: "v",
|
|
484
|
-
};
|
|
485
|
-
let modString = "";
|
|
486
|
-
for (const mod of replayMods) {
|
|
487
|
-
for (const property in replayModsConstants) {
|
|
488
|
-
if (!(property in replayModsConstants)) {
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
if (!mod.includes(property)) {
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
modString +=
|
|
495
|
-
replayModsConstants[property];
|
|
496
|
-
break;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
return modString;
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* Converts replay mods to regular mod string.
|
|
503
|
-
*/
|
|
504
|
-
convertMods(replayMods) {
|
|
505
|
-
return osu_base_1.ModUtil.droidStringToMods(this.convertDroidMods(replayMods));
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Checks if a play is using 3 fingers.
|
|
509
|
-
*
|
|
510
|
-
* Requires `analyze()` to be called first and `map` to be defined as `DroidStarRating` or `RebalanceDroidStarRating`.
|
|
511
|
-
*/
|
|
512
|
-
checkFor3Finger() {
|
|
513
|
-
if (!(this.map instanceof osu_difficulty_calculator_1.DroidStarRating ||
|
|
514
|
-
this.map instanceof osu_rebalance_difficulty_calculator_1.DroidStarRating) ||
|
|
515
|
-
!this.data) {
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
const threeFingerChecker = new ThreeFingerChecker_1.ThreeFingerChecker(this.map, this.data);
|
|
519
|
-
const result = threeFingerChecker.check();
|
|
520
|
-
this.is3Finger = result.is3Finger;
|
|
521
|
-
this.tapPenalty = result.penalty;
|
|
522
|
-
this.hasBeenCheckedFor3Finger = true;
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Checks if a play is using 2 hands.
|
|
526
|
-
*
|
|
527
|
-
* Requires `analyze()` to be called first and `map` to be defined as `DroidStarRating` or `RebalanceDroidStarRating`.
|
|
528
|
-
*/
|
|
529
|
-
checkFor2Hand() {
|
|
530
|
-
if (!(this.map instanceof osu_difficulty_calculator_1.DroidStarRating ||
|
|
531
|
-
this.map instanceof osu_rebalance_difficulty_calculator_1.DroidStarRating) ||
|
|
532
|
-
!this.data) {
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
const twoHandChecker = new TwoHandChecker_1.TwoHandChecker(this.map, this.data);
|
|
536
|
-
this.is2Hand = twoHandChecker.check();
|
|
537
|
-
this.hasBeenCheckedFor2Hand = true;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
exports.ReplayAnalyzer = ReplayAnalyzer;
|
|
541
|
-
//# sourceMappingURL=ReplayAnalyzer.js.map
|