@rian8337/osu-base 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.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/beatmap/Beatmap.js +223 -0
- package/dist/beatmap/Parser.js +620 -0
- package/dist/beatmap/hitobjects/Circle.js +20 -0
- package/dist/beatmap/hitobjects/HitObject.js +74 -0
- package/dist/beatmap/hitobjects/Slider.js +143 -0
- package/dist/beatmap/hitobjects/Spinner.js +26 -0
- package/dist/beatmap/hitobjects/sliderObjects/HeadCircle.js +10 -0
- package/dist/beatmap/hitobjects/sliderObjects/RepeatPoint.js +21 -0
- package/dist/beatmap/hitobjects/sliderObjects/SliderTick.js +21 -0
- package/dist/beatmap/hitobjects/sliderObjects/TailCircle.js +10 -0
- package/dist/beatmap/timings/BreakPoint.js +34 -0
- package/dist/beatmap/timings/DifficultyControlPoint.js +22 -0
- package/dist/beatmap/timings/TimingControlPoint.js +22 -0
- package/dist/beatmap/timings/TimingPoint.js +12 -0
- package/dist/constants/ParserConstants.js +19 -0
- package/dist/constants/PathType.js +13 -0
- package/dist/constants/modes.js +11 -0
- package/dist/constants/objectTypes.js +12 -0
- package/dist/constants/rankedStatus.js +16 -0
- package/dist/index.js +64 -0
- package/dist/mathutil/Interpolation.js +9 -0
- package/dist/mathutil/MathUtils.js +41 -0
- package/dist/mathutil/Vector2.js +79 -0
- package/dist/mods/Mod.js +9 -0
- package/dist/mods/ModAuto.js +21 -0
- package/dist/mods/ModAutopilot.js +21 -0
- package/dist/mods/ModDoubleTime.js +21 -0
- package/dist/mods/ModEasy.js +21 -0
- package/dist/mods/ModFlashlight.js +21 -0
- package/dist/mods/ModHalfTime.js +21 -0
- package/dist/mods/ModHardRock.js +21 -0
- package/dist/mods/ModHidden.js +21 -0
- package/dist/mods/ModNightCore.js +21 -0
- package/dist/mods/ModNoFail.js +21 -0
- package/dist/mods/ModPerfect.js +21 -0
- package/dist/mods/ModPrecise.js +21 -0
- package/dist/mods/ModReallyEasy.js +21 -0
- package/dist/mods/ModRelax.js +21 -0
- package/dist/mods/ModScoreV2.js +21 -0
- package/dist/mods/ModSmallCircle.js +21 -0
- package/dist/mods/ModSpunOut.js +21 -0
- package/dist/mods/ModSuddenDeath.js +21 -0
- package/dist/mods/ModTouchDevice.js +21 -0
- package/dist/tools/MapInfo.js +559 -0
- package/dist/utils/APIRequestBuilder.js +144 -0
- package/dist/utils/Accuracy.js +96 -0
- package/dist/utils/HitWindow.js +56 -0
- package/dist/utils/MapStats.js +212 -0
- package/dist/utils/ModUtil.js +137 -0
- package/dist/utils/PathApproximator.js +269 -0
- package/dist/utils/Precision.js +31 -0
- package/dist/utils/SliderPath.js +187 -0
- package/dist/utils/Utils.js +53 -0
- package/package.json +43 -0
- package/typings/index.d.ts +1951 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Parser = void 0;
|
|
4
|
+
const Beatmap_1 = require("./Beatmap");
|
|
5
|
+
const TimingControlPoint_1 = require("./timings/TimingControlPoint");
|
|
6
|
+
const DifficultyControlPoint_1 = require("./timings/DifficultyControlPoint");
|
|
7
|
+
const BreakPoint_1 = require("./timings/BreakPoint");
|
|
8
|
+
const Circle_1 = require("./hitobjects/Circle");
|
|
9
|
+
const Slider_1 = require("./hitobjects/Slider");
|
|
10
|
+
const Spinner_1 = require("./hitobjects/Spinner");
|
|
11
|
+
const PathType_1 = require("../constants/PathType");
|
|
12
|
+
const Precision_1 = require("../utils/Precision");
|
|
13
|
+
const objectTypes_1 = require("../constants/objectTypes");
|
|
14
|
+
const Vector2_1 = require("../mathutil/Vector2");
|
|
15
|
+
const SliderPath_1 = require("../utils/SliderPath");
|
|
16
|
+
const MapStats_1 = require("../utils/MapStats");
|
|
17
|
+
const MathUtils_1 = require("../mathutil/MathUtils");
|
|
18
|
+
const ParserConstants_1 = require("../constants/ParserConstants");
|
|
19
|
+
/**
|
|
20
|
+
* A beatmap parser with just enough data for pp calculation.
|
|
21
|
+
*/
|
|
22
|
+
class Parser {
|
|
23
|
+
constructor() {
|
|
24
|
+
/**
|
|
25
|
+
* The parsed beatmap.
|
|
26
|
+
*/
|
|
27
|
+
this.map = new Beatmap_1.Beatmap();
|
|
28
|
+
/**
|
|
29
|
+
* The amount of lines of `.osu` file.
|
|
30
|
+
*/
|
|
31
|
+
this.line = 0;
|
|
32
|
+
/**
|
|
33
|
+
* The currently processed line.
|
|
34
|
+
*/
|
|
35
|
+
this.currentLine = "";
|
|
36
|
+
/**
|
|
37
|
+
* The previously processed line.
|
|
38
|
+
*/
|
|
39
|
+
this.lastPosition = "";
|
|
40
|
+
/**
|
|
41
|
+
* The currently processed section.
|
|
42
|
+
*/
|
|
43
|
+
this.section = "";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parses a beatmap.
|
|
47
|
+
*
|
|
48
|
+
* This will process a `.osu` file and returns the current instance of the parser for easy chaining.
|
|
49
|
+
*
|
|
50
|
+
* @param str The `.osu` file to parse.
|
|
51
|
+
* @param mods The mods to parse the beatmap for.
|
|
52
|
+
*/
|
|
53
|
+
parse(str, mods = []) {
|
|
54
|
+
const lines = str.split("\n");
|
|
55
|
+
for (let i = 0; i < lines.length; ++i) {
|
|
56
|
+
this.processLine(lines[i]);
|
|
57
|
+
}
|
|
58
|
+
// Objects may be out of order *only* if a user has manually edited an .osu file.
|
|
59
|
+
// Unfortunately there are "ranked" maps in this state (example: https://osu.ppy.sh/s/594828).
|
|
60
|
+
// Sort is used to guarantee that the parsing order of hitobjects with equal start times is maintained (stably-sorted).
|
|
61
|
+
this.map.objects.sort((a, b) => {
|
|
62
|
+
return a.startTime - b.startTime;
|
|
63
|
+
});
|
|
64
|
+
if (this.map.formatVersion >= 6) {
|
|
65
|
+
this.applyStacking(0, this.map.objects.length - 1);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.applyStackingOld();
|
|
69
|
+
}
|
|
70
|
+
const circleSize = new MapStats_1.MapStats({
|
|
71
|
+
cs: this.map.cs,
|
|
72
|
+
mods,
|
|
73
|
+
}).calculate().cs;
|
|
74
|
+
const scale = (1 - (0.7 * (circleSize - 5)) / 5) / 2;
|
|
75
|
+
this.map.objects.forEach((h) => {
|
|
76
|
+
h.scale = scale;
|
|
77
|
+
if (h instanceof Slider_1.Slider) {
|
|
78
|
+
h.nestedHitObjects.forEach((n) => {
|
|
79
|
+
n.scale = scale;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Logs the line at which an exception occurs.
|
|
87
|
+
*/
|
|
88
|
+
logError() {
|
|
89
|
+
return ("at line " +
|
|
90
|
+
this.line +
|
|
91
|
+
"\n" +
|
|
92
|
+
this.currentLine +
|
|
93
|
+
"\n" +
|
|
94
|
+
"-> " +
|
|
95
|
+
this.lastPosition +
|
|
96
|
+
" <-");
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Processes a line of the file.
|
|
100
|
+
*/
|
|
101
|
+
processLine(line) {
|
|
102
|
+
this.currentLine = this.lastPosition = line;
|
|
103
|
+
++this.line;
|
|
104
|
+
// comments
|
|
105
|
+
if (line.startsWith(" ") || line.startsWith("_")) {
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
// now that we've handled space comments we can trim space
|
|
109
|
+
line = this.currentLine = line.trim();
|
|
110
|
+
// c++ style comments
|
|
111
|
+
if (line.startsWith("//")) {
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
// [SectionName]
|
|
115
|
+
if (line.startsWith("[")) {
|
|
116
|
+
if (this.section === "Difficulty" && !this.map.ar) {
|
|
117
|
+
this.map.ar = this.map.od;
|
|
118
|
+
}
|
|
119
|
+
this.section = line.substring(1, line.length - 1);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
if (!line) {
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
switch (this.section) {
|
|
126
|
+
case "General":
|
|
127
|
+
this.general();
|
|
128
|
+
break;
|
|
129
|
+
case "Metadata":
|
|
130
|
+
this.metadata();
|
|
131
|
+
break;
|
|
132
|
+
case "Difficulty":
|
|
133
|
+
this.difficulty();
|
|
134
|
+
break;
|
|
135
|
+
case "Events":
|
|
136
|
+
this.events();
|
|
137
|
+
break;
|
|
138
|
+
case "TimingPoints":
|
|
139
|
+
this.timingPoints();
|
|
140
|
+
break;
|
|
141
|
+
case "HitObjects":
|
|
142
|
+
// Need to check if the beatmap doesn't have an uninherited timing point.
|
|
143
|
+
// This exists in cases such as /b/2290233 where the beatmap has been
|
|
144
|
+
// edited by the user.
|
|
145
|
+
//
|
|
146
|
+
// In lazer, the default BPM is set to 60 (60000 / 1000).
|
|
147
|
+
if (this.map.timingPoints.length === 0) {
|
|
148
|
+
this.map.timingPoints.push(new TimingControlPoint_1.TimingControlPoint({
|
|
149
|
+
time: Number.NEGATIVE_INFINITY,
|
|
150
|
+
msPerBeat: 1000,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
this.objects();
|
|
154
|
+
break;
|
|
155
|
+
default: {
|
|
156
|
+
const fmtpos = line.indexOf("file format v");
|
|
157
|
+
if (fmtpos < 0) {
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
this.map.formatVersion = parseInt(line.substring(fmtpos + 13));
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Sets the last position of the current parser state.
|
|
168
|
+
*
|
|
169
|
+
* This is useful to debug syntax errors.
|
|
170
|
+
*/
|
|
171
|
+
setPosition(str) {
|
|
172
|
+
this.lastPosition = str.trim();
|
|
173
|
+
return this.lastPosition;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Logs any syntax errors into the console.
|
|
177
|
+
*/
|
|
178
|
+
warn(message) {
|
|
179
|
+
console.warn(message);
|
|
180
|
+
console.warn(this.logError());
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Processes a property of the beatmap. This takes the current line as parameter.
|
|
184
|
+
*
|
|
185
|
+
* For example, `ApproachRate:9` will be split into `[ApproachRate, 9]`.
|
|
186
|
+
*/
|
|
187
|
+
property() {
|
|
188
|
+
const s = this.currentLine.split(":");
|
|
189
|
+
s[0] = this.setPosition(s[0]);
|
|
190
|
+
s[1] = this.setPosition(s.slice(1).join(":"));
|
|
191
|
+
return s;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Processes the general section of a beatmap.
|
|
195
|
+
*/
|
|
196
|
+
general() {
|
|
197
|
+
const p = this.property();
|
|
198
|
+
if (p[0] === "StackLeniency") {
|
|
199
|
+
this.map.stackLeniency = parseFloat(p[1]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Processes the metadata section of a beatmap.
|
|
204
|
+
*/
|
|
205
|
+
metadata() {
|
|
206
|
+
const p = this.property();
|
|
207
|
+
switch (p[0]) {
|
|
208
|
+
case "Title":
|
|
209
|
+
this.map.title = p[1];
|
|
210
|
+
break;
|
|
211
|
+
case "TitleUnicode":
|
|
212
|
+
this.map.titleUnicode = p[1];
|
|
213
|
+
break;
|
|
214
|
+
case "Artist":
|
|
215
|
+
this.map.artist = p[1];
|
|
216
|
+
break;
|
|
217
|
+
case "ArtistUnicode":
|
|
218
|
+
this.map.artistUnicode = p[1];
|
|
219
|
+
break;
|
|
220
|
+
case "Creator":
|
|
221
|
+
this.map.creator = p[1];
|
|
222
|
+
break;
|
|
223
|
+
case "Version":
|
|
224
|
+
this.map.version = p[1];
|
|
225
|
+
break;
|
|
226
|
+
case "BeatmapID":
|
|
227
|
+
this.map.beatmapId = parseInt(p[1]);
|
|
228
|
+
break;
|
|
229
|
+
case "BeatmapSetID":
|
|
230
|
+
this.map.beatmapSetId = parseInt(p[1]);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Processes the events section of a beatmap.
|
|
236
|
+
*/
|
|
237
|
+
events() {
|
|
238
|
+
const s = this.currentLine.split(",");
|
|
239
|
+
if (s[0] !== "2" && s[0] !== "Break")
|
|
240
|
+
return;
|
|
241
|
+
this.map.breakPoints.push(new BreakPoint_1.BreakPoint({
|
|
242
|
+
startTime: parseInt(this.setPosition(s[1])),
|
|
243
|
+
endTime: parseInt(this.setPosition(s[2])),
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Processes the difficulty section of a beatmap.
|
|
248
|
+
*/
|
|
249
|
+
difficulty() {
|
|
250
|
+
const p = this.property();
|
|
251
|
+
switch (p[0]) {
|
|
252
|
+
case "CircleSize":
|
|
253
|
+
this.map.cs = parseFloat(this.setPosition(p[1]));
|
|
254
|
+
break;
|
|
255
|
+
case "OverallDifficulty":
|
|
256
|
+
this.map.od = parseFloat(this.setPosition(p[1]));
|
|
257
|
+
break;
|
|
258
|
+
case "ApproachRate":
|
|
259
|
+
this.map.ar = parseFloat(this.setPosition(p[1]));
|
|
260
|
+
break;
|
|
261
|
+
case "HPDrainRate":
|
|
262
|
+
this.map.hp = parseFloat(this.setPosition(p[1]));
|
|
263
|
+
break;
|
|
264
|
+
case "SliderMultiplier":
|
|
265
|
+
this.map.sv = parseFloat(this.setPosition(p[1]));
|
|
266
|
+
break;
|
|
267
|
+
case "SliderTickRate":
|
|
268
|
+
this.map.tickRate = parseFloat(this.setPosition(p[1]));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Processes the timing points section of a beatmap.
|
|
273
|
+
*/
|
|
274
|
+
timingPoints() {
|
|
275
|
+
const s = this.currentLine.split(",");
|
|
276
|
+
if (s.length > 8) {
|
|
277
|
+
this.warn("Timing point with trailing values");
|
|
278
|
+
}
|
|
279
|
+
else if (s.length < 2) {
|
|
280
|
+
return this.warn("Ignoring malformed timing point");
|
|
281
|
+
}
|
|
282
|
+
const time = this.map.getOffsetTime(parseFloat(this.setPosition(s[0])));
|
|
283
|
+
if (!this.isNumberValid(time)) {
|
|
284
|
+
return this.warn("Ignoring malformed timing point: Value is invalid, too low, or too high");
|
|
285
|
+
}
|
|
286
|
+
const msPerBeat = parseFloat(this.setPosition(s[1]));
|
|
287
|
+
if (!this.isNumberValid(msPerBeat)) {
|
|
288
|
+
return this.warn("Ignoring malformed timing point: Value is invalid, too low, or too high");
|
|
289
|
+
}
|
|
290
|
+
const speedMultiplier = msPerBeat < 0 ? 100 / -msPerBeat : 1;
|
|
291
|
+
if (msPerBeat >= 0) {
|
|
292
|
+
this.map.timingPoints.push(new TimingControlPoint_1.TimingControlPoint({
|
|
293
|
+
time: time,
|
|
294
|
+
msPerBeat: msPerBeat,
|
|
295
|
+
}));
|
|
296
|
+
}
|
|
297
|
+
// Remove the last difficulty control point if another difficulty control point overrides it at the same time.
|
|
298
|
+
if (this.map.difficultyTimingPoints.at(-1)?.time === time) {
|
|
299
|
+
this.map.difficultyTimingPoints.pop();
|
|
300
|
+
}
|
|
301
|
+
this.map.difficultyTimingPoints.push(new DifficultyControlPoint_1.DifficultyControlPoint({
|
|
302
|
+
time: time,
|
|
303
|
+
speedMultiplier: speedMultiplier,
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Processes the objects section of a beatmap.
|
|
308
|
+
*/
|
|
309
|
+
objects() {
|
|
310
|
+
const s = this.currentLine.split(",");
|
|
311
|
+
if (s.length > 11) {
|
|
312
|
+
this.warn("Object with trailing values");
|
|
313
|
+
}
|
|
314
|
+
else if (s.length < 4) {
|
|
315
|
+
return this.warn("Ignoring malformed hitobject");
|
|
316
|
+
}
|
|
317
|
+
const time = this.map.getOffsetTime(parseFloat(this.setPosition(s[2])));
|
|
318
|
+
const type = parseInt(this.setPosition(s[3]));
|
|
319
|
+
if (!this.isNumberValid(time) || isNaN(type)) {
|
|
320
|
+
return this.warn("Ignoring malformed hitobject: Value is invalid, too low, or too high");
|
|
321
|
+
}
|
|
322
|
+
const position = new Vector2_1.Vector2(parseFloat(this.setPosition(s[0])), parseFloat(this.setPosition(s[1])));
|
|
323
|
+
if (!this.isVectorValid(position)) {
|
|
324
|
+
return this.warn("Ignoring malformed hitobject: Value is invalid, too low, or too high");
|
|
325
|
+
}
|
|
326
|
+
if (type & objectTypes_1.objectTypes.circle) {
|
|
327
|
+
const object = new Circle_1.Circle({
|
|
328
|
+
startTime: time,
|
|
329
|
+
type: type,
|
|
330
|
+
position: position,
|
|
331
|
+
});
|
|
332
|
+
++this.map.circles;
|
|
333
|
+
this.map.objects.push(object);
|
|
334
|
+
}
|
|
335
|
+
else if (type & objectTypes_1.objectTypes.slider) {
|
|
336
|
+
if (s.length < 8) {
|
|
337
|
+
return this.warn("Ignoring malformed slider");
|
|
338
|
+
}
|
|
339
|
+
const repetitions = Math.max(parseInt(this.setPosition(s[6])), ParserConstants_1.ParserConstants.MIN_REPETITIONS_VALUE);
|
|
340
|
+
if (!this.isNumberValid(repetitions, 0, ParserConstants_1.ParserConstants.MAX_REPETITIONS_VALUE)) {
|
|
341
|
+
return this.warn("Ignoring malformed slider: Value is invalid, too low, or too high");
|
|
342
|
+
}
|
|
343
|
+
const distance = Math.max(0, parseFloat(this.setPosition(s[7])));
|
|
344
|
+
if (!this.isNumberValid(distance, 0, ParserConstants_1.ParserConstants.MAX_COORDINATE_VALUE)) {
|
|
345
|
+
return this.warn("Ignoring malformed slider: Value is invalid, too low, or too high");
|
|
346
|
+
}
|
|
347
|
+
const speedMultiplierTimingPoint = this.map.difficultyControlPointAt(time);
|
|
348
|
+
const msPerBeatTimingPoint = this.map.timingControlPointAt(time);
|
|
349
|
+
const points = [new Vector2_1.Vector2(0, 0)];
|
|
350
|
+
const pointSplit = this.setPosition(s[5]).split("|");
|
|
351
|
+
let pathType = this.convertPathType(pointSplit.shift());
|
|
352
|
+
for (const point of pointSplit) {
|
|
353
|
+
const temp = point.split(":");
|
|
354
|
+
const vec = new Vector2_1.Vector2(+temp[0], +temp[1]);
|
|
355
|
+
if (!this.isVectorValid(vec)) {
|
|
356
|
+
return this.warn("Ignoring malformed slider: Value is invalid, too low, or too high");
|
|
357
|
+
}
|
|
358
|
+
points.push(vec.subtract(position));
|
|
359
|
+
}
|
|
360
|
+
// A special case for old beatmaps where the first
|
|
361
|
+
// control point is in the position of the slider.
|
|
362
|
+
if (points[0].equals(points[1])) {
|
|
363
|
+
points.shift();
|
|
364
|
+
}
|
|
365
|
+
// Edge-case rules (to match stable).
|
|
366
|
+
if (pathType === PathType_1.PathType.PerfectCurve) {
|
|
367
|
+
if (points.length !== 3) {
|
|
368
|
+
pathType = PathType_1.PathType.Bezier;
|
|
369
|
+
}
|
|
370
|
+
else if (Precision_1.Precision.almostEqualsNumber(0, (points[1].y - points[0].y) *
|
|
371
|
+
(points[2].x - points[0].x) -
|
|
372
|
+
(points[1].x - points[0].x) *
|
|
373
|
+
(points[2].y - points[0].y))) {
|
|
374
|
+
// osu-stable special-cased colinear perfect curves to a linear path
|
|
375
|
+
pathType = PathType_1.PathType.Linear;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const path = new SliderPath_1.SliderPath({
|
|
379
|
+
pathType: pathType,
|
|
380
|
+
controlPoints: points,
|
|
381
|
+
expectedDistance: distance,
|
|
382
|
+
});
|
|
383
|
+
const object = new Slider_1.Slider({
|
|
384
|
+
position: position,
|
|
385
|
+
startTime: time,
|
|
386
|
+
type: type,
|
|
387
|
+
repetitions: repetitions,
|
|
388
|
+
path: path,
|
|
389
|
+
speedMultiplier: MathUtils_1.MathUtils.clamp(speedMultiplierTimingPoint.speedMultiplier, ParserConstants_1.ParserConstants.MIN_SPEEDMULTIPLIER_VALUE, ParserConstants_1.ParserConstants.MAX_SPEEDMULTIPLIER_VALUE),
|
|
390
|
+
msPerBeat: msPerBeatTimingPoint.msPerBeat,
|
|
391
|
+
mapSliderVelocity: this.map.sv,
|
|
392
|
+
mapTickRate: this.map.tickRate,
|
|
393
|
+
// Prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
|
394
|
+
// This results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
|
395
|
+
//
|
|
396
|
+
// This additional check is used in case BPM goes very low or very high.
|
|
397
|
+
// When lazer is final, this should be revisited.
|
|
398
|
+
tickDistanceMultiplier: this.isNumberValid(msPerBeatTimingPoint.msPerBeat, ParserConstants_1.ParserConstants.MIN_MSPERBEAT_VALUE, ParserConstants_1.ParserConstants.MAX_MSPERBEAT_VALUE)
|
|
399
|
+
? this.map.formatVersion < 8
|
|
400
|
+
? 1 / speedMultiplierTimingPoint.speedMultiplier
|
|
401
|
+
: 1
|
|
402
|
+
: 0,
|
|
403
|
+
});
|
|
404
|
+
++this.map.sliders;
|
|
405
|
+
this.map.objects.push(object);
|
|
406
|
+
}
|
|
407
|
+
else if (type & objectTypes_1.objectTypes.spinner) {
|
|
408
|
+
const object = new Spinner_1.Spinner({
|
|
409
|
+
startTime: time,
|
|
410
|
+
type: type,
|
|
411
|
+
duration: parseInt(this.setPosition(s[5])) - time,
|
|
412
|
+
});
|
|
413
|
+
if (!this.isNumberValid(object.duration)) {
|
|
414
|
+
return this.warn("Ignoring malformed spinner: Value is invalid, too low, or too high");
|
|
415
|
+
}
|
|
416
|
+
++this.map.spinners;
|
|
417
|
+
this.map.objects.push(object);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Converts string slider path to a `PathType`.
|
|
422
|
+
*/
|
|
423
|
+
convertPathType(input) {
|
|
424
|
+
switch (input) {
|
|
425
|
+
case "B":
|
|
426
|
+
return PathType_1.PathType.Bezier;
|
|
427
|
+
case "L":
|
|
428
|
+
return PathType_1.PathType.Linear;
|
|
429
|
+
case "P":
|
|
430
|
+
return PathType_1.PathType.PerfectCurve;
|
|
431
|
+
default:
|
|
432
|
+
return PathType_1.PathType.Catmull;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Applies stacking to hitobjects for beatmap version 6 or above.
|
|
437
|
+
*/
|
|
438
|
+
applyStacking(startIndex, endIndex) {
|
|
439
|
+
const stackDistance = 3;
|
|
440
|
+
let timePreempt = 600;
|
|
441
|
+
const ar = this.map.ar;
|
|
442
|
+
if (ar > 5) {
|
|
443
|
+
timePreempt = 1200 + ((450 - 1200) * (ar - 5)) / 5;
|
|
444
|
+
}
|
|
445
|
+
else if (ar < 5) {
|
|
446
|
+
timePreempt = 1200 - ((1200 - 1800) * (5 - ar)) / 5;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
timePreempt = 1200;
|
|
450
|
+
}
|
|
451
|
+
let extendedEndIndex = endIndex;
|
|
452
|
+
const stackThreshold = timePreempt * this.map.stackLeniency;
|
|
453
|
+
if (endIndex < this.map.objects.length - 1) {
|
|
454
|
+
for (let i = endIndex; i >= startIndex; --i) {
|
|
455
|
+
let stackBaseIndex = i;
|
|
456
|
+
for (let n = stackBaseIndex + 1; n < this.map.objects.length; ++n) {
|
|
457
|
+
const stackBaseObject = this.map.objects[stackBaseIndex];
|
|
458
|
+
if (stackBaseObject instanceof Spinner_1.Spinner) {
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
const objectN = this.map.objects[n];
|
|
462
|
+
if (objectN instanceof Spinner_1.Spinner) {
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
const endTime = stackBaseObject.endTime;
|
|
466
|
+
if (objectN.startTime - endTime > stackThreshold) {
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
const endPositionDistanceCheck = stackBaseObject instanceof Slider_1.Slider
|
|
470
|
+
? stackBaseObject.endPosition.getDistance(objectN.position) < stackDistance
|
|
471
|
+
: false;
|
|
472
|
+
if (stackBaseObject.position.getDistance(objectN.position) <
|
|
473
|
+
stackDistance ||
|
|
474
|
+
endPositionDistanceCheck) {
|
|
475
|
+
stackBaseIndex = n;
|
|
476
|
+
objectN.stackHeight = 0;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (stackBaseIndex > extendedEndIndex) {
|
|
480
|
+
extendedEndIndex = stackBaseIndex;
|
|
481
|
+
if (extendedEndIndex === this.map.objects.length - 1) {
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
let extendedStartIndex = startIndex;
|
|
488
|
+
for (let i = extendedEndIndex; i > startIndex; --i) {
|
|
489
|
+
let n = i;
|
|
490
|
+
let objectI = this.map.objects[i];
|
|
491
|
+
if (objectI.stackHeight !== 0 ||
|
|
492
|
+
objectI.type & objectTypes_1.objectTypes.spinner) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (objectI.type & objectTypes_1.objectTypes.circle) {
|
|
496
|
+
while (--n >= 0) {
|
|
497
|
+
const objectN = this.map.objects[n];
|
|
498
|
+
if (objectN instanceof Spinner_1.Spinner) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const endTime = objectN.endTime;
|
|
502
|
+
if (objectI.startTime - endTime > stackThreshold) {
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
if (n < extendedStartIndex) {
|
|
506
|
+
objectN.stackHeight = 0;
|
|
507
|
+
extendedStartIndex = n;
|
|
508
|
+
}
|
|
509
|
+
const endPositionDistanceCheck = objectN instanceof Slider_1.Slider
|
|
510
|
+
? objectN.endPosition.getDistance(objectI.position) < stackDistance
|
|
511
|
+
: false;
|
|
512
|
+
if (endPositionDistanceCheck) {
|
|
513
|
+
const offset = objectI.stackHeight - objectN.stackHeight + 1;
|
|
514
|
+
for (let j = n + 1; j <= i; ++j) {
|
|
515
|
+
const objectJ = this.map.objects[j];
|
|
516
|
+
if (objectN.endPosition.getDistance(objectJ.position) < stackDistance) {
|
|
517
|
+
objectJ.stackHeight -= offset;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
if (objectN.position.getDistance(objectI.position) <
|
|
523
|
+
stackDistance) {
|
|
524
|
+
objectN.stackHeight = objectI.stackHeight + 1;
|
|
525
|
+
objectI = objectN;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
else if (objectI instanceof Slider_1.Slider) {
|
|
530
|
+
while (--n >= startIndex) {
|
|
531
|
+
const objectN = this.map.objects[n];
|
|
532
|
+
if (objectN instanceof Spinner_1.Spinner) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (objectI.startTime - objectN.startTime >
|
|
536
|
+
stackThreshold) {
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
const objectNEndPosition = objectN instanceof Circle_1.Circle
|
|
540
|
+
? objectN.position
|
|
541
|
+
: objectN.endPosition;
|
|
542
|
+
if (objectNEndPosition.getDistance(objectI.position) <
|
|
543
|
+
stackDistance) {
|
|
544
|
+
objectN.stackHeight = objectI.stackHeight + 1;
|
|
545
|
+
objectI = objectN;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Applies stacking to hitobjects for beatmap version 5 or below.
|
|
553
|
+
*/
|
|
554
|
+
applyStackingOld() {
|
|
555
|
+
const stackDistance = 3;
|
|
556
|
+
let timePreempt = 600;
|
|
557
|
+
const ar = this.map.ar;
|
|
558
|
+
if (ar > 5) {
|
|
559
|
+
timePreempt = 1200 + ((450 - 1200) * (ar - 5)) / 5;
|
|
560
|
+
}
|
|
561
|
+
else if (ar < 5) {
|
|
562
|
+
timePreempt = 1200 - ((1200 - 1800) * (5 - ar)) / 5;
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
timePreempt = 1200;
|
|
566
|
+
}
|
|
567
|
+
for (let i = 0; i < this.map.objects.length; ++i) {
|
|
568
|
+
const currentObject = this.map.objects[i];
|
|
569
|
+
if (currentObject.stackHeight !== 0 &&
|
|
570
|
+
!(currentObject instanceof Slider_1.Slider)) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
let startTime = currentObject.endTime;
|
|
574
|
+
let sliderStack = 0;
|
|
575
|
+
for (let j = i + 1; j < this.map.objects.length; ++j) {
|
|
576
|
+
const stackThreshold = timePreempt * this.map.stackLeniency;
|
|
577
|
+
if (this.map.objects[j].startTime - stackThreshold >
|
|
578
|
+
startTime) {
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
// The start position of the hitobject, or the position at the end of the path if the hitobject is a slider
|
|
582
|
+
const position2 = currentObject instanceof Slider_1.Slider
|
|
583
|
+
? currentObject.endPosition
|
|
584
|
+
: currentObject.position;
|
|
585
|
+
if (this.map.objects[j].position.getDistance(currentObject.position) < stackDistance) {
|
|
586
|
+
++currentObject.stackHeight;
|
|
587
|
+
startTime = this.map.objects[j].endTime;
|
|
588
|
+
}
|
|
589
|
+
else if (this.map.objects[j].position.getDistance(position2) <
|
|
590
|
+
stackDistance) {
|
|
591
|
+
// Case for sliders - bump notes down and right, rather than up and left.
|
|
592
|
+
++sliderStack;
|
|
593
|
+
this.map.objects[j].stackHeight -= sliderStack;
|
|
594
|
+
startTime = this.map.objects[j].endTime;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Checks if a number is within a given threshold.
|
|
601
|
+
*
|
|
602
|
+
* @param num The number to check.
|
|
603
|
+
* @param min The minimum threshold. Defaults to `-ParserConstants.MAX_PARSE_VALUE`.
|
|
604
|
+
* @param max The maximum threshold. Defaults to `ParserConstants.MAX_PARSE_VALUE`.
|
|
605
|
+
*/
|
|
606
|
+
isNumberValid(num, min = -ParserConstants_1.ParserConstants.MAX_PARSE_VALUE, max = ParserConstants_1.ParserConstants.MAX_PARSE_VALUE) {
|
|
607
|
+
return num >= min && num <= max;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Checks if each coordinates of a vector is within a given threshold.
|
|
611
|
+
*
|
|
612
|
+
* @param vec The vector to check.
|
|
613
|
+
* @param limit The threshold. Defaults to `ParserConstants.MAX_COORDINATE_VALUE`.
|
|
614
|
+
*/
|
|
615
|
+
isVectorValid(vec, min = -ParserConstants_1.ParserConstants.MAX_COORDINATE_VALUE, max = ParserConstants_1.ParserConstants.MAX_COORDINATE_VALUE) {
|
|
616
|
+
return (this.isNumberValid(vec.x, min, max) &&
|
|
617
|
+
this.isNumberValid(vec.y, min, max));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
exports.Parser = Parser;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Circle = void 0;
|
|
4
|
+
const HitObject_1 = require("./HitObject");
|
|
5
|
+
/**
|
|
6
|
+
* Represents a circle in a beatmap.
|
|
7
|
+
*
|
|
8
|
+
* All we need from circles is their position. All positions
|
|
9
|
+
* stored in the objects are in playfield coordinates (512*384
|
|
10
|
+
* rectangle).
|
|
11
|
+
*/
|
|
12
|
+
class Circle extends HitObject_1.HitObject {
|
|
13
|
+
constructor(values) {
|
|
14
|
+
super(values);
|
|
15
|
+
}
|
|
16
|
+
toString() {
|
|
17
|
+
return `Position: [${this.position.x}, ${this.position.y}]`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.Circle = Circle;
|