@rian8337/osu-droid-replay-analyzer 1.0.0

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.
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TwoHandChecker = void 0;
4
+ const osu_base_1 = require("@rian8337/osu-base");
5
+ const __1 = require("..");
6
+ const IndexedHitObject_1 = require("./objects/IndexedHitObject");
7
+ /**
8
+ * Utility to check whether or not a beatmap is two-handed.
9
+ */
10
+ class TwoHandChecker {
11
+ /**
12
+ * @param map The beatmap to analyze.
13
+ * @param data The data of the replay.
14
+ */
15
+ constructor(map, data) {
16
+ /**
17
+ * A cursor data array that only contains `movementType.DOWN` and `movementType.MOVE` movement ID occurrences.
18
+ */
19
+ this.downMoveCursorInstances = [];
20
+ /**
21
+ * The hitobjects of the beatmap that have been assigned with their respective cursor index.
22
+ */
23
+ this.indexedHitObjects = [];
24
+ /**
25
+ * The minimum count of a cursor index occurrence to be valid.
26
+ *
27
+ * This is used to prevent excessive penalty by splitting the beatmap into
28
+ * those that do not worth any strain.
29
+ */
30
+ this.minCursorIndexCount = 5;
31
+ this.map = map;
32
+ this.data = data;
33
+ const stats = new osu_base_1.MapStats({
34
+ od: this.map.map.od,
35
+ mods: this.map.mods.filter((m) => !osu_base_1.ModUtil.speedChangingMods
36
+ .map((v) => v.droidString)
37
+ .includes(m.droidString)),
38
+ }).calculate();
39
+ this.hitWindow = new osu_base_1.DroidHitWindow(stats.od);
40
+ }
41
+ /**
42
+ * Checks if a beatmap is two-handed.
43
+ */
44
+ check() {
45
+ this.filterCursorInstances();
46
+ if (this.downMoveCursorInstances.filter((v) => v.size > 0).length <= 1) {
47
+ return false;
48
+ }
49
+ this.indexHitObjects();
50
+ this.applyPenalty();
51
+ return true;
52
+ }
53
+ /**
54
+ * Filters the original cursor instances, returning only those with `movementType.DOWN` and `movementType.MOVE` movement ID.
55
+ */
56
+ filterCursorInstances() {
57
+ for (let i = 0; i < this.data.cursorMovement.length; ++i) {
58
+ const cursorInstance = this.data.cursorMovement[i];
59
+ const newCursorData = {
60
+ size: 0,
61
+ time: [],
62
+ x: [],
63
+ y: [],
64
+ id: [],
65
+ };
66
+ for (let j = 0; j < cursorInstance.size; ++j) {
67
+ if (cursorInstance.id[j] === __1.movementType.UP) {
68
+ continue;
69
+ }
70
+ ++newCursorData.size;
71
+ newCursorData.time.push(cursorInstance.time[j]);
72
+ newCursorData.x.push(cursorInstance.x[j]);
73
+ newCursorData.y.push(cursorInstance.y[j]);
74
+ newCursorData.id.push(cursorInstance.id[j]);
75
+ }
76
+ this.downMoveCursorInstances.push(newCursorData);
77
+ }
78
+ }
79
+ /**
80
+ * Converts hitobjects into indexed hit objects.
81
+ */
82
+ indexHitObjects() {
83
+ const objects = this.map.objects;
84
+ const objectData = this.data.hitObjectData;
85
+ const indexes = [];
86
+ for (let i = 0; i < this.map.objects.length; ++i) {
87
+ const current = objects[i];
88
+ const currentData = objectData[i];
89
+ const index = this.getCursorIndex(current, currentData);
90
+ indexes.push(index);
91
+ this.indexedHitObjects.push(new IndexedHitObject_1.IndexedHitObject(current, index));
92
+ }
93
+ console.log(indexes.filter((v) => v !== -1).length, "cursors found,", indexes.filter((v) => v === -1).length, "not found");
94
+ const indexCounts = osu_base_1.Utils.initializeArray(this.downMoveCursorInstances.length, 0);
95
+ for (const index of indexes) {
96
+ if (index === -1) {
97
+ continue;
98
+ }
99
+ ++indexCounts[index];
100
+ }
101
+ const mainCursorIndex = indexCounts.indexOf(Math.max(...indexCounts));
102
+ const ignoredCursorIndexes = [];
103
+ for (let i = 0; i < indexCounts.length; ++i) {
104
+ if (indexCounts[i] < this.minCursorIndexCount) {
105
+ ignoredCursorIndexes.push(i);
106
+ }
107
+ }
108
+ this.indexedHitObjects.forEach((indexedHitObject) => {
109
+ if (indexedHitObject.cursorIndex === -1 ||
110
+ ignoredCursorIndexes.includes(indexedHitObject.cursorIndex)) {
111
+ indexedHitObject.cursorIndex = mainCursorIndex;
112
+ }
113
+ });
114
+ for (let i = 0; i < this.downMoveCursorInstances.length; ++i) {
115
+ console.log("Index", i, "count:", indexes.filter((v) => v === i).length);
116
+ }
117
+ }
118
+ /**
119
+ * Gets the cursor index that hits the given object.
120
+ *
121
+ * @param object The object to check.
122
+ * @param data The replay data of the object.
123
+ * @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.
124
+ */
125
+ getCursorIndex(object, data) {
126
+ if (object.object instanceof osu_base_1.Spinner ||
127
+ data.result === __1.hitResult.RESULT_0) {
128
+ return -1;
129
+ }
130
+ const isPrecise = this.data.convertedMods.some((m) => m instanceof osu_base_1.ModPrecise);
131
+ let hitWindowLength;
132
+ switch (data.result) {
133
+ case __1.hitResult.RESULT_300:
134
+ hitWindowLength = this.hitWindow.hitWindowFor300(isPrecise);
135
+ break;
136
+ case __1.hitResult.RESULT_100:
137
+ hitWindowLength = this.hitWindow.hitWindowFor100(isPrecise);
138
+ break;
139
+ default:
140
+ hitWindowLength = this.hitWindow.hitWindowFor50(isPrecise);
141
+ }
142
+ const hitTime = object.object.startTime;
143
+ const maximumHitTime = hitTime + hitWindowLength;
144
+ const minimumHitTime = hitTime - hitWindowLength;
145
+ const cursorInformations = [];
146
+ for (let i = 0; i < this.downMoveCursorInstances.length; ++i) {
147
+ const c = this.downMoveCursorInstances[i];
148
+ let minDistance = Number.POSITIVE_INFINITY;
149
+ let minHitTime = 0;
150
+ for (let j = 0; j < c.size; ++j) {
151
+ if (c.time[j] < minimumHitTime) {
152
+ continue;
153
+ }
154
+ // For some reason, some cursor instances repeat itself,
155
+ // so just skip it to save time.
156
+ if (c.time[j + 1] === c.time[j]) {
157
+ continue;
158
+ }
159
+ if (c.time[j - 1] > maximumHitTime) {
160
+ break;
161
+ }
162
+ let hitPosition = new osu_base_1.Vector2(c.x[j], c.y[j]);
163
+ let distanceToObject = object.object.stackedPosition.getDistance(hitPosition);
164
+ if (minDistance > distanceToObject) {
165
+ minDistance = distanceToObject;
166
+ minHitTime = c.time[j];
167
+ }
168
+ minDistance = Math.min(minDistance, object.object.stackedPosition.getDistance(hitPosition));
169
+ if (c.id[j + 1] === __1.movementType.MOVE ||
170
+ c.id[j] === __1.movementType.MOVE) {
171
+ // Interpolate cursor position between two occurrences
172
+ const initialPosition = new osu_base_1.Vector2(c.x[j], c.y[j]);
173
+ const nextPosition = new osu_base_1.Vector2(c.x[j + 1], c.y[j + 1]);
174
+ const displacement = nextPosition.subtract(initialPosition);
175
+ for (let mSecPassed = c.time[j]; mSecPassed <= Math.min(c.time[j + 1], maximumHitTime); ++mSecPassed) {
176
+ const progress = (mSecPassed - c.time[j]) /
177
+ (c.time[j + 1] - c.time[j]);
178
+ hitPosition = initialPosition.add(displacement.scale(progress));
179
+ distanceToObject =
180
+ object.object.stackedPosition.getDistance(hitPosition);
181
+ if (minDistance > distanceToObject) {
182
+ minDistance = distanceToObject;
183
+ minHitTime = mSecPassed;
184
+ }
185
+ }
186
+ }
187
+ }
188
+ if (minDistance <= object.object.radius) {
189
+ cursorInformations.push({
190
+ cursorIndex: i,
191
+ hitTimeDiff: Math.abs(minHitTime - hitTime),
192
+ });
193
+ }
194
+ }
195
+ if (cursorInformations.length === 0) {
196
+ return -1;
197
+ }
198
+ // Now we look at which cursor is closest to hit time
199
+ const minHitTimeDiff = Math.min(...cursorInformations.map((v) => {
200
+ return v.hitTimeDiff;
201
+ }));
202
+ return (cursorInformations.find((c) => c.hitTimeDiff === minHitTimeDiff)
203
+ ?.cursorIndex);
204
+ }
205
+ /**
206
+ * Applies penalty to the original star rating instance.
207
+ */
208
+ applyPenalty() {
209
+ const beatmaps = new Array(this.downMoveCursorInstances.length);
210
+ this.indexedHitObjects.forEach((o) => {
211
+ if (!beatmaps[o.cursorIndex]) {
212
+ const map = osu_base_1.Utils.deepCopy(this.map.map);
213
+ beatmaps[o.cursorIndex] = map;
214
+ }
215
+ beatmaps[o.cursorIndex].objects.push(o.object.object);
216
+ });
217
+ this.map.objects.length = 0;
218
+ beatmaps.forEach((beatmap) => {
219
+ if (!beatmap) {
220
+ return;
221
+ }
222
+ const starRating = osu_base_1.Utils.deepCopy(this.map);
223
+ starRating.map = beatmap;
224
+ starRating.generateDifficultyHitObjects(osu_base_1.modes.droid);
225
+ starRating.objects[0].deltaTime =
226
+ starRating.objects[0].object.startTime -
227
+ this.indexedHitObjects[0].object.object.startTime;
228
+ starRating.objects[0].strainTime = Math.max(50, starRating.objects[0].deltaTime);
229
+ this.map.objects.push(...starRating.objects);
230
+ });
231
+ this.map.objects.sort((a, b) => {
232
+ return a.startTime - b.startTime;
233
+ });
234
+ this.map.calculateAll();
235
+ }
236
+ }
237
+ exports.TwoHandChecker = TwoHandChecker;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BeatmapSection = void 0;
4
+ /**
5
+ * Represents a section of a beatmap.
6
+ */
7
+ class BeatmapSection {
8
+ /**
9
+ * @param firstObjectIndex The index of the first `DifficultyHitObject` of this beatmap section.
10
+ * @param lastObjectIndex The index of the last `DifficultyHitObject` of this beatmap section.
11
+ */
12
+ constructor(firstObjectIndex, lastObjectIndex) {
13
+ this.firstObjectIndex = firstObjectIndex;
14
+ this.lastObjectIndex = lastObjectIndex;
15
+ }
16
+ }
17
+ exports.BeatmapSection = BeatmapSection;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ThreeFingerBeatmapSection = void 0;
4
+ const BeatmapSection_1 = require("./BeatmapSection");
5
+ /**
6
+ * A section of a beatmap. This is used to detect dragged sections.
7
+ */
8
+ class ThreeFingerBeatmapSection extends BeatmapSection_1.BeatmapSection {
9
+ constructor(values) {
10
+ super(values.firstObjectIndex, values.lastObjectIndex);
11
+ this.isDragged = values.isDragged;
12
+ this.dragFingerIndex = values.dragFingerIndex;
13
+ }
14
+ }
15
+ exports.ThreeFingerBeatmapSection = ThreeFingerBeatmapSection;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IndexedHitObject = void 0;
4
+ /**
5
+ * Contains information about which cursor index hits a hitobject.
6
+ */
7
+ class IndexedHitObject {
8
+ /**
9
+ * @param object The underlying difficulty hitobject.
10
+ * @param cursorIndex The index of the cursor that hits the hitobject.
11
+ */
12
+ constructor(object, cursorIndex) {
13
+ this.object = object;
14
+ this.cursorIndex = cursorIndex;
15
+ }
16
+ }
17
+ exports.IndexedHitObject = IndexedHitObject;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hitResult = void 0;
4
+ /**
5
+ * The result of a hit in an osu!droid replay.
6
+ */
7
+ var hitResult;
8
+ (function (hitResult) {
9
+ /**
10
+ * Miss (0).
11
+ */
12
+ hitResult[hitResult["RESULT_0"] = 1] = "RESULT_0";
13
+ /**
14
+ * Meh (50).
15
+ */
16
+ hitResult[hitResult["RESULT_50"] = 2] = "RESULT_50";
17
+ /**
18
+ * Great (100).
19
+ */
20
+ hitResult[hitResult["RESULT_100"] = 3] = "RESULT_100";
21
+ /**
22
+ * Good (300).
23
+ */
24
+ hitResult[hitResult["RESULT_300"] = 4] = "RESULT_300";
25
+ })(hitResult = exports.hitResult || (exports.hitResult = {}));
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.movementType = void 0;
4
+ /**
5
+ * Movement type of a cursor in an osu!droid replay.
6
+ */
7
+ var movementType;
8
+ (function (movementType) {
9
+ movementType[movementType["DOWN"] = 0] = "DOWN";
10
+ movementType[movementType["MOVE"] = 1] = "MOVE";
11
+ movementType[movementType["UP"] = 2] = "UP";
12
+ })(movementType = exports.movementType || (exports.movementType = {}));
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CursorData = void 0;
4
+ /**
5
+ * Represents a cursor instance in an osu!droid replay.
6
+ *
7
+ * Stores cursor movement data such as x and y coordinates, movement size, etc.
8
+ *
9
+ * This is used when analyzing replays using replay analyzer.
10
+ */
11
+ class CursorData {
12
+ constructor(values) {
13
+ this.size = values.size;
14
+ this.time = values.time;
15
+ this.x = values.x;
16
+ this.y = values.y;
17
+ this.id = values.id;
18
+ }
19
+ }
20
+ exports.CursorData = CursorData;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReplayData = void 0;
4
+ const osu_base_1 = require("@rian8337/osu-base");
5
+ /**
6
+ * Represents a replay data in an osu!droid replay.
7
+ *
8
+ * Stores generic information about an osu!droid replay such as player name, MD5 hash, time set, etc.
9
+ *
10
+ * This is used when analyzing replays using replay analyzer.
11
+ */
12
+ class ReplayData {
13
+ constructor(values) {
14
+ this.replayVersion = values.replayVersion;
15
+ this.folderName = values.folderName;
16
+ this.fileName = values.fileName;
17
+ this.hash = values.hash;
18
+ this.time = new Date(values.time || 0);
19
+ this.hit300k = values.hit300k || 0;
20
+ this.hit100k = values.hit100k || 0;
21
+ this.score = values.score || 0;
22
+ this.maxCombo = values.maxCombo || 0;
23
+ this.accuracy = values.accuracy || new osu_base_1.Accuracy({});
24
+ this.isFullCombo = values.isFullCombo || false;
25
+ this.playerName = values.playerName || "";
26
+ this.rawMods = values.rawMods || "";
27
+ this.rank = values.rank || "";
28
+ this.convertedMods = values.convertedMods || [];
29
+ this.cursorMovement = values.cursorMovement;
30
+ this.hitObjectData = values.hitObjectData;
31
+ this.speedModification = values.speedModification || 1;
32
+ this.forcedAR = values.forcedAR;
33
+ }
34
+ }
35
+ exports.ReplayData = ReplayData;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReplayObjectData = void 0;
4
+ /**
5
+ * Represents a hitobject in an osu!droid replay.
6
+ *
7
+ * Stores information about hitobjects in an osu!droid replay such as hit offset, tickset, and hit result.
8
+ *
9
+ * This is used when analyzing replays using replay analyzer.
10
+ */
11
+ class ReplayObjectData {
12
+ constructor(values) {
13
+ this.accuracy = values.accuracy;
14
+ this.tickset = values.tickset;
15
+ this.result = values.result;
16
+ }
17
+ }
18
+ exports.ReplayObjectData = ReplayObjectData;
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
10
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ __exportStar(require("./data/CursorData"), exports);
14
+ __exportStar(require("./constants/hitResult"), exports);
15
+ __exportStar(require("./constants/movementType"), exports);
16
+ __exportStar(require("./ReplayAnalyzer"), exports);
17
+ __exportStar(require("./data/ReplayData"), exports);
18
+ __exportStar(require("./data/ReplayObjectData"), exports);
19
+ __exportStar(require("./analysis/ThreeFingerChecker"), exports);
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@rian8337/osu-droid-replay-analyzer",
3
+ "version": "1.0.0",
4
+ "description": "A replay analyzer for analyzing osu!droid replay files.",
5
+ "keywords": [
6
+ "osu",
7
+ "osu-droid",
8
+ "osu-replay-analyzer"
9
+ ],
10
+ "author": "Rian8337 <52914632+Rian8337@users.noreply.github.com>",
11
+ "homepage": "https://github.com/Rian8337/osu-droid-module#readme",
12
+ "license": "MIT",
13
+ "main": "dist/index.js",
14
+ "types": "typings/index.d.ts",
15
+ "files": [
16
+ "dist/**",
17
+ "typings/**"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/Rian8337/osu-droid-module.git"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc",
25
+ "prepare": "dts-gen -m java-deserialization -o -f ./node_modules/java-deserialization/src/index.d.ts && npm run build",
26
+ "test": "echo \"No tests for this module\""
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/Rian8337/osu-droid-module/issues"
30
+ },
31
+ "dependencies": {
32
+ "@rian8337/osu-base": "^1.0.0",
33
+ "@rian8337/osu-difficulty-calculator": "^1.0.0",
34
+ "@rian8337/osu-rebalance-difficulty-calculator": "^1.0.0",
35
+ "java-deserialization": "^0.1.0",
36
+ "unzipper": "^0.10.11"
37
+ },
38
+ "devDependencies": {
39
+ "@types/unzipper": "^0.10.5",
40
+ "dts-gen": "^0.6.0",
41
+ "typescript": "^4.5.5"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "gitHead": "e4fdb9c1ed6f90e70651c1aedfdb964b61cb240d"
47
+ }