diff --git a/api/character.zig b/api/character.zig index 6b7ff9b..6bff90d 100644 --- a/api/character.zig +++ b/api/character.zig @@ -44,7 +44,7 @@ air: CombatStats, equipment: Equipment, -inventory_max_items: i64, +inventory_max_items: u64, inventory: Inventory, pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character { @@ -54,7 +54,14 @@ pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character const x = try json_utils.getIntegerRequired(obj, "x"); const y = try json_utils.getIntegerRequired(obj, "y"); const name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty; - assert(name.len > 0); + if (name.len == 0) { + return error.InvalidName; + } + + const inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty; + if (inventory_max_items < 0) { + return error.InvalidInventoryMaxItems; + } return Character{ .allocator = allocator, @@ -84,7 +91,7 @@ pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character .equipment = try Equipment.parse(api, obj), - .inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty, + .inventory_max_items = @intCast(inventory_max_items), .inventory = try Inventory.parse(api, inventory) }; } diff --git a/cli/main.zig b/cli/main.zig index 160a077..990193e 100644 --- a/cli/main.zig +++ b/cli/main.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const Artificer = @import("artificer"); const Allocator = std.mem.Allocator; @@ -29,6 +30,11 @@ pub fn main() !void { var artificer = try Artificer.init(allocator, token); defer artificer.deinit(); + if (builtin.mode != .Debug) { + std.log.info("Prefetching server data", .{}); + try artificer.server.prefetch(); + } + std.log.info("Starting main loop", .{}); while (true) { const waitUntil = artificer.nextStepAt(); diff --git a/gui/main.zig b/gui/main.zig index 4ae120f..3db53e5 100644 --- a/gui/main.zig +++ b/gui/main.zig @@ -1,18 +1,76 @@ +const std = @import("std"); +const Artificer = @import("artificer"); const rl = @import("raylib"); +const Allocator = std.mem.Allocator; + +const srcery = @import("./srcery.zig"); + +const UI = @import("./ui.zig"); +const UIStack = @import("./ui_stack.zig"); +const RectUtils = @import("./rect_utils.zig"); + +fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 { + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + + if (args.len < 2) { + return null; + } + + const filename = args[1]; + const cwd = std.fs.cwd(); + var token_buffer: [256]u8 = undefined; + const token = try cwd.readFile(filename, &token_buffer); + + return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t ")); +} + +fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, brain: Artificer.Brain) void { + const name_height = 20; + UI.drawTextCentered(ui.font, brain.name, .{ + .x = RectUtils.center(rect).x, + .y = rect.y + name_height/2 + }, 20, 2, srcery.bright_white); +} pub fn main() anyerror!void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken; + defer allocator.free(token); + + var artificer = try Artificer.init(allocator, token); + defer artificer.deinit(); + rl.initWindow(800, 450, "Artificer"); defer rl.closeWindow(); rl.setTargetFPS(60); + var ui = UI.init(); + defer ui.deinit(); + while (!rl.windowShouldClose()) { + if (std.time.timestamp() > artificer.nextStepAt()) { + try artificer.step(); + } + + const screen_size = rl.Vector2.init( + @floatFromInt(rl.getScreenWidth()), + @floatFromInt(rl.getScreenHeight()) + ); rl.beginDrawing(); defer rl.endDrawing(); - rl.clearBackground(rl.Color.white); + rl.clearBackground(srcery.black); - rl.drawText("Congrats! You created your first window!", 190, 200, 20, rl.Color.light_gray); + var info_stack = UIStack.init(rl.Rectangle.init(0, 0, screen_size.x, screen_size.y), .left_to_right); + for (artificer.characters.items) |brain| { + const info_width = screen_size.x / @as(f32, @floatFromInt(artificer.characters.items.len)); + drawCharacterInfo(&ui, info_stack.next(info_width), brain); + } } } diff --git a/gui/rect_utils.zig b/gui/rect_utils.zig new file mode 100644 index 0000000..94cb0b9 --- /dev/null +++ b/gui/rect_utils.zig @@ -0,0 +1,142 @@ +const rl = @import("raylib"); +const Rect = rl.Rectangle; + +pub const AlignX = enum { left, center, right }; +pub const AlignY = enum { top, center, bottom }; + +pub fn initCentered(rect: Rect, width: f32, height: f32) Rect { + const unused_width = rect.width - width; + const unused_height = rect.height - height; + return Rect.init(rect.x + unused_width / 2, rect.y + unused_height / 2, width, height); +} + +pub fn center(rect: Rect) rl.Vector2 { + return rl.Vector2{ + .x = rect.x + rect.width / 2, + .y = rect.y + rect.height / 2, + }; +} + +pub fn bottomLeft(rect: Rect) rl.Vector2 { + return rl.Vector2{ + .x = rect.x, + .y = rect.y + rect.height, + }; +} + +pub fn bottomRight(rect: Rect) rl.Vector2 { + return rl.Vector2{ + .x = rect.x + rect.width, + .y = rect.y + rect.height, + }; +} + +pub fn topLeft(rect: Rect) rl.Vector2 { + return rl.Vector2{ + .x = rect.x, + .y = rect.y, + }; +} + +pub fn topRight(rect: Rect) rl.Vector2 { + return rl.Vector2{ + .x = rect.x + rect.width, + .y = rect.y, + }; +} + +pub fn aligned(rect: Rect, align_x: AlignX, align_y: AlignY) rl.Vector2 { + const x = switch(align_x) { + .left => rect.x, + .center => rect.x + rect.width/2, + .right => rect.x + rect.width, + }; + + const y = switch(align_y) { + .top => rect.y, + .center => rect.y + rect.height/2, + .bottom => rect.y + rect.height, + }; + + return rl.Vector2.init(x, y); +} + +pub fn shrink(rect: Rect, x: f32, y: f32) rl.Rectangle { + return Rect.init(rect.x + x, rect.y + y, rect.width - 2 * x, rect.height - 2 * y); +} + +pub fn shrinkX(rect: rl.Rectangle, offset: f32) rl.Rectangle { + return shrink(rect, offset, 0); +} + +pub fn shrinkY(rect: rl.Rectangle, offset: f32) rl.Rectangle { + return shrink(rect, 0, offset); +} + +pub fn shrinkTop(rect: rl.Rectangle, offset: f32) rl.Rectangle { + return Rect.init(rect.x, rect.y + offset, rect.width, rect.height - offset); +} + +pub fn grow(rect: Rect, x: f32, y: f32) rl.Rectangle { + return shrink(rect, -x, -y); +} + +pub fn growY(rect: Rect, offset: f32) rl.Rectangle { + return grow(rect, 0, offset); +} + +pub fn position(rect: rl.Rectangle) rl.Vector2 { + return rl.Vector2.init(rect.x, rect.y); +} + +pub fn isInside(rect: rl.Rectangle, x: f32, y: f32) bool { + return (rect.x <= x and x < rect.x + rect.width) and (rect.y < y and y < rect.y + rect.height); +} + +pub fn isInsideVec2(rect: rl.Rectangle, vec2: rl.Vector2) bool { + return isInside(rect, vec2.x, vec2.y); +} + +pub fn top(rect: rl.Rectangle) f32 { + return rect.y; +} + +pub fn bottom(rect: rl.Rectangle) f32 { + return rect.y + rect.height; +} + +pub fn left(rect: rl.Rectangle) f32 { + return rect.x; +} + +pub fn right(rect: rl.Rectangle) f32 { + return rect.x + rect.width; +} + +pub fn verticalSplit(rect: rl.Rectangle, left_side_width: f32) [2]rl.Rectangle { + var left_side = rect; + left_side.width = left_side_width; + + var right_side = rect; + right_side.x += left_side_width; + right_side.width -= left_side_width; + + return .{ + left_side, + right_side + }; +} + +pub fn horizontalSplit(rect: rl.Rectangle, top_side_height: f32) [2]rl.Rectangle { + var top_side = rect; + top_side.height = top_side_height; + + var bottom_side = rect; + bottom_side.y += top_side_height; + bottom_side.height -= top_side_height; + + return .{ + top_side, + bottom_side + }; +} diff --git a/gui/srcery.zig b/gui/srcery.zig new file mode 100644 index 0000000..2271ee3 --- /dev/null +++ b/gui/srcery.zig @@ -0,0 +1,43 @@ +const rl = @import("raylib"); + +fn rgb(r: u8, g: u8, b: u8) rl.Color { + return rl.Color.init(r, g, b, 255); +} + +// Primary +pub const black = rgb(28 , 27 , 25 ); +pub const red = rgb(239, 47 , 39 ); +pub const green = rgb(81 , 159, 80 ); +pub const yellow = rgb(251, 184, 41 ); +pub const blue = rgb(44 , 120, 191); +pub const magenta = rgb(224, 44 , 109); +pub const cyan = rgb(10 , 174, 179); +pub const white = rgb(186, 166, 127); +pub const bright_black = rgb(145, 129, 117); +pub const bright_red = rgb(247, 83 , 65 ); +pub const bright_green = rgb(152, 188, 55 ); +pub const bright_yellow = rgb(254, 208, 110); +pub const bright_blue = rgb(104, 168, 228); +pub const bright_magenta = rgb(255, 92 , 143); +pub const bright_cyan = rgb(43 , 228, 208); +pub const bright_white = rgb(252, 232, 195); + +// Secondary +pub const orange = rgb(255, 95, 0); +pub const bright_orange = rgb(255, 135, 0); +pub const hard_black = rgb(18, 18, 18); +pub const teal = rgb(0, 128, 128); + +// Grays +pub const xgray1 = rgb(38 , 38 , 38 ); +pub const xgray2 = rgb(48 , 48 , 48 ); +pub const xgray3 = rgb(58 , 58 , 58 ); +pub const xgray4 = rgb(68 , 68 , 68 ); +pub const xgray5 = rgb(78 , 78 , 78 ); +pub const xgray6 = rgb(88 , 88 , 88 ); +pub const xgray7 = rgb(98 , 98 , 98 ); +pub const xgray8 = rgb(108, 108, 108); +pub const xgray9 = rgb(118, 118, 118); +pub const xgray10 = rgb(128, 128, 128); +pub const xgray11 = rgb(138, 138, 138); +pub const xgray12 = rgb(148, 148, 148); diff --git a/gui/ui.zig b/gui/ui.zig new file mode 100644 index 0000000..e05ae08 --- /dev/null +++ b/gui/ui.zig @@ -0,0 +1,159 @@ +const rl = @import("raylib"); +const std = @import("std"); + +const UI = @This(); + +font: rl.Font, + +pub fn init() UI { + return UI{ + .font = rl.getFontDefault() + }; +} + +pub fn deinit(self: UI) void { + rl.unloadFont(self.font); +} + +// Reimplementation of `GetGlyphIndex` from raylib in src/rtext.c +fn GetGlyphIndex(font: rl.Font, codepoint: i32) usize { + var index: usize = 0; + + var fallbackIndex: usize = 0; // Get index of fallback glyph '?' + + for (0..@intCast(font.glyphCount), font.glyphs) |i, glyph| { + if (glyph.value == '?') fallbackIndex = i; + + if (glyph.value == codepoint) + { + index = i; + break; + } + } + + if ((index == 0) and (font.glyphs[0].value != codepoint)) index = fallbackIndex; + + return index; +} + +fn GetCodePointNext(text: []const u8, next: *usize) i32 { + var letter: i32 = '?'; + + if (std.unicode.utf8ByteSequenceLength(text[0])) |codepointSize| { + next.* = codepointSize; + if (std.unicode.utf8Decode(text[0..codepointSize])) |codepoint| { + letter = @intCast(codepoint); + } else |_| {} + } else |_| {} + + return letter; +} + +// NOTE: Line spacing is a global variable, use SetTextLineSpacing() to setup +const textLineSpacing = 2; // TODO: Assume that line spacing is not changed. + +// Reimplementation of `rl.drawTextEx`, so a null terminated would not be required +pub fn drawTextEx(font: rl.Font, text: []const u8, position: rl.Vector2, font_size: f32, spacing: f32, tint: rl.Color) void { + var used_font = font; + if (font.texture.id == 0) { + used_font = rl.getFontDefault(); + } + + var text_offset_y: f32 = 0; + var text_offset_x: f32 = 0; + + const scale_factor = font_size / @as(f32, @floatFromInt(used_font.baseSize)); + + var i: usize = 0; + while (i < text.len) { + var next: usize = 0; + + const letter = GetCodePointNext(text[i..], &next); + const index = GetGlyphIndex(font, letter); + + i += next; + + if (letter == '\n') { + text_offset_x = 0; + text_offset_y += (font_size + textLineSpacing); + } else { + if (letter != ' ' and letter != '\t') { + rl.drawTextCodepoint(font, letter, .{ + .x = position.x + text_offset_x, + .y = position.y + text_offset_y, + }, font_size, tint); + } + + if (font.glyphs[index].advanceX == 0) { + text_offset_x += font.recs[index].width*scale_factor + spacing; + } else { + text_offset_x += @as(f32, @floatFromInt(font.glyphs[index].advanceX))*scale_factor + spacing; + } + } + } +} + +// Reimplementation of `rl.measureTextEx`, so a null terminated would not be required +pub fn measureTextEx(font: rl.Font, text: []const u8, fontSize: f32, spacing: f32) rl.Vector2 { + var textSize = rl.Vector2.init(0, 0); + + if (font.texture.id == 0) return textSize; // Security check + + var tempByteCounter: i32 = 0; // Used to count longer text line num chars + var byteCounter: i32 = 0; + + var textWidth: f32 = 0; + var tempTextWidth: f32 = 0; // Used to count longer text line width + + var textHeight: f32 = fontSize; + const scaleFactor: f32 = fontSize/@as(f32, @floatFromInt(font.baseSize)); + + var i: usize = 0; + while (i < text.len) + { + byteCounter += 1; + + var next: usize = 0; + + const letter = GetCodePointNext(text[i..], &next); + const index = GetGlyphIndex(font, letter); + + i += next; + + if (letter != '\n') + { + if (font.glyphs[index].advanceX != 0) { + textWidth += @floatFromInt(font.glyphs[index].advanceX); + } else { + textWidth += font.recs[index].width; + textWidth += @floatFromInt(font.glyphs[index].offsetX); + } + } + else + { + if (tempTextWidth < textWidth) tempTextWidth = textWidth; + byteCounter = 0; + textWidth = 0; + + textHeight += (fontSize + textLineSpacing); + } + + if (tempByteCounter < byteCounter) tempByteCounter = byteCounter; + } + + if (tempTextWidth < textWidth) tempTextWidth = textWidth; + + textSize.x = tempTextWidth*scaleFactor + @as(f32, @floatFromInt(tempByteCounter - 1)) * spacing; + textSize.y = textHeight; + + return textSize; +} + +pub fn drawTextCentered(font: rl.Font, text: []const u8, position: rl.Vector2, font_size: f32, spacing: f32, color: rl.Color) void { + const text_size = measureTextEx(font, text, font_size, spacing); + const adjusted_position = rl.Vector2{ + .x = position.x - text_size.x/2, + .y = position.y - text_size.y/2, + }; + drawTextEx(font, text, adjusted_position, font_size, spacing, color); +} diff --git a/gui/ui_stack.zig b/gui/ui_stack.zig new file mode 100644 index 0000000..90bec37 --- /dev/null +++ b/gui/ui_stack.zig @@ -0,0 +1,42 @@ +const rl = @import("raylib"); +const Stack = @This(); + +pub const Direction = enum { + top_to_bottom, + bottom_to_top, + left_to_right +}; + +unused_box: rl.Rectangle, +dir: Direction, +gap: f32 = 0, + +pub fn init(box: rl.Rectangle, dir: Direction) Stack { + return Stack{ + .unused_box = box, + .dir = dir + }; +} + +pub fn next(self: *Stack, size: f32) rl.Rectangle { + return switch (self.dir) { + .top_to_bottom => { + const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, self.unused_box.width, size); + self.unused_box.y += size; + self.unused_box.y += self.gap; + return next_box; + }, + .bottom_to_top => { + const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y + self.unused_box.height - size, self.unused_box.width, size); + self.unused_box.height -= size; + self.unused_box.height -= self.gap; + return next_box; + }, + .left_to_right => { + const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, size, self.unused_box.height); + self.unused_box.x += size; + self.unused_box.x += self.gap; + return next_box; + }, + }; +} diff --git a/lib/brain.zig b/lib/brain.zig index af75fd2..205b260 100644 --- a/lib/brain.zig +++ b/lib/brain.zig @@ -400,7 +400,7 @@ pub fn isTaskFinished(self: *CharacterBrain) bool { pub fn performTask(self: *CharacterBrain, api: *Server) !void { if (self.task == null) { - std.log.debug("[{s}] idle", .{self.name}); + // TODO: std.log.debug("[{s}] idle", .{self.name}); return; } diff --git a/lib/root.zig b/lib/root.zig index 2500b95..304909d 100644 --- a/lib/root.zig +++ b/lib/root.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Api = @import("artifacts-api"); const Allocator = std.mem.Allocator; -const CharacterBrain = @import("./brain.zig"); +pub const Brain = @import("./brain.zig"); const Artificer = @This(); @@ -10,7 +10,7 @@ const expiration_margin: u64 = 100 * std.time.ns_per_ms; // 100ms const server_down_retry_interval = 5; // minutes server: Api.Server, -characters: std.ArrayList(CharacterBrain), +characters: std.ArrayList(Brain), paused_until: ?i64 = null, @@ -20,16 +20,15 @@ pub fn init(allocator: Allocator, token: []const u8) !Artificer { try server.setToken(token); - var characters = std.ArrayList(CharacterBrain).init(allocator); - defer characters.deinit(); - // TODO: Add character deinit + var characters = std.ArrayList(Brain).init(allocator); + errdefer characters.deinit(); // TODO: Add character deinit - // const chars = try api.listMyCharacters(); - // defer chars.deinit(); + const chars = try server.listMyCharacters(); + defer chars.deinit(); - // for (chars.items) |char| { - // try scheduler.addCharacter(char.name); - // } + for (chars.items) |char| { + try characters.append(try Brain.init(allocator, char.name)); + } return Artificer{ .server = server, @@ -37,11 +36,12 @@ pub fn init(allocator: Allocator, token: []const u8) !Artificer { }; } -pub fn deinit(self: Artificer) void { +pub fn deinit(self: *Artificer) void { for (self.characters.items) |brain| { brain.deinit(); } self.characters.deinit(); + self.server.deinit(); } pub fn step(self: *Artificer) !void { @@ -93,7 +93,7 @@ pub fn nextStepAt(self: *Artificer) i64 { return earliestCooldown(self.characters.items, &self.server) orelse 0; } -fn earliestCooldown(characters: []CharacterBrain, api: *Api.Server) ?i64 { +fn earliestCooldown(characters: []Brain, api: *Api.Server) ?i64 { var earliest_cooldown: ?i64 = null; for (characters) |*brain| { if (brain.action_queue.items.len == 0) continue; @@ -109,7 +109,7 @@ fn earliestCooldown(characters: []CharacterBrain, api: *Api.Server) ?i64 { return earliest_cooldown; } -fn runNextActions(characters: []CharacterBrain, api: *Api.Server) !void { +fn runNextActions(characters: []Brain, api: *Api.Server) !void { const maybe_earliest_cooldown = earliestCooldown(characters, api); if (maybe_earliest_cooldown == null) return;