diff --git a/.gitignore b/.gitignore index 3cef7be..911fee5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ zig-cache/ +sample_*.kble diff --git a/kbleformat.md b/kbleformat.md index 0103c1a..f97442f 100644 --- a/kbleformat.md +++ b/kbleformat.md @@ -7,6 +7,6 @@ Encoding: * first byte indicates how big is a chunk of data (in bytes) * second and third bytes indicates level width * fourth and fifth bytes indicates level height -* 7th => (7 + data_size * width * height) contains level data +* 6th => (6 + data_size * width * height) contains level data If the encoding is incorrect, return an error or crash the program. diff --git a/src/level.zig b/src/level.zig index bdda7c5..28d38ea 100644 --- a/src/level.zig +++ b/src/level.zig @@ -18,6 +18,7 @@ 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, @@ -49,13 +50,17 @@ pub fn init(allocator: *std.mem.Allocator, width: u16, height: u16) !Self { return self; } -/// Load level content from given absolute path. Expect the KBLE file format (see -/// `kbleformat.md` for more details. +/// 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_load(allocator: *std.mem.Allocator, kble_file_path: []const u8) !Self { var self: Self = undefined; - const expected_bytes_per_cell = @sizeOf(cell_type); - // Open directory. var dir: std.fs.Dir = std.fs.cwd(); // Open file in read mode. @@ -114,14 +119,50 @@ pub fn init_load(allocator: *std.mem.Allocator, kble_file_path: []const u8) !Sel return self; } -/// Free the level content. -pub fn deinit(self: *Self, allocator: *std.mem.Allocator) void { - allocator.free(self.selection); - allocator.free(self.content); +/// 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: []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.createFile(kble_file_path, std.fs.File.CreateFlags{}); + defer file.close(); + // Get writer. + var writer: std.fs.File.Writer = file.writer(); + + // Write first 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); + } + } } /// Draw level tiles from `offset` to fill the window. -pub fn draw(self: *Self, scale: u16, offset: Vec2) void { +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; @@ -147,7 +188,7 @@ pub fn draw(self: *Self, scale: u16, offset: Vec2) void { } /// Draw selection from `offset` to fill the window. -pub fn draw_selection(self: *Self, scale: u16, offset: Vec2) void { +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; @@ -261,11 +302,30 @@ test "select rectangle area" { expect(!level.selection[245]); } -test "load level from existing file" { +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_load(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_load(allocator, "sample_large.kble"); + defer level.deinit(allocator); + + expect(level.width == 300); + expect(level.height == 1); }