// SPDX-License-Identifier: MIT // Copyright (c) 2021 KikooDX // This file is part of [KBLE](https://sr.ht/~kikoodx/kble), which is // MIT licensed. The MIT license requires this copyright notice to be // included in all copies and substantial portions of the software. //! Level structure, grid containing tile data. const ray = @cImport({ @cInclude("raylib.h"); }); const std = @import("std"); const expect = std.testing.expect; const Vec2 = @import("vec2.zig"); const Rect = @import("rect.zig"); const SelectionUpdate = @import("movement.zig").SelectionUpdate; const Self = @This(); pub const cell_type = u16; const expected_bytes_per_cell = @sizeOf(cell_type); width: u16, height: u16, content: []cell_type, selection: []bool, /// Create structure and allocate required memory. The `content` array size will /// be equal to `width` times `height`. pub fn init(allocator: *std.mem.Allocator, width: u16, height: u16) !Self { var self = Self{ .width = width, .height = height, .content = undefined, .selection = undefined, }; // Try to allocate necessary memory. const size: u32 = @intCast(u32, width) * @intCast(u32, height); self.content = try allocator.alloc(cell_type, size); errdefer allocator.free(self.content); self.selection = try allocator.alloc(bool, size); // Fill with default value to avoid undefined behavior. var i: u32 = 0; while (i < size) : (i += 1) { self.content[i] = 0; self.selection[i] = false; } return self; } /// Free the level content. pub fn deinit(self: *Self, allocator: *std.mem.Allocator) void { allocator.free(self.selection); allocator.free(self.content); } /// Load level content from given absolute or relative path. /// Expect the KBLE file format (see `kbleformat.md` for more details). pub fn init_read(allocator: *std.mem.Allocator, kble_file_path: [*:0]const u8) !Self { var self: Self = undefined; // Open directory. var dir: std.fs.Dir = std.fs.cwd(); // Open file in read mode. const file: std.fs.File = try dir.openFileZ(kble_file_path, std.fs.File.OpenFlags{ .read = true, .write = false, }); defer file.close(); // Get reader. var reader: std.fs.File.Reader = file.reader(); // Read first byte and check than the value matches the current format version. { const version = try reader.readByte(); if (version != 0) unreachable; } // Read second byte and check than the value matches the size of cell_type. { const cell_spec_byte = try reader.readByte(); if (cell_spec_byte != expected_bytes_per_cell) unreachable; } // Read four bytes and use them for width and height. Two bytes each. { const width_byte1: u8 = try reader.readByte(); const width_byte2: u8 = try reader.readByte(); const height_byte1: u8 = try reader.readByte(); const height_byte2: u8 = try reader.readByte(); const width: u16 = @intCast(u16, width_byte1) * 256 + @intCast(u16, width_byte2); const height: u16 = @intCast(u16, height_byte1) * 256 + @intCast(u16, height_byte2); std.log.info("Cell size: {}\nWidth/height: {}·{} ; {}·{} => {} ; {}", .{ expected_bytes_per_cell, width_byte1, width_byte2, height_byte1, height_byte2, width, height, }); // Non-null width and height. if (width == 0) unreachable; if (height == 0) unreachable; self = try Self.init(allocator, width, height); } // Read the rest of the file and assign to .content accordingly. { var i: u32 = 0; while (i < self.width * self.height) : (i += 1) { const byte1 = try reader.readByte(); const byte2 = try reader.readByte(); const cell = @intCast(u16, byte1) * 256 + @intCast(u16, byte2); self.content[i] = cell; } } return self; } /// Write header and level content to given absolute or relative path. /// Uses the KBLE file format (see `kbleformat.md` for more details). pub fn write(self: Self, kble_file_path: [*:0]const u8) !void { // Open directory. var dir: std.fs.Dir = std.fs.cwd(); // Open file in write mode. const file: std.fs.File = try dir.createFileZ(kble_file_path, std.fs.File.CreateFlags{}); defer file.close(); // Get writer. var writer: std.fs.File.Writer = file.writer(); // Write first byte, kbleformat version number (hopefully will never have to increase). try writer.writeByte(0); // Write second byte, indicates cell size in bytes. try writer.writeByte(expected_bytes_per_cell); // Write level height and level width. { const width_byte1: u8 = @intCast(u8, self.width / 256); const width_byte2: u8 = @intCast(u8, self.width % 256); const height_byte1: u8 = @intCast(u8, self.height / 256); const height_byte2: u8 = @intCast(u8, self.height % 256); std.log.info("{}·{} ; {}·{}", .{ width_byte1, width_byte2, height_byte1, height_byte2 }); try writer.writeByte(width_byte1); try writer.writeByte(width_byte2); try writer.writeByte(height_byte1); try writer.writeByte(height_byte2); } // Write level content. { var i: u32 = 0; while (i < self.width * self.height) : (i += 1) { const cell: u16 = self.content[i]; const byte1: u8 = @intCast(u8, cell / 256); const byte2: u8 = @intCast(u8, cell % 256); try writer.writeByte(byte1); try writer.writeByte(byte2); } } } /// Wrapper around `init_read`. pub fn action_read(self: *Self, allocator: *std.mem.Allocator, kble_file_path: [*:0]const u8) void { self.deinit(allocator); self.* = Self.init_read(allocator, kble_file_path) catch unreachable; } /// Wrapper around `write`. pub fn action_write(self: *Self, allocator: *std.mem.Allocator, kble_file_path: [*:0]const u8) void { self.write(kble_file_path) catch unreachable; } /// Draw level tiles from `offset` to fill the window. pub fn draw(self: Self, scale: u16, offset: Vec2) void { // Pixel position (were we draw). var x: Vec2.int_type = 0; var y: Vec2.int_type = 0; // Cursor position. var cx: Vec2.int_type = offset.x; while (cx < self.width) { var cy: Vec2.int_type = offset.y; while (cy < self.height) { const cell_content: cell_type = self.content[cy * self.width + cx]; const color = switch (cell_content) { 0 => ray.Color{ .r = 26, .g = 26, .b = 26, .a = 255 }, 1 => ray.Color{ .r = 144, .g = 144, .b = 144, .a = 255 }, else => ray.PURPLE, //unknown }; ray.DrawRectangle(x + 1, y + 1, scale - 2, scale - 2, color); y += scale; cy += 1; } y = 0; x += scale; cx += 1; } } /// Draw selection from `offset` to fill the window. pub fn draw_selection(self: Self, scale: u16, offset: Vec2) void { // Pixel position (were we draw). var x: Vec2.int_type = 0; var y: Vec2.int_type = 0; // Cursor position. var cx: Vec2.int_type = offset.x; while (cx < self.width) { var cy: Vec2.int_type = offset.y; while (cy < self.height) { if (self.selection[cy * self.width + cx]) ray.DrawRectangleLines(x, y, scale, scale, ray.WHITE); y += scale; cy += 1; } y = 0; x += scale; cx += 1; } } /// Set the `state` of a cell at `cursor` if possible pub fn select_cell(self: *Self, cursor: Vec2, state: bool) void { const target: u32 = @intCast(u32, cursor.y) * self.width + @intCast(u32, cursor.x); if (cursor.x < self.width and cursor.y < self.height and target < self.width * self.height and target >= 0) { self.selection[target] = state; } } /// Reset state of selection: `state` everywhere. fn select_clear(self: *Self, state: bool) void { const size: u32 = @intCast(u32, self.width) * @intCast(u32, self.height); var i: u32 = 0; while (i < size) : (i += 1) { self.selection[i] = state; } } /// Change state of all the cells in the rectangle range. pub fn select_rect(self: *Self, rect: Rect, state: bool) void { var cx: Rect.int_type = rect.left_x; while (cx <= rect.right_x) { defer cx += 1; var cy = rect.top_y; while (cy <= rect.bottom_y) { defer cy += 1; self.select_cell(Vec2{ .x = cx, .y = cy }, state); } } } /// Apply selection update to selection *kof*. pub fn apply_selection_update(self: *Self, selection_update: SelectionUpdate) void { // Apply changes. self.select_rect(selection_update.area, selection_update.state); } test "create level buffer" { // Create allocator. var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = &arena.allocator; // Initialize level struct and allocate space (twice 'cause why not?). var level: Self = try Self.init(allocator, 64, 32); level.deinit(allocator); level = try Self.init(allocator, 256, 256); defer level.deinit(allocator); level.content[128] = 32; level.selection[128] = true; expect(level.width == 256); expect(level.height == 256); expect(level.content[128] == 32); expect(level.selection[128]); } test "clear selection" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = &arena.allocator; var level: Self = try Self.init(allocator, 16, 16); defer level.deinit(allocator); level.selection[255] = true; level.select_clear(false); expect(!level.selection[255]); } test "select rectangle area" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = &arena.allocator; var level: Self = try Self.init(allocator, 16, 16); defer level.deinit(allocator); level.select_rect(Rect{ .left_x = 4, .right_x = 20, .top_y = 1, .bottom_y = 14, }, true); expect(!level.selection[14]); expect(!level.selection[19]); expect(level.selection[20]); expect(level.selection[31]); expect(!level.selection[245]); } test "load level from sample file and save it" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = &arena.allocator; var level: Self = try Self.init_read(allocator, "sample.kble"); defer level.deinit(allocator); try level.write("sample_out.kble"); } test "write large level to file and load it back" { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = &arena.allocator; var level: Self = try Self.init(allocator, 300, 1); try level.write("sample_large.kble"); level.deinit(allocator); level = try Self.init_read(allocator, "sample_large.kble"); defer level.deinit(allocator); expect(level.width == 300); expect(level.height == 1); }