commit 5bfa8b2c51d06b00b59773c4e48734eed07589c6 Author: Sander Schobers Date: Fri Jun 2 12:21:32 2023 +0200 Basic setup. - Allegro wrapper. - Setting up of new window etc. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74ee24f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vscode +zig-cache +zig-out +allegro.path diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7d45c1 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# TINS 2023 + +# Building + +For building you need: +- A recent (0.11.0-dev) Zig compiler. +- Allegro 5.28 development libraries. + +You can use `allegro.path` if your Allegro development libraries can't be natively found. In this file you can put the path to a directory contain the Allegro `lib`, `bin` and `include` folders. + +# Licenses + +## Cabin font + +[Open Font License version 1.1](src/assets/fonts/Cabin-OFL.txt) diff --git a/allegro.path b/allegro.path new file mode 100644 index 0000000..e69de29 diff --git a/allegro/allegro.zig b/allegro/allegro.zig new file mode 100644 index 0000000..63cb0f7 --- /dev/null +++ b/allegro/allegro.zig @@ -0,0 +1,697 @@ +const std = @import("std"); + +pub const c = @import("c.zig").c; + +pub const Bitmap = struct { + native: *c.ALLEGRO_BITMAP, + + pub fn destroy(self: Bitmap) void { + destroyBitmap(self); + } + + pub fn draw(self: Bitmap, x: f32, y: f32, flags: DrawFlags) void { + drawBitmap(self, x, y, flags); + } + + pub fn drawCentered(self: Bitmap, x: f32, y: f32) void { + self.draw(x - 0.5 * @intToFloat(f32, self.width()), y - 0.5 * @intToFloat(f32, self.height()), DrawFlags{}); + } + + pub fn drawCenteredScaled(self: Bitmap, x: f32, y: f32, s: f32) void { + const w = s * @intToFloat(f32, self.width()); + const h = s * @intToFloat(f32, self.height()); + self.drawScaled(x - 0.5 * w, y - 0.5 * h, w, h, DrawFlags{}); + } + + pub fn drawScaled(self: Bitmap, x: f32, y: f32, w: f32, h: f32, flags: DrawFlags) void { + drawScaledBitmap(self, 0, 0, @intToFloat(f32, self.width()), @intToFloat(f32, self.height()), x, y, w, h, flags); + } + + pub fn drawTinted(self: Bitmap, tint: Color, x: f32, y: f32, flags: DrawFlags) void { + drawTintedBitmap(self, tint, x, y, flags); + } + + pub fn drawTintedCentered(self: Bitmap, tint: Color, x: f32, y: f32) void { + self.drawTinted(tint, x - 0.5 * @intToFloat(f32, self.width()), y - 0.5 * @intToFloat(f32, self.height()), DrawFlags{}); + } + + pub fn drawTintedCenteredScaled(self: Bitmap, tint: Color, x: f32, y: f32, s: f32) void { + const w = s * @intToFloat(f32, self.width()); + const h = s * @intToFloat(f32, self.height()); + self.drawTintedScaled(tint, x - 0.5 * w, y - 0.5 * h, w, h, DrawFlags{}); + } + + pub fn drawTintedScaled(self: Bitmap, tint: Color, x: f32, y: f32, w: f32, h: f32, flags: DrawFlags) void { + drawTintedScaledBitmap(self, tint, 0, 0, @intToFloat(f32, self.width()), @intToFloat(f32, self.height()), x, y, w, h, flags); + } + + pub fn height(self: Bitmap) i32 { + return getBitmapHeight(self); + } + + pub fn width(self: Bitmap) i32 { + return getBitmapWidth(self); + } +}; + +pub const Color = struct { + native: c.ALLEGRO_COLOR, + + fn hexToU4(hex: u8) u4 { + return switch (hex) { + '0'...'9' => @intCast(u4, hex - '0'), + 'a'...'f' => @intCast(u4, hex - 'a') + 10, + 'A'...'F' => @intCast(u4, hex - 'A') + 10, + else => unreachable, + }; + } + + pub fn initFromHex(hex: []const u8) Color { + if (hex.len == 0) unreachable; + + const left: usize = if (hex[0] == '#') 1 else 0; + + if (hex.len < left + 6) unreachable; + + const r = (@intCast(u8, hexToU4(hex[left])) << 4) + @intCast(u8, hexToU4(hex[left + 1])); + const g = (@intCast(u8, hexToU4(hex[left + 2])) << 4) + @intCast(u8, hexToU4(hex[left + 3])); + const b = (@intCast(u8, hexToU4(hex[left + 4])) << 4) + @intCast(u8, hexToU4(hex[left + 5])); + if (hex.len < left + 8) { + return mapRgb(r, g, b); + } + const a = (@intCast(u8, hexToU4(hex[left + 6])) << 4) + @intCast(u8, hexToU4(hex[left + 7])); + return mapRgba(r, g, b, a); + } +}; + +pub const Display = struct { + native: *c.ALLEGRO_DISPLAY, + + pub fn destroy(self: Display) void { + destroyDisplay(self); + } + + pub fn eventSource(self: Display) EventSource { + return getDisplayEventSource(self); + } + + pub fn height(self: Display) i32 { + return getDisplayHeight(self); + } + + pub fn width(self: Display) i32 { + return getDisplayWidth(self); + } +}; + +pub const DrawFlags = packed struct(c_int) { + FLIP_HORIZONTAL: bool = false, // 0x00001 + FLIP_VERTICAL: bool = false, // 0x00002 + _NOT_USED: u30 = 0, +}; + +pub const DrawTextFlags = enum(c_int) { + NO_KERNING = -1, + ALIGN_LEFT = 0, + ALIGN_CENTER = 1, + ALIGN_RIGHT = 2, + ALIGN_LEFT_INTEGER = 4, + ALIGN_CENTER_INTEGER = 5, + ALIGN_RIGHT_INTEGER = 6, +}; + +pub const Event = c.ALLEGRO_EVENT; + +pub const EventQueue = struct { + native: *c.ALLEGRO_EVENT_QUEUE, + + pub fn destroy(self: EventQueue) void { + destroyEventQueue(self); + } + + pub fn drop(self: EventQueue) void { + dropNextEvent(self); + } + + pub fn flush(self: EventQueue) void { + flushEventQueue(self); + } + + pub fn get(self: EventQueue, event: *Event) bool { + return getNextEvent(self, event); + } + + pub fn isEmpty(self: EventQueue) bool { + return isEventQueueEmpty(self); + } + + pub fn peek(self: EventQueue, event: *Event) void { + peekNextEvent(self, event); + } + + pub fn register(self: EventQueue, source: EventSource) void { + registerEventSource(self, source); + } + + pub fn registerDisplay(self: EventQueue, display: Display) void { + self.register(display.eventSource()); + } + + pub fn registerKeyboard(self: EventQueue) void { + self.register(getKeyboardEventSource()); + } + + pub fn registerMouse(self: EventQueue) void { + self.register(getMouseEventSource()); + } + + pub fn registerTimer(self: EventQueue, timer: Timer) void { + self.register(timer); + } + + pub fn unregister(self: EventQueue, source: EventSource) void { + unregisterEventSource(self, source); + } + + pub fn unregisterDisplay(self: EventQueue, display: Display) void { + self.unregister(display.eventSource()); + } + + pub fn wait(self: EventQueue, event: *Event) void { + waitForEvent(self, event); + } +}; + +pub const EventSource = struct { + native: *c.ALLEGRO_EVENT_SOURCE, +}; + +pub const File = struct { + native: *c.ALLEGRO_FILE, + + pub fn close(file: File) void { + fclose(file); + } +}; + +pub const Font = struct { + native: *c.ALLEGRO_FONT, + + pub fn destroy(self: Font) void { + destroyFont(self); + } + + pub fn draw(self: Font, color: Color, x: f32, y: f32, flags: DrawTextFlags, text: [*:0]const u8) void { + drawText(self, color, x, y, flags, text); + } + + pub fn textWidth(self: Font, text: [*:0]const u8) i32 { + return getTextWidth(self, text); + } +}; + +pub const LoadBitmapFlags = packed struct(c_int) { + _NOT_USED_1: bool = false, + KEEP_BITMAP_FORMAT: bool = false, // 0x0002 // was a bitmap flag in 5.0 + _NOT_USED_2: u7 = 0, + NO_PREMULTIPLIED_ALPHA: bool = false, // 0x0200 // was a bitmap flag in 5.0 + _NOT_USED_3: bool = false, + KEEP_INDEX: bool = false, // 0x0800 + _NOT_USED_4: u20 = 0, +}; + +pub const LoadTtfFontFlags = packed struct(c_int) { + NO_KERNING: bool = false, + MONOCHROME: bool = false, + NO_AUTOHINT: bool = false, + _NOT_USED: u29 = 0, +}; + +pub const NewBitmapFlags = packed struct(c_int) { + MEMORY_BITMAP: bool = false, // 0x0001, + _KEEP_BITMAP_FORMAT: bool = false, // 0x0002, + FORCE_LOCKING: bool = false, // 0x0004, + NO_PRESERVE_TEXTURE: bool = false, // 0x0008, + _ALPHA_TEST: bool = false, // 0x0010, + _INTERNAL_OPENGL: bool = false, // 0x0020, + MIN_LINEAR: bool = false, // 0x0040, + MAG_LINEAR: bool = false, // 0x0080, + MIPMAP: bool = false, // 0x0100, + _NO_PREMULTIPLIED_ALPHA: bool = false, // 0x0200, + VIDEO_BITMAP: bool = false, // 0x0400, + _NOT_USED_1: bool = false, // 0x0800 + CONVERT_BITMAP: bool = false, // 0x1000, + _NOT_USED_4: u19 = 0, +}; + +pub const NewDisplayFlags = packed struct(c_int) { + WINDOWED: bool = false, + FULLSCREEN: bool = false, + OPENGL: bool = false, + DIRECT3D_INTERNAL: bool = false, + RESIZABLE: bool = false, + FRAMELESS: bool = false, + GENERATE_EXPOSE_EVENTS: bool = false, + OPENGL_3_0: bool = false, + OPENGL_FORWARD_COMPATIBLE: bool = false, + FULLSCREEN_WINDOW: bool = false, + MINIMIZED: bool = false, + PROGRAMMABLE_PIPELINE: bool = false, + GTK_TOPLEVEL_INTERNAL: bool = false, + MAXIMIZED: bool = false, + OPENGL_ES_PROFILE: bool = false, + OPENGL_CORE_PROFILE: bool = false, + _NOT_USED: u16 = 0, +}; + +pub const NewDisplayOption = enum(c_int) { + RED_SIZE, // 0 + GREEN_SIZE, // 1 + BLUE_SIZE, // 2 + ALPHA_SIZE, // 3 + RED_SHIFT, // 4 + GREEN_SHIFT, // 5 + BLUE_SHIFT, // 6 + ALPHA_SHIFT, // 7 + ACC_RED_SIZE, // 8 + ACC_GREEN_SIZE, // 9 + ACC_BLUE_SIZE, // 10 + ACC_ALPHA_SIZE, // 11 + STEREO, // 12 + AUX_BUFFERS, // 13 + COLOR_SIZE, // 14 + DEPTH_SIZE, // 15 + STENCIL_SIZE, // 16 + SAMPLE_BUFFERS, // 17 + SAMPLES, // 18 + RENDER_METHOD, // 19 + FLOAT_COLOR, // 20 + FLOAT_DEPTH, // 21 + SINGLE_BUFFER, // 22 + SWAP_METHOD, // 23 + COMPATIBLE_DISPLAY, // 24 + UPDATE_DISPLAY_REGION, // 25 + VSYNC, // 26 + MAX_BITMAP_SIZE, // 27 + SUPPORT_NPOT_BITMAP, // 28 + CAN_DRAW_INTO_BITMAP, // 29 + SUPPORT_SEPARATE_ALPHA, // 30 + AUTO_CONVERT_BITMAPS, // 31 + SUPPORTED_ORIENTATIONS, // 32 + OPENGL_MAJOR_VERSION, // 33 + OPENGL_MINOR_VERSION, // 34 + DEFAULT_SHADER_PLATFORM, // 35 +}; + +pub const OptionImportance = enum(c_int) { + DONTCARE, // 0 + REQUIRE, // 1 + SUGGEST, // 2 +}; + +pub const PixelFormat = enum(c_int) { + ANY, // 0 + ANY_NO_ALPHA, // 1 + ANY_WITH_ALPHA, // 2 + ANY_15_NO_ALPHA, // 3 + ANY_16_NO_ALPHA, // 4 + ANY_16_WITH_ALPHA, // 5 + ANY_24_NO_ALPHA, // 6 + ANY_32_NO_ALPHA, // 7 + ANY_32_WITH_ALPHA, // 8 + ARGB_8888, // 9 + RGBA_8888, // 10 + ARGB_4444, // 11 + RGB_888, // 12 // 24 bit format + RGB_565, // 13 + RGB_555, // 14 + RGBA_5551, // 15 + ARGB_1555, // 16 + ABGR_8888, // 17 + XBGR_8888, // 18 + BGR_888, // 19 // 24 bit format + BGR_565, // 20 + BGR_555, // 21 + RGBX_8888, // 22 + XRGB_8888, // 23 + ABGR_F32, // 24 + ABGR_8888_LE, // 25 + RGBA_4444, // 26 + SINGLE_CHANNEL_8, // 27 + COMPRESSED_RGBA_DXT1, // 28 + COMPRESSED_RGBA_DXT3, // 29 + COMPRESSED_RGBA_DXT5, // 30 + ALLEGRO_NUM_PIXEL_FORMATS, +}; + +pub const Timer = struct { + native: *c.ALLEGRO_TIMER, + + pub fn destroy(self: Timer) void { + destroyTimer(self); + } + + pub fn eventSource(self: Timer) EventSource { + return getTimerEventSource(self); + } + + pub fn start(self: Timer) void { + startTimer(self); + } +}; + +pub fn acknowledgeResize(display: Display) bool { + return c.al_acknowledge_resize(display.native); +} + +pub fn clearToColor(color: Color) void { + c.al_clear_to_color(color.native); +} + +pub fn convertMemoryBitmaps() void { + c.al_convert_memory_bitmaps(); +} + +pub fn createBitmap(width: i32, height: i32) !Bitmap { + const bitmap = c.al_create_bitmap(width, height); + if (bitmap) |native| { + return Bitmap{ .native = native }; + } + return error.FailedToCreateBitmap; +} + +pub fn createDisplay(width: i32, height: i32) !Display { + const display = c.al_create_display(width, height); + if (display) |native| { + return Display{ .native = native }; + } + return error.FailedToCreateDisplay; +} + +pub fn createEventQueue() !EventQueue { + const queue = c.al_create_event_queue(); + if (queue) |native| { + return EventQueue{ .native = native }; + } + return error.FailedToCreateEventQueue; +} + +pub fn createTimer(interval: f32) !Timer { + const timer = c.al_create_timer(interval); + if (timer) |native| { + return Timer{ .native = native }; + } + return error.FailedToCreateTimer; +} + +pub fn destroyBitmap(bitmap: Bitmap) void { + c.al_destroy_bitmap(bitmap.native); +} + +pub fn destroyDisplay(display: Display) void { + c.al_destroy_display(display.native); +} + +pub fn destroyEventQueue(queue: EventQueue) void { + c.al_destroy_event_queue(queue.native); +} + +pub fn destroyFont(font: Font) void { + c.al_destroy_font(font.native); +} + +pub fn destroyTimer(timer: Timer) void { + c.al_destroy_timer(timer.native); +} + +pub fn drawBitmap(bitmap: Bitmap, dx: f32, dy: f32, flags: DrawFlags) void { + c.al_draw_bitmap(bitmap.native, dx, dy, @bitCast(c_int, flags)); +} + +pub fn drawScaledBitmap(bitmap: Bitmap, sx: f32, sy: f32, sw: f32, sh: f32, dx: f32, dy: f32, dw: f32, dh: f32, flags: DrawFlags) void { + c.al_draw_scaled_bitmap(bitmap.native, sx, sy, sw, sh, dx, dy, dw, dh, @bitCast(c_int, flags)); +} + +pub fn drawText(font: Font, color: Color, x: f32, y: f32, flags: DrawTextFlags, text: [*:0]const u8) void { + c.al_draw_text(font.native, color.native, x, y, @enumToInt(flags), text); +} + +pub fn drawTintedBitmap(bitmap: Bitmap, tint: Color, dx: f32, dy: f32, flags: DrawFlags) void { + c.al_draw_tinted_bitmap(bitmap.native, tint.native, dx, dy, @bitCast(c_int, flags)); +} + +pub fn drawTintedScaledBitmap(bitmap: Bitmap, tint: Color, sx: f32, sy: f32, sw: f32, sh: f32, dx: f32, dy: f32, dw: f32, dh: f32, flags: DrawFlags) void { + c.al_draw_tinted_scaled_bitmap(bitmap.native, tint.native, sx, sy, sw, sh, dx, dy, dw, dh, @bitCast(c_int, flags)); +} + +pub fn dropNextEvent(queue: EventQueue) bool { + return c.al_drop_next_event(queue.native); +} + +pub fn fclose(file: File) bool { + return c.al_fclose(file.native); +} + +pub fn flipDisplay() void { + c.al_flip_display(); +} + +pub fn flushEventQueue(queue: EventQueue) void { + c.al_flush_event_queue(queue.native); +} + +pub fn isEventQueueEmpty(queue: EventQueue) bool { + return c.al_is_event_queue_empty(queue.native); +} + +pub fn getBackbuffer(display: Display) !Bitmap { + const bitmap = c.al_get_backbuffer(display.native); + if (bitmap) |native| { + return Bitmap{ .native = native }; + } + return error.FailedToGetBackBufferForDisplay; +} + +pub fn getBitmapHeight(bitmap: Bitmap) c_int { + return c.al_get_bitmap_height(bitmap.native); +} + +pub fn getBitmapWidth(bitmap: Bitmap) c_int { + return c.al_get_bitmap_width(bitmap.native); +} + +pub fn getCurrentDisplay() !Display { + const display = c.al_get_current_display(); + if (display) |native| { + return Display{ .native = native }; + } + return error.NoCurrentDisplay; +} + +pub fn getDisplayEventSource(display: Display) EventSource { + return EventSource{ .native = c.al_get_display_event_source(display.native) }; +} + +pub fn getDisplayFlags(display: Display) NewDisplayFlags { + return @bitCast(NewDisplayFlags, c.al_get_display_flags(display.native)); +} + +pub fn getDisplayHeight(display: Display) i32 { + return c.al_get_display_height(display.native); +} + +pub fn getDisplayWidth(display: Display) i32 { + return c.al_get_display_width(display.native); +} + +pub fn getFontAscent(font: Font) i32 { + return c.al_get_font_ascent(font.native); +} + +pub fn getFontDescent(font: Font) i32 { + return c.al_get_font_descent(font.native); +} + +pub fn getFontLineHeight(font: Font) i32 { + return c.al_get_font_line_height(font.native); +} + +pub fn getKeyboardEventSource() EventSource { + return EventSource{ .native = c.al_get_keyboard_event_source() }; +} + +pub fn getNewBitmapFlags() NewBitmapFlags { + return c.al_set_new_bitmap_flags(); +} + +pub fn getNewDisplayOption(option: NewDisplayOption, importance: *OptionImportance) c_int { + return c.al_get_new_display_option(@enumToInt(option), @ptrCast(*c_int, importance)); +} + +pub fn getNewBitmapFormat() PixelFormat { + return c.al_set_new_bitmap_format(); +} + +pub fn getNewDisplayFlags() NewDisplayFlags { + return c.al_get_new_display_flags(); +} + +pub fn getNextEvent(queue: EventQueue, event: *Event) bool { + return c.al_get_next_event(queue.native, event); +} + +pub fn getMouseEventSource() EventSource { + return EventSource{ .native = c.al_get_mouse_event_source() }; +} + +pub fn getTextWidth(font: Font, text: [*:0]const u8) void { + c.al_get_text_width(font.native, &text[0]); +} + +pub fn getTime() f64 { + return c.al_get_time(); +} + +pub fn getTimerEventSource(timer: Timer) EventSource { + return EventSource{ .native = c.al_get_timer_event_source(timer.native) }; +} + +pub fn init() bool { + return c.al_init(); +} + +pub fn initFontAddon() bool { + return c.al_init_font_addon(); +} + +pub fn initImageAddon() bool { + return c.al_init_image_addon(); +} + +pub fn initTtfAddon() bool { + return c.al_init_ttf_addon(); +} + +pub fn installKeyboard() bool { + return c.al_install_keyboard(); +} + +pub fn installMouse() bool { + return c.al_install_mouse(); +} + +pub fn loadBitmap(path: [*:0]const u8) !Bitmap { + const bitmap = c.al_load_bitmap(path); + if (bitmap) |native| { + return Bitmap{ .native = native }; + } + return error.FailedToLoadBitmap; +} + +pub fn loadBitmapF(file: File, ident: []const u8) !Bitmap { + const bitmap = c.al_load_bitmap_f(file.native, &ident[0]); + if (bitmap) |native| { + return Bitmap{ .native = native }; + } + return error.FailedToLoadBitmap; +} + +pub fn loadBitmapFont(path: [*:0]const u8) !Font { + const font = c.al_load_bitmap_font(path); + if (font) |native| { + return Font{ .native = native }; + } + return error.FailedToLoadFont; +} + +pub fn loadFont(path: [*:0]const u8, size: i32, flags: c_int) !Font { + const font = c.al_load_font(path, size, flags); + if (font) |native| { + return Font{ .native = native }; + } + return error.FailedToLoadFont; +} + +pub fn loadTtfFont(path: [*:0]const u8, size: i32, flags: LoadTtfFontFlags) !Font { + const font = c.al_load_ttf_font(path, size, @bitCast(c_int, flags)); + if (font) |native| { + return Font{ .native = native }; + } + return error.FailedToLoadFont; +} + +pub fn mapRgb(r: u8, g: u8, b: u8) Color { + return Color{ .native = c.al_map_rgb(r, g, b) }; +} + +pub fn mapRgba(r: u8, g: u8, b: u8, a: u8) Color { + return Color{ .native = c.al_map_rgba(r, g, b, a) }; +} + +pub fn mapRgbaF(r: f32, g: f32, b: f32, a: f32) Color { + return Color{ .native = c.al_map_rgba_f(r, g, b, a) }; +} + +pub fn mapRgbF(r: f32, g: f32, b: f32) Color { + return Color{ .native = c.al_map_rgb_f(r, g, b) }; +} + +pub fn openMemfile(data: []u8, mode: []const u8) !File { + const file = c.al_open_memfile(&data[0], @intCast(i64, data.len), &mode[0]); + if (file) |native| { + return File{ .native = native }; + } + return error.FailedToOpenMemfile; +} + +pub fn peekNextEvent(queue: EventQueue, event: *Event) bool { + return c.al_peek_next_event(queue.native, event); +} + +pub fn registerEventSource(queue: EventQueue, source: EventSource) void { + c.al_register_event_source(queue.native, source.native); +} + +pub fn setDisplayFlag(display: Display, flag: NewDisplayFlags, on: bool) bool { + return c.al_set_display_flag(display.native, @bitCast(c_int, flag), on); +} + +pub fn setNewBitmapFlags(flags: NewBitmapFlags) void { + c.al_set_new_bitmap_flags(@bitCast(c_int, flags)); +} + +pub fn setNewBitmapFormat(format: PixelFormat) void { + c.al_set_new_bitmap_format(format); +} + +pub fn setNewDisplayFlags(flags: NewDisplayFlags) void { + c.al_set_new_display_flags(@bitCast(c_int, flags)); +} + +pub fn setNewDisplayOption(option: NewDisplayOption, value: i32, importance: OptionImportance) void { + c.al_set_new_display_option(@enumToInt(option), value, @enumToInt(importance)); +} + +pub fn setTargetBitmap(bitmap: Bitmap) void { + c.al_set_target_bitmap(bitmap.native); +} + +pub fn setTargetBackbuffer(display: Display) void { + c.al_set_target_bitmap(display.native); +} + +pub fn startTimer(timer: Timer) void { + c.al_start_timer(timer.native); +} + +pub fn unregisterEventSource(queue: EventQueue, source: EventSource) void { + c.al_unregister_event_source(queue.native, source.native); +} + +pub fn waitForEvent(queue: EventQueue, event: *Event) void { + c.al_wait_for_event(queue.native, event); +} + +pub fn waitForVsync() bool { + return c.al_wait_for_vsync(); +} diff --git a/allegro/build.zig b/allegro/build.zig new file mode 100644 index 0000000..75acdb7 --- /dev/null +++ b/allegro/build.zig @@ -0,0 +1,24 @@ +const std = @import("std"); + +fn current_file() []const u8 { + return @src().file; +} + +const cwd = std.fs.path.dirname(current_file()).?; +const path_separator = std.fs.path.sep_str; + +/// add this package to exe +pub fn addTo(comptime allegro_dir: []const u8, exe: *std.build.LibExeObjStep) void { + exe.addAnonymousModule("allegro", .{ .source_file = .{ .path = cwd ++ path_separator ++ "allegro.zig" } }); + if (allegro_dir.len > 0) { + exe.addIncludePath(allegro_dir ++ path_separator ++ "include"); + exe.addLibraryPath(allegro_dir ++ path_separator ++ "lib"); + } + exe.linkLibC(); + + exe.linkSystemLibrary("allegro"); + exe.linkSystemLibrary("allegro_font"); + exe.linkSystemLibrary("allegro_image"); + exe.linkSystemLibrary("allegro_memfile"); + exe.linkSystemLibrary("allegro_ttf"); +} diff --git a/allegro/c.zig b/allegro/c.zig new file mode 100644 index 0000000..2f14d1c --- /dev/null +++ b/allegro/c.zig @@ -0,0 +1,8 @@ +pub const c = @cImport({ + @cInclude("stdint.h"); + @cInclude("allegro5/allegro.h"); + @cInclude("allegro5/allegro_font.h"); + @cInclude("allegro5/allegro_image.h"); + @cInclude("allegro5/allegro_memfile.h"); + @cInclude("allegro5/allegro_ttf.h"); +}); diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..1d79485 --- /dev/null +++ b/build.zig @@ -0,0 +1,75 @@ +const std = @import("std"); +const allegro = @import("allegro/build.zig"); + +const allegroPath = @embedFile("allegro.path"); + +// Although this function looks imperative, note that its job is to +// declaratively construct a build graph that will be executed by an external +// runner. +pub fn build(b: *std.Build) void { + // Standard target options allows the person running `zig build` to choose + // what target to build for. Here we do not override the defaults, which + // means any target is allowed, and the default is native. Other options + // for restricting supported target set are available. + const target = b.standardTargetOptions(.{}); + + // Standard optimization options allow the person running `zig build` to select + // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not + // set a preferred release mode, allowing the user to decide how to optimize. + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "tins2023", + // In this case the main source file is merely a path, however, in more + // complicated build scripts, this could be a generated file. + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + allegro.addTo(allegroPath, exe); + + // This declares intent for the executable to be installed into the + // standard location when the user invokes the "install" step (the default + // step when running `zig build`). + b.installArtifact(exe); + + // This *creates* a Run step in the build graph, to be executed when another + // step is evaluated that depends on it. The next line below will establish + // such a dependency. + const run_cmd = b.addRunArtifact(exe); + + // By making the run step depend on the install step, it will be run from the + // installation directory rather than directly from within the cache directory. + // This is not necessary, however, if the application depends on other installed + // files, this ensures they will be present and in the expected location. + run_cmd.step.dependOn(b.getInstallStep()); + + // This allows the user to pass arguments to the application in the build + // command itself, like this: `zig build run -- arg1 arg2 etc` + if (b.args) |args| { + run_cmd.addArgs(args); + } + + // This creates a build step. It will be visible in the `zig build --help` menu, + // and can be selected like this: `zig build run` + // This will evaluate the `run` step rather than the default, which is "install". + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + // Creates a step for unit testing. This only builds the test executable + // but does not run it. + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const run_unit_tests = b.addRunArtifact(unit_tests); + + // Similar to creating the run step earlier, this exposes a `test` step to + // the `zig build --help` menu, providing a way for the user to request + // running the unit tests. + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/src/assets/fonts/Cabin-OFL.txt b/src/assets/fonts/Cabin-OFL.txt new file mode 100644 index 0000000..27f0a41 --- /dev/null +++ b/src/assets/fonts/Cabin-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2018 The Cabin Project Authors (https://github.com/impallari/Cabin.git) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/assets/fonts/Cabin-Regular.ttf b/src/assets/fonts/Cabin-Regular.ttf new file mode 100644 index 0000000..5ca7f6f Binary files /dev/null and b/src/assets/fonts/Cabin-Regular.ttf differ diff --git a/src/assets/images/opaque.png b/src/assets/images/opaque.png new file mode 100644 index 0000000..dee54f4 Binary files /dev/null and b/src/assets/images/opaque.png differ diff --git a/src/assets/images/title_untitled.png b/src/assets/images/title_untitled.png new file mode 100644 index 0000000..46f3419 Binary files /dev/null and b/src/assets/images/title_untitled.png differ diff --git a/src/context.zig b/src/context.zig new file mode 100644 index 0000000..9051967 --- /dev/null +++ b/src/context.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const allegro = @import("allegro"); +const engine = @import("engine.zig"); +const Palette = @import("palette.zig").Palette; +const Scene = @import("scene.zig").Scene; + +pub const Context = struct { + const DefaultDisplayWidth = 1280; + const DefaultDisplayHeight = 720; + + allocator: std.mem.Allocator, + palette: Palette = undefined, + shouldQuit: bool = false, + scene: ?Scene = null, + + display: allegro.Display, + events: allegro.EventQueue, + + viewport: engine.Viewport, + fonts: engine.Fonts, + textures: engine.Textures, + + pub fn init(allocator: std.mem.Allocator) !Context { + _ = allegro.init(); + _ = allegro.initImageAddon(); + _ = allegro.installKeyboard(); + _ = allegro.installMouse(); + _ = allegro.initFontAddon(); + _ = allegro.initTtfAddon(); + + allegro.setNewDisplayOption(allegro.NewDisplayOption.VSYNC, 1, allegro.OptionImportance.REQUIRE); + allegro.setNewDisplayOption(allegro.NewDisplayOption.SAMPLE_BUFFERS, 1, allegro.OptionImportance.REQUIRE); + allegro.setNewDisplayOption(allegro.NewDisplayOption.SAMPLES, 4, allegro.OptionImportance.REQUIRE); + allegro.setNewDisplayFlags(allegro.NewDisplayFlags{ .RESIZABLE = true }); + + const viewport = engine.Viewport.init(DefaultDisplayWidth, DefaultDisplayHeight); + const display = try allegro.createDisplay(DefaultDisplayWidth, DefaultDisplayHeight); + + const events = try allegro.createEventQueue(); + + events.registerDisplay(display); + events.registerKeyboard(); + events.registerMouse(); + + return Context{ + .allocator = allocator, + .display = display, + .events = events, + .viewport = viewport, + .fonts = engine.Fonts.init(allocator), + .textures = engine.Textures.init(allocator), + }; + } + + fn exitScene(self: *Context) void { + if (self.scene) |scene| { + scene.exit(self); + scene.deinit(); + self.scene = null; + } + } + + pub fn deinit(self: *Context) void { + self.events.destroy(); + self.display.destroy(); + self.exitScene(); + self.fonts.deinit(); + self.textures.deinit(); + } + + pub fn quit(self: *Context) void { + self.shouldQuit = true; + } + + pub fn resized(self: *Context, width: i32, height: i32) void { + self.viewport.update(width, height); + } + + pub fn switchToScene(self: *Context, comptime SceneType: type, build: ?*const fn (*SceneType) void) !void { + self.exitScene(); + + const scene = try Scene.init(SceneType, self.allocator, build); + self.scene = scene; + scene.enter(self); + } + + pub fn toggleFullScreen(self: *Context) void { + var displayFlags = allegro.getDisplayFlags(self.display); + _ = allegro.setDisplayFlag(self.display, allegro.NewDisplayFlags{ .FULLSCREEN_WINDOW = true }, !displayFlags.FULLSCREEN_WINDOW); + self.viewport.update(self.display.width(), self.display.height()); + } +}; diff --git a/src/engine.zig b/src/engine.zig new file mode 100644 index 0000000..5ad1421 --- /dev/null +++ b/src/engine.zig @@ -0,0 +1,11 @@ +const assets = @import("engine/assets.zig"); + +pub const Fonts = assets.Fonts; +pub const OpaquePtr = @import("engine/opaque_ptr.zig").OpaquePtr; +pub const Point = @import("engine/point.zig").Point; +pub const PointF = @import("engine/point_f.zig").PointF; +pub const Rectangle = @import("engine/rectangle.zig").Rectangle; +pub const RectangleF = @import("engine/rectangle_f.zig").RectangleF; +pub const Scene = @import("engine/scene.zig").Scene; +pub const Textures = assets.Textures; +pub const Viewport = @import("engine/viewport.zig").Viewport; diff --git a/src/engine/assets.zig b/src/engine/assets.zig new file mode 100644 index 0000000..a5d31cf --- /dev/null +++ b/src/engine/assets.zig @@ -0,0 +1,97 @@ +const std = @import("std"); +const allegro = @import("allegro"); + +pub fn Assets(comptime Asset: type, comptime destroy: *const fn (Asset) void) type { + return struct { + const Self = @This(); + + assets: std.StringHashMap(Asset), + + pub fn init(allocator: std.mem.Allocator) Self { + return Self{ + .assets = std.StringHashMap(Asset).init(allocator), + }; + } + + pub fn deinit(self: *Self) void { + var assets = self.assets.iterator(); + while (assets.next()) |entry| { + destroy(entry.value_ptr.*); + } + self.assets.deinit(); + } + + pub fn get(self: Self, name: []const u8) ?Asset { + return self.assets.get(name); + } + + pub fn put(self: *Self, name: []const u8, asset: Asset) !void { + try self.assets.put(name, asset); + } + }; +} + +pub const Fonts = struct { + fn destroy(font: allegro.Font) void { + font.destroy(); + } + + const Map = Assets(allegro.Font, Fonts.destroy); + + assets: Map, + + pub fn init(allocator: std.mem.Allocator) Fonts { + return Fonts{ + .assets = Map.init(allocator), + }; + } + + pub fn deinit(self: *Fonts) void { + self.assets.deinit(); + } + + pub fn addFromFileTTF(self: *Fonts, name: []const u8, path: [*:0]const u8, size: i32) !void { + const font = try allegro.loadTtfFont(path, size, allegro.LoadTtfFontFlags{}); + try self.assets.put(name, font); + } + + pub fn get(self: Fonts, name: []const u8) ?allegro.Font { + return self.assets.get(name); + } +}; + +pub const Textures = struct { + fn destroy(bitmap: allegro.Bitmap) void { + bitmap.destroy(); + } + const Map = Assets(allegro.Bitmap, destroy); + + assets: Map, + + pub fn init(allocator: std.mem.Allocator) Textures { + return Textures{ + .assets = Map.init(allocator), + }; + } + + pub fn deinit(self: *Textures) void { + self.assets.deinit(); + } + + pub fn addFromFile(self: *Textures, name: []const u8, path: [*:0]const u8) !void { + const bitmap = try allegro.loadBitmap(path); + try self.assets.put(name, bitmap); + } + + pub fn addFromMemoryPNG(self: *Textures, name: []const u8, data: []const u8) !void { + const memoryFile = try allegro.openMemfile(@constCast(data), "r"); + defer _ = allegro.fclose(memoryFile); + + const bitmap = try allegro.loadBitmapF(memoryFile, ".png"); + try self.assets.put(name, bitmap); + } + + pub fn get(self: Textures, name: []const u8) ?allegro.Bitmap { + return self.assets.get(name); + } +}; diff --git a/src/engine/opaque_ptr.zig b/src/engine/opaque_ptr.zig new file mode 100644 index 0000000..3920574 --- /dev/null +++ b/src/engine/opaque_ptr.zig @@ -0,0 +1,43 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub const OpaquePtr = struct { + allocator: Allocator, + object: usize = undefined, + virtualTable: VirtualTable = undefined, + + const VirtualTable = struct { + destroy: *const fn (OpaquePtr) void, + }; + + pub fn Object(comptime Type: type) type { + return struct { + ptr: *Type, + opaquePtr: OpaquePtr, + }; + } + + pub fn create(comptime Type: type, allocator: Allocator) !Object(Type) { + const ptr = try allocator.create(Type); + const opaq = OpaquePtr{ + .allocator = allocator, + .object = @ptrToInt(ptr), + .virtualTable = .{ + .destroy = struct { + fn destroy(self: OpaquePtr) void { + const object = @intToPtr(*Type, self.object); + self.allocator.destroy(object); + } + }.destroy, + }, + }; + return Object(Type){ + .ptr = ptr, + .opaquePtr = opaq, + }; + } + + pub fn destroy(self: OpaquePtr) void { + self.virtualTable.destroy(self); + } +}; diff --git a/src/engine/point.zig b/src/engine/point.zig new file mode 100644 index 0000000..11944af --- /dev/null +++ b/src/engine/point.zig @@ -0,0 +1,30 @@ +const PointF = @import("point_f.zig").PointF; + +pub const Point = struct { + x: i64, + y: i64, + + pub fn init(x: i64, y: i64) Point { + return Point{ .x = x, .y = y }; + } + + pub fn initUsize(x: usize, y: usize) Point { + return Point.init(@intCast(i64, x), @intCast(i64, y)); + } + + pub fn add(self: Point, other: Point) Point { + return Point{ .x = self.x + other.x, .y = self.y + other.y }; + } + + pub fn float(self: Point) PointF { + return PointF{ .x = @intToFloat(f32, self.x), .y = @intToFloat(f32, self.y) }; + } + + pub fn multiply(self: Point, factor: i64) Point { + return Point{ .x = self.x * factor, .y = self.y * factor }; + } + + pub fn subtract(self: Point, other: Point) Point { + return Point{ .x = self.x - other.x, .y = self.y - other.y }; + } +}; diff --git a/src/engine/point_f.zig b/src/engine/point_f.zig new file mode 100644 index 0000000..2746fdd --- /dev/null +++ b/src/engine/point_f.zig @@ -0,0 +1,36 @@ +const std = @import("std"); + +const Point = @import("point.zig").Point; + +pub const PointF = struct { + x: f32, + y: f32, + + pub fn init(x: f32, y: f32) PointF { + return PointF{ .x = x, .y = y }; + } + + pub fn add(self: PointF, other: PointF) PointF { + return PointF{ .x = self.x + other.x, .y = self.y + other.y }; + } + + pub fn ceil(self: PointF) PointF { + return PointF{ .x = std.math.ceil(self.x), .y = std.math.ceil(self.y) }; + } + + pub fn floor(self: PointF) PointF { + return PointF{ .x = std.math.floor(self.x), .y = std.math.floor(self.y) }; + } + + pub fn integer(self: PointF) Point { + return Point{ .x = @floatToInt(i64, self.x), .y = @floatToInt(i64, self.y) }; + } + + pub fn multiply(self: PointF, factor: f32) PointF { + return PointF{ .x = self.x * factor, .y = self.y * factor }; + } + + pub fn subtract(self: PointF, other: PointF) PointF { + return PointF{ .x = self.x - other.x, .y = self.y - other.y }; + } +}; diff --git a/src/engine/rectangle.zig b/src/engine/rectangle.zig new file mode 100644 index 0000000..ef38cee --- /dev/null +++ b/src/engine/rectangle.zig @@ -0,0 +1,75 @@ +const Point = @import("point.zig").Point; + +pub const Rectangle = struct { + min: Point, + max: Point, + + pub fn init(min: Point, max: Point) Rectangle { + if (min.x > max.x) unreachable; + if (min.y > max.y) unreachable; + + return Rectangle{ .min = min, .max = max }; + } + + pub fn initAbsolute(x1: i64, y1: i64, x2: i64, y2: i64) Rectangle { + if (x1 < x2) { + if (y1 < y2) { + return Rectangle{ + .min = Point{ .x = x1, .y = y1 }, + .max = Point{ .x = x2, .y = y2 }, + }; + } + return Rectangle{ + .min = Point{ .x = x1, .y = y2 }, + .max = Point{ .x = x2, .y = y1 }, + }; + } + if (y1 < y2) { + return Rectangle{ + .min = Point{ .x = x2, .y = y1 }, + .max = Point{ .x = x1, .y = y2 }, + }; + } + return Rectangle{ + .min = Point{ .x = x2, .y = y2 }, + .max = Point{ .x = x1, .y = y1 }, + }; + } + + pub fn initRelative(x: i64, y: i64, w: i64, h: i64) Rectangle { + if (w < 0) { + if (h < 0) { + return Rectangle{ + .min = Point{ .x = x + w, .y = y + h }, + .max = Point{ .x = x, .y = y }, + }; + } + return Rectangle{ + .min = Point{ .x = x + w, .y = y }, + .max = Point{ .x = x, .y = y + h }, + }; + } + if (h < 0) { + return Rectangle{ + .min = Point{ .x = x, .y = y + h }, + .max = Point{ .x = x + w, .y = y }, + }; + } + return Rectangle{ + .min = Point{ .x = x, .y = y }, + .max = Point{ .x = x + w, .y = y + h }, + }; + } + + pub fn center(self: Rectangle) Point { + return Point.init(@divTrunc(self.min.x + self.max.x, 2), @divTrunc(self.min.y + self.max.y, 2)); + } + + pub fn height(self: Rectangle) i64 { + return self.max.y - self.min.y; + } + + pub fn width(self: Rectangle) i64 { + return self.max.x - self.min.x; + } +}; diff --git a/src/engine/rectangle_f.zig b/src/engine/rectangle_f.zig new file mode 100644 index 0000000..6dd3abe --- /dev/null +++ b/src/engine/rectangle_f.zig @@ -0,0 +1,75 @@ +const PointF = @import("point_f.zig").PointF; + +pub const RectangleF = struct { + min: PointF, + max: PointF, + + pub fn init(min: PointF, max: PointF) RectangleF { + if (min.x > max.x) unreachable; + if (min.y > max.y) unreachable; + + return RectangleF{ .min = min, .max = max }; + } + + pub fn initAbsolute(x1: f32, y1: f32, x2: f32, y2: f32) RectangleF { + if (x1 < x2) { + if (y1 < y2) { + return RectangleF{ + .min = PointF{ .x = x1, .y = y1 }, + .max = PointF{ .x = x2, .y = y2 }, + }; + } + return RectangleF{ + .min = PointF{ .x = x1, .y = y2 }, + .max = PointF{ .x = x2, .y = y1 }, + }; + } + if (y1 < y2) { + return RectangleF{ + .min = PointF{ .x = x2, .y = y1 }, + .max = PointF{ .x = x1, .y = y2 }, + }; + } + return RectangleF{ + .min = PointF{ .x = x2, .y = y2 }, + .max = PointF{ .x = x1, .y = y1 }, + }; + } + + pub fn initRelative(x: f32, y: f32, w: f32, h: f32) RectangleF { + if (w < 0) { + if (h < 0) { + return RectangleF{ + .min = PointF{ .x = x + w, .y = y + h }, + .max = PointF{ .x = x, .y = y }, + }; + } + return RectangleF{ + .min = PointF{ .x = x + w, .y = y }, + .max = PointF{ .x = x, .y = y + h }, + }; + } + if (h < 0) { + return RectangleF{ + .min = PointF{ .x = x, .y = y + h }, + .max = PointF{ .x = x + w, .y = y }, + }; + } + return RectangleF{ + .min = PointF{ .x = x, .y = y }, + .max = PointF{ .x = x + w, .y = y + h }, + }; + } + + pub fn center(self: RectangleF) PointF { + return self.min.add(self.max).multiply(0.5); + } + + pub fn height(self: RectangleF) f32 { + return self.max.y - self.min.y; + } + + pub fn width(self: RectangleF) f32 { + return self.max.x - self.min.x; + } +}; diff --git a/src/engine/scene.zig b/src/engine/scene.zig new file mode 100644 index 0000000..ea026e3 --- /dev/null +++ b/src/engine/scene.zig @@ -0,0 +1,90 @@ +const std = @import("std"); +const OpaquePtr = @import("opaque_ptr.zig").OpaquePtr; + +pub fn Scene(comptime Context: type, comptime Event: type) type { + return struct { + const Self = @This(); + + object: usize = undefined, + virtualTable: VirtualTable = undefined, + opaquePtr: ?OpaquePtr = null, + + const VirtualTable = struct { + enter: *const fn (Self, *Context) void, + exit: *const fn (Self, *Context) void, + handle: *const fn (Self, *Context, Event) anyerror!void, + update: *const fn (Self, *Context, f32) void, + render: *const fn (Self, *Context) void, + }; + + pub fn enter(self: Self, ctx: *Context) void { + self.virtualTable.enter(self, ctx); + } + + pub fn exit(self: Self, ctx: *Context) void { + self.virtualTable.exit(self, ctx); + } + + pub fn handle(self: Self, ctx: *Context, event: Event) !void { + try self.virtualTable.handle(self, ctx, event); + } + + pub fn update(self: Self, ctx: *Context, dt: f32) void { + self.virtualTable.update(self, ctx, dt); + } + + pub fn render(self: Self, ctx: *Context) void { + self.virtualTable.render(self, ctx); + } + + pub fn makeOpaque(comptime SceneType: type, object: *SceneType) Self { + return Self{ .object = @ptrToInt(object), .virtualTable = .{ + .enter = struct { + fn enter(self: Self, ctx: *Context) void { + const scene = @intToPtr(*SceneType, self.object); + scene.enter(ctx); + } + }.enter, + .exit = struct { + fn exit(self: Self, ctx: *Context) void { + const scene = @intToPtr(*SceneType, self.object); + scene.exit(ctx); + } + }.exit, + .handle = struct { + fn handle(self: Self, ctx: *Context, event: Event) !void { + const scene = @intToPtr(*SceneType, self.object); + try scene.handle(ctx, event); + } + }.handle, + .update = struct { + fn update(self: Self, ctx: *Context, dt: f32) void { + const scene = @intToPtr(*SceneType, self.object); + scene.update(ctx, dt); + } + }.update, + .render = struct { + fn render(self: Self, ctx: *Context) void { + const scene = @intToPtr(*SceneType, self.object); + scene.render(ctx); + } + }.render, + } }; + } + + pub fn init(comptime SceneType: type, allocator: std.mem.Allocator, build: ?*const fn (*SceneType) void) !Self { + const object = try OpaquePtr.create(SceneType, allocator); + var scene = Self.makeOpaque(SceneType, object.ptr); + if (build) |b| { + b(object.ptr); + } + return scene; + } + + pub fn deinit(self: Self) void { + if (self.opaquePtr) |ptr| { + ptr.destroy(); + } + } + }; +} diff --git a/src/engine/viewport.zig b/src/engine/viewport.zig new file mode 100644 index 0000000..c53e34d --- /dev/null +++ b/src/engine/viewport.zig @@ -0,0 +1,54 @@ +const PointF = @import("point_f.zig").PointF; +const RectangleF = @import("rectangle_f.zig").RectangleF; + +pub const Viewport = struct { + unscaledWidth: i32, + unscaledHeight: i32, + + actualWidth: i32, + actualHeight: i32, + + scaledWidth: i32, + scaledHeight: i32, + bounds: RectangleF, + scale: f32, + + pub fn init(width: i32, height: i32) Viewport { + return Viewport{ + .unscaledWidth = width, + .unscaledHeight = height, + .actualWidth = width, + .actualHeight = height, + .scaledWidth = width, + .scaledHeight = height, + .bounds = RectangleF.initRelative(0, 0, @intToFloat(f32, width), @intToFloat(f32, height)), + .scale = 1.0, + }; + } + + pub fn center(self: Viewport) PointF { + return self.bounds.center(); + } + + pub fn update(self: *Viewport, width: i32, height: i32) void { + self.actualWidth = width; + self.actualHeight = height; + + const horizontalRatio = @intToFloat(f32, width) / @intToFloat(f32, self.unscaledWidth); + const verticalRatio = @intToFloat(f32, height) / @intToFloat(f32, self.unscaledHeight); + + if (horizontalRatio < verticalRatio) { + self.scaledWidth = width; + self.scaledHeight = @floatToInt(i32, horizontalRatio * @intToFloat(f32, self.unscaledHeight)); + const top = @divFloor(height - self.scaledHeight, 2); + self.bounds = RectangleF.initRelative(0, @intToFloat(f32, top), @intToFloat(f32, width), @intToFloat(f32, self.scaledHeight)); + self.scale = horizontalRatio; + } else { + self.scaledWidth = @floatToInt(i32, verticalRatio * @intToFloat(f32, self.unscaledWidth)); + self.scaledHeight = height; + const left = @divFloor(width - self.scaledWidth, 2); + self.bounds = RectangleF.initRelative(@intToFloat(f32, left), 0, @intToFloat(f32, self.scaledWidth), @intToFloat(f32, height)); + self.scale = verticalRatio; + } + } +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..ad4e4ed --- /dev/null +++ b/src/main.zig @@ -0,0 +1,98 @@ +const std = @import("std"); +const allegro = @import("allegro"); +const engine = @import("engine.zig"); +const Context = @import("context.zig").Context; +const Palette = @import("palette.zig").Palette; +const TitleScene = @import("title_scene.zig").TitleScene; + +fn currentFile() []const u8 { + return @src().file; +} + +const sourceDir = std.fs.path.dirname(currentFile()).?; +const assetsDir = sourceDir ++ "/assets"; + +fn hexColor(hex: []const u8) allegro.Color { + return allegro.Color.initFromHex(hex); +} + +pub fn main() !void { + // Primary: #551485 + // Secondary: #e44b50 + // Tertiary: #479be6 + // Neutral: #948f94 + + var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var context = try Context.init(allocator); + defer context.deinit(); + context.palette = Palette{ + .primary = .{ .background = hexColor("#7d42ad"), .text = hexColor("#ffffff") }, + .primaryContainer = .{ .background = hexColor("#f2daff"), .text = hexColor("#2e004e") }, + .secondary = .{ .background = hexColor("#b32731"), .text = hexColor("#ffffff") }, + .secondaryContainer = .{ .background = hexColor("#ffdad8"), .text = hexColor("#410006") }, + .tertiary = .{ .background = hexColor("#0062a0"), .text = hexColor("#ffffff") }, + .tertiaryContainer = .{ .background = hexColor("#d0e4ff"), .text = hexColor("#001d35") }, + .background = .{ .background = hexColor("#fffbff"), .text = hexColor("#2b0052") }, + .outline = hexColor("#7c757e"), + }; + + allegro.setNewBitmapFlags(allegro.NewBitmapFlags{ .MIN_LINEAR = true, .MAG_LINEAR = true }); + try context.textures.addFromFile("opaque", assetsDir ++ "/images/opaque.png"); + try context.textures.addFromFile("title_untitled", assetsDir ++ "/images/title_untitled.png"); + allegro.convertMemoryBitmaps(); + + try context.fonts.addFromFileTTF("sub", assetsDir ++ "/fonts/Cabin-Regular.ttf", 16); + try context.fonts.addFromFileTTF("default", assetsDir ++ "/fonts/Cabin-Regular.ttf", 32); + + try context.switchToScene(TitleScene, null); + + var t = allegro.getTime(); + while (!context.shouldQuit) { + const scene = if (context.scene) |scene| scene else { + break; + }; + + while (!context.events.isEmpty()) { + var event: allegro.Event = undefined; + _ = context.events.get(&event); + switch (event.type) { + allegro.c.ALLEGRO_EVENT_DISPLAY_CLOSE => context.quit(), + allegro.c.ALLEGRO_EVENT_DISPLAY_RESIZE => { + context.resized(event.display.width, event.display.height); + _ = allegro.acknowledgeResize(allegro.Display{ .native = event.display.source.? }); + }, + allegro.c.ALLEGRO_EVENT_KEY_CHAR => { + switch (event.keyboard.keycode) { + allegro.c.ALLEGRO_KEY_ESCAPE => context.quit(), + allegro.c.ALLEGRO_KEY_F11 => context.toggleFullScreen(), + else => {}, + } + }, + else => {}, + } + if (context.shouldQuit) { + break; + } + try scene.handle(&context, &event); + } + + const newT = allegro.getTime(); + const deltaT = @floatCast(f32, newT - t); + t = newT; + scene.update(&context, deltaT); + + scene.render(&context); + + allegro.flipDisplay(); + } +} + +test "simple test" { + var list = std.ArrayList(i32).init(std.testing.allocator); + defer list.deinit(); // try commenting this out and see if zig detects the memory leak! + try list.append(42); + try std.testing.expectEqual(@as(i32, 42), list.pop()); +} diff --git a/src/main_menu_scene.zig b/src/main_menu_scene.zig new file mode 100644 index 0000000..a126546 --- /dev/null +++ b/src/main_menu_scene.zig @@ -0,0 +1,36 @@ +const allegro = @import("allegro"); +const Context = @import("context.zig").Context; + +pub const MainMenuScene = struct { + pub fn enter(self: *MainMenuScene, ctx: *Context) void { + _ = ctx; + _ = self; + } + + pub fn exit(self: *MainMenuScene, ctx: *Context) void { + _ = ctx; + _ = self; + } + + pub fn handle(self: *MainMenuScene, ctx: *Context, event: *allegro.Event) !void { + _ = event; + _ = ctx; + _ = self; + } + + pub fn update(self: *MainMenuScene, ctx: *Context, dt: f32) void { + _ = self; + _ = dt; + _ = ctx; + } + + pub fn render(self: *MainMenuScene, ctx: *Context) void { + _ = self; + allegro.clearToColor(ctx.palette.background.background); + + const center = ctx.viewport.center(); + ctx.textures.get("title_untitled").?.drawTintedCenteredScaled(ctx.palette.background.text, center.x, ctx.viewport.bounds.min.y + ctx.viewport.scale * 100, 0.5 * ctx.viewport.scale); + + allegro.drawText(ctx.fonts.get("default").?, ctx.palette.background.text, center.x, ctx.viewport.bounds.min.y + ctx.viewport.scale * 200, allegro.DrawTextFlags.ALIGN_CENTER, "Play game"); + } +}; diff --git a/src/palette.zig b/src/palette.zig new file mode 100644 index 0000000..4e6ff82 --- /dev/null +++ b/src/palette.zig @@ -0,0 +1,14 @@ +const allegro = @import("allegro"); + +pub const Palette = struct { + pub const Combination = struct { background: allegro.Color, text: allegro.Color }; + + primary: Combination, + primaryContainer: Combination, + secondary: Combination, + secondaryContainer: Combination, + tertiary: Combination, + tertiaryContainer: Combination, + background: Combination, + outline: allegro.Color, +}; diff --git a/src/scene.zig b/src/scene.zig new file mode 100644 index 0000000..afbc46d --- /dev/null +++ b/src/scene.zig @@ -0,0 +1,5 @@ +const allegro = @import("allegro"); +const engine = @import("engine.zig"); +const Context = @import("context.zig").Context; + +pub const Scene = engine.Scene(Context, *allegro.Event); diff --git a/src/title_scene.zig b/src/title_scene.zig new file mode 100644 index 0000000..a1fadda --- /dev/null +++ b/src/title_scene.zig @@ -0,0 +1,45 @@ +const allegro = @import("allegro"); +const Context = @import("context.zig").Context; +const MainMenuScene = @import("main_menu_scene.zig").MainMenuScene; + +pub const TitleScene = struct { + offset: f32, + + pub fn enter(self: *TitleScene, ctx: *Context) void { + _ = ctx; + _ = self; + } + + pub fn exit(self: *TitleScene, ctx: *Context) void { + _ = ctx; + _ = self; + } + + pub fn handle(self: *TitleScene, ctx: *Context, event: *allegro.Event) !void { + switch (event.type) { + allegro.c.ALLEGRO_EVENT_KEY_CHAR => { + switch (event.keyboard.keycode) { + allegro.c.ALLEGRO_KEY_SPACE => try ctx.switchToScene(MainMenuScene, null), + else => {}, + } + }, + else => {}, + } + _ = self; + } + + pub fn update(self: *TitleScene, ctx: *Context, dt: f32) void { + _ = self; + _ = dt; + _ = ctx; + } + + pub fn render(self: *TitleScene, ctx: *Context) void { + _ = self; + allegro.clearToColor(ctx.palette.background.background); + ctx.textures.get("opaque").?.drawTintedScaled(ctx.palette.tertiaryContainer.background, ctx.viewport.bounds.min.x, ctx.viewport.bounds.min.y, ctx.viewport.bounds.width(), ctx.viewport.bounds.height(), allegro.DrawFlags{}); + + const center = ctx.viewport.center(); + ctx.textures.get("title_untitled").?.drawTintedCenteredScaled(ctx.palette.background.text, center.x, center.y, ctx.viewport.scale); + } +};