// Miscellaneous functions // Copyright (C) 2021 Nguyễn Gia Phong // // This file is part of Black Shades. // // Black Shades is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Black Shades is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Black Shades. If not, see . usingnamespace @cImport({ @cInclude("AL/al.h"); @cInclude("GL/gl.h"); @cInclude("GL/glu.h"); @cInclude("lodepng.h"); }); const Dir = std.fs.Dir; const TokenIterator = std.mem.TokenIterator; const allocPrint = std.fmt.allocPrint; const allocator = std.heap.c_allocator; const assert = std.debug.assert; const count = std.mem.count; const cwd = std.fs.cwd; const endsWith = std.mem.endsWith; const eql = std.mem.eql; const free = std.c.free; const join = std.fs.path.joinZ; const maxInt = std.math.maxInt; const parseFloat = std.fmt.parseFloat; const parseInt = std.fmt.parseInt; const sep = std.fs.path.sep; const span = std.mem.span; const startsWith = std.mem.startsWith; const std = @import("std"); const tokenize = std.mem.tokenize; const al = @import("zeal"); const gf = @import("gfz"); const ini = @import("ini"); const data_dir = @import("build_options").data_dir ++ [_]u8{ sep }; /// Return whether the given keyboard key is pressed. export fn keyPress(key: c_int) bool { const window = (gf.Window.getCurrent() catch unreachable).?; return (window.getKey(@intToEnum(gf.Key, key)) catch unreachable) == .press; } /// Read given file to heap, allocated by C allocator. fn readFile(dir: Dir, comptime fmt: []const u8, args: anytype) ![]const u8 { const filename = try allocPrint(allocator, fmt, args); defer allocator.free(filename); // Don't judge me, take care of me! return dir.readFileAlloc(allocator, filename, maxInt(usize)); } const Frame = extern struct { joints: [20]extern struct { x: f32, y: f32, z: f32, }, speed: f32, }; export fn loadAnimation(name: [*:0]const u8) extern struct { ptr: [*]Frame, len: usize, } { var dir = cwd().openDir(data_dir ++ "animations", .{}) catch unreachable; defer dir.close(); const anim_file = readFile(dir, "{s}{c}index.tsv", .{ name, sep, // $animation/index.tsv }) catch unreachable; defer allocator.free(anim_file); const length = count(u8, anim_file, "\t") - 1; var anim = tokenize(anim_file, "\n"); _ = anim.next().?; // ignore field names const frames = allocator.alloc(Frame, length) catch unreachable; for (frames) |*frame| { var values = tokenize(anim.next().?, "\t"); const frame_file = readFile(dir, "{s}{c}frames{c}{s}.tsv", .{ name, sep, sep, values.next().?, // $animation/frames/$frame.tsv }) catch unreachable; defer allocator.free(frame_file); frame.speed = parseFloat(f32, values.next().?) catch unreachable; var joints = tokenize(frame_file, "\n"); _ = joints.next().?; // ignore field names for (frame.joints) |*joint| { var coordinates = tokenize(joints.next().?, "\t"); joint.* = .{ .x = parseFloat(f32, coordinates.next().?) catch unreachable, .y = parseFloat(f32, coordinates.next().?) catch unreachable, .z = parseFloat(f32, coordinates.next().?) catch unreachable, }; } } return .{ .ptr = frames.ptr, .len = frames.len }; } /// Parse boolean values. pub fn parseBool(s: []const u8) !bool { if (eql(u8, s, "false")) return false; if (eql(u8, s, "true")) return true; return error.InvalidCharacter; } const Joint = extern struct { label: i8, x: f32, y: f32, z: f32, length: f32, model: u8, visible: bool, lower: bool, parent: i8, pub fn load(self: *Joint, row: []const u8) !void { var values = tokenize(row, "\t"); self.label = try parseInt(i8, values.next().?, 10); self.x = try parseFloat(f32, values.next().?); self.y = try parseFloat(f32, values.next().?); self.z = try parseFloat(f32, values.next().?); self.length = try parseFloat(f32, values.next().?); self.model = try parseInt(u8, values.next().?, 10); self.visible = try parseBool(values.next().?); self.lower = try parseBool(values.next().?); self.parent = try parseInt(i8, values.next().?, 10); } }; /// Load joints in character's skeleton. export fn loadJoints(joints: [*]Joint) void { const file = readFile(cwd(), data_dir ++ "joints.tsv", .{}) catch unreachable; defer allocator.free(file); var tsv = tokenize(file, "\n"); _ = tsv.next().?; // ignore field names var i = @as(u8, 0); while (tsv.next()) |row| : (i += 1) joints[i].load(row) catch unreachable; } const Vertex = extern struct { x: f32, y: f32, z: f32, }; const Face = extern struct { // Only support triangles v: [3]u16, r: f32, g: f32, b: f32, }; const OffIterator = struct { token_iterator: TokenIterator, pub fn init(buffer: []const u8) OffIterator { var self = .{ .token_iterator = tokenize(buffer, "\n") }; if (!endsWith(u8, self.token_iterator.next().?, "OFF")) self.token_iterator.reset(); return self; } pub fn next(self: *OffIterator) ?TokenIterator { while (self.token_iterator.next()) |line| { var words = tokenize(line, " "); if (words.next()) |word| { // not empty if (!startsWith(u8, word, "#")) { // not comment words.reset(); return words; } } } return null; } }; /// Load model from given OFF file. export fn loadModel(path: [*:0]const u8) extern struct { vertices: extern struct { ptr: [*]Vertex, len: usize, }, faces: extern struct { ptr: [*]Face, len: usize, }, } { const file = readFile(cwd(), data_dir ++ "models{c}{s}", .{ sep, path, }) catch unreachable; defer allocator.free(file); var lines = OffIterator.init(file); var counts = lines.next().?; const vertex_count = parseInt(usize, counts.next().?, 10) catch unreachable; const face_count = parseInt(usize, counts.next().?, 10) catch unreachable; const vertices = allocator.alloc(Vertex, vertex_count) catch unreachable; for (vertices) |*vertex| { var numbers = lines.next().?; vertex.* = .{ .x = parseFloat(f32, numbers.next().?) catch unreachable, .y = parseFloat(f32, numbers.next().?) catch unreachable, .z = parseFloat(f32, numbers.next().?) catch unreachable, }; } const faces = allocator.alloc(Face, face_count) catch unreachable; for (faces) |*face| { var numbers = lines.next().?; assert(eql(u8, numbers.next().?, "3")); face.* = .{ .v = .{ parseInt(u16, numbers.next().?, 10) catch unreachable, parseInt(u16, numbers.next().?, 10) catch unreachable, parseInt(u16, numbers.next().?, 10) catch unreachable, }, .r = parseFloat(f32, numbers.next().?) catch unreachable, .g = parseFloat(f32, numbers.next().?) catch unreachable, .b = parseFloat(f32, numbers.next().?) catch unreachable, }; } return .{ .vertices = .{ .ptr = vertices.ptr , .len = vertices.len }, .faces = .{ .ptr = faces.ptr , .len = faces.len }, }; } const Muscle = extern struct { length: f32, initlen: f32, minlen: f32, maxlen: f32, flag: bool, visible: bool, parent1: i8, parent2: i8, pub fn load(self: *Muscle, row: []const u8) !void { var values = tokenize(row, "\t"); self.length = try parseFloat(f32, values.next().?); self.initlen = try parseFloat(f32, values.next().?); self.minlen = try parseFloat(f32, values.next().?); self.maxlen = try parseFloat(f32, values.next().?); self.flag = try parseBool(values.next().?); self.visible = try parseBool(values.next().?); self.parent1 = try parseInt(i8, values.next().?, 10); self.parent2 = try parseInt(i8, values.next().?, 10); } }; /// Load muscles in character's skeleton. export fn loadMuscles(muscles: [*]Muscle) void { const file = readFile(cwd(), data_dir ++ "muscles.tsv", .{}) catch unreachable; defer allocator.free(file); var tsv = tokenize(file, "\n"); _ = tsv.next().?; // ignore field names var i = @as(u8, 0); while (tsv.next()) |row| : (i += 1) muscles[i].load(row) catch unreachable; } pub const Scores = extern struct { // Don't underestimate players! high_score: usize = 0, completed: bool = false, }; /// Read scores from user data directory and return it. pub fn loadScores(base_dir: []const u8) !Scores { var dir = try cwd().openDir(base_dir, .{}); defer dir.close(); var scores = Scores{}; const path = "blackshades" ++ [_]u8{ sep } ++ "scores.ini"; const input = dir.openFile(path, .{}) catch return scores; defer input.close(); var parser = ini.parse(allocator, input.reader()); defer parser.deinit(); while (try parser.next()) |record| switch (record) { .section => {}, .property => |kv| if (eql(u8, kv.key, "high score")) { scores.high_score = try parseInt(usize, kv.value, 10); } else if (eql(u8, kv.key, "completed")) { scores.completed = try parseBool(kv.value); } else return error.InvalidData, else => return error.InvalidData, }; return scores; } /// Load audio file into an OpenAL buffer and return it. export fn loadSound(filename: [*:0]const u8) u32 { const path = join(allocator, &.{ data_dir ++ "sounds", span(filename) }) catch unreachable; defer allocator.free(path); const audio = al.Audio.read(allocator, span(path)) catch unreachable; defer audio.free(); const buffer = al.Buffer.init(audio) catch unreachable; return buffer.reference; } fn check(errorString: fn (c_uint) callconv(.C) [*c]const u8, status: anytype) void { if (status != 0) @panic(span(errorString(@intCast(c_uint, status)))); } /// Load PNG file into an OpenGL buffer and return it. export fn loadTexture(filename: [*:0]const u8) GLuint { const file = readFile(cwd(), data_dir ++ "textures{c}{s}", .{ sep, filename, }) catch unreachable; defer allocator.free(file); var data: [*c]u8 = undefined; var w: c_uint = undefined; var h: c_uint = undefined; check(lodepng_error_text, lodepng_decode32(&data, &w, &h, file.ptr, file.len)); defer free(data); var texture: GLuint = undefined; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); defer glBindTexture(GL_TEXTURE_2D, 0); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); const width = @intCast(GLint, w); const height = @intCast(GLint, h); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, 4, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); check(gluErrorString, gluBuild2DMipmaps(GL_TEXTURE_2D, 4, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data)); return texture; } /// Move sound source to given position and play it. export fn playSound(source: u32, x: f32, y: f32, z: f32) void { const src = al.Source{ .reference = source }; _ = alGetError(); // TODO: remove when completely migrate to zeal src.setPosition(.{ x / 32, y / 32, z / 32 }) catch unreachable; src.play() catch unreachable; } // TODO: replace with @maximum from Zig 0.9+ fn maximum(a: anytype, b: @TypeOf(a)) @TypeOf(a) { return if (a > b) a else b; } /// Write scores to user data directory. pub fn saveScores(base_dir: []const u8, current: Scores) !void { const previous = try loadScores(base_dir); const dir_path = try join(allocator, &.{ base_dir, "blackshades" }); defer allocator.free(dir_path); var dir = try cwd().openDir(dir_path, .{}); defer dir.close(); const format = "[scores]\nhigh score = {}\ncompleted = {}\n"; const data = try allocPrint(allocator, format, .{ maximum(previous.high_score, current.high_score), previous.completed or current.completed, }); defer allocator.free(data); try dir.writeFile("scores.ini", data); }