~evan/hasu

04341cef68ba12a9c541a66a8d727427877e7aac — Evan Johsnton a month ago master
init
5 files changed, 661 insertions(+), 0 deletions(-)

A .gitignore
A beatmap/beatmap.ha
A beatmap/error.ha
A beatmap/parse.ha
A main.ha
A  => .gitignore +1 -0
@@ 1,1 @@
hasu

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)!;
};