sonolus-next-rush-plus-engine 1.5.8 → 1.5.9

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
@@ -1,7 +1,13 @@
1
1
  import { type LevelData } from '@sonolus/core';
2
+ type ExtendedEntityDataField = {
3
+ name: string;
4
+ value?: number;
5
+ ref?: string;
6
+ };
2
7
  export type ExtendedEntityData = {
3
8
  archetype: string;
4
- data: Record<string, number | string>;
9
+ name?: string;
10
+ data: ExtendedEntityDataField[];
5
11
  };
6
12
  export type ExtendedLevelData = {
7
13
  bgmOffset: number;
@@ -9,3 +15,4 @@ export type ExtendedLevelData = {
9
15
  };
10
16
  /** Convert a PJSekaiExtendedLevelData to a Level Data (Next Sekai) */
11
17
  export declare const extendedToLevelData: (data: ExtendedLevelData, offset?: number) => LevelData;
18
+ export {};
@@ -82,43 +82,87 @@ const guideKindMapping = {
82
82
  6: ConnectorKind.GUIDE_CYAN,
83
83
  7: ConnectorKind.GUIDE_BLACK,
84
84
  };
85
+ /** data 배열 → 평탄한 Record로 변환 (value → number, ref → string) */
86
+ const flattenData = (fields) => {
87
+ const result = {};
88
+ for (const field of fields) {
89
+ if (field.value !== undefined)
90
+ result[field.name] = field.value;
91
+ else if (field.ref !== undefined)
92
+ result[field.name] = field.ref;
93
+ }
94
+ return result;
95
+ };
96
+ /** 숫자 값 읽기 */
85
97
  const getNum = (data, key, defaultValue = 0) => {
86
98
  const val = data[key];
87
- if (val === undefined || val === null)
88
- return defaultValue;
89
- const n = Number(val);
90
- return isNaN(n) ? defaultValue : n;
99
+ if (typeof val === 'number')
100
+ return val;
101
+ if (typeof val === 'string') {
102
+ const n = Number(val);
103
+ return isNaN(n) ? defaultValue : n;
104
+ }
105
+ return defaultValue;
106
+ };
107
+ /** 참조 값 읽기 (number | string) */
108
+ const getEntityRef = (data, key) => {
109
+ const val = data[key];
110
+ if (val === undefined)
111
+ return undefined;
112
+ return val;
113
+ };
114
+ /** 참조로 FlatEntity 조회 (number → 인덱스, string → 이름) */
115
+ const resolveEntity = (ref, entities, entitiesByName) => {
116
+ if (typeof ref === 'number')
117
+ return entities[ref];
118
+ return entitiesByName.get(ref);
91
119
  };
92
120
  /** Convert a PJSekaiExtendedLevelData to a Level Data (Next Sekai) */
93
121
  export const extendedToLevelData = (data, offset = 0) => {
122
+ const flatEntities = data.entities.map((e) => ({
123
+ archetype: e.archetype,
124
+ name: e.name,
125
+ data: flattenData(e.data),
126
+ }));
127
+ const entitiesByName = new Map();
128
+ for (const entity of flatEntities) {
129
+ if (entity.name)
130
+ entitiesByName.set(entity.name, entity);
131
+ }
132
+ const resolve = (ref) => {
133
+ if (ref === undefined)
134
+ return undefined;
135
+ return resolveEntity(ref, flatEntities, entitiesByName);
136
+ };
94
137
  const allIntermediateEntities = [];
95
138
  const createIntermediate = (archetype, entityData) => {
96
- const intermediateEntity = { archetype, data: entityData };
97
- allIntermediateEntities.push(intermediateEntity);
98
- return intermediateEntity;
139
+ const entity = { archetype, data: entityData };
140
+ allIntermediateEntities.push(entity);
141
+ return entity;
99
142
  };
100
143
  createIntermediate('Initialization', {});
101
- for (const entity of data.entities) {
102
- if (entity.archetype === '#BPM_CHANGE') {
103
- createIntermediate('#BPM_CHANGE', {
104
- '#BEAT': getNum(entity.data, '#BEAT'),
105
- '#BPM': getNum(entity.data, '#BPM'),
106
- });
107
- }
144
+ for (const entity of flatEntities) {
145
+ if (entity.archetype !== '#BPM_CHANGE')
146
+ continue;
147
+ createIntermediate('#BPM_CHANGE', {
148
+ '#BEAT': getNum(entity.data, '#BEAT'),
149
+ '#BPM': getNum(entity.data, '#BPM'),
150
+ });
108
151
  }
109
152
  const timescaleGroupsByIndex = new Map();
110
- for (let i = 0; i < data.entities.length; i++) {
111
- const entity = data.entities[i];
153
+ const timescaleGroupsByName = new Map();
154
+ for (let i = 0; i < flatEntities.length; i++) {
155
+ const entity = flatEntities[i];
112
156
  if (entity.archetype !== 'TimeScaleGroup')
113
157
  continue;
114
158
  const groupIntermediate = createIntermediate('TimeScaleGroup', {});
115
159
  timescaleGroupsByIndex.set(i, groupIntermediate);
116
- let rawChangeIdx = getNum(entity.data, 'first', -1);
117
- if (rawChangeIdx < 0)
118
- continue;
160
+ if (entity.name)
161
+ timescaleGroupsByName.set(entity.name, groupIntermediate);
162
+ let rawChangeRef = getEntityRef(entity.data, 'first');
119
163
  let lastChangeIntermediate = null;
120
- while (true) {
121
- const rawChange = data.entities[rawChangeIdx];
164
+ while (rawChangeRef !== undefined) {
165
+ const rawChange = resolve(rawChangeRef);
122
166
  if (!rawChange)
123
167
  break;
124
168
  const changeIntermediate = createIntermediate('TimeScaleChange', {
@@ -135,89 +179,115 @@ export const extendedToLevelData = (data, offset = 0) => {
135
179
  groupIntermediate.data['first'] = changeIntermediate;
136
180
  }
137
181
  lastChangeIntermediate = changeIntermediate;
138
- const nextIdx = getNum(rawChange.data, 'next', 0);
139
- if (nextIdx <= 0)
140
- break;
141
- rawChangeIdx = nextIdx;
182
+ rawChangeRef = getEntityRef(rawChange.data, 'next');
142
183
  }
143
184
  }
144
- const notesByOriginalIndex = new Map();
145
- for (let i = 0; i < data.entities.length; i++) {
146
- const entity = data.entities[i];
185
+ const resolveTimescaleGroup = (ref) => {
186
+ if (ref === undefined)
187
+ return undefined;
188
+ if (typeof ref === 'number')
189
+ return timescaleGroupsByIndex.get(ref);
190
+ return timescaleGroupsByName.get(ref);
191
+ };
192
+ const notesByIndex = new Map();
193
+ const notesByName = new Map();
194
+ for (let i = 0; i < flatEntities.length; i++) {
195
+ const entity = flatEntities[i];
147
196
  const mappedArchetype = noteTypeMapping[entity.archetype];
148
197
  if (!mappedArchetype)
149
198
  continue;
150
- const noteIntermediate = createIntermediate(mappedArchetype, {
199
+ const tsg = resolveTimescaleGroup(getEntityRef(entity.data, 'timeScaleGroup'));
200
+ const noteData = {
151
201
  '#BEAT': getNum(entity.data, '#BEAT'),
152
202
  lane: getNum(entity.data, 'lane', 0),
153
203
  size: getNum(entity.data, 'size', 0),
154
204
  direction: flickDirectionMapping[getNum(entity.data, 'direction', 0)],
155
205
  segmentKind: ConnectorKind.ACTIVE_NORMAL,
156
- });
157
- notesByOriginalIndex.set(i, noteIntermediate);
206
+ };
207
+ if (tsg !== undefined)
208
+ noteData['#TIMESCALE_GROUP'] = tsg;
209
+ const noteIntermediate = createIntermediate(mappedArchetype, noteData);
210
+ notesByIndex.set(i, noteIntermediate);
211
+ if (entity.name)
212
+ notesByName.set(entity.name, noteIntermediate);
158
213
  }
159
- const connectorsByOriginalIndex = new Map();
160
- for (let i = 0; i < data.entities.length; i++) {
161
- const entity = data.entities[i];
214
+ const resolveNote = (ref) => {
215
+ if (ref === undefined)
216
+ return undefined;
217
+ if (typeof ref === 'number')
218
+ return notesByIndex.get(ref);
219
+ return notesByName.get(ref);
220
+ };
221
+ const connectorsByIndex = new Map();
222
+ const connectorsByName = new Map();
223
+ for (let i = 0; i < flatEntities.length; i++) {
224
+ const entity = flatEntities[i];
162
225
  const connectorKind = activeConnectorKindMapping[entity.archetype];
163
226
  if (!connectorKind)
164
227
  continue;
165
- const head = notesByOriginalIndex.get(getNum(entity.data, 'head'));
166
- const tail = notesByOriginalIndex.get(getNum(entity.data, 'tail'));
167
- const segmentHead = notesByOriginalIndex.get(getNum(entity.data, 'start'));
168
- const segmentTail = notesByOriginalIndex.get(getNum(entity.data, 'end'));
169
- if (!head || !tail || !segmentHead || !segmentTail)
228
+ const headNote = resolveNote(getEntityRef(entity.data, 'head'));
229
+ const tailNote = resolveNote(getEntityRef(entity.data, 'tail'));
230
+ const segmentHeadNote = resolveNote(getEntityRef(entity.data, 'start'));
231
+ const segmentTailNote = resolveNote(getEntityRef(entity.data, 'end'));
232
+ if (!headNote || !tailNote || !segmentHeadNote || !segmentTailNote)
170
233
  continue;
171
234
  const connectorIntermediate = createIntermediate('Connector', {
172
- head,
173
- tail,
174
- segmentHead,
175
- segmentTail,
176
- activeHead: segmentHead,
177
- activeTail: segmentTail,
235
+ head: headNote,
236
+ tail: tailNote,
237
+ segmentHead: segmentHeadNote,
238
+ segmentTail: segmentTailNote,
239
+ activeHead: segmentHeadNote,
240
+ activeTail: segmentTailNote,
178
241
  });
179
- head.data['connectorEase'] = easeTypeMapping[getNum(entity.data, 'ease', 0)];
180
- head.data['segmentKind'] = connectorKind;
181
- tail.data['segmentKind'] = connectorKind;
182
- segmentHead.data['segmentKind'] = connectorKind;
183
- connectorsByOriginalIndex.set(i, connectorIntermediate);
242
+ headNote.data['connectorEase'] =
243
+ easeTypeMapping[getNum(entity.data, 'ease', 0)] ?? EaseType.LINEAR;
244
+ headNote.data['segmentKind'] = connectorKind;
245
+ tailNote.data['segmentKind'] = connectorKind;
246
+ segmentHeadNote.data['segmentKind'] = connectorKind;
247
+ connectorsByIndex.set(i, connectorIntermediate);
248
+ if (entity.name)
249
+ connectorsByName.set(entity.name, connectorIntermediate);
184
250
  }
185
- for (const [i, note] of notesByOriginalIndex.entries()) {
186
- const entity = data.entities[i];
187
- const timescaleGroupIndex = getNum(entity.data, 'timeScaleGroup', -1);
188
- if (timescaleGroupIndex >= 0 && timescaleGroupsByIndex.has(timescaleGroupIndex)) {
189
- note.data['#TIMESCALE_GROUP'] = timescaleGroupsByIndex.get(timescaleGroupIndex);
190
- }
191
- const attachIndex = getNum(entity.data, 'attach', -1);
192
- if (attachIndex > 0) {
193
- const attachConnector = connectorsByOriginalIndex.get(attachIndex);
251
+ const resolveConnector = (ref) => {
252
+ if (ref === undefined)
253
+ return undefined;
254
+ if (typeof ref === 'number')
255
+ return connectorsByIndex.get(ref);
256
+ return connectorsByName.get(ref);
257
+ };
258
+ for (let i = 0; i < flatEntities.length; i++) {
259
+ const entity = flatEntities[i];
260
+ const note = notesByIndex.get(i);
261
+ if (!note)
262
+ continue;
263
+ const attachRef = getEntityRef(entity.data, 'attach');
264
+ if (attachRef !== undefined) {
265
+ const attachConnector = resolveConnector(attachRef);
194
266
  if (attachConnector) {
195
267
  note.data['attachHead'] = attachConnector.data['head'];
196
268
  note.data['attachTail'] = attachConnector.data['tail'];
197
269
  note.data['isAttached'] = 1;
198
270
  }
199
271
  }
200
- const slideIndex = getNum(entity.data, 'slide', -1);
201
- if (slideIndex > 0) {
202
- const slideConnector = connectorsByOriginalIndex.get(slideIndex);
272
+ const slideRef = getEntityRef(entity.data, 'slide');
273
+ if (slideRef !== undefined) {
274
+ const slideConnector = resolveConnector(slideRef);
203
275
  if (slideConnector) {
204
276
  note.data['activeHead'] = slideConnector.data['activeHead'];
205
277
  }
206
278
  }
207
279
  }
208
- for (let i = 0; i < data.entities.length; i++) {
209
- const entity = data.entities[i];
280
+ for (const entity of flatEntities) {
210
281
  if (entity.archetype !== 'SimLine')
211
282
  continue;
212
- const left = notesByOriginalIndex.get(getNum(entity.data, 'a'));
213
- const right = notesByOriginalIndex.get(getNum(entity.data, 'b'));
214
- if (left && right) {
283
+ const left = resolveNote(getEntityRef(entity.data, 'a'));
284
+ const right = resolveNote(getEntityRef(entity.data, 'b'));
285
+ if (left && right)
215
286
  createIntermediate('SimLine', { left, right });
216
- }
217
287
  }
218
288
  const anchorsByBeat = new Map();
219
289
  const anchorPositions = new Map();
220
- const getAnchor = (beat, lane, size, timescaleGroup, pos, segmentKind = -1, segmentAlpha = -1, connectorEase = -1) => {
290
+ const getAnchor = (beat, lane, size, tsg, pos, segmentKind = -1, segmentAlpha = -1, connectorEase = -1) => {
221
291
  const anchors = anchorsByBeat.get(beat);
222
292
  if (anchors) {
223
293
  for (const anchor of anchors) {
@@ -225,7 +295,7 @@ export const extendedToLevelData = (data, offset = 0) => {
225
295
  continue;
226
296
  if (anchor.data['lane'] === lane &&
227
297
  anchor.data['size'] === size &&
228
- anchor.data['#TIMESCALE_GROUP'] === timescaleGroup &&
298
+ anchor.data['#TIMESCALE_GROUP'] === tsg &&
229
299
  (segmentKind === -1 ||
230
300
  anchor.data['segmentKind'] === segmentKind ||
231
301
  anchor.data['segmentKind'] === -1) &&
@@ -246,48 +316,49 @@ export const extendedToLevelData = (data, offset = 0) => {
246
316
  }
247
317
  }
248
318
  }
249
- const anchor = createIntermediate('AnchorNote', {
319
+ const anchorData = {
250
320
  '#BEAT': beat,
251
321
  lane,
252
322
  size,
253
- ...(timescaleGroup !== undefined ? { '#TIMESCALE_GROUP': timescaleGroup } : {}),
254
323
  segmentKind,
255
324
  segmentAlpha,
256
325
  connectorEase,
257
- });
326
+ };
327
+ if (tsg !== undefined)
328
+ anchorData['#TIMESCALE_GROUP'] = tsg;
329
+ const anchor = createIntermediate('AnchorNote', anchorData);
258
330
  if (!anchorsByBeat.has(beat))
259
331
  anchorsByBeat.set(beat, []);
260
332
  anchorsByBeat.get(beat).push(anchor);
261
333
  anchorPositions.set(anchor, new Set([pos]));
262
334
  return anchor;
263
335
  };
264
- for (let i = 0; i < data.entities.length; i++) {
265
- const entity = data.entities[i];
336
+ for (const entity of flatEntities) {
266
337
  if (entity.archetype !== 'Guide')
267
338
  continue;
268
339
  const startBeat = getNum(entity.data, 'startBeat');
269
340
  const startLane = getNum(entity.data, 'startLane');
270
341
  const startSize = getNum(entity.data, 'startSize');
271
- const startTimeScaleGroup = timescaleGroupsByIndex.get(getNum(entity.data, 'startTimeScaleGroup'));
342
+ const startTSG = resolveTimescaleGroup(getEntityRef(entity.data, 'startTimeScaleGroup'));
272
343
  const headBeat = getNum(entity.data, 'headBeat');
273
344
  const headLane = getNum(entity.data, 'headLane');
274
345
  const headSize = getNum(entity.data, 'headSize');
275
- const headTimeScaleGroup = timescaleGroupsByIndex.get(getNum(entity.data, 'headTimeScaleGroup'));
346
+ const headTSG = resolveTimescaleGroup(getEntityRef(entity.data, 'headTimeScaleGroup'));
276
347
  const tailBeat = getNum(entity.data, 'tailBeat');
277
348
  const tailLane = getNum(entity.data, 'tailLane');
278
349
  const tailSize = getNum(entity.data, 'tailSize');
279
- const tailTimeScaleGroup = timescaleGroupsByIndex.get(getNum(entity.data, 'tailTimeScaleGroup'));
350
+ const tailTSG = resolveTimescaleGroup(getEntityRef(entity.data, 'tailTimeScaleGroup'));
280
351
  const endBeat = getNum(entity.data, 'endBeat');
281
352
  const endLane = getNum(entity.data, 'endLane');
282
353
  const endSize = getNum(entity.data, 'endSize');
283
- const endTimeScaleGroup = timescaleGroupsByIndex.get(getNum(entity.data, 'endTimeScaleGroup'));
284
- const ease = easeTypeMapping[getNum(entity.data, 'ease', 0)];
354
+ const endTSG = resolveTimescaleGroup(getEntityRef(entity.data, 'endTimeScaleGroup'));
355
+ const ease = easeTypeMapping[getNum(entity.data, 'ease', 0)] ?? EaseType.LINEAR;
285
356
  const [startAlpha, endAlpha] = fadeAlphaMapping[getNum(entity.data, 'fade', 1)];
286
- const kind = guideKindMapping[getNum(entity.data, 'color', 0)];
287
- const start = getAnchor(startBeat, startLane, startSize, startTimeScaleGroup, 'segmentHead', kind, startAlpha);
288
- const end = getAnchor(endBeat, endLane, endSize, endTimeScaleGroup, 'segmentTail', kind, endAlpha);
289
- const head = getAnchor(headBeat, headLane, headSize, headTimeScaleGroup, 'head', kind, -1, ease);
290
- const tail = getAnchor(tailBeat, tailLane, tailSize, tailTimeScaleGroup, 'tail', kind);
357
+ const kind = guideKindMapping[getNum(entity.data, 'color', 0)] ?? ConnectorKind.GUIDE_NEUTRAL;
358
+ const start = getAnchor(startBeat, startLane, startSize, startTSG, 'segmentHead', kind, startAlpha);
359
+ const end = getAnchor(endBeat, endLane, endSize, endTSG, 'segmentTail', kind, endAlpha);
360
+ const head = getAnchor(headBeat, headLane, headSize, headTSG, 'head', kind, -1, ease);
361
+ const tail = getAnchor(tailBeat, tailLane, tailSize, tailTSG, 'tail', kind);
291
362
  createIntermediate('Connector', { head, tail, segmentHead: start, segmentTail: end });
292
363
  }
293
364
  for (const anchorList of anchorsByBeat.values()) {
@@ -305,9 +376,8 @@ export const extendedToLevelData = (data, offset = 0) => {
305
376
  continue;
306
377
  const head = entity.data['head'];
307
378
  const tail = entity.data['tail'];
308
- if (head && tail) {
379
+ if (head && tail)
309
380
  head.data['next'] = tail;
310
- }
311
381
  }
312
382
  allIntermediateEntities.sort((a, b) => {
313
383
  const isInitA = a.archetype === 'Initialization' ? 0 : 1;
@@ -321,7 +391,7 @@ export const extendedToLevelData = (data, offset = 0) => {
321
391
  const entities = [];
322
392
  const intermediateToRef = new Map();
323
393
  let entityRefCounter = 0;
324
- const getRef = (intermediateEntity) => {
394
+ const getOutputRef = (intermediateEntity) => {
325
395
  let ref = intermediateToRef.get(intermediateEntity);
326
396
  if (ref !== undefined)
327
397
  return ref;
@@ -332,21 +402,18 @@ export const extendedToLevelData = (data, offset = 0) => {
332
402
  for (const intermediateEntity of allIntermediateEntities) {
333
403
  const entity = {
334
404
  archetype: intermediateEntity.archetype,
335
- name: getRef(intermediateEntity),
405
+ name: getOutputRef(intermediateEntity),
336
406
  data: [],
337
407
  };
338
408
  for (const [dataName, dataValue] of Object.entries(intermediateEntity.data)) {
339
409
  if (typeof dataValue === 'number') {
340
410
  entity.data.push({ name: dataName, value: dataValue });
341
411
  }
342
- else if (typeof dataValue === 'string') {
343
- const asNum = Number(dataValue);
344
- if (!isNaN(asNum)) {
345
- entity.data.push({ name: dataName, value: asNum });
346
- }
347
- }
348
412
  else if (dataValue !== undefined && dataValue !== null) {
349
- entity.data.push({ name: dataName, ref: getRef(dataValue) });
413
+ entity.data.push({
414
+ name: dataName,
415
+ ref: getOutputRef(dataValue),
416
+ });
350
417
  }
351
418
  }
352
419
  entities.push(entity);
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ import { extendedToLevelData, type ExtendedEntityData, type ExtendedLevelData }
8
8
  export { susToUSC, mmwsToUSC, uscToLevelData, ucmmwsToLevelData, extendedToLevelData, type ExtendedEntityData, type ExtendedLevelData, };
9
9
  export * from './usc/index.js';
10
10
  export declare const convertToLevelData: (input: string | Uint8Array | USC | LevelData, offset?: number) => LevelData;
11
- export declare const version = "1.5.8";
11
+ export declare const version = "1.5.9";
12
12
  export declare const databaseEngineItem: {
13
13
  readonly name: "next-rush-plus";
14
14
  readonly version: 13;
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ export const convertToLevelData = (input, offset = 0) => {
60
60
  }
61
61
  return uscToLevelData(usc, offset, true, true);
62
62
  };
63
- export const version = '1.5.8';
63
+ export const version = '1.5.9';
64
64
  export const databaseEngineItem = {
65
65
  name: 'next-rush-plus',
66
66
  version: 13,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonolus-next-rush-plus-engine",
3
- "version": "1.5.8",
3
+ "version": "1.5.9",
4
4
  "description": "A new Project Sekai inspired engine for Sonolus",
5
5
  "author": "Hyeon2",
6
6
  "repository": {