kipphi 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "kipphi",
3
+ "description": "Parse your Phigros Chart(.rpe.json or .kpa.json) into an editor-friendly format.",
4
+ "version": "2.0.0",
5
+ "author": "Team Zincs (https://github.com/TeamZincs)",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/TeamZincs/kipphi.git"
10
+ },
11
+ "module": "index.ts",
12
+ "main": "index.ts",
13
+ "type": "module",
14
+ "devDependencies": {
15
+ "@types/bun": "latest"
16
+ },
17
+ "peerDependencies": {
18
+ "typescript": "^5"
19
+ }
20
+ }
@@ -0,0 +1,425 @@
1
+ import type { Chart, UIName } from "./chart";
2
+ import { type TimeT, type ChartDataRPE, type MetaData, type JudgeLineDataRPE, type EventLayerDataRPE, type EventDataRPELike, EventType, InterpreteAs, type NoteDataRPE, type EventValueESType, EventValueType } from "./chartTypes";
3
+ import { SegmentedEasing, BezierEasing, NormalEasing, fixedEasing, TemplateEasing, Easing } from "./easing";
4
+ import { EasedEvaluator, ExpressionEvaluator, NumericEasedEvaluator, type EasedEvaluatorConstructorOfType, type EasedEvaluatorOfType } from "./evaluator";
5
+ import { EventEndNode, EventNode, EventNodeSequence, EventStartNode, type EventNodeLike } from "./event";
6
+ import type { JudgeLine } from "./judgeline";
7
+ import type { NNList, HNList, NNOrHead } from "./note";
8
+ import { TC, TimeCalculator } from "./time";
9
+ import { NodeType, numberToRatio } from "./util";
10
+
11
+ /// #declaration:global
12
+
13
+ const getInnerEasing = (easing: Easing) => easing instanceof SegmentedEasing ? easing.easing : easing;
14
+
15
+ /**
16
+ * 全生命周期只会编译一次,想多次就再构造一个
17
+ */
18
+ export class RPEChartCompiler {
19
+ sequenceMap: Map<EventNodeSequence<any>, EventNodeSequence<any>> = new Map();
20
+ interpolationStep: TimeT = [0, 1, 16];
21
+ constructor(public chart: Chart) {}
22
+
23
+ compileChart(): ChartDataRPE {
24
+ console.time("compileChart")
25
+ const chart = this.chart;
26
+ const judgeLineGroups = chart.judgeLineGroups.map(group => group.name);
27
+ const judgeLineList = chart.judgeLines.map(line => this.compileJudgeLine(line));
28
+ const BPMList = chart.timeCalculator.dump();
29
+ const META: MetaData = {
30
+ RPEVersion: 1,
31
+ background: 'illustration.png',
32
+ charter: chart.charter,
33
+ composer: chart.composer,
34
+ illustration: chart.illustrator,
35
+ id: Math.random().toString().slice(2, 10),
36
+ level: chart.level,
37
+ name: chart.name,
38
+ offset: chart.offset,
39
+ song: chart.name
40
+ };
41
+
42
+ for (const uiName of ["bar", "combo", "combonumber", "level", "name", "pause", "score"] satisfies UIName[]) {
43
+ const target: JudgeLine | null = chart[`${uiName}Attach` satisfies keyof Chart];
44
+ if (!target) {
45
+ continue;
46
+ }
47
+ const lineData = judgeLineList[target.id];
48
+ // RPEJSON里面一条线只能绑一个UI,KPAJSON可以绑多个
49
+ // 所以如果绑了多个,自动给它们创建子线
50
+ if (lineData.attachUI) {
51
+ judgeLineList.push({
52
+ Group: 0,
53
+ Name: "Auto created for " + uiName,
54
+ Texture: "line.png",
55
+ attachUI: uiName,
56
+ notes: [],
57
+ bpmfactor: 1.0,
58
+ eventLayers: [],
59
+ father: target.id,
60
+ isCover: lineData.isCover,
61
+ numOfNotes: 0
62
+ } satisfies Partial<JudgeLineDataRPE> as JudgeLineDataRPE)
63
+ } else {
64
+ lineData.attachUI = uiName;
65
+ }
66
+ }
67
+
68
+
69
+ console.timeEnd("compileChart");
70
+ return {
71
+ BPMList,
72
+ META,
73
+ judgeLineList,
74
+ judgeLineGroup: judgeLineGroups,
75
+ multiLineString: '',
76
+ multiScale: 1.0,
77
+ chartTime: chart.rpeChartingTime * 60,
78
+ kpaChartTime: chart.chartingTime,
79
+ };
80
+ }
81
+
82
+ compileJudgeLine(judgeLine: JudgeLine): JudgeLineDataRPE {
83
+ const chart = this.chart;
84
+ const notes = this.compileNNLists([...judgeLine.nnLists.values()], [...judgeLine.hnLists.values()]);
85
+
86
+ return {
87
+ notes: notes,
88
+ Group: chart.judgeLineGroups.indexOf(judgeLine.group),
89
+ Name: judgeLine.name,
90
+ Texture: judgeLine.texture,
91
+ bpmfactor: 1.0,
92
+ eventLayers: judgeLine.eventLayers.map((layer): EventLayerDataRPE => ({
93
+ moveXEvents: layer.moveX ? this.dumpEventNodeSequence(layer.moveX) : null,
94
+ moveYEvents: layer.moveY ? this.dumpEventNodeSequence(layer.moveY) : null,
95
+ rotateEvents: layer.rotate ? this.dumpEventNodeSequence(layer.rotate) : null,
96
+ alphaEvents: layer.alpha ? this.dumpEventNodeSequence(layer.alpha) : null,
97
+ speedEvents: layer.speed ? this.dumpEventNodeSequence(layer.speed) : null
98
+ })),
99
+ extended: {
100
+ scaleXEvents: judgeLine.extendedLayer.scaleX ? this.dumpEventNodeSequence(judgeLine.extendedLayer.scaleX) : null,
101
+ scaleYEvents: judgeLine.extendedLayer.scaleY ? this.dumpEventNodeSequence(judgeLine.extendedLayer.scaleY) : null,
102
+ textEvents: judgeLine.extendedLayer.text ? this.dumpEventNodeSequence(judgeLine.extendedLayer.text) : null,
103
+ colorEvents: judgeLine.extendedLayer.color ? this.dumpEventNodeSequence(judgeLine.extendedLayer.color) : null
104
+ },
105
+ father: judgeLine.father?.id ?? -1,
106
+ isCover: judgeLine.cover ? 1 : 0,
107
+ numOfNotes: notes.length,
108
+ anchor: judgeLine.anchor,
109
+ rotateWithFather: judgeLine.rotatesWithFather,
110
+ isGif: 0,
111
+ zOrder: judgeLine.zOrder
112
+ };
113
+ }
114
+
115
+ compileEasedEvent<VT extends EventValueESType>(
116
+ snode: EventStartNode<VT> & { evaluator: EasedEvaluatorOfType<VT>},
117
+ getValue: (node: EventStartNode<VT> | EventEndNode<VT>) => VT
118
+ ): EventDataRPELike<VT> {
119
+ const endNode = snode.next as EventEndNode<VT>;
120
+ const evaluator = snode.evaluator;
121
+ const easing = evaluator.easing;
122
+ const isSegmented = easing instanceof SegmentedEasing;
123
+ const innerEasing = isSegmented ? easing.easing : easing;
124
+
125
+ return {
126
+ bezier: innerEasing instanceof BezierEasing ? 1 : 0,
127
+ bezierPoints: innerEasing instanceof BezierEasing ?
128
+ [innerEasing.cp1[0], innerEasing.cp1[1], innerEasing.cp2[0], innerEasing.cp2[1]] : // 修正了这里 cp2.y 的引用
129
+ [0, 0, 0, 0],
130
+ easingLeft: isSegmented ? easing.left : 0.0,
131
+ easingRight: isSegmented ? easing.right : 1.0,
132
+ easingType: easing instanceof NormalEasing ?
133
+ easing.rpeId ?? 1 :
134
+ null,
135
+ end: getValue(easing === fixedEasing ? snode : endNode),
136
+ endTime: endNode.time,
137
+ linkgroup: 0, // 假设默认值为 0
138
+ start: getValue(snode),
139
+ startTime: snode.time
140
+ }
141
+ }
142
+
143
+ dumpEventNodeSequence<VT>(sequence: EventNodeSequence<VT>): EventDataRPELike<VT>[] {
144
+ const nodes: EventDataRPELike<VT>[] = [];
145
+ const interpolationStep = this.interpolationStep;
146
+ if (!(sequence.type === EventType.color || sequence.type === EventType.text)) {
147
+ // @ts-ignore 烦死了烦死了烦死了
148
+ sequence = this.substitute(sequence);
149
+ }
150
+ let node = sequence.head.next;
151
+ // 唯一真史
152
+ const getValue = (sequence.type === EventType.text
153
+ ? (node: EventStartNode<string> | EventEndNode<string>) => {
154
+ const interpretedAs = node instanceof EventStartNode ? node.interpretedAs : node.previous.interpretedAs;
155
+ return interpretedAs === InterpreteAs.str ? node.value : "%P%" + node.value;
156
+ }
157
+ : (node: EventStartNode<number> | EventEndNode<number>) => node.value) as unknown as (node: EventStartNode<VT> | EventEndNode<VT>) => VT;
158
+ while (true) {
159
+ const end = node.next;
160
+ if (end.type === NodeType.TAIL) break;
161
+ if (node.evaluator instanceof ExpressionEvaluator) {
162
+ let cur = node.time;
163
+ const endTime = end.time;
164
+ let value = getValue(node);
165
+ for (; TC.lt(cur, endTime);) {
166
+ const nextTime = TC.validateIp(TC.add(cur, interpolationStep));
167
+ const nextValue = node.getValueAt(TC.toBeats(nextTime))
168
+ nodes.push({
169
+ bezier: 0,
170
+ bezierPoints: [0, 0, 0, 0],
171
+ easingLeft: 0.0,
172
+ easingRight: 0.0,
173
+ easingType: 1,
174
+ start: value,
175
+ startTime: cur,
176
+ end: nextValue,
177
+ endTime: nextTime,
178
+ linkgroup: 0
179
+ });
180
+ cur = nextTime;
181
+ value = nextValue;
182
+ }
183
+ // 所切割的事件长度并不必然是step的整数倍
184
+ nodes.push({
185
+ bezier: 0,
186
+ bezierPoints: [0, 0, 0, 0],
187
+ easingLeft: 0.0,
188
+ easingRight: 0.0,
189
+ easingType: 1,
190
+ start: value,
191
+ startTime: cur,
192
+ end: end.value,
193
+ endTime: endTime,
194
+ linkgroup: 0
195
+ })
196
+
197
+ } else {
198
+ nodes.push(this.compileEvent(node, getValue));
199
+ }
200
+ node = end.next;
201
+ }
202
+ nodes.push(node.dumpAsLast());
203
+
204
+ return nodes
205
+ }
206
+
207
+ compileNNLists(nnLists: NNList[], hnLists: HNList[]): NoteDataRPE[] {
208
+ const noteLists = nnLists.map(list => this.nnListToArray(list));
209
+ const holdLists = hnLists.map(list => this.nnListToArray(list));
210
+ const ret: NoteDataRPE[] = []
211
+ const time = (list: NoteDataRPE[]) => list.length === 0 ? [Infinity, 0, 1] as TimeT : list[list.length - 1].startTime;
212
+ const concatWithOrder = (lists: NoteDataRPE[][]) => {
213
+ if (lists.length === 0) return;
214
+ // 先按最早的时间排序
215
+ lists.sort((a, b) => {
216
+ return TimeCalculator.gt(time(a), time(b)) ? 1 : -1;
217
+ });
218
+ // 每次从lists中第一个list pop一个data加入到结果,然后冒泡调整这个list的位置
219
+ while (lists[0].length > 0) {
220
+ const list = lists[0];
221
+ // 只需要pop就可以了,pop复杂度O(1),这是倒序的原因
222
+ const node = list.pop();
223
+ ret.push(node);
224
+ let i = 0;
225
+ while (i + 1 < lists.length && TimeCalculator.gt(time(lists[i]), time(lists[i + 1]))) {
226
+ const temp = lists[i];
227
+ lists[i] = lists[i + 1];
228
+ lists[i + 1] = temp;
229
+ i++;
230
+ }
231
+ }
232
+
233
+ };
234
+ concatWithOrder(noteLists);
235
+ concatWithOrder(holdLists);
236
+ return ret;
237
+ }
238
+ /**
239
+ * 倒序转换为数组
240
+ * @param nnList
241
+ * @returns 一个按照时间降序排列的数组
242
+ */
243
+ nnListToArray(nnList: NNList) {
244
+ const notes: NoteDataRPE[] = [];
245
+ let node: NNOrHead = nnList.tail.previous;
246
+ while (node.type !== NodeType.HEAD) {
247
+ for (let each of node.notes) {
248
+ notes.push(each.dumpRPE(this.chart.timeCalculator));
249
+ }
250
+ node = node.previous;
251
+ }
252
+ return notes;
253
+ }
254
+
255
+ /**
256
+ * 将当前序列中所有通过模板缓动引用了其他序列的事件直接展开为被引用的序列内容
257
+ * transform all events that reference other sequences by template easing
258
+ * into the content of the referenced sequence
259
+ * 有点类似于MediaWiki的{{subst:templateName}}
260
+ * @param map 由TemplateEasingLib提供
261
+ * @returns
262
+ */
263
+ substitute<VT extends EventValueESType>(seq: EventNodeSequence<VT>): EventNodeSequence<VT> {
264
+ const map = this.sequenceMap;
265
+ if (map.has(seq)) {
266
+ return map.get(seq);
267
+ }
268
+ let currentNode: EventStartNode<VT> = seq.head.next;
269
+ const newSeq = new EventNodeSequence<VT>(seq.type, seq.effectiveBeats);
270
+ const valueType = seq.type === EventType.color
271
+ ? EventValueType.color : seq.type === EventType.text
272
+ ? EventValueType.text : EventValueType.numeric;
273
+ map.set(seq, newSeq);
274
+ let currentPos: EventNodeLike<NodeType.HEAD, VT> | EventEndNode<VT> = newSeq.head;
275
+ while (true) {
276
+ if (!currentNode || (currentNode.next.type === NodeType.TAIL)) {
277
+ break;
278
+ }
279
+ const endNode = currentNode.next;
280
+ const evaluator = currentNode.evaluator;
281
+ let innerEasing: Easing;
282
+ if ( evaluator
283
+ && evaluator instanceof EasedEvaluator
284
+ && (innerEasing = getInnerEasing(evaluator.easing)) instanceof TemplateEasing
285
+ ) {
286
+ const EvaluatorConstructor = evaluator.constructor as EasedEvaluatorConstructorOfType<VT>;
287
+ const srcSeq = this.substitute(innerEasing.eventNodeSequence);
288
+ const easing = evaluator.easing;
289
+ const isSegmented = easing instanceof SegmentedEasing;
290
+ const startValue = currentNode.value;
291
+ const endValue = currentNode.next.value;
292
+ const startTime = currentNode.time;
293
+ const timeDelta = TC.sub(currentNode.next.time, startTime);
294
+
295
+
296
+ let srcStart: number, srcEnd: number, leftDividedNodeSrc: EventStartNode, rightDividedNode: EventStartNode,
297
+ srcStartTime: TimeT, srcTimeDelta: TimeT, toStopAt: EventStartNode;
298
+ if (isSegmented) {
299
+ const totalDuration = TC.sub(srcSeq.tail.previous.time, srcSeq.head.next.time);
300
+ srcStart = srcSeq.getValueAt(easing.left * srcSeq.effectiveBeats)
301
+ srcEnd = srcSeq.getValueAt(easing.right * srcSeq.effectiveBeats, true);
302
+ leftDividedNodeSrc = srcSeq.getNodeAt(easing.left * srcSeq.effectiveBeats);
303
+ rightDividedNode = srcSeq.getNodeAt(easing.right * srcSeq.effectiveBeats, true);
304
+ toStopAt = rightDividedNode.next.next;
305
+ srcStartTime = TC.mul(totalDuration, numberToRatio(easing.left));
306
+ const srcEndTime = TC.mul(totalDuration, numberToRatio(easing.right));
307
+ TC.validateIp(srcStartTime);
308
+ TC.validateIp(srcEndTime);
309
+ srcTimeDelta = TC.sub(srcEndTime, srcStartTime);
310
+ TC.validateIp(srcTimeDelta);
311
+ } else {
312
+ srcStart = srcSeq.head.next.value;
313
+ srcEnd = srcSeq.tail.previous.value;
314
+ leftDividedNodeSrc = srcSeq.head.next;
315
+ rightDividedNode = srcSeq.tail.previous;
316
+ toStopAt = rightDividedNode;
317
+ srcStartTime = srcSeq.head.next.time;
318
+ srcTimeDelta = TC.sub(srcSeq.tail.previous.time, srcStartTime);
319
+ }
320
+
321
+ const srcDelta = srcEnd - srcStart;
322
+ const ratio = TC.div(timeDelta, srcTimeDelta)
323
+
324
+ const convert: (v: number) => VT
325
+ = (value: number) => evaluator.convert(startValue, endValue, (value - srcStart) / srcDelta);
326
+ // 我恨TS没有运算符重载
327
+ const convertTime: (t: TimeT) => TimeT
328
+ = (time: TimeT) => TC.validateIp(TC.add(startTime, TC.mul(TC.sub(time, srcStartTime), ratio)));
329
+
330
+ const first = currentNode.clone();
331
+ EventNode.connect(currentPos, first)
332
+ // 处理第一个节点的截段
333
+ if (isSegmented) {
334
+ const left = easing.left * srcSeq.effectiveBeats
335
+ if (left - TC.toBeats(leftDividedNodeSrc.time) > 1e-6) {
336
+ // 断言:这里left不会大于有效拍数
337
+ const newLeft = left / (TC.toBeats((leftDividedNodeSrc.next as EventEndNode).time) - TC.toBeats(leftDividedNodeSrc.time))
338
+ // 如果切到的这个位置是表达式求值器,这是没办法保留缓动的,只能运用表达式求值器
339
+ if (leftDividedNodeSrc.evaluator instanceof ExpressionEvaluator) {
340
+ const exprEvaluator = leftDividedNodeSrc.evaluator as ExpressionEvaluator<number>;
341
+ first.evaluator = new ExpressionEvaluator("t");
342
+ // 强行修改
343
+ // @ts-ignore
344
+ first.evaluator.func = (t: number) => {
345
+ const value = exprEvaluator.func(
346
+ t * (TC.getDelta((first.next as EventEndNode).time, first.time) * (1 - left)) / srcSeq.effectiveBeats
347
+ ); // 脑细胞杀器
348
+ return convert(value);
349
+ }
350
+ } else {
351
+ // 否则就是带缓动求值器
352
+ first.evaluator = new EvaluatorConstructor(
353
+ new SegmentedEasing((leftDividedNodeSrc.evaluator as NumericEasedEvaluator).easing, newLeft, 1.0)
354
+ );
355
+
356
+ }
357
+ } else {
358
+ if (leftDividedNodeSrc.evaluator instanceof ExpressionEvaluator) {
359
+ first.evaluator = new ExpressionEvaluator("t");
360
+ // 也不知道对不对
361
+ // @ts-ignore
362
+ first.evaluator.func = (t: number) => {
363
+ return convert(leftDividedNodeSrc.evaluator.eval(leftDividedNodeSrc, t))
364
+ }
365
+ } else {
366
+ first.evaluator = new EvaluatorConstructor((leftDividedNodeSrc.evaluator as NumericEasedEvaluator).easing, evaluator.interpretedAs);
367
+ }
368
+ }
369
+ } else {
370
+ if (leftDividedNodeSrc.evaluator instanceof ExpressionEvaluator) {
371
+ first.evaluator = new ExpressionEvaluator("t");
372
+ // 也不知道对不对
373
+ // @ts-ignore
374
+ first.evaluator.func = (t: number) => {
375
+ return convert(leftDividedNodeSrc.evaluator.eval(leftDividedNodeSrc, t))
376
+ }
377
+ } else {
378
+ first.evaluator = new EvaluatorConstructor((leftDividedNodeSrc.evaluator as NumericEasedEvaluator).easing, evaluator.interpretedAs);
379
+ }
380
+ }
381
+ let prev = first
382
+ // 这里在到toStopAt之前一直都是非尾的
383
+ for (let n: EventEndNode<number> = leftDividedNodeSrc.next as EventEndNode<number>; n.next !== toStopAt; n = n.next.next) {
384
+ const endNode = n;
385
+ const startNode = n.next;
386
+ const newEnd = new EventEndNode(convertTime(endNode.time), convert(endNode.value));
387
+ const newStart = new EventStartNode(convertTime(startNode.time), convert(startNode.value));
388
+ newStart.evaluator = new EvaluatorConstructor(startNode.evaluator.easing);
389
+ EventNode.connect(prev, newEnd)
390
+ EventNode.connect(newEnd, newStart);
391
+ prev = newStart;
392
+ }
393
+ // 处理最后一个节点的截段
394
+ if (isSegmented) {
395
+ const right = easing.right * srcSeq.effectiveBeats
396
+ if (TC.toBeats((rightDividedNode.next as EventEndNode).time) - right > 1e-6) {
397
+ // 断言:这里right不会大于有效拍数
398
+ const newRight = right / (TC.toBeats((rightDividedNode.next as EventEndNode).time) - TC.toBeats(rightDividedNode.time))
399
+ // 这时候prev是最后一个subst的node
400
+ prev.easing = new SegmentedEasing(rightDividedNode.easing, 0.0, newRight)
401
+ }
402
+
403
+ }
404
+ const endNode = currentNode.next.clone();
405
+ EventNode.connect(prev, endNode);
406
+ currentPos = endNode;
407
+ endNode.value = isSegmented ? endNode.value : convert((srcSeq.tail.previous.previous as EventEndNode).value);
408
+ } else {
409
+ const newStartNode = currentNode.clone();
410
+ const newEndNode = endNode.clone();
411
+ EventNode.connect(currentPos, newStartNode)
412
+ EventNode.connect(newStartNode, newEndNode);
413
+ currentPos = newEndNode;
414
+ }
415
+ currentNode = endNode.next;
416
+ }
417
+ const lastStart = currentNode.clone();
418
+ EventNode.connect(currentPos, lastStart);
419
+ EventNode.connect(lastStart, newSeq.tail)
420
+ return newSeq;
421
+ }
422
+ }
423
+
424
+
425
+ /// #enddeclaration