sonolus-next-rush-plus-engine 1.3.7 → 1.3.8

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
package/dist/index.d.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { LevelData } from '@sonolus/core';
2
2
  import { mmwsToUSC } from './mmw/convert.js';
3
+ import { ucmmwsToLevelData } from './mmw/convert.js';
3
4
  import { susToUSC } from './sus/convert.js';
4
5
  import { uscToLevelData } from './usc/convert.js';
5
6
  import { USC } from './usc/index.js';
6
- export { susToUSC, mmwsToUSC, uscToLevelData };
7
+ export { susToUSC, mmwsToUSC, uscToLevelData, ucmmwsToLevelData };
7
8
  export * from './usc/index.js';
8
9
  export declare const convertToLevelData: (input: string | Uint8Array | USC | LevelData, offset?: number) => LevelData;
9
- export declare const version = "1.3.7";
10
+ export declare const version = "1.3.8";
10
11
  export declare const databaseEngineItem: {
11
12
  readonly name: "next-rush-plus";
12
13
  readonly version: 13;
package/dist/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { detectMMWSType } from './mmw/analyze.js';
2
2
  import { mmwsToUSC } from './mmw/convert.js';
3
+ import { ucmmwsToLevelData } from './mmw/convert.js';
3
4
  import { susToUSC } from './sus/convert.js';
4
5
  import { uscToLevelData } from './usc/convert.js';
5
6
  import { isUSC } from './usc/analyze.js';
6
7
  import { isLevelData } from './LevelData/analyze.js';
7
8
  import { isPJSK } from './pjsk/analyze.js';
8
9
  import { pjskToUSC } from './pjsk/convert.js';
9
- export { susToUSC, mmwsToUSC, uscToLevelData };
10
+ export { susToUSC, mmwsToUSC, uscToLevelData, ucmmwsToLevelData };
10
11
  export * from './usc/index.js';
11
12
  export const convertToLevelData = (input, offset = 0) => {
12
13
  if (isLevelData(input)) {
@@ -17,11 +18,20 @@ export const convertToLevelData = (input, offset = 0) => {
17
18
  usc = input;
18
19
  }
19
20
  else if (input instanceof Uint8Array) {
20
- usc = mmwsToUSC(input);
21
- if (detectMMWSType(input) == 'MMWS')
22
- return uscToLevelData(usc, offset, true, true);
23
- else if (detectMMWSType(input) == 'CCMMWS')
24
- return uscToLevelData(usc, offset, false, false);
21
+ const type = detectMMWSType(input);
22
+ if (type === 'UCMMWS') {
23
+ return ucmmwsToLevelData(input);
24
+ }
25
+ if (type === 'MMWS' || type === 'CCMMWS') {
26
+ usc = mmwsToUSC(input);
27
+ if (type === 'MMWS') {
28
+ return uscToLevelData(usc, offset, true, true);
29
+ }
30
+ else {
31
+ return uscToLevelData(usc, offset, false, false);
32
+ }
33
+ }
34
+ throw new Error('Unsupported Uint8Array type for MMWS');
25
35
  }
26
36
  else if (typeof input === 'string') {
27
37
  try {
@@ -49,7 +59,7 @@ export const convertToLevelData = (input, offset = 0) => {
49
59
  }
50
60
  return uscToLevelData(usc, offset, true, true);
51
61
  };
52
- export const version = '1.3.7';
62
+ export const version = '1.3.8';
53
63
  export const databaseEngineItem = {
54
64
  name: 'next-rush-plus',
55
65
  version: 13,
@@ -1,4 +1,4 @@
1
- declare const FlickType: readonly ["none", "up", "left", "right"];
1
+ declare const FlickType: readonly ["none", "up", "left", "right", "down", "down_left", "down_right"];
2
2
  type FlickType = (typeof FlickType)[number];
3
3
  declare const StepType: readonly ["visible", "hidden", "ignored"];
4
4
  type StepType = (typeof StepType)[number];
@@ -27,6 +27,9 @@ export type Score = {
27
27
  tick: number;
28
28
  speed: number;
29
29
  layer: number;
30
+ skip?: number;
31
+ ease?: number;
32
+ hideNotes?: boolean;
30
33
  }[];
31
34
  skills: number[];
32
35
  fever: {
@@ -42,6 +45,7 @@ export type Score = {
42
45
  flags: {
43
46
  critical: boolean;
44
47
  friction: boolean;
48
+ dummy?: boolean;
45
49
  };
46
50
  layer: number;
47
51
  }[];
@@ -50,6 +54,7 @@ export type Score = {
50
54
  startHidden: boolean;
51
55
  endHidden: boolean;
52
56
  guide: boolean;
57
+ dummy?: boolean;
53
58
  };
54
59
  start: {
55
60
  tick: number;
@@ -92,6 +97,7 @@ export type Score = {
92
97
  flags: {
93
98
  critical: boolean;
94
99
  friction: boolean;
100
+ dummy?: boolean;
95
101
  };
96
102
  layer: number;
97
103
  }[];
@@ -1,5 +1,5 @@
1
1
  import { BinarySeeker } from '../lib/binaryseeker/mod.js';
2
- const FlickType = ['none', 'up', 'left', 'right'];
2
+ const FlickType = ['none', 'up', 'left', 'right', 'down', 'down_left', 'down_right'];
3
3
  const StepType = ['visible', 'hidden', 'ignored'];
4
4
  const EaseType = ['linear', 'easeIn', 'easeOut', 'easeInOut', 'easeOutIn'];
5
5
  /** Detect MMWS Type */
@@ -11,16 +11,27 @@ export const detectMMWSType = (mmws) => {
11
11
  export const analyze = (mmws) => {
12
12
  const buffer = new BinarySeeker(mmws.buffer);
13
13
  const header = buffer.readString();
14
- if (header !== 'MMWS' && header !== 'CCMMWS') {
14
+ if (header !== 'MMWS' && header !== 'CCMMWS' && header !== 'UCMMWS') {
15
15
  throw new Error('Invalid MMWS file');
16
16
  }
17
- const version = buffer.readInt16LE();
18
- let cyanvasVersion = buffer.readInt16LE();
19
- if (version < 3) {
20
- throw new Error('Unsupported MMWS version');
17
+ let version = 0;
18
+ let cyanvasVersion = 0;
19
+ let untitledVersion = 0;
20
+ if (header === 'UCMMWS') {
21
+ untitledVersion = buffer.readInt32LE();
22
+ cyanvasVersion = 6;
23
+ version = 4;
24
+ }
25
+ else if (header === 'CCMMWS') {
26
+ cyanvasVersion = Math.max(buffer.readInt16LE(), 1);
27
+ version = buffer.readInt16LE();
21
28
  }
22
- if (header === 'CCMMWS' && cyanvasVersion === 0) {
23
- cyanvasVersion = 1;
29
+ else {
30
+ version = buffer.readInt16LE();
31
+ cyanvasVersion = buffer.readInt16LE();
32
+ }
33
+ if (version < 3 && header !== 'UCMMWS') {
34
+ throw new Error('Unsupported MMWS version');
24
35
  }
25
36
  const metadataPointer = buffer.readUInt32LE();
26
37
  const eventsPointer = buffer.readUInt32LE();
@@ -28,7 +39,6 @@ export const analyze = (mmws) => {
28
39
  const holdsPointer = buffer.readUInt32LE();
29
40
  const damagesPointer = cyanvasVersion > 0 ? buffer.readUInt32LE() : 0;
30
41
  const layersPointer = cyanvasVersion >= 4 ? buffer.readUInt32LE() : 0;
31
- // const waypointsPointer = cyanvasVersion >= 5 ? buffer.readUInt32LE() : 0
32
42
  buffer.seek(metadataPointer);
33
43
  const metadata = {
34
44
  title: buffer.readString(),
@@ -36,8 +46,11 @@ export const analyze = (mmws) => {
36
46
  artist: buffer.readString(),
37
47
  musicFile: buffer.readString(),
38
48
  musicOffset: buffer.readFloat32LE(),
39
- jacketFile: buffer.readString(),
49
+ jacketFile: version >= 2 ? buffer.readString() : '',
40
50
  };
51
+ if (cyanvasVersion >= 1) {
52
+ buffer.readInt32LE();
53
+ }
41
54
  buffer.seek(eventsPointer);
42
55
  const events = {
43
56
  timeSignatures: [],
@@ -64,29 +77,49 @@ export const analyze = (mmws) => {
64
77
  bpm: buffer.readFloat32LE(),
65
78
  });
66
79
  }
67
- const hispeedChangesCount = buffer.readInt32LE();
68
- for (let i = 0; i < hispeedChangesCount; i++) {
69
- events.hispeedChanges.push({
70
- tick: buffer.readInt32LE(),
71
- speed: buffer.readFloat32LE(),
72
- layer: cyanvasVersion >= 4 ? buffer.readInt32LE() : 0,
73
- });
80
+ if (version >= 3) {
81
+ const hispeedChangesCount = buffer.readInt32LE();
82
+ for (let i = 0; i < hispeedChangesCount; i++) {
83
+ const tick = buffer.readInt32LE();
84
+ const speed = buffer.readFloat32LE();
85
+ const layer = cyanvasVersion >= 4 ? buffer.readInt32LE() : 0;
86
+ let skip = 0;
87
+ let ease = 0;
88
+ let hideNotes = false;
89
+ if (untitledVersion >= 2) {
90
+ skip = buffer.readFloat32LE();
91
+ ease = buffer.readInt16LE();
92
+ hideNotes = !!buffer.readInt16LE();
93
+ }
94
+ events.hispeedChanges.push({
95
+ tick,
96
+ speed,
97
+ layer,
98
+ skip,
99
+ ease,
100
+ hideNotes,
101
+ });
102
+ }
74
103
  }
75
- const skillsCount = buffer.readInt32LE();
76
- for (let i = 0; i < skillsCount; i++) {
77
- events.skills.push(buffer.readInt32LE());
104
+ if (version >= 2) {
105
+ const skillsCount = buffer.readInt32LE();
106
+ for (let i = 0; i < skillsCount; i++) {
107
+ const skillTick = buffer.readInt32LE();
108
+ events.skills.push(skillTick);
109
+ }
110
+ events.fever.start = buffer.readInt32LE();
111
+ events.fever.end = buffer.readInt32LE();
78
112
  }
79
- events.fever.start = buffer.readInt32LE();
80
- events.fever.end = buffer.readInt32LE();
81
113
  buffer.seek(tapsPointer);
82
114
  const taps = [];
83
115
  const tapsCount = buffer.readInt32LE();
84
116
  for (let i = 0; i < tapsCount; i++) {
85
117
  const tick = buffer.readInt32LE();
86
- const lane = buffer.readInt32LE();
87
- const width = buffer.readInt32LE();
118
+ const lane = cyanvasVersion >= 6 ? buffer.readFloat32LE() : buffer.readInt32LE();
119
+ const width = cyanvasVersion >= 6 ? buffer.readFloat32LE() : buffer.readInt32LE();
88
120
  const layer = cyanvasVersion >= 4 ? buffer.readInt32LE() : 0;
89
- const flickType = FlickType[buffer.readInt32LE()];
121
+ const flickTypeRaw = buffer.readInt32LE();
122
+ const flickType = FlickType[flickTypeRaw] || 'none';
90
123
  const flags = buffer.readInt32LE();
91
124
  taps.push({
92
125
  tick,
@@ -96,6 +129,7 @@ export const analyze = (mmws) => {
96
129
  flags: {
97
130
  critical: !!(flags & (1 << 0)),
98
131
  friction: !!(flags & (1 << 1)),
132
+ dummy: cyanvasVersion >= 6 && untitledVersion >= 1 ? !!(flags & (1 << 2)) : false,
99
133
  },
100
134
  layer,
101
135
  });
@@ -106,8 +140,8 @@ export const analyze = (mmws) => {
106
140
  for (let i = 0; i < holdsCount; i++) {
107
141
  const holdFlags = version >= 4 ? buffer.readInt32LE() : 0;
108
142
  const startTick = buffer.readInt32LE();
109
- const startLane = buffer.readInt32LE();
110
- const startWidth = buffer.readInt32LE();
143
+ const startLane = cyanvasVersion >= 6 ? buffer.readFloat32LE() : buffer.readInt32LE();
144
+ const startWidth = cyanvasVersion >= 6 ? buffer.readFloat32LE() : buffer.readInt32LE();
111
145
  const startLayer = cyanvasVersion >= 4 ? buffer.readInt32LE() : 0;
112
146
  const startFlags = buffer.readInt32LE();
113
147
  const startEase = EaseType[buffer.readInt32LE()];
@@ -117,10 +151,10 @@ export const analyze = (mmws) => {
117
151
  const steps = [];
118
152
  for (let j = 0; j < stepsCount; j++) {
119
153
  const tick = buffer.readInt32LE();
120
- const lane = buffer.readInt32LE();
121
- const width = buffer.readInt32LE();
154
+ const lane = cyanvasVersion >= 6 ? buffer.readFloat32LE() : buffer.readInt32LE();
155
+ const width = cyanvasVersion >= 6 ? buffer.readFloat32LE() : buffer.readInt32LE();
122
156
  const layer = cyanvasVersion >= 4 ? buffer.readInt32LE() : 0;
123
- buffer.readInt32LE(); // unused flags
157
+ buffer.readInt32LE();
124
158
  const type = StepType[buffer.readInt32LE()];
125
159
  const ease = EaseType[buffer.readInt32LE()];
126
160
  steps.push({
@@ -133,16 +167,18 @@ export const analyze = (mmws) => {
133
167
  });
134
168
  }
135
169
  const endTick = buffer.readInt32LE();
136
- const endLane = buffer.readInt32LE();
137
- const endWidth = buffer.readInt32LE();
170
+ const endLane = cyanvasVersion >= 6 ? buffer.readFloat32LE() : buffer.readInt32LE();
171
+ const endWidth = cyanvasVersion >= 6 ? buffer.readFloat32LE() : buffer.readInt32LE();
138
172
  const endLayer = cyanvasVersion >= 4 ? buffer.readInt32LE() : 0;
139
- const endFlickType = FlickType[buffer.readInt32LE()];
173
+ const endFlickTypeRaw = buffer.readInt32LE();
174
+ const endFlickType = FlickType[endFlickTypeRaw] || 'none';
140
175
  const endFlags = buffer.readInt32LE();
141
176
  holds.push({
142
177
  flags: {
143
178
  startHidden: !!(holdFlags & (1 << 0)),
144
179
  endHidden: !!(holdFlags & (1 << 1)),
145
180
  guide: !!(holdFlags & (1 << 2)),
181
+ dummy: !!(holdFlags & (1 << 3)),
146
182
  },
147
183
  start: {
148
184
  tick: startTick,
@@ -183,10 +219,11 @@ export const analyze = (mmws) => {
183
219
  const damagesCount = buffer.readInt32LE();
184
220
  for (let i = 0; i < damagesCount; i++) {
185
221
  const tick = buffer.readInt32LE();
186
- const lane = buffer.readInt32LE();
187
- const width = buffer.readInt32LE();
222
+ const lane = cyanvasVersion >= 6 ? buffer.readFloat32LE() : buffer.readInt32LE();
223
+ const width = cyanvasVersion >= 6 ? buffer.readFloat32LE() : buffer.readInt32LE();
188
224
  const layer = cyanvasVersion >= 4 ? buffer.readInt32LE() : 0;
189
- const flickType = FlickType[buffer.readInt32LE()];
225
+ const flickTypeRaw = buffer.readInt32LE();
226
+ const flickType = FlickType[flickTypeRaw] || 'none';
190
227
  const flags = buffer.readInt32LE();
191
228
  damages.push({
192
229
  tick,
@@ -197,6 +234,7 @@ export const analyze = (mmws) => {
197
234
  flags: {
198
235
  critical: !!(flags & (1 << 0)),
199
236
  friction: !!(flags & (1 << 1)),
237
+ dummy: cyanvasVersion >= 6 && untitledVersion >= 1 ? !!(flags & (1 << 2)) : false,
200
238
  },
201
239
  });
202
240
  }
@@ -1,4 +1,5 @@
1
1
  import { USC } from '../usc/index.js';
2
+ import { type LevelData } from '@sonolus/core';
2
3
  /**
3
4
  * Convert MMWS or CCMMWS to a USC
4
5
  */
@@ -7,3 +8,7 @@ export declare const mmwsToUSC: (mmws: Uint8Array) => USC;
7
8
  * Convert CCMMWS to a USC
8
9
  */
9
10
  export declare const ccmmwsToUSC: (mmws: Uint8Array) => USC;
11
+ /**
12
+ * Convert UCMMWS to a LevelData
13
+ */
14
+ export declare const ucmmwsToLevelData: (mmws: Uint8Array) => LevelData;
@@ -1,5 +1,57 @@
1
1
  import { USCColor, } from '../usc/index.js';
2
2
  import { analyze } from './analyze.js';
3
+ const SONOLUS_CONNECTOR_EASES = {
4
+ outin: 5,
5
+ out: 3,
6
+ linear: 1,
7
+ in: 2,
8
+ inout: 4,
9
+ };
10
+ const mmwEaseToSonolusEase = (ease) => {
11
+ switch (ease) {
12
+ case 'linear':
13
+ return SONOLUS_CONNECTOR_EASES.linear;
14
+ case 'easeIn':
15
+ return SONOLUS_CONNECTOR_EASES.in;
16
+ case 'easeOut':
17
+ return SONOLUS_CONNECTOR_EASES.out;
18
+ case 'easeInOut':
19
+ return SONOLUS_CONNECTOR_EASES.inout;
20
+ case 'easeOutIn':
21
+ return SONOLUS_CONNECTOR_EASES.outin;
22
+ default:
23
+ return SONOLUS_CONNECTOR_EASES.linear;
24
+ }
25
+ };
26
+ const EPSILON = 1e-6;
27
+ const TICKS_PER_BEAT = 480;
28
+ const laneToSonolusLane = (lane, width) => {
29
+ return lane - 6 + width / 2;
30
+ };
31
+ const flickTypeToDirection = (type) => {
32
+ switch (type) {
33
+ case 'up':
34
+ return 0;
35
+ case 'left':
36
+ return 1;
37
+ case 'right':
38
+ return 2;
39
+ case 'down':
40
+ return 3;
41
+ case 'down_left':
42
+ return 4;
43
+ case 'down_right':
44
+ return 5;
45
+ default:
46
+ return 0;
47
+ }
48
+ };
49
+ const resolveConnectorKind = (isCritical, isDummy) => {
50
+ if (isDummy) {
51
+ return isCritical ? 52 : 51;
52
+ }
53
+ return isCritical ? 1 : 0;
54
+ };
3
55
  const mmwsEaseToUSCEase = {
4
56
  linear: 'linear',
5
57
  easeOut: 'out',
@@ -7,7 +59,6 @@ const mmwsEaseToUSCEase = {
7
59
  easeInOut: 'inout',
8
60
  easeOutIn: 'outin',
9
61
  };
10
- const ticksPerBeat = 480;
11
62
  const laneToUSCLane = ({ lane, width }) => {
12
63
  return lane - 6 + width / 2;
13
64
  };
@@ -23,7 +74,7 @@ export const mmwsToUSC = (mmws) => {
23
74
  for (const bpmChange of score.events.bpmChanges) {
24
75
  usc.objects.push({
25
76
  type: 'bpm',
26
- beat: bpmChange.tick / ticksPerBeat,
77
+ beat: bpmChange.tick / TICKS_PER_BEAT,
27
78
  bpm: bpmChange.bpm,
28
79
  });
29
80
  }
@@ -34,10 +85,16 @@ export const mmwsToUSC = (mmws) => {
34
85
  for (const hispeedChange of score.events.hispeedChanges) {
35
86
  const key = hispeedChange.layer;
36
87
  if (!tsGroups.has(key)) {
37
- throw new Error('Invalid layer');
88
+ if (!tsGroups.has(0))
89
+ tsGroups.set(0, []);
90
+ tsGroups.get(0).push({
91
+ beat: hispeedChange.tick / TICKS_PER_BEAT,
92
+ timeScale: hispeedChange.speed,
93
+ });
94
+ continue;
38
95
  }
39
96
  tsGroups.get(key).push({
40
- beat: hispeedChange.tick / ticksPerBeat,
97
+ beat: hispeedChange.tick / TICKS_PER_BEAT,
41
98
  timeScale: hispeedChange.speed,
42
99
  });
43
100
  }
@@ -50,14 +107,14 @@ export const mmwsToUSC = (mmws) => {
50
107
  for (const tap of score.taps) {
51
108
  const uscTap = {
52
109
  type: 'single',
53
- beat: tap.tick / ticksPerBeat,
110
+ beat: tap.tick / TICKS_PER_BEAT,
54
111
  timeScaleGroup: tap.layer,
55
112
  critical: tap.flags.critical,
56
113
  lane: laneToUSCLane(tap),
57
114
  size: tap.width / 2,
58
115
  trace: tap.flags.friction,
59
116
  };
60
- if (tap.flickType !== 'none') {
117
+ if (tap.flickType === 'up' || tap.flickType === 'left' || tap.flickType === 'right') {
61
118
  uscTap.direction = tap.flickType;
62
119
  }
63
120
  usc.objects.push(uscTap);
@@ -65,7 +122,7 @@ export const mmwsToUSC = (mmws) => {
65
122
  for (const hold of score.holds) {
66
123
  const uscStartNote = {
67
124
  type: 'start',
68
- beat: hold.start.tick / ticksPerBeat,
125
+ beat: hold.start.tick / TICKS_PER_BEAT,
69
126
  timeScaleGroup: hold.start.layer,
70
127
  critical: hold.start.flags.critical,
71
128
  ease: mmwsEaseToUSCEase[hold.start.ease],
@@ -79,14 +136,16 @@ export const mmwsToUSC = (mmws) => {
79
136
  };
80
137
  const uscEndNote = {
81
138
  type: 'end',
82
- beat: hold.end.tick / ticksPerBeat,
139
+ beat: hold.end.tick / TICKS_PER_BEAT,
83
140
  timeScaleGroup: hold.end.layer,
84
141
  critical: hold.end.flags.critical,
85
142
  lane: laneToUSCLane(hold.end),
86
143
  size: hold.end.width / 2,
87
144
  judgeType: hold.flags.endHidden ? 'none' : hold.end.flags.friction ? 'trace' : 'normal',
88
145
  };
89
- if (hold.end.flickType !== 'none') {
146
+ if (hold.end.flickType === 'up' ||
147
+ hold.end.flickType === 'left' ||
148
+ hold.end.flickType === 'right') {
90
149
  uscEndNote.direction = hold.end.flickType;
91
150
  }
92
151
  if (hold.flags.guide) {
@@ -95,7 +154,7 @@ export const mmwsToUSC = (mmws) => {
95
154
  fade: hold.fadeType === 0 ? 'out' : hold.fadeType === 1 ? 'none' : 'in',
96
155
  color: Object.entries(USCColor).find(([, i]) => i === hold.guideColor)[0],
97
156
  midpoints: [hold.start, ...hold.steps, hold.end].map((step) => ({
98
- beat: step.tick / ticksPerBeat,
157
+ beat: step.tick / TICKS_PER_BEAT,
99
158
  lane: laneToUSCLane(step),
100
159
  size: step.width / 2,
101
160
  timeScaleGroup: step.layer,
@@ -111,7 +170,7 @@ export const mmwsToUSC = (mmws) => {
111
170
  connections: [
112
171
  uscStartNote,
113
172
  ...hold.steps.map((step) => {
114
- const beat = step.tick / ticksPerBeat;
173
+ const beat = step.tick / TICKS_PER_BEAT;
115
174
  const lane = laneToUSCLane(step);
116
175
  const size = step.width / 2;
117
176
  if (step.type === 'ignored') {
@@ -146,7 +205,7 @@ export const mmwsToUSC = (mmws) => {
146
205
  for (const damage of score.damages) {
147
206
  const uscDamage = {
148
207
  type: 'damage',
149
- beat: damage.tick / ticksPerBeat,
208
+ beat: damage.tick / TICKS_PER_BEAT,
150
209
  timeScaleGroup: damage.layer,
151
210
  lane: laneToUSCLane(damage),
152
211
  size: damage.width / 2,
@@ -159,3 +218,464 @@ export const mmwsToUSC = (mmws) => {
159
218
  * Convert CCMMWS to a USC
160
219
  */
161
220
  export const ccmmwsToUSC = mmwsToUSC;
221
+ /**
222
+ * Convert UCMMWS to a LevelData
223
+ */
224
+ export const ucmmwsToLevelData = (mmws) => {
225
+ const score = analyze(mmws);
226
+ const entities = [];
227
+ const allIntermediateEntities = [];
228
+ const simLineEligibleNotes = [];
229
+ const timeScaleGroupIntermediates = [];
230
+ const createIntermediate = (archetype, data, isSimEligible = false) => {
231
+ const intermediateEntity = { archetype, data };
232
+ allIntermediateEntities.push(intermediateEntity);
233
+ if (isSimEligible) {
234
+ simLineEligibleNotes.push(intermediateEntity);
235
+ }
236
+ return intermediateEntity;
237
+ };
238
+ createIntermediate('Initialization', {});
239
+ createIntermediate('Stage', {});
240
+ if (score.events.bpmChanges.length === 0) {
241
+ createIntermediate('#BPM_CHANGE', { '#BEAT': 0, '#BPM': 120 });
242
+ }
243
+ for (const bpm of score.events.bpmChanges) {
244
+ createIntermediate('#BPM_CHANGE', {
245
+ '#BEAT': bpm.tick / TICKS_PER_BEAT,
246
+ '#BPM': bpm.bpm,
247
+ });
248
+ }
249
+ for (const skillTick of score.events.skills) {
250
+ createIntermediate('Skill', { '#BEAT': skillTick / TICKS_PER_BEAT });
251
+ }
252
+ if (score.events.fever.start > 0) {
253
+ createIntermediate('FeverChance', { '#BEAT': score.events.fever.start / TICKS_PER_BEAT });
254
+ }
255
+ if (score.events.fever.end > 0) {
256
+ createIntermediate('FeverStart', { '#BEAT': score.events.fever.end / TICKS_PER_BEAT });
257
+ }
258
+ const layerChanges = new Map();
259
+ for (let i = 0; i < Math.max(1, score.numLayers); i++) {
260
+ layerChanges.set(i, []);
261
+ }
262
+ for (const hs of score.events.hispeedChanges) {
263
+ if (!layerChanges.has(hs.layer)) {
264
+ if (!layerChanges.has(0))
265
+ layerChanges.set(0, []);
266
+ layerChanges.get(0).push(hs);
267
+ continue;
268
+ }
269
+ layerChanges.get(hs.layer).push(hs);
270
+ }
271
+ for (let i = 0; i < Math.max(1, score.numLayers); i++) {
272
+ const changes = layerChanges.get(i) || [];
273
+ if (changes.length === 0 || changes[0].tick !== 0) {
274
+ changes.unshift({
275
+ tick: 0,
276
+ speed: 1,
277
+ layer: i,
278
+ skip: 0,
279
+ ease: 0,
280
+ hideNotes: false,
281
+ });
282
+ }
283
+ changes.sort((a, b) => a.tick - b.tick);
284
+ const groupIntermediate = createIntermediate('#TIMESCALE_GROUP', {});
285
+ timeScaleGroupIntermediates[i] = groupIntermediate;
286
+ let lastChangeIntermediate = null;
287
+ for (const hs of changes) {
288
+ const data = {
289
+ '#BEAT': hs.tick / TICKS_PER_BEAT,
290
+ '#TIMESCALE': hs.speed === 0 ? 0.000001 : hs.speed,
291
+ '#TIMESCALE_SKIP': hs.skip || 0,
292
+ '#TIMESCALE_EASE': hs.ease || 0,
293
+ '#TIMESCALE_GROUP': groupIntermediate,
294
+ };
295
+ if (hs.hideNotes) {
296
+ data['hideNotes'] = 1;
297
+ }
298
+ const newChangeIntermediate = createIntermediate('#TIMESCALE_CHANGE', data);
299
+ if (lastChangeIntermediate === null) {
300
+ groupIntermediate.data['first'] = newChangeIntermediate;
301
+ }
302
+ else {
303
+ lastChangeIntermediate.data['next'] = newChangeIntermediate;
304
+ }
305
+ lastChangeIntermediate = newChangeIntermediate;
306
+ }
307
+ }
308
+ for (const tap of score.taps) {
309
+ const name_parts = [];
310
+ name_parts.push(tap.flags.critical ? 'Critical' : 'Normal');
311
+ if (tap.flickType !== 'none') {
312
+ name_parts.push(tap.flags.friction ? 'TraceFlick' : 'Flick');
313
+ }
314
+ else {
315
+ name_parts.push(tap.flags.friction ? 'Trace' : 'Tap');
316
+ }
317
+ name_parts.push('Note');
318
+ const archetype = tap.flags.dummy ? `Fake${name_parts.join('')}` : name_parts.join('');
319
+ const timeScaleGroupRef = timeScaleGroupIntermediates[tap.layer] || timeScaleGroupIntermediates[0];
320
+ const data = {
321
+ '#BEAT': tap.tick / TICKS_PER_BEAT,
322
+ lane: laneToSonolusLane(tap.lane, tap.width),
323
+ size: tap.width,
324
+ isAttached: 0,
325
+ connectorEase: SONOLUS_CONNECTOR_EASES.linear,
326
+ isSeparator: 0,
327
+ segmentKind: tap.flags.critical ? 2 : 1,
328
+ segmentAlpha: 1,
329
+ '#TIMESCALE_GROUP': timeScaleGroupRef,
330
+ };
331
+ if (tap.flickType !== 'none') {
332
+ data['direction'] = flickTypeToDirection(tap.flickType);
333
+ }
334
+ createIntermediate(archetype, data, true);
335
+ }
336
+ for (const damage of score.damages) {
337
+ const archetype = damage.flags.dummy ? 'FakeDamageNote' : 'DamageNote';
338
+ const timeScaleGroupRef = timeScaleGroupIntermediates[damage.layer] || timeScaleGroupIntermediates[0];
339
+ createIntermediate(archetype, {
340
+ '#BEAT': damage.tick / TICKS_PER_BEAT,
341
+ lane: laneToSonolusLane(damage.lane, damage.width),
342
+ size: damage.width,
343
+ direction: 0,
344
+ isAttached: 0,
345
+ connectorEase: SONOLUS_CONNECTOR_EASES.linear,
346
+ isSeparator: 0,
347
+ segmentKind: 1,
348
+ segmentAlpha: 1,
349
+ '#TIMESCALE_GROUP': timeScaleGroupRef,
350
+ });
351
+ }
352
+ for (const hold of score.holds) {
353
+ const timeScaleGroupRef = timeScaleGroupIntermediates[hold.start.layer] || timeScaleGroupIntermediates[0];
354
+ if (hold.flags.guide) {
355
+ const points = [hold.start, ...hold.steps, hold.end];
356
+ points.sort((a, b) => a.tick - b.tick);
357
+ let prevMidpointIntermediate = null;
358
+ let headMidpointIntermediate = null;
359
+ const guideConnectors = [];
360
+ const stepSize = Math.max(1, points.length - 1);
361
+ let stepIdx = 0;
362
+ for (const point of points) {
363
+ let segmentAlpha = 1;
364
+ if (hold.fadeType === 0) {
365
+ segmentAlpha = 1 - 0.8 * (stepIdx / stepSize);
366
+ }
367
+ else if (hold.fadeType === 2) {
368
+ segmentAlpha = 1 - 0.8 * ((stepSize - stepIdx) / stepSize);
369
+ }
370
+ const sonolusColorId = 101 + hold.guideColor;
371
+ const midpointIntermediate = createIntermediate('AnchorNote', {
372
+ '#BEAT': point.tick / TICKS_PER_BEAT,
373
+ lane: laneToSonolusLane(point.lane, point.width),
374
+ size: point.width,
375
+ direction: 0,
376
+ isAttached: 0,
377
+ connectorEase: 'ease' in point
378
+ ? mmwEaseToSonolusEase(point.ease)
379
+ : SONOLUS_CONNECTOR_EASES.linear,
380
+ isSeparator: 0,
381
+ segmentKind: sonolusColorId,
382
+ segmentAlpha: segmentAlpha,
383
+ segmentLayer: 0,
384
+ '#TIMESCALE_GROUP': timeScaleGroupRef,
385
+ });
386
+ if (headMidpointIntermediate === null) {
387
+ headMidpointIntermediate = midpointIntermediate;
388
+ }
389
+ if (prevMidpointIntermediate !== null) {
390
+ const connectorIntermediate = createIntermediate('Connector', {
391
+ head: prevMidpointIntermediate,
392
+ tail: midpointIntermediate,
393
+ });
394
+ guideConnectors.push(connectorIntermediate);
395
+ prevMidpointIntermediate.data['next'] = midpointIntermediate;
396
+ }
397
+ prevMidpointIntermediate = midpointIntermediate;
398
+ stepIdx++;
399
+ }
400
+ if (headMidpointIntermediate && prevMidpointIntermediate) {
401
+ for (const conn of guideConnectors) {
402
+ conn.data['segmentHead'] = headMidpointIntermediate;
403
+ conn.data['segmentTail'] = prevMidpointIntermediate;
404
+ conn.data['activeHead'] = headMidpointIntermediate;
405
+ conn.data['activeTail'] = prevMidpointIntermediate;
406
+ }
407
+ if (hold.fadeType === 2) {
408
+ headMidpointIntermediate.data['segmentAlpha'] = 0;
409
+ }
410
+ else if (hold.fadeType === 0) {
411
+ prevMidpointIntermediate.data['segmentAlpha'] = 0;
412
+ }
413
+ }
414
+ }
415
+ else {
416
+ let prevJointIntermediate = null;
417
+ let prevNoteIntermediate = null;
418
+ let headNoteIntermediate = null;
419
+ let currentSegmentHead = null;
420
+ const queuedAttachIntermediates = [];
421
+ const connectors = [];
422
+ const pendingSegmentConnectors = [];
423
+ const events = [];
424
+ events.push({
425
+ tick: hold.start.tick,
426
+ lane: hold.start.lane,
427
+ width: hold.start.width,
428
+ type: 'start',
429
+ critical: hold.start.flags.critical,
430
+ friction: hold.start.flags.friction,
431
+ flick: 'none',
432
+ ease: hold.start.ease,
433
+ });
434
+ for (const step of hold.steps) {
435
+ if (step.type === 'ignored') {
436
+ events.push({
437
+ tick: step.tick,
438
+ lane: step.lane,
439
+ width: step.width,
440
+ type: 'attach',
441
+ critical: hold.start.flags.critical,
442
+ friction: false,
443
+ flick: 'none',
444
+ ease: step.ease,
445
+ });
446
+ }
447
+ else {
448
+ if (step.type === 'visible') {
449
+ events.push({
450
+ tick: step.tick,
451
+ lane: step.lane,
452
+ width: step.width,
453
+ type: 'tick',
454
+ critical: hold.start.flags.critical,
455
+ friction: false,
456
+ flick: 'none',
457
+ ease: step.ease,
458
+ });
459
+ }
460
+ else {
461
+ events.push({
462
+ tick: step.tick,
463
+ lane: step.lane,
464
+ width: step.width,
465
+ type: 'attach',
466
+ critical: hold.start.flags.critical,
467
+ friction: false,
468
+ flick: 'none',
469
+ ease: step.ease,
470
+ });
471
+ }
472
+ }
473
+ }
474
+ events.push({
475
+ tick: hold.end.tick,
476
+ lane: hold.end.lane,
477
+ width: hold.end.width,
478
+ type: 'end',
479
+ critical: hold.end.flags.critical,
480
+ friction: hold.end.flags.friction,
481
+ flick: hold.end.flickType,
482
+ });
483
+ events.sort((a, b) => a.tick - b.tick);
484
+ let nextHiddenTickBeat = Math.floor((hold.start.tick / TICKS_PER_BEAT) * 2 + 1) / 2;
485
+ for (const event of events) {
486
+ let isSimLineEligible = false;
487
+ let isAttached = false;
488
+ const name_parts = [];
489
+ if (event.type === 'start') {
490
+ if (hold.flags.startHidden) {
491
+ name_parts.push('Anchor');
492
+ }
493
+ else {
494
+ name_parts.push(event.critical ? 'Critical' : 'Normal');
495
+ name_parts.push('Head');
496
+ name_parts.push(event.friction ? 'Trace' : 'Tap');
497
+ isSimLineEligible = true;
498
+ }
499
+ }
500
+ else if (event.type === 'end') {
501
+ if (hold.flags.endHidden) {
502
+ name_parts.push('Anchor');
503
+ }
504
+ else {
505
+ name_parts.push(event.critical ? 'Critical' : 'Normal');
506
+ name_parts.push('Tail');
507
+ if (event.flick !== 'none') {
508
+ name_parts.push(event.friction ? 'TraceFlick' : 'Flick');
509
+ }
510
+ else {
511
+ name_parts.push(event.friction ? 'Trace' : 'Release');
512
+ }
513
+ isSimLineEligible = true;
514
+ }
515
+ }
516
+ else if (event.type === 'tick') {
517
+ name_parts.push(event.critical ? 'Critical' : 'Normal');
518
+ name_parts.push('Tick');
519
+ }
520
+ else {
521
+ isAttached = true;
522
+ if (event.critical) {
523
+ name_parts.push('Critical');
524
+ name_parts.push('Tick');
525
+ }
526
+ else {
527
+ name_parts.push('TransientHiddenTick');
528
+ }
529
+ }
530
+ name_parts.push('Note');
531
+ let archetype = name_parts.join('');
532
+ if (hold.flags.dummy)
533
+ archetype = `Fake${archetype}`;
534
+ const data = {
535
+ '#BEAT': event.tick / TICKS_PER_BEAT,
536
+ lane: laneToSonolusLane(event.lane, event.width),
537
+ size: event.width,
538
+ isAttached: isAttached ? 1 : 0,
539
+ connectorEase: event.ease
540
+ ? mmwEaseToSonolusEase(event.ease)
541
+ : SONOLUS_CONNECTOR_EASES.linear,
542
+ isSeparator: 0,
543
+ segmentKind: event.critical ? 2 : 1,
544
+ segmentAlpha: 1,
545
+ segmentLayer: 0,
546
+ '#TIMESCALE_GROUP': timeScaleGroupRef,
547
+ };
548
+ if (event.flick && event.flick !== 'none') {
549
+ data['direction'] = flickTypeToDirection(event.flick);
550
+ }
551
+ else {
552
+ data['direction'] = 0;
553
+ }
554
+ const connectionIntermediate = createIntermediate(archetype, data, isSimLineEligible);
555
+ if (headNoteIntermediate === null) {
556
+ headNoteIntermediate = connectionIntermediate;
557
+ }
558
+ if (currentSegmentHead === null) {
559
+ currentSegmentHead = connectionIntermediate;
560
+ }
561
+ connectionIntermediate.data['activeHead'] = headNoteIntermediate;
562
+ if (isAttached) {
563
+ queuedAttachIntermediates.push(connectionIntermediate);
564
+ }
565
+ else {
566
+ if (prevJointIntermediate !== null) {
567
+ for (const attachIntermediate of queuedAttachIntermediates) {
568
+ attachIntermediate.data['attachHead'] = prevJointIntermediate;
569
+ attachIntermediate.data['attachTail'] = connectionIntermediate;
570
+ }
571
+ queuedAttachIntermediates.length = 0;
572
+ while (nextHiddenTickBeat + EPSILON <
573
+ connectionIntermediate.data['#BEAT']) {
574
+ createIntermediate('TransientHiddenTickNote', {
575
+ '#BEAT': nextHiddenTickBeat,
576
+ '#TIMESCALE_GROUP': timeScaleGroupRef,
577
+ lane: connectionIntermediate.data['lane'],
578
+ size: connectionIntermediate.data['size'],
579
+ direction: 0,
580
+ isAttached: 1,
581
+ connectorEase: SONOLUS_CONNECTOR_EASES.linear,
582
+ isSeparator: 0,
583
+ segmentKind: 1,
584
+ segmentAlpha: 0,
585
+ activeHead: headNoteIntermediate,
586
+ attachHead: prevJointIntermediate,
587
+ attachTail: connectionIntermediate,
588
+ });
589
+ nextHiddenTickBeat += 0.5;
590
+ }
591
+ const connectorIntermediate = createIntermediate('Connector', {
592
+ head: prevJointIntermediate,
593
+ tail: connectionIntermediate,
594
+ kind: resolveConnectorKind(event.critical, !!hold.flags.dummy),
595
+ });
596
+ connectors.push(connectorIntermediate);
597
+ pendingSegmentConnectors.push(connectorIntermediate);
598
+ }
599
+ prevJointIntermediate = connectionIntermediate;
600
+ }
601
+ if (prevNoteIntermediate !== null) {
602
+ prevNoteIntermediate.data['next'] = connectionIntermediate;
603
+ }
604
+ prevNoteIntermediate = connectionIntermediate;
605
+ }
606
+ if (headNoteIntermediate && prevJointIntermediate) {
607
+ if (currentSegmentHead) {
608
+ for (const conn of pendingSegmentConnectors) {
609
+ conn.data['segmentHead'] = currentSegmentHead;
610
+ conn.data['segmentTail'] = prevJointIntermediate;
611
+ }
612
+ }
613
+ for (const conn of connectors) {
614
+ conn.data['activeHead'] = headNoteIntermediate;
615
+ conn.data['activeTail'] = prevJointIntermediate;
616
+ }
617
+ }
618
+ }
619
+ }
620
+ simLineEligibleNotes.sort((a, b) => {
621
+ const beatA = a.data['#BEAT'];
622
+ const beatB = b.data['#BEAT'];
623
+ if (Math.abs(beatA - beatB) > EPSILON)
624
+ return beatA - beatB;
625
+ const laneA = a.data['lane'];
626
+ const laneB = b.data['lane'];
627
+ return laneA - laneB;
628
+ });
629
+ const simGroups = [];
630
+ let currentSimGroup = [];
631
+ for (const simNote of simLineEligibleNotes) {
632
+ if (currentSimGroup.length === 0 ||
633
+ Math.abs(simNote.data['#BEAT'] - currentSimGroup[0].data['#BEAT']) < 1e-2) {
634
+ currentSimGroup.push(simNote);
635
+ }
636
+ else {
637
+ simGroups.push(currentSimGroup);
638
+ currentSimGroup = [simNote];
639
+ }
640
+ }
641
+ if (currentSimGroup.length > 0)
642
+ simGroups.push(currentSimGroup);
643
+ for (const simGroup of simGroups) {
644
+ for (let i = 0; i < simGroup.length - 1; i++) {
645
+ createIntermediate('SimLine', {
646
+ left: simGroup[i],
647
+ right: simGroup[i + 1],
648
+ });
649
+ }
650
+ }
651
+ const intermediateToRef = new Map();
652
+ let entityRefCounter = 0;
653
+ const getRef = (intermediateEntity) => {
654
+ let ref = intermediateToRef.get(intermediateEntity);
655
+ if (ref)
656
+ return ref;
657
+ ref = (entityRefCounter++).toString(16);
658
+ intermediateToRef.set(intermediateEntity, ref);
659
+ return ref;
660
+ };
661
+ for (const intermediateEntity of allIntermediateEntities) {
662
+ const entity = {
663
+ archetype: intermediateEntity.archetype,
664
+ name: getRef(intermediateEntity),
665
+ data: [],
666
+ };
667
+ for (const [dataName, dataValue] of Object.entries(intermediateEntity.data)) {
668
+ if (typeof dataValue === 'number') {
669
+ entity.data.push({ name: dataName, value: dataValue });
670
+ }
671
+ else if (dataValue !== undefined) {
672
+ entity.data.push({ name: dataName, ref: getRef(dataValue) });
673
+ }
674
+ }
675
+ entities.push(entity);
676
+ }
677
+ return {
678
+ bgmOffset: score.metadata.musicOffset / 1000,
679
+ entities,
680
+ };
681
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonolus-next-rush-plus-engine",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "description": "A new Project Sekai inspired engine for Sonolus",
5
5
  "author": "Hyeon2",
6
6
  "repository": {