// Miscellaneous functions // Copyright (C) 2002 David Rosen // Copyright (C) 2021-2023, 2025 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 . const Dir = std.fs.Dir; const allocPrint = std.fmt.allocPrint; const allocator = std.heap.c_allocator; const count = std.mem.count; const cwd = std.fs.cwd; const eql = std.mem.eql; const join = std.fs.path.joinZ; const len = std.mem.len; 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 std = @import("std"); const tokenizeScalar = std.mem.tokenizeScalar; const al = @import("zeal"); const gf = @import("gfz"); const ini = @import("ini"); const c = @import("cimport.zig"); pub 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(@enumFromInt(key)) catch unreachable) == .press; } /// Read given file to heap, allocated by C allocator. pub 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 = tokenizeScalar(u8, anim_file, '\n'); _ = anim.next().?; // ignore field names const frames = allocator.alloc(Frame, length) catch unreachable; for (frames) |*frame| { var values = tokenizeScalar(u8, 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 = tokenizeScalar(u8, frame_file, '\n'); _ = joints.next().?; // ignore field names for (&frame.joints) |*joint| { var coordinates = tokenizeScalar(u8, 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 { 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 = tokenizeScalar(u8, row, '\t'); 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 = tokenizeScalar(u8, 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 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 = tokenizeScalar(u8, 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 = tokenizeScalar(u8, 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, path) catch unreachable; defer audio.free(); const buffer = al.Buffer.init(audio) catch unreachable; return buffer.reference; } /// 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 }; _ = c.alGetError(); // TODO: remove when completely migrate to zeal src.setPosition(.{ x / 32, y / 32, z / 32 }) catch unreachable; src.play() catch unreachable; } /// 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().makeOpenPath(dir_path, .{}); defer dir.close(); const format = "[scores]\nhigh score = {}\ncompleted = {}\n"; const data = try allocPrint(allocator, format, .{ @max(previous.high_score, current.high_score), previous.completed or current.completed, }); defer allocator.free(data); try dir.writeFile(.{ .sub_path = "scores.ini", .data = data }); } const Text = struct { texture: u32, base: u32, }; export fn buildFont(text: *Text) void { text.base = c.glGenLists(256); c.glBindTexture(c.GL_TEXTURE_2D, text.texture); for (0..256) |i| { // Character coords const x = @as(f32, @floatFromInt(i % 16)) / 16.0; const y = @as(f32, @floatFromInt(i / 16)) / 16.0; c.glNewList(@intCast(text.base + i), c.GL_COMPILE); // Use A Quad For Each Character c.glBegin(c.GL_QUADS); // Bottom left c.glTexCoord2f(x, 1 - y - 0.0625 + 0.001); c.glVertex2i(0,0); // Bottom right c.glTexCoord2f(x + 0.0625, 1 - y - 0.0625 + 0.001); c.glVertex2i(16, 0); // Top right c.glTexCoord2f(x + 0.0625, 1 - y - 0.001); c.glVertex2i(16, 16); // Top left c.glTexCoord2f(x, 1 - y - 0.001); c.glVertex2i(0, 16); c.glEnd(); // Move to the right of the character c.glTranslated(10, 0, 0); c.glEndList(); } } export fn glPrint(text: *const Text, x: f64, y: f64, str: [*:0]const u8, set: bool, size: f32, width: f32, height: f32) void { c.glTexEnvi(c.GL_TEXTURE_ENV, c.GL_TEXTURE_ENV_MODE, c.GL_MODULATE); c.glBindTexture(c.GL_TEXTURE_2D, text.texture); c.glDisable(c.GL_DEPTH_TEST); c.glDisable(c.GL_LIGHTING); c.glBlendFunc(c.GL_SRC_ALPHA, c.GL_ONE_MINUS_SRC_ALPHA); c.glMatrixMode(c.GL_PROJECTION); c.glPushMatrix(); c.glLoadIdentity(); c.glOrtho(0, width, 0, height, -100, 100); c.glMatrixMode(c.GL_MODELVIEW); c.glPushMatrix(); c.glLoadIdentity(); c.glScalef(size, size, 1); // Position the text (0,0 - bottom left) c.glTranslated(x, y, 0); // Choose the font set (0 or 1) if (set) c.glListBase(text.base + 128 - 32) else c.glListBase(text.base -% 32); // Write to screen c.glCallLists(@intCast(len(str)), c.GL_BYTE, str); // Restore c.glMatrixMode(c.GL_PROJECTION); c.glPopMatrix(); c.glMatrixMode(c.GL_MODELVIEW); c.glPopMatrix(); c.glEnable(c.GL_DEPTH_TEST); c.glTexEnvi(c.GL_TEXTURE_ENV, c.GL_TEXTURE_ENV_MODE, c.GL_MODULATE); } /// OpenGL fog state. const Fog = extern struct { color: [4]f32, density: f32, start: f32, end: f32, }; /// Set fog effect. export fn setFog(fog: *Fog, r: f32, g: f32, b: f32, start: f32, end: f32, density: f32) void { fog.color = .{r, g, b, 1.0}; fog.density = density; fog.start = start; fog.end = end; resetFog(fog); } /// Set temporary fog effect. export fn tempFog(fog: *const Fog, r: f32, g: f32, b: f32) void { const color = [4]f32{r, g, b, 1.0}; c.glFogi(c.GL_FOG_MODE, c.GL_LINEAR); c.glFogfv(c.GL_FOG_COLOR, &color); c.glFogf(c.GL_FOG_DENSITY, fog.density); c.glFogi(c.GL_FOG_HINT, c.GL_DONT_CARE); c.glFogf(c.GL_FOG_START, fog.start); c.glFogf(c.GL_FOG_END, fog.end); c.glEnable(c.GL_FOG); } /// Reset fog effect. export fn resetFog(fog: *const Fog) void { c.glFogi(c.GL_FOG_MODE, c.GL_LINEAR); c.glFogfv(c.GL_FOG_COLOR, &fog.color); c.glFogf(c.GL_FOG_DENSITY, fog.density); c.glFogi(c.GL_FOG_HINT, c.GL_DONT_CARE); c.glFogf(c.GL_FOG_START, fog.start); c.glFogf(c.GL_FOG_END, fog.end); c.glEnable(c.GL_FOG); }