mirror of https://git.sr.ht/~kikoodx/kble
345 lines
11 KiB
Zig
345 lines
11 KiB
Zig
// 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 = @import("raylib.zig");
|
|
const std = @import("std");
|
|
const expect = std.testing.expect;
|
|
|
|
const conf = @import("conf.zig");
|
|
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: ray.Color = conf.cell_color(cell_content);
|
|
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);
|
|
}
|