sonolus-next-rush-engine 2.3.2 → 2.3.3

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.
Binary file
Binary file
@@ -67,6 +67,20 @@ const activeSlideStartArchetypes = new Set([
67
67
  'NormalTraceSlideStartNote',
68
68
  'CriticalTraceSlideStartNote',
69
69
  ]);
70
+ const slideTickArchetypes = new Set([
71
+ 'IgnoredSlideTickNote',
72
+ 'NormalSlideTickNote',
73
+ 'CriticalSlideTickNote',
74
+ 'HiddenSlideTickNote',
75
+ 'NormalAttachedSlideTickNote',
76
+ 'CriticalAttachedSlideTickNote',
77
+ ]);
78
+ const scoredSlideTickArchetypes = new Set([
79
+ 'NormalSlideTickNote',
80
+ 'CriticalSlideTickNote',
81
+ 'NormalAttachedSlideTickNote',
82
+ 'CriticalAttachedSlideTickNote',
83
+ ]);
70
84
  const flickDirectionMapping = {
71
85
  [-1]: FlickDirection.UP_LEFT,
72
86
  0: FlickDirection.UP_OMNI,
@@ -94,6 +108,18 @@ const guideKindMapping = {
94
108
  6: ConnectorKind.GUIDE_CYAN,
95
109
  7: ConnectorKind.GUIDE_BLACK,
96
110
  };
111
+ function lastIndexLeq(arr, x, key) {
112
+ let lo = 0;
113
+ let hi = arr.length;
114
+ while (lo < hi) {
115
+ const mid = (lo + hi) >> 1;
116
+ if (key(arr[mid]) <= x)
117
+ lo = mid + 1;
118
+ else
119
+ hi = mid;
120
+ }
121
+ return lo - 1;
122
+ }
97
123
  class ExtData {
98
124
  constructor(entities) {
99
125
  this.byArch = new Map();
@@ -142,15 +168,22 @@ class EntityBuilder {
142
168
  return this.values['#BEAT'] ?? -1;
143
169
  }
144
170
  }
171
+ const fieldMapCache = new WeakMap();
145
172
  function getField(e, name) {
146
- const field = e.data.find((x) => x.name === name);
147
- if (!field)
148
- return undefined;
149
- if (field.value !== undefined)
150
- return field.value;
151
- if (field.ref !== undefined)
152
- return field.ref;
153
- return undefined;
173
+ let fields = fieldMapCache.get(e);
174
+ if (!fields) {
175
+ fields = new Map();
176
+ for (const field of e.data) {
177
+ if (fields.has(field.name))
178
+ continue;
179
+ if (field.value !== undefined)
180
+ fields.set(field.name, field.value);
181
+ else if (field.ref !== undefined)
182
+ fields.set(field.name, field.ref);
183
+ }
184
+ fieldMapCache.set(e, fields);
185
+ }
186
+ return fields.get(name);
154
187
  }
155
188
  function getNum(e, name, def = 0) {
156
189
  const val = getField(e, name);
@@ -236,23 +269,13 @@ export const extendedToLevelData = (data, offset = 0) => {
236
269
  function beatToTime(beat) {
237
270
  if (bpmChangeInfos.length === 0)
238
271
  return (beat * 60) / 120;
239
- let current = bpmChangeInfos[0];
240
- for (const change of bpmChangeInfos) {
241
- if (change.beat > beat)
242
- break;
243
- current = change;
244
- }
272
+ const current = bpmChangeInfos[Math.max(0, lastIndexLeq(bpmChangeInfos, beat, (c) => c.beat))];
245
273
  return current.time + ((beat - current.beat) * 60) / current.bpm;
246
274
  }
247
275
  function timeToBeat(time) {
248
276
  if (bpmChangeInfos.length === 0)
249
277
  return (time * 120) / 60;
250
- let current = bpmChangeInfos[0];
251
- for (const change of bpmChangeInfos) {
252
- if (change.time > time)
253
- break;
254
- current = change;
255
- }
278
+ const current = bpmChangeInfos[Math.max(0, lastIndexLeq(bpmChangeInfos, time, (c) => c.time))];
256
279
  return current.beat + ((time - current.time) * current.bpm) / 60;
257
280
  }
258
281
  for (const { e } of ext.getByArch('#BPM_CHANGE')) {
@@ -281,6 +304,8 @@ export const extendedToLevelData = (data, offset = 0) => {
281
304
  changeInfos.push({
282
305
  beat: getNum(raw, '#BEAT'),
283
306
  timeScale: getNum(raw, 'timeScale'),
307
+ time: 0,
308
+ scaledTime: 0,
284
309
  });
285
310
  const change = new EntityBuilder('#TIMESCALE_CHANGE');
286
311
  change.set('#BEAT', getNum(raw, '#BEAT'));
@@ -303,6 +328,14 @@ export const extendedToLevelData = (data, offset = 0) => {
303
328
  }
304
329
  finalEntities.push(...changes);
305
330
  changeInfos.sort((a, b) => a.beat - b.beat);
331
+ for (let i = 0; i < changeInfos.length; i++) {
332
+ changeInfos[i].time = beatToTime(changeInfos[i].beat);
333
+ changeInfos[i].scaledTime =
334
+ i === 0
335
+ ? changeInfos[i].time
336
+ : changeInfos[i - 1].scaledTime +
337
+ (changeInfos[i].time - changeInfos[i - 1].time) * changeInfos[i - 1].timeScale;
338
+ }
306
339
  timescaleChangesByIndex.set(idx, changeInfos);
307
340
  if (e.name)
308
341
  timescaleChangesByName.set(e.name, changeInfos);
@@ -324,46 +357,31 @@ export const extendedToLevelData = (data, offset = 0) => {
324
357
  function timeToScaledTime(time, changes) {
325
358
  if (changes.length === 0)
326
359
  return time;
327
- const firstTime = beatToTime(changes[0].beat);
328
- if (time < firstTime)
360
+ if (time < changes[0].time)
329
361
  return time;
330
- let scaledTime = firstTime;
331
- for (let i = 0; i < changes.length; i++) {
332
- const start = changes[i];
333
- const startTime = beatToTime(start.beat);
334
- const endTime = i === changes.length - 1 ? undefined : beatToTime(changes[i + 1].beat);
335
- if (endTime === undefined || time < endTime) {
336
- return scaledTime + (time - startTime) * start.timeScale;
337
- }
338
- scaledTime += (endTime - startTime) * start.timeScale;
339
- }
340
- return time;
362
+ const current = changes[Math.max(0, lastIndexLeq(changes, time, (c) => c.time))];
363
+ return current.scaledTime + (time - current.time) * current.timeScale;
341
364
  }
342
365
  function scaledTimeToTime(scaledTime, changes) {
343
366
  if (changes.length === 0)
344
367
  return scaledTime;
345
- const firstTime = beatToTime(changes[0].beat);
346
- if (scaledTime < firstTime)
368
+ if (scaledTime < changes[0].time)
347
369
  return scaledTime;
348
- let currentScaledTime = firstTime;
349
370
  for (let i = 0; i < changes.length; i++) {
350
371
  const start = changes[i];
351
- const startTime = beatToTime(start.beat);
352
- const endTime = i === changes.length - 1 ? undefined : beatToTime(changes[i + 1].beat);
353
- if (endTime === undefined) {
372
+ if (i === changes.length - 1) {
354
373
  if (start.timeScale === 0)
355
374
  return Number.POSITIVE_INFINITY;
356
- return startTime + (scaledTime - currentScaledTime) / start.timeScale;
375
+ return start.time + (scaledTime - start.scaledTime) / start.timeScale;
357
376
  }
358
- const nextScaledTime = currentScaledTime + (endTime - startTime) * start.timeScale;
359
- const minScaledTime = Math.min(currentScaledTime, nextScaledTime);
360
- const maxScaledTime = Math.max(currentScaledTime, nextScaledTime);
377
+ const next = changes[i + 1];
378
+ const minScaledTime = Math.min(start.scaledTime, next.scaledTime);
379
+ const maxScaledTime = Math.max(start.scaledTime, next.scaledTime);
361
380
  if (minScaledTime <= scaledTime && scaledTime <= maxScaledTime) {
362
- if (Math.abs(nextScaledTime - currentScaledTime) < 1e-6)
363
- return startTime;
364
- return lerp(startTime, endTime, unlerp(currentScaledTime, nextScaledTime, scaledTime));
381
+ if (Math.abs(next.scaledTime - start.scaledTime) < 1e-6)
382
+ return start.time;
383
+ return lerp(start.time, next.time, unlerp(start.scaledTime, next.scaledTime, scaledTime));
365
384
  }
366
- currentScaledTime = nextScaledTime;
367
385
  }
368
386
  return scaledTime;
369
387
  }
@@ -399,8 +417,16 @@ export const extendedToLevelData = (data, offset = 0) => {
399
417
  return undefined;
400
418
  }
401
419
  function getTimeScaleAt(changes, beat) {
402
- const change = [...changes].reverse().find((change) => change.beat < beat - 1e-6);
403
- return change?.timeScale ?? 1;
420
+ let lo = 0;
421
+ let hi = changes.length;
422
+ while (lo < hi) {
423
+ const mid = (lo + hi) >> 1;
424
+ if (changes[mid].beat < beat - 1e-6)
425
+ lo = mid + 1;
426
+ else
427
+ hi = mid;
428
+ }
429
+ return lo > 0 ? changes[lo - 1].timeScale : 1;
404
430
  }
405
431
  function shouldUseStartAsHead(startRef, headRef) {
406
432
  if (startRef === headRef)
@@ -491,22 +517,10 @@ export const extendedToLevelData = (data, offset = 0) => {
491
517
  return getNum(tailOriginal, '#BEAT') < getNum(headOriginal, '#BEAT') - 1e-6;
492
518
  }
493
519
  function isSlideTickRef(ref) {
494
- return [
495
- 'IgnoredSlideTickNote',
496
- 'NormalSlideTickNote',
497
- 'CriticalSlideTickNote',
498
- 'HiddenSlideTickNote',
499
- 'NormalAttachedSlideTickNote',
500
- 'CriticalAttachedSlideTickNote',
501
- ].includes(resolveOriginal(ext, ref)?.archetype ?? '');
520
+ return slideTickArchetypes.has(resolveOriginal(ext, ref)?.archetype ?? '');
502
521
  }
503
522
  function isScoredSlideTickRef(ref) {
504
- return [
505
- 'NormalSlideTickNote',
506
- 'CriticalSlideTickNote',
507
- 'NormalAttachedSlideTickNote',
508
- 'CriticalAttachedSlideTickNote',
509
- ].includes(resolveOriginal(ext, ref)?.archetype ?? '');
523
+ return scoredSlideTickArchetypes.has(resolveOriginal(ext, ref)?.archetype ?? '');
510
524
  }
511
525
  function getUltimateTailRef(archetype, startRef, tailRef) {
512
526
  let ultimateTailRef = tailRef;
@@ -622,23 +636,37 @@ export const extendedToLevelData = (data, offset = 0) => {
622
636
  return getUltimateStartRef(archetype, startRef, headRef);
623
637
  }
624
638
  const activeTailRefsByStart = new Map();
639
+ let activeTailCandidatesByStart;
640
+ function getActiveTailCandidates(key) {
641
+ if (!activeTailCandidatesByStart) {
642
+ activeTailCandidatesByStart = new Map();
643
+ for (const { e } of ext.connectors) {
644
+ const startRef = getField(e, 'start');
645
+ const headRef = getField(e, 'head');
646
+ if (isIgnoredSlideTickRef(headRef))
647
+ continue;
648
+ const endRef = getField(e, 'end');
649
+ const connectorActiveStartRef = getConnectorActiveStartRef(e.archetype, startRef, headRef, endRef);
650
+ const { tailRef } = resolveConnectorTailRef(e.archetype, startRef, getField(e, 'tail'));
651
+ const connectorKey = refKey(connectorActiveStartRef);
652
+ const candidate = { archetype: e.archetype, endRef, tailRef };
653
+ const list = activeTailCandidatesByStart.get(connectorKey);
654
+ if (list)
655
+ list.push(candidate);
656
+ else
657
+ activeTailCandidatesByStart.set(connectorKey, [candidate]);
658
+ }
659
+ }
660
+ return activeTailCandidatesByStart.get(key) ?? [];
661
+ }
625
662
  function getActiveTailRef(activeStartRef) {
626
663
  const key = refKey(activeStartRef);
627
664
  if (activeTailRefsByStart.has(key))
628
665
  return activeTailRefsByStart.get(key);
629
666
  let activeTailRef;
630
667
  let activeTailBeat = Number.NEGATIVE_INFINITY;
631
- for (const { e } of ext.connectors) {
632
- const startRef = getField(e, 'start');
633
- const headRef = getField(e, 'head');
634
- if (isIgnoredSlideTickRef(headRef))
635
- continue;
636
- const endRef = getField(e, 'end');
637
- const connectorActiveStartRef = getConnectorActiveStartRef(e.archetype, startRef, headRef, endRef);
638
- if (refKey(connectorActiveStartRef) !== key)
639
- continue;
640
- const { tailRef } = resolveConnectorTailRef(e.archetype, startRef, getField(e, 'tail'));
641
- const candidateTailRef = endRef ?? getUltimateTailRef(e.archetype, activeStartRef, tailRef);
668
+ for (const { archetype, endRef, tailRef } of getActiveTailCandidates(key)) {
669
+ const candidateTailRef = endRef ?? getUltimateTailRef(archetype, activeStartRef, tailRef);
642
670
  const candidateTailBeat = getRefBeat(candidateTailRef);
643
671
  if (candidateTailBeat >= activeTailBeat) {
644
672
  activeTailBeat = candidateTailBeat;
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ import { USC } from './usc/index.js';
7
7
  export * from './usc/index.js';
8
8
  export { type ExtendedEntityData, type ExtendedEntityDataField, extendedToLevelData, mmwsToUSC, susToUSC, ucmmwsToLevelData, uscToLevelData, };
9
9
  export declare const convertToLevelData: (input: string | Uint8Array | USC | LevelData, offset?: number) => LevelData;
10
- export declare const version = "2.3.2";
10
+ export declare const version = "2.3.3";
11
11
  export declare const databaseEngineItem: {
12
12
  readonly name: "next-rush";
13
13
  readonly version: 13;
package/dist/index.js CHANGED
@@ -65,7 +65,7 @@ export const convertToLevelData = (input, offset = 0) => {
65
65
  }
66
66
  return uscToLevelData(usc, offset, true, true);
67
67
  };
68
- export const version = '2.3.2';
68
+ export const version = '2.3.3';
69
69
  export const databaseEngineItem = {
70
70
  name: 'next-rush',
71
71
  version: 13,
@@ -25,6 +25,7 @@ const mmwEaseToSonolusEase = (ease) => {
25
25
  };
26
26
  const EPSILON = 1e-6;
27
27
  const TICKS_PER_BEAT = 480;
28
+ const uscColorByValue = new Map(Object.entries(USCColor).map(([name, value]) => [value, name]));
28
29
  const laneToSonolusLane = (lane, width) => {
29
30
  return lane - 6 + width / 2;
30
31
  };
@@ -152,7 +153,7 @@ export const mmwsToUSC = (mmws) => {
152
153
  const uscGuide = {
153
154
  type: 'guide',
154
155
  fade: hold.fadeType === 0 ? 'out' : hold.fadeType === 1 ? 'none' : 'in',
155
- color: Object.entries(USCColor).find(([, i]) => i === hold.guideColor)?.[0],
156
+ color: uscColorByValue.get(hold.guideColor),
156
157
  midpoints: [hold.start, ...hold.steps, hold.end].map((step) => ({
157
158
  beat: step.tick / TICKS_PER_BEAT,
158
159
  lane: laneToUSCLane(step),
@@ -58,10 +58,24 @@ export const analyze = (sus) => {
58
58
  timeScaleChanges.push(scaledChanges);
59
59
  return newIndex;
60
60
  };
61
+ const measureChangesAsc = [...measureChanges].reverse();
62
+ const timeScaleGroupChangesAsc = [...timeScaleGroupChanges].reverse();
63
+ let measureChangeCursor = -1;
64
+ let timeScaleGroupChangeCursor = -1;
61
65
  lines.forEach((line, index) => {
62
66
  const [header, data] = line;
63
- const measureOffset = measureChanges.find(([changeIndex]) => changeIndex <= index)?.[1] ?? 0;
64
- const timeScaleGroupName = timeScaleGroupChanges.find(([changeIndex]) => changeIndex <= index)?.[1] ?? '00';
67
+ while (measureChangeCursor + 1 < measureChangesAsc.length &&
68
+ measureChangesAsc[measureChangeCursor + 1][0] <= index) {
69
+ measureChangeCursor++;
70
+ }
71
+ while (timeScaleGroupChangeCursor + 1 < timeScaleGroupChangesAsc.length &&
72
+ timeScaleGroupChangesAsc[timeScaleGroupChangeCursor + 1][0] <= index) {
73
+ timeScaleGroupChangeCursor++;
74
+ }
75
+ const measureOffset = measureChangeCursor >= 0 ? measureChangesAsc[measureChangeCursor][1] : 0;
76
+ const timeScaleGroupName = timeScaleGroupChangeCursor >= 0
77
+ ? timeScaleGroupChangesAsc[timeScaleGroupChangeCursor][1]
78
+ : '00';
65
79
  let timeScaleGroup = timeScaleGroups.get(timeScaleGroupName);
66
80
  if (timeScaleGroup === undefined) {
67
81
  timeScaleGroup = timeScaleGroups.size;
@@ -168,14 +182,18 @@ const getTicksPerBeat = (meta) => {
168
182
  };
169
183
  const getBarLengths = (lines, measureChanges) => {
170
184
  const barLengths = [];
185
+ const measureChangesAsc = [...measureChanges].reverse();
186
+ let cursor = -1;
171
187
  lines.forEach((line, index) => {
172
188
  const [header, data] = line;
189
+ while (cursor + 1 < measureChangesAsc.length && measureChangesAsc[cursor + 1][0] <= index) {
190
+ cursor++;
191
+ }
173
192
  if (header.length !== 5)
174
193
  return;
175
194
  if (!header.endsWith('02'))
176
195
  return;
177
- const measure = +header.substring(0, 3) +
178
- (measureChanges.find(([changeIndex]) => changeIndex <= index)?.[1] ?? 0);
196
+ const measure = +header.substring(0, 3) + (cursor >= 0 ? measureChangesAsc[cursor][1] : 0);
179
197
  if (Number.isNaN(measure))
180
198
  return;
181
199
  barLengths.push({ measure, length: +data });
@@ -192,12 +210,20 @@ const getToTick = (barLengths, ticksPerBeat) => {
192
210
  ticks += (measure - prev.measure) * prev.length * ticksPerBeat;
193
211
  }
194
212
  return { measure, ticksPerMeasure: length * ticksPerBeat, ticks };
195
- })
196
- .reverse();
213
+ });
197
214
  return (measure, p, q) => {
198
- const bar = bars.find((bar) => measure >= bar.measure);
199
- if (!bar)
215
+ let lo = 0;
216
+ let hi = bars.length;
217
+ while (lo < hi) {
218
+ const mid = (lo + hi) >> 1;
219
+ if (bars[mid].measure <= measure)
220
+ lo = mid + 1;
221
+ else
222
+ hi = mid;
223
+ }
224
+ if (lo === 0)
200
225
  throw new Error('Unexpected missing bar');
226
+ const bar = bars[lo - 1];
201
227
  return (bar.ticks +
202
228
  (measure - bar.measure) * bar.ticksPerMeasure +
203
229
  (p * bar.ticksPerMeasure) / q);
@@ -11,6 +11,7 @@ export const chsLikeToUSC = (score) => {
11
11
  const preventSingles = new Set();
12
12
  const dedupeSingles = new Set();
13
13
  const dedupeSlides = new Map();
14
+ const removedSlides = new Set();
14
15
  const requests = {
15
16
  sideLane: false,
16
17
  laneOffset: 0,
@@ -309,12 +310,14 @@ export const chsLikeToUSC = (score) => {
309
310
  const key = getKey(startNote);
310
311
  const dupe = dedupeSlides.get(key);
311
312
  if (dupe)
312
- objects.splice(objects.indexOf(dupe), 1);
313
+ removedSlides.add(dupe);
313
314
  dedupeSlides.set(key, object);
314
315
  }
315
316
  return {
316
317
  offset: score.offset,
317
- objects,
318
+ objects: removedSlides.size
319
+ ? objects.filter((object) => !removedSlides.has(object))
320
+ : objects,
318
321
  };
319
322
  };
320
323
  const getKey = (note) => `${note.lane}-${note.tick}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonolus-next-rush-engine",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "description": "Perspective-lane rhythm game for Sonolus",
5
5
  "author": "Hyeon2",
6
6
  "repository": {