nise/nise-replay-viewer/osu-parsers/lib/browser.mjs
2024-03-04 17:22:06 +01:00

3289 lines
86 KiB
JavaScript

import {
Color4,
EventType,
StoryboardSample,
StoryboardAnimation,
Vector2,
StoryboardSprite,
LayerType,
StoryboardVideo,
CompoundType,
CommandType,
ParameterType,
BlendingParameters,
Origins,
Anchor,
LoopType,
BeatmapBreakEvent,
SampleSet,
HitObject,
SliderPath,
CommandLoop,
CommandTrigger,
HitType,
HitSound,
PathType,
EffectType,
TimeSignature,
ControlPointType,
ReplayButtonState,
LegacyReplayFrame,
Storyboard,
SampleBank,
PathPoint,
HitSample,
TimingPoint,
DifficultyPoint,
EffectPoint,
SamplePoint,
LifeBarFrame,
Beatmap,
ScoreInfo,
Replay,
Score,
} from "osu-classes";
import { decompress, compress } from "lzma-js-simple-v2";
class Parsing {
static parseInt(input, parseLimit = this.MAX_PARSE_VALUE, allowNaN = false) {
return this._getValue(parseInt(input), parseLimit, allowNaN);
}
static parseFloat(
input,
parseLimit = this.MAX_PARSE_VALUE,
allowNaN = false
) {
return this._getValue(parseFloat(input), parseLimit, allowNaN);
}
static parseEnum(enumObj, input) {
const value = input.trim();
const rawValue = parseInt(value);
if (rawValue in enumObj) {
return rawValue;
}
if (value in enumObj) {
return enumObj[value];
}
throw new Error("Unknown enum value!");
}
static parseByte(input) {
const value = parseInt(input);
if (value < 0) {
throw new Error("Value must be greater than 0!");
}
if (value > 255) {
throw new Error("Value must be less than 255!");
}
return this._getValue(value);
}
static _getValue(value, parseLimit = this.MAX_PARSE_VALUE, allowNaN = false) {
if (value < -parseLimit) {
throw new Error("Value is too low!");
}
if (value > parseLimit) {
throw new Error("Value is too high!");
}
if (!allowNaN && Number.isNaN(value)) {
throw new Error("Not a number");
}
return value;
}
}
Parsing.MAX_COORDINATE_VALUE = 131072;
Parsing.MAX_PARSE_VALUE = 2147483647;
class BeatmapColorDecoder {
static handleLine(line, output) {
const [key, ...values] = line.split(":");
const rgba = values
.join(":")
.trim()
.split(",")
.map((c) => Parsing.parseByte(c));
if (rgba.length !== 3 && rgba.length !== 4) {
throw new Error(
`Color specified in incorrect format (should be R,G,B or R,G,B,A): ${rgba.join(
","
)}`
);
}
const color = new Color4(rgba[0], rgba[1], rgba[2], rgba[3]);
this.addColor(color, output, key.trim());
}
static addColor(color, output, key) {
if (key === "SliderTrackOverride") {
output.colors.sliderTrackColor = color;
return;
}
if (key === "SliderBorder") {
output.colors.sliderBorderColor = color;
return;
}
if (key.startsWith("Combo")) {
output.colors.comboColors.push(color);
}
}
}
class BeatmapDifficultyDecoder {
static handleLine(line, beatmap) {
const [key, ...values] = line.split(":");
const value = values.join(":").trim();
switch (key.trim()) {
case "CircleSize":
beatmap.difficulty.circleSize = Parsing.parseFloat(value);
break;
case "HPDrainRate":
beatmap.difficulty.drainRate = Parsing.parseFloat(value);
break;
case "OverallDifficulty":
beatmap.difficulty.overallDifficulty = Parsing.parseFloat(value);
break;
case "ApproachRate":
beatmap.difficulty.approachRate = Parsing.parseFloat(value);
break;
case "SliderMultiplier":
beatmap.difficulty.sliderMultiplier = Parsing.parseFloat(value);
break;
case "SliderTickRate":
beatmap.difficulty.sliderTickRate = Parsing.parseFloat(value);
}
}
}
class BeatmapEditorDecoder {
static handleLine(line, beatmap) {
const [key, ...values] = line.split(":");
const value = values.join(":").trim();
switch (key.trim()) {
case "Bookmarks":
beatmap.editor.bookmarks = value.split(",").map((v) => +v);
break;
case "DistanceSpacing":
beatmap.editor.distanceSpacing = Math.max(0, Parsing.parseFloat(value));
break;
case "BeatDivisor":
beatmap.editor.beatDivisor = Parsing.parseInt(value);
break;
case "GridSize":
beatmap.editor.gridSize = Parsing.parseInt(value);
break;
case "TimelineZoom":
beatmap.editor.timelineZoom = Math.max(0, Parsing.parseFloat(value));
}
}
}
class StoryboardEventDecoder {
static handleLine(line, storyboard) {
const depth = this._getDepth(line);
if (depth > 0) {
line = line.substring(depth);
}
if (depth < 2 && this._storyboardSprite) {
this._timelineGroup = this._storyboardSprite.timelineGroup;
}
switch (depth) {
case 0:
return this._handleElement(line, storyboard);
case 1:
return this._handleCompoundOrCommand(line);
case 2:
return this._handleCommand(line);
}
}
static _handleElement(line, storyboard) {
let _a;
const data = line.split(",");
const eventType = this.parseEventType(data[0]);
if (
(_a = this._storyboardSprite) === null || _a === void 0
? void 0
: _a.hasCommands
) {
this._storyboardSprite.updateCommands();
this._storyboardSprite.adjustTimesToCommands();
this._storyboardSprite.resetValuesToCommands();
}
switch (eventType) {
case EventType.Video: {
const layer = storyboard.getLayerByType(LayerType.Video);
const offset = Parsing.parseInt(data[1]);
const path = data[2].replace(/"/g, "");
layer.elements.push(new StoryboardVideo(path, offset));
return;
}
case EventType.Sprite: {
const layer = storyboard.getLayerByType(this.parseLayerType(data[1]));
const origin = this.parseOrigin(data[2]);
const anchor = this.convertOrigin(origin);
const path = data[3].replace(/"/g, "");
const x = Parsing.parseFloat(data[4], Parsing.MAX_COORDINATE_VALUE);
const y = Parsing.parseFloat(data[5], Parsing.MAX_COORDINATE_VALUE);
this._storyboardSprite = new StoryboardSprite(
path,
origin,
anchor,
new Vector2(x, y)
);
layer.elements.push(this._storyboardSprite);
return;
}
case EventType.Animation: {
const layer = storyboard.getLayerByType(this.parseLayerType(data[1]));
const origin = this.parseOrigin(data[2]);
const anchor = this.convertOrigin(origin);
const path = data[3].replace(/"/g, "");
const x = Parsing.parseFloat(data[4], Parsing.MAX_COORDINATE_VALUE);
const y = Parsing.parseFloat(data[5], Parsing.MAX_COORDINATE_VALUE);
const frameCount = Parsing.parseInt(data[6]);
let frameDelay = Parsing.parseFloat(data[7]);
if (storyboard.fileFormat < 6) {
frameDelay = Math.round(0.015 * frameDelay) * 1.186 * (1000 / 60);
}
const loopType = this.parseLoopType(data[8]);
this._storyboardSprite = new StoryboardAnimation(
path,
origin,
anchor,
new Vector2(x, y),
frameCount,
frameDelay,
loopType
);
layer.elements.push(this._storyboardSprite);
return;
}
case EventType.Sample: {
const time = Parsing.parseFloat(data[1]);
const layer = storyboard.getLayerByType(this.parseLayerType(data[2]));
const path = data[3].replace(/"/g, "");
const volume = data.length > 4 ? Parsing.parseInt(data[4]) : 100;
const sample = new StoryboardSample(path, time, volume);
layer.elements.push(sample);
}
}
}
static _handleCompoundOrCommand(line) {
let _a, _b;
const data = line.split(",");
const compoundType = data[0];
switch (compoundType) {
case CompoundType.Trigger: {
this._timelineGroup =
(_a = this._storyboardSprite) === null || _a === void 0
? void 0
: _a.addTrigger(
data[1],
data.length > 2 ? Parsing.parseFloat(data[2]) : -Infinity,
data.length > 3 ? Parsing.parseFloat(data[3]) : Infinity,
data.length > 4 ? Parsing.parseInt(data[4]) : 0
);
return;
}
case CompoundType.Loop: {
this._timelineGroup =
(_b = this._storyboardSprite) === null || _b === void 0
? void 0
: _b.addLoop(
Parsing.parseFloat(data[1]),
Math.max(0, Parsing.parseInt(data[2]) - 1)
);
return;
}
default:
this._handleCommand(line);
}
}
static _handleCommand(line) {
let _a, _b, _c, _d, _e, _f, _g, _h, _j;
const data = line.split(",");
const type = data[0];
const easing = Parsing.parseInt(data[1]);
const startTime = Parsing.parseInt(data[2]);
const endTime = Parsing.parseInt(data[3] || data[2]);
switch (type) {
case CommandType.Fade: {
const startValue = Parsing.parseFloat(data[4]);
const endValue =
data.length > 5 ? Parsing.parseFloat(data[5]) : startValue;
(_a = this._timelineGroup) === null || _a === void 0
? void 0
: _a.alpha.add(
type,
easing,
startTime,
endTime,
startValue,
endValue
);
return;
}
case CommandType.Scale: {
const startValue = Parsing.parseFloat(data[4]);
const endValue =
data.length > 5 ? Parsing.parseFloat(data[5]) : startValue;
(_b = this._timelineGroup) === null || _b === void 0
? void 0
: _b.scale.add(
type,
easing,
startTime,
endTime,
startValue,
endValue
);
return;
}
case CommandType.VectorScale: {
const startX = Parsing.parseFloat(data[4]);
const startY = Parsing.parseFloat(data[5]);
const endX = data.length > 6 ? Parsing.parseFloat(data[6]) : startX;
const endY = data.length > 7 ? Parsing.parseFloat(data[7]) : startY;
(_c = this._timelineGroup) === null || _c === void 0
? void 0
: _c.vectorScale.add(
type,
easing,
startTime,
endTime,
new Vector2(startX, startY),
new Vector2(endX, endY)
);
return;
}
case CommandType.Rotation: {
const startValue = Parsing.parseFloat(data[4]);
const endValue =
data.length > 5 ? Parsing.parseFloat(data[5]) : startValue;
(_d = this._timelineGroup) === null || _d === void 0
? void 0
: _d.rotation.add(
type,
easing,
startTime,
endTime,
startValue,
endValue
);
return;
}
case CommandType.Movement: {
const startX = Parsing.parseFloat(data[4]);
const startY = Parsing.parseFloat(data[5]);
const endX = data.length > 6 ? Parsing.parseFloat(data[6]) : startX;
const endY = data.length > 7 ? Parsing.parseFloat(data[7]) : startY;
(_e = this._timelineGroup) === null || _e === void 0
? void 0
: _e.x.add(
CommandType.MovementX,
easing,
startTime,
endTime,
startX,
endX
);
(_f = this._timelineGroup) === null || _f === void 0
? void 0
: _f.y.add(
CommandType.MovementY,
easing,
startTime,
endTime,
startY,
endY
);
return;
}
case CommandType.MovementX: {
const startValue = Parsing.parseFloat(data[4]);
const endValue =
data.length > 5 ? Parsing.parseFloat(data[5]) : startValue;
(_g = this._timelineGroup) === null || _g === void 0
? void 0
: _g.x.add(type, easing, startTime, endTime, startValue, endValue);
return;
}
case CommandType.MovementY: {
const startValue = Parsing.parseFloat(data[4]);
const endValue =
data.length > 5 ? Parsing.parseFloat(data[5]) : startValue;
(_h = this._timelineGroup) === null || _h === void 0
? void 0
: _h.y.add(type, easing, startTime, endTime, startValue, endValue);
return;
}
case CommandType.Color: {
const startRed = Parsing.parseFloat(data[4]);
const startGreen = Parsing.parseFloat(data[5]);
const startBlue = Parsing.parseFloat(data[6]);
const endRed = data.length > 7 ? Parsing.parseFloat(data[7]) : startRed;
const endGreen =
data.length > 8 ? Parsing.parseFloat(data[8]) : startGreen;
const endBlue =
data.length > 9 ? Parsing.parseFloat(data[9]) : startBlue;
(_j = this._timelineGroup) === null || _j === void 0
? void 0
: _j.color.add(
type,
easing,
startTime,
endTime,
new Color4(startRed, startGreen, startBlue, 1),
new Color4(endRed, endGreen, endBlue, 1)
);
return;
}
case CommandType.Parameter: {
return this._handleParameterCommand(data);
}
}
throw new Error(`Unknown command type: ${type}`);
}
static _handleParameterCommand(data) {
let _a, _b, _c;
const type = CommandType.Parameter;
const easing = Parsing.parseInt(data[1]);
const startTime = Parsing.parseInt(data[2]);
const endTime = Parsing.parseInt(data[3] || data[2]);
const parameter = data[4];
switch (parameter) {
case ParameterType.BlendingMode: {
const startValue = BlendingParameters.Additive;
const endValue =
startTime === endTime
? BlendingParameters.Additive
: BlendingParameters.Inherit;
(_a = this._timelineGroup) === null || _a === void 0
? void 0
: _a.blendingParameters.add(
type,
easing,
startTime,
endTime,
startValue,
endValue,
parameter
);
return;
}
case ParameterType.HorizontalFlip:
(_b = this._timelineGroup) === null || _b === void 0
? void 0
: _b.flipH.add(
type,
easing,
startTime,
endTime,
true,
startTime === endTime,
parameter
);
return;
case ParameterType.VerticalFlip: {
(_c = this._timelineGroup) === null || _c === void 0
? void 0
: _c.flipV.add(
type,
easing,
startTime,
endTime,
true,
startTime === endTime,
parameter
);
return;
}
}
throw new Error(`Unknown parameter type: ${parameter}`);
}
static parseEventType(input) {
if (input.startsWith(" ") || input.startsWith("_")) {
return EventType.StoryboardCommand;
}
try {
return Parsing.parseEnum(EventType, input);
} catch {
throw new Error(`Unknown event type: ${input}`);
}
}
static parseLayerType(input) {
try {
return Parsing.parseEnum(LayerType, input);
} catch {
throw new Error(`Unknown layer type: ${input}`);
}
}
static parseOrigin(input) {
try {
return Parsing.parseEnum(Origins, input);
} catch {
return Origins.TopLeft;
}
}
static convertOrigin(origin) {
switch (origin) {
case Origins.TopLeft:
return Anchor.TopLeft;
case Origins.TopCentre:
return Anchor.TopCentre;
case Origins.TopRight:
return Anchor.TopRight;
case Origins.CentreLeft:
return Anchor.CentreLeft;
case Origins.Centre:
return Anchor.Centre;
case Origins.CentreRight:
return Anchor.CentreRight;
case Origins.BottomLeft:
return Anchor.BottomLeft;
case Origins.BottomCentre:
return Anchor.BottomCentre;
case Origins.BottomRight:
return Anchor.BottomRight;
}
return Anchor.TopLeft;
}
static parseLoopType(input) {
try {
return Parsing.parseEnum(LoopType, input);
} catch {
return LoopType.LoopForever;
}
}
static _getDepth(line) {
let depth = 0;
for (const char of line) {
if (char !== " " && char !== "_") {
break;
}
++depth;
}
return depth;
}
}
class BeatmapEventDecoder {
static handleLine(line, beatmap, sbLines, offset) {
const data = line.split(",").map((v, i) => (i ? v.trim() : v));
const eventType = StoryboardEventDecoder.parseEventType(data[0]);
switch (eventType) {
case EventType.Background:
beatmap.events.backgroundPath = data[2].replace(/"/g, "");
break;
case EventType.Break: {
const start = Parsing.parseFloat(data[1]) + offset;
const end = Math.max(start, Parsing.parseFloat(data[2]) + offset);
const breakEvent = new BeatmapBreakEvent(start, end);
if (!beatmap.events.breaks) {
beatmap.events.breaks = [];
}
beatmap.events.breaks.push(breakEvent);
break;
}
case EventType.Video:
case EventType.Sample:
case EventType.Sprite:
case EventType.Animation:
case EventType.StoryboardCommand:
if (sbLines) {
sbLines.push(line);
}
}
}
}
class BeatmapGeneralDecoder {
static handleLine(line, beatmap, offset) {
const [key, ...values] = line.split(":");
const value = values.join(":").trim();
switch (key.trim()) {
case "AudioFilename":
beatmap.general.audioFilename = value;
break;
case "AudioHash":
beatmap.general.audioHash = value;
break;
case "OverlayPosition":
beatmap.general.overlayPosition = value;
break;
case "SkinPreference":
beatmap.general.skinPreference = value;
break;
case "AudioLeadIn":
beatmap.general.audioLeadIn = Parsing.parseInt(value);
break;
case "PreviewTime":
beatmap.general.previewTime = Parsing.parseInt(value) + offset;
break;
case "Countdown":
beatmap.general.countdown = Parsing.parseInt(value);
break;
case "StackLeniency":
beatmap.general.stackLeniency = Parsing.parseFloat(value);
break;
case "Mode":
beatmap.originalMode = Parsing.parseInt(value);
break;
case "CountdownOffset":
beatmap.general.countdownOffset = Parsing.parseInt(value);
break;
case "SampleSet":
beatmap.general.sampleSet = SampleSet[value];
break;
case "LetterboxInBreaks":
beatmap.general.letterboxInBreaks = value === "1";
break;
case "StoryFireInFront":
beatmap.general.storyFireInFront = value === "1";
break;
case "UseSkinSprites":
beatmap.general.useSkinSprites = value === "1";
break;
case "AlwaysShowPlayfield":
beatmap.general.alwaysShowPlayfield = value === "1";
break;
case "EpilepsyWarning":
beatmap.general.epilepsyWarning = value === "1";
break;
case "SpecialStyle":
beatmap.general.specialStyle = value === "1";
break;
case "WidescreenStoryboard":
beatmap.general.widescreenStoryboard = value === "1";
break;
case "SamplesMatchPlaybackRate":
beatmap.general.samplesMatchPlaybackRate = value === "1";
}
}
}
class HittableObject extends HitObject {
constructor() {
super(...arguments);
this.isNewCombo = false;
this.comboOffset = 0;
}
clone() {
const cloned = super.clone();
cloned.isNewCombo = this.isNewCombo;
cloned.comboOffset = this.comboOffset;
return cloned;
}
}
class HoldableObject extends HitObject {
constructor() {
super(...arguments);
this.endTime = 0;
this.nodeSamples = [];
}
get duration() {
return this.endTime - this.startTime;
}
clone() {
const cloned = super.clone();
cloned.endTime = this.endTime;
cloned.nestedHitObjects = this.nestedHitObjects.map((h) => h.clone());
cloned.nodeSamples = this.nodeSamples.map((n) => n.map((s) => s.clone()));
return cloned;
}
}
class SlidableObject extends HitObject {
constructor() {
super(...arguments);
this.repeats = 0;
this.velocity = 1;
this.path = new SliderPath();
this.legacyLastTickOffset = 36;
this.nodeSamples = [];
this.isNewCombo = false;
this.comboOffset = 0;
}
get duration() {
return this.spans * this.spanDuration;
}
get endTime() {
return this.startTime + this.duration;
}
get spans() {
return this.repeats + 1;
}
set spans(value) {
this.repeats = value - 1;
}
get spanDuration() {
return this.distance / this.velocity;
}
get distance() {
return this.path.distance;
}
set distance(value) {
this.path.distance = value;
}
applyDefaultsToSelf(controlPoints, difficulty) {
super.applyDefaultsToSelf(controlPoints, difficulty);
const timingPoint = controlPoints.timingPointAt(this.startTime);
const difficultyPoint = controlPoints.difficultyPointAt(this.startTime);
const scoringDistance =
SlidableObject.BASE_SCORING_DISTANCE *
difficulty.sliderMultiplier *
difficultyPoint.sliderVelocity;
this.velocity = scoringDistance / timingPoint.beatLength;
}
clone() {
const cloned = super.clone();
cloned.legacyLastTickOffset = this.legacyLastTickOffset;
cloned.nodeSamples = this.nodeSamples.map((n) => n.map((s) => s.clone()));
cloned.velocity = this.velocity;
cloned.repeats = this.repeats;
cloned.path = this.path.clone();
cloned.isNewCombo = this.isNewCombo;
cloned.comboOffset = this.comboOffset;
return cloned;
}
}
SlidableObject.BASE_SCORING_DISTANCE = 100;
class SpinnableObject extends HitObject {
constructor() {
super(...arguments);
this.endTime = 0;
this.isNewCombo = false;
this.comboOffset = 0;
}
get duration() {
return this.endTime - this.startTime;
}
clone() {
const cloned = super.clone();
cloned.endTime = this.endTime;
cloned.isNewCombo = this.isNewCombo;
cloned.comboOffset = this.comboOffset;
return cloned;
}
}
let FileFormat;
(function (FileFormat) {
FileFormat["Beatmap"] = ".osu";
FileFormat["Storyboard"] = ".osb";
FileFormat["Replay"] = ".osr";
})(FileFormat || (FileFormat = {}));
let LineType;
(function (LineType) {
LineType[(LineType["FileFormat"] = 0)] = "FileFormat";
LineType[(LineType["Section"] = 1)] = "Section";
LineType[(LineType["Empty"] = 2)] = "Empty";
LineType[(LineType["Data"] = 3)] = "Data";
LineType[(LineType["Break"] = 4)] = "Break";
})(LineType || (LineType = {}));
let Section;
(function (Section) {
Section["General"] = "General";
Section["Editor"] = "Editor";
Section["Metadata"] = "Metadata";
Section["Difficulty"] = "Difficulty";
Section["Events"] = "Events";
Section["TimingPoints"] = "TimingPoints";
Section["Colours"] = "Colours";
Section["HitObjects"] = "HitObjects";
Section["Variables"] = "Variables";
Section["Fonts"] = "Fonts";
Section["CatchTheBeat"] = "CatchTheBeat";
Section["Mania"] = "Mania";
})(Section || (Section = {}));
const browserFSOperation = function () {
throw new Error(
"Filesystem operations are not available in a browser environment"
);
};
class BeatmapColorEncoder {
static encodeColors(beatmap) {
const colors = beatmap.colors;
if (Object.keys(colors).length === 1 && !colors.comboColors.length) {
return "";
}
const encoded = ["[Colours]"];
colors.comboColors.forEach((color, i) => {
encoded.push(`Combo${i + 1}:${color}`);
});
if (colors.sliderTrackColor) {
encoded.push(`SliderTrackOverride:${colors.sliderTrackColor}`);
}
if (colors.sliderBorderColor) {
encoded.push(`SliderBorder:${colors.sliderBorderColor}`);
}
return encoded.join("\n");
}
}
class BeatmapDifficultyEncoder {
static encodeDifficultySection(beatmap) {
const encoded = ["[Difficulty]"];
const difficulty = beatmap.difficulty;
encoded.push(`HPDrainRate:${difficulty.drainRate}`);
encoded.push(`CircleSize:${difficulty.circleSize}`);
encoded.push(`OverallDifficulty:${difficulty.overallDifficulty}`);
encoded.push(`ApproachRate:${difficulty.approachRate}`);
encoded.push(`SliderMultiplier:${difficulty.sliderMultiplier}`);
encoded.push(`SliderTickRate:${difficulty.sliderTickRate}`);
return encoded.join("\n");
}
}
class BeatmapEditorEncoder {
static encodeEditorSection(beatmap) {
const encoded = ["[Editor]"];
const editor = beatmap.editor;
encoded.push(`Bookmarks:${editor.bookmarks.join(",")}`);
encoded.push(`DistanceSpacing:${editor.distanceSpacing}`);
encoded.push(`BeatDivisor:${editor.beatDivisor}`);
encoded.push(`GridSize:${editor.gridSize}`);
encoded.push(`TimelineZoom:${editor.timelineZoom}`);
return encoded.join("\n");
}
}
class StoryboardEventEncoder {
static encodeEventSection(storyboard) {
const encoded = [];
encoded.push("[Events]");
encoded.push("//Background and Video events");
encoded.push(this.encodeVideos(storyboard));
encoded.push(this.encodeStoryboard(storyboard));
return encoded.join("\n");
}
static encodeVideos(storyboard) {
const encoded = [];
const video = storyboard.getLayerByType(LayerType.Video);
if (video.elements.length > 0) {
encoded.push(this.encodeStoryboardLayer(video));
}
return encoded.join("\n");
}
static encodeStoryboard(storyboard) {
const encoded = [];
encoded.push("//Storyboard Layer 0 (Background)");
const background = storyboard.getLayerByType(LayerType.Background);
if (background.elements.length > 0) {
encoded.push(this.encodeStoryboardLayer(background));
}
encoded.push("//Storyboard Layer 1 (Fail)");
const fail = storyboard.getLayerByType(LayerType.Fail);
if (fail.elements.length > 0) {
encoded.push(this.encodeStoryboardLayer(fail));
}
encoded.push("//Storyboard Layer 2 (Pass)");
const pass = storyboard.getLayerByType(LayerType.Pass);
if (pass.elements.length > 0) {
encoded.push(this.encodeStoryboardLayer(pass));
}
encoded.push("//Storyboard Layer 3 (Foreground)");
const foreground = storyboard.getLayerByType(LayerType.Foreground);
if (foreground.elements.length > 0) {
encoded.push(this.encodeStoryboardLayer(foreground));
}
encoded.push("//Storyboard Layer 4 (Overlay)");
const overlay = storyboard.getLayerByType(LayerType.Overlay);
if (overlay.elements.length > 0) {
encoded.push(this.encodeStoryboardLayer(overlay));
}
return encoded.join("\n");
}
static encodeStoryboardLayer(layer) {
const encoded = [];
layer.elements.forEach((element) => {
let _a, _b, _c;
encoded.push(this.encodeStoryboardElement(element, layer.name));
const elementWithCommands = element;
(_a =
elementWithCommands === null || elementWithCommands === void 0
? void 0
: elementWithCommands.loops) === null || _a === void 0
? void 0
: _a.forEach((loop) => {
if (loop.commands.length > 0) {
encoded.push(this.encodeCompound(loop));
encoded.push(this.encodeTimelineGroup(loop, 2));
}
});
if (
((_b =
elementWithCommands === null || elementWithCommands === void 0
? void 0
: elementWithCommands.timelineGroup) === null || _b === void 0
? void 0
: _b.commands.length) > 0
) {
encoded.push(
this.encodeTimelineGroup(elementWithCommands.timelineGroup)
);
}
(_c =
elementWithCommands === null || elementWithCommands === void 0
? void 0
: elementWithCommands.triggers) === null || _c === void 0
? void 0
: _c.forEach((trigger) => {
if (trigger.commands.length > 0) {
encoded.push(this.encodeCompound(trigger));
encoded.push(this.encodeTimelineGroup(trigger, 2));
}
});
});
return encoded.join("\n");
}
static encodeStoryboardElement(element, layer) {
if (element instanceof StoryboardAnimation) {
return [
"Animation",
layer,
Origins[element.origin],
`"${element.filePath}"`,
element.startPosition,
element.frameCount,
element.frameDelay,
element.loopType,
].join(",");
}
if (element instanceof StoryboardSprite) {
return [
"Sprite",
layer,
Origins[element.origin],
`"${element.filePath}"`,
element.startPosition,
].join(",");
}
if (element instanceof StoryboardSample) {
return [
"Sample",
element.startTime,
layer,
`"${element.filePath}"`,
element.volume,
].join(",");
}
if (element instanceof StoryboardVideo) {
return ["Video", element.startTime, element.filePath, "0,0"].join(",");
}
return "";
}
static encodeCompound(compound, depth = 1) {
const indentation = "".padStart(depth, " ");
if (compound instanceof CommandLoop) {
return (
indentation +
[compound.type, compound.loopStartTime, compound.totalIterations].join(
","
)
);
}
if (compound instanceof CommandTrigger) {
return (
indentation +
[
compound.type,
compound.triggerName,
compound.triggerStartTime,
compound.triggerEndTime,
compound.groupNumber,
].join(",")
);
}
return "";
}
static encodeTimelineGroup(timelineGroup, depth = 1) {
const indentation = "".padStart(depth, " ");
const encoded = [];
const commands = timelineGroup.commands;
let shouldSkip = false;
for (let i = 0; i < commands.length; ++i) {
if (shouldSkip) {
shouldSkip = false;
continue;
}
if (i < commands.length - 1) {
const current = commands[i];
const next = commands[i + 1];
const currentMoveX = current.type === CommandType.MovementX;
const nextMoveY = next.type === CommandType.MovementY;
const sameEasing = current.easing === next.easing;
const sameStartTime = current.startTime === next.startTime;
const sameEndTime = current.endTime === next.endTime;
const sameCommand = sameEasing && sameStartTime && sameEndTime;
if (currentMoveX && nextMoveY && sameCommand) {
encoded.push(indentation + this.encodeMoveCommand(current, next));
shouldSkip = true;
continue;
}
}
encoded.push(indentation + this.encodeCommand(commands[i]));
}
return encoded.join("\n");
}
static encodeMoveCommand(moveX, moveY) {
const encoded = [
CommandType.Movement,
moveX.easing,
moveX.startTime,
moveX.startTime !== moveX.endTime ? moveX.endTime : "",
moveX.startValue,
moveY.startValue,
];
const equalX = moveX.startValue === moveX.endValue;
const equalY = moveY.startValue === moveY.endValue;
if (!equalX || !equalY) {
encoded.push(`${moveX.endValue},${moveY.endValue}`);
}
return encoded.join(",");
}
static encodeCommand(command) {
const encoded = [
command.type,
command.easing,
command.startTime,
command.startTime !== command.endTime ? command.endTime : "",
this._encodeCommandParams(command),
];
return encoded.join(",");
}
static _encodeCommandParams(command) {
if (command.type === CommandType.Parameter) {
return command.parameter;
}
if (command.type === CommandType.Color) {
const toRGB = (c) => `${c.red},${c.green},${c.blue}`;
const colorCommand = command;
const start = colorCommand.startValue;
const end = colorCommand.endValue;
return this._areValuesEqual(command)
? toRGB(start)
: toRGB(start) + "," + toRGB(end);
}
return this._areValuesEqual(command)
? `${command.startValue}`
: `${command.startValue},${command.endValue}`;
}
static _areValuesEqual(command) {
if (command.type === CommandType.VectorScale) {
const vectorCommand = command;
return vectorCommand.startValue.equals(vectorCommand.endValue);
}
if (command.type === CommandType.Color) {
const colorCommand = command;
return colorCommand.startValue.equals(colorCommand.endValue);
}
return command.startValue === command.endValue;
}
}
class BeatmapEventEncoder {
static encodeEventSection(beatmap) {
const encoded = [];
const events = beatmap.events;
encoded.push("[Events]");
encoded.push("//Background and Video events");
if (events.backgroundPath) {
encoded.push(`0,0,"${events.backgroundPath}",0,0`);
}
if (events.storyboard) {
encoded.push(StoryboardEventEncoder.encodeVideos(events.storyboard));
}
encoded.push("//Break Periods");
if (events.breaks && events.breaks.length > 0) {
events.breaks.forEach((b) => {
encoded.push(
`${EventType[EventType.Break]},${b.startTime},${b.endTime}`
);
});
}
if (events.storyboard) {
encoded.push(StoryboardEventEncoder.encodeStoryboard(events.storyboard));
}
return encoded.join("\n");
}
}
class BeatmapGeneralEncoder {
static encodeGeneralSection(beatmap) {
const encoded = ["[General]"];
const general = beatmap.general;
encoded.push(`AudioFilename:${general.audioFilename}`);
encoded.push(`AudioLeadIn:${general.audioLeadIn}`);
if (general.audioHash) {
encoded.push(`AudioHash:${general.audioHash}`);
}
encoded.push(`PreviewTime:${general.previewTime}`);
encoded.push(`Countdown:${general.countdown}`);
encoded.push(`SampleSet:${SampleSet[general.sampleSet]}`);
encoded.push(`StackLeniency:${general.stackLeniency}`);
encoded.push(`Mode:${beatmap.mode}`);
encoded.push(`LetterboxInBreaks:${+general.letterboxInBreaks}`);
if (general.storyFireInFront) {
encoded.push(`StoryFireInFront:${+general.storyFireInFront}`);
}
encoded.push(`UseSkinSprites:${+general.useSkinSprites}`);
if (general.alwaysShowPlayfield) {
encoded.push(`AlwaysShowPlayfield:${+general.alwaysShowPlayfield}`);
}
encoded.push(`OverlayPosition:${general.overlayPosition}`);
encoded.push(`SkinPreference:${general.skinPreference}`);
encoded.push(`EpilepsyWarning:${+general.epilepsyWarning}`);
encoded.push(`CountdownOffset:${general.countdownOffset}`);
encoded.push(`SpecialStyle:${+general.specialStyle}`);
encoded.push(`WidescreenStoryboard:${+general.widescreenStoryboard}`);
encoded.push(
`SamplesMatchPlaybackRate:${+general.samplesMatchPlaybackRate}`
);
return encoded.join("\n");
}
}
class BeatmapHitObjectEncoder {
static encodeHitObjects(beatmap) {
const encoded = ["[HitObjects]"];
const difficulty = beatmap.difficulty;
const hitObjects = beatmap.hitObjects;
hitObjects.forEach((hitObject) => {
const general = [];
const position = hitObject.startPosition;
const startPosition = new Vector2(
position ? position.x : 256,
position ? position.y : 192
);
if (beatmap.mode === 3) {
const totalColumns = Math.trunc(Math.max(1, difficulty.circleSize));
const multiplier = Math.round((512 / totalColumns) * 100000) / 100000;
const column = hitObject.startX;
startPosition.x =
Math.ceil(column * multiplier) + Math.trunc(multiplier / 2);
}
general.push(startPosition.toString());
general.push(hitObject.startTime.toString());
general.push(hitObject.hitType.toString());
general.push(hitObject.hitSound.toString());
const extras = [];
if (hitObject.hitType & HitType.Slider) {
const slider = hitObject;
extras.push(this.encodePathData(slider, startPosition));
} else if (hitObject.hitType & HitType.Spinner) {
const spinner = hitObject;
extras.push(this.encodeEndTimeData(spinner));
} else if (hitObject.hitType & HitType.Hold) {
const hold = hitObject;
extras.push(this.encodeEndTimeData(hold));
}
const set = [];
const normal = hitObject.samples.find(
(s) => s.hitSound === HitSound[HitSound.Normal]
);
const addition = hitObject.samples.find(
(s) => s.hitSound !== HitSound[HitSound.Normal]
);
let normalSet = SampleSet.None;
let additionSet = SampleSet.None;
if (normal) {
normalSet = SampleSet[normal.sampleSet];
}
if (addition) {
additionSet = SampleSet[addition.sampleSet];
}
set[0] = normalSet.toString();
set[1] = additionSet.toString();
set[2] = hitObject.samples[0].customIndex.toString();
set[3] = hitObject.samples[0].volume.toString();
set[4] = hitObject.samples[0].filename;
extras.push(set.join(":"));
const generalLine = general.join(",");
const extrasLine =
hitObject.hitType & HitType.Hold ? extras.join(":") : extras.join(",");
encoded.push([generalLine, extrasLine].join(","));
});
return encoded.join("\n");
}
static encodePathData(slider, offset) {
const path = [];
let lastType;
slider.path.controlPoints.forEach((point, i) => {
if (point.type !== null) {
let needsExplicitSegment =
point.type !== lastType || point.type === PathType.PerfectCurve;
if (i > 1) {
const p1 = offset.add(slider.path.controlPoints[i - 1].position);
const p2 = offset.add(slider.path.controlPoints[i - 2].position);
if (~~p1.x === ~~p2.x && ~~p1.y === ~~p2.y) {
needsExplicitSegment = true;
}
}
if (needsExplicitSegment) {
path.push(slider.path.curveType);
lastType = point.type;
} else {
path.push(
`${offset.x + point.position.x}:${offset.y + point.position.y}`
);
}
}
if (i !== 0) {
path.push(
`${offset.x + point.position.x}:${offset.y + point.position.y}`
);
}
});
const data = [];
data.push(path.join("|"));
data.push((slider.repeats + 1).toString());
data.push(slider.distance.toString());
const adds = [];
const sets = [];
slider.nodeSamples.forEach((node, nodeIndex) => {
adds[nodeIndex] = HitSound.None;
sets[nodeIndex] = [SampleSet.None, SampleSet.None];
node.forEach((sample, sampleIndex) => {
if (sampleIndex === 0) {
sets[nodeIndex][0] = SampleSet[sample.sampleSet];
} else {
adds[nodeIndex] |= HitSound[sample.hitSound];
sets[nodeIndex][1] = SampleSet[sample.sampleSet];
}
});
});
data.push(adds.join("|"));
data.push(sets.map((set) => set.join(":")).join("|"));
return data.join(",");
}
static encodeEndTimeData(hitObject) {
return hitObject.endTime.toString();
}
}
class BeatmapMetadataEncoder {
static encodeMetadataSection(beatmap) {
const encoded = ["[Metadata]"];
const metadata = beatmap.metadata;
encoded.push(`Title:${metadata.title}`);
encoded.push(`TitleUnicode:${metadata.titleUnicode}`);
encoded.push(`Artist:${metadata.artist}`);
encoded.push(`ArtistUnicode:${metadata.artistUnicode}`);
encoded.push(`Creator:${metadata.creator}`);
encoded.push(`Version:${metadata.version}`);
encoded.push(`Source:${metadata.source}`);
encoded.push(`Tags:${metadata.tags.join(" ")}`);
encoded.push(`BeatmapID:${metadata.beatmapId}`);
encoded.push(`BeatmapSetID:${metadata.beatmapSetId}`);
return encoded.join("\n");
}
}
class BeatmapTimingPointEncoder {
static encodeControlPoints(beatmap) {
const encoded = ["[TimingPoints]"];
beatmap.controlPoints.groups.forEach((group) => {
const points = group.controlPoints;
const timing = points.find((c) => c.beatLength);
if (timing) {
encoded.push(this.encodeGroup(group, true));
}
encoded.push(this.encodeGroup(group));
});
return encoded.join("\n");
}
static encodeGroup(group, useTiming = false) {
const { difficultyPoint, effectPoint, samplePoint, timingPoint } =
this.updateActualPoints(group);
const startTime = group.startTime;
let beatLength = -100;
if (difficultyPoint !== null) {
beatLength /= difficultyPoint.sliderVelocity;
}
let sampleSet = SampleSet.None;
let customIndex = 0;
let volume = 100;
if (samplePoint !== null) {
sampleSet = SampleSet[samplePoint.sampleSet];
customIndex = samplePoint.customIndex;
volume = samplePoint.volume;
}
let effects = EffectType.None;
if (effectPoint !== null) {
const kiai = effectPoint.kiai ? EffectType.Kiai : EffectType.None;
const omitFirstBarLine = effectPoint.omitFirstBarLine
? EffectType.OmitFirstBarLine
: EffectType.None;
effects |= kiai | omitFirstBarLine;
}
let timeSignature = TimeSignature.SimpleQuadruple;
let uninherited = 0;
if (useTiming && timingPoint !== null) {
beatLength = timingPoint.beatLength;
timeSignature = timingPoint.timeSignature;
uninherited = 1;
}
return [
startTime,
beatLength,
timeSignature,
sampleSet,
customIndex,
volume,
uninherited,
effects,
].join(",");
}
static updateActualPoints(group) {
let timingPoint = null;
group.controlPoints.forEach((point) => {
if (
point.pointType === ControlPointType.DifficultyPoint &&
!point.isRedundant(this.lastDifficultyPoint)
) {
this.lastDifficultyPoint = point;
}
if (
point.pointType === ControlPointType.EffectPoint &&
!point.isRedundant(this.lastEffectPoint)
) {
this.lastEffectPoint = point;
}
if (
point.pointType === ControlPointType.SamplePoint &&
!point.isRedundant(this.lastSamplePoint)
) {
this.lastSamplePoint = point;
}
if (point.pointType === ControlPointType.TimingPoint) {
timingPoint = point;
}
});
return {
timingPoint,
difficultyPoint: this.lastDifficultyPoint,
effectPoint: this.lastEffectPoint,
samplePoint: this.lastSamplePoint,
};
}
}
BeatmapTimingPointEncoder.lastDifficultyPoint = null;
BeatmapTimingPointEncoder.lastEffectPoint = null;
BeatmapTimingPointEncoder.lastSamplePoint = null;
class ReplayEncoder {
static encodeLifeBar(frames) {
if (!frames.length) {
return "";
}
return frames.map((f) => `${f.startTime}|${f.health}`).join(",");
}
static encodeReplayFrames(frames, beatmap) {
const encoded = [];
if (frames) {
let lastTime = 0;
frames.forEach((frame) => {
let _a, _b, _c;
const time = Math.round(frame.startTime);
const legacyFrame = this._getLegacyFrame(frame, beatmap);
const encodedData = [
time - lastTime,
(_a =
legacyFrame === null || legacyFrame === void 0
? void 0
: legacyFrame.mouseX) !== null && _a !== void 0
? _a
: 0,
(_b =
legacyFrame === null || legacyFrame === void 0
? void 0
: legacyFrame.mouseY) !== null && _b !== void 0
? _b
: 0,
(_c =
legacyFrame === null || legacyFrame === void 0
? void 0
: legacyFrame.buttonState) !== null && _c !== void 0
? _c
: ReplayButtonState.None,
];
encoded.push(encodedData.join("|"));
lastTime = time;
});
}
encoded.push("-12345|0|0|0");
return encoded.join(",");
}
static _getLegacyFrame(frame, beatmap) {
if (frame instanceof LegacyReplayFrame) {
return frame;
}
const convertibleFrame = frame;
if (convertibleFrame.toLegacy) {
return convertibleFrame.toLegacy(beatmap);
}
throw new Error(
"Some of the replay frames can not be converted to the legacy format!"
);
}
}
const textDecoder = new TextDecoder();
function concatBufferViews(list) {
if (list.length <= 0) {
return new Uint8Array(0);
}
const bufferLength = list.reduce((len, buf) => len + buf.byteLength, 0);
const arrayBuffer = new Uint8Array(bufferLength);
list.reduce((offset, view) => {
arrayBuffer.set(new Uint8Array(view.buffer), offset);
return offset + view.byteLength;
}, 0);
return arrayBuffer;
}
function stringifyBuffer(data) {
if (typeof data === "string") {
return data;
}
return textDecoder.decode(data);
}
class SerializationWriter {
constructor() {
this._bytesWritten = 0;
this._views = [];
}
get bytesWritten() {
return this._bytesWritten;
}
finish() {
return concatBufferViews(this._views);
}
writeByte(value) {
return this._update(1, new Uint8Array([value]));
}
writeBytes(value) {
this._bytesWritten += value.byteLength;
this._views.push(value);
return value.byteLength;
}
writeShort(value) {
return this._update(2, new Uint16Array([value]));
}
writeInteger(value) {
return this._update(4, new Int32Array([value]));
}
writeLong(value) {
return this._update(8, new BigInt64Array([value]));
}
writeDate(date) {
const epochTicks = BigInt(62135596800000);
const ticks = BigInt(date.getTime());
return this.writeLong((ticks + epochTicks) * BigInt(1e4));
}
writeString(value) {
if (value.length === 0) {
return this.writeByte(0x00);
}
let bytesWritten = this.writeByte(0x0b);
bytesWritten += this.writeULEB128(value.length);
const view = new Uint8Array(value.length);
for (let i = 0; i < value.length; ++i) {
view[i] = value.charCodeAt(i);
bytesWritten++;
}
this._update(bytesWritten, view);
return bytesWritten;
}
writeULEB128(value) {
let byte = 0;
let bytesWritten = 0;
do {
byte = value & 0x7f;
value >>= 7;
if (value !== 0) {
byte |= 0x80;
}
bytesWritten += this.writeByte(byte);
} while (value !== 0);
return bytesWritten;
}
_update(bytesWritten, buffer) {
this._bytesWritten += bytesWritten;
this._views.push(buffer);
return bytesWritten;
}
}
class BeatmapEncoder {
async encodeToPath(path, beatmap) {
if (!path.endsWith(FileFormat.Beatmap)) {
path += FileFormat.Beatmap;
}
try {
await browserFSOperation(browserFSOperation(path), { recursive: true });
await browserFSOperation(path, await this.encodeToString(beatmap));
} catch (err) {
const reason = err.message || err;
throw new Error(`Failed to encode a beatmap: ${reason}`);
}
}
encodeToString(beatmap) {
let _a;
if (
!(beatmap === null || beatmap === void 0 ? void 0 : beatmap.fileFormat)
) {
return "";
}
const fileFormat =
(_a = beatmap.fileFormat) !== null && _a !== void 0
? _a
: BeatmapEncoder.FIRST_LAZER_VERSION;
const encoded = [
`osu file format v${fileFormat}`,
BeatmapGeneralEncoder.encodeGeneralSection(beatmap),
BeatmapEditorEncoder.encodeEditorSection(beatmap),
BeatmapMetadataEncoder.encodeMetadataSection(beatmap),
BeatmapDifficultyEncoder.encodeDifficultySection(beatmap),
BeatmapEventEncoder.encodeEventSection(beatmap),
BeatmapTimingPointEncoder.encodeControlPoints(beatmap),
BeatmapColorEncoder.encodeColors(beatmap),
BeatmapHitObjectEncoder.encodeHitObjects(beatmap),
];
return encoded.join("\n\n") + "\n";
}
}
BeatmapEncoder.FIRST_LAZER_VERSION = 128;
class StoryboardEncoder {
async encodeToPath(path, storyboard) {
if (!path.endsWith(FileFormat.Storyboard)) {
path += FileFormat.Storyboard;
}
try {
await browserFSOperation(browserFSOperation(path), { recursive: true });
await browserFSOperation(path, await this.encodeToString(storyboard));
} catch (err) {
const reason = err.message || err;
throw new Error(`Failed to encode a storyboard: ${reason}`);
}
}
encodeToString(storyboard) {
if (!(storyboard instanceof Storyboard)) {
return "";
}
const encoded = [
`osu file format v${storyboard.fileFormat}`,
StoryboardEventEncoder.encodeEventSection(storyboard),
];
return encoded.join("\n\n") + "\n";
}
}
class LZMA {
static async decompress(data) {
try {
return await this._lzma.decompress(data);
} catch {
return "";
}
}
static async compress(data) {
try {
return await this._lzma.compress(data, 1);
} catch {
return new Uint8Array([]);
}
}
}
LZMA._lzma = getLZMAInstance();
function getLZMAInstance() {
return {
decompress(data) {
return new Promise((res, rej) => {
decompress(data, (result, err) => {
err
? rej(err)
: res(
typeof result === "string"
? result
: new Uint8Array(result).toString()
);
});
});
},
compress(data) {
return new Promise((res, rej) => {
compress(data, 1, (result, err) => {
err ? rej(err) : res(new Uint8Array(result));
});
});
},
};
}
class ScoreEncoder {
async encodeToPath(path, score, beatmap) {
if (!path.endsWith(FileFormat.Replay)) {
path += FileFormat.Replay;
}
const data = await this.encodeToBuffer(score, beatmap);
try {
await browserFSOperation(browserFSOperation(path), { recursive: true });
await browserFSOperation(path, new Uint8Array(data));
} catch (err) {
const reason = err.message || err;
throw new Error(`Failed to encode a score: ${reason}`);
}
}
async encodeToBuffer(score, beatmap) {
let _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
if (
typeof ((_a =
score === null || score === void 0 ? void 0 : score.info) === null ||
_a === void 0
? void 0
: _a.id) !== "number"
) {
return new Uint8Array();
}
const writer = new SerializationWriter();
try {
writer.writeByte(score.info.rulesetId);
writer.writeInteger(
(_c =
(_b = score.replay) === null || _b === void 0
? void 0
: _b.gameVersion) !== null && _c !== void 0
? _c
: ScoreEncoder.DEFAULT_GAME_VERSION
);
writer.writeString(
(_d = score.info.beatmapHashMD5) !== null && _d !== void 0 ? _d : ""
);
writer.writeString(score.info.username);
writer.writeString(
(_f =
(_e = score.replay) === null || _e === void 0
? void 0
: _e.hashMD5) !== null && _f !== void 0
? _f
: ""
);
writer.writeShort(score.info.count300);
writer.writeShort(score.info.count100);
writer.writeShort(score.info.count50);
writer.writeShort(score.info.countGeki);
writer.writeShort(score.info.countKatu);
writer.writeShort(score.info.countMiss);
writer.writeInteger(score.info.totalScore);
writer.writeShort(score.info.maxCombo);
writer.writeByte(Number(score.info.perfect));
writer.writeInteger(
((_h =
(_g = score.info.mods) === null || _g === void 0
? void 0
: _g.bitwise) !== null && _h !== void 0
? _h
: Number(score.info.rawMods)) || 0
);
writer.writeString(
ReplayEncoder.encodeLifeBar(
(_k =
(_j = score.replay) === null || _j === void 0
? void 0
: _j.lifeBar) !== null && _k !== void 0
? _k
: []
)
);
writer.writeDate(score.info.date);
if (score.replay) {
const replayData = ReplayEncoder.encodeReplayFrames(
score.replay.frames,
beatmap
);
const encodedData = await LZMA.compress(replayData);
writer.writeInteger(encodedData.byteLength);
writer.writeBytes(encodedData);
} else {
writer.writeInteger(0);
}
writer.writeLong(BigInt(score.info.id));
return writer.finish();
} catch (err) {
const reason = err.message || err;
throw new Error(`Failed to encode a score: '${reason}'`);
}
}
}
ScoreEncoder.DEFAULT_GAME_VERSION = 20230621;
class BeatmapHitObjectDecoder {
static handleLine(line, beatmap, offset) {
const data = line.split(",").map((v) => v.trim());
const hitType = Parsing.parseInt(data[3]);
const hitObject = this.createHitObject(hitType);
hitObject.startX = Parsing.parseInt(data[0], Parsing.MAX_COORDINATE_VALUE);
hitObject.startY = Parsing.parseInt(data[1], Parsing.MAX_COORDINATE_VALUE);
hitObject.startTime = Parsing.parseFloat(data[2]) + offset;
hitObject.hitType = hitType;
hitObject.hitSound = Parsing.parseInt(data[4]);
const bankInfo = new SampleBank();
this.addExtras(
data.slice(5),
hitObject,
bankInfo,
offset,
beatmap.fileFormat
);
this.addComboOffset(hitObject, beatmap);
if (hitObject.samples.length === 0) {
hitObject.samples = this.convertSoundType(hitObject.hitSound, bankInfo);
}
beatmap.hitObjects.push(hitObject);
}
static addComboOffset(hitObject, beatmap) {
const isStandard = beatmap.originalMode === 0;
const isCatch = beatmap.originalMode === 2;
if (!isStandard && !isCatch) {
return;
}
const comboObject = hitObject;
const comboOffset = Math.trunc(
(hitObject.hitType & HitType.ComboOffset) >> 4
);
const newCombo = !!(hitObject.hitType & HitType.NewCombo);
if (
hitObject.hitType & HitType.Normal ||
hitObject.hitType & HitType.Slider
) {
comboObject.isNewCombo = newCombo || this._forceNewCombo;
comboObject.comboOffset = comboOffset + this._extraComboOffset;
this._forceNewCombo = false;
this._extraComboOffset = 0;
}
if (hitObject.hitType & HitType.Spinner) {
this._forceNewCombo = beatmap.fileFormat <= 8 || newCombo || false;
this._extraComboOffset += comboOffset;
}
}
static addExtras(data, hitObject, bankInfo, offset, fileFormat) {
if (hitObject.hitType & HitType.Normal && data.length > 0) {
this.readCustomSampleBanks(data[0], bankInfo);
}
if (hitObject.hitType & HitType.Slider) {
return this.addSliderExtras(data, hitObject, bankInfo, fileFormat);
}
if (hitObject.hitType & HitType.Spinner) {
return this.addSpinnerExtras(data, hitObject, bankInfo, offset);
}
if (hitObject.hitType & HitType.Hold) {
return this.addHoldExtras(data, hitObject, bankInfo, offset);
}
}
static addSliderExtras(extras, slider, bankInfo, fileFormat) {
const pathString = extras[0];
const offset = slider.startPosition;
const repeats = Parsing.parseInt(extras[1]);
if (slider.repeats > 9000) {
throw new Error("Repeat count is way too high");
}
slider.repeats = Math.max(0, repeats - 1);
slider.path.controlPoints = this.convertPathString(
pathString,
offset,
fileFormat
);
slider.path.curveType = slider.path.controlPoints[0].type;
if (extras.length > 2) {
const length = Parsing.parseFloat(
extras[2],
Parsing.MAX_COORDINATE_VALUE
);
slider.path.expectedDistance = Math.max(0, length);
}
if (extras.length > 5) {
this.readCustomSampleBanks(extras[5], bankInfo);
}
slider.samples = this.convertSoundType(slider.hitSound, bankInfo);
slider.nodeSamples = this.getSliderNodeSamples(extras, slider, bankInfo);
}
static addSpinnerExtras(extras, spinner, bankInfo, offset) {
spinner.endTime = Parsing.parseInt(extras[0]) + offset;
if (extras.length > 1) {
this.readCustomSampleBanks(extras[1], bankInfo);
}
}
static addHoldExtras(extras, hold, bankInfo, offset) {
hold.endTime = hold.startTime;
if (extras.length > 0 && extras[0]) {
const ss = extras[0].split(":");
hold.endTime = Math.max(hold.endTime, Parsing.parseFloat(ss[0])) + offset;
this.readCustomSampleBanks(ss.slice(1).join(":"), bankInfo);
}
}
static getSliderNodeSamples(extras, slider, bankInfo) {
const nodes = slider.repeats + 2;
const nodeBankInfos = [];
for (let i = 0; i < nodes; ++i) {
nodeBankInfos.push(bankInfo.clone());
}
if (extras.length > 4 && extras[4].length > 0) {
const sets = extras[4].split("|");
for (let i = 0; i < nodes; ++i) {
if (i >= sets.length) {
break;
}
this.readCustomSampleBanks(sets[i], nodeBankInfos[i]);
}
}
const nodeSoundTypes = [];
for (let i = 0; i < nodes; ++i) {
nodeSoundTypes.push(slider.hitSound);
}
if (extras.length > 3 && extras[3].length > 0) {
const adds = extras[3].split("|");
for (let i = 0; i < nodes; ++i) {
if (i >= adds.length) {
break;
}
nodeSoundTypes[i] = parseInt(adds[i]) || HitSound.None;
}
}
const nodeSamples = [];
for (let i = 0; i < nodes; i++) {
nodeSamples.push(
this.convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])
);
}
return nodeSamples;
}
static convertPathString(pathString, offset, fileFormat) {
const pathSplit = pathString.split("|").map((p) => p.trim());
const controlPoints = [];
let startIndex = 0;
let endIndex = 0;
let isFirst = true;
while (++endIndex < pathSplit.length) {
if (pathSplit[endIndex].length > 1) {
continue;
}
const points = pathSplit.slice(startIndex, endIndex);
const endPoint =
endIndex < pathSplit.length - 1 ? pathSplit[endIndex + 1] : null;
const convertedPoints = this.convertPoints(
points,
endPoint,
isFirst,
offset,
fileFormat
);
for (const point of convertedPoints) {
controlPoints.push(...point);
}
startIndex = endIndex;
isFirst = false;
}
if (endIndex > startIndex) {
const points = pathSplit.slice(startIndex, endIndex);
const convertedPoints = this.convertPoints(
points,
null,
isFirst,
offset,
fileFormat
);
for (const point of convertedPoints) {
controlPoints.push(...point);
}
}
return controlPoints;
}
static *convertPoints(points, endPoint, isFirst, offset, fileFormat) {
const readOffset = isFirst ? 1 : 0;
const endPointLength = endPoint !== null ? 1 : 0;
const vertices = [];
if (readOffset === 1) {
vertices[0] = new PathPoint();
}
for (let i = 1; i < points.length; ++i) {
vertices[readOffset + i - 1] = readPoint(points[i], offset);
}
if (endPoint !== null) {
vertices[vertices.length - 1] = readPoint(endPoint, offset);
}
let type = this.convertPathType(points[0]);
if (type === PathType.PerfectCurve) {
if (vertices.length !== 3) {
type = PathType.Bezier;
} else if (isLinear(vertices)) {
type = PathType.Linear;
}
}
vertices[0].type = type;
let startIndex = 0;
let endIndex = 0;
while (++endIndex < vertices.length - endPointLength) {
if (
!vertices[endIndex].position.equals(vertices[endIndex - 1].position)
) {
continue;
}
const isStableBeatmap = fileFormat < BeatmapEncoder.FIRST_LAZER_VERSION;
if (type === PathType.Catmull && endIndex > 1 && isStableBeatmap) {
continue;
}
if (endIndex === vertices.length - endPointLength - 1) {
continue;
}
vertices[endIndex - 1].type = type;
yield vertices.slice(startIndex, endIndex);
startIndex = endIndex + 1;
}
if (endIndex > startIndex) {
yield vertices.slice(startIndex, endIndex);
}
function readPoint(point, offset) {
const coords = point.split(":").map((v) => {
return Math.trunc(Parsing.parseFloat(v, Parsing.MAX_COORDINATE_VALUE));
});
const pos = new Vector2(coords[0], coords[1]).fsubtract(offset);
return new PathPoint(pos);
}
function isLinear(p) {
const yx =
(p[1].position.y - p[0].position.y) *
(p[2].position.x - p[0].position.x);
const xy =
(p[1].position.x - p[0].position.x) *
(p[2].position.y - p[0].position.y);
const acceptableDifference = 0.001;
return Math.abs(yx - xy) < acceptableDifference;
}
}
static convertPathType(type) {
switch (type) {
default:
case "C":
return PathType.Catmull;
case "B":
return PathType.Bezier;
case "L":
return PathType.Linear;
case "P":
return PathType.PerfectCurve;
}
}
static readCustomSampleBanks(hitSample, bankInfo) {
if (!hitSample) {
return;
}
const split = hitSample.split(":");
bankInfo.normalSet = Parsing.parseInt(split[0]);
bankInfo.additionSet = Parsing.parseInt(split[1]);
if (bankInfo.additionSet === SampleSet.None) {
bankInfo.additionSet = bankInfo.normalSet;
}
if (split.length > 2) {
bankInfo.customIndex = Parsing.parseInt(split[2]);
}
if (split.length > 3) {
bankInfo.volume = Math.max(0, Parsing.parseInt(split[3]));
}
bankInfo.filename = split.length > 4 ? split[4] : "";
}
static convertSoundType(type, bankInfo) {
if (bankInfo.filename) {
const sample = new HitSample();
sample.filename = bankInfo.filename;
sample.volume = bankInfo.volume;
return [sample];
}
const soundTypes = [new HitSample()];
soundTypes[0].hitSound = HitSound[HitSound.Normal];
soundTypes[0].sampleSet = SampleSet[bankInfo.normalSet];
soundTypes[0].isLayered =
type !== HitSound.None && !(type & HitSound.Normal);
if (type & HitSound.Finish) {
const sample = new HitSample();
sample.hitSound = HitSound[HitSound.Finish];
soundTypes.push(sample);
}
if (type & HitSound.Whistle) {
const sample = new HitSample();
sample.hitSound = HitSound[HitSound.Whistle];
soundTypes.push(sample);
}
if (type & HitSound.Clap) {
const sample = new HitSample();
sample.hitSound = HitSound[HitSound.Clap];
soundTypes.push(sample);
}
soundTypes.forEach((sound, i) => {
sound.sampleSet =
i !== 0
? SampleSet[bankInfo.additionSet]
: SampleSet[bankInfo.normalSet];
sound.volume = bankInfo.volume;
sound.customIndex = 0;
if (bankInfo.customIndex >= 2) {
sound.customIndex = bankInfo.customIndex;
}
});
return soundTypes;
}
static createHitObject(hitType) {
if (hitType & HitType.Normal) {
return new HittableObject();
}
if (hitType & HitType.Slider) {
return new SlidableObject();
}
if (hitType & HitType.Spinner) {
return new SpinnableObject();
}
if (hitType & HitType.Hold) {
return new HoldableObject();
}
throw new Error(`Unknown hit object type: ${hitType}!`);
}
}
BeatmapHitObjectDecoder._forceNewCombo = false;
BeatmapHitObjectDecoder._extraComboOffset = 0;
class BeatmapMetadataDecoder {
static handleLine(line, beatmap) {
const [key, ...values] = line.split(":");
const value = values.join(":").trim();
switch (key.trim()) {
case "Title":
beatmap.metadata.title = value;
break;
case "TitleUnicode":
beatmap.metadata.titleUnicode = value;
break;
case "Artist":
beatmap.metadata.artist = value;
break;
case "ArtistUnicode":
beatmap.metadata.artistUnicode = value;
break;
case "Creator":
beatmap.metadata.creator = value;
break;
case "Version":
beatmap.metadata.version = value;
break;
case "Source":
beatmap.metadata.source = value;
break;
case "Tags":
beatmap.metadata.tags = value.split(" ");
break;
case "BeatmapID":
beatmap.metadata.beatmapId = Parsing.parseInt(value);
break;
case "BeatmapSetID":
beatmap.metadata.beatmapSetId = Parsing.parseInt(value);
}
}
}
class BeatmapTimingPointDecoder {
static handleLine(line, beatmap, offset) {
this.controlPoints = beatmap.controlPoints;
const data = line.split(",");
let timeSignature = TimeSignature.SimpleQuadruple;
let sampleSet = SampleSet[SampleSet.None];
let customIndex = 0;
let volume = 100;
let timingChange = true;
let effects = EffectType.None;
if (data.length > 2) {
switch (data.length) {
default:
case 8:
effects = Parsing.parseInt(data[7]);
case 7:
timingChange = data[6] === "1";
case 6:
volume = Parsing.parseInt(data[5]);
case 5:
customIndex = Parsing.parseInt(data[4]);
case 4:
sampleSet = SampleSet[Parsing.parseInt(data[3])];
case 3:
timeSignature = Parsing.parseInt(data[2]);
}
}
if (timeSignature < 1) {
throw new Error("The numerator of a time signature must be positive.");
}
const startTime = Parsing.parseFloat(data[0]) + offset;
const beatLength = Parsing.parseFloat(
data[1],
Parsing.MAX_PARSE_VALUE,
true
);
let bpmMultiplier = 1;
let speedMultiplier = 1;
if (beatLength < 0) {
speedMultiplier = 100 / -beatLength;
bpmMultiplier = Math.min(Math.fround(-beatLength), 10000);
bpmMultiplier = Math.max(10, bpmMultiplier) / 100;
}
if (timingChange && Number.isNaN(beatLength)) {
throw new Error("Beat length cannot be NaN in a timing control point");
}
if (timingChange) {
const timingPoint = new TimingPoint();
timingPoint.beatLength = beatLength;
timingPoint.timeSignature = timeSignature;
this.addControlPoint(timingPoint, startTime, true);
}
const difficultyPoint = new DifficultyPoint();
difficultyPoint.bpmMultiplier = bpmMultiplier;
difficultyPoint.sliderVelocity = speedMultiplier;
difficultyPoint.generateTicks = !Number.isNaN(beatLength);
difficultyPoint.isLegacy = true;
this.addControlPoint(difficultyPoint, startTime, timingChange);
const effectPoint = new EffectPoint();
effectPoint.kiai = (effects & EffectType.Kiai) > 0;
effectPoint.omitFirstBarLine = (effects & EffectType.OmitFirstBarLine) > 0;
if (beatmap.originalMode === 1 || beatmap.originalMode === 3) {
effectPoint.scrollSpeed = speedMultiplier;
}
this.addControlPoint(effectPoint, startTime, timingChange);
const samplePoint = new SamplePoint();
samplePoint.sampleSet = sampleSet;
samplePoint.customIndex = customIndex;
samplePoint.volume = volume;
this.addControlPoint(samplePoint, startTime, timingChange);
}
static addControlPoint(point, time, timingChange) {
if (time !== this.pendingTime) {
this.flushPendingPoints();
}
timingChange
? this.pendingPoints.unshift(point)
: this.pendingPoints.push(point);
this.pendingTime = time;
}
static flushPendingPoints() {
const pendingTime = this.pendingTime;
const pendingPoints = this.pendingPoints;
const controlPoints = this.controlPoints;
const pendingTypes = this.pendingTypes;
let i = pendingPoints.length;
while (--i >= 0) {
if (pendingTypes.includes(pendingPoints[i].pointType)) {
continue;
}
pendingTypes.push(pendingPoints[i].pointType);
controlPoints.add(pendingPoints[i], pendingTime);
}
this.pendingPoints = [];
this.pendingTypes = [];
}
}
BeatmapTimingPointDecoder.pendingTime = 0;
BeatmapTimingPointDecoder.pendingTypes = [];
BeatmapTimingPointDecoder.pendingPoints = [];
class ReplayDecoder {
static decodeLifeBar(data) {
if (!data) {
return [];
}
const lifeBarFrames = [];
const frames = data.split(",");
for (let i = 0; i < frames.length; ++i) {
if (!frames[i]) {
continue;
}
const frameData = frames[i].split("|");
if (frameData.length < 2) {
continue;
}
const frame = this.handleLifeBarFrame(frameData);
lifeBarFrames.push(frame);
}
return lifeBarFrames;
}
static handleLifeBarFrame(frameData) {
return new LifeBarFrame(
Parsing.parseInt(frameData[0]),
Parsing.parseFloat(frameData[1])
);
}
static decodeReplayFrames(data) {
if (!data) {
return [];
}
let lastTime = 0;
let lastKeyPress = null;
const replayFrames = [];
const frames = data.split(",");
for (let i = 0; i < frames.length; ++i) {
if (!frames[i]) {
continue;
}
const frameData = frames[i].split("|");
if (frameData.length < 4) {
continue;
}
if (frameData[0] === "-12345") {
continue;
}
let currentKeyPress = Parsing.parseInt(frameData[3]);
if (currentKeyPress === lastKeyPress) {
currentKeyPress = 0; // Set to 0 if key press state hasn't changed
} else {
lastKeyPress = currentKeyPress; // Update lastKeyPress with new state
}
const replayFrame = new LegacyReplayFrame(
0.0,
Parsing.parseFloat(frameData[0]),
new Vector2(
Parsing.parseFloat(frameData[1], Parsing.MAX_COORDINATE_VALUE),
Parsing.parseFloat(frameData[2], Parsing.MAX_COORDINATE_VALUE)
),
currentKeyPress
);
lastTime += replayFrame.interval;
if (i < 2 && replayFrame.mouseX === 256 && replayFrame.mouseY === -500) {
continue;
}
if (replayFrame.interval < 0) {
continue;
}
replayFrame.startTime = lastTime;
replayFrames.push(replayFrame);
}
return replayFrames;
}
}
class SerializationReader {
constructor(buffer) {
this._bytesRead = 0;
this.view = new DataView(new Uint8Array(buffer).buffer);
}
get bytesRead() {
return this._bytesRead;
}
get remainingBytes() {
return this.view.byteLength - this._bytesRead;
}
readByte() {
return this.view.getUint8(this._bytesRead++);
}
readBytes(length) {
const bytes = this.view.buffer.slice(
this._bytesRead,
this._bytesRead + length
);
this._bytesRead += length;
return new Uint8Array(bytes);
}
readShort() {
const value = this.view.getUint16(this._bytesRead, true);
this._bytesRead += 2;
return value;
}
readInteger() {
const value = this.view.getInt32(this._bytesRead, true);
this._bytesRead += 4;
return value;
}
readLong() {
const value = this.view.getBigInt64(this._bytesRead, true);
this._bytesRead += 8;
return value;
}
readDate() {
const epochTicks = 62135596800000;
return new Date(Number(this.readLong() / BigInt(1e4)) - epochTicks);
}
readULEB128() {
let val = 0;
let shift = 0;
let byte = 0;
do {
byte = this.readByte();
val |= (byte & 0x7f) << shift;
shift += 7;
} while ((byte & 0x80) !== 0);
return val;
}
readString() {
if (this.readByte() !== 0x0b) {
return "";
}
const length = this.readULEB128();
return length > 0 ? stringifyBuffer(this.readBytes(length)) : "";
}
}
class StoryboardGeneralDecoder {
static handleLine(line, storyboard) {
const [key, ...values] = line.split(":").map((v) => v.trim());
const value = values.join(" ");
switch (key) {
case "UseSkinSprites":
storyboard.useSkinSprites = value === "1";
}
}
}
class StoryboardVariableDecoder {
static handleLine(line, variables) {
if (!line.startsWith("$")) {
return;
}
const pair = line.split("=");
if (pair.length === 2) {
variables.set(pair[0], pair[1].trimEnd());
}
}
static decodeVariables(line, variables) {
if (!line.includes("$") || !variables.size) {
return line;
}
variables.forEach((value, key) => {
line = line.replace(key, value);
});
return line;
}
}
class Decoder {
async _getFileBuffer(path) {
try {
await browserFSOperation(path);
} catch {
throw new Error("File doesn't exist!");
}
try {
return await browserFSOperation(path);
} catch {
throw new Error("File can't be read!");
}
}
async _getFileUpdateDate(path) {
try {
return (await browserFSOperation(path)).mtime;
} catch {
throw new Error("Failed to get last file update date!");
}
}
}
class SectionMap extends Map {
constructor() {
super(...arguments);
this.currentSection = null;
}
get(section) {
let _a;
return (_a = super.get(section)) !== null && _a !== void 0 ? _a : false;
}
set(section, state = true) {
return super.set(section, state);
}
reset() {
this.forEach((_, key, map) => {
map.set(key, true);
});
this.currentSection = null;
return this;
}
get hasEnabledSections() {
for (const state of this.values()) {
if (state) {
return true;
}
}
return false;
}
get isSectionEnabled() {
return this.currentSection ? this.get(this.currentSection) : false;
}
}
class SectionDecoder extends Decoder {
constructor() {
super(...arguments);
this._lines = null;
this._sectionMap = new SectionMap();
}
_getLines(data) {
let lines = null;
if (data.constructor === Array) {
lines = data;
}
if (!lines || !lines.length) {
throw new Error("Data not found!");
}
return lines;
}
_parseLine(line, output) {
if (this._shouldSkipLine(line)) {
return LineType.Empty;
}
line = this._preprocessLine(line);
if (line.includes("osu file format v")) {
return LineType.FileFormat;
}
if (line.startsWith("[") && line.endsWith("]")) {
const section = line.slice(1, -1);
if (this._sectionMap.currentSection) {
this._sectionMap.set(this._sectionMap.currentSection, false);
this._sectionMap.currentSection = null;
}
if (!this._sectionMap.hasEnabledSections) {
return LineType.Break;
}
if (section in Section) {
this._sectionMap.currentSection = section;
}
return LineType.Section;
}
if (!this._sectionMap.isSectionEnabled) {
return LineType.Empty;
}
try {
this._parseSectionData(line, output);
return LineType.Data;
} catch {
return LineType.Empty;
}
}
_parseSectionData(line, output) {
const outputWithColors = output;
if (this._sectionMap.currentSection !== Section.Colours) {
return;
}
if (
!(outputWithColors === null || outputWithColors === void 0
? void 0
: outputWithColors.colors)
) {
return;
}
BeatmapColorDecoder.handleLine(line, outputWithColors);
}
_preprocessLine(line) {
if (this._sectionMap.currentSection !== Section.Metadata) {
line = this._stripComments(line);
}
return line.trimEnd();
}
_shouldSkipLine(line) {
return typeof line !== "string" || !line || line.startsWith("//");
}
_stripComments(line) {
const index = line.indexOf("//");
return index > 0 ? line.substring(0, index) : line;
}
_reset() {
this._sectionMap.reset();
this._lines = null;
}
_setEnabledSections(options) {
this._sectionMap.set(
Section.Colours,
options === null || options === void 0 ? void 0 : options.parseColours
);
}
}
class StoryboardDecoder extends SectionDecoder {
constructor() {
super(...arguments);
this._variables = new Map();
}
async decodeFromPath(firstPath, secondPath) {
if (
!firstPath.endsWith(FileFormat.Beatmap) &&
!firstPath.endsWith(FileFormat.Storyboard)
) {
throw new Error(
`Wrong format of the first file! Only ${FileFormat.Beatmap} and ${FileFormat.Storyboard} files are supported!`
);
}
if (typeof secondPath === "string") {
if (!secondPath.endsWith(FileFormat.Storyboard)) {
throw new Error(
`Wrong format of the second file! Only ${FileFormat.Storyboard} files are supported as a second argument!`
);
}
}
try {
const firstData = await this._getFileBuffer(firstPath);
const secondData =
typeof secondPath === "string"
? await this._getFileBuffer(firstPath)
: undefined;
return this.decodeFromBuffer(firstData, secondData);
} catch (err) {
const reason = err.message || err;
throw new Error(`Failed to decode a storyboard: '${reason}'`);
}
}
decodeFromBuffer(firstBuffer, secondBuffer) {
const firstString = stringifyBuffer(firstBuffer);
const secondString = secondBuffer
? stringifyBuffer(secondBuffer)
: undefined;
return this.decodeFromString(firstString, secondString);
}
decodeFromString(firstString, secondString) {
if (typeof firstString !== "string") {
firstString = String(firstString);
}
if (
typeof secondString !== "string" &&
typeof secondString !== "undefined"
) {
secondString = String(secondString);
}
const firstData = firstString.split(/\r?\n/);
const secondData =
secondString === null || secondString === void 0
? void 0
: secondString.split(/\r?\n/);
return this.decodeFromLines(firstData, secondData);
}
decodeFromLines(firstData, secondData) {
const storyboard = new Storyboard();
this._reset();
this._setEnabledSections();
this._lines = [
...this._getLines(firstData),
...(secondData ? this._getLines(secondData) : []),
];
for (let i = 0; i < this._lines.length; ++i) {
const type = this._parseLine(this._lines[i], storyboard);
if (type === LineType.Break) {
break;
}
}
storyboard.variables = this._variables;
return storyboard;
}
_parseLine(line, storyboard) {
if (line.includes("osu file format v")) {
storyboard.fileFormat = Parsing.parseInt(line.split("v")[1]);
return LineType.FileFormat;
}
return super._parseLine(line, storyboard);
}
_parseSectionData(line, storyboard) {
switch (this._sectionMap.currentSection) {
case Section.General:
return StoryboardGeneralDecoder.handleLine(line, storyboard);
case Section.Events:
return StoryboardEventDecoder.handleLine(line, storyboard);
case Section.Variables:
return StoryboardVariableDecoder.handleLine(line, this._variables);
}
super._parseSectionData(line, storyboard);
}
_setEnabledSections() {
super._setEnabledSections();
this._sectionMap.set(Section.General);
this._sectionMap.set(Section.Variables);
this._sectionMap.set(Section.Events);
}
_preprocessLine(line) {
line = StoryboardVariableDecoder.decodeVariables(line, this._variables);
return super._preprocessLine(line);
}
_reset() {
super._reset();
this._sectionMap.reset();
this._sectionMap.currentSection = Section.Events;
}
}
class BeatmapDecoder extends SectionDecoder {
constructor() {
super(...arguments);
this._offset = 0;
this._sbLines = null;
}
async decodeFromPath(path, options) {
if (!path.endsWith(FileFormat.Beatmap)) {
throw new Error(
`Wrong file format! Only ${FileFormat.Beatmap} files are supported!`
);
}
try {
const data = await this._getFileBuffer(path);
const beatmap = this.decodeFromBuffer(data, options);
beatmap.fileUpdateDate = await this._getFileUpdateDate(path);
return beatmap;
} catch (err) {
const reason = err.message || err;
throw new Error(`Failed to decode a beatmap: ${reason}`);
}
}
decodeFromBuffer(data, options) {
return this.decodeFromString(stringifyBuffer(data), options);
}
decodeFromString(str, options) {
str = typeof str !== "string" ? String(str) : str;
return this.decodeFromLines(str.split(/\r?\n/), options);
}
decodeFromLines(data, options) {
const beatmap = new Beatmap();
this._reset();
this._lines = this._getLines(data);
this._setEnabledSections(typeof options !== "boolean" ? options : {});
this._sbLines = this._shouldParseStoryboard(options) ? [] : null;
const fileFormatLine = this._lines[0].trim();
if (!fileFormatLine.startsWith("osu file format v")) {
throw new Error("Not a valid beatmap!");
}
for (let i = 0; i < this._lines.length; ++i) {
const type = this._parseLine(this._lines[i], beatmap);
if (type === LineType.Break) {
break;
}
}
BeatmapTimingPointDecoder.flushPendingPoints();
for (let i = 0; i < beatmap.hitObjects.length; ++i) {
beatmap.hitObjects[i].applyDefaults(
beatmap.controlPoints,
beatmap.difficulty
);
}
beatmap.hitObjects.sort((a, b) => a.startTime - b.startTime);
if (this._sbLines && this._sbLines.length) {
const storyboardDecoder = new StoryboardDecoder();
beatmap.events.storyboard = storyboardDecoder.decodeFromLines(
this._sbLines
);
beatmap.events.storyboard.useSkinSprites = beatmap.general.useSkinSprites;
beatmap.events.storyboard.colors = beatmap.colors;
beatmap.events.storyboard.fileFormat = beatmap.fileFormat;
}
return beatmap;
}
_parseLine(line, beatmap) {
if (line.includes("osu file format v")) {
beatmap.fileFormat = Parsing.parseInt(line.split("v")[1]);
return LineType.FileFormat;
}
return super._parseLine(line, beatmap);
}
_parseSectionData(line, beatmap) {
switch (this._sectionMap.currentSection) {
case Section.General:
return BeatmapGeneralDecoder.handleLine(line, beatmap, this._offset);
case Section.Editor:
return BeatmapEditorDecoder.handleLine(line, beatmap);
case Section.Metadata:
return BeatmapMetadataDecoder.handleLine(line, beatmap);
case Section.Difficulty:
return BeatmapDifficultyDecoder.handleLine(line, beatmap);
case Section.Events:
return BeatmapEventDecoder.handleLine(
line,
beatmap,
this._sbLines,
this._offset
);
case Section.TimingPoints:
return BeatmapTimingPointDecoder.handleLine(
line,
beatmap,
this._offset
);
case Section.HitObjects:
return BeatmapHitObjectDecoder.handleLine(line, beatmap, this._offset);
}
super._parseSectionData(line, beatmap);
}
_setEnabledSections(options) {
super._setEnabledSections(options);
this._sectionMap.set(
Section.General,
options === null || options === void 0 ? void 0 : options.parseGeneral
);
this._sectionMap.set(
Section.Editor,
options === null || options === void 0 ? void 0 : options.parseEditor
);
this._sectionMap.set(
Section.Metadata,
options === null || options === void 0 ? void 0 : options.parseMetadata
);
this._sectionMap.set(
Section.Difficulty,
options === null || options === void 0 ? void 0 : options.parseDifficulty
);
this._sectionMap.set(
Section.Events,
options === null || options === void 0 ? void 0 : options.parseEvents
);
this._sectionMap.set(
Section.TimingPoints,
options === null || options === void 0
? void 0
: options.parseTimingPoints
);
this._sectionMap.set(
Section.HitObjects,
options === null || options === void 0 ? void 0 : options.parseHitObjects
);
}
_shouldParseStoryboard(options) {
let _a, _b;
const parsingOptions = options;
const storyboardFlag =
(_a =
parsingOptions === null || parsingOptions === void 0
? void 0
: parsingOptions.parseStoryboard) !== null && _a !== void 0
? _a
: options;
const parseSb = typeof storyboardFlag === "boolean" ? storyboardFlag : true;
const parseEvents =
(_b =
parsingOptions === null || parsingOptions === void 0
? void 0
: parsingOptions.parseEvents) !== null && _b !== void 0
? _b
: true;
return parseEvents && parseSb;
}
}
BeatmapDecoder.EARLY_VERSION_TIMING_OFFSET = 24;
class ScoreDecoder extends Decoder {
async decodeFromPath(path, parseReplay = true) {
if (!path.endsWith(FileFormat.Replay)) {
throw new Error(
`Wrong file format! Only ${FileFormat.Replay} files are supported!`
);
}
try {
const data = await this._getFileBuffer(path);
return await this.decodeFromBuffer(data, parseReplay);
} catch (err) {
const reason = err.message || err;
throw new Error(`Failed to decode a score: '${reason}'`);
}
}
async decodeFromBuffer(buffer, parseReplay = true) {
const scoreInfo = new ScoreInfo();
scoreInfo.id = 2398933774;
scoreInfo.accuracy = 0;
scoreInfo.count50 = 0;
scoreInfo.count100 = 0;
scoreInfo.count300 = 0;
scoreInfo.countGeki = 0;
scoreInfo.countKatu = 0;
scoreInfo.countMiss = 0;
scoreInfo.totalScore = 0;
scoreInfo.maxCombo = 0;
scoreInfo.perfect = false;
scoreInfo.rawMods = 0;
scoreInfo.date = new Date();
scoreInfo.beatmapHashMD5 = "d41d8cd98f00b204e9800998ecf8427e";
scoreInfo.username = "Unknown";
scoreInfo.rulesetId = 0;
let replay = null;
replay = new Replay();
replay.rawFrames = buffer;
replay.frames = ReplayDecoder.decodeReplayFrames(buffer);
return new Score(scoreInfo, replay);
}
_parseScoreId(version, reader) {
if (version >= 20140721) {
return Number(reader.readLong());
}
if (version >= 20121008) {
return reader.readInteger();
}
return 0;
}
}
export {
BeatmapDecoder,
BeatmapEncoder,
HittableObject,
HoldableObject,
ScoreDecoder,
ScoreEncoder,
SlidableObject,
SpinnableObject,
StoryboardDecoder,
StoryboardEncoder,
};