A => .gitignore +1 -0
A => beatmap/beatmap.ha +248 -0
@@ 1,248 @@
+use sort;
+
+export type beatmap = struct {
+ version: str,
+ general: general,
+ editor: editor,
+ metadata: metadata,
+ difficulty: difficulty,
+ events: []event,
+ timing_points: []timing_point,
+ colours: colours,
+ hit_objects: []hit_object,
+};
+
+export type colour = struct {
+ r: u8,
+ g: u8,
+ b: u8,
+};
+
+export type colours = struct {
+ combos: [](int, colour),
+ slider_track_override: colour,
+ slider_border: colour,
+};
+
+fn cmp_combo(a: const *void, b: const *void) int = {
+ const a = *(a: const *(int, colour)), b = *(b: const *(int, colour));
+ return if (a.0 < b.0) -1
+ else if (a.0 > b.0) 1
+ else 0;
+};
+
+fn insert_colour(cs: *colours, element: (int, colour)) void = {
+ let i = sort::lbisect(cs.combos, size((int, colour)), &element, &cmp_combo);
+ insert(cs.combos[i], element);
+};
+
+fn get_colour (cs: *colours, index: int) (colour | void) = {
+ let key = (index, colour {...});
+ match (sort::search(cs.combos, size((int, colour)), &key, &cmp_combo)) {
+ case void => void;
+ case let i: size =>
+ return cs.combos[i].1;
+ };
+};
+
+export type countdown = enum {
+ NONE,
+ NORMAL,
+ HALF,
+ DOUBLE,
+};
+
+export type sample_set = enum {
+ NORMAL,
+ SOFT,
+ DRUM,
+};
+
+export type mode = enum {
+ OSU,
+ TAIKO,
+ CATCH,
+ MANIA,
+};
+
+export type overlay_position = enum {
+ NO_CHANGE,
+ BELOW,
+ ABOVE,
+};
+
+export const general_default: general = general {
+ audio_filename = "",
+ audio_lead_in = 0,
+ audio_hash = "",
+ preview_time = -1,
+ countdown = countdown::NORMAL,
+ sample_set = sample_set::NORMAL,
+ stack_leniency = 0.7,
+ mode = mode::OSU,
+ letterbox_in_breaks = false,
+ story_fire_in_front = true,
+ use_skin_sprites = false,
+ always_show_playfield = false,
+ overlay_position = overlay_position::NO_CHANGE,
+ skin_preference = "",
+ epilepsy_warning = false,
+ countdown_offset = 0,
+ special_style = false,
+ widescreen_storyboard = false,
+ samples_match_playback_rate = false,
+};
+
+export type general = struct {
+ audio_filename: str,
+ audio_lead_in: int,
+ audio_hash: str,
+ preview_time: int,
+ countdown: countdown,
+ sample_set: sample_set,
+ stack_leniency: f64,
+ mode: mode,
+ letterbox_in_breaks: bool,
+ story_fire_in_front: bool,
+ use_skin_sprites: bool,
+ always_show_playfield: bool,
+ overlay_position: overlay_position,
+ skin_preference: str,
+ epilepsy_warning: bool,
+ countdown_offset: int,
+ special_style: bool,
+ widescreen_storyboard: bool,
+ samples_match_playback_rate: bool,
+};
+
+export type editor = struct {
+ bookmarks: []int,
+ distance_spacing: f64,
+ beat_divisor: int,
+ grid_size: int,
+ timeline_zoom: f64,
+};
+
+export type metadata = struct {
+ title: str,
+ title_unicode: str,
+ artist: str,
+ artist_unicode: str,
+ creator: str,
+ version: str,
+ source: str,
+ tags: []str,
+ beatmap_id: int,
+ beatmap_set_id: int,
+};
+
+export type difficulty = struct {
+ hp_drain_rate: f64,
+ circle_size: f64,
+ overall_difficulty: f64,
+ approach_rate: f64,
+ slider_multiplier: f64,
+ slider_tick_rate: f64,
+};
+
+export type event = struct {
+ start_time: int,
+ event: (background | video | _break),
+};
+
+export type background = struct {
+ filename: str,
+ x_offset: int,
+ y_offset: int,
+};
+
+export type video = struct {
+ filename: str,
+ x_offset: int,
+ y_offset: int,
+};
+
+export type _break = struct {
+ end_time: int,
+};
+
+export type timing_point = struct {
+ time: int,
+ beat_length: f64,
+ meter: int,
+ sample_set: sample_set,
+ sample_index: int,
+ volume: int,
+ uninherited: bool,
+ effects: effects,
+};
+
+export type effects = enum u8 {
+ KIAI_TIME = 1 << 0,
+ OMMIT_FIRST_BARLINE = 1 << 3,
+};
+
+export type hit_sample = struct {
+ normal_set: sample_set,
+ addition_set: sample_set,
+ index: (int | void),
+ volume: (int | void),
+ filename: (str | void),
+};
+
+export const default_sample: hit_sample = hit_sample {
+ normal_set = 0: sample_set,
+ addition_set = 0: sample_set,
+ index = 0,
+ volume = 0,
+ filename = "",
+};
+
+export type hit_sound = enum u8 {
+ NORMAL = 1 << 0,
+ WHISTLE = 1 << 1,
+ FINISH = 1 << 2,
+ CLAP = 1 << 3,
+};
+
+export type hit_object = struct {
+ x: int,
+ y: int,
+ time: int,
+ hit_sound: hit_sound,
+ hit_sample: hit_sample,
+ new: bool,
+ skip: u8,
+ hit_object: (circle | slider | spinner | hold),
+};
+
+export type circle = void;
+
+export type curve_type = enum {
+ BEZIER,
+ CENTRIPETAL_CATMULL_ROM,
+ LINEAR,
+ PERFECT_CIRCLE,
+};
+
+export type curve_point = struct {
+ x: int,
+ y: int,
+};
+
+export type slider = struct {
+ curve_type: curve_type,
+ curve_points: []curve_point,
+ slides: int,
+ length: f64,
+ edge_sounds: []hit_sound,
+ edge_sets: []hit_sample,
+};
+
+export type spinner = struct {
+ end_time: int,
+};
+
+export type hold = struct {
+ end_time: int,
+};
A => beatmap/error.ha +23 -0
@@ 1,23 @@
+use encoding::utf8;
+use strconv;
+use io;
+use fmt;
+
+export type invalid = !struct {
+ kind: str,
+ value: (str | void),
+};
+
+export type error = !(utf8::invalid | io::error | strconv::error | invalid);
+
+export fn strerror(e: error) str = match (e) {
+case let e: invalid =>
+ static let buf: [256]u8 = [0...];
+ return fmt::bsprintf(buf, "invalid {}: {}", e.kind, e.value);
+case utf8::invalid =>
+ return "invalid utf8";
+case let e: io::error =>
+ return io::strerror(e);
+case let e: strconv::error =>
+ return strconv::strerror(e);
+};
A => beatmap/parse.ha +372 -0
@@ 1,372 @@
+use io;
+use bufio;
+use strings;
+use strconv;
+
+type section = enum {
+ VERSION,
+ GENERAL,
+ EDITOR,
+ METADATA,
+ DIFFICULTY,
+ EVENTS,
+ TIMING_POINTS,
+ COLOURS,
+ HIT_OBJECTS,
+};
+
+type entry = struct {
+ key: str,
+ value: str,
+};
+
+fn splitentry(line: str, delim: str) entry = {
+ let split = strings::splitn(line, delim, 2);
+ defer free(split);
+ return entry {
+ key = split[0],
+ value = split[1],
+ };
+};
+
+fn general_entry(general: *general, line: str) (void | error) = {
+ let e = splitentry(line, ": ");
+ let ival = strconv::stoi(e.value);
+ switch (e.key) {
+ case "AudioFilename" => general.audio_filename = e.key;
+ case "AudioLeadIn" => general.audio_lead_in = ival?;
+ case "AudioHash" => general.audio_hash = e.key;
+ case "PreviewTime" => general.preview_time = ival?;
+ case "Countdown" => general.countdown = ival?: countdown;
+ case "SampleSet" =>
+ general.sample_set = switch (e.value) {
+ case "Normal" => yield sample_set::NORMAL;
+ case "Soft" => yield sample_set::SOFT;
+ case "Drum" => yield sample_set::DRUM;
+ case => return invalid {kind = "sample set", value = e.value};
+ };
+ case "StackLeniency" => general.stack_leniency = strconv::stof64(e.value)?;
+ case "Mode" => general.mode = ival?: mode;
+ case "LetterboxInBreaks" => general.letterbox_in_breaks = ival? != 0;
+ case "StoryFireInFront" => general.story_fire_in_front = ival? != 0;
+ case "UseSkinSprites" => general.use_skin_sprites = ival? != 0;
+ case "AlwaysShowPlayfield" => general.always_show_playfield = ival? != 0;
+ case "OverlayPosition" =>
+ general.overlay_position = switch (e.value) {
+ case "NoChange" => yield overlay_position::NO_CHANGE;
+ case "Below" => yield overlay_position::BELOW;
+ case "Above" => yield overlay_position::ABOVE;
+ case => return invalid {kind = "overlay position", value = e.value};
+ };
+ case "SkinPreference" => general.skin_preference = strings::dup(e.value);
+ case "EpilepsyWarning" => general.epilepsy_warning = ival? != 0;
+ case "CountdownOffset" => general.countdown_offset = ival?;
+ case "SpecialStyle" => general.special_style = ival? != 0;
+ case "WidescreenStoryboard" => general.widescreen_storyboard = ival? != 0;
+ case "SamplesMatchPlaybackRate" => general.samples_match_playback_rate = ival? != 0;
+ };
+};
+
+fn editor_entry(editor: *editor, line: str) (void | error) = {
+ let e = splitentry(line, ": ");
+ let ival = strconv::stoi(e.value);
+ let fval = strconv::stof64(e.value);
+ switch (e.key) {
+ case "Bookmarks" =>
+ let split = strings::split(e.value, ",");
+ defer free(split);
+ for (let i = 0z; i < len(split); i += 1) append(editor.bookmarks, strconv::stoi(split[i])?);
+ case "DistanceSpacing" => editor.distance_spacing = fval?;
+ case "BeatDivisor" => editor.beat_divisor = ival?;
+ case "GridSize" => editor.grid_size = ival?;
+ case "TimelineZoom" => editor.timeline_zoom = fval?;
+ case => return invalid {kind = "editor entry", value = e.key};
+ };
+};
+
+fn metadata_entry(metadata: *metadata, line: str) (void | error) = {
+ let e = splitentry(line, ":");
+ switch (e.key) {
+ case "Title" => metadata.title = strings::dup(e.value);
+ case "TitleUnicode" => metadata.title_unicode = strings::dup(e.value);
+ case "Artist" => metadata.artist = strings::dup(e.value);
+ case "ArtistUnicode" => metadata.artist_unicode = strings::dup(e.value);
+ case "Creator" => metadata.creator = strings::dup(e.value);
+ case "Version" => metadata.version = strings::dup(e.value);
+ case "Source" => metadata.source = strings::dup(e.value);
+ case "Tags" => metadata.tags = strings::dupall(strings::split(e.value, " "));
+ case "BeatmapID" => metadata.beatmap_id = strconv::stoi(e.value)?;
+ case "BeatmapSetID" => metadata.beatmap_set_id = strconv::stoi(e.value)?;
+ case => return invalid {kind = "metatdata entry", value = e.key};
+ };
+};
+
+fn difficulty_entry(difficulty: *difficulty, line: str) (void | error) = {
+ let e = splitentry(line, ":");
+ let value = strconv::stof64(e.value)?;
+ switch (e.key) {
+ case "HPDrainRate" => difficulty.hp_drain_rate = value;
+ case "CircleSize" => difficulty.circle_size = value;
+ case "OverallDifficulty" => difficulty.overall_difficulty = value;
+ case "ApproachRate" => difficulty.approach_rate = value;
+ case "SliderMultiplier" => difficulty.slider_multiplier = value;
+ case "SliderTickRate" => difficulty.slider_tick_rate = value;
+ case => return invalid {kind = "difficulty entry", value = e.key};
+ };
+};
+
+type ev_type = enum {
+ BACKGROUND,
+ VIDEO,
+ BREAK,
+};
+
+fn parse_ev_type(in: str) (ev_type | error) = {
+ match (strconv::stoi(in)) {
+ case let i: int => return i: ev_type;
+ case =>
+ switch (in) {
+ case "Video" => return ev_type::VIDEO;
+ case "Break" => return ev_type::BREAK;
+ case => return invalid {kind = "event type", value = in};
+ };
+ };
+};
+
+fn events_entry(events: *[]event, line: str) (void | error) = {
+ let split = strings::split(line, ",");
+ let _type = parse_ev_type(split[0])?;
+ let start_time = strconv::stoi(split[1])?;
+ let ev = switch (_type) {
+ case ev_type::BACKGROUND, ev_type::VIDEO =>
+ let filename = split[2];
+ let xoffset = 0;
+ let yoffset = 0;
+ if (len(split) > 2) {
+ xoffset = strconv::stoi(split[3])?;
+ yoffset = strconv::stoi(split[4])?;
+ };
+ yield switch (_type) {
+ case ev_type::BACKGROUND =>
+ yield background {
+ filename = filename,
+ x_offset = xoffset,
+ y_offset = yoffset,
+ };
+ case ev_type::VIDEO =>
+ yield video {
+ filename = filename,
+ x_offset = xoffset,
+ y_offset = yoffset,
+ };
+ };
+ case ev_type::BREAK =>
+ yield _break {
+ end_time = strconv::stoi(split[2])?,
+ };
+ };
+ append(events, event {
+ start_time = start_time,
+ event = ev,
+ });
+};
+
+fn timing_points_entry(timing_points: *[]timing_point, line: str) (void | error) = {
+ let split = strings::split(line, ",");
+ let i = 0z;
+ let time = strconv::stoi(split[i])?; i += 1;
+ let beat_length = strconv::stof64(split[i])?; i += 1;
+ let meter = strconv::stoi(split[i])?; i += 1;
+ let sample_set = strconv::stoi(split[i])?: sample_set; i += 1;
+ let sample_index = strconv::stoi(split[i])?; i += 1;
+ let volume = strconv::stoi(split[i])?; i += 1;
+ let uninherited = strconv::stoi(split[i])? != 0; i += 1;
+ let eff = strconv::stoi(split[i])?; i += 1;
+ append(timing_points, timing_point {
+ time = time,
+ beat_length = beat_length,
+ meter = meter,
+ sample_set = sample_set,
+ sample_index = sample_index,
+ volume = volume,
+ uninherited = uninherited,
+ effects = eff: effects,
+ });
+};
+
+fn parse_colour(in: str) (colour | error) = {
+ let split = strings::split(in, ",");
+ if (len(split) < 3) return invalid {kind = "colour", value = in};
+ return colour {
+ r = strconv::stou8(split[0])?,
+ g = strconv::stou8(split[1])?,
+ b = strconv::stou8(split[2])?,
+ };
+};
+
+fn colours_entry(colours: *colours, line: str) (void | error) = {
+ let split = strings::split(line, " : ");
+ let c = parse_colour(split[1])?;
+ switch (split[0]) {
+ case "SliderTrackOverride" => colours.slider_track_override = c;
+ case "Slider_Border" => colours.slider_border = c;
+ case =>
+ let key = strconv::stoi(strings::trimprefix(split[0], "Combo"))?;
+ insert_colour(colours, (key, c));
+ };
+ defer free(split);
+};
+
+type ho_type = enum {
+ CIRCLE,
+ SLIDER,
+ SPINNER,
+ HOLD,
+};
+
+fn bitflag_to_type(b: u8) (ho_type | error) = {
+ if ((b & (1 << 0)) != 0) return ho_type::CIRCLE;
+ if ((b & (1 << 1)) != 0) return ho_type::SLIDER;
+ if ((b & (1 << 3)) != 0) return ho_type::SPINNER;
+ if ((b & (1 << 7)) != 0) return ho_type::HOLD;
+ return invalid {kind = "bitflag", value = void};
+};
+
+fn parse_curve(s: str) ((curve_type, []curve_point) | error) = {
+ let split = strings::split(s, "|");
+ let curve_type = switch (split[0]) {
+ case "B" => yield curve_type::BEZIER;
+ case "C" => yield curve_type::CENTRIPETAL_CATMULL_ROM;
+ case "L" => yield curve_type::LINEAR;
+ case "P" => yield curve_type::PERFECT_CIRCLE;
+ case => return invalid {kind = "curve type", value = split[0]};
+ };
+ split = split[1..];
+ let curve_points = []: []curve_point;
+ for (let i = 0z; i < len(split); i += 1) {
+ let split = strings::split(split[i], ":");
+ append(curve_points, curve_point {
+ x = strconv::stoi(split[0])?,
+ y = strconv::stoi(split[1])?,
+ });
+ };
+ return (curve_type, curve_points);
+};
+
+fn hit_objects_entry(hit_objects: *[]hit_object, line: str) (void | error) = {
+ let split = strings::split(line, ",");
+ defer free(split);
+ let i = 0z;
+ let x = strconv::stoi(split[i])?; i += 1;
+ let y = strconv::stoi(split[i])?; i += 1;
+ let time = strconv::stoi(split[i])?; i += 1;
+ let _type = strconv::stou8(split[3])?; i += 1;
+ let ho_type = bitflag_to_type(_type)?;
+ let new = (_type & (1 << 2)) != 0;
+ let skip = (_type >> 6) & 0b00000111; // XXX maybe wrong and too lazy to test
+ let sound = strconv::stou8(split[4])?: hit_sound; i += 1;
+ let object = switch (ho_type) {
+ case ho_type::CIRCLE =>
+ yield circle;
+ case ho_type::SLIDER =>
+ let curve = parse_curve(split[i])?; i += 1;
+ let slides = strconv::stoi(split[i])?; i += 1;
+ let length = strconv::stof64(split[i])?; i += 1;
+ let edge_sounds_str = strings::split(split[i], "|"); i += 1;
+ let edge_sets_str = strings::split(split[i], "|"); i += 1;
+ let edge_sounds = []: []hit_sound;
+ for (let i = 0z; i < len(edge_sounds_str); i += 1)
+ append(edge_sounds, strconv::stou8(edge_sounds_str[i])?: hit_sound);
+ let edge_sets = []: []hit_sample;
+ for (let i = 0z; i < len(edge_sets_str); i += 1) {
+ let split = strings::split(edge_sets_str[i], ":");
+ defer free(split);
+ append(edge_sets, hit_sample {
+ normal_set = strconv::stou8(split[0])?: sample_set,
+ addition_set = strconv::stou8(split[1])?: sample_set,
+ index = void,
+ volume = void,
+ filename = void,
+ });
+ };
+ yield slider {
+ curve_type = curve.0,
+ curve_points = curve.1,
+ slides = slides,
+ length = length,
+ edge_sounds = edge_sounds,
+ edge_sets = edge_sets,
+ };
+ case ho_type::SPINNER =>
+ defer i += 1;
+ yield spinner {
+ end_time = strconv::stoi(split[i])?,
+ };
+ case ho_type::HOLD =>
+ defer i += 1;
+ yield spinner {
+ end_time = strconv::stoi(split[i])?,
+ };
+ };
+ let sample: hit_sample = default_sample;
+ if (i < len(split)) {
+ let split = strings::split(split[i], ":");
+ sample = hit_sample {
+ normal_set = strconv::stou8(split[0])?: sample_set,
+ addition_set = strconv::stou8(split[1])?: sample_set,
+ index = strconv::stoi(split[2])?,
+ volume = strconv::stoi(split[3])?,
+ filename = strings::dup(split[4]),
+ };
+ };
+ i += 1;
+ append(hit_objects, hit_object {
+ x = x,
+ y = y,
+ time = time,
+ hit_sound = sound,
+ hit_sample = sample,
+ new = new,
+ skip = skip,
+ hit_object = object,
+ });
+};
+
+export fn parse(in: io::handle) (*beatmap | error) = {
+ let section = section::VERSION;
+ let beatmap = alloc(beatmap {...});
+ beatmap.general = general_default;
+ for (true) match (bufio::scanline(in)?) {
+ case let line: []u8 =>
+ let line = strings::fromutf8(line)?;
+ defer free(line);
+ line = strings::trimsuffix(line, "\r");
+ if (strings::hasprefix(line, "//")) continue;
+ switch (line) {
+ case "" => void;
+ case "[General]" => section = section::GENERAL;
+ case "[Editor]" => section = section::EDITOR;
+ case "[Metadata]" => section = section::METADATA;
+ case "[Difficulty]" => section = section::DIFFICULTY;
+ case "[Events]" => section = section::EVENTS;
+ case "[TimingPoints]" => section = section::TIMING_POINTS;
+ case "[Colours]" => section = section::COLOURS;
+ case "[HitObjects]" => section = section::HIT_OBJECTS;
+ case =>
+ switch (section) {
+ case section::VERSION => if (beatmap.version == "") beatmap.version = strings::dup(line);
+ case section::GENERAL => general_entry(&beatmap.general, line)?;
+ case section::EDITOR => editor_entry(&beatmap.editor, line)?;
+ case section::METADATA => metadata_entry(&beatmap.metadata, line)?;
+ case section::DIFFICULTY => difficulty_entry(&beatmap.difficulty, line)?;
+ case section::EVENTS => events_entry(&beatmap.events, line)?;
+ case section::TIMING_POINTS => timing_points_entry(&beatmap.timing_points, line)?;
+ case section::COLOURS => colours_entry(&beatmap.colours, line)?;
+ case section::HIT_OBJECTS => hit_objects_entry(&beatmap.hit_objects, line)?;
+ };
+ };
+ case io::EOF =>
+ break;
+ };
+ return beatmap;
+};
A => main.ha +17 -0
@@ 1,17 @@
+use beatmap;
+use os;
+use fmt;
+
+export fn main() void = {
+ let beatmap = match (beatmap::parse(os::stdin)) {
+ case let e: beatmap::error =>
+ fmt::fatal(beatmap::strerror(e));
+ case let b: *beatmap::beatmap =>
+ yield b;
+ };
+ fmt::println(beatmap.metadata.title)!;
+ fmt::println(beatmap.version)!;
+ fmt::println(beatmap.metadata.beatmap_id)!;
+ fmt::println(beatmap.difficulty.approach_rate)!;
+ fmt::println(beatmap.general.widescreen_storyboard)!;
+};