@rian8337/osu-base 2.1.1 → 3.0.0-beta.1

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
@@ -311,7 +311,9 @@ class APIRequestBuilder {
311
311
  })
312
312
  .on("complete", async (response) => {
313
313
  ++this.fetchAttempts;
314
- if (response.statusCode !== 200 && this.fetchAttempts < 5) {
314
+ const { statusCode } = response;
315
+ if ((statusCode === 500 || statusCode === 503) &&
316
+ this.fetchAttempts < 5) {
315
317
  console.error(`Request to ${url} failed; ${this.fetchAttempts} attempts so far; retrying`);
316
318
  await Utils.sleep(0.2);
317
319
  return resolve(this.sendRequest());
@@ -390,7 +392,7 @@ exports.objectTypes = void 0;
390
392
  })(exports.objectTypes || (exports.objectTypes = {}));
391
393
 
392
394
  /**
393
- * Based on `Vector2` class in C#.
395
+ * Represents a two-dimensional vector.
394
396
  */
395
397
  class Vector2 {
396
398
  /**
@@ -1299,12 +1301,20 @@ class DifficultyControlPoint extends ControlPoint {
1299
1301
  * The slider speed multiplier of the control point.
1300
1302
  */
1301
1303
  speedMultiplier;
1304
+ /**
1305
+ * Whether or not slider ticks should be generated at this control point.
1306
+ *
1307
+ * This exists for backwards compatibility with maps that abuse NaN slider velocity behavior on osu!stable (e.g. /b/2628991).
1308
+ */
1309
+ generateTicks;
1302
1310
  constructor(values) {
1303
1311
  super(values);
1304
1312
  this.speedMultiplier = values.speedMultiplier;
1313
+ this.generateTicks = values.generateTicks;
1305
1314
  }
1306
1315
  isRedundant(existing) {
1307
- return this.speedMultiplier === existing.speedMultiplier;
1316
+ return (this.speedMultiplier === existing.speedMultiplier &&
1317
+ this.generateTicks === existing.generateTicks);
1308
1318
  }
1309
1319
  toString() {
1310
1320
  return ("{ time: " +
@@ -1323,6 +1333,7 @@ class DifficultyControlPointManager extends ControlPointManager {
1323
1333
  defaultControlPoint = new DifficultyControlPoint({
1324
1334
  time: 0,
1325
1335
  speedMultiplier: 1,
1336
+ generateTicks: true,
1326
1337
  });
1327
1338
  controlPointAt(time) {
1328
1339
  return this.binarySearchWithFallback(time);
@@ -1717,26 +1728,46 @@ class Beatmap {
1717
1728
  */
1718
1729
  maxDroidScore(stats) {
1719
1730
  let scoreMultiplier = 1;
1720
- if (stats.mods.every((m) => m.droidRanked)) {
1721
- let scoreSpeedMultiplier = 1;
1722
- const speedMultiplier = stats.speedMultiplier;
1723
- if (speedMultiplier > 1) {
1724
- scoreSpeedMultiplier += (speedMultiplier - 1) * 0.24;
1725
- }
1726
- else if (speedMultiplier < 1) {
1727
- scoreSpeedMultiplier = Math.pow(0.3, (1 - speedMultiplier) * 4);
1731
+ for (const mod of stats.mods) {
1732
+ if (mod.isApplicableToDroid()) {
1733
+ scoreMultiplier *= mod.droidScoreMultiplier;
1728
1734
  }
1729
- scoreMultiplier =
1730
- stats.mods.reduce((a, v) => a * v.scoreMultiplier, 1) *
1731
- scoreSpeedMultiplier;
1735
+ }
1736
+ const { speedMultiplier } = stats;
1737
+ if (speedMultiplier >= 1) {
1738
+ scoreMultiplier *= 1 + (speedMultiplier - 1) * 0.24;
1732
1739
  }
1733
1740
  else {
1734
- scoreMultiplier = 0;
1741
+ scoreMultiplier *= Math.pow(0.3, (1 - speedMultiplier) * 4);
1735
1742
  }
1736
- return this.maxScore(1 +
1743
+ const difficultyMultiplier = 1 +
1737
1744
  this.difficulty.od / 10 +
1738
1745
  this.difficulty.hp / 10 +
1739
- (this.difficulty.cs - 3) / 4, scoreMultiplier);
1746
+ (this.difficulty.cs - 3) / 4;
1747
+ let combo = 0;
1748
+ let score = 0;
1749
+ for (const object of this.hitObjects.objects) {
1750
+ if (!(object instanceof Slider)) {
1751
+ score += Math.floor(300 + (300 * combo * difficultyMultiplier) / 25);
1752
+ ++combo;
1753
+ continue;
1754
+ }
1755
+ const ticksPerSpan = object.ticks;
1756
+ const totalTicks = ticksPerSpan * (object.repeats + 1);
1757
+ // Apply slider head.
1758
+ score += 30;
1759
+ ++combo;
1760
+ // Apply slider repeats.
1761
+ score += 30 * object.repeats;
1762
+ combo += object.repeats;
1763
+ // Apply slider ticks.
1764
+ score += 10 * totalTicks;
1765
+ combo += totalTicks;
1766
+ // Apply slider end.
1767
+ score += Math.floor(300 + (300 * combo * difficultyMultiplier) / 25);
1768
+ ++combo;
1769
+ }
1770
+ return Math.floor(score * scoreMultiplier);
1740
1771
  }
1741
1772
  /**
1742
1773
  * Calculates the osu!standard maximum score of the beatmap without taking spinner bonus into account.
@@ -1746,6 +1777,12 @@ class Beatmap {
1746
1777
  maxOsuScore(mods = []) {
1747
1778
  const accumulatedDiffPoints = this.difficulty.cs + this.difficulty.hp + this.difficulty.od;
1748
1779
  let difficultyMultiplier = 2;
1780
+ let scoreMultiplier = 1;
1781
+ for (const mod of mods) {
1782
+ if (mod.isApplicableToOsu()) {
1783
+ scoreMultiplier *= mod.pcScoreMultiplier;
1784
+ }
1785
+ }
1749
1786
  switch (true) {
1750
1787
  case accumulatedDiffPoints <= 5:
1751
1788
  difficultyMultiplier = 2;
@@ -1763,15 +1800,6 @@ class Beatmap {
1763
1800
  difficultyMultiplier = 6;
1764
1801
  break;
1765
1802
  }
1766
- return this.maxScore(difficultyMultiplier, mods.reduce((a, v) => a * v.scoreMultiplier, 1));
1767
- }
1768
- /**
1769
- * Calculates the maximum score with a given difficulty and score multiplier.
1770
- *
1771
- * @param difficultyMultiplier The difficulty multiplier.
1772
- * @param scoreMultiplier The score multiplier.
1773
- */
1774
- maxScore(difficultyMultiplier, scoreMultiplier) {
1775
1803
  let combo = 0;
1776
1804
  let score = 0;
1777
1805
  for (const object of this.hitObjects.objects) {
@@ -1782,11 +1810,18 @@ class Beatmap {
1782
1810
  ++combo;
1783
1811
  continue;
1784
1812
  }
1785
- const tickCount = object.nestedHitObjects.filter((v) => v instanceof SliderTick).length;
1786
- // Apply sliderhead, slider repeats, and slider ticks
1787
- score += 30 * (object.repeats + 1) + 10 * tickCount;
1788
- combo += tickCount + (object.repeats + 1);
1789
- // Apply sliderend
1813
+ const ticksPerSpan = object.ticks;
1814
+ const totalTicks = ticksPerSpan * (object.repeats + 1);
1815
+ // Apply slider head.
1816
+ score += 30;
1817
+ ++combo;
1818
+ // Apply slider repeats.
1819
+ score += 30 * object.repeats;
1820
+ combo += object.repeats;
1821
+ // Apply slider ticks.
1822
+ score += 10 * totalTicks;
1823
+ combo += totalTicks;
1824
+ // Apply slider end.
1790
1825
  score += Math.floor(300 +
1791
1826
  (300 * combo * difficultyMultiplier * scoreMultiplier) / 25);
1792
1827
  ++combo;
@@ -1999,160 +2034,163 @@ class OsuHitWindow extends HitWindow {
1999
2034
  * Represents a mod.
2000
2035
  */
2001
2036
  class Mod {
2037
+ /**
2038
+ * Whether this mod can be applied to osu!droid.
2039
+ */
2040
+ isApplicableToDroid() {
2041
+ return "droidRanked" in this;
2042
+ }
2043
+ /**
2044
+ * Whether this mod can be applied to osu!standard.
2045
+ */
2046
+ isApplicableToOsu() {
2047
+ return "pcRanked" in this;
2048
+ }
2002
2049
  }
2003
2050
 
2004
2051
  /**
2005
2052
  * Represents the DoubleTime mod.
2006
2053
  */
2007
2054
  class ModDoubleTime extends Mod {
2008
- scoreMultiplier = 1.12;
2009
2055
  acronym = "DT";
2010
2056
  name = "DoubleTime";
2011
2057
  droidRanked = true;
2012
2058
  pcRanked = true;
2059
+ droidScoreMultiplier = 1.12;
2060
+ pcScoreMultiplier = 1.12;
2013
2061
  bitwise = 1 << 6;
2014
2062
  droidString = "d";
2015
- droidOnly = false;
2016
2063
  }
2017
2064
 
2018
2065
  /**
2019
2066
  * Represents the HalfTime mod.
2020
2067
  */
2021
2068
  class ModHalfTime extends Mod {
2022
- scoreMultiplier = 0.3;
2023
2069
  acronym = "HT";
2024
2070
  name = "HalfTime";
2025
2071
  droidRanked = true;
2026
2072
  pcRanked = true;
2073
+ droidScoreMultiplier = 0.3;
2074
+ pcScoreMultiplier = 0.3;
2027
2075
  bitwise = 1 << 8;
2028
2076
  droidString = "t";
2029
- droidOnly = false;
2030
2077
  }
2031
2078
 
2032
2079
  /**
2033
2080
  * Represents the NightCore mod.
2034
2081
  */
2035
2082
  class ModNightCore extends Mod {
2036
- scoreMultiplier = 1.12;
2037
2083
  acronym = "NC";
2038
2084
  name = "NightCore";
2039
2085
  droidRanked = true;
2040
2086
  pcRanked = true;
2087
+ droidScoreMultiplier = 1.12;
2088
+ pcScoreMultiplier = 1.12;
2041
2089
  bitwise = 1 << 9;
2042
2090
  droidString = "c";
2043
- droidOnly = false;
2044
2091
  }
2045
2092
 
2046
2093
  /**
2047
2094
  * Represents the HardRock mod.
2048
2095
  */
2049
2096
  class ModHardRock extends Mod {
2050
- scoreMultiplier = 1.06;
2051
2097
  acronym = "HR";
2052
2098
  name = "HardRock";
2053
2099
  bitwise = 1 << 4;
2054
2100
  droidRanked = true;
2055
2101
  pcRanked = true;
2102
+ droidScoreMultiplier = 1.06;
2103
+ pcScoreMultiplier = 1.06;
2056
2104
  droidString = "r";
2057
- droidOnly = false;
2058
2105
  }
2059
2106
 
2060
2107
  /**
2061
2108
  * Represents the Easy mod.
2062
2109
  */
2063
2110
  class ModEasy extends Mod {
2064
- scoreMultiplier = 0.5;
2065
2111
  acronym = "EZ";
2066
2112
  name = "Easy";
2067
2113
  droidRanked = true;
2068
2114
  pcRanked = true;
2115
+ droidScoreMultiplier = 0.5;
2116
+ pcScoreMultiplier = 0.5;
2069
2117
  bitwise = 1 << 1;
2070
2118
  droidString = "e";
2071
- droidOnly = false;
2072
2119
  }
2073
2120
 
2074
2121
  /**
2075
2122
  * Represents the Precise mod.
2076
2123
  */
2077
2124
  class ModPrecise extends Mod {
2078
- scoreMultiplier = 1.06;
2079
2125
  acronym = "PR";
2080
2126
  name = "Precise";
2081
2127
  droidRanked = true;
2082
- pcRanked = false;
2083
- bitwise = Number.NaN;
2128
+ droidScoreMultiplier = 1.06;
2084
2129
  droidString = "s";
2085
- droidOnly = true;
2086
2130
  }
2087
2131
 
2088
2132
  /**
2089
2133
  * Represents the SmallCircle mod.
2090
2134
  */
2091
2135
  class ModSmallCircle extends Mod {
2092
- scoreMultiplier = 1.06;
2093
2136
  acronym = "SC";
2094
2137
  name = "SmallCircle";
2095
2138
  droidRanked = false;
2096
- pcRanked = false;
2097
- bitwise = Number.NaN;
2139
+ droidScoreMultiplier = 1.06;
2098
2140
  droidString = "m";
2099
- droidOnly = true;
2100
2141
  }
2101
2142
 
2102
2143
  /**
2103
2144
  * Represents the ReallyEasy mod.
2104
2145
  */
2105
2146
  class ModReallyEasy extends Mod {
2106
- scoreMultiplier = 0.4;
2107
2147
  acronym = "RE";
2108
2148
  name = "ReallyEasy";
2109
2149
  droidRanked = false;
2110
- pcRanked = false;
2111
- bitwise = Number.NaN;
2150
+ droidScoreMultiplier = 0.4;
2112
2151
  droidString = "l";
2113
- droidOnly = true;
2114
2152
  }
2115
2153
 
2116
2154
  /**
2117
2155
  * Represents the Auto mod.
2118
2156
  */
2119
2157
  class ModAuto extends Mod {
2120
- scoreMultiplier = 0;
2121
2158
  acronym = "AT";
2122
2159
  name = "Autoplay";
2123
2160
  droidRanked = false;
2124
2161
  pcRanked = false;
2162
+ droidScoreMultiplier = 1;
2163
+ pcScoreMultiplier = 1;
2125
2164
  bitwise = 1 << 11;
2126
2165
  droidString = "a";
2127
- droidOnly = false;
2128
2166
  }
2129
2167
 
2130
2168
  /**
2131
2169
  * Represents the Autopilot mod.
2132
2170
  */
2133
2171
  class ModAutopilot extends Mod {
2134
- scoreMultiplier = 0;
2135
2172
  acronym = "AP";
2136
2173
  name = "Autopilot";
2137
2174
  droidRanked = false;
2138
2175
  pcRanked = false;
2176
+ droidScoreMultiplier = 0.001;
2177
+ pcScoreMultiplier = 0;
2139
2178
  bitwise = 1 << 13;
2140
2179
  droidString = "p";
2141
- droidOnly = false;
2142
2180
  }
2143
2181
 
2144
2182
  /**
2145
2183
  * Represents the Flashlight mod.
2146
2184
  */
2147
2185
  class ModFlashlight extends Mod {
2148
- scoreMultiplier = 1.12;
2149
2186
  acronym = "FL";
2150
2187
  name = "Flashlight";
2151
2188
  droidRanked = false;
2152
2189
  pcRanked = true;
2190
+ droidScoreMultiplier = 1.12;
2191
+ pcScoreMultiplier = 1.12;
2153
2192
  bitwise = 1 << 10;
2154
2193
  droidString = "i";
2155
- droidOnly = false;
2156
2194
  }
2157
2195
 
2158
2196
  /**
@@ -2161,112 +2199,106 @@ class ModFlashlight extends Mod {
2161
2199
  class ModHidden extends Mod {
2162
2200
  static fadeInDurationMultiplier = 0.4;
2163
2201
  static fadeOutDurationMultiplier = 0.3;
2164
- scoreMultiplier = 1.06;
2165
2202
  acronym = "HD";
2166
2203
  name = "Hidden";
2167
2204
  bitwise = 1 << 3;
2168
2205
  droidRanked = true;
2169
2206
  pcRanked = true;
2207
+ droidScoreMultiplier = 1.06;
2208
+ pcScoreMultiplier = 1.06;
2170
2209
  droidString = "h";
2171
- droidOnly = false;
2172
2210
  }
2173
2211
 
2174
2212
  /**
2175
2213
  * Represents the NoFail mod.
2176
2214
  */
2177
2215
  class ModNoFail extends Mod {
2178
- scoreMultiplier = 0.5;
2179
2216
  acronym = "NF";
2180
2217
  name = "NoFail";
2181
2218
  droidRanked = true;
2182
2219
  pcRanked = true;
2220
+ droidScoreMultiplier = 0.5;
2221
+ pcScoreMultiplier = 0.5;
2183
2222
  bitwise = 1 << 0;
2184
2223
  droidString = "n";
2185
- droidOnly = false;
2186
2224
  }
2187
2225
 
2188
2226
  /**
2189
2227
  * Represents the Perfect mod.
2190
2228
  */
2191
2229
  class ModPerfect extends Mod {
2192
- scoreMultiplier = 1;
2193
2230
  acronym = "PF";
2194
2231
  name = "Perfect";
2195
2232
  droidRanked = false;
2196
2233
  pcRanked = true;
2234
+ droidScoreMultiplier = 1;
2235
+ pcScoreMultiplier = 1;
2197
2236
  bitwise = 1 << 14;
2198
2237
  droidString = "f";
2199
- droidOnly = false;
2200
2238
  }
2201
2239
 
2202
2240
  /**
2203
2241
  * Represents the Relax mod.
2204
2242
  */
2205
2243
  class ModRelax extends Mod {
2206
- scoreMultiplier = 0;
2207
2244
  acronym = "RX";
2208
2245
  name = "Relax";
2209
2246
  droidRanked = false;
2210
2247
  pcRanked = false;
2248
+ droidScoreMultiplier = 0.001;
2249
+ pcScoreMultiplier = 0;
2211
2250
  bitwise = 1 << 7;
2212
2251
  droidString = "x";
2213
- droidOnly = false;
2214
2252
  }
2215
2253
 
2216
2254
  /**
2217
2255
  * Represents the ScoreV2 mod.
2218
2256
  */
2219
2257
  class ModScoreV2 extends Mod {
2220
- scoreMultiplier = 1;
2221
2258
  acronym = "V2";
2222
2259
  name = "ScoreV2";
2223
2260
  droidRanked = false;
2224
2261
  pcRanked = false;
2262
+ droidScoreMultiplier = 1;
2263
+ pcScoreMultiplier = 1;
2225
2264
  bitwise = 1 << 29;
2226
2265
  droidString = "v";
2227
- droidOnly = false;
2228
2266
  }
2229
2267
 
2230
2268
  /**
2231
2269
  * Represents the SpunOut mod.
2232
2270
  */
2233
2271
  class ModSpunOut extends Mod {
2234
- scoreMultiplier = 0.9;
2235
2272
  acronym = "SO";
2236
2273
  name = "SpunOut";
2237
- droidRanked = false;
2238
2274
  pcRanked = true;
2275
+ pcScoreMultiplier = 0.9;
2239
2276
  bitwise = 1 << 12;
2240
- droidString = "";
2241
- droidOnly = false;
2242
2277
  }
2243
2278
 
2244
2279
  /**
2245
2280
  * Represents the SuddenDeath mod.
2246
2281
  */
2247
2282
  class ModSuddenDeath extends Mod {
2248
- scoreMultiplier = 1;
2249
2283
  acronym = "SD";
2250
2284
  name = "Sudden Death";
2251
2285
  droidRanked = false;
2252
2286
  pcRanked = true;
2287
+ droidScoreMultiplier = 1;
2288
+ pcScoreMultiplier = 1;
2253
2289
  bitwise = 1 << 5;
2254
2290
  droidString = "u";
2255
- droidOnly = false;
2256
2291
  }
2257
2292
 
2258
2293
  /**
2259
2294
  * Represents the TouchDevice mod.
2260
2295
  */
2261
2296
  class ModTouchDevice extends Mod {
2262
- scoreMultiplier = 1;
2263
2297
  acronym = "TD";
2264
2298
  name = "TouchDevice";
2265
- droidRanked = true;
2266
2299
  pcRanked = true;
2300
+ pcScoreMultiplier = 1;
2267
2301
  bitwise = 1 << 2;
2268
- droidString = "";
2269
- droidOnly = false;
2270
2302
  }
2271
2303
 
2272
2304
  /**
@@ -2331,7 +2363,8 @@ class ModUtil {
2331
2363
  * @param options Options for parsing behavior.
2332
2364
  */
2333
2365
  static droidStringToMods(str, options) {
2334
- return this.processParsingOptions(this.allMods.filter((m) => m.droidString && str.toLowerCase().includes(m.droidString)), options);
2366
+ return this.processParsingOptions(this.allMods.filter((m) => m.isApplicableToDroid() &&
2367
+ str.toLowerCase().includes(m.droidString)), options);
2335
2368
  }
2336
2369
  /**
2337
2370
  * Gets a list of mods from a PC modbits.
@@ -2340,7 +2373,7 @@ class ModUtil {
2340
2373
  * @param options Options for parsing behavior.
2341
2374
  */
2342
2375
  static pcModbitsToMods(modbits, options) {
2343
- return this.processParsingOptions(this.allMods.filter((m) => m.bitwise & modbits), options);
2376
+ return this.processParsingOptions(this.allMods.filter((m) => m.isApplicableToOsu() && (m.bitwise & modbits)), options);
2344
2377
  }
2345
2378
  /**
2346
2379
  * Gets a list of mods from a PC mod string, such as "HDHR".
@@ -2381,11 +2414,10 @@ class ModUtil {
2381
2414
  */
2382
2415
  static checkIncompatibleMods(mods) {
2383
2416
  for (const incompatibleMod of this.incompatibleMods) {
2384
- const fulfilledMods = mods.filter((m) => incompatibleMod.map((v) => v.acronym).includes(m.acronym));
2417
+ const fulfilledMods = mods.filter((m) => incompatibleMod.some((v) => m.acronym === v.acronym));
2385
2418
  if (fulfilledMods.length > 1) {
2386
- mods = mods.filter((m) => !incompatibleMod
2387
- .map((v) => v.acronym)
2388
- .includes(m.acronym));
2419
+ mods = mods.filter((m) => incompatibleMod
2420
+ .every((v) => m.acronym !== v.acronym));
2389
2421
  // Keep the first selected mod
2390
2422
  mods.push(fulfilledMods[0]);
2391
2423
  }
@@ -3304,6 +3336,9 @@ class Decoder {
3304
3336
  for (let line of str.split("\n")) {
3305
3337
  this.currentLine = line;
3306
3338
  ++this.line;
3339
+ if (this.shouldSkipLine(line)) {
3340
+ continue;
3341
+ }
3307
3342
  if (this.section !== BeatmapSection.metadata) {
3308
3343
  // Comments should not be stripped from metadata lines, as the song metadata may contain "//" as valid data.
3309
3344
  const index = line.indexOf("//");
@@ -3341,6 +3376,15 @@ class Decoder {
3341
3376
  }
3342
3377
  return this;
3343
3378
  }
3379
+ /**
3380
+ * Determines whether a line should be skipped.
3381
+ *
3382
+ * @param line The line to determine.
3383
+ * @returns Whether the line should be skipped.
3384
+ */
3385
+ shouldSkipLine(line) {
3386
+ return !line || line.trimStart().startsWith("//");
3387
+ }
3344
3388
  /**
3345
3389
  * Internal decoder function for decoding a line.
3346
3390
  *
@@ -3420,29 +3464,43 @@ class SectionDecoder {
3420
3464
  * @param str The string to parse.
3421
3465
  * @param min The minimum threshold. Defaults to `-ParserConstants.MAX_PARSE_VALUE`.
3422
3466
  * @param max The maximum threshold. Defaults to `ParserConstants.MAX_PARSE_VALUE`.
3467
+ * @param allowNaN Whether to allow NaN.
3423
3468
  * @returns The parsed integer.
3424
3469
  */
3425
- tryParseInt(str, min = -ParserConstants.MAX_PARSE_VALUE, max = ParserConstants.MAX_PARSE_VALUE) {
3470
+ tryParseInt(str, min = -ParserConstants.MAX_PARSE_VALUE, max = ParserConstants.MAX_PARSE_VALUE, allowNaN = false) {
3426
3471
  const num = parseInt(str);
3427
- if (!this.isNumberValid(num, min, max)) {
3428
- throw new RangeError(`Couldn't parse ${str} into an int: value is either invalid, too low, or too high`);
3472
+ if (num < min) {
3473
+ throw new RangeError("Value is too low");
3474
+ }
3475
+ if (num > max) {
3476
+ throw new RangeError("Value is too high");
3477
+ }
3478
+ if (!allowNaN && Number.isNaN(num)) {
3479
+ throw new RangeError("Not a number");
3429
3480
  }
3430
3481
  return num;
3431
3482
  }
3432
3483
  /**
3433
3484
  * Attempts to parse a string into a float.
3434
3485
  *
3435
- * Throws an exception when the resulting value is invalid (such as NaN), too low, or too high.
3486
+ * Throws an exception when the resulting value is too low or too high.
3436
3487
  *
3437
3488
  * @param str The string to parse.
3438
3489
  * @param min The minimum threshold. Defaults to `-ParserConstants.MAX_PARSE_VALUE`.
3439
3490
  * @param max The maximum threshold. Defaults to `ParserConstants.MAX_PARSE_VALUE`.
3491
+ * @param allowNaN Whether to allow NaN.
3440
3492
  * @returns The parsed float.
3441
3493
  */
3442
- tryParseFloat(str, min = -ParserConstants.MAX_PARSE_VALUE, max = ParserConstants.MAX_PARSE_VALUE) {
3494
+ tryParseFloat(str, min = -ParserConstants.MAX_PARSE_VALUE, max = ParserConstants.MAX_PARSE_VALUE, allowNaN = false) {
3443
3495
  const num = parseFloat(str);
3444
- if (!this.isNumberValid(num, min, max)) {
3445
- throw new RangeError(`Couldn't parse ${str} into a float: value is either invalid, too low, or too high`);
3496
+ if (num < min) {
3497
+ throw new RangeError("Value is too low");
3498
+ }
3499
+ if (num > max) {
3500
+ throw new RangeError("Value is too high");
3501
+ }
3502
+ if (!allowNaN && Number.isNaN(num)) {
3503
+ throw new RangeError("Not a number");
3446
3504
  }
3447
3505
  return num;
3448
3506
  }
@@ -3502,8 +3560,8 @@ class BeatmapHitObjectsDecoder extends SectionDecoder {
3502
3560
  }
3503
3561
  const repetitions = Math.max(0, this.tryParseInt(this.setPosition(s[6]), -ParserConstants.MAX_PARSE_VALUE, ParserConstants.MAX_REPETITIONS_VALUE));
3504
3562
  const distance = Math.max(0, this.tryParseFloat(this.setPosition(s[7])));
3505
- const speedMultiplierTimingPoint = this.target.controlPoints.difficulty.controlPointAt(time);
3506
- const msPerBeatTimingPoint = this.target.controlPoints.timing.controlPointAt(time);
3563
+ const difficultyControlPoint = this.target.controlPoints.difficulty.controlPointAt(time);
3564
+ const timingControlPoint = this.target.controlPoints.timing.controlPointAt(time);
3507
3565
  const points = [new Vector2(0, 0)];
3508
3566
  const pointSplit = this.setPosition(s[5]).split("|");
3509
3567
  let pathType = pointSplit.shift();
@@ -3573,6 +3631,23 @@ class BeatmapHitObjectsDecoder extends SectionDecoder {
3573
3631
  comboOffset += this.extraComboOffset;
3574
3632
  this.forceNewCombo = false;
3575
3633
  this.extraComboOffset = 0;
3634
+ let tickDistanceMultiplier = Number.POSITIVE_INFINITY;
3635
+ if (difficultyControlPoint.generateTicks) {
3636
+ if (this.isNumberValid(timingControlPoint.msPerBeat, ParserConstants.MIN_MSPERBEAT_VALUE, ParserConstants.MAX_MSPERBEAT_VALUE)) {
3637
+ // Prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
3638
+ // This results in more (or less) ticks being generated in <v8 maps for the same time duration.
3639
+ //
3640
+ // This additional check is used in case BPM goes very low or very high.
3641
+ // When lazer is final, this should be revisited.
3642
+ tickDistanceMultiplier =
3643
+ this.target.formatVersion < 8
3644
+ ? 1 / difficultyControlPoint.speedMultiplier
3645
+ : 1;
3646
+ }
3647
+ else {
3648
+ tickDistanceMultiplier = 0;
3649
+ }
3650
+ }
3576
3651
  object = new Slider({
3577
3652
  position: position,
3578
3653
  startTime: time,
@@ -3582,20 +3657,11 @@ class BeatmapHitObjectsDecoder extends SectionDecoder {
3582
3657
  nodeSamples: nodeSamples,
3583
3658
  repetitions: repetitions,
3584
3659
  path: path,
3585
- speedMultiplier: MathUtils.clamp(speedMultiplierTimingPoint.speedMultiplier, ParserConstants.MIN_SPEEDMULTIPLIER_VALUE, ParserConstants.MAX_SPEEDMULTIPLIER_VALUE),
3586
- msPerBeat: msPerBeatTimingPoint.msPerBeat,
3660
+ speedMultiplier: MathUtils.clamp(difficultyControlPoint.speedMultiplier, ParserConstants.MIN_SPEEDMULTIPLIER_VALUE, ParserConstants.MAX_SPEEDMULTIPLIER_VALUE),
3661
+ msPerBeat: timingControlPoint.msPerBeat,
3587
3662
  mapSliderVelocity: this.target.difficulty.sliderMultiplier,
3588
3663
  mapTickRate: this.target.difficulty.sliderTickRate,
3589
- // Prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
3590
- // This results in more (or less) ticks being generated in <v8 maps for the same time duration.
3591
- //
3592
- // This additional check is used in case BPM goes very low or very high.
3593
- // When lazer is final, this should be revisited.
3594
- tickDistanceMultiplier: this.isNumberValid(msPerBeatTimingPoint.msPerBeat, ParserConstants.MIN_MSPERBEAT_VALUE, ParserConstants.MAX_MSPERBEAT_VALUE)
3595
- ? this.target.formatVersion < 8
3596
- ? 1 / speedMultiplierTimingPoint.speedMultiplier
3597
- : 1
3598
- : 0,
3664
+ tickDistanceMultiplier: tickDistanceMultiplier,
3599
3665
  });
3600
3666
  }
3601
3667
  else if (type & exports.objectTypes.spinner) {
@@ -4143,7 +4209,9 @@ class BeatmapControlPointsDecoder extends SectionDecoder {
4143
4209
  throw new Error("Ignoring malformed timing point");
4144
4210
  }
4145
4211
  const time = this.target.getOffsetTime(this.tryParseFloat(this.setPosition(s[0])));
4146
- const msPerBeat = this.tryParseFloat(this.setPosition(s[1]));
4212
+ // msPerBeat is allowed to be NaN to handle an edge case in which some
4213
+ // beatmaps use NaN slider velocity to disable slider tick generation.
4214
+ const msPerBeat = this.tryParseFloat(this.setPosition(s[1]), undefined, undefined, true);
4147
4215
  let timeSignature = 4;
4148
4216
  if (s.length >= 3) {
4149
4217
  timeSignature = this.tryParseInt(this.setPosition(s[2]));
@@ -4170,7 +4238,14 @@ class BeatmapControlPointsDecoder extends SectionDecoder {
4170
4238
  kiaiMode = !!(effectBitFlags & EffectFlags.kiai);
4171
4239
  omitFirstBarSignature = !!(effectBitFlags & EffectFlags.omitFirstBarLine);
4172
4240
  }
4173
- if (msPerBeat >= 0) {
4241
+ let timingChange = true;
4242
+ if (s.length >= 7) {
4243
+ timingChange = s[6] === "1";
4244
+ }
4245
+ if (timingChange) {
4246
+ if (Number.isNaN(msPerBeat)) {
4247
+ throw new Error("Beat length cannot be NaN in a timing control point");
4248
+ }
4174
4249
  this.target.controlPoints.timing.add(new TimingControlPoint({
4175
4250
  time: time,
4176
4251
  msPerBeat: msPerBeat,
@@ -4179,7 +4254,9 @@ class BeatmapControlPointsDecoder extends SectionDecoder {
4179
4254
  }
4180
4255
  this.target.controlPoints.difficulty.add(new DifficultyControlPoint({
4181
4256
  time: time,
4257
+ // If msPerBeat is NaN, speedMultiplier should still be 1 because all comparisons against NaN are false.
4182
4258
  speedMultiplier: msPerBeat < 0 ? 100 / -msPerBeat : 1,
4259
+ generateTicks: !Number.isNaN(msPerBeat),
4183
4260
  }));
4184
4261
  this.target.controlPoints.effect.add(new EffectControlPoint({
4185
4262
  time: time,